尚未检测到更新。GitHub 发布版本后会显示在这里。
个人 GitHub 账号下公开仓库的所有者可以登录并验证所有权,自行管理介绍,无需管理员批准。
登录并生成验证文件,在 30 分钟内将其提交到此仓库的默认分支。其他分支或分叉仓库中的文件无效。然后在下方检查文件,即可无需管理员批准直接管理项目。请保留文件以维持管理权限。
如需登录,将前往登录页面。登录后请回到此处,再次点击所选的验证按钮。
在项目文件夹中运行 /nomorevibe verify,即可验证所有权并自行管理介绍。
一分钟完成验证Vegan Grove web: Next.js 15 public site and member area for a privacy-first vegan community and activism platform. MapLibre maps, httpOnly sessions, no analytics. Hosted on AWS Amplify.
翻译准备中
取自仓库 README
正在加载完整 README。
查看完整 README ↗Public pages rendered on the server, a member area behind an httpOnly cookie, and no session token in the browser.
Vegan Grove is a privacy-first vegan community and activism platform for Southern California. This repository is the website at vegangrove.org: a Next.js 15 App Router application that renders the public places, events, groves, media, and guides pages on the server and serves the authenticated /app area for the feed, messages, friends, the companion, and settings. It owns no data and talks to nothing but the Vegan Grove API and OpenFreeMap tiles.
| Repository | Role | Stack | Deploys to |
|---|---|---|---|
| vegan-grove-api | REST API, Socket.IO, workers, the only reader of the database | Express 5, Mongoose 9, zod, pino, Socket.IO, vitest | One EC2 instance, PM2 behind nginx, us-east-1 |
| vegan-grove-web (this repo) | Public site and the /app member area | Next.js 15, Tailwind 4, shadcn/ui, MapLibre GL | AWS Amplify Hosting, us-east-1, on push to main |
| vegan-grove-mobile | iOS and Android app | Expo SDK 57, expo-router, TanStack Query, MapLibre | EAS Build, App Store and Play |
| vegan-grove-docs | Product, privacy, and architecture docs | PokeDocs on Docusaurus 3 | AWS Amplify Hosting, us-east-1, on push to main |
Docs: docs.vegangrove.org. Product: vegangrove.org. Both domains and api.vegangrove.org are launching.
The browser never holds the session. It posts credentials to a Next route handler, the handler exchanges them with the API, and the token comes back only as an httpOnly cookie. Server components forward that cookie as a Bearer header; client components call the same API for public data and are never handed a token.
sequenceDiagram
participant B as Browser
participant N as Next 15 (Amplify)
participant A as API
participant T as OpenFreeMap tiles
B->>N: POST /api/session (mode, email, password)
N->>A: POST /api/auth/login
A-->>N: token and user
N-->>B: Set-Cookie vg_session (httpOnly, Secure, SameSite=Lax) and the user as JSON
Note over B,N: The token never reaches browser JavaScript
B->>N: GET /app/feed with the cookie
N->>N: middleware checks that the cookie exists
N->>A: GET /api/me with Authorization Bearer, from a server component
A-->>N: user
N-->>B: rendered HTML
B->>T: vector tiles for /places (no key, no cookie)
B->>A: GET /api/places?bbox=w,s,e,n (public)
B->>N: DELETE /api/session
N->>A: POST /api/auth/logout
N-->>B: cookie cleared
Public pages are server components that call the API without a session and revalidate every 60 seconds; when the API is down they render an honest empty state instead of a 500. Pages under /app are rendered per request because they read the cookie. The map is the only page that is client-only from the top, because MapLibre needs window; its tile worker is self-hosted under public/maplibre/ because Turbopack hashes the worker and its shared chunk separately, which breaks the worker's relative import and leaves the map blank.
nvm use # Node 24, from .nvmrc
npm ci
cp .env.example .env.local # the dev defaults work against a local API on :4000
npm run dev # next dev --turbopack, http://localhost:3000
npm run validate # biome check, tsc --noEmit, next build
npx playwright install chromium # once, then:
npm run test:e2e # Playwright against a mock API, see tests/fixtures
The site expects vegan-grove-api at NEXT_PUBLIC_API_BASE_URL (default http://localhost:4000/api) and runs without it. Husky runs Biome on staged code and secretlint on every staged file.
Playwright starts two servers of its own: the mock API on port 4010 (MOCK_API_PORT) and a Next dev server on port 3000 (PLAYWRIGHT_PORT) pointed at it. It never reuses a dev server you started by hand, because that one points at a different API. New shadcn components come from npx shadcn@latest add <name>, not from hand-written files, so upgrades stay clean.
| Script | What it does |
|---|---|
npm run dev | next dev --turbopack |
npm run build | next build --turbopack, what Amplify runs |
npm run predev, npm run prebuild | copy MapLibre's worker and shared chunk into public/maplibre/ (run automatically) |
npm start | next start on a built app |
npm run lint | biome check . |
npm run lint:fix | biome check --write . |
npm run typecheck | tsc --noEmit |
npm run test:e2e | Playwright in chromium against a mock of the API (tests/fixtures/mock-api.mjs) that serves fixtures in the API's real envelopes and honors the places query params: home, the map container, markers from map-pins, the vegan-level, type, and chain filters and their URL round-trip, the marker popup and the side list, a detail page from { place } with phone, hours, and the map preview, a 404 for an unknown slug, the /app redirect, security headers |
npm run validate | lint, typecheck, build: the CI contract and the PR gate (Playwright stays separate) |
npm run prepare | installs the husky hooks |
Names only in .env.example; never commit a .env file. On Amplify, set these in the console; the amplify.yml preBuild step copies NEXT_PUBLIC_* and SESSION_* into .env.production so next build can inline them.
| Variable | Purpose | Shape |
|---|---|---|
NEXT_PUBLIC_API_BASE_URL | API base including /api; its origin is added to the CSP connect-src | URL, dev default http://localhost:4000/api |
NEXT_PUBLIC_MEDIA_CDN_ORIGIN | Origin that serves uploaded images and posters; added to img-src, media-src, and images.remotePatterns | origin, no path |
NEXT_PUBLIC_SITE_URL | Canonical origin for metadata, robots.txt, sitemap.xml, llms.txt | origin, default https://vegangrove.org |
SESSION_COOKIE_SECURE | Force the cookie Secure flag; unset means secure in production only | true or false |
src/
app/
(site)/ public pages under the shared header and footer, plus login and signup
app/ member area: the layout validates the session, then feed, messages, friends, companion, settings
api/session/route.ts the only code that sees the session token; sets and clears vg_session
llms.txt/ robots.txt/ sitemap.xml/ route handlers built from src/lib/site.ts
globals.css the --vg-* tokens mapped onto shadcn's variables; no other file defines a color
layout.tsx html, ThemeProvider (dark by default), system font stacks
components/
ui/ shadcn components on Base UI, regenerated with the shadcn CLI, not edited by hand
places/ places-map.tsx (MapLibre, client only) and its dynamic-import island
auth/ app/ events/ media/ forms, member nav, cards, the youtube-nocookie trailer embed
screen.tsx the Context / Action / Support layout every page uses
lib/
api.ts apiFetch, the one HTTP client for browser and server
api.server.ts forwards the cookie as a Bearer header for server components
session.ts cookie name and options, shared with the middleware
loaders.ts public-page loaders that never turn an API outage into a 500
middleware.ts redirects /app/* without a cookie to /login?next=
public/maplibre/ MapLibre worker files, copied on predev and prebuild, gitignored
scripts/ copy-maplibre-worker.mjs
tests/ smoke.spec.ts (Playwright)
next.config.ts CSP, HSTS, nosniff, X-Frame-Options, Referrer-Policy, Permissions-Policy
amplify.yml Amplify build: env allowlist into .env.production, npm ci, next build, .next artifacts
/, /places with the MapLibre map island querying GET /api/places?bbox= as the viewport settles, /places/[slug], and the list and detail pages for /events, /groves, /media, /guides. The last four call routes the API still answers with 501, so they render their empty states until the API lands them./login (password, plus a magic-link request) and /signup post to /api/session, which proxies the API and sets the cookie; DELETE /api/session signs out and revokes the session upstream.middleware.ts bounces /app/* without a cookie, the /app layout validates the session with GET /api/me and bounces expired cookies the same way, and /app/settings shows the member's handle, home area, and public-posts switch with a working sign out./privacy, /terms (an outline), robots.txt, sitemap.xml from the public route list, llms.txt, a not-found page, dark and light themes./app/feed, /app/messages, /app/friends, and /app/companion are shells with disabled controls and the right copy; each carries a TODO(m2) naming the API routes it will call./login until the native and web flows exist./handles/[handle] public-posts view. It is allowed by privacy rule 1 and deliberately not built in the scaffold; when it is, it reads GET /api/handles/:handle/posts and nothing else.'unsafe-inline' for scripts until the app moves to a per-request nonce./messages namespace yet.SameSite=Lax cookie named vg_session, Secure in production, 30 days to match the API's sliding expiry. The session route returns the user, never the token.localStorage, no client code reads the cookie, and api.ts only accepts a token from server code.default-src 'self', allows connect-src to the API origin, OpenFreeMap tiles, and the Bunny CDN only, sets frame-ancestors 'none', and limits frames to youtube-nocookie.com for click-to-load trailers.GET /api/stats. Any new external origin has to be added to the CSP in next.config.ts and justified in the PR.The full promise, data inventory, and threat model: docs.vegangrove.org/privacy.
The code is public so anyone can audit how member data is handled; read CONTRIBUTING.md before opening a PR and SOUL.md before changing a screen. Report vulnerabilities through the process in SECURITY.md, never in a public issue. The LICENSE is proprietary: read it, study it, contribute to it, and do not redistribute it.
Built by Wes Huber · Sibling of The Trick Book · Docs by PokeDocs