From 03ad49f523d85d069d155418b8617b06e062a5a1 Mon Sep 17 00:00:00 2001 From: john kevin asprec Date: Fri, 4 Sep 2026 07:08:57 +0800 Subject: [PATCH] feat(website): add search-intent titles, FAQ schema, and breadcrumbs Give Google unique snippets and FAQ rich results, and keep robots/sitemap from being cached as HTML. Co-authored-by: Cursor --- docs/DESIGN_SYSTEM.md | 2 + docs/HANDOFF.md | 7 +- docs/PROGRESS.md | 26 +++--- docs/design/website-seo-faq.md | 30 +++++++ packages/website/index.html | 20 +++-- packages/website/inject-seo.mjs | 85 ++++++++++++++++++- packages/website/nginx.conf | 2 + packages/website/server.mjs | 7 +- .../website/src/components/Breadcrumbs.tsx | 24 ++++++ packages/website/src/components/SeoHead.tsx | 11 ++- .../website/src/components/SiteFooter.tsx | 1 + packages/website/src/faq.json | 22 +++++ packages/website/src/pages/CommunityPage.tsx | 2 + packages/website/src/pages/ContributePage.tsx | 2 + packages/website/src/pages/DocsPage.tsx | 2 + packages/website/src/pages/HomePage.tsx | 19 +++++ packages/website/src/seo.routes.json | 16 ++-- packages/website/src/seo.ts | 52 ++++++++++++ packages/website/src/styles.css | 54 ++++++++++++ 19 files changed, 347 insertions(+), 37 deletions(-) create mode 100644 docs/design/website-seo-faq.md create mode 100644 packages/website/src/components/Breadcrumbs.tsx create mode 100644 packages/website/src/faq.json diff --git a/docs/DESIGN_SYSTEM.md b/docs/DESIGN_SYSTEM.md index e65ca29..af15ee8 100644 --- a/docs/DESIGN_SYSTEM.md +++ b/docs/DESIGN_SYSTEM.md @@ -32,3 +32,5 @@ Tap targets ≥ 40px; body text contrast AA. | PackageCard | Chrome / VS Code / CLI | | DocsSection | Package docs blocks | | Not found | Unknown path: H1 + Home / Docs | +| Breadcrumbs | Inner pages: Home / current | +| FAQ | Home `
` list; copy must match FAQ JSON-LD | diff --git a/docs/HANDOFF.md b/docs/HANDOFF.md index 2b9cc75..1400695 100644 --- a/docs/HANDOFF.md +++ b/docs/HANDOFF.md @@ -4,7 +4,6 @@ ## Current state -- **Outcome:** Website prepared for Google indexing (path URLs, robots, sitemap, unique titles). -- **Shipped:** BrowserRouter; hash `#/…` rewrite; `public/robots.txt` + `sitemap.xml`; `src/seo.ts` + `SeoHead`; `inject-seo.mjs`; HTTP 404 + `NotFoundPage`; origin `https://lexai.juankibin.space`. -- **Verified:** `website:build` OK. Local `node server.mjs`: `/` `/docs` `/robots.txt` `/sitemap.xml` 200 (docs title unique; sitemap `application/xml`); `/no-such-page` 404. -- **Next smallest action:** Redeploy Coolify, then Search Console: verify property, submit sitemap, request index of `/` and `/docs`. +- **Outcome:** Sitemap accepted in GSC. SEO pass: intent titles, home FAQ + FAQPage JSON-LD, breadcrumbs, lastmod sitemap, 5-min cache on robots/sitemap. +- **Verified:** `website:typecheck` + `website:build` OK (inject wrote route HTML + sitemap.xml). +- **Next smallest action:** Merge/deploy this to `main`, purge Cloudflare `/robots.txt`, then in GSC request indexing for `/` and `/docs`. diff --git a/docs/PROGRESS.md b/docs/PROGRESS.md index aeed01b..57b4110 100644 --- a/docs/PROGRESS.md +++ b/docs/PROGRESS.md @@ -2,34 +2,28 @@ > For the owner. What works, how to see it, and what's waiting on you — plain language. -**Updated:** 2026-09-03 · **Overall:** Website is ready for Google Search; needs a Coolify redeploy, then Search Console +**Updated:** 2026-09-04 · **Overall:** Search Console sitemap is success; next deploy the SEO copy/FAQ pass ## What works now - Chrome extension: select on any page → AI action → Replace/Copy - VS Code extension: select in editor → LexAI context menu / Command Palette → selection replaced -- Website (`packages/website`): Home, Docs, Contribute, Community at real paths (`/docs`, not `/#/docs`). `/robots.txt` and `/sitemap.xml` are in the build. +- Website: real paths, sitemap accepted by Google. Repo now has stronger titles, a home FAQ, and richer structured data (not live until you deploy) -## See it yourself — website on Coolify +## See it yourself -1. Pull/push this change, then in Coolify set **Base Directory** to `packages/website` (no leading slash). -2. Turn **Is it a static site?** **off**. -3. Either **Build Pack: Dockerfile** and port **80**, or **Nixpacks** and port **3000**. -4. Redeploy, then open `https://lexai.juankibin.space/` — docs at `/docs`. -5. In [Google Search Console](https://search.google.com/search-console): add that URL, verify ownership, submit `https://lexai.juankibin.space/sitemap.xml`, request indexing for Home and Docs. - -Local check: `npm run website:build` then `npm run website:start` and visit http://127.0.0.1:3000/ +1. Merge/push this change, Coolify on **main**, redeploy `packages/website`. +2. Home should end with an FAQ. Docs should show Home / Docs crumbs. +3. Search Console → URL inspection → request indexing for `https://lexai.juankibin.space/` and `/docs`. ## Waiting on you | # | Decision | Options (recommended bold) | What it unblocks | | --- | --- | --- | --- | -| 1 | Redeploy the Coolify website with the settings above | **Dockerfile + port 80** / Nixpacks + port 3000 | public LexAI site + crawlable URLs | -| 2 | Google Search Console: verify + submit sitemap | **Do this after redeploy** / later | Google indexing | -| 3 | Confirm Cursor mid/strong model picker IDs | **Keep written IDs** / send exact picker strings | reliable Task dispatch | -| 4 | Grammarly reference screenshots? | **Later** / capture into `docs/reference/` | Chrome UX gauntlet | -| 5 | Publish VS Code extension to Marketplace? | **Stay local for now** / set publisher + publish | public install | +| 1 | Deploy this SEO pass to production | **Redeploy Coolify from main** / wait | Google seeing the new titles/FAQ | +| 2 | Request indexing for Home and Docs | **Do after deploy** / later | Faster first index | +| 3 | Grammarly reference screenshots? | **Later** / capture into `docs/reference/` | Chrome UX gauntlet | ## Next up — proceeds without you -- Optional: VS Code Prompt Builder / style quick-pick / Marketplace packaging when you ask +- Rankings take days to weeks; no extra code until the new HTML is live diff --git a/docs/design/website-seo-faq.md b/docs/design/website-seo-faq.md new file mode 100644 index 0000000..31a349c --- /dev/null +++ b/docs/design/website-seo-faq.md @@ -0,0 +1,30 @@ +# Home FAQ (SEO) + +## User + job + +Someone comparing LexAI to Grammarly or Copilot gets a direct answer without leaving Home. Success: they install or open Docs. + +## Flow + +Home → FAQ section → expand one question → Docs or store CTA already on the page. Target: read in place (0 extra pages). + +## Screen states + +| State | What | +| --- | --- | +| Success | Five `
`; first collapsed | +| Empty / loading / error / offline | Static copy; no extra states | + +## Copy + +Questions and answers live in `packages/website/src/faq.json` only (visible text = JSON-LD). + +## Accessibility + +`summary` min-height 44px; native details/summary keyboard. Breadcrumbs: `nav aria-label="Breadcrumb"` on Docs, Contribute, Community. + +## Acceptance + +1. Home `#faq` shows the five Q&As from `faq.json`. +2. Built `dist/index.html` includes `FAQPage` JSON-LD with the same questions. +3. Docs/Contribute/Community show Home / current breadcrumbs. diff --git a/packages/website/index.html b/packages/website/index.html index 323021e..a358083 100644 --- a/packages/website/index.html +++ b/packages/website/index.html @@ -6,23 +6,31 @@ + - + + - + @@ -31,7 +39,7 @@ href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap" rel="stylesheet" /> - LexAI — BYO-LLM writing & code assist + LexAI — Grammarly-style writing help with your own API key `, + ); return out; } @@ -50,6 +110,7 @@ for (const route of routes) { description: route.description, url, robots: 'index, follow', + ld: jsonLd(route), }), ); written.push(file); @@ -62,8 +123,30 @@ writeFileSync( description: 'That address is not a LexAI page.', url: `${origin}/`, robots: 'noindex, follow', + ld: JSON.stringify({ + '@context': 'https://schema.org', + '@graph': [{ '@type': 'WebSite', name: 'LexAI', url: `${origin}/` }, softwareGraph()], + }), }), ); written.push('404.html'); +const today = new Date().toISOString().slice(0, 10); +const sitemap = ` + +${routes + .map( + (r, i) => ` + ${r.path === '/' ? `${origin}/` : `${origin}${r.path}`} + ${today} + ${i < 2 ? 'weekly' : 'monthly'} + ${r.path === '/' ? '1.0' : r.path === '/docs' ? '0.8' : '0.5'} + `, + ) + .join('\n')} + +`; +writeFileSync(join(root, 'dist', 'sitemap.xml'), sitemap); +written.push('sitemap.xml'); + console.log('SEO HTML written for', written.join(', ')); diff --git a/packages/website/nginx.conf b/packages/website/nginx.conf index 8b5b155..a4e3ae7 100644 --- a/packages/website/nginx.conf +++ b/packages/website/nginx.conf @@ -11,11 +11,13 @@ server { } location = /robots.txt { + add_header Cache-Control "public, max-age=300"; try_files /robots.txt =404; } location = /sitemap.xml { default_type application/xml; + add_header Cache-Control "public, max-age=300"; try_files /sitemap.xml =404; } diff --git a/packages/website/server.mjs b/packages/website/server.mjs index dd0c15d..61cf79a 100644 --- a/packages/website/server.mjs +++ b/packages/website/server.mjs @@ -82,7 +82,12 @@ const server = createServer((req, res) => { } const type = MIME[extname(file).toLowerCase()] ?? 'application/octet-stream'; - res.writeHead(status, { 'Content-Type': type }); + const headers = { 'Content-Type': type }; + const base = file.replace(/\\/g, '/'); + if (base.endsWith('/robots.txt') || base.endsWith('/sitemap.xml')) { + headers['Cache-Control'] = 'public, max-age=300'; + } + res.writeHead(status, headers); if (req.method === 'HEAD') { res.end(); return; diff --git a/packages/website/src/components/Breadcrumbs.tsx b/packages/website/src/components/Breadcrumbs.tsx new file mode 100644 index 0000000..060219e --- /dev/null +++ b/packages/website/src/components/Breadcrumbs.tsx @@ -0,0 +1,24 @@ +import { Link } from 'react-router-dom'; + +type Crumb = { label: string; to?: string }; + +export function Breadcrumbs({ items }: { items: Crumb[] }) { + return ( + + ); +} diff --git a/packages/website/src/components/SeoHead.tsx b/packages/website/src/components/SeoHead.tsx index e9fc7dc..515716b 100644 --- a/packages/website/src/components/SeoHead.tsx +++ b/packages/website/src/components/SeoHead.tsx @@ -1,6 +1,6 @@ import { useEffect } from 'react'; import { useLocation } from 'react-router-dom'; -import { canonicalUrl, seoForPath, SITE_ORIGIN } from '../seo'; +import { canonicalUrl, jsonLdForPath, seoForPath, SITE_ORIGIN } from '../seo'; function upsertMeta(selector: string, attrs: Record, content: string) { let el = document.head.querySelector(selector) as HTMLMetaElement | null; @@ -41,6 +41,15 @@ export function SeoHead() { upsertMeta('meta[property="og:url"]', { property: 'og:url' }, url); upsertMeta('meta[name="twitter:title"]', { name: 'twitter:title' }, seo.title); upsertMeta('meta[name="twitter:description"]', { name: 'twitter:description' }, seo.description); + let ld = document.head.querySelector( + 'script[type="application/ld+json"]', + ) as HTMLScriptElement | null; + if (!ld) { + ld = document.createElement('script'); + ld.type = 'application/ld+json'; + document.head.appendChild(ld); + } + ld.textContent = JSON.stringify(jsonLdForPath(pathname)); }, [pathname]); return null; diff --git a/packages/website/src/components/SiteFooter.tsx b/packages/website/src/components/SiteFooter.tsx index 4ee868a..3fe1ae8 100644 --- a/packages/website/src/components/SiteFooter.tsx +++ b/packages/website/src/components/SiteFooter.tsx @@ -8,6 +8,7 @@ export function SiteFooter() {

