Skip to content

External data

On my Now page, I include a calendar of events, as well a list of books and articles I am currently reading. The data comes from external services, and I want to host it on my own server.

Since I don't want to update my website after every change of those sources, I download their data via a cron job on my server. I then include it in my website dynamically.

Zotero references

The Zotero API lets me download a formatted list of references in a citation style of my choice. It gives me complete HTML, including markup to let visitors easily import any of the references into their own bibliography manager. See the cron job below.

In the page, I include this content, with a crude function to create real hyperlinks:

Show a bibliography
bibliography.html
<div id="bibliography"></div>
<script>
fetch('zotero.html')
  .then(response => response.text())
  .then(html => {
    const linkedHtml = html.replace(
      /(https?:\/\/[^\s<]+)/g,
      '<a href="$1" target="_blank" rel="noopener">$1</a>'
    );
    document.getElementById('bibliography').outerHTML = linkedHtml;
  });
</script>

Google Calendar events

I keep a public Google calendar of events. It is easy to download the data as ICS file (see the cron job below). You can then subscribe to the calendar from my website, without having to go through Google.

I use FullCalendar to display the calendar. It is again a little crude, but does the job. I have downloaded the scripts to my own server.

Show a calendar of events
calendar.html
<script src='/assets/javascripts/fullcalendar/index.global.min.js'></script>
<script src="/assets/javascripts/fullcalendar/ical.min.js"></script>
<script src="/assets/javascripts/fullcalendar/icalendar.global.min.js"></script>
<style>
  /* Make pointer show when hovering events */
  .fc-event, .fc-event-title, .fc-daygrid-event {
    cursor: pointer;
  }

  .fc-event, .fc-daygrid-event {
    background-color: #fcc200; /* Replace with any color you prefer */
    border-color: #da9100;     /* Optional: match border to background */
  }

  .fc-h-event .fc-event-main {
    color: #000;
  }
</style>

<script>
  function formatDateParts(date) {
    // Get date parts in CET/CEST
    const options = {timeZone: 'Europe/Amsterdam'};
    return {
      year: date.toLocaleString('en-UK', {...options, year: 'numeric'}),
      month: date.toLocaleString('en-UK', {...options, month: 'long'}),
      day: date.toLocaleString('en-UK', {...options, day: 'numeric'})
    };
  }

  function getDisplayEndDate(start, end) {
    if (!end) return null;
    if (end.getHours() === 0 && end.getMinutes() === 0 && end.getSeconds() === 0) {
      let adjusted = new Date(end.getTime());
      adjusted.setDate(adjusted.getDate() - 1);
      return adjusted;
    }
    return end;
  }

  function formatDateRange(startDate, endDate) {
    const start = formatDateParts(startDate);
    const end = endDate ? formatDateParts(endDate) : null;

    if (!endDate || (start.year === end.year && start.month === end.month && start.day === end.day)) {
      // Single-day event
      return `${start.day} ${start.month} ${start.year}`;
    } else if (start.year === end.year && start.month === end.month) {
      // Same month and year
      return `${start.day}${end.day} ${start.month} ${start.year}`;
    } else if (start.year === end.year) {
      // Same year, different month
      return `${start.day} ${start.month}${end.day} ${end.month} ${start.year}`;
    } else {
      // Different years
      return `${start.day} ${start.month} ${start.year}${end.day} ${end.month} ${end.year}`;
    }
  }

  document.addEventListener('DOMContentLoaded', function() {
    var calendarEl = document.getElementById('calendar');
    var calendar = new FullCalendar.Calendar(calendarEl, {
      initialView: 'dayGridMonth',
      events: {
        url: './events.ics',
        format: 'ics'
      },
      eventClick: function(info) {
        let startDate = info.event.start;
        let rawEndDate = info.event.end;
        let endDate = getDisplayEndDate(startDate, rawEndDate);
        let dateText = formatDateRange(startDate, endDate);

        document.getElementById('modalContent').innerHTML =
          `<strong>${info.event.title}</strong><br>` +
          (info.event.extendedProps.description || 'No description') + '<br><br>' +
          `${dateText} (` + (info.event.extendedProps.location || 'No location') + ')';
        document.getElementById('eventModal').style.display = 'block';
      }
    });
    calendar.render();
  });
</script>
<div id='calendar'></div>
<!-- Modal structure, hidden by default -->
<div id="eventModal" style="display:none; padding: 1em 0;">
  <div id="modalContent"></div>
  <button class="md-button" onclick="document.getElementById('eventModal').style.display='none'">Close</button>
</div>

Cron job

Since the data does not really change often, a cron job on my server simply calls a script to update the external sources once or twice a day. In my devcontainer:

update-webdate.sh
#!/bin/bash

cd /workspace/docs # (1)!

curl 'https://calendar.google.com/calendar/ical/mpgoip03vf1l192nes61jmluks%40group.calendar.google.com/public/basic.ics' -o now/events.ics
curl 'https://api.zotero.org/users/25224/items?tag=now&format=bib&style=apa' -o now/zotero.html
  1. Change to the proper directory on the server for the cron job.