spomeni/description.md
2026-06-20 18:17:30 +02:00

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.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)

// 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:

// 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.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