djedi-react
v5.5.0
Published
Djedi CMS React client
Downloads
87
Readme
djedi-react
Requires djedi-cms version 1.2.1 or later.
Contents
Installation
npm install djedi-react react react-dom isomorphic-unfetch
import { Node, NodeContext, ForceNodes, djedi, md } from "djedi-react";
Optional Babel plugin (for better errors, prefetching and server-side rendering):
// .babelrc
{
plugins: ["djedi-react/babel-plugin"],
}
See also options and Django settings.
Browser support
All the latest browsers plus Internet Explorer 11 (which requires a Promise
polyfill).
Usage
The library is designed to mimic the Django template tags as closely as
possible. In Django world there are two tags, node
and blocknode
:
{% node 'page/title.txt' default='Djedi' %}
{% blocknode 'page/body.md' %}
## I’m a djedi apprentice
This is fun!
{% endblocknode %}
In React world, there’s no need for two tags. (blocknode
can do everything
that node
can do – node
is just a shortcut for simple nodes.) So
djedi-react
only has <Node>
, which closely mimics blocknode
.
import { Node, djedi, md } from "djedi-react";
function Home() {
return (
<div>
<h1>
{/* Simple node. */}
<Node uri="home/title">Welcome!</Node>
</h1>
{/* Markdown node with variable interpolation. */}
<Node uri="home/text.md" url="https://example.com">{md`
## Using markdown
Some text and a [link]({url}).
`}</Node>
{/* Image node. */}
<Node uri="home/image.img" />
<footer>
<p>
{/* Alternate interpolation syntax for convenience. */}
<Node uri="home/footer" year={new Date().getFullYear()}>
© [year] 5 Monkeys
</Node>
</p>
</footer>
</div>
);
}
// Optional: For server-side rendering (Next.js example).
Page.getInitialProps = async () => {
await djedi.prefetch();
const nodes = djedi.track();
return { nodes };
// (You also need to call `djedi.addNodes(nodes)` somewhere.)
};
// Inject the admin sidebar, if the user has permission.
djedi.injectAdmin();
Server-side rendering
Rendering <Node>
s automatically fires requests to get node contents (but
batching and caching are used to avoid excessive requests). While this works
great in the browser, it’s not so good for server-side rendering where you
typically only do an initial render. You’d end up with rendering “Loading…” for
all nodes.
The idea is to call await djedi.prefetch()
before rendering. That will fetch
needed nodes and put them in the cache. Then, when rendering, they’ll all be
available straight away. (Mostly.)
The next step is to track which nodes are rendered. Before rendering, call
const nodes = djedi.track()
. The returned nodes
is an object that will be
mutated during rendering. You need to serialize nodes
(after rendering)
and pass it along in the HTML response. Then, in the browser, you need to call
djedi.addNodes(nodes)
to put the rendered nodes into cache again. Otherwise
you’d end up fetching all the nodes again from the browser. You’d also get
“server and client did not match” warnings from React since the actual node
contents would be rendered on the server, but “Loading…” would be rendered in
the browser (during the initial render).
So, how exactly does djedi.prefetch
know what to fetch? It’s all thanks to the
Babel plugin mentioned in Installation.
For example, it does the following transformation:
import React from "react";
import { Node } from "djedi-react";
export default function SomeComponent() {
return <Node uri="uri">default</Node>;
}
⬇
var _djedi_uri = "uri",
_djedi_default = "default";
import { djedi as _djedi } from "djedi-react";
_djedi.reportPrefetchableNode({
uri: _djedi_uri,
value: _djedi_default,
});
import React from "react";
import { Node } from "djedi-react";
export default function SomeComponent() {
return <Node uri={_djedi_uri}>{_djedi_default}</Node>;
}
So just by import
ing that file, djedi-react will know about the node (via the
djedi.reportPrefetchableNode
call) – even before <SomeComponent>
is actually
rendered!
The Babel plugin has some constraints, though:
- Nodes with dynamic
uri
(such asuri={`store/${storeId}/intro`}
) are not supported. They still work in the browser, but will render “Loading…” on the server. You can usedjedi.prefetch({ extra: [...] })
for this case – see djedi.prefetch. - Nodes with dynamic
children
are not supported either. That’s a bad idea anyway. - You cannot rename
Node
to something else when importing it. It must be called exactlyNode
. - It is assumed that your build system can handle
import { djedi } from "djedi-react"
.
There’s a fine detail to keep in mind about the prefetching. Remember how
import
ing a file is enough to let djedi-react know about the nodes in there?
For an SPA you might end up import
ing every page and component of your whole
site at startup. That means that calling djedi.prefetch()
will try to load
every single node of the whole site just to render a single page. The best way
to solve this is to use code splitting (Next.js does that by default), which
is good for performance anyway. If that’s not possible for you, you can pass a
filter function to djedi.prefetch
. For example:
djedi.prefetch({ filter: uri => uri.path.startsWith("home/") });
Using Djedi, it is common practice to group nodes by page. For example,
home/title.txt
, home/text.md
, about/title.txt
, about/text.md
, etc. So
loading all nodes starting with for example home/
might work out.
Security
Note that the cache of fetched nodes is global and thus shared across different users!
This means that if you use any kind of dynamic nodes you need to be careful. There are two things an attacker could do otherwise:
- Put a bad default in the cache. Never use dynamic defaults. That’s a bad idea anyway.
- Fill up the cache with junk. Validate the dynamic parts of URI:s.
For example:
// Make sure to check that `storeId` is an existing store ID before rendering!
// Imagine `storeId` coming from a URL parameter: `?store=12`. Then somebody
// could request `?store=10001`, `?store=10002`, `?store=10003` …
// `?store=999999` progressively filling up the cache with junk "Welcome to our
// store!" nodes.
<Node uri={`store/${storeId}/intro`}>Welcome to our store!</Node>
Also be careful with the following methods, since they put nodes into the cache:
djedi.prefetch({ extra: [...] })
djedi.get
djedi.getBatched
djedi.addNodes
Reference
import { Node, NodeContext, ForceNodes, djedi, md } from "djedi-react";
Node
This is the main part of the library.
<Node uri="uri" edit={true}>
default value
</Node>
When rendered, it automatically makes a request to fetch the node content. Batching and caching are used to avoid excessive requests.
If the node already exists in the cache, it is immediately rendered. Otherwise,
setTimeout
is used to make a request in a couple of milliseconds (see
djedi.options.batchInterval). Requests for other nodes
rendered during that time are batched together. In other words, nodes will be
fetched at most once per djedi.options.batchInterval
milliseconds.
Props
| Prop | Type | Default |
| -------------------------- | ------------------- | --------------------------------------------- |
| uri | string
| required |
| children | string
| undefined
|
| edit | boolean
| true
|
| render | function
| djedi.options.defaultRender |
| ...variables | { [string]: any }
| {}
|
uri
string
Required
The node URI.
children
string
Default: undefined
The default value of the node.
NOTE: You may only pass a single string. No nested elements. No JSX interpolations. This is because nodes are simple URI→string content mappings. The default value is sent to the backend as a string, and the backend returns a another string for display.
<Node uri="uri">
This is OK
</Node>
The following won’t work, because the <em>
part is not a string but another
JSX element.
<Node uri="uri">
This is <em>not</em> OK!
</Node>
If you meant to include an <em>
element in the default value, the best way is
to use the md template tag:
<Node uri="uri">{md`
This <em>is</em> OK!
`}</Node>
Escaping also works, but might not be very readable:
<Node uri="uri">
This <em>is</em> OK!
</Node>
But don’t use a non-tagged template literal, because then you’ll end up with extraneous whitespace in the default value, which can be especially troublesome for markdown:
// DON’T DO THIS!
<Node uri="uri.md">{`
This will be a code block in markdown! (The line starts with 4 spaces.)
`}</Node>
// The md tag dedents the value as expected.
<Node uri="uri.md">{md`
This will be a paragraph in markdown.
`}</Node>
// This also works, because of Babel’s JSX whitespace rules.
<Node uri="uri.md">
This will be a paragraph in markdown.
</Node>
edit
boolean
Default: true
Whether or not the node should be auto-updated after editing it in the admin
sidebar. Editable nodes are wrapped in a <span>
while non-editable nodes are
rendered without any enclosing tag. (The name “edit” might be a bit confusing,
but it is inherited from the Django template tags.)
Note: It’s not possible to render HTML without a wrapper element in React.
So you can’t do edit={false}
and expect the node value to be treated as HTML.
A common use-case for using {% node 'uri' edit=False %}
in Django templates is
to put a node somewhere where you can’t use HTML, such in a placeholder
attribute. See the Search example component for how to
do that in React.
render
function
Default: djedi.options.defaultRender
The function receives two arguments (the current state and the current language) and must return a React node (in other words, anything that you can return from a React component).
See djedi.options.defaultRender for how to implement this function.
Use cases for this prop include:
- Using a node for something that isn’t an element, such as an attribute. See the Search example component for an example.
- Parsing a node value and rendering a custom component. See the Toplist example component for an example.
- Customizing the loading and error states for a specific node.
...variables
{ [string]: any }
Default: {}
All remaining props are passed as variables to be interpolated into the node value.
<Node uri="uri" name="Bob">
Hello, [name]!
</Node>
The above example uses [name]
rather than {name}
(like you might be used to
from Django templates), because {name}
already has another meaning in JSX:
<Node uri="uri" name="Bob">
{/* ReferenceError: name is not defined */}
Hello, {name}!
</Node>
You can still use the {name}
syntax. [name]
is just a convenience to avoid
escaping or using strings. For example:
<Node uri="uri" name="Bob">{md`
Hello, {name}!
`}</Node>
{name}
and [name]
chunks that you did not pass any value for are left as-is.
The Babel plugin mentioned in Installation warns you if you’ve accidentally used JSX or template string interpolation instead of variables. That’s an anti-pattern, since it won’t work if the user edits the node (unlike variable props).
NodeContext
If your site supports multiple languages, you can pass the current language to
NodeContext.Provider
to have all nodes re-render whenever the language changes
(and get the correct language on the first render). See also
languages.
<NodeContext.Provider value={currentLanguage}>
<App />
</NodeContext.Provider>
ForceNodes
This component lets you force one or more nodes to be fetched and appear in the Djedi sidebar, without rendering them to the DOM. This is useful if you have a node that is only shown under certain circumstances or only for a short duration, such as an error message, and you want that node to be editable from the Djedi sidebar without having to trigger it first.
ForceNodes
is typically used like this:
- Save a
<Node>
into a variable. - Use it where you want.
- Add
<ForceNodes>{yourNode}</ForceNodes>
at the end of your component.
ForceNodes
only takes the children
prop, and all children must be <Node>
s
or (nested) arrays/objects of <Node>
s.
const ERROR_MESSAGE = <Node uri="error">Error</Node>;
function SomeComponent({ error }) {
return (
<div>
<p>Some stuff</p>
{error && <div>{ERROR_MESSAGE}</div>}
<ForceNodes>{ERROR_MESSAGE}</ForceNodes>
</div>
);
}
You can pass multiple nodes like so:
<ForceNodes>
{node1}
{node2}
</ForceNodes>
Sometimes it’s nice to store a bunch of nodes in an object. <ForceNodes>
accepts that as well:
const ERRORS = {
invalid: <Node uri="error/invalid">Invalid data.</Node>,
missing: <Node uri="error/missing">Missing data.</Node>,
};
<ForceNodes>{ERRORS}</ForceNodes>;
djedi
The djedi
object is not React specific and lets you:
- Configure options.
- Load nodes manually, mostly for server-side rendering.
- Make integrations with other frameworks than React.
This could be extracted into its own package if other framework integrations are made in the future.
Options
You can configure djedi-react by mutating djedi.options
.
You most likely want to set these:
djedi.options.baseUrl = "/cms";
djedi.options.languages.default = "sv-se";
djedi.options.fetch = unfetch; // From the "isomorphic-unfetch" package.
These are the toplevel keys:
baseUrl
string
Default: "/djedi"
The base URL to make requests to. By default, requests go to for example
/djedi/nodes
. By setting baseUrl: "https://api.example.com/cms"
, requests
would go to https://api.example.com/cms/nodes
instead.
You probably want to set the baseUrl
conditionally based on whether the code
runs on the server (an internal network URL) or in the browser (a public URL).
batchInterval
number
Default: 10 (milliseconds)
The requests for all <Node>
s that are rendered within this number of
milliseconds are batched together into a single request. This is to cut down on
the number of requests made to the backend, and to allow the backend to do more
efficient batch database lookups.
Behind the scenes, <Node>
uses djedi.getBatched, so
technically speaking this option only configures that method, not <Node>
by
itself.
Setting batchInterval: 0
disables batching altogeher, making
djedi.getBatched behave just like djedi.get.
defaultRender
function
Default: See below.
The function receives two arguments (the current state and the current language) and must return anything that the rendering implementation allows (such as a React node).
Nodes can be in one of three states: loading
, error
or success
. They start
out in the loading
state, and then progress into either error
(if fetching
the node content failed) or success
(if fetching the node content succeeded).
If a node was already in the cache, the loading
state is skipped, going
straight to success
.
Here’s the default implementation:
(state, { language }) => {
// If you override this, you could switch on `language` as well to support
// multiple languages.
switch (state.type) {
case "loading":
return "Loading…";
case "error":
return `Failed to fetch content 😞 (${
state.error.response != null ? state.error.response.status : -1
})`;
case "success":
return state.content;
default:
return null;
}
};
These are the possible states:
{ type: "loading" }
: The node is loading.{ type: "error", error: Error }
: Fetching the node failed. Theerror
property contains anError
instance.error.response
is theResponse
from the promise returned by djedi.options.fetch (if any), with two extra properties:__input
: object. The sent JSON object.__output
: string (or any JSON type). The response body if available.
{ type: "success", content: string | React.Node }
: Fetching the node succeeded. Thecontent
property is a string containing the fetched value ifedit=false
and a React node (a<span>
) otherwise.
You can use this option to translate the default “Loading” and “Error” messages to another language, or perhaps render a spinner instead of “Loading…” (maybe even after a small timeout).
fetch
function
Required. Default: Throws an error.
A function that implements the standard fetch interface – and works both on the server and in the browser. isomorphic-unfetch is recommended, but you can use anything you like.
This option lets you:
- Use the same
fetch
as the rest of your app. - Modify the requests, such as adding headers.
import unfetch from "isomorphic-unfetch";
// Simple version:
djedi.options.fetch = unfetch;
// Adding headers:
djedi.options.fetch = (url, options = {}) =>
unfetch(url, {
...options,
headers: {
...options.headers,
Authorization: AUTH,
},
});
languages
object
Default: See below.
The most important part of this option is setting the default language. In the Django backend, it defaults to the LANGUAGE_CODE Django setting, but djedi-react can’t know about that value so it needs its own defaults.
This is the default value:
{
default: "en-us",
additional: [],
}
For example, to set the default to sv-se
and adding de-de
as an additional
language:
{
default: "sv-se",
additional: ["de-de"],
}
The above allows passing de-de
as language to some of the djedi
methods, as
well as to NodeContext. The full list of available languages
must be passed explicitly, to avoid accidentally allowing hackers to fill up the
cache with nodes for bogus languages.
uri
object
Default: See below.
The Django backend also allows customizing defaults and separators for the node URIs. If you do that, you need to make the same customizations in djedi-react.
This is the default value:
({
defaults: {
scheme: "i18n",
namespace: "",
path: "",
ext: "txt",
version: "",
},
namespaceByScheme: {
i18n: "{language}",
l10n: "local",
g11n: "global",
},
separators: {
scheme: "://",
namespace: "@",
path: "/",
ext: ".",
version: "#",
},
});
In namespaceByScheme
, the special value "{language}"
is replaced with the
language passed to some of the djedi
methods.
Common methods
Some of these methods are commonly used in server-side rendering scenarios, but are also useful for avoiding excessive loading indicators.
Interfaces used by several methods:
type Node = {
uri: string,
value: string | undefined,
};
type Nodes = {
[uri: string]: string | undefined,
};
Node
represents a single node, while Nodes
represents a dict of nodes (a
URI→value mapping).
When Node
or Nodes
is used as input:
uri
may be a partial URI, such ashome/title
.value
is the default value, if any.
When Node
or Nodes
is used as output:
uri
is an absolute URI, such asi18n://en-us@home/title.txt
.value
is the final value, if any. The final value can be the default value (if any), or a value entered by the user in the admin sidebar. It can also be transformed (such as markdown→HTML).
djedi.injectAdmin(): Promise<boolean>
Fetches an HTML snippet for the admin sidebar and appends it to <body>
.
If the user does not have admin permissions, it does not append anything. The permission check is session cookie based: Cookies are sent along with the request, and if a 403 response is received that is treated as not having permission.
The returned Promise
resolves to a boolean indicating whether any HTML was
inserted, or rejects with an error if the request fails.
It’s common to call this method when your application starts up. If your site has a login/logout feature, you might want to call the method again after login, in case the user is an admin.
On the server, this method no-ops and always returns Promise<false>
.
djedi.removeAdmin(): void
Removes the admin sidebar inserted by djedi.injectAdmin.
This is useful if you have a login/logout feature on your site, and some users are admins. When logging out an admin, you probably want to remove the admin sidebar as well. If you don’t, djedi-react will likely reload the sidebar after the logout because of DOM/node changes. That will cause the sidebar iframe to fail to load because you’re not logged in anymore, but the now defunct iframe will still stick around.
On the server, this method no-ops.
djedi.prefetch({ filter?: Uri => boolean, extra?: Array<Node>, language?: string } = {}): Promise<void>
Fetches all nodes that djedi.reportPrefetchableNode has reported, and adds the nodes to the cache. Useful for server-side rendering, and for avoiding excessive loading indicators.
By calling this before rendering, node contents will be available straight away. (Mostly.) No loading indicators. No re-renders.
You may optionally pass a filter function, that decides which URIs to fetch (by
returning true
or false
). The passed URI can look like this:
({
scheme: "i18n",
namespace: "en-us",
path: "some/path/text",
ext: "txt",
version: "",
});
You can also pass some extra nodes to be prefetched. That’s useful when you have nodes with dynamic URIs that the Babel plugin cannot automatically do its thing with. These won’t be matched against the filter function.
function prefetch({ storeId }) {
return djedi.prefetch({
filter: uri => uri.path.startsWith("some/path/"),
extra: [
{ uri: `some/${dynamic}/path`, value: "default" },
makeStoreInfoNode(storeId),
],
});
}
function makeStoreInfoNode(storeId) {
return {
uri: `store/${storeId}/intro`,
value: md`
<h1>Welcome!</h1>
Check out our latest offers.
`,
};
}
function Page({ storeId }) {
const node = makeStoreInfoNode(storeId);
return <Node uri={node.uri}>{node.value}</Node>;
}
By passing in language
, you can choose which language to use.
Note that the first prefetch with a new language will request all nodes
reported by djedi.reportPrefetchableNode, not just the ones for the current
page (because djedi-react can’t know which nodes are required for the current
page).
Nodes behind conditionals
There’s a scenario where you cannot rely on all nodes on a page being available
straight away after a successful djedi.prefetch()
.
Here’s an example:
import React from "react";
import { Node } from "djedi-react";
import HelpPopup from "../components/HelpPopup";
// Imagine `djedi.prefetch()`, `djedi.track()` and `djedi.addNodes(nodes)` have
// been called before this renders (as expected).
export default function SomePage() {
return (
<div>
<Node uri="SomePage/intro">Hello!</Node>
<HelpPopup />
</div>
);
}
As seen above, the page contains a <Node>
.
The <HelpPopup />
component also contains a <Node>
, for its help text, but
that text isn’t shown until the user hovers a little question mark icon.
The first time this is rendered on the server, neither the page nor
HelpPopup.js
has run yet, so the djedi.reportPrefetchableNode()
calls
inserted by the Babel plugin are executed. This way djedi.prefetch()
knows
about both the page node and the help text node.
But djedi.track()
will only catch the nodes that were actually rendered. That
is, the page node but not the HelpPopup node. This is to keep things consistent.
The next time this is rendered on the server, the JavaScript files in question
have already been run and as such won’t run again – including the
djedi.reportPrefetchableNode()
calls. So on a second render the server
wouldn’t know that the <HelpPopup />
could potentially be used. To avoid
surprises, the same nodes should always be returned to the browser.
<HelpPopup>
might make DOM measurements to position the popup. In this case it
is not safe to rely on the node content being available straight away – it might
load later. This is a case where using ForceNodes or the
render prop can be helpful. Using them, you can make the node load
earlier and/or update things when state.type
changes.
djedi.track(): Object
Returns an object that will be mutated by all subsequent djedi.get
and
djedi.getBatched
calls – until the next djedi.track
call. Used to track
which nodes are actually rendered during server-side rendering.
djedi.addNodes(nodes: Nodes): void
Adds the given nodes to the cache. Usually comes from djedi.track
and done in
the browser after server-side rendering.
djedi.setCache(ttl: number): void
number
Default: 20e3 (20 seconds) on the server, Infinity in the browser.
The ttl specifies how often to refresh a node. When the ttl has passed the node is still used, but a request to refresh it is made, so that future lookups get a fresh value. This means that the cache may serve stale nodes, and nothing is ever deleted from the cache, only added. The cache must be able to fit all of your nodes in memory.
The cache needs to work this way due to djedi.prefetch()
. When the server
starts, djedi.prefetch
does not know about any nodes of your site. During the
first request, djedi.reportPrefetchableNode
will be called for the pages and
components imported to render that request. So djedi.prefetch
will prefetch
those. When somebody visits another page, djedi.reportPrefetchableNode
will be
called for all new nodes of that page, making djedi.prefetch
learn about those
as well. djedi.prefetch
will go through all of the nodes it knows about, but
only request those not in the cache.
If nodes were deleted from the cache when the ttl had passed, djedi.prefetch()
could eventually refetch all nodes on your entire site when visiting any page!
When somebody requests a page for the second time, no
djedi.reportPrefetchableNode
calls will be made. They only run when a module
is imported the first time. So djedi.prefetch
won’t do anything. The page will
be rendered with whatever nodes are in cache, but all rendered nodes will be
refetched for future use if the ttl has passed.
The goal with the server side rendering is to never render “Loading…”. Any other text is better, even if it is stale. The only way to achieve this is to make sure that all nodes always have something in the cache.
Previous versions of djedi-react allowed passing a custom cache to this method. This has been removed since the caching has gotten the very specific behavior mentioned above and it’s not clear if it’s an actual use case to provide your own cache.
Less common methods
These methods are useful if you need to manually fetch nodes (rather than using
the <Node>
component). (Don’t forget that you also can use the
render prop.)
djedi.get(node: Node, callback: (Node | Error) => void, options = {}): void
Fetch a single node. This takes a callback instead of a Promise
, so the
callback can be invoked synchronously if the node already exists in cache
(Promise
s are always asynchronous). This is crucial for server-side
rendering support.
Note that the callback is called with either a Node
or an Error
. You can use
node instanceof Error
as a check.
Options:
- language:
string
. Which language to use.
djedi.getBatched(node: Node, callback: (Node | Error) => void, options = {}): void
Like djedi.get
, but doesn’t make a network request straight away, batching up
with other djedi.getBatched
requests made during
djedi.options.batchInterval).
djedi.reportPrefetchableNode(node: Node): void
Registers the passed node as available for prefetching. You most likely won’t use this method directly. Instead, it will be automatically inserted into your code by a Babel plugin. See server-side rendering.
Methods for rendering implementations
These methods are used by the <Node>
implementation, and would be useful for
other rendering integrations than React. You will probably never call these
yourself.
djedi.reportRenderedNode(node: Node, options = {}): void
Report that a node has been rendered, so that window.DJEDI_NODES
and the admin
sidebar can be kept up-to-date.
Options:
- language:
string
. Which language to use.
djedi.reportRemovedNode(uri: string, options = {}): void
Report that a node has been removed, so that window.DJEDI_NODES
and the admin
sidebar can be kept up-to-date.
Options:
- language:
string
. Which language to use.
djedi.element(uri: string): object
Returns an object containing the tag name and attributes for the wrapping element of an editable node. It is then up to the rendering implementation to use this information in some way.
({
tag: "span",
attributes: {
"data-i18n": "<full URI>",
},
});
Other methods
Might be useful for unit tests, but otherwise you’ll probably won’t need these.
djedi.resetState(): void
Resets the nodes cache, window.DJEDI_NODES
and all other state.
djedi.resetOptions(): void
Resets djedi.options back to the defaults.
md
This is a template literal tag. It’s meant to be used for the children
of
<Node>
in some cases:
- When your default text contains characters that are annoying to escape in JSX.
- When your default is markdown.
- When you define the default as a string outside
<Node>
, as in the djedi.prefetch example.
Why tag the template literal with md
? Because md
dedents your string, so
that you can format it nicely without worrying about extraneous whitespace
(which can turn text into code blocks in markdown, for example).
<Node uri="home/text.md">{md`
## Using markdown
Some **bold** text.
<video src="/media/intro.webm"></video>
function some(code) {
return code;
}
`}</Node>
// Equivalent to:
<Node uri="uri">{md`## Using markdown
Some **bold** text.
<video src="/media/intro.webm"></video>
function some(code) {
return code;
}`}</Node>
So why is it called md
and not dedent
? If you use Prettier, it will
automatically format the contents of template literal tagged with md
as
markdown, which is very convenient. This is useful even if the value is plain
text (markdown formatting usually works well there too).
As an extra bonus, the Babel plugin mentioned in Installation
optimizes the tag away (md`text`
→ "text"
) when used inside a <Node>
.
Django settings
Cross-domain
If you run your React frontend and your Django backend on different domains, you need to add some extra settings on the Django side.
Let’s say the React frontend lives on example.com
while the Django backend
lives on api.example.com
. Then you need a couple of things:
CORS headers, for example using django-cors-headers or nginx. This is to allow djedi-react making AJAX requests to fetch nodes. You need to allow
GET
andPOST
requests with credentials (cookies) fromexample.com
.DJEDI_XSS_DOMAIN = 'example.com'
in your Django settings file. This is to allow the admin sidebar iframe to reach into its parent page. This should be a common superdomain of the frontend and backend domains. See document.domain for more information.SESSION_COOKIE_DOMAIN = '.example.com'
in your Django settings file. djedi.injectAdmin needs to make a request for the admin sidebar, and has to send along the session cookie created when logging in to the Django admin onapi.example.com
. This makes the cookie available on both domains.MEDIA_URL = 'https://api.example.com/media/'
or similar, if you use the image plugin. When Serving static files during development you might want to setMEDIA_URL = '/media/'
and proxy/media/
on the frontend to the api. The point here is that Djedi returns<img src="...">
snippets to djedi-react, and it is up to you to make sure that the URLs in thesrc
attribute work.
If the two domains do not share the same super domain (such as site.com
and
api.com
) you need to set up a proxy server on the React frontend domain. For
example, you could proxy site.com/djedi
to api.com/djedi
.
Prettier and markdown
One of the reasons to use the md tag is that it allows Prettier to automatically format your markdown.
Djedi uses Python-Markdown, which unfortunately handles list items differently from CommonMark (which Prettier uses): Python-Markdown/markdown#751.
To work around this issue, you can add the mdx_truly_sane_lists extension to your requirements and configure Djedi to use it:
# https://github.com/Python-Markdown/markdown/issues/751
DJEDI = {"MD": {"EXTENSIONS": ["mdx_truly_sane_lists"]}}
Development
You can either install Node.js 10 (with npm 6) or use docker.
npm scripts
npm start
: Start the Next.js example dev server. http://localhost:3000npm run watch
: Start Jest in watch mode. Outside docker you can usenpm run jest -- --watch
instead.npm run eslint
: Run ESLint (including Prettier).npm run eslint:fix
: Autofix ESLint errors.npm run prettier
: Run Prettier for files other than JS.npm run doctoc
: Run doctoc on README.md.npm run jest
: Run unit tests.npm run coverage
: Run unit tests with code coverage.npm build
: Compile with Babel.npm test
: Check that everything works.npm publish
: Publish to npm, but only ifnpm test
passes.
docker
# Build:
docker-compse build
# Start containers:
docker-compose up -d
# Run some npm script:
docker-compose exec node npm test
# Run Jest in watch mode:
docker run --rm -it -v /absolute/path/to/djedi-cms/djedi-react:/code -v /code/node_modules djedi-cms_node npm run watch
Directories
src/
: Source code.test/
: Tests.dist/
: Compiled code, built bynpm run build
. This is what is published in the npm package.pages/
andcomponents/
: Next.js example app.stub/
: An empty djedi-react package to fool Next.js packages check.
docker and permissions
docker runs as root, so files that it creates are owned by root. If you run docker and then try to run some npm scripts outside docker, they might fail because of permissions. One solution is to remove the owned-by-root files first:
npm start
:sudo rm -r .next
npm run build
:sudo rm -r dist