Tour: Templates & Markdown
Rex handlers can read files, render markdown, and inject content into HTML templates. This is the content pipeline that powers every page in this tour.
The Pipeline
1 fs.read — read markdown from _content/ or templates from _layouts/
2 markdown.render — convert markdown to HTML (via pulldown-cmark)
3 template.render — inject into an HTML layout using mustache syntax
/* The 3-step content pipeline */ layout = fs.read("routes/_layouts/page.html") content = fs.read("routes/_content/sample-article.md") html-body = markdown.render(content) template.render(layout, {title: "My Page", body: html-body})
Template Syntax
Templates use mustache-style substitution:
{{key}}— HTML-escaped value (safe for user input){{{key}}}— raw/unescaped (for pre-rendered HTML like markdown output)
The layout template for this app:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>{{title}} — rex-serve tour</title> <link rel="icon" type="image/png" href="/favicon.png"> <link rel="stylesheet" href="/style.css"> </head> <body> <header> <div class="container"> <a class="logo" href="/">rex-serve</a> <nav> <a href="/tour/static-files">Static</a> <a href="/tour/routing">Routing</a> <a href="/tour/middleware">Middleware</a> <a href="/tour/templates">Templates</a> <a href="/tour/api">API</a> <a href="/tour/experience">DX Report</a> <a href="/tour/cursors">Cursors</a> <a href="/tour/deseret">𐐔𐐯𐑅𐐨𐑉𐐯𐐻</a> </nav> </div> </header> <main class="container"> {{{body}}} </main> <footer> <div class="container">{{{footer}}}</div> </footer> <script>document.querySelectorAll('pre').forEach(p=>{p.style.position='relative';const b=document.createElement('button');b.className='copy-btn';b.textContent='copy';p.appendChild(b);b.onclick=e=>{e.stopPropagation();const t=document.createElement('textarea');t.value=p.textContent.replace(/copy$/,'').trim();t.style.cssText='position:fixed;opacity:0';document.body.appendChild(t);t.select();document.execCommand('copy');document.body.removeChild(t);b.textContent='copied!';setTimeout(()=>b.textContent='copy',1500)}});(function(){var w,t;function c(){w=new WebSocket('ws://'+location.host+'/__reload');w.onmessage=function(e){console.log('[rex-serve] changed:',e.data);location.reload()};w.onclose=function(){t=setTimeout(c,1000)};w.onerror=function(){w.close()}}c()})() </script> </body> </html>
Private Directories
The _content/ and _layouts/ directories start with _,
so the server will never serve them directly. Try requesting
/_content/sample-article.md — you'll get a 404. But handlers can read
them freely via fs.read().
Live Example: Rendered Markdown
Below is _content/sample-article.md rendered through the pipeline:
Sample Article
This article was loaded from _content/sample-article.md on the filesystem.
How It Got Here
The handler at routes/tour/templates.rex did three things:
- Read this file with
fs.read("routes/_content/sample-article.md") - Converted it to HTML with
markdown.render(content) - Injected the HTML into
_layouts/page.htmlwithtemplate.render(layout, {body: html})
Markdown Features
Rex-serve uses pulldown-cmark for rendering. It supports:
- Bold and italic
inline codeand code blocks- Links and images
- Lists (ordered and unordered)
- Blockquotes
Like this one. Markdown makes it easy to author content without HTML.
/* You can even show Rex code in markdown */
when content do
html = markdown.render(content)
template.render(layout, {body: html})
end
This Page's Source
/* Tour Stop 4: Templates & Markdown */ res.headers.content-type = "text/html; charset=utf-8" layout = fs.read("routes/_layouts/page.html") unless layout do status = 500 return "layout not found" end /* Render the sample article to demonstrate the pipeline */ sample-md = fs.read("routes/_content/sample-article.md") sample-html = when sample-md do html.raw(markdown.render(sample-md)) end /* Pre-highlight sources */ pipeline-snippet = "/* The 3-step content pipeline */ layout = fs.read(\"routes/_layouts/page.html\") content = fs.read(\"routes/_content/sample-article.md\") html-body = markdown.render(content) template.render(layout, {title: \"My Page\", body: html-body})" layout-source = fs.read("routes/_layouts/page.html") hl-layout = when layout-source do html.raw(html.highlight-html(layout-source)) end self-source = fs.read("routes/tour/templates.rex") hl-self = when self-source do html.raw(html.highlight(self-source)) end body = html`<h1>Tour: Templates & Markdown</h1> <p class="source-link"><a href="/tour/api">Next: API →</a></p> <p>Rex handlers can read files, render markdown, and inject content into HTML templates. This is the content pipeline that powers every page in this tour.</p> <h2>The Pipeline</h2> <div class="card"> <p><span class="badge badge-green">1</span> <strong>fs.read</strong> — read markdown from <code>_content/</code> or templates from <code>_layouts/</code></p> <p><span class="badge badge-yellow">2</span> <strong>markdown.render</strong> — convert markdown to HTML (via pulldown-cmark)</p> <p><span class="badge badge-blue">3</span> <strong>template.render</strong> — inject into an HTML layout using mustache syntax</p> </div> <pre>${html.raw(html.highlight(pipeline-snippet))}</pre> <h2>Template Syntax</h2> <p>Templates use mustache-style substitution:</p> <ul> <li><code>{{key}}</code> — HTML-escaped value (safe for user input)</li> <li><code>{{{key}}}</code> — raw/unescaped (for pre-rendered HTML like markdown output)</li> </ul> <p>The layout template for this app:</p> <pre>${hl-layout}</pre> <h2>Private Directories</h2> <p>The <code>_content/</code> and <code>_layouts/</code> directories start with <code>_</code>, so the server will never serve them directly. Try requesting <code>/_content/sample-article.md</code> — you'll get a 404. But handlers can read them freely via <code>fs.read()</code>.</p> <h2>Live Example: Rendered Markdown</h2> <p>Below is <code>_content/sample-article.md</code> rendered through the pipeline:</p> <div class="card">${sample-html}</div> <h2>This Page's Source</h2> <pre>${hl-self}</pre>` template.render(layout, { title:"Templates" body:body footer:"<a href='/tour/middleware'>← Middleware</a> · <a href='/tour/api'>API →</a>" })