Discourse - User-Curated Timelines Using Calendar Events and Collections

Discourse Historical Timeline Architecture

Using Calendar/Event Plugin Metadata with Collections Plugin


Current State Analysis

Discourse Calendar/Event Plugin

  • Stores event data in post content using [event][/event] tags
  • Event metadata (start date, end date, custom fields) stored in post raw as JSON
  • API endpoint: /discourse-post-event/events.json?include_details=true
  • Supports ISO 8601 datetime format for start/end times
  • Custom fields allowed but stored in post raw only (not separate DB columns)
  • Topic-level display: event dates can display in topic title

Collections Plugin

  • Creates curated lists of linked Topics
  • Supports sorting and organization by sections
  • Can include any URL or topic
  • Currently lacks native sorting by date/custom metadata
  • No direct integration with event metadata

Implementation Strategy

Option 1: Minimal Integration (Low Effort, Good UX)

Approach:

  1. Users create Topics in a “Historical Events” category
  2. Each Topic uses the Calendar Event plugin to define event date/time
  3. Users manually create Collections to group Topics into “timelines”
  4. Topics display event dates in their titles (enable setting: display_post_event_date_on_topic_title)
  5. Within each Collection, event Topics are sorted chronologically by hand or by user preference

Pros:

  • Works with stock plugins - no custom code needed
  • User-friendly: leverage existing UI for event creation
  • Visual timeline emerges naturally from event dates in titles
  • Collections provide curation layer for narrative flow

Cons:

  • No automatic chronological sorting within Collections
  • Manual collection management required
  • Cannot query/filter events programmatically without custom work

Option 2: Semi-Automated (Medium Effort, Full Sorting) ✓ RECOMMENDED

Approach:

  1. Create a small custom plugin that adds a post custom field: timeline_date
  2. Populate this field by parsing Calendar Event plugin data from post raw
  3. Expose this field via API for programmatic access
  4. Build a modified Collections UI that sorts by timeline_date
  5. Optionally: Tag topics automatically (e.g., historical-event) for filtering

Plugin Structure (Ruby):

# plugin.rb
after_initialize do
  # Register custom field on posts
  Post.register_custom_field_type(:timeline_date, :string)
  
  # Listen to event creation/update
  DiscourseEvent.on(:post_created) do |post|
    extract_event_date_from_post(post)
  end
  
  DiscourseEvent.on(:post_updated) do |post|
    extract_event_date_from_post(post)
  end
  
  # Expose via API serializer
  add_to_serializer(:post, :timeline_date) do
    object.custom_fields&.dig('timeline_date')
  end
end

def extract_event_date_from_post(post)
  # Parse [event][/event] tags from post.raw
  # Extract start date from event JSON
  # Store in custom field
end

Benefits:

  • Automatic date extraction from event data
  • Chronological sorting becomes possible
  • API-queryable for external tools (n8n workflows)
  • Lightweight—minimal overhead

Option 3: Full-Featured Custom Plugin (High Effort, Maximum Control)

Approach:
Build comprehensive “Timeline” plugin that:

  • Integrates with Calendar/Event plugin
  • Creates topic custom fields for event metadata (date, description, era, region, etc.)
  • Provides specialized UI for browsing/filtering events by:
    • Date range (year, decade, century)
    • Category/geography
    • Tags (eras, empires, movements)
  • Renders interactive timelines (visual representation)
  • Syncs with Collections for narrative curation

Architecture:

Topic
├── Event Date (from Calendar plugin)
├── Topic Custom Fields:
│   ├── era (Medieval, Renaissance, Modern)
│   ├── region (Europe, Asia, Americas)
│   ├── significance_level (1-5)
│   └── related_persons (JSON array)
├── Tags (hierarchical: History > Medieval > European)
└── Collections (curated narrative timelines)

Recommended Path: Option 2 Hybrid Approach

Phase 1: Immediate (Week 1)

  1. Enable Discourse Calendar/Event plugin (likely already installed)
  2. Configure: display_post_event_date_on_topic_title = true
  3. Create “Historical Events” category
  4. Create sample event topics with dates
  5. Create Collections manually linking events into narrative timelines
  6. Test Collections UI with date-sorted topics

Phase 2: Enhancement (Week 2-3)

  1. Develop small custom plugin to extract event dates to post custom field
  2. Modify Collections theme component to sort by timeline_date
  3. Test with larger dataset
  4. Create Data Explorer query to find/report on events

Phase 3: Automation (Week 4+)

  1. Build n8n workflow to:
    • Query /discourse-post-event/events.json?include_details=true
    • Filter by category
    • Generate reports of upcoming/historical events
    • Auto-tag events based on metadata patterns
  2. Optional: Create admin dashboard showing all events and their collections

Data Flow Diagram

User creates Topic with Event
    ↓
Calendar/Event plugin stores [event][/event] in post.raw
    ↓
Custom plugin parses event JSON → extracts start_date
    ↓
Stores start_date in post custom field: timeline_date
    ↓
Collections query API → retrieves topics with timeline_date
    ↓
Collections UI sorts topics chronologically
    ↓
User views Collection as historical timeline
    ↓
User can create multiple Collections for different narratives
    (e.g., "Fall of Rome", "Age of Exploration", "Industrial Revolution")

Implementation: Custom Plugin Code

File: plugin.rb

# frozen_string_literal: true

# name: discourse-historical-timeline
# about: Integration with Calendar/Event plugin for historical timelines
# version: 0.1
# authors: Your Name
# url: https://github.com/yourusername/discourse-historical-timeline

