跳到主要内容
blog-hexo-astro
返回

如何在本博客写一篇新笔记:完整教程

前置:本文用到的技术栈与部署方式已就绪 → Astro 7 + AstroPaper v6 主题 + Tailwind 4 + Pagefind 搜索 + KaTeX 公式 + Vercel 部署。基础设施由模板与配置管,写文章本身只需要 Markdown。

📌 文章页右侧已上线浮动目录,会自动从 h2/h3 生成;当前滚到的章节会高亮,桌面端常驻、平板/手机点击右下角按钮展开。

文件放哪#

src/content/posts/
├── astro/                 ← 按主题分类(子文件夹名 = URL 段)
│   ├── content-collections.md
│   └── tailwind-v4.md
├── hexo/
│   └── vs-astro.md
├── setup/                 ← 博客搭建相关文章
│   └── how-to-write-a-post.md
├── _drafts/               ← 下划线开头 = 草稿,不发布
│   └── wip-article.md
└── welcome.md             ← 没分类的文章
情况处理
文章主题明确进对应子目录
文章跨多个主题选最相关的,或放根目录
还没想好分类先放根目录 src/content/posts/,以后再调整
草稿 / 半成品必须放 _drafts/(下划线开头)

目录名建议用英文或拼音。中文目录名也能用,但 URL 里会变成拼音或乱码。

文件名建议英文 + kebab-case:

# 推荐
touch src/content/posts/astro/my-new-feature.md
touch src/content/posts/setup/how-to-write-a-post.md

# 不推荐
touch src/content/posts/我的文章.md

frontmatter#

每个 .md 文件顶部必须有 YAML frontmatter(--- 之间):

---
title: "我的新文章标题"
description: "一两句简介,用于 SEO meta 和 RSS 摘要,必填。"
pubDatetime: 2026-10-08
tags: ["astro", "blog"]
featured: false
---

必填字段#

字段类型示例
titlestring"如何写一篇博客"
descriptionstring"Astro 博客完整教程。"
pubDatetimeDate2026-10-08(ISO 8601 格式)

可选字段#

字段类型默认值说明
authorstringconfig.site.author文章作者
modDatetimeDate|nullnull最后修改时间(显示「更新于」)
tagsstring[]["others"]标签数组,至少 1 个
featuredbooleanfalsetrue 时进首页「精选」section
ogImageimage|string—自定义 OG 图,优先级高于全局
canonicalURLstring—SEO 规范 URL
hideEditPostbooleanfalse单独隐藏「编辑此页」
draftbooleanfalse标记草稿
timezonestringAsia/Shanghai发布时间对应时区(IANA)

正文 Markdown 写法#

基础语法#

## 二级标题

### 三级标题

**加粗**、*斜体*、`行内代码`

- 无序列表
- 列表项
  - 嵌套列表

1. 有序列表
2. 第二项

> 引用块

