Developer portal, free browser toolbox, and project showcase.
www.linacre.site · Toolbox · Projects · Games
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/LIN4CRE/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, feeds, llms.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.