mobile wallpaper 1mobile wallpaper 2mobile wallpaper 3mobile wallpaper 4mobile wallpaper 5mobile wallpaper 6
2397 字
6 分钟
博客「点赞功能」开发笔记:我是如何被 URL 路径坑了一把
2026-03-21

阅读前提:本文适合使用 Astro、Hugo、Hexo 等静态站点生成器(SSG)开发博客的开发者。如果你也在为静态博客添加需要识别”当前文章”的功能,这篇文章可能对你有帮助。

核心要点:在静态网站生成(SSG)中,构建时无法获取运行时 URL。为实现”识别当前文章”的功能,必须采用父传子(Props 传递)模式,将文章标识符从构建时已知的父组件显式传递给需要它的子组件。


这篇文章能帮你解决什么问题#

给博客加了点赞功能,需要知道用户在看哪篇文章。本来觉得很简单——“从浏览器地址栏读一下 URL 不就行了?”

结果一试,点赞按钮在文章页不显示,在首页反而显示了。

这篇文章就记录我踩的坑,以及最后怎么解决的。


先来认识两个角色#

在说问题之前,先介绍两个重要的概念。

1. 浏览器能做的事 vs 不能做的事#

浏览器能做的浏览器不能做的
用户访问网页时显示正确内容知道”当前”是哪篇文章(构建时)
点击按钮执行代码访问还没生成的页面
读取地址栏的 URL在网站还没”盖好”的时候就知道文章列表

第二列听起来怪怪的,但这恰恰是静态网站生成(SSG)的核心特点。

2. 什么是”静态网站生成”#

普通网站:有人来访问 → 服务器实时查数据库 → 返回页面

静态网站:网站管理员”打包”网站时就把所有页面都生成好了 → 用户访问直接拿现成的

就像:

  • 动态网站 = 你去餐厅点餐,厨师现场做
  • 静态网站 = 方便盒饭,饭已经做好了,来直接拿

问题来了:饭做好的时候,厨师并不知道”现在是谁来拿”。


我踩的坑#

第一步:我觉得很简单#

点赞需要知道是哪篇文章,URL 里不是有吗?比如:

https://blog.example.com/posts/dns-guide/

我写了这样的代码:

// 在浏览器里运行的代码
const path = window.location.pathname; // 获取地址栏路径
const slug = path.split('/').filter(Boolean).pop(); // 取出最后一截

以为这样就能拿到 dns-guide,然后告诉点赞接口:“帮我给这篇文章 +1”。

第二步:电脑端怎么都不显示#

代码写好了,但点赞按钮就是不出来。

我开始排查:

  • 组件引用对不对?✓ 对
  • CSS 是不是藏起来了?✓ 不是
  • JS 报错了吗?✓ 没报错

后来发现,点赞按钮在文章页面不显示,在首页反而显示了。

第三步:意识到问题的根源#

我用的是 Astro 静态博客,部署时所有页面一次性生成

构建的过程是这样的:

  1. Astro 读取所有文章
  2. 为每篇文章生成一个页面文件(dns-guide.htmlmarkdown-tutorial.html…)
  3. 上传到服务器

问题在于:生成页面的时候,代码根本不知道最终用户会访问哪个 URL


为什么从 URL 读取行不通#

关键概念:构建时 vs 运行时#

在继续之前,必须弄清楚两个阶段:

阶段说明有什么
构建时(Build Time)网站代码被”打包”的阶段没有浏览器,没有用户,没有 URL
运行时(Runtime)用户真正访问页面的阶段有浏览器,有 window.location

window.location.pathname运行时的浏览器 API,只有用户访问页面时才存在。

核心问题:静态生成的 JavaScript 是”通用”的#

你可能会想:既然构建时不知道 URL,运行时再读不就行了?

技术上可以,但不是最佳方案。

关键点在于:静态生成时,所有页面的 JavaScript 代码是完全一样的

这导致两种截然不同的架构模式:

方案数据与视图的关系本质
运行时读 URL数据与视图分离先加载空白视图,再异步获取数据
Props 传递数据与视图同步生成视图加载时数据已就位

想象一下,如果采用”运行时读 URL”的方式:

// 这个代码在所有文章页面都是一样的
const slug = window.location.pathname.split('/').filter(Boolean).pop();
fetch(`/api/like?slug=${slug}`);

这会导致:

  1. 页面 HTML 已经加载完成,但点赞数还是空的
  2. JavaScript 才开始执行,去读 URL,发请求
  3. 等到响应返回,才能显示点赞数
  4. 用户看到的是:先看到空白,再看到数字闪烁

而用 Props 传递的方式,实现了数据与视图同步生成——页面构建时,slug 就已经注入到组件里,用户访问时直接看到完整的点赞数。

类比说明#

还是用盒饭来理解:

  1. 动态网站:厨师知道谁来,现场做(运行时渲染)
  2. 静态网站:厨师不知道谁来,但可以在每个盒饭上提前贴好标签
  3. 问题:如果厨师说”你来之前告诉我你叫什么名,我再写上去”,那你得等(运行时读取 URL)
  4. 更好的做法:直接印好”姓名:______“的标签,来的人填上就走(Props 传递数据)

正确的做法#

核心思路:让”父亲”告诉”孩子”#

既然页面不知道自己是谁,那就让知道的人传递给它。这在 React/Vue/Astro 等现代框架中叫做 Props 传递(父传子)

组件层级关系:

[posts/[...slug].astro] ← 知道 entry.id 是 "dns-guide/index.md"
↓ (props: postSlug)
[MainGridLayout.astro] ← 原样传递
↓ (props: postSlug)
[FloatingLikeButton.astro] ← 用 removeFileExtension() 提取 slug
↓ (props: slug)
[LikeButton.svelte] ← 使用 slug,发请求获取点赞数

