Skip to content

Commit 7ea0795

Browse files
planeyangclaude
andauthored
feat(blog): Implement Phase 11 - Periodics & References content types (#77)
* feat(blog): Implement Phase 11 - Periodics & References content types This PR implements Phase 11 of the blog redesign, adding support for Periodics (digests, changelogs, notes) and References (bibliographies, reading lists, resources, tools) content types. ## Changes ### Types & Infrastructure (11A) - Extended `types/content.ts` with PeriodicType, ReferenceCategory types - Added Topic values: research, design, learning - Created `lib/periodics.ts` and `lib/references.ts` content loaders - Updated `lib/constants.ts` with labels and helper functions - Added `lib/mdx-options.ts` with remark-gfm for table support ### Routes & Components (11B) - Created Periodic components: PeriodicCard, PeriodicList, PeriodicHeader - Created Reference components: ReferenceCard, ReferenceList, ReferenceHeader - Created index pages: PeriodicsIndexPage, ReferencesIndexPage - Added routes: /periodics, /periodics/[slug], /references, /references/[slug] - Added Chinese routes: /zh/periodics, /zh/references - Updated SiteHeader navigation with new links - Added Storybook stories for all new components ### Content Migration (11C) - Migrated 22 digest files to apps/blog/content/periodics/ - Migrated 4 collection files to apps/blog/content/references/ - Created migration scripts: scripts/migrate-digest.js, scripts/migrate-collection.js ### UI Localization (11D) - Added translations in en.json and zh.json for: - Navigation links (nav.periodics, nav.references) - Periodic types (digest, changelog, notes) - Reference categories (resources, bibliography, reading-list, tools) - New topics (research, design, learning) ### Testing (11E) - Added unit tests: periodics.test.ts, references.test.ts - All 155 tests pass 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * docs: Update plan files to mark Phase 11 as complete - Updated IMPLEMENTATION_PLAN.md Phase Summary table - Updated ARCHITECTURE.md Implementation Status section - All 11 phases are now complete 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 5200aa8 commit 7ea0795

68 files changed

Lines changed: 7993 additions & 10 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

apps/blog/__tests__/periodics.test.ts

Lines changed: 461 additions & 0 deletions
Large diffs are not rendered by default.

apps/blog/__tests__/references.test.ts

Lines changed: 507 additions & 0 deletions
Large diffs are not rendered by default.

apps/blog/app/essays/[slug]/page.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import type { Metadata } from 'next';
33
import { MDXRemote } from 'next-mdx-remote/rsc';
44
import { getEssayBySlug, getEssaySlugs, getTranslation } from '@/lib/essays';
55
import { getMDXComponents } from '@/components/mdx/MDXComponents';
6+
import { mdxOptions } from '@/lib/mdx-options';
67
import { EssayLayout, EssayHeader } from '@/components/essay';
78

89
interface EssayPageProps {
@@ -86,7 +87,7 @@ export default async function EssayPage({ params }: EssayPageProps) {
8687
/>
8788
}
8889
>
89-
<MDXRemote source={content} components={getMDXComponents()} />
90+
<MDXRemote source={content} components={getMDXComponents()} options={{ mdxOptions }} />
9091
</EssayLayout>
9192
);
9293
}
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import { notFound } from 'next/navigation';
2+
import type { Metadata } from 'next';
3+
import { MDXRemote } from 'next-mdx-remote/rsc';
4+
import { getPeriodicBySlug, getPeriodicSlugs, getPeriodicTranslation } from '@/lib/periodics';
5+
import { getMDXComponents } from '@/components/mdx/MDXComponents';
6+
import { mdxOptions } from '@/lib/mdx-options';
7+
import { EssayLayout } from '@/components/essay';
8+
import { PeriodicHeader } from '@/components/periodic';
9+
10+
interface PeriodicPageProps {
11+
params: Promise<{ slug: string }>;
12+
}
13+
14+
/**
15+
* Generate static paths for all periodics
16+
*/
17+
export async function generateStaticParams() {
18+
const slugs = getPeriodicSlugs();
19+
return slugs.map((slug) => ({ slug }));
20+
}
21+
22+
/**
23+
* Generate metadata for the periodic page
24+
*/
25+
export async function generateMetadata({
26+
params,
27+
}: PeriodicPageProps): Promise<Metadata> {
28+
const { slug } = await params;
29+
const periodic = getPeriodicBySlug(slug);
30+
31+
if (!periodic) {
32+
return {
33+
title: 'Periodic Not Found',
34+
};
35+
}
36+
37+
// Build hreflang alternates
38+
const alternates: Metadata['alternates'] = {
39+
languages: {
40+
'en': `/periodics/${slug}`,
41+
},
42+
};
43+
44+
// Check for Chinese translation
45+
const zhTranslation = getPeriodicTranslation(slug, 'zh');
46+
if (zhTranslation && zhTranslation.slug !== slug) {
47+
alternates.languages!['zh'] = `/zh/periodics/${zhTranslation.slug}`;
48+
}
49+
50+
return {
51+
title: periodic.title,
52+
description: periodic.description || `${periodic.title} - Issue #${periodic.issue}`,
53+
openGraph: {
54+
title: periodic.title,
55+
description: periodic.description || `${periodic.title} - Issue #${periodic.issue}`,
56+
type: 'article',
57+
publishedTime: periodic.date,
58+
tags: periodic.topics,
59+
},
60+
alternates,
61+
};
62+
}
63+
64+
/**
65+
* Periodic page component
66+
*/
67+
export default async function PeriodicPage({ params }: PeriodicPageProps) {
68+
const { slug } = await params;
69+
const periodic = getPeriodicBySlug(slug);
70+
71+
if (!periodic) {
72+
notFound();
73+
}
74+
75+
const { title, description, date, issue, type, topics, readingTime, content } =
76+
periodic;
77+
78+
return (
79+
<EssayLayout
80+
header={
81+
<PeriodicHeader
82+
issue={issue}
83+
type={type}
84+
topics={topics}
85+
title={title}
86+
description={description}
87+
date={date}
88+
readingTime={readingTime}
89+
/>
90+
}
91+
>
92+
<MDXRemote source={content} components={getMDXComponents()} options={{ mdxOptions }} />
93+
</EssayLayout>
94+
);
95+
}

