Skip to content

NxT-Solutions/City-Map-Poster-Generator

Repository files navigation

City Map Poster Generator

Public, no-auth city poster generator built with a Bun + Turborepo monorepo:

  • apps/web: Next.js (App Router) + Tailwind + shadcn/ui-style components + TanStack Query
  • apps/api: Go API + Redis queue + MinIO/S3 artifact storage + pure-Go renderer
  • .docker/local: local Docker compose stack
  • .docker/production: production deploy compose + env template

Preview

Antwerp poster preview

Stack

  • Frontend: Next.js 16, React 19, Tailwind, react-hook-form, zod, framer-motion
  • Backend: Go (chi, go-redis, AWS SDK S3), Redis queue worker, MinIO/S3, Cloudflare Turnstile
  • Rendering: OpenStreetMap vector fetch (Overpass + Nominatim), layered map poster renderer
  • Tooling: Bun workspaces, Turborepo, Biome, TypeScript

Node Runtime (nvm + latest LTS)

nvm install --lts
nvm use --lts
node -v

Expected version: v24.14.0.

Quick Start

  1. Install JS deps:
bun install
  1. Copy environment template:
cp .env.example .env
  1. Start full stack:
docker compose -f .docker/local/compose.yaml -f .docker/local/compose.dev.yaml up -d --build
  1. Open:
  • Web: http://localhost:3000
  • API: http://localhost:8000
  • MinIO Console: http://localhost:9001

Local Dev

Use Docker for backend infra/services and run frontend on host for HMR. Go API and worker also run with container HMR (via air) in this mode:

bun run dev:backend
bun run dev:web

One-command variant:

bun run dev:local

Useful backend commands:

bun run dev:backend:logs
bun run dev:backend:down

Production-like backend (compiled binaries, no HMR):

docker compose -f .docker/local/compose.yaml up -d redis minio api worker

Scripts

bun run dev           # docker backend + web HMR
bun run dev:web
bun run dev:api       # go API (requires local Go toolchain)
bun run dev:worker    # go worker (requires local Go toolchain)

bun run lint          # biome (web) + go vet (api)
bun run check-types   # tsc/next + go test
bun run format        # biome format

Deployments (Hetzner + GHCR)

Deployment files:

  • .docker/production/compose.yaml
  • .docker/production/.env.example
  • .docker/production/images/api.Dockerfile
  • .docker/production/images/web.Dockerfile

Workflows:

  • .github/workflows/production-deploy.yml
    • Trigger: push tag v* (for example v1.0.0) or manual workflow_dispatch
    • Builds and pushes:
      • ghcr.io/<owner>/city-map-api:production
      • ghcr.io/<owner>/city-map-web:production
    • Deploys on production VPS via SSH
  • .github/workflows/release.yml
    • Trigger: push tag v*
    • Creates GitHub Release automatically with generated notes

Required GitHub repository secrets:

  • PRODUCTION_HOST
  • PRODUCTION_USER
  • PRODUCTION_SSH_KEY
  • PRODUCTION_PORT (optional, defaults to 22)
  • PRODUCTION_PATH (example: /opt/city-map-poster-generator)
  • GHCR_USERNAME (for VPS pulls)
  • GHCR_TOKEN (PAT with read:packages for VPS pulls)

Recommended GitHub repository variables (build-time web config):

  • PRODUCTION_NEXT_PUBLIC_API_BASE_URL
  • PRODUCTION_NEXT_PUBLIC_TURNSTILE_SITE_KEY
  • PRODUCTION_NEXT_PUBLIC_SITE_URL

First-time VPS bootstrap:

mkdir -p /opt/city-map-poster-generator/.docker/production

On first deploy run, the production workflow copies .env.example to .env on the VPS and stops once so you can fill real values before re-running.

API Endpoints

  • GET /health
  • GET /v2/themes
  • GET /v2/locations
  • GET /v2/fonts
  • POST /v2/preview
  • POST /v2/jobs
  • GET /v2/jobs/{jobId}
  • GET /v2/jobs/{jobId}/download

Feature Coverage

  • Required: city, country
  • Optional: latitude/longitude overrides, distance, dimensions, font family
  • Units and dimensions:
    • Default size: 30 x 40 cm
    • Centimeters mode: 10 cm min, 200 cm max
    • Inches mode: 5 in min, 80 in max
  • Distance range: 1000 m to 18000 m
  • Themes: all bundled built-in themes
  • Export formats: png, svg, pdf
  • allThemes: generate every theme + ZIP output
  • Preview caching + artifact storage with presigned downloads
  • Live Preview renderer mode: local-wasm by default, automatic fallback to server-fallback when local rendering is unavailable or times out
  • Google Fonts searchable picker for fontFamily
  • Custom Google-font rendering for PNG/PDF when GOOGLE_FONTS_API_KEY is configured

Theme Gallery Previews

Static theme gallery assets remain in:

  • apps/web/public/theme-previews/<theme-id>.svg

CAPTCHA and Rate Limiting

  • Turnstile verification for generation endpoint (/v2/jobs) when CAPTCHA_REQUIRED=true
  • IP rate limits (defaults):
    • Location search: 60 / 10 min
    • Font search: 120 / 10 min
    • Preview and render snapshot: 20 / 10 min
    • Jobs and exports: 3 / 10 min
    • Concurrent jobs: 2
  • Development-only bypass:
    • In non-production (APP_ENV != production), the header Dev settings toggle can reveal a full-width development card above map controls and live preview.
    • That card exposes toggles for disabling API rate limits and CAPTCHA checks for local testing.
    • Production ignores dev bypass headers.

Notes

  • Geocoding uses public Nominatim by default. For production throughput, use a dedicated provider.
  • Generated artifacts are short-lived (24h retention target).