如何使用 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
来查看默认应用
瞧!
更新目录结构
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
,让我们将它们重构到一个名为 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 并点击文章
它就在那里!
解析 Markdown
目前,这不会解析 Markdown。 如果您写下这样的内容
它将显示为
为了解析 Markdown,我们需要导入模块 gfm
,并将 post.content
传递给函数 gfm.render()
。
让我们在 import_map.json
中添加这一行
"$gfm": "https://deno.land/x/[email protected]/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 的帮助!