新增内容引擎文章发布接口与详情页

This commit is contained in:
amb
2026-09-11 17:19:09 +08:00
parent ba6aab722e
commit 726485dccb
16 changed files with 958 additions and 102 deletions
@@ -0,0 +1,75 @@
import type { Metadata } from 'next';
import { notFound } from 'next/navigation';
import { articleUrl, getPublishedArticle } from '@/lib/articles';
const siteUrl = (process.env.NEXT_PUBLIC_SITE_URL || process.env.PUBLIC_SITE_URL || 'https://www.aiameiba.com').replace(/\/$/, '');
const formatDate = (value: string) => {
const date = new Date(value);
return Number.isNaN(date.getTime()) ? value : date.toLocaleDateString('zh-CN');
};
const paragraphs = (body: string) => body.split(/\n\s*\n/).map(item => item.trim()).filter(Boolean);
export async function generateMetadata({ params }: { params: { identifier: string } }): Promise<Metadata> {
const article = await getPublishedArticle(params.identifier);
if (!article) return { title: '文章不存在' };
const localUrl = `${siteUrl}${articleUrl(article)}`;
const canonical = article.canonical_url || localUrl;
return {
title: article.title,
description: article.summary,
alternates: { canonical },
openGraph: {
type: 'article',
title: article.title,
description: article.summary,
url: canonical,
images: article.cover_image_url ? [{ url: article.cover_image_url }] : undefined,
publishedTime: article.published_at,
modifiedTime: article.updated_at,
authors: [article.author_name],
},
};
}
export default async function KnowledgeArticlePage({ params }: { params: { identifier: string } }) {
const article = await getPublishedArticle(params.identifier);
if (!article) notFound();
const localUrl = `${siteUrl}${articleUrl(article)}`;
const canonical = article.canonical_url || localUrl;
const jsonLd = {
'@context': 'https://schema.org',
'@type': 'Article',
headline: article.title,
description: article.summary,
url: canonical,
mainEntityOfPage: localUrl,
author: { '@type': 'Organization', name: article.author_name },
publisher: { '@type': 'Organization', name: '阿美巴AI', url: siteUrl },
datePublished: article.published_at,
dateModified: article.updated_at,
image: article.cover_image_url || undefined,
articleSection: article.category,
};
return (
<div className="container-narrow">
<nav className="breadcrumb" aria-label="面包屑导航">
<ol><li><a href="/">首页</a></li><li><a href="/knowledge">知识中心</a></li><li><span aria-current="page">{article.title}</span></li></ol>
</nav>
<article className="knowledge-article">
<script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }} />
<header className="knowledge-article-header">
<p className="article-meta"><span>{article.category}</span><span>{article.author_name}</span><time dateTime={article.published_at}>发布于 {formatDate(article.published_at)}</time><time dateTime={article.updated_at}>更新于 {formatDate(article.updated_at)}</time></p>
<h1>{article.title}</h1>
{article.summary && <p className="knowledge-article-summary">{article.summary}</p>}
</header>
<div className="knowledge-article-body">
{paragraphs(article.body_plain).map((paragraph, index) => <p key={index}>{paragraph}</p>)}
</div>
<footer className="knowledge-article-footer"><a href="/knowledge">← 返回知识中心</a></footer>
</article>
</div>
);
}
+30 -93
View File
@@ -1,134 +1,71 @@
import type { Metadata } from 'next';
import { generateSEO, generateBreadcrumbSchema } from '@/lib/seo';
import { getSiteSettings } from '@/lib/settings';
import { articleUrl, getPublishedArticles } from '@/lib/articles';
const API_URL = process.env.API_URL || 'http://localhost:24731';
interface ArticleItem { id: string; title: string; slug: string; category: string; summary: string; author: string; date: string; }
async function getArticles(): Promise<ArticleItem[]> {
try {
const res = await fetch(`${API_URL}/api/contents/article?pageSize=100`, { cache: 'no-store' });
if (!res.ok) return [];
const data = await res.json();
return (data.list || []).map((item: any) => ({ ...item, id: item.id || item.content_id, summary: item.summary || item.body || '', date: item.date || item.created_at || '' }));
} catch { return []; }
}
const categories = ['通用深度', '百科认知', '行业观察', '方法论', '问答', '指南干货'];
export const metadata: Metadata = {
...generateSEO({
title: '知识中心',
description: '企业进化的知识体系与方法论,包括企业进化、AI时代企业、企业经营等分类',
description: '企业进化的知识体系与方法论,包括通用深度、行业观察、方法论和指南干货。',
canonical: '/knowledge',
}),
};
const formatDate = (value: string) => {
const date = new Date(value);
return Number.isNaN(date.getTime()) ? '' : date.toLocaleDateString('zh-CN');
};
export default async function KnowledgePage() {
const settings = await getSiteSettings();
const articles = await getArticles();
const [settings, articles] = await Promise.all([getSiteSettings(), getPublishedArticles()]);
const breadcrumbs = [
{ name: '首页', url: '/' },
{ name: '知识中心', url: '/knowledge' },
];
// 知识分类
const categories = [
{ id: 'evolution', name: '企业进化', count: 0 },
{ id: 'ai-era', name: 'AI时代企业', count: 0 },
{ id: 'management', name: '企业经营', count: 0 },
{ id: 'organization', name: '组织重塑', count: 0 },
{ id: 'values', name: '四大价值', count: 0 },
{ id: 'ai-organization', name: 'AI组织', count: 0 },
{ id: 'enterprise-management', name: '企业管理', count: 0 },
{ id: 'methodology', name: '方法论', count: 0 },
{ id: 'cases', name: '案例', count: 0 },
{ id: 'observation', name: '企业观察', count: 0 },
{ id: 'viewpoint', name: '阿美巴观点', count: 0 },
{ id: 'faq', name: 'FAQ', count: 0 },
];
// 文章列表 - 从CMS获取published状态的文章
return (
<>
{/* 结构化数据 */}
<script
type="application/ld+json"
dangerouslySetInnerHTML={{
__html: JSON.stringify(generateBreadcrumbSchema(breadcrumbs)),
}}
/>
<script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(generateBreadcrumbSchema(breadcrumbs)) }} />
<div className="container">
{/* 面包屑导航 */}
<nav className="breadcrumb" aria-label="面包屑导航">
<ol>
{breadcrumbs.map((item, index) => (
<li key={index}>
{index < breadcrumbs.length - 1 ? (
<a href={item.url}>{item.name}</a>
) : (
<span aria-current="page">{item.name}</span>
)}
</li>
))}
</ol>
<ol><li><a href="/">首页</a></li><li><span aria-current="page">知识中心</span></li></ol>
</nav>
<h1>{settings['knowledge.title'] || '知识中心'}</h1>
<section className="content-section">
<h2>{settings['knowledge.systemTitle'] || '知识体系'}</h2>
<p>
{settings['knowledge.intro'] || '知识中心汇集了企业进化的知识体系与方法论,帮助企业理解和掌握AI时代的经营管理方法。'}
</p>
<p>{settings['knowledge.intro'] || '知识中心汇集了企业进化的知识体系与方法论,帮助企业理解和掌握AI时代的经营管理方法。'}</p>
</section>
<section className="content-section">
<h2>{settings['knowledge.categoryTitle'] || '知识分类'}</h2>
<section className="content-section" aria-labelledby="knowledge-categories">
<h2 id="knowledge-categories">{settings['knowledge.categoryTitle'] || '知识分类'}</h2>
<div className="category-grid">
{categories.map((category) => (
<div key={category.id} className="category-card">
<h3>{category.name}</h3>
<p>{category.count} 篇文章</p>
</div>
))}
{categories.map((category) => {
const count = articles.filter(article => article.category === category).length;
return <div key={category} className="category-card"><h3>{category}</h3><p>{count} 篇文章</p></div>;
})}
</div>
</section>
<section className="content-section">
<h2>{settings['knowledge.articleTitle'] || '最新文章'}</h2>
{articles.length > 0 ? (
<section className="content-section" aria-labelledby="knowledge-articles">
<h2 id="knowledge-articles">{settings['knowledge.articleTitle'] || '最新文章'}</h2>
{articles.length ? (
<div className="articles-list">
{articles.map((article) => (
<div key={article.id} className="article-card">
<h3>
<a href={`/knowledge/${article.slug}`}>{article.title}</a>
</h3>
<p className="article-meta">
<span>{article.category}</span>
<span>{article.author}</span>
<span>{article.date}</span>
</p>
<article key={article.content_id} className="article-card">
<p className="article-meta"><span>{article.category}</span><span>{article.author_name}</span><span>{formatDate(article.published_at)}</span></p>
<h3><a href={articleUrl(article)}>{article.title}</a></h3>
<p>{article.summary}</p>
</div>
<a className="article-read-more" href={articleUrl(article)}>阅读全文</a>
</article>
))}
</div>
) : (
<div className="empty-state">
<p>暂无文章数据</p>
<p>文章内容正在完善中,请稍后再来查看。</p>
</div>
<div className="empty-state"><p>暂无已发布文章</p><p>文章内容正在完善中,请稍后再来查看。</p></div>
)}
</section>
<section className="related-section">
<h2>相关知识</h2>
<ul>
<li><a href="/evolution">企业进化</a></li>
<li><a href="/value">四大价值</a></li>
<li><a href="/concepts">概念</a></li>
<li><a href="/faq">FAQ</a></li>
</ul>
</section>
</div>
</>
);
+20
View File
@@ -0,0 +1,20 @@
import { getPublishedArticles, articleUrl } from '@/lib/articles';
export async function GET() {
const articles = await getPublishedArticles();
const baseUrl = (process.env.PUBLIC_SITE_URL || process.env.NEXT_PUBLIC_SITE_URL || 'https://www.aiameiba.com').replace(/\/$/, '');
const lines = [
'# 阿美巴AI官网已发布文章索引',
'',
'以下内容为文章标题、摘要和稳定详情地址;正文请访问对应详情页。',
'',
...articles.flatMap(article => [
`## ${article.title}`,
`- 地址:${baseUrl}${articleUrl(article)}`,
`- 分类:${article.category}`,
`- 摘要:${article.summary}`,
'',
]),
];
return new Response(lines.join('\n'), { headers: { 'Content-Type': 'text/plain; charset=utf-8' } });
}
+19
View File
@@ -0,0 +1,19 @@
import { getPublishedArticles, articleUrl } from '@/lib/articles';
export async function GET() {
const articles = await getPublishedArticles();
const baseUrl = (process.env.PUBLIC_SITE_URL || process.env.NEXT_PUBLIC_SITE_URL || 'https://www.aiameiba.com').replace(/\/$/, '');
const lines = [
'# 阿美巴AI官网',
'',
'> AI时代企业进化平台,提供企业进化相关的知识与方法。',
'',
'## 主要入口',
`- [知识中心](${baseUrl}/knowledge): 已发布文章索引。`,
`- [站点地图](${baseUrl}/sitemap.xml): 公开页面与文章 URL。`,
'',
'## 最新文章',
...articles.slice(0, 20).map(article => `- [${article.title}](${baseUrl}${articleUrl(article)}): ${article.summary}`),
];
return new Response(lines.join('\n'), { headers: { 'Content-Type': 'text/plain; charset=utf-8' } });
}
+11 -7
View File
@@ -1,8 +1,9 @@
import { MetadataRoute } from 'next';
import { articleUrl, getPublishedArticles } from '@/lib/articles';
const BASE_URL = process.env.NEXT_PUBLIC_SITE_URL || 'https://ameiba.com';
const BASE_URL = (process.env.NEXT_PUBLIC_SITE_URL || process.env.PUBLIC_SITE_URL || 'https://www.aiameiba.com').replace(/\/$/, '');
export default function sitemap(): MetadataRoute.Sitemap {
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
// 静态页面
const staticPages = [
{ url: BASE_URL, lastModified: new Date(), changeFrequency: 'weekly' as const, priority: 1.0 },
@@ -27,10 +28,13 @@ export default function sitemap(): MetadataRoute.Sitemap {
{ url: `${BASE_URL}/contact`, lastModified: new Date(), changeFrequency: 'monthly' as const, priority: 0.6 },
];
// 动态页面(从CMS获取)
// 这里需要在实际部署时从API获取数据
// 暂时返回静态页面
const dynamicPages: MetadataRoute.Sitemap = [];
const articles = await getPublishedArticles();
const dynamicPages: MetadataRoute.Sitemap = articles.map(article => ({
url: `${BASE_URL}${articleUrl(article)}`,
lastModified: new Date(article.updated_at || article.published_at),
changeFrequency: 'monthly' as const,
priority: 0.7,
}));
return [...staticPages, ...dynamicPages];
}
}
+31
View File
@@ -0,0 +1,31 @@
import type { PublicArticle } from '@/types';
const API_URL = process.env.API_URL || 'http://localhost:24731';
export async function getPublishedArticles(): Promise<PublicArticle[]> {
try {
const response = await fetch(`${API_URL}/api/public/articles?pageSize=100`, {
next: { revalidate: 60 },
});
if (!response.ok) return [];
const data = await response.json();
return data.list || [];
} catch {
return [];
}
}
export async function getPublishedArticle(identifier: string): Promise<PublicArticle | null> {
try {
const response = await fetch(`${API_URL}/api/public/articles/${encodeURIComponent(identifier)}`, {
next: { revalidate: 60 },
});
if (!response.ok) return null;
return response.json();
} catch {
return null;
}
}
export const articleUrl = (article: Pick<PublicArticle, 'content_id' | 'slug'>) =>
`/knowledge/${article.slug || article.content_id}`;
+50
View File
@@ -610,6 +610,56 @@ body {
gap: 12px;
}
.article-meta {
display: flex;
flex-wrap: wrap;
gap: 6px 14px;
color: var(--color-ink-soft);
}
.article-read-more {
display: inline-block;
margin-top: 6px;
color: var(--color-accent);
font-weight: 700;
}
.knowledge-article {
padding: 24px 0 80px;
}
.knowledge-article-header {
padding: 22px 0 32px;
border-bottom: 1px solid var(--color-line);
}
.knowledge-article-header h1 {
margin: 16px 0;
font-size: clamp(2rem, 5vw, 3.25rem);
letter-spacing: -0.055em;
}
.knowledge-article-summary {
margin: 0;
color: var(--color-ink-soft);
font-size: 1.1rem;
}
.knowledge-article-body {
padding: 38px 0;
font-size: 1.08rem;
}
.knowledge-article-body p {
margin-bottom: 1.35em;
white-space: pre-wrap;
}
.knowledge-article-footer {
padding-top: 22px;
border-top: 1px solid var(--color-line);
}
.faq-item {
padding: 22px 24px;
border: 1px solid var(--color-line);
+21 -1
View File
@@ -56,6 +56,26 @@ export interface Article extends Content {
author?: string;
}
export interface PublicArticle {
content_id: string;
slug?: string | null;
title: string;
body_plain: string;
summary: string;
category: string;
author_name: string;
cover_image_url?: string | null;
canonical_url?: string | null;
primary_keyword?: string | null;
direction?: string | null;
target_audience?: string | null;
revision: number;
published_at: string;
updated_at: string;
entities: string[];
questions: string[];
}
export interface Case extends Content {
content_type: 'case';
client_name?: string;
@@ -124,4 +144,4 @@ export interface JsonLdData {
'@context': string;
'@type': string;
[key: string]: any;
}
}