Basker Docs

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's the right model for most content. For data that changes frequently or needs to load lazily — a calendar paginated by month, an event search box, an upcoming-shows widget on a sidebar — the FrontStage API lets the browser fetch it directly.

You'll 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 we wire up fetch, it's 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're promoting this season").

The FrontStage API path you're 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 just renders a placeholder div and a script tag — the actual list comes from the JS file you'll 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",
      "default": "What's on"
    },
    {
      "type": "number",
      "name": "limit",
      "label": "Number of events",
      "default": 10
    }
  ]
}
{% endschema %}

A data-upcoming-events attribute on the <ul> carries the limit through to the JS so the same script can power multiple instances on a page with different limits.

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 — no API key, no CORS setup. The select query parameter trims the response to just the fields you actually use, keeping the payload small. 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, and refresh the dev-server tab.

If your test site has event instances, the block fetches and renders them. If it doesn't, you'll see the empty state. Either way, opening the browser dev tools' network tab should show a GET /api/event-instances.json request returning JSON.

The dev server proxies same-origin requests against your production CMS, so the events you see in the calendar are the events on your live Basker site. Worth knowing if you're testing against a tenant with real upcoming shows.

What's next

The theme has every moving part it needs: layout, templates, blocks, settings, a live data feed. Time to validate it and ship.

Validate and upload — run the theme checker, package the ZIP, and upload through the admin.

Going deeper

On this page