Skip to content

Commit 623bfc1

Browse files
feat(blog): enhance heading utilities with locale support
Add locale-aware blog heading utilities and improve validation. Support both zh-CN and en-US blog directories with unified heading structure verification. Co-Authored-By: Hagicode <noreply@hagicode.com> Signed-off-by: newbe36524 <newbe36524@qq.com>
1 parent 210ddf4 commit 623bfc1

3 files changed

Lines changed: 68 additions & 8 deletions

File tree

scripts/blog-heading-utils.mjs

Lines changed: 31 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,27 @@ import fs from 'node:fs';
22
import path from 'node:path';
33

44
export const blogContentDir = path.resolve(process.cwd(), 'src/content/docs/blog');
5+
export const englishBlogContentDir = path.resolve(process.cwd(), 'src/content/docs/en/blog');
56
export const distDir = path.resolve(process.cwd(), 'dist');
7+
export const blogLocaleDirs = {
8+
zh: 'src/content/docs/blog',
9+
en: 'src/content/docs/en/blog',
10+
};
611

712
export function normalizeText(value) {
8-
return value.replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ').trim();
13+
return decodeHtmlEntities(value)
14+
.replace(/<[^>]+>/g, ' ')
15+
.replace(/\s+/g, ' ')
16+
.trim();
17+
}
18+
19+
function decodeHtmlEntities(value) {
20+
return value
21+
.replace(/&#39;/g, "'")
22+
.replace(/&quot;/g, '"')
23+
.replace(/&amp;/g, '&')
24+
.replace(/&lt;/g, '<')
25+
.replace(/&gt;/g, '>');
926
}
1027

1128
export function splitFrontmatter(source) {
@@ -123,24 +140,31 @@ export function listRenderedHtmlFiles(relativeDir) {
123140
return results.sort();
124141
}
125142

126-
export function getBlogSourceEntries() {
127-
if (!fs.existsSync(blogContentDir)) {
128-
throw new Error(`Missing blog content directory: ${blogContentDir}`);
143+
export function getBlogSourceEntries({ locale = 'zh', rootDir = process.cwd() } = {}) {
144+
const relativeDir = blogLocaleDirs[locale];
145+
if (!relativeDir) {
146+
throw new Error(`Unsupported blog locale: ${locale}`);
147+
}
148+
149+
const contentDir = path.resolve(rootDir, relativeDir);
150+
if (!fs.existsSync(contentDir)) {
151+
throw new Error(`Missing blog content directory: ${contentDir}`);
129152
}
130153

131154
return fs
132-
.readdirSync(blogContentDir)
155+
.readdirSync(contentDir)
133156
.filter((name) => name.endsWith('.md') || name.endsWith('.mdx'))
134157
.sort()
135158
.map((name) => {
136-
const relativePath = path.join('src/content/docs/blog', name);
137-
const fullPath = path.join(blogContentDir, name);
159+
const relativePath = path.join(relativeDir, name);
160+
const fullPath = path.join(contentDir, name);
138161
const source = readTextFile(fullPath);
139162
const { frontmatter, body, bodyStartLine } = splitFrontmatter(source);
140163
const slug = name.replace(/\.(md|mdx)$/i, '');
141164
return {
142165
fullPath,
143166
relativePath,
167+
locale,
144168
slug,
145169
title: parseFrontmatterTitle(frontmatter),
146170
body,

scripts/verify-blog-heading-structure.mjs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,8 +72,11 @@ function verifyCollectionRoutes() {
7272
}
7373

7474
function verifyPostRoutes() {
75-
for (const entry of getBlogSourceEntries()) {
75+
for (const entry of getBlogSourceEntries({ locale: 'zh' })) {
7676
verifyPostRoute(path.posix.join('blog', entry.slug, 'index.html'), entry.title);
77+
}
78+
79+
for (const entry of getBlogSourceEntries({ locale: 'en' })) {
7780
verifyPostRoute(path.posix.join('en/blog', entry.slug, 'index.html'), entry.title);
7881
}
7982
}

tests/blog-heading-validation.test.mjs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
11
import assert from 'node:assert/strict';
2+
import fs from 'node:fs';
3+
import os from 'node:os';
4+
import path from 'node:path';
25
import test from 'node:test';
36

47
import {
58
extractH1Texts,
9+
getBlogSourceEntries,
610
hiddenFirstPanelRuleExists,
711
parseFrontmatterTitle,
812
scanMarkdownH1s,
@@ -27,7 +31,36 @@ test('extractH1Texts normalizes nested markup', () => {
2731
assert.deepEqual(extractH1Texts(html), ['Docs Blog']);
2832
});
2933

34+
test('extractH1Texts decodes common HTML entities before comparison', () => {
35+
const html = '<h1>HagiCode&#39;s &quot;quoting&quot; test</h1>';
36+
assert.deepEqual(extractH1Texts(html), [`HagiCode's "quoting" test`]);
37+
});
38+
3039
test('hiddenFirstPanelRuleExists detects hidden title panel CSS', () => {
3140
assert.equal(hiddenFirstPanelRuleExists('.content-panel:first-of-type{display:none}'), true);
3241
assert.equal(hiddenFirstPanelRuleExists('.content-panel:first-of-type{display:block}'), false);
3342
});
43+
44+
test('getBlogSourceEntries reads localized blog titles from the matching locale directory', () => {
45+
const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), 'docs-blog-heading-'));
46+
fs.mkdirSync(path.join(rootDir, 'src/content/docs/blog'), { recursive: true });
47+
fs.mkdirSync(path.join(rootDir, 'src/content/docs/en/blog'), { recursive: true });
48+
49+
fs.writeFileSync(
50+
path.join(rootDir, 'src/content/docs/blog/example.mdx'),
51+
'---\ntitle: 中文标题\n---\n\n内容\n',
52+
'utf8',
53+
);
54+
fs.writeFileSync(
55+
path.join(rootDir, 'src/content/docs/en/blog/example.mdx'),
56+
'---\ntitle: English Title\n---\n\nBody\n',
57+
'utf8',
58+
);
59+
60+
const zhEntries = getBlogSourceEntries({ locale: 'zh', rootDir });
61+
const enEntries = getBlogSourceEntries({ locale: 'en', rootDir });
62+
63+
assert.equal(zhEntries[0].title, '中文标题');
64+
assert.equal(enEntries[0].title, 'English Title');
65+
assert.equal(enEntries[0].locale, 'en');
66+
});

0 commit comments

Comments
 (0)