新增内容引擎文章发布接口与详情页
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -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' } });
|
||||
}
|
||||
@@ -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' } });
|
||||
}
|
||||
@@ -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];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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}`;
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user