apps/blog/app/periodics/page.tsx

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import type { Metadata } from 'next';
2+
import { PeriodicsIndexPage } from '@/components/pages';
3+
4+
export const metadata: Metadata = {
5+
title: 'Periodics',
6+
description:
7+
'Curated digests, changelogs, and notes on technology, AI, and more.',
8+
alternates: {
9+
languages: {
10+
'en': '/periodics',
11+
'zh': '/zh/periodics',
12+
},
13+
},
14+
};
15+
16+
export default function PeriodicsPage() {
17+
return <PeriodicsIndexPage language="en" />;
18+
}
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
import { notFound } from 'next/navigation';
2+
import type { Metadata } from 'next';
3+
import { MDXRemote } from 'next-mdx-remote/rsc';
4+
import { getReferenceBySlug, getReferenceSlugs, getReferenceTranslation } from '@/lib/references';
5+
import { getMDXComponents } from '@/components/mdx/MDXComponents';
6+
import { mdxOptions } from '@/lib/mdx-options';
7+
import { EssayLayout } from '@/components/essay';
8+
import { ReferenceHeader } from '@/components/reference';
9+
10+
interface ReferencePageProps {
11+
params: Promise<{ slug: string }>;
12+
}
13+
14+
/**
15+
* Generate static paths for all references
16+
*/
17+
export async function generateStaticParams() {
18+
const slugs = getReferenceSlugs();
19+
return slugs.map((slug) => ({ slug }));
20+
}
21+
22+
/**
23+
* Generate metadata for the reference page
24+
*/
25+
export async function generateMetadata({
26+
params,
27+
}: ReferencePageProps): Promise<Metadata> {
28+
const { slug } = await params;
29+
const reference = getReferenceBySlug(slug);
30+
31+
if (!reference) {
32+
return {
33+
title: 'Reference Not Found',
34+
};
35+
}
36+
37+
// Build hreflang alternates
38+
const alternates: Metadata['alternates'] = {
39+
languages: {
40+
'en': `/references/${slug}`,
41+
},
42+
};
43+
44+
// Check for Chinese translation
45+
const zhTranslation = getReferenceTranslation(slug, 'zh');
46+
if (zhTranslation && zhTranslation.slug !== slug) {
47+
alternates.languages!['zh'] = `/zh/references/${zhTranslation.slug}`;
48+
}
49+
50+
return {
51+
title: reference.title,
52+
description: reference.description,
53+
openGraph: {
54+
title: reference.title,
55+
description: reference.description,
56+
type: 'article',
57+
publishedTime: reference.date,
58+
modifiedTime: reference.updated,
59+
tags: reference.topics,
60+
},
61+
alternates,
62+
};
63+
}
64+
65+
/**
66+
* Reference page component
67+
*/
68+
export default async function ReferencePage({ params }: ReferencePageProps) {
69+
const { slug } = await params;
70+
const reference = getReferenceBySlug(slug);
71+
72+
if (!reference) {
73+
notFound();
74+
}
75+
76+
const { title, description, date, updated, category, topics, itemCount, readingTime, content } =
77+
reference;
78+
79+
return (
80+
<EssayLayout
81+
header={
82+
<ReferenceHeader
83+
category={category}
84+
topics={topics}
85+
title={title}
86+
description={description}
87+
date={date}
88+
updated={updated}
89+
itemCount={itemCount}
90+
readingTime={readingTime}
91+
/>
92+
}
93+
>
94+
<MDXRemote source={content} components={getMDXComponents()} options={{ mdxOptions }} />
95+
</EssayLayout>
96+
);
97+
}

