# 🏛️ 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 `<ClerkProvider>`
- [ ] 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 `<head>` meta tags
- Render **optimized images** via Next.js `<Image>` 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 <TemplateClassic data={data} />
    case 2: return <TemplateModern data={data} />
    case 3: return <TemplateMinimal data={data} />
  }
}
```

### Phase 5: QR Code Generation (Day 5)

```typescript
// src/lib/qrcode.ts
import QRCode from 'qrcode'

export async function generateMonumentQR(subdomain: string): Promise<string> {
  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 |
