首页
/ Astro Hacker News 模板实战:基于 SSR 与动态路由的 Hacker News 克隆

Astro Hacker News 模板实战:基于 SSR 与动态路由的 Hacker News 克隆

2026-09-04 12:10:20作者:郜逊炳

本文以 Astro 官方仓库中的 Hacker News 示例模板(examples/hackernews)为主体,完整讲解这个示例的创建方式、项目结构、路由设计与命令用法,并结合仓库中的真实源码(动态路由页面、API 封装层、条件渲染组件、Node 适配器配置)深入剖析它如何用纯服务端渲染(SSR)实现一个完整的 Hacker News 克隆站点。读完本文,你可以直接复刻该模板,并理解其中 [id].astro 动态路由、rest 路由 [...stories].astro 分页、output: 'server' + @astrojs/node 部署等核心机制的落地方式。

一、快速创建项目

模板提供了标准的一键创建命令:

npm create astro@latest -- --template hackernews

该命令会拉取 hackernews 模板生成完整项目,无需手工搭建路由与布局。模板对应的仓库目录为 examples/hackernews,其中 package.json 声明了核心依赖:astro ^7.2.10 与 SSR 适配器 @astrojs/node ^11.1.5,并要求 Node.js >=22.12.0(见 package.json)。

二、项目结构

README 中给出的目录结构如下:

/
├── public/
│   └── favicon.svg
├── src/
│   ├── components/
│   ├── layouts/
│   │   └── Layout.astro
│   └── pages/
        └── stories/
            └── [id].astro
        └── users/
            └── [id].astro
│       └── [...stories].astro
└── package.json

Astro 在 src/pages/ 目录中寻找 .astro.md 文件,每个页面按其文件名映射为一个路由。由于 Hacker News 的文章列表与用户信息是持续变化的,示例使用 [id].astro 这样的动态路由,只在特定页面被请求时才构建页面。src/components/ 本身没有特殊含义,但约定存放 Astro/React/Vue/Svelte/Preact 组件。静态资源(如图片)放在 public/ 目录,例如 favicon.svg 在构建后会以 /favicon.svg 访问。

仓库实际文件布局与该结构一致,另有一个 src/lib/api.ts 封装 API 请求、src/types.ts 定义数据类型(见 src/types.ts)。

三、SSR 部署:@astrojs/node 与 standalone 模式

该模板是纯 SSR 站点,配置文件 astro.config.mjs 完整内容如下:

import node from '@astrojs/node';
import { defineConfig } from 'astro/config';

export default defineConfig({
	output: 'server',
	adapter: node({
		mode: 'standalone',
	}),
});

两个关键配置:

  • output: 'server':让所有路由默认按服务端渲染模式处理。页面中的 frontmatter 在每次请求时于服务端执行,数据在请求时实时获取。
  • adapter: node({ mode: 'standalone' }):使用 @astrojs/node 适配器,以 standalone 模式将站点部署到 Node 运行时目标上。standalone 模式会产出可独立分发的服务器目录,便于在容器或 Node 环境中直接启动。

其他部署目标(Cloudflare、Netlify、Vercel 等)可换用对应适配器,配置方式不变:只需在 adapter 中替换为相应适配器即可。

四、数据层:统一封装的两个 API

所有数据请求集中在 src/lib/api.ts,它按路径前缀路由到两个不同的数据源:

const story = (path: string) => `https://node-hnapi.herokuapp.com/${path}`;
const user = (path: string) => `https://hacker-news.firebaseio.com/v0/${path}.json`;

export default async function fetchAPI(path: string) {
	const url = path.startsWith('user') ? user(path) : story(path);
	const headers = { 'User-Agent': 'chrome' };

	try {
		let response = await fetch(url, { headers });
		let text = await response.text();
		try {
			if (text === null) {
				return { error: 'Not found' };
			}
			return JSON.parse(text);
		} catch (e) {
			console.error(`Received from API: ${text}`);
			console.error(e);
			return { error: e };
		}
	} catch (error) {
		return { error };
	}
}

从源码结构看,其设计要点有三:

  1. 双数据源分流:以 user 开头的请求走 Hacker News Firebase 官方 API(user/<id>.json),其余走 HN API 代理服务(item/<id>news?page=N 等)。
  2. 容错优先:任何失败路径都返回 { error } 对象而非抛出异常,让页面层可以用条件渲染优雅降级。
  3. 类型契约src/types.ts 定义了 IStoryIUser 和可递归的 IComment 接口,页面中通过 as IStoryas IUser 断言获取类型安全。

五、路由设计:动态路由与 rest 分页

5.1 文章详情页:src/pages/stories/[id].astro

sources 目录下的 stories/[id].astroAstro.paramsid,请求时拉取文章与全部评论:

---
const { id } = Astro.params as { id: string };
const story = (await fetchAPI(`item/${id}`)) as IStory;
---
<Layout>
	<header>
		<a href={story.url} target="_blank"><h1>{story.title}</h1></a>
		<Show when={story.domain}>
			<span class="host">({story.domain})</span>
		</Show>
		...
	</header>
	<main>
		<ul class="comment-children">
			<For each={story.comments}>
				{(comment: IComment) => <Comment comment={comment} />}
			</For>
		</ul>
	</main>
</Layout>

这正是 README 所说"动态路由只在页面被请求时构建"的体现:frontmatter 中的 await 在每次请求时执行,/stories/123/stories/456 各自命中同一模板。

