Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 62 additions & 0 deletions .github/workflows/docsite.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
name: Docsite

on:
push:
branches:
- main
paths:
- "docsite/**"
- ".github/workflows/docsite.yml"
pull_request:
types: [opened, synchronize, reopened]
branches:
- main
paths:
- "docsite/**"
- ".github/workflows/docsite.yml"

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
seo:
runs-on: blacksmith-4vcpu-ubuntu-2404
timeout-minutes: 8
env:
MISE_EXPERIMENTAL: "1"
steps:
- name: ⬇️ Checkout repo
uses: actions/checkout@v4

- name: 🛠️ Setup mise
uses: jdx/mise-action@v2
with:
install: true
cache: true
experimental: true

- name: 📦 Install root workspace
run: pnpm install --frozen-lockfile

- name: 💾 Cache pnpm dlx (Lighthouse CI)
uses: actions/cache@v4
with:
path: ~/.cache/pnpm/dlx
key: pnpm-dlx-${{ runner.os }}-lhci-0.14.0

- name: 🌐 Setup Chrome
uses: browser-actions/setup-chrome@v1

- name: 🔎 SEO audit (Lighthouse CI)
timeout-minutes: 5
run: pnpm --filter docsite check:seo

- name: 📤 Upload report
if: failure()
uses: actions/upload-artifact@v4
with:
name: lighthouseci-report
path: docsite/.lighthouseci/**
retention-days: 14
if-no-files-found: ignore
1 change: 1 addition & 0 deletions docsite/.gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
.lighthouseci
.source
.tanstack
.wrangler
Expand Down
42 changes: 42 additions & 0 deletions docsite/.lighthouserc.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
const fs = require("node:fs");
const path = require("node:path");

const SITE = "http://127.0.0.1:4173";
const DOCS_ROOT = path.join(__dirname, "content/docs");

function collectDocUrls(dir, urlPrefix = "/docs") {
const urls = [];
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
const full = path.join(dir, entry.name);
if (entry.isDirectory()) {
urls.push(...collectDocUrls(full, `${urlPrefix}/${entry.name}`));
} else if (entry.name.endsWith(".mdx")) {
const slug = entry.name.replace(/\.mdx$/, "");
urls.push(slug === "index" ? urlPrefix : `${urlPrefix}/${slug}`);
}
}
return urls;
}

const urls = ["/", ...collectDocUrls(DOCS_ROOT)].map((p) => `${SITE}${p}`);

module.exports = {
ci: {
collect: {
startServerCommand: "pnpm exec vite preview --host 127.0.0.1 --port 4173 --strictPort",
url: urls,
numberOfRuns: 1,
settings: {
chromeFlags: "--no-sandbox",
},
},
assert: {
assertions: {
"categories:seo": ["error", { minScore: 1.0 }],
"categories:best-practices": ["error", { minScore: 1.0 }],
"categories:accessibility": ["warn", { minScore: 0.9 }],
"categories:performance": ["warn", { minScore: 0.5 }],
},
},
},
};
24 changes: 24 additions & 0 deletions docsite/CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# docsite — agent notes

Workspace-specific guidance. The general project layout and run commands live in [README.md](./README.md).

## SEO audit (Lighthouse CI)

`@lhci/cli` is **not** in `package.json` — it would add ~30 MB to every dev install just for the audit. It is pulled on-demand via `pnpm dlx` inside the `check:seo` script:

```
pnpm --filter docsite check:seo
```

Same command runs locally and in CI ([`.github/workflows/docsite.yml`](../.github/workflows/docsite.yml)). `lhci autorun` handles the full lifecycle natively: starts the preview server via `startServerCommand`, runs Lighthouse against each URL, asserts against budgets, and stops the server cleanly.

The pinned `@lhci/cli@X.Y.Z` version lives in two places that **must be kept in sync**:

1. The `pnpm dlx` invocation in `package.json` (`@lhci/cli@X.Y.Z`).
2. The cache key in [`docsite.yml`](../.github/workflows/docsite.yml) (`pnpm-dlx-...-lhci-X.Y.Z`).

When bumping the version, update both — otherwise CI keeps restoring the old `~/.cache/pnpm/dlx` and the new bin is re-downloaded every run, defeating the cache.

URLs and budgets live in [`.lighthouserc.cjs`](./.lighthouserc.cjs). The URL list is auto-derived by walking `content/docs/**/*.mdx` — new docs are picked up automatically. Budgets: SEO and best-practices pinned at 100 (`error`); accessibility ≥90 and performance ≥50 (`warn`). Any regression in `meta-description`, `document-title`, `is-crawlable`, etc. fails the PR.

**Why Lighthouse CI instead of Unlighthouse:** an earlier version of this workflow used Unlighthouse. It worked but `unlighthouse-ci` leaks Chrome process handles after a successful scan and never exits cleanly — under GitHub Actions the step would hang several minutes after the audit finished. LHCI handles process cleanup correctly and is the more battle-tested CI primitive.
3 changes: 2 additions & 1 deletion docsite/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@
"dev": "vite dev",
"build": "vite build",
"preview": "vite preview",
"deploy": "vite build && wrangler deploy"
"deploy": "vite build && wrangler deploy",
"check:seo": "vite build && pnpm dlx @lhci/cli@0.14.0 autorun"
},
"dependencies": {
"@fontsource-variable/geist": "^5.2.8",
Expand Down
2 changes: 1 addition & 1 deletion docsite/src/components/landing/close.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ export function Close() {
"
>
<a
href="/docs/getting-started/quickstart"
href={LANDING_META.docsHref}
className="
group inline-flex min-w-[168px] items-center justify-center gap-2.5
rounded-lg border border-landing-accent bg-landing-accent
Expand Down
2 changes: 1 addition & 1 deletion docsite/src/components/landing/data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,6 @@ export const WHY_POINTS = [
export const LANDING_META = {
version: "v0.2.0",
githubUrl: "https://github.com/variableland/env",
docsHref: "/docs/getting-started/quickstart",
docsHref: "/docs",
publishDate: "2026",
} as const;
4 changes: 2 additions & 2 deletions docsite/src/components/landing/hero.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { ArrowRight, Check, Copy } from "lucide-react";
import { useState } from "react";
import { RUNTIMES } from "./data.ts";
import { LANDING_META, RUNTIMES } from "./data.ts";
import { HeroCodeCard } from "./hero-code-card.tsx";

type Snippets = {
Expand Down Expand Up @@ -87,7 +87,7 @@ export function Hero({ snippets }: { snippets: Snippets }) {
"
>
<a
href="/docs/getting-started/quickstart"
href={LANDING_META.docsHref}
className="
group inline-flex items-center justify-center gap-2.5
rounded-lg border border-landing-accent bg-landing-accent
Expand Down
10 changes: 10 additions & 0 deletions docsite/src/routes/docs/$.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,14 @@ export const Route = createFileRoute("/docs/$")({
await clientLoader.preload(data.path);
return data;
},
head: ({ loaderData }) => ({
meta: loaderData
? [
{ title: `${loaderData.title} — @vlandoss/env docs` },
...(loaderData.description ? [{ name: "description", content: loaderData.description }] : []),
]
: [],
}),
});

const serverLoader = createServerFn({ method: "GET" })
Expand All @@ -29,6 +37,8 @@ const serverLoader = createServerFn({ method: "GET" })
return {
path: page.path,
url: page.url,
title: page.data.title,
description: page.data.description,
pageTree: await source.serializePageTree(source.getPageTree()),
};
});
Expand Down
Loading
Loading