Pull live event data
Build a calendar block fed by the FrontStage API so the upcoming-events list always reflects what's on
Until now, everything in your theme has rendered server-side from records the editor has shaped. That is the right model for most content. Some data changes frequently or needs to load lazily. Examples include a calendar paginated by month, an event search box, or an upcoming-shows widget on a sidebar. For that data, the FrontStage API lets the browser fetch it directly.
You will build a block called upcoming-events that fetches the next ten event instances from your site and renders them client-side.
Two ways to render an event list
Before you add the fetch call, it is worth knowing the alternative. Blocks can declare a relationship setting that lets the editor pick specific events:
{
"type": "relationship",
"name": "events",
"label": "Events",
"relationTo": "events",
"hasMany": true
}The picked events arrive on block.events as a fully hydrated drop, ready to render server-side with {% for event in block.events %}. Use this when the editor curates a specific list ("the four shows we promote this season").
The FrontStage API path you are about to build is the right call when:
- The list should always be live. Adding an event in the admin makes it appear without a redeploy.
- The widget needs pagination, infinite scroll, or filtering by date.
- The data feeds something interactive (a calendar grid, a search dropdown).
Step 1: The block markup
Create blocks/upcoming-events.liquid. The Liquid file renders a placeholder div and a script tag. The actual list comes from the JS file you will add next.
<section id="upcoming-events-{{ block.id }}" class="block-upcoming-events">
{% if block.title %}
<h2 class="block-upcoming-events__title">{{ block.title }}</h2>
{% endif %}
<ul class="block-upcoming-events__list" data-upcoming-events="{{ block.limit | default: 10 }}">
<li class="block-upcoming-events__loading">Loading…</li>
</ul>
</section>
<script src="{{ 'upcoming-events.js' | asset_url }}" defer></script>
{% schema %}
{
"name": "upcoming-events",
"label": "Upcoming events",
"singular": "Upcoming events",
"plural": "Upcoming events lists",
"settings": [
{
"type": "text",
"name": "title",
"label": "Section title",
"defaultValue": "What's on"
},
{
"type": "number",
"name": "limit",
"label": "Number of events",
"defaultValue": 10
}
]
}
{% endschema %}A data-upcoming-events attribute on the <ul> carries the limit to the JS. This lets the same script power multiple instances on a page, each with a different limit.
Add the new block to your templates' allowed-blocks arrays:
"blocks": ["text-section", "image-feature", "feature-grid", "upcoming-events"]Step 2: The JavaScript
Create assets/upcoming-events.js:
const dateFormatter = new Intl.DateTimeFormat(undefined, {
weekday: 'short',
day: 'numeric',
month: 'short',
hour: '2-digit',
minute: '2-digit',
});
async function loadUpcomingEvents(target) {
const limit = Number(target.dataset.upcomingEvents) || 10;
const url = `/api/event-instances.json?limit=${limit}&select=startDate,event.title,event.url,venue.title`;
try {
const res = await fetch(url, { headers: { Accept: 'application/json' } });
if (!res.ok) throw new Error(`Request failed: ${res.status}`);
const { docs } = await res.json();
render(target, docs);
} catch (error) {
target.innerHTML = '<li class="block-upcoming-events__error">Could not load events.</li>';
console.error(error);
}
}
function render(target, docs) {
if (!docs.length) {
target.innerHTML = '<li class="block-upcoming-events__empty">No events scheduled.</li>';
return;
}
target.innerHTML = docs
.map((doc) => {
const date = dateFormatter.format(new Date(doc.startDate));
const title = doc.event?.title ?? 'Untitled';
const url = doc.event?.url ?? '#';
const venue = doc.venue?.title ?? '';
return `
<li class="upcoming-event">
<a href="${url}">
<time datetime="${doc.startDate}">${date}</time>
<strong>${title}</strong>
${venue ? `<span class="upcoming-event__venue">${venue}</span>` : ''}
</a>
</li>
`;
})
.join('');
}
document.querySelectorAll('[data-upcoming-events]').forEach(loadUpcomingEvents);The endpoint is same-origin, so it needs no API key and no CORS setup. The select query parameter trims the response to just the fields you use. This keeps the payload small. The full reference for the endpoint is on Event instances.
Step 3: Try it locally
Save both files. The dev server reloads. Add the new "Upcoming events" block to a page in the admin. Save the page. Refresh the dev-server tab.
If your test site has event instances, the block fetches and renders them. If it does not, you will see the empty state. Either way, open the browser developer tools' network tab. It should show a GET /api/event-instances.json request returning JSON.
The dev server proxies same-origin requests to the Basker environment and workspace selected in LocalStage. The events you see are real records from that workspace, so check which workspace is active before testing.
What's next
The theme has every moving part it needs: layout, templates, blocks, settings, a live data feed. It is time to validate the theme and ship it.
Validate and upload: run the theme checker, package the ZIP, and upload through the admin.
Going deeper
- FrontStage API: Event instances: every query parameter, the full response shape, caching notes.
- FrontStage API overview: when to use the FrontStage API vs the Partners API.
- Blocks: block lifecycle, drop semantics, when to use a relationship instead.