| status | done | |
|---|---|---|
| depends |
|
|
| specs |
|
|
| issues |
|
|
| pr | 104 |
#83 phase 3 — closes the <ComingSoon /> placeholders on /pages/mission, /pages/leadership, /pages/code-of-conduct, /pages/hackathons per behaviors/app-shell.md:
"The
/pages/*URLs serve static content pages authored as MDX/Markdown in the code repo (apps/web/src/content/pages/). They have no per-page screen spec — the content is the spec."
This plan ships the plumbing (content directory, markdown→HTML build-time renderer, the /pages/:slug route). The actual copy is placeholder that calls itself out — porting the legacy laddr-site text is a content task, not an engineering one. Filed as a follow-up.
- behaviors/app-shell.md —
/pages/*URL pattern + content authoring location.
Add marked to apps/web for client-side markdown rendering. Choice rationale:
- Static pages are not user content — the CLAUDE.md "no client markdown" rule explicitly applies to user-supplied content (bios, project overviews, blog bodies). Build-time-static content is essentially JSX.
markedis tiny (~30 KB min+gz) and battle-tested.- The alternative — using
@cfp/shared'srenderMarkdownserver-side via a build-time Vite plugin — adds more tooling than the v1 needs.
No DOMPurify dance: zero XSS surface on content that lives in the bundle.
apps/web/src/content/pages/:
mission.mdleadership.mdcode-of-conduct.mdhackathons.md
Each carries a placeholder body that names itself ("This page's content hasn't been ported from the legacy site yet — see [issue ref] to help.") plus an H1 + a paragraph. Real copy ports from codeforphilly.org as a content PR.
apps/web/src/pages/StaticPage.tsx:
import.meta.glob('@/content/pages/*.md', { query: '?raw', import: 'default', eager: true })builds a slug → markdown source map at build time.- The component reads
:slugfrom the route, looks up the matching source, parses withmarked, and renders inside aprosetypographic container. - Unknown slug →
<NotFound />.
apps/web/src/App.tsx:
- Replace
{ path: '/pages/:slug', element: <ComingSoon /> }with<StaticPage />.
Reuse the existing typographic styles from MarkdownView.tsx (a prose container with tailwind targeting for headings, lists, code, blockquotes). DRY by extracting to a shared MarkdownContent wrapper, or just copy the class list — copy is cheaper for v1.
apps/web/tests/StaticPage.test.tsx:
- Renders the H1 from
mission.md. - Renders an unknown slug as NotFound.
- Renders all four bundled pages (smoke).
-
npm install markedlands as its own commit. -
apps/web/src/content/pages/has 4 markdown files. -
/pages/mission,/pages/leadership,/pages/code-of-conduct,/pages/hackathonsall render their content. -
/pages/nonexistentrenders the NotFound screen. -
npm run type-check && npm run lint && npm testclean.
import.meta.globis Vite-specific. Confirmed by the existing codebase using Vite — same mechanism would need a polyfill or alternative if we ever switched bundlers. Out of scope to worry about.- Bundle size.
markedadds ~30 KB. Acceptable for static-content rendering. If bundle pressure becomes a concern later, swap to a Vite plugin that pre-renders to HTML at build time and import the HTML directly. - Placeholder content is honest about being placeholder, but a casual visitor will still see "this hasn't been ported yet" on real pages. Trade-off: ship the plumbing now so the spec is satisfied; content PR follows.
Three commits: plan-open, npm install marked (with the exact
command in the body, per the generated-files-commit-first convention),
content + StaticPage + tests.
Surprises:
marked.parseis sync-by-default in v18. Earlier versions returnedstring | Promise<string>depending on extension config; v18+ defaults to sync unless a custom async extension is registered. The{ async: false }arg is belt-and-suspenders.- The
proseclass duplication.MarkdownView.tsxandStaticPage.tsxcarry similar Tailwindproseconfigs. Considered extracting to a shared wrapper, but the consumers diverge subtly:MarkdownViewis for compact embedded markdown (project overviews, update bodies) and usesprose-sm;StaticPageis for full-width documentation and usesprose-sm sm:prose-base. Plus heading scales differ. Three-similar-lines vs. premature abstraction — kept the copy. - No DOMPurify dance. Static-page content is build-time-static
source; no XSS surface.
dangerouslySetInnerHTMLis the right tool here even though the name reads scary.
- Port real copy from the legacy site. Each of the four pages
carries placeholder text that names itself as such. The real text
lives at
codeforphilly.org/site-root/pages/. Tracked as — content-PR task; will file a tracking issue when content review has someone owning it. - Phase 4 —
/projects/:slug/buzz/newform stays the last open piece of #83. Deferred to plan —plans/buzz-new-form.md. - MDX upgrade. If
/pages/leadershipever needs to render embedded React components (e.g., a live leadership-roster card), swap to@mdx-js/rollup. None for v1 — pure markdown is sufficient.