Compare commits
5 Commits
33042cde79
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 1d4cfe773c | |||
| 1f7dbd80d6 | |||
| b005f02b7b | |||
| 4cdccb0276 | |||
| ddd0cc5795 |
@@ -50,14 +50,19 @@ Ask the user if they want to preview with `npm run dev` before publishing.
|
|||||||
|
|
||||||
## Step 5: Publish
|
## Step 5: Publish
|
||||||
|
|
||||||
Execute the two-step deployment:
|
**IMPORTANT**: The `.gitmodules` URL for `content/` points to GitHub. The CI/CD server clones the submodule from that URL, so the content submodule **must be pushed to GitHub first** before pushing the main repo. Otherwise the server will check out stale content and posts will disappear from the site.
|
||||||
|
|
||||||
|
Execute the deployment in order:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 1. Commit and push content submodule
|
# 1. Commit content submodule
|
||||||
git -C content add . && git -C content commit -m "Add new post: <title>" && git -C content push
|
git -C content add . && git -C content commit -m "Add new post: <title>"
|
||||||
|
|
||||||
# 2. Update main repo submodule pointer and push (triggers CI/CD)
|
# 2. Push content submodule to ALL remotes (GitHub first — CI/CD depends on it)
|
||||||
git add content && git commit -m "Update content submodule" && git push
|
git -C content push github main && git -C content push origin main
|
||||||
|
|
||||||
|
# 3. Update main repo submodule pointer, commit, and push to both remotes
|
||||||
|
git add content && git commit -m "Update content submodule" && git push origin main && git push github main
|
||||||
```
|
```
|
||||||
|
|
||||||
Confirm both pushes succeeded. The CI/CD pipeline on git.gbanyan.net will handle deployment automatically (and crontab mirrors to gitea.gbanyan.net).
|
Confirm all pushes succeeded. The CI/CD pipeline on git.gbanyan.net will handle deployment automatically (and crontab mirrors to gitea.gbanyan.net).
|
||||||
|
|||||||
@@ -1,6 +1,17 @@
|
|||||||
import { ImageResponse } from '@vercel/og';
|
import { ImageResponse } from '@vercel/og';
|
||||||
import { NextRequest } from 'next/server';
|
import { NextRequest } from 'next/server';
|
||||||
|
|
||||||
|
const fontCache = new Map<string, ArrayBuffer>();
|
||||||
|
|
||||||
|
async function loadFont(url: string): Promise<ArrayBuffer> {
|
||||||
|
const cached = fontCache.get(url);
|
||||||
|
if (cached) return cached;
|
||||||
|
const res = await fetch(url);
|
||||||
|
const data = await res.arrayBuffer();
|
||||||
|
fontCache.set(url, data);
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
export async function GET(request: NextRequest) {
|
export async function GET(request: NextRequest) {
|
||||||
try {
|
try {
|
||||||
const { searchParams } = new URL(request.url);
|
const { searchParams } = new URL(request.url);
|
||||||
@@ -10,6 +21,14 @@ export async function GET(request: NextRequest) {
|
|||||||
const description = searchParams.get('description') || '';
|
const description = searchParams.get('description') || '';
|
||||||
const tags = searchParams.get('tags')?.split(',').slice(0, 3) || [];
|
const tags = searchParams.get('tags')?.split(',').slice(0, 3) || [];
|
||||||
|
|
||||||
|
// Load CJK font for Chinese text rendering
|
||||||
|
const fontData = await loadFont(
|
||||||
|
'https://cdn.jsdelivr.net/fontsource/fonts/noto-sans-tc@latest/chinese-traditional-400-normal.woff'
|
||||||
|
);
|
||||||
|
const fontBoldData = await loadFont(
|
||||||
|
'https://cdn.jsdelivr.net/fontsource/fonts/noto-sans-tc@latest/chinese-traditional-700-normal.woff'
|
||||||
|
);
|
||||||
|
|
||||||
const imageResponse = new ImageResponse(
|
const imageResponse = new ImageResponse(
|
||||||
(
|
(
|
||||||
<div
|
<div
|
||||||
@@ -20,6 +39,7 @@ export async function GET(request: NextRequest) {
|
|||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
alignItems: 'flex-start',
|
alignItems: 'flex-start',
|
||||||
justifyContent: 'space-between',
|
justifyContent: 'space-between',
|
||||||
|
fontFamily: '"Noto Sans TC", sans-serif',
|
||||||
backgroundColor: '#0f172a',
|
backgroundColor: '#0f172a',
|
||||||
backgroundImage: 'radial-gradient(circle at 25px 25px, #1e293b 2%, transparent 0%), radial-gradient(circle at 75px 75px, #1e293b 2%, transparent 0%)',
|
backgroundImage: 'radial-gradient(circle at 25px 25px, #1e293b 2%, transparent 0%), radial-gradient(circle at 75px 75px, #1e293b 2%, transparent 0%)',
|
||||||
backgroundSize: '100px 100px',
|
backgroundSize: '100px 100px',
|
||||||
@@ -155,6 +175,10 @@ export async function GET(request: NextRequest) {
|
|||||||
{
|
{
|
||||||
width: 1200,
|
width: 1200,
|
||||||
height: 630,
|
height: 630,
|
||||||
|
fonts: [
|
||||||
|
{ name: 'Noto Sans TC', data: fontData, weight: 400 as const, style: 'normal' as const },
|
||||||
|
{ name: 'Noto Sans TC', data: fontBoldData, weight: 700 as const, style: 'normal' as const },
|
||||||
|
],
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import Image from 'next/image';
|
|||||||
import { notFound } from 'next/navigation';
|
import { notFound } from 'next/navigation';
|
||||||
import type { Metadata } from 'next';
|
import type { Metadata } from 'next';
|
||||||
import { allPosts } from 'contentlayer2/generated';
|
import { allPosts } from 'contentlayer2/generated';
|
||||||
import { getPostBySlug, getRelatedPosts, getPostNeighbors } from '@/lib/posts';
|
import { getPostBySlug, getRelatedPosts, getPostNeighbors, getTagSlug } from '@/lib/posts';
|
||||||
import { siteConfig } from '@/lib/config';
|
import { siteConfig } from '@/lib/config';
|
||||||
import { ReadingProgress } from '@/components/reading-progress';
|
import { ReadingProgress } from '@/components/reading-progress';
|
||||||
import { ScrollReveal } from '@/components/scroll-reveal';
|
import { ScrollReveal } from '@/components/scroll-reveal';
|
||||||
@@ -40,6 +40,11 @@ export async function generateMetadata({ params }: Props): Promise<Metadata> {
|
|||||||
ogImageUrl.searchParams.set('tags', post.tags.slice(0, 3).join(','));
|
ogImageUrl.searchParams.set('tags', post.tags.slice(0, 3).join(','));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Prefer post's feature_image for social cards; fall back to dynamic OG
|
||||||
|
const imageUrl = post.feature_image
|
||||||
|
? `${siteConfig.url}${post.feature_image.replace('../assets', '/assets')}`
|
||||||
|
: ogImageUrl.toString();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
title: post.title,
|
title: post.title,
|
||||||
description: post.description || post.title,
|
description: post.description || post.title,
|
||||||
@@ -61,7 +66,7 @@ export async function generateMetadata({ params }: Props): Promise<Metadata> {
|
|||||||
tags: post.tags,
|
tags: post.tags,
|
||||||
images: [
|
images: [
|
||||||
{
|
{
|
||||||
url: ogImageUrl.toString(),
|
url: imageUrl,
|
||||||
width: 1200,
|
width: 1200,
|
||||||
height: 630,
|
height: 630,
|
||||||
alt: post.title,
|
alt: post.title,
|
||||||
@@ -72,7 +77,7 @@ export async function generateMetadata({ params }: Props): Promise<Metadata> {
|
|||||||
card: 'summary_large_image',
|
card: 'summary_large_image',
|
||||||
title: post.title,
|
title: post.title,
|
||||||
description: post.description || post.title,
|
description: post.description || post.title,
|
||||||
images: [ogImageUrl.toString()],
|
images: [imageUrl],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -217,9 +222,7 @@ export default async function BlogPostPage({ params }: Props) {
|
|||||||
{post.tags.map((t) => (
|
{post.tags.map((t) => (
|
||||||
<Link
|
<Link
|
||||||
key={t}
|
key={t}
|
||||||
href={`/tags/${encodeURIComponent(
|
href={`/tags/${encodeURIComponent(getTagSlug(t))}`}
|
||||||
t.toLowerCase().replace(/\s+/g, '-')
|
|
||||||
)}`}
|
|
||||||
className="tag-chip rounded-full bg-accent-soft px-3 py-1 text-sm text-accent-textLight dark:bg-slate-800 dark:text-slate-100 dark:hover:bg-slate-700 dark:hover:text-white"
|
className="tag-chip rounded-full bg-accent-soft px-3 py-1 text-sm text-accent-textLight dark:bg-slate-800 dark:text-slate-100 dark:hover:bg-slate-700 dark:hover:text-white"
|
||||||
>
|
>
|
||||||
#{t}
|
#{t}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import Image from 'next/image';
|
|||||||
import { notFound } from 'next/navigation';
|
import { notFound } from 'next/navigation';
|
||||||
import type { Metadata } from 'next';
|
import type { Metadata } from 'next';
|
||||||
import { allPages } from 'contentlayer2/generated';
|
import { allPages } from 'contentlayer2/generated';
|
||||||
import { getPageBySlug } from '@/lib/posts';
|
import { getPageBySlug, getTagSlug } from '@/lib/posts';
|
||||||
import { siteConfig } from '@/lib/config';
|
import { siteConfig } from '@/lib/config';
|
||||||
import { ReadingProgress } from '@/components/reading-progress';
|
import { ReadingProgress } from '@/components/reading-progress';
|
||||||
import { PostLayout } from '@/components/post-layout';
|
import { PostLayout } from '@/components/post-layout';
|
||||||
@@ -130,9 +130,7 @@ export default async function StaticPage({ params }: Props) {
|
|||||||
{page.tags.map((t) => (
|
{page.tags.map((t) => (
|
||||||
<Link
|
<Link
|
||||||
key={t}
|
key={t}
|
||||||
href={`/tags/${encodeURIComponent(
|
href={`/tags/${encodeURIComponent(getTagSlug(t))}`}
|
||||||
t.toLowerCase().replace(/\s+/g, '-')
|
|
||||||
)}`}
|
|
||||||
className="tag-chip rounded-full bg-accent-soft px-3 py-1 text-sm text-accent-textLight dark:bg-slate-800 dark:text-slate-100"
|
className="tag-chip rounded-full bg-accent-soft px-3 py-1 text-sm text-accent-textLight dark:bg-slate-800 dark:text-slate-100"
|
||||||
>
|
>
|
||||||
#{t}
|
#{t}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { MetadataRoute } from 'next';
|
import { MetadataRoute } from 'next';
|
||||||
import { allPosts, allPages } from 'contentlayer2/generated';
|
import { allPosts, allPages } from 'contentlayer2/generated';
|
||||||
|
import { getTagSlug } from '@/lib/posts';
|
||||||
|
|
||||||
export default function sitemap(): MetadataRoute.Sitemap {
|
export default function sitemap(): MetadataRoute.Sitemap {
|
||||||
const siteUrl = process.env.NEXT_PUBLIC_SITE_URL || 'http://localhost:3000';
|
const siteUrl = process.env.NEXT_PUBLIC_SITE_URL || 'http://localhost:3000';
|
||||||
@@ -58,7 +59,7 @@ export default function sitemap(): MetadataRoute.Sitemap {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const tagPages = allTags.map((tag) => ({
|
const tagPages = allTags.map((tag) => ({
|
||||||
url: `${siteUrl}/tags/${encodeURIComponent(tag.toLowerCase().replace(/\s+/g, '-'))}`,
|
url: `${siteUrl}/tags/${encodeURIComponent(getTagSlug(tag))}`,
|
||||||
lastModified: new Date(),
|
lastModified: new Date(),
|
||||||
changeFrequency: 'weekly' as const,
|
changeFrequency: 'weekly' as const,
|
||||||
priority: 0.5,
|
priority: 0.5,
|
||||||
|
|||||||
2
content
2
content
Submodule content updated: 99cff93cca...7b52c564dc
2
next-env.d.ts
vendored
2
next-env.d.ts
vendored
@@ -1,6 +1,6 @@
|
|||||||
/// <reference types="next" />
|
/// <reference types="next" />
|
||||||
/// <reference types="next/image-types/global" />
|
/// <reference types="next/image-types/global" />
|
||||||
import "./.next/dev/types/routes.d.ts";
|
import "./.next/types/routes.d.ts";
|
||||||
|
|
||||||
// NOTE: This file should not be edited
|
// NOTE: This file should not be edited
|
||||||
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
||||||
|
|||||||
Reference in New Issue
Block a user