@hutusi/amytis 1.5.6 → 1.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/CHANGELOG.md +94 -0
  2. package/CLAUDE.md +3 -2
  3. package/GEMINI.md +13 -6
  4. package/README.md +1 -1
  5. package/TODO.md +21 -76
  6. package/bun.lock +18 -3
  7. package/content/about.mdx +1 -0
  8. package/content/about.zh.mdx +21 -0
  9. package/content/flows/2026/02/20.md +16 -0
  10. package/content/links.mdx +42 -0
  11. package/content/links.zh.mdx +41 -0
  12. package/content/posts/2026-02-20-i18n-routing-considerations.mdx +150 -0
  13. package/content/posts/multimedia-showcase/index.mdx +261 -0
  14. package/content/privacy.mdx +32 -0
  15. package/content/privacy.zh.mdx +32 -0
  16. package/docs/ARCHITECTURE.md +11 -2
  17. package/docs/CONTRIBUTING.md +4 -2
  18. package/docs/deployment.md +9 -1
  19. package/eslint.config.mjs +2 -0
  20. package/package.json +5 -4
  21. package/public/next-image-export-optimizer-hashes.json +0 -3
  22. package/scripts/copy-assets.ts +1 -1
  23. package/site.config.ts +126 -44
  24. package/src/app/[slug]/page.tsx +0 -10
  25. package/src/app/archive/page.tsx +38 -10
  26. package/src/app/books/[slug]/page.tsx +18 -0
  27. package/src/app/flows/[year]/[month]/[day]/page.tsx +21 -4
  28. package/src/app/layout.tsx +48 -21
  29. package/src/app/page.tsx +135 -72
  30. package/src/app/posts/[slug]/page.tsx +6 -12
  31. package/src/app/search.json/route.ts +4 -0
  32. package/src/app/series/[slug]/page.tsx +18 -0
  33. package/src/app/subscribe/page.tsx +17 -0
  34. package/src/app/tags/[tag]/page.tsx +9 -26
  35. package/src/app/tags/page.tsx +3 -8
  36. package/src/components/AuthorCard.tsx +43 -0
  37. package/src/components/Comments.tsx +20 -4
  38. package/src/components/ExternalLinks.tsx +6 -2
  39. package/src/components/Footer.tsx +35 -26
  40. package/src/components/LanguageProvider.tsx +0 -5
  41. package/src/components/LanguageSwitch.tsx +117 -6
  42. package/src/components/LocaleSwitch.tsx +33 -0
  43. package/src/components/Navbar.tsx +31 -8
  44. package/src/components/PostNavigation.tsx +55 -0
  45. package/src/components/PostSidebar.tsx +172 -126
  46. package/src/components/ReadingProgressBar.tsx +6 -21
  47. package/src/components/RelatedPosts.tsx +1 -1
  48. package/src/components/Search.tsx +420 -70
  49. package/src/components/SelectedBooksSection.tsx +12 -6
  50. package/src/components/ShareBar.tsx +115 -0
  51. package/src/components/SimpleLayoutHeader.tsx +5 -14
  52. package/src/components/SubscribePage.tsx +298 -0
  53. package/src/components/TagContentTabs.tsx +103 -0
  54. package/src/components/TagPageHeader.tsx +7 -13
  55. package/src/components/TagSidebar.tsx +142 -0
  56. package/src/components/TagsIndexClient.tsx +156 -0
  57. package/src/hooks/useScrollY.ts +41 -0
  58. package/src/i18n/translations.ts +110 -2
  59. package/src/layouts/PostLayout.tsx +34 -7
  60. package/src/layouts/SimpleLayout.tsx +53 -15
  61. package/src/lib/markdown.ts +71 -15
  62. package/src/lib/search-utils.test.ts +163 -0
  63. package/src/lib/search-utils.ts +39 -0
  64. package/src/types/pagefind.d.ts +42 -0
  65. package/src/components/TableOfContents.tsx +0 -158