[外部链接](https://astro.build/)
[内部文章链接](/posts/welcome/)

标题自动锚点#

每个 H2/H3 标题自动带 # 链接(鼠标 hover 时显示):

## 我的章节            ← 渲染为 <h2 id="我的章节">我的章节 #</h2>
### 子章节              ← 渲染为 <h3 id="子章节">子章节 #</h3>

深链接形如 https://<your-domain>/posts/xxx/#我的章节。

提示块(callout)#

rehype-callouts 渲染的 5 种彩色块。

源码:

> [!NOTE]
> 这是 Note(蓝色)

> [!TIP]
> 这是 Tip(绿色)

> [!WARNING]
> 这是 Warning(黄色)

> [!IMPORTANT]
> 这是 Important(紫色)

> [!CAUTION]
> 这是 Caution(红色)

实际渲染:

Note

这是 Note(蓝色)

适合说明背景、补充信息。

Tip

这是 Tip(绿色)

适合分享小技巧、推荐做法。

Warning

这是 Warning(黄色)

适合提示有坑、需要谨慎。

Important

这是 Important(紫色)

适合强调关键步骤。

Caution

这是 Caution(红色)

适合警告危险操作。

代码块#

Shiki 提供语法高亮(双主题:min-light + night-owl):

```ts
const greeting: string = "hello";
console.log(greeting);
```

```python
def fib(n):
    return n if n < 2 else fib(n-1) + fib(n-2)
```

```bash
npm install astro
```

支持的语言:完整列表。常见的有 ts / js / tsx / jsx / python / bash / json / yaml / css / html / md 等。

行内 diff / highlight 标记:

```ts
function foo() {
  console.log("hello");  // [!code --]
  console.log("hi");     // [!code ++]
  console.log("hey");     // [!code highlight]
}
```

效果:

表格#

| 列 1 | 列 2 | 列 3 |
| --- | --- | --- |
| A1 | A2 | A3 |
| B1 | B2 | B3 |

渲染:

列 1列 2列 3
A1A2A3
B1B2B3

对齐方式(用 : 标记):

| 左对齐 | 居中 | 右对齐 |
| :--- | :---: | ---: |
| A1 | A2 | A3 |
| B1 | B2 | B3 |
左对齐居中右对齐
A1A2A3
B1B2B3

数学公式#

本站用 KaTeX 渲染(remark-math + rehype-katex)。

行内公式:用 $ ... $ 包起来(前后留空格或紧贴非字母字符,避免误识别)。

源码:

勾股定理:$a^2 + b^2 = c^2$。

渲染:勾股定理:a2+b2=c2a^2 + b^2 = c^2。

块级公式:用 $$ ... $$ 包,独立成行(前后空一行)。

源码:

$$
f(x) = \int_{-\infty}^{\infty} \hat{f}(\xi) e^{2\pi i \xi x} d\xi
$$

渲染:

f(x)=∫−∞∞f^(ξ)e2πiξxdξf(x) = \int_{-\infty}^{\infty} \hat{f}(\xi) e^{2\pi i \xi x} d\xi

多行公式对齐:用 aligned 环境。

源码:

$$
\begin{aligned}
f(x) &= x^2 + 2x + 1 \\
     &= (x+1)^2
\end{aligned}
$$

渲染:

f(x)=x2+2x+1=(x+1)2\begin{aligned} f(x) &= x^2 + 2x + 1 \\ &= (x+1)^2 \end{aligned}

注意:本站 remark-math 配置只识别 $...$ 和 $$...$$。LaTeX 原生的 \(...\) / \[...\] 不会触发公式渲染,会被当作普通文本输出。

常用语法速查:KaTeX Supported Functions

图片#

Astro 项目里的图片分两类:

路径处理方式适合场景
public/img/...原样复制到 dist文章配图、引用频率低的素材
src/assets/...被 Vite 处理(hash 化、压缩)组件复用图、必须 import

写文章时永远用 public/。src/assets/ 里的图必须先 import 才能用,.md 文件不支持 import 语句(.mdx 才支持)。

步骤:

  1. 把图片放到 public/img/posts/<主题目录>/

    mkdir -p public/img/posts/setup
    cp ~/Downloads/screenshot.png public/img/posts/setup/
  2. 在 markdown 里用相对路径引用

    ![截图说明](/img/posts/setup/screenshot.png)

带标题的图(图说):

![配图说明](/img/posts/setup/screenshot.png "图 1:博客首页")

<img> 标签(可控制尺寸、对齐):

<img
  src="/img/posts/setup/screenshot.png"
  alt="截图说明"
  width="800"
  loading="lazy"
  class="mx-auto rounded-lg shadow-md"
/>

loading="lazy" 让图片懒加载,长文章必备。

音频#

音频文件放 public/audio/,然后在 markdown 里直接写 HTML5 <audio> 标签。

1. 把音频放进 public/

mkdir -p public/audio
cp ~/Downloads/podcast-ep01.mp3 public/audio/

2. 源码(markdown 里的写法)

<audio src="/audio/podcast-ep01.mp3" controls preload="metadata"></audio>

3. 实际渲染(CommonMark 限制:HTML 块级标签必须在白名单内;不在的标签如 <audio> 必须被 <p> 或 <div> 包裹才会被识别)

上面会显示浏览器原生 audio 控件,但因为这是教程示例文件,路径 /audio/example.mp3 并不存在,点击播放会 404。你的真实部署中这个标签会正常工作。

<audio> 常用属性

属性说明
controls显示播放控件(必加)
preload="none"不预加载,按需播放
preload="metadata"预加载元数据(时长、封面等),省流量(推荐)
preload="auto"整个文件预加载
loop循环播放
autoplay自动播放(几乎所有浏览器都禁用 autoplay,慎用)

带标题的音频(用 <figure>)

<figure>
  <figcaption>第 1 期:建站流水账</figcaption>
  <audio src="/audio/podcast-ep01.mp3" controls preload="metadata"></audio>
</figure>

视频#

视频文件放 public/video/,markdown 里直接写 HTML5 <video> 标签。

1. 把视频放进 public/

mkdir -p public/video
cp ~/Downloads/demo.mp4 public/video/

2. 源码

<video src="/video/demo.mp4" controls preload="metadata" width="800"></video>

3. 实际渲染

同样,这是教程示例,文件不存在仅供看控件。

<video> 常用属性

属性说明
controls显示播放控件
width / height设定宽高,避免布局抖动(推荐两个都设)
preload="metadata"预加载元数据
loop循环播放
muted autoplay playsinline静音自动播放(移动端必备)
poster="..."视频封面图(强烈推荐,海报图)

多格式 fallback(更稳):浏览器自动选能播的格式。

<video controls preload="metadata" width="800" poster="/img/posts/setup/video-cover.png">
  <source src="/video/demo.webm" type="video/webm" />
  <source src="/video/demo.mp4" type="video/mp4" />
  你的浏览器不支持 video 标签。
</video>

外部视频(YouTube / B 站)直接嵌入 iframe:

<iframe
  src="https://www.youtube.com/embed/VIDEO_ID"
  width="800"
  height="450"
  loading="lazy"
  allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
  allowfullscreen
></iframe>

本地预览#

npm run dev
# 等价于:astro dev
# 默认监听 http://localhost:4321/

打开 http://localhost:4321/posts/<主题>/<文件名>/ 即可访问你刚写的文章。

构建 + 部署#

# 1. 构建(包含类型检查 + pagefind 索引生成)
npm run build

这条命令等价于:

astro check && astro build && pagefind --site dist && cp -r dist/pagefind public/

四步依次执行:

  1. astro check:TypeScript 类型检查 + Astro 诊断
  2. astro build:生成静态文件到 dist/
  3. pagefind:扫描 dist/ 生成搜索索引到 dist/pagefind/
  4. cp:把 pagefind 索引复制到 public/pagefind/,下次 dev 启动就能搜

构建成功后产物大小约几 MB(5 篇文章约 1-2 MB)。

部署到 Vercel(已配置好):

git add .
git commit -m "post: <主题>/<文章名>"
git push origin main   # 30 秒后线上自动更新

Vercel 看 GitHub webhook 触发部署,跟 git author email 无关。

常见错误#

❌ pubDatetime 缺省或格式错#

Error: pubDatetime is required

修复:

pubDatetime: 2026-10-08       # ✅
pubDatetime: "2026-10-08"     # ✅
pubDatetime: 2026-10-08 12:30 # ✅
pubDatetime: "10/08/2026"     # ❌ 格式错

❌ 图片 404#

GET /img/posts/foo/bar.jpg → 404

修复:

# 确认文件存在
ls public/img/posts/foo/bar.jpg
# 不存在就创建 + 复制
mkdir -p public/img/posts/foo
cp /path/to/source.jpg public/img/posts/foo/bar.jpg

❌ 音频 / 视频不播放#

排查顺序:

  1. 浏览器直接访问 /audio/xxx.mp3,看 200 还是 404
  2. 文件 MIME 类型对吗(.mp3 / .mp4 / .webm)
  3. dev server 缓存:删 node_modules/.vite 重启

❌ 公式显示源码不渲染#

$E=mc^2$ 渲染为 E=mc^2(纯文本),说明 remark-math / rehype-katex 没装或没配。

修复:

npm install remark-math rehype-katex katex

astro.config.mjs 的 markdown.processor.unified 加上:

remarkPlugins: [..., remarkMath],
rehypePlugins: [..., rehypeKatex],

Layout.astro 顶部加:

import "katex/dist/katex.min.css";

❌ tags 为空#

tags: []     # ❌ 不会被接受
tags: ["others"]  # 默认值,会归到「其他」

建议至少 1 个有意义的标签。

❌ frontmatter 引号 / 冒号#

title: 我的标题              # ✅ 无特殊字符时
title: "我的标题"            # ✅ 推荐加引号
title: '我的标题'            # ✅
title: my: title with colon  # ❌ 解析错

经验法则:标题、描述、含特殊字符的字段都加引号。

引入远程表情包(BlobCatPlus 为例)#

以 BlobCatPlus 表情包 为例,演示 3 种引入方式。

先看 JSON 结构#

{
  "blobcatplus": {
    "type": "image",
    "container": [
      { "text": "cats", "icon": "<img src='https://cdn.jsdelivr.net/gh/infinitesum/Twikoo-emoji@master/Blob/cats.png'>" },
      { "text": "cry",  "icon": "<img src='https://cdn.jsdelivr.net/gh/infinitesum/Twikoo-emoji@master/Blob/blobcatcry.png'>" }
    ]
  }
}

关键:每条记录的 icon 字段是个现成的 <img> 字符串,不是裸 URL 。

方式 1:单图直引(最简单,零代码)#

从 JSON 里挑个图,提取 icon 里的 src,直接用 markdown 写:

![猫猫](https://cdn.jsdelivr.net/gh/infinitesum/Twikoo-emoji@master/Blob/cats.png)
![哭哭](https://cdn.jsdelivr.net/gh/infinitesum/Twikoo-emoji@master/Blob/blobcatcry.png)

适用场景:只需插 1-2 个表情时。缺点:要手动挑 + 复制粘贴,75 个表情会抄到手软。

BlobCatPlus(75 个 blobcat(已标注常见情绪))#

表情代码说明
cats![cats](https://cdn.jsdelivr.net/gh/infinitesum/Twikoo-emoji@master/Blob/cats.png)猫猫
meow_blush![meow_blush](https://cdn.jsdelivr.net/gh/infinitesum/Twikoo-emoji@master/Blob/meow_blush.png)害羞喵
affect![affect](https://cdn.jsdelivr.net/gh/infinitesum/Twikoo-emoji@master/Blob/affect.png)假装
t![t](https://cdn.jsdelivr.net/gh/infinitesum/Twikoo-emoji@master/Blob/blobcatt.png)搞怪
dizzy![dizzy](https://cdn.jsdelivr.net/gh/infinitesum/Twikoo-emoji@master/Blob/blobcatdizzy.png)晕乎乎
acry![acry](https://cdn.jsdelivr.net/gh/infinitesum/Twikoo-emoji@master/Blob/ablobcatcry.png)哭了
adead![adead](https://cdn.jsdelivr.net/gh/infinitesum/Twikoo-emoji@master/Blob/ablobcatdead.png)阵亡
blobbanhammerr![blobbanhammerr](https://cdn.jsdelivr.net/gh/infinitesum/Twikoo-emoji@master/Blob/blobbanhammerr.png)抡大锤
-aww![-aww](https://cdn.jsdelivr.net/gh/infinitesum/Twikoo-emoji@master/Blob/blobcat-aww.png)哇哦
aknitsweats![aknitsweats](https://cdn.jsdelivr.net/gh/infinitesum/Twikoo-emoji@master/Blob/ablobcatknitsweats.png)手织汗
a__reeee![a__reeee](https://cdn.jsdelivr.net/gh/infinitesum/Twikoo-emoji@master/Blob/A_BlobCat_REEEE.png)尖叫
a__nervous![a__nervous](https://cdn.jsdelivr.net/gh/infinitesum/Twikoo-emoji@master/Blob/A_BlobCat_Nervous.png)紧张
a__code![a__code](https://cdn.jsdelivr.net/gh/infinitesum/Twikoo-emoji@master/Blob/A_BlobCat_Code.png)敲代码
bouncefast![bouncefast](https://cdn.jsdelivr.net/gh/infinitesum/Twikoo-emoji@master/Blob/bouncefast.png)蹦跶
comfyuee![comfyuee](https://cdn.jsdelivr.net/gh/infinitesum/Twikoo-emoji@master/Blob/comfyuee.png)舒适微笑
comfyslep![comfyslep](https://cdn.jsdelivr.net/gh/infinitesum/Twikoo-emoji@master/Blob/comfyslep.png)睡了
comfynight![comfynight](https://cdn.jsdelivr.net/gh/infinitesum/Twikoo-emoji@master/Blob/comfynight.png)晚安
yandere![yandere](https://cdn.jsdelivr.net/gh/infinitesum/Twikoo-emoji@master/Blob/blobcatyandere.png)病娇
verified![verified](https://cdn.jsdelivr.net/gh/infinitesum/Twikoo-emoji@master/Blob/blobcatverified.png)已认证
umm![umm](https://cdn.jsdelivr.net/gh/infinitesum/Twikoo-emoji@master/Blob/blobcatumm.png)嗯…
triumph![triumph](https://cdn.jsdelivr.net/gh/infinitesum/Twikoo-emoji@master/Blob/blobcattriumph.png)胜利
think![think](https://cdn.jsdelivr.net/gh/infinitesum/Twikoo-emoji@master/Blob/blobcatthink.png)思考
snuggle![snuggle](https://cdn.jsdelivr.net/gh/infinitesum/Twikoo-emoji@master/Blob/blobcatsnuggle.png)蹭蹭
snapped![snapped](https://cdn.jsdelivr.net/gh/infinitesum/Twikoo-emoji@master/Blob/blobcatsnapped.png)抓狂
sipsweat![sipsweat](https://cdn.jsdelivr.net/gh/infinitesum/Twikoo-emoji@master/Blob/blobcatsipsweat.png)擦汗
shocked![shocked](https://cdn.jsdelivr.net/gh/infinitesum/Twikoo-emoji@master/Blob/blobcatshocked.png)震惊
scared![scared](https://cdn.jsdelivr.net/gh/infinitesum/Twikoo-emoji@master/Blob/blobcatscared.png)害怕
saitama![saitama](https://cdn.jsdelivr.net/gh/infinitesum/Twikoo-emoji@master/Blob/blobcatsaitama.png)一拳
sadreach![sadreach](https://cdn.jsdelivr.net/gh/infinitesum/Twikoo-emoji@master/Blob/blobcatsadreach.png)伸手求安慰
read![read](https://cdn.jsdelivr.net/gh/infinitesum/Twikoo-emoji@master/Blob/blobcatread.png)阅读
presentred![presentred](https://cdn.jsdelivr.net/gh/infinitesum/Twikoo-emoji@master/Blob/blobcatpresentred.png)送红礼
police![police](https://cdn.jsdelivr.net/gh/infinitesum/Twikoo-emoji@master/Blob/blobcatpolice.png)警察
photo![photo](https://cdn.jsdelivr.net/gh/infinitesum/Twikoo-emoji@master/Blob/blobcatphoto.png)拍照
peekaboo![peekaboo](https://cdn.jsdelivr.net/gh/infinitesum/Twikoo-emoji@master/Blob/blobcatpeekaboo.png)躲猫猫
peek2![peek2](https://cdn.jsdelivr.net/gh/infinitesum/Twikoo-emoji@master/Blob/blobcatpeek2.png)偷看
openmouth![openmouth](https://cdn.jsdelivr.net/gh/infinitesum/Twikoo-emoji@master/Blob/blobcatopenmouth.png)张嘴
notlikethis![notlikethis](https://cdn.jsdelivr.net/gh/infinitesum/Twikoo-emoji@master/Blob/blobcatnotlikethis.png)不是吧
nom![nom](https://cdn.jsdelivr.net/gh/infinitesum/Twikoo-emoji@master/Blob/blobcatnomblobcat.png)吃吃
neutral![neutral](https://cdn.jsdelivr.net/gh/infinitesum/Twikoo-emoji@master/Blob/blobcatneutral.png)淡定
meltthumb![meltthumb](https://cdn.jsdelivr.net/gh/infinitesum/Twikoo-emoji@master/Blob/blobcatmeltthumb.png)融化
melt![melt](https://cdn.jsdelivr.net/gh/infinitesum/Twikoo-emoji@master/Blob/blobcatmelt.png)融化了
love![love](https://cdn.jsdelivr.net/gh/infinitesum/Twikoo-emoji@master/Blob/blobcatlove.png)爱
kissheart![kissheart](https://cdn.jsdelivr.net/gh/infinitesum/Twikoo-emoji@master/Blob/blobcatkissheart.png)亲亲
highfive![highfive](https://cdn.jsdelivr.net/gh/infinitesum/Twikoo-emoji@master/Blob/blobcathighfive.png)击掌
heartpride![heartpride](https://cdn.jsdelivr.net/gh/infinitesum/Twikoo-emoji@master/Blob/blobcatheartpride.png)骄傲心
heart![heart](https://cdn.jsdelivr.net/gh/infinitesum/Twikoo-emoji@master/Blob/blobcatheart.png)心心
headphones![headphones](https://cdn.jsdelivr.net/gh/infinitesum/Twikoo-emoji@master/Blob/blobcatheadphones.png)戴耳机
googlytrash![googlytrash](https://cdn.jsdelivr.net/gh/infinitesum/Twikoo-emoji@master/Blob/blobcatgooglytrash.png)垃圾堆
googlycry![googlycry](https://cdn.jsdelivr.net/gh/infinitesum/Twikoo-emoji@master/Blob/blobcatgooglycry.png)斗鸡眼哭
gay![gay](https://cdn.jsdelivr.net/gh/infinitesum/Twikoo-emoji@master/Blob/blobcatgay.png)彩虹
flower![flower](https://cdn.jsdelivr.net/gh/infinitesum/Twikoo-emoji@master/Blob/blobcatflower.png)送花
flip![flip](https://cdn.jsdelivr.net/gh/infinitesum/Twikoo-emoji@master/Blob/blobcatflip.png)翻桌
fingerguns![fingerguns](https://cdn.jsdelivr.net/gh/infinitesum/Twikoo-emoji@master/Blob/blobcatfingerguns.png)比枪
fearful![fearful](https://cdn.jsdelivr.net/gh/infinitesum/Twikoo-emoji@master/Blob/blobcatfearful.png)害怕
facepalm![facepalm](https://cdn.jsdelivr.net/gh/infinitesum/Twikoo-emoji@master/Blob/blobcatfacepalm.png)捂脸
disturbed![disturbed](https://cdn.jsdelivr.net/gh/infinitesum/Twikoo-emoji@master/Blob/blobcatdisturbed.png)不安
died![died](https://cdn.jsdelivr.net/gh/infinitesum/Twikoo-emoji@master/Blob/blobcatdied.png)死了
dead![dead](https://cdn.jsdelivr.net/gh/infinitesum/Twikoo-emoji@master/Blob/blobcatdead.png)死亡
cry![cry](https://cdn.jsdelivr.net/gh/infinitesum/Twikoo-emoji@master/Blob/blobcatcry.png)哭
comfysweat![comfysweat](https://cdn.jsdelivr.net/gh/infinitesum/Twikoo-emoji@master/Blob/blobcatcomfysweat.png)舒适汗
comfy![comfy](https://cdn.jsdelivr.net/gh/infinitesum/Twikoo-emoji@master/Blob/blobcatcomfy.png)舒适
comftears![comftears](https://cdn.jsdelivr.net/gh/infinitesum/Twikoo-emoji@master/Blob/blobcatcomftears.png)舒适泪
coffee![coffee](https://cdn.jsdelivr.net/gh/infinitesum/Twikoo-emoji@master/Blob/blobcatcoffee.png)咖啡
caged![caged](https://cdn.jsdelivr.net/gh/infinitesum/Twikoo-emoji@master/Blob/blobcatcaged.png)关笼子
box![box](https://cdn.jsdelivr.net/gh/infinitesum/Twikoo-emoji@master/Blob/blobcatbox.png)箱子
blush![blush](https://cdn.jsdelivr.net/gh/infinitesum/Twikoo-emoji@master/Blob/blobcatblush.png)害羞
angry![angry](https://cdn.jsdelivr.net/gh/infinitesum/Twikoo-emoji@master/Blob/blobcatangry.png)生气
alt![alt](https://cdn.jsdelivr.net/gh/infinitesum/Twikoo-emoji@master/Blob/blobcatalt.png)alternative
0_0![0_0](https://cdn.jsdelivr.net/gh/infinitesum/Twikoo-emoji@master/Blob/blobcat0_0.png)瞪眼
awave![awave](https://cdn.jsdelivr.net/gh/infinitesum/Twikoo-emoji@master/Blob/ablobcatwave.png)挥手
areachrev![areachrev](https://cdn.jsdelivr.net/gh/infinitesum/Twikoo-emoji@master/Blob/ablobcatreachrev.png)伸手
arainbow![arainbow](https://cdn.jsdelivr.net/gh/infinitesum/Twikoo-emoji@master/Blob/ablobcatrainbow.png)彩虹
aheartbroken![aheartbroken](https://cdn.jsdelivr.net/gh/infinitesum/Twikoo-emoji@master/Blob/ablobcatheartbroken.png)心碎
aheart![aheart](https://cdn.jsdelivr.net/gh/infinitesum/Twikoo-emoji@master/Blob/ablobcatheart.png)比心
aattentionreverse![aattentionreverse](https://cdn.jsdelivr.net/gh/infinitesum/Twikoo-emoji@master/Blob/ablobcatattentionreverse.png)求关注(反)

方式 2:Astro 组件 + build 时静态 fetch(推荐)#

第 1 步:新建 src/components/EmojiGrid.astro

---
type Props = {
  /** 表情包 JSON 远程 URL */
  src?: string;
  /** 展示几个,默认 20 */
  limit?: number;
};

const {
  src = "https://cdn.jsdelivr.net/gh/infinitesum/Twikoo-emoji@master/blobcatplus.json",
  limit = 20,
} = Astro.props;

// build 期间拉取一次,输出静态 HTML
const res = await fetch(src);
if (!res.ok) throw new Error(`Failed to fetch ${src}: ${res.status}`);

const data = (await res.json()) as {
  aru: { container: { text: string; icon: string }[] };
};
const items = data.aru.container.slice(0, limit);
---

<ul class="my-6 grid grid-cols-8 gap-2 sm:grid-cols-10 md:grid-cols-12">
  {items.map(({ text, icon }) => (
    <li class="aspect-square overflow-hidden rounded" title={text} set:html={icon} />
  ))}
</ul>

第 2 步:把文章后缀从 .md 改成 .mdx(仅这一篇需要组件的文章需要 MDX)

第 3 步:文章里写

---
title: "小豆泥表情包"
description: "Twikoo 小豆泥表情包展示。"
---

import EmojiGrid from "@/components/EmojiGrid.astro";

## 小豆泥表情包一览

下面是从 BlobCatPlus 表情包取的前 20 个 blobcat:

<EmojiGrid limit={20} />

npm run build 会触发一次 fetch,把表情网格烘成静态 HTML,线上访问零请求。

方式 3:客户端运行时 fetch#

如果数据需要「拉新不重新 build」,用客户端 fetch:

---
// src/components/EmojiGridClient.astro
const {
  src = "https://cdn.jsdelivr.net/gh/infinitesum/Twikoo-emoji@master/blobcatplus.json",
  limit = 20,
} = Astro.props;
---

<div
  data-emoji-src={src}
  data-emoji-limit={limit}
  class="my-6 grid grid-cols-8 gap-2 sm:grid-cols-10 md:grid-cols-12"
>
  <noscript>需要 JavaScript 才能加载表情包。</noscript>
</div>

<script>
  document.querySelectorAll<HTMLDivElement>("[data-emoji-src]").forEach((el) => {
    const src = el.dataset.emojiSrc!;
    const limit = Number(el.dataset.emojiLimit ?? 20);
    fetch(src)
      .then((r) => r.json())
      .then((data) => {
        const items = (data.aru.container as { text: string; icon: string }[]).slice(0, limit);
        el.innerHTML = items
          .map(({ text, icon }) => `<span title="${text}">${icon}</span>`)
          .join("");
      });
  });
</script>

.mdx 中用法和方式 2 一样。

CORS#

cdn.jsdelivr.net 默认允许跨域 fetch。方式 2/3 都能直接走,不用代理。raw.githubusercontent.com 则会被拦,不要用裸 GitHub URL。

进阶:MDX + 组件混用#

把文件后缀从 .md 改成 .mdx,就能在 markdown 里 import 任何 Astro 组件:

---
title: "组件混用"
---

import Callout from "@/components/Callout.astro";
import { Image } from "astro:assets";
import chartPng from "@/assets/charts/growth.png";

<Callout type="tip">
  这是从 .astro 组件渲染的提示块。
</Callout>

<Image src={chartPng} alt="增长图" />

常规 markdown 仍然可用:

- 列表项
- **加粗**

.mdx 适合需要插组件的复杂文章,普通文章保持 .md 即可。

发布前 checklist#

完工。



上一篇
八朝古都开封:被黄河反复掩埋又重生的城
下一篇
Agent Loop:一个循环,就是模型与真实世界之间的全部距离(Agent架构实操一)