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
Two clusters. The content cluster sits next to the logo; the utility cluster is pinned to the right edge and carries the outbound links, search, auth, and the call to action.
Content cluster, items in this order:
| Item | Target | Style |
|---|---|---|
| Projects | /projects |
text link |
| Events | /events |
text link |
| Help Wanted | /help-wanted |
text link |
| Members | /members |
text link |
| About ▾ | dropdown | text link with caret |
Utility cluster, items in this order (left to right):
| Item | Target | Style |
|---|---|---|
| Slack | /chat |
icon-only link, accessible name "Join Code for Philly on Slack", opens in a new tab; a full-page navigation to the server redirect (chat.md) |
| GitHub | https://github.com/CodeForPhilly |
icon-only external link, accessible name "Code for Philly on GitHub", opens in a new tab |
| Search 🔍 | inline expand | icon button |
| Sign in / account menu | see Auth controls | button / avatar dropdown |
| Volunteer | /volunteer |
button (success, filled) — emphasized because it's the call to action |
Volunteer is the rightmost element in the header. It sits after the auth control rather than among the content links so the call to action reads as the header's terminal step, not as one more section.
Slack is always one step from the header. It is where the community actually works, and a newcomer looking for it should not have to scroll to the footer. The footer's Connect column keeps its Slack link too.
The desktop header starts at lg (1024px). Below that the content cluster and the utility cluster do not fit on one line (the content links wrap and Volunteer runs off the edge), so tablets in portrait get the mobile header and its sheet.
- 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.
Second from the right in the utility cluster — between Search and the Volunteer button. On mobile the auth control sits in the header bar itself, outside the sheet.
- 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 — the content-cluster links, the About items under an "About" label, the Slack and GitHub links, and Volunteer last, mirroring the desktop order. Search is inside the sheet, not inline. The auth control stays in the header bar, outside the sheet.
Every item in the sheet closes the sheet when activated.
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> - The dropdown is an ARIA combobox:
ArrowDown/ArrowUpmove a highlight through the results (including the "See all results" link, wrapping at either end); focus stays in the input Enterwith a highlighted result navigates to that result; with no highlight it submits to/search?q=<q>Escapecloses the dropdown and clears the query
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) |
/events |
(none) |
/events/past |
Events › Past |
/events/:slug |
Events › <title> |
/events/:slug/edit |
Events › <title> › Edit |
/events/new |
Events › New event |
/events/series/:slug |
Events › <series title> |
/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) - Events →
/events - 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]").
Retryre-fetches every active query and dismisses the banner. When the failed call has nothing to re-issue (the header typeahead, which re-runs on the next keystroke), the button readsDismissinstead — the visible label always says what the button does - 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
- The mobile sheet is a dialog with the accessible name "Menu"
- At ≥ lg the header exposes two navigation landmarks: "Primary navigation" (the content cluster) and "Utility" (Slack, GitHub, search, auth, Volunteer)
- Every icon-only control carries an accessible name; controls with visible text use that text as their accessible name rather than duplicating it in a label
A print stylesheet hides the header, footer, breadcrumbs, search dropdown, and any auth controls. Pages print as their content area.