|
| 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 | +``` |
0 commit comments