13 KiB
13 KiB
🏛️ 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)
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)
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.localwith publishable + secret keys) - Set up
app/layout.tsxwith<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-inand/sign-uppages using Clerk components - Create
/dashboard— protected page showing user's monument (or "create one" CTA) - Create
/onboardingwizard 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/publishto 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:
- Validate input (text length, image count, template ID)
- Check subdomain availability
- Store in DB (User + Image records)
- Generate QR code via
qrcode.toBuffer(subdomainUrl, { type: 'png' }) - Return QR code as base64 data URL + public monument URL
- (Optional) Store QR code image in blob storage
Phase 3: Subdomain Routing & Middleware (Day 4)
// 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.appas 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:
// 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)
// 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.tsxper 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.appin Vercel project → Domains - Add
monuments.appapex domain - Run
npx prisma migrate deployon 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 |