Fix PPR empty generateStaticParams error and update dependencies

- Add placeholder fallback to generateStaticParams for cacheComponents compatibility
- Update npm packages within semver range (next 16.1.6, react 19.2.4, shiki 3.22.0, etc.)
- Add /new-post skill for blog publishing workflow
- Update CLAUDE.md with git remote mirroring docs

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-06 14:39:17 +08:00
parent 9c7f2463aa
commit 661b67cc01
6 changed files with 848 additions and 916 deletions

View File

@@ -0,0 +1,63 @@
# New Blog Post
Create and publish a new blog post for the personal blog.
## Step 1: Gather Information
Ask the user for the following using AskUserQuestion (all in one prompt):
1. **Title** — the article title (Chinese or English)
2. **Tags** — offer existing tags from past posts as multi-select options: `Medicine - 醫學`, `Writings - 創作`, `Hardware - 硬體`, `Software - 軟體`, `Unboxing - 開箱`. User can also input custom tags.
3. **Feature image** — options: no image, provide a URL or Unsplash link, or provide a local file path
4. **Description** — short excerpt for SEO/article list, or skip for now
## Step 2: Create the Post File
Create the markdown file at `content/posts/<title>.md` with this frontmatter format:
```yaml
---
title: <title>
slug: <english-slug-derived-from-title>
published_at: '<current-ISO-date>'
description: <description if provided>
tags:
- <tag1>
- <tag2>
authors:
- Gbanyan
feature_image: ../assets/<slug>.jpg
---
```
If the user provides article content, add it after the frontmatter.
## Step 3: Handle Feature Image
If the user provides an Unsplash URL:
1. Extract the real image URL by running: `curl -sL "<unsplash-page-url>" | grep -oE 'https://images\.unsplash\.com/photo-[^"? ]+' | head -1`
2. Download at 1920px width: `curl -sL -o content/assets/<slug>.jpg "<image-url>?w=1920&q=90"`
3. Optimize with jpegoptim: `jpegoptim --max=85 --strip-all --all-progressive content/assets/<slug>.jpg`
4. Verify the image visually using the Read tool
If the user provides a local file path, copy it to `content/assets/<slug>.jpg` and optimize.
If no image, omit `feature_image` from frontmatter.
## Step 4: Preview (Optional)
Ask the user if they want to preview with `npm run dev` before publishing.
## Step 5: Publish
Execute the two-step deployment:
```bash
# 1. Commit and push content submodule
git -C content add . && git -C content commit -m "Add new post: <title>" && git -C content push
# 2. Update main repo submodule pointer and push (triggers CI/CD)
git add content && git commit -m "Update content submodule" && git push
```
Confirm both pushes succeeded. The CI/CD pipeline on git.gbanyan.net will handle deployment automatically (and crontab mirrors to gitea.gbanyan.net).

View File

@@ -56,7 +56,7 @@ The `content/` directory is a git submodule pointing to a separate `personal-blo
## Deployment ## Deployment
Push to `main` on the Gitea remote (`git.gbanyan.net`) triggers CI/CD automatically (server-side hook). No Dockerfile or workflow file in this repo. Two Git remotes are involved: `git.gbanyan.net` (SSH, primary push target) and `gitea.gbanyan.net` (HTTPS, Gitea web UI). A crontab on the server automatically mirrors `git.gbanyan.net``gitea.gbanyan.net`. Push to `main` on `git.gbanyan.net` triggers CI/CD automatically (server-side hook). No Dockerfile or workflow file in this repo.
**Content-only update** (new/edited posts) — both steps are required to trigger deploy: **Content-only update** (new/edited posts) — both steps are required to trigger deploy:
1. Commit and push inside `content/` submodule: `git -C content add . && git -C content commit -m "..." && git -C content push` 1. Commit and push inside `content/` submodule: `git -C content add . && git -C content commit -m "..." && git -C content push`

View File

@@ -15,9 +15,10 @@ import { FooterCue } from '@/components/footer-cue';
import { JsonLd } from '@/components/json-ld'; import { JsonLd } from '@/components/json-ld';
export function generateStaticParams() { export function generateStaticParams() {
return allPosts.map((post) => ({ const params = allPosts.map((post) => ({
slug: post.slug || post.flattenedPath slug: post.slug || post.flattenedPath
})); }));
return params.length > 0 ? params : [{ slug: '__placeholder__' }];
} }
interface Props { interface Props {

View File

@@ -12,9 +12,10 @@ import { SectionDivider } from '@/components/section-divider';
import { JsonLd } from '@/components/json-ld'; import { JsonLd } from '@/components/json-ld';
export function generateStaticParams() { export function generateStaticParams() {
return allPages.map((page) => ({ const params = allPages.map((page) => ({
slug: page.slug || page.flattenedPath slug: page.slug || page.flattenedPath
})); }));
return params.length > 0 ? params : [{ slug: '__placeholder__' }];
} }
interface Props { interface Props {

View File

@@ -15,9 +15,10 @@ export function generateStaticParams() {
slugs.add(getTagSlug(tag)); slugs.add(getTagSlug(tag));
} }
} }
return Array.from(slugs).map((slug) => ({ const params = Array.from(slugs).map((slug) => ({
tag: slug tag: slug
})); }));
return params.length > 0 ? params : [{ tag: '__placeholder__' }];
} }
interface Props { interface Props {

1690
package-lock.json generated

File diff suppressed because it is too large Load Diff