From f56cd83c08eccc2f42d65a306317782fab9740b2 Mon Sep 17 00:00:00 2001 From: Sarthak Agrawal Date: Sun, 11 Oct 2026 23:37:11 +0530 Subject: [PATCH 01/20] perf: rolepatch LCP pass 3 (home, pricing, tools), restore Clarity interaction loader Co-Authored-By: Claude Sonnet 5.5 --- landing-astro/scripts/inline-critical-css.mjs | 5 +- landing-astro/src/pages/index.astro | 4 +- public/fonts/space-grotesk.woff2 | Bin 0 -> 49208 bytes src/__tests__/deferred-analytics.test.ts | 109 ++++++++++++++++++ src/app/pricing/page.tsx | 4 +- src/app/readability.css | 17 ++- src/app/tools/page.tsx | 4 +- src/lib/analytics.ts | 5 +- src/lib/foundry-monitoring.ts | 49 ++++++-- 9 files changed, 176 insertions(+), 21 deletions(-) create mode 100644 public/fonts/space-grotesk.woff2 create mode 100644 src/__tests__/deferred-analytics.test.ts diff --git a/landing-astro/scripts/inline-critical-css.mjs b/landing-astro/scripts/inline-critical-css.mjs index 1d9985f..a2e5842 100644 --- a/landing-astro/scripts/inline-critical-css.mjs +++ b/landing-astro/scripts/inline-critical-css.mjs @@ -19,7 +19,6 @@ export async function runAstroInlineCss() { [...source.matchAll(styles)] .map((match) => match[1]) .join('\n') - .replaceAll('font-display:swap', 'font-display:optional') ), }).code.toString(); // Restrict first paint to the header and complete hero at every breakpoint. @@ -178,8 +177,8 @@ export async function runAstroInlineCss() { if (!hasMobileCtaRule) throw new Error('Critical CSS is missing the mobile CTA stacking rule'); // Register initial fonts only once. Repeating @font-face in a late stylesheet - // creates new FontFace objects with fresh display timelines: even `optional` - // can then select an already-cached Geist and rewrap the painted hero. + // creates new FontFace objects with fresh display timelines and can rewrap + // the painted hero. Keep swap and the metric fallback in the initial sheet. const deferred = transform({ filename: 'home.css', code: Buffer.from(css), diff --git a/landing-astro/src/pages/index.astro b/landing-astro/src/pages/index.astro index d313f6e..3cc562c 100644 --- a/landing-astro/src/pages/index.astro +++ b/landing-astro/src/pages/index.astro @@ -50,7 +50,7 @@ content.page.jsonLd = [jsonLd, { data-vitals-routes="changelog,docs,faq" > @@ -71,6 +71,8 @@ content.page.jsonLd = [jsonLd, { --font-geist-mono: ui-monospace, 'SFMono-Regular', Menlo, monospace; } body { margin: 0; } + /* Paint the complete hero immediately instead of animating blur/position. */ + main .hero-in { animation: none; } '; + const document = 'Résumé'; + const result = decode(ensureLeadingHtmlDoctype(encoder.encode(script + document))); + expect(result).toBe(`${script}${document.slice(15)}`); + expect(result.match(//g)).toHaveLength(1); + }); + it('preserves normal documents and does not invent a doctype or move arbitrary content', () => { + for (const html of [ + 'Normal', + 'No declaration', + '

Other prefix

