Compare commits
10 Commits
efdae32ae1
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| fc80e9b08c | |||
|
|
03ad49f523 | ||
| 1788b2b5be | |||
|
|
3150e16248 | ||
| 85f0fc0dc5 | |||
|
|
2eb6eed16a | ||
|
|
2b2bdeac7a | ||
| fb9c13fbd5 | |||
| 9895244535 | |||
| df0bcae250 |
@@ -22,6 +22,16 @@ Add an entry when a choice is hard to reverse, shapes future work, or a future m
|
||||
|
||||
## Log
|
||||
|
||||
### 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,6 @@
|
||||
|
||||
## 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:** Sitemap accepted in GSC. SEO pass: intent titles, home FAQ + FAQPage JSON-LD, breadcrumbs, lastmod sitemap, 5-min cache on robots/sitemap.
|
||||
- **Verified:** `website:typecheck` + `website:build` OK (inject wrote route HTML + sitemap.xml).
|
||||
- **Next smallest action:** Merge/deploy this to `main`, purge Cloudflare `/robots.txt`, then in GSC request indexing for `/` and `/docs`.
|
||||
|
||||
@@ -32,7 +32,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 sitemap is success; next deploy the SEO copy/FAQ pass
|
||||
|
||||
## What works now
|
||||
|
||||
- Chrome extension: select on any page → AI action → Replace/Copy
|
||||
- VS Code extension: select in editor → LexAI context menu / Command Palette → selection replaced
|
||||
- Website (`packages/website`): Home, Docs, Contribute, Community — 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