跳至主要内容
Deno 2 终于来了 🎉️
了解更多
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。

让我们运行 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 文件,并包含以下前置内容

---
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 文件中,让我们创建一个自定义处理程序来获取文章并通过组件呈现它。 注意,我们可以访问 ctx.params.slug,因为我们在文件名 [slug].tsx 中使用了方括号。

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,并将 post.content 传递给函数 gfm.render()

让我们在 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 隔离云,您可以在其中托管任意 JavaScript。 它非常适合托管无服务器函数以及整个网站和应用程序。

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

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

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

下一步?

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

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

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