32 KiB
Website Restructure Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Restructure the single-page OCR app into a multi-section marketing website with dedicated workspace, docs, and blog pages.
Architecture: SPA with react-router-dom layout routes. MarketingLayout (Navbar + Footer) wraps Home/Docs/Blog. AppLayout wraps the OCR workspace at /app. Markdown content compiled at build time via a Vite plugin. SEO handled by react-helmet-async + vite-plugin-prerender.
Tech Stack: React 18, react-router-dom, Tailwind CSS, react-helmet-async, vite-plugin-prerender, remark/rehype (existing), gray-matter
File Structure
src/
├── components/
│ ├── home/
│ │ ├── HeroSection.tsx — Hero with OCR demo + CTA
│ │ ├── FeaturesSection.tsx — Feature cards grid
│ │ ├── HowItWorksSection.tsx — 3-step flow
│ │ ├── PricingSection.tsx — Price cards
│ │ └── ContactSection.tsx — Contact info + form
│ ├── layout/
│ │ ├── MarketingNavbar.tsx — Full site nav
│ │ ├── AppNavbar.tsx — Workspace nav (from Navbar.tsx)
│ │ ├── Footer.tsx — Site footer
│ │ ├── MarketingLayout.tsx — MarketingNavbar + Outlet + Footer
│ │ └── AppLayout.tsx — AppNavbar + Outlet
│ └── seo/
│ └── SEOHead.tsx — react-helmet-async wrapper
├── pages/
│ ├── HomePage.tsx
│ ├── WorkspacePage.tsx — migrated from App.tsx
│ ├── DocsListPage.tsx
│ ├── DocDetailPage.tsx
│ ├── BlogListPage.tsx
│ └── BlogDetailPage.tsx
├── lib/
│ └── content.ts — Load markdown manifests
├── routes/
│ └── AppRouter.tsx — Updated with layout routes
content/
├── docs/
│ ├── en/getting-started.md
│ └── zh/getting-started.md
└── blog/
├── en/2026-03-25-introducing-texpixel.md
└── zh/2026-03-25-introducing-texpixel.md
scripts/
└── build-content.ts — Compile markdown to JSON
Task 1: Install dependencies and setup
Files:
-
Modify:
package.json -
Step 1: Install react-helmet-async and gray-matter
npm install react-helmet-async gray-matter
- Step 2: Wrap app with HelmetProvider
In src/main.tsx, add HelmetProvider wrapping:
import { HelmetProvider } from 'react-helmet-async';
// Wrap inside StrictMode:
<HelmetProvider>
<BrowserRouter>
<AuthProvider>
<LanguageProvider>
<AppRouter />
</LanguageProvider>
</AuthProvider>
</BrowserRouter>
</HelmetProvider>
- Step 3: Commit
git add package.json package-lock.json src/main.tsx
git commit -m "feat: install react-helmet-async and gray-matter, add HelmetProvider"
Task 2: Create SEOHead component
Files:
-
Create:
src/components/seo/SEOHead.tsx -
Step 1: Create SEOHead component
import { Helmet } from 'react-helmet-async';
interface SEOHeadProps {
title: string;
description: string;
path: string;
type?: 'website' | 'article';
image?: string;
publishedTime?: string;
noindex?: boolean;
}
const BASE_URL = 'https://texpixel.com';
export default function SEOHead({
title,
description,
path,
type = 'website',
image = 'https://cdn.texpixel.com/public/og-cover.png',
publishedTime,
noindex = false,
}: SEOHeadProps) {
const url = `${BASE_URL}${path}`;
const fullTitle = path === '/' ? title : `${title} | TexPixel`;
return (
<Helmet>
<title>{fullTitle}</title>
<meta name="description" content={description} />
<link rel="canonical" href={url} />
{noindex && <meta name="robots" content="noindex, nofollow" />}
{/* Open Graph */}
<meta property="og:title" content={fullTitle} />
<meta property="og:description" content={description} />
<meta property="og:url" content={url} />
<meta property="og:type" content={type} />
<meta property="og:image" content={image} />
<meta property="og:site_name" content="TexPixel" />
{publishedTime && <meta property="article:published_time" content={publishedTime} />}
{/* Twitter */}
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content={fullTitle} />
<meta name="twitter:description" content={description} />
<meta name="twitter:image" content={image} />
</Helmet>
);
}
- Step 2: Commit
git add src/components/seo/SEOHead.tsx
git commit -m "feat: add SEOHead component with react-helmet-async"
Task 3: Create layout components
Files:
-
Create:
src/components/layout/MarketingNavbar.tsx -
Create:
src/components/layout/AppNavbar.tsx -
Create:
src/components/layout/Footer.tsx -
Create:
src/components/layout/MarketingLayout.tsx -
Create:
src/components/layout/AppLayout.tsx -
Step 1: Create MarketingNavbar
Full-width navbar with logo, nav links (Home, Docs, Blog), anchor links (Pricing, Contact on home page), language switcher, and CTA button to /app. Use useLocation to show anchor links only when on /. Responsive with mobile hamburger menu.
// src/components/layout/MarketingNavbar.tsx
import { useState } from 'react';
import { Link, useLocation } from 'react-router-dom';
import { Languages, ChevronDown, Check, Menu, X } from 'lucide-react';
import { useLanguage } from '../../contexts/LanguageContext';
export default function MarketingNavbar() {
const { language, setLanguage, t } = useLanguage();
const location = useLocation();
const [showLangMenu, setShowLangMenu] = useState(false);
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
const isHome = location.pathname === '/';
const navLinks = [
{ to: '/', label: t.marketing?.nav?.home ?? 'Home' },
{ to: '/docs', label: t.marketing?.nav?.docs ?? 'Docs' },
{ to: '/blog', label: t.marketing?.nav?.blog ?? 'Blog' },
];
const anchorLinks = isHome
? [
{ href: '#pricing', label: t.marketing?.nav?.pricing ?? 'Pricing' },
{ href: '#contact', label: t.marketing?.nav?.contact ?? 'Contact' },
]
: [];
return (
<nav className="h-16 bg-white border-b border-gray-200 flex items-center justify-between px-6 flex-shrink-0 z-[60] relative">
{/* Logo */}
<Link to="/" className="flex items-center gap-2">
<img src="/texpixel-app-icon.svg" alt="TexPixel" className="w-8 h-8" />
<span className="text-xl font-bold text-gray-900 tracking-tight">TexPixel</span>
</Link>
{/* Desktop Nav */}
<div className="hidden md:flex items-center gap-6">
{navLinks.map((link) => (
<Link
key={link.to}
to={link.to}
className={`text-sm font-medium transition-colors ${
location.pathname === link.to ? 'text-blue-600' : 'text-gray-700 hover:text-gray-900'
}`}
>
{link.label}
</Link>
))}
{anchorLinks.map((link) => (
<a
key={link.href}
href={link.href}
className="text-sm font-medium text-gray-700 hover:text-gray-900 transition-colors"
>
{link.label}
</a>
))}
</div>
{/* Right actions */}
<div className="flex items-center gap-3">
{/* Language Switcher */}
<div className="relative">
<button
onClick={() => setShowLangMenu(!showLangMenu)}
className="flex items-center gap-2 px-3 py-2 hover:bg-gray-100 rounded-lg text-gray-700 text-sm font-medium transition-colors"
>
<Languages size={18} />
<span className="hidden sm:inline">{language === 'en' ? 'EN' : '中文'}</span>
<ChevronDown size={14} className={`transition-transform duration-200 ${showLangMenu ? 'rotate-180' : ''}`} />
</button>
{showLangMenu && (
<div className="absolute right-0 top-full mt-2 w-32 bg-white rounded-xl shadow-lg border border-gray-200 py-1 z-50">
{(['en', 'zh'] as const).map((lang) => (
<button
key={lang}
onClick={() => { setLanguage(lang); setShowLangMenu(false); }}
className={`w-full flex items-center justify-between px-4 py-2 text-sm transition-colors hover:bg-gray-50 ${language === lang ? 'text-blue-600 font-medium' : 'text-gray-700'}`}
>
{lang === 'en' ? 'English' : '简体中文'}
{language === lang && <Check size={14} />}
</button>
))}
</div>
)}
</div>
{/* CTA */}
<Link
to="/app"
className="hidden sm:inline-flex items-center px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white text-sm font-medium rounded-lg transition-colors"
>
{t.marketing?.nav?.launchApp ?? 'Launch App'}
</Link>
{/* Mobile menu toggle */}
<button className="md:hidden p-2" onClick={() => setMobileMenuOpen(!mobileMenuOpen)}>
{mobileMenuOpen ? <X size={20} /> : <Menu size={20} />}
</button>
</div>
{/* Mobile menu */}
{mobileMenuOpen && (
<div className="absolute top-16 left-0 right-0 bg-white border-b border-gray-200 shadow-lg md:hidden z-50 py-4 px-6 flex flex-col gap-3">
{navLinks.map((link) => (
<Link key={link.to} to={link.to} className="text-sm font-medium text-gray-700 py-2" onClick={() => setMobileMenuOpen(false)}>
{link.label}
</Link>
))}
{anchorLinks.map((link) => (
<a key={link.href} href={link.href} className="text-sm font-medium text-gray-700 py-2" onClick={() => setMobileMenuOpen(false)}>
{link.label}
</a>
))}
<Link to="/app" className="text-sm font-medium text-blue-600 py-2" onClick={() => setMobileMenuOpen(false)}>
{t.marketing?.nav?.launchApp ?? 'Launch App'}
</Link>
</div>
)}
</nav>
);
}
- Step 2: Create AppNavbar
Simplified version of current Navbar.tsx for the workspace. Keep language switcher, reward, contact, guide, help — remove marketing nav links. Add a "Back to Home" link.
Copy current src/components/Navbar.tsx content into src/components/layout/AppNavbar.tsx. Add a Link to / (home icon or "TexPixel" logo links to /). Keep all existing functionality (reward modal, contact dropdown, language switcher, guide button).
- Step 3: Create Footer
// src/components/layout/Footer.tsx
import { Link } from 'react-router-dom';
import { useLanguage } from '../../contexts/LanguageContext';
export default function Footer() {
const { t } = useLanguage();
return (
<footer className="bg-gray-900 text-gray-400 py-12 px-6">
<div className="max-w-6xl mx-auto grid grid-cols-1 md:grid-cols-4 gap-8">
{/* Brand */}
<div>
<div className="flex items-center gap-2 mb-4">
<img src="/texpixel-app-icon.svg" alt="TexPixel" className="w-6 h-6" />
<span className="text-white font-bold">TexPixel</span>
</div>
<p className="text-sm">{t.marketing?.footer?.tagline ?? 'AI-powered math formula recognition'}</p>
</div>
{/* Product */}
<div>
<h4 className="text-white font-semibold mb-3 text-sm">{t.marketing?.footer?.product ?? 'Product'}</h4>
<div className="flex flex-col gap-2 text-sm">
<Link to="/app" className="hover:text-white transition-colors">{t.marketing?.nav?.launchApp ?? 'Launch App'}</Link>
<a href="/#pricing" className="hover:text-white transition-colors">{t.marketing?.nav?.pricing ?? 'Pricing'}</a>
</div>
</div>
{/* Resources */}
<div>
<h4 className="text-white font-semibold mb-3 text-sm">{t.marketing?.footer?.resources ?? 'Resources'}</h4>
<div className="flex flex-col gap-2 text-sm">
<Link to="/docs" className="hover:text-white transition-colors">{t.marketing?.nav?.docs ?? 'Docs'}</Link>
<Link to="/blog" className="hover:text-white transition-colors">{t.marketing?.nav?.blog ?? 'Blog'}</Link>
</div>
</div>
{/* Contact */}
<div>
<h4 className="text-white font-semibold mb-3 text-sm">{t.marketing?.footer?.contactTitle ?? 'Contact'}</h4>
<div className="flex flex-col gap-2 text-sm">
<a href="mailto:yogecoder@gmail.com" className="hover:text-white transition-colors">yogecoder@gmail.com</a>
</div>
</div>
</div>
<div className="max-w-6xl mx-auto mt-8 pt-8 border-t border-gray-800 text-sm text-center">
© {new Date().getFullYear()} TexPixel. All rights reserved.
</div>
</footer>
);
}
- Step 4: Create MarketingLayout and AppLayout
// src/components/layout/MarketingLayout.tsx
import { Outlet } from 'react-router-dom';
import MarketingNavbar from './MarketingNavbar';
import Footer from './Footer';
export default function MarketingLayout() {
return (
<div className="min-h-screen flex flex-col bg-white">
<MarketingNavbar />
<main className="flex-1">
<Outlet />
</main>
<Footer />
</div>
);
}
// src/components/layout/AppLayout.tsx
import { Outlet } from 'react-router-dom';
import AppNavbar from './AppNavbar';
export default function AppLayout() {
return (
<div className="h-screen flex flex-col bg-gray-50 font-sans text-gray-900 overflow-hidden">
<AppNavbar />
<div className="flex-1 flex overflow-hidden">
<Outlet />
</div>
</div>
);
}
- Step 5: Commit
git add src/components/layout/
git commit -m "feat: add layout components (MarketingNavbar, AppNavbar, Footer, layouts)"
Task 4: Add marketing translations
Files:
-
Modify:
src/lib/translations.ts -
Step 1: Add marketing section to translations
Add marketing key to both en and zh objects in translations.ts:
marketing: {
nav: {
home: 'Home', // zh: '首页'
docs: 'Docs', // zh: '文档'
blog: 'Blog', // zh: '博客'
pricing: 'Pricing', // zh: '价格'
contact: 'Contact', // zh: '联系我们'
launchApp: 'Launch App', // zh: '启动应用'
},
hero: {
title: 'Convert Math Formulas to LaTeX in Seconds',
// zh: '数学公式秒级转换为 LaTeX'
subtitle: 'AI-powered OCR for handwritten and printed mathematical formulas. Get LaTeX, MathML, and Markdown output instantly.',
// zh: 'AI 驱动的手写和印刷体数学公式识别,即时输出 LaTeX、MathML 和 Markdown。'
cta: 'Try It Free', // zh: '免费试用'
ctaSecondary: 'Learn More', // zh: '了解更多'
},
features: {
title: 'Features', // zh: '功能特性'
subtitle: 'Everything you need for formula recognition',
// zh: '公式识别所需的一切'
items: [
{ title: 'Handwriting Recognition', description: 'Accurately recognize handwritten math formulas from photos or scans' },
{ title: 'Multi-Format Output', description: 'Export to LaTeX, MathML, Markdown, Word, and more' },
{ title: 'PDF Support', description: 'Upload PDF documents and extract formulas automatically' },
{ title: 'Batch Processing', description: 'Process multiple files at once for maximum efficiency' },
{ title: 'High Accuracy', description: 'Powered by advanced AI models for industry-leading accuracy' },
{ title: 'Free to Start', description: 'Get started with free uploads, no credit card required' },
],
// zh versions of items array
},
howItWorks: {
title: 'How It Works', // zh: '使用流程'
steps: [
{ title: 'Upload', description: 'Upload an image or PDF containing math formulas' },
{ title: 'Recognize', description: 'Our AI analyzes and recognizes the formulas' },
{ title: 'Export', description: 'Copy or export results in your preferred format' },
],
},
pricing: {
title: 'Pricing', // zh: '价格方案'
subtitle: 'Choose the plan that fits your needs',
// zh: '选择适合您的方案'
plans: [
{ name: 'Free', price: '$0', period: '/month', features: ['3 uploads/day', 'LaTeX & Markdown output', 'Community support'], cta: 'Get Started' },
{ name: 'Pro', price: '$9.9', period: '/month', features: ['Unlimited uploads', 'All export formats', 'Priority processing', 'API access'], cta: 'Coming Soon', popular: true },
{ name: 'Enterprise', price: 'Custom', period: '', features: ['Custom volume', 'Dedicated support', 'SLA guarantee', 'On-premise option'], cta: 'Contact Us' },
],
},
contact: {
title: 'Contact Us', // zh: '联系我们'
subtitle: 'Get in touch with our team',
// zh: '与我们的团队取得联系'
nameLabel: 'Name', // zh: '姓名'
emailLabel: 'Email', // zh: '邮箱'
messageLabel: 'Message', // zh: '留言'
send: 'Send Message', // zh: '发送消息'
sending: 'Sending...', // zh: '发送中...'
sent: 'Message sent!', // zh: '消息已发送!'
qqGroup: 'QQ Group', // zh: 'QQ 群'
},
footer: {
tagline: 'AI-powered math formula recognition',
// zh: 'AI 驱动的数学公式识别'
product: 'Product', // zh: '产品'
resources: 'Resources', // zh: '资源'
contactTitle: 'Contact', // zh: '联系方式'
},
},
- Step 2: Commit
git add src/lib/translations.ts
git commit -m "feat: add marketing translations for en and zh"
Task 5: Create Home page sections
Files:
-
Create:
src/components/home/HeroSection.tsx -
Create:
src/components/home/FeaturesSection.tsx -
Create:
src/components/home/HowItWorksSection.tsx -
Create:
src/components/home/PricingSection.tsx -
Create:
src/components/home/ContactSection.tsx -
Create:
src/pages/HomePage.tsx -
Step 1: Create HeroSection
Hero with product tagline, a mini drag-and-drop demo area (visual only, clicking it navigates to /app), and CTA buttons. Use Tailwind for gradient backgrounds and animations.
Key elements:
-
Large heading from
t.marketing.hero.title -
Subtitle from
t.marketing.hero.subtitle -
Primary CTA button → links to
/app -
Secondary CTA button → scrolls to
#features -
A decorative mock preview showing a formula being converted (static image or CSS illustration)
-
Step 2: Create FeaturesSection
6-card grid from t.marketing.features.items. Each card has an icon (from lucide-react), title, and description. Use icons: PenTool, FileOutput, FileText, Layers, Zap, Gift.
- Step 3: Create HowItWorksSection
3-step horizontal flow with numbered circles, title, description. Steps from t.marketing.howItWorks.steps. Use icons: Upload, Cpu, Download.
- Step 4: Create PricingSection
3-column card layout from t.marketing.pricing.plans. Middle card (Pro) has popular: true → highlighted border/badge. CTA buttons: Free → link to /app, Pro → disabled "Coming Soon", Enterprise → link to #contact.
Section has id="pricing" for anchor navigation.
- Step 5: Create ContactSection
Two-column layout. Left: contact info (email, QQ group). Right: form with name, email, message fields + submit button. Form initially just shows a success toast on submit (no backend). id="contact" for anchor nav.
- Step 6: Create HomePage
// src/pages/HomePage.tsx
import SEOHead from '../components/seo/SEOHead';
import HeroSection from '../components/home/HeroSection';
import FeaturesSection from '../components/home/FeaturesSection';
import HowItWorksSection from '../components/home/HowItWorksSection';
import PricingSection from '../components/home/PricingSection';
import ContactSection from '../components/home/ContactSection';
import { useLanguage } from '../contexts/LanguageContext';
export default function HomePage() {
const { t } = useLanguage();
return (
<>
<SEOHead
title="TexPixel - AI Math Formula Recognition | LaTeX, MathML OCR Tool"
description={t.marketing.hero.subtitle}
path="/"
/>
<HeroSection />
<FeaturesSection />
<HowItWorksSection />
<PricingSection />
<ContactSection />
</>
);
}
- Step 7: Commit
git add src/components/home/ src/pages/HomePage.tsx
git commit -m "feat: add Home page with Hero, Features, HowItWorks, Pricing, Contact sections"
Task 6: Migrate App.tsx to WorkspacePage
Files:
-
Create:
src/pages/WorkspacePage.tsx -
Modify:
src/App.tsx(will become thin redirect or removed) -
Step 1: Create WorkspacePage
Move all logic from App.tsx into WorkspacePage.tsx. Remove the outer <div className="h-screen flex flex-col ..."> and <Navbar /> wrappers since AppLayout provides those. Keep the inner flex container with LeftSidebar, FilePreview, ResultPanel, modals, and loading overlay.
The component should render:
<>
<SEOHead title="Workspace" description="..." path="/app" noindex />
{/* Left Sidebar */}
<div ref={sidebarRef} ...>
<LeftSidebar ... />
{/* Resize Handle */}
</div>
{/* Middle: FilePreview */}
<div className="flex-1 ..."><FilePreview ... /></div>
{/* Right: ResultPanel */}
<div className="flex-1 ..."><ResultPanel ... /></div>
{/* Modals */}
{showUploadModal && <UploadModal ... />}
{showAuthModal && <AuthModal ... />}
<UserGuide ... />
{loading && <div>...</div>}
</>
Note: AppLayout already provides <div className="h-screen flex flex-col ..."> and <AppNavbar /> and <div className="flex-1 flex overflow-hidden">, so WorkspacePage renders directly inside that flex container.
- Step 2: Update App.tsx
Replace App.tsx with a simple redirect to maintain backward compatibility if anything imports it:
import { Navigate } from 'react-router-dom';
export default function App() {
return <Navigate to="/" replace />;
}
- Step 3: Verify build
npm run typecheck
- Step 4: Commit
git add src/pages/WorkspacePage.tsx src/App.tsx
git commit -m "feat: migrate App.tsx logic to WorkspacePage"
Task 7: Update AppRouter with layout routes
Files:
-
Modify:
src/routes/AppRouter.tsx -
Step 1: Update AppRouter
import { lazy, Suspense } from 'react';
import { Routes, Route } from 'react-router-dom';
import MarketingLayout from '../components/layout/MarketingLayout';
import AppLayout from '../components/layout/AppLayout';
import AuthCallbackPage from '../pages/AuthCallbackPage';
const HomePage = lazy(() => import('../pages/HomePage'));
const WorkspacePage = lazy(() => import('../pages/WorkspacePage'));
const DocsListPage = lazy(() => import('../pages/DocsListPage'));
const DocDetailPage = lazy(() => import('../pages/DocDetailPage'));
const BlogListPage = lazy(() => import('../pages/BlogListPage'));
const BlogDetailPage = lazy(() => import('../pages/BlogDetailPage'));
function LoadingFallback() {
return (
<div className="min-h-screen flex items-center justify-center">
<div className="w-12 h-12 border-4 border-blue-600 border-t-transparent rounded-full animate-spin" />
</div>
);
}
export default function AppRouter() {
return (
<Suspense fallback={<LoadingFallback />}>
<Routes>
<Route element={<MarketingLayout />}>
<Route path="/" element={<HomePage />} />
<Route path="/docs" element={<DocsListPage />} />
<Route path="/docs/:slug" element={<DocDetailPage />} />
<Route path="/blog" element={<BlogListPage />} />
<Route path="/blog/:slug" element={<BlogDetailPage />} />
</Route>
<Route element={<AppLayout />}>
<Route path="/app" element={<WorkspacePage />} />
</Route>
<Route path="/auth/google/callback" element={<AuthCallbackPage />} />
</Routes>
</Suspense>
);
}
- Step 2: Commit
git add src/routes/AppRouter.tsx
git commit -m "feat: update AppRouter with layout routes and lazy loading"
Task 8: Create placeholder Docs and Blog pages
Files:
-
Create:
src/pages/DocsListPage.tsx -
Create:
src/pages/DocDetailPage.tsx -
Create:
src/pages/BlogListPage.tsx -
Create:
src/pages/BlogDetailPage.tsx -
Step 1: Create DocsListPage
List page showing available docs. For now, hardcode a few placeholder entries. Each entry links to /docs/:slug. Include SEOHead.
import { Link } from 'react-router-dom';
import SEOHead from '../components/seo/SEOHead';
import { useLanguage } from '../contexts/LanguageContext';
const docs = [
{ slug: 'getting-started', title: 'Getting Started', titleZh: '快速开始', description: 'Learn how to use TexPixel', descriptionZh: '了解如何使用 TexPixel' },
];
export default function DocsListPage() {
const { language } = useLanguage();
return (
<>
<SEOHead title="Documentation" description="TexPixel documentation and guides" path="/docs" />
<div className="max-w-4xl mx-auto py-16 px-6">
<h1 className="text-3xl font-bold text-gray-900 mb-8">{language === 'en' ? 'Documentation' : '文档'}</h1>
<div className="space-y-4">
{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">
<h2 className="text-lg font-semibold text-gray-900">{language === 'en' ? doc.title : doc.titleZh}</h2>
<p className="text-gray-600 mt-1 text-sm">{language === 'en' ? doc.description : doc.descriptionZh}</p>
</Link>
))}
</div>
</div>
</>
);
}
- Step 2: Create DocDetailPage
Placeholder that reads :slug from params and shows a coming-soon message.
import { useParams } from 'react-router-dom';
import SEOHead from '../components/seo/SEOHead';
export default function DocDetailPage() {
const { slug } = useParams<{ slug: string }>();
return (
<>
<SEOHead title={slug ?? 'Doc'} description={`Documentation: ${slug}`} path={`/docs/${slug}`} />
<div className="max-w-4xl mx-auto py-16 px-6">
<h1 className="text-3xl font-bold text-gray-900 mb-4">{slug}</h1>
<p className="text-gray-600">Content coming soon.</p>
</div>
</>
);
}
- Step 3: Create BlogListPage and BlogDetailPage
Same pattern as docs. BlogListPage shows placeholder blog entries with date and title. BlogDetailPage reads :slug param.
- Step 4: Commit
git add src/pages/DocsListPage.tsx src/pages/DocDetailPage.tsx src/pages/BlogListPage.tsx src/pages/BlogDetailPage.tsx
git commit -m "feat: add placeholder Docs and Blog pages"
Task 9: Build content pipeline (Markdown → JSON)
Files:
-
Create:
content/docs/en/getting-started.md -
Create:
content/docs/zh/getting-started.md -
Create:
content/blog/en/2026-03-25-introducing-texpixel.md -
Create:
content/blog/zh/2026-03-25-introducing-texpixel.md -
Create:
scripts/build-content.ts -
Create:
src/lib/content.ts -
Modify:
package.json(add build:content script) -
Step 1: Create sample markdown files
Each file has frontmatter (title, description, slug, date, tags, order) and body content.
- Step 2: Create build-content script
Node script that:
- Scans
content/docs/{en,zh}/andcontent/blog/{en,zh}/ - Parses frontmatter with
gray-matter - Compiles markdown body with
remark+rehype→ HTML string - Outputs
public/content/docs-manifest.jsonandpublic/content/blog-manifest.json - Outputs individual
public/content/docs/{lang}/{slug}.jsonandpublic/content/blog/{lang}/{slug}.json
Manifest format:
{
"en": [{ "slug": "getting-started", "title": "...", "description": "...", "date": "...", "tags": [], "order": 1 }],
"zh": [...]
}
Individual file format:
{ "meta": { ... frontmatter ... }, "html": "<p>compiled html</p>" }
- Step 3: Create content loader utility
// src/lib/content.ts
import type { Language } from './translations';
export interface ContentMeta {
slug: string;
title: string;
description: string;
date: string;
tags: string[];
order?: number;
cover?: string;
}
export interface ContentManifest {
en: ContentMeta[];
zh: ContentMeta[];
}
export interface ContentItem {
meta: ContentMeta;
html: string;
}
const BASE = '/content';
export async function loadManifest(type: 'docs' | 'blog'): Promise<ContentManifest> {
const res = await fetch(`${BASE}/${type}-manifest.json`);
return res.json();
}
export async function loadContent(type: 'docs' | 'blog', lang: Language, slug: string): Promise<ContentItem> {
const res = await fetch(`${BASE}/${type}/${lang}/${slug}.json`);
return res.json();
}
- Step 4: Add npm script
In package.json, add:
"build:content": "npx tsx scripts/build-content.ts",
"build": "npm run build:content && vite build"
- Step 5: Commit
git add content/ scripts/build-content.ts src/lib/content.ts package.json
git commit -m "feat: add markdown content pipeline with build script"
Task 10: Wire Docs/Blog pages to content pipeline
Files:
-
Modify:
src/pages/DocsListPage.tsx -
Modify:
src/pages/DocDetailPage.tsx -
Modify:
src/pages/BlogListPage.tsx -
Modify:
src/pages/BlogDetailPage.tsx -
Step 1: Update DocsListPage to load from manifest
Use useEffect + loadManifest('docs') to fetch doc list. Render based on current language.
- Step 2: Update DocDetailPage to load content
Use useEffect + loadContent('docs', language, slug) to fetch and render HTML. Use dangerouslySetInnerHTML for the compiled HTML (safe since we control the source markdown). Apply Tailwind typography classes (prose).
- Step 3: Update Blog pages similarly
Same pattern. BlogListPage shows date + cover image. BlogDetailPage renders article with type="article" in SEOHead.
- Step 4: Run build:content and test
npm run build:content && npm run dev
Visit /docs, /docs/getting-started, /blog, /blog/introducing-texpixel.
- Step 5: Commit
git add src/pages/
git commit -m "feat: wire Docs and Blog pages to markdown content pipeline"
Task 11: Update sitemap and SEO infrastructure
Files:
-
Modify:
public/sitemap.xml -
Modify:
public/robots.txt -
Modify:
index.html -
Step 1: Update sitemap.xml
Add all new routes: /, /app, /docs, /docs/getting-started, /blog, /blog/introducing-texpixel. Set appropriate changefreq and priority.
- Step 2: Update robots.txt
Add Disallow: /app to prevent indexing of the workspace.
- Step 3: Clean up index.html
Since SEOHead now manages per-page meta tags via react-helmet-async, simplify index.html to only keep the base defaults. Remove the inline language detection script (LanguageContext handles this).
- Step 4: Commit
git add public/sitemap.xml public/robots.txt index.html
git commit -m "feat: update sitemap, robots.txt, and index.html for new routes"
Task 12: Final verification
- Step 1: Type check
npm run typecheck
- Step 2: Lint
npm run lint
- Step 3: Build
npm run build
- Step 4: Test all routes in dev
npm run dev
Visit: /, /app, /docs, /docs/getting-started, /blog, /blog/introducing-texpixel
Verify:
-
Home page shows all sections, anchor links work
-
/appworkspace functions as before -
Docs/Blog pages load content
-
Language switching works across all pages
-
Mobile responsive nav works
-
Step 5: Commit any fixes and final commit
git add -A
git commit -m "feat: complete website restructure with marketing pages, docs, and blog"