第一步:文章页面传递身份#

src/pages/posts/[...slug].astro
<MainGridLayout
postSlug={entry.id} <!-- 把文章的"身份证号"传下去 -->
>

注意:这里传的是 entry.id,不是 URL slug。entry.id 是 Astro 通过 getCollection 等 API 获取文章元数据时返回的属性,在构建时就已经确定了

entry.id 长这样:

  • dns-guide/index.md
  • markdown-tutorial.md

为什么用 entry.id 而不是 URL slug? 因为 entry.id 是 Astro 的内部标识符,在构建阶段就可以获取到,而 URL 是用户访问时才存在的。

第二步:每一层都原样传递#

src/layouts/MainGridLayout.astro
<FloatingLikeButton postSlug={postSlug} />
src/components/FloatingLikeButton.astro
const { postSlug } = Astro.props;
const slug = removeFileExtension(postSlug); // 关键:去掉 .md 后缀

第三步:核心实现#

关键改进:组件自己从 URL 提取 slug,而不是依赖外部 props 传入。这样 Swup 切换页面时组件能自动更新。

1. 从 URL 提取 slug#

// 从 URL 提取 slug
function extractSlugFromUrl(pathname: string): string {
const cleanPath = pathname.replace(/^\/|\/$/g, '').replace(/\.html$/, '');
if (cleanPath.match(/^posts\/.+/)) {
const parts = cleanPath.split('/');
return parts[parts.length - 1] || parts[parts.length - 2];
}
return cleanPath;
}

2. 处理 Swup 页面切换#

博客使用 Swup 实现页面无刷新切换,需要监听 swup:page:view 事件来处理页面切换:

onMount(() => {
const pathname = window.location.pathname;
// 只有文章页才显示按钮
if (!pathname.includes('/posts/')) {
mounted = false;
return;
}
const currentSlug = extractSlugFromUrl(pathname);
mounted = true;
fetchLikes(currentSlug);
// 监听 Swup 页面切换事件
const handlePageView = () => {
const newPathname = window.location.pathname;
// 切换到非文章页时隐藏按钮
if (!newPathname.includes('/posts/')) {
mounted = false;
return;
}
const newSlug = extractSlugFromUrl(newPathname);
mounted = true;
fetchLikes(newSlug);
};
document.addEventListener('swup:page:view', handlePageView);
return () => {
document.removeEventListener('swup:page:view', handlePageView);
};
});

3. 关键点总结#

问题解决方案
Props 传递的 slug 在 Swup 切换时不更新组件自己从 URL 提取 slug
非文章页也显示按钮每次检查 URL,只有 /posts/ 开头才显示
页面切换后状态错乱监听 swup:page:view,切换时重新获取数据

补充知识:几个容易搞混的概念#

entry.id vs slug vs 文件路径#

概念例子来源作用
文件路径src/content/posts/dns-guide/index.md开发者组织文件文件在电脑里的位置
entry.iddns-guide/index.mdAstro getCollection() API 返回构建时标识符,连接文件系统与页面生成
URL slugdns-guide从 entry.id 派生URL 中使用的简短标识

关键理解entry.id 是构建时的标识符,它是连接”文件系统中的文章”与”生成的页面”之间的桥梁。正因为它在构建时已知,才能可靠地传递给子组件。

扩展名要小心#

Astro 的 entry.id 包含文件扩展名,不能直接当 slug 用:

// ❌ entry.id 带着扩展名
"dns-guide/index.md"
// ✅ 用函数去掉扩展名
removeFileExtension("dns-guide/index.md") // → "dns-guide"

总结#

踩坑总结#

错误做法问题正确做法(Props 传递)
从 URL 读路径构建时不知道”当前”是谁通过 props 显式传递
直接用 entry.id包含 .md 后缀removeFileExtension() 处理
假设”当前页面”概念SSG 没有这个每个页面显式传递自己的身份

一句话记住#

静态网站构建时不知道”现在是谁”,数据必须从父到子显式传递。

关键代码#

// 去掉文件扩展名
function removeFileExtension(id: string): string {
const result = id.replace(/\.(md|mdx|markdown)$/i, "");
return result || 'index';
}
<!-- 父传子的标准模式 -->
<ChildComponent dataFromParent={parentKnowsThis} />

扩展思考#

掌握了 Props 传递(父传子)的方法后,还可以进一步探索:

方向核心概念关键词
按用户隔离需要知道是谁点的赞用户认证(Auth)、状态管理
SSR 方案服务器端直接获取 URL服务端中间件、动态路由
CSR 方案页面加载后客户端获取客户端状态管理、首屏加载优化

学习路径建议#

  • 初学者:优先掌握 Props 传递(父传子)模式,这是 SSG 的标准实践
  • 进阶:需要用户个性化功能时,探索用户认证与状态管理
  • 扩展:考虑技术栈迁移时,了解 SSR/CSR 方案作为备选

方案适用边界#

本文介绍的 Props 传递(父传子)模式是 SSG 框架下的标准实践,适合纯静态博客。

对于支持混合渲染的框架(如 Next.js 同时支持 SSG/SSR/CSR),需要根据具体页面的渲染方式选择策略:

  • SSG 页面:用 Props 传递(数据与视图同步)
  • SSR 页面:可以用服务端获取 URL
  • CSR 页面:运行时读取或客户端路由(数据与视图分离)
分享

如果这篇文章对你有帮助,欢迎分享给更多人!

博客「点赞功能」开发笔记:我是如何被 URL 路径坑了一把
https://bayunmoyu.com/posts/astro-url-slug/
作者
八云墨玉
发布于
2026-03-21
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时

封面
Sample Song
Sample Artist
封面
Sample Song
Sample Artist
0:00 / 0:00