A production travel blog platform built around the live Next.js app in web/. It supports rich publishing, expiring previews, post revisions, maps, galleries, comments, bookmarks, public accounts, a public wishlist, GitHub-based admin tooling, newsletters, and production SEO for jsquaredadventures.com.
Live Site: jsquaredadventures.com
| Layer | Technology |
|---|---|
| App | Next.js 16 App Router |
| Language | TypeScript (strict: true) |
| Database | Turso / libSQL + Drizzle ORM |
| Public Auth | Supabase Auth |
| Admin Auth | Auth.js + GitHub OAuth |
| Storage | Cloudinary |
| Routing / geocoding | Geoapify |
| Resend (newsletter + comment notifications) | |
| Rate limiting | Upstash Redis in deployed envs, in-memory locally/tests |
| Observability | Sentry (optional) |
| Styling | TailwindCSS 4 + CSS variables |
| Testing | Vitest + Playwright |
| Deployment | Vercel |
| Runtime / package manager | pnpm (primary), bun (dev-only for bunx) |
jsquared_blog/
├── web/ # Active Next.js application
├── docs/ # Plans, handoffs, deployment, workflow notes
├── testImages/ # Test image fixtures
└── .sentryclirc # Sentry CLI org/project config
- pnpm
- Node.js >= 22.13
- Turso database credentials
- Supabase project credentials
- Cloudinary credentials
- GitHub OAuth app for admin auth
pnpm installCopy web/.env.example to web/.env.local and fill in the values your environment needs.
For unit tests, Vitest loads web/.env.test.local automatically via the env-loader. Copy the example file to get started (no real credentials needed for unit tests):
cp web/.env.test.example web/.env.test.localRequired app/runtime variables:
TURSO_DATABASE_URL=...
TURSO_AUTH_TOKEN=...
SUPABASE_URL=...
SUPABASE_ANON_KEY=...
NEXT_PUBLIC_SUPABASE_URL=...
NEXT_PUBLIC_SUPABASE_ANON_KEY=...
NEXT_PUBLIC_SITE_URL=...
AUTH_SECRET=...
AUTH_GITHUB_ID=...
AUTH_GITHUB_SECRET=...
AUTH_ADMIN_GITHUB_IDS=...
CLOUDINARY_CLOUD_NAME=...
CLOUDINARY_API_KEY=...
CLOUDINARY_API_SECRET=...Optional integrations and operational variables:
NEXT_PUBLIC_STADIA_MAPS_API_KEY=...
ROUTING_PROVIDER=geoapify
GEOCODING_PROVIDER=geoapify
GEOAPIFY_API_KEY=...
ROUTE_PLANNER_TIMEOUT_MS=10000
GEOCODING_TIMEOUT_MS=8000
ROUTE_PLANNER_MAX_STOPS=10
RESEND_API_KEY=...
RESEND_FROM_EMAIL=...
COMMENT_NOTIFICATION_TO_EMAIL=...
RESEND_NEWSLETTER_SEGMENT_ID=...
UPSTASH_REDIS_REST_URL=...
UPSTASH_REDIS_REST_TOKEN=...
CRON_SECRET=...
NEXT_PUBLIC_SENTRY_DSN=...
SENTRY_AUTH_TOKEN=...
SUPABASE_SERVICE_ROLE_KEY=... # tooling / seed / import scriptsNotes:
CRON_SECRETis optional only for local loopback development, but required for deployed cron routes.- Upstash credentials are required in deployed environments because rate limiting fails closed there.
- Newsletter signup safely returns a non-fatal skipped result when Resend newsletter config is absent.
SENTRY_AUTH_TOKENis only needed when uploading source maps during production builds.- Optional Playwright/E2E helper variables also live in
web/.env.example;pnpm run seed:e2ewrites managed fixture values toweb/.env.test.local.
cd web
pnpm run devApp runs at http://localhost:3000.
Admin:
cd web
pnpm run e2e:capture-admin-statePublic fixture:
cd web
pnpm run seed:e2e
pnpm run e2e:capture-public-state- Rich text editor (Tiptap) with canonical JSON storage
- Safe derived HTML/plain-text rendering for prose
- Legacy HTML migration support behind allowlist sanitization
- Draft / Published / Scheduled post status
- Expiring preview links for unpublished content
- Post revision history with admin restore
- Multiple images per post with carousel gallery
- Focal point editing and EXIF metadata capture for gallery assets
- Cloudinary-backed optimized image delivery (
f_auto,q_auto) - Categories, tags, series, location metadata, song metadata, and previews
- Infinite scroll with Intersection Observer
- Server-side search (title, description, category, tags)
- Seasonal homepage hero and grouped feed sections
- Reading progress, related posts, breadcrumbs, and reduced-motion support
- Map/location blocks for geotagged stories
- Optional per-story song metadata block with Spotify embeds or external listen links
- Per-post view tracking with cookie dedupe
- Public wishlist page with map/list rendering for future destinations
- Visited wishlist stops excluded by default, with an opt-in include-visited toggle
- Admin wishlist editor supports place names, autocompleted locations, optional info links, short descriptions, and public/private visibility
- New wishlist entries default to public visibility
- Admins can check off a wishlist item into the post flow while keeping it visible until the linked post is actually published
- Linked published posts are excluded from public wishlist surfaces automatically
- Display name and avatar customization
- Letter avatars, preset icons, or uploaded avatars
- Theme preference persistence
- Account settings page
- Admin access is allowlisted by GitHub provider user id.
- Persisted admin accounts stay distinct per GitHub provider user id.
- Session
githubLogincontinues to come from the live GitHub profile for operator attribution. - Persisted admin display/avatar identity intentionally resolves to shared site-owner branding instead of each admin's live GitHub avatar.
- Comments with replies, likes, and owner deletion
- Admin comment moderation
- Bookmarks for signed-in public users
- Share buttons and reading time estimates
- Newsletter signup
- Dynamic sitemap.xml
- RSS feed (
/feed.xml) - Open Graph + Twitter Cards
- JSON-LD structured data
- Proper heading hierarchy and processed table-of-contents headings
cd web
pnpm run dev
pnpm run lint
pnpm run lint:fix
pnpm exec tsc --noEmit
pnpm run test
pnpm run test:watch
pnpm run test:e2e
pnpm run build
pnpm run build:analyze
# Drizzle / Turso
pnpm run db:generate
pnpm run db:migrate
pnpm run db:import:supabase
# E2E helpers
pnpm run e2e:capture-admin-state
pnpm run e2e:capture-public-state
pnpm run seed:e2e
# Content helpers
pnpm run seed:wishlist # Seed example wishlist destinations
pnpm exec tsx ./scripts/seed-series-categories.ts # Seed series/categories
pnpm exec tsx ./scripts/seed-rich-content.ts # Seed sample rich content
pnpm exec tsx ./scripts/seed-locations.ts # Seed location metadataSee docs/DISASTER-RECOVERY.md for recovering accidentally deleted posts via Turso point-in-time branching.
Apply Drizzle migrations against Turso from web/:
cd web
pnpm run db:migrateOptional local content helpers:
cd web
pnpm exec tsx ./scripts/seed-series-categories.ts
pnpm exec tsx ./scripts/seed-rich-content.tsusers/auth_accounts- Local authorization + provider-linked identitiesprofiles- Public profile data, avatars, theme preferenceposts- Blog posts, scheduling, content format, view count, location and song metadatapost_preview_tokens- Expiring preview access tokenspost_revisions- Admin revision history and restore snapshotsmedia_assets- Uploaded media metadata and EXIF fieldspost_images- Gallery ordering, focal points, captionspost_links- External links per postseries- Linked post series metadataseasons- Seasonal homepage hero groupingcomments- Post comments with visibility / moderation statecomment_likes- Comment likes (one per user)post_bookmarks- Saved posts per public usercategories,tags,post_tags- Taxonomywishlist_places- Travel wishlist entries with location datasessions- (managed by Auth.js/Turso adapter)
The live app deploys from web/ to Vercel. Use web/.env.example, web/src/lib/env.ts, and web/next.config.ts as the current source of truth for runtime, build, and observability variables.
For repo-specific Vercel CLI usage, troubleshooting, and manual deploy/inspection commands, see docs/VERCEL-CLI-REFERENCE.md.
GET / # Homepage / feed
GET /posts/[slug] # Published post detail
GET /preview/[id]?token=... # Admin or token preview
GET /map # World map of posts
GET /wishlist # Public travel wishlist
GET /admin/wishlist # Admin wishlist editor
GET /admin/tags # Admin tag management
GET /admin/seasons # Admin season management
GET /category/[category] # Category feed
GET /tag/[slug] # Tag feed
GET /series/[slug] # Series detail
GET /bookmarks # Signed-in saved posts
GET /account # Public account page
GET /settings # Theme/settings page
GET /api/posts # Paginated published posts
GET /api/posts/[postId]/comments # List comments
POST /api/posts/[postId]/comments # Add comment or reply (public auth)
GET /api/posts/[postId]/bookmark # Bookmark status
POST /api/posts/[postId]/bookmark # Toggle bookmark
POST /api/posts/[postId]/view # Increment deduped view count
POST /api/comments/[commentId]/like
DELETE /api/comments/[commentId]
GET /api/bookmarks
GET /api/account/profile
PATCH /api/account/profile
POST /api/account/avatar
POST /api/newsletter
GET /api/admin/posts
POST /api/admin/posts/clone
POST /api/admin/posts/preview
POST /api/admin/song-preview # Admin-only song metadata preview/autofill
POST /api/admin/posts/bulk-status
GET /api/admin/posts/warnings # Content warning checks
GET /api/admin/location-autocomplete # Location autocomplete
POST /api/admin/comments/moderate
GET /api/admin/posts/[postId]/comments
GET /api/admin/posts/[postId]/revisions
GET /api/admin/posts/[postId]/revisions/[revisionId]
POST /api/admin/posts/[postId]/revisions/[revisionId]/restore
GET /api/admin/series/[seriesId]/part-numbers
POST /api/admin/uploads/images
GET /api/cron/publish-scheduled
GET /api/cron/keep-supabase-awake
GET /sitemap.xml
GET /feed.xml
| Check | Tool | Command |
|---|---|---|
| Type-check | TypeScript | cd web && pnpm exec tsc --noEmit |
| Lint | ESLint | cd web && pnpm run lint |
| Unit tests | Vitest | cd web && pnpm run test |
| E2E smoke | Playwright | cd web && pnpm run test:e2e |
| Admin auth capture | Playwright | cd web && pnpm run e2e:capture-admin-state |
| Public auth capture | Playwright | cd web && pnpm run e2e:capture-public-state |
| CI | Vercel | Auto-deploys on push to main |
Input validation uses Zod at API and action trust boundaries.
Content sanitization uses an allowlist-based sanitize-html pipeline before rendering prose. sanitize-html is pinned to exact 2.17.5 to avoid the XSS vulnerability in 2.17.3. Comment content is additionally stripped of HTML tags via Zod .transform(stripHtmlTags) before storage.
Security headers ship from web/src/proxy.ts (dynamic CSP/nonces) and web/next.config.ts (remaining static headers).
Middleware (web/src/proxy.ts) handles CSRF protection on state-changing admin requests and dynamic CSP headers with per-request nonces. The file is named proxy.ts but exports a proxy() function and Next.js middleware config matcher.
| Document | Purpose |
|---|---|
| docs/ARCHITECTURE.md | System architecture, auth flow, data model, API routes, deployment |
| docs/CODING.md | Codemap: file patterns, conventions, component organization |
| docs/SETUP.md | Environment setup, database config, running tests, integrations |
| docs/KNOWLEDGE_BASE.md | Living reference of project-specific gotchas (SQLite, Drizzle, Turso, React 19, Next.js). Agents add to this when they discover new patterns. |
| docs/IMPROVEMENTS.md | Prioritized backlog of tech debt, features, security, and perf work |
| docs/DISASTER-RECOVERY.md | Post deletion recovery via Turso PITR |
| docs/VERCEL-CLI-REFERENCE.md | Vercel CLI operational reference |
| docs/STYLEGUIDE.md | Design tokens, component patterns, and conventions |
| docs/ROADMAP.md | Active branch tracking and implementation status |
| docs/CHANGELOG.md | Version history and completed work log |
MIT License - see LICENSE