feat: share/og pipeline, history insights, scanner UX upgrades#10
Merged
sunnypatell merged 28 commits intomainfrom Apr 25, 2026
Merged
feat: share/og pipeline, history insights, scanner UX upgrades#10sunnypatell merged 28 commits intomainfrom
sunnypatell merged 28 commits intomainfrom
Conversation
- keyed by sha-256 of the full prompt so template edits auto-bust entries - lru eviction at 200 entries, 24h ttl per entry - cache check sits behind the rate limiter so abuse protection still applies - response now includes _cached flag (additive, non-breaking) - per-region in-memory; cold starts lose it. swap to vercel kv later if needed
- robots.txt allows crawlers, blocks /api, /history, /login - sitemap lists public routes (/, /scanner, /about) with lastmod, changefreq, priority - both routes derive origin from request, so they auto-track preview/prod domains - robots.txt prerendered for cdn-friendly delivery
- HSTS (1y, includeSubDomains, preload), Referrer-Policy, Permissions-Policy - X-Content-Type-Options nosniff, X-Frame-Options DENY - applied as defaults so per-route headers (e.g. /api/analyze) take precedence - covers both regular routes and the docs static-serving path
- scoreLLM accepts an external AbortSignal and returns a discriminated result so callers distinguish 'cancelled' from 'error' (deterministic fallback no longer kicks in when the user cancels mid-flight) - scoresStore tracks an AbortController; startScoring aborts any prior scan and returns the signal; reset/setError also abort cleanly - scanner page threads the signal through and bails silently on cancel, preventing the stale-results race when re-scanning during an LLM call
…try-After - Retry-After header now matches the actual reset window (not hardcoded 60s) - response body adds retryAfter (seconds) and a clearer reason in the error message - check both windows before incrementing; previously a daily-limit failure still consumed a minute slot, double-charging the user - discriminated return type from checkRateLimit for cleaner call-site handling
- scoreLLM now distinguishes 429 from generic error and parses both Retry-After header and the response body's retryAfter field - scoresStore tracks an absolute llmRetryAtMs timestamp so the countdown stays accurate across re-renders - ScoreDashboard ticks once per second only while the toast is visible and a retry timestamp is set; the line auto-clears when it hits 0
- classification.test.ts locks down score-tier boundaries (>= not >), label/color mapping, and behavior on negative inputs - fallback.test.ts covers shape, experience-level/education/role-type detection, and the four suggestion-trigger paths - 32 new tests, all green; total now 138 across 16 files
- cache.ts owns the LRU result cache (hashPrompt, getCached, setCached) - rate-limiter.ts owns the per-IP minute/day windows and the discriminated result - +server.ts shrinks from ~360 to ~210 lines and reads as orchestration - behavior unchanged; module-level state preserved by keeping Maps inside the new files co-located tests in tests/unit/api/ cover hash determinism/length, cache round-trip and overwrite, and rate-limit boundaries (MAX_RPM exactly, inter-IP independence, retry-after shape). 17 new tests, all green.
- new comparison.ts pure function returns per-platform deltas, average
delta, passing-count delta, and improved/regressed/unchanged tallies
- ScoreDashboard renders a colored band ("score went from 67 to 78 +11")
with up/down/flat states, only on fresh scans for authenticated users
with prior history; suppressed when viewing a history snapshot
- store snapshots scanHistory[0] at startScoring time into
previousScanForComparison so the band stays correct during the brief
race between finishScoring (results visible) and the async
saveToHistory -> loadHistory cycle
- 6 unit tests cover empty inputs, mixed deltas, sign handling, missing
platforms, and stable platform ordering. 161 tests total, all green.
- new SeoHead component emits title/description/canonical/og/twitter on every public route, with og:url and canonical derived from the request origin so preview deployments bind to their own URLs automatically - removed the static og/twitter block from app.html since per-route tags would otherwise render duplicates - landing page also emits SoftwareApplication json-ld (with offers price 0 to mark it free) for rich-result eligibility on Google - json-ld output escapes '<' to its unicode form so a future user- controlled field can never close the surrounding <script> tag dry-tested: dev server up, curl'd /, /scanner, /about - each shows distinct og:title/description/url, twitter:* tags, canonical link, no duplicates. ld+json parsed cleanly via node. typecheck + lint + 161 tests all green. dev server stopped, working tree clean.
… for it - $lib/firebase exports a memoized getFirebase() that dynamic-imports firebase/app, firebase/auth, firebase/firestore on first use; type-only Auth/Firestore imports are erased at compile time so they don't bundle - authStore, scoresStore, and Hero all updated to await getFirebase() before touching auth/db; method-level dynamic imports keep each method pulling only the firestore symbols it needs - before: node 0 (root layout = every route) eagerly statically imported the firebase chunk (~488kb min). after: zero firebase references in any node bundle - the chunk only ships when a consumer code path runs dry-tested: typecheck + lint + 161 tests green; production build clean; post-build inspection of nodes 0-7 shows firebase-imports=0 on all and firebase-strings=0 except node 3 which has the literal word in the about-page tech-stack list (not a runtime dep). dev server boots, /, /scanner, /about all return 200. dev server stopped, working tree clean.
- src/routes/+error.svelte: branded error page handles 404/429/5xx and
surfaces $page.error.message for client errors; matches the dark
glassmorphic theme and reuses SeoHead with noIndex
- src/routes/healthz/+server.ts: liveness probe for uptime checkers;
returns {status,timestamp} with no-store and the standard headers
- src/routes/api/csp-report/+server.ts: receives violation reports and
console.warns them so vercel log aggregation surfaces issues without
touching firestore or any paid tier
- hooks.server.ts: emits Content-Security-Policy-Report-Only with a
policy sized for sveltekit hydration, google fonts, firebase auth +
firestore, and the LLM proxies; report-uri points at /api/csp-report.
the csp-report endpoint itself is excluded from CSP injection to
avoid feedback loops on its own error responses
dry-tested: typecheck + lint + 161 tests; dev server probed with curl:
- /healthz -> 200 with valid json + headers
- bogus path -> 404 rendering branded error card
- / -> Content-Security-Policy-Report-Only present
- /api/csp-report POST -> 204 with no csp header on its own response
each history entry shows a small +N/-N pill next to the average score, computed against the chronologically prior scan (history is newest-first so history[i+1] is the previous one). only renders when there's a non-zero delta and a prior scan exists. zero-allocation reuse of the existing scoresStore.history derivation.
when the comparison band shows a positive delta, render a small "Share +N" pill that opens twitter intent with pre-filled text referencing the user's actual delta and the site URL. origin pulled from window.location so preview deploys share their own URL. self-contained, no new deps, no infra. only renders when delta > 0 so users who tied or regressed don't see a (false) "share" prompt.
…rison rate-limiter: at >MAX_MAP_SIZE (10k) the daily map sits above threshold continuously, and the old code ran the O(n) cleanup on every request. throttled to at most once per CLEANUP_INTERVAL_MS (30s) so cleanup cost is bounded regardless of map size. matters past ~10k unique daily IPs. scores store: a rapid re-scan before the previous saveToHistory's async loadHistory completes left scanHistory[0] stale, so the snapshot used by the comparison band pointed at the scan BEFORE the immediate previous one. fixed by preferring the currently-visible results (in-memory truth) over scanHistory[0] when computing the snapshot at startScoring.
@vercel/og + an edge route at /api/og generates per-share PNG cards (score / verdict / passing-count, optionally a +N or -N delta pill) from query params. element tree built as plain objects so we don't need JSX in the project. new /share route reads the same query params, renders a polished landing card, and emits og:image pointing at /api/og with the same query - linkedin/twitter previews show the user's actual score. SeoHead's noIndex isn't applied here so crawlers DO index the page. dry-tested: dev server boots; curl /api/og returns image/png with valid PNG magic bytes; /share emits og:image=/api/og?... in head. @vercel/og's default cache-control (no-cache, no-store) is left as-is since user-specific share URLs should refetch when params change. ShareBadge wiring is a follow-up - this iteration ships the infra.
ShareBadge: - shareUrl derives /share?score=X&pass=Y&total=Z from window.location - new "Copy share link" button (Clipboard API with execCommand fallback for insecure origins), shows "Link copied" for 1.8s on success - shareToLinkedIn now appends shareUrl to the post text so linkedin's crawler fetches /share and renders the dynamic OG card in the preview ScoreDashboard: - improvement twitter-intent button now points at /share?score=...&delta=... rather than the homepage, so twitter's preview shows the actual delta via the /api/og PNG instead of a generic site card
symmetry with the linkedin button. uses twitter intent with the same /share URL so the preview card shows the dynamic OG (per-share PNG with the user's actual score), not a generic site preview. shareText is too long for X's 280-char cap so a tighter version is built inline; URL is appended via intent's url param.
login: - normalizeEmail trims and lowercases on submit so "User@Example.com" and "user@example.com" land on the same firebase account instead of silently creating duplicates - displayName trimmed and capped at 80 chars on submit; input gets maxlength=80 and autocomplete=name SeoHead conversions: - /login: indexable (entry point for "ats screener login" searches), with proper canonical / og / twitter cards - /history: noIndex (auth-gated, would 404 to crawlers anyway), so search engines don't waste crawl budget hitting redirect-to-login
JD preview (in JobDescriptionInput): - as the user types, the parser runs on a 400ms debounce and shows detected role/industry/experience-level chips plus the top 12 extracted skills as chips - when a resume is loaded, matched skills get a green check and the preview shows "X of Y in your resume" - real-time signal of how well this resume aligns with this JD before the user even hits scan - parser is dynamically imported so compromise/skills-taxonomy don't ship in the layout chunk for landing-page visitors build fix (CRITICAL - this would fail on Vercel): - /api/og had runtime: 'edge' which caused esbuild to bundle hooks + root.js for the edge runtime. that bundle includes references to node:crypto / node:fs / node:path that edge can't resolve, so the whole production build broke - runtime: 'edge' is also deprecated in adapter-vercel as of recent versions - moved /api/og to default node runtime (which @vercel/og >=0.6 supports) and added maxDuration: 30 - moved docs-serving from hooks.server.ts to a /docs/[...slug]/+server.ts catchall, so fs/path live in a node-runtime route instead of polluting the shared bundle dry-tested: pnpm build:app succeeds; dev server boots, /api/og returns valid PNG (magic bytes verified), /share emits correct og:image, /docs/[...slug] catchall returns 404 when no doc exists. typecheck + lint + 161 tests all green.
ScoreCard now accepts an optional previousScore prop and renders a small +N/-N pill anchored at the bottom-right of the score ring when the platform's score changed since the prior scan. ScoreDashboard builds a Map<system, previousScore> from comparison.platforms once and threads it through, so each card knows its own delta without recomputing. a11y polish: aria-expanded added to the three big disclosure toggles (ScoreBreakdown system row, JobDescriptionInput "Add JD", ScanHistory list) so screen readers announce the open/closed state correctly.
new $engine/suggestions/templates module classifies a suggestion's free-text summary into one of seven categories (quantification, action-verbs, missing-keywords, skills-section, short-resume, sections, format) via regex patterns and attaches a concrete tip + before/after example. pure functions, deterministic, no LLM call. ScoreDashboard's expanded suggestion view now renders the example inline as a side-by-side red/green diff pair so the user sees exactly what to change instead of just being told something is wrong. collapses to a single column under 640px. 11 unit tests in tests/unit/suggestions/templates.test.ts cover the classifier (per-category cues + ambiguous-text null) and the example shape (every type maps to a fully-populated record). 172 tests total.
new $engine/scorer/timeline.ts contains the pure geometry helper that takes the scan history (any order), sorts it ascending by timestamp, and returns evenly-spaced points + a path D string + an area D string for a left-to-right trajectory chart. inverted y so higher scores render visually higher; honors a custom viewBox. new ScoreTimeline component is a self-contained SVG chart - no chart library dep. each point renders a colored dot using the same tier palette as the dashboard. pointer-move snaps to the nearest dot and shows a tooltip with score / mode / file / date. tooltip flips left of the dot when near the right edge to avoid clipping. /history embeds the chart above the grid when >=2 scans exist; below that threshold it stays hidden. 7 unit tests cover sort order, y inversion, path shape, area closing, point clamping, and viewBox override. 179 tests total.
@vercel/og's ImageResponse hardcodes Cache-Control: no-cache, no-store and the constructor's headers option only CONCATENATES rather than replaces (verified locally - the response shipped with both directives contradicting each other). re-wrapping the rendered bytes in a fresh Response is the only way to actually set the header. new policy: public, max-age=3600 (browser 1h), s-maxage=86400 (vercel cdn 1d), stale-while-revalidate=604800 (7d background refresh), immutable. since the URL is fully content-addressed by score+pass+ total+delta, every unique share combination caches forever - repeat hits from linkedin/twitter crawlers and individual visitors all serve from the edge with zero function invocation. real cost protection that scales linearly with NEW share combos rather than total clicks. dry-tested via curl: cache-control header now contains exactly the intended directives, no contradictions. typecheck + build clean.
ScoreDashboard: - "Copy" pill on each before/after example block (uses Clipboard API with execCommand fallback for insecure origins). copiedKey is per-block so multiple copies show their own "copied" state without trampling each other. e.stopPropagation prevents the copy click bubbling to the surrounding suggestion-card and collapsing the block. role=button + tabindex=0 + keydown(Enter|Space) because the outer card is already a <button> and html disallows nested buttons /history: - new $engine/scorer/journey.computeJourneyStats pure function derives total improvement, best score, scan count, strongest-gain platform, and days-span from the user's history (sorted ascending, per-platform delta only when both first and latest have it) - inline stats card renders above the timeline when >=2 scans exist; big numbers, color-coded for positive/negative 4 unit tests cover null/empty input, single-scan no-delta, two-scan delta + best-platform, and input-order independence. 183 tests total.
ScoreBreakdown rendered `<li>{suggestion}</li>` directly. when the LLM
returned StructuredSuggestion objects (with summary/details/impact/
platforms) instead of plain strings, svelte's text interpolation fell
back to String(obj) and shipped "[object Object]" to the page. fixed
with type-narrowing helpers (same pattern report.ts and ScoreDashboard
already use); structured suggestions now show their summary plus a
nested list of their details.
ScoreDashboard had the same class of bug in its non-deduplicated path
at the !structured fallback (`<p>{suggestion}</p>`). hardened with a
typeof guard so any malformed value renders empty rather than
"[object Object]".
JobDescriptionInput's debounced parser ran inside a fire-and-forget
async IIFE with no .catch(); a transient parser failure would surface
as an unhandled promise rejection. wrapped in try/catch with a
console.warn so the scan flow stays unaffected.
docs:
- api/rate-limits: documents the new 429 body shape (retryAfter field
+ reason discriminator) and the Retry-After header
- api/endpoints: documents the _cached response field, the in-memory
cache behavior, and lists the new auxiliary endpoints (/healthz,
/robots.txt, /sitemap.xml, /api/og, /share, /api/csp-report)
- pnpm build:docs run; static/docs refreshed
dry-tested via curl: /healthz, /robots.txt, /sitemap.xml, /api/og
(magic bytes valid + correct cache-control), /share, rate limiter
(11th request 429 with retry-after: 60), and a full LLM cache-hit
round-trip (46s -> 81ms, 573x). 183 tests + lint + typecheck + build
all green.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
There was a problem hiding this comment.
Pull request overview
This PR adds share/OG infrastructure and dashboard “progress over time” UX on top of the existing scanner, while also improving operational behavior (rate limiting, caching) and bundle size (lazy-loaded Firebase). It also addresses the structured-suggestion rendering bug ([object Object]) via type narrowing in the UI.
Changes:
- Adds dynamic sharing pipeline:
/sharelanding page +/api/ogPNG generator (cached) + expanded ShareBadge actions. - Adds history/comparison UX: scan comparison deltas, per-platform delta pills,
/historyjourney stats + timeline chart. - Improves resilience/cost/perf: in-memory prompt-result cache for
/api/analyze, improved rate-limiter shape/headers, lazy-loaded Firebase SDK, baseline security headers + CSP reporting.
Reviewed changes
Copilot reviewed 46 out of 47 changed files in this pull request and generated 9 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/unit/suggestions/templates.test.ts | Unit tests for suggestion classification + before/after examples mapping. |
| tests/unit/scorer/timeline.test.ts | Unit tests for timeline SVG geometry helper. |
| tests/unit/scorer/journey.test.ts | Unit tests for cross-scan journey stats computation. |
| tests/unit/scorer/comparison.test.ts | Unit tests for scan-to-scan comparison/deltas. |
| tests/unit/scorer/classification.test.ts | Unit tests for score tier classification/labels/colors. |
| tests/unit/llm/fallback.test.ts | Unit tests for deterministic fallback analysis behavior. |
| tests/unit/api/rate-limiter.test.ts | Unit tests for new discriminated rate limiter result + retryAfterSec. |
| tests/unit/api/cache.test.ts | Unit tests for SHA-256 hashing and in-memory LRU cache behavior. |
| src/routes/sitemap.xml/+server.ts | Adds dynamic sitemap.xml endpoint for public routes. |
| src/routes/share/+page.svelte | Adds branded share landing page emitting dynamic OG image URL. |
| src/routes/scanner/+page.svelte | Adds abortable scoring flow + migrates to SeoHead. |
| src/routes/robots.txt/+server.ts | Adds dynamic robots.txt endpoint (but currently marked prerender). |
| src/routes/login/+page.svelte | Adds email normalization + displayName constraints + SeoHead. |
| src/routes/history/+page.svelte | Adds journey stats + timeline component + noindex SEO. |
| src/routes/healthz/+server.ts | Adds JSON liveness probe endpoint. |
| src/routes/docs/[...slug]/+server.ts | Adds node-runtime docs catchall server for directory-style docs URLs. |
| src/routes/api/og/+server.ts | Adds dynamic OG PNG endpoint using @vercel/og with caching headers. |
| src/routes/api/csp-report/+server.ts | Adds CSP report receiver endpoint logging to server logs. |
| src/routes/api/analyze/rate-limiter.ts | Extracts and improves per-IP rate limiting (retryAfterSec + throttled cleanup). |
| src/routes/api/analyze/cache.ts | Adds in-memory SHA-256 keyed LRU cache for analyze results. |
| src/routes/api/analyze/+server.ts | Integrates rate limiting + cache; adds Retry-After + _cached flag. |
| src/routes/about/+page.svelte | Migrates About route metadata to SeoHead. |
| src/routes/+page.svelte | Adds SeoHead + JSON-LD SoftwareApplication schema. |
| src/routes/+error.svelte | Adds branded error page with noindex SEO. |
| src/lib/stores/scores.svelte.ts | Adds abortable scoring, retry countdown plumbing, history snapshot for comparisons, lazy Firestore imports. |
| src/lib/stores/auth.svelte.ts | Lazily imports Firebase auth + defers auth listener setup. |
| src/lib/firebase.ts | Introduces getFirebase() lazy initialization wrapper for Firebase SDK. |
| src/lib/engine/suggestions/templates.ts | Adds deterministic suggestion classification + before/after example templates. |
| src/lib/engine/scorer/timeline.ts | Adds testable SVG timeline geometry helper. |
| src/lib/engine/scorer/journey.ts | Adds journey stats helper across scan history. |
| src/lib/engine/scorer/comparison.ts | Adds scan comparison helper to compute deltas and counts. |
| src/lib/engine/llm/client.ts | Adds cancellable LLM scoring + explicit status results + rate-limit retry hint. |
| src/lib/components/upload/JobDescriptionInput.svelte | Adds debounced live JD parsing preview + a11y aria-expanded. |
| src/lib/components/seo/SeoHead.svelte | New component to emit per-route SEO/OG/Twitter/canonical metadata. |
| src/lib/components/scoring/ShareBadge.svelte | Adds share URL building + copy link + X share + LinkedIn text includes /share. |
| src/lib/components/scoring/ScoreTimeline.svelte | Adds interactive SVG timeline chart with hover tooltip. |
| src/lib/components/scoring/ScoreDashboard.svelte | Adds comparison band, per-card deltas, suggestion templates + copy buttons, retry countdown UI. |
| src/lib/components/scoring/ScoreCard.svelte | Adds per-platform delta pill display. |
| src/lib/components/scoring/ScoreBreakdown.svelte | Fixes [object Object] via structured-suggestion narrowing + details rendering. |
| src/lib/components/scoring/ScanHistory.svelte | Adds per-row delta pills + aria-expanded on toggle. |
| src/lib/components/landing/Hero.svelte | Lazily loads Firestore for user count on landing page. |
| src/hooks.server.ts | Removes docs serving from hooks; adds baseline security headers + CSP report-only header injection. |
| src/app.html | Removes global OG/twitter/title/description; notes SeoHead is responsible. |
| pnpm-lock.yaml | Locks new dependency tree including @vercel/og. |
| package.json | Adds @vercel/og dependency. |
| docs/src/content/docs/api/rate-limits.md | Documents new 429 response shape + Retry-After usage. |
| docs/src/content/docs/api/endpoints.md | Documents _cached and new auxiliary endpoints. |
Files not reviewed (1)
- pnpm-lock.yaml: Language not supported
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
copilot review (PR #10): - ScoreDashboard: change outer suggestion-card from <button> to <div role=button> so the inner copy buttons are valid HTML, restore real <button>s for the copy controls, drop the unsafe KeyboardEvent->MouseEvent cast (widen copyExample to Event) - ScoreDashboard: typeof guard on the !structured suggestion fallback so a malformed value renders empty instead of "[object Object]" - /api/og + /share: clamp pass to <= total so a tampered URL like ?pass=6&total=1 cannot render "6 of 1 ATS systems passed" - timeline.ts: clamp averageScore to [0, 100] before computing y so out-of-range scores still render inside the chart padding box - robots.txt: drop prerender so the Sitemap URL tracks the request origin on preview deploys - docs catchall: explicit path-traversal guard via resolve + startsWith on docsRoot, plus realpath check against symlink escapes - hooks: clarify CSP comment (browsers ignore CSP on non-document contexts, header is benign on json/image responses) - app.html: drop the static <meta name=robots>; SeoHead now emits a single robots tag per route (indexable or noindex) so noIndex pages no longer ship conflicting directives - migrate $app/stores -> $app/state across SeoHead, Navbar, +error, /share (deprecated in newer SvelteKit) vercel preview deploys were failing with "PUBLIC_FIREBASE_API_KEY is not exported by virtual:env/static/public" because preview env scopes those vars to Production only. switch firebase.ts to $env/dynamic/ public so the build succeeds when vars are missing; firebase init fails at runtime on preview, which is the expectation (auth is not expected to work on preview) governance: - bump version 0.1.0 -> 0.2.0 - CHANGELOG.md gains a 0.2.0 entry covering the full PR scope - SECURITY.md documents the new defenses (security headers, CSP report-only, retry-after, pass/total clamp, path-traversal guard, email normalization) - pnpm build:docs run, static/docs refreshed quality gate: 184 tests + lint + format + typecheck + production build all green. dry-tested via curl: /api/og pass=6&total=1 clamps correctly, /share shows "of 1 systems" and og:image=...pass=1&total=1, single <meta name=robots> in <head>, /docs/../etc/passwd returns 404.
sunnypatell
added a commit
that referenced
this pull request
Apr 25, 2026
PR #10's CDN cache absorbs identical repeat requests at the edge, but a client with Cache-Control: no-cache, a fresh edge region, or any other CDN bypass lands on the serverless function and re-renders the same image. each render is a few hundred ms of CPU plus satori allocation; not free at any scale, real cost at 50k users. new Map<string, ArrayBuffer> caches the rendered bytes per param tuple (score|pass|total|delta), bounded at 200 entries which is roughly 12MB of resident memory per function instance, well under vercel hobby's per-function ceiling. implementation: - LRU bump on hit (delete + re-insert keeps most-recent at the tail) - oldest-out on miss when at capacity (Map insertion order is stable, so .keys().next().value is the oldest entry) - ArrayBuffer chosen over Uint8Array so the Response constructor accepts it as BodyInit without a cast dry-tested: full gate green at 208/208. live smoke on dev: fresh params 73ms memoed repeat 16ms (4.5x faster, byte-identical output)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
chips at the share/og pipeline, dashboard polish, and a bunch of cost/perf wins. one render bug surfaced during local testing and is folded in. copilot review feedback is addressed in
99fc5d0.share + virality
/api/ogPNG endpoint (via@vercel/og), content-addressed byscore/pass/total/deltaand CDN-cached ats-maxage=86400, so repeat shares of the same link cost zero function compute/sharelanding page reads the same query params and emitsog:imagepointing at/api/og, so when LinkedIn / X / iMessage scrape a share URL the preview card renders the actual score/shareURL so its crawler can fetch the rich previewcomparison + history
/historyview shows a journey stats card (total improvement, best score, strongest-gain platform, days span) and an SVG line chart of scores over time with hover tooltipsstartScoringfixes a race where a rapid re-scan compared against the wrong "previous" entry while the firestore save was still in flightscanner UX
/api/analyzereturns 429cost / perf
/api/analyze, SHA-256 keyed by full prompt; same-input requests skip the LLM entirely (verified live: 46s to 81ms, ~570x)_cachedflag (additive, non-breaking)Retry-Afterheader +retryAfterbody field, distinguishes minute vs daily reason, no longer double-charges minute slots on daily failures@vercel/ogresponse re-wrapped to actually setCache-Control(the constructor's headers option concatenates rather than replaces, verified locally with curl)security + production-grade
robots.txt+sitemap.xmlroutes (origin-tracking, no prerender)Content-Security-Policy-Report-Onlyheader +/api/csp-reportendpoint logging violations to vercel logs (no firestore writes, $0)SeoHeadcomponent with og/twitter/canonical, plus JSON-LDSoftwareApplicationon the landing page;app.htmlno longer ships a staticrobotstag (single source of truth)+error.sveltefor 404/429/5xx/healthzjson liveness probe/docs/[...slug]catchall validates resolved paths stay understatic/docs(resolve + startsWith + realpath) so traversal attempts return 404bug fixes
hooks.server.tsinto a/docs/[...slug]catchall so node-onlyfs/pathimports don't pollute the shared bundle (was a real production-blocking build error otherwise)/api/ogmoved off the deprecatedruntime: 'edge'config to default node runtimefirebase.tsmigrated to$env/dynamic/publicso vercel preview builds no longer fail whenPUBLIC_FIREBASE_*is scoped to Production onlycopilot review (
99fc5d0)passclamped to<= totalin/api/ogand/shareso a tampered URL like?pass=6&total=1cannot render impossible state[0, 100]so anomalous scores still render inside the chart padding box<button>to<div role="button">so the inner copy<button>s are valid HTML; restored real<button>on the copy controls; widenedcopyExampleevent param fromMouseEventtoEvent(no more unsafe casts)$app/storesto$app/stateacross SeoHead, Navbar, +error, /sharequality gate
pnpm build:app)/healthz,/robots.txt,/sitemap.xml,/api/og(PNG bytes + cache headers),/share(og:image), rate limiter (11th request 429 with retry-after: 60), full LLM cache-hit round-trip (46s to 81ms), pass/total clamp on tampered URLs, single robots tag in head, /docs traversal blockeddocs
api/endpoints.md: documents the_cachedfield + lists new auxiliary endpointsapi/rate-limits.md: documents the new 429 body shape withretryAfterfield +Retry-Afterheaderpnpm build:docsrun,static/docsrefreshed