@hutusi/amytis 1.5.5
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.
- package/.github/workflows/ci.yml +33 -0
- package/.github/workflows/publish.yml +53 -0
- package/AGENTS.md +41 -0
- package/CLAUDE.md +200 -0
- package/GEMINI.md +84 -0
- package/README.md +172 -0
- package/TODO.md +76 -0
- package/bun.lock +1530 -0
- package/content/about.mdx +23 -0
- package/content/books/sample-book/index.mdx +24 -0
- package/content/books/sample-book/introduction.mdx +34 -0
- package/content/books/sample-book/setup.mdx +48 -0
- package/content/books/sample-book/writing-content.mdx +49 -0
- package/content/flows/2026/02/05.md +8 -0
- package/content/flows/2026/02/10.mdx +8 -0
- package/content/flows/2026/02/15.md +8 -0
- package/content/flows/2026/02/18.mdx +14 -0
- package/content/posts/2026-01-12-the-art-of-algorithms.mdx +49 -0
- package/content/posts/2026-01-15-nested-image-test/images/test.svg +5 -0
- package/content/posts/2026-01-15-nested-image-test/index.mdx +27 -0
- package/content/posts/2026-01-21-kitchen-sink/assets/test.svg +5 -0
- package/content/posts/2026-01-21-kitchen-sink/index.mdx +169 -0
- package/content/posts/asynchronous-javascript.mdx +49 -0
- package/content/posts/draft-post.mdx +13 -0
- package/content/posts/future-post.mdx +12 -0
- package/content/posts/legacy-markdown.md +60 -0
- package/content/posts/markdown-features.mdx +78 -0
- package/content/posts/modern-css-layouts.mdx +45 -0
- package/content/posts/multilingual-test.mdx +124 -0
- package/content/posts/syntax-highlighting-showcase.mdx +528 -0
- package/content/posts/understanding-react-hooks.mdx +48 -0
- package/content/posts/welcome-to-amytis.mdx +21 -0
- package/content/posts//344/270/255/346/226/207/346/265/213/350/257/225/346/226/207/347/253/240.mdx +54 -0
- package/content/series/ai-nexus-weekly/index.mdx +10 -0
- package/content/series/ai-nexus-weekly/week-1.mdx +20 -0
- package/content/series/ai-nexus-weekly/week-10.mdx +20 -0
- package/content/series/ai-nexus-weekly/week-11.mdx +20 -0
- package/content/series/ai-nexus-weekly/week-12.mdx +20 -0
- package/content/series/ai-nexus-weekly/week-2.mdx +20 -0
- package/content/series/ai-nexus-weekly/week-3.mdx +20 -0
- package/content/series/ai-nexus-weekly/week-4.mdx +20 -0
- package/content/series/ai-nexus-weekly/week-5.mdx +20 -0
- package/content/series/ai-nexus-weekly/week-6.mdx +20 -0
- package/content/series/ai-nexus-weekly/week-7.mdx +20 -0
- package/content/series/ai-nexus-weekly/week-8.mdx +20 -0
- package/content/series/ai-nexus-weekly/week-9.mdx +20 -0
- package/content/series/digital-garden/01-philosophy/index.mdx +23 -0
- package/content/series/digital-garden/01-philosophy.mdx +30 -0
- package/content/series/digital-garden/02-architecture.mdx +19 -0
- package/content/series/digital-garden/index.mdx +11 -0
- package/content/series/markdown-showcase/index.mdx +11 -0
- package/content/series/markdown-showcase/mathematical-notation.mdx +32 -0
- package/content/series/markdown-showcase/syntax-highlighting.mdx +119 -0
- package/content/series/markdown-showcase/visuals-and-diagrams.mdx +27 -0
- package/content/series/nextjs-deep-dive/01-getting-started.mdx +66 -0
- package/content/series/nextjs-deep-dive/02-routing-mastery/assets/diagram.svg +8 -0
- package/content/series/nextjs-deep-dive/02-routing-mastery/assets/m-p-model.png +0 -0
- package/content/series/nextjs-deep-dive/02-routing-mastery/index.mdx +138 -0
- package/content/series/nextjs-deep-dive/index.mdx +12 -0
- package/docs/ARCHITECTURE.md +103 -0
- package/docs/CONTRIBUTING.md +86 -0
- package/docs/deployment.md +319 -0
- package/eslint.config.mjs +18 -0
- package/next.config.ts +25 -0
- package/package.json +81 -0
- package/postcss.config.mjs +7 -0
- package/public/file.svg +1 -0
- package/public/globe.svg +1 -0
- package/public/icon.svg +9 -0
- package/public/logo.svg +11 -0
- package/public/next-image-export-optimizer-hashes.json +7 -0
- package/public/next.svg +1 -0
- package/public/screenshot.png +0 -0
- package/public/vercel.svg +1 -0
- package/public/window.svg +1 -0
- package/scripts/copy-assets.ts +211 -0
- package/scripts/new-flow.ts +47 -0
- package/scripts/new-from-images.ts +141 -0
- package/scripts/new-from-pdf.ts +105 -0
- package/scripts/new-post.ts +98 -0
- package/scripts/new-series.ts +40 -0
- package/scripts/series-draft.ts +136 -0
- package/site.config.ts +91 -0
- package/src/app/[slug]/page.tsx +67 -0
- package/src/app/archive/page.tsx +147 -0
- package/src/app/authors/[author]/page.tsx +210 -0
- package/src/app/books/[slug]/[chapter]/page.tsx +54 -0
- package/src/app/books/[slug]/page.tsx +156 -0
- package/src/app/books/page.tsx +63 -0
- package/src/app/favicon.ico +0 -0
- package/src/app/feed.xml/route.ts +44 -0
- package/src/app/flows/[year]/[month]/[day]/page.tsx +105 -0
- package/src/app/flows/[year]/[month]/page.tsx +72 -0
- package/src/app/flows/[year]/page.tsx +82 -0
- package/src/app/flows/page/[page]/page.tsx +63 -0
- package/src/app/flows/page.tsx +38 -0
- package/src/app/globals.css +406 -0
- package/src/app/layout.tsx +114 -0
- package/src/app/page/[page]/page.tsx +60 -0
- package/src/app/page.tsx +110 -0
- package/src/app/posts/[slug]/page.tsx +119 -0
- package/src/app/posts/page/[page]/page.tsx +58 -0
- package/src/app/posts/page.tsx +40 -0
- package/src/app/search.json/route.ts +49 -0
- package/src/app/series/[slug]/page/[page]/page.tsx +141 -0
- package/src/app/series/[slug]/page.tsx +139 -0
- package/src/app/series/page.tsx +96 -0
- package/src/app/sitemap.ts +112 -0
- package/src/app/tags/[tag]/page.tsx +76 -0
- package/src/app/tags/page.tsx +37 -0
- package/src/components/Analytics.tsx +49 -0
- package/src/components/AuthorStats.tsx +34 -0
- package/src/components/BookMobileNav.tsx +171 -0
- package/src/components/BookSidebar.tsx +275 -0
- package/src/components/CodeBlock.tsx +110 -0
- package/src/components/Comments.tsx +63 -0
- package/src/components/CoverImage.tsx +93 -0
- package/src/components/CuratedSeriesSection.tsx +124 -0
- package/src/components/ExternalLinks.tsx +45 -0
- package/src/components/FeaturedStoriesSection.tsx +106 -0
- package/src/components/FlowCalendarSidebar.tsx +249 -0
- package/src/components/FlowContent.tsx +96 -0
- package/src/components/FlowTimelineEntry.tsx +34 -0
- package/src/components/Footer.tsx +104 -0
- package/src/components/Hero.tsx +126 -0
- package/src/components/HorizontalScroll.tsx +128 -0
- package/src/components/LanguageProvider.tsx +80 -0
- package/src/components/LanguageSwitch.tsx +17 -0
- package/src/components/LatestWritingSection.tsx +45 -0
- package/src/components/MarkdownRenderer.tsx +135 -0
- package/src/components/Mermaid.tsx +89 -0
- package/src/components/Navbar.tsx +243 -0
- package/src/components/PageHeader.tsx +39 -0
- package/src/components/Pagination.tsx +120 -0
- package/src/components/PostCard.tsx +30 -0
- package/src/components/PostList.tsx +104 -0
- package/src/components/PostSidebar.tsx +225 -0
- package/src/components/ReadingProgressBar.tsx +37 -0
- package/src/components/RecentNotesSection.tsx +56 -0
- package/src/components/RelatedPosts.tsx +34 -0
- package/src/components/Search.tsx +151 -0
- package/src/components/SelectedBooksSection.tsx +80 -0
- package/src/components/SeriesCatalog.tsx +112 -0
- package/src/components/SeriesList.tsx +167 -0
- package/src/components/SeriesSidebar.tsx +132 -0
- package/src/components/SimpleLayoutHeader.tsx +38 -0
- package/src/components/Skeleton.tsx +131 -0
- package/src/components/TableOfContents.tsx +158 -0
- package/src/components/Tag.tsx +47 -0
- package/src/components/TagPageHeader.tsx +38 -0
- package/src/components/ThemeProvider.tsx +12 -0
- package/src/components/ThemeToggle.tsx +68 -0
- package/src/components/TranslatedText.tsx +13 -0
- package/src/fonts/Inter-Bold.woff2 +0 -0
- package/src/fonts/Inter-Regular.woff2 +0 -0
- package/src/fonts/LibreBaskerville-Bold.ttf +0 -0
- package/src/fonts/LibreBaskerville-Italic.ttf +0 -0
- package/src/fonts/LibreBaskerville-Regular.ttf +0 -0
- package/src/i18n/translations.ts +135 -0
- package/src/layouts/BookLayout.tsx +109 -0
- package/src/layouts/PostLayout.tsx +118 -0
- package/src/layouts/SimpleLayout.tsx +31 -0
- package/src/lib/i18n.ts +35 -0
- package/src/lib/markdown.test.ts +127 -0
- package/src/lib/markdown.ts +1067 -0
- package/src/lib/rehype-image-metadata.ts +54 -0
- package/src/lib/shuffle.ts +11 -0
- package/templates/default.mdx +13 -0
- package/tests/e2e/navigation.test.ts +51 -0
- package/tests/e2e/series-routes.test.ts +63 -0
- package/tests/e2e/smoke.test.ts +19 -0
- package/tests/integration/markdown-features.test.ts +54 -0
- package/tests/integration/posts.test.ts +57 -0
- package/tests/integration/reading-time-headings.test.ts +79 -0
- package/tests/integration/series-draft.test.ts +46 -0
- package/tests/integration/series.test.ts +79 -0
- package/tests/tooling/new-from-images.test.ts +173 -0
- package/tests/tooling/new-post.test.ts +72 -0
- package/tsconfig.json +34 -0
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
|
|
4
|
+
const args = process.argv.slice(2);
|
|
5
|
+
const valuedFlags = ['--template', '--prefix', '--series'];
|
|
6
|
+
const title = args.filter(arg => !arg.startsWith('--') && !valuedFlags.includes(args[args.indexOf(arg) - 1]))[0];
|
|
7
|
+
const templateArgIndex = args.indexOf('--template');
|
|
8
|
+
const templateName = templateArgIndex > -1 ? args[templateArgIndex + 1] : 'default';
|
|
9
|
+
const prefixArgIndex = args.indexOf('--prefix');
|
|
10
|
+
const prefix = prefixArgIndex > -1 ? args[prefixArgIndex + 1] : '';
|
|
11
|
+
const seriesArgIndex = args.indexOf('--series');
|
|
12
|
+
const series = seriesArgIndex > -1 ? args[seriesArgIndex + 1] : '';
|
|
13
|
+
const useFolder = args.includes('--folder');
|
|
14
|
+
const useMd = args.includes('--md');
|
|
15
|
+
|
|
16
|
+
if (!title) {
|
|
17
|
+
console.error('Please provide a post title.');
|
|
18
|
+
console.error('Usage: bun new <title> [--template <name>] [--prefix <name>] [--series <slug>] [--folder] [--md]');
|
|
19
|
+
process.exit(1);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const slug = title
|
|
23
|
+
.toLowerCase()
|
|
24
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
25
|
+
.replace(/(^-|-$)+/g, '');
|
|
26
|
+
|
|
27
|
+
const date = new Date().toISOString().split('T')[0];
|
|
28
|
+
const ext = useMd ? '.md' : '.mdx';
|
|
29
|
+
const prefixedSlug = prefix ? `${prefix}-${slug}` : slug;
|
|
30
|
+
let targetPath = '';
|
|
31
|
+
|
|
32
|
+
if (series) {
|
|
33
|
+
// Series posts go into content/series/<slug>/ without date prefix
|
|
34
|
+
const seriesDir = path.join(process.cwd(), 'content', 'series', series);
|
|
35
|
+
if (!fs.existsSync(seriesDir)) {
|
|
36
|
+
console.error(`Error: Series directory "${series}" does not exist at ${seriesDir}`);
|
|
37
|
+
process.exit(1);
|
|
38
|
+
}
|
|
39
|
+
if (useFolder) {
|
|
40
|
+
const dirPath = path.join(seriesDir, prefixedSlug);
|
|
41
|
+
if (!fs.existsSync(dirPath)) {
|
|
42
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
|
43
|
+
}
|
|
44
|
+
fs.mkdirSync(path.join(dirPath, 'images'), { recursive: true });
|
|
45
|
+
targetPath = path.join(dirPath, `index${ext}`);
|
|
46
|
+
} else {
|
|
47
|
+
targetPath = path.join(seriesDir, `${prefixedSlug}${ext}`);
|
|
48
|
+
}
|
|
49
|
+
} else if (useFolder) {
|
|
50
|
+
const dirName = `${date}-${prefixedSlug}`;
|
|
51
|
+
const dirPath = path.join(process.cwd(), 'content', 'posts', dirName);
|
|
52
|
+
if (!fs.existsSync(dirPath)) {
|
|
53
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
|
54
|
+
}
|
|
55
|
+
fs.mkdirSync(path.join(dirPath, 'images'), { recursive: true });
|
|
56
|
+
targetPath = path.join(dirPath, `index${ext}`);
|
|
57
|
+
} else {
|
|
58
|
+
const filename = `${date}-${prefixedSlug}${ext}`;
|
|
59
|
+
targetPath = path.join(process.cwd(), 'content', 'posts', filename);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const templatePath = path.join(process.cwd(), 'templates', `${templateName}${ext}`);
|
|
63
|
+
// Fallback to .mdx template if .md specific template doesn't exist
|
|
64
|
+
const fallbackTemplatePath = path.join(process.cwd(), 'templates', `${templateName}.mdx`);
|
|
65
|
+
|
|
66
|
+
let content = '';
|
|
67
|
+
|
|
68
|
+
if (fs.existsSync(templatePath)) {
|
|
69
|
+
content = fs.readFileSync(templatePath, 'utf8');
|
|
70
|
+
} else if (fs.existsSync(fallbackTemplatePath)) {
|
|
71
|
+
content = fs.readFileSync(fallbackTemplatePath, 'utf8');
|
|
72
|
+
} else {
|
|
73
|
+
// Fallback default template if file not found
|
|
74
|
+
content = `---
|
|
75
|
+
title: "{{title}}"
|
|
76
|
+
date: "{{date}}"
|
|
77
|
+
excerpt: ""
|
|
78
|
+
category: "Uncategorized"
|
|
79
|
+
tags: []
|
|
80
|
+
authors: ["Amytis"]
|
|
81
|
+
layout: "post"
|
|
82
|
+
draft: false
|
|
83
|
+
latex: false
|
|
84
|
+
---
|
|
85
|
+
|
|
86
|
+
Write your content here...
|
|
87
|
+
`;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
content = content.replace(/{{title}}/g, title).replace(/{{date}}/g, date);
|
|
91
|
+
|
|
92
|
+
if (fs.existsSync(targetPath)) {
|
|
93
|
+
console.error(`Error: Post already exists at ${targetPath}`);
|
|
94
|
+
process.exit(1);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
fs.writeFileSync(targetPath, content);
|
|
98
|
+
console.log(`Created new post: ${targetPath}`);
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import GithubSlugger from 'github-slugger';
|
|
4
|
+
|
|
5
|
+
const args = process.argv.slice(2);
|
|
6
|
+
const title = args[0];
|
|
7
|
+
|
|
8
|
+
if (!title) {
|
|
9
|
+
console.error('Please provide a series title.');
|
|
10
|
+
console.error('Usage: bun run new-series "My Series Name"');
|
|
11
|
+
process.exit(1);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const slugger = new GithubSlugger();
|
|
15
|
+
const slug = slugger.slug(title);
|
|
16
|
+
const seriesDir = path.join(process.cwd(), 'content', 'series', slug);
|
|
17
|
+
|
|
18
|
+
if (fs.existsSync(seriesDir)) {
|
|
19
|
+
console.error(`Series "${slug}" already exists.`);
|
|
20
|
+
process.exit(1);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
fs.mkdirSync(seriesDir, { recursive: true });
|
|
24
|
+
fs.mkdirSync(path.join(seriesDir, 'images'));
|
|
25
|
+
|
|
26
|
+
const date = new Date().toISOString().split('T')[0];
|
|
27
|
+
|
|
28
|
+
const content = `---
|
|
29
|
+
title: "${title}"
|
|
30
|
+
excerpt: "A description for ${title}."
|
|
31
|
+
date: "${date}"
|
|
32
|
+
coverImage: "https://images.unsplash.com/photo-1618005182384-a83a8bd57fbe?auto=format&fit=crop&w=800&q=80"
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
Welcome to the ${title} series.
|
|
36
|
+
`;
|
|
37
|
+
|
|
38
|
+
fs.writeFileSync(path.join(seriesDir, 'index.mdx'), content);
|
|
39
|
+
|
|
40
|
+
console.log(`Created new series at content/series/${slug}/index.mdx`);
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import matter from 'gray-matter';
|
|
4
|
+
|
|
5
|
+
const args = process.argv.slice(2);
|
|
6
|
+
const seriesSlug = args.find(arg => !arg.startsWith('--'));
|
|
7
|
+
const undraft = args.includes('--undraft');
|
|
8
|
+
|
|
9
|
+
if (!seriesSlug) {
|
|
10
|
+
console.error('Please provide a series slug.');
|
|
11
|
+
console.error('Usage: bun run series-draft <series-slug> [--undraft]');
|
|
12
|
+
console.error('');
|
|
13
|
+
console.error('Options:');
|
|
14
|
+
console.error(' --undraft Remove draft status instead of setting it');
|
|
15
|
+
process.exit(1);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const contentDir = path.join(process.cwd(), 'content', 'posts');
|
|
19
|
+
const seriesDir = path.join(process.cwd(), 'content', 'series', seriesSlug);
|
|
20
|
+
|
|
21
|
+
// Check if series exists
|
|
22
|
+
if (!fs.existsSync(seriesDir)) {
|
|
23
|
+
console.error(`Error: Series "${seriesSlug}" not found at ${seriesDir}`);
|
|
24
|
+
process.exit(1);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Read series metadata to get manual posts list
|
|
28
|
+
let manualPosts: string[] = [];
|
|
29
|
+
const seriesIndexMdx = path.join(seriesDir, 'index.mdx');
|
|
30
|
+
const seriesIndexMd = path.join(seriesDir, 'index.md');
|
|
31
|
+
let seriesIndexPath = '';
|
|
32
|
+
|
|
33
|
+
if (fs.existsSync(seriesIndexMdx)) {
|
|
34
|
+
seriesIndexPath = seriesIndexMdx;
|
|
35
|
+
} else if (fs.existsSync(seriesIndexMd)) {
|
|
36
|
+
seriesIndexPath = seriesIndexMd;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (seriesIndexPath) {
|
|
40
|
+
const seriesContent = fs.readFileSync(seriesIndexPath, 'utf8');
|
|
41
|
+
const { data } = matter(seriesContent);
|
|
42
|
+
if (data.posts && Array.isArray(data.posts)) {
|
|
43
|
+
manualPosts = data.posts;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Find all post files that belong to this series
|
|
48
|
+
const postFiles: { path: string; slug: string }[] = [];
|
|
49
|
+
|
|
50
|
+
// 1. Check posts in series folder
|
|
51
|
+
if (fs.existsSync(seriesDir)) {
|
|
52
|
+
const items = fs.readdirSync(seriesDir, { withFileTypes: true });
|
|
53
|
+
for (const item of items) {
|
|
54
|
+
if (item.name === 'index.mdx' || item.name === 'index.md') continue;
|
|
55
|
+
|
|
56
|
+
if (item.isFile() && (item.name.endsWith('.mdx') || item.name.endsWith('.md'))) {
|
|
57
|
+
const slug = item.name.replace(/\.mdx?$/, '');
|
|
58
|
+
postFiles.push({ path: path.join(seriesDir, item.name), slug });
|
|
59
|
+
} else if (item.isDirectory()) {
|
|
60
|
+
const indexMdx = path.join(seriesDir, item.name, 'index.mdx');
|
|
61
|
+
const indexMd = path.join(seriesDir, item.name, 'index.md');
|
|
62
|
+
if (fs.existsSync(indexMdx)) {
|
|
63
|
+
postFiles.push({ path: indexMdx, slug: item.name });
|
|
64
|
+
} else if (fs.existsSync(indexMd)) {
|
|
65
|
+
postFiles.push({ path: indexMd, slug: item.name });
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// 2. Check posts in content/posts with series frontmatter or in manual list
|
|
72
|
+
if (fs.existsSync(contentDir)) {
|
|
73
|
+
const items = fs.readdirSync(contentDir, { withFileTypes: true });
|
|
74
|
+
for (const item of items) {
|
|
75
|
+
let filePath = '';
|
|
76
|
+
let slug = '';
|
|
77
|
+
|
|
78
|
+
if (item.isFile() && (item.name.endsWith('.mdx') || item.name.endsWith('.md'))) {
|
|
79
|
+
filePath = path.join(contentDir, item.name);
|
|
80
|
+
slug = item.name.replace(/\.mdx?$/, '').replace(/^\d{4}-\d{2}-\d{2}-/, '');
|
|
81
|
+
} else if (item.isDirectory()) {
|
|
82
|
+
const indexMdx = path.join(contentDir, item.name, 'index.mdx');
|
|
83
|
+
const indexMd = path.join(contentDir, item.name, 'index.md');
|
|
84
|
+
if (fs.existsSync(indexMdx)) {
|
|
85
|
+
filePath = indexMdx;
|
|
86
|
+
} else if (fs.existsSync(indexMd)) {
|
|
87
|
+
filePath = indexMd;
|
|
88
|
+
}
|
|
89
|
+
slug = item.name.replace(/^\d{4}-\d{2}-\d{2}-/, '');
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (!filePath) continue;
|
|
93
|
+
|
|
94
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
95
|
+
const { data } = matter(content);
|
|
96
|
+
|
|
97
|
+
// Include if series matches or slug is in manual posts list
|
|
98
|
+
if (data.series === seriesSlug || manualPosts.includes(slug)) {
|
|
99
|
+
// Avoid duplicates
|
|
100
|
+
if (!postFiles.find(p => p.path === filePath)) {
|
|
101
|
+
postFiles.push({ path: filePath, slug });
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (postFiles.length === 0) {
|
|
108
|
+
console.log(`No posts found in series "${seriesSlug}".`);
|
|
109
|
+
process.exit(0);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
console.log(`Found ${postFiles.length} post(s) in series "${seriesSlug}":`);
|
|
113
|
+
|
|
114
|
+
let updated = 0;
|
|
115
|
+
for (const { path: filePath, slug } of postFiles) {
|
|
116
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
117
|
+
const { data, content: body } = matter(content);
|
|
118
|
+
|
|
119
|
+
const currentDraft = data.draft === true;
|
|
120
|
+
const targetDraft = !undraft;
|
|
121
|
+
|
|
122
|
+
if (currentDraft === targetDraft) {
|
|
123
|
+
console.log(` [skip] ${slug} - already ${targetDraft ? 'draft' : 'published'}`);
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
data.draft = targetDraft;
|
|
128
|
+
|
|
129
|
+
const newContent = matter.stringify(body, data);
|
|
130
|
+
fs.writeFileSync(filePath, newContent);
|
|
131
|
+
|
|
132
|
+
console.log(` [${targetDraft ? 'draft' : 'publish'}] ${slug}`);
|
|
133
|
+
updated++;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
console.log(`\nUpdated ${updated} post(s).`);
|
package/site.config.ts
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
export const siteConfig = {
|
|
2
|
+
title: { en: "Amytis", zh: "Amytis" },
|
|
3
|
+
description: { en: "A minimalist digital garden for growing thoughts and sharing knowledge.", zh: "一个极简的数字花园,用于培育思想和分享知识。" },
|
|
4
|
+
baseUrl: "https://example.com", // Replace with your actual domain
|
|
5
|
+
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",
|
|
19
|
+
},
|
|
20
|
+
series: {
|
|
21
|
+
navbar: ["digital-garden", "markdown-showcase", "ai-nexus-weekly"], // Slugs of series to show in navbar
|
|
22
|
+
},
|
|
23
|
+
books: {
|
|
24
|
+
navbar: [] as string[], // Slugs of books to show in navbar dropdown
|
|
25
|
+
},
|
|
26
|
+
archive: {
|
|
27
|
+
showAuthors: true,
|
|
28
|
+
},
|
|
29
|
+
pagination: {
|
|
30
|
+
posts: 5,
|
|
31
|
+
series: 1,
|
|
32
|
+
flows: 20,
|
|
33
|
+
},
|
|
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'
|
|
39
|
+
hero: {
|
|
40
|
+
tagline: { en: "Digital Garden", zh: "数字花园" },
|
|
41
|
+
title: { en: "Cultivating Digital Knowledge", zh: "培育数字知识" },
|
|
42
|
+
subtitle: { en: "A minimalist digital garden for growing thoughts and sharing knowledge.", zh: "一个极简的数字花园,用于培育思想和分享知识。" },
|
|
43
|
+
},
|
|
44
|
+
about: {
|
|
45
|
+
title: { en: "About Amytis", zh: "关于 Amytis" },
|
|
46
|
+
subtitle: { en: "Learn more about the philosophy and technology behind this digital garden.", zh: "了解这座数字花园背后的理念与技术。" },
|
|
47
|
+
},
|
|
48
|
+
flows: {
|
|
49
|
+
recentCount: 5,
|
|
50
|
+
},
|
|
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,
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
i18n: {
|
|
62
|
+
defaultLocale: 'en',
|
|
63
|
+
locales: ['en', 'zh'],
|
|
64
|
+
},
|
|
65
|
+
analytics: {
|
|
66
|
+
provider: 'umami', // 'umami' | 'plausible' | 'google' | null
|
|
67
|
+
umami: {
|
|
68
|
+
websiteId: process.env.NEXT_PUBLIC_UMAMI_WEBSITE_ID || '', // Your Umami Website ID
|
|
69
|
+
src: process.env.NEXT_PUBLIC_UMAMI_URL || 'https://us.umami.is/script.js', // Default or self-hosted URL
|
|
70
|
+
},
|
|
71
|
+
plausible: {
|
|
72
|
+
domain: '', // Your domain
|
|
73
|
+
src: 'https://plausible.io/js/script.js',
|
|
74
|
+
},
|
|
75
|
+
google: {
|
|
76
|
+
measurementId: '', // G-XXXXXXXXXX
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
comments: {
|
|
80
|
+
provider: 'giscus', // 'giscus' | 'disqus' | null
|
|
81
|
+
giscus: {
|
|
82
|
+
repo: 'hutusi/amytis', // username/repo
|
|
83
|
+
repoId: 'R_kgDOQ1YSwA',
|
|
84
|
+
category: 'Announcements',
|
|
85
|
+
categoryId: 'DIC_kwDOQ1YSwM4C2NmL',
|
|
86
|
+
},
|
|
87
|
+
disqus: {
|
|
88
|
+
shortname: '',
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
};
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { getPageBySlug, getAllPages } from '@/lib/markdown';
|
|
2
|
+
import { notFound } from 'next/navigation';
|
|
3
|
+
import PostLayout from '@/layouts/PostLayout';
|
|
4
|
+
import SimpleLayout from '@/layouts/SimpleLayout';
|
|
5
|
+
import { Metadata } from 'next';
|
|
6
|
+
import { siteConfig } from '../../../site.config';
|
|
7
|
+
import { resolveLocale } from '@/lib/i18n';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Generates the static paths for all top-level pages at build time.
|
|
11
|
+
*/
|
|
12
|
+
export async function generateStaticParams() {
|
|
13
|
+
const pages = getAllPages();
|
|
14
|
+
return pages.map((page) => ({
|
|
15
|
+
slug: page.slug,
|
|
16
|
+
}));
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const dynamicParams = false;
|
|
20
|
+
|
|
21
|
+
export async function generateMetadata({ params }: { params: Promise<{ slug: string }> }): Promise<Metadata> {
|
|
22
|
+
const { slug: rawSlug } = await params;
|
|
23
|
+
const slug = decodeURIComponent(rawSlug);
|
|
24
|
+
const page = getPageBySlug(slug);
|
|
25
|
+
|
|
26
|
+
if (!page) {
|
|
27
|
+
return { title: 'Page Not Found' };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return {
|
|
31
|
+
title: `${page.title} | ${resolveLocale(siteConfig.title)}`,
|
|
32
|
+
description: page.excerpt,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export default async function Page({
|
|
37
|
+
params,
|
|
38
|
+
}: {
|
|
39
|
+
params: Promise<{ slug: string }>;
|
|
40
|
+
}) {
|
|
41
|
+
const { slug: rawSlug } = await params;
|
|
42
|
+
const slug = decodeURIComponent(rawSlug);
|
|
43
|
+
const page = getPageBySlug(slug);
|
|
44
|
+
|
|
45
|
+
if (!page) {
|
|
46
|
+
notFound();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Determine layout based on frontmatter, defaulting to 'simple' for pages
|
|
50
|
+
const layout = page.layout || 'simple';
|
|
51
|
+
|
|
52
|
+
if (layout === 'post') {
|
|
53
|
+
return <PostLayout post={page} />;
|
|
54
|
+
}
|
|
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
|
+
return <SimpleLayout post={page} />;
|
|
67
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import Link from 'next/link';
|
|
2
|
+
import { getAllPosts, PostData } from '@/lib/markdown';
|
|
3
|
+
import { siteConfig } from '../../../site.config';
|
|
4
|
+
import { resolveLocale } from '@/lib/i18n';
|
|
5
|
+
import PageHeader from '@/components/PageHeader';
|
|
6
|
+
|
|
7
|
+
export const metadata = {
|
|
8
|
+
title: `Archive | ${resolveLocale(siteConfig.title)}`,
|
|
9
|
+
description: 'A complete list of all notes and thoughts.',
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
// Use month number as key for reliable sorting across all locales
|
|
13
|
+
type GroupedPosts = Record<string, Record<string, PostData[]>>;
|
|
14
|
+
|
|
15
|
+
const locale = siteConfig.i18n.defaultLocale === 'zh' ? 'zh-CN' : siteConfig.i18n.defaultLocale;
|
|
16
|
+
|
|
17
|
+
function getMonthName(monthNum: number): string {
|
|
18
|
+
return new Date(2000, monthNum - 1).toLocaleString(locale, { month: 'long' });
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function groupPostsByDate(posts: PostData[]): GroupedPosts {
|
|
22
|
+
const groups: GroupedPosts = {};
|
|
23
|
+
|
|
24
|
+
posts.forEach((post) => {
|
|
25
|
+
const date = new Date(post.date);
|
|
26
|
+
const year = date.getFullYear().toString();
|
|
27
|
+
const month = (date.getMonth() + 1).toString();
|
|
28
|
+
|
|
29
|
+
if (!groups[year]) {
|
|
30
|
+
groups[year] = {};
|
|
31
|
+
}
|
|
32
|
+
if (!groups[year][month]) {
|
|
33
|
+
groups[year][month] = [];
|
|
34
|
+
}
|
|
35
|
+
groups[year][month].push(post);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
return groups;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export default function ArchivePage() {
|
|
42
|
+
const posts = getAllPosts();
|
|
43
|
+
const groupedPosts = groupPostsByDate(posts);
|
|
44
|
+
const showAuthors = siteConfig.archive?.showAuthors;
|
|
45
|
+
|
|
46
|
+
// Sort years descending to show newest content first
|
|
47
|
+
const years = Object.keys(groupedPosts).sort((a, b) => Number(b) - Number(a));
|
|
48
|
+
const totalPosts = posts.length;
|
|
49
|
+
|
|
50
|
+
return (
|
|
51
|
+
<div className="layout-main">
|
|
52
|
+
<PageHeader
|
|
53
|
+
titleKey="archive"
|
|
54
|
+
subtitleKey="archive_subtitle"
|
|
55
|
+
subtitleOneKey="archive_subtitle_one"
|
|
56
|
+
count={years.length}
|
|
57
|
+
subtitleParams={{ count: totalPosts, years: years.length }}
|
|
58
|
+
/>
|
|
59
|
+
|
|
60
|
+
<main className="max-w-4xl mx-auto">
|
|
61
|
+
<div className="space-y-24">
|
|
62
|
+
{years.map((year) => {
|
|
63
|
+
// Sort months within the year in descending order (December -> January)
|
|
64
|
+
const months = Object.keys(groupedPosts[year]).sort((a, b) => Number(b) - Number(a));
|
|
65
|
+
|
|
66
|
+
// Calculate total posts for the year
|
|
67
|
+
const yearTotal = months.reduce((total, month) => total + groupedPosts[year][month].length, 0);
|
|
68
|
+
|
|
69
|
+
return (
|
|
70
|
+
<section key={year} className="relative grid grid-cols-1 md:grid-cols-[200px_1fr] gap-8 md:gap-16">
|
|
71
|
+
{/* Year Marker */}
|
|
72
|
+
<div className="relative">
|
|
73
|
+
<div className="sticky top-24 lg:top-32 text-left md:text-right">
|
|
74
|
+
<h2 className="text-4xl md:text-5xl font-serif font-bold text-muted/50">
|
|
75
|
+
{year}
|
|
76
|
+
</h2>
|
|
77
|
+
<span className="block text-xs font-bold uppercase tracking-widest text-muted mt-2">
|
|
78
|
+
{yearTotal} Posts
|
|
79
|
+
</span>
|
|
80
|
+
</div>
|
|
81
|
+
</div>
|
|
82
|
+
|
|
83
|
+
{/* Content Timeline */}
|
|
84
|
+
<div className="relative border-l-2 border-muted/20 pl-8 md:pl-12 space-y-16">
|
|
85
|
+
{months.map((month) => {
|
|
86
|
+
const monthPosts = groupedPosts[year][month];
|
|
87
|
+
return (
|
|
88
|
+
<div key={month} className="relative">
|
|
89
|
+
{/* Month Marker - positioned relative to border */}
|
|
90
|
+
<div className="absolute -left-[calc(2rem+1px)] md:-left-[calc(3rem+1px)] -translate-x-1/2 top-1.5 w-3 h-3 rounded-full bg-background border-2 border-accent/50"></div>
|
|
91
|
+
|
|
92
|
+
<h3 className="text-base font-sans font-bold uppercase tracking-widest text-accent mb-8">
|
|
93
|
+
{getMonthName(Number(month))}
|
|
94
|
+
<span className="ml-2 text-xs font-normal text-muted/60">({monthPosts.length})</span>
|
|
95
|
+
</h3>
|
|
96
|
+
|
|
97
|
+
<ul className="space-y-6">
|
|
98
|
+
{monthPosts.map((post) => {
|
|
99
|
+
const dateObj = new Date(post.date);
|
|
100
|
+
const day = dateObj.getDate().toString().padStart(2, '0');
|
|
101
|
+
|
|
102
|
+
return (
|
|
103
|
+
<li key={post.slug} className="group">
|
|
104
|
+
<Link href={`/posts/${post.slug}`} className="block no-underline">
|
|
105
|
+
<div className="flex flex-col sm:flex-row sm:items-baseline justify-between gap-2 sm:gap-6">
|
|
106
|
+
<div className="flex items-baseline gap-6">
|
|
107
|
+
<span className="font-mono text-base text-muted shrink-0 w-8">
|
|
108
|
+
{day}
|
|
109
|
+
</span>
|
|
110
|
+
<div className="flex items-baseline gap-2">
|
|
111
|
+
<h4 className="text-xl font-serif font-medium text-heading/80 group-hover:text-accent transition-colors duration-200">
|
|
112
|
+
{post.title}
|
|
113
|
+
</h4>
|
|
114
|
+
{post.series && (
|
|
115
|
+
<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"
|
|
118
|
+
>
|
|
119
|
+
{post.series}
|
|
120
|
+
</span>
|
|
121
|
+
)}
|
|
122
|
+
</div>
|
|
123
|
+
</div>
|
|
124
|
+
{showAuthors && post.authors.length > 0 && (
|
|
125
|
+
<span className="text-sm font-sans italic text-muted/60 shrink-0 hidden sm:block">
|
|
126
|
+
{post.authors.join(', ')}
|
|
127
|
+
</span>
|
|
128
|
+
)}
|
|
129
|
+
</div>
|
|
130
|
+
</Link>
|
|
131
|
+
</li>
|
|
132
|
+
);
|
|
133
|
+
})}
|
|
134
|
+
</ul>
|
|
135
|
+
</div>
|
|
136
|
+
);
|
|
137
|
+
})}
|
|
138
|
+
</div>
|
|
139
|
+
</section>
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
})}
|
|
143
|
+
</div>
|
|
144
|
+
</main>
|
|
145
|
+
</div>
|
|
146
|
+
);
|
|
147
|
+
}
|