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:
- Users create Topics in a “Historical Events” category
- Each Topic uses the Calendar Event plugin to define event date/time
- Users manually create Collections to group Topics into “timelines”
- Topics display event dates in their titles (enable setting:
display_post_event_date_on_topic_title) - 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:
- Create a small custom plugin that adds a post custom field:
timeline_date - Populate this field by parsing Calendar Event plugin data from post raw
- Expose this field via API for programmatic access
- Build a modified Collections UI that sorts by
timeline_date - 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)
- Enable Discourse Calendar/Event plugin (likely already installed)
- Configure:
display_post_event_date_on_topic_title= true - Create “Historical Events” category
- Create sample event topics with dates
- Create Collections manually linking events into narrative timelines
- Test Collections UI with date-sorted topics
Phase 2: Enhancement (Week 2-3)
- Develop small custom plugin to extract event dates to post custom field
- Modify Collections theme component to sort by timeline_date
- Test with larger dataset
- Create Data Explorer query to find/report on events
Phase 3: Automation (Week 4+)
- 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
- Query
- 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:
- HTTP Request node → Query posts with timeline_date
- Sort by timeline_date (ascending/descending)
- Filter by category or tag
- 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
- Timeline Visualization: Add FullCalendar.io view in Collections showing events on interactive calendar
- Export: Allow exporting Collections as:
- JSON for external systems
- iCal for calendar apps
- Markdown timeline for publishing
- Intelligent Linking: Auto-detect related events and suggest connections
- Granularity: Support century/decade/year-only dates for ancient history
- Relationships: Add “related_events” field to track causality/influence
- 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"
}
]
}
}
}
]
}