Calendar Snippet

Hi!
First a bit of praise! I’m one of the lead developers for Boots Online Doctor (https://onlinedoctor.boots.com). My team absolutely loves Hugo; we moved over from Webpack/ETA because it was taking 45+ minutes to compile our site. Our site now consists of around 150 pages, and compile time is floating around 500ms for a full rebuild - It’s absolutely brilliant, and honestly don’t know how it works this fast (or what we would do without it!)

Over the weekend I was updating my personal website (https://soen.one), and was needing to show a calendar of all the events I was planning on attending; and found out there wasn’t really any good code on here to render a calendar. So I thought I’d share what I wrote in case others could use it:

{{ $startDate := time.AsTime "2024-01-01" }}
{{ $endDate := time.AsTime "2024-12-31" }}
{{ $oneDay := time.ParseDuration "24h"}}

{{ $duration := $endDate.Sub $startDate }}

{{ $days :=  div $duration.Hours 24 }}

{{ $currentdate := $startDate }}

{{ $lastmonth := 0 }}

{{ range seq $days }}
    {{ $isSunday := strings.Contains $currentdate.Weekday "Sunday"}}
    {{ $isSaturday := strings.Contains $currentdate.Weekday "Saturday"}}

    {{ if ne $currentdate.Month $lastmonth }}
        {{ $lastmonth = $currentdate.Month }}

        <h1>{{ $currentdate.Month }}</h1>

        <table border="1">
        <tr>
            <td>Sunday</td>
            <td>Monday</td>
            <td>Tuesday</td>
            <td>Wednesday</td>
            <td>Thursday</td>
            <td>Friday</td>
            <td>Saturday</td>
        </tr>
            <tr>
                {{ $paddingDate := time.AsTime "2024-12-01" }} {{/* Don't change this date! - This is constant for (any) month that begins with Sunday */}} 
                {{ $monthStartDateString := print (time.Format "2006-01" $currentdate) "-01" }}
                {{ $monthStartDate := time.AsTime $monthStartDateString }}

                {{ range seq 0 6 }}               
                    {{ if strings.Contains $paddingDate.Weekday $monthStartDate.Weekday }}
                        {{ break }}
                    {{ end }}
                    <td></td>
                    {{ $paddingDate = $paddingDate.Add $oneDay }} 
                {{ end }}
    {{ else }}
        {{ if $isSunday }}
            <tr>
        {{ end }}
    {{ end }}

    <td>
        {{ $currentdate.Day }}  
    </td>

    {{ if $isSaturday }}
        </tr>
    {{ end }}

    {{/*  Calculate date for next loop  */}}
    {{ $currentdate = $currentdate.Add $oneDay  }}

    {{ if ne $currentdate.Month $lastmonth }}
        </table>
    {{ end }}
{{ end }}

I’m sure the code could be improved, but it essentially works, and I just wanted to save someone the pain of having to figure it out from scratch, and give back to an amazing project.

Thanks again!
Soen

3 Likes

Both sites are really nice!

https://soen.one may be the first podcast example I’ve seen; a great fit for an SSG like Hugo.

Very Nice ! How are you connecting the data to the calendar ?

Thanks !

Thankyou! I’m also doing something clever on the podcast page; I’m actually pulling the XML as a datasource in hugo direct from SoundCloud that hosts my podcast episodes; and then iterating over that to build the page. It’s a very elegant solution, though it does mean rebuilding the site when I release a new episode. But I restart the server once a day, and I’m debating just adding a bit of scripting to rebuild the hugo site when that happens to keep it updated.

1 Like

Thankyou! I actually have a quick and dirty node.js script that logs onto a google calendar and pulls a list of events in JSON format; I write this direct to a file in the data folder so that Hugo can consume it as a data source; and when the dates line up within the range loop, I simply output an extra div.

Node.js script:

const fs = require('fs').promises;
const path = require('path');
const process = require('process');
const {authenticate} = require('@google-cloud/local-auth');
const {google} = require('googleapis');

// If modifying these scopes, delete token.json.
const SCOPES = ['https://www.googleapis.com/auth/calendar.readonly'];
// The file token.json stores the user's access and refresh tokens, and is
// created automatically when the authorization flow completes for the first
// time.
const TOKEN_PATH = path.join(process.cwd(), 'token.json');
const CREDENTIALS_PATH = path.join(process.cwd(), 'credentials.json');

/**
 * Reads previously authorized credentials from the save file.
 *
 * @return {Promise<OAuth2Client|null>}
 */
async function loadSavedCredentialsIfExist() {
  try {
    const content = await fs.readFile(TOKEN_PATH);
    const credentials = JSON.parse(content);
    return google.auth.fromJSON(credentials);
  } catch (err) {
    return null;
  }
}

/**
 * Serializes credentials to a file compatible with GoogleAUth.fromJSON.
 *
 * @param {OAuth2Client} client
 * @return {Promise<void>}
 */
async function saveCredentials(client) {
  const content = await fs.readFile(CREDENTIALS_PATH);
  const keys = JSON.parse(content);
  const key = keys.installed || keys.web;
  const payload = JSON.stringify({
    type: 'authorized_user',
    client_id: key.client_id,
    client_secret: key.client_secret,
    refresh_token: client.credentials.refresh_token,
  });
  await fs.writeFile(TOKEN_PATH, payload);
}

/**
 * Load or request or authorization to call APIs.
 *
 */
async function authorize() {
  let client = await loadSavedCredentialsIfExist();
  if (client) {
    return client;
  }
  client = await authenticate({
    scopes: SCOPES,
    keyfilePath: CREDENTIALS_PATH,
  });
  if (client.credentials) {
    await saveCredentials(client);
  }
  return client;
}


/**
 * Lists the next 10 events on the user's primary calendar.
 * @param {google.auth.OAuth2} auth An authorized OAuth2 client.
 */
async function listEvents(auth) {
    var d = new Date("01-01-2024 00:00");
    d.setFullYear(new Date().getFullYear());
  const calendar = google.calendar({version: 'v3', auth});
  const res = await calendar.events.list({
    calendarId: <enter your calendar ID>',
    timeMin: d.toISOString(),
    singleEvents: true,
    orderBy: 'startTime',
  });
  const events = res.data.items;
  if (!events || events.length === 0) {
    console.log('No upcoming events found.');
    return;
  }

  fs.writeFile("./data/calendar.json", JSON.stringify(events));
  console.log("calendar.json written.");
}

authorize().then(listEvents).catch(console.error);

And here’s the hugo snippet:

        {{ range site.Data.calendar }}

        {{/*  if event's start.date is greater or equal than today's date
            and if event's end.date is less than or equal today's date  */}}

            {{ $eventStartDate := time.AsTime .start.date }}
            {{ $eventEndDate := time.AsTime .end.date }}

            {{ if and (ge $currentdate $eventStartDate) (lt $currentdate $eventEndDate) }}

                {{ if .description }}
                    {{ $url := findRESubmatch `<a.*?>(.*?)</a>` .description 1 }}
                    {{ $url = index (index $url 0) 1 }}
                    <a href="{{ $url }}" title="Click to view more event details and to buy tickets">
                {{ end }}
                <cal-event>
                    {{.summary}}

                    {{ if .location }}
                        <cal-location>
                            {{.location}}
                        </cal-location>
                    {{ end }}
                </cal-event>

                {{ if .description }}
                    </a>
                {{ end }}

            {{ end }}

        {{ end }}

I would love to be able to do that 100% within Hugo and I do believe it is actually possible, I just had limited time to work on it yesterday; and Google essentially provides you the node.js script I use in their Quick Start guide.

2 Likes