Dimer Content
Create markdown collections and snippets and later render them to HTML
Note: This package is ESM only
Dimer content offers an API to create a collection of markdown files or define independent markdown snippets. Later, you can query the content files from the collection or snippets and render them to HTML.
The Markdown to HTML conversion process is powered by @dimerapp/markdown, @dimerapp/shiki, and optionally you can use @dimerapp/edge to render the AST to HTML using Edge templates.
Think of this package as a Swiss army knife for rendering Markdown to HTML with complete control over each Markdown node.
Install the package from the npm packages registry.
npm i @dimerapp/content
yarn add @dimerapp/content
Let's start by creating a collection first. Each collection has a database file, a JSON file with one or more entries.
import { Collection } from '@dimerapp/content'
const docs = new Collection().db('./docs/db.json').urlPrefix('/docs')
await docs.boot()
The docs.boot
method will load the database file from the disk and validates its contents. The JSON file must have an array of collection entries, and each must have the following properties.
- permalink: A unique URL for the collection entry.
- title: Human readable title for the collection entry.
- contentPath: The relative path to the markdown file.
[
{
"permalink": "/introduction",
"title": "Introduction",
"contentPath": "./introduction.md"
}
]
Once you have defined the collection, you can use the findByPermalink
method to find entries and render them to HTML.
Following is a complete example of finding collection entries and rendering them during an HTTP request.
import { Collection } from '@dimerapp/content'
import { createServer } from 'node:http'
import { parse } from 'node:url'
const docs = new Collection().db('./docs/db.json').urlPrefix('/docs')
await docs.boot()
createServer((req, res) => {
const { pathname } = parse(req.url, false)
const entry = docs.findByPermalink(pathname)
if (!entry) {
res.statusCode = 404
res.end('404')
return
}
res.statusCode = 200
res.setHeader('content-type', 'text/html')
res.end(await entry.render())
})
The entry.render
method will render the Markdown file to HTML. The real magic happens when you can control how the Markdown is rendered.
Use the Edge template engine and the @dimer/edge package to customize markdown rendering.
npm i @dimerapp/edge edge.js
The edge.mount
method in the following example defines the root directory for finding templates.
import { Edge } from 'edge.js'
import { fileURLToPath } from 'node:url'
import { dimer, RenderingPipeline } from '@dimerapp/edge'
import { Collection, Renderer } from '@dimerapp/content'
const edge = new Edge()
const viewsDir = new URL('./views', import.meta.url)
edge.mount(fileURLToPath(viewsDir))
edge.use(dimer)
The rendering pipeline is used to hook into the Markdown AST to the HTML rendering process and use custom Edge components for rendering AST nodes.
import { Edge } from 'edge.js'
import { fileURLToPath } from 'node:url'
import { dimer, RenderingPipeline } from '@dimerapp/edge'
import { Collection, Renderer } from '@dimerapp/content'
const edge = new Edge()
const viewsDir = new URL('./views', import.meta.url)
edge.mount(fileURLToPath(viewsDir))
edge.use(dimer)
const pipeline = new RenderingPipeline()
const renderer = new Renderer(edge, pipeline)
Finally, we have to share the renderer instance with the collection and create a basic edge template to render the Markdown.
const renderer = new Renderer(edge, pipeline)
const collection = new Collection()
.db('./content/db.json')
.urlPrefix('/docs')
.useRenderer(renderer)
.useTemplate('docs')
await collection.boot()
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title></title>
</head>
<body>
@!component('dimer_contents', { nodes: file.ast.children, pipeline })~
</body>
</html>
Once the initial setup is done, you can use the collection.findByPermalink
method to find an entry and call the entry.render
method to render the Markdown to HTML.
However, this time, you can use the pipeline
instance to hook into the markdown rendering process.
In the following example, we check if an AST node has a CSS class alert
and render an edge component alert.edge
file. The pipeline.component
method accepts the component file path as the first argument and its data as the second argument.
import { hasClass } from '@dimerapp/edge/utils'
const pipeline = new RenderingPipeline()
pipeline.use((node) => {
if (hasClass(node, 'alert')) {
return pipeline.component('alert', { node })
}
})
Let's create the alert.edge
template and write the following code inside it.
<div {{ dimer.utils.stringifyAttributes(node.properties) }}>
<div class="alert_icon">
@if(dimer.utils.hasClass('alert-info'))
// Info icon svg
@elseif(dimer.utils.hasClass('alert-tip'))
// Tip icon svg
@elseif(dimer.utils.hasClass('alert-warning'))
// Warning icon svg
@end
</div>
<div class="alert_contents">
@!component('dimer_contents', { nodes: node.children, pipeline })~
</div>
</div>
- The
node
is an AST node of the HAST syntax tree. It has the following properties.{ type: 'element', tagName: 'div', properties: { className: ['alert', 'alert-tip'], }, children: [ // ...children nodes ] }
- The
dimer.utils.stringifyAttributes
method takes thenode.properties
object and converts it into a string of HTML attributes. - Using the Edge conditionals, we render a different icon for each alert type.
- Finally, we use the
dimer_contents
component to render the children nodes of the alert node.
Dimer content uses Shiki for rendering code blocks, and you can define a custom theme using the renderer.codeBlocksTheme
method.
See also: List of inbuilt themes.
const renderer = new Renderer(edge, pipeline)
// Use an inbuilt theme
renderer.codeBlocksTheme('material-theme-palenight')
// Load custom them from a JSON file
renderer.codeBlocksTheme(new URL('./custom-theme.json', import.meta.url))
Register a custom VSCode language grammar file using the renderer.registerLanguage
method.
See also: List of inbuilt languages
const renderer = new Renderer(edge, pipeline)
Shiki.registerLanguage({
scopeName: 'text.html.edge',
id: 'edge',
path: fileURLToPath(new URL('../edge.tmLanguage.json', import.meta.url)),
})
You can get an array of collection entries using the collection.all
method. The return value is an array of CollectionEntry class instances.
const collection = new Collection().db('./content/db.json').urlPrefix('/docs')
await collection.boot()
console.log(collection.all())
When defining links in Markdown, you can link to Markdown files across all the collections, and Dimer content will replace the file path with the entry permalink.
In the following example, we create a link to the ./foo.md
file. However, this link will be replaced behind the scenes with the permalink of the ./foo.md
file defined inside the database JSON file.
[Learn more](./foo.md)
Snippets are independent markdown files (without any collection) that you can register and render inside existing Edge templates.
For example, You are creating a homepage using HTML and want to display code examples. You can create a snippet for each code example and render it anywhere.
import { Snippet } from '@dimerapp/content'
const routeExampleFile = new URL('./routes.md', import.meta.url)
const routingExample = Snippet.create(routeExampleFile)
edge.render('home', {
routingExample,
})
Inside the home.edge
template, you can call the routingExample.render
method to render the snippet to HTML.
{{{ await routingExample.render() }}}
Snippets can also be created from raw text. For example:
const routingExample = Snippet.createFromContents(`
router.get('posts', async ({ view }) => {
return view.render('posts/index')
})
// Handle POST request
router.post('posts', async ({ request }) => {
return request.body()
})
`)
edge.render('home', {
routingExample,
})