Compare commits
5 Commits
efdae32ae1
...
dev
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e87fddc707 | ||
|
|
03ad49f523 | ||
|
|
3150e16248 | ||
|
|
2eb6eed16a | ||
|
|
2b2bdeac7a |
@@ -4,7 +4,7 @@ Two hooks, both small, both readable in a minute, both safe to delete. They exis
|
||||
|
||||
| Hook | Event | What it does |
|
||||
| --- | --- | --- |
|
||||
| `session-context.mjs` | `sessionStart` | Injects the session's starting facts: whether the model lanes are bound (and whether the picker drifted from the recorded lead), the handoff's next action, and any of the four capped context files currently over its cap. |
|
||||
| `session-context.mjs` | `sessionStart` | Injects the session's starting facts: whether the model lanes are bound (and whether the picker drifted from the recorded lead), the handoff's next action, any of the four capped context files currently over its cap, and the standing workflow (commit after each task; `graphify update .` at session end). |
|
||||
| `guard-destructive.mjs` | `beforeShellExecution` | Returns `ask` — never `deny` — for force pushes, `rm -rf`, migrations, deploys, infra changes, pipe-to-shell, and friends, with the reason named and the gate quoted back to the agent. Routine `git push` is deliberately not on the list. |
|
||||
|
||||
## Why these two
|
||||
|
||||
@@ -132,6 +132,10 @@ function main(payload) {
|
||||
notes.push(`Context caps EXCEEDED: ${over.join(" · ")}. Run /memory-sync before adding anything.`);
|
||||
}
|
||||
|
||||
notes.push(
|
||||
"Workflow: commit after each landed task (no secrets). Session end: graphify update .",
|
||||
);
|
||||
|
||||
return notes.length
|
||||
? { additional_context: `Project state (from .cursor/hooks/session-context.mjs):\n- ${notes.join("\n- ")}` }
|
||||
: {};
|
||||
|
||||
22
.cursor/rules/graphify.mdc
Normal file
22
.cursor/rules/graphify.mdc
Normal file
@@ -0,0 +1,22 @@
|
||||
---
|
||||
description: graphify knowledge graph context
|
||||
alwaysApply: true
|
||||
---
|
||||
|
||||
This project has a graphify knowledge graph at graphify-out/.
|
||||
|
||||
**MANDATORY: Before using Read, Grep, Glob, or Bash to explore the codebase, you MUST run graphify first:**
|
||||
- `graphify query "<question>"` — scoped subgraph for any codebase or architecture question
|
||||
- `graphify path "<A>" "<B>"` — dependency path between two symbols
|
||||
- `graphify explain "<concept>"` — all nodes related to a concept
|
||||
|
||||
This applies to YOU and to every subagent you spawn. Include this rule explicitly in every subagent prompt that involves code exploration. Do not skip graphify because files are "already known" or because you are executing a plan — the graph surfaces cross-file dependencies and INFERRED edges that grep and Read cannot find.
|
||||
|
||||
Only use Read/Grep/Glob directly when:
|
||||
1. graphify has already oriented you and you need to modify or debug specific lines
|
||||
2. `graphify-out/graph.json` does not exist yet
|
||||
|
||||
- If `graphify-out/wiki/index.md` exists, navigate it instead of reading raw files
|
||||
- Read `graphify-out/GRAPH_REPORT.md` only for broad architecture review when query/path/explain do not surface enough context
|
||||
- After modifying code files, run `graphify update .` to keep the graph current (AST-only, no API cost).
|
||||
- At **session end**, always run `graphify update .` even if the last edits were docs-only, so the next session's queries match the tree.
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -11,3 +11,6 @@ dist/
|
||||
# Playwright
|
||||
test-results/
|
||||
.tmp-*
|
||||
|
||||
# Graphify knowledge graph (local, regenerable)
|
||||
graphify-out/
|
||||
|
||||
@@ -200,6 +200,7 @@ When the user corrects you, a test or review proves a mistake, or you discover a
|
||||
## Lessons
|
||||
|
||||
- Website Coolify deploys must `npm start` (`server.mjs`) or the Dockerfile; Vite preview is not production.
|
||||
- After each landed task, commit (no secrets); session end run `graphify update .`.
|
||||
|
||||
## Memory protocol
|
||||
|
||||
@@ -208,8 +209,8 @@ Subagents have no memory and cannot see this conversation — durable knowledge
|
||||
## State continuity and proactive improvement
|
||||
|
||||
- Do not rely on the transcript for project state. At the start of a resumed or fresh conversation, invoke `/resume-project` before planning or editing.
|
||||
- Before ending a substantial task, update `docs/HANDOFF.md` with the verified state, changed paths, checks run, risks, and next smallest action, and promote durable knowledge per the memory protocol.
|
||||
- At every phase seal and session end, refresh `docs/PROGRESS.md` for the owner in plain language. `HANDOFF.md` speaks to the next agent; `PROGRESS.md` speaks to the owner.
|
||||
- Before ending a substantial task, update `docs/HANDOFF.md` with the verified state, changed paths, checks run, risks, and next smallest action, and promote durable knowledge per the memory protocol. Then **commit** the landed files (no secrets, no `graphify-out/`); do not push unless asked.
|
||||
- At every phase seal and session end, refresh `docs/PROGRESS.md` for the owner in plain language. Then run `graphify update .` (AST-only) so `graphify-out/` matches the tree. `HANDOFF.md` speaks to the next agent; `PROGRESS.md` speaks to the owner.
|
||||
- After a verified recurring mistake, correction, or workflow gap, invoke `/continuous-improvement` and delegate changes to `system-steward`.
|
||||
- Improve agents, skills, and rules only from concrete evidence. Keep changes small and testable; never silently change lane routing, tool permissions, hooks, safety rules, or external-action authority.
|
||||
|
||||
|
||||
@@ -337,6 +337,7 @@ Keep rules general, evidence-backed, and under 20 words. Never add secrets, pers
|
||||
- Style inline; Tailwind classes do nothing until PostCSS is wired.
|
||||
- Verify CHANGELOG entries against code, not commit subjects or handoff summaries.
|
||||
- Website Coolify deploys must `npm start` (`server.mjs`) or the Dockerfile; Vite preview is not production.
|
||||
- After each landed task, commit (no secrets); session end run `graphify update .`.
|
||||
|
||||
## Memory protocol
|
||||
|
||||
@@ -354,8 +355,8 @@ Run `/memory-sync` at a phase change, before ending a long run, or when any capp
|
||||
## State continuity and proactive improvement
|
||||
|
||||
- Do not rely on the chat transcript for project state. At the start of a resumed, compacted, or fresh session, invoke `/resume-project` before planning or editing.
|
||||
- Before ending a substantial task, update `docs/HANDOFF.md` with the verified state, changed paths, checks run, risks, and the next smallest action, and promote any durable new knowledge per the memory protocol.
|
||||
- At every phase seal and session end, refresh `docs/PROGRESS.md` for the owner in plain language: what newly works and how to see it, the *Waiting on you* queue, and what proceeds without them. `HANDOFF.md` speaks to the next agent; `PROGRESS.md` speaks to the owner.
|
||||
- Before ending a substantial task, update `docs/HANDOFF.md` with the verified state, changed paths, checks run, risks, and the next smallest action, and promote any durable new knowledge per the memory protocol. Then **commit** the landed files (no secrets, no `graphify-out/`); do not push unless asked.
|
||||
- At every phase seal and session end, refresh `docs/PROGRESS.md` for the owner in plain language: what newly works and how to see it, the *Waiting on you* queue, and what proceeds without them. Then run `graphify update .`. `HANDOFF.md` speaks to the next agent; `PROGRESS.md` speaks to the owner.
|
||||
- After a verified recurring mistake, user correction, or workflow gap, invoke `/continuous-improvement`. Opus delegates changes to `system-steward`.
|
||||
- Improve agents and skills only from concrete evidence. Keep changes small, testable, and versioned; never silently change model routing, tool permissions, safety rules, or external-action authority.
|
||||
|
||||
|
||||
@@ -22,6 +22,26 @@ Add an entry when a choice is hard to reverse, shapes future work, or a future m
|
||||
|
||||
## Log
|
||||
|
||||
### D-2026-09-04-01 — Commit each landed task; graphify update at session end
|
||||
|
||||
- **Status:** accepted
|
||||
- **Context:** Graph and git lagged behind work (SEO sat uncommitted; graph needed a manual deep rebuild).
|
||||
- **Decision:** After every completed task, commit landed files (never secrets or `graphify-out/`). Do not push unless asked. At session end, run `graphify update .`.
|
||||
- **Alternatives considered:** Commit only when asked (lost work); full `/graphify --mode deep` every session (too expensive).
|
||||
- **Consequences:** More small commits on `dev`. Graph stays AST-current; docs/images still need an occasional deep extract.
|
||||
- **Verification:** Session-start hook reminds the workflow; `graphify-out/` stays gitignored.
|
||||
- **Owner / date:** John Kevin, 2026-09-04
|
||||
|
||||
### D-2026-09-03-01 — Website path URLs for Google indexing
|
||||
|
||||
- **Status:** accepted
|
||||
- **Context:** The marketing site used HashRouter (`/#/docs`). Google does not treat hash fragments as indexable URLs. Production already had SPA fallbacks.
|
||||
- **Decision:** Switch to BrowserRouter; prerender unique HTML for `/`, `/docs`, `/contribute`, `/community`; ship `/robots.txt` and `/sitemap.xml`; unknown paths return HTTP 404; rewrite legacy `#/…` hashes on load.
|
||||
- **Alternatives considered:** Keep hashes (rejected: not crawlable as pages); full SSR (rejected: extra stack for a static brochure).
|
||||
- **Consequences:** Coolify/nginx must keep SPA fallback for the four routes. Old social posts with `#/docs` still work via the hash rewrite.
|
||||
- **Verification:** `npm run website:build`; GET `/robots.txt`, `/sitemap.xml`, `/docs` 200 with unique titles; GET `/no-such-page` 404.
|
||||
- **Owner / date:** John Kevin / lead, 2026-09-03
|
||||
|
||||
### D-2026-08-13-01 — VS Code port: shared `src/lib` + native v1 UX
|
||||
|
||||
- **Status:** accepted
|
||||
|
||||
@@ -31,3 +31,6 @@ Tap targets ≥ 40px; body text contrast AA.
|
||||
| Feature list | Two-column hairline rows |
|
||||
| PackageCard | Chrome / VS Code / CLI |
|
||||
| DocsSection | Package docs blocks |
|
||||
| Not found | Unknown path: H1 + Home / Docs |
|
||||
| Breadcrumbs | Inner pages: Home / current |
|
||||
| FAQ | Home `<details>` list; copy must match FAQ JSON-LD |
|
||||
|
||||
@@ -4,8 +4,7 @@
|
||||
|
||||
## Current state
|
||||
|
||||
- **Outcome:** Coolify `Cannot GET /` fixed — website now has a production static server and a Dockerfile.
|
||||
- **Cause:** Nixpacks built Vite `dist/` but had no `start` command; README also told Coolify to publish `/dist` (absolute empty path).
|
||||
- **Shipped:** `packages/website/server.mjs` (`npm start` on `0.0.0.0:$PORT`); `Dockerfile` + `nginx.conf`; `nixpacks.toml` `[start]`; Vite `base` is `/`.
|
||||
- **Verified:** `website:typecheck` + `website:build`; local `node server.mjs` → GET `/` 200 (LexAI HTML), `/healthz` 200, JS asset `text/javascript`.
|
||||
- **Next smallest action:** Redeploy the Coolify app with Base Directory `packages/website` (no leading slash), static site **off**, Dockerfile port 80 or Nixpacks port 3000.
|
||||
- **Outcome:** Standing workflow: commit after each landed task; session end `graphify update .`.
|
||||
- **Shipped:** `AGENTS.md` / `CLAUDE.md` lessons + state continuity; `.cursor/rules/graphify.mdc`; session-start reminder; `D-2026-09-04-01`.
|
||||
- **Verified:** docs/rules updated; graphify-out gitignored.
|
||||
- **Next smallest action:** Continue product work; this session will commit this change and run `graphify update .`.
|
||||
|
||||
@@ -6,6 +6,16 @@ This is the project’s shared, evidence-backed memory of mistakes worth prevent
|
||||
|
||||
`AGENTS.md` → `## Lessons` is the canonical active rule list loaded every session. Keep supporting evidence here; mirror an active rule here only when its detail is useful for maintenance. Keep at most 12 active rules, each short and imperative.
|
||||
|
||||
### L-2026-09-04-01 — Commit each landed task; session end `graphify update .`
|
||||
|
||||
- **Status:** active
|
||||
- **Trigger:** Owner asked to stop leaving finished work uncommitted and the knowledge graph stale.
|
||||
- **Root cause / failure boundary:** Commit-on-request plus no session-end graph refresh.
|
||||
- **Prevention:** Commit after each landed task (no secrets). Session end: `graphify update .`.
|
||||
- **Evidence:** Owner instruction 2026-09-04; graphify CLI `update`.
|
||||
- **Eval:** manual guardrail — git status + graphify-out mtime
|
||||
- **Owner / review:** lead / if auto-commit becomes noisy
|
||||
|
||||
### L-2026-08-17-01 — Serve website `dist/` on `0.0.0.0:$PORT`; do not use Vite preview in production
|
||||
|
||||
- **Status:** active
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
- Prefer encrypted Chrome key path; keep plaintext `apiKey` fallback until migration.
|
||||
- VS Code / CLI must not import `@lib/crypto` or `@lib/messaging`.
|
||||
- Root scripts: `install:all`, `chrome:*`, `vscode:*`, `cli:*`, `website:*`.
|
||||
- After each landed task: git commit (no secrets; `graphify-out/` ignored). Session end: `graphify update .`. Do not push unless asked.
|
||||
|
||||
## Environment quirks
|
||||
|
||||
@@ -32,7 +33,7 @@
|
||||
| Shared lib | `src/lib/` |
|
||||
| VS Code | `packages/vscode/` |
|
||||
| CLI | `packages/cli/` |
|
||||
| Website | `packages/website/` (Vite React; Home, Docs, Contribute, Community). Production: `npm start` → `server.mjs`, or `Dockerfile` (nginx :80). Coolify base `packages/website`, static site off. |
|
||||
| Website | `packages/website/` (Vite React; Home, Docs, Contribute, Community). BrowserRouter paths `/docs` etc. Origin `https://lexai.juankibin.space`. Production: `npm start` → `server.mjs`, or `Dockerfile` (nginx :80). Coolify base `packages/website`, static site off. |
|
||||
| OSS meta | root `README.md`, `LICENSE`, `CONTRIBUTING.md`, `CODE_OF_CONDUCT.md` |
|
||||
| Issue/PR templates | `.gitea/ISSUE_TEMPLATE/`, `.gitea/PULL_REQUEST_TEMPLATE.md` |
|
||||
| CI workflows | `.gitea/workflows/ci-{chrome,vscode,cli,website}.yml` (no deploy/release yet) |
|
||||
|
||||
@@ -2,33 +2,28 @@
|
||||
|
||||
> For the owner. What works, how to see it, and what's waiting on you — plain language.
|
||||
|
||||
**Updated:** 2026-08-17 · **Overall:** Website Coolify 404 is fixed in the repo; needs a redeploy
|
||||
**Updated:** 2026-09-04 · **Overall:** Search Console is live; agents now commit after each task and refresh the code graph at session end
|
||||
|
||||
## 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 — production server answers `GET /` (this was the Coolify `Cannot GET /` failure)
|
||||
- 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 the domain — you should see the LexAI home page, not `Cannot GET /`.
|
||||
5. Docs live at `https://your-domain/#/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 |
|
||||
| 2 | Confirm Cursor mid/strong model picker IDs | **Keep written IDs** / send exact picker strings | reliable Task dispatch |
|
||||
| 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 |
|
||||
| 4 | Publish VS Code extension to Marketplace? | **Stay local for now** / set publisher + publish | public install |
|
||||
|
||||
## 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
|
||||
|
||||
@@ -8,6 +8,7 @@ _No active task contracts._
|
||||
|
||||
## Done (recent)
|
||||
|
||||
- **T-WEB-SEO-01** — Google indexing prep for `packages/website` (path URLs, robots/sitemap, unique titles, 404) (2026-09-03).
|
||||
- **T-VSCODE-01** — VS Code LexAI v1 (`packages/vscode`): native commands/menus, Secret Storage, shared `@lib` via esbuild; verified with `vscode:typecheck` + `vscode:build` + root typecheck/tests (2026-08-13).
|
||||
|
||||
## Task contract format
|
||||
|
||||
47
docs/design/website-indexing.md
Normal file
47
docs/design/website-indexing.md
Normal file
@@ -0,0 +1,47 @@
|
||||
# Website indexing (Google)
|
||||
|
||||
## User + job
|
||||
|
||||
Someone searching for a BYO-LLM writing/code assist tool finds LexAI and lands on the matching page (home, docs, contribute, or community). Success: the snippet title and URL match the page they open.
|
||||
|
||||
## Flow
|
||||
|
||||
Search result → HTTPS path (`/`, `/docs`, `/contribute`, `/community`) → page. Old `#/docs` links redirect to `/docs` with no extra click. Unknown paths: one screen, link home. Target: 0 taps after the SERP click.
|
||||
|
||||
## Screen states
|
||||
|
||||
| State | What |
|
||||
| --- | --- |
|
||||
| Success | Existing Home / Docs / Contribute / Community; browser tab title matches the route |
|
||||
| Not found | H1 “Page not found”, one sentence, primary “Home”, secondary “Docs” |
|
||||
| Loading / empty / offline | Unchanged marketing pages (static). No new loading chrome |
|
||||
|
||||
## Components
|
||||
|
||||
Reuse `SiteHeader`, `SiteFooter`, `.page` / `.docs-content` / `.btn`. No new design-system pattern except **Not found** (same as `.page`).
|
||||
|
||||
## Copy (en)
|
||||
|
||||
- Title (404): Page not found
|
||||
- Body: That address is not a LexAI page. Check the URL or go back home.
|
||||
- Primary: Home
|
||||
- Secondary: Docs
|
||||
|
||||
Route titles/descriptions live in `src/seo.ts` (not on-page H1s).
|
||||
|
||||
## Accessibility
|
||||
|
||||
404 is a real `h1`; both actions are text links/buttons ≥ 40px; focus order follows visual order.
|
||||
|
||||
## Acceptance
|
||||
|
||||
1. `/docs`, `/contribute`, `/community` are path URLs (not `#/…`).
|
||||
2. `/robots.txt` allows `/` and names the sitemap.
|
||||
3. `/sitemap.xml` lists the four indexable URLs on `https://lexai.juankibin.space`.
|
||||
4. Each of those URLs has a unique `<title>` and meta description in the HTML file Google fetches.
|
||||
5. Unknown path shows the 404 copy; crawlers get HTTP 404.
|
||||
6. `#/docs` becomes `/docs`.
|
||||
|
||||
## Out of scope
|
||||
|
||||
Search Console verification token, sitemap ping, paid ads, i18n locales beyond en.
|
||||
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.
|
||||
@@ -28,7 +28,7 @@ Preview production build locally: `npm run website:preview` (Vite, localhost onl
|
||||
|
||||
Production start (what Coolify should run): `npm start` — serves `dist/` on `0.0.0.0:$PORT` (default 3000).
|
||||
|
||||
Uses HashRouter so deep links work even without SPA rewrite rules. The production server still maps unknown paths to `index.html`.
|
||||
Uses BrowserRouter. Production nginx/Node serve `/docs`, `/contribute`, and `/community` as real paths (legacy `#/docs` links rewrite on load). Unknown paths return HTTP 404.
|
||||
|
||||
## Deploy on Coolify
|
||||
|
||||
@@ -44,7 +44,7 @@ This package is a static Vite build. No LexAI API keys or env vars. Do not add `
|
||||
4. Branch: `dev` or `main`.
|
||||
5. **Base Directory:** `packages/website` — no leading slash. Root `npm run build` builds Chrome, not this site.
|
||||
6. Optional **Watch Paths:** `packages/website`.
|
||||
7. Set the domain, deploy, open `https://your-domain/` (docs: `https://your-domain/#/docs`).
|
||||
7. Set the domain, deploy, open `https://lexai.juankibin.space/` (docs: `/docs`).
|
||||
|
||||
### Option A — Dockerfile (recommended)
|
||||
|
||||
@@ -66,3 +66,13 @@ Do **not** tick "Is it a static site?" with Publish Directory `/dist`. The leadi
|
||||
If you already created the resource: uncheck static site, set Base Directory and port as above, then Redeploy (no need to recreate the app).
|
||||
|
||||
Auto-deploy: Coolify resource → **Webhooks** → copy the URL + secret into Gitea **Settings → Webhooks** (push events).
|
||||
|
||||
## Google Search
|
||||
|
||||
After the site is live on `https://lexai.juankibin.space/`:
|
||||
|
||||
1. Open [Google Search Console](https://search.google.com/search-console), add a URL-prefix property for that origin, and verify (HTML file or DNS).
|
||||
2. Sitemaps → submit `https://lexai.juankibin.space/sitemap.xml`.
|
||||
3. URL inspection → request indexing for `/` and `/docs`.
|
||||
|
||||
Crawlers should see `Allow: /` at `/robots.txt` and unique titles on `/`, `/docs`, `/contribute`, and `/community`.
|
||||
|
||||
229
packages/website/SOCIAL_POSTS.md
Normal file
229
packages/website/SOCIAL_POSTS.md
Normal file
@@ -0,0 +1,229 @@
|
||||
# LexAI — Social media posts
|
||||
|
||||
Ready-to-copy posts for promoting [LexAI](https://lexai.juankibin.space/).
|
||||
|
||||
**Website:** https://lexai.juankibin.space/
|
||||
**Chrome Web Store:** https://chromewebstore.google.com/detail/bagpcheidbkfgijnnmolnkgagibbjfnk
|
||||
**VS Code Marketplace:** https://marketplace.visualstudio.com/items?itemName=JuanKibin.lexai-vscode
|
||||
**Repository:** https://git.juankibin.space/kibin/LexAI
|
||||
|
||||
---
|
||||
|
||||
## Prompt Builder — featured angle
|
||||
|
||||
Use these when you want **Prompt Builder** front and center. It works in three places: Chrome (**Make Prompt**), VS Code, and the CLI (`lexai prompt`).
|
||||
|
||||
### LinkedIn — Prompt Builder focus
|
||||
|
||||
Bad prompts waste good models.
|
||||
|
||||
LexAI **Prompt Builder** turns a rough idea into a paste-ready, engineered prompt — before you send it to Claude, Cursor, ChatGPT, or any other agent.
|
||||
|
||||
How it works:
|
||||
|
||||
- **Chrome** — select any text → **Make Prompt** → copy a structured prompt for your next session
|
||||
- **VS Code / Cursor** — select code or notes → Make Prompt with pattern, persona, and format options
|
||||
- **CLI** — `lexai prompt "add rate limiting to the auth routes"` → engineered prompt on stdout, ready to pipe or paste
|
||||
|
||||
Pick a pattern (role, chain-of-thought, and more), set persona and output format, and get something you can actually hand to an AI agent.
|
||||
|
||||
Same BYO-key model: your API key, your provider, no LexAI servers.
|
||||
|
||||
Try it: https://lexai.juankibin.space/
|
||||
Docs: https://lexai.juankibin.space/docs
|
||||
|
||||
#PromptEngineering #AI #OpenSource #DeveloperTools #BYOLLM
|
||||
|
||||
### X / Twitter — Prompt Builder focus
|
||||
|
||||
> Stop pasting vague prompts into Claude and Cursor.
|
||||
>
|
||||
> LexAI Prompt Builder: rough idea → engineered, paste-ready prompt.
|
||||
>
|
||||
> Chrome · VS Code · CLI (`lexai prompt`)
|
||||
> Your key. No subscription.
|
||||
>
|
||||
> https://lexai.juankibin.space/
|
||||
|
||||
### Short demo tweet
|
||||
|
||||
> "add rate limiting to auth" →
|
||||
>
|
||||
> LexAI Prompt Builder →
|
||||
>
|
||||
> structured prompt you paste into Cursor ✓
|
||||
>
|
||||
> Browser, editor, or terminal. BYO API key.
|
||||
> https://lexai.juankibin.space/
|
||||
|
||||
---
|
||||
|
||||
## LinkedIn
|
||||
|
||||
### Option A — Full launch post
|
||||
|
||||
I built LexAI because I was tired of paying for Grammarly *and* Copilot when I already had an LLM API key.
|
||||
|
||||
LexAI is an open-source writing, coding, and **prompt-engineering** assistant that uses **your** API key — no LexAI account, no servers, no subscription.
|
||||
|
||||
What it does:
|
||||
|
||||
- **Prompt Builder** — turn a rough idea into a paste-ready prompt (Chrome, VS Code, or CLI). Patterns, persona, and format built in.
|
||||
- **Chrome extension** — select text on any webpage → Fix, Rephrase, Shorten, Expand, Explain, or **Make Prompt**
|
||||
- **VS Code / Cursor extension** — select code, give an instruction, apply a suggestion with file context
|
||||
- **CLI** — `lexai prompt "your idea"` outputs an engineered prompt to stdout — pipe it straight into your workflow
|
||||
|
||||
Your key stays on your device. Text goes only to the provider you choose (OpenAI, Anthropic, Groq, or OpenRouter).
|
||||
|
||||
MIT licensed. Audit it, fork it, ship your own build.
|
||||
|
||||
Learn more: https://lexai.juankibin.space/
|
||||
Chrome: https://chromewebstore.google.com/detail/bagpcheidbkfgijnnmolnkgagibbjfnk
|
||||
VS Code: https://marketplace.visualstudio.com/items?itemName=JuanKibin.lexai-vscode
|
||||
|
||||
#OpenSource #AI #PromptEngineering #DeveloperTools #Productivity #BYOLLM
|
||||
|
||||
### Option B — Short
|
||||
|
||||
Skip the second subscription — and stop writing bad prompts by hand.
|
||||
|
||||
LexAI = Grammarly-style writing in Chrome + Copilot-style code assist in VS Code + a **Prompt Builder** that engineers paste-ready prompts from a rough idea.
|
||||
|
||||
Your LLM API key. No LexAI servers. No fee.
|
||||
|
||||
Open source (MIT): https://lexai.juankibin.space/
|
||||
|
||||
#AI #OpenSource #PromptEngineering #BuildInPublic
|
||||
|
||||
---
|
||||
|
||||
## X / Twitter
|
||||
|
||||
### Launch tweet
|
||||
|
||||
LexAI: BYO-LLM writing, coding, and **prompt engineering** — no subscription, no account, no LexAI servers.
|
||||
|
||||
✨ Prompt Builder — rough idea → paste-ready prompt
|
||||
🌐 Chrome: fix/rephrase/**make prompt** on any page
|
||||
💻 VS Code: select code → instruct → apply
|
||||
⌨️ CLI: `lexai prompt "your idea"`
|
||||
|
||||
Open source (MIT)
|
||||
https://lexai.juankibin.space/
|
||||
|
||||
### Thread opener
|
||||
|
||||
Tired of paying Grammarly AND Copilot — and still writing vague prompts for Claude/Cursor?
|
||||
|
||||
I built LexAI: your key, your provider, your device. Writing help + code assist + **Prompt Builder**. 🧵
|
||||
|
||||
*(Reply with one tweet per feature — lead with Prompt Builder, then Chrome, VS Code, CLI + links)*
|
||||
|
||||
### Thread reply — Prompt Builder (tweet 2)
|
||||
|
||||
> Feature 1: **Prompt Builder**
|
||||
>
|
||||
> Type a rough idea → get a structured, paste-ready prompt.
|
||||
>
|
||||
> Use it in Chrome (Make Prompt), VS Code, or terminal:
|
||||
> `lexai prompt "debug flaky tests on CI"`
|
||||
>
|
||||
> Pipe to clipboard. Paste into Cursor. Done.
|
||||
|
||||
### Short punchy
|
||||
|
||||
Your API key. Your provider. No LexAI cloud.
|
||||
|
||||
**Prompt Builder** in browser, editor, and CLI. Writing help in Chrome. Code assist in VS Code.
|
||||
|
||||
https://lexai.juankibin.space/ · MIT
|
||||
|
||||
---
|
||||
|
||||
## Facebook / Instagram
|
||||
|
||||
Meet LexAI — AI writing, coding, and **prompt engineering** without another monthly bill.
|
||||
|
||||
If you already have an OpenAI, Anthropic, Groq, or OpenRouter API key, LexAI puts it to work:
|
||||
|
||||
✨ **Prompt Builder** — rough idea → engineered, paste-ready prompt (Chrome, VS Code, or CLI)
|
||||
✍️ Fix grammar and rephrase text on any website
|
||||
💻 Get coding suggestions in VS Code or Cursor
|
||||
🛠️ Run `lexai prompt` from the terminal and pipe the result anywhere
|
||||
|
||||
Stop guessing what to type into ChatGPT or Cursor. LexAI structures the prompt for you.
|
||||
|
||||
Your data stays yours — no LexAI account, no servers, no tracking.
|
||||
|
||||
Free & open source.
|
||||
|
||||
👉 https://lexai.juankibin.space/
|
||||
|
||||
#LexAI #AI #PromptEngineering #OpenSource #ProductivityHack
|
||||
|
||||
---
|
||||
|
||||
## Reddit / Hacker News
|
||||
|
||||
### Title
|
||||
|
||||
Show HN: LexAI – BYO-LLM writing help, code assist, and Prompt Builder (Chrome + VS Code + CLI)
|
||||
|
||||
### Body
|
||||
|
||||
LexAI is an open-source toolkit for people who already have an LLM API key — an alternative to Grammarly + Copilot subscriptions, with a built-in **Prompt Builder** for agent workflows.
|
||||
|
||||
**Prompt Builder** (the part I use most):
|
||||
|
||||
- Turn a rough idea into a paste-ready, engineered prompt
|
||||
- Available in Chrome (**Make Prompt**), VS Code, and CLI
|
||||
- Patterns (role, chain-of-thought, etc.), persona, output format, and style options
|
||||
- CLI example: `lexai prompt "add rate limiting to the express auth routes"` → stdout, pipe-safe
|
||||
|
||||
**Also:**
|
||||
|
||||
- Chrome MV3 extension: select text → Fix / Rephrase / Shorten / Expand / Explain / Make Prompt
|
||||
- VS Code extension: workspace-aware Code Assist (select code, instruct, apply)
|
||||
- Shared provider core: OpenAI, Anthropic, Groq, OpenRouter
|
||||
|
||||
No LexAI backend, account, or telemetry. Keys stay on-device (encrypted in Chrome, Secret Storage in VS Code).
|
||||
|
||||
Site: https://lexai.juankibin.space/
|
||||
Docs: https://lexai.juankibin.space/docs
|
||||
Repo: https://git.juankibin.space/kibin/LexAI
|
||||
Chrome: https://chromewebstore.google.com/detail/bagpcheidbkfgijnnmolnkgagibbjfnk
|
||||
VS Code: https://marketplace.visualstudio.com/items?itemName=JuanKibin.lexai-vscode
|
||||
|
||||
MIT licensed. Feedback and contributions welcome.
|
||||
|
||||
---
|
||||
|
||||
## Threads / Bluesky
|
||||
|
||||
Built LexAI for people who already pay for an LLM API key and don't want Grammarly + Copilot subscriptions on top.
|
||||
|
||||
Highlight: **Prompt Builder** — rough idea in, engineered prompt out. Works in the browser (Make Prompt), VS Code, and terminal (`lexai prompt`). Paste straight into Claude or Cursor.
|
||||
|
||||
Also: writing help in Chrome, code assist in VS Code. Your key never hits LexAI servers.
|
||||
|
||||
Open source: https://lexai.juankibin.space/
|
||||
|
||||
---
|
||||
|
||||
## Hashtag reference
|
||||
|
||||
| Platform | Suggested tags |
|
||||
| --------- | -------------- |
|
||||
| LinkedIn | `#OpenSource` `#AI` `#PromptEngineering` `#DeveloperTools` `#Productivity` `#BuildInPublic` `#Privacy` |
|
||||
| X | `#PromptEngineering` `#BYOLLM` `#OpenSource` `#DevTools` `#AI` |
|
||||
| Instagram | `#LexAI` `#AI` `#PromptEngineering` `#OpenSource` `#Tech` `#Productivity` |
|
||||
|
||||
---
|
||||
|
||||
## Posting tips
|
||||
|
||||
1. **Lead with Prompt Builder** when posting to dev/AI communities — "rough idea → paste-ready prompt" is a clear, immediate win.
|
||||
2. **Show a before/after** — screenshot a one-line idea next to the engineered prompt LexAI outputs; that demo converts better than feature lists.
|
||||
3. **CLI clip for dev Twitter** — record `lexai prompt "..." | Set-Clipboard` (or pipe to terminal) and paste into Cursor in the next frame.
|
||||
4. **Pin the website link** in your bio or first comment if the platform limits link visibility.
|
||||
5. **CTA by audience** — prompt engineers → CLI + Make Prompt; developers → VS Code + repo; writers/general → Chrome Web Store.
|
||||
@@ -3,9 +3,34 @@
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="robots" content="index, follow" />
|
||||
<meta
|
||||
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/" />
|
||||
<meta name="theme-color" content="#f7f7f5" />
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:site_name" content="LexAI" />
|
||||
<meta
|
||||
property="og:title"
|
||||
content="LexAI — Grammarly-style writing help with your own API key"
|
||||
/>
|
||||
<meta
|
||||
property="og:description"
|
||||
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: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:title"
|
||||
content="LexAI — Grammarly-style writing help with your own API key"
|
||||
/>
|
||||
<meta
|
||||
name="twitter:description"
|
||||
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="preconnect" href="https://fonts.googleapis.com" />
|
||||
@@ -14,7 +39,28 @@
|
||||
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap"
|
||||
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">
|
||||
{
|
||||
"@context": "https://schema.org",
|
||||
"@graph": [
|
||||
{
|
||||
"@type": "WebSite",
|
||||
"name": "LexAI",
|
||||
"url": "https://lexai.juankibin.space/",
|
||||
"description": "Bring-your-own-LLM writing and code assist for Chrome, VS Code, and the CLI."
|
||||
},
|
||||
{
|
||||
"@type": "SoftwareApplication",
|
||||
"name": "LexAI",
|
||||
"applicationCategory": "BrowserApplication",
|
||||
"operatingSystem": "Chrome, VS Code, Windows, macOS, Linux",
|
||||
"url": "https://lexai.juankibin.space/",
|
||||
"offers": { "@type": "Offer", "price": "0", "priceCurrency": "USD" }
|
||||
}
|
||||
]
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
152
packages/website/inject-seo.mjs
Normal file
152
packages/website/inject-seo.mjs
Normal file
@@ -0,0 +1,152 @@
|
||||
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
||||
import { dirname, join } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const root = dirname(fileURLToPath(import.meta.url));
|
||||
const distHtml = join(root, 'dist', 'index.html');
|
||||
const { origin, routes } = JSON.parse(
|
||||
readFileSync(join(root, 'src', 'seo.routes.json'), 'utf8'),
|
||||
);
|
||||
const faq = JSON.parse(readFileSync(join(root, 'src', 'faq.json'), 'utf8'));
|
||||
|
||||
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;
|
||||
out = out.replace(/<title>[\s\S]*?<\/title>/, `<title>${title}</title>`);
|
||||
out = out.replace(
|
||||
/(<meta\s+name="description"\s+content=")[^"]*(")/,
|
||||
`$1${description}$2`,
|
||||
);
|
||||
out = out.replace(/(<meta\s+name="robots"\s+content=")[^"]*(")/, `$1${robots}$2`);
|
||||
out = out.replace(/(<meta\s+property="og:title"\s+content=")[^"]*(")/, `$1${title}$2`);
|
||||
out = out.replace(
|
||||
/(<meta\s+property="og:description"\s+content=")[^"]*(")/,
|
||||
`$1${description}$2`,
|
||||
);
|
||||
out = out.replace(/(<meta\s+property="og:url"\s+content=")[^"]*(")/, `$1${url}$2`);
|
||||
out = out.replace(/(<meta\s+name="twitter:title"\s+content=")[^"]*(")/, `$1${title}$2`);
|
||||
out = out.replace(
|
||||
/(<meta\s+name="twitter:description"\s+content=")[^"]*(")/,
|
||||
`$1${description}$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;
|
||||
}
|
||||
|
||||
const template = readFileSync(distHtml, 'utf8');
|
||||
if (!template.includes('rel="canonical"') || !template.includes('og:title')) {
|
||||
console.error('dist/index.html is missing SEO tags — check index.html');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const written = [];
|
||||
for (const route of routes) {
|
||||
const url = route.path === '/' ? `${origin}/` : `${origin}${route.path}`;
|
||||
const file = route.path === '/' ? 'index.html' : `${route.path.replace(/^\//, '')}/index.html`;
|
||||
const dest = join(root, 'dist', file);
|
||||
mkdirSync(dirname(dest), { recursive: true });
|
||||
writeFileSync(
|
||||
dest,
|
||||
apply(template, {
|
||||
title: route.title,
|
||||
description: route.description,
|
||||
url,
|
||||
robots: 'index, follow',
|
||||
ld: jsonLd(route),
|
||||
}),
|
||||
);
|
||||
written.push(file);
|
||||
}
|
||||
|
||||
writeFileSync(
|
||||
join(root, 'dist', '404.html'),
|
||||
apply(template, {
|
||||
title: 'Page not found — LexAI',
|
||||
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 = `<?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(', '));
|
||||
@@ -10,7 +10,28 @@ server {
|
||||
return 200 'ok';
|
||||
}
|
||||
|
||||
location / {
|
||||
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;
|
||||
}
|
||||
|
||||
location ~ ^/(docs|contribute|community)/?$ {
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
|
||||
error_page 404 =404 /404.html;
|
||||
|
||||
location = /404.html {
|
||||
internal;
|
||||
}
|
||||
|
||||
location / {
|
||||
try_files $uri $uri/ =404;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc --noEmit && vite build",
|
||||
"build": "tsc --noEmit && vite build && node inject-seo.mjs",
|
||||
"preview": "vite preview",
|
||||
"start": "node server.mjs",
|
||||
"typecheck": "tsc --noEmit"
|
||||
|
||||
4
packages/website/public/robots.txt
Normal file
4
packages/website/public/robots.txt
Normal file
@@ -0,0 +1,4 @@
|
||||
User-agent: *
|
||||
Allow: /
|
||||
|
||||
Sitemap: https://lexai.juankibin.space/sitemap.xml
|
||||
23
packages/website/public/sitemap.xml
Normal file
23
packages/website/public/sitemap.xml
Normal file
@@ -0,0 +1,23 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
||||
<url>
|
||||
<loc>https://lexai.juankibin.space/</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<priority>1.0</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://lexai.juankibin.space/docs</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<priority>0.8</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://lexai.juankibin.space/contribute</loc>
|
||||
<changefreq>monthly</changefreq>
|
||||
<priority>0.5</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://lexai.juankibin.space/community</loc>
|
||||
<changefreq>monthly</changefreq>
|
||||
<priority>0.5</priority>
|
||||
</url>
|
||||
</urlset>
|
||||
@@ -20,8 +20,21 @@ const MIME = {
|
||||
'.webp': 'image/webp',
|
||||
'.woff': 'font/woff',
|
||||
'.woff2': 'font/woff2',
|
||||
'.xml': 'application/xml; charset=utf-8',
|
||||
};
|
||||
|
||||
const SPA_ROUTES = new Set(['/', '/docs', '/contribute', '/community']);
|
||||
|
||||
function requestPath(urlPath) {
|
||||
const decoded = decodeURIComponent((urlPath ?? '/').split('?')[0] || '/');
|
||||
if (decoded.length > 1 && decoded.endsWith('/')) return decoded.slice(0, -1) || '/';
|
||||
return decoded || '/';
|
||||
}
|
||||
|
||||
function isSpaRoute(urlPath) {
|
||||
return SPA_ROUTES.has(requestPath(urlPath));
|
||||
}
|
||||
|
||||
function fileUnderRoot(urlPath) {
|
||||
const decoded = decodeURIComponent((urlPath ?? '/').split('?')[0] || '/');
|
||||
const relative = decoded.replace(/^\/+/, '').replaceAll('/', sep);
|
||||
@@ -54,8 +67,14 @@ const server = createServer((req, res) => {
|
||||
}
|
||||
|
||||
let file = candidate;
|
||||
if (!existsSync(file) || statSync(file).isDirectory()) {
|
||||
file = join(root, 'index.html');
|
||||
let status = 200;
|
||||
if (existsSync(file) && statSync(file).isDirectory()) {
|
||||
const nested = join(file, 'index.html');
|
||||
file = existsSync(nested) ? nested : join(root, 'index.html');
|
||||
} else if (!existsSync(file)) {
|
||||
const notFound = join(root, '404.html');
|
||||
file = existsSync(notFound) && !isSpaRoute(req.url ?? '/') ? notFound : join(root, 'index.html');
|
||||
if (!isSpaRoute(req.url ?? '/')) status = 404;
|
||||
}
|
||||
if (!existsSync(file)) {
|
||||
send(res, 500, 'dist/ missing — run npm run build before start');
|
||||
@@ -63,7 +82,12 @@ const server = createServer((req, res) => {
|
||||
}
|
||||
|
||||
const type = MIME[extname(file).toLowerCase()] ?? 'application/octet-stream';
|
||||
res.writeHead(200, { '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;
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
import { Navigate, Route, Routes } from 'react-router-dom';
|
||||
import { Route, Routes } from 'react-router-dom';
|
||||
import { SeoHead } from './components/SeoHead';
|
||||
import { SiteHeader } from './components/SiteHeader';
|
||||
import { SiteFooter } from './components/SiteFooter';
|
||||
import { HomePage } from './pages/HomePage';
|
||||
import { DocsPage } from './pages/DocsPage';
|
||||
import { ContributePage } from './pages/ContributePage';
|
||||
import { CommunityPage } from './pages/CommunityPage';
|
||||
import { NotFoundPage } from './pages/NotFoundPage';
|
||||
|
||||
export default function App() {
|
||||
return (
|
||||
<div className="shell">
|
||||
<SeoHead />
|
||||
<SiteHeader />
|
||||
<main>
|
||||
<Routes>
|
||||
@@ -16,7 +19,7 @@ export default function App() {
|
||||
<Route path="/docs" element={<DocsPage />} />
|
||||
<Route path="/contribute" element={<ContributePage />} />
|
||||
<Route path="/community" element={<CommunityPage />} />
|
||||
<Route path="*" element={<Navigate to="/" replace />} />
|
||||
<Route path="*" element={<NotFoundPage />} />
|
||||
</Routes>
|
||||
</main>
|
||||
<SiteFooter />
|
||||
|
||||
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>
|
||||
);
|
||||
}
|
||||
56
packages/website/src/components/SeoHead.tsx
Normal file
56
packages/website/src/components/SeoHead.tsx
Normal file
@@ -0,0 +1,56 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { canonicalUrl, jsonLdForPath, seoForPath, SITE_ORIGIN } from '../seo';
|
||||
|
||||
function upsertMeta(selector: string, attrs: Record<string, string>, content: string) {
|
||||
let el = document.head.querySelector(selector) as HTMLMetaElement | null;
|
||||
if (!el) {
|
||||
el = document.createElement('meta');
|
||||
for (const [k, v] of Object.entries(attrs)) el.setAttribute(k, v);
|
||||
document.head.appendChild(el);
|
||||
}
|
||||
el.setAttribute('content', content);
|
||||
}
|
||||
|
||||
function upsertCanonical(href: string) {
|
||||
let el = document.head.querySelector('link[rel="canonical"]') as HTMLLinkElement | null;
|
||||
if (!el) {
|
||||
el = document.createElement('link');
|
||||
el.rel = 'canonical';
|
||||
document.head.appendChild(el);
|
||||
}
|
||||
el.href = href;
|
||||
}
|
||||
|
||||
export function SeoHead() {
|
||||
const { pathname } = useLocation();
|
||||
|
||||
useEffect(() => {
|
||||
const seo = seoForPath(pathname);
|
||||
const url = seo.index ? canonicalUrl(seo.path) : `${SITE_ORIGIN}${pathname}`;
|
||||
document.title = seo.title;
|
||||
upsertMeta('meta[name="description"]', { name: 'description' }, seo.description);
|
||||
upsertMeta(
|
||||
'meta[name="robots"]',
|
||||
{ name: 'robots' },
|
||||
seo.index ? 'index, follow' : 'noindex, follow',
|
||||
);
|
||||
upsertCanonical(url);
|
||||
upsertMeta('meta[property="og:title"]', { property: 'og:title' }, seo.title);
|
||||
upsertMeta('meta[property="og:description"]', { property: 'og:description' }, seo.description);
|
||||
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;
|
||||
}
|
||||
@@ -8,6 +8,7 @@ export function SiteFooter() {
|
||||
<p style={{ margin: 0 }}>© {new Date().getFullYear()} LexAI · MIT</p>
|
||||
<div style={{ display: 'flex', gap: '1rem', flexWrap: 'wrap' }}>
|
||||
<Link to="/docs">Docs</Link>
|
||||
<Link to={{ pathname: '/', hash: 'faq' }}>FAQ</Link>
|
||||
<Link to="/contribute">Contribute</Link>
|
||||
<Link to="/community">Community</Link>
|
||||
<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."
|
||||
}
|
||||
]
|
||||
121
packages/website/src/freeProviders.ts
Normal file
121
packages/website/src/freeProviders.ts
Normal file
@@ -0,0 +1,121 @@
|
||||
export type FreeTierBadge = 'best' | 'no-card' | 'trial';
|
||||
|
||||
export type FreeProvider = {
|
||||
id: 'groq' | 'openrouter' | 'anthropic' | 'openai';
|
||||
name: string;
|
||||
badge?: FreeTierBadge;
|
||||
badgeLabel?: string;
|
||||
summary: string;
|
||||
creditCard: string;
|
||||
limits: string;
|
||||
lexaiModel: string;
|
||||
signupUrl: string;
|
||||
docsUrl: string;
|
||||
steps: string[];
|
||||
};
|
||||
|
||||
export const FREE_PROVIDER_BADGES: Record<FreeTierBadge, string> = {
|
||||
best: 'Best free option',
|
||||
'no-card': 'No card needed',
|
||||
trial: 'Trial credits',
|
||||
};
|
||||
|
||||
/** How to use LexAI at $0 — provider free tiers as of 2026. Limits change; verify on each site. */
|
||||
export const FREE_PROVIDERS: FreeProvider[] = [
|
||||
{
|
||||
id: 'groq',
|
||||
name: 'Groq',
|
||||
badge: 'best',
|
||||
badgeLabel: 'Best free option',
|
||||
summary:
|
||||
'Ongoing free API access with no credit card. Rate limits apply, but there is no per-token charge on the free tier — ideal for daily writing help and light coding assist.',
|
||||
creditCard: 'Not required',
|
||||
limits: 'Rate-limited (roughly 30 requests/min; daily caps vary by model). Check the Groq console for your org.',
|
||||
lexaiModel: 'llama-3.3-70b-versatile',
|
||||
signupUrl: 'https://console.groq.com/keys',
|
||||
docsUrl: 'https://console.groq.com/docs/overview',
|
||||
steps: [
|
||||
'Create a Groq account at console.groq.com — no payment method needed.',
|
||||
'Generate an API key under API Keys.',
|
||||
'In LexAI Options (Chrome), Settings (VS Code), or LEXAI_PROVIDER=groq (CLI): choose Groq, paste the key.',
|
||||
'Optional: set model to llama-3.1-8b-instant for faster, lighter requests on tighter limits.',
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'openrouter',
|
||||
name: 'OpenRouter',
|
||||
badge: 'no-card',
|
||||
badgeLabel: 'No card needed',
|
||||
summary:
|
||||
'25+ models at $0 per token via the :free suffix or the openrouter/free router. One API key covers many models — good for experimenting before you commit to a host.',
|
||||
creditCard: 'Not required for free models',
|
||||
limits:
|
||||
'About 20 requests/min. ~50 free requests/day without credits; ~1,000/day after a one-time $10 credit purchase (credits never expire).',
|
||||
lexaiModel: 'openrouter/free',
|
||||
signupUrl: 'https://openrouter.ai/settings/keys',
|
||||
docsUrl: 'https://openrouter.ai/docs/guides/routing/model-variants/free',
|
||||
steps: [
|
||||
'Sign up at openrouter.ai and create an API key.',
|
||||
'In LexAI, choose OpenRouter and paste the key.',
|
||||
'Set model to openrouter/free (auto-picks a free model) or a specific slug such as meta-llama/llama-3.3-70b-instruct:free.',
|
||||
'Optional: add $10 in credits once to raise the daily free-model cap to ~1,000 requests — still $0 per token on free routes.',
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'anthropic',
|
||||
name: 'Anthropic',
|
||||
badge: 'trial',
|
||||
badgeLabel: 'Trial credits',
|
||||
summary:
|
||||
'New Console accounts receive a small one-time API credit (often around $5) with no card required to claim. Enough to try Claude-powered writing and Code Assist before prepaid billing.',
|
||||
creditCard: 'Not required to claim starter credit',
|
||||
limits: 'One-time credit balance; then pay-as-you-go. Use Haiku models to stretch the trial further.',
|
||||
lexaiModel: 'claude-3-5-haiku-20241022',
|
||||
signupUrl: 'https://console.anthropic.com/',
|
||||
docsUrl: 'https://docs.anthropic.com/en/api/getting-started',
|
||||
steps: [
|
||||
'Create an account at console.anthropic.com and claim the new-user API credit.',
|
||||
'Create an API key under API Keys.',
|
||||
'In LexAI, choose Anthropic, paste the key, and keep the default Haiku model for lowest cost.',
|
||||
'When credits run out, add prepaid balance or switch to Groq or OpenRouter for ongoing free usage.',
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'openai',
|
||||
name: 'OpenAI',
|
||||
badge: 'trial',
|
||||
badgeLabel: 'Limited trial',
|
||||
summary:
|
||||
'No permanent free API tier. Some new accounts still receive a short-lived trial credit; otherwise a minimum prepaid purchase is required. Mentioned for completeness — Groq or OpenRouter are better $0 paths.',
|
||||
creditCard: 'Required for ongoing use; trial availability varies',
|
||||
limits: 'Trial credits (if offered) expire after a few months. Prepaid billing from about $5 minimum after trial.',
|
||||
lexaiModel: 'gpt-4o-mini',
|
||||
signupUrl: 'https://platform.openai.com/api-keys',
|
||||
docsUrl: 'https://platform.openai.com/docs/guides/rate-limits',
|
||||
steps: [
|
||||
'Create an account at platform.openai.com and check Billing for any trial balance.',
|
||||
'If trial credit is available, create an API key and use it in LexAI with provider OpenAI.',
|
||||
'Prefer gpt-4o-mini to conserve tokens during evaluation.',
|
||||
'For sustained free usage, use Groq or OpenRouter instead — OpenAI is pay-as-you-go after any trial.',
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export const LEXAI_FREE_HIGHLIGHTS = [
|
||||
{
|
||||
title: 'LexAI is always $0',
|
||||
body: 'No LexAI subscription, account, or backend. You only connect a provider API key you control.',
|
||||
},
|
||||
{
|
||||
title: 'Same key, three apps',
|
||||
body: 'Chrome extension, VS Code / Cursor, and CLI Prompt Builder all use the same supported providers.',
|
||||
},
|
||||
{
|
||||
title: 'Start with Groq or OpenRouter',
|
||||
body: 'Both offer real ongoing free API access without a credit card — enough for grammar fixes, rewrites, and light Code Assist.',
|
||||
},
|
||||
{
|
||||
title: 'Your key stays local',
|
||||
body: 'Keys are encrypted in Chrome, stored in VS Code Secret Storage, or read from env in the CLI — never sent to LexAI.',
|
||||
},
|
||||
] as const;
|
||||
@@ -1,13 +1,19 @@
|
||||
import { StrictMode } from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import { HashRouter } from 'react-router-dom';
|
||||
import { BrowserRouter } from 'react-router-dom';
|
||||
import App from './App';
|
||||
import './styles.css';
|
||||
|
||||
const hash = window.location.hash;
|
||||
if (hash.startsWith('#/')) {
|
||||
const path = hash.slice(1) || '/';
|
||||
window.history.replaceState(null, '', path + window.location.search);
|
||||
}
|
||||
|
||||
createRoot(document.getElementById('root')!).render(
|
||||
<StrictMode>
|
||||
<HashRouter>
|
||||
<BrowserRouter>
|
||||
<App />
|
||||
</HashRouter>
|
||||
</BrowserRouter>
|
||||
</StrictMode>,
|
||||
);
|
||||
|
||||
@@ -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 (
|
||||
<div className="wrap page">
|
||||
<article className="docs-content">
|
||||
<Breadcrumbs items={[{ label: 'Home', to: '/' }, { label: 'Community' }]} />
|
||||
<h1>Community</h1>
|
||||
<p className="intro">
|
||||
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';
|
||||
|
||||
export function ContributePage() {
|
||||
return (
|
||||
<div className="wrap page">
|
||||
<article className="docs-content">
|
||||
<Breadcrumbs items={[{ label: 'Home', to: '/' }, { label: 'Contribute' }]} />
|
||||
<h1>Contribute</h1>
|
||||
<p className="intro">
|
||||
LexAI is a monorepo: Chrome, VS Code, CLI, website, and a shared library. Please follow
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
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';
|
||||
|
||||
function scrollToSection(id: string) {
|
||||
@@ -17,6 +19,7 @@ export function DocsPage() {
|
||||
|
||||
const nav = [
|
||||
['overview', 'Overview'],
|
||||
['free', 'Free LLM setup'],
|
||||
['chrome', 'Chrome'],
|
||||
['vscode', 'VS Code'],
|
||||
['cli', 'CLI'],
|
||||
@@ -36,6 +39,7 @@ export function DocsPage() {
|
||||
</aside>
|
||||
|
||||
<article className="docs-content">
|
||||
<Breadcrumbs items={[{ label: 'Home', to: '/' }, { label: 'Docs' }]} />
|
||||
<h1>Documentation</h1>
|
||||
<p className="intro">
|
||||
LexAI is an open-source, bring-your-own-LLM assistant. This page covers every package and
|
||||
@@ -62,6 +66,91 @@ export function DocsPage() {
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section className="docs-block" id="free">
|
||||
<h2>Free LLM setup</h2>
|
||||
<p>
|
||||
LexAI is free — no subscription, no LexAI account. To use it at $0 ongoing, connect a
|
||||
provider that offers a free API tier. Groq and OpenRouter are the most practical; Anthropic
|
||||
and OpenAI offer small trial credits but not a permanent free API.
|
||||
</p>
|
||||
<h3>Recommended path</h3>
|
||||
<ol>
|
||||
<li>
|
||||
Install LexAI (Chrome, VS Code, or CLI) — see <a href="#install">Install</a>.
|
||||
</li>
|
||||
<li>
|
||||
Sign up for <a href="https://console.groq.com/keys" target="_blank" rel="noreferrer">Groq</a>{' '}
|
||||
(no credit card) or{' '}
|
||||
<a href="https://openrouter.ai/settings/keys" target="_blank" rel="noreferrer">
|
||||
OpenRouter
|
||||
</a>{' '}
|
||||
(no card for free models).
|
||||
</li>
|
||||
<li>Create an API key on that provider's dashboard.</li>
|
||||
<li>
|
||||
In LexAI: choose the provider, paste the key, and set the model slug below (Chrome
|
||||
Options → Model; VS Code Settings → Model; CLI{' '}
|
||||
<code>LEXAI_MODEL</code>).
|
||||
</li>
|
||||
<li>Select text and run an action — requests go directly to your provider, not LexAI.</li>
|
||||
</ol>
|
||||
<h3>Providers</h3>
|
||||
{FREE_PROVIDERS.map((provider) => (
|
||||
<div key={provider.id} className="docs-provider">
|
||||
<h4>{provider.name}</h4>
|
||||
<p>{provider.summary}</p>
|
||||
<ul>
|
||||
<li>
|
||||
<strong>Credit card:</strong> {provider.creditCard}
|
||||
</li>
|
||||
<li>
|
||||
<strong>Limits:</strong> {provider.limits}
|
||||
</li>
|
||||
<li>
|
||||
<strong>Suggested LexAI model:</strong> <code>{provider.lexaiModel}</code>
|
||||
</li>
|
||||
</ul>
|
||||
<p>
|
||||
<strong>Setup</strong>
|
||||
</p>
|
||||
<ol>
|
||||
{provider.steps.map((step) => (
|
||||
<li key={step}>{step}</li>
|
||||
))}
|
||||
</ol>
|
||||
<div className="docs-meta">
|
||||
<a className="btn btn-ghost" href={provider.signupUrl} target="_blank" rel="noreferrer">
|
||||
Get API key
|
||||
</a>
|
||||
<a className="btn btn-ghost" href={provider.docsUrl} target="_blank" rel="noreferrer">
|
||||
Provider docs
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
<h3>CLI example (Groq, free tier)</h3>
|
||||
<pre className="mono">{`# PowerShell
|
||||
$env:LEXAI_API_KEY = "gsk_..."
|
||||
$env:LEXAI_PROVIDER = "groq"
|
||||
$env:LEXAI_MODEL = "llama-3.3-70b-versatile"
|
||||
lexai prompt "rewrite this email to sound more professional"
|
||||
|
||||
# bash
|
||||
export LEXAI_API_KEY=gsk_...
|
||||
export LEXAI_PROVIDER=groq
|
||||
export LEXAI_MODEL=llama-3.3-70b-versatile
|
||||
lexai prompt "summarize this README"`}</pre>
|
||||
<h3>CLI example (OpenRouter, free models)</h3>
|
||||
<pre className="mono">{`export LEXAI_API_KEY=sk-or-...
|
||||
export LEXAI_PROVIDER=openrouter
|
||||
export LEXAI_MODEL=openrouter/free
|
||||
lexai prompt "turn this bullet list into a user story"`}</pre>
|
||||
<p>
|
||||
Limits and trial amounts change. Check each provider's pricing and rate-limit pages
|
||||
before relying on a free tier in production.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section className="docs-block" id="chrome">
|
||||
<h2>Chrome extension</h2>
|
||||
<p>
|
||||
|
||||
@@ -1,5 +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<typeof useNavigate>, id: string) {
|
||||
@@ -57,7 +59,8 @@ export function HomePage() {
|
||||
<h1>Skip Grammarly and Copilot subscriptions</h1>
|
||||
<p className="hero-lead">
|
||||
Writing help on any webpage. Coding assist in VS Code or Cursor. You bring the API key —
|
||||
LexAI has no account, no servers, and no fee.
|
||||
LexAI has no account, no servers, and no fee. Use Groq or OpenRouter for ongoing free
|
||||
API access.
|
||||
</p>
|
||||
<div className="cta-row">
|
||||
<a className="btn btn-primary" href={CHROME_STORE} target="_blank" rel="noreferrer">
|
||||
@@ -104,6 +107,66 @@ export function HomePage() {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="section" id="free">
|
||||
<div className="wrap">
|
||||
<p className="section-kicker">Use it free</p>
|
||||
<h2>LexAI costs nothing — your provider can too</h2>
|
||||
<p className="section-lead">
|
||||
LexAI never charges a subscription. Connect a free-tier API key from a supported provider
|
||||
and get Grammarly-style writing help and Copilot-style coding assist without paying
|
||||
Grammarly or Copilot.
|
||||
</p>
|
||||
<ul className="free-highlights">
|
||||
{LEXAI_FREE_HIGHLIGHTS.map((item) => (
|
||||
<li key={item.title}>
|
||||
<strong>{item.title}</strong>
|
||||
<p>{item.body}</p>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<div className="provider-grid">
|
||||
{FREE_PROVIDERS.map((provider) => (
|
||||
<article key={provider.id} className="provider-card">
|
||||
<div className="provider-card-head">
|
||||
<strong>{provider.name}</strong>
|
||||
{provider.badge ? (
|
||||
<span className={`provider-badge provider-badge-${provider.badge}`}>
|
||||
{provider.badgeLabel ?? FREE_PROVIDER_BADGES[provider.badge]}
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
<p>{provider.summary}</p>
|
||||
<dl className="provider-meta">
|
||||
<div>
|
||||
<dt>Credit card</dt>
|
||||
<dd>{provider.creditCard}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>LexAI model</dt>
|
||||
<dd>
|
||||
<code>{provider.lexaiModel}</code>
|
||||
</dd>
|
||||
</div>
|
||||
</dl>
|
||||
<a
|
||||
className="more"
|
||||
href={provider.signupUrl}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
Get API key →
|
||||
</a>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
<p className="free-footnote">
|
||||
Rate limits and trial amounts change — verify on each provider's site. Full setup
|
||||
steps are in{' '}
|
||||
<Link to="/docs#free">Free LLM setup</Link>.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="section" id="features">
|
||||
<div className="wrap">
|
||||
<p className="section-kicker">Product</p>
|
||||
@@ -170,6 +233,24 @@ export function HomePage() {
|
||||
</ul>
|
||||
</div>
|
||||
</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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
20
packages/website/src/pages/NotFoundPage.tsx
Normal file
20
packages/website/src/pages/NotFoundPage.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
export function NotFoundPage() {
|
||||
return (
|
||||
<div className="wrap page">
|
||||
<article className="docs-content">
|
||||
<h1>Page not found</h1>
|
||||
<p className="intro">That address is not a LexAI page. Check the URL or go back home.</p>
|
||||
<div className="cta-row">
|
||||
<Link className="btn btn-primary" to="/">
|
||||
Home
|
||||
</Link>
|
||||
<Link className="btn btn-ghost" to="/docs">
|
||||
Docs
|
||||
</Link>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
25
packages/website/src/seo.routes.json
Normal file
25
packages/website/src/seo.routes.json
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"origin": "https://lexai.juankibin.space",
|
||||
"routes": [
|
||||
{
|
||||
"path": "/",
|
||||
"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": "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 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": "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."
|
||||
}
|
||||
]
|
||||
}
|
||||
86
packages/website/src/seo.ts
Normal file
86
packages/website/src/seo.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
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;
|
||||
|
||||
export type SeoRoute = {
|
||||
path: string;
|
||||
title: string;
|
||||
description: string;
|
||||
index: boolean;
|
||||
};
|
||||
|
||||
export const INDEXABLE_ROUTES: SeoRoute[] = routesFile.routes.map((r) => ({
|
||||
...r,
|
||||
index: true,
|
||||
}));
|
||||
|
||||
export const NOT_FOUND_SEO: SeoRoute = {
|
||||
path: '',
|
||||
title: 'Page not found — LexAI',
|
||||
description: 'That address is not a LexAI page.',
|
||||
index: false,
|
||||
};
|
||||
|
||||
const byPath = new Map(INDEXABLE_ROUTES.map((r) => [r.path, r]));
|
||||
|
||||
export function seoForPath(pathname: string): SeoRoute {
|
||||
const normalized = pathname.replace(/\/+$/, '') || '/';
|
||||
return byPath.get(normalized) ?? NOT_FOUND_SEO;
|
||||
}
|
||||
|
||||
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<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);
|
||||
}
|
||||
|
||||
.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;
|
||||
@@ -373,6 +427,162 @@ a.package-card {
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
/* Free tier section */
|
||||
.free-highlights {
|
||||
display: grid;
|
||||
gap: 0;
|
||||
list-style: none;
|
||||
margin: 0 0 2rem;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
@media (min-width: 700px) {
|
||||
.free-highlights {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
column-gap: 3rem;
|
||||
}
|
||||
}
|
||||
|
||||
.free-highlights li {
|
||||
padding: 1rem 0;
|
||||
border-top: 1px solid var(--line);
|
||||
}
|
||||
|
||||
.free-highlights strong {
|
||||
display: block;
|
||||
margin-bottom: 0.25rem;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.free-highlights p {
|
||||
margin: 0;
|
||||
color: var(--muted);
|
||||
font-size: 0.92rem;
|
||||
}
|
||||
|
||||
.provider-grid {
|
||||
display: grid;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
@media (min-width: 700px) {
|
||||
.provider-grid {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.provider-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.55rem;
|
||||
padding: 1.2rem 1.15rem 1.25rem;
|
||||
border-radius: var(--radius);
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--line);
|
||||
}
|
||||
|
||||
.provider-card-head {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.provider-card strong {
|
||||
font-size: 0.98rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.provider-card > p {
|
||||
margin: 0;
|
||||
flex: 1;
|
||||
color: var(--muted);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.provider-badge {
|
||||
display: inline-block;
|
||||
padding: 0.15rem 0.45rem;
|
||||
border-radius: 4px;
|
||||
font-size: 0.68rem;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.03em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.provider-badge-best {
|
||||
background: #eef2ff;
|
||||
color: var(--indigo-deep);
|
||||
}
|
||||
|
||||
.provider-badge-no-card {
|
||||
background: #ecfdf5;
|
||||
color: #047857;
|
||||
}
|
||||
|
||||
.provider-badge-trial {
|
||||
background: #fef9ec;
|
||||
color: #92400e;
|
||||
}
|
||||
|
||||
.provider-meta {
|
||||
display: grid;
|
||||
gap: 0.45rem;
|
||||
margin: 0;
|
||||
font-size: 0.82rem;
|
||||
}
|
||||
|
||||
.provider-meta dt {
|
||||
margin: 0;
|
||||
font-weight: 600;
|
||||
color: var(--ink);
|
||||
}
|
||||
|
||||
.provider-meta dd {
|
||||
margin: 0.1rem 0 0;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.provider-card .more {
|
||||
font-weight: 500;
|
||||
font-size: 0.82rem;
|
||||
color: var(--indigo);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.provider-card .more:hover {
|
||||
color: var(--indigo-deep);
|
||||
}
|
||||
|
||||
.free-footnote {
|
||||
margin: 1.25rem 0 0;
|
||||
font-size: 0.88rem;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.docs-provider {
|
||||
margin-top: 1.5rem;
|
||||
padding-top: 1.25rem;
|
||||
border-top: 1px solid var(--line);
|
||||
}
|
||||
|
||||
.docs-provider:first-of-type {
|
||||
margin-top: 0.75rem;
|
||||
}
|
||||
|
||||
.docs-provider h4 {
|
||||
margin: 0 0 0.35rem;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
letter-spacing: -0.01em;
|
||||
color: var(--ink);
|
||||
text-transform: none;
|
||||
}
|
||||
|
||||
.docs-provider > p {
|
||||
margin: 0 0 0.5rem;
|
||||
}
|
||||
|
||||
/* Inner pages (Contribute, Community) */
|
||||
.page {
|
||||
padding: 2.75rem 0 4.5rem;
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
"skipLibCheck": true,
|
||||
"moduleResolution": "Bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
|
||||
Reference in New Issue
Block a user