Parcourir la source

feat(seo): P1 technical SEO v1.3.0

- Metadata 全页 4 语言 + OG/Twitter + canonical(lib/seo.ts)
- hreflang 4 语言 + x-default;sitemap 36 条目 + robots + IndexNow API
- JSON-LD:Organization/WebSite/Product/BreadcrumbList(无 FAQPage)
- SSG 静态化 47/47(TTFB ~13ms)
- 性能基线文档 + VERSION v1.3.0 + CHANGELOG + README
DoTouch Dev il y a 2 semaines
Parent
commit
836665fc5c

+ 2 - 2
.omo/run-continuation/ses_0229f653bffedHZYFR3ILxjlpg.json

@@ -1,10 +1,10 @@
 {
   "sessionID": "ses_0229f653bffedHZYFR3ILxjlpg",
-  "updatedAt": "2026-08-08T20:01:18.326Z",
+  "updatedAt": "2026-08-09T01:25:07.275Z",
   "sources": {
     "background-task": {
       "state": "idle",
-      "updatedAt": "2026-08-08T20:01:18.326Z"
+      "updatedAt": "2026-08-09T01:25:07.275Z"
     }
   }
 }

+ 3 - 0
README.md

@@ -106,6 +106,9 @@ docs/
 │   ├── top3-transit-compliance-20260808.md    全球 Top3 中转站合规对比
 │   └── alibaba-bailian-terms-20260808.md      阿里云百炼条款(转售需书面许可)
+├── seo/                      SEO 方案与性能基线
+│   ├── seo-strategy-v1.0.md          SEO 方案(P1 已执行,P2/P3 待执行)
+│   └── performance-baseline-v1.3.0.md 性能基线(SSG TTFB ~13ms)
 ├── models/                  模型调研(阿里云百炼真实扫描)
 │   ├── dashscope-models-20260808.json    236 个在售模型全量清单
 │   └── dashscope-pricing-20260808.md     价格与收费方式(含 7 折策略)

+ 1 - 1
VERSION

@@ -1 +1 @@
-v1.2.0 (demo)
+v1.3.0 (seo)

BIN
apps/web/public/og.png


+ 7 - 0
apps/web/src/app/[locale]/console/page.tsx

@@ -18,3 +18,10 @@ export default function ConsolePage() {
     </div>
   );
 }
+
+import type { Metadata } from 'next';
+
+export const metadata: Metadata = {
+  robots: { index: false, follow: false },
+};
+

+ 27 - 0
apps/web/src/app/[locale]/docs/page.tsx

@@ -12,12 +12,27 @@ import { AuthModal } from '@/components/AuthModal';
 import { DocsSidebar } from '@/components/docs/DocsSidebar';
 import { DocsSections } from '@/components/docs/DocsSections';
 