package/site.config.ts CHANGED
@@ -1,67 +1,140 @@
1
+ export interface NavItem {
2
+ name: string;
3
+ url: string;
4
+ weight: number;
5
+ dropdown?: string[];
6
+ }
7
+
8
+ // Defined up-front so footer.connect can reference these URLs without duplication
9
+ const social = {
10
+ github: "https://github.com/hutusi/amytis",
11
+ twitter: "https://twitter.com/hutusi",
12
+ email: "mailto:huziyong@gmail.com",
13
+ };
14
+
1
15
  export const siteConfig = {
16
+
17
+ // ── Site identity ─────────────────────────────────────────────────────────
2
18
  title: { en: "Amytis", zh: "Amytis" },
3
19
  description: { en: "A minimalist digital garden for growing thoughts and sharing knowledge.", zh: "一个极简的数字花园,用于培育思想和分享知识。" },
4
20
  baseUrl: "https://example.com", // Replace with your actual domain
21
+ ogImage: "/og-image.png", // Default OG/social preview image — place a 1200×630 PNG at public/og-image.png
5
22
  footerText: { en: `© ${new Date().getFullYear()} Amytis. All rights reserved.`, zh: `© ${new Date().getFullYear()} Amytis. 保留所有权利。` },
6
- nav: [
7
- { name: "Home", url: "/", weight: 1 },
8
- { name: "Flow", url: "/flows", weight: 1.1 },
9
- { name: "Books", url: "/books", weight: 1.3 },
10
- { name: "Series", url: "/series", weight: 1.5 },
11
- { name: "Archive", url: "/archive", weight: 2 },
12
- { name: "Tags", url: "/tags", weight: 3 },
13
- { name: "About", url: "/about", weight: 4 },
14
- ],
15
- social: {
16
- github: "https://github.com/hutusi/amytis",
17
- twitter: "https://twitter.com/hutusi",
18
- email: "mailto:huziyong@gmail.com",
23
+
24
+ // ── i18n ──────────────────────────────────────────────────────────────────
25
+ i18n: {
26
+ defaultLocale: 'en',
27
+ locales: ['en', 'zh'],
19
28
  },
20
- series: {
21
- navbar: ["digital-garden", "markdown-showcase", "ai-nexus-weekly"], // Slugs of series to show in navbar
29
+
30
+ // ── Navigation ────────────────────────────────────────────────────────────
31
+ nav: [
32
+ { name: "Flow", url: "/flows", weight: 1 },
33
+ { name: "Posts", url: "/posts", weight: 2 },
34
+ { name: "Series", url: "/series", weight: 3, dropdown: ["digital-garden", "markdown-showcase", "ai-nexus-weekly"] },
35
+ { name: "Books", url: "/books", weight: 4, dropdown: [] },
36
+ { name: "About", url: "/about", weight: 5 },
37
+ ] as NavItem[],
38
+
39
+ // ── Footer ────────────────────────────────────────────────────────────────
40
+ footer: {
41
+ explore: [
42
+ { name: "Archive", url: "/archive", weight: 1 },
43
+ { name: "Tags", url: "/tags", weight: 2 },
44
+ { name: "Links", url: "/links", weight: 3 },
45
+ { name: "About", url: "/about", weight: 4 },
46
+ ],
47
+ connect: [
48
+ { name: "GitHub", url: social.github, weight: 1 },
49
+ { name: "Twitter", url: social.twitter, weight: 2 },
50
+ { name: "RSS Feed", url: "/feed.xml", weight: 3 },
51
+ { name: "Subscribe", url: "/subscribe", weight: 4 },
52
+ ],
53
+ builtWith: {
54
+ show: true,
55
+ url: "https://github.com/hutusi/amytis",
56
+ text: { en: "Built with Amytis", zh: "基于 Amytis 构建" },
57
+ },
22
58
  },
23
- books: {
24
- navbar: [] as string[], // Slugs of books to show in navbar dropdown
59
+
60
+ // ── Social & sharing ──────────────────────────────────────────────────────
61
+ social,
62
+ share: {
63
+ enabled: true,
64
+ // Supported: twitter, facebook, linkedin, weibo, reddit, hackernews,
65
+ // telegram, bluesky, mastodon, douban, zhihu, copy
66
+ platforms: ['twitter', 'facebook', 'linkedin', 'weibo', 'copy'],
25
67
  },
26
- archive: {
27
- showAuthors: true,
68
+ subscribe: {
69
+ substack: '', // Substack publication URL, e.g., 'https://yourname.substack.com'
70
+ telegram: '', // Telegram channel URL, e.g., 'https://t.me/yourchannel'
71
+ wechat: {
72
+ qrCode: '', // Path to QR image in public/, e.g., '/images/wechat-qr.png'
73
+ account: '', // WeChat official account ID/name shown below QR
74
+ },
75
+ email: '', // Newsletter/mailing list URL (distinct from social.email contact address)
28
76
  },
29
- pagination: {
30
- posts: 5,
31
- series: 1,
32
- flows: 20,
77
+
78
+ // ── Features ──────────────────────────────────────────────────────────────
79
+ features: {
80
+ posts: {
81
+ enabled: true,
82
+ name: { en: "Posts", zh: "文章" },
83
+ },
84
+ series: {
85
+ enabled: true,
86
+ name: { en: "Series", zh: "系列" },
87
+ },
88
+ books: {
89
+ enabled: true,
90
+ name: { en: "Books", zh: "书籍" },
91
+ },
92
+ flows: {
93
+ enabled: true,
94
+ name: { en: "Flow", zh: "随笔" },
95
+ },
33
96
  },
34
- includeDateInUrl: false,
35
- // trailingSlash is configured in next.config.ts (Next.js handles URL normalization)
36
- showFuturePosts: false,
37
- toc: true,
38
- themeColor: 'default', // 'default' | 'blue' | 'rose' | 'amber'
97
+
98
+ // ── Homepage ──────────────────────────────────────────────────────────────
39
99
  hero: {
40
100
  tagline: { en: "Digital Garden", zh: "数字花园" },
41
101
  title: { en: "Cultivating Digital Knowledge", zh: "培育数字知识" },
42
102
  subtitle: { en: "A minimalist digital garden for growing thoughts and sharing knowledge.", zh: "一个极简的数字花园,用于培育思想和分享知识。" },
43
103
  },
44
- about: {
45
- title: { en: "About Amytis", zh: "关于 Amytis" },
46
- subtitle: { en: "Learn more about the philosophy and technology behind this digital garden.", zh: "了解这座数字花园背后的理念与技术。" },
104
+ homepage: {
105
+ sections: [
106
+ { id: 'hero', enabled: true, weight: 1 },
107
+ { id: 'featured-series', enabled: true, weight: 2, maxItems: 6, scrollThreshold: 2 },
108
+ { id: 'featured-books', enabled: true, weight: 3, maxItems: 4, scrollThreshold: 2 },
109
+ { id: 'featured-posts', enabled: true, weight: 4, maxItems: 4, scrollThreshold: 1 },
110
+ { id: 'latest-posts', enabled: true, weight: 5, maxItems: 5 },
111
+ { id: 'recent-flows', enabled: true, weight: 6, maxItems: 5 },
112
+ ],
47
113
  },
48
- flows: {
49
- recentCount: 5,
114
+
115
+ // ── Content ───────────────────────────────────────────────────────────────
116
+ pagination: {
117
+ posts: 5,
118
+ series: 1,
119
+ flows: 20,
50
120
  },
51
- featured: {
52
- series: {
53
- scrollThreshold: 2, // Enable scrolling when more than this number
54
- maxItems: 6,
55
- },
56
- stories: {
57
- scrollThreshold: 1, // Enable scrolling when more than this number
58
- maxItems: 4,
121
+ posts: {
122
+ toc: true,
123
+ showFuturePosts: false,
124
+ includeDateInUrl: false,
125
+ // trailingSlash is configured in next.config.ts (Next.js handles URL normalization)
126
+ archive: {
127
+ showAuthors: true,
59
128
  },
60
129
  },
61
- i18n: {
62
- defaultLocale: 'en',
63
- locales: ['en', 'zh'],
130
+ flows: {
131
+ recentCount: 5,
64
132
  },
133
+
134
+ // ── Appearance ────────────────────────────────────────────────────────────
135
+ themeColor: 'default', // 'default' | 'blue' | 'rose' | 'amber'
136
+
137
+ // ── Analytics ─────────────────────────────────────────────────────────────
65
138
  analytics: {
66
139
  provider: 'umami', // 'umami' | 'plausible' | 'google' | null
67
140
  umami: {
@@ -76,6 +149,8 @@ export const siteConfig = {
76
149
  measurementId: '', // G-XXXXXXXXXX
77
150
  },
78
151
  },
152
+
153
+ // ── Comments ──────────────────────────────────────────────────────────────
79
154
  comments: {
80
155
  provider: 'giscus', // 'giscus' | 'disqus' | null
81
156
  giscus: {
@@ -88,4 +163,11 @@ export const siteConfig = {
88
163
  shortname: '',
89
164
  },
90
165
  },
166
+
167
+ // ── Authors ───────────────────────────────────────────────────────────────
168
+ authors: {
169
+ // Map display name (as used in post frontmatter) to author profile
170
+ // "Author Name": { bio: "Short bio shown in author card below each post." },
171
+ } as Record<string, { bio?: string }>,
172
+
91
173
  };
@@ -53,15 +53,5 @@ export default async function Page({
53
53
  return <PostLayout post={page} />;
54
54
  }
55
55
 
56
- if (slug === 'about' && siteConfig.about) {
57
- return (
58
- <SimpleLayout
59
- post={page}
60
- titleOverride={siteConfig.about.title}
61
- subtitleOverride={siteConfig.about.subtitle}
62
- />
63
- );
64
- }
65
-
66
56
  return <SimpleLayout post={page} />;
67
57
  }
@@ -1,7 +1,7 @@
1
1
  import Link from 'next/link';
2
- import { getAllPosts, PostData } from '@/lib/markdown';
2
+ import { getAllPosts, getSeriesData, PostData } from '@/lib/markdown';
3
3
  import { siteConfig } from '../../../site.config';
4
- import { resolveLocale } from '@/lib/i18n';
4
+ import { resolveLocale, t } from '@/lib/i18n';
5
5
  import PageHeader from '@/components/PageHeader';
6
6
 
7
7
  export const metadata = {
@@ -41,12 +41,20 @@ function groupPostsByDate(posts: PostData[]): GroupedPosts {
41
41
  export default function ArchivePage() {
42
42
  const posts = getAllPosts();
43
43
  const groupedPosts = groupPostsByDate(posts);
44
- const showAuthors = siteConfig.archive?.showAuthors;
44
+ const showAuthors = siteConfig.posts?.archive?.showAuthors;
45
45
 
46
46
  // Sort years descending to show newest content first
47
47
  const years = Object.keys(groupedPosts).sort((a, b) => Number(b) - Number(a));
48
48
  const totalPosts = posts.length;
49
49
 
50
+ // Build series slug → title map (one lookup per unique series)
51
+ const seriesSlugs = [...new Set(posts.filter(p => p.series).map(p => p.series!))];
52
+ const seriesTitleMap: Record<string, string> = {};
53
+ for (const slug of seriesSlugs) {
54
+ const data = getSeriesData(slug);
55
+ if (data) seriesTitleMap[slug] = data.title;
56
+ }
57
+
50
58
  return (
51
59
  <div className="layout-main">
52
60
  <PageHeader
@@ -57,7 +65,25 @@ export default function ArchivePage() {
57
65
  subtitleParams={{ count: totalPosts, years: years.length }}
58
66
  />
59
67
 
60
- <main className="max-w-4xl mx-auto">
68
+ <main>
69
+ {/* Year-jump navigation — mirrors content grid to align with timeline column */}
70
+ {years.length > 1 && (
71
+ <div className="grid grid-cols-1 md:grid-cols-[200px_1fr] gap-8 md:gap-16 mb-16">
72
+ <div />
73
+ <nav aria-label="Jump to year" className="flex flex-wrap items-center gap-2">
74
+ {years.map(year => (
75
+ <a
76
+ key={year}
77
+ href={`#${year}`}
78
+ className="text-xs font-mono text-muted hover:text-accent border border-muted/20 hover:border-accent/40 rounded px-3 py-1 transition-all duration-200 no-underline"
79
+ >
80
+ {year}
81
+ </a>
82
+ ))}
83
+ </nav>
84
+ </div>
85
+ )}
86
+
61
87
  <div className="space-y-24">
62
88
  {years.map((year) => {
63
89
  // Sort months within the year in descending order (December -> January)
@@ -67,7 +93,7 @@ export default function ArchivePage() {
67
93
  const yearTotal = months.reduce((total, month) => total + groupedPosts[year][month].length, 0);
68
94
 
69
95
  return (
70
- <section key={year} className="relative grid grid-cols-1 md:grid-cols-[200px_1fr] gap-8 md:gap-16">
96
+ <section key={year} id={year} className="relative grid grid-cols-1 md:grid-cols-[200px_1fr] gap-8 md:gap-16">
71
97
  {/* Year Marker */}
72
98
  <div className="relative">
73
99
  <div className="sticky top-24 lg:top-32 text-left md:text-right">
@@ -75,7 +101,7 @@ export default function ArchivePage() {
75
101
  {year}
76
102
  </h2>
77
103
  <span className="block text-xs font-bold uppercase tracking-widest text-muted mt-2">
78
- {yearTotal} Posts
104
+ {yearTotal} {t('posts')}
79
105
  </span>
80
106
  </div>
81
107
  </div>
@@ -91,7 +117,9 @@ export default function ArchivePage() {
91
117
 
92
118
  <h3 className="text-base font-sans font-bold uppercase tracking-widest text-accent mb-8">
93
119
  {getMonthName(Number(month))}
94
- <span className="ml-2 text-xs font-normal text-muted/60">({monthPosts.length})</span>
120
+ <span className="ml-2 inline-flex items-center text-[10px] font-mono text-muted/60 bg-muted/10 rounded px-1.5 py-0.5 align-middle leading-none">
121
+ {monthPosts.length}
122
+ </span>
95
123
  </h3>
96
124
 
97
125
  <ul className="space-y-6">
@@ -113,10 +141,10 @@ export default function ArchivePage() {
113
141
  </h4>
114
142
  {post.series && (
115
143
  <span
116
- title={post.series}
117
- className="text-[10px] font-sans font-medium uppercase tracking-wider text-accent/60 border border-accent/20 rounded px-1.5 py-0.5 shrink-0 leading-none max-w-[10ch] truncate inline-block align-baseline"
144
+ title={seriesTitleMap[post.series] ?? post.series}
145
+ className="text-[10px] font-sans font-medium uppercase tracking-wider text-accent/60 border border-accent/20 rounded px-1.5 py-0.5 shrink-0 leading-none max-w-[14ch] truncate inline-block align-baseline"
118
146
  >
119
- {post.series}
147
+ {seriesTitleMap[post.series] ?? post.series}
120
148
  </span>
121
149
  )}
122
150
  </div>
@@ -22,9 +22,27 @@ export async function generateMetadata({ params }: { params: Promise<{ slug: str
22
22
  return { title: 'Book Not Found' };
23
23
  }
24
24
 
25
+ const ogImage = book.coverImage && !book.coverImage.startsWith('text:') && !book.coverImage.startsWith('./')
26
+ ? book.coverImage
27
+ : siteConfig.ogImage;
28
+
25
29
  return {
26
30
  title: `${book.title} | ${resolveLocale(siteConfig.title)}`,
27
31
  description: book.excerpt,
32
+ openGraph: {
33
+ title: book.title,
34
+ description: book.excerpt,
35
+ type: 'website',
36
+ url: `${siteConfig.baseUrl}/books/${slug}`,
37
+ siteName: resolveLocale(siteConfig.title),
38
+ images: [{ url: ogImage, width: 1200, height: 630, alt: book.title }],
39
+ },
40
+ twitter: {
41
+ card: ogImage !== siteConfig.ogImage ? 'summary_large_image' : 'summary',
42
+ title: book.title,
43
+ description: book.excerpt,
44
+ images: [ogImage],
45
+ },
28
46
  };
29
47
  }
30
48
 
@@ -5,6 +5,7 @@ import { notFound } from 'next/navigation';
5
5
  import { t, resolveLocale } from '@/lib/i18n';
6
6
  import FlowCalendarSidebar from '@/components/FlowCalendarSidebar';
7
7
  import MarkdownRenderer from '@/components/MarkdownRenderer';
8
+ import ShareBar from '@/components/ShareBar';
8
9
  import Link from 'next/link';
9
10
 
10
11
  export function generateStaticParams() {
@@ -24,6 +25,19 @@ export async function generateMetadata({ params }: { params: Promise<{ year: str
24
25
  return {
25
26
  title: `${flow.title} | ${resolveLocale(siteConfig.title)}`,
26
27
  description: flow.excerpt,
28
+ openGraph: {
29
+ title: flow.title,
30
+ description: flow.excerpt,
31
+ type: 'article',
32
+ publishedTime: flow.date,
33
+ url: `${siteConfig.baseUrl}/flows/${year}/${month}/${day}`,
34
+ siteName: resolveLocale(siteConfig.title),
35
+ },
36
+ twitter: {
37
+ card: 'summary',
38
+ title: flow.title,
39
+ description: flow.excerpt,
40
+ },
27
41
  };
28
42
  }
29
43
 
@@ -36,6 +50,7 @@ export default async function FlowPage({ params }: { params: Promise<{ year: str
36
50
  const allFlows = getAllFlows();
37
51
  const entryDates = allFlows.map(f => f.date);
38
52
  const { prev, next } = getAdjacentFlows(flow.slug);
53
+ const flowUrl = `${siteConfig.baseUrl}/flows/${year}/${month}/${day}`;
39
54
 
40
55
  return (
41
56
  <div className="layout-main">
@@ -62,7 +77,7 @@ export default async function FlowPage({ params }: { params: Promise<{ year: str
62
77
  <article className="flex-1 min-w-0">
63
78
  {/* Header */}
64
79
  <header className="mb-8">
65
- <time className="text-sm font-mono text-accent">{flow.date}</time>
80
+ <time className="text-sm font-mono text-accent" data-pagefind-meta="date[content]">{flow.date}</time>
66
81
  <h1 className="mt-2 text-3xl md:text-4xl font-serif font-bold text-heading">{flow.title}</h1>
67
82
  </header>
68
83
 
@@ -71,14 +86,16 @@ export default async function FlowPage({ params }: { params: Promise<{ year: str
71
86
  <MarkdownRenderer content={flow.content} />
72
87
  </div>
73
88
 
89
+ <ShareBar url={flowUrl} title={flow.title} className="mt-8 mb-2" />
90
+
74
91
  {/* Prev/Next navigation */}
75
- <nav className="mt-16 pt-8 border-t border-muted/20 grid grid-cols-2 gap-4">
92
+ <nav className="mt-12 pt-12 border-t border-muted/20 grid grid-cols-2 gap-4">
76
93
  {prev ? (
77
94
  <Link
78
95
  href={`/flows/${prev.slug}`}
79
96
  className="group text-left no-underline"
80
97
  >
81
- <span className="text-xs text-muted">Older</span>
98
+ <span className="text-xs text-muted">{t('older')}</span>
82
99
  <div className="text-sm font-medium text-heading group-hover:text-accent transition-colors truncate">
83
100
  {prev.title}
84
101
  </div>
@@ -90,7 +107,7 @@ export default async function FlowPage({ params }: { params: Promise<{ year: str
90
107
  href={`/flows/${next.slug}`}
91
108
  className="group text-right no-underline"
92
109
  >
93
- <span className="text-xs text-muted">Newer</span>
110
+ <span className="text-xs text-muted">{t('newer')}</span>
94
111
  <div className="text-sm font-medium text-heading group-hover:text-accent transition-colors truncate">
95
112
  {next.title}
96
113
  </div>
@@ -6,7 +6,7 @@ import Analytics from "@/components/Analytics";
6
6
  import { siteConfig } from "../../site.config";
7
7
  import { ThemeProvider } from "@/components/ThemeProvider";
8
8
  import { LanguageProvider } from "@/components/LanguageProvider";
9
- import { getAllSeries, getAllBooks } from "@/lib/markdown";
9
+ import { getAllSeries, getAllBooks, getSeriesData } from "@/lib/markdown";
10
10
  import { resolveLocale } from "@/lib/i18n";
11
11
  import "./globals.css";
12
12
 
@@ -47,6 +47,12 @@ const baskerville = localFont({
47
47
  variable: "--font-baskerville",
48
48
  });
49
49
 
50
+ const siteTwitterHandle = (() => {
51
+ const url = siteConfig.social?.twitter ?? '';
52
+ const m = url.match(/(?:twitter\.com|x\.com)\/([^/?#]+)/);
53
+ return m ? `@${m[1]}` : undefined;
54
+ })();
55
+
50
56
  export const metadata: Metadata = {
51
57
  metadataBase: new URL(siteConfig.baseUrl),
52
58
  title: resolveLocale(siteConfig.title),
@@ -54,6 +60,17 @@ export const metadata: Metadata = {
54
60
  icons: {
55
61
  icon: "/icon.svg",
56
62
  },
63
+ openGraph: {
64
+ siteName: resolveLocale(siteConfig.title),
65
+ locale: siteConfig.i18n.defaultLocale,
66
+ type: 'website',
67
+ images: [{ url: siteConfig.ogImage, width: 1200, height: 630 }],
68
+ },
69
+ twitter: {
70
+ card: 'summary',
71
+ site: siteTwitterHandle,
72
+ creator: siteTwitterHandle,
73
+ },
57
74
  };
58
75
 
59
76
  export default function RootLayout({
@@ -61,33 +78,43 @@ export default function RootLayout({
61
78
  }: Readonly<{
62
79
  children: React.ReactNode;
63
80
  }>) {
64
- const allSeries = getAllSeries();
65
- const featuredSeries = siteConfig.series?.navbar;
66
-
67
- const seriesKeys = Object.keys(allSeries).sort();
68
- const filteredKeys = featuredSeries
69
- ? seriesKeys.filter(slug => featuredSeries.includes(slug))
70
- : seriesKeys.slice(0, 5);
81
+ const features = siteConfig.features;
71
82
 
72
- const seriesList = filteredKeys.map(slug => ({
73
- name: allSeries[slug][0]?.series || slug,
74
- slug,
75
- }));
83
+ // Build series list for navbar (only when series feature is enabled)
84
+ const seriesNavItem = siteConfig.nav.find(item => item.url === '/series');
85
+ const featuredSeries = seriesNavItem?.dropdown;
86
+ let seriesList: { name: string; slug: string }[] = [];
87
+ if (features?.series?.enabled !== false) {
88
+ const allSeries = getAllSeries();
89
+ const seriesKeys = Object.keys(allSeries).sort();
90
+ const filteredKeys = featuredSeries && featuredSeries.length > 0
91
+ ? seriesKeys.filter(slug => featuredSeries.includes(slug))
92
+ : seriesKeys.slice(0, 5);
93
+ seriesList = filteredKeys.map(slug => ({
94
+ name: getSeriesData(slug)?.title || allSeries[slug][0]?.series || slug,
95
+ slug,
96
+ }));
97
+ }
76
98
 
77
- // Build books list for navbar
78
- const allBooks = getAllBooks();
79
- const featuredBookSlugs = siteConfig.books?.navbar;
80
- const booksList = featuredBookSlugs && featuredBookSlugs.length > 0
81
- ? allBooks
82
- .filter(book => featuredBookSlugs.includes(book.slug))
83
- .map(book => ({ name: book.title, slug: book.slug }))
84
- : allBooks.map(book => ({ name: book.title, slug: book.slug }));
99
+ // Build books list for navbar (only when books feature is enabled)
100
+ const booksNavItem = siteConfig.nav.find(item => item.url === '/books');
101
+ const featuredBookSlugs = booksNavItem?.dropdown;
102
+ let booksList: { name: string; slug: string }[] = [];
103
+ if (features?.books?.enabled !== false) {
104
+ const allBooks = getAllBooks();
105
+ booksList = featuredBookSlugs && featuredBookSlugs.length > 0
106
+ ? allBooks
107
+ .filter(book => featuredBookSlugs.includes(book.slug))
108
+ .map(book => ({ name: book.title, slug: book.slug }))
109
+ : allBooks.slice(0, 5).map(book => ({ name: book.title, slug: book.slug }));
110
+ }
85
111
 
86
112
  return (
87
- <html lang="en" suppressHydrationWarning>
113
+ <html lang={siteConfig.i18n.defaultLocale} suppressHydrationWarning>
88
114
  <body
89
115
  className={`${inter.variable} ${baskerville.variable} font-sans min-h-screen transition-colors duration-300`}
90
116
  data-palette={siteConfig.themeColor}
117
+ suppressHydrationWarning
91
118
  >
92
119
  {/* Skip to main content link for accessibility */}
93
120
  <a