feat(seo): 结构化数据/每页元数据/列表页metadata/hreflang修正/OG分享图
- 详情页注入 Article+BreadcrumbList JSON-LD,工具页 SoftwareApplication - 课程/技能/文章/练习 动态 generateMetadata(canonical/OG) - 新增 skills/contents/practices/prompts/models 列表页 metadata - 修正 hreflang(删除不存在的/en),用 next/og 生成 OG 分享图
This commit is contained in:
@@ -48,6 +48,19 @@
|
|||||||
|
|
||||||
> **说明:** 后端 `orders` / `payment` 模块代码保留(取证后可复用),但前端已不再调用付费购买流程。
|
> **说明:** 后端 `orders` / `payment` 模块代码保留(取证后可复用),但前端已不再调用付费购买流程。
|
||||||
|
|
||||||
|
### SEO 基础优化 — v1.2.0
|
||||||
|
|
||||||
|
| 项目 | 改动 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| 结构化数据 | 详情页注入 JSON-LD | 课程/技能/文章/练习:`Article` + `BreadcrumbList`;工具:`SoftwareApplication`(含免费 Offer) |
|
||||||
|
| 每页元数据 | 详情页 `generateMetadata` | 课程/技能/文章/练习动态生成 unique title/description + canonical + OG |
|
||||||
|
| 列表页元数据 | 新增 `skills/contents/practices/prompts/models` 的 `layout.tsx` | 补齐各频道独立 title/description/canonical,消除与首页重复 |
|
||||||
|
| hreflang | 修正 `layout.tsx` | 删除不存在的 `/en` 备用语言,仅保留 `zh-CN` 自引用 |
|
||||||
|
| OG 分享图 | 新增 `app/opengraph-image.tsx` | 用 `next/og` 在构建期生成 1200x630 分享图(Latin 文案,规避 CJK 字体缺失),替换原缺失的 `/images/og-image.png` |
|
||||||
|
| 工具页 | 增强 `SoftwareApplication` JSON-LD | 补充 publisher / offers(免费) / operatingSystem |
|
||||||
|
|
||||||
|
> **已知最大盲点(待办):** `courses/[id]`、`skills/[id]`、`contents/[id]`、`practices/[id]` 的**正文内容仍在客户端 `useEffect` 拉取**,静态 HTML 中无正文,百度等弱 JS 渲染的搜索引擎抓不到实质内容(`tools/[slug]` 已是服务端渲染,正常)。需将详情页改为「服务端取数 → 作为 `initialData` 传入客户端组件」以把正文写入静态 HTML。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 待办(下一阶段)
|
## 待办(下一阶段)
|
||||||
|
|||||||
@@ -1,4 +1,17 @@
|
|||||||
import { API_BASE } from '@/lib/config';
|
import type { Metadata } from 'next';
|
||||||
|
import { API_BASE, SITE_URL } from '@/lib/config';
|
||||||
|
import { articleJsonLd, breadcrumbJsonLd, absoluteUrl } from '@/lib/seo';
|
||||||
|
import ContentDetailClient from './client';
|
||||||
|
|
||||||
|
async function getContent(id: string) {
|
||||||
|
try {
|
||||||
|
const res = await fetch(`${API_BASE}/contents/${id}`, { next: { revalidate: 3600 } });
|
||||||
|
if (!res.ok) return null;
|
||||||
|
return res.json();
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export async function generateStaticParams() {
|
export async function generateStaticParams() {
|
||||||
try {
|
try {
|
||||||
@@ -12,8 +25,45 @@ export async function generateStaticParams() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
import ContentDetailClient from './client';
|
export async function generateMetadata({ params }: { params: { id: string } }): Promise<Metadata> {
|
||||||
|
const content = await getContent(params.id);
|
||||||
export default function ContentDetailPage() {
|
if (!content) return { title: '文章未找到' };
|
||||||
return <ContentDetailClient />;
|
const title = content.title;
|
||||||
|
const description = content.summary || content.content?.slice(0, 120) || `${content.title} - 宇之然 AI 文章`;
|
||||||
|
const url = absoluteUrl(`/contents/${content.id}`);
|
||||||
|
return {
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
alternates: { canonical: `/contents/${content.id}` },
|
||||||
|
openGraph: { type: 'article', title, description, url },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function ContentDetailPage({ params }: { params: { id: string } }) {
|
||||||
|
const content = await getContent(params.id);
|
||||||
|
const url = absoluteUrl(`/contents/${params.id}`);
|
||||||
|
const jsonLd = content
|
||||||
|
? [
|
||||||
|
articleJsonLd({
|
||||||
|
headline: content.title,
|
||||||
|
description: content.summary || undefined,
|
||||||
|
url,
|
||||||
|
datePublished: content.publishedAt || content.createdAt,
|
||||||
|
}),
|
||||||
|
breadcrumbJsonLd([
|
||||||
|
{ name: '首页', url: SITE_URL },
|
||||||
|
{ name: '文章', url: absoluteUrl('/contents') },
|
||||||
|
{ name: content.title, url },
|
||||||
|
]),
|
||||||
|
]
|
||||||
|
: null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{jsonLd && (
|
||||||
|
<script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }} />
|
||||||
|
)}
|
||||||
|
<ContentDetailClient />
|
||||||
|
</>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import type { Metadata } from 'next';
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: 'AI 文章与教程 - 宇之然 AI',
|
||||||
|
description: '宇之然 AI 精选文章与实战教程,涵盖 AI 通识、提示词工程、智能体、模型百科等,帮助你系统掌握人工智能。',
|
||||||
|
alternates: { canonical: '/contents' },
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function ContentsLayout({ children }: { children: React.ReactNode }) {
|
||||||
|
return children;
|
||||||
|
}
|
||||||
@@ -1,4 +1,17 @@
|
|||||||
import { API_BASE } from '@/lib/config';
|
import type { Metadata } from 'next';
|
||||||
|
import { API_BASE, SITE_URL } from '@/lib/config';
|
||||||
|
import { articleJsonLd, breadcrumbJsonLd, absoluteUrl } from '@/lib/seo';
|
||||||
|
import CourseDetailClient from './client';
|
||||||
|
|
||||||
|
async function getCourse(id: string) {
|
||||||
|
try {
|
||||||
|
const res = await fetch(`${API_BASE}/courses/${id}`, { next: { revalidate: 3600 } });
|
||||||
|
if (!res.ok) return null;
|
||||||
|
return res.json();
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export async function generateStaticParams() {
|
export async function generateStaticParams() {
|
||||||
try {
|
try {
|
||||||
@@ -12,8 +25,49 @@ export async function generateStaticParams() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
import CourseDetailClient from './client';
|
export async function generateMetadata({ params }: { params: { id: string } }): Promise<Metadata> {
|
||||||
|
const course = await getCourse(params.id);
|
||||||
export default function CourseDetailPage() {
|
if (!course) return { title: '课程未找到' };
|
||||||
return <CourseDetailClient />;
|
const title = course.title;
|
||||||
|
const description = course.description || `${course.title} - 宇之然 AI 课程,系统学习 AI 技能`;
|
||||||
|
const url = absoluteUrl(`/courses/${course.id}`);
|
||||||
|
return {
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
alternates: { canonical: `/courses/${course.id}` },
|
||||||
|
openGraph: {
|
||||||
|
type: 'article',
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
url,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function CourseDetailPage({ params }: { params: { id: string } }) {
|
||||||
|
const course = await getCourse(params.id);
|
||||||
|
const url = absoluteUrl(`/courses/${params.id}`);
|
||||||
|
const jsonLd = course
|
||||||
|
? [
|
||||||
|
articleJsonLd({
|
||||||
|
headline: course.title,
|
||||||
|
description: course.description || undefined,
|
||||||
|
url,
|
||||||
|
}),
|
||||||
|
breadcrumbJsonLd([
|
||||||
|
{ name: '首页', url: SITE_URL },
|
||||||
|
{ name: '课程', url: absoluteUrl('/courses') },
|
||||||
|
{ name: course.title, url },
|
||||||
|
]),
|
||||||
|
]
|
||||||
|
: null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{jsonLd && (
|
||||||
|
<script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }} />
|
||||||
|
)}
|
||||||
|
<CourseDetailClient />
|
||||||
|
</>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,7 +35,6 @@ export const metadata: Metadata = {
|
|||||||
canonical: '/',
|
canonical: '/',
|
||||||
languages: {
|
languages: {
|
||||||
'zh-CN': '/',
|
'zh-CN': '/',
|
||||||
'en': '/en',
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
openGraph: {
|
openGraph: {
|
||||||
@@ -45,20 +44,11 @@ export const metadata: Metadata = {
|
|||||||
title: '宇之然 AI - AI 工具与知识社区',
|
title: '宇之然 AI - AI 工具与知识社区',
|
||||||
description: '让每个人都能用好 AI - AI 工具指南、提示词库、智能体教程、模型百科、AI 沙盒实战',
|
description: '让每个人都能用好 AI - AI 工具指南、提示词库、智能体教程、模型百科、AI 沙盒实战',
|
||||||
url: SITE_URL,
|
url: SITE_URL,
|
||||||
images: [
|
|
||||||
{
|
|
||||||
url: '/images/og-image.png',
|
|
||||||
width: 1200,
|
|
||||||
height: 630,
|
|
||||||
alt: '宇之然 AI - AI 工具与知识社区',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
twitter: {
|
twitter: {
|
||||||
card: 'summary_large_image',
|
card: 'summary_large_image',
|
||||||
title: '宇之然 AI - AI 工具与知识社区',
|
title: '宇之然 AI - AI 工具与知识社区',
|
||||||
description: '让每个人都能用好 AI - AI 工具指南、提示词库、智能体教程、模型百科、AI 沙盒实战',
|
description: '让每个人都能用好 AI - AI 工具指南、提示词库、智能体教程、模型百科、AI 沙盒实战',
|
||||||
images: ['/images/og-image.png'],
|
|
||||||
},
|
},
|
||||||
verification: {
|
verification: {
|
||||||
google: 'your-google-verification-code',
|
google: 'your-google-verification-code',
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import type { Metadata } from 'next';
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: 'AI 模型能力导览 - 宇之然 AI',
|
||||||
|
description: '探索 AI 能为你做什么,从对话写作到编程实战,按能力场景找到适合你的 AI 模型与使用方式。',
|
||||||
|
alternates: { canonical: '/models' },
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function ModelsLayout({ children }: { children: React.ReactNode }) {
|
||||||
|
return children;
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import { ImageResponse } from 'next/og';
|
||||||
|
|
||||||
|
export const size = { width: 1200, height: 630 };
|
||||||
|
export const contentType = 'image/png';
|
||||||
|
|
||||||
|
export default function OGImage() {
|
||||||
|
return new ImageResponse(
|
||||||
|
(
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
justifyContent: 'center',
|
||||||
|
padding: '80px',
|
||||||
|
background: 'linear-gradient(135deg, #0c8ee7 0%, #6d28d9 100%)',
|
||||||
|
color: 'white',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', fontSize: 38, opacity: 0.92, letterSpacing: 1 }}>
|
||||||
|
Yuzhiran AI
|
||||||
|
</div>
|
||||||
|
<div style={{ display: 'flex', fontSize: 84, fontWeight: 800, marginTop: 28, lineHeight: 1.1 }}>
|
||||||
|
AI Tools & Knowledge
|
||||||
|
</div>
|
||||||
|
<div style={{ display: 'flex', fontSize: 84, fontWeight: 800, lineHeight: 1.1 }}>
|
||||||
|
Community
|
||||||
|
</div>
|
||||||
|
<div style={{ display: 'flex', fontSize: 34, marginTop: 30, opacity: 0.92 }}>
|
||||||
|
Tools, Prompts, Skills & Sandbox for everyone
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
{ ...size },
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,10 +1,57 @@
|
|||||||
|
import type { Metadata } from 'next';
|
||||||
|
import { API_BASE, SITE_URL } from '@/lib/config';
|
||||||
|
import { articleJsonLd, breadcrumbJsonLd, absoluteUrl } from '@/lib/seo';
|
||||||
import PracticeDetailClient from './client';
|
import PracticeDetailClient from './client';
|
||||||
|
|
||||||
|
async function getPractice(id: string) {
|
||||||
|
try {
|
||||||
|
const res = await fetch(`${API_BASE}/practices/${id}`, { next: { revalidate: 3600 } });
|
||||||
|
if (!res.ok) return null;
|
||||||
|
return res.json();
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function generateStaticParams() {
|
export function generateStaticParams() {
|
||||||
const ids = Array.from({ length: 20 }, (_, i) => i + 1);
|
const ids = Array.from({ length: 20 }, (_, i) => i + 1);
|
||||||
return ids.map(id => ({ id: String(id) }));
|
return ids.map((id) => ({ id: String(id) }));
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function PracticeDetailPage() {
|
export async function generateMetadata({ params }: { params: { id: string } }): Promise<Metadata> {
|
||||||
return <PracticeDetailClient />;
|
const practice = await getPractice(params.id);
|
||||||
|
if (!practice) return { title: '练习未找到' };
|
||||||
|
const title = practice.title;
|
||||||
|
const description = practice.description || `${practice.title} - 宇之然 AI 练习,实战演练获得即时评分`;
|
||||||
|
const url = absoluteUrl(`/practices/${practice.id}`);
|
||||||
|
return {
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
alternates: { canonical: `/practices/${practice.id}` },
|
||||||
|
openGraph: { type: 'article', title, description, url },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function PracticeDetailPage({ params }: { params: { id: string } }) {
|
||||||
|
const practice = await getPractice(params.id);
|
||||||
|
const url = absoluteUrl(`/practices/${params.id}`);
|
||||||
|
const jsonLd = practice
|
||||||
|
? [
|
||||||
|
articleJsonLd({ headline: practice.title, description: practice.description || undefined, url }),
|
||||||
|
breadcrumbJsonLd([
|
||||||
|
{ name: '首页', url: SITE_URL },
|
||||||
|
{ name: '练习', url: absoluteUrl('/practices') },
|
||||||
|
{ name: practice.title, url },
|
||||||
|
]),
|
||||||
|
]
|
||||||
|
: null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{jsonLd && (
|
||||||
|
<script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }} />
|
||||||
|
)}
|
||||||
|
<PracticeDetailClient />
|
||||||
|
</>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import type { Metadata } from 'next';
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: 'AI 实战练习 - 宇之然 AI',
|
||||||
|
description: '通过真实场景练习 AI 技能,提交答案获得即时评分与反馈,覆盖编程、写作、数据分析等多种场景。',
|
||||||
|
alternates: { canonical: '/practices' },
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function PracticesLayout({ children }: { children: React.ReactNode }) {
|
||||||
|
return children;
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import type { Metadata } from 'next';
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: 'AI 提示词库 - 宇之然 AI',
|
||||||
|
description: '宇之然 AI 提示词库收录 200+ 精选提示词模板,覆盖编程、写作、办公等场景,支持一键复制到沙盒使用。',
|
||||||
|
alternates: { canonical: '/prompts' },
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function PromptsLayout({ children }: { children: React.ReactNode }) {
|
||||||
|
return children;
|
||||||
|
}
|
||||||
@@ -1,10 +1,57 @@
|
|||||||
|
import type { Metadata } from 'next';
|
||||||
|
import { API_BASE, SITE_URL } from '@/lib/config';
|
||||||
|
import { articleJsonLd, breadcrumbJsonLd, absoluteUrl } from '@/lib/seo';
|
||||||
import SkillDetailClient from './client';
|
import SkillDetailClient from './client';
|
||||||
|
|
||||||
|
async function getSkill(id: string) {
|
||||||
|
try {
|
||||||
|
const res = await fetch(`${API_BASE}/skills/${id}`, { next: { revalidate: 3600 } });
|
||||||
|
if (!res.ok) return null;
|
||||||
|
return res.json();
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function generateStaticParams() {
|
export function generateStaticParams() {
|
||||||
const skillIds = ['general-chat', 'coding', 'writing', 'study', 'english', 'prompt-engineering', 'data-analysis', 'career'];
|
const skillIds = ['general-chat', 'coding', 'writing', 'study', 'english', 'prompt-engineering', 'data-analysis', 'career'];
|
||||||
return skillIds.map(id => ({ id }));
|
return skillIds.map((id) => ({ id }));
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function SkillDetailPage() {
|
export async function generateMetadata({ params }: { params: { id: string } }): Promise<Metadata> {
|
||||||
return <SkillDetailClient />;
|
const skill = await getSkill(params.id);
|
||||||
|
if (!skill) return { title: '技能未找到' };
|
||||||
|
const title = skill.name;
|
||||||
|
const description = skill.description || `${skill.name} - 宇之然 AI 技能,免费学习与实践`;
|
||||||
|
const url = absoluteUrl(`/skills/${skill.id}`);
|
||||||
|
return {
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
alternates: { canonical: `/skills/${skill.id}` },
|
||||||
|
openGraph: { type: 'article', title, description, url },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function SkillDetailPage({ params }: { params: { id: string } }) {
|
||||||
|
const skill = await getSkill(params.id);
|
||||||
|
const url = absoluteUrl(`/skills/${params.id}`);
|
||||||
|
const jsonLd = skill
|
||||||
|
? [
|
||||||
|
articleJsonLd({ headline: skill.name, description: skill.description || undefined, url }),
|
||||||
|
breadcrumbJsonLd([
|
||||||
|
{ name: '首页', url: SITE_URL },
|
||||||
|
{ name: '技能库', url: absoluteUrl('/skills') },
|
||||||
|
{ name: skill.name, url },
|
||||||
|
]),
|
||||||
|
]
|
||||||
|
: null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{jsonLd && (
|
||||||
|
<script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }} />
|
||||||
|
)}
|
||||||
|
<SkillDetailClient />
|
||||||
|
</>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import type { Metadata } from 'next';
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: 'AI 技能库 - 免费学习与实践',
|
||||||
|
description: '宇之然 AI 技能库收录可组合的 AI 学习技能模块,覆盖基础、技术、创意、教育等领域,免费学习并在沙盒中实战练习。',
|
||||||
|
alternates: { canonical: '/skills' },
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function SkillsLayout({ children }: { children: React.ReactNode }) {
|
||||||
|
return children;
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import { Metadata } from 'next';
|
import { Metadata } from 'next';
|
||||||
import { notFound } from 'next/navigation';
|
import { notFound } from 'next/navigation';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
|
import { SITE_URL } from '@/lib/config';
|
||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Card } from '@/components/ui/card';
|
import { Card } from '@/components/ui/card';
|
||||||
@@ -73,9 +74,17 @@ export default async function ToolDetailPage({ params }: { params: { slug: strin
|
|||||||
'@context': 'https://schema.org',
|
'@context': 'https://schema.org',
|
||||||
'@type': 'SoftwareApplication',
|
'@type': 'SoftwareApplication',
|
||||||
name: tool.name,
|
name: tool.name,
|
||||||
description: tool.description,
|
description: tool.description || '',
|
||||||
url: tool.url,
|
url: tool.url,
|
||||||
applicationCategory: 'AI工具',
|
applicationCategory: 'UtilitiesApplication',
|
||||||
|
operatingSystem: 'Web',
|
||||||
|
offers: { '@type': 'Offer', price: '0', priceCurrency: 'CNY' },
|
||||||
|
publisher: {
|
||||||
|
'@type': 'Organization',
|
||||||
|
name: '宇之然 AI',
|
||||||
|
url: SITE_URL,
|
||||||
|
logo: { '@type': 'ImageObject', url: `${SITE_URL}/favicon.png` },
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// 解析描述中的结构化字段
|
// 解析描述中的结构化字段
|
||||||
|
|||||||
@@ -0,0 +1,59 @@
|
|||||||
|
import { SITE_URL } from './config';
|
||||||
|
|
||||||
|
export function absoluteUrl(path: string): string {
|
||||||
|
if (/^https?:\/\//.test(path)) return path;
|
||||||
|
return `${SITE_URL}${path.startsWith('/') ? '' : '/'}${path}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function articleJsonLd(data: {
|
||||||
|
headline: string;
|
||||||
|
description?: string;
|
||||||
|
url: string;
|
||||||
|
datePublished?: string;
|
||||||
|
image?: string;
|
||||||
|
authorName?: string;
|
||||||
|
}) {
|
||||||
|
return {
|
||||||
|
'@context': 'https://schema.org',
|
||||||
|
'@type': 'Article',
|
||||||
|
headline: data.headline,
|
||||||
|
description: data.description || '',
|
||||||
|
url: data.url,
|
||||||
|
...(data.datePublished ? { datePublished: data.datePublished } : {}),
|
||||||
|
author: { '@type': 'Organization', name: data.authorName || '宇之然 AI', url: SITE_URL },
|
||||||
|
publisher: {
|
||||||
|
'@type': 'Organization',
|
||||||
|
name: '宇之然 AI',
|
||||||
|
url: SITE_URL,
|
||||||
|
logo: { '@type': 'ImageObject', url: `${SITE_URL}/favicon.png` },
|
||||||
|
},
|
||||||
|
...(data.image ? { image: data.image } : {}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function softwareAppJsonLd(data: { name: string; description?: string; url: string }) {
|
||||||
|
return {
|
||||||
|
'@context': 'https://schema.org',
|
||||||
|
'@type': 'SoftwareApplication',
|
||||||
|
name: data.name,
|
||||||
|
description: data.description || '',
|
||||||
|
url: data.url,
|
||||||
|
applicationCategory: 'UtilitiesApplication',
|
||||||
|
operatingSystem: 'Web',
|
||||||
|
offers: { '@type': 'Offer', price: '0', priceCurrency: 'CNY' },
|
||||||
|
publisher: { '@type': 'Organization', name: '宇之然 AI', url: SITE_URL },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function breadcrumbJsonLd(items: { name: string; url: string }[]) {
|
||||||
|
return {
|
||||||
|
'@context': 'https://schema.org',
|
||||||
|
'@type': 'BreadcrumbList',
|
||||||
|
itemListElement: items.map((it, i) => ({
|
||||||
|
'@type': 'ListItem',
|
||||||
|
position: i + 1,
|
||||||
|
name: it.name,
|
||||||
|
item: it.url,
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user