跳至主要内容
Building a blog with Fresh.

如何使用 Fresh 构建博客

Fresh 是一个边缘优先的 Web 框架,默认情况下不向客户端交付任何 JavaScript,且无需构建步骤。它针对速度进行了优化,并且当与 Deno Deploy 一起托管在边缘时,可以非常容易地获得完美的 Lighthouse 页面速度评分

本文将向您展示如何使用 Fresh 构建您自己的 markdown 博客,并使用 Deno Deploy 将其部署到边缘。

在此处查看源代码。

创建一个新的 Fresh 应用

Fresh 自带安装脚本。只需运行

deno run -A -r https://fresh.deno.dev my-fresh-blog

我们将为 Tailwind 和 VSCode 选择 yes。

让我们运行 deno task start 来查看默认应用

Our default fresh app

瞧!

更新目录结构

Fresh 初始化脚本搭建了一个通用的应用目录。所以让我们修改它以适应博客的用途。

让我们添加一个 posts 文件夹,它将包含所有 markdown 文件

$ mkdir posts

并删除不必要的 componentsislandsroutes/api 文件夹

$ rm -rf components/ islands/ routes/api

最终的顶层目录结构应如下所示

my-fresh-blog/
├── .vscode
├── posts
├── routes
├── static
├── deno.json
├── dev.ts
├── fresh.gen.ts
├── import_map.json
├── main.ts
├── README.md
└── twins.config.ts

编写一个虚拟博客帖子

让我们在 ./posts 中创建一个名为 first-blog-post.md 的简单 markdown 文件,并包含以下 frontmatter

---
title: This is my first blog post!
published_at: 2022-11-04T15:00:00.000Z
snippet: This is an excerpt of my first blog post.
---

Hello, world!

接下来,让我们更新路由以渲染博客帖子。

更新路由

让我们从 index.tsx 开始,它将渲染博客索引页。随意删除此文件中的所有内容,以便我们可以从头开始。

获取帖子数据

我们将为 Post 对象创建一个接口,其中包含所有属性及其类型。现在我们先保持简单

interface Post {
  slug: string;
  title: string;
  publishedAt: Date;
  content: string;
  snippet: string;
}

接下来,让我们创建一个 自定义 handler 函数,它将从 posts 文件夹中抓取数据,并将其转换为我们可以使用 tsx 轻松渲染的数据。

import { Handlers } from "$fresh/server.ts";

export const handler: Handlers<Post[]> = {
  async GET(_req, ctx) {
    const posts = await getPosts();
    return ctx.render(posts);
  },
};

让我们定义一个名为 getPosts 的辅助函数,它将从 ./posts 目录读取文件,并将它们作为 Post 数组返回。目前,我们可以将其放在同一个文件中。

async function getPosts(): Promise<Post[]> {
  const files = Deno.readDir("./posts");
  const promises = [];
  for await (const file of files) {
    const slug = file.name.replace(".md", "");
    promises.push(getPost(slug));
  }
  const posts = await Promise.all(promises) as Post[];
  posts.sort((a, b) => b.publishedAt.getTime() - a.publishedAt.getTime());
  return posts;
}

我们还将定义一个名为 getPost 的辅助函数,该函数接受 slug 并返回单个 Post。同样,现在让我们将其放在同一个文件中。

// Importing two new std lib functions to help with parsing front matter and joining file paths.
import { extract } from "$std/encoding/front_matter.ts";
import { join } from "$std/path/mod.ts";

async function getPost(slug: string): Promise<Post | null> {
  const text = await Deno.readTextFile(join("./posts", `${slug}.md`));
  const { attrs, body } = extract(text);
  return {
    slug,
    title: attrs.title,
    publishedAt: new Date(attrs.published_at),
    content: body,
    snippet: attrs.snippet,
  };
}

现在让我们使用这些函数并渲染博客索引页!

渲染博客索引页

每个路由文件都必须导出一个返回组件的默认函数。

我们将我们的主要导出函数命名为 BlogIndexPage,并通过它渲染帖子数据

import { PageProps } from "$fresh/server.ts";

export default function BlogIndexPage(props: PageProps<Post[]>) {
  const posts = props.data;
  return (
    <main class="max-w-screen-md px-4 pt-16 mx-auto">
      <h1 class="text-5xl font-bold">Blog</h1>
      <div class="mt-8">
        {posts.map((post) => <PostCard post={post} />)}
      </div>
    </main>
  );
}

我们还需要定义 <PostCard>

