This is a Quartz component I vibecoded that displays a notes based on date using a frontmatter element named “holiday” and either the date or one of a preset list of named holidays like Christmas or Easter. It only appears on the index page of the site and is set to display the next seven days worth of notes.
To install run the following:
npx quartz plugin add github:UndefeatedOrca/quartz-holiday-calendarThe full code for the Quartz 4 version of this plugin as of 12/29/25 is at the end of this note.
Repo is here: UndefeatedOrca/quartz-holiday-calendar
Function
Although I vibecoded this entire thing, and also don’t understand typescript, here’s what I understand.
The component does three things
- Calculate the dates of the various moving holidays - Easter, Memorial Day, Thanksgiving, etc. - and assigns them and the preset holidays names
- Searches the content folder for files with the
holidayfrontmatter property and checks the contents for either valid holiday names or dates in the MM/DD format - Checks the date and displays a list of notes that are set for either today or a configurable amount of days in the future
Holiday Aliases
Current List
The following is a list of current holidays that will work with an alias
Moving Holidays (Calculated):
Christian/Liturgical:
advent1- first Sunday of Adventadvent2- second Sunday of Adventadvent3- third Sunday of Adventadvent4- fourth Sunday of Adventshrove-tuesday(ormardi-gras)ash-wednesdaypalm-sundaygood-fridayeasterpentecosttrinity-sundaychrist-the-king
US Federal/Observances:mlk-daypresidents-daymemorial-daylabor-daythanksgiving
Other:mothers-dayfathers-day
Fixed Date Holidays With Aliases:
US Civic:new-years-day(1/1)new-years-eve(12/31)independence-day(7/4)juneteenth(6/19)veterans-day(11/11)pearl-harbor-day(12/7)
Religious:epiphany(1/6)valentines-day(2/14)st-patricks-day(3/17)halloween(10/31)all-saints-day(11/1)all-souls-day(11/2)christmas-eve(12/24)christmas(12/25)
Other:groundhog-day(2/2)cinco-de-mayo(5/5)earth-day(4/22)d-day(6/6)
Things to Add
- Tax Day
- Election Day
- Full list of observances from 2019 Book of Common Prayer
- Other fun days
Potential Improvements
In the future, I’d like to update this component (either myself or vibecoding) with a modular loading system so that users could add or remove lists of relevant dates like:
- US Federal Holidays
- State Specific Holidays
- Country Specific Holidays
- Various Liturgical Calendars
- Personal dates of significance or birthdays
The categories would also allow custom formatting depending on date so users could bold more significant days or color code different holidays in a way that was either generally useful to readers or portrayed fast information to superusers.
- Red highlighting for Red Letter Holy Days
- Bolding for US Federal holidays
- Italics for dates only personally significant
- etc.
Whether this ever gets off the ground remains to be seen
Quartz 4 Installation
- Download HolidayCalendar.tsx and add it to your ./quartz/components folder
- Edit index.ts to export the component
- Add the component to quartz.layout.ts. To change the number of upcoming days shown, use the
showUpcomingDaysargument as shown below:Component.HolidayCalendar({ showUpcomingDays: 30 })
Code
// When adding dates to this file, remember that Date objects in JavaScript are 0-indexed
import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types"
import { resolveRelative } from "../util/path"
import { QuartzPluginData } from "../plugins/vfile"
interface HolidayCalendarOptions {
showUpcomingDays?: number // How many days ahead to show
}
const defaultOptions: HolidayCalendarOptions = {
showUpcomingDays: 30,
}
// Easter calculation using Computus algorithm (Anonymous Gregorian algorithm)
function calculateEaster(year: number): Date {
const a = year % 19
const b = Math.floor(year / 100)
const c = year % 100
const d = Math.floor(b / 4)
const e = b % 4
const f = Math.floor((b + 8) / 25)
const g = Math.floor((b - f + 1) / 3)
const h = (19 * a + b - d - g + 15) % 30
const i = Math.floor(c / 4)
const k = c % 4
const l = (32 + 2 * e + 2 * i - h - k) % 7
const m = Math.floor((a + 11 * h + 22 * l) / 451)
const month = Math.floor((h + l - 7 * m + 114) / 31)
const day = ((h + l - 7 * m + 114) % 31) + 1
return new Date(year, month - 1, day)
}
// Calculate Advent Sunday (4 Sundays before Christmas, numbered 0-3)
function calculateAdvent(year: number, sundayNumber: number): Date {
const christmas = new Date(year, 11, 25) // December 25
const christmasDay = christmas.getDay()
// Calculate days from Christmas back to the nearest Sunday
const daysToSunday = christmasDay === 0 ? 0 : christmasDay
// Go back 4 weeks from that Sunday to get advent0 (first Sunday of Advent)
const advent0Date = 25 - daysToSunday - 21 // 3 full weeks (21 days) before the last Sunday before Christmas
const advent0 = new Date(year, 11, advent0Date)
// If advent0 would be before November 27, it means we need to adjust
// (Advent always starts on or after November 27, and no later than December 3)
if (advent0Date < 27) {
// Christmas is early in the week, so we need to go back one more week
advent0.setDate(advent0Date + 7)
}
// Return the requested Advent Sunday (0-3)
const adventDate = new Date(advent0)
adventDate.setDate(advent0.getDate() + (sundayNumber * 7))
return adventDate
}
// Calculate Christ the King Sunday (last Sunday before Advent, which is the Sunday before advent1)
function calculateChristKing(year: number): Date {
const advent1 = calculateAdvent(year, 0) // First Sunday of Advent
const christKing = new Date(advent1)
christKing.setDate(advent1.getDate() - 7)
return christKing
}
// Calculate nth weekday of month (e.g., 3rd Monday of January)
function getNthWeekdayOfMonth(year: number, month: number, weekday: number, n: number): Date {
const firstDay = new Date(year, month, 1)
const firstWeekday = firstDay.getDay()
// Calculate days until first occurrence of target weekday
let daysUntilWeekday = (weekday - firstWeekday + 7) % 7
// Calculate the date of nth occurrence
const targetDate = 1 + daysUntilWeekday + (n - 1) * 7
return new Date(year, month, targetDate)
}
// Calculate last weekday of month
function getLastWeekdayOfMonth(year: number, month: number, weekday: number): Date {
// Start from last day of month and work backwards
const lastDay = new Date(year, month + 1, 0)
const lastDayWeekday = lastDay.getDay()
const daysBack = (lastDayWeekday - weekday + 7) % 7
return new Date(year, month, lastDay.getDate() - daysBack)
}
// Calculate all moving holidays for a given year
function calculateMovingHolidays(year: number): Map<string, Date> {
const holidays = new Map<string, Date>()
// Easter-based holidays
const easter = calculateEaster(year)
holidays.set("easter", easter)
const goodFriday = new Date(easter)
goodFriday.setDate(easter.getDate() - 2)
holidays.set("good-friday", goodFriday)
const ashWednesday = new Date(easter)
ashWednesday.setDate(easter.getDate() - 46)
holidays.set("ash-wednesday", ashWednesday)
const shroveTuesday = new Date(easter)
shroveTuesday.setDate(easter.getDate() - 47)
holidays.set("shrove-tuesday", shroveTuesday)
holidays.set("mardi-gras", shroveTuesday)
const pentecost = new Date(easter)
pentecost.setDate(easter.getDate() + 49)
holidays.set("pentecost", pentecost)
const trinitySunday = new Date(easter)
trinitySunday.setDate(easter.getDate() + 56)
holidays.set("trinity-sunday", trinitySunday)
const palmSunday = new Date(easter)
palmSunday.setDate(easter.getDate() - 7)
holidays.set("palm-sunday", palmSunday)
// Advent Sundays (1-4, the four Sundays of Advent)
holidays.set("advent1", calculateAdvent(year, 0))
holidays.set("advent2", calculateAdvent(year, 1))
holidays.set("advent3", calculateAdvent(year, 2))
holidays.set("advent4", calculateAdvent(year, 3))
// Christ the King
holidays.set("christ-the-king", calculateChristKing(year))
// US Federal holidays
holidays.set("mlk-day", getNthWeekdayOfMonth(year, 0, 1, 3)) // 3rd Monday of January
holidays.set("presidents-day", getNthWeekdayOfMonth(year, 1, 1, 3)) // 3rd Monday of February
holidays.set("memorial-day", getLastWeekdayOfMonth(year, 4, 1)) // Last Monday of May
holidays.set("labor-day", getNthWeekdayOfMonth(year, 8, 1, 1)) // 1st Monday of September
holidays.set("thanksgiving", getNthWeekdayOfMonth(year, 10, 4, 4)) // 4th Thursday of November
// Other holidays
holidays.set("mothers-day", getNthWeekdayOfMonth(year, 4, 0, 2)) // 2nd Sunday of May
holidays.set("fathers-day", getNthWeekdayOfMonth(year, 5, 0, 3)) // 3rd Sunday of June
// Fixed date holidays (US Civic)
holidays.set("new-years-day", new Date(year, 0, 1))
holidays.set("new-years-eve", new Date(year, 11, 31))
holidays.set("valentines-day", new Date(year, 1, 14))
holidays.set("st-patricks-day", new Date(year, 2, 17))
holidays.set("independence-day", new Date(year, 6, 4))
holidays.set("juneteenth", new Date(year, 5, 19))
holidays.set("veterans-day", new Date(year, 10, 11))
holidays.set("pearl-harbor-day", new Date(year, 11, 7))
// Fixed date holidays (Religious)
holidays.set("epiphany", new Date(year, 0, 6)) // January 6
holidays.set("halloween", new Date(year, 9, 31)) // October 31
holidays.set("all-saints-day", new Date(year, 10, 1)) // November 1
holidays.set("all-souls-day", new Date(year, 10, 2)) // November 2
holidays.set("christmas-eve", new Date(year, 11, 24)) // December 24
holidays.set("christmas", new Date(year, 11, 25)) // December 25
// Fixed date holidays (Other popular)
holidays.set("groundhog-day", new Date(year, 1, 2))
holidays.set("cinco-de-mayo", new Date(year, 4, 5))
holidays.set("earth-day", new Date(year, 3, 22))
holidays.set("d-day", new Date(year, 5, 6))
return holidays
}
export default ((opts?: Partial<HolidayCalendarOptions>) => {
const options: HolidayCalendarOptions = { ...defaultOptions, ...opts }
const HolidayCalendar: QuartzComponent = (props: QuartzComponentProps) => {
const { allFiles, fileData } = props
// Get current year and calculate all moving holidays
const today = new Date()
const currentYear = today.getFullYear()
const movingHolidays = calculateMovingHolidays(currentYear)
// Create a map of dates to holiday names for reverse lookup
const dateToHolidays = new Map<string, string[]>()
movingHolidays.forEach((date, holidayName) => {
const dateKey = `${String(date.getMonth() + 1).padStart(2, "0")}/${String(date.getDate()).padStart(2, "0")}`
if (!dateToHolidays.has(dateKey)) {
dateToHolidays.set(dateKey, [])
}
dateToHolidays.get(dateKey)!.push(holidayName)
})
// Get all files with holiday frontmatter
const holidayPattern = /^(\d{2})\/(\d{2})$/
const holidayNotes: Map<string, QuartzPluginData[]> = new Map()
allFiles.forEach((file) => {
const holiday = file.frontmatter?.holiday
if (!holiday) return
// Handle both single date strings and arrays of dates
const dates = Array.isArray(holiday) ? holiday : [holiday]
dates.forEach((dateStr: string) => {
// Check if it's a fixed date (MM/DD format)
const fixedMatch = String(dateStr).match(holidayPattern)
if (fixedMatch) {
const [, month, day] = fixedMatch
const dateKey = `${month}/${day}`
if (!holidayNotes.has(dateKey)) {
holidayNotes.set(dateKey, [])
}
holidayNotes.get(dateKey)!.push(file)
}
// Check if it's a moving holiday name
else if (movingHolidays.has(String(dateStr).toLowerCase())) {
const holidayDate = movingHolidays.get(String(dateStr).toLowerCase())!
const dateKey = `${String(holidayDate.getMonth() + 1).padStart(2, "0")}/${String(holidayDate.getDate()).padStart(2, "0")}`
if (!holidayNotes.has(dateKey)) {
holidayNotes.set(dateKey, [])
}
holidayNotes.get(dateKey)!.push(file)
}
})
})
const todayKey = `${String(today.getMonth() + 1).padStart(2, "0")}/${String(today.getDate()).padStart(2, "0")}`
const todayNotes = holidayNotes.get(todayKey) || []
// Get upcoming dates
const upcomingHolidays: Array<{ date: Date; dateKey: string; notes: QuartzPluginData[]; holidayNames: string[] }> = []
for (let i = 1; i <= options.showUpcomingDays!; i++) {
const futureDate = new Date(today)
futureDate.setDate(today.getDate() + i)
const futureDateKey = `${String(futureDate.getMonth() + 1).padStart(2, "0")}/${String(futureDate.getDate()).padStart(2, "0")}`
if (holidayNotes.has(futureDateKey)) {
upcomingHolidays.push({
date: futureDate,
dateKey: futureDateKey,
notes: holidayNotes.get(futureDateKey)!,
holidayNames: dateToHolidays.get(futureDateKey) || [],
})
}
}
const formatDate = (date: Date) => {
return date.toLocaleDateString("en-US", { month: "long", day: "numeric" })
}
const formatHolidayName = (name: string) => {
return name.split("-").map(word => word.charAt(0).toUpperCase() + word.slice(1)).join(" ")
}
const renderNoteList = (notes: QuartzPluginData[]) => {
return (
<ul style="margin: 0.5rem 0; padding-left: 1.5rem;">
{notes.map((note) => {
const href = resolveRelative(fileData.slug!, note.slug!)
return (
<li key={note.slug}>
<a href={href} class="internal">
{note.frontmatter?.title || note.slug}
</a>
</li>
)
})}
</ul>
)
}
// Don't render if no holiday content
if (holidayNotes.size === 0) {
return null
}
const todayHolidayNames = dateToHolidays.get(todayKey) || []
return (
<div class="holiday-calendar" style="margin: 1.5 rem 0; padding: 1.5rem; border: 1px solid var(--lightgray); border-radius: 8px; background: var(--light); max height 800px; overflow-y: auto;">
{todayNotes.length > 0 && (
<div style="margin-bottom: 0.25rem;">
<h4 style="margin: 0 0 0.5rem 0;">
{formatDate(today)}
{todayHolidayNames.length > 0 && (
<span>
{" - "}{todayHolidayNames.map(formatHolidayName).join(", ")}
</span>
)}
</h4>
{renderNoteList(todayNotes)}
</div>
)}
{upcomingHolidays.length > 0 && (
<div>
{upcomingHolidays.map(({ date, dateKey, notes, holidayNames }) => (
<div key={dateKey} style="margin-bottom: 0.25rem;">
<h5 style="margin: 0 0 0.25rem 0; color: var(--darkgray);">
{formatDate(date)}
{holidayNames.length > 0 && (
<span>
{" - "}{holidayNames.map(formatHolidayName).join(", ")}
</span>
)}
</h5>
{renderNoteList(notes)}
</div>
))}
</div>
)}
{todayNotes.length === 0 && upcomingHolidays.length === 0 && (
<p style="color: var(--gray); font-style: italic;">No holidays in the next {options.showUpcomingDays} days</p>
)}
</div>
)
}
return HolidayCalendar
}) satisfies QuartzComponentConstructorcurrent code
This might not work
The old code requires a new sync to update the calendar. In theory, this doesn’t. In practice, I updated it at a time when there aren’t any holidays so I actually have no idea if it worked or not.
// When adding dates to this file, remember that Date objects in JavaScript are 0-indexed
import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types"
import { resolveRelative } from "../util/path"
interface HolidayCalendarOptions {
showUpcomingDays?: number // How many days ahead to show
}
const defaultOptions: HolidayCalendarOptions = {
showUpcomingDays: 30,
}
// Easter calculation using Computus algorithm (Anonymous Gregorian algorithm)
function calculateEaster(year: number): Date {
const a = year % 19
const b = Math.floor(year / 100)
const c = year % 100
const d = Math.floor(b / 4)
const e = b % 4
const f = Math.floor((b + 8) / 25)
const g = Math.floor((b - f + 1) / 3)
const h = (19 * a + b - d - g + 15) % 30
const i = Math.floor(c / 4)
const k = c % 4
const l = (32 + 2 * e + 2 * i - h - k) % 7
const m = Math.floor((a + 11 * h + 22 * l) / 451)
const month = Math.floor((h + l - 7 * m + 114) / 31)
const day = ((h + l - 7 * m + 114) % 31) + 1
return new Date(year, month - 1, day)
}
// Calculate Advent Sunday (4 Sundays before Christmas, numbered 0-3)
function calculateAdvent(year: number, sundayNumber: number): Date {
const christmas = new Date(year, 11, 25) // December 25
const christmasDay = christmas.getDay()
// Calculate days from Christmas back to the nearest Sunday
const daysToSunday = christmasDay === 0 ? 0 : christmasDay
// Go back 4 weeks from that Sunday to get advent0 (first Sunday of Advent)
const advent0Date = 25 - daysToSunday - 21 // 3 full weeks (21 days) before the last Sunday before Christmas
const advent0 = new Date(year, 11, advent0Date)
// If advent0 would be before November 27, it means we need to adjust
// (Advent always starts on or after November 27, and no later than December 3)
if (advent0Date < 27) {
// Christmas is early in the week, so we need to go back one more week
advent0.setDate(advent0Date + 7)
}
// Return the requested Advent Sunday (0-3)
const adventDate = new Date(advent0)
adventDate.setDate(advent0.getDate() + (sundayNumber * 7))
return adventDate
}
// Calculate Christ the King Sunday (last Sunday before Advent, which is the Sunday before advent1)
function calculateChristKing(year: number): Date {
const advent1 = calculateAdvent(year, 0) // First Sunday of Advent
const christKing = new Date(advent1)
christKing.setDate(advent1.getDate() - 7)
return christKing
}
// Calculate nth weekday of month (e.g., 3rd Monday of January)
function getNthWeekdayOfMonth(year: number, month: number, weekday: number, n: number): Date {
const firstDay = new Date(year, month, 1)
const firstWeekday = firstDay.getDay()
// Calculate days until first occurrence of target weekday
let daysUntilWeekday = (weekday - firstWeekday + 7) % 7
// Calculate the date of nth occurrence
const targetDate = 1 + daysUntilWeekday + (n - 1) * 7
return new Date(year, month, targetDate)
}
// Calculate last weekday of month
function getLastWeekdayOfMonth(year: number, month: number, weekday: number): Date {
// Start from last day of month and work backwards
const lastDay = new Date(year, month + 1, 0)
const lastDayWeekday = lastDay.getDay()
const daysBack = (lastDayWeekday - weekday + 7) % 7
return new Date(year, month, lastDay.getDate() - daysBack)
}
// Calculate all moving holidays for a given year
function calculateMovingHolidays(year: number): Map<string, Date> {
const holidays = new Map<string, Date>()
// Easter-based holidays
const easter = calculateEaster(year)
holidays.set("easter", easter)
const goodFriday = new Date(easter)
goodFriday.setDate(easter.getDate() - 2)
holidays.set("good-friday", goodFriday)
const ashWednesday = new Date(easter)
ashWednesday.setDate(easter.getDate() - 46)
holidays.set("ash-wednesday", ashWednesday)
const shroveTuesday = new Date(easter)
shroveTuesday.setDate(easter.getDate() - 47)
holidays.set("shrove-tuesday", shroveTuesday)
holidays.set("mardi-gras", shroveTuesday)
const pentecost = new Date(easter)
pentecost.setDate(easter.getDate() + 49)
holidays.set("pentecost", pentecost)
const trinitySunday = new Date(easter)
trinitySunday.setDate(easter.getDate() + 56)
holidays.set("trinity-sunday", trinitySunday)
const palmSunday = new Date(easter)
palmSunday.setDate(easter.getDate() - 7)
holidays.set("palm-sunday", palmSunday)
// Advent Sundays (1-4, the four Sundays of Advent)
holidays.set("advent1", calculateAdvent(year, 0))
holidays.set("advent2", calculateAdvent(year, 1))
holidays.set("advent3", calculateAdvent(year, 2))
holidays.set("advent4", calculateAdvent(year, 3))
// Christ the King
holidays.set("christ-the-king", calculateChristKing(year))
// US Federal holidays
holidays.set("mlk-day", getNthWeekdayOfMonth(year, 0, 1, 3)) // 3rd Monday of January
holidays.set("presidents-day", getNthWeekdayOfMonth(year, 1, 1, 3)) // 3rd Monday of February
holidays.set("memorial-day", getLastWeekdayOfMonth(year, 4, 1)) // Last Monday of May
holidays.set("labor-day", getNthWeekdayOfMonth(year, 8, 1, 1)) // 1st Monday of September
holidays.set("thanksgiving", getNthWeekdayOfMonth(year, 10, 4, 4)) // 4th Thursday of November
// Other holidays
holidays.set("mothers-day", getNthWeekdayOfMonth(year, 4, 0, 2)) // 2nd Sunday of May
holidays.set("fathers-day", getNthWeekdayOfMonth(year, 5, 0, 3)) // 3rd Sunday of June
// Fixed date holidays (US Civic)
holidays.set("new-years-day", new Date(year, 0, 1))
holidays.set("new-years-eve", new Date(year, 11, 31))
holidays.set("valentines-day", new Date(year, 1, 14))
holidays.set("st-patricks-day", new Date(year, 2, 17))
holidays.set("independence-day", new Date(year, 6, 4))
holidays.set("juneteenth", new Date(year, 5, 19))
holidays.set("veterans-day", new Date(year, 10, 11))
holidays.set("pearl-harbor-day", new Date(year, 11, 7))
// Fixed date holidays (Religious)
holidays.set("epiphany", new Date(year, 0, 6)) // January 6
holidays.set("halloween", new Date(year, 9, 31)) // October 31
holidays.set("all-saints-day", new Date(year, 10, 1)) // November 1
holidays.set("all-souls-day", new Date(year, 10, 2)) // November 2
holidays.set("christmas-eve", new Date(year, 11, 24)) // December 24
holidays.set("christmas", new Date(year, 11, 25)) // December 25
// Fixed date holidays (Other popular)
holidays.set("groundhog-day", new Date(year, 1, 2))
holidays.set("cinco-de-mayo", new Date(year, 4, 5))
holidays.set("earth-day", new Date(year, 3, 22))
holidays.set("d-day", new Date(year, 5, 6))
return holidays
}
// Serialize holiday note data for client-side use
interface SerializedNote {
slug: string
title: string
href: string
}
interface SerializedHolidayEntry {
/** MM/DD */
dateKey: string
/** ISO date string for the year the entry was calculated for */
isoDate: string
notes: SerializedNote[]
holidayNames: string[]
}
export default ((opts?: Partial<HolidayCalendarOptions>) => {
const options: HolidayCalendarOptions = { ...defaultOptions, ...opts }
const HolidayCalendar: QuartzComponent = (props: QuartzComponentProps) => {
const { allFiles, fileData } = props
// We need to cover this year AND next year so that a page loaded in
// late December can still show entries that fall in early January.
const buildYear = new Date().getFullYear()
const years = [buildYear, buildYear + 1]
// Build a combined MM/DD → Date map across both years so we can resolve
// moving-holiday *names* (e.g. "easter") to their actual calendar dates.
// When the same MM/DD appears in both years we keep both; the client will
// pick whichever one is currently relevant.
const movingHolidaysByYear: Map<number, Map<string, Date>> = new Map()
years.forEach((y) => movingHolidaysByYear.set(y, calculateMovingHolidays(y)))
// Helper: convert a Date → "MM/DD"
const toDateKey = (d: Date) =>
`${String(d.getMonth() + 1).padStart(2, "0")}/${String(d.getDate()).padStart(2, "0")}`
// ── Build a complete entry map: dateKey → { notes[], holidayNames[], isoDate } ──
// We compute one entry per (dateKey × year) pair so the client can always
// resolve the correct year from "today".
const entryMap = new Map<
string, // "MM/DD|YYYY"
{ dateKey: string; isoDate: string; notes: SerializedNote[]; holidayNames: string[] }
>()
const getOrCreate = (dateKey: string, year: number, date: Date) => {
const key = `${dateKey}|${year}`
if (!entryMap.has(key)) {
entryMap.set(key, {
dateKey,
isoDate: date.toISOString(),
notes: [],
holidayNames: [],
})
}
return entryMap.get(key)!
}
// Populate holidayNames from the moving-holiday maps
years.forEach((year) => {
const map = movingHolidaysByYear.get(year)!
map.forEach((date, name) => {
const dk = toDateKey(date)
const entry = getOrCreate(dk, year, date)
if (!entry.holidayNames.includes(name)) entry.holidayNames.push(name)
})
})
// Populate notes from file frontmatter
const holidayPattern = /^(\d{2})\/(\d{2})$/
allFiles.forEach((file) => {
const holiday = file.frontmatter?.holiday
if (!holiday) return
const dates = Array.isArray(holiday) ? holiday : [holiday]
dates.forEach((dateStr: string) => {
const fixedMatch = String(dateStr).match(holidayPattern)
if (fixedMatch) {
// Fixed date (MM/DD) — applies to every year
const [, month, day] = fixedMatch
const dk = `${month}/${day}`
years.forEach((year) => {
const date = new Date(year, Number(month) - 1, Number(day))
const entry = getOrCreate(dk, year, date)
entry.notes.push({
slug: file.slug!,
title: String(file.frontmatter?.title ?? file.slug),
href: resolveRelative(fileData.slug!, file.slug!),
})
})
} else {
// Moving holiday name — resolve per year
const normalised = String(dateStr).toLowerCase()
years.forEach((year) => {
const map = movingHolidaysByYear.get(year)!
if (!map.has(normalised)) return
const date = map.get(normalised)!
const dk = toDateKey(date)
const entry = getOrCreate(dk, year, date)
const note: SerializedNote = {
slug: file.slug!,
title: String(file.frontmatter?.title ?? file.slug),
href: resolveRelative(fileData.slug!, file.slug!),
}
if (!entry.notes.find((n) => n.slug === note.slug)) {
entry.notes.push(note)
}
})
}
})
})
// Only emit entries that actually have notes attached
const serialized: SerializedHolidayEntry[] = Array.from(entryMap.values()).filter(
(e) => e.notes.length > 0,
)
if (serialized.length === 0) return null
const dataJson = JSON.stringify(serialized)
const showDays = options.showUpcomingDays!
return (
<div
class="holiday-calendar"
data-holiday-entries={dataJson}
data-show-upcoming-days={String(showDays)}
style="margin: 1.5rem 0; padding: 1.5rem; border: 1px solid var(--lightgray); border-radius: 8px; background: var(--light); max-height: 800px; overflow-y: auto;"
>
{/* Content is rendered client-side; this placeholder avoids layout shift */}
<p class="holiday-calendar-loading" style="color: var(--gray); font-style: italic;">
Loading calendar…
</p>
</div>
)
}
// ── Client-side script ──────────────────────────────────────────────────────
// Quartz runs afterDOMLoaded scripts after every client-side navigation,
// so "today" is always evaluated at the moment the user views the page —
// no rebuild required.
HolidayCalendar.afterDOMLoaded = `
(function () {
function formatHolidayName(name) {
return name.split("-").map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(" ")
}
function formatDate(dateStr) {
// dateStr is an ISO string; parse as local date to avoid UTC-offset shift
const [year, month, day] = dateStr.split("T")[0].split("-").map(Number)
const d = new Date(year, month - 1, day)
return d.toLocaleDateString("en-US", { month: "long", day: "numeric" })
}
function renderNoteList(notes) {
return "<ul style='margin:0.5rem 0;padding-left:1.5rem;'>" +
notes.map(n =>
"<li><a href='" + n.href + "' class='internal'>" + n.title + "</a></li>"
).join("") +
"</ul>"
}
function render(container) {
const raw = container.dataset.holidayEntries
const showDays = parseInt(container.dataset.showUpcomingDays || "30", 10)
if (!raw) return
const entries = JSON.parse(raw) // SerializedHolidayEntry[]
const today = new Date()
// Strip time component so day-diff maths works cleanly
today.setHours(0, 0, 0, 0)
// Build a map: isoDate-prefix (YYYY-MM-DD) → entry
const byDate = new Map()
entries.forEach(entry => {
const prefix = entry.isoDate.split("T")[0]
byDate.set(prefix, entry)
})
// Find today's entry and upcoming entries
const pad = n => String(n).padStart(2, "0")
const todayPrefix = today.getFullYear() + "-" + pad(today.getMonth() + 1) + "-" + pad(today.getDate())
const todayEntry = byDate.get(todayPrefix) || null
const upcoming = []
for (let i = 1; i <= showDays; i++) {
const future = new Date(today)
future.setDate(today.getDate() + i)
const prefix = future.getFullYear() + "-" + pad(future.getMonth() + 1) + "-" + pad(future.getDate())
if (byDate.has(prefix)) {
upcoming.push(byDate.get(prefix))
}
}
let html = ""
if (todayEntry) {
const names = todayEntry.holidayNames.map(formatHolidayName).join(", ")
html += "<div style='margin-bottom:0.25rem;'>"
html += "<h4 style='margin:0 0 0.5rem 0;'>"
html += formatDate(todayEntry.isoDate)
if (names) html += " <span>- " + names + "</span>"
html += "</h4>"
html += renderNoteList(todayEntry.notes)
html += "</div>"
}
upcoming.forEach(entry => {
const names = entry.holidayNames.map(formatHolidayName).join(", ")
html += "<div style='margin-bottom:0.25rem;'>"
html += "<h5 style='margin:0 0 0.25rem 0;color:var(--darkgray);'>"
html += formatDate(entry.isoDate)
if (names) html += " <span>- " + names + "</span>"
html += "</h5>"
html += renderNoteList(entry.notes)
html += "</div>"
})
if (!todayEntry && upcoming.length === 0) {
html = "<p style='color:var(--gray);font-style:italic;'>No holidays in the next " + showDays + " days</p>"
}
container.innerHTML = html
}
document.querySelectorAll(".holiday-calendar[data-holiday-entries]").forEach(render)
})()
`
return HolidayCalendar
}) satisfies QuartzComponentConstructor