Repository source: docs/plans/mitrapclub_about_copy/mitrapclub_about_copy_step3_development_plan.md
Completion Contract
Normal entry: a visitor loads
/about/on any profile.End-to-end outcome:
mitrapclub's about page shows club-voiced intro and all three sections (identity/participation/backup), with every existing functional link (/users/,/activity/,/tools/backup/,/api/,/llms.txt) still working;zenmemes/chouse/qdbrender byte-identical to today.Required recovery: none — static content, no new failure mode.
Deployment/external verification: not applicable.
Release condition:
./v3 testpasses in full; a manual byte-diff of rendered/about/HTML confirms no change forzenmemes/chouse/qdb.
Key Risks
High risk: the three sections' current paragraphs contain inline
<a href>links embedded in prose. The existing data-driven pattern (communityParagraphs) escapes text with$e(), which would turn those links into literal escaped text. Resolution: render these three sections' paragraphs unescaped in the template, the same trust boundary the template already uses for its own hardcoded HTML — this is developer-authored constant data (a private PHPconst), never user input, so no new XSS surface opens. Early validation: Stage 2's manual link-click check. Mitigation: keep this content path non-dynamic (no runtime/user write ever reachesEDITORIAL).No existing automated test asserts the exact wording of the intro sentence or the three sections, so a mismatch between the new per-profile text and today's hardcoded text for
zenmemes/chouse/qdbwould be a silent regression. Early validation: explicit manual byte-diff of rendered/about/HTML before/after each stage. Mitigation: copy today's exact hardcoded strings verbatim into their per-profile fields; don't paraphrase.Writing genuinely good club-voiced copy for the identity/participation/backup sections is a content-quality judgment call, not a pass/fail test (carried from Step 2). Mitigation: keep meaning faithful to today's text; treat wording as iterable after this ships.
Stage 1
Goal: make the intro sentence and the three sections' heading/paragraph content per-profile, wired through
ProfilePresentationContent::about().Dependencies: none.
Expected changes:
ProfilePresentationContent::EDITORIALgains, for all four profiles:introText(a%s-templated string, same pattern asdocsIntro),graphHeading/graphParagraphs,participationHeading/participationParagraphs,portableHeading/portableParagraphs(eachHeadinga string, eachParagraphsalist<string>).For
zenmemes/boston/qdb: these new fields hold today's exact hardcoded template strings verbatim (including the inline<a href>markup for the three sections), sosprintf(introText, displayName)reproduces today's computed introduction byte-for-byte.For
mitrapclub: new club-voiced versions of all four pieces, preserving the same functional links and meaning.ProfilePresentationContent::about()'sintroductionkey is now built fromsprintf($editorial['introText'], $displayName)instead of the current fixed concatenation;about()also returnsgraphHeading/graphParagraphs/participationHeading/participationParagraphs/portableHeading/portableParagraphs.Return-type docblocks on
about()/editorial()updated to match.
Verification approach:
./v3 testfull suite.Render
ProfilePresentationContent::about()for all four profiles; diff theintroductionvalue forzenmemes/chouse/qdbagainst today's exact computed string (must match verbatim); confirmmitrapclub's is the new club-voiced text.templates/pages/about.phpis untouched in this stage, so the three sections still render their old hardcoded markup regardless of the new (as-yet-unused)about()fields — only the intro paragraph can visibly change in this stage.
Risks or open questions:
Impact: a typo or paraphrase in the "unchanged" profiles'
introTextsilently alters their rendered intro sentence.Early warning / validation: byte-diff check above, done before moving to Stage 2.
Mitigation: copy-paste today's exact string rather than retyping it.
Canonical components/API contracts touched:
ProfilePresentationContent::EDITORIAL,about(),editorial().
Stage 2
Goal: switch the three hardcoded sections in the about template to the new per-profile data.
Dependencies: Stage 1 (the data must exist first).
Expected changes:
templates/pages/about.php: the "graph," "participation," and "portable"<section>blocks read$aboutContent['graphHeading']/['graphParagraphs'](and the participation/portable equivalents) instead of hardcoded markup — heading escaped via$e()as today; paragraphs rendered unescaped (see Key Risks) to preserve inline links.Same section order, same
data-about-sectionattributes, same surrounding structure (hackable-section gate and social-links block untouched).
Verification approach:
./v3 testfull suite.Manual byte-diff of rendered
/about/HTML forzenmemes/chouse/qdbagainst a pre-Stage-1 snapshot — must be identical.Render
mitrapclub's/about/: confirm club-voiced headings/paragraphs appear and every link (/users/,/activity/,/tools/backup/,/api/,/llms.txt) is a real, clickable anchor, not escaped text.
Risks or open questions:
Impact: unescaped paragraph rendering is only safe because this content is a private PHP constant.
Early warning / validation: confirm no other code path ever writes into
ProfilePresentationContent::EDITORIALat runtime (it's aprivate const, so this is structurally guaranteed, not just a convention).Mitigation: none needed beyond the structural guarantee — noted so a future contributor doesn't casually make this field runtime-writable.
Canonical components/API contracts touched:
templates/pages/about.php.
Waiting for "Approved Step 3" before branching and starting Step 4.