Every screen on the site is rendered inside the same chrome — a sticky header, an optional breadcrumb row, the page content area, and a footer. The shell carries navigation, search, and auth state. Individual screen specs declare their content; this spec declares the frame.
Every URL except /login (which intentionally renders a minimal shell). The GitHub OAuth callback handler and the account-claim screens (not yet specified) will also use the minimal shell.
┌─────────────────────────────────────────────────────┐
│ Header │ sticky
├─────────────────────────────────────────────────────┤
│ Breadcrumbs (optional) │ conditional
├─────────────────────────────────────────────────────┤
│ │
│ Page content │ per-screen spec
│ │
├─────────────────────────────────────────────────────┤
│ Footer │
└─────────────────────────────────────────────────────┘
Sticky at the top of the viewport. Background opaque, slight shadow on scroll.
- Logo (
/img/logo.png) — 32px tall, links to/ - Site name "Code for Philly" — visible at ≥ md, hidden below to save space
Primary nav, items in this order:
| Item | Target | Style |
|---|---|---|
| Projects | /projects |
text link |
| Help Wanted | /help-wanted |
text link |
| Members | /members |
text link |
| Volunteer | /volunteer |
button (success, filled) — emphasized because it's the call to action |
| About ▾ | dropdown | text link with caret |
| Search 🔍 | inline expand | icon button |
- Mission →
/pages/mission - Leadership →
/pages/leadership - Code of Conduct →
/pages/code-of-conduct - Hackathons →
/pages/hackathons - Sponsor →
/sponsor - Contact →
/contact(not yet specified; rendered asmailto:for v1)
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. Source copy ports from codeforphilly.org/site-root/pages/ in the legacy repo.
- Anonymous: "Sign in" (primary button) →
/login. There is no separate "Sign up" button — sign-in and sign-up are the same flow once GitHub OAuth is specified (first sign-in creates the account). - User: Avatar + name dropdown:
- "My profile" →
/members/<slug> - "Account settings" →
/account - separator
- "My projects" →
/projects?memberSlug=<slug> - separator
- "Sign out" → calls
POST /api/auth/logout, redirects to/
- "My profile" →
- Staff: Same dropdown plus a "Staff tools" section:
- "Manage tags" →
/tags?staff=true - "Recent staff actions" → deferred placeholder
- "Manage tags" →
- Administrator: Staff items plus "Admin" section:
- "Manage members" →
/members?staff=true&accountLevel=all
- "Manage members" →
Header collapses to: logo + hamburger menu + auth control. Hamburger opens a sheet (right-side overlay) with all nav items stacked vertically. Search is inside the sheet, not inline.
Single search input in the header. Behavior:
- Placeholder: "Search projects, members, tags…"
- On focus, expands to fit the available space
- As the user types (debounced 200ms), a dropdown shows up to 12 results across types (4 per category, see groups below) — matches laddr's site-wide search but in a typeahead form instead of a full results page
- Result groups:
- "Projects" —
GET /api/projects?q=...&perPage=4 - "Members" —
GET /api/people?q=...&perPage=4 - "Tags" —
GET /api/tags?q=...&perPage=4
- "Projects" —
- "See all results for
<q>" link at the bottom →/search?q=<q> Enterkey submits to/search?q=<q>
The /search results page is deferred; for v1 we ship only the typeahead and the /search page renders a redirect-to-projects with the q prefilled. Tracked in deferred.md as a follow-up.
Sort order within each group: relevance (SQLite FTS5 BM25 rank — see behaviors/storage.md).
Optional row below the header. A screen opts in by declaring a breadcrumbs trail; screens without one render the page content immediately under the header.
| Screen | Breadcrumb trail |
|---|---|
/projects |
(none — top-level) |
/projects/:slug |
Projects › <title> |
/projects/:slug/edit |
Projects › <title> › Edit |
/projects/create |
Projects › New project |
/members |
(none) |
/members/:slug |
Members › <fullName> |
/tags/:namespace/:slug |
Tags › <namespace> › <title> |
/help-wanted |
(none) |
/account |
Settings |
Each segment except the last is a link to the parent route.
Three columns at ≥ md, stacked below.
- Active Projects →
/projects - Start a Project →
/projects/create(or external link for anonymous) - Hackathons →
/pages/hackathons - Members →
/members - Help Wanted →
/help-wanted - Blog →
/blog
- Mission →
/pages/mission - Leadership →
/pages/leadership - Code of Conduct →
/pages/code-of-conduct - Sponsor →
/sponsor - Contact →
/contact
- Slack →
/chat - Newsletter signup (defer)
- Social icons: Instagram, LinkedIn, Facebook, Meetup, Mastodon, Bluesky
- Twitter/X is not in v1; see deferred.md
- "Copyright © Code for Philly 2011 – {currentYear}" (year computed at render time)
- "Open source — view this site on GitHub" link to the rewrite repo
- "Powered by laddr" credit is gone (see deferred.md)
The shell reads:
GET /api/auth/meon first load — once, then cached in a React context- The current route — for active-link styling and breadcrumb construction
It does not block initial paint waiting on me. Auth controls render skeletons until the response arrives.
- Top-of-page progress bar (1–2px) animates during full-page navigations
- Top-of-page red banner when an API call returns 5xx ("Something went wrong. We're looking at it. [Retry]")
- A red banner when offline ("You're offline. Some features may not work.") — driven by
navigator.onLine
- Logo is a link with visible text inside it ("Code for Philly")
- Skip link at the very top: "Skip to main content" → focuses the
<main>element - All dropdowns are keyboard-navigable
- The mobile sheet traps focus while open and returns it to the trigger on close
A print stylesheet hides the header, footer, breadcrumbs, search dropdown, and any auth controls. Pages print as their content area.