feat: wire Docs and Blog pages to markdown content pipeline
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,14 +1,45 @@
|
|||||||
|
import { useEffect, useState } from 'react';
|
||||||
import { useParams } from 'react-router-dom';
|
import { useParams } from 'react-router-dom';
|
||||||
import SEOHead from '../components/seo/SEOHead';
|
import SEOHead from '../components/seo/SEOHead';
|
||||||
|
import { useLanguage } from '../contexts/LanguageContext';
|
||||||
|
import { loadContent, type ContentItem } from '../lib/content';
|
||||||
|
|
||||||
export default function BlogDetailPage() {
|
export default function BlogDetailPage() {
|
||||||
const { slug } = useParams<{ slug: string }>();
|
const { slug } = useParams<{ slug: string }>();
|
||||||
|
const { language } = useLanguage();
|
||||||
|
const [content, setContent] = useState<ContentItem | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (slug) {
|
||||||
|
loadContent('blog', language, slug)
|
||||||
|
.then(setContent)
|
||||||
|
.catch(() => setContent(null));
|
||||||
|
}
|
||||||
|
}, [slug, language]);
|
||||||
|
|
||||||
|
if (!content) {
|
||||||
|
return (
|
||||||
|
<div className="max-w-4xl mx-auto py-16 px-6">
|
||||||
|
<p className="text-gray-600">{language === 'en' ? 'Loading...' : '加载中...'}</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<SEOHead title={slug ?? 'Blog'} description={`Blog post: ${slug}`} path={`/blog/${slug}`} type="article" />
|
<SEOHead
|
||||||
|
title={content.meta.title}
|
||||||
|
description={content.meta.description}
|
||||||
|
path={`/blog/${slug}`}
|
||||||
|
type="article"
|
||||||
|
publishedTime={content.meta.date}
|
||||||
|
/>
|
||||||
<div className="max-w-4xl mx-auto py-16 px-6">
|
<div className="max-w-4xl mx-auto py-16 px-6">
|
||||||
<h1 className="text-3xl font-bold text-gray-900 mb-4 capitalize">{slug?.replace(/-/g, ' ')}</h1>
|
<div className="text-sm text-gray-400 mb-4">{content.meta.date}</div>
|
||||||
<p className="text-gray-600">Content coming soon.</p>
|
<article
|
||||||
|
className="prose prose-gray max-w-none"
|
||||||
|
dangerouslySetInnerHTML={{ __html: content.html }}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,20 +1,19 @@
|
|||||||
|
import { useEffect, useState } from 'react';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import SEOHead from '../components/seo/SEOHead';
|
import SEOHead from '../components/seo/SEOHead';
|
||||||
import { useLanguage } from '../contexts/LanguageContext';
|
import { useLanguage } from '../contexts/LanguageContext';
|
||||||
|
import { loadManifest, type ContentMeta } from '../lib/content';
|
||||||
const posts = [
|
|
||||||
{
|
|
||||||
slug: 'introducing-texpixel',
|
|
||||||
title: 'Introducing TexPixel',
|
|
||||||
titleZh: 'TexPixel 介绍',
|
|
||||||
description: 'Meet TexPixel — your AI-powered formula recognition tool',
|
|
||||||
descriptionZh: '认识 TexPixel — 你的 AI 公式识别工具',
|
|
||||||
date: '2026-03-25',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
export default function BlogListPage() {
|
export default function BlogListPage() {
|
||||||
const { language } = useLanguage();
|
const { language } = useLanguage();
|
||||||
|
const [posts, setPosts] = useState<ContentMeta[]>([]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
loadManifest('blog').then(manifest => {
|
||||||
|
setPosts(manifest[language] || []);
|
||||||
|
});
|
||||||
|
}, [language]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<SEOHead title="Blog" description="TexPixel blog — updates, tutorials, and insights" path="/blog" />
|
<SEOHead title="Blog" description="TexPixel blog — updates, tutorials, and insights" path="/blog" />
|
||||||
@@ -24,8 +23,15 @@ export default function BlogListPage() {
|
|||||||
{posts.map((post) => (
|
{posts.map((post) => (
|
||||||
<Link key={post.slug} to={`/blog/${post.slug}`} className="block p-6 bg-gray-50 rounded-lg hover:bg-gray-100 transition-colors">
|
<Link key={post.slug} to={`/blog/${post.slug}`} className="block p-6 bg-gray-50 rounded-lg hover:bg-gray-100 transition-colors">
|
||||||
<div className="text-xs text-gray-400 mb-2">{post.date}</div>
|
<div className="text-xs text-gray-400 mb-2">{post.date}</div>
|
||||||
<h2 className="text-lg font-semibold text-gray-900">{language === 'en' ? post.title : post.titleZh}</h2>
|
<h2 className="text-lg font-semibold text-gray-900">{post.title}</h2>
|
||||||
<p className="text-gray-600 mt-1 text-sm">{language === 'en' ? post.description : post.descriptionZh}</p>
|
<p className="text-gray-600 mt-1 text-sm">{post.description}</p>
|
||||||
|
{post.tags.length > 0 && (
|
||||||
|
<div className="flex gap-2 mt-3">
|
||||||
|
{post.tags.map(tag => (
|
||||||
|
<span key={tag} className="text-xs bg-gray-200 text-gray-600 px-2 py-0.5 rounded">{tag}</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</Link>
|
</Link>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,14 +1,42 @@
|
|||||||
|
import { useEffect, useState } from 'react';
|
||||||
import { useParams } from 'react-router-dom';
|
import { useParams } from 'react-router-dom';
|
||||||
import SEOHead from '../components/seo/SEOHead';
|
import SEOHead from '../components/seo/SEOHead';
|
||||||
|
import { useLanguage } from '../contexts/LanguageContext';
|
||||||
|
import { loadContent, type ContentItem } from '../lib/content';
|
||||||
|
|
||||||
export default function DocDetailPage() {
|
export default function DocDetailPage() {
|
||||||
const { slug } = useParams<{ slug: string }>();
|
const { slug } = useParams<{ slug: string }>();
|
||||||
|
const { language } = useLanguage();
|
||||||
|
const [content, setContent] = useState<ContentItem | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (slug) {
|
||||||
|
loadContent('docs', language, slug)
|
||||||
|
.then(setContent)
|
||||||
|
.catch(() => setContent(null));
|
||||||
|
}
|
||||||
|
}, [slug, language]);
|
||||||
|
|
||||||
|
if (!content) {
|
||||||
|
return (
|
||||||
|
<div className="max-w-4xl mx-auto py-16 px-6">
|
||||||
|
<p className="text-gray-600">{language === 'en' ? 'Loading...' : '加载中...'}</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<SEOHead title={slug ?? 'Doc'} description={`Documentation: ${slug}`} path={`/docs/${slug}`} />
|
<SEOHead
|
||||||
|
title={content.meta.title}
|
||||||
|
description={content.meta.description}
|
||||||
|
path={`/docs/${slug}`}
|
||||||
|
/>
|
||||||
<div className="max-w-4xl mx-auto py-16 px-6">
|
<div className="max-w-4xl mx-auto py-16 px-6">
|
||||||
<h1 className="text-3xl font-bold text-gray-900 mb-4 capitalize">{slug?.replace(/-/g, ' ')}</h1>
|
<article
|
||||||
<p className="text-gray-600">Content coming soon.</p>
|
className="prose prose-gray max-w-none"
|
||||||
|
dangerouslySetInnerHTML={{ __html: content.html }}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,13 +1,19 @@
|
|||||||
|
import { useEffect, useState } from 'react';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import SEOHead from '../components/seo/SEOHead';
|
import SEOHead from '../components/seo/SEOHead';
|
||||||
import { useLanguage } from '../contexts/LanguageContext';
|
import { useLanguage } from '../contexts/LanguageContext';
|
||||||
|
import { loadManifest, type ContentMeta } from '../lib/content';
|
||||||
const docs = [
|
|
||||||
{ slug: 'getting-started', title: 'Getting Started', titleZh: '快速开始', description: 'Learn how to use TexPixel for formula recognition', descriptionZh: '了解如何使用 TexPixel 进行公式识别' },
|
|
||||||
];
|
|
||||||
|
|
||||||
export default function DocsListPage() {
|
export default function DocsListPage() {
|
||||||
const { language } = useLanguage();
|
const { language } = useLanguage();
|
||||||
|
const [docs, setDocs] = useState<ContentMeta[]>([]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
loadManifest('docs').then(manifest => {
|
||||||
|
setDocs(manifest[language] || []);
|
||||||
|
});
|
||||||
|
}, [language]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<SEOHead title="Documentation" description="TexPixel documentation and guides" path="/docs" />
|
<SEOHead title="Documentation" description="TexPixel documentation and guides" path="/docs" />
|
||||||
@@ -16,8 +22,8 @@ export default function DocsListPage() {
|
|||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{docs.map((doc) => (
|
{docs.map((doc) => (
|
||||||
<Link key={doc.slug} to={`/docs/${doc.slug}`} className="block p-6 bg-gray-50 rounded-lg hover:bg-gray-100 transition-colors">
|
<Link key={doc.slug} to={`/docs/${doc.slug}`} className="block p-6 bg-gray-50 rounded-lg hover:bg-gray-100 transition-colors">
|
||||||
<h2 className="text-lg font-semibold text-gray-900">{language === 'en' ? doc.title : doc.titleZh}</h2>
|
<h2 className="text-lg font-semibold text-gray-900">{doc.title}</h2>
|
||||||
<p className="text-gray-600 mt-1 text-sm">{language === 'en' ? doc.description : doc.descriptionZh}</p>
|
<p className="text-gray-600 mt-1 text-sm">{doc.description}</p>
|
||||||
</Link>
|
</Link>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user