apps/blog/app/references/page.tsx

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import type { Metadata } from 'next';
2+
import { ReferencesIndexPage } from '@/components/pages';
3+
4+
export const metadata: Metadata = {
5+
title: 'References',
6+
description:
7+
'Curated resources, bibliographies, and reading lists.',
8+
alternates: {
9+
languages: {
10+
'en': '/references',
11+
'zh': '/zh/references',
12+
},
13+
},
14+
};
15+
16+
export default function ReferencesPage() {
17+
return <ReferencesIndexPage language="en" />;
18+
}

apps/blog/app/zh/essays/[slug]/page.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import type { Metadata } from 'next';
33
import { MDXRemote } from 'next-mdx-remote/rsc';
44
import { getEssayBySlug, getEssaySlugsByLanguage, getTranslation } from '@/lib/essays';
55
import { getMDXComponents } from '@/components/mdx/MDXComponents';
6+
import { mdxOptions } from '@/lib/mdx-options';
67
import { EssayLayout, EssayHeader } from '@/components/essay';
78

89
interface ZhEssayPageProps {
@@ -93,7 +94,7 @@ export default async function ZhEssayPage({ params }: ZhEssayPageProps) {
9394
/>
9495
}
9596
>
96-
<MDXRemote source={content} components={getMDXComponents()} />
97+
<MDXRemote source={content} components={getMDXComponents()} options={{ mdxOptions }} />
9798
</EssayLayout>
9899
);
99100
}
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import { notFound } from 'next/navigation';
2+
import type { Metadata } from 'next';
3+
import { MDXRemote } from 'next-mdx-remote/rsc';
4+
import { getPeriodicBySlug, getPeriodicSlugsByLanguage, getPeriodicTranslation } from '@/lib/periodics';
5+
import { getMDXComponents } from '@/components/mdx/MDXComponents';
6+
import { mdxOptions } from '@/lib/mdx-options';
7+
import { EssayLayout } from '@/components/essay';
8+
import { PeriodicHeader } from '@/components/periodic';
9+
10+
interface PeriodicPageProps {
11+
params: Promise<{ slug: string }>;
12+
}
13+
14+
/**
15+
* Generate static paths for all Chinese periodics
16+
*/
17+
export async function generateStaticParams() {
18+
const slugs = getPeriodicSlugsByLanguage('zh');
19+
return slugs.map((slug) => ({ slug }));
20+
}
21+
22+
/**
23+
* Generate metadata for the periodic page
24+
*/
25+
export async function generateMetadata({
26+
params,
27+
}: PeriodicPageProps): Promise<Metadata> {
28+
const { slug } = await params;
29+
const periodic = getPeriodicBySlug(slug);
30+
31+
if (!periodic) {
32+
return {
33+
title: '文摘未找到',
34+
};
35+
}
36+
37+
// Build hreflang alternates
38+
const alternates: Metadata['alternates'] = {
39+
languages: {
40+
'zh': `/zh/periodics/${slug}`,
41+
},
42+
};
43+
44+
// Check for English translation
45+
const enTranslation = getPeriodicTranslation(slug, 'en');
46+
if (enTranslation && enTranslation.slug !== slug) {
47+
alternates.languages!['en'] = `/periodics/${enTranslation.slug}`;
48+
}
49+
50+
return {
51+
title: periodic.title,
52+
description: periodic.description || `${periodic.title} - 第${periodic.issue}期`,
53+
openGraph: {
54+
title: periodic.title,
55+
description: periodic.description || `${periodic.title} - 第${periodic.issue}期`,
56+
type: 'article',
57+
publishedTime: periodic.date,
58+
tags: periodic.topics,
59+
},
60+
alternates,
61+
};
62+
}
63+
64+
/**
65+
* Chinese periodic page component
66+
*/
67+
export default async function ZhPeriodicPage({ params }: PeriodicPageProps) {
68+
const { slug } = await params;
69+
const periodic = getPeriodicBySlug(slug);
70+
71+
if (!periodic) {
72+
notFound();
73+
}
74+
75+
const { title, description, date, issue, type, topics, readingTime, content } =
76+
periodic;
77+
78+
return (
79+
<EssayLayout
80+
header={
81+
<PeriodicHeader
82+
issue={issue}
83+
type={type}
84+
topics={topics}
85+
title={title}
86+
description={description}
87+
date={date}
88+
readingTime={readingTime}
89+
language="zh"
90+
/>
91+
}
92+
>
93+
<MDXRemote source={content} components={getMDXComponents()} options={{ mdxOptions }} />
94+
</EssayLayout>
95+
);
96+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import type { Metadata } from 'next';
2+
import { PeriodicsIndexPage } from '@/components/pages';
3+
4+
export const metadata: Metadata = {
5+
title: '文摘',
6+
description: '精选文摘、更新日志和笔记',
7+
alternates: {
8+
languages: {
9+
'en': '/periodics',
10+
'zh': '/zh/periodics',
11+
},
12+
},
13+
};
14+
15+
export default function ZhPeriodicsPage() {
16+
return <PeriodicsIndexPage language="zh" />;
17+
}

0 commit comments

Comments
 (0)