jedmund-svelte/PRD-enhanced-tag-system.md

16 KiB
Raw Blame History

Product Requirements Document: Enhanced Tag System

Overview

Upgrade the current JSON-based tag system to a relational database model with advanced tagging features including tag filtering, related posts, tag management, and an improved tag input UI with typeahead functionality.

Goals

  • Enable efficient querying and filtering of posts by tags
  • Provide tag management capabilities for content curation
  • Show related posts based on shared tags
  • Implement intuitive tag input with typeahead and keyboard shortcuts
  • Build analytics and insights around tag usage
  • Maintain backward compatibility during migration

Technical Constraints

  • Framework: SvelteKit with Svelte 5 runes mode
  • Database: PostgreSQL with Prisma ORM
  • Hosting: Railway (existing infrastructure)
  • Design System: Use existing admin component library
  • Performance: Tag operations should be sub-100ms

Current State vs Target State

Current Implementation

  • Tags stored as JSON arrays: tags: ['announcement', 'meta', 'cms']
  • Simple display-only functionality
  • No querying capabilities
  • Manual tag input with Add button

Target Implementation

  • Relational many-to-many tag system
  • Full CRUD operations for tags
  • Advanced filtering and search
  • Typeahead tag input with keyboard navigation
  • Tag analytics and management interface

Database Schema Changes

New Tables

