feat(website): add search-intent titles, FAQ schema, and breadcrumbs
All checks were successful
CI — Website / Website — typecheck, build (pull_request) Successful in 1m2s
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>
This commit is contained in:
@@ -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 |
|
||||||
|
|||||||
@@ -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`.
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
30
docs/design/website-seo-faq.md
Normal file
30
docs/design/website-seo-faq.md
Normal 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.
|
||||||
@@ -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 & 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 & 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 & 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",
|
||||||
|
|||||||
@@ -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(', '));
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
24
packages/website/src/components/Breadcrumbs.tsx
Normal file
24
packages/website/src/components/Breadcrumbs.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
22
packages/website/src/faq.json
Normal file
22
packages/website/src/faq.json
Normal 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."
|
||||||
|
}
|
||||||
|
]
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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."
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 };
|
||||||
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user