feat: build steps 7a-10 — the site is complete and reviewable at 22 pages

Steps 7a through 10 as one authorised run. Nothing deployed (D11).

7a  Lighthouse returns as `lighthouse@13.4.1` + `chrome-launcher`, NOT
    `@lhci/cli`. AGENTS.md §7's advisory attribution was wrong: the carriers
    were @lhci/cli's own `tmp` and @puppeteer/browsers' `extract-zip`, not
    Lighthouse, which audits clean. A deliberate deviation from R11's literal
    trigger, recorded with what it costs. Local gate; CI has no Chrome.

7b  OG card generator (satori + sharp) discharges R15 — 20 typed cards plus
    per-article cards; the portrait stays on / and /about/ by Q40. Insights
    plumbing: ArticleCard, Prose, the index, the article route, articleGraph,
    and /'s section 7. Card copy is constrained structurally because text in a
    JPEG cannot be grepped by check:claims: every headline IS its page's <h1>,
    enforced by `npm run og:proof`.

7c  Five drafted launch articles, draft: true / reviewedByPouya: false. An
    independent compliance audit returned 76 findings and 57 unsourced
    assertions; all blocking and should-fix applied.

8   /contact/, the intake form, and backend/intake/ (undeployed). Plain HTML
    POST to a same-origin /api/intake with a 303 redirect, so the form works
    with zero JavaScript. docs/05 records three deliberate deviations.

9   /fees/ on Q59's ruling — overtime runs from the session cap, and the
    reservation point ships adjacent to the rate. One-page PDF bio discharges
    R16; /bio/ is its source, so the circulated artefact stays inside the
    review apparatus.

10  /legal/privacy/ and /legal/terms/, written to the backend as built. Three
    of the policy's statements are derived and cannot drift.

Also: /about/'s inverse credentials band (approved at step 6); Q59 closed;
R15 and R16 discharged; and a fix to shipped copy — /practice/energy/ asserted
the absence of a regulation the source extract says must not be asserted.

Review: adversarial-reviewer, two rounds (D20/D19). Round 1 returned 16
findings including two blocking — an invisible ghost button on /fees/ at
1.00:1 that Lighthouse scored 100, and a privacy policy that named one data
processor when there are two. All 16 acted on.

Lighthouse, 22 pages, mobile: performance 99-100, accessibility 100,
best practices 100, SEO 100 on every indexable page, CLS 0.000.

