Add PRD
This commit is contained in:
parent
1f1d7551fb
commit
75974f1750
1 changed files with 422 additions and 0 deletions
422
PRD-cms-functionality.md
Normal file
422
PRD-cms-functionality.md
Normal 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
|
||||
Loading…
Reference in a new issue