5.2 用户详情页:src/pages/users/[id].astro

users/[id].astro 展示了 API 错误返回的降级用法——利用 Show 组件与 fallback 插槽,在用户不存在时显示 "User not found.":

---
const { id } = Astro.params as { id: string };
const user = (await fetchAPI(`user/${id}`)) as IUser;
---
<Show when={user}>
	<Show when={!user.error}>
		<h1 slot="fallback">User not found.</h1>
		<h1>User : {user.id}</h1>
		<ul class="meta">
			<li><span class="label">Created:</span>{user.created}</li>
			<li><span class="label">Karma:</span>{user.karma}</li>
			...
	</Show>
</Show>

5.3 列表与分页:src/pages/[...stories].astro

rest 路由 [...stories].astro 是理解路由映射的核心。rest 路由参数 stories 会接收 URL 首段,模板用它区分五个板块并做服务端分页:

const mapStories = {
	top: 'news',
	new: 'newest',
	show: 'show',
	ask: 'ask',
	job: 'jobs',
};

const page = safeParseInt(Astro.url.searchParams.get('page'), 1);
const type =
	Astro.params.stories && Astro.params.stories in mapStories
		? (Astro.params.stories.toString() as keyof typeof mapStories)
		: 'top';

const stories = (await fetchAPI(`${mapStories[type]}?page=${page}`)) as IStory[];

路由与 API 参数的映射关系为:/ 与非法路径回落到 top(对应 news),/newnewest/showshow/askask/jobjobs。分页通过 ?page=N 查询参数实现,页面内以 stories?.length >= 29 作为"还有下一页"的判断依据(HN 每页约 30 条,代理 API 每页 29 条)。这体现了 SSR 的优势:URL 完全可分享、可被爬虫索引,无需任何客户端状态。

六、组件层:条件渲染与递归列表

README 提到 src/components/ 是存放组件的约定位置,该模板在这里实现了一组非常有教学价值的通用组件:

6.1 Show:条件渲染 + fallback 插槽

components/Show.astro 全文仅两行核心逻辑,是 Angular 风格的条件容器:

{!!when ? <slot /> : <slot name="fallback" />}

when 为真时渲染默认插槽,否则渲染名为 fallback 的命名插槽——前文用户页的 "User not found." 就依赖这一机制。

6.2 For:可迭代对象的统一循环

components/For.astro 接收 each(任意 Iterable),用异步生成器逐项渲染默认插槽,并在集合为空时渲染 fallback 插槽,统一了"遍历 + 空态"两种处理。

6.3 CommentAstro.self 实现无限层递归

评论天然是任意深度的树结构。components/Comment.astroAstro.self 自引用实现递归渲染,无需客户端代码:

<Show when={comment.comments.length}>
	<Toggle open>
		<For each={comment.comments}>
			{(comment: IComment) => <Astro.self comment={comment} />}
		</For>
	</Show>
</li>

Astro.self 在组件内部引用组件自身,配合 set:html={comment.content} 直接输出 API 返回的 HTML 评论正文,整个嵌套评论树完全在服务端一次性生成。

6.4 LayoutNav

layouts/Layout.astro 提供 <html><head>(含 Astro.generator meta)与 <slot />,各页面通过插槽组合复用;components/Nav.astro 定义了 HN/New/Show/Ask/Jobs 五个导航链接,并用 aria-current={href === Astro.url.pathname ? 'page' : undefined} 在服务端标记当前页——这同样印证了导航状态判断发生在服务端渲染阶段。

七、常用命令

所有命令在项目根目录的终端中执行(见 README 命令表):

命令 作用
npm install 安装依赖
npm run dev localhost:4321 启动本地开发服务器
npm run build 构建生产站点到 ./dist/
npm run preview 在部署前本地预览构建产物
npm run astro ... 运行 astro addastro check 等 CLI 命令
npm run astro -- --help 查看 Astro CLI 帮助

对应 package.json 中的 scripts 定义:devastro devbuildastro buildpreviewastro previewastro → 直接透传 astro 命令。由于配置了 Node 适配器且 output: 'server'build 产出的 dist/ 中除静态资源外还包含可运行的 SSR 服务器代码,preview 即在该服务器上运行。

TypeScript 方面,tsconfig.json 继承 astro/tsconfigs/strict 严格模式,并引入 Astro 生成的类型声明 .astro/types.d.ts,因此 Astro.paramsAstro.url 等对象在页面中享有完整类型提示。

八、小结

这个模板的价值在于用最小代码展示了一套完整的 Astro SSR 工作流:

  1. 一条命令创建项目(npm create astro@latest -- --template hackernews);
  2. output: 'server' + @astrojs/node standalone 完成 Node 端 SSR 部署配置;
  3. [id].astro 动态路由承载请求时数据拉取,[...stories].astro rest 路由 + 查询参数实现服务端分页与多板块映射;
  4. fetchAPI 统一错误兜底 + Show/For 条件与循环组件 + Astro.self 递归,全程零客户端 JS 即可渲染任意深度的评论树。

若你想在此基础上扩展(如加入 Content Collections、缓存策略或其他框架岛屿组件),建议从 examples/ 目录下的其他模板(如 ssrblog)对照参考。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.12 K
2.72 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
527
590
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
904
1.82 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
854
1.34 K
docsdocs
暂无描述
Markdown
889
5.78 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.52 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.33 K
1.45 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
980
502
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
540
384