© {new Date().getFullYear()} LexAI · MIT

Docs + FAQ Contribute Community diff --git a/packages/website/src/faq.json b/packages/website/src/faq.json new file mode 100644 index 0000000..a3f37ed --- /dev/null +++ b/packages/website/src/faq.json @@ -0,0 +1,22 @@ +[ + { + "q": "Is LexAI free?", + "a": "Yes. LexAI has no subscription and no account. You only pay the LLM provider you choose. Groq and OpenRouter have free-tier keys you can use to start." + }, + { + "q": "Do I need my own API key?", + "a": "Yes. LexAI is bring-your-own-LLM: paste a key for OpenAI, Anthropic, Groq, or OpenRouter in Chrome Options or VS Code settings. There is no LexAI backend." + }, + { + "q": "Is LexAI a Grammarly alternative?", + "a": "For writing help, yes: select text on any webpage and Fix, Rephrase, Shorten, Expand, or Explain. Unlike Grammarly, your key stays on your device and text goes only to the provider you pick." + }, + { + "q": "How is LexAI different from GitHub Copilot?", + "a": "In VS Code or Cursor you select code, give an instruction, and apply a suggestion with file context. It is not tab-complete autocomplete. You use your own provider key instead of a Copilot subscription." + }, + { + "q": "Does LexAI store my text or API key?", + "a": "No LexAI servers sit in the path. The Chrome key is stored on your device; VS Code uses Secret Storage. Text is sent only to the provider endpoint you configure. There is no LexAI telemetry." + } +] diff --git a/packages/website/src/pages/CommunityPage.tsx b/packages/website/src/pages/CommunityPage.tsx index 390aa09..5e86893 100644 --- a/packages/website/src/pages/CommunityPage.tsx +++ b/packages/website/src/pages/CommunityPage.tsx @@ -1,4 +1,5 @@ import { Link } from 'react-router-dom'; +import { Breadcrumbs } from '../components/Breadcrumbs'; import { CHROME_STORE, REPO, @@ -12,6 +13,7 @@ export function CommunityPage() { return (
+

Community

LexAI is maintained in the open. Use these links to install, report issues, and follow the diff --git a/packages/website/src/pages/ContributePage.tsx b/packages/website/src/pages/ContributePage.tsx index 929e4c3..edf1cb2 100644 --- a/packages/website/src/pages/ContributePage.tsx +++ b/packages/website/src/pages/ContributePage.tsx @@ -1,9 +1,11 @@ +import { Breadcrumbs } from '../components/Breadcrumbs'; import { REPO, REPO_CONDUCT, REPO_CONTRIBUTING } from '../links'; export function ContributePage() { return (

+

Contribute

LexAI is a monorepo: Chrome, VS Code, CLI, website, and a shared library. Please follow diff --git a/packages/website/src/pages/DocsPage.tsx b/packages/website/src/pages/DocsPage.tsx index eee2a1a..145a255 100644 --- a/packages/website/src/pages/DocsPage.tsx +++ b/packages/website/src/pages/DocsPage.tsx @@ -1,5 +1,6 @@ import { useEffect } from 'react'; import { Link } from 'react-router-dom'; +import { Breadcrumbs } from '../components/Breadcrumbs'; import { FREE_PROVIDERS } from '../freeProviders'; import { CHROME_STORE, REPO, VSCODE_MARKETPLACE } from '../links'; @@ -38,6 +39,7 @@ export function DocsPage() {

+

Documentation

LexAI is an open-source, bring-your-own-LLM assistant. This page covers every package and diff --git a/packages/website/src/pages/HomePage.tsx b/packages/website/src/pages/HomePage.tsx index da52506..d2cdf2d 100644 --- a/packages/website/src/pages/HomePage.tsx +++ b/packages/website/src/pages/HomePage.tsx @@ -1,6 +1,7 @@ import { Link, useNavigate } from 'react-router-dom'; import { LogoMark } from '../components/LogoMark'; import { FREE_PROVIDERS, FREE_PROVIDER_BADGES, LEXAI_FREE_HIGHLIGHTS } from '../freeProviders'; +import faq from '../faq.json'; import { CHROME_STORE, REPO, VSCODE_MARKETPLACE } from '../links'; function openDocsSection(navigate: ReturnType, id: string) { @@ -232,6 +233,24 @@ export function HomePage() {

+ +
+
+

FAQ

+

Questions

+

+ Short answers for people comparing LexAI to Grammarly, Copilot, and hosted writing tools. +

+
+ {faq.map((item) => ( +
+ {item.q} +

{item.a}

+
+ ))} +
+
+
); } diff --git a/packages/website/src/seo.routes.json b/packages/website/src/seo.routes.json index 201748c..3375a0a 100644 --- a/packages/website/src/seo.routes.json +++ b/packages/website/src/seo.routes.json @@ -3,23 +3,23 @@ "routes": [ { "path": "/", - "title": "LexAI — BYO-LLM writing & code assist", - "description": "LexAI — bring-your-own-LLM writing and code assist. Chrome, VS Code, and CLI. No LexAI servers, no subscription." + "title": "LexAI — Grammarly-style writing help with your own API key", + "description": "Free Chrome extension and VS Code assist. Bring your own OpenAI, Anthropic, Groq, or OpenRouter key. No LexAI servers, account, or subscription." }, { "path": "/docs", - "title": "Docs — LexAI", - "description": "Install and use LexAI in Chrome, VS Code, and the CLI. Free LLM setup, privacy, and the shared core." + "title": "LexAI docs — Chrome, VS Code, CLI, and free LLM setup", + "description": "Install LexAI, add a free Groq or OpenRouter key, and use writing actions in Chrome or code assist in VS Code. Privacy and shared-core notes included." }, { "path": "/contribute", - "title": "Contribute — LexAI", - "description": "How to report bugs, set up the LexAI monorepo, and send focused pull requests." + "title": "Contribute to LexAI — open-source BYO-LLM assistant", + "description": "Report bugs, set up the LexAI monorepo (Chrome, VS Code, CLI, website), and send focused pull requests. MIT license, Code of Conduct." }, { "path": "/community", - "title": "Community — LexAI", - "description": "LexAI source, issues, Chrome Web Store, VS Code Marketplace, and Code of Conduct." + "title": "LexAI community — source, issues, and installs", + "description": "Chrome Web Store, VS Code Marketplace, Gitea repository, issue tracker, and Code of Conduct for the LexAI project." } ] } diff --git a/packages/website/src/seo.ts b/packages/website/src/seo.ts index 0fe551e..67fcc80 100644 --- a/packages/website/src/seo.ts +++ b/packages/website/src/seo.ts @@ -1,4 +1,6 @@ +import faq from './faq.json'; import routesFile from './seo.routes.json'; +import { CHROME_STORE, REPO, VSCODE_MARKETPLACE } from './links'; export const SITE_ORIGIN = routesFile.origin; @@ -32,3 +34,53 @@ export function canonicalUrl(path: string): string { if (!path || path === '/') return `${SITE_ORIGIN}/`; return `${SITE_ORIGIN}${path.startsWith('/') ? path : `/${path}`}`; } + +function softwareGraph() { + return { + '@type': 'SoftwareApplication', + name: 'LexAI', + applicationCategory: 'BrowserApplication', + applicationSubCategory: 'Writing assistance', + operatingSystem: 'Chrome, VS Code, Windows, macOS, Linux', + url: `${SITE_ORIGIN}/`, + downloadUrl: [CHROME_STORE, VSCODE_MARKETPLACE], + license: `${REPO}/src/branch/main/LICENSE`, + sameAs: [REPO, CHROME_STORE, VSCODE_MARKETPLACE], + offers: { '@type': 'Offer', price: '0', priceCurrency: 'USD' }, + }; +} + +export function jsonLdForPath(pathname: string): Record { + const seo = seoForPath(pathname); + const url = seo.index ? canonicalUrl(seo.path) : `${SITE_ORIGIN}${pathname}`; + const graph: unknown[] = [ + { + '@type': 'WebSite', + name: 'LexAI', + url: `${SITE_ORIGIN}/`, + description: seoForPath('/').description, + }, + softwareGraph(), + ]; + + if (seo.path === '/') { + graph.push({ + '@type': 'FAQPage', + mainEntity: faq.map((item) => ({ + '@type': 'Question', + name: item.q, + acceptedAnswer: { '@type': 'Answer', text: item.a }, + })), + }); + } else if (seo.index) { + graph.push({ + '@type': 'BreadcrumbList', + itemListElement: [ + { '@type': 'ListItem', position: 1, name: 'Home', item: `${SITE_ORIGIN}/` }, + { '@type': 'ListItem', position: 2, name: seo.title, item: url }, + ], + }); + } + + return { '@context': 'https://schema.org', '@graph': graph }; +} diff --git a/packages/website/src/styles.css b/packages/website/src/styles.css index 05344d8..02356cd 100644 --- a/packages/website/src/styles.css +++ b/packages/website/src/styles.css @@ -264,6 +264,60 @@ main { color: var(--muted); } +.faq-list { + display: grid; + gap: 0.5rem; +} + +.faq-item { + background: var(--surface); + border: 1px solid var(--line); + border-radius: var(--radius); +} + +.faq-item summary { + cursor: pointer; + list-style: none; + min-height: 44px; + padding: 0.75rem 1rem; + font-weight: 560; +} + +.faq-item summary::-webkit-details-marker { + display: none; +} + +.faq-item p { + margin: 0; + padding: 0 1rem 1rem; + color: var(--muted); +} + +.crumbs { + margin: 0 0 1rem; + font-size: 0.875rem; +} + +.crumbs ol { + display: flex; + flex-wrap: wrap; + gap: 0.35rem; + margin: 0; + padding: 0; + list-style: none; + color: var(--muted); +} + +.crumbs li:not(:last-child)::after { + content: '/'; + margin-left: 0.35rem; + color: var(--line); +} + +.crumbs a { + color: var(--muted); +} + .feature-list { display: grid; gap: 0;