+
 export default function DocsPage() {
   const t = useTranslations();
   const locale = useLocale();
+  // BreadcrumbList 结构化数据
+  const breadcrumbJson = {
+    '@context': 'https://schema.org',
+    '@type': 'BreadcrumbList',
+    itemListElement: [
+      { '@type': 'ListItem', position: 1, name: 'Home', item: `${siteUrl()}/${locale}` },
+      { '@type': 'ListItem', position: 2, name: 'Docs', item: `${siteUrl()}/${locale}/docs` },
+    ],
+  };
+
 
   return (
     <div className="flex min-h-screen flex-col">
+      <script
+        type="application/ld+json"
+        dangerouslySetInnerHTML={{ __html: JSON.stringify(breadcrumbJson) }}
+      />
       <SiteHeader />
       <main className="flex-1">
         <div className="mx-auto max-w-[1160px] px-6">
@@ -53,3 +68,15 @@ export default function DocsPage() {
     </div>
   );
 }
+
+import type { Metadata } from 'next';
+import { alternatesFor, pageMeta, siteUrl } from '@/lib/seo';
+
+type MetaProps = { params: Promise<{ locale: string }> };
+
+export async function generateMetadata({ params }: MetaProps): Promise<Metadata> {
+  const { locale } = await params;
+  const m = pageMeta(locale, 'docs');
+  return { title: m.title, description: m.desc, alternates: alternatesFor('/docs', locale) };
+}
+

+ 75 - 11
apps/web/src/app/[locale]/layout.tsx

@@ -1,37 +1,101 @@
 /**
- * 根布局 - 全局布局包裹,设置默认语言
+ * 根布局 - 全局布局包裹,设置默认语言;SSG 静态化(4 locale)
+ * P1 SEO:metadataBase / 多语言 title·desc·OG / Organization+WebSite JSON-LD
  */
 import type { Metadata } from 'next';
 import { notFound } from 'next/navigation';
 import { NextIntlClientProvider } from 'next-intl';
-import { getMessages } from 'next-intl/server';
+import { getMessages, unstable_setRequestLocale } from 'next-intl/server';
 import '../globals.css';
+import { LOCALES, pageMeta, siteUrl, type Locale } from '@/lib/seo';
 
 type Props = {
   children: React.ReactNode;
   params: Promise<{ locale: string }>;
 };
 
-const locales = ['vi', 'en', 'zh-CN', 'zh-TW'] as const;
+/** SSG:预渲染全部 4 个语言 */
+export function generateStaticParams() {
+  return LOCALES.map((locale) => ({ locale }));
+}
 
-export const metadata: Metadata = {
-  title: 'DoTouch.AI - AI Compute Hub',
-  description: 'AI API Reseller Platform for Vietnamese and overseas users.',
-};
+export async function generateMetadata({ params }: Props): Promise<Metadata> {
+  const { locale } = await params;
+  const m = pageMeta(locale, 'home');
+  const base = siteUrl();
+  return {
+    metadataBase: new URL(base),
+    title: { template: '%s | DoTouch.AI', default: m.title },
+    description: m.desc,
+    alternates: {
+      canonical: `${base}/${locale}`,
+      languages: {
+        vi: `${base}/vi`,
+        en: `${base}/en`,
+        'zh-CN': `${base}/zh-CN`,
+        'zh-TW': `${base}/zh-TW`,
+        'x-default': `${base}/vi`,
+      },
+    },
+    openGraph: {
+      type: 'website',
+      siteName: 'DoTouch.AI',
+      title: m.title,
+      description: m.desc,
+      locale: locale === 'zh-CN' ? 'zh_CN' : locale === 'zh-TW' ? 'zh_TW' : locale,
+      images: [{ url: '/og.png', width: 1024, height: 1024, alt: 'DoTouch.AI' }],
+    },
+    twitter: {
+      card: 'summary_large_image',
+      title: m.title,
+      description: m.desc,
+      images: ['/og.png'],
+    },
+  };
+}
 
 export default async function RootLayout({ children, params }: Props) {
   const { locale } = await params;
-  if (!locales.includes(locale as (typeof locales)[number])) {
+  if (!(LOCALES as readonly string[]).includes(locale)) {
     notFound();
   }
+  // SSG 静态渲染需要显式设置请求 locale(next-intl)
+  unstable_setRequestLocale(locale);
   const messages = await getMessages();
 
+  // Organization + WebSite 结构化数据(每语言 inLanguage)
+  const base = siteUrl();
+  const orgJson = {
+    '@context': 'https://schema.org',
+    '@type': 'Organization',
+    '@id': `${base}#organization`,
+    name: 'DoTouch.AI',
+    url: base,
+    logo: `${base}/og.png`,
+    description: 'Affordable Chinese AI API platform for Vietnam and the world.',
+    sameAs: [],
+  };
+  const siteJson = {
+    '@context': 'https://schema.org',
+    '@type': 'WebSite',
+    '@id': `${base}#website`,
+    name: 'DoTouch.AI',
+    url: `${base}/${locale}`,
+    inLanguage: locale as Locale,
+  };
+
   return (
     <html lang={locale}>
       <body className="bg-brand-bg text-brand-text antialiased">
-        <NextIntlClientProvider messages={messages}>
-          {children}
-        </NextIntlClientProvider>
+        <script
+          type="application/ld+json"
+          dangerouslySetInnerHTML={{ __html: JSON.stringify(orgJson) }}
+        />
+        <script
+          type="application/ld+json"
+          dangerouslySetInnerHTML={{ __html: JSON.stringify(siteJson) }}
+        />
+        <NextIntlClientProvider messages={messages}>{children}</NextIntlClientProvider>
       </body>
     </html>
   );

+ 42 - 1
apps/web/src/app/[locale]/models/[id]/page.tsx

@@ -26,6 +26,24 @@ type Props = {
 const LOCALES = ['vi', 'en', 'zh-CN', 'zh-TW'] as const;
 
 /** 静态生成 4 语言 × 4 模型 id(非法 id 走 notFound) */
+import type { Metadata } from 'next';
+import { alternatesFor, MODEL_NAMES, pageMeta, siteUrl } from '@/lib/seo';
+import { MODELS } from '@/lib/models-data';
+
+type MetaProps = { params: Promise<{ locale: string; id: string }> };
+
+export async function generateMetadata({ params }: MetaProps): Promise<Metadata> {
+  const { locale, id } = await params;
+  const m = pageMeta(locale, 'detail');
+  const display = MODEL_NAMES[id] ?? id;
+  return {
+    title: m.title.replace('{model}', display),
+    description: m.desc.replace('{model}', display),
+    alternates: alternatesFor(`/models/${id}`, locale),
+  };
+}
+
+
 export function generateStaticParams() {
   return LOCALES.flatMap((locale) => MODEL_IDS.map((id) => ({ locale, id })));
 }
@@ -41,8 +59,31 @@ export default async function ModelDetailPage({ params }: Props) {
   const t = await getTranslations({ locale });
   const tm = await getTranslations({ locale, namespace: 'models' });
 
-  return (
+  
+  // Product 结构化数据(模型定价,USD per 1M tokens)
+  const productJson = model
+    ? {
+        '@context': 'https://schema.org',
+        '@type': 'Product',
+        name: MODEL_NAMES[model.id] ?? model.id,
+        description: 'AI model API on DoTouch.AI',
+        offers: {
+          '@type': 'Offer',
+          priceCurrency: 'USD',
+          price: model.inUsd.toFixed(2),
+          description: 'Input price per 1M tokens',
+        },
+      }
+    : null;
+
+return (
     <div className="flex min-h-screen flex-col">
+      {productJson && (
+        <script
+          type="application/ld+json"
+          dangerouslySetInnerHTML={{ __html: JSON.stringify(productJson) }}
+        />
+      )}
       <SiteHeader />
       <main className="flex-1">
         <div className="mx-auto max-w-[1160px] px-6">

+ 27 - 0
apps/web/src/app/[locale]/models/page.tsx

@@ -10,13 +10,28 @@ import { SupportFab } from '@/components/SupportFab';
 import { AuthModal } from '@/components/AuthModal';
 import { ModelsTable } from '@/components/models/ModelsTable';
 
+
 export default function ModelsPage() {
   const t = useTranslations();
   const tm = useTranslations('models');
   const locale = useLocale();
+  // BreadcrumbList 结构化数据
+  const breadcrumbJson = {
+    '@context': 'https://schema.org',
+    '@type': 'BreadcrumbList',
+    itemListElement: [
+      { '@type': 'ListItem', position: 1, name: 'Home', item: `${siteUrl()}/${locale}` },
+      { '@type': 'ListItem', position: 2, name: 'Models', item: `${siteUrl()}/${locale}/models` },
+    ],
+  };
+
 
   return (
     <div className="flex min-h-screen flex-col">
+      <script
+        type="application/ld+json"
+        dangerouslySetInnerHTML={{ __html: JSON.stringify(breadcrumbJson) }}
+      />
       <SiteHeader />
       <main className="flex-1">
         <div className="mx-auto max-w-[1160px] px-6">
@@ -54,3 +69,15 @@ export default function ModelsPage() {
     </div>
   );
 }
+
+import type { Metadata } from 'next';
+import { alternatesFor, pageMeta, siteUrl } from '@/lib/seo';
+
+type MetaProps = { params: Promise<{ locale: string }> };
+
+export async function generateMetadata({ params }: MetaProps): Promise<Metadata> {
+  const { locale } = await params;
+  const m = pageMeta(locale, 'models');
+  return { title: m.title, description: m.desc, alternates: alternatesFor('/models', locale) };
+}
+

+ 12 - 0
apps/web/src/app/[locale]/page.tsx

@@ -38,3 +38,15 @@ export default function HomePage() {
     </div>
   );
 }
+
+import type { Metadata } from 'next';
+import { alternatesFor, pageMeta } from '@/lib/seo';
+
+type MetaProps = { params: Promise<{ locale: string }> };
+
+export async function generateMetadata({ params }: MetaProps): Promise<Metadata> {
+  const { locale } = await params;
+  const m = pageMeta(locale, 'home');
+  return { title: m.title, description: m.desc, alternates: alternatesFor('', locale) };
+}
+

+ 27 - 0
apps/web/src/app/[locale]/pricing/catalog/page.tsx

@@ -12,12 +12,27 @@ import { RateBox } from '@/components/pricing/RateBox';
 import { CatalogTabs } from '@/components/pricing/CatalogTabs';
 import { BillingPolicy } from '@/components/pricing/BillingPolicy';
 
+
 export default function PricingCatalogPage() {
   const t = useTranslations();
   const locale = useLocale();
+  // BreadcrumbList 结构化数据
+  const breadcrumbJson = {
+    '@context': 'https://schema.org',
+    '@type': 'BreadcrumbList',
+    itemListElement: [
+      { '@type': 'ListItem', position: 1, name: 'Home', item: `${siteUrl()}/${locale}` },
+      { '@type': 'ListItem', position: 2, name: 'Model Catalog', item: `${siteUrl()}/${locale}/pricing/catalog` },
+    ],
+  };
+
 
   return (
     <div className="flex min-h-screen flex-col">
+      <script
+        type="application/ld+json"
+        dangerouslySetInnerHTML={{ __html: JSON.stringify(breadcrumbJson) }}
+      />
       <SiteHeader />
       <main className="flex-1">
         <div className="mx-auto max-w-[1160px] px-6 py-8">
@@ -82,3 +97,15 @@ export default function PricingCatalogPage() {
     </div>
   );
 }
+
+import type { Metadata } from 'next';
+import { alternatesFor, pageMeta, siteUrl } from '@/lib/seo';
+
+type MetaProps = { params: Promise<{ locale: string }> };
+
+export async function generateMetadata({ params }: MetaProps): Promise<Metadata> {
+  const { locale } = await params;
+  const m = pageMeta(locale, 'catalog');
+  return { title: m.title, description: m.desc, alternates: alternatesFor('/pricing/catalog', locale) };
+}
+

+ 27 - 0
apps/web/src/app/[locale]/pricing/page.tsx

@@ -13,12 +13,27 @@ import { PriceTable } from '@/components/pricing/PriceTable';
 import { BillingRules } from '@/components/pricing/BillingRules';
 import { PricingFaq } from '@/components/pricing/PricingFaq';
 
+
 export default function PricingPage() {
   const t = useTranslations();
   const locale = useLocale();
+  // BreadcrumbList 结构化数据
+  const breadcrumbJson = {
+    '@context': 'https://schema.org',
+    '@type': 'BreadcrumbList',
+    itemListElement: [
+      { '@type': 'ListItem', position: 1, name: 'Home', item: `${siteUrl()}/${locale}` },
+      { '@type': 'ListItem', position: 2, name: 'Pricing', item: `${siteUrl()}/${locale}/pricing` },
+    ],
+  };
+
 
   return (
     <div className="flex min-h-screen flex-col">
+      <script
+        type="application/ld+json"
+        dangerouslySetInnerHTML={{ __html: JSON.stringify(breadcrumbJson) }}
+      />
       <SiteHeader />
       <main className="flex-1">
         <div className="mx-auto max-w-[1160px] px-6 py-8">
@@ -81,3 +96,15 @@ export default function PricingPage() {
     </div>
   );
 }
+
+import type { Metadata } from 'next';
+import { alternatesFor, pageMeta, siteUrl } from '@/lib/seo';
+
+type MetaProps = { params: Promise<{ locale: string }> };
+
+export async function generateMetadata({ params }: MetaProps): Promise<Metadata> {
+  const { locale } = await params;
+  const m = pageMeta(locale, 'pricing');
+  return { title: m.title, description: m.desc, alternates: alternatesFor('/pricing', locale) };
+}
+

+ 43 - 0
apps/web/src/app/api/indexnow/route.ts

@@ -0,0 +1,43 @@
+/**
+ * IndexNow 收录通知 - 内容更新后即时通知 Bing/Yandex 等
+ * 用法:GET /api/indexnow?url=https://dotouch.ai/vi/pricing
+ * 需配置环境变量 INDEXNOW_KEY(IndexNow 平台申请的密钥);未配置返回 501
+ */
+import { NextResponse } from 'next/server';
+import { siteUrl } from '@/lib/seo';
+
+export async function GET(request: Request) {
+  const key = process.env['INDEXNOW_KEY'];
+  if (!key) {
+    return NextResponse.json(
+      { ok: false, message: 'INDEXNOW_KEY 未配置(IndexNow 平台申请)' },
+      { status: 501 },
+    );
+  }
+
+  const url = new URL(request.url).searchParams.get('url');
+  if (!url || !url.startsWith('http')) {
+    return NextResponse.json({ ok: false, message: '缺少 url 参数' }, { status: 400 });
+  }
+
+  // 仅允许通知本站 URL(防滥用)
+  const base = siteUrl();
+  if (!url.startsWith(base)) {
+    return NextResponse.json({ ok: false, message: 'url 不在本站域内' }, { status: 400 });
+  }
+
+  try {
+    const resp = await fetch(
+      `https://api.indexnow.org/indexnow?url=${encodeURIComponent(url)}&key=${encodeURIComponent(key)}`,
+    );
+    if (resp.ok) {
+      return NextResponse.json({ ok: true });
+    }
+    return NextResponse.json(
+      { ok: false, status: resp.status, message: 'IndexNow 推送失败' },
+      { status: 502 },
+    );
+  } catch {
+    return NextResponse.json({ ok: false, message: 'IndexNow 服务不可达' }, { status: 502 });
+  }
+}

+ 17 - 0
apps/web/src/app/robots.ts

@@ -0,0 +1,17 @@
+/**
+ * robots.txt 生成 - 允许抓取,排除后台/API/控制台
+ * P1 SEO
+ */
+import type { MetadataRoute } from 'next';
+import { siteUrl } from '@/lib/seo';
+
+export default function robots(): MetadataRoute.Robots {
+  return {
+    rules: {
+      userAgent: '*',
+      allow: '/',
+      disallow: ['/admin', '/api', '/console'],
+    },
+    sitemap: `${siteUrl()}/sitemap.xml`,
+  };
+}

+ 47 - 0
apps/web/src/app/sitemap.ts

@@ -0,0 +1,47 @@
+/**
+ * sitemap.xml 生成 - 全路由 × 4 语言 + 模型详情页
+ * P1 SEO:收录全量索引页面,自动适配 NEXT_PUBLIC_SITE_URL
+ */
+import type { MetadataRoute } from 'next';
+import { LOCALES, siteUrl } from '@/lib/seo';
+import { MODEL_IDS } from '@/lib/models-data';
+
+/** 静态路由(路径 + 优先级) */
+const ROUTES: Array<{ path: string; priority: number }> = [
+  { path: '', priority: 1.0 },
+  { path: '/models', priority: 0.8 },
+  { path: '/pricing', priority: 0.8 },
+  { path: '/pricing/catalog', priority: 0.6 },
+  { path: '/docs', priority: 0.6 },
+];
+
+export default function sitemap(): MetadataRoute.Sitemap {
+  const base = siteUrl();
+  const entries: MetadataRoute.Sitemap = [];
+
+  // 静态路由 × 4 语言
+  for (const locale of LOCALES) {
+    for (const route of ROUTES) {
+      entries.push({
+        url: `${base}/${locale}${route.path}`,
+        lastModified: new Date('2026-08-09'),
+        changeFrequency: route.priority >= 0.8 ? 'weekly' : 'monthly',
+        priority: route.priority,
+      });
+    }
+  }
+
+  // 模型详情 × 4 语言 × 4 模型
+  for (const locale of LOCALES) {
+    for (const id of MODEL_IDS) {
+      entries.push({
+        url: `${base}/${locale}/models/${id}`,
+        lastModified: new Date('2026-08-09'),
+        changeFrequency: 'monthly',
+        priority: 0.7,
+      });
+    }
+  }
+
+  return entries;
+}

+ 147 - 0
apps/web/src/lib/seo.ts

@@ -0,0 +1,147 @@
+/**
+ * SEO 工具库 - 站点 URL / 多语言路径 / 元数据辅助(P1 技术 SEO)
+ * 站点地址经 NEXT_PUBLIC_SITE_URL 注入(演示 localhost:3000,生产 https://dotouch.ai)
+ */
+import type { Metadata } from 'next';
+
+export const LOCALES = ['vi', 'en', 'zh-CN', 'zh-TW'] as const;
+export type Locale = (typeof LOCALES)[number];
+
+/** 站点基址(尾无斜杠) */
+export function siteUrl(): string {
+  const u = process.env['NEXT_PUBLIC_SITE_URL'] || 'http://localhost:3000';
+  return u.replace(/\/$/, '');
+}
+
+/** 各页面 4 语言 title/description(本地化撰写,非直译) */
+type PageMeta = { title: string; desc: string };
+export const SEO_META: Record<Locale, Record<string, PageMeta>> = {
+  vi: {
+    home: {
+      title: 'API AI giá rẻ cho Việt Nam | DeepSeek, GLM, Qwen',
+      desc: 'DoTouch.AI – Nền tảng API AI Trung Quốc giá rẻ (DeepSeek V4, GLM 5.2, Qwen). Tương thích OpenAI, giá bằng 70% Trung Quốc, thanh toán MoMo/ZaloPay.',
+    },
+    models: {
+      title: 'Mô hình AI | DeepSeek, GLM, Qwen API giá rẻ',
+      desc: 'Danh sách mô hình AI tại DoTouch.AI: DeepSeek V4, GLM 5.2, Qwen Max. So sánh ngữ cảnh, giá cả và khả năng. OpenAI-compatible.',
+    },
+    detail: {
+      title: '{model} API – Giá rẻ 30%, OpenAI Compatible',
+      desc: 'Gọi {model} qua API tại DoTouch.AI với giá rẻ hơn 30%. Xem giá, thông số, khả năng và ví dụ code.',
+    },
+    pricing: {
+      title: 'Bảng giá API AI | Giá bằng 70% Trung Quốc',
+      desc: 'Bảng giá API AI tại DoTouch.AI – trả trước, tính theo token, giá chỉ bằng 70% giá nội địa Trung Quốc. Thanh toán MoMo, ZaloPay, Stripe.',
+    },
+    catalog: {
+      title: 'Model Catalog · 236 mô hình | DoTouch.AI',
+      desc: 'Toàn bộ 236 mô hình AI từ Alibaba DashScope tại DoTouch.AI. Tìm mô hình phù hợp theo nhu cầu và ngân sách.',
+    },
+    docs: {
+      title: 'Tài liệu API | DoTouch.AI',
+      desc: 'Tài liệu API DoTouch.AI: bắt đầu nhanh, API Reference, mã lỗi và quy tắc tính phí.',
+    },
+  },
+  en: {
+    home: {
+      title: 'Affordable AI API for Vietnam | DeepSeek, GLM, Qwen',
+      desc: 'DoTouch.AI – Affordable Chinese LLM API platform (DeepSeek V4, GLM 5.2, Qwen). OpenAI-compatible, 70% of China price, MoMo/ZaloPay payments.',
+    },
+    models: {
+      title: 'AI Models | DeepSeek, GLM, Qwen API',
+      desc: 'DoTouch.AI model catalog: DeepSeek V4, GLM 5.2, Qwen Max. Compare context, pricing and capabilities. OpenAI-compatible.',
+    },
+    detail: {
+      title: '{model} API – 30% Cheaper, OpenAI Compatible',
+      desc: 'Call {model} via DoTouch.AI API at 30% lower cost. See pricing, specs, capabilities and code examples.',
+    },
+    pricing: {
+      title: 'AI API Pricing | 70% of China Price',
+      desc: 'DoTouch.AI API pricing – prepaid, metered per token, at only 70% of China domestic price. Pay with MoMo, ZaloPay, Stripe.',
+    },
+    catalog: {
+      title: 'Model Catalog · 236 Models | DoTouch.AI',
+      desc: 'All 236 AI models from Alibaba DashScope on DoTouch.AI. Find the right model for your needs and budget.',
+    },
+    docs: {
+      title: 'API Documentation | DoTouch.AI',
+      desc: 'DoTouch.AI API docs: quickstart, API reference, error codes and billing rules.',
+    },
+  },
+  'zh-CN': {
+    home: {
+      title: '低价 AI API | DeepSeek、GLM、Qwen 越南直连',
+      desc: 'DoTouch.AI – 国产大模型 API 中转平台(DeepSeek V4、GLM 5.2、Qwen)。OpenAI 兼容,国内价 7 折,支持 MoMo/ZaloPay 等本地支付。',
+    },
+    models: {
+      title: '模型列表 | DeepSeek、GLM、Qwen API',
+      desc: 'DoTouch.AI 模型列表:DeepSeek V4、GLM 5.2、Qwen Max。对比上下文、价格与能力,OpenAI 兼容。',
+    },
+    detail: {
+      title: '{model} API – 7 折低价,OpenAI 兼容',
+      desc: '通过 DoTouch.AI API 以 7 折价格调用 {model}。查看价格、规格、能力与代码示例。',
+    },
+    pricing: {
+      title: 'API 定价 | 国内价 7 折',
+      desc: 'DoTouch.AI API 定价 – 预充值、按 token 计量,仅国内价 7 折。支持 MoMo、ZaloPay、Stripe 支付。',
+    },
+    catalog: {
+      title: '模型大全 · 236 个模型 | DoTouch.AI',
+      desc: 'DoTouch.AI 上阿里云百炼全部 236 个在售模型。按需求与预算找到合适模型。',
+    },
+    docs: {
+      title: 'API 文档 | DoTouch.AI',
+      desc: 'DoTouch.AI API 文档:快速开始、API 参考、错误码与计费规则。',
+    },
+  },
+  'zh-TW': {
+    home: {
+      title: '低價 AI API | DeepSeek、GLM、Qwen 越南直連',
+      desc: 'DoTouch.AI – 國產大模型 API 中轉平台(DeepSeek V4、GLM 5.2、Qwen)。OpenAI 相容,中國國內價 7 折,支援 MoMo/ZaloPay 等本地支付。',
+    },
+    models: {
+      title: '模型列表 | DeepSeek、GLM、Qwen API',
+      desc: 'DoTouch.AI 模型列表:DeepSeek V4、GLM 5.2、Qwen Max。比較上下文、價格與能力,OpenAI 相容。',
+    },
+    detail: {
+      title: '{model} API – 7 折低價,OpenAI 相容',
+      desc: '透過 DoTouch.AI API 以 7 折價格呼叫 {model}。查看價格、規格、能力與程式碼範例。',
+    },
+    pricing: {
+      title: 'API 定價 | 中國國內價 7 折',
+      desc: 'DoTouch.AI API 定價 – 預儲值、按 token 計量,僅中國國內價 7 折。支援 MoMo、ZaloPay、Stripe 支付。',
+    },
+    catalog: {
+      title: '模型大全 · 236 個模型 | DoTouch.AI',
+      desc: 'DoTouch.AI 上阿里雲百煉全部 236 個在售模型。按需求與預算找到合適模型。',
+    },
+    docs: {
+      title: 'API 文件 | DoTouch.AI',
+      desc: 'DoTouch.AI API 文件:快速開始、API 參考、錯誤碼與計費規則。',
+    },
+  },
+};
+
+/** 构建多语言 alternates(canonical + hreflang 4 语言 + x-default) */
+export function alternatesFor(path: string, locale: string): Metadata['alternates'] {
+  const base = siteUrl();
+  const languages: Record<string, string> = {};
+  for (const l of LOCALES) {
+    languages[l] = `${base}/${l}${path}`;
+  }
+  languages['x-default'] = `${base}/vi${path}`;
+  return { canonical: `${base}/${locale}${path}`, languages };
+}
+
+/** 取当前语言某页面的 title/description */
+export function pageMeta(locale: string, page: string): PageMeta {
+  return (SEO_META[locale as Locale]?.[page] ?? SEO_META.en[page]) as PageMeta;
+}
+
+/** 模型 ID → 展示名(SEO 标题用) */
+export const MODEL_NAMES: Record<string, string> = {
+  'glm-5.2': 'GLM 5.2',
+  'deepseek-v4': 'DeepSeek V4',
+  'deepseek-v4-pro': 'DeepSeek V4 Pro',
+  'qwen-max': 'Qwen Max',
+};

Fichier diff supprimé car celui-ci est trop grand
+ 0 - 0
apps/web/tsconfig.tsbuildinfo


+ 18 - 0
docs/CHANGELOG.md

@@ -1,5 +1,23 @@
 # DoTouch.AI Changelog
 
+# DoTouch.AI Changelog
+
+## v1.3.0 (seo) — 2026-08-09
+**P1 技术 SEO 落地(方案:docs/seo/seo-strategy-v1.0.md)**
+- Metadata 全页:4 语言 title/description/canonical/OG/Twitter(lib/seo.ts 多语言元数据表)
+- hreflang:4 语言 + x-default(metadata.alternates.languages)
+- sitemap.xml(36 条目:5 路由×4 语言 + 4 模型×4 语言)+ robots.txt(排除 /admin /api /console)
+- IndexNow API 路由(/api/indexnow,INDEXNOW_KEY 守护)
+- JSON-LD:Organization + WebSite(全站)+ Product(模型详情,真实定价)+ BreadcrumbList(子页)——无 FAQPage(Google 已停富结果)
+- SSG 静态化:47/47 静态页(TTFB ~13ms)
+- OG 图复用 B1 质感稿(public/og.png)
+- 性能基线:docs/seo/performance-baseline-v1.3.0.md
+- 站点 URL:NEXT_PUBLIC_SITE_URL(演示 localhost / 生产 dotouch.ai)
+
+## v1.2.0 (demo) — 2026-08-09
+
+
+
 ## v1.2.0 (demo) — 2026-08-09
 **里程碑:全功能本地演示版**(Docker 演示环境可完整走通)
 - 官网全站 8 路由落地 Next.js(首页/模型/详情/定价/模型大全 236/文档/控制台/管理后台)

+ 35 - 0
docs/seo/performance-baseline-v1.3.0.md

@@ -0,0 +1,35 @@
+# DoTouch.AI 性能基线 v1.3.0(P1 SEO 落地后)
+
+> 日期:2026-08-09 | 环境:生产构建(`pnpm build`)本地 `next start`
+> 方法:SSG 静态页面实测(HTTP 计时);完整 Lighthouse/CrUX 需生产域名部署后补充
+
+## 核心数据
+
+| 指标 | 实测值 | 目标 | 状态 |
+|---|---|---|---|
+| TTFB(/vi 首页) | **12-15ms**(3 次实测 12.3/13.3/14.7ms,取中位 13ms) | < 100ms | ✅ |
+| 渲染模式 | 全站 **SSG 静态**(47/47 静态页) | ○ 静态 | ✅ |
+| 首页 JS 首载 | 118 kB(含框架) | — | 待优化 |
+| 控制台页 JS | 114 kB | — | 待优化 |
+
+## 路由包体(build 输出)
+
+| 路由 | First Load JS |
+|---|---|
+| /[locale] 首页 | 118 kB |
+| /[locale]/models | 119 kB |
+| /[locale]/models/[id] | ~119 kB |
+| /[locale]/pricing/catalog | 121 kB |
+| /[locale]/console | 114 kB |
+| /admin | 105 kB |
+
+## 说明与后续
+
+1. **TTFB 极优**(SSG 静态 + 无外部阻塞)——LCP 主要取决于客户端 JS 解析与图片加载
+2. **Lighthouse 完整审计**:需生产域名(`https://dotouch.ai`)部署后,用 Playwright+Chromium 实测 mobile/desktop 各 3-5 轮取中位数(方法见 frontend skill perfection 规则集)
+3. **CrUX 现场数据**:上线后通过 Search Console / PageSpeed Insights 获取(25 周趋势)
+4. 已知优化项(后续 P4):OG 图 4MB 偏大(可压至 <200KB 提升 LCP 的 image decode);控制台 JS 按需分割
+
+## 变更影响
+
+- SSG 静态化使所有页面即时返回(TTFB 13ms 级),显著利于抓取预算与用户体验

+ 1 - 1
docs/seo/seo-strategy-v1.0.md

@@ -1,6 +1,6 @@
 # DoTouch.AI SEO 方案 v1.1(定稿)
 
-> 日期:2026-08-09 | 状态:**方案定稿 ✅(2026-08-09 决策确认,尚未执行代码)**
+> 日期:2026-08-09 | 状态:**P1 已执行 ✅(v1.3.0 落地,2026-08-09)| P2/P3 待执行**
 > 适用站点:Next.js 14 多语言站(vi/en/zh-CN/zh-TW)+ 管理后台(/admin,无 SEO 需求)
 > 主目标市场:**越南**(次:东南亚/全球英文开发者)
 

Certains fichiers n'ont pas été affichés car il y a eu trop de fichiers modifiés dans ce diff