## 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!