Skip to content

Commit 51fdf1f

Browse files
macdonstcolepeters
andauthored
Rendering Markdown in Enhance (#202)
* Rendering Markdown in Enhance * Add 'frontmatter' to spellcheck * Add filenames to code examples * Add info on rendering md with custom elements * Apply suggestions from code review * Address Cole's review comments * moar spellcheck * Apply suggestions from code review * Remove custom order --------- Signed-off-by: macdonst <simon.macdonald@gmail.com> Co-authored-by: Cole Peters <cole@colepeters.com>
1 parent 86d7115 commit 51fdf1f

File tree

3 files changed

+245
-1
lines changed

3 files changed

+245
-1
lines changed
Lines changed: 242 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,242 @@
1+
---
2+
title: Rendering Markdown
3+
links:
4+
- "Arcdown": https://github.com/architect/arcdown/blob/main/readme.md
5+
---
6+
7+
Enhance can be used to render Markdown with minimal effort — in fact, this very site is itself an Enhance app that renders Markdown to HTML on demand. You can dig into the [source code](https://github.com/enhance-dev/enhance.dev) to see exactly how we've set it up, or follow along below.
8+
9+
## Arcdown
10+
11+
When rendering Markdown to HTML in Enhance projects, we rely on [Arcdown](https://github.com/architect/arcdown), which packages together our preferred conventions for parsing Markdown files. Under the hood, Arcdown makes use of [markdown-it](https://markdown-it.github.io/), an excellent JavaScript Markdown parser that is highly configurable via a wealth of plugins.
12+
13+
Add the Arcdown package to your project:
14+
15+
```bash
16+
npm install arcdown
17+
```
18+
19+
Here's a quick example of parsing a markdown string with Arcdown:
20+
21+
```javascript
22+
import { Arcdown } from 'arcdown'
23+
24+
const mdString = `
25+
---
26+
title: Hello World
27+
category: Examples
28+
---
29+
30+
## Foo Bar
31+
32+
lorem ipsum _dolor_ sit **amet**
33+
34+
[Enhance](https://enhance.dev/)
35+
`.trim()
36+
37+
const arcdown = new Arcdown()
38+
const result = await arcdown.render(mdString)
39+
```
40+
41+
The result of the render returns an object with the following properties:
42+
43+
Property | Description
44+
-: | :-
45+
`html` | The Markdown document content, converted to HTML
46+
`tocHtml` | The document's table of contents, converted to HTML
47+
`title` | The document title, lifted from the document's frontmatter.
48+
`slug` | A URL-friendly slug of the title (possibly empty). Synonymous with links in the table of contents.
49+
`frontmatter` | An object containing all remaining frontmatter (possibly empty)
50+
51+
Arcdown follows the convention of markdown-it as being highly configurable. For example, you can always disable functionality if you don't need things like a table of contents or syntax highlighting.
52+
53+
> For more information on configuring Arcdown see [Configuration](https://github.com/architect/arcdown#configuration)
54+
55+
## Parsing Markdown in an API route
56+
57+
In Enhance apps, Markdown parsing is optimally performed as part an API route. After parsing, we can then pass the result to our page route as part of [the store](/docs/elements/state/store). Let's assume you have a new Enhance project, structured like this:
58+
59+
```
60+
app
61+
├── markdown
62+
| └── example.md
63+
└── pages
64+
└── index.html
65+
```
66+
67+
Now we want to be able to parse the `app/markdown/example.md` file. In order to do that, we'll first need an API route. Create a new folder called `app/api/markdown` and add a catch-all API route named `app/api/markdown/$$.mjs`. Alternatively, you can run the Enhance CLI command:
68+
69+
```bash
70+
enhance gen api --path 'markdown/$$'
71+
```
72+
73+
> **Note:** the single quotes around 'markdown/$$' are important as they prevent the shell from doing variable substitution.
74+
75+
Now our project looks like this:
76+
77+
```
78+
app
79+
├── api
80+
| └── markdown
81+
| └── $$.mjs
82+
├── markdown
83+
| └── example.md
84+
└── pages
85+
└── index.html
86+
```
87+
88+
In our `app/api/markdown/$$.mjs` file, we'll need to import a few packages and instantiate Arcdown.
89+
90+
```javascript
91+
import { readFileSync } from 'fs'
92+
import { URL } from 'url'
93+
import { Arcdown } from 'arcdown'
94+
const arcdown = new Arcdown()
95+
```
96+
97+
Next, in our API route's `get` function, we'll need to figure out which file the user has requested. To do that, we'll determine the document path from the incoming request:
98+
99+
```javascript
100+
const { path: activePath } = req
101+
let docPath = activePath.replace(/^\/?docs\//, '') || 'index'
102+
if (docPath.endsWith('/')) {
103+
docPath += 'index' // trailing slash == index.md file
104+
}
105+
```
106+
107+
Next, we'll use the `docPath` to read our Markdown file from the `app/markdown` folder:
108+
109+
```javascript
110+
const docURL = new URL(`../../${docPath}.md`, import.meta.url)
111+
const docMarkdown = readFileSync(docURL.pathname, 'utf-8')
112+
```
113+
114+
Once we have the Markdown string, we can transform it into HTML using Arcdown and add it to the `store`:
115+
116+
```javascript
117+
const doc = await arcdown.render(docMarkdown)
118+
return {
119+
json: { doc }
120+
}
121+
```
122+
123+
The entire function looks like this:
124+
125+
<doc-code filename="app/api/markdown/$$.mjs">
126+
127+
```javascript
128+
import { readFileSync } from 'fs'
129+
import { URL } from 'url'
130+
import { Arcdown } from 'arcdown'
131+
const arcdown = new Arcdown()
132+
133+
export async function get (req) {
134+
// Get requested path
135+
const { path: activePath } = req
136+
let docPath = activePath.replace(/^\/?docs\//, '') || 'index'
137+
if (docPath.endsWith('/')) {
138+
docPath += 'index' // trailing slash == index.md file
139+
}
140+
141+
// Read markdown file
142+
const docURL = new URL(`../../${docPath}.md`, import.meta.url)
143+
const docMarkdown = readFileSync(docURL.pathname, 'utf-8')
144+
145+
// Convert to HTML and add to store
146+
const doc = await arcdown.render(docMarkdown)
147+
return {
148+
json: { doc }
149+
}
150+
}
151+
```
152+
153+
</doc-code>
154+
155+
> **Note:** We’re omitting a lot of error checking for the sake of brevity, but check out this [hardened example](https://github.com/enhance-dev/enhance.dev/blob/main/app/api/docs/%24%24.mjs) for more details on how to handle 404s.
156+
157+
## Displaying Markdown
158+
159+
Now that we've successfully transformed our Markdown into HTML, let's go about displaying it in a page. First, we'll create a new catch-all page under `app/pages/markdown/$$.html`, and a new web component to display the Markdown at `app/elements/doc-content.mjs`. You can create these files manually, or by running the Enhance CLI commands:
160+
161+
```bash
162+
enhance gen page --path 'markdown/$$'
163+
enhance gen element --name doc-content
164+
```
165+
166+
Our project structure now looks like this:
167+
168+
```
169+
app
170+
├── api
171+
| └── markdown
172+
| └── $$.mjs
173+
├── elements
174+
| └── doc-content.mjs
175+
├── markdown
176+
| └── example.md
177+
└── pages
178+
├── markdown
179+
| └── $$.mjs
180+
└── index.html
181+
```
182+
183+
For the purposes of this example, we'll keep our `app/pages/markdown/$$.html` simple by including only the web component we just created:
184+
185+
<doc-code filename="app/pages/markdown/$$.html">
186+
187+
```html
188+
<doc-content></doc-content>
189+
```
190+
191+
</doc-code>
192+
193+
In the web component we just created, we'll read the result of the transformation off the `store`. Then we'll return the rendered content to be displayed on our page:
194+
195+
<doc-code filename="app/elements/doc-content.mjs">
196+
197+
```javascript
198+
export default function DocContent ({ html, state }) {
199+
const { store } = state
200+
const { doc } = store
201+
return html`
202+
<h1 class="text3">${doc.title}</h1>
203+
<div>
204+
${doc.html}
205+
</div>
206+
`
207+
}
208+
```
209+
210+
</doc-code>
211+
212+
Test it out by starting the development server:
213+
214+
```bash
215+
enhance dev
216+
```
217+
218+
Then open a browser tab to [localhost:3333/markdown/example](https://localhost:3333/markdown/example) and you’ll see the rendered markdown file.
219+
220+
That's all you need in order to get started using markdown in an Enhance app.
221+
222+
## Using custom elements in markdown
223+
224+
It is totally possible to include custom elements in Markdown source files and then have the generated markup rendered by Enhance SSR.
225+
226+
When authoring Markdown with custom elements, it is best to add blank lines around opening and closing tags. For example on this page we use the `doc-code` custom element to provide syntax highlighting of source code. In Markdown, use of a custom element would look like:
227+
228+
```
229+
# Custom Elements in Markdown
230+
231+
Custom HTML elements in Markdown are awesome!
232+
233+
<my-rad-elem hype="9001">
234+
235+
## Some really cool info
236+
237+
> This is rendered as `<h2>` and `<blockquote>` inside `<my-rad-elem>`
238+
239+
</my-rad-elem>
240+
241+
Hey, that's pretty neat!
242+
```

app/docs/nav-data.mjs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,7 @@ export const data = [
146146
items: [{ slug: 'webdriverio', label: 'WebdriverIO' }],
147147
},
148148
'architect-migration',
149+
'rendering-markdown',
149150
],
150151
},
151152
/*

scripts/dictionary.txt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,4 +112,5 @@ Noto
112112
Menlo
113113
monospace
114114
hexadecimal
115-
115+
frontmatter
116+
404s

0 commit comments

Comments
 (0)