` | 表格 |
+
+## MDX 编译缓存
+
+`NotroContent` 内部使用 `compileMdxCached()`,它以 markdown 字符串的哈希为键缓存编译后的 MDX 模块。单次构建中相同页面的重新渲染会重用缓存的模块,无需重新调用 MDX 编译器。
+
+## CSS 包装器
+
+`notro-markdown` CSS 类应用于包装 `NotroContent` 输出的元素(由模板中的父元素应用,而非 `NotroContent` 本身)。`notro-theme.css` 中的该类为以下内容设置作用域样式:
+
+- `` / `` 块
+- 任务列表复选框
+- 表格
+- 引用
+
+```astro
+
+
+
+
+```
+
+## 类型参考
+
+```ts
+import type { NotionComponents, ClassMapKeys } from "notro-loader";
+
+interface NotroContentProps {
+ markdown: string;
+ linkToPages?: Record;
+ classMap?: Partial>;
+ components?: Partial;
+}
+```
diff --git a/scripts/push-translations.py b/scripts/push-translations.py
new file mode 100644
index 00000000..78929cc0
--- /dev/null
+++ b/scripts/push-translations.py
@@ -0,0 +1,95 @@
+#!/usr/bin/env python3
+"""Push Japanese and Chinese translation files to Notion docs data source."""
+
+import json
+import os
+import re
+import subprocess
+import sys
+from pathlib import Path
+
+NOTION_TOKEN = os.environ.get("NOTION_TOKEN")
+# database_id for data_source_id 1126b8b6-8958-825a-941b-87e5a047a3a0 (NOTION_DATASOURCE_ID)
+DOCS_DB_ID = "31c6b8b6-8958-81c0-96fd-e63fec21205b"
+CONTENT_DIR = Path(__file__).parent.parent / "content" / "docs"
+
+if not NOTION_TOKEN:
+ print("ERROR: NOTION_TOKEN is not set", file=sys.stderr)
+ sys.exit(1)
+
+
+def parse_frontmatter(content: str) -> tuple[dict, str]:
+ """Extract frontmatter and body from markdown content."""
+ if not content.startswith("---"):
+ return {}, content
+ end = content.index("---", 3)
+ fm_text = content[3:end].strip()
+ body = content[end + 3:].strip()
+ fm = {}
+ for line in fm_text.splitlines():
+ if ":" in line:
+ key, _, val = line.partition(":")
+ fm[key.strip()] = val.strip()
+ return fm, body
+
+
+def create_notion_page(title: str, slug: str, markdown_body: str) -> dict:
+ """Create a page in the Notion docs data source."""
+ payload = {
+ "parent": {"database_id": DOCS_DB_ID},
+ "properties": {
+ "Name": {"title": [{"text": {"content": title}}]},
+ "Slug": {"rich_text": [{"text": {"content": slug}}]},
+ "Public": {"checkbox": True},
+ },
+ "markdown": markdown_body,
+ }
+ payload_json = json.dumps(payload)
+
+ result = subprocess.run(
+ [
+ "curl", "-s",
+ "https://api.notion.com/v1/pages",
+ "-H", f"Authorization: Bearer {NOTION_TOKEN}",
+ "-H", "Notion-Version: 2026-03-11",
+ "-H", "Content-Type: application/json",
+ "-d", payload_json,
+ ],
+ capture_output=True,
+ text=True,
+ )
+ return json.loads(result.stdout)
+
+
+def main():
+ files = sorted(CONTENT_DIR.glob("*.md"))
+
+ print(f"Found {len(files)} files to push")
+
+ success = 0
+ errors = 0
+
+ for filepath in files:
+ content = filepath.read_text(encoding="utf-8")
+ fm, body = parse_frontmatter(content)
+ title = fm.get("title", filepath.stem)
+ slug = fm.get("slug", "")
+
+ print(f" Creating: {slug} ({title})")
+ response = create_notion_page(title, slug, body)
+
+ if response.get("object") == "page":
+ page_id = response["id"]
+ print(f" ✓ Created page {page_id}")
+ success += 1
+ else:
+ print(f" ✗ Error: {response.get('message', response)}")
+ errors += 1
+
+ print(f"\nDone: {success} created, {errors} errors")
+ if errors > 0:
+ sys.exit(1)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/templates/blog/src/components/Footer.astro b/templates/blog/src/components/Footer.astro
index b7332abb..c8929224 100644
--- a/templates/blog/src/components/Footer.astro
+++ b/templates/blog/src/components/Footer.astro
@@ -1,10 +1,57 @@
---
import config from "../config";
+
+const { pathname } = Astro.url;
+
+let currentLang = "en";
+let basePath = pathname;
+if (pathname.startsWith("/blog/ja/")) {
+ currentLang = "ja";
+ basePath = "/blog/" + pathname.slice("/blog/ja/".length);
+} else if (pathname.startsWith("/blog/zh-cn/")) {
+ currentLang = "zh-cn";
+ basePath = "/blog/" + pathname.slice("/blog/zh-cn/".length);
+}
+
+const afterBlog = basePath.slice("/blog/".length);
+const isArticle =
+ afterBlog.length > 0 && !/^\d+\/$/.test(afterBlog) && !afterBlog.startsWith("tag/");
+
+function langUrl(target: string): string {
+ if (!isArticle) return "/blog/";
+ if (target === "en") return basePath;
+ return `/blog/${target}/${basePath.slice("/blog/".length)}`;
+}
+
+const langs = [
+ { code: "en", label: "English" },
+ { code: "ja", label: "日本語" },
+ { code: "zh-cn", label: "中文" },
+];
---