## Render a post For the first iteration, let's focus on just being able to render a single post. The filename without the extension is the *slug*, the final path component that identifies the content. We'll create a route handler that serves content using the slug, so the URL to view the content from `posts/my-first-post.md` on localhost for our example will look like this: http://localhost:8000/my-first-post To parameterize the route handler, create a file called `routes/[slug].tsx`. ``` touch routes/\[slug\].tsx ``` ### Load the post content Our route handler will need a way to read and transform the markdown content into html. We'll delegate that functionality to a helper function that we put into a separate module: ``` mkdir lib touch lib/posts.ts ``` For the first go at this, we'll simply return the raw markdown content without transforming it. Add the following code to `lib/posts.ts`: ```typescript import { join } from "@std/path"; const POSTS_DIR = "./posts"; export interface Post { slug: string; content: string; } // Just return the raw markdown content for now export async function getPost(slug: string): Promise<Post> { const text = await Deno.readTextFile(join(POSTS_DIR, `${slug}.md`)); const post = { slug, content: text, }; return post; } ``` We import `join` from `@std/path` in the standard library, so add that to your import map in `deno.json`: ``` deno add jsr:@std/path ``` ### Render the raw post Now we can implement our slug handler. Add the following to `routes/[slug].tsx`: ```typescript import { Handlers, PageProps } from "$fresh/server.ts"; import { getPost, Post } from "../lib/posts.ts"; export const handler: Handlers<Post> = { async GET(_req, ctx) { try { const post = await getPost(ctx.params.slug); return ctx.render(post); } catch (err) { console.error(err); return ctx.renderNotFound(); } }, }; export default function PostPage(props: PageProps<Post>) { const post = props.data; return ( <> <main> <div dangerouslySetInnerHTML={{ __html: post.content }} /> </main> </> ); } ``` If it isn't already running from before, start the server again: ``` deno task start ``` Navigate to the following URL in your browser: http://localhost:8000/my-first-post You should be able to view the raw page content in your browser. The next steps are to be able to parse the front matter and render the content body markdown as HTML. ## Parse the front matter The standard library supports [parsing front matter](https://jsr.io/@std/front-matter). Add the following to the `deno.json` import map: ``` deno add jsr:@std/front-matter ``` We're using YAML in the front matter for our posts, so we need to import [extractYaml](https://jsr.io/@std/front-matter#yaml) and make a few other changes to `lib/posts.ts`. The updated version should look like this: ```typescript import { extractYaml } from "@std/front-matter"; import { join } from "@std/path"; const POSTS_DIR = "./posts"; // This is what gets parsed from the post front matter interface FrontMatter { title: string; published_at: string; blurb: string; } // This is what gets used for rendering export interface Post { slug: string; title: string; publishedAt: Date | null; blurb: string; content: string; } export async function getPost(slug: string): Promise<Post> { const text = await Deno.readTextFile(join(POSTS_DIR, `${slug}.md`)); const { attrs, body } = extractYaml<FrontMatter>(text); const post = { slug, title: attrs.title, publishedAt: attrs.published_at ? new Date(attrs.published_at) : null, blurb: attrs.blurb, content: body, }; return post; } ``` ## Render the post markdown To render to HTML, we'll need to add the [GitHub Flavored Markdown rendering package](https://jsr.io/@deno/gfm): ``` deno add jsr:@deno/gfm ``` Update `routes/[slug].tsx`: ```typescript import { CSS, render } from "@deno/gfm"; import { Head } from "$fresh/runtime.ts"; import { Handlers, PageProps } from "$fresh/server.ts"; import { getPost, Post } from "../lib/posts.ts"; export const handler: Handlers<Post> = { async GET(_req, ctx) { try { const post = await getPost(ctx.params.slug); return post.publishedAt ? ctx.render(post) : ctx.renderNotFound(); } catch (err) { console.error(err); return ctx.renderNotFound(); } }, }; export default function PostPage(props: PageProps<Post>) { const post = props.data; return ( <> <Head> <style dangerouslySetInnerHTML={{__html: CSS}}/> </Head> <main> <h1>{post.title}</h1> <time> {post.publishedAt!.toLocaleDateString("en-us", { year: "numeric", month: "long", day: "numeric", })} </time> <div class="markdown-body" dangerouslySetInnerHTML={{__html: render(post.content)}} /> </main> </> ); } ``` We're now injecting CSS into the page as well as rendering a title, the time of publication, and the content. Of course, the only styling we've added is specifically to support markdown elements, but nothing else looks pretty. Let's fix that with a bit of Tailwind. Here's final version of `routes/[slug].tsx`: ```ts import { CSS, render } from "@deno/gfm"; import { Head } from "$fresh/runtime.ts"; import { Handlers, PageProps } from "$fresh/server.ts"; import { getPost, Post } from "../lib/posts.ts"; export const handler: Handlers<Post> = { async GET(_req, ctx) { try { const post = await getPost(ctx.params.slug); return post.publishedAt ? ctx.render(post) : ctx.renderNotFound(); } catch (err) { console.error(err); return ctx.renderNotFound(); } }, }; export default function PostPage(props: PageProps<Post>) { const post = props.data; return ( <> <Head> <style dangerouslySetInnerHTML={{__html: CSS}}/> </Head> <main class="max-w-screen-md px-4 pt-16 mx-auto"> <h1 class="text-5xl font-bold">{post.title}</h1> <time class="text-gray-500"> {post.publishedAt!.toLocaleDateString("en-us", { year: "numeric", month: "long", day: "numeric", })} </time> <div class="mt-8 markdown-body" dangerouslySetInnerHTML={{__html: render(post.content)}} /> </main> </> ); } ``` Now take look. Its looking more like a proper blog post now!