@hutusi/amytis 1.12.0 → 1.14.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.
- package/CHANGELOG.md +29 -0
- package/GEMINI.md +9 -1
- package/README.md +26 -17
- package/README.zh.md +180 -100
- package/bun.lock +78 -74
- package/content/books/notes-on-thinking/cost-of-certainty.mdx +9 -0
- package/content/books/notes-on-thinking/index.mdx +16 -0
- package/content/books/notes-on-thinking/mental-models.mdx +9 -0
- package/content/books/the-pragmatic-writer/finding-your-voice.mdx +9 -0
- package/content/books/the-pragmatic-writer/index.mdx +18 -0
- package/content/books/the-pragmatic-writer/the-editing-loop.mdx +9 -0
- package/content/books/the-pragmatic-writer/why-writing-matters.mdx +9 -0
- package/content/flows/2026/03/01.md +9 -0
- package/content/flows/2026/03/03.md +9 -0
- package/content/flows/2026/03/05.md +10 -0
- package/content/flows/2026/03/07.md +11 -0
- package/content/posts/images/vibrant-waves.jpg +0 -0
- package/content/posts/welcome-to-amytis.mdx +3 -0
- package/content/series/markdown-showcase/index.mdx +2 -1
- package/content/series/markdown-showcase/mathematical-notation.mdx +8 -4
- package/content/series/markdown-showcase/syntax-highlighting.mdx +9 -5
- package/content/series/markdown-showcase/visuals-and-diagrams.mdx +8 -4
- package/content/{posts → series/markdown-showcase}//344/270/255/346/226/207/346/265/213/350/257/225/346/226/207/347/253/240.mdx +12 -7
- package/content/series/modern-web-dev/index.mdx +4 -2
- package/docs/ARCHITECTURE.md +8 -1
- package/docs/DIGITAL_GARDEN.md +22 -1
- package/package.json +12 -12
- package/public/next-image-export-optimizer-hashes.json +3 -2
- package/scripts/new-flow.ts +1 -0
- package/site.config.example.ts +3 -4
- package/site.config.ts +6 -7
- package/src/app/[slug]/[postSlug]/page.tsx +19 -2
- package/src/app/[slug]/page/[page]/page.tsx +26 -5
- package/src/app/[slug]/page.tsx +28 -8
- package/src/app/all.atom/route.ts +7 -0
- package/src/app/all.xml/route.ts +7 -0
- package/src/app/archive/page.tsx +7 -4
- package/src/app/feed.atom/route.ts +2 -57
- package/src/app/feed.xml/route.ts +2 -64
- package/src/app/flows/[year]/[month]/[day]/page.tsx +13 -0
- package/src/app/flows/feed.atom/route.ts +7 -0
- package/src/app/flows/feed.xml/route.ts +7 -0
- package/src/app/page.tsx +1 -2
- package/src/app/posts/[slug]/page.tsx +28 -9
- package/src/app/posts/feed.atom/route.ts +9 -0
- package/src/app/posts/feed.xml/route.ts +9 -0
- package/src/app/series/[slug]/page.tsx +46 -4
- package/src/components/CuratedSeriesSection.tsx +7 -11
- package/src/components/FeaturedStoriesSection.tsx +1 -1
- package/src/components/FlowCalendarSidebar.tsx +1 -1
- package/src/components/FlowContent.tsx +2 -1
- package/src/components/FlowTimelineEntry.tsx +7 -1
- package/src/components/Footer.tsx +6 -6
- package/src/components/HorizontalScroll.tsx +5 -14
- package/src/components/MarkdownRenderer.test.tsx +6 -0
- package/src/components/MarkdownRenderer.tsx +18 -16
- package/src/components/Navbar.tsx +1 -1
- package/src/components/PostList.tsx +20 -36
- package/src/components/PostSidebar.tsx +1 -1
- package/src/components/RecentNotesSection.tsx +4 -0
- package/src/components/SelectedBooksSection.tsx +65 -25
- package/src/components/SeriesCatalog.tsx +9 -7
- package/src/i18n/translations.ts +2 -0
- package/src/layouts/PostLayout.tsx +1 -1
- package/src/layouts/SimpleLayout.tsx +3 -3
- package/src/lib/feed-utils.ts +158 -18
- package/src/lib/markdown.ts +26 -5
- package/src/lib/urls.ts +9 -4
- package/tests/e2e/mobile/mobile-compat.spec.ts +58 -0
- package/tests/e2e/navigation.test.ts +26 -0
- package/tests/integration/collections.test.ts +17 -2
- package/tests/integration/feed-utils.test.ts +52 -0
- package/tests/integration/flow-title.test.ts +53 -0
- package/tests/integration/markdown-features.test.ts +3 -3
- package/tests/integration/reading-time-headings.test.ts +2 -2
- package/tests/unit/static-params.test.ts +155 -22
- package/tests/unit/urls.test.ts +10 -12
- /package/content/posts/{multilingual-test.mdx → multilingual-test-/344/270/255/346/226/207/351/225/277/346/240/207/351/242/230.mdx"} +0 -0
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: 'JSDoc type comments'
|
|
3
|
+
tags: ["typescript", "tooling"]
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
Finally moved a mid-sized project from JSDoc type comments to proper TypeScript. The migration took about half a day — less than expected.
|
|
7
|
+
|
|
8
|
+
The biggest win is not autocompletion or error catching (though those matter). It is that the types act as documentation that stays in sync with the code. Prose comments lie; types don't compile if they do.
|
|
9
|
+
|
|
10
|
+
One friction point: `strict: true` surfaces a lot of implicit `any` that was quietly hiding real assumptions. Fixing each one forced me to think about what the code actually expected — which is the point.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
---
|
|
2
|
+
tags: ["ai", "workflow"]
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# Using Claude Code
|
|
6
|
+
|
|
7
|
+
Been using Claude Code for a week now as a daily coding assistant. A few observations so far.
|
|
8
|
+
|
|
9
|
+
It is most useful for tasks where the shape of the solution is clear but the execution is tedious — refactoring, writing tests, tracking down why a CSS rule isn't applying. For genuinely novel design problems, talking through the approach matters more than generating code.
|
|
10
|
+
|
|
11
|
+
The most important habit: read every diff before accepting it. Not because the code is wrong (it usually isn't), but because understanding what changed keeps you in the loop on your own codebase.
|
|
Binary file
|
|
@@ -5,12 +5,15 @@ excerpt: "The first seeds of our digital garden."
|
|
|
5
5
|
category: "General"
|
|
6
6
|
tags: ["introduction", "digital garden", "nextjs"]
|
|
7
7
|
author: "Amytis Team"
|
|
8
|
+
coverImage: "images/vibrant-waves.jpg"
|
|
8
9
|
---
|
|
9
10
|
|
|
10
11
|
# Welcome to Amytis
|
|
11
12
|
|
|
12
13
|
This is your new digital garden. Built with **Next.js**, **Tailwind CSS**, and **Bun**.
|
|
13
14
|
|
|
15
|
+

|
|
16
|
+
|
|
14
17
|
## Why Amytis?
|
|
15
18
|
|
|
16
19
|
Amytis of Media was the wife of Nebuchadnezzar II, for whom the Hanging Gardens of Babylon were reportedly built. This digital garden is a space for thoughts to grow and flourish.
|
|
@@ -5,7 +5,8 @@ date: "2026-02-13"
|
|
|
5
5
|
coverImage: "/images/lake.jpg"
|
|
6
6
|
featured: true
|
|
7
7
|
sort: "manual"
|
|
8
|
-
|
|
8
|
+
redirectFrom:
|
|
9
|
+
- /series/markdown-showcase-old
|
|
9
10
|
---
|
|
10
11
|
|
|
11
12
|
Amytis supports a wide range of MDX features. This series serves as both a reference and a demonstration.
|
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
---
|
|
2
|
-
title:
|
|
3
|
-
date:
|
|
4
|
-
category:
|
|
5
|
-
tags:
|
|
2
|
+
title: Mathematical Notation
|
|
3
|
+
date: '2026-02-14'
|
|
4
|
+
category: Science
|
|
5
|
+
tags:
|
|
6
|
+
- math
|
|
7
|
+
- latex
|
|
6
8
|
latex: true
|
|
9
|
+
redirectFrom:
|
|
10
|
+
- /posts/mathematical-notation-demo
|
|
7
11
|
---
|
|
8
12
|
|
|
9
13
|
Amytis uses `rehype-katex` to render beautiful mathematical formulas.
|
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
---
|
|
2
|
-
title:
|
|
3
|
-
date:
|
|
4
|
-
category:
|
|
5
|
-
tags:
|
|
2
|
+
title: Syntax Highlighting
|
|
3
|
+
date: '2026-02-15'
|
|
4
|
+
category: Engineering
|
|
5
|
+
tags:
|
|
6
|
+
- code
|
|
7
|
+
- typescript
|
|
6
8
|
featured: true
|
|
7
|
-
coverImage:
|
|
9
|
+
coverImage: /images/vibrant-waves.jpg
|
|
10
|
+
redirectFrom:
|
|
11
|
+
- /posts/syntax-highlighting
|
|
8
12
|
---
|
|
9
13
|
|
|
10
14
|
Amytis provides beautiful syntax highlighting for dozens of programming languages. Here are a few examples within the series context.
|
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
---
|
|
2
|
-
title:
|
|
3
|
-
date:
|
|
4
|
-
category:
|
|
5
|
-
tags:
|
|
2
|
+
title: Visuals and Diagrams
|
|
3
|
+
date: '2026-02-13'
|
|
4
|
+
category: Design
|
|
5
|
+
tags:
|
|
6
|
+
- visuals
|
|
7
|
+
- mermaid
|
|
8
|
+
redirectFrom:
|
|
9
|
+
- /posts/visuals-and-diagrams
|
|
6
10
|
---
|
|
7
11
|
|
|
8
12
|
Visualizing information is key to understanding. Amytis integrates Mermaid.js for diagrams and handles images with ease.
|
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
---
|
|
2
|
-
title:
|
|
3
|
-
date:
|
|
4
|
-
excerpt:
|
|
5
|
-
category:
|
|
6
|
-
tags:
|
|
7
|
-
|
|
8
|
-
|
|
2
|
+
title: 中文测试文章
|
|
3
|
+
date: '2026-02-09'
|
|
4
|
+
excerpt: 这是一篇用于测试中文文件名和内容支持的文章。
|
|
5
|
+
category: 测试
|
|
6
|
+
tags:
|
|
7
|
+
- 中文
|
|
8
|
+
- 测试
|
|
9
|
+
- i18n
|
|
10
|
+
author: Amytis
|
|
11
|
+
coverImage: 'text:中文测试'
|
|
12
|
+
redirectFrom:
|
|
13
|
+
- /posts/中文测试文章
|
|
9
14
|
---
|
|
10
15
|
|
|
11
16
|
# 中文文件名支持测试
|
|
@@ -5,9 +5,11 @@ excerpt: "A curated path through modern web development: JavaScript fundamentals
|
|
|
5
5
|
date: "2026-03-01"
|
|
6
6
|
featured: true
|
|
7
7
|
items:
|
|
8
|
-
- post: asynchronous-javascript
|
|
9
|
-
- post: understanding-react-hooks
|
|
8
|
+
- post: posts/asynchronous-javascript
|
|
9
|
+
- post: posts/understanding-react-hooks
|
|
10
10
|
- series: nextjs-deep-dive
|
|
11
|
+
- post: markdown-showcase/中文测试文章
|
|
12
|
+
- post: digital-garden/02-architecture
|
|
11
13
|
---
|
|
12
14
|
|
|
13
15
|
This collection assembles the essential reading for anyone building modern web applications. Start with the JavaScript fundamentals that underpin everything, move into React patterns, then go deep on Next.js.
|
package/docs/ARCHITECTURE.md
CHANGED
|
@@ -57,7 +57,14 @@ src/app/
|
|
|
57
57
|
authors/[author]/page.tsx
|
|
58
58
|
archive/page.tsx
|
|
59
59
|
graph/page.tsx
|
|
60
|
-
feed.xml/route.ts
|
|
60
|
+
feed.xml/route.ts # Main curated RSS feed
|
|
61
|
+
feed.atom/route.ts # Main curated Atom feed
|
|
62
|
+
all.xml/route.ts # Firehose RSS feed
|
|
63
|
+
all.atom/route.ts # Firehose Atom feed
|
|
64
|
+
posts/feed.xml/route.ts # Posts-only RSS feed
|
|
65
|
+
posts/feed.atom/route.ts # Posts-only Atom feed
|
|
66
|
+
flows/feed.xml/route.ts # Flows-only RSS feed
|
|
67
|
+
flows/feed.atom/route.ts # Flows-only Atom feed
|
|
61
68
|
search.json/route.ts
|
|
62
69
|
sitemap.ts
|
|
63
70
|
[slug]/page.tsx # Static pages + custom series listing path
|
package/docs/DIGITAL_GARDEN.md
CHANGED
|
@@ -37,12 +37,33 @@ The `/graph` route visualizes your entire digital garden as an interactive netwo
|
|
|
37
37
|
- **Edges**: Represent wiki-links connecting them.
|
|
38
38
|
- **Interaction**: Click a node to navigate to that page.
|
|
39
39
|
|
|
40
|
-
### 5.
|
|
40
|
+
### 5. Collections (`/series`)
|
|
41
|
+
While a "Series" is typically a linear progression of posts (e.g., Part 1, Part 2), a **Collection** allows you to manually curate an arbitrary list of posts and other series into a single grouped page.
|
|
42
|
+
|
|
43
|
+
To create a collection, set `type: collection` in a series index file:
|
|
44
|
+
|
|
45
|
+
- **Location:** `content/series/[collection-slug]/index.mdx`
|
|
46
|
+
- **Frontmatter:**
|
|
47
|
+
```yaml
|
|
48
|
+
---
|
|
49
|
+
title: "Modern Web Development"
|
|
50
|
+
type: collection
|
|
51
|
+
items:
|
|
52
|
+
- post: posts/asynchronous-javascript # Namespaced reference to a standalone post
|
|
53
|
+
- post: ai-nexus-weekly/week-11 # Namespaced reference to a post inside another series
|
|
54
|
+
- post: understanding-react-hooks # Fallback reference (searches all posts by slug)
|
|
55
|
+
- series: nextjs-deep-dive # Embeds all posts from another series
|
|
56
|
+
---
|
|
57
|
+
```
|
|
58
|
+
Using a namespace (`folder/slug`) in the `post` definition prevents slug collisions and explicitly targets the exact file location.
|
|
59
|
+
|
|
60
|
+
### 6. Flows (`/flows`)
|
|
41
61
|
Flows are a stream-style collection of daily notes, micro-blogging, or imported chat logs. They are ideal for quick thoughts that don't necessarily warrant a full blog post.
|
|
42
62
|
|
|
43
63
|
- **Location:** `content/flows/YYYY/MM/DD.mdx`
|
|
44
64
|
- **Navigation:** Grouped by date in a timeline view.
|
|
45
65
|
- **Importing:** Use `bun run new-flow-from-chat` to bring in external conversations.
|
|
66
|
+
- **Optional Title:** Set `title` in frontmatter or use an `# Heading` in the content to display a title alongside the date.
|
|
46
67
|
|
|
47
68
|
## How to Use
|
|
48
69
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hutusi/amytis",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.14.0",
|
|
4
4
|
"description": "A high-performance digital garden and blog engine with Next.js 16 and Tailwind CSS v4",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -48,16 +48,16 @@
|
|
|
48
48
|
"github-slugger": "^2.0.0",
|
|
49
49
|
"gray-matter": "^4.0.3",
|
|
50
50
|
"image-size": "^2.0.2",
|
|
51
|
-
"katex": "^0.16.
|
|
52
|
-
"mermaid": "^11.
|
|
53
|
-
"next": "16.1
|
|
51
|
+
"katex": "^0.16.42",
|
|
52
|
+
"mermaid": "^11.13.0",
|
|
53
|
+
"next": "16.2.1",
|
|
54
54
|
"next-image-export-optimizer": "^1.20.1",
|
|
55
55
|
"next-themes": "^0.4.6",
|
|
56
56
|
"react": "19.2.4",
|
|
57
57
|
"react-dom": "19.2.4",
|
|
58
|
-
"react-icons": "^5.
|
|
58
|
+
"react-icons": "^5.6.0",
|
|
59
59
|
"react-markdown": "^10.1.0",
|
|
60
|
-
"react-syntax-highlighter": "^16.1.
|
|
60
|
+
"react-syntax-highlighter": "^16.1.1",
|
|
61
61
|
"rehype-katex": "^7.0.1",
|
|
62
62
|
"rehype-raw": "^7.0.0",
|
|
63
63
|
"rehype-slug": "^6.0.0",
|
|
@@ -72,22 +72,22 @@
|
|
|
72
72
|
},
|
|
73
73
|
"devDependencies": {
|
|
74
74
|
"@playwright/test": "^1.58.2",
|
|
75
|
-
"@tailwindcss/postcss": "^4.
|
|
76
|
-
"@types/bun": "^1.3.
|
|
75
|
+
"@tailwindcss/postcss": "^4.2.2",
|
|
76
|
+
"@types/bun": "^1.3.11",
|
|
77
77
|
"@types/d3": "^7.4.3",
|
|
78
78
|
"@types/hast": "^3.0.4",
|
|
79
79
|
"@types/image-size": "^0.8.0",
|
|
80
80
|
"@types/mdast": "^4.0.4",
|
|
81
|
-
"@types/node": "^24.
|
|
81
|
+
"@types/node": "^24.12.0",
|
|
82
82
|
"@types/react": "^19.2.14",
|
|
83
83
|
"@types/react-dom": "^19.2.3",
|
|
84
84
|
"@types/react-syntax-highlighter": "^15.5.13",
|
|
85
85
|
"babel-plugin-react-compiler": "1.0.0",
|
|
86
|
-
"eslint": "^9.
|
|
87
|
-
"eslint-config-next": "16.1
|
|
86
|
+
"eslint": "^9.39.4",
|
|
87
|
+
"eslint-config-next": "16.2.1",
|
|
88
88
|
"pagefind": "^1.4.0",
|
|
89
89
|
"pdf-to-img": "^5.0.0",
|
|
90
|
-
"tailwindcss": "^4.
|
|
90
|
+
"tailwindcss": "^4.2.2",
|
|
91
91
|
"typescript": "^5.9.3"
|
|
92
92
|
},
|
|
93
93
|
"ignoreScripts": [
|
|
@@ -68,7 +68,8 @@
|
|
|
68
68
|
"images/lake.jpg": "imAORQhxpmoU3jzKBMNFJuFSa0UgiSf2Dmea5Rj8-8M=",
|
|
69
69
|
"images/mountains.jpg": "FsrkZws9EKMqHCk1Hc6i6nEIcTRcrMBa4ddgqR6oRaI=",
|
|
70
70
|
"images/screenshot.png": "FAqbAgLRbWbYq9yJ4iggq2aKxRD8hdeDICc3DI14yhg=",
|
|
71
|
-
"images/vibrant-waves.
|
|
71
|
+
"images/vibrant-waves.jpg": "vdBm72ev5ETPM5H2CDK6tqph5t8N5nTCSApBJp2lW6U=",
|
|
72
72
|
"images/wechat-qr.jpg": "DNIzz0Wcl8WP0h-jHHgZ9LEvf3ZKOXHgxlvpw3gK2ME=",
|
|
73
|
-
"posts/02-routing-mastery/assets/m-p-model.png": "fDmvlEkZnE-UCvPK4gmDkJD7SU8coOTl4iw5hpsmcWI="
|
|
73
|
+
"posts/02-routing-mastery/assets/m-p-model.png": "fDmvlEkZnE-UCvPK4gmDkJD7SU8coOTl4iw5hpsmcWI=",
|
|
74
|
+
"posts/images/vibrant-waves.jpg": "YogfFJOm4PQV1g3iMRpCL1pKtjPXpMeJDFf6NTILcBQ="
|
|
74
75
|
}
|
package/scripts/new-flow.ts
CHANGED
package/site.config.example.ts
CHANGED
|
@@ -143,7 +143,7 @@ export const siteConfig = {
|
|
|
143
143
|
{ id: 'featured-posts', enabled: true, weight: 2, maxItems: 4 },
|
|
144
144
|
{ id: 'latest-posts', enabled: true, weight: 3, maxItems: 3 },
|
|
145
145
|
{ id: 'recent-flows', enabled: false, weight: 4, maxItems: 8 },
|
|
146
|
-
{ id: 'featured-series', enabled: true, weight: 5, maxItems: 6
|
|
146
|
+
{ id: 'featured-series', enabled: true, weight: 5, maxItems: 6 },
|
|
147
147
|
{ id: 'featured-books', enabled: false, weight: 6, maxItems: 4 },
|
|
148
148
|
],
|
|
149
149
|
},
|
|
@@ -177,10 +177,9 @@ export const siteConfig = {
|
|
|
177
177
|
},
|
|
178
178
|
series: {
|
|
179
179
|
// When true, posts in a series are served at /[series-slug]/[post-slug]
|
|
180
|
-
// instead of the default posts basePath. Defaults to
|
|
181
|
-
// existing deployments — enable explicitly and run `add-series-redirects` first.
|
|
180
|
+
// instead of the default posts basePath. Defaults to true.
|
|
182
181
|
// customPaths entries always take precedence over autoPaths.
|
|
183
|
-
autoPaths:
|
|
182
|
+
autoPaths: true,
|
|
184
183
|
// Per-series custom URL prefix for posts within that series.
|
|
185
184
|
// Overrides autoPaths for the specified series.
|
|
186
185
|
// e.g., { 'weeklies': 'weeklies' } → posts served at /weeklies/[slug]
|
package/site.config.ts
CHANGED
|
@@ -33,7 +33,7 @@ export const siteConfig = {
|
|
|
33
33
|
favicon: "/icon.svg",
|
|
34
34
|
},
|
|
35
35
|
description: { en: "Amytis — an elegant open-source framework for building your personal digital garden.", zh: "Amytis — 优雅的开源数字花园框架。" },
|
|
36
|
-
baseUrl: "https://
|
|
36
|
+
baseUrl: "https://amytis.vercel.app", // Replace with your actual domain
|
|
37
37
|
ogImage: "/og-image.png", // Default OG/social preview image — place a 1200×630 PNG at public/og-image.png
|
|
38
38
|
footerText: { en: `© ${new Date().getFullYear()} Amytis. All rights reserved.`, zh: `© ${new Date().getFullYear()} Amytis. 保留所有权利。` },
|
|
39
39
|
|
|
@@ -140,9 +140,9 @@ export const siteConfig = {
|
|
|
140
140
|
sections: [
|
|
141
141
|
{ id: 'hero', enabled: true, weight: 1 },
|
|
142
142
|
{ id: 'featured-posts', enabled: true, weight: 2, maxItems: 4 },
|
|
143
|
-
{ id: 'latest-posts', enabled: true, weight: 3, maxItems:
|
|
144
|
-
{ id: 'recent-flows', enabled: true, weight: 4, maxItems:
|
|
145
|
-
{ id: 'featured-series', enabled: true, weight: 5, maxItems: 6
|
|
143
|
+
{ id: 'latest-posts', enabled: true, weight: 3, maxItems: 4 },
|
|
144
|
+
{ id: 'recent-flows', enabled: true, weight: 4, maxItems: 7 },
|
|
145
|
+
{ id: 'featured-series', enabled: true, weight: 5, maxItems: 6 },
|
|
146
146
|
{ id: 'featured-books', enabled: true, weight: 6, maxItems: 4 },
|
|
147
147
|
],
|
|
148
148
|
},
|
|
@@ -176,10 +176,9 @@ export const siteConfig = {
|
|
|
176
176
|
},
|
|
177
177
|
series: {
|
|
178
178
|
// When true, posts in a series are served at /[series-slug]/[post-slug]
|
|
179
|
-
// instead of the default posts basePath. Defaults to
|
|
180
|
-
// existing deployments — enable explicitly and run `add-series-redirects` first.
|
|
179
|
+
// instead of the default posts basePath. Defaults to true.
|
|
181
180
|
// customPaths entries always take precedence over autoPaths.
|
|
182
|
-
autoPaths:
|
|
181
|
+
autoPaths: true,
|
|
183
182
|
// Per-series custom URL prefix for posts within that series.
|
|
184
183
|
// Overrides autoPaths for the specified series.
|
|
185
184
|
// e.g., { 'weeklies': 'weeklies' } → posts served at /weeklies/[slug]
|
|
@@ -49,7 +49,7 @@ export async function generateStaticParams() {
|
|
|
49
49
|
const pageSlugSet = getAllPages().map(p => p.slug);
|
|
50
50
|
validateSeriesAutoPaths(allSeriesSlugs, [...pageSlugSet, ...Object.values(customPaths)]); // Throws if any slug collides with a reserved route, static page, or customPaths prefix
|
|
51
51
|
for (const seriesSlug of allSeriesSlugs) {
|
|
52
|
-
if (seriesSlug
|
|
52
|
+
if (Object.hasOwn(customPaths, seriesSlug)) continue; // Already handled by customPaths above
|
|
53
53
|
allSeriesMap[seriesSlug].forEach(post => { params.push({ slug: seriesSlug, postSlug: post.slug }); });
|
|
54
54
|
}
|
|
55
55
|
}
|
|
@@ -61,10 +61,27 @@ export async function generateStaticParams() {
|
|
|
61
61
|
if (segments.length !== 2) continue;
|
|
62
62
|
const [fromPrefix, fromPostSlug] = segments;
|
|
63
63
|
if (from === getPostUrl(post)) continue; // skip if this is already the canonical path
|
|
64
|
+
// Skip /posts/* entries when basePath is 'posts' — handled by posts/[slug]/page.tsx instead
|
|
65
|
+
if (fromPrefix === 'posts' && basePath === 'posts') continue;
|
|
64
66
|
params.push({ slug: fromPrefix, postSlug: fromPostSlug });
|
|
65
67
|
}
|
|
66
68
|
}
|
|
67
69
|
|
|
70
|
+
// Work around Next dev static-param checks for percent-encoded Unicode postSlugs
|
|
71
|
+
// under `output: "export"` — dev server may receive percent-encoded forms of Unicode paths.
|
|
72
|
+
// Include encoded variants in development only; production export keeps raw segment values.
|
|
73
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
74
|
+
const existing = new Set(params.map(p => `${p.slug}/${p.postSlug}`));
|
|
75
|
+
for (const p of [...params]) {
|
|
76
|
+
const encodedPostSlug = encodeURIComponent(p.postSlug);
|
|
77
|
+
const key = `${p.slug}/${encodedPostSlug}`;
|
|
78
|
+
if (!existing.has(key)) {
|
|
79
|
+
existing.add(key);
|
|
80
|
+
params.push({ slug: p.slug, postSlug: encodedPostSlug });
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
68
85
|
// Placeholder keeps Next.js happy with output: export when no custom paths configured.
|
|
69
86
|
// dynamicParams = false ensures any unrecognised slug/postSlug combo returns 404.
|
|
70
87
|
return params.length > 0 ? params : [{ slug: '_', postSlug: '_' }];
|
|
@@ -153,7 +170,7 @@ export default async function PrefixPostPage({
|
|
|
153
170
|
const customPaths = getSeriesCustomPaths();
|
|
154
171
|
const isValidBasePath = prefix === basePath && basePath !== 'posts';
|
|
155
172
|
const matchedSeriesSlug = Object.entries(customPaths).find(([, path]) => path === prefix)?.[0];
|
|
156
|
-
const isAutoSeriesPath = getSeriesAutoPaths() && !(prefix
|
|
173
|
+
const isAutoSeriesPath = getSeriesAutoPaths() && !Object.hasOwn(customPaths, prefix) && getSeriesData(prefix) !== null;
|
|
157
174
|
const isLegacyRedirect = post.redirectFrom?.includes(currentPath) ?? false;
|
|
158
175
|
|
|
159
176
|
if (!isValidBasePath && !matchedSeriesSlug && !isAutoSeriesPath && !isLegacyRedirect) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { getListingPosts, getSeriesData, getSeriesPosts, getSeriesAuthors, getAuthorSlug } from '@/lib/markdown';
|
|
1
|
+
import { getListingPosts, getAllSeries, getSeriesData, getSeriesPosts, getSeriesAuthors, getAuthorSlug } from '@/lib/markdown';
|
|
2
2
|
import PostList from '@/components/PostList';
|
|
3
3
|
import SeriesCatalog from '@/components/SeriesCatalog';
|
|
4
4
|
import Pagination from '@/components/Pagination';
|
|
@@ -9,11 +9,18 @@ import { t, resolveLocale } from '@/lib/i18n';
|
|
|
9
9
|
import PageHeader from '@/components/PageHeader';
|
|
10
10
|
import CoverImage from '@/components/CoverImage';
|
|
11
11
|
import Link from 'next/link';
|
|
12
|
-
import { getPostsBasePath, getSeriesCustomPaths } from '@/lib/urls';
|
|
12
|
+
import { getPostsBasePath, getSeriesCustomPaths, getSeriesAutoPaths } from '@/lib/urls';
|
|
13
13
|
|
|
14
14
|
const POST_PAGE_SIZE = siteConfig.pagination.posts;
|
|
15
15
|
const SERIES_PAGE_SIZE = siteConfig.pagination.series;
|
|
16
16
|
|
|
17
|
+
function resolveSeriesSlug(prefix: string, customPaths: Record<string, string>): string | undefined {
|
|
18
|
+
return (
|
|
19
|
+
Object.entries(customPaths).find(([, path]) => path === prefix)?.[0] ??
|
|
20
|
+
(getSeriesAutoPaths() && !Object.hasOwn(customPaths, prefix) && getSeriesData(prefix) ? prefix : undefined)
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
|
|
17
24
|
export async function generateStaticParams() {
|
|
18
25
|
const params: { slug: string; page: string }[] = [];
|
|
19
26
|
|
|
@@ -28,7 +35,8 @@ export async function generateStaticParams() {
|
|
|
28
35
|
}
|
|
29
36
|
|
|
30
37
|
// Series custom paths — paginated series listing (page 2+)
|
|
31
|
-
|
|
38
|
+
const customPaths = getSeriesCustomPaths();
|
|
39
|
+
for (const [seriesSlug, customPath] of Object.entries(customPaths)) {
|
|
32
40
|
const posts = getSeriesPosts(seriesSlug);
|
|
33
41
|
const totalPages = Math.ceil(posts.length / SERIES_PAGE_SIZE);
|
|
34
42
|
for (let i = 2; i <= totalPages; i++) {
|
|
@@ -36,6 +44,19 @@ export async function generateStaticParams() {
|
|
|
36
44
|
}
|
|
37
45
|
}
|
|
38
46
|
|
|
47
|
+
// Series auto-paths — paginated series listing (page 2+)
|
|
48
|
+
const customPathValues = new Set(Object.values(customPaths));
|
|
49
|
+
if (getSeriesAutoPaths()) {
|
|
50
|
+
for (const [seriesSlug, posts] of Object.entries(getAllSeries())) {
|
|
51
|
+
if (Object.hasOwn(customPaths, seriesSlug)) continue; // series has its own customPaths key override — skip
|
|
52
|
+
if (customPathValues.has(seriesSlug)) continue; // slug collides with another series' custom path value — skip
|
|
53
|
+
const totalPages = Math.ceil(posts.length / SERIES_PAGE_SIZE);
|
|
54
|
+
for (let i = 2; i <= totalPages; i++) {
|
|
55
|
+
params.push({ slug: seriesSlug, page: i.toString() });
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
39
60
|
// Placeholder keeps Next.js happy with output: export when no custom paths configured.
|
|
40
61
|
// dynamicParams = false ensures any unrecognised slug/page combo returns 404.
|
|
41
62
|
return params.length > 0 ? params : [{ slug: '_', page: '2' }];
|
|
@@ -51,7 +72,7 @@ export async function generateMetadata({
|
|
|
51
72
|
const { slug: prefix, page } = await params;
|
|
52
73
|
const basePath = getPostsBasePath();
|
|
53
74
|
const customPaths = getSeriesCustomPaths();
|
|
54
|
-
const matchedSeriesSlug =
|
|
75
|
+
const matchedSeriesSlug = resolveSeriesSlug(prefix, customPaths);
|
|
55
76
|
|
|
56
77
|
if (prefix === basePath && basePath !== 'posts') {
|
|
57
78
|
return {
|
|
@@ -82,7 +103,7 @@ export default async function PrefixPageRoute({
|
|
|
82
103
|
|
|
83
104
|
const basePath = getPostsBasePath();
|
|
84
105
|
const customPaths = getSeriesCustomPaths();
|
|
85
|
-
const matchedSeriesSlug =
|
|
106
|
+
const matchedSeriesSlug = resolveSeriesSlug(prefix, customPaths);
|
|
86
107
|
|
|
87
108
|
// Custom posts basePath listing
|
|
88
109
|
if (prefix === basePath && basePath !== 'posts') {
|
package/src/app/[slug]/page.tsx
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { getPageBySlug, getAllPages, getAllPosts, getListingPosts, getSeriesData, getSeriesPosts, getSeriesAuthors, getAuthorSlug } from '@/lib/markdown';
|
|
1
|
+
import { getPageBySlug, getAllPages, getAllPosts, getAllSeries, getListingPosts, getSeriesData, getSeriesPosts, getSeriesAuthors, getAuthorSlug } from '@/lib/markdown';
|
|
2
2
|
import { notFound } from 'next/navigation';
|
|
3
3
|
import PostLayout from '@/layouts/PostLayout';
|
|
4
4
|
import SimpleLayout from '@/layouts/SimpleLayout';
|
|
@@ -11,12 +11,19 @@ import { Metadata } from 'next';
|
|
|
11
11
|
import { siteConfig } from '../../../site.config';
|
|
12
12
|
import { resolveLocale, t } from '@/lib/i18n';
|
|
13
13
|
import PageHeader from '@/components/PageHeader';
|
|
14
|
-
import { getPostsBasePath, getSeriesCustomPaths, getPostUrl } from '@/lib/urls';
|
|
14
|
+
import { getPostsBasePath, getSeriesCustomPaths, getSeriesAutoPaths, getPostUrl, RESERVED_ROUTE_SEGMENTS } from '@/lib/urls';
|
|
15
15
|
import RedirectPage from '@/components/RedirectPage';
|
|
16
16
|
|
|
17
17
|
const POST_PAGE_SIZE = siteConfig.pagination.posts;
|
|
18
18
|
const SERIES_PAGE_SIZE = siteConfig.pagination.series;
|
|
19
19
|
|
|
20
|
+
function resolveSeriesSlug(slug: string, customPaths: Record<string, string>): string | undefined {
|
|
21
|
+
return (
|
|
22
|
+
Object.entries(customPaths).find(([, path]) => path === slug)?.[0] ??
|
|
23
|
+
(getSeriesAutoPaths() && !Object.hasOwn(customPaths, slug) && getSeriesData(slug) ? slug : undefined)
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
|
|
20
27
|
/**
|
|
21
28
|
* Generates the static paths for all top-level pages at build time,
|
|
22
29
|
* plus any custom URL prefixes configured for posts or series.
|
|
@@ -32,19 +39,32 @@ export async function generateStaticParams() {
|
|
|
32
39
|
}
|
|
33
40
|
|
|
34
41
|
// Add series custom path listings (e.g. /weeklies)
|
|
35
|
-
|
|
42
|
+
const customPaths = getSeriesCustomPaths();
|
|
43
|
+
for (const customPath of Object.values(customPaths)) {
|
|
36
44
|
params.push({ slug: customPath });
|
|
37
45
|
}
|
|
38
46
|
|
|
47
|
+
// Add series auto-path listings (e.g. /my-series) when autoPaths is enabled
|
|
48
|
+
const customPathValues = new Set(Object.values(customPaths));
|
|
49
|
+
const autoPathSlugs: string[] = [];
|
|
50
|
+
if (getSeriesAutoPaths()) {
|
|
51
|
+
for (const seriesSlug of Object.keys(getAllSeries())) {
|
|
52
|
+
if (Object.hasOwn(customPaths, seriesSlug)) continue; // series has its own customPaths key override — skip
|
|
53
|
+
if (customPathValues.has(seriesSlug)) continue; // slug collides with another series' custom path value — skip
|
|
54
|
+
autoPathSlugs.push(seriesSlug);
|
|
55
|
+
params.push({ slug: seriesSlug });
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
39
59
|
// Add single-segment redirectFrom paths (e.g. /old-slug).
|
|
40
60
|
// Throws on any alias that collides with a reserved top-level slug or a
|
|
41
61
|
// duplicate alias across posts — strict build catches misconfiguration early.
|
|
42
62
|
const reservedSlugs = new Set([
|
|
43
63
|
...pages.map(p => p.slug),
|
|
44
64
|
basePath,
|
|
45
|
-
...Object.values(
|
|
46
|
-
|
|
47
|
-
|
|
65
|
+
...Object.values(customPaths),
|
|
66
|
+
...autoPathSlugs,
|
|
67
|
+
...RESERVED_ROUTE_SEGMENTS,
|
|
48
68
|
]);
|
|
49
69
|
for (const post of getAllPosts()) {
|
|
50
70
|
for (const from of post.redirectFrom ?? []) {
|
|
@@ -82,7 +102,7 @@ export async function generateMetadata({ params }: { params: Promise<{ slug: str
|
|
|
82
102
|
|
|
83
103
|
// Series custom paths
|
|
84
104
|
const customPaths = getSeriesCustomPaths();
|
|
85
|
-
const matchedSeriesSlug =
|
|
105
|
+
const matchedSeriesSlug = resolveSeriesSlug(slug, customPaths);
|
|
86
106
|
if (matchedSeriesSlug) {
|
|
87
107
|
const seriesData = getSeriesData(matchedSeriesSlug);
|
|
88
108
|
if (seriesData) {
|
|
@@ -145,7 +165,7 @@ export default async function Page({
|
|
|
145
165
|
|
|
146
166
|
// Check if slug matches a series custom path
|
|
147
167
|
const customPaths = getSeriesCustomPaths();
|
|
148
|
-
const matchedSeriesSlug =
|
|
168
|
+
const matchedSeriesSlug = resolveSeriesSlug(slug, customPaths);
|
|
149
169
|
if (matchedSeriesSlug) {
|
|
150
170
|
const seriesData = getSeriesData(matchedSeriesSlug);
|
|
151
171
|
const allPosts = getSeriesPosts(matchedSeriesSlug);
|
package/src/app/archive/page.tsx
CHANGED
|
@@ -128,12 +128,12 @@ export default function ArchivePage() {
|
|
|
128
128
|
<li key={post.slug} className="group">
|
|
129
129
|
<Link href={getPostUrl(post)} className="block no-underline">
|
|
130
130
|
<div className="flex flex-col sm:flex-row sm:items-baseline justify-between gap-2 sm:gap-6">
|
|
131
|
-
<div className="flex items-baseline gap-6">
|
|
131
|
+
<div className="flex items-baseline gap-6 min-w-0 flex-1">
|
|
132
132
|
<span className="font-mono text-base text-muted shrink-0 w-8">
|
|
133
133
|
{day}
|
|
134
134
|
</span>
|
|
135
|
-
<div className="flex items-baseline gap-2">
|
|
136
|
-
<h4 className="text-xl font-serif font-medium text-heading/80 group-hover:text-accent transition-colors duration-200">
|
|
135
|
+
<div className="flex items-baseline gap-2 min-w-0 flex-1">
|
|
136
|
+
<h4 className="text-xl font-serif font-medium text-heading/80 group-hover:text-accent transition-colors duration-200 truncate">
|
|
137
137
|
{post.title}
|
|
138
138
|
</h4>
|
|
139
139
|
{post.series && (
|
|
@@ -147,7 +147,10 @@ export default function ArchivePage() {
|
|
|
147
147
|
</div>
|
|
148
148
|
</div>
|
|
149
149
|
{showAuthors && post.authors.length > 0 && (
|
|
150
|
-
<span
|
|
150
|
+
<span
|
|
151
|
+
title={post.authors.join(', ')}
|
|
152
|
+
className="text-sm font-sans italic text-muted/60 hidden sm:block max-w-[12rem] md:max-w-[16rem] lg:max-w-[20rem] truncate text-right shrink-0"
|
|
153
|
+
>
|
|
151
154
|
{post.authors.join(', ')}
|
|
152
155
|
</span>
|
|
153
156
|
)}
|