I finished reading Bury Our Bones in the Midnight Soil by V.E. Schwab tonight. Loved it.

Finally finished Hollow Knight, now on to Silksong.

25 Sep, 2025 at 8:28 PM

25 Sep, 2025 at 7:53 PM

The Marvelous Mrs. Maisel
Season 6
Finished watching The Marvelous Mrs. Maisel.
RSVP to Nuit Blanche on 4 Oct, 2025 at 7:00 PM
I am going to
.Oct
04
Saturday, 04 October 2025
7:00 PM
All Over Toronto
Toronto, Ontario
RSVP to Side Project Social on 25 Sep, 2025 at 6:00 PM
I am going to
.Sep
25
Thursday, 25 September 2025
6:00 PM
1RG
Toronto, Ontario
RSVP to Destroyer - Dan's Boogie Tour on 8 Oct, 2025 at 8:00 PM
I am going to
.Oct
09
Thursday, 09 October 2025
8:00 PM
The Opera House
Toronto, Ontario
Super excited to see Destroyer next week!
Nails Painted With Black Onyx
Left Hand
Black Onyx Little finger on left hand
Black Onyx Ring finger on left hand
Black Onyx Middle finger on left hand
Black Onyx Index finger on left hand
Black Onyx Thumb finger on left hand
Right Hand
Black Onyx Little finger on right hand
Black Onyx Ring finger on right hand
Black Onyx Middle finger on right hand
Black Onyx Index finger on right hand
Black Onyx Thumb finger on right hand
Building a Calendar Interface in Astro
I wanted my website to have a clean, static calendar interface to showcase all my RSVP posts. At first, I considered using FullCalendar, an excellent package, but a bit too heavy for my needs. I wanted something that rendered at build time, without extra client-side scripting. Building a lightweight calendar in Astro with date-fns
was surprisingly straightforward.

Screenshot of the calendar interface for this website.
The Code
I am using date-fns to do a lot of the heavy lifting here.
Get the Static Paths
The getStaticPaths
generates multiple page routes from a single .astro
page component.
import { getCollection } from "astro:content";
import dateFns from "date-fns";
export const getStaticPaths = () => {
// The events collection is pretty generic, it would look something like
// this:
//
// const eventsCollection = defineCollection({
// loader: glob({ pattern: "*.md", base: "./src/content/events" }),
// schema: z.object({
// name: z.string(),
// startsAt: z.date(),
// endsAt: z.date().optional(),
// isAllDay: z.boolean(),
// }),
// })
const allEvents = getCollection("events");
// We want to get the upper and lower bound of the calendar pages without it
// we would have to have every possible month.
const eventDates = allEvents
.flatMap((post) => [post.data.startsAt, post.data.endsAt])
.filter(Boolean) as Date[];
const upperBound = dateFns.endOfMonth(dateFns.max(eventDates));
const lowerBound = dateFns.startOfMonth(dateFns.min(eventDates));
// We want to capture all the years and their months between the upper and
// lower bounds.
const yearsBetween = dateFns
.eachYearOfInterval({
start: lowerBound,
end: upperBound,
})
.map((yearStart) => {
const yearEnd = dateFns.endOfYear(yearStart);
const months = dateFns.eachMonthOfInterval({
start: dateFns.isBefore(yearStart, lowerBound) ? lowerBound : yearStart,
end: dateFns.isAfter(yearEnd, upperBound) ? upperBound : yearEnd,
});
return { months };
});
// Finally we can build all the pages that will be generated.
return yearsBetween.flatMap(({ months }) =>
months.map((monthStart) => {
const monthEnd = dateFns.endOfMonth(monthStart);
// Adjust the start and end to the nearest Monday and Sunday for a full
// week of coverage, this does mean the week will start on a Monday and
// end on a Sunday.
const calendarMonthStart = dateFns.isMonday(monthStart)
? monthStart
: dateFns.previousMonday(monthStart);
const calendarMonthEnd = dateFns.isSunday(monthEnd)
? monthEnd
: dateFns.nextSunday(monthEnd);
const days = dateFns
.eachDayOfInterval({
start: calendarMonthStart,
end: calendarMonthEnd,
})
.map((day) => ({
day,
events: allEvents.filter((event) =>
dateFns.isSameDay(event.data.startsAt, day),
),
}));
return {
params: {
// You'll want to change this to how your filename is, mine is
// `[...yyyymmm].astro`.
yyyymm: dateFns.format(monthStart, "yyyy-MM"),
},
props: {
start: calendarMonthStart,
end: calendarMonthEnd,
days,
},
};
}),
);
};
Build the Interface
The above builds three props: start
(first day of the calendar), end
(last day), and days
(an array of days and their events). I use Tailwind CSS for styling this website, I’ll be using their CSS classes.
{
/**
* The border attribute doesn't really work with grids, so to get this to
* work, we are going to add a slight gap between the grid's cells and
* subtract the margin a bit on the right. This means we'll have a
* tic-tac-toe border kind of like this:
*
* MON | TUE | WED | THU | FRI | SAT | SUN
* -----------------------------------------
* 29 | 31 | 1 | 2 | 3 | 4 | 5
* ...
* 27 | 28 | 29 | 30 | 31 | 1 | 2
*/
}
<div class:list={["-mr-0.5 grid grid-cols-7 gap-0.5 bg-black"]}>
{
["mon", "tue", "wed", "thu", "fri", "sat", "sun"].map((weekDay) => (
<h2
class:list={[...className, "bg-white text-right font-bold uppercase"]}
>
{weekDay}
</h2>
))
}
{
days.map(({ day, events }) => (
<div class:list={["h-32 bg-white"]}>
<h3 class:list={["px-1 py-0.5 text-right font-semibold"]}>
{dateFns.format(day, "d")}
</h3>
<ul class:list={["space-y-0.5 overflow-y-hidden text-sm"]}>
{events.map((event) => (
<li>
<a
href={`/events/${event.id}`}
class:list={[
"flex justify-between rounded-sm px-1 py-0.5",
"hover:bg-black/5",
]}
>
<span class:list={["block flex-shrink truncate"]}>
{event.data.name}
</span>
{!event.data.isAllDay && (
<span class:list={["block flex-shrink-0 text-black/75"]}>
{dateFns.format(event.data.startsAt, "h:mm a")}
</span>
)}
</a>
</li>
))}
</ul>
</div>
))
}
</div>
Wrap Up
Now my website has a calendar interface to my RSVP posts.
RSVP to Subvert x New Feeling: Music and Co-ops Social & Salon on 26 Sep, 2025 at 6:00 PM
I am going to
.Sep
26
Friday, 26 September 2025
6:00 PM
It's Ok* Studios
Toronto, Ontario
Alexandra Ciufudean explores the IndieWeb, where people’s personal websites are pushing back against the corporate internet.

