Files
doc_ai_frontend/docs/superpowers/plans/2026-03-25-website-restructure.md
2026-03-25 14:06:37 +08:00

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">
        &copy; {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:

  1. Scans content/docs/{en,zh}/ and content/blog/{en,zh}/
  2. Parses frontmatter with gray-matter
  3. Compiles markdown body with remark + rehype → HTML string
  4. Outputs public/content/docs-manifest.json and public/content/blog-manifest.json
  5. Outputs individual public/content/docs/{lang}/{slug}.json and public/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

  • /app workspace 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"