完善官网 SEO 地基与精确路由

补充 robots、sitemap、SEO 元信息、结构化数据与首页语义内容
统一三套 Nginx 与 Pingora 的 62 条 SPA 路由和真实 404 行为
新增路由一致性检查、网关测试并同步技术文档与项目记忆
This commit is contained in:
2026-07-10 12:14:44 +08:00
parent e464e62697
commit 09fec601b1
26 changed files with 681 additions and 27 deletions
+6 -2
View File
@@ -161,7 +161,9 @@ describe('App title sync', () => {
test('主站阶段变化会同步浏览器与宿主标题', () => {
renderApp();
expect(appTitleMock.syncAppTitle).toHaveBeenLastCalledWith('陶泥儿');
expect(appTitleMock.syncAppTitle).toHaveBeenLastCalledWith(
'陶泥儿 Genarrative|游戏美术AI创作工具与美术Agent工作台',
);
act(() => {
fireEvent.click(screen.getByRole('button', { name: '打开拼图创作' }));
@@ -187,7 +189,9 @@ describe('App title sync', () => {
fireEvent.click(screen.getByRole('button', { name: '退出 RPG' }));
});
expect(appTitleMock.syncAppTitle).toHaveBeenLastCalledWith('陶泥儿');
expect(appTitleMock.syncAppTitle).toHaveBeenLastCalledWith(
'陶泥儿 Genarrative|游戏美术AI创作工具与美术Agent工作台',
);
});
test('启动时回读宿主 runtime 后刷新壳能力 UI', async () => {
@@ -158,11 +158,11 @@ describe('CreationLandingView', () => {
expect(screen.getByRole('main', { name: '陶泥儿创作主页' })).toBeTruthy();
expect(
screen.getByRole('heading', {
name: '陶泥儿 - 开启全民精品游戏创作',
name: '游戏美术AI工作台,让个人创作者更快做出角色、场景与宣发素材',
}),
).toBeTruthy();
expect(
screen.getByText('登录即送100泥点,可以免费制作50个素材'),
screen.getByText(/陶泥儿 Genarrative 以美术 Agent 与无限画布/u),
).toBeTruthy();
expect(screen.getByRole('heading', { name: '创作工具' })).toBeTruthy();
expect(screen.getByText('游戏视觉规范')).toBeTruthy();
@@ -859,8 +859,13 @@ export function CreationLandingView({
<section className="creation-landing__hero">
<div className="creation-landing__hero-copy">
<span className="creation-landing__eyebrow">陶泥儿创作工具</span>
<h1>陶泥儿 - 开启全民精品游戏创作</h1>
<p>登录即送100泥点,可以免费制作50个素材</p>
<h1>
游戏美术AI工作台,让个人创作者更快做出角色、场景与宣发素材
</h1>
<p>
陶泥儿 Genarrative 以美术 Agent
与无限画布帮助个人创作者准备角色、场景与宣发素材;登录即送100泥点,可以免费制作50个素材。
</p>
<div className="creation-landing__hero-actions">
<PlatformActionButton
size="md"
@@ -95,7 +95,10 @@ test('renders mobile home welcome copy and closes with the acknowledge button',
);
expect(screen.getByRole('dialog', { name: '欢迎' })).toBeTruthy();
expect(screen.getByRole('heading', { name: '欢迎' })).toBeTruthy();
expect(
screen.getByRole('heading', { level: 2, name: '欢迎' }),
).toBeTruthy();
expect(screen.queryByRole('heading', { level: 1 })).toBeNull();
const copy = document.querySelector(
'.platform-mobile-home-welcome-dialog__copy',
);
@@ -68,7 +68,7 @@ export function PlatformMobileHomeWelcomeDialog({
alt=""
className="platform-mobile-home-welcome-dialog__icon"
/>
<h1 className="platform-mobile-home-welcome-dialog__title">欢迎</h1>
<h2 className="platform-mobile-home-welcome-dialog__title">欢迎</h2>
<p className="platform-mobile-home-welcome-dialog__copy">
尼嚎,新陶泥er!
<br />
@@ -378,6 +378,9 @@ const {
};
});
const HOME_SEO_HEADING =
'游戏美术AI工作台,让个人创作者更快做出角色、场景与宣发素材';
const {
mockGetPublicAuthUserByCode,
mockGetPublicAuthUserById,
@@ -4280,6 +4283,39 @@ test('desktop home shows empty states when public shelves are unavailable', () =
expect(screen.getByText('暂时还没有推荐作品。')).toBeTruthy();
});
test('desktop home exposes one product h1 and section h2 headings', () => {
renderLoggedOutHomeView(vi.fn(), {}, 'home', true);
const productHeading = screen.getByRole('heading', {
level: 1,
name: HOME_SEO_HEADING,
});
expect(screen.getAllByRole('heading', { level: 1 })).toHaveLength(1);
expect(productHeading.closest('button')).toBeNull();
expect(
screen.getByRole('heading', { level: 2, name: '今日游戏' }),
).toBeTruthy();
expect(
screen.getByRole('heading', { level: 2, name: '推荐' }),
).toBeTruthy();
expect(
screen.getByRole('heading', { level: 2, name: '作品分类' }),
).toBeTruthy();
});
test('mobile shell exposes one product h1 outside interactive content', () => {
renderLoggedOutHomeView(vi.fn(), {}, 'profile');
const productHeading = screen.getByRole('heading', {
level: 1,
name: HOME_SEO_HEADING,
});
expect(screen.getAllByRole('heading', { level: 1 })).toHaveLength(1);
expect(productHeading.closest('button')).toBeNull();
});
test('logged in create tab shows real wallet balance beside the brand', () => {
mockNarrowMobileLayout();
+20 -2
View File
@@ -1020,13 +1020,27 @@ function SectionHeader({ title, detail }: { title: string; detail: string }) {
<div className="text-[10px] font-semibold tracking-[0.26em] text-[var(--platform-text-soft)]">
{detail}
</div>
<div className="mt-1 text-base font-semibold text-[var(--platform-text-strong)]">
<h2 className="mt-1 text-base font-semibold text-[var(--platform-text-strong)]">
{title}
</div>
</h2>
</div>
);
}
function HomeSeoIntro() {
return (
<header className="mb-2 min-w-0 shrink-0 px-1">
<h1 className="text-sm font-semibold leading-5 text-[var(--platform-text-strong)]">
游戏美术AI工作台,让个人创作者更快做出角色、场景与宣发素材
</h1>
<p className="mt-0.5 text-xs leading-5 text-[var(--platform-text-soft)]">
在陶泥儿 Genarrative,用美术 Agent
快速生成与迭代素材,再在无限画布中整理角色、场景和宣发灵感。
</p>
</header>
);
}
function PublicCodeSearchBar({
value,
onChange,
@@ -4578,6 +4592,8 @@ export function RpgEntryHomeView({
const desktopHomeContent: ReactNode = (
<div className={DESKTOP_PAGE_STAGE_CLASS}>
<HomeSeoIntro />
{platformError ? (
<PlatformStatusMessage
tone="error"
@@ -5160,6 +5176,8 @@ export function RpgEntryHomeView({
</div>
) : null}
<HomeSeoIntro />
<div className="platform-tab-panel-stack min-w-0 flex-1">
{tabPanels}
</div>
+5 -1
View File
@@ -3,6 +3,7 @@
import { afterEach, describe, expect, test, vi } from 'vitest';
import {
APP_HOME_TITLE,
resolveAppTitleForSelectionStage,
syncAppTitle,
} from './appTitle';
@@ -22,7 +23,10 @@ afterEach(() => {
describe('appTitle', () => {
test('按平台阶段生成可读页面标题', () => {
expect(resolveAppTitleForSelectionStage('platform')).toBe('陶泥儿');
expect(resolveAppTitleForSelectionStage('platform')).toBe(APP_HOME_TITLE);
expect(resolveAppTitleForSelectionStage('creation-home')).toBe(
APP_HOME_TITLE,
);
expect(resolveAppTitleForSelectionStage('work-detail')).toBe(
'作品详情 - 陶泥儿',
);
+5 -2
View File
@@ -2,6 +2,8 @@ import type { SelectionStage } from '../components/platform-entry';
import { setHostAppTitle } from './host-bridge/hostBridge';
const APP_TITLE_BRAND = '陶泥儿';
export const APP_HOME_TITLE =
'陶泥儿 Genarrative|游戏美术AI创作工具与美术Agent工作台';
const APP_TITLE_BY_SELECTION_STAGE: Partial<Record<SelectionStage, string>> = {
'agent-workspace': 'RPG 创作',
@@ -21,6 +23,7 @@ const APP_TITLE_BY_SELECTION_STAGE: Partial<Record<SelectionStage, string>> = {
'creative-agent-workspace': '创意 Agent',
'custom-world-generating': 'RPG 生成中',
'custom-world-result': 'RPG 结果',
'creation-home': APP_HOME_TITLE,
detail: '世界详情',
'jump-hop-gallery-detail': '跳一跳详情',
'jump-hop-generating': '跳一跳生成中',
@@ -31,7 +34,7 @@ const APP_TITLE_BY_SELECTION_STAGE: Partial<Record<SelectionStage, string>> = {
'match3d-generating': '抓大鹅生成中',
'match3d-result': '抓大鹅结果',
'match3d-runtime': '抓大鹅',
platform: APP_TITLE_BRAND,
platform: APP_HOME_TITLE,
'profile-feedback': '反馈与投诉',
'puzzle-agent-workspace': '拼图创作',
'puzzle-clear-generating': '拼消消生成中',
@@ -61,7 +64,7 @@ const APP_TITLE_BY_SELECTION_STAGE: Partial<Record<SelectionStage, string>> = {
export function resolveAppTitleForSelectionStage(stage: SelectionStage) {
const title = APP_TITLE_BY_SELECTION_STAGE[stage] ?? APP_TITLE_BRAND;
if (title === APP_TITLE_BRAND) {
if (title === APP_TITLE_BRAND || title === APP_HOME_TITLE) {
return title;
}