diff --git a/PRD-cms-functionality.md b/PRD-cms-functionality.md new file mode 100644 index 0000000..34482b1 --- /dev/null +++ b/PRD-cms-functionality.md @@ -0,0 +1,422 @@ +# Product Requirements Document: Multi-Content CMS + +## Overview +Add a comprehensive CMS to the personal portfolio site to manage multiple content types: Projects (Work section), Posts (Universe section), and Photos/Albums (Photos and Universe sections). + +## Goals +- Enable dynamic content creation across all site sections +- Provide rich text editing for long-form content (BlockNote) +- Support different content types with appropriate editing interfaces +- Store all content in PostgreSQL database (Railway-compatible) +- Display content instantly after publishing +- Maintain the existing design aesthetic + +## Technical Constraints +- **Hosting**: Railway (no direct file system access) +- **Database**: PostgreSQL add-on available +- **Framework**: SvelteKit +- **Editor**: BlockNote for rich text, custom forms for structured data + +## Core Features + +### 1. Database Schema +```sql +-- Projects table (for /work) +CREATE TABLE projects ( + id SERIAL PRIMARY KEY, + slug VARCHAR(255) UNIQUE NOT NULL, + title VARCHAR(255) NOT NULL, + subtitle VARCHAR(255), + description TEXT, + year INTEGER NOT NULL, + client VARCHAR(255), + role VARCHAR(255), + technologies JSONB, -- Array of tech stack + featured_image VARCHAR(500), + gallery JSONB, -- Array of image URLs + external_url VARCHAR(500), + case_study_content JSONB, -- BlockNote JSON format + display_order INTEGER DEFAULT 0, + status VARCHAR(50) DEFAULT 'draft', + published_at TIMESTAMP, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +-- Posts table (for /universe) +CREATE TABLE posts ( + id SERIAL PRIMARY KEY, + slug VARCHAR(255) UNIQUE NOT NULL, + title VARCHAR(255) NOT NULL, + content JSONB NOT NULL, -- BlockNote JSON format + excerpt TEXT, + featured_image VARCHAR(500), + tags JSONB, -- Array of tags + status VARCHAR(50) DEFAULT 'draft', + published_at TIMESTAMP, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +-- Albums table +CREATE TABLE albums ( + id SERIAL PRIMARY KEY, + slug VARCHAR(255) UNIQUE NOT NULL, + title VARCHAR(255) NOT NULL, + description TEXT, + date DATE, + location VARCHAR(255), + cover_photo_id INTEGER REFERENCES photos(id), + status VARCHAR(50) DEFAULT 'draft', + show_in_universe BOOLEAN DEFAULT false, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +-- Photos table +CREATE TABLE photos ( + id SERIAL PRIMARY KEY, + album_id INTEGER REFERENCES albums(id) ON DELETE CASCADE, + filename VARCHAR(255) NOT NULL, + url VARCHAR(500) NOT NULL, + thumbnail_url VARCHAR(500), + width INTEGER, + height INTEGER, + exif_data JSONB, + caption TEXT, + display_order INTEGER DEFAULT 0, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +-- Media table (general uploads) +CREATE TABLE media ( + id SERIAL PRIMARY KEY, + filename VARCHAR(255) NOT NULL, + mime_type VARCHAR(100) NOT NULL, + size INTEGER NOT NULL, + url TEXT NOT NULL, + thumbnail_url TEXT, + width INTEGER, + height INTEGER, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); +``` + +### 2. Image Handling Strategy + +#### For Posts (BlockNote Integration) +- **Storage**: Images embedded in posts are stored in the `media` table +- **BlockNote Custom Block**: Create custom image block that: + - Uploads to `/api/media/upload` on drop/paste + - Returns media ID and URL + - Stores reference as `{ type: "image", mediaId: 123, url: "...", alt: "..." }` +- **Advantages**: + - Images flow naturally with content + - Can add captions, alt text inline + - Supports drag-and-drop repositioning + - No orphaned images (tracked by mediaId) + +#### For Projects +- **Featured Image**: Single image reference stored in `featured_image` field +- **Gallery Images**: Array of media IDs stored in `gallery` JSONB field +- **Case Study Content**: Uses same BlockNote approach as Posts +- **Storage Pattern**: + ```json + { + "featured_image": "https://cdn.../image1.jpg", + "gallery": [ + { "mediaId": 123, "url": "...", "caption": "..." }, + { "mediaId": 124, "url": "...", "caption": "..." } + ] + } + ``` + +#### Media Table Enhancement +```sql +-- Add content associations to media table +ALTER TABLE media ADD COLUMN used_in JSONB DEFAULT '[]'; +-- Example: [{ "type": "post", "id": 1 }, { "type": "project", "id": 3 }] +``` + +### 3. Content Type Editors +- **Projects**: Form-based editor with: + - Metadata fields (title, year, client, role) + - Technology tag selector + - Featured image picker (opens media library) + - Gallery manager (grid view with reordering) + - Optional BlockNote editor for case studies +- **Posts**: Full BlockNote editor with: + - Custom image block implementation + - Drag-and-drop image upload + - Media library integration + - Image optimization on upload + - Auto-save including image references +- **Photos/Albums**: Media-focused interface with: + - Bulk photo upload + - Drag-and-drop ordering + - EXIF data extraction + - Album metadata editing + +### 4. BlockNote Custom Image Block + +```typescript +// Custom image block schema for BlockNote +const ImageBlock = { + type: "image", + content: { + mediaId: number, + url: string, + thumbnailUrl?: string, + alt?: string, + caption?: string, + width?: number, + height?: number, + alignment?: "left" | "center" | "right" | "full" + } +} + +// Example BlockNote content with images +{ + "blocks": [ + { "type": "heading", "content": "Project Overview" }, + { "type": "paragraph", "content": "This project..." }, + { + "type": "image", + "content": { + "mediaId": 123, + "url": "https://cdn.../full.jpg", + "thumbnailUrl": "https://cdn.../thumb.jpg", + "alt": "Project screenshot", + "caption": "The main dashboard view", + "alignment": "full" + } + }, + { "type": "paragraph", "content": "As shown above..." } + ] +} +``` + +### 5. Media Library Component + +- **Modal Interface**: Opens from BlockNote toolbar or form fields +- **Features**: + - Grid view of all uploaded media + - Search by filename + - Filter by type (image/video) + - Filter by usage (unused/used) + - Upload new files + - Select existing media +- **Returns**: Media object with ID and URLs + +### 6. Image Processing Pipeline + +1. **Upload**: User drops/selects image +2. **Processing**: + - Generate unique filename + - Create multiple sizes: + - Thumbnail (300px) + - Medium (800px) + - Large (1600px) + - Original + - Extract metadata (dimensions, EXIF) +3. **Storage**: Upload to CDN +4. **Database**: Create media record with all URLs +5. **Association**: Update `used_in` when embedded + +### 7. API Endpoints +```typescript +// Projects +GET /api/projects +POST /api/projects +GET /api/projects/[slug] +PUT /api/projects/[id] +DELETE /api/projects/[id] + +// Posts +GET /api/posts +POST /api/posts +GET /api/posts/[slug] +PUT /api/posts/[id] +DELETE /api/posts/[id] + +// Albums & Photos +GET /api/albums +POST /api/albums +GET /api/albums/[slug] +PUT /api/albums/[id] +DELETE /api/albums/[id] +POST /api/albums/[id]/photos +DELETE /api/photos/[id] +PUT /api/photos/[id]/order + +// Media upload +POST /api/media/upload +POST /api/media/bulk-upload +GET /api/media // Browse with filters +DELETE /api/media/[id] // Delete if unused +GET /api/media/[id]/usage // Check where media is used +``` + +### 8. Media Management & Cleanup + +#### Orphaned Media Prevention +- **Reference Tracking**: `used_in` field tracks all content using each media item +- **On Save**: Update media associations when content is saved +- **On Delete**: Remove associations when content is deleted +- **Cleanup Task**: Periodic job to identify truly orphaned media + +#### BlockNote Integration Details +```javascript +// Custom upload handler for BlockNote +const handleImageUpload = async (file) => { + const formData = new FormData(); + formData.append('file', file); + formData.append('context', 'post'); // or 'project' + + const response = await fetch('/api/media/upload', { + method: 'POST', + body: formData + }); + + const media = await response.json(); + + // Return format expected by BlockNote + return { + mediaId: media.id, + url: media.url, + thumbnailUrl: media.thumbnail_url, + width: media.width, + height: media.height + }; +}; +``` + +### 9. Admin Interface +- **Route**: `/admin` (completely separate from public routes) +- **Dashboard**: Overview of all content types +- **Content Lists**: + - Projects with preview thumbnails + - Posts with publish status + - Albums with photo counts +- **Content Editors**: Type-specific editing interfaces +- **Media Library**: Browse all uploaded files + +### 10. Public Display Integration +- **Work page**: Dynamic project grid from database +- **Universe page**: + - Mixed feed of posts and albums (marked with `show_in_universe`) + - Chronological ordering + - Different card styles for posts vs photo albums +- **Photos page**: Album grid with masonry layout +- **Individual pages**: `/work/[slug]`, `/universe/[slug]`, `/photos/[slug]` + +## Implementation Phases + +### Phase 1: Foundation (Week 1) +- Set up PostgreSQL database with full schema +- Create database connection utilities +- Implement media upload infrastructure +- Build admin route structure and navigation + +### Phase 2: Content Types (Week 2-3) +- **Posts**: BlockNote integration, CRUD APIs +- **Projects**: Form builder, gallery management +- **Albums/Photos**: Bulk upload, EXIF extraction +- Create content type list views in admin + +### Phase 3: Public Display (Week 4) +- Replace static project data with dynamic +- Build Universe mixed feed (posts + albums) +- Update Photos page with dynamic albums +- Implement individual content pages + +### Phase 4: Polish & Optimization (Week 5) +- Image optimization and CDN caching +- Admin UI improvements +- Search and filtering +- Performance optimization + +## Technical Decisions + +### Database Choice: PostgreSQL +- Native JSON support for BlockNote content +- Railway provides managed PostgreSQL +- Familiar, battle-tested solution + +### Media Storage Options +1. **Cloudinary** (Recommended) + - Free tier sufficient for personal use + - Automatic image optimization + - Easy API integration + +2. **AWS S3** + - More control but requires AWS account + - Additional complexity for signed URLs + +### Image Integration Summary +- **Posts**: Use BlockNote's custom image blocks with inline placement +- **Projects**: + - Featured image: Single media reference + - Gallery: Array of media IDs with ordering + - Case studies: BlockNote blocks (same as posts) +- **Albums**: Direct photos table relationship +- **Storage**: All images go through media table for consistent handling +- **Association**: Track usage with `used_in` JSONB field to prevent orphans + +### Authentication (Future) +- Initially: No auth (rely on obscure admin URL) +- Future: Add simple password protection or OAuth + +## Development Checklist + +### Infrastructure +- [ ] Set up PostgreSQL on Railway +- [ ] Create database schema and migrations +- [ ] Set up Cloudinary/S3 for media storage +- [ ] Configure environment variables + +### Dependencies +- [ ] `@blocknote/core` & `@blocknote/react` +- [ ] `@prisma/client` or `postgres` driver +- [ ] `exifr` for EXIF data extraction +- [ ] `sharp` or Cloudinary SDK for image processing +- [ ] Form validation library (Zod/Valibot) + +### Admin Interface +- [ ] Admin layout and navigation +- [ ] Content type switcher +- [ ] List views for each content type +- [ ] Form builders for Projects +- [ ] BlockNote wrapper for Posts +- [ ] Photo uploader with drag-and-drop +- [ ] Media library browser + +### APIs +- [ ] CRUD endpoints for all content types +- [ ] Media upload with progress +- [ ] Bulk operations (delete, publish) +- [ ] Search and filtering endpoints + +### Public Display +- [ ] Dynamic Work page +- [ ] Mixed Universe feed +- [ ] Photos masonry grid +- [ ] Individual content pages +- [ ] SEO meta tags + +## Open Questions +1. Should Albums have a "featured" flag for homepage display? +2. Do we want version history for content? +3. Should photos support individual publishing vs entire albums? +4. How should we handle project case study layouts (templates)? +5. Do we need scheduled publishing? +6. Should Universe support different post types (link posts, quotes)? + +## Success Metrics +- Can create and publish any content type within 2-3 minutes +- Content appears on site immediately after publishing +- Bulk photo upload handles 50+ images smoothly +- No accidental data loss (auto-save works reliably) +- Page load performance remains fast (<2s) +- Admin interface works well on tablet/desktop +- Media uploads show progress and handle failures gracefully \ No newline at end of file