after_initialize do
  # Register custom field for timeline date
  Post.register_custom_field_type(:timeline_date, :string)
  
  # Expose timeline_date in post serializer
  add_to_serializer(:post, :timeline_date) do
    object.custom_fields&.dig('timeline_date')
  end
  
  # Hook into post creation/update to extract event date
  DiscourseEvent.on(:post_created) do |post|
    ::HistoricalTimeline::EventExtractor.new(post).extract_and_store_date
  end
  
  DiscourseEvent.on(:post_updated) do |post|
    ::HistoricalTimeline::EventExtractor.new(post).extract_and_store_date
  end
end

module HistoricalTimeline
  class EventExtractor
    def initialize(post)
      @post = post
    end
    
    def extract_and_store_date
      # Parse [event]...[/event] tags from post.raw
      event_match = @post.raw.match(/\[event\](.*?)\[\/event\]/m)
      return unless event_match
      
      begin
        event_data = JSON.parse(event_match[1])
        start_date = event_data['start']
        
        if start_date.present?
          @post.custom_fields['timeline_date'] = start_date
          @post.save_custom_fields(true)
        end
      rescue JSON::ParserError
        # Event data malformed, skip
      end
    end
  end
end

File: assets/javascripts/discourse/components/timeline-sorted-collection.gjs

// Modified Collections component to sort by timeline_date
import Component from '@glimmer/component';
import { sort } from '@ember/object/computed';

export default class TimelineSortedCollection extends Component {
  // Sort topics by timeline_date field
  @sort('collection.topics', 'sortProperties')
  sortedTopics;
  
  sortProperties = ['timeline_date:asc'];
}

API Query Examples

Get all events by date:

GET /posts.json?f=has_timeline_date&category=6&page=1

Response includes:

{
  "posts": [
    {
      "id": 123,
      "topic_id": 456,
      "title": "Fall of Rome 410 AD",
      "custom_fields": {
        "timeline_date": "0410-08-24T00:00:00Z"
      }
    }
  ]
}

n8n Workflow:

  1. HTTP Request node → Query posts with timeline_date
  2. Sort by timeline_date (ascending/descending)
  3. Filter by category or tag
  4. Output to webhook, email, or external calendar

Data Storage Reference

Data Storage Location Access Method
Event start/end dates Post raw (event JSON tags) Calendar plugin UI
Timeline date Post custom field API, post serializer
Topic metadata (era, region) Topic custom fields Topic API
Tags (e.g., #medieval-europe) Topic tags table Tag API
Narrative ordering Collections linking Collections API
User-curated timeline Collection relationships Collections UI

Testing Checklist

  • [ ] Create event topic with Calendar/Event plugin
  • [ ] Verify event date appears in topic title
  • [ ] Confirm post custom field receives extracted date
  • [ ] Test Collections display with 3+ event topics
  • [ ] Verify sorting by timeline_date in Collections
  • [ ] Query events via API and verify timeline_date returned
  • [ ] Create n8n workflow to fetch and sort events
  • [ ] Test with category filters
  • [ ] Test with tag filters
  • [ ] Generate Data Explorer report of all events

Future Enhancements

  1. Timeline Visualization: Add FullCalendar.io view in Collections showing events on interactive calendar
  2. Export: Allow exporting Collections as:
    • JSON for external systems
    • iCal for calendar apps
    • Markdown timeline for publishing
  3. Intelligent Linking: Auto-detect related events and suggest connections
  4. Granularity: Support century/decade/year-only dates for ancient history
  5. Relationships: Add “related_events” field to track causality/influence
  6. Maps: Integrate with mapping plugin to show geographical distribution of events

Data Explorer Query: Find All Events

SELECT
  t.id AS topic_id,
  t.title,
  t.category_id,
  c.name AS category_name,
  pcf.value AS timeline_date,
  STRING_AGG(tags.name, ', ') AS tags
FROM topics t
JOIN posts p ON t.id = p.topic_id AND p.post_number = 1
LEFT JOIN categories c ON t.category_id = c.id
LEFT JOIN post_custom_fields pcf ON p.id = pcf.post_id AND pcf.name = 'timeline_date'
LEFT JOIN topic_tags tt ON t.id = tt.topic_id
LEFT JOIN tags ON tt.tag_id = tags.id
WHERE pcf.value IS NOT NULL
  AND t.deleted_at IS NULL
  AND t.archetype = 'regular'
GROUP BY t.id, t.title, t.category_id, c.name, pcf.value
ORDER BY pcf.value ASC;

n8n Workflow Template

{
  "nodes": [
    {
      "name": "HTTP Request",
      "type": "n8n-nodes-base.httpRequest",
      "parameters": {
        "url": "https://your-forum.com/discourse-post-event/events.json",
        "method": "GET",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "options": {
          "qs": {
            "include_details": "true"
          }
        }
      }
    },
    {
      "name": "Sort by Date",
      "type": "n8n-nodes-base.sort",
      "parameters": {
        "sortFieldsUi": {
          "sortField": [
            {
              "fieldName": "starts_at",
              "order": "ascending"
            }
          ]
        }
      }
    },
    {
      "name": "Filter by Category",
      "type": "n8n-nodes-base.filter",
      "parameters": {
        "conditions": {
          "string": [
            {
              "value1": "={{ $json.category_id }}",
              "operation": "equals",
              "value2": "6"
            }
          ]
        }
      }
    }
  ]
}