|
| 1 | +# SEO Improvement Plan for ooxml.dev |
| 2 | + |
| 3 | +## Context |
| 4 | + |
| 5 | +ooxml.dev gets ~270 sessions/quarter with only 40 from organic search. But organic visitors have excellent engagement (3+ min avg, 12% bounce) — proving the content is valuable. The problem is zero `/docs/*` pages get organic traffic because Google can't index a client-side SPA. All doc content is static (defined in `docs.ts`), making pre-rendering straightforward. |
| 6 | + |
| 7 | +## Approach: Build-time pre-rendering + SEO metadata |
| 8 | + |
| 9 | +Use `react-dom/server`'s `renderToString` in a custom build script to generate static HTML for each route. No framework migration, no new runtime dependencies. |
| 10 | + |
| 11 | +## Implementation |
| 12 | + |
| 13 | +### 1. Pre-render script (`apps/web/scripts/prerender.tsx`) |
| 14 | + |
| 15 | +Runs after `vite build`: |
| 16 | +1. Reads `dist/index.html` as template (has hashed CSS/JS from Vite) |
| 17 | +2. For each route, renders the page component to HTML via `renderToString` |
| 18 | +3. Injects per-page `<title>`, `<meta>`, OG tags, canonical URL, JSON-LD into `<head>` |
| 19 | +4. Writes to correct path (e.g., `dist/docs/tables/index.html`) |
| 20 | +5. Client-side React still loads and takes over for interactivity |
| 21 | + |
| 22 | +**Routes to pre-render:** |
| 23 | +- `/` (Home) |
| 24 | +- `/mcp` (MCP page) |
| 25 | +- `/spec` (shell only — content is API-dependent) |
| 26 | +- `/docs` + all 7 doc pages from `docs.ts` |
| 27 | + |
| 28 | +**SuperDocPreview handling:** Add `typeof window === 'undefined'` guard to render the XML as a static `<pre><code>` block during SSR. React hydrates the interactive version client-side. |
| 29 | + |
| 30 | +### 2. SEO metadata (`apps/web/src/data/seo.ts`) |
| 31 | + |
| 32 | +Per-route metadata: |
| 33 | +- `<title>` — e.g., "OOXML Tables (w:tbl) — Structure & Implementation | ooxml.dev" |
| 34 | +- `<meta name="description">` — from `docs.ts` descriptions |
| 35 | +- `<link rel="canonical">` — `https://ooxml.dev{path}` |
| 36 | +- Open Graph tags (og:title, og:description, og:url, og:type) |
| 37 | +- Twitter card meta |
| 38 | + |
| 39 | +For doc pages, auto-generate from `docs.ts` (title, badge, description). |
| 40 | + |
| 41 | +### 3. Client-side title updates (`useDocumentTitle` hook) |
| 42 | + |
| 43 | +Simple `useEffect` hook so browser tab title updates during SPA navigation. Used in DocsPage, Home, Mcp, SpecExplorer. |
| 44 | + |
| 45 | +### 4. Structured data (JSON-LD) |
| 46 | + |
| 47 | +Injected by prerender script per page: |
| 48 | +- Doc pages: `TechArticle` schema |
| 49 | +- Home: `WebSite` schema with `SearchAction` for `/spec?q=` |
| 50 | + |
| 51 | +### 5. Sitemap generation |
| 52 | + |
| 53 | +Generated by prerender script → `dist/sitemap.xml`: |
| 54 | +- All routes with `<loc>`, `<changefreq>`, `<priority>` |
| 55 | +- Auto-includes new doc pages from `docs.ts` |
| 56 | + |
| 57 | +### 6. robots.txt (`apps/web/public/robots.txt`) |
| 58 | + |
| 59 | +``` |
| 60 | +User-agent: * |
| 61 | +Allow: / |
| 62 | +Sitemap: https://ooxml.dev/sitemap.xml |
| 63 | +``` |
| 64 | +Plus existing AI crawler blocks. |
| 65 | + |
| 66 | +### 7. Build script update |
| 67 | + |
| 68 | +```diff |
| 69 | +- "build": "tsc && vite build" |
| 70 | ++ "build": "tsc && vite build && bun scripts/prerender.tsx" |
| 71 | +``` |
| 72 | + |
| 73 | +## Files to create/modify |
| 74 | + |
| 75 | +| File | Action | |
| 76 | +|------|--------| |
| 77 | +| `apps/web/scripts/prerender.tsx` | **Create** — core prerender + sitemap generation | |
| 78 | +| `apps/web/src/data/seo.ts` | **Create** — per-route SEO metadata | |
| 79 | +| `apps/web/src/hooks/useDocumentTitle.ts` | **Create** — client-side title hook | |
| 80 | +| `apps/web/src/components/SuperDocPreview.tsx` | **Modify** — add SSR fallback | |
| 81 | +| `apps/web/src/pages/docs/Page.tsx` | **Modify** — use useDocumentTitle | |
| 82 | +| `apps/web/src/pages/Home.tsx` | **Modify** — use useDocumentTitle | |
| 83 | +| `apps/web/src/pages/Mcp.tsx` | **Modify** — use useDocumentTitle | |
| 84 | +| `apps/web/src/pages/SpecExplorer.tsx` | **Modify** — use useDocumentTitle | |
| 85 | +| `apps/web/public/robots.txt` | **Create** | |
| 86 | +| `apps/web/package.json` | **Modify** — update build script | |
| 87 | + |
| 88 | +## Verification |
| 89 | + |
| 90 | +1. `bun run build` from `apps/web/` |
| 91 | +2. Inspect `dist/docs/tables/index.html` — should contain full HTML content, correct `<title>`, meta tags, JSON-LD |
| 92 | +3. Inspect `dist/sitemap.xml` — should list all routes |
| 93 | +4. Serve `dist/` locally (`bunx serve dist`) and verify: |
| 94 | + - Pages load with correct content before JS executes (disable JS in browser) |
| 95 | + - Interactive features (SuperDocPreview, spec search) work after JS loads |
| 96 | + - SPA navigation updates browser tab title |
| 97 | +5. Deploy and submit sitemap to Google Search Console |
| 98 | + |
| 99 | +## Post-deploy |
| 100 | + |
| 101 | +- Set up Google Search Console if not already done |
| 102 | +- Submit sitemap |
| 103 | +- Request indexing for key doc pages |
| 104 | +- Monitor indexing progress over 2-4 weeks |
0 commit comments