# 🏛️ City Monuments Memories — Platform Architecture & Implementation Plan ## 1. Tech Stack | Layer | Technology | Why | |-------|-----------|-----| | **Framework** | **Next.js 15 (App Router)** | Full-stack React framework — server components, API routes, middleware for subdomain routing | | **Auth** | **Clerk** | Required by spec; handles sign-up/sign-in, user profiles, session management | | **Database** | **PostgreSQL + Prisma ORM** | Relational data model (users, monuments, memories); Prisma for type-safe queries & migrations | | **File Upload** | **UploadThing** | Built for Next.js, handles image resizing, CDN delivery, max 3 files validation | | **QR Code** | **`qrcode` (npm)** | Pure JS, generates PNG/SVG from server, 1M+ weekly downloads | | **Subdomain** | **Next.js Middleware + Vercel Wildcard Domains** | `*.monuments.app` rewrites to dynamic route `[subdomain]/page.tsx` | | **Templates** | **React Server Components + Tailwind CSS** | 3 pre-designed HTML landing page templates as React components; server-rendered for SEO | | **Deployment** | **Vercel** | Native Next.js support, wildcard domains, serverless functions, Edge middleware | | **Storage** | **Vercel Blob / AWS S3** | For uploaded monument images | --- ## 2. Data Model (Prisma Schema) ```prisma model User { id String @id @default(cuid()) clerkId String @unique email String? name String? subdomain String @unique // e.g. "eiffel-tower" templateId Int // 1, 2, or 3 title String? // monument page title description String? // user-written text published Boolean @default(false) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt images Image[] } model Image { id String @id @default(cuid()) url String // CDN URL from UploadThing key String // UploadThing file key order Int // display order (1-3) userId String user User @relation(fields: [userId], references: [id], onDelete: Cascade) createdAt DateTime @default(now()) } ``` --- ## 3. Application Flow ``` ┌─────────────────────────┐ │ monuments.app (landing) │ └────────┬────────────────┘ │ Sign Up via Clerk ▼ ┌─────────────────────────────┐ │ Dashboard / Profile Setup │ │ - Pick template (1/2/3) │ │ - Enter monument name/text │ │ - Upload up to 3 photos │ │ - Choose custom subdomain │ └─────────────┬───────────────┘ │ Publish ▼ ┌────────────────────────────────────────┐ │ 1. Generate subdomain + HTML landing │ │ 2. Generate QR code (→ subdomain URL) │ │ 3. Store everything in DB │ └────────────────┬───────────────────────┘ │ ▼ ┌────────────────────────────────────────┐ │ eiffel-tower.monuments.app │ │ → SSR landing page from template │ │ → Displays text + 3 photos │ └────────────────────────────────────────┘ ``` --- ## 4. Implementation Plan — Phase by Phase ### Phase 0: Project Setup (Day 1) ```bash npx create-next-app@latest city-monuments --typescript --tailwind --app npm install @clerk/nextjs prisma @prisma/client @uploadthing/react uploadthing qrcode qrcode @types/qrcode ``` - [ ] Create Next.js 15 project with TypeScript + Tailwind - [ ] Configure Clerk (`.env.local` with publishable + secret keys) - [ ] Set up `app/layout.tsx` with `` - [ ] Initialize Prisma with PostgreSQL - [ ] Set up UploadThing with file router (max 3 images, 5MB each, image only) ### Phase 1: Authentication & Onboarding (Days 2-3) - [ ] Configure Clerk middleware (`src/middleware.ts`) - [ ] Create `/sign-in` and `/sign-up` pages using Clerk components - [ ] Create `/dashboard` — protected page showing user's monument (or "create one" CTA) - [ ] Create `/onboarding` wizard page: - **Step 1:** Enter monument name and description (text area) - **Step 2:** Upload up to 3 photos (UploadThing dropzone, drag-to-reorder) - **Step 3:** Choose subdomain slug (check availability via API) - **Step 4:** Pick template (3 visual card selections) - [ ] On submit: call `/api/publish` to save everything ### Phase 2: API Routes (Days 3-4) | Route | Method | Purpose | |-------|--------|---------| | `/api/publish` | POST | Saves user data, generates subdomain, returns QR code | | `/api/check-subdomain` | GET | `?slug=xyz` — returns `{available: bool}` | | `/api/monument/[subdomain]` | GET | Public JSON data for a monument page | | `/api/upload` | POST | UploadThing endpoint (auto-generated) | | `/api/user/monument` | GET/PUT | Get/update current user's monument data | **Key logic in `/api/publish`:** 1. Validate input (text length, image count, template ID) 2. Check subdomain availability 3. Store in DB (User + Image records) 4. Generate QR code via `qrcode.toBuffer(subdomainUrl, { type: 'png' })` 5. Return QR code as base64 data URL + public monument URL 6. (Optional) Store QR code image in blob storage ### Phase 3: Subdomain Routing & Middleware (Day 4) ```typescript // src/middleware.ts import { clerkMiddleware } from '@clerk/nextjs/server' import { NextResponse } from 'next/server' import type { NextRequest } from 'next/server' export default clerkMiddleware(async (auth, req: NextRequest) => { const url = req.nextUrl const hostname = req.headers.get('host') || '' // Extract subdomain (e.g. "eiffel-tower" from "eiffel-tower.monuments.app") const subdomain = hostname .replace('www.', '') .replace('.monuments.app', '') // If it's a subdomain (not apex), rewrite to the dynamic page if (subdomain && !subdomain.includes('.') && hostname.includes('.monuments.app')) { url.pathname = `/${subdomain}` return NextResponse.rewrite(url) } return NextResponse.next() }) export const config = { matcher: ['/((?!_next|api|static|.*\\..*).*)'] } ``` - [ ] Create `app/[subdomain]/page.tsx` — fetches monument data, renders template - [ ] Handle 404 for unknown subdomains - [ ] Configure Vercel: add `*.monuments.app` as wildcard domain ### Phase 4: Template System (Day 5) Create 3 React Server Components that render pure HTML landing pages: | Template | Vibe | Layout | |----------|------|--------| | **Template 1 — "Classic"** | Clean, serif font, historical archive feel | Header image hero → description → 3-column photo grid | | **Template 2 — "Modern"** | Bold, full-bleed images, sans-serif | Full-screen photo carousel → floating text overlay → gallery | | **Template 3 — "Minimal"** | Whitespace-heavy, single-column, journal style | Title → paragraph → horizontal image strip → footer | All templates: - Are **React Server Components** (no JS shipped for visitors!) - Use **Tailwind CSS** for styling - Are **SEO-friendly** with proper `` meta tags - Render **optimized images** via Next.js `` component - Include the **QR code badge** in the footer ("Scan to visit this monument") - Support **Open Graph** metadata for social sharing Example template rendering function: ```typescript // src/lib/templates.tsx export function renderTemplate(templateId: number, data: MonumentData) { switch (templateId) { case 1: return case 2: return case 3: return } } ``` ### Phase 5: QR Code Generation (Day 5) ```typescript // src/lib/qrcode.ts import QRCode from 'qrcode' export async function generateMonumentQR(subdomain: string): Promise { const url = `https://${subdomain}.monuments.app` const qrBuffer = await QRCode.toBuffer(url, { type: 'png', width: 400, margin: 2, color: { dark: '#1a1a2e', light: '#ffffff' } }) // Store buffer to Vercel Blob and return public URL // OR return as data URL for download return `data:image/png;base64,${qrBuffer.toString('base64')}` } ``` - [ ] Generate QR on publish - [ ] Show QR in dashboard for download (PNG) - [ ] Optionally embed QR on the monument page footer ### Phase 6: Dashboard & User Experience (Days 6-7) - [ ] Dashboard: view/edit monument details - [ ] Template preview (live switching) - [ ] Image reorder/delete - [ ] QR download button - [ ] Share link + copy-to-clipboard - [ ] "Unpublish" button - [ ] Loading skeletons, error states, empty states ### Phase 7: Polish & Production (Day 8+) - [ ] SEO — dynamic metadata per monument page (generateMetadata) - [ ] Analytics — Vercel Analytics or Plausible - [ ] Rate limiting on API routes - [ ] Image optimization (UploadThing does auto-resize/webp) - [ ] Custom 404 page for unknown subdomains - [ ] Proper error boundaries - [ ] Loading UI (`loading.tsx` per route) --- ## 5. Folder Structure ``` src/ ├── app/ │ ├── layout.tsx # Root layout with ClerkProvider │ ├── page.tsx # Landing page (monuments.app) │ ├── [subdomain]/ │ │ └── page.tsx # Dynamic monument landing page (SSR) │ ├── sign-in/[[...sign-in]]/page.tsx │ ├── sign-up/[[...sign-up]]/page.tsx │ ├── dashboard/ │ │ ├── page.tsx # Dashboard home │ │ └── preview/ │ │ └── page.tsx # Live template preview │ └── api/ │ ├── publish/route.ts │ ├── check-subdomain/route.ts │ ├── user/monument/route.ts │ └── uploadthing/route.ts ├── components/ │ ├── templates/ │ │ ├── TemplateClassic.tsx │ │ ├── TemplateModern.tsx │ │ └── TemplateMinimal.tsx │ ├── OnboardingWizard.tsx │ ├── ImageUploader.tsx │ ├── SubdomainPicker.tsx │ ├── TemplatePicker.tsx │ └── QRDisplay.tsx ├── lib/ │ ├── prisma.ts # Prisma client singleton │ ├── templates.tsx # Template registry │ ├── qrcode.ts # QR generation logic │ └── uploadthing.ts # UploadThing config ├── middleware.ts # Clerk + subdomain routing ├── types/ │ └── index.ts # Shared TypeScript types └── styles/ └── globals.css ``` --- ## 6. Deployment Checklist (Vercel) - [ ] Set environment variables in Vercel dashboard - [ ] Configure `*.monuments.app` in Vercel project → Domains - [ ] Add `monuments.app` apex domain - [ ] Run `npx prisma migrate deploy` on production DB - [ ] Configure Clerk production URLs (from localhost → monuments.app) - [ ] Set up UploadThing production env vars - [ ] Enable Vercel Analytics --- ## 7. Future Enhancements (v2) - **Multi-language** — i18n support for monument descriptions - **Audio guide** — embed audio narration per monument - **Map integration** — pin monuments on a city map - **Social sharing** — share to Instagram/TikTok with QR - **Analytics per monument** — visit counter, scan counter - **Custom domain** — allow users to bring their own domain (e.g., `eiffeltower.com`) - **Admin panel** — moderate content, flagged monuments --- ## 8. Key Technical Decisions Summary | Decision | Choice | Reasoning | |----------|--------|-----------| | Auth | Clerk | Specified requirement; great DX with Next.js | | Subdomain routing | Middleware rewrite | Clean URLs, no path-based routing needed | | Image storage | UploadThing | Built-in Next.js integration, CDN, auto-optimization | | QR generation | `qrcode` npm | Pure JS, works server-side, PNG/SVG output | | DB ORM | Prisma | Type-safe, auto-generated types, easy migrations | | Templates | React Server Components | Zero client JS for public pages, fast SSR | | Styling | Tailwind CSS | Rapid prototyping, consistent design system | | Hosting | Vercel | One-command deploy, wildcard domains, Edge middleware |