如何使用 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 来查看默认应用

瞧!
更新目录结构
Fresh 的初始化脚本会搭建一个通用应用目录。所以让我们修改它以适应博客的需求。
让我们添加一个 posts 文件夹,其中将包含所有 markdown 文件
$ mkdir posts并删除不必要的 components、islands 和 routes/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

开局不错!
但是点击帖子还不奏效。我们来修复它。
创建帖子页面
在 /routes/ 中,我们将 [name].tsx 重命名为 [slug].tsx。
然后,在 [slug].tsx 中,我们将做与 index.tsx 类似的事情:创建一个自定义处理器来获取单个帖子,并导出一个渲染页面的默认组件。
由于我们将重用辅助函数 getPosts 和 getPost,以及接口 Post,让我们将它们重构到一个名为 utils 的新文件夹下的独立实用程序文件 posts.ts 中。
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 并点击帖子

它就在那儿!
解析 Markdown
目前,这还不支持解析 Markdown。如果你这样写

它会显示成这样

为了解析 Markdown,我们需要导入模块 gfm 并将 post.content 通过函数 gfm.render() 传递。
让我们将这行添加到 import_map.json
"$gfm": "https://deno.land/x/gfm@0.1.26/mod.ts"然后,在 /routes/[slug].tsx 中,我们需要从 $gfm 导入 CSS 和 render 函数,以及从 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 看起来好多了

部署到边缘
Deno Deploy 是我们全球分布的 V8 隔离云,您可以在其中托管任意 JavaScript。它非常适合托管无服务器函数以及整个网站和应用程序。
我们可以通过以下步骤轻松地将新博客部署到 Deno Deploy。
- 为您的新博客创建一个 GitHub 仓库
- 访问 https://dash.deno.com/ 并连接您的 GitHub
- 选择您的 GitHub 组织或用户、仓库和分支
- 选择“自动”部署模式,并将
main.ts作为入口点 - 点击“链接”,这将开始部署
部署完成后,您将收到一个可访问的 URL。这是一个在线版本。
下一步是什么?
这是一个使用 Fresh 构建博客的简单教程,它演示了 Fresh 如何从文件系统检索数据,并将其全部在服务器上渲染为 HTML。
如需更深入的讲解,请观看 Luca 制作并部署到 Deno Deploy 的博客视频。
遇到困难?在 我们的 Discord 或 Twitter 上寻求 Fresh 和 Deno 的帮助!