function PostCard(props: { post: Post }) {
  const { post } = props;
  return (
    <div class="py-8 border(t gray-200)">
      <a class="sm:col-span-2" href={`/${post.slug}`}>
        <h3 class="text(3xl gray-900) font-bold">
          {post.title}
        </h3>
        <time class="text-gray-500">
          {new Date(post.publishedAt).toLocaleDateString("en-us", {
            year: "numeric",
            month: "long",
            day: "numeric",
          })}
        </time>
        <div class="mt-4 text-gray-900">
          {post.snippet}
        </div>
      </a>
    </div>
  );
}

让我们使用 deno task start 运行我们的服务器并检查 localhost

A first look at our blog index page

很棒的开始!

但是点击帖子还不能工作。让我们修复它。

创建帖子页面

/routes/ 中,让我们将 [name].tsx 重命名为 [slug].tsx

然后,在 [slug].tsx 中,我们将执行类似于 index.tsx 的操作:创建一个自定义处理程序来获取单个帖子,并导出一个渲染页面的默认组件。

由于我们将重用辅助函数 getPostsgetPost,以及接口 Post,让我们将它们重构到一个名为 posts.ts 的单独实用程序文件中,该文件位于名为 utils 的新文件夹下

my-fresh-blog/
…
├── utils
│   └── posts.ts
…

注意:您可以将 "/": "./", "@/": "./" 添加到您的 import_map.json,以便您可以从 posts.ts 导入,路径相对于根目录

import { getPost } from "@/utils/posts.ts";

在我们的 /routes/[slug].tsx 文件中,让我们创建一个自定义处理程序来获取帖子并通过组件渲染它。请注意,由于我们在文件名 [slug].tsx 中使用了方括号,因此我们可以访问 ctx.params.slug

import { Handlers } from "$fresh/server.ts";
import { getPost, Post } from "@/utils/posts.ts";

export const handler: Handlers<Post> = {
  async GET(_req, ctx) {
    const post = await getPost(ctx.params.slug);
    if (post === null) return ctx.renderNotFound();
    return ctx.render(post);
  },
};

然后,让我们创建用于渲染 post 的主组件

import { PageProps }

export default function PostPage(props: PageProps<Post>) {
  const post = props.data;
  return (
    <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">
        {new Date(post.publishedAt).toLocaleDateString("en-us", {
          year: "numeric",
          month: "long",
          day: "numeric"
        })}
      </time>
      <div class="mt-8"
        dangerouslySetInnerHTML={{ __html: post.content }}
        />
    </main>
  )
}

让我们检查我们的 localhost:8000 并点击帖子

Our first blog post

它在那里!

解析 markdown

目前,这不会解析 markdown。如果您写了类似这样的内容

raw markdown blog post file

它会像这样显示

unprocessed markdown on the blog

为了解析 markdown,我们需要导入模块 gfm,并通过函数 gfm.render() 传递 post.content

让我们将此行添加到 import_map.json

"$gfm": "https://deno.land/x/[email protected]/mod.ts"

然后,在 /routes/[slug].tsx 中,我们需要从 $gfm 导入 CSSrender 函数,以及从 Fresh 导入 <Head>

import { CSS, render } from "$gfm";
import { Head } from "$fresh/runtime.ts";

并更新我们的 PostPage 组件为

export default function PostPage(props: PageProps<Post>) {
  const post = props.data;
  return (
    <>
      <Head>
        <style dangerouslySetInnerHTML={{ __html: CSS }} />
      </Head>
      // ...
      <div
        class="mt-8 markdown-body"
        dangerouslySetInnerHTML={{ __html: render(post.content) }}
      />
    </>
  );
}

请注意,我们需要在 div 上包含类 markdown-body,以便 gfm 样式表生效。

现在 markdown 看起来好多了

markdown working even better

部署到边缘

Deno Deploy 是我们全球分布式的 v8 isolate 云,您可以在其中托管任意 JavaScript。它非常适合托管无服务器函数以及整个网站和应用程序。

我们可以通过以下步骤轻松地将我们的新博客部署到 Deno Deploy。

  • 为您的新博客创建一个 GitHub 仓库
  • 转到 https://dash.deno.com/ 并连接您的 GitHub
  • 选择您的 GitHub 组织或用户、仓库和分支
  • 选择“自动”部署模式,并将 main.ts 作为入口点
  • 单击“Link”,这将开始部署

部署完成后,您将收到一个可以访问的 URL。 这是一个实时版本。

下一步是什么?

这是一个关于使用 Fresh 构建博客的简单教程,演示了 Fresh 如何从文件系统检索数据,并将其渲染为 HTML,所有这些都在服务器端完成。

有关更深入的演练,请查看 Luca 的这个视频,他构建了一个博客并将其部署到 Deno Deploy

遇到困难?在我们的 DiscordTwitter 上获取有关 Fresh 和 Deno 的帮助!