Developer portal, free browser toolbox, and project showcase.
www.linacre.site Β· Tools Β· Games Β· About
A React 19 single-page application with prerendered static routes, deployed on Vercel. It serves three purposes:
- A free browser toolbox β JSON, Base64, VAT, hashing, text and URL utilities that run entirely client-side. Nothing is uploaded anywhere.
- A project showcase β live builds, downloadable releases, and inspectable source.
- A games hub β playable browser games plus Slime Factory Tycoon, an open-source Roblox idle-tycoon.
Everything below is measured on the live site, not estimated.
| Metric | Value | Threshold |
|---|---|---|
| TypeScript errors (strict) | 0 | β |
| Test suite | 18 passing | β |
| axe-core WCAG 2.2 AA β mobile | 0 violations | 0 |
| axe-core WCAG 2.2 AA β desktop | 0 violations | 0 |
| CLS β mobile | 0.0155 | < 0.10 |
| CLS β desktop | 0.0023 | < 0.10 |
| LCP | ~1.0 s | < 2.5 s |
| TTFB | ~150 ms | < 800 ms |
Reproduce with npm run verify, or see Testing for the browser
measurements.
| Layer | Choice | Why |
|---|---|---|
| Framework | React 19 | Concurrent rendering, useReducedMotion, modern Suspense |
| Language | TypeScript 5.8, strict | strict, noUnusedLocals, noUnusedParameters, noImplicitOverride |
| Build | Vite 7 | Fast HMR, Rollup manual chunking |
| Styling | Tailwind CSS v4 | Vite plugin, no PostCSS config needed |
| Animation | Motion | Respects prefers-reduced-motion throughout |
| Testing | Vitest + React Testing Library | Shares the Vite config; jsdom environment |
| Linting | ESLint 9 flat config | typescript-eslint + react-hooks |
| Formatting | Prettier 3 | |
| Backend | Express (bundled with esbuild) | Small API surface for contact and status |
| Hosting | Vercel | Static output plus a Node function |
Requirements: Node 22+ and npm.
git clone https://github.com/DLinacre/linacre.site.git
cd linacre.site
npm ci
npm run dev # http://localhost:3000| Script | Does |
|---|---|
npm run dev |
Vite dev server |
npm run build |
Vite build β prerender 22 routes β bundle the API |
npm run preview |
Serve the production build locally |
npm run typecheck |
tsc --noEmit under strict mode |
npm run lint |
ESLint |
npm run lint:fix |
ESLint with --fix |
npm run format |
Prettier write |
npm run format:check |
Prettier check (CI) |
npm test |
Vitest, single run |
npm run test:watch |
Vitest watch mode |
npm run test:coverage |
V8 coverage report |
npm run verify |
typecheck + lint + test β run this before pushing |
linacre.site/
βββ api/
β βββ server.ts Express API (contact, status, proxies)
βββ scripts/
β βββ prerender.mjs Static-renders 22 routes + sitemap.xml + llms-full.txt
β βββ agents/ Maintenance tooling
βββ src/
β βββ App.tsx Shell: routing, theming, layout
β βββ main.tsx Entry point, service worker registration
β βββ components/ Feature components, lazily loaded
β β βββ GameShowcase.tsx Manifest-driven game showcase
β β βββ __tests__/ Component tests
β βββ lib/ design-system, audioEngine, emblemRenderer
β βββ data/ Static content + synced game manifests
β βββ test/setup.ts jsdom polyfills, per-test cleanup
βββ public/ Fonts, icons, game art
βββ route-meta.json Per-route SEO metadata
βββ vercel.json Headers, redirects, caching
βββ vite.config.ts Build, chunking, and Vitest config
Prerendering over SSR. scripts/prerender.mjs emits static HTML for 22
routes at build time. Crawlers get real content with no JS; users get an SPA
after hydration. No server rendering cost.
Manual vendor chunks. React and Motion are split into long-cached chunks, so shipping app code doesn't invalidate a ~96 KB Motion download.
Route-level code splitting. Every page is React.lazy. The Suspense
fallback reserves min-h-[100svh] β without that reservation the footer paints
high and drops on hydration, which previously caused a 0.42 CLS on every
page.
Self-hosted variable fonts. Three families, unicode-range subset into
latin/latin-ext, font-display: swap, preloaded, immutable cache headers. No
third-party font requests.
Verified with axe-core against WCAG 2.0/2.1/2.2 A and AA at 390 px and 1440 px: 0 violations, 27 passing checks on both.
- Full ARIA tabs pattern β
tablistcontainer,aria-controlsβaria-labelledbywiring, rovingtabindex, arrow-key navigation - 44 px minimum touch targets
prefers-reduced-motionhonoured throughout- Opaque surfaces behind text, so contrast is deterministic rather than dependent on whatever scrolls underneath
- Semantic landmarks, skip link, visible focus rings
The tabs contract is covered by tests, so the fix can't silently regress.
npm test # 18 tests
npm run test:coverage # V8 coverageThe GameShowcase suite encodes the project's honesty guarantees as
executable contracts:
- No fabricated player, visit, download, or rating figures
- Honest empty states β "Coming Soon" rather than placeholder imagery
- The Play CTA stays disabled until the game is genuinely published
- Structured data contains no invented
aggregateRatingoroffers
If someone later adds a fake metric, CI fails. The rule stops being a convention and becomes something the build enforces.
Browser-level checks (axe-core, Core Web Vitals) are run with Playwright against the live deployment β see the audit in the game repository.
Auto-deploys from main via the Vercel GitHub integration. No manual step.
# Manual deploy, if ever needed
npx vercel deploy --prod --yesvercel.json sets the security headers and caching:
| Header | Value |
|---|---|
content-security-policy |
default-src 'self', object-src 'none', frame-ancestors 'none' |
strict-transport-security |
max-age=63072000; includeSubDomains; preload |
permissions-policy |
camera, mic, geolocation, payment, USB, sensors, interest-cohort all disabled |
x-frame-options |
DENY |
referrer-policy |
strict-origin-when-cross-origin |
Hashed assets are immutable, max-age=31536000; HTML is must-revalidate.
All optional β the site builds and runs without them. Absent keys disable their feature rather than breaking the build.
| Variable | Purpose |
|---|---|
GEMINI_API_KEY |
AI assistant |
UPSTASH_REDIS_REST_URL / _TOKEN |
API rate limiting |
RESEND_API_KEY |
Contact form delivery |
npm run verify # must pass before opening a PR- Keep TypeScript strict β no new
anywithout a comment explaining why. - Any UI change needs a mobile (390 px) check.
- Accessibility regressions are treated as bugs, not polish.
- Comments should explain why, not what.
npm run dev fails on a fresh clone β you need Node 22+. Check with
node -v.
Tests fail with matchMedia is not a function β you're outside the Vitest
environment. Run npm test, which loads src/test/setup.ts.
Build succeeds but a route 404s in preview β routes are prerendered at build
time. Add the path to scripts/prerender.mjs and route-meta.json.
Fonts flash on first load β expected. font-display: swap is deliberate;
blocking paint on a font download is worse.
Playwright can't launch Chromium on Linux β install system libraries:
sudo apt-get install -y libnspr4 libnss3 libasound2t64.
MIT β see LICENSE.