975 lines
32 KiB
Markdown
975 lines
32 KiB
Markdown
|
|
# 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**
|
||
|
|
|
||
|
|
```bash
|
||
|
|
npm install react-helmet-async gray-matter
|
||
|
|
```
|
||
|
|
|
||
|
|
- [ ] **Step 2: Wrap app with HelmetProvider**
|
||
|
|
|
||
|
|
In `src/main.tsx`, add `HelmetProvider` wrapping:
|
||
|
|
|
||
|
|
```tsx
|
||
|
|
import { HelmetProvider } from 'react-helmet-async';
|
||
|
|
|
||
|
|
// Wrap inside StrictMode:
|
||
|
|
<HelmetProvider>
|
||
|
|
<BrowserRouter>
|
||
|
|
<AuthProvider>
|
||
|
|
<LanguageProvider>
|
||
|
|
<AppRouter />
|
||
|
|
</LanguageProvider>
|
||
|
|
</AuthProvider>
|
||
|
|
</BrowserRouter>
|
||
|
|
</HelmetProvider>
|
||
|
|
```
|
||
|
|
|
||
|
|
- [ ] **Step 3: Commit**
|
||
|
|
|
||
|
|
```bash
|
||
|
|
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**
|
||
|
|
|
||
|
|
```tsx
|
||
|
|
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**
|
||
|
|
|
||
|
|
```bash
|
||
|
|
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.
|
||
|
|
|
||
|
|
```tsx
|
||
|
|
// 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**
|
||
|
|
|
||
|
|
```tsx
|
||
|
|
// 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**
|
||
|
|
|
||
|
|
```tsx
|
||
|
|
// 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>
|
||
|
|
);
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
```tsx
|
||
|
|
// 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**
|
||
|
|
|
||
|
|
```bash
|
||
|
|
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`:
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
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**
|
||
|
|
|
||
|
|
```bash
|
||
|
|
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**
|
||
|
|
|
||
|
|
```tsx
|
||
|
|
// 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**
|
||
|
|
|
||
|
|
```bash
|
||
|
|
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:
|
||
|
|
```tsx
|
||
|
|
<>
|
||
|
|
<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:
|
||
|
|
|
||
|
|
```tsx
|
||
|
|
import { Navigate } from 'react-router-dom';
|
||
|
|
export default function App() {
|
||
|
|
return <Navigate to="/" replace />;
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
- [ ] **Step 3: Verify build**
|
||
|
|
|
||
|
|
```bash
|
||
|
|
npm run typecheck
|
||
|
|
```
|
||
|
|
|
||
|
|
- [ ] **Step 4: Commit**
|
||
|
|
|
||
|
|
```bash
|
||
|
|
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**
|
||
|
|
|
||
|
|
```tsx
|
||
|
|
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**
|
||
|
|
|
||
|
|
```bash
|
||
|
|
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.
|
||
|
|
|
||
|
|
```tsx
|
||
|
|
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.
|
||
|
|
|
||
|
|
```tsx
|
||
|
|
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**
|
||
|
|
|
||
|
|
```bash
|
||
|
|
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:
|
||
|
|
```json
|
||
|
|
{
|
||
|
|
"en": [{ "slug": "getting-started", "title": "...", "description": "...", "date": "...", "tags": [], "order": 1 }],
|
||
|
|
"zh": [...]
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
Individual file format:
|
||
|
|
```json
|
||
|
|
{ "meta": { ... frontmatter ... }, "html": "<p>compiled html</p>" }
|
||
|
|
```
|
||
|
|
|
||
|
|
- [ ] **Step 3: Create content loader utility**
|
||
|
|
|
||
|
|
```tsx
|
||
|
|
// 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:
|
||
|
|
```json
|
||
|
|
"build:content": "npx tsx scripts/build-content.ts",
|
||
|
|
"build": "npm run build:content && vite build"
|
||
|
|
```
|
||
|
|
|
||
|
|
- [ ] **Step 5: Commit**
|
||
|
|
|
||
|
|
```bash
|
||
|
|
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**
|
||
|
|
|
||
|
|
```bash
|
||
|
|
npm run build:content && npm run dev
|
||
|
|
```
|
||
|
|
|
||
|
|
Visit `/docs`, `/docs/getting-started`, `/blog`, `/blog/introducing-texpixel`.
|
||
|
|
|
||
|
|
- [ ] **Step 5: Commit**
|
||
|
|
|
||
|
|
```bash
|
||
|
|
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**
|
||
|
|
|
||
|
|
```bash
|
||
|
|
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**
|
||
|
|
|
||
|
|
```bash
|
||
|
|
npm run typecheck
|
||
|
|
```
|
||
|
|
|
||
|
|
- [ ] **Step 2: Lint**
|
||
|
|
|
||
|
|
```bash
|
||
|
|
npm run lint
|
||
|
|
```
|
||
|
|
|
||
|
|
- [ ] **Step 3: Build**
|
||
|
|
|
||
|
|
```bash
|
||
|
|
npm run build
|
||
|
|
```
|
||
|
|
|
||
|
|
- [ ] **Step 4: Test all routes in dev**
|
||
|
|
|
||
|
|
```bash
|
||
|
|
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**
|
||
|
|
|
||
|
|
```bash
|
||
|
|
git add -A
|
||
|
|
git commit -m "feat: complete website restructure with marketing pages, docs, and blog"
|
||
|
|
```
|