This commit is contained in:
Justin Edmund 2025-05-26 18:41:06 -07:00
parent 1f1d7551fb
commit 75974f1750

422
PRD-cms-functionality.md Normal file
View file

@ -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