refact: update ui
This commit is contained in:
974
docs/superpowers/plans/2026-03-25-website-restructure.md
Normal file
974
docs/superpowers/plans/2026-03-25-website-restructure.md
Normal file
@@ -0,0 +1,974 @@
|
||||
# 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"
|
||||
```
|
||||
@@ -0,0 +1,75 @@
|
||||
# Website Restructure Design
|
||||
|
||||
## Overview
|
||||
Restructure the single-page OCR app into a multi-section website with Home, Docs, Blog, Pricing, Contact modules while maintaining the core OCR tool functionality in a dedicated workspace page.
|
||||
|
||||
## Routes
|
||||
|
||||
| Route | Page | SEO | Layout |
|
||||
|-------|------|-----|--------|
|
||||
| `/` | HomePage (Hero + Features + HowItWorks + Pricing + Contact) | Prerender | MarketingLayout |
|
||||
| `/app` | WorkspacePage (current 3-panel OCR layout) | noindex | AppLayout |
|
||||
| `/docs` | DocsListPage | Prerender | MarketingLayout |
|
||||
| `/docs/:slug` | DocDetailPage | Prerender | MarketingLayout |
|
||||
| `/blog` | BlogListPage | Prerender | MarketingLayout |
|
||||
| `/blog/:slug` | BlogDetailPage | Prerender | MarketingLayout |
|
||||
| `/auth/google/callback` | AuthCallbackPage | noindex | None |
|
||||
|
||||
## Home Page Sections
|
||||
1. **Hero** — Product tagline + mini upload demo + CTA to `/app`
|
||||
2. **Features** — Feature cards grid (formula recognition, multi-format export, etc.)
|
||||
3. **How it works** — 3-step process
|
||||
4. **Pricing** — Static price cards (Free / Pro / Enterprise), payment integration later
|
||||
5. **Contact** — Contact info display + form submission
|
||||
|
||||
## SEO Strategy
|
||||
- **Prerendering**: `vite-plugin-prerender` for static HTML generation of marketing pages
|
||||
- **Meta tags**: `react-helmet-async` for per-page title, description, OG/Twitter cards
|
||||
- **Structured data**: JSON-LD (SoftwareApplication, Article, TechArticle)
|
||||
- **Sitemap**: Auto-generated at build time from routes + markdown content
|
||||
- **i18n SEO**: `hreflang` tags for zh/en
|
||||
- `/app` route gets `noindex`
|
||||
|
||||
## Markdown Content System
|
||||
```
|
||||
content/
|
||||
├── docs/{en,zh}/*.md
|
||||
└── blog/{en,zh}/*.md
|
||||
```
|
||||
- Frontmatter: title, description, slug, date, tags, order, cover
|
||||
- Build script scans content/, parses frontmatter, generates manifest JSON
|
||||
- Body compiled via remark + rehype (reusing existing KaTeX pipeline)
|
||||
|
||||
## Component Architecture
|
||||
|
||||
### Layouts
|
||||
- `MarketingLayout` — MarketingNavbar + Outlet + Footer
|
||||
- `AppLayout` — AppNavbar + Outlet
|
||||
|
||||
### New Pages
|
||||
- `src/pages/HomePage.tsx`
|
||||
- `src/pages/WorkspacePage.tsx` (migrated from App.tsx)
|
||||
- `src/pages/DocsListPage.tsx`
|
||||
- `src/pages/DocDetailPage.tsx`
|
||||
- `src/pages/BlogListPage.tsx`
|
||||
- `src/pages/BlogDetailPage.tsx`
|
||||
|
||||
### Home Sections
|
||||
- `src/components/home/HeroSection.tsx`
|
||||
- `src/components/home/FeaturesSection.tsx`
|
||||
- `src/components/home/HowItWorksSection.tsx`
|
||||
- `src/components/home/PricingSection.tsx`
|
||||
- `src/components/home/ContactSection.tsx`
|
||||
|
||||
### Shared Layout Components
|
||||
- `src/components/layout/MarketingNavbar.tsx`
|
||||
- `src/components/layout/AppNavbar.tsx` (evolved from current Navbar.tsx)
|
||||
- `src/components/layout/Footer.tsx`
|
||||
|
||||
## Migration
|
||||
- `App.tsx` core logic → `WorkspacePage.tsx`
|
||||
- `Navbar.tsx` → split into `MarketingNavbar` + `AppNavbar`
|
||||
- All existing components (LeftSidebar, FilePreview, ResultPanel, etc.) unchanged, referenced by WorkspacePage
|
||||
|
||||
## Lazy Loading
|
||||
All page components use `React.lazy()` + `Suspense` to keep initial bundle small.
|
||||
Reference in New Issue
Block a user