Compare commits

2 Commits

Author SHA1 Message Date
fc80e9b08c Merge pull request 'feat(website): add search-intent titles, FAQ schema, and breadcrumbs' (#6) from dev into main
All checks were successful
CI — Website / Website — typecheck, build (push) Successful in 59s
Reviewed-on: #6
2026-09-03 23:15:19 +00:00
john kevin asprec
03ad49f523 feat(website): add search-intent titles, FAQ schema, and breadcrumbs
All checks were successful
CI — Website / Website — typecheck, build (pull_request) Successful in 1m2s
Give Google unique snippets and FAQ rich results, and keep robots/sitemap from being cached as HTML.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-04 07:08:57 +08:00
19 changed files with 347 additions and 37 deletions

View File

@@ -32,3 +32,5 @@ Tap targets ≥ 40px; body text contrast AA.
| PackageCard | Chrome / VS Code / CLI | | PackageCard | Chrome / VS Code / CLI |
| DocsSection | Package docs blocks | | DocsSection | Package docs blocks |
| Not found | Unknown path: H1 + Home / Docs | | Not found | Unknown path: H1 + Home / Docs |
| Breadcrumbs | Inner pages: Home / current |
| FAQ | Home `<details>` list; copy must match FAQ JSON-LD |

View File

@@ -4,7 +4,6 @@
## Current state ## Current state
- **Outcome:** Website prepared for Google indexing (path URLs, robots, sitemap, unique titles). - **Outcome:** Sitemap accepted in GSC. SEO pass: intent titles, home FAQ + FAQPage JSON-LD, breadcrumbs, lastmod sitemap, 5-min cache on robots/sitemap.
- **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:typecheck` + `website:build` OK (inject wrote route HTML + sitemap.xml).
- **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:** Merge/deploy this to `main`, purge Cloudflare `/robots.txt`, then in GSC request indexing for `/` and `/docs`.
- **Next smallest action:** Redeploy Coolify, then Search Console: verify property, submit sitemap, request index of `/` and `/docs`.

View File

@@ -2,34 +2,28 @@
> For the owner. What works, how to see it, and what's waiting on you — plain language. > 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 ## What works now
- Chrome extension: select on any page → AI action → Replace/Copy - Chrome extension: select on any page → AI action → Replace/Copy
- VS Code extension: select in editor → LexAI context menu / Command Palette → selection replaced - 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). 1. Merge/push this change, Coolify on **main**, redeploy `packages/website`.
2. Turn **Is it a static site?** **off**. 2. Home should end with an FAQ. Docs should show Home / Docs crumbs.
3. Either **Build Pack: Dockerfile** and port **80**, or **Nixpacks** and port **3000**. 3. Search Console → URL inspection → request indexing for `https://lexai.juankibin.space/` and `/docs`.
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/
## Waiting on you ## Waiting on you
| # | Decision | Options (recommended bold) | What it unblocks | | # | 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 | | 1 | Deploy this SEO pass to production | **Redeploy Coolify from main** / wait | Google seeing the new titles/FAQ |
| 2 | Google Search Console: verify + submit sitemap | **Do this after redeploy** / later | Google indexing | | 2 | Request indexing for Home and Docs | **Do after deploy** / later | Faster first index |
| 3 | Confirm Cursor mid/strong model picker IDs | **Keep written IDs** / send exact picker strings | reliable Task dispatch | | 3 | Grammarly reference screenshots? | **Later** / capture into `docs/reference/` | Chrome UX gauntlet |
| 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 |
## Next up — proceeds without you ## 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

View File

@@ -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 `<details>`; 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.

View File

@@ -6,23 +6,31 @@
<meta name="robots" content="index, follow" /> <meta name="robots" content="index, follow" />
<meta <meta
name="description" name="description"
content="LexAI — bring-your-own-LLM writing and code assist. Chrome, VS Code, and CLI. No LexAI servers, no subscription." content="Free Chrome extension and VS Code assist. Bring your own OpenAI, Anthropic, Groq, or OpenRouter key. No LexAI servers, account, or subscription."
/> />
<link rel="canonical" href="https://lexai.juankibin.space/" /> <link rel="canonical" href="https://lexai.juankibin.space/" />
<meta name="theme-color" content="#f7f7f5" />
<meta property="og:type" content="website" /> <meta property="og:type" content="website" />
<meta property="og:site_name" content="LexAI" /> <meta property="og:site_name" content="LexAI" />
<meta property="og:title" content="LexAI — BYO-LLM writing &amp; code assist" /> <meta
property="og:title"
content="LexAI — Grammarly-style writing help with your own API key"
/>
<meta <meta
property="og:description" property="og:description"
content="LexAI — bring-your-own-LLM writing and code assist. Chrome, VS Code, and CLI. No LexAI servers, no subscription." content="Free Chrome extension and VS Code assist. Bring your own OpenAI, Anthropic, Groq, or OpenRouter key. No LexAI servers, account, or subscription."
/> />
<meta property="og:url" content="https://lexai.juankibin.space/" /> <meta property="og:url" content="https://lexai.juankibin.space/" />
<meta property="og:image" content="https://lexai.juankibin.space/icon.svg" /> <meta property="og:image" content="https://lexai.juankibin.space/icon.svg" />
<meta property="og:image:alt" content="LexAI logo" />
<meta name="twitter:card" content="summary" /> <meta name="twitter:card" content="summary" />
<meta name="twitter:title" content="LexAI — BYO-LLM writing &amp; code assist" /> <meta
name="twitter:title"
content="LexAI — Grammarly-style writing help with your own API key"
/>
<meta <meta
name="twitter:description" name="twitter:description"
content="LexAI — bring-your-own-LLM writing and code assist. Chrome, VS Code, and CLI. No LexAI servers, no subscription." content="Free Chrome extension and VS Code assist. Bring your own OpenAI, Anthropic, Groq, or OpenRouter key. No LexAI servers, account, or subscription."
/> />
<link rel="icon" type="image/svg+xml" href="/icon.svg" /> <link rel="icon" type="image/svg+xml" href="/icon.svg" />
<link rel="preconnect" href="https://fonts.googleapis.com" /> <link rel="preconnect" href="https://fonts.googleapis.com" />
@@ -31,7 +39,7 @@
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap"
rel="stylesheet" rel="stylesheet"
/> />
<title>LexAI — BYO-LLM writing &amp; code assist</title> <title>LexAI — Grammarly-style writing help with your own API key</title>
<script type="application/ld+json"> <script type="application/ld+json">
{ {
"@context": "https://schema.org", "@context": "https://schema.org",

View File

@@ -7,8 +7,64 @@ const distHtml = join(root, 'dist', 'index.html');
const { origin, routes } = JSON.parse( const { origin, routes } = JSON.parse(
readFileSync(join(root, 'src', 'seo.routes.json'), 'utf8'), readFileSync(join(root, 'src', 'seo.routes.json'), 'utf8'),
); );
const faq = JSON.parse(readFileSync(join(root, 'src', 'faq.json'), 'utf8'));
function apply(html, { title, description, url, robots }) { const CHROME_STORE =
'https://chromewebstore.google.com/detail/bagpcheidbkfgijnnmolnkgagibbjfnk';
const VSCODE_MARKETPLACE =
'https://marketplace.visualstudio.com/items?itemName=JuanKibin.lexai-vscode';
const REPO = 'https://git.juankibin.space/kibin/LexAI';
const homeDescription = routes.find((r) => r.path === '/')?.description ?? '';
function softwareGraph() {
return {
'@type': 'SoftwareApplication',
name: 'LexAI',
applicationCategory: 'BrowserApplication',
applicationSubCategory: 'Writing assistance',
operatingSystem: 'Chrome, VS Code, Windows, macOS, Linux',
url: `${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' },
};
}
function jsonLd(route) {
const url = route.path === '/' ? `${origin}/` : `${origin}${route.path}`;
const graph = [
{
'@type': 'WebSite',
name: 'LexAI',
url: `${origin}/`,
description: homeDescription,
},
softwareGraph(),
];
if (route.path === '/') {
graph.push({
'@type': 'FAQPage',
mainEntity: faq.map((item) => ({
'@type': 'Question',
name: item.q,
acceptedAnswer: { '@type': 'Answer', text: item.a },
})),
});
} else {
graph.push({
'@type': 'BreadcrumbList',
itemListElement: [
{ '@type': 'ListItem', position: 1, name: 'Home', item: `${origin}/` },
{ '@type': 'ListItem', position: 2, name: route.title, item: url },
],
});
}
return JSON.stringify({ '@context': 'https://schema.org', '@graph': graph });
}
function apply(html, { title, description, url, robots, ld }) {
let out = html; let out = html;
out = out.replace(/<title>[\s\S]*?<\/title>/, `<title>${title}</title>`); out = out.replace(/<title>[\s\S]*?<\/title>/, `<title>${title}</title>`);
out = out.replace( out = out.replace(
@@ -28,6 +84,10 @@ function apply(html, { title, description, url, robots }) {
`$1${description}$2`, `$1${description}$2`,
); );
out = out.replace(/(<link\s+rel="canonical"\s+href=")[^"]*(")/, `$1${url}$2`); out = out.replace(/(<link\s+rel="canonical"\s+href=")[^"]*(")/, `$1${url}$2`);
out = out.replace(
/<script type="application\/ld\+json">[\s\S]*?<\/script>/,
`<script type="application/ld+json">${ld}</script>`,
);
return out; return out;
} }
@@ -50,6 +110,7 @@ for (const route of routes) {
description: route.description, description: route.description,
url, url,
robots: 'index, follow', robots: 'index, follow',
ld: jsonLd(route),
}), }),
); );
written.push(file); written.push(file);
@@ -62,8 +123,30 @@ writeFileSync(
description: 'That address is not a LexAI page.', description: 'That address is not a LexAI page.',
url: `${origin}/`, url: `${origin}/`,
robots: 'noindex, follow', robots: 'noindex, follow',
ld: JSON.stringify({
'@context': 'https://schema.org',
'@graph': [{ '@type': 'WebSite', name: 'LexAI', url: `${origin}/` }, softwareGraph()],
}),
}), }),
); );
written.push('404.html'); written.push('404.html');
const today = new Date().toISOString().slice(0, 10);
const sitemap = `<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
${routes
.map(
(r, i) => ` <url>
<loc>${r.path === '/' ? `${origin}/` : `${origin}${r.path}`}</loc>
<lastmod>${today}</lastmod>
<changefreq>${i < 2 ? 'weekly' : 'monthly'}</changefreq>
<priority>${r.path === '/' ? '1.0' : r.path === '/docs' ? '0.8' : '0.5'}</priority>
</url>`,
)
.join('\n')}
</urlset>
`;
writeFileSync(join(root, 'dist', 'sitemap.xml'), sitemap);
written.push('sitemap.xml');
console.log('SEO HTML written for', written.join(', ')); console.log('SEO HTML written for', written.join(', '));

View File

@@ -11,11 +11,13 @@ server {
} }
location = /robots.txt { location = /robots.txt {
add_header Cache-Control "public, max-age=300";
try_files /robots.txt =404; try_files /robots.txt =404;
} }
location = /sitemap.xml { location = /sitemap.xml {
default_type application/xml; default_type application/xml;
add_header Cache-Control "public, max-age=300";
try_files /sitemap.xml =404; try_files /sitemap.xml =404;
} }

View File

@@ -82,7 +82,12 @@ const server = createServer((req, res) => {
} }
const type = MIME[extname(file).toLowerCase()] ?? 'application/octet-stream'; 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') { if (req.method === 'HEAD') {
res.end(); res.end();
return; return;

View File

@@ -0,0 +1,24 @@
import { Link } from 'react-router-dom';
type Crumb = { label: string; to?: string };
export function Breadcrumbs({ items }: { items: Crumb[] }) {
return (
<nav className="crumbs" aria-label="Breadcrumb">
<ol>
{items.map((item, i) => {
const last = i === items.length - 1;
return (
<li key={item.label}>
{last || !item.to ? (
<span aria-current={last ? 'page' : undefined}>{item.label}</span>
) : (
<Link to={item.to}>{item.label}</Link>
)}
</li>
);
})}
</ol>
</nav>
);
}

View File

@@ -1,6 +1,6 @@
import { useEffect } from 'react'; import { useEffect } from 'react';
import { useLocation } from 'react-router-dom'; 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<string, string>, content: string) { function upsertMeta(selector: string, attrs: Record<string, string>, content: string) {
let el = document.head.querySelector(selector) as HTMLMetaElement | null; 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[property="og:url"]', { property: 'og:url' }, url);
upsertMeta('meta[name="twitter:title"]', { name: 'twitter:title' }, seo.title); upsertMeta('meta[name="twitter:title"]', { name: 'twitter:title' }, seo.title);
upsertMeta('meta[name="twitter:description"]', { name: 'twitter:description' }, seo.description); 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]); }, [pathname]);
return null; return null;

View File

@@ -8,6 +8,7 @@ export function SiteFooter() {
<p style={{ margin: 0 }}>© {new Date().getFullYear()} LexAI · MIT</p> <p style={{ margin: 0 }}>© {new Date().getFullYear()} LexAI · MIT</p>
<div style={{ display: 'flex', gap: '1rem', flexWrap: 'wrap' }}> <div style={{ display: 'flex', gap: '1rem', flexWrap: 'wrap' }}>
<Link to="/docs">Docs</Link> <Link to="/docs">Docs</Link>
<Link to={{ pathname: '/', hash: 'faq' }}>FAQ</Link>
<Link to="/contribute">Contribute</Link> <Link to="/contribute">Contribute</Link>
<Link to="/community">Community</Link> <Link to="/community">Community</Link>
<a href={REPO} target="_blank" rel="noreferrer"> <a href={REPO} target="_blank" rel="noreferrer">

View File

@@ -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."
}
]

View File

@@ -1,4 +1,5 @@
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { Breadcrumbs } from '../components/Breadcrumbs';
import { import {
CHROME_STORE, CHROME_STORE,
REPO, REPO,
@@ -12,6 +13,7 @@ export function CommunityPage() {
return ( return (
<div className="wrap page"> <div className="wrap page">
<article className="docs-content"> <article className="docs-content">
<Breadcrumbs items={[{ label: 'Home', to: '/' }, { label: 'Community' }]} />
<h1>Community</h1> <h1>Community</h1>
<p className="intro"> <p className="intro">
LexAI is maintained in the open. Use these links to install, report issues, and follow the LexAI is maintained in the open. Use these links to install, report issues, and follow the

View File

@@ -1,9 +1,11 @@
import { Breadcrumbs } from '../components/Breadcrumbs';
import { REPO, REPO_CONDUCT, REPO_CONTRIBUTING } from '../links'; import { REPO, REPO_CONDUCT, REPO_CONTRIBUTING } from '../links';
export function ContributePage() { export function ContributePage() {
return ( return (
<div className="wrap page"> <div className="wrap page">
<article className="docs-content"> <article className="docs-content">
<Breadcrumbs items={[{ label: 'Home', to: '/' }, { label: 'Contribute' }]} />
<h1>Contribute</h1> <h1>Contribute</h1>
<p className="intro"> <p className="intro">
LexAI is a monorepo: Chrome, VS Code, CLI, website, and a shared library. Please follow LexAI is a monorepo: Chrome, VS Code, CLI, website, and a shared library. Please follow

View File

@@ -1,5 +1,6 @@
import { useEffect } from 'react'; import { useEffect } from 'react';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { Breadcrumbs } from '../components/Breadcrumbs';
import { FREE_PROVIDERS } from '../freeProviders'; import { FREE_PROVIDERS } from '../freeProviders';
import { CHROME_STORE, REPO, VSCODE_MARKETPLACE } from '../links'; import { CHROME_STORE, REPO, VSCODE_MARKETPLACE } from '../links';
@@ -38,6 +39,7 @@ export function DocsPage() {
</aside> </aside>
<article className="docs-content"> <article className="docs-content">
<Breadcrumbs items={[{ label: 'Home', to: '/' }, { label: 'Docs' }]} />
<h1>Documentation</h1> <h1>Documentation</h1>
<p className="intro"> <p className="intro">
LexAI is an open-source, bring-your-own-LLM assistant. This page covers every package and LexAI is an open-source, bring-your-own-LLM assistant. This page covers every package and

View File

@@ -1,6 +1,7 @@
import { Link, useNavigate } from 'react-router-dom'; import { Link, useNavigate } from 'react-router-dom';
import { LogoMark } from '../components/LogoMark'; import { LogoMark } from '../components/LogoMark';
import { FREE_PROVIDERS, FREE_PROVIDER_BADGES, LEXAI_FREE_HIGHLIGHTS } from '../freeProviders'; import { FREE_PROVIDERS, FREE_PROVIDER_BADGES, LEXAI_FREE_HIGHLIGHTS } from '../freeProviders';
import faq from '../faq.json';
import { CHROME_STORE, REPO, VSCODE_MARKETPLACE } from '../links'; import { CHROME_STORE, REPO, VSCODE_MARKETPLACE } from '../links';
function openDocsSection(navigate: ReturnType<typeof useNavigate>, id: string) { function openDocsSection(navigate: ReturnType<typeof useNavigate>, id: string) {
@@ -232,6 +233,24 @@ export function HomePage() {
</ul> </ul>
</div> </div>
</section> </section>
<section className="section" id="faq">
<div className="wrap">
<p className="section-kicker">FAQ</p>
<h2>Questions</h2>
<p className="section-lead">
Short answers for people comparing LexAI to Grammarly, Copilot, and hosted writing tools.
</p>
<div className="faq-list">
{faq.map((item) => (
<details key={item.q} className="faq-item">
<summary>{item.q}</summary>
<p>{item.a}</p>
</details>
))}
</div>
</div>
</section>
</> </>
); );
} }

View File

@@ -3,23 +3,23 @@
"routes": [ "routes": [
{ {
"path": "/", "path": "/",
"title": "LexAI — BYO-LLM writing & code assist", "title": "LexAI — Grammarly-style writing help with your own API key",
"description": "LexAI — bring-your-own-LLM writing and code assist. Chrome, VS Code, and CLI. No LexAI servers, no subscription." "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", "path": "/docs",
"title": "Docs — LexAI", "title": "LexAI docs — Chrome, VS Code, CLI, and free LLM setup",
"description": "Install and use LexAI in Chrome, VS Code, and the CLI. Free LLM setup, privacy, and the shared core." "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", "path": "/contribute",
"title": "Contribute — LexAI", "title": "Contribute to LexAI — open-source BYO-LLM assistant",
"description": "How to report bugs, set up the LexAI monorepo, and send focused pull requests." "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", "path": "/community",
"title": "Community — LexAI", "title": "LexAI community — source, issues, and installs",
"description": "LexAI source, issues, Chrome Web Store, VS Code Marketplace, and Code of Conduct." "description": "Chrome Web Store, VS Code Marketplace, Gitea repository, issue tracker, and Code of Conduct for the LexAI project."
} }
] ]
} }

View File

@@ -1,4 +1,6 @@
import faq from './faq.json';
import routesFile from './seo.routes.json'; import routesFile from './seo.routes.json';
import { CHROME_STORE, REPO, VSCODE_MARKETPLACE } from './links';
export const SITE_ORIGIN = routesFile.origin; export const SITE_ORIGIN = routesFile.origin;
@@ -32,3 +34,53 @@ export function canonicalUrl(path: string): string {
if (!path || path === '/') return `${SITE_ORIGIN}/`; if (!path || path === '/') return `${SITE_ORIGIN}/`;
return `${SITE_ORIGIN}${path.startsWith('/') ? path : `/${path}`}`; 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<string, unknown> {
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 };
}

View File

@@ -264,6 +264,60 @@ main {
color: var(--muted); 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 { .feature-list {
display: grid; display: grid;
gap: 0; gap: 0;