feat(ssr): 扩展 SSR 覆盖至 /page/:slug 与 /user/:id
- feedPaths 白名单加入自定义单页与用户主页(兼容 .html 伪静态)
- head.ts 新增 buildPageHead(ogType=article, WebPage JSON-LD)
与 buildUserHead(ogType=profile, Person JSON-LD)
- feedData 新增 loadPageData / loadUserData,并行取页面/用户
数据 + 壳层,404 走 buildNotFoundHead + noindex
- feedSeed 按 useSessionResource 的 key 约定 seed:
sitepage:${slug}、user:${id}、user:${id}:posts:1:${size},
使 SitePageView / UserProfilePage 首帧直接命中快照渲染
- MainLayout postOutline 首帧派生扩展 /page/:slug 分支,
文章目录随正文一同 SSR
- 验证:两页 SSR HTML 含完整内容与 head,0 hydration 错误,
hydration 后仅剩 pageview 请求;404 正确;已有页面无回归
This commit is contained in:
@@ -12,7 +12,7 @@ import { useAuth } from '../hooks/useAuth';
|
|||||||
import { useTheme, useMediaQuery } from '../hooks/useTheme';
|
import { useTheme, useMediaQuery } from '../hooks/useTheme';
|
||||||
import { useOverlayA11y, moveTabIndex } from '../hooks/useOverlayA11y';
|
import { useOverlayA11y, moveTabIndex } from '../hooks/useOverlayA11y';
|
||||||
import { api } from '../api/client';
|
import { api } from '../api/client';
|
||||||
import type { Board, RecentComment, RecentUser, ForumStats, TagCount, User, UserActivityStats, UserPublic } from '../api/types';
|
import type { Board, RecentComment, RecentUser, ForumStats, SitePage, TagCount, User, UserActivityStats, UserPublic } from '../api/types';
|
||||||
import { extractHeadingsFromHtml, type PostHeading } from '../utils/postHeadings';
|
import { extractHeadingsFromHtml, type PostHeading } from '../utils/postHeadings';
|
||||||
import { renderPostContentHtml } from '../utils/postContent';
|
import { renderPostContentHtml } from '../utils/postContent';
|
||||||
import { getSessionSnapshot } from '../utils/sessionPageCache';
|
import { getSessionSnapshot } from '../utils/sessionPageCache';
|
||||||
@@ -80,28 +80,49 @@ export default function MainLayout() {
|
|||||||
// 直接从中派生右栏作者与目录——子组件的 setPostOutline 在 effect 中,SSR 不执行,
|
// 直接从中派生右栏作者与目录——子组件的 setPostOutline 在 effect 中,SSR 不执行,
|
||||||
// 不走这一步作者卡/目录会整体退化为客户端挂载。首帧后的 SPA 导航仍由 effect 上报。
|
// 不走这一步作者卡/目录会整体退化为客户端挂载。首帧后的 SPA 导航仍由 effect 上报。
|
||||||
const [postOutline, setPostOutline] = useState<PostOutlineState>(() => {
|
const [postOutline, setPostOutline] = useState<PostOutlineState>(() => {
|
||||||
const m = loc.pathname.match(/^\/post\/(\d+)/);
|
const openLinks = getCachedForumLimits()?.open_content_links_in_new_tab;
|
||||||
if (!m || /\/edit$/.test(loc.pathname)) return null;
|
// 帖子详情:从 post 快照派生作者卡 + 目录
|
||||||
const id = Number(m[1]);
|
const postM = loc.pathname.match(/^\/post\/(\d+)/);
|
||||||
if (!id) return null;
|
if (postM && !/\/edit$/.test(loc.pathname)) {
|
||||||
const snap = getSessionSnapshot<PostDetailSnapshot>(postDetailCacheKey(id));
|
const id = Number(postM[1]);
|
||||||
if (!snap?.post) return null;
|
if (id) {
|
||||||
const content = snap.post.content ?? '';
|
const snap = getSessionSnapshot<PostDetailSnapshot>(postDetailCacheKey(id));
|
||||||
const rendered = content.trim()
|
if (snap?.post) {
|
||||||
? renderPostContentHtml(content, !!user, {
|
const content = snap.post.content ?? '';
|
||||||
openLinksInNewTab: getCachedForumLimits()?.open_content_links_in_new_tab,
|
const rendered = content.trim()
|
||||||
})
|
? renderPostContentHtml(content, !!user, { openLinksInNewTab: openLinks })
|
||||||
: '';
|
: '';
|
||||||
return {
|
return {
|
||||||
headings: extractHeadingsFromHtml(rendered),
|
headings: extractHeadingsFromHtml(rendered),
|
||||||
scrollRoot: null,
|
scrollRoot: null,
|
||||||
title: '文章目录',
|
title: '文章目录',
|
||||||
author: snap.post.user ?? null,
|
author: snap.post.user ?? null,
|
||||||
publishedAt: snap.post.created_at,
|
publishedAt: snap.post.created_at,
|
||||||
viewCount: snap.post.view_count,
|
viewCount: snap.post.view_count,
|
||||||
authorProfile: snap.authorProfile ?? null,
|
authorProfile: snap.authorProfile ?? null,
|
||||||
authorStats: snap.authorStats ?? null,
|
authorStats: snap.authorStats ?? null,
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 自定义单页:从 sitepage 快照派生目录(无作者卡)
|
||||||
|
const pageM = loc.pathname.match(/^\/page\/([^/]+?)(?:\.html?)?$/);
|
||||||
|
if (pageM) {
|
||||||
|
const slug = pageM[1];
|
||||||
|
const page = getSessionSnapshot<SitePage>(`sitepage:${slug}`);
|
||||||
|
if (page) {
|
||||||
|
const content = page.content ?? '';
|
||||||
|
const rendered = content.trim()
|
||||||
|
? renderPostContentHtml(content, !!user, { openLinksInNewTab: openLinks })
|
||||||
|
: '';
|
||||||
|
return {
|
||||||
|
headings: extractHeadingsFromHtml(rendered),
|
||||||
|
scrollRoot: null,
|
||||||
|
title: '文章目录',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
});
|
});
|
||||||
const [asideOpen, setAsideOpen] = useState(false);
|
const [asideOpen, setAsideOpen] = useState(false);
|
||||||
const [sidebarOpen, setSidebarOpen] = useState(false);
|
const [sidebarOpen, setSidebarOpen] = useState(false);
|
||||||
|
|||||||
@@ -10,11 +10,11 @@
|
|||||||
import { redirect, render } from 'vike/abort';
|
import { redirect, render } from 'vike/abort';
|
||||||
import type { PageContextServer } from 'vike/types';
|
import type { PageContextServer } from 'vike/types';
|
||||||
import { serverApi } from '../api/serverClient';
|
import { serverApi } from '../api/serverClient';
|
||||||
import type { Comment, PostDetailResponse, UserActivityStats, UserPublic } from '../api/types';
|
import type { Comment, PostDetailResponse, PostItem, SitePage, UserActivityStats, UserPublic } from '../api/types';
|
||||||
import type { HomeBootPayload } from '../utils/homeBoot';
|
import type { HomeBootPayload } from '../utils/homeBoot';
|
||||||
import { buildPostDetailSnapshot } from '../utils/homeBoot';
|
import { buildPostDetailSnapshot } from '../utils/homeBoot';
|
||||||
import { matchFeedSsrPath } from './feedPaths';
|
import { matchFeedSsrPath } from './feedPaths';
|
||||||
import { buildFeedHead, buildNotFoundHead, buildPostHead } from './head';
|
import { buildFeedHead, buildNotFoundHead, buildPageHead, buildPostHead, buildUserHead } from './head';
|
||||||
import type { FeedData } from './feedSeed';
|
import type { FeedData } from './feedSeed';
|
||||||
|
|
||||||
export type { FeedData };
|
export type { FeedData };
|
||||||
@@ -41,6 +41,12 @@ export async function loadFeedData(pageContext: PageContextServer): Promise<Feed
|
|||||||
if (match.kind === 'post') {
|
if (match.kind === 'post') {
|
||||||
return loadPostData(match.id, pathname, { cookie, apiHeaders });
|
return loadPostData(match.id, pathname, { cookie, apiHeaders });
|
||||||
}
|
}
|
||||||
|
if (match.kind === 'page') {
|
||||||
|
return loadPageData(match.id, pathname, { cookie, apiHeaders });
|
||||||
|
}
|
||||||
|
if (match.kind === 'user') {
|
||||||
|
return loadUserData(match.id, pathname, { cookie, apiHeaders });
|
||||||
|
}
|
||||||
return loadFeedListData(match.kind, match.id, pathname, searchParams, { cookie, apiHeaders });
|
return loadFeedListData(match.kind, match.id, pathname, searchParams, { cookie, apiHeaders });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -128,3 +134,84 @@ async function loadFeedListData(
|
|||||||
|
|
||||||
return { kind, boot, head: buildFeedHead(boot, base) };
|
return { kind, boot, head: buildFeedHead(boot, base) };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 自定义单页:page + 壳层并行取数 */
|
||||||
|
async function loadPageData(slug: string, pathname: string, api: ApiCtx): Promise<FeedData> {
|
||||||
|
if (!slug) {
|
||||||
|
throw render(404, { head: buildNotFoundHead(pathname) });
|
||||||
|
}
|
||||||
|
|
||||||
|
const [pageResp, boot] = await Promise.all([
|
||||||
|
serverApi<{ page: SitePage }>(`/api/pages/${encodeURIComponent(slug)}`, api),
|
||||||
|
serverApi<HomeBootPayload>('/api/home-boot', api),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const brand = boot?.branding ?? null;
|
||||||
|
const base = brand?.site_url?.trim() ?? '';
|
||||||
|
const page = pageResp?.page;
|
||||||
|
|
||||||
|
// 页面不存在或未发布:404 + noindex
|
||||||
|
if (!page || !page.published) {
|
||||||
|
throw render(404, {
|
||||||
|
head: buildNotFoundHead(pathname, brand, base),
|
||||||
|
boot: boot ?? undefined,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// slug 与页面组件 parsePermalinkSlug() 对齐(小写),保证 useSessionResource key 命中
|
||||||
|
return {
|
||||||
|
kind: 'page',
|
||||||
|
boot: boot ?? undefined,
|
||||||
|
sitePage: { slug: page.slug.toLowerCase(), page },
|
||||||
|
head: buildPageHead(page, brand, boot?.limits ?? null, base),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 用户主页:profile + 首屏帖子 + 壳层并行取数 */
|
||||||
|
async function loadUserData(id: string, pathname: string, api: ApiCtx): Promise<FeedData> {
|
||||||
|
const uid = /^\d+$/.test(id) ? Number(id) : NaN;
|
||||||
|
if (Number.isNaN(uid) || uid <= 0) {
|
||||||
|
throw render(404, { head: buildNotFoundHead(pathname) });
|
||||||
|
}
|
||||||
|
|
||||||
|
const [profileResp, boot] = await Promise.all([
|
||||||
|
serverApi<{ user: UserPublic; stats: UserActivityStats }>(`/api/users/${uid}`, api),
|
||||||
|
serverApi<HomeBootPayload>('/api/home-boot', api),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const brand = boot?.branding ?? null;
|
||||||
|
const base = brand?.site_url?.trim() ?? '';
|
||||||
|
const profile = profileResp?.user;
|
||||||
|
|
||||||
|
// 用户不存在:404 + noindex
|
||||||
|
if (!profile) {
|
||||||
|
throw render(404, {
|
||||||
|
head: buildNotFoundHead(pathname, brand, base),
|
||||||
|
boot: boot ?? undefined,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 首屏帖子列表(第 1 页,page_size 从 limits 取)
|
||||||
|
const limits = boot?.limits ?? null;
|
||||||
|
const pageSize = (limits?.page_size_default ?? 0) > 0 ? limits!.page_size_default : 20;
|
||||||
|
const postsResp = await serverApi<{ posts: PostItem[]; total: number }>(
|
||||||
|
`/api/posts?user_id=${uid}&page=1&size=${pageSize}&sort=latest`,
|
||||||
|
api,
|
||||||
|
);
|
||||||
|
const posts = Array.isArray(postsResp?.posts) ? postsResp.posts : [];
|
||||||
|
const postTotal = postsResp?.total ?? 0;
|
||||||
|
|
||||||
|
return {
|
||||||
|
kind: 'user',
|
||||||
|
boot: boot ?? undefined,
|
||||||
|
userProfile: {
|
||||||
|
id: uid,
|
||||||
|
profile,
|
||||||
|
stats: profileResp?.stats ?? null,
|
||||||
|
posts,
|
||||||
|
postTotal,
|
||||||
|
pageSize,
|
||||||
|
},
|
||||||
|
head: buildUserHead(profile, brand, limits, base),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,20 +1,25 @@
|
|||||||
/**
|
/**
|
||||||
* SSR feed 页路由匹配(Vike +route.ts 用)。
|
* SSR feed 页路由匹配(Vike +route.ts 用)。
|
||||||
*
|
*
|
||||||
* 真渲染白名单:首页 `/`、板块页 `/board/:id`、帖子详情 `/post/:id`
|
* 真渲染白名单:首页 `/`、板块页 `/board/:id`、帖子详情 `/post/:id`、
|
||||||
|
* 自定义单页 `/page/:slug`、用户主页 `/user/:id`
|
||||||
* (兼容 `123.html` 伪静态)。其余 URL 一律走 SPA 壳,后续扩展 SEO 页只改这里。
|
* (兼容 `123.html` 伪静态)。其余 URL 一律走 SPA 壳,后续扩展 SEO 页只改这里。
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export type FeedSsrMatch =
|
export type FeedSsrMatch =
|
||||||
| { kind: 'home'; id: '' }
|
| { kind: 'home'; id: '' }
|
||||||
| { kind: 'board'; id: string }
|
| { kind: 'board'; id: string }
|
||||||
| { kind: 'post'; id: string };
|
| { kind: 'post'; id: string }
|
||||||
|
| { kind: 'page'; id: string }
|
||||||
|
| { kind: 'user'; id: string };
|
||||||
|
|
||||||
const FEED_PATTERNS: Array<{ re: RegExp; pick: (m: RegExpMatchArray) => FeedSsrMatch }> = [
|
const FEED_PATTERNS: Array<{ re: RegExp; pick: (m: RegExpMatchArray) => FeedSsrMatch }> = [
|
||||||
{ re: /^\/$/, pick: () => ({ kind: 'home', id: '' }) },
|
{ re: /^\/$/, pick: () => ({ kind: 'home', id: '' }) },
|
||||||
{ re: /^\/board\/([^/]+)$/, pick: (m) => ({ kind: 'board', id: m[1] }) },
|
{ re: /^\/board\/([^/]+)$/, pick: (m) => ({ kind: 'board', id: m[1] }) },
|
||||||
// 123 与 123.html(伪静态后缀由后台配置,两种形态都接住)
|
// 123 与 123.html(伪静态后缀由后台配置,两种形态都接住)
|
||||||
{ re: /^\/post\/([^/]+?)(?:\.html?)?$/, pick: (m) => ({ kind: 'post', id: m[1] }) },
|
{ re: /^\/post\/([^/]+?)(?:\.html?)?$/, pick: (m) => ({ kind: 'post', id: m[1] }) },
|
||||||
|
{ re: /^\/page\/([^/]+?)(?:\.html?)?$/, pick: (m) => ({ kind: 'page', id: m[1] }) },
|
||||||
|
{ re: /^\/user\/([^/]+?)(?:\.html?)?$/, pick: (m) => ({ kind: 'user', id: m[1] }) },
|
||||||
];
|
];
|
||||||
|
|
||||||
export function matchFeedSsrPath(urlPathname: string): FeedSsrMatch | null {
|
export function matchFeedSsrPath(urlPathname: string): FeedSsrMatch | null {
|
||||||
|
|||||||
@@ -13,11 +13,22 @@ import { setSessionSnapshot } from '../utils/sessionPageCache';
|
|||||||
import type { PageHead } from './head';
|
import type { PageHead } from './head';
|
||||||
|
|
||||||
export type FeedData = {
|
export type FeedData = {
|
||||||
kind: 'home' | 'board' | 'post' | 'notfound';
|
kind: 'home' | 'board' | 'post' | 'page' | 'user' | 'notfound';
|
||||||
/** feed 聚合(帖子列表 / 板块 / 统计 / 侧栏 widget / 登录态) */
|
/** feed 聚合(帖子列表 / 板块 / 统计 / 侧栏 widget / 登录态) */
|
||||||
boot?: HomeBootPayload;
|
boot?: HomeBootPayload;
|
||||||
/** 帖子详情会话快照(PostDetailPage navType=POP 时优先消费,不再二次请求) */
|
/** 帖子详情会话快照(PostDetailPage navType=POP 时优先消费,不再二次请求) */
|
||||||
snapshot?: PostDetailSnapshot;
|
snapshot?: PostDetailSnapshot;
|
||||||
|
/** 自定义单页内容(SitePageView useSessionResource key=sitepage:${slug} 首帧命中) */
|
||||||
|
sitePage?: { slug: string; page: import('../api/types').SitePage };
|
||||||
|
/** 用户主页资料 + 首屏帖子列表(UserProfilePage 首帧命中) */
|
||||||
|
userProfile?: {
|
||||||
|
id: number;
|
||||||
|
profile: import('../api/types').UserPublic;
|
||||||
|
stats: import('../api/types').UserActivityStats | null;
|
||||||
|
posts: import('../api/types').PostItem[];
|
||||||
|
postTotal: number;
|
||||||
|
pageSize: number;
|
||||||
|
};
|
||||||
/** <head> 元信息(服务端专用,onRenderHtml 消费) */
|
/** <head> 元信息(服务端专用,onRenderHtml 消费) */
|
||||||
head?: PageHead;
|
head?: PageHead;
|
||||||
};
|
};
|
||||||
@@ -27,4 +38,12 @@ export function seedFeedData(data: FeedData | null | undefined): void {
|
|||||||
if (!data) return;
|
if (!data) return;
|
||||||
if (data.boot) seedHomeBoot(data.boot);
|
if (data.boot) seedHomeBoot(data.boot);
|
||||||
if (data.snapshot) setSessionSnapshot(postDetailCacheKey(data.snapshot.post.id), data.snapshot);
|
if (data.snapshot) setSessionSnapshot(postDetailCacheKey(data.snapshot.post.id), data.snapshot);
|
||||||
|
if (data.sitePage) {
|
||||||
|
setSessionSnapshot(`sitepage:${data.sitePage.slug}`, data.sitePage.page);
|
||||||
|
}
|
||||||
|
if (data.userProfile) {
|
||||||
|
const { id, profile, stats, posts, postTotal, pageSize } = data.userProfile;
|
||||||
|
setSessionSnapshot(`user:${id}`, { profile, stats });
|
||||||
|
setSessionSnapshot(`user:${id}:posts:1:${pageSize}`, { posts, total: postTotal });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
* 纯函数、两端安全(无 DOM / Node 依赖);renderHeadTags 产物由 onRenderHtml 注入 <head>,
|
* 纯函数、两端安全(无 DOM / Node 依赖);renderHeadTags 产物由 onRenderHtml 注入 <head>,
|
||||||
* 客户端路由切换时由 usePageSEO 接管(两者字段语义保持一致)。
|
* 客户端路由切换时由 usePageSEO 接管(两者字段语义保持一致)。
|
||||||
*/
|
*/
|
||||||
import type { ForumLimitsPublic, PostItem, SiteBranding, User } from '../api/types';
|
import type { ForumLimitsPublic, PostItem, SiteBranding, SitePage, User, UserPublic } from '../api/types';
|
||||||
import type { HomeBootPayload } from '../utils/homeBoot';
|
import type { HomeBootPayload } from '../utils/homeBoot';
|
||||||
import { normalizePermalinkExt } from '../utils/permalink';
|
import { normalizePermalinkExt } from '../utils/permalink';
|
||||||
|
|
||||||
@@ -156,6 +156,10 @@ function feedUserPath(id: number, limits?: ForumLimitsPublic | null): string {
|
|||||||
return `/user/${id}${suffixOf(limits)}`;
|
return `/user/${id}${suffixOf(limits)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function feedPagePath(slug: string, limits?: ForumLimitsPublic | null): string {
|
||||||
|
return `/page/${slug}${suffixOf(limits)}`;
|
||||||
|
}
|
||||||
|
|
||||||
function toISO(time?: string): string {
|
function toISO(time?: string): string {
|
||||||
if (!time) return '';
|
if (!time) return '';
|
||||||
const d = new Date(time);
|
const d = new Date(time);
|
||||||
@@ -268,6 +272,77 @@ export function buildPostHead(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 自定义单页 head(关于我们/版规等,ogType=article) */
|
||||||
|
export function buildPageHead(
|
||||||
|
page: SitePage,
|
||||||
|
brand: SiteBranding | null,
|
||||||
|
limits: ForumLimitsPublic | null,
|
||||||
|
base: string,
|
||||||
|
): PageHead {
|
||||||
|
const siteName = siteNameOf(brand);
|
||||||
|
const content = page.content ?? '';
|
||||||
|
const plain = stripHTMLForSearch(content);
|
||||||
|
const desc = truncateRunes(plain || page.title, DESC_MAX);
|
||||||
|
const canonical = absoluteURL(base, feedPagePath(page.slug, limits));
|
||||||
|
const defImage = defaultShareImageOf(brand);
|
||||||
|
const ogImage = absoluteURL(base, firstImageURL(content)) || defImage;
|
||||||
|
|
||||||
|
const jsonLd: Record<string, unknown> = {
|
||||||
|
'@context': 'https://schema.org',
|
||||||
|
'@type': 'WebPage',
|
||||||
|
name: page.title,
|
||||||
|
description: desc,
|
||||||
|
url: canonical,
|
||||||
|
datePublished: toISO(page.created_at),
|
||||||
|
dateModified: toISO(page.updated_at || page.created_at),
|
||||||
|
};
|
||||||
|
if (ogImage) jsonLd.image = [ogImage];
|
||||||
|
|
||||||
|
return {
|
||||||
|
title: pageTitle(page.title, siteName),
|
||||||
|
description: desc,
|
||||||
|
canonical: canonical || undefined,
|
||||||
|
ogType: 'article',
|
||||||
|
ogImage: ogImage || undefined,
|
||||||
|
siteName,
|
||||||
|
jsonLd: JSON.stringify(jsonLd),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 用户主页 head(ogType=profile,Person JSON-LD) */
|
||||||
|
export function buildUserHead(
|
||||||
|
profile: UserPublic,
|
||||||
|
brand: SiteBranding | null,
|
||||||
|
limits: ForumLimitsPublic | null,
|
||||||
|
base: string,
|
||||||
|
): PageHead {
|
||||||
|
const siteName = siteNameOf(brand);
|
||||||
|
const name = displayNameOf(profile);
|
||||||
|
const sig = (profile.signature ?? '').trim();
|
||||||
|
const desc = truncateRunes(sig || `${name} 的主页`, DESC_MAX);
|
||||||
|
const canonical = absoluteURL(base, feedUserPath(profile.id, limits));
|
||||||
|
const ogImage = absoluteURL(base, profile.avatar ?? '') || defaultShareImageOf(brand);
|
||||||
|
|
||||||
|
const jsonLd: Record<string, unknown> = {
|
||||||
|
'@context': 'https://schema.org',
|
||||||
|
'@type': 'Person',
|
||||||
|
name,
|
||||||
|
description: desc,
|
||||||
|
url: canonical,
|
||||||
|
};
|
||||||
|
if (ogImage) jsonLd.image = ogImage;
|
||||||
|
|
||||||
|
return {
|
||||||
|
title: pageTitle(`${name} 的主页`, siteName),
|
||||||
|
description: desc,
|
||||||
|
canonical: canonical || undefined,
|
||||||
|
ogType: 'profile',
|
||||||
|
ogImage: ogImage || undefined,
|
||||||
|
siteName,
|
||||||
|
jsonLd: JSON.stringify(jsonLd),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/** 404 页 head(移植 Go notFoundPageMeta) */
|
/** 404 页 head(移植 Go notFoundPageMeta) */
|
||||||
export function buildNotFoundHead(
|
export function buildNotFoundHead(
|
||||||
pathname: string,
|
pathname: string,
|
||||||
|
|||||||
Reference in New Issue
Block a user