An independent, resident-centered way to explore San Francisco parks, recreation destinations, amenities, nearby transit, and official next steps. The site is a statically generated Next.js application built from reviewed public data; it is not affiliated with or endorsed by the City of San Francisco.
- Conventional search, 12 activity entry points, nine broad browsing areas, a collapsible desktop filter panel, an accessible mobile filter dialog, shareable URLs, minimum amenity and acreage thresholds, and 15-result progressive batches
- Permanent destination pages with amenities and reviewed evergreen context
- MapLibre results with two-size destination markers, stable in-place sorting, and optional Google Maps embeds
- List and photo presentations for the same filtered Explore results, with calm gradient placeholders where a destination lacks approved photography
- Build-time Muni, BART, and Caltrain guidance from 511 SF Bay
- Official program and reservation handoffs
- A static project blog for announcing destination enrichment and product updates
- A public photo archive with caption and photographer search, source filters, 20-image progressive batches, and a keyboard-accessible large-image viewer
- A rotating set of four image-supported featured parks selected from ten detailed destinations on each homepage load
- Statement-level source links, with a scannable panel when an overview uses multiple references
- Keyboard-accessible, responsive layouts supporting 320 CSS pixels and wider
- A responsive header with mutually exclusive Menu and Search panels below 880 pixels; opening either panel dims the homepage hero without animation when the user prefers reduced motion
The current accessibility baseline and remaining manual checks are documented
in docs/accessibility-audit.md.
Create one Markdown file in content/blog/. Its filename becomes the page
address, and the opening front matter supplies the title, summary, and date.
Posts use standard Markdown, including headings, emphasis, links, quotations,
and lists. Optional actionLabel and actionHref fields add a closing
call-to-action button. The local editor can visually select a cover, build a
slideshow from reviewed archive images, apply an archive, custom, or hidden
caption to each slide, and upload blog-only images to Vercel Blob. Image credit
remains visible regardless of caption choice. Content Collections validates the
front matter, compiles the Markdown, and generates the typed content used by the
blog index and individual static pages during the next build.
- 249 statically generated destination pages
- Official-page review complete for every destination: 209 enriched, 29 reviewed with no additions, and 11 without a matching official page
- 198 destinations with approved evergreen narrative records; the broader enriched total also includes reviewed facility-directory features
- 3,314 transit stops and stations across 85 routes
- 131 visible approved park images across 58 destinations; Explore uses an explicitly labeled gradient when a destination lacks reviewed photography
- 2 generated editorial illustrations supporting the programs and reservations guide
- Official park-property area available for all 249 public destinations
- 141 automated checks, plus TypeScript validation and a static production build
For local editing, run npm run admin with Node.js 24 and open
http://127.0.0.1:4180. Manage notices, reviewed map-position overrides, all
cataloged photos, area and neighborhood landing pages, Blob uploads, and
Markdown blog posts and slideshows without exposing editing on the live site. Saves
update project files immediately; publishing still requires building and
deploying. See local editor instructions for backups,
expiration dates, and image rights.
The Areas navigation section provides static landing pages for every populated area and neighborhood in the Explore filters. Area pages lead to neighborhoods, and neighborhood pages lead to every matching park. Park-page breadcrumbs retain that area and neighborhood path; direct park links use the first valid source-ordered neighborhood path. Brief geographic context is paraphrased from linked Wikipedia or LocalWiki pages; these project browsing groups are not presented as official boundaries.
Optional park-page notices are configured separately from evergreen content; see park alerts. Lafayette Park currently shows a clearly labeled placeholder notice for layout testing only.
Requires Node.js 20 or newer.
npm install
npm run devOpen http://localhost:4173. To validate and produce the static export:
npm test
npm run check
npm run buildThe deployable static site is written to out/.
The deployed static site needs no required environment variables. To enable the
on-demand Google Maps embed, add NEXT_PUBLIC_GOOGLE_MAPS_EMBED_API_KEY in
Vercel and restrict the key to the Maps Embed API and approved website
referrers. The results map uses the project's reviewed OpenFreeMap style and
does not require configuration.
Do not add 511_API_KEY, SF_PARKS_CONTACT, or PIXABAY_API_KEY to Vercel.
Those values are used only by local data-maintenance scripts, not by the public
site or production build. The committed normalized, presentation, and search
datasets allow Vercel and a fresh clone to build without repeating imports.
Approved images can be prepared locally and delivered from a public Vercel Blob store. Camera originals should remain in a separate personal archive. The repository retains the reviewed attribution records and the generated delivery URLs, but not future batches of binary photographs.
Create a public Blob store for the Vercel project, then pull its
BLOB_READ_WRITE_TOKEN into .env.local. Place a reviewed batch temporarily
in the ignored public/media/ intake directory. Prepare responsive WebP
variants and upload the immutable, content-hashed files with:
npm run prepare:blob-images
npm run upload:blob-imagesThe preparation report is ignored as reproducible output. The upload command
updates data/media/blob-assets.json, which is intended to be committed after
the uploaded images have been reviewed in the site. Image delivery is resolved
during the static build, so expanding the registry does not send the complete
catalog to interactive browser components. After upload and verification,
archive the source photographs outside the repository and clear the local
intake directory; the application does not require those source files.
Manage captions, alt text, credits, visibility, gallery order, destination featured-image choices, and catalog removal in the local editor. A featured image becomes that park's default in Explore photo tiles, homepage cards, and other automatic representative-image placements without changing its gallery order. Removing an entry retains its public Blob file for recovery. All cataloged photographs appear in the editor, including new uploads. The CSV review workflow has been retired.
The public Photos page includes visible destination photographs and any blog-only upload that has actually been used by a published post. Unused blog-only uploads remain private to the local editorial workflow. Archive tiles load in batches of 20, and every published image retains its reviewed creator, source, and license links. Area and neighborhood pages link into geographically filtered archive views; visitors can change or clear that scope on the archive page itself.
Blog posts use the generic San Francisco park illustration by default. To choose another reviewed Blob asset for a post card, add its stable registry path and meaningful alt text to the Markdown frontmatter:
image:
path: /media/alamo-square-01.jpg
alt: View across Alamo Square's lawn toward surrounding homes.The path must already exist in data/media/blob-assets.json; an unknown path
fails the production build rather than publishing a broken image.
To add a slideshow to a post, choose two or more reviewed images in the local
editor. Alt text, creator credits, source links, and licenses are inherited
from data/media/media-manifest.json. Each slide can use its archive caption,
provide a post-specific caption, or suppress the descriptive caption while
retaining the image credit:
gallery:
- path: /media/alamo-square-01.jpg
captionMode: database
- path: /media/sigmund-stern-recreation-grove-01.jpg
captionMode: custom
caption: A post-specific description of this moment in the project.
- path: /media/golden-gate-park-02.jpg
captionMode: noneThe earlier path-only list remains valid and is treated as database mode.
Raw downloads are intentionally excluded from Git because they are large and reproducible. Approved DataSF sources can be imported with:
npm run import:datasf -- datasf-rec-park-properties
npm run normalize
npm run build:destinations
npm run build:searchNormalization performs a coordinate-evidence review on every refresh. A usable
source point is retained when it falls inside—or within 0.5 mile of a
representative point derived from—the record's official geometry. A missing,
null-island, or seriously displaced point is replaced with a deterministic
point inside that geometry and recorded under coordinateCorrections in
data/normalized/normalization-report.json. If neither form of evidence is
usable, the destination receives no map or directions links. The application
also supports documented, reviewed corrections in
data/normalization/overrides.json for
cases where a technically valid source point is not a useful public map
marker. Each manual point requires evidence, a reason, a reviewer, and a review
date; the rejected source point remains in the normalization audit trail. See
docs/coordinate-overrides.md for the entry
format and rebuild procedure.
Refresh scheduled regional transit data with:
npm run import:transit
npm run normalize:transitFor local refreshes, place 511_API_KEY in .env.local, provide
SF_PARKS_CONTACT when running DataSF imports, and add PIXABAY_API_KEY only
when intentionally rerunning the deferred image-discovery pilot. These private
values must not use a NEXT_PUBLIC_ prefix.
All sources must be declared and approved in data/sources.json. Validate that
registry with npm run validate:sources before publishing refreshed data.
Regenerate the destination-review summary with:
npm run report:official-page-reviewThe report distinguishes published enrichment, completed reviews with no useful additions, missing official pages, and genuinely pending work. A completed “no additions” result is not an unfinished review.