Landing page and documentation site for Vespa UI, built with React, Vite, and Elastic UI (EUI). Deploys as a static site to GitHub Pages.
npm install
npm run devOther scripts:
npm run build # typecheck + production build to dist/
npm run preview # serve the production build locally
npm run typecheck # tsc --noEmit onlywebsite/
├── src/
│ ├── content/docs/ # Markdown docs content — see "Writing docs" below
│ ├── components/Seo.tsx # Per-route <head> tags (title, description, canonical, OG, JSON-LD)
│ ├── layout/ # Header/nav shell, page container
│ ├── lib/ # Markdown/frontmatter parsing, docs tree builder
│ ├── pages/ # Home, About, Credits, Docs
│ ├── theme.ts # EUI theme overrides for the brand palette
│ └── colorMode.tsx # Light/dark toggle (persisted to localStorage)
├── scripts/generate-seo.mjs # Builds sitemap.xml, llms.txt, llms-full.txt, docs-raw/ — see below
├── public/
│ ├── robots.txt # Allows standard + AI/LLM crawlers, points to sitemap.xml
│ └── 404.html # GitHub Pages SPA redirect trick (see "Structure" above)
└── .github isn't here — workflows live at ../.github/workflows/:
- deploy-website.yml # builds and deploys to GitHub Pages
- generate-seo-files.yml # regenerates sitemap.xml/llms.txt on doc changes
Routing uses BrowserRouter with real paths (/docs/getting-started/installation, not
#/docs/...) so each page has its own crawlable, indexable URL — important for SEO, since search
engines treat different hash fragments of the same URL as one page. GitHub Pages has no
server-side rewrite rules, so public/404.html + the inline script in index.html implement the
standard SPA-for-GitHub-Pages redirect trick to
make deep links work anyway.
Docs are plain Markdown files under src/content/docs/. Commit a .md file and it appears on the
site and in the sidebar automatically — no route or nav entry to wire up by hand.
content/docs/
├── 01-getting-started/
│ ├── 01-introduction.md
│ └── 02-installation.md
└── 02-architecture/
└── 01-overview.md
-
A leading
NN-on a file or folder name controls sort order and is stripped from the slug/title. -
Each folder becomes a collapsible group in the sidebar; folders can be nested arbitrarily deep.
-
Files placed directly in
content/docs/(no folder) appear ungrouped at the top of the sidebar. -
Optional frontmatter per file:
--- title: Custom Title description: One sentence shown under the page title. order: 10 ---
-
Link to another doc page with a site-relative path:
[Installation](/docs/getting-started/installation).
See src/content/docs/99-writing-docs.md for the same guide rendered on the live site.
Brand colors (#61D790 green / #020202 ink) are applied by overriding EUI's Borealis theme
tokens in src/theme.ts — see the comments there for why specific derived shades
are used for text/links/buttons instead of the raw brand green (WCAG contrast).
Because this is a client-rendered SPA, there's no per-route server response to attach different
<head> content to — src/components/Seo.tsx does it at runtime
instead, setting document.title, the meta description, the robots tag, a canonical link,
Open Graph / Twitter tags, and (on docs pages) a BreadcrumbList JSON-LD block. Every page
(Home, About, Credits, DocsPage, NotFound) renders a <Seo> with its own title and
description; index.html carries sitewide defaults (Organization / WebSite /
SoftwareApplication JSON-LD) for crawlers that only fetch the initial HTML.
npm run generate:seo (also run automatically by npm run build, and by
../.github/workflows/generate-seo-files.yml whenever docs content changes) reads
src/content/docs/**/*.md and writes into public/:
sitemap.xml— every static page plus every doc page, with<lastmod>from the file's mtime.llms.txt/llms-full.txt— an llms.txt-format index and full content dump for LLMs/agents.docs-raw/<slug>.md— a plain-Markdown mirror of every doc page. AI/LLM crawlers (GPTBot, ClaudeBot, PerplexityBot, etc.) generally fetch over plain HTTP without executing JavaScript, so without this they'd see only the near-empty SPA shell;llms.txtand the sitemap link to these raw files instead of the HTML routes for that reason.
public/robots.txt allows those crawlers explicitly (GEO — being retrievable and citable by AI
answer engines) alongside standard search bots, and points to sitemap.xml.
If you move off the default GitHub Pages org-site URL (https://vespa-ui.github.io/),
update all of:
SITE_ORIGINinsrc/components/Seo.tsxSITE_ORIGIN/SITE_BASEin../.github/workflows/generate-seo-files.yml'senv:block (or pass them when runningnpm run generate:seolocally)- the
Sitemap:line and canonical URLs baked intoindex.html pathSegmentsToKeepinpublic/404.html(1for a GitHub Pages project site,0for a custom domain or user/org page — same condition as theVITE_BASEoverride above)
.github/workflows/deploy-website.yml builds this repo and publishes it to GitHub
Pages on every push to main. The workflow reads the Pages base path from
configure-pages so asset URLs always match where the site is served.
For the site to live at https://vespa-ui.github.io/ (root /), the
repository must be named vespa-ui.github.io. A project page named
website is always served from https://vespa-ui.github.io/website/ — that
prefix comes from GitHub, not from the app code.
If you're serving from a custom domain instead:
- Set
VITE_BASE=/(the workflow already does this for org pages), and - Add a
CNAMEfile with your domain underpublic/.
In the repository settings, set Settings → Pages → Source to GitHub Actions.