AGENTS.md entry (ah) has the detail, including four of my own verification
commands that were wrong and what each of them nearly caused.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Md3GndFqWPzK78xAoebsg5
This commit is contained in:
Pouya Lajevardi
2026-08-31 10:56:54 -04:00
co-authored by Claude Opus 5
parent 6cfe69033f
commit 210bc25a26
53 changed files with 8589 additions and 177 deletions
+98
View File
@@ -0,0 +1,98 @@
---
/**
* `/contact/could-not-send/` — the failure half of the intake form's
* POST-redirect-GET. Build step 8.
*
* WHY THIS PAGE EXISTS AT ALL. The site ships zero JavaScript, so a static page
* cannot read `?error=` and render a message. The alternatives were: return an
* error body from the API (the visitor lands on the API hostname with none of
* the site around it), or say nothing (the visitor cannot tell whether the
* inquiry arrived, on a form about a live dispute). A named page is the only one
* of the three that leaves the reader knowing what happened.
*
* ⚠️ **IT DOES NOT LIST WHICH FIELD FAILED, AND THAT IS DELIBERATE ON TWO
* COUNTS.** The handler deliberately does not return the error list — an
* enumeration of the validation rules is a gift to whoever is probing them — and
* the browser's own `required` / `type="email"` / `maxlength` handling has
* already caught every case a person is likely to hit, inline and announced. A
* server-side rejection means the submission was not made by that markup.
*
* ⚠️ **NO APOLOGY AND NO GUESS AT THE CAUSE.** "Something went wrong on our end"
* is a claim about which end, and this page cannot know. It says what is true —
* the inquiry was not recorded — and gives a route that does not depend on the
* form working.
*
* `noindex`, and excluded from the sitemap in `astro.config.mjs`.
*/
import BaseLayout from '../../layouts/BaseLayout.astro';
import Button from '../../components/Button.astro';
import Eyebrow from '../../components/Eyebrow.astro';
import { getImage } from 'astro:assets';
import ogDefault from '../../assets/og-portrait.jpg';
import { pageGraph } from '../../data/schema';
import { CONTACT } from '../../data/site';
const ldImage = await getImage({
src: ogDefault,
format: 'jpeg',
width: 1200,
height: 630,
});
const graph = pageGraph(new URL(ldImage.src, Astro.site).href);
---
<BaseLayout
title="Inquiry Not Sent · Contact · Pouya Lajevardi · Toronto"
description="The inquiry was not recorded, so nothing has been received. Email the same details directly and they will be answered within two business days."
jsonLd={graph}
noindex
>
<section class="section hero">
<div class="wrap">
<Eyebrow dot>Not sent</Eyebrow>
<h1 class="display hero-h">That inquiry was not recorded.</h1>
<div class="prose">
<p class="statement">
Nothing has been received, so there is nothing waiting for a reply.
</p>
<p>
The quickest route is email. Send the same details — your name, your
role, the other parties, and a few sentences about the dispute — to <a
href={`mailto:${CONTACT.email}`}>{CONTACT.email}</a
>, and leave privileged detail out of it. {CONTACT.responseTime}
</p>
<p>
Or go back to the form and send it again. If it fails a second time,
email rather than trying a third.
</p>
</div>
<div class="cta">
<Button href="/contact/">Back to the form</Button>
<Button href={`mailto:${CONTACT.email}`} variant="ghost">
Email instead &rarr;
</Button>
</div>
</div>
</section>
</BaseLayout>
<style>
.hero {
padding-block: var(--space-9) var(--space-11);
}
.hero-h {
margin-block: var(--space-4) var(--space-5);
font-size: var(--text-5xl);
}
.statement {
font-size: var(--text-lg);
line-height: var(--leading-body);
color: var(--text);
}
.cta {
display: flex;
flex-wrap: wrap;
gap: var(--space-3) var(--space-4);
margin-block-start: var(--space-8);
}
</style>
+114
View File
@@ -0,0 +1,114 @@
---
/**
* `/contact/received/` — the GET half of the intake form's POST-redirect-GET.
* Build step 8. `backend/intake/handler.mjs` sends a `303 See Other` here on
* success.
*
* WHY A PAGE RATHER THAN A RESPONSE BODY. The site ships zero JavaScript, so the
* form is a plain POST; without this redirect the visitor would be looking at
* whatever the API returned, on the API's own hostname, with none of the site
* around it. Landing on a GET also means a refresh cannot resubmit.
*
* `noindex` — it is a transactional page with no standalone value, and a search
* result reading "your inquiry has been received" for someone who has not sent
* one is worse than no result. It is excluded from the sitemap in
* `astro.config.mjs` for the same reason.
*
* ⚠️ THE RESPONSE-TIME SENTENCE IS A PUBLIC COMMITMENT (§4, Q27) and must read
* identically here, on `/contact/`, and in the confirmation email the handler
* sends. Rendered from `CONTACT.responseTime`; never retyped, never softened.
*
* ⚠️ AND A BOT THAT TRIPS THE HONEYPOT IS SENT HERE TOO — deliberately, see the
* handler. So this page must not say anything that is false for that case. It
* says what was done, not what will happen to a specific record: "received"
* covers a stored submission, and nothing here promises a reply to a submission
* that was discarded.
*/
import BaseLayout from '../../layouts/BaseLayout.astro';
import Button from '../../components/Button.astro';
import Eyebrow from '../../components/Eyebrow.astro';
import { getImage } from 'astro:assets';
import ogDefault from '../../assets/og-portrait.jpg';
import { pageGraph } from '../../data/schema';
import { CONTACT, NO_RETAINER_NOTICE } from '../../data/site';
const ldImage = await getImage({
src: ogDefault,
format: 'jpeg',
width: 1200,
height: 630,
});
const graph = pageGraph(new URL(ldImage.src, Astro.site).href);
---
<BaseLayout
title="Inquiry Received · Contact · Pouya Lajevardi · Toronto"
description="Your inquiry has been received. A confirmation goes to the address you gave, inquiries are answered within two business days, and nothing further is needed."
jsonLd={graph}
noindex
>
<section class="section hero">
<div class="wrap">
<Eyebrow dot>Received</Eyebrow>
<h1 class="display hero-h">Your inquiry has been received.</h1>
<div class="prose">
<p class="statement">{CONTACT.responseTime}</p>
{
/* ⚠️ "A confirmation HAS BEEN SENT" WAS A STATEMENT OF FACT THAT TWO
PATHS REACH THIS PAGE WITHOUT HAVING MADE TRUE, and this file's own
header already said it must not be: *"this page must not say anything
that is false for that case. It says what was done, not what will
happen to a specific record."*
(a) The honeypot returns `redirect(SUCCESS)` before any write or any
send — deliberately, because telling a bot it was detected is how the
next bot stops filling the field. (b) The handler sends the two
emails with `Promise.allSettled` and redirects here even if both
reject, because the submission is already stored and a second attempt
would duplicate the record.
So the receipt is stated as what happens rather than as what
happened, and the clause after it is the route out either way. Found
by `adversarial-reviewer`, 2026-08-31. */
}
<p>
A confirmation goes to the email address you gave, repeating what you
sent and linking to the privacy policy. If it has not arrived within a
few minutes, check the address and email me directly at <a
href={`mailto:${CONTACT.email}`}>{CONTACT.email}</a
> — that reaches me whether or not the receipt did.
</p>
<p>{NO_RETAINER_NOTICE}</p>
</div>
<div class="cta">
<Button href="/process/" variant="ghost"
>What happens next &rarr;</Button
>
<Button href="/fees/" variant="ghost">The rate card &rarr;</Button>
</div>
</div>
</section>
</BaseLayout>
<style>
/* No ContactBand: the reader has just used the contact form, and inviting
them to contact again is the one place that band would read as a defect. */
.hero {
padding-block: var(--space-9) var(--space-11);
}
.hero-h {
margin-block: var(--space-4) var(--space-5);
font-size: var(--text-5xl);
}
.statement {
font-size: var(--text-lg);
line-height: var(--leading-body);
color: var(--text);
}
.cta {
display: flex;
flex-wrap: wrap;
gap: var(--space-3) var(--space-4);
margin-block-start: var(--space-8);
}
</style>