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.
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.
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.