', + ]) { + const original = encoder.encode(html); + expect(ensureLeadingHtmlDoctype(original)).toBe(original); + } + }); +}); diff --git a/e2e/navigation.spec.ts b/e2e/navigation.spec.ts index ca3ccbd..1aeac51 100644 --- a/e2e/navigation.spec.ts +++ b/e2e/navigation.spec.ts @@ -21,6 +21,7 @@ test.describe('public pages load', () => { test('pricing page renders', async ({ page }) => { await page.goto('/pricing'); + expect(await page.evaluate(() => document.compatMode)).toBe('CSS1Compat'); await expect(page.getByText(/Starter|Pro|Bulk/i).first()).toBeVisible(); }); @@ -33,6 +34,7 @@ test.describe('public pages load', () => { test('tools index links to diff and keywords', async ({ page }) => { await page.goto('/tools'); + expect(await page.evaluate(() => document.compatMode)).toBe('CSS1Compat'); await expect(page.getByRole('link', { name: /diff/i })).toBeVisible(); await expect(page.getByRole('link', { name: /keyword/i })).toBeVisible(); }); diff --git a/html-document.mjs b/html-document.mjs new file mode 100644 index 0000000..ed2a0ce --- /dev/null +++ b/html-document.mjs @@ -0,0 +1,18 @@ +/** Preserve Next's early polyfill while keeping its doctype first (standards mode). */ +export function ensureLeadingHtmlDoctype(body) { + const bytes = body instanceof Uint8Array ? body : new Uint8Array(body); + const prefix = new TextDecoder().decode(bytes.subarray(0, 1024)); + const doctype = //i.exec(prefix); + if (!doctype || doctype.index === 0) return bytes; + const before = prefix.slice(0, doctype.index); + // Only repair the observed Next inlineCss ordering; leave other documents alone. + if (!/^(?:\s*]*>[\s\S]*?<\/script>)+\s*$/i.test(before)) return bytes; + const encoder = new TextEncoder(); + const offset = encoder.encode(before).length; + const declaration = encoder.encode(doctype[0]); + const result = new Uint8Array(bytes.length); + result.set(declaration); + result.set(bytes.subarray(0, offset), declaration.length); + result.set(bytes.subarray(offset + declaration.length), offset + declaration.length); + return result; +} diff --git a/worker.mjs b/worker.mjs index 3067bec..f04a216 100644 --- a/worker.mjs +++ b/worker.mjs @@ -12,6 +12,7 @@ // All non-GET, non-`/` requests pass straight through to OpenNext. import openNext from './.open-next/worker.js'; +import { ensureLeadingHtmlDoctype } from './html-document.mjs'; import { withTiming } from './timing.mjs'; import { CONTENT_SECURITY_POLICY } from './security-policy.mjs'; import { handleAgentEdge } from './agent-edge.mjs'; @@ -285,7 +286,7 @@ export default { // silently dropping the inlined critical-CSS chunk somewhere in the // stream-fork; reading once and constructing both responses from // the same Uint8Array sidesteps the streaming edge case entirely. - const body = await response.arrayBuffer(); + const body = ensureLeadingHtmlDoctype(await response.arrayBuffer()); const headers = new Headers(response.headers); headers.set('Cache-Control', DOCUMENT_EDGE_CACHE_CONTROL); From edb6fce51e51190f40e58f8573ad4f55f7d153ed Mon Sep 17 00:00:00 2001 From: Sarthak Agrawal Date: Tue, 13 Oct 2026 10:38:09 +0000 Subject: [PATCH 08/20] Share the monochrome product mark across application navigation --- src/components/rolepatch-mark.tsx | 15 +++++++++++++++ src/components/site-nav.tsx | 7 +++---- src/components/workspace-interior.tsx | 7 +++---- 3 files changed, 21 insertions(+), 8 deletions(-) create mode 100644 src/components/rolepatch-mark.tsx diff --git a/src/components/rolepatch-mark.tsx b/src/components/rolepatch-mark.tsx new file mode 100644 index 0000000..e34e4a8 --- /dev/null +++ b/src/components/rolepatch-mark.tsx @@ -0,0 +1,15 @@ +import { ArrowUpRight } from 'lucide-react'; +import { cn } from '@/lib/utils'; + +export function RolePatchMark({ className }: { className?: string }) { + return ( + + + ); +} diff --git a/src/components/site-nav.tsx b/src/components/site-nav.tsx index 80c744b..09b9582 100644 --- a/src/components/site-nav.tsx +++ b/src/components/site-nav.tsx @@ -1,10 +1,11 @@ 'use client'; -import { ArrowUpRight, Menu, X } from 'lucide-react'; +import { Menu, X } from 'lucide-react'; import Link from 'next/link'; import { usePathname } from 'next/navigation'; import { useState } from 'react'; +import { RolePatchMark } from '@/components/rolepatch-mark'; import { TokenBalance } from '@/components/token-balance'; import { UserMenu } from '@/components/user-menu'; @@ -38,9 +39,7 @@ export function SiteNav() { prefetch={false} className="font-semibold text-foreground mr-auto sm:mr-6 flex items-center gap-2" > - - + RolePatch diff --git a/src/components/workspace-interior.tsx b/src/components/workspace-interior.tsx index 4725bf9..a0e641a 100644 --- a/src/components/workspace-interior.tsx +++ b/src/components/workspace-interior.tsx @@ -1,8 +1,9 @@ 'use client'; -import { ArrowUpRight, BriefcaseBusiness, FileText } from 'lucide-react'; +import { BriefcaseBusiness, FileText } from 'lucide-react'; import Link from 'next/link'; import { useMemo, useState, type ReactNode } from 'react'; +import { RolePatchMark } from '@/components/rolepatch-mark'; import { TokenBalance } from '@/components/token-balance'; import { UserMenu } from '@/components/user-menu'; import { WorkspaceThemeProvider } from '@/components/workspace-theme'; @@ -53,9 +54,7 @@ function WorkspaceSidebar({ setOpenMobile(false)}> - - - + RolePatch Your workspace From 2a4277423dc53edb2eef3ed383a3ca3b6f4eb858 Mon Sep 17 00:00:00 2001 From: Sarthak Agrawal Date: Tue, 13 Oct 2026 10:44:48 +0000 Subject: [PATCH 09/20] Record local UX and performance evidence with open release gates --- .../resume-first-performance-2026-10-13.json | 112 ++++++++++++++++++ 1 file changed, 112 insertions(+) create mode 100644 docs/operations/evidence/resume-first-performance-2026-10-13.json diff --git a/docs/operations/evidence/resume-first-performance-2026-10-13.json b/docs/operations/evidence/resume-first-performance-2026-10-13.json new file mode 100644 index 0000000..998ba86 --- /dev/null +++ b/docs/operations/evidence/resume-first-performance-2026-10-13.json @@ -0,0 +1,112 @@ +{ + "date": "2026-10-13", + "repository": "Significant-Hobbies/rolepatch", + "issues": [ + 32, + 39 + ], + "pull_request": 34, + "verified_remote_main": "eb355d08d5df1a9bec92ef8f77511cb216b10912", + "unchanged_remote_pr_head": "ecb8c423aba3722c71a6c63855df6099ddc1bdeb", + "measured_code_sha": "edb6fce51e51190f40e58f8573ad4f55f7d153ed", + "history": "Current main merged into retained PR34 branch; both original author commits remain ancestors. No rebases or force pushes.", + "runtime": "wrangler dev --local --env cache; full cf:build; anonymous public routes only", + "workload": "Lighthouse 12.8.2 mobile defaults; simulated throttling; cold browser storage each run; sequential 3 runs per route; external HTTPS blocked to prevent outbound provider/analytics traffic", + "baseline_note": "Retained production build at verified main; source branch changed during baseline but built Worker artifacts remained main.", + "main": { + "/": { + "median_lcp_ms": 1600.5108, + "median_performance": 100, + "best_practices": [ + 100, + 100, + 100 + ] + }, + "/pricing": { + "median_lcp_ms": 3637.28025, + "median_performance": 89.0, + "best_practices": [ + 100, + 100, + 100 + ] + }, + "/tools": { + "median_lcp_ms": 4421.040149999999, + "median_performance": 83.0, + "best_practices": [ + 100, + 100, + 100 + ] + } + }, + "candidate": { + "/": { + "median_lcp_ms": 1740.6149, + "median_performance": 100, + "best_practices": [ + 100, + 100, + 100 + ] + }, + "/pricing": { + "median_lcp_ms": 2662.948400000001, + "median_performance": 95.0, + "best_practices": [ + 100, + 100, + 100 + ] + }, + "/tools": { + "median_lcp_ms": 2920.2155999999995, + "median_performance": 95.0, + "best_practices": [ + 100, + 100, + 100 + ] + } + }, + "checks": { + "quality": "passed: Biome, types, coverage, docs, extension types, unused, complexity, duplication, cycles, dependencies, suppressions, hygiene", + "unit_passed": 762, + "unit_skipped": 1, + "focused_browser_passed": 24, + "responsive_widths": [ + 390, + 768, + 1440 + ], + "responsive_routes": [ + "/", + "/pricing", + "/tools", + "/resume-builder", + "/jobs" + ], + "production_build": "complete cf:build passed" + }, + "lcp_gate_ms": 2500, + "lcp_gate_met": false, + "best_practices_preserved": true, + "publication": { + "status": "blocked", + "git_push": "HTTP 403: Permission denied to configured account", + "github_connector": "HTTP 403: Resource not accessible by integration creating a tree", + "remote_ci": "No new run possible because code could not be pushed; old PR checks still fail. Local required checks pass.", + "pr_metadata": "HTTP 403: Resource not accessible by integration updating PR body; PR metadata remains unchanged." + }, + "open_gates": [ + "Any unmet local LCP medians", + "GitHub write access, actual branch push, and fresh remote CI", + "Human/editorial/taste acceptance", + "Real-device acceptance", + "Production acceptance, including reported Cloudflare 401 noise", + "PDF/DOCX import end-to-end AI structuring and real OAuth #12 were excluded" + ], + "boundaries": "Local synthetic guest checks only. No credentials inspected or changed; no owner resumes/research/prepurge read; no live providers, paid services, deploys, releases, merges, production configuration or remote migrations. No skipped or synthetic checks claimed as live acceptance." +} From 6cfe5a46c651f02b3fabe7976ab84045f68b61db Mon Sep 17 00:00:00 2001 From: Sarthak Agrawal Date: Tue, 13 Oct 2026 12:41:16 +0000 Subject: [PATCH 10/20] Reduce public heading font transfer and avoid speculative tool bundles --- e2e/public-performance.spec.ts | 20 +++++++++ public/fonts/space-grotesk-public-700.woff2 | Bin 0 -> 25176 bytes scripts/build-public-display-font.py | 47 ++++++++++++++++++++ src/app/pricing/page.tsx | 6 ++- src/app/readability.css | 12 +++++ src/app/tools/page.tsx | 7 ++- 6 files changed, 90 insertions(+), 2 deletions(-) create mode 100644 e2e/public-performance.spec.ts create mode 100644 public/fonts/space-grotesk-public-700.woff2 create mode 100644 scripts/build-public-display-font.py diff --git a/e2e/public-performance.spec.ts b/e2e/public-performance.spec.ts new file mode 100644 index 0000000..ea64ee9 --- /dev/null +++ b/e2e/public-performance.spec.ts @@ -0,0 +1,20 @@ +import { expect, test } from '@playwright/test'; + +test('tools keeps all cards and loads the selected tool on navigation', async ({ page }) => { + const prefetched: string[] = []; + page.on('request', (request) => { + if ( + request.headers()['next-router-prefetch'] && + new URL(request.url()).pathname.startsWith('/tools/') + ) + prefetched.push(request.url()); + }); + await page.goto('/tools'); + await expect(page.getByRole('link', { name: /Use tool/ })).toHaveCount(6); + await page.waitForLoadState('networkidle'); + expect(prefetched).toEqual([]); + await page.getByRole('link', { name: /ATS Keyword Checker/ }).click(); + await expect(page).toHaveURL(/\/tools\/keywords$/); + await expect(page.locator('textarea').first()).toBeVisible(); + await expect(page.locator('textarea').nth(1)).toBeVisible(); +}); diff --git a/public/fonts/space-grotesk-public-700.woff2 b/public/fonts/space-grotesk-public-700.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..97437ef84ea389cc7410bea75358b89d45a4830f GIT binary patch literal 25176 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 250 +for name in encoded.getGlyphOrder(): + before, after = RecordingPen(), RecordingPen() + original.getGlyphSet()[name].draw(before) + encoded.getGlyphSet()[name].draw(after) + assert before.value == after.value, name + assert original['hmtx'][name] == encoded['hmtx'][name], name +for table in ['hhea', 'OS/2']: + for metric in ['ascent', 'descent', 'lineGap', 'sTypoAscender', 'sTypoDescender', 'sTypoLineGap']: + if hasattr(original[table], metric): + assert getattr(original[table], metric) == getattr(encoded[table], metric) +print(f'{path}: {path.stat().st_size} bytes; outlines match the serialized 700 instance, widths and line metrics unchanged.') diff --git a/src/app/pricing/page.tsx b/src/app/pricing/page.tsx index 8a1196d..67889ba 100644 --- a/src/app/pricing/page.tsx +++ b/src/app/pricing/page.tsx @@ -41,7 +41,11 @@ async function hasRecentPayment(): Promise { } export default async function PricingPage() { - preload('/fonts/space-grotesk.woff2', { as: 'font', type: 'font/woff2', crossOrigin: '' }); + preload('/fonts/space-grotesk-public-700.woff2', { + as: 'font', + type: 'font/woff2', + crossOrigin: '', + }); const [balance, paymentVerified] = await Promise.all([getTokenBalance(), hasRecentPayment()]); return ( diff --git a/src/app/readability.css b/src/app/readability.css index 0896de5..2f83207 100644 --- a/src/app/readability.css +++ b/src/app/readability.css @@ -85,3 +85,15 @@ .job-row > :nth-child(7) { grid-column: 2; grid-row: 3; } } @media (max-width: 1023px) { .outline-layout { grid-template-columns: minmax(0, 1fr); gap: 24px; } .outline-index { position: static; } } + +/* Same 700-weight outlines; the complete face handles all other characters. */ +@font-face { + font-family: "RolePatch Public Display"; + src: url("/fonts/space-grotesk-public-700.woff2") format("woff2"); + font-style: normal; + font-weight: 700; + font-display: optional; +} +.precision-app-nav ~ .performance-page :is(h1, h2) { + font-family: "RolePatch Public Display", "RolePatch Display", "RolePatch Display Fallback", var(--font-sans); +} diff --git a/src/app/tools/page.tsx b/src/app/tools/page.tsx index 4b9ed63..cae38eb 100644 --- a/src/app/tools/page.tsx +++ b/src/app/tools/page.tsx @@ -50,7 +50,11 @@ const tools = [ ]; export default function ToolsPage() { - preload('/fonts/space-grotesk.woff2', { as: 'font', type: 'font/woff2', crossOrigin: '' }); + preload('/fonts/space-grotesk-public-700.woff2', { + as: 'font', + type: 'font/woff2', + crossOrigin: '', + }); return (
@@ -67,6 +71,7 @@ export default function ToolsPage() {
From fcf84ba25375fe0eeb12229c37aade0fa7c05ea5 Mon Sep 17 00:00:00 2001 From: Sarthak Agrawal Date: Tue, 13 Oct 2026 12:52:54 +0000 Subject: [PATCH 11/20] Negotiate byte-preserving Brotli for public pricing and tools documents --- __tests__/html-encoding.test.ts | 76 +++++++++++++++++++++++++++++++++ html-encoding.mjs | 63 +++++++++++++++++++++++++++ worker.mjs | 5 ++- 3 files changed, 142 insertions(+), 2 deletions(-) create mode 100644 __tests__/html-encoding.test.ts create mode 100644 html-encoding.mjs diff --git a/__tests__/html-encoding.test.ts b/__tests__/html-encoding.test.ts new file mode 100644 index 0000000..7c33517 --- /dev/null +++ b/__tests__/html-encoding.test.ts @@ -0,0 +1,76 @@ +// @vitest-environment node +import { brotliDecompressSync } from 'node:zlib'; +import { describe, expect, it } from 'vitest'; +import { encodePublicHtmlResponse } from '../html-encoding.mjs'; + +const html = 'Résumé — all meaningful content.'; +const response = () => + new Response(html, { + headers: { + 'content-type': 'text/html; charset=utf-8', + 'content-length': String(Buffer.byteLength(html)), + etag: '"document"', + vary: 'RSC', + 'cache-control': 'public, max-age=0, must-revalidate', + }, + }); + +describe('public HTML content encoding', () => { + it('round-trips every decoded byte and retains cache and variant headers', async () => { + const encoded = await encodePublicHtmlResponse( + response(), + new Request('https://rolepatch.com/pricing', { + headers: { 'accept-encoding': 'gzip, deflate, br' }, + }) + ); + expect(encoded.headers.get('content-encoding')).toBe('br'); + expect(encoded.headers.get('content-length')).toBeNull(); + expect(encoded.headers.get('vary')).toBe('RSC, Accept-Encoding'); + expect(encoded.headers.get('etag')).toBe('W/"document"'); + expect(encoded.headers.get('cache-control')).toBe('public, max-age=0, must-revalidate'); + expect(brotliDecompressSync(Buffer.from(await encoded.arrayBuffer()))).toEqual( + Buffer.from(html) + ); + }); + it('honors identity, q=0 and higher gzip preference', async () => { + for (const accept of ['', 'gzip', 'br;q=0, gzip', 'br;q=0.5, gzip;q=1']) { + const original = response(); + expect( + await encodePublicHtmlResponse( + original, + new Request('https://rolepatch.com/tools', { headers: { 'accept-encoding': accept } }) + ) + ).toBe(original); + expect(await original.text()).toBe(html); + } + }); + it('leaves other routes, mutations, errors and existing encodings alone', async () => { + for (const [path, method] of [ + ['/resume-builder', 'GET'], + ['/tools', 'POST'], + ]) { + const original = response(); + expect( + await encodePublicHtmlResponse( + original, + new Request('https://rolepatch.com' + path, { + method, + headers: { 'accept-encoding': 'br' }, + }) + ) + ).toBe(original); + } + for (const original of [ + new Response('error', { status: 500 }), + new Response('already encoded', { + headers: { 'content-type': 'text/html', 'content-encoding': 'gzip' }, + }), + ]) + expect( + await encodePublicHtmlResponse( + original, + new Request('https://rolepatch.com/pricing', { headers: { 'accept-encoding': 'br' } }) + ) + ).toBe(original); + }); +}); diff --git a/html-encoding.mjs b/html-encoding.mjs new file mode 100644 index 0000000..89dbca2 --- /dev/null +++ b/html-encoding.mjs @@ -0,0 +1,63 @@ +import { brotliCompressSync, constants } from 'node:zlib'; + +function encodingQuality(header, encoding) { + const entry = header + .split(',') + .find((item) => item.trim().split(';')[0].toLowerCase() === encoding); + if (!entry) return 0; + const quality = /;\s*q=([0-9.]+)/i.exec(entry); + return quality ? Number(quality[1]) : 1; +} + +/** Negotiate only public pricing/tools HTML; the cache keeps its identity bytes. */ +export async function encodePublicHtmlResponse(response, request) { + const path = new URL(request.url).pathname; + const accepted = request.headers.get('accept-encoding') ?? ''; + const br = encodingQuality(accepted, 'br'); + if ( + !['/pricing', '/tools'].includes(path) || + request.method !== 'GET' || + response.status !== 200 || + !response.headers.get('content-type')?.includes('text/html') || + response.headers.has('content-encoding') || + br <= 0 || + br < encodingQuality(accepted, 'gzip') + ) + return response; + const body = new Uint8Array(await response.arrayBuffer()); + let encoded; + try { + encoded = brotliCompressSync(body, { + params: { + [constants.BROTLI_PARAM_QUALITY]: 6, + [constants.BROTLI_PARAM_MODE]: constants.BROTLI_MODE_TEXT, + }, + }); + } catch { + // An unsupported runtime must still return the complete original document. + return new Response(body, response); + } + const headers = new Headers(response.headers); + headers.set('content-encoding', 'br'); + headers.delete('content-length'); + headers.set( + 'vary', + [ + ...new Set([ + ...(headers.get('vary') ?? '') + .split(',') + .map((v) => v.trim()) + .filter(Boolean), + 'Accept-Encoding', + ]), + ].join(', ') + ); + const etag = headers.get('etag'); + if (etag && !etag.startsWith('W/')) headers.set('etag', `W/${etag}`); + return new Response(encoded, { + status: response.status, + statusText: response.statusText, + headers, + encodeBody: 'manual', + }); +} diff --git a/worker.mjs b/worker.mjs index f04a216..070a324 100644 --- a/worker.mjs +++ b/worker.mjs @@ -13,6 +13,7 @@ import openNext from './.open-next/worker.js'; import { ensureLeadingHtmlDoctype } from './html-document.mjs'; +import { encodePublicHtmlResponse } from './html-encoding.mjs'; import { withTiming } from './timing.mjs'; import { CONTENT_SECURITY_POLICY } from './security-policy.mjs'; import { handleAgentEdge } from './agent-edge.mjs'; @@ -265,7 +266,7 @@ export default { const hit = new Response(cached.body, cached); hit.headers.set('Cache-Control', DOCUMENT_CLIENT_CACHE_CONTROL); hit.headers.set('x-edge-cache', 'HIT'); - return hit; + return encodePublicHtmlResponse(hit, request); } const response = await openNext.fetch(request, env, ctx); @@ -304,7 +305,7 @@ export default { }); clientResponse.headers.set('Cache-Control', DOCUMENT_CLIENT_CACHE_CONTROL); clientResponse.headers.set('x-edge-cache', 'MISS'); - return clientResponse; + return encodePublicHtmlResponse(clientResponse, request); } catch (err) { console.error( `[error] ${request.method} ${new URL(request.url).pathname}:`, From f8c3db4f03fa97dcebc229ebf9c93173003037f5 Mon Sep 17 00:00:00 2001 From: Sarthak Agrawal Date: Tue, 13 Oct 2026 13:00:03 +0000 Subject: [PATCH 12/20] Revert "Negotiate byte-preserving Brotli for public pricing and tools documents" This reverts commit fcf84ba25375fe0eeb12229c37aade0fa7c05ea5. --- __tests__/html-encoding.test.ts | 76 --------------------------------- html-encoding.mjs | 63 --------------------------- worker.mjs | 5 +-- 3 files changed, 2 insertions(+), 142 deletions(-) delete mode 100644 __tests__/html-encoding.test.ts delete mode 100644 html-encoding.mjs diff --git a/__tests__/html-encoding.test.ts b/__tests__/html-encoding.test.ts deleted file mode 100644 index 7c33517..0000000 --- a/__tests__/html-encoding.test.ts +++ /dev/null @@ -1,76 +0,0 @@ -// @vitest-environment node -import { brotliDecompressSync } from 'node:zlib'; -import { describe, expect, it } from 'vitest'; -import { encodePublicHtmlResponse } from '../html-encoding.mjs'; - -const html = 'Résumé — all meaningful content.'; -const response = () => - new Response(html, { - headers: { - 'content-type': 'text/html; charset=utf-8', - 'content-length': String(Buffer.byteLength(html)), - etag: '"document"', - vary: 'RSC', - 'cache-control': 'public, max-age=0, must-revalidate', - }, - }); - -describe('public HTML content encoding', () => { - it('round-trips every decoded byte and retains cache and variant headers', async () => { - const encoded = await encodePublicHtmlResponse( - response(), - new Request('https://rolepatch.com/pricing', { - headers: { 'accept-encoding': 'gzip, deflate, br' }, - }) - ); - expect(encoded.headers.get('content-encoding')).toBe('br'); - expect(encoded.headers.get('content-length')).toBeNull(); - expect(encoded.headers.get('vary')).toBe('RSC, Accept-Encoding'); - expect(encoded.headers.get('etag')).toBe('W/"document"'); - expect(encoded.headers.get('cache-control')).toBe('public, max-age=0, must-revalidate'); - expect(brotliDecompressSync(Buffer.from(await encoded.arrayBuffer()))).toEqual( - Buffer.from(html) - ); - }); - it('honors identity, q=0 and higher gzip preference', async () => { - for (const accept of ['', 'gzip', 'br;q=0, gzip', 'br;q=0.5, gzip;q=1']) { - const original = response(); - expect( - await encodePublicHtmlResponse( - original, - new Request('https://rolepatch.com/tools', { headers: { 'accept-encoding': accept } }) - ) - ).toBe(original); - expect(await original.text()).toBe(html); - } - }); - it('leaves other routes, mutations, errors and existing encodings alone', async () => { - for (const [path, method] of [ - ['/resume-builder', 'GET'], - ['/tools', 'POST'], - ]) { - const original = response(); - expect( - await encodePublicHtmlResponse( - original, - new Request('https://rolepatch.com' + path, { - method, - headers: { 'accept-encoding': 'br' }, - }) - ) - ).toBe(original); - } - for (const original of [ - new Response('error', { status: 500 }), - new Response('already encoded', { - headers: { 'content-type': 'text/html', 'content-encoding': 'gzip' }, - }), - ]) - expect( - await encodePublicHtmlResponse( - original, - new Request('https://rolepatch.com/pricing', { headers: { 'accept-encoding': 'br' } }) - ) - ).toBe(original); - }); -}); diff --git a/html-encoding.mjs b/html-encoding.mjs deleted file mode 100644 index 89dbca2..0000000 --- a/html-encoding.mjs +++ /dev/null @@ -1,63 +0,0 @@ -import { brotliCompressSync, constants } from 'node:zlib'; - -function encodingQuality(header, encoding) { - const entry = header - .split(',') - .find((item) => item.trim().split(';')[0].toLowerCase() === encoding); - if (!entry) return 0; - const quality = /;\s*q=([0-9.]+)/i.exec(entry); - return quality ? Number(quality[1]) : 1; -} - -/** Negotiate only public pricing/tools HTML; the cache keeps its identity bytes. */ -export async function encodePublicHtmlResponse(response, request) { - const path = new URL(request.url).pathname; - const accepted = request.headers.get('accept-encoding') ?? ''; - const br = encodingQuality(accepted, 'br'); - if ( - !['/pricing', '/tools'].includes(path) || - request.method !== 'GET' || - response.status !== 200 || - !response.headers.get('content-type')?.includes('text/html') || - response.headers.has('content-encoding') || - br <= 0 || - br < encodingQuality(accepted, 'gzip') - ) - return response; - const body = new Uint8Array(await response.arrayBuffer()); - let encoded; - try { - encoded = brotliCompressSync(body, { - params: { - [constants.BROTLI_PARAM_QUALITY]: 6, - [constants.BROTLI_PARAM_MODE]: constants.BROTLI_MODE_TEXT, - }, - }); - } catch { - // An unsupported runtime must still return the complete original document. - return new Response(body, response); - } - const headers = new Headers(response.headers); - headers.set('content-encoding', 'br'); - headers.delete('content-length'); - headers.set( - 'vary', - [ - ...new Set([ - ...(headers.get('vary') ?? '') - .split(',') - .map((v) => v.trim()) - .filter(Boolean), - 'Accept-Encoding', - ]), - ].join(', ') - ); - const etag = headers.get('etag'); - if (etag && !etag.startsWith('W/')) headers.set('etag', `W/${etag}`); - return new Response(encoded, { - status: response.status, - statusText: response.statusText, - headers, - encodeBody: 'manual', - }); -} diff --git a/worker.mjs b/worker.mjs index 070a324..f04a216 100644 --- a/worker.mjs +++ b/worker.mjs @@ -13,7 +13,6 @@ import openNext from './.open-next/worker.js'; import { ensureLeadingHtmlDoctype } from './html-document.mjs'; -import { encodePublicHtmlResponse } from './html-encoding.mjs'; import { withTiming } from './timing.mjs'; import { CONTENT_SECURITY_POLICY } from './security-policy.mjs'; import { handleAgentEdge } from './agent-edge.mjs'; @@ -266,7 +265,7 @@ export default { const hit = new Response(cached.body, cached); hit.headers.set('Cache-Control', DOCUMENT_CLIENT_CACHE_CONTROL); hit.headers.set('x-edge-cache', 'HIT'); - return encodePublicHtmlResponse(hit, request); + return hit; } const response = await openNext.fetch(request, env, ctx); @@ -305,7 +304,7 @@ export default { }); clientResponse.headers.set('Cache-Control', DOCUMENT_CLIENT_CACHE_CONTROL); clientResponse.headers.set('x-edge-cache', 'MISS'); - return encodePublicHtmlResponse(clientResponse, request); + return clientResponse; } catch (err) { console.error( `[error] ${request.method} ${new URL(request.url).pathname}:`, From 56eca5c565456fa3c8b7cc13d27782e08aef1545 Mon Sep 17 00:00:00 2001 From: Sarthak Agrawal Date: Tue, 13 Oct 2026 13:00:34 +0000 Subject: [PATCH 13/20] Use the standard Latin heading subset with complete-face fallback --- public/fonts/space-grotesk-public-700.woff2 | Bin 25176 -> 16520 bytes scripts/build-public-display-font.py | 5 +++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/public/fonts/space-grotesk-public-700.woff2 b/public/fonts/space-grotesk-public-700.woff2 index 97437ef84ea389cc7410bea75358b89d45a4830f..c33afe9e6c37ac035094a532b1f9a7c052b5bf3e 100644 GIT binary patch literal 16520 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 literal 25176 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 250 +assert len(encoded.getBestCmap()) >= 180 +assert all(code in encoded.getBestCmap() for code in range(0x20, 0x7F)) for name in encoded.getGlyphOrder(): before, after = RecordingPen(), RecordingPen() original.getGlyphSet()[name].draw(before) From ad337113f12080027b017d466da094027bbf7c8d Mon Sep 17 00:00:00 2001 From: Sarthak Agrawal Date: Tue, 13 Oct 2026 13:12:59 +0000 Subject: [PATCH 14/20] Scope Next utility generation to its complete application source --- src/app/globals.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/globals.css b/src/app/globals.css index 557e64c..43133e6 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -1,4 +1,4 @@ -@import "tailwindcss"; +@import "tailwindcss" source("../"); @import "./precision.css"; @import "./readability.css"; @import "./workspace.css"; From 9033f64603972a469e21db0eddbe9b844653a78a Mon Sep 17 00:00:00 2001 From: Sarthak Agrawal Date: Tue, 13 Oct 2026 15:03:43 +0000 Subject: [PATCH 15/20] Reduce public document styles and load account UI only for signed-in sessions --- __tests__/html-document.test.ts | 27 +++++- .../0016-public-document-rendering.md | 51 ++++++++++ e2e/public-performance.spec.ts | 62 ++++++++++++ html-document.mjs | 13 +++ next.config.ts | 6 +- src/__tests__/account-menu.test.tsx | 2 +- src/app/globals.css | 1 - src/components/signed-in-user-menu.tsx | 84 ++++++++++++++++ src/components/user-menu.tsx | 97 ++++++------------- src/components/workspace-interior.tsx | 2 + worker.mjs | 7 +- 11 files changed, 275 insertions(+), 77 deletions(-) create mode 100644 docs/architecture/decisions/0016-public-document-rendering.md create mode 100644 src/components/signed-in-user-menu.tsx diff --git a/__tests__/html-document.test.ts b/__tests__/html-document.test.ts index 42b9fb8..f20a22d 100644 --- a/__tests__/html-document.test.ts +++ b/__tests__/html-document.test.ts @@ -1,6 +1,6 @@ // @vitest-environment node import { describe, expect, it } from 'vitest'; -import { ensureLeadingHtmlDoctype } from '../html-document.mjs'; +import { deferPublicDocumentScripts, ensureLeadingHtmlDoctype } from '../html-document.mjs'; const encoder = new TextEncoder(); const decode = (body: Uint8Array) => new TextDecoder().decode(body); @@ -24,3 +24,28 @@ describe('anonymous cached HTML standards mode', () => { } }); }); + +describe('public document hydration scheduling', () => { + it('defers only async Next chunks and preserves inline Flight, content and other scripts', () => { + const html = + '

Résumé

' + + '' + + '' + + '' + + '' + + ''; + for (const route of ['/pricing', '/tools']) { + expect(decode(deferPublicDocumentScripts(encoder.encode(html), route))).toBe( + html + .replace('runtime.js" async=""', 'runtime.js" defer') + .replace('client.js" async', 'client.js" defer') + ); + } + }); + it('leaves every other route byte-identical', () => { + const body = encoder.encode(''); + for (const route of ['/', '/jobs', '/resume-builder', '/tools/keywords', '/pricing/other']) { + expect(deferPublicDocumentScripts(body, route)).toBe(body); + } + }); +}); diff --git a/docs/architecture/decisions/0016-public-document-rendering.md b/docs/architecture/decisions/0016-public-document-rendering.md new file mode 100644 index 0000000..93773ed --- /dev/null +++ b/docs/architecture/decisions/0016-public-document-rendering.md @@ -0,0 +1,51 @@ +--- +title: Public document rendering +--- + +# ADR-0016: Public document rendering + +Date: 2026-10-13 +Status: Accepted for the source candidate; no production release + +## Context + +Pricing and the tools catalog have server-rendered text, but their initial +document also carried the complete application stylesheet twice: in the HTML +and in Next's Flight payload. Guest navigation imported the authenticated +dropdown and sidebar primitives even when there was no signed-in account. +These costs compete with the public text rendering path tracked in +[issue #39](https://github.com/Significant-Hobbies/rolepatch/issues/39). + +## Decision + +Use Next's documented external-stylesheet mode. Keep the complete stylesheets +render-blocking, and retain the existing critical-CSS build step for +prerendered files. Import workspace-only styles from the already lazy workspace +interior. Their scoped rules and the public content remain unchanged. + +Load the signed-in menu through `next/dynamic` after a session supplies a user. +Keep the existing guest sign-in path and all authenticated menu actions. A +disabled account placeholder reserves space while the account module loads; +it exposes neither settings nor sign-out until those actions are ready. + +For anonymous `/pricing` and `/tools` documents only, use native `defer` for +Next's async chunk scripts. URLs, inline Flight data and script order remain +intact. Auth-bearing requests, RSC responses and other routes retain their +existing Worker path. There are no timers or interaction-based hydration gates. + +## Consequences + +The public document avoids duplicated inline CSS and guest-only visits avoid +account UI code. External CSS adds a request waterfall, so production-build +Lighthouse comparisons must verify the trade-off. Menu, tool and builder +readiness need interaction assertions, not just screenshots or static HTML. +Responsive geometry and workspace theme portals must remain consistent. + +## Alternatives considered + +- Global `inlineCss`: Next documents its initial Flight duplication; keeping + the full stylesheet inline preserves the measured document overhead. +- Deferring every route or removing hydration: rejected because workspace, + account and mutation flows need the existing Next runtime. +- Native `defer` alone: did not meet the pricing gate; dependency and stylesheet + reductions are necessary alongside scheduling. diff --git a/e2e/public-performance.spec.ts b/e2e/public-performance.spec.ts index ea64ee9..ebbdaeb 100644 --- a/e2e/public-performance.spec.ts +++ b/e2e/public-performance.spec.ts @@ -1,5 +1,67 @@ import { expect, test } from '@playwright/test'; +test('account menu becomes usable after a synthetic session loads its account-only code', async ({ + page, +}) => { + const synthetic = { + user: { + id: 'public-performance-fixture', + name: 'Sample Reader', + email: 'sample@example.org', + emailVerified: false, + createdAt: '2026-01-01T00:00:00.000Z', + updatedAt: '2026-01-01T00:00:00.000Z', + }, + session: { + id: 'fixture-session', + token: 'synthetic-local-only', + userId: 'public-performance-fixture', + expiresAt: '2030-01-01T00:00:00.000Z', + createdAt: '2026-01-01T00:00:00.000Z', + updatedAt: '2026-01-01T00:00:00.000Z', + }, + }; + await page.route('**/api/auth/get-session**', (route) => route.fulfill({ json: synthetic })); + for (const path of ['/pricing', '/resume-builder']) { + await page.goto(path); + if (path === '/resume-builder' && (page.viewportSize()?.width ?? 0) < 768) { + await page.getByRole('button', { name: 'Toggle Sidebar', exact: true }).click(); + } + const account = page.getByRole('button', { name: 'Account menu', exact: true }); + await expect(account).toBeEnabled(); + await account.click(); + await expect(page.getByRole('menuitem', { name: 'Account settings' })).toHaveAttribute( + 'href', + '/settings' + ); + await expect(page.getByRole('menuitem', { name: 'Sign out' })).toBeVisible(); + await expect(page.getByRole('menuitem', { name: /pricing|buy|billing/i })).toHaveCount(0); + await page.keyboard.press('Escape'); + } +}); + +for (const route of ['/pricing', '/tools']) { + test(`${route} hydrates its guest controls without losing server content`, async ({ page }) => { + const errors: string[] = []; + page.on('pageerror', (error) => errors.push(error.message)); + await page.setViewportSize({ width: 390, height: 844 }); + await page.goto(route); + await expect(page.getByRole('heading', { level: 1 })).toBeVisible(); + await expect(page.getByRole('button', { name: 'Sign in', exact: true })).toBeEnabled(); + await page.getByRole('button', { name: 'Open menu' }).click(); + await expect(page.getByRole('button', { name: 'Close menu' })).toHaveAttribute( + 'aria-expanded', + 'true' + ); + await page.getByRole('button', { name: 'Close menu' }).click(); + await expect(page.getByRole('button', { name: 'Open menu' })).toHaveAttribute( + 'aria-expanded', + 'false' + ); + expect(errors).toEqual([]); + }); +} + test('tools keeps all cards and loads the selected tool on navigation', async ({ page }) => { const prefetched: string[] = []; page.on('request', (request) => { diff --git a/html-document.mjs b/html-document.mjs index ed2a0ce..5af0746 100644 --- a/html-document.mjs +++ b/html-document.mjs @@ -16,3 +16,16 @@ export function ensureLeadingHtmlDoctype(body) { result.set(bytes.subarray(offset + declaration.length), offset + declaration.length); return result; } + +/** Let SSR text paint before document-order Next hydration on public catalog pages. */ +export function deferPublicDocumentScripts(body, pathname) { + if (pathname !== '/pricing' && pathname !== '/tools') return body; + const html = new TextDecoder().decode(body); + return new TextEncoder().encode( + html.replace(/]*>/gi, (tag) => { + if (!/\bsrc="\/_next\/static\/chunks\/[^"<>]+\.js"/.test(tag)) return tag; + if (!/\sasync(?:="")?(?=\s|>)/.test(tag)) return tag; + return tag.replace(/\sasync(?:="")?(?=\s|>)/, ' defer'); + }) + ); +} diff --git a/next.config.ts b/next.config.ts index d6aaa02..602cd2f 100644 --- a/next.config.ts +++ b/next.config.ts @@ -30,9 +30,9 @@ const securityHeaders = [ ]; const nextConfig: NextConfig = { - // Include styles in dynamic responses too: the post-build inliner only - // reaches prerendered HTML, so /pricing otherwise waits on external CSS. - experimental: { inlineCss: true }, + // Complete stylesheets remain render-blocking on dynamic pages. Static + // pages also retain the post-build critical-CSS inliner. Next's inlineCss + // duplicates our full application stylesheet in the initial Flight payload. // This repository lives below Fleet's root lockfile. Keep standalone output // rooted here so OpenNext finds `.next/standalone/.next` consistently. outputFileTracingRoot: process.cwd(), diff --git a/src/__tests__/account-menu.test.tsx b/src/__tests__/account-menu.test.tsx index b8be116..efac77c 100644 --- a/src/__tests__/account-menu.test.tsx +++ b/src/__tests__/account-menu.test.tsx @@ -17,7 +17,7 @@ it('places settings inside the signed-in account menu without a purchase link', const user = userEvent.setup(); render(); expect(screen.queryByRole('link', { name: 'Account settings' })).not.toBeInTheDocument(); - await user.click(screen.getByRole('button', { name: 'Account menu' })); + await user.click(await screen.findByRole('button', { name: 'Account menu' })); expect(screen.getByRole('menuitem', { name: 'Account settings' })).toHaveAttribute( 'href', '/settings' diff --git a/src/app/globals.css b/src/app/globals.css index 43133e6..e80a98a 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -1,7 +1,6 @@ @import "tailwindcss" source("../"); @import "./precision.css"; @import "./readability.css"; -@import "./workspace.css"; :root { color-scheme: dark; diff --git a/src/components/signed-in-user-menu.tsx b/src/components/signed-in-user-menu.tsx new file mode 100644 index 0000000..39508f4 --- /dev/null +++ b/src/components/signed-in-user-menu.tsx @@ -0,0 +1,84 @@ +'use client'; + +import { ChevronsUpDown, LogOut, Settings } from 'lucide-react'; +import Link from 'next/link'; +import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'; +import { Button } from '@/components/ui/button'; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from '@/components/ui/dropdown-menu'; +import { SidebarMenu, SidebarMenuButton, SidebarMenuItem } from '@/components/ui/sidebar'; +import { authClient } from '@/lib/auth-client'; + +export function SignedInUserMenu({ + user, + sidebar, +}: { + user: typeof authClient.$Infer.Session.user; + sidebar: boolean; +}) { + const { name, email, image } = user; + const avatar = ( + + + + {(name ?? '?') + .split(/\s+/) + .map((part) => part[0]) + .slice(0, 2) + .join('')} + + + ); + const menu = ( + + + {sidebar ? ( + + {avatar} + + {name} + Personal account + + + + ) : ( + + )} + + + {email} + + + + + Account settings + + + + { + authClient.signOut(); + }} + > + + Sign out + + + + ); + return sidebar ? ( + + {menu} + + ) : ( + menu + ); +} diff --git a/src/components/user-menu.tsx b/src/components/user-menu.tsx index dea44f8..a5e15f6 100644 --- a/src/components/user-menu.tsx +++ b/src/components/user-menu.tsx @@ -1,21 +1,16 @@ 'use client'; -import { ChevronsUpDown, LogOut, Settings } from 'lucide-react'; -import Link from 'next/link'; -import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'; +import dynamic from 'next/dynamic'; +import { Suspense } from 'react'; import { Button } from '@/components/ui/button'; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuLabel, - DropdownMenuSeparator, - DropdownMenuTrigger, -} from '@/components/ui/dropdown-menu'; -import { SidebarMenu, SidebarMenuButton, SidebarMenuItem } from '@/components/ui/sidebar'; import { authClient } from '@/lib/auth-client'; import { captureAuthFailure } from '@/lib/foundry-monitoring'; +// Guest navigation does not need the authenticated dropdown or sidebar primitives. +const SignedInUserMenu = dynamic(() => + import('./signed-in-user-menu').then((module) => module.SignedInUserMenu) +); + export function UserMenu({ sidebar = false }: { sidebar?: boolean }) { const { data: session, isPending } = authClient.useSession(); if (!session?.user) { @@ -48,63 +43,27 @@ export function UserMenu({ sidebar = false }: { sidebar?: boolean }) { ); } - const { name, email, image } = session.user; - const avatar = ( - - - - {(name ?? '?') - .split(/\s+/) - .map((part) => part[0]) - .slice(0, 2) - .join('')} - - - ); - const menu = ( - - - {sidebar ? ( - - {avatar} - - {name} - Personal account - - - - ) : ( - - )} - - - {email} - - - - - Account settings - - - - { - authClient.signOut(); - }} + return ( + - - Sign out - - - - ); - return sidebar ? ( - - {menu} - - ) : ( - menu + + {(session.user.name ?? '?') + .split(/\s+/) + .map((part) => part[0]) + .slice(0, 2) + .join('')} + + + } + > + + ); } diff --git a/src/components/workspace-interior.tsx b/src/components/workspace-interior.tsx index a0e641a..72482c0 100644 --- a/src/components/workspace-interior.tsx +++ b/src/components/workspace-interior.tsx @@ -1,5 +1,7 @@ 'use client'; +import '@/app/workspace.css'; + import { BriefcaseBusiness, FileText } from 'lucide-react'; import Link from 'next/link'; import { useMemo, useState, type ReactNode } from 'react'; diff --git a/worker.mjs b/worker.mjs index f04a216..6cd42ba 100644 --- a/worker.mjs +++ b/worker.mjs @@ -12,7 +12,7 @@ // All non-GET, non-`/` requests pass straight through to OpenNext. import openNext from './.open-next/worker.js'; -import { ensureLeadingHtmlDoctype } from './html-document.mjs'; +import { deferPublicDocumentScripts, ensureLeadingHtmlDoctype } from './html-document.mjs'; import { withTiming } from './timing.mjs'; import { CONTENT_SECURITY_POLICY } from './security-policy.mjs'; import { handleAgentEdge } from './agent-edge.mjs'; @@ -286,7 +286,10 @@ export default { // silently dropping the inlined critical-CSS chunk somewhere in the // stream-fork; reading once and constructing both responses from // the same Uint8Array sidesteps the streaming edge case entirely. - const body = ensureLeadingHtmlDoctype(await response.arrayBuffer()); + const body = deferPublicDocumentScripts( + ensureLeadingHtmlDoctype(await response.arrayBuffer()), + url.pathname + ); const headers = new Headers(response.headers); headers.set('Cache-Control', DOCUMENT_EDGE_CACHE_CONTROL); From 8f99256355c556e251cd27477c75c7a15acfc9ca Mon Sep 17 00:00:00 2001 From: Sarthak Agrawal Date: Tue, 13 Oct 2026 15:16:58 +0000 Subject: [PATCH 16/20] Retain native Next async scheduling alongside public dependency reductions --- __tests__/html-document.test.ts | 27 +------------------ .../0016-public-document-rendering.md | 10 +++---- html-document.mjs | 12 --------- worker.mjs | 7 ++--- 4 files changed, 7 insertions(+), 49 deletions(-) diff --git a/__tests__/html-document.test.ts b/__tests__/html-document.test.ts index f20a22d..42b9fb8 100644 --- a/__tests__/html-document.test.ts +++ b/__tests__/html-document.test.ts @@ -1,6 +1,6 @@ // @vitest-environment node import { describe, expect, it } from 'vitest'; -import { deferPublicDocumentScripts, ensureLeadingHtmlDoctype } from '../html-document.mjs'; +import { ensureLeadingHtmlDoctype } from '../html-document.mjs'; const encoder = new TextEncoder(); const decode = (body: Uint8Array) => new TextDecoder().decode(body); @@ -24,28 +24,3 @@ describe('anonymous cached HTML standards mode', () => { } }); }); - -describe('public document hydration scheduling', () => { - it('defers only async Next chunks and preserves inline Flight, content and other scripts', () => { - const html = - '

Résumé

' + - '' + - '' + - '' + - '' + - ''; - for (const route of ['/pricing', '/tools']) { - expect(decode(deferPublicDocumentScripts(encoder.encode(html), route))).toBe( - html - .replace('runtime.js" async=""', 'runtime.js" defer') - .replace('client.js" async', 'client.js" defer') - ); - } - }); - it('leaves every other route byte-identical', () => { - const body = encoder.encode(''); - for (const route of ['/', '/jobs', '/resume-builder', '/tools/keywords', '/pricing/other']) { - expect(deferPublicDocumentScripts(body, route)).toBe(body); - } - }); -}); diff --git a/docs/architecture/decisions/0016-public-document-rendering.md b/docs/architecture/decisions/0016-public-document-rendering.md index 93773ed..0e5cb62 100644 --- a/docs/architecture/decisions/0016-public-document-rendering.md +++ b/docs/architecture/decisions/0016-public-document-rendering.md @@ -28,10 +28,8 @@ Keep the existing guest sign-in path and all authenticated menu actions. A disabled account placeholder reserves space while the account module loads; it exposes neither settings nor sign-out until those actions are ready. -For anonymous `/pricing` and `/tools` documents only, use native `defer` for -Next's async chunk scripts. URLs, inline Flight data and script order remain -intact. Auth-bearing requests, RSC responses and other routes retain their -existing Worker path. There are no timers or interaction-based hydration gates. +Retain Next's native async script scheduling, inline Flight data and Worker +cache/auth boundaries. There are no timers or interaction-based hydration gates. ## Consequences @@ -47,5 +45,5 @@ Responsive geometry and workspace theme portals must remain consistent. the full stylesheet inline preserves the measured document overhead. - Deferring every route or removing hydration: rejected because workspace, account and mutation flows need the existing Next runtime. -- Native `defer` alone: did not meet the pricing gate; dependency and stylesheet - reductions are necessary alongside scheduling. +- Native `defer`: did not consistently meet the gate and makes hydration wait + for stylesheets; keep the framework's original scheduling. diff --git a/html-document.mjs b/html-document.mjs index 5af0746..a93ac8d 100644 --- a/html-document.mjs +++ b/html-document.mjs @@ -17,15 +17,3 @@ export function ensureLeadingHtmlDoctype(body) { return result; } -/** Let SSR text paint before document-order Next hydration on public catalog pages. */ -export function deferPublicDocumentScripts(body, pathname) { - if (pathname !== '/pricing' && pathname !== '/tools') return body; - const html = new TextDecoder().decode(body); - return new TextEncoder().encode( - html.replace(/]*>/gi, (tag) => { - if (!/\bsrc="\/_next\/static\/chunks\/[^"<>]+\.js"/.test(tag)) return tag; - if (!/\sasync(?:="")?(?=\s|>)/.test(tag)) return tag; - return tag.replace(/\sasync(?:="")?(?=\s|>)/, ' defer'); - }) - ); -} diff --git a/worker.mjs b/worker.mjs index 6cd42ba..f04a216 100644 --- a/worker.mjs +++ b/worker.mjs @@ -12,7 +12,7 @@ // All non-GET, non-`/` requests pass straight through to OpenNext. import openNext from './.open-next/worker.js'; -import { deferPublicDocumentScripts, ensureLeadingHtmlDoctype } from './html-document.mjs'; +import { ensureLeadingHtmlDoctype } from './html-document.mjs'; import { withTiming } from './timing.mjs'; import { CONTENT_SECURITY_POLICY } from './security-policy.mjs'; import { handleAgentEdge } from './agent-edge.mjs'; @@ -286,10 +286,7 @@ export default { // silently dropping the inlined critical-CSS chunk somewhere in the // stream-fork; reading once and constructing both responses from // the same Uint8Array sidesteps the streaming edge case entirely. - const body = deferPublicDocumentScripts( - ensureLeadingHtmlDoctype(await response.arrayBuffer()), - url.pathname - ); + const body = ensureLeadingHtmlDoctype(await response.arrayBuffer()); const headers = new Headers(response.headers); headers.set('Cache-Control', DOCUMENT_EDGE_CACHE_CONTROL); From 3cf2f9357bc9e2317ea8b7ee156b6f194d3d3ee8 Mon Sep 17 00:00:00 2001 From: Sarthak Agrawal Date: Tue, 13 Oct 2026 15:17:50 +0000 Subject: [PATCH 17/20] Restore the unchanged document helper bytes --- html-document.mjs | 1 - 1 file changed, 1 deletion(-) diff --git a/html-document.mjs b/html-document.mjs index a93ac8d..ed2a0ce 100644 --- a/html-document.mjs +++ b/html-document.mjs @@ -16,4 +16,3 @@ export function ensureLeadingHtmlDoctype(body) { result.set(bytes.subarray(offset + declaration.length), offset + declaration.length); return result; } - From d7414e1711f91fae5a8502176108f5a66c645b5a Mon Sep 17 00:00:00 2001 From: Sarthak Agrawal Date: Tue, 13 Oct 2026 15:37:33 +0000 Subject: [PATCH 18/20] Load complete application utilities outside the public catalog --- .../0016-public-document-rendering.md | 4 +- src/app/application-theme.css | 25 +++++++++++ src/app/application-utilities.css | 7 ++++ src/app/globals.css | 42 ++++++++----------- src/components/application-styles.tsx | 7 ++++ src/components/signed-in-user-menu.tsx | 2 + src/components/workspace-shell.tsx | 19 ++++++--- 7 files changed, 75 insertions(+), 31 deletions(-) create mode 100644 src/app/application-theme.css create mode 100644 src/app/application-utilities.css create mode 100644 src/components/application-styles.tsx diff --git a/docs/architecture/decisions/0016-public-document-rendering.md b/docs/architecture/decisions/0016-public-document-rendering.md index 0e5cb62..9eee6d9 100644 --- a/docs/architecture/decisions/0016-public-document-rendering.md +++ b/docs/architecture/decisions/0016-public-document-rendering.md @@ -21,7 +21,9 @@ These costs compete with the public text rendering path tracked in Use Next's documented external-stylesheet mode. Keep the complete stylesheets render-blocking, and retain the existing critical-CSS build step for prerendered files. Import workspace-only styles from the already lazy workspace -interior. Their scoped rules and the public content remain unchanged. +interior. Generate eager utilities from catalog and shared navigation sources; +the complete application utility set loads on other routes and with the +authenticated menu. Their rules and the public content remain unchanged. Load the signed-in menu through `next/dynamic` after a session supplies a user. Keep the existing guest sign-in path and all authenticated menu actions. A diff --git a/src/app/application-theme.css b/src/app/application-theme.css new file mode 100644 index 0000000..238bbad --- /dev/null +++ b/src/app/application-theme.css @@ -0,0 +1,25 @@ +/* Shared utility aliases keep both stylesheet sets identical. */ +@theme inline { + --color-background: var(--background); + --color-foreground: var(--foreground); + --color-card: var(--card); + --color-card-foreground: var(--card-foreground); + --color-popover: var(--popover); + --color-popover-foreground: var(--popover-foreground); + --color-primary: var(--primary); + --color-primary-foreground: var(--primary-foreground); + --color-secondary: var(--secondary); + --color-secondary-foreground: var(--secondary-foreground); + --color-muted: var(--muted); + --color-muted-foreground: var(--muted-foreground); + --color-accent: var(--accent); + --color-accent-foreground: var(--accent-foreground); + --color-destructive: var(--destructive); + --color-destructive-foreground: var(--destructive-foreground); + --color-border: var(--border); + --color-input: var(--input); + --color-ring: var(--ring); + --font-sans: Arial, system-ui, sans-serif; + --font-serif: "Ibarra Real Nova", serif; + --font-mono: var(--font-geist-mono); +} diff --git a/src/app/application-utilities.css b/src/app/application-utilities.css new file mode 100644 index 0000000..ada42dd --- /dev/null +++ b/src/app/application-utilities.css @@ -0,0 +1,7 @@ +/* Complete utilities for every other route and the authenticated account UI. */ +@import "tailwindcss/theme.css" layer(theme); +@import "./application-theme.css"; +@layer utilities { + @tailwind utilities; +} +@source "../"; diff --git a/src/app/globals.css b/src/app/globals.css index e80a98a..568c082 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -1,6 +1,21 @@ -@import "tailwindcss" source("../"); +@import "tailwindcss" source(none); +@import "./application-theme.css"; @import "./precision.css"; @import "./readability.css"; +@source "./layout.tsx"; +@source "./error.tsx"; +@source "./global-error.tsx"; +@source "./not-found.tsx"; +@source "./pricing/page.tsx"; +@source "./tools/page.tsx"; +@source "../components/site-nav.tsx"; +@source "../components/rolepatch-mark.tsx"; +@source "../components/token-balance.tsx"; +@source "../components/user-menu.tsx"; +@source "../components/pricing-cards.tsx"; +@source "../components/saasmaker-feedback.tsx"; +@source "../components/saasmaker-feedback-widget.tsx"; +@source "../components/ui/button.tsx"; :root { color-scheme: dark; @@ -26,30 +41,7 @@ --ring: #e4e4e7; } -@theme inline { - --color-background: var(--background); - --color-foreground: var(--foreground); - --color-card: var(--card); - --color-card-foreground: var(--card-foreground); - --color-popover: var(--popover); - --color-popover-foreground: var(--popover-foreground); - --color-primary: var(--primary); - --color-primary-foreground: var(--primary-foreground); - --color-secondary: var(--secondary); - --color-secondary-foreground: var(--secondary-foreground); - --color-muted: var(--muted); - --color-muted-foreground: var(--muted-foreground); - --color-accent: var(--accent); - --color-accent-foreground: var(--accent-foreground); - --color-destructive: var(--destructive); - --color-destructive-foreground: var(--destructive-foreground); - --color-border: var(--border); - --color-input: var(--input); - --color-ring: var(--ring); - --font-sans: Arial, system-ui, sans-serif; - --font-serif: "Ibarra Real Nova", serif; - --font-mono: var(--font-geist-mono); -} + body { background: var(--background); diff --git a/src/components/application-styles.tsx b/src/components/application-styles.tsx new file mode 100644 index 0000000..ba502be --- /dev/null +++ b/src/components/application-styles.tsx @@ -0,0 +1,7 @@ +'use client'; + +import '@/app/application-utilities.css'; + +export function ApplicationStyles() { + return null; +} diff --git a/src/components/signed-in-user-menu.tsx b/src/components/signed-in-user-menu.tsx index 39508f4..d6f87b6 100644 --- a/src/components/signed-in-user-menu.tsx +++ b/src/components/signed-in-user-menu.tsx @@ -1,5 +1,7 @@ 'use client'; +import '@/app/application-utilities.css'; + import { ChevronsUpDown, LogOut, Settings } from 'lucide-react'; import Link from 'next/link'; import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'; diff --git a/src/components/workspace-shell.tsx b/src/components/workspace-shell.tsx index cd92a9c..994b056 100644 --- a/src/components/workspace-shell.tsx +++ b/src/components/workspace-shell.tsx @@ -9,18 +9,27 @@ import { SiteNav } from '@/components/site-nav'; const WorkspaceInterior = dynamic(() => import('./workspace-interior').then((module) => module.WorkspaceInterior) ); +const ApplicationStyles = dynamic(() => + import('./application-styles').then((module) => module.ApplicationStyles) +); export function WorkspaceShell({ children }: { children: ReactNode }) { const pathname = usePathname() ?? ''; const builder = pathname === '/resume-builder' || pathname.startsWith('/editor/'); const workspace = builder || pathname === '/jobs' || pathname === '/dashboard' || pathname.startsWith('/tailor/'); - return workspace ? ( - {children} - ) : ( + const catalog = pathname === '/pricing' || pathname === '/tools'; + return ( <> - - {children} + {!catalog && } + {workspace ? ( + {children} + ) : ( + <> + + {children} + + )} ); } From 7f494a52d4730da493518ca619e057e552cfa19c Mon Sep 17 00:00:00 2001 From: Sarthak Agrawal Date: Wed, 14 Oct 2026 01:59:00 +0530 Subject: [PATCH 19/20] fix: preserve workspace palette in lazy account UI --- e2e/public-performance.spec.ts | 11 +++++++++++ src/app/application-theme.css | 16 ++++++++++++++++ src/components/signed-in-user-menu.tsx | 1 + 3 files changed, 28 insertions(+) diff --git a/e2e/public-performance.spec.ts b/e2e/public-performance.spec.ts index ebbdaeb..c923294 100644 --- a/e2e/public-performance.spec.ts +++ b/e2e/public-performance.spec.ts @@ -3,6 +3,10 @@ import { expect, test } from '@playwright/test'; test('account menu becomes usable after a synthetic session loads its account-only code', async ({ page, }) => { + await page.emulateMedia({ colorScheme: 'light' }); + await page.addInitScript(() => { + localStorage.setItem('rolepatch-workspace-theme', 'light'); + }); const synthetic = { user: { id: 'public-performance-fixture', @@ -27,9 +31,16 @@ test('account menu becomes usable after a synthetic session loads its account-on if (path === '/resume-builder' && (page.viewportSize()?.width ?? 0) < 768) { await page.getByRole('button', { name: 'Toggle Sidebar', exact: true }).click(); } + if (path === '/resume-builder') { + await expect(page.locator('[data-sidebar="sidebar"]')).toHaveCSS( + 'background-color', + 'oklch(0.985 0 0)' + ); + } const account = page.getByRole('button', { name: 'Account menu', exact: true }); await expect(account).toBeEnabled(); await account.click(); + await expect(page.getByRole('menu')).toHaveCSS('background-color', 'oklch(1 0 0)'); await expect(page.getByRole('menuitem', { name: 'Account settings' })).toHaveAttribute( 'href', '/settings' diff --git a/src/app/application-theme.css b/src/app/application-theme.css index 238bbad..b9a0cc9 100644 --- a/src/app/application-theme.css +++ b/src/app/application-theme.css @@ -19,6 +19,22 @@ --color-border: var(--border); --color-input: var(--input); --color-ring: var(--ring); + --color-chart-1: var(--chart-1); + --color-chart-2: var(--chart-2); + --color-chart-3: var(--chart-3); + --color-chart-4: var(--chart-4); + --color-chart-5: var(--chart-5); + --color-sidebar: var(--sidebar); + --color-sidebar-foreground: var(--sidebar-foreground); + --color-sidebar-primary: var(--sidebar-primary); + --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); + --color-sidebar-accent: var(--sidebar-accent); + --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); + --color-sidebar-border: var(--sidebar-border); + --color-sidebar-ring: var(--sidebar-ring); + --radius-lg: var(--radius); + --radius-md: calc(var(--radius) - 2px); + --radius-sm: calc(var(--radius) - 4px); --font-sans: Arial, system-ui, sans-serif; --font-serif: "Ibarra Real Nova", serif; --font-mono: var(--font-geist-mono); diff --git a/src/components/signed-in-user-menu.tsx b/src/components/signed-in-user-menu.tsx index d6f87b6..cdb4c06 100644 --- a/src/components/signed-in-user-menu.tsx +++ b/src/components/signed-in-user-menu.tsx @@ -1,6 +1,7 @@ 'use client'; import '@/app/application-utilities.css'; +import '@/app/workspace.css'; import { ChevronsUpDown, LogOut, Settings } from 'lucide-react'; import Link from 'next/link'; From 0703989cce1dba97694c4b775a330dea87005c7c Mon Sep 17 00:00:00 2001 From: Sarthak Agrawal Date: Wed, 14 Oct 2026 02:04:37 +0530 Subject: [PATCH 20/20] refactor: use shared workspace utility aliases --- src/app/workspace.css | 36 ------------------------------------ 1 file changed, 36 deletions(-) diff --git a/src/app/workspace.css b/src/app/workspace.css index 9bdc597..7e62403 100644 --- a/src/app/workspace.css +++ b/src/app/workspace.css @@ -37,42 +37,6 @@ color: var(--foreground); color-scheme: light; } -@theme inline { - --color-background: var(--background); - --color-foreground: var(--foreground); - --color-card: var(--card); - --color-card-foreground: var(--card-foreground); - --color-popover: var(--popover); - --color-popover-foreground: var(--popover-foreground); - --color-primary: var(--primary); - --color-primary-foreground: var(--primary-foreground); - --color-secondary: var(--secondary); - --color-secondary-foreground: var(--secondary-foreground); - --color-muted: var(--muted); - --color-muted-foreground: var(--muted-foreground); - --color-accent: var(--accent); - --color-accent-foreground: var(--accent-foreground); - --color-destructive: var(--destructive); - --color-border: var(--border); - --color-input: var(--input); - --color-ring: var(--ring); - --color-chart-1: var(--chart-1); - --color-chart-2: var(--chart-2); - --color-chart-3: var(--chart-3); - --color-chart-4: var(--chart-4); - --color-chart-5: var(--chart-5); - --color-sidebar: var(--sidebar); - --color-sidebar-foreground: var(--sidebar-foreground); - --color-sidebar-primary: var(--sidebar-primary); - --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); - --color-sidebar-accent: var(--sidebar-accent); - --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); - --color-sidebar-border: var(--sidebar-border); - --color-sidebar-ring: var(--sidebar-ring); - --radius-lg: var(--radius); - --radius-md: calc(var(--radius) - 2px); - --radius-sm: calc(var(--radius) - 4px); -} .rolepatch-workspace main { animation: none; } .rolepatch-workspace [id] { scroll-margin-top: 1.5rem; } @layer base {