-- Tags table
CREATE TABLE tags (
  id SERIAL PRIMARY KEY,
  name VARCHAR(100) UNIQUE NOT NULL,
  slug VARCHAR(100) UNIQUE NOT NULL,
  description TEXT,
  color VARCHAR(7), -- Hex color for tag styling
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- Post-Tag junction table
CREATE TABLE post_tags (
  id SERIAL PRIMARY KEY,
  post_id INTEGER REFERENCES posts(id) ON DELETE CASCADE,
  tag_id INTEGER REFERENCES tags(id) ON DELETE CASCADE,
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  UNIQUE(post_id, tag_id)
);

-- Tag usage analytics (optional)
CREATE TABLE tag_analytics (
  id SERIAL PRIMARY KEY,
  tag_id INTEGER REFERENCES tags(id) ON DELETE CASCADE,
  usage_count INTEGER DEFAULT 1,
  last_used TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

Prisma Schema Updates

model Tag {
  id          Int       @id @default(autoincrement())
  name        String    @unique @db.VarChar(100)
  slug        String    @unique @db.VarChar(100)
  description String?   @db.Text
  color       String?   @db.VarChar(7)  // Hex color
  createdAt   DateTime  @default(now())
  updatedAt   DateTime  @updatedAt
  
  // Relations
  posts       PostTag[]
  
  @@index([name])
  @@index([slug])
}

model PostTag {
  id        Int      @id @default(autoincrement())
  postId    Int
  tagId     Int
  createdAt DateTime @default(now())
  
  // Relations
  post      Post     @relation(fields: [postId], references: [id], onDelete: Cascade)
  tag       Tag      @relation(fields: [tagId], references: [id], onDelete: Cascade)
  
  @@unique([postId, tagId])
  @@index([postId])
  @@index([tagId])
}

// Update existing Post model
model Post {
  // ... existing fields
  tags      PostTag[]  // Replace: tags Json?
}

Core Features

1. Tag Management Interface

Admin Tag Manager (/admin/tags)

  • Tag List View

    • DataTable with tag name, usage count, created date
    • Search and filter capabilities
    • Bulk operations (delete, merge, rename)
    • Color coding and visual indicators
  • Tag Detail/Edit View

    • Edit tag name, description, color
    • View all posts using this tag
    • Usage analytics and trends
    • Merge with other tags functionality

Tag Analytics Dashboard

  • Usage Statistics

    • Most/least used tags
    • Tag usage trends over time
    • Orphaned tags (no posts)
    • Tag co-occurrence patterns
  • Tag Insights

    • Suggested tag consolidations
    • Similar tags detection
    • Tag performance metrics

2. Enhanced Tag Input Component (TagInput.svelte)

Features

  • Typeahead Search: Real-time search of existing tags
  • Keyboard Navigation: Arrow keys to navigate suggestions
  • Instant Add: Press Enter to add tag without button click
  • Visual Feedback: Highlight matching text in suggestions
  • Tag Validation: Prevent duplicates and invalid characters
  • Quick Actions: Backspace to remove last tag

Component API

interface TagInputProps {
  tags: string[] | Tag[]           // Current tags
  suggestions?: Tag[]              // Available tags for typeahead
  placeholder?: string             // Input placeholder text
  maxTags?: number                // Maximum number of tags
  allowNew?: boolean              // Allow creating new tags
  size?: 'small' | 'medium' | 'large'
  disabled?: boolean
  onTagAdd?: (tag: Tag) => void
  onTagRemove?: (tag: Tag) => void
  onTagCreate?: (name: string) => void
}

Svelte 5 Implementation

<script lang="ts">
  let {
    tags = $bindable([]),
    suggestions = [],
    placeholder = "Add tags...",
    maxTags = 10,
    allowNew = true,
    size = 'medium',
    disabled = false,
    onTagAdd,
    onTagRemove,
    onTagCreate
  }: TagInputProps = $props()

  let inputValue = $state('')
  let showSuggestions = $state(false)
  let selectedIndex = $state(-1)
  let inputElement: HTMLInputElement

  // Filtered suggestions based on input
  let filteredSuggestions = $derived(
    suggestions.filter(tag => 
      tag.name.toLowerCase().includes(inputValue.toLowerCase()) &&
      !tags.some(t => t.id === tag.id)
    )
  )

  // Handle keyboard navigation
  function handleKeydown(e: KeyboardEvent) {
    switch (e.key) {
      case 'Enter':
        e.preventDefault()
        if (selectedIndex >= 0) {
          addExistingTag(filteredSuggestions[selectedIndex])
        } else if (inputValue.trim() && allowNew) {
          createNewTag(inputValue.trim())
        }
        break
      case 'ArrowDown':
        e.preventDefault()
        selectedIndex = Math.min(selectedIndex + 1, filteredSuggestions.length - 1)
        break
      case 'ArrowUp':
        e.preventDefault()
        selectedIndex = Math.max(selectedIndex - 1, -1)
        break
      case 'Backspace':
        if (!inputValue && tags.length > 0) {
          removeTag(tags[tags.length - 1])
        }
        break
      case 'Escape':
        showSuggestions = false
        selectedIndex = -1
        break
    }
  }
</script>

3. Post Filtering by Tags

Frontend Components

  • Tag Filter Bar: Multi-select tag filtering
  • Tag Cloud: Visual tag representation with usage counts
  • Search Integration: Combine text search with tag filters

API Endpoints

// GET /api/posts?tags=javascript,react&operation=AND
// GET /api/posts?tags=design,ux&operation=OR
interface PostsQueryParams {
  tags?: string[]               // Tag names or IDs
  operation?: 'AND' | 'OR'     // How to combine multiple tags
  page?: number
  limit?: number
  status?: 'published' | 'draft'
}

// GET /api/tags/suggest?q=java
interface TagSuggestResponse {
  tags: Array<{
    id: number
    name: string
    slug: string
    usageCount: number
  }>
}

Implementation

  • Algorithm: Find posts sharing the most tags
  • Weighting: Consider tag importance and recency
  • Exclusions: Don't show current post in related list
  • Limit: Show 3-6 related posts maximum
<script lang="ts">
  let { postId, tags, limit = 4 }: {
    postId: number
    tags: Tag[]
    limit?: number
  } = $props()

  let relatedPosts = $state<Post[]>([])

  $effect(async () => {
    const tagIds = tags.map(t => t.id)
    const response = await fetch(`/api/posts/related?postId=${postId}&tagIds=${tagIds.join(',')}&limit=${limit}`)
    relatedPosts = await response.json()
  })
</script>

API Specification

Tag Management APIs

// GET /api/tags - List all tags
interface TagsResponse {
  tags: Tag[]
  total: number
  page: number
  limit: number
}

// POST /api/tags - Create new tag
interface CreateTagRequest {
  name: string
  description?: string
  color?: string
}

// PUT /api/tags/[id] - Update tag
interface UpdateTagRequest {
  name?: string
  description?: string
  color?: string
}

// DELETE /api/tags/[id] - Delete tag
// Returns: 204 No Content

// POST /api/tags/merge - Merge tags
interface MergeTagsRequest {
  sourceTagIds: number[]
  targetTagId: number
}

// GET /api/tags/[id]/posts - Get posts for tag
interface TagPostsResponse {
  posts: Post[]
  tag: Tag
  total: number
}

// GET /api/tags/analytics - Tag usage analytics
interface TagAnalyticsResponse {
  mostUsed: Array<{ tag: Tag; count: number }>
  leastUsed: Array<{ tag: Tag; count: number }>
  trending: Array<{ tag: Tag; growth: number }>
  orphaned: Tag[]
}

Enhanced Post APIs

// GET /api/posts/related?postId=123&tagIds=1,2,3&limit=4
interface RelatedPostsResponse {
  posts: Array<{
    id: number
    title: string
    slug: string
    excerpt?: string
    publishedAt: string
    tags: Tag[]
    sharedTagsCount: number  // Number of tags in common
  }>
}

// PUT /api/posts/[id]/tags - Update post tags
interface UpdatePostTagsRequest {
  tagIds: number[]
}

User Interface Components

1. TagInput Component Features

Visual States

  • Default: Clean input with placeholder
  • Focused: Show suggestions dropdown
  • Typing: Filter and highlight matches
  • Selected: Navigate with keyboard
  • Adding: Smooth animation for new tags
  • Full: Disable input when max tags reached

Accessibility

  • ARIA Labels: Proper labeling for screen readers
  • Keyboard Navigation: Full keyboard accessibility
  • Focus Management: Logical tab order
  • Announcements: Screen reader feedback for actions

2. Tag Display Components

TagPill Component

<script lang="ts">
  let {
    tag,
    size = 'medium',
    removable = false,
    clickable = false,
    showCount = false,
    onRemove,
    onClick
  }: TagPillProps = $props()
</script>

<span 
  class="tag-pill tag-pill-{size}"
  style="--tag-color: {tag.color}"
  class:clickable
  class:removable
  onclick={onClick}
>
  {tag.name}
  {#if showCount}
    <span class="tag-count">({tag.usageCount})</span>
  {/if}
  {#if removable}
    <button onclick={onRemove} class="tag-remove">×</button>
  {/if}
</span>

TagCloud Component

<script lang="ts">
  let {
    tags,
    maxTags = 50,
    minFontSize = 12,
    maxFontSize = 24,
    onClick
  }: TagCloudProps = $props()

  // Calculate font sizes based on usage
  let tagSizes = $derived(calculateTagSizes(tags, minFontSize, maxFontSize))
</script>

3. Admin Interface Updates

Posts List with Tag Filtering

  • Filter Bar: Multi-select tag filter above posts list
  • Tag Pills: Show tags on each post item
  • Quick Filter: Click tag to filter by that tag
  • Clear Filters: Easy way to reset all filters

Posts Edit Form Integration

  • Replace Current: Swap existing tag input with new TagInput
  • Preserve UX: Maintain current metadata popover
  • Tag Management: Quick access to create/edit tags

Migration Strategy

Phase 1: Database Migration (Week 1)

  1. Create Migration Script

    • Create new tables (tags, post_tags)
    • Migrate existing JSON tags to relational format
    • Create indexes for performance
  2. Data Migration

    • Extract unique tags from existing posts
    • Create tag records with auto-generated slugs
    • Create post_tag relationships
    • Validate data integrity
  3. Backward Compatibility

    • Keep original tags JSON field temporarily
    • Dual-write to both systems during transition

Phase 2: API Development (Week 1-2)

  1. Tag Management APIs

    • CRUD operations for tags
    • Tag suggestions and search
    • Analytics endpoints
  2. Enhanced Post APIs

    • Update post endpoints for relational tags
    • Related posts algorithm
    • Tag filtering capabilities
  3. Testing & Validation

    • Unit tests for all endpoints
    • Performance testing for queries
    • Data consistency checks

Phase 3: Frontend Components (Week 2-3)

  1. Core Components

    • TagInput with typeahead
    • TagPill and TagCloud
    • Tag management interface
  2. Integration

    • Update MetadataPopover
    • Add tag filtering to posts list
    • Implement related posts component
  3. Admin Interface

    • Tag management dashboard
    • Analytics views
    • Bulk operations interface

Phase 4: Features & Polish (Week 3-4)

  1. Advanced Features

    • Tag merging functionality
    • Usage analytics
    • Tag suggestions based on content
  2. Performance Optimization

    • Query optimization
    • Caching strategies
    • Load testing
  3. Cleanup

    • Remove JSON tags field
    • Documentation updates
    • Final testing

Success Metrics

Performance

  • Tag search responses under 50ms
  • Post filtering responses under 100ms
  • Page load times maintained or improved

Usability

  • Reduced clicks to add tags (eliminate Add button)
  • Faster tag input with typeahead
  • Improved content discovery through related posts

Content Management

  • Ability to merge duplicate tags
  • Insights into tag usage patterns
  • Better content organization capabilities

Analytics

  • Track tag usage growth over time
  • Identify content gaps through tag analysis
  • Measure impact on content engagement

Technical Considerations

Performance

  • Database Indexes: Proper indexing on tag names and relationships
  • Query Optimization: Efficient joins for tag filtering
  • Caching: Cache popular tag lists and related posts
  • Pagination: Handle large tag lists efficiently

Data Integrity

  • Constraints: Prevent duplicate tag names
  • Cascading Deletes: Properly handle tag/post deletions
  • Validation: Ensure tag names follow naming conventions
  • Backup Strategy: Safe migration with rollback capability

User Experience

  • Progressive Enhancement: Graceful degradation if JS fails
  • Loading States: Smooth loading indicators
  • Error Handling: Clear error messages for users
  • Responsive Design: Works well on all device sizes

Future Enhancements

Advanced Features (Post-MVP)

  • Hierarchical Tags: Parent/child tag relationships
  • Tag Synonyms: Alternative names for the same concept
  • Auto-tagging: ML-based tag suggestions from content
  • Tag Templates: Predefined tag sets for different content types

Integrations

  • External APIs: Import tags from external sources
  • Search Integration: Enhanced search with tag faceting
  • Analytics: Deep tag performance analytics
  • Content Recommendations: AI-powered related content

Risk Assessment

High Risk

  • Data Migration: Complex migration of existing tag data
  • Performance Impact: New queries might affect page load times
  • User Adoption: Users need to learn new tag input interface

Mitigation Strategies

  • Staged Rollout: Deploy to staging first, then gradual production rollout
  • Performance Monitoring: Continuous monitoring during migration
  • User Training: Clear documentation and smooth UX transitions
  • Rollback Plan: Ability to revert to JSON tags if needed

Success Criteria

Must Have

  • All existing tags migrated successfully
  • Tag input works with keyboard-only navigation
  • Posts can be filtered by single or multiple tags
  • Related posts show based on shared tags
  • Performance remains acceptable (< 100ms for most operations)

Should Have

  • Tag management interface for admins
  • Tag usage analytics and insights
  • Ability to merge duplicate tags
  • Tag color coding and visual improvements

Could Have

  • Tag auto-suggestions based on post content
  • Tag trending and popularity metrics
  • Advanced tag analytics and reporting
  • Integration with external tag sources

Timeline

Total Duration: 4 weeks

  • Week 1: Database migration and API development
  • Week 2: Core frontend components and basic integration
  • Week 3: Advanced features and admin interface
  • Week 4: Polish, testing, and production deployment

Conclusion

This enhanced tag system will significantly improve content organization, discoverability, and management capabilities while providing a modern, intuitive user interface built with Svelte 5 runes. The migration strategy ensures minimal disruption while delivering substantial improvements in functionality and user experience.