{"id":15045981,"url":"https://github.com/denoland/fresh-blog-example","last_synced_at":"2025-10-19T21:32:21.033Z","repository":{"id":65977111,"uuid":"557006925","full_name":"denoland/fresh-blog-example","owner":"denoland","description":"An example for building a blog with Fresh.","archived":false,"fork":false,"pushed_at":"2024-10-07T13:45:41.000Z","size":30,"stargazers_count":30,"open_issues_count":2,"forks_count":13,"subscribers_count":16,"default_branch":"main","last_synced_at":"2025-01-30T06:11:13.301Z","etag":null,"topics":["deno","fresh","typescript"],"latest_commit_sha":null,"homepage":"https://fresh-blog-example.deno.dev/","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/denoland.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2022-10-24T23:21:55.000Z","updated_at":"2024-10-21T04:13:27.000Z","dependencies_parsed_at":"2024-09-25T01:59:20.767Z","dependency_job_id":"9688e4b6-26c5-43e6-b346-413c4d67ed2b","html_url":"https://github.com/denoland/fresh-blog-example","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/denoland%2Ffresh-blog-example","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/denoland%2Ffresh-blog-example/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/denoland%2Ffresh-blog-example/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/denoland%2Ffresh-blog-example/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/denoland","download_url":"https://codeload.github.com/denoland/fresh-blog-example/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":237221176,"owners_count":19274447,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["deno","fresh","typescript"],"created_at":"2024-09-24T20:52:32.609Z","updated_at":"2025-10-19T21:32:20.738Z","avatar_url":"https://github.com/denoland.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# How to Build a Blog with Fresh\n\n_This is an example repo of a blog built with Fresh. Original tutorial can be\n[viewed here](https://deno.com/blog/build-a-blog-with-fresh)._\n\n[Fresh](https://fresh.deno.dev) is an edge-first web framework that delivers\nzero JavaScript to the client by default with no build step. It’s optimized for\nspeed and, when hosted on the edge with [Deno Deploy](/deploy), can be\n[fairly trivial to get a perfect Lighthouse pagespeed score](https://deno.com/blog/ecommerce-with-perfect-lighthouse-score).\n\nThis post will show you how to build your own markdown blog with Fresh and\ndeploy it to the edge with Deno Deploy.\n\n## Create a new Fresh app\n\nFresh comes with its own install script. Simply run:\n\n```shell\ndeno run -A -r https://fresh.deno.dev my-fresh-blog\n```\n\nWe’ll select yes for [Tailwind](https://tailwindcss.com/) and VSCode.\n\nLet’s run `deno task start` to see the default app:\n\n![Our default fresh app](https://deno.com/blog/build-a-blog-with-fresh/default-fresh-app.png)\n\nVoila!\n\n## Update the directory structure\n\nThe Fresh init script scaffolds a generic app directory. So let’s modify it to\nfit the purposes of a blog.\n\nLet’s add a `posts` folder that will contain all markdown files:\n\n```shell\n$ mkdir posts\n```\n\nAnd remove the unnecessary `components`, `islands`, and `routes/api` folders:\n\n```shell\n$ rm -rf components/ islands/ routes/api\n```\n\nThe final top-level directory structure should look something like this:\n\n```\nmy-fresh-blog/\n├── .vscode\n├── posts\n├── routes\n├── static\n├── deno.json\n├── dev.ts\n├── fresh.gen.ts\n├── import_map.json\n├── main.ts\n├── README.md\n└── twins.config.ts\n```\n\n## Write a dummy blog post\n\nLet’s create a simple markdown file called `first-blog-post.md` in `./posts` and\ninclude the following frontmatter:\n\n```markdown\n---\ntitle: This is my first blog post!\npublished_at: 2022-11-04T15:00:00.000Z\nsnippet: This is an excerpt of my first blog post.\n---\n\nHello, world!\n```\n\nNext, let’s update the routes to render the blog posts.\n\n## Update the routes\n\nLet’s start with `index.tsx`, which will render the blog index page. Feel free\nto delete everything in this file so we can start from scratch.\n\n### Getting post data\n\nWe’ll create an interface for a Post object, which includes all of the\nproperties and their types. We’ll keep it simple for now:\n\n```ts\ninterface Post {\n  name: string;\n  title: string;\n  publish_date: string;\n  snippet: string;\n}\n```\n\nNext, let’s create a\n[custom `handler` function](https://fresh.deno.dev/docs/getting-started/custom-handlers)\nthat will grab the data from the `posts` folder and transform them into data\nthat we can easily render with tsx.\n\n```ts\nimport { Handlers } from \"$fresh/server.ts\";\n\nexport const handler: Handlers\u003cPost[]\u003e = {\n  async GET(_req, ctx) {\n    const posts = await getPosts();\n    return ctx.render(posts);\n  },\n};\n```\n\nLet’s define a helper function called `getPosts`, which will read the files from\n`./posts` directory and return them as a `Post` array. For now, we can stick it\nin the same file.\n\n```ts\nasync function getPosts(): Promise\u003cPost[]\u003e {\n  const files = Deno.readDir(“./posts”);\n  const promises = [];\n  for await (const file of files) {\n    const slug = file.name.replace(\".md\", \"\");\n    promises.push(getPost(slug));\n  }\n  const posts = await Promise.all(promises) as Post[];\n  posts.sort((a, b) =\u003e b.publishedAt.getTime() - a.publishedAt.getTime());\n  return posts;\n}\n```\n\nWe’ll also define a helper function called `getPost`, a function that accepts\n`slug` and returns a single `Post`. Again, let’s stick it in the same file for\nnow.\n\n```ts\n// Importing two new std lib functions to help with parsing front matter and joining file paths.\nimport { extract } from “$std/encoding/front_matter.ts”\nimport { join } from “$std/path/mod.ts”\n\nasync function getPost(slug: string): Promise\u003cPost | null\u003e {\n  const text = await Deno.readTextFile(join(‘./posts’, `${slug}.md`));\n  const { attrs, body } = extract\u003c{ title: string, published_at: string, snippet: string}\u003e(text);\n  return {\n    slug,\n    title: attrs.title,\n    publishedAt: new Date(attrs.published_at),\n    content: body,\n    snippet: attrs.snippet\n  }\n}\n```\n\nNow let’s put these functions to use and render the blog index page!\n\n### Rendering the blog index page\n\nEach route file must export a default function that returns a component.\n\nWe’ll name our main export function `BlogIndexPage` and render the post data\nthrough that:\n\n```tsx\nimport { PageProps } from “$fresh/server.ts”;\n\nexport default function BlogIndexPage(props: PageProps\u003cPost[]\u003e) {\n  const props = props.data;\n  return (\n    \u003cmain class=\"max-w-screen-md px-4 pt-16 mx-auto\"\u003e\n      \u003ch1 class=\"text-5xl font-bold\"\u003eBlog\u003c/h1\u003e\n      \u003cdiv class=\"mt-8\"\u003e\n        {posts.map((post) =\u003e \u003cPostCard post={post} /\u003e)}\n      \u003c/div\u003e\n    \u003c/main\u003e\n  )\n}\n```\n\nLet’s run our server with `deno task start` and check localhost:\n\n![A first look at our blog index page](https://deno.com/blog/build-a-blog-with-fresh/blog-index-page.png)\n\nAwesome start!\n\nBut clicking on the post doesn’t work yet. Let’s fix that.\n\n### Creating the post page\n\nIn `/routes/`, let’s rename `[name].tsx` to `[slug].tsx`.\n\nThen, in `[slug].tsx`, we’ll do something similar to `index.tsx`: create a\ncustom handler to get a single post and export a default component that renders\nthe page.\n\nSince we’ll be reusing the helper functions `getPosts` and `getPost`, as well as\nthe interface `Post`, let’s refactor them into a separate utility file called\n`posts.ts` under a new folder called `utils`:\n\n```\nmy-fresh-blog/\n…\n├── utils\n│   └── posts.ts\n…\n```\n\nNote:\n[you can add `”/”: “./”, “@/”: “./”` to your `import_map.json`](https://deno.land/manual/linking_to_external_code/import_maps)\nso that you can import from `posts.ts` with a path relative to root:\n\n```ts\nimport { getPost } from “@/utils/posts.ts”\n```\n\nIn our `/routes/[slug].tsx` file, let’s create a custom handler to get the post\nand render it through the component. Note that we can access `ctx.params.slug`\nsince we used square brackets in the filename `[slug].tsx`.\n\n```ts\nimport { Handlers } from “$fresh/server.ts”;\nimport { getPost, Post } from “@/utils/posts.ts”;\n\nexport const handler: Handlers\u003cPost\u003e = {\n  async GET(_req, ctx) {\n    const post = await getPost(ctx.params.slug);\n    if (post === null) return ctx.renderNotFound();\n    return ctx.render(post);\n  }\n}\n```\n\nThen, let’s create the main component for rendering `post`:\n\n```tsx\nimport { PageProps }\n\nexport default function PostPage(props: PageProps\u003cPost\u003e) {\n  const post = props.data;\n  return (\n    \u003cmain class=\"max-w-screen-md px-4 pt-16 mx-auto\"\u003e\n      \u003ch1 class=\"text-5xl font-bold\"\u003e{post.title}\u003c/h1\u003e\n      \u003ctime class=\"text-gray-500\"\u003e\n        {new Date(post.publishedAt).toLocaleDateString(\"en-us\", {\n          year: \"numeric\",\n          month: \"long\",\n          day: \"numeric\"\n        })}\n      \u003c/time\u003e\n      \u003cdiv class=\"mt-8\"\n        dangerouslySetInnerHTML={{ __html: post.content }}\n        /\u003e\n    \u003c/main\u003e\n  )\n}\n```\n\nLet’s check our localhost:8000 and click on the post:\n\n![Our first blog post](https://deno.com/blog/build-a-blog-with-fresh/our-first-blog-post.png)\n\nThere it is!\n\n### Parsing markdown\n\nCurrently, this does not parse markdown. If you write something like this:\n\n![raw markdown blog post file](https://deno.com/blog/build-a-blog-with-fresh/raw-markdown-in-our-first-blog-post.png)\n\nIt’ll show up like this:\n\n![unprocessed markdown on the blog](https://deno.com/blog/build-a-blog-with-fresh/our-first-blog-post-with-failed-markdown.png)\n\nIn order to parse markdown, we’ll need to import the module\n[`gfm`](https://deno.land/x/gfm/mod.ts) and pass `post.content` through the\nfunction [`gfm.render()`](https://deno.land/x/gfm/mod.ts?s=render).\n\nLet’s add this line to `import_map.json`:\n\n```ts\n“$gfm”: “https://deno.land/x/gfm@0.1.26/mod.ts”\n```\n\nAnd update the `\u003cdiv\u003e` in our component to be:\n\n```tsx\n\u003cdiv\n  class=\"mt-8\"\n  dangerouslySetInnerHTML={{ __html: gfm.render(post.content) }}\n/\u003e;\n```\n\nNow, our post looks like:\n\n![markdown working successfully](https://deno.com/blog/build-a-blog-with-fresh/our-first-blog-post-with-markdown.png)\n\nBetter, but we can make this look nicer by injecting `gfm.css` as a style tag.\nSince [`gfm`](https://deno.land/x/gfm/@mod.ts) ships with a stylesheet, we can\naccess that and include it like such:\n\n```ts\n// Other dependencies...\nimport { CSS, render } from \"$gfm\";\n\n// ...\n\nexport default function PostPage(props: PageProps\u003cPost\u003e) {\n  const post = props.data;\n  return (\n    \u003c\u003e\n      \u003cHead\u003e\n        \u003cstyle dangerouslySetInnerHTML={{ __html: CSS }} /\u003e\n      \u003c/Head\u003e\n      // ...\n      \u003cdiv\n        class=\"mt-8 markdown-body\"\n        dangerouslySetInnerHTML={{ __html: render(post.content) }}\n      /\u003e\n    \u003c/\u003e\n  );\n}\n```\n\nNote we'll need to include the class `markdown-body` on the div for the gfm\nstylesheet to work.\n\nNow markdown looks much better:\n\n![markdown working even better](https://deno.com/blog/build-a-blog-with-fresh/better-markdown-styling.png)\n\n## Deploying to the edge\n\n[Deno Deploy](/deploy) is our globally distributed v8 isolate cloud where you\ncan host arbitrary JavaScript. It’s great for hosting serverless functions as\nwell as entire websites and applications.\n\nWe can easily deploy our new blog to Deno Deploy with the following steps.\n\n- Create a GitHub repo for your new blog\n- Go to https://dash.deno.com/ and connect your GitHub\n- Select your GitHub organization or user, repository and branch\n- Select “Automatic” deployment mode Select `main.ts` as an entry point\n- Click “Link”, which will start the deployment\n\nWhen the deployment is complete, you’ll receive a URL that you can visit.\n[Here's a live version.](https://fresh-blog-example.deno.dev/)\n\n## What’s next?\n\nThis is a simple tutorial on building a blog with Fresh that demonstrates how\nFresh retrieves data from a filesystem, which it renders into HTML, all on the\nserver.\n\n_Stuck? Get help with Fresh and Deno on [our Discord](https://discord.gg/deno)\nor [Twitter](https://twitter.com/deno_land)!_\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdenoland%2Ffresh-blog-example","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fdenoland%2Ffresh-blog-example","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdenoland%2Ffresh-blog-example/lists"}