Documentation Index
Fetch the complete documentation index at: https://documentation.outpost.pub/llms.txt
Use this file to discover all available pages before exploring further.
When an event is published to Ghost, Outpost injects structured data into the post’s code injection footer. Your Ghost theme can read this data to render event details, registration CTAs, countdowns, and more.
Code Injection JSON
Outpost adds a <script> tag with JSON data to every event post:
<script id="event" type="application/json">
{
"date": "March 15, 2026",
"event_day": "Sunday",
"start_time": "10:00 am",
"end_time": "11:00 am",
"time_zone": "America/New_York",
"time_zone_abbr": "EDT",
"location": "Online",
"platform_url": "https://zoom.us/j/123456789",
"registration_deadline": "March 14, 2026",
"access": "anyone",
"event_state": "registration_open",
"outpost_ghost_event_id": "march-2026-webinar",
"event_type": "webinar",
"custom_fields": {
"speaker_name": "Jane Smith",
"session_type": "Workshop"
}
}
</script>
Reading Data in Your Theme
JavaScript
document.addEventListener('DOMContentLoaded', function() {
const eventScript = document.getElementById('event');
if (!eventScript) return;
try {
const eventData = JSON.parse(eventScript.textContent);
// Access standard fields
console.log(eventData.date); // "March 15, 2026"
console.log(eventData.event_state); // "registration_open"
console.log(eventData.access); // "anyone"
// Access custom fields
if (eventData.custom_fields) {
console.log(eventData.custom_fields.speaker_name); // "Jane Smith"
console.log(eventData.custom_fields.session_type); // "Workshop"
}
} catch (e) {
console.error('Failed to parse event data', e);
}
});
Handlebars (with inline script)
Since Handlebars templates run server-side and the JSON is in code injection (rendered client-side), you’ll typically use JavaScript to read the data and update the DOM:
{{!-- In your post.hbs or custom template --}}
<div class="event-details">
<div class="event-date" data-event-field="date"></div>
<div class="event-time" data-event-field="start_time"></div>
<div class="event-location" data-event-field="location"></div>
<div class="event-speaker" data-event-field="custom_fields.speaker_name"></div>
</div>
<script>
(function() {
const eventScript = document.getElementById('event');
if (!eventScript) return;
const data = JSON.parse(eventScript.textContent);
document.querySelectorAll('[data-event-field]').forEach(el => {
const field = el.dataset.eventField;
const value = field.split('.').reduce((obj, key) => obj?.[key], data);
if (value) el.textContent = value;
});
})();
</script>
Standard Field Reference
| Field | Type | Example | Description |
|---|
date | string | "March 15, 2026" | Formatted event date (uses blog’s date format) |
event_day | string | "Sunday" | Day of the week |
start_time | string | "10:00 am" | Event start time |
end_time | string | "11:00 am" | Event end time |
time_zone | string | "America/New_York" | IANA timezone identifier |
time_zone_abbr | string | "EDT" | Localized timezone abbreviation for the event date |
location | string | "Online" | Event location/venue |
platform_url | string | "https://zoom.us/..." | Virtual event platform link |
registration_deadline | string | "March 14, 2026" | Registration cutoff date (or event date if not set) |
access | string | "anyone" | Access level enum (see below) |
event_state | string | "registration_open" | Current lifecycle state (see below) |
outpost_ghost_event_id | string | "march-2026-webinar" | Unique event identifier (GUID/slug) |
event_type | string | "webinar" | Event type key from manifest |
custom_fields | object | {...} | Custom field values defined by manifest |
Access Levels
The access field indicates who can register for the event:
| Value | Description | Theme Behavior |
|---|
anyone | All logged-in members (free and paid) | Show standard registration CTA |
free | Free-tier members and above | Show registration CTA for logged-in users |
paid | Only paid subscribers | Show upgrade CTA for free members |
Example: Conditional Rendering by Access
const eventData = JSON.parse(document.getElementById('event').textContent);
if (eventData.access === 'paid') {
// Show "Paid subscribers only" badge
document.querySelector('.event-access-badge').textContent = 'Paid Subscribers Only';
document.querySelector('.event-access-badge').classList.add('premium');
}
Event States
The event_state field reflects the event’s current lifecycle position:
| Value | Description | Typical Theme Behavior |
|---|
registration_open | Event is upcoming, registration is open | Show registration CTA |
registration_closed | Deadline passed, event hasn’t happened | Show “Registration closed” message |
event_passed | Event date/time has passed | Show “This event has ended” message |
Example: State-Based UI
const eventData = JSON.parse(document.getElementById('event').textContent);
const ctaContainer = document.querySelector('.event-cta');
switch (eventData.event_state) {
case 'registration_open':
ctaContainer.innerHTML = '<button class="register-btn">Register Now</button>';
break;
case 'registration_closed':
ctaContainer.innerHTML = '<p class="closed">Registration is closed</p>';
break;
case 'event_passed':
ctaContainer.innerHTML = '<p class="past">This event has ended</p>';
break;
}
Custom Fields
Custom fields defined in the manifest are nested under the custom_fields object. The keys match what you defined in your outpost-manifest.yaml:
# In your manifest
custom_fields:
- key: speaker_name
type: text
- key: session_type
type: select
options: [Workshop, Keynote, Panel]
// In your theme JavaScript
const eventData = JSON.parse(document.getElementById('event').textContent);
if (eventData.custom_fields?.speaker_name) {
document.querySelector('.speaker-name').textContent =
eventData.custom_fields.speaker_name;
}
if (eventData.custom_fields?.session_type) {
document.querySelector('.session-type').textContent =
eventData.custom_fields.session_type;
}
Custom Field Value Types
| Manifest Type | JSON Type | Example Value |
|---|
text | string | "Jane Smith" |
textarea | string | "A longer description..." |
number | number | 60 |
select | string | "Workshop" |
url | string | "https://example.com" |
date | string | "2026-03-15" (ISO format) |
toggle | boolean | true |
image | string | "https://cdn.example.com/image.jpg" |
Event Defaults Partial
Outpost also injects messaging defaults and portal URLs via the outpost-event-defaults.hbs partial during theme processing:
<script id="outpost-event-defaults" type="application/json">
{
"messaging": {
"can_register": {
"cta_text": "Join this event",
"button_text": "Register"
},
"registered": {
"cta_text": "You're registered!",
"button_text": "Add to Calendar"
},
...
},
"urls": {
"signin": "#/portal/signin",
"signup": "#/portal/signup",
"upgrade": "#/portal/account"
}
}
</script>
This provides:
- Messaging: Default CTA text and button labels for each audience state
- URLs: Ghost portal links for sign-in, sign-up, and account/upgrade
Reading Event Defaults
const defaultsScript = document.getElementById('outpost-event-defaults');
if (defaultsScript) {
const defaults = JSON.parse(defaultsScript.textContent);
// Access messaging for current state
const messaging = defaults.messaging?.can_register;
if (messaging) {
document.querySelector('.cta-text').textContent = messaging.cta_text;
document.querySelector('.cta-button').textContent = messaging.button_text;
}
// Access portal URLs
const signInUrl = defaults.urls?.signin || '#/portal/signin';
}
Complete Theme Example
Here’s a complete example of rendering event data in a custom Ghost template:
{{!-- custom-webinar.hbs --}}
{{!< default}}
<article class="event-post">
<header class="event-header">
<h1>{{title}}</h1>
{{#if feature_image}}
<img src="{{feature_image}}" alt="{{title}}" class="event-image" />
{{/if}}
</header>
<div class="event-meta" id="event-meta-container">
<!-- Populated by JavaScript from code injection -->
</div>
<div class="event-content">
{{content}}
</div>
<div class="event-registration">
<!-- Outpost widget renders here -->
<div class="outpost-ghost-event-container"></div>
</div>
</article>
<script>
document.addEventListener('DOMContentLoaded', function() {
const eventScript = document.getElementById('event');
if (!eventScript) return;
const event = JSON.parse(eventScript.textContent);
const container = document.getElementById('event-meta-container');
container.innerHTML = `
<div class="event-datetime">
<span class="event-date">${event.date}</span>
<span class="event-time">${event.start_time} - ${event.end_time}</span>
<span class="event-timezone">${event.time_zone}</span>
</div>
<div class="event-location">${event.location}</div>
${event.custom_fields?.speaker_name ? `
<div class="event-speaker">
<strong>Speaker:</strong> ${event.custom_fields.speaker_name}
</div>
` : ''}
${event.event_state === 'event_passed' ? `
<div class="event-status past">This event has ended</div>
` : ''}
`;
});
</script>
Ghost Post Properties
In addition to code injection, Outpost sets these Ghost post properties:
| Property | Value |
|---|
title | Event name |
custom_excerpt | Event description |
feature_image | Event featured image (if uploaded) |
custom_template | Template from manifest post_template |
tags | Merged from default_tags + event type tags |
These are accessible via standard Ghost Handlebars helpers:
<h1>{{title}}</h1>
<p class="excerpt">{{excerpt}}</p>
{{#if feature_image}}
<img src="{{feature_image}}" />
{{/if}}
API Endpoint for Registration
The event widget uses Outpost’s plugin API for registration. If you’re building a custom registration UI:
// Register a member for an event
const response = await fetch(
`/plugin-api/ghost-event/${apiKey}/${eventGuid}/subscribe/${memberUid}`,
{ method: 'POST' }
);
// Cancel registration
const response = await fetch(
`/plugin-api/ghost-event/${apiKey}/${eventGuid}/unsubscribe/${memberUid}`,
{ method: 'POST' }
);
// Get calendar file
window.location.href = `/plugin-api/ghost-event/${apiKey}/${eventGuid}/calendar.ics`;
The apiKey is your blog’s Outpost GUID (available in the outpost-api-key.hbs partial). The memberUid is the logged-in member’s Ghost UUID.