10 Sep, 2025 at 10:51 AM

6 Sep, 2025 at 10:57 AM
Nails Painted With Big Apple Red
Left Hand
Big Apple Red Little finger on left hand
Big Apple Red Ring finger on left hand
Big Apple Red Middle finger on left hand
No polish Index finger on left hand
Big Apple Red Thumb finger on left hand
Right Hand
No polish Little finger on right hand
Big Apple Red Ring finger on right hand
Big Apple Red Middle finger on right hand
Big Apple Red Index finger on right hand
Big Apple Red Thumb finger on right hand
JSONFeed in Astro with @astrojs/rss
I wanted this website to publish both an RSS feed and a JSONFeed. The catch: I didn’t want to maintain two separate feed pipelines.
Astro already has a handy @astrojs/rss
package that outputs RSS feeds. So I thought—why not reuse the same data shape to generate JSONFeed as well?
Turns out, it’s pretty straightforward.
The Code
Here’s how I reused the @astrojs/rss
shape to build a JSONFeed endpoint in Astro.
Formatting the Feed
I built a transformer function to take the RSSOptions
and convert them to be a valid JSONFeed 1.1 top-level.
import type { APIContext } from "astro";
import { getCollection } from "astro:content";
import type { RSSFeedItem, RSSOptions } from "@astrojs/rss";
export const formatJsonFeed = (rssOptions: RSSOptions) => {
const { site, title, description } = rssOptions;
const homePageUrl = site instanceof URL ? site.href : site;
return {
version: "https://jsonfeed.org/version/1.1",
title: title,
home_page_url: homePageUrl,
feed_url: new URL("/feed.json", homePageUrl).href,
description: description,
items: [],
};
};
Formatting Feed Items
Next was transforming RSS items to be valid JSONFeed 1.1 items.
export const formatJsonFeedItem = (rssFeedItem: RSSFeedItem) => {
const { link, content, title, pubDate, description } = rssFeedItem;
if (!link) {
throw new Error("RSS feed item must have a link.");
}
return {
id: link,
url: link,
title: title,
summary: description,
date_published: pubDate?.toISOString(),
content_html: content || "",
};
};
Putting It Together
Next, you’ll need to create a Static File Endpoint to serve the JSONFeed. I put mine at /src/pages/feed.json.ts
, which servers it at https://example.com/feed.json
.
export async function GET(context: APIContext): Promise<Response> {
const posts = await getCollection("posts");
const jsonFeedItems = posts.map((post) =>
formatJsonFeedItem({
link: url.href,
title: post.data.name,
pubDate: post.data.publishedAt,
}),
);
const jsonFeed = formatJsonFeed({
title: "Website Feed",
description: "This is a JSONFeed!!!",
site: context.site,
items: jsonFeedItems,
});
return new Response(JSON.stringify(jsonFeed, null, 2), {
headers: { "Content-Type": "application/feed+json" },
});
}
Auto-Discovery of Your New JSONFeed
To make your JSONFeed discoverable by browsers and feed readers, add a <link>
tag in your site’s <head>
. For example:
<link
rel="alternate"
type="application/rss+xml"
title="RSS Feed"
href="/rss.xml"
/>
<link
rel="alternate"
type="application/feed+json"
title="JSONFeed"
href="/feed.json"
/>
This way, clients that support feed auto-discovery will automatically pick up both formats.
Wrap Up
Now my site has two feeds: RSS for the traditionalists and JSONFeed for the modernists. If you’re already using @astrojs/rss
, this pattern makes JSONFeed nearly free. Give it a try—and let me know if you’d like me to extend this into Atom or other formats. 🌱
