diff --git a/apps/web/app/(home)/blog/[slug]/page.tsx b/apps/web/app/(home)/blog/[slug]/page.tsx index b5d58f6..1334036 100644 --- a/apps/web/app/(home)/blog/[slug]/page.tsx +++ b/apps/web/app/(home)/blog/[slug]/page.tsx @@ -96,6 +96,36 @@ function postJsonLd(slug: string, post: ReturnType) { isPartOf: { '@id': website['@id'] }, about: { '@type': 'SoftwareSourceCode', name: appName, codeRepository: githubUrl }, }, + ...faqJsonLd(post), ], }; } + +/** + * A post's `## FAQ` section as FAQPage data, built from the post itself so the two can't drift: + * every heading after `## FAQ` is a question, and the text under it is the answer. + */ +function faqJsonLd(post: ReturnType) { + const { headings, contents } = post.structuredData; + const start = headings.findIndex((heading) => heading.content === 'FAQ'); + if (start === -1) return []; + + return [ + { + '@type': 'FAQPage', + mainEntity: headings.slice(start + 1).map((heading) => ({ + '@type': 'Question', + name: heading.content, + acceptedAnswer: { + '@type': 'Answer', + text: contents + .filter((block) => block.heading === heading.id) + .map((block) => block.content) + .join(' ') + // Search text keeps inline code backticks; an answer is plain text. + .replaceAll('`', ''), + }, + })), + }, + ]; +} diff --git a/apps/web/content/blog/finding-free-time-across-calendars.mdx b/apps/web/content/blog/finding-free-time-across-calendars.mdx index 8ce1fe4..a422c13 100644 --- a/apps/web/content/blog/finding-free-time-across-calendars.mdx +++ b/apps/web/content/blog/finding-free-time-across-calendars.mdx @@ -8,7 +8,7 @@ Here's a feature request that sounds like an afternoon's work: show the times wh You fetch two lists of busy times, merge them and look for the gaps. The merge really is the easy part. The hard part is getting a busy time you can trust from each provider, because the three big calendar APIs each answer "when is this person busy?" in a different way, and one of them barely answers it at all. -## Google: a real free/busy endpoint +## How does Google Calendar report busy time? Google is the easy one. You `POST /freeBusy` with a window and a list of calendar ids, and get back busy ranges per calendar: @@ -29,7 +29,7 @@ There are two things to notice. First, Google has already applied its own rules: The only limit worth knowing is that one request covers at most 50 calendars, so a big team needs batching. -## Outlook: an endpoint you probably can't use +## Why not use Outlook's getSchedule? Microsoft Graph does have a free/busy endpoint, `getSchedule`. You give it email addresses and a window, and it gives you back an `availabilityView` string with one digit per time slot: @@ -47,7 +47,7 @@ Interestingly, `getSchedule` writes "working elsewhere" as `0`, which is free. I Time zones need care too. Graph names zones the Windows way, like `"Pacific Standard Time"`, not `"America/Los_Angeles"`, and a `Prefer: outlook.timezone` header changes which zone it writes times in. For busy time, ask for UTC with `Prefer: outlook.timezone="UTC"` and you skip the mapping entirely. -## iCloud and CalDAV: compute it yourself +## How do you get busy time from iCloud and CalDAV? CalDAV is the protocol behind iCloud, Fastmail, Nextcloud, Zoho and most self-hosted calendars. On paper it has free/busy too: RFC 4791 defines a `free-busy-query` REPORT. In practice I didn't want a feature where whether it works depends on which server the user happens to be on, so calsdk treats CalDAV the same way as Outlook: list the events and compute. @@ -70,7 +70,7 @@ What comes back is raw iCalendar text, and everything is now your problem. Recur Some servers have extra quirks. Zoho answers a calendar query without the event data, so every event has to be fetched again with a `calendar-multiget`. If one of those fetches fails, it's tempting to skip that event. Don't: a missing event is time that looks free. calsdk fails the whole listing instead. -## The gotchas that cut across all three +## What catches people out on every provider? All-day events are dates, not instants. A holiday on November 9 means November 9 in the calendar's own time zone, so to turn it into a busy range you need that zone, which you only get from the calendar list. If you can't get it, the safe choice is to block the day in every zone, from UTC+14 through UTC-12. That covers more than a day, but it never shows a holiday as free. @@ -80,7 +80,7 @@ Declined invitations are a judgment call. The event fallback in calsdk only look Rate limits are real once you list a week of events for 20 people. Retry `429`s after the `Retry-After` the provider sends, and keep the window as small as the feature allows. -## The merge, and why it should fail closed +## How do you merge busy times without double booking? With trustworthy busy ranges in hand, the algorithm is short: @@ -126,3 +126,25 @@ try { Slots come back as UTC instants, ready to show in each viewer's own zone. The [free time guide](/docs/guides/free-time) covers slot intervals, overlapping slots and booking the one the user picks. If you want to poke at it without connecting a real account, the [quickstart](/docs/quickstart) runs against an in-memory calendar in a couple of minutes. + +## FAQ + +### Does Outlook have a free/busy API? + +Yes: Microsoft Graph's `getSchedule`. It doesn't support personal Microsoft accounts (outlook.com and hotmail.com), and it gives up past 1000 entries in the window. Listing events with `calendarView` and computing busy time yourself works for every account. + +### Does iCloud support free/busy queries? + +iCloud speaks CalDAV, and RFC 4791 defines a `free-busy-query` REPORT, but support varies between CalDAV servers. Listing events with a `calendar-query` REPORT and computing busy time yourself works the same on every server. + +### Should tentative and declined events count as busy? + +Count everything except events marked "show as free" as busy, including tentative ones. calsdk also treats a declined invite that is still on the calendar as busy, because offering a taken slot is worse than hiding a free one. + +### What should happen when one calendar can't be read? + +Fail and say which calendar it was. Slots computed without that calendar are guesses that can clash. calsdk's `findFreeTime` throws `FreeBusyUnavailableError` with the provider and calendar id. + +### How many calendars can one Google freeBusy request check? + +50. Batch the request for larger teams. diff --git a/apps/web/content/blog/incremental-calendar-sync.mdx b/apps/web/content/blog/incremental-calendar-sync.mdx index dd36ede..32c3499 100644 --- a/apps/web/content/blog/incremental-calendar-sync.mdx +++ b/apps/web/content/blog/incremental-calendar-sync.mdx @@ -8,7 +8,7 @@ Sooner or later, every app that shows a user's calendar wants its own copy of it Google Calendar, Microsoft Outlook and CalDAV servers like iCloud can all tell you "here's what changed since last time". They just do it in three quite different ways, and each one breaks in its own way. Here's what I learned wiring up all three. -## Google: sync tokens +## How do Google Calendar sync tokens work? Google has the cleanest model. You list a calendar's events once with no filters, follow `pageToken` through every page, and the last page hands you a `nextSyncToken`: @@ -28,7 +28,7 @@ Tokens also expire, for example after a while unused, or when the calendar's sha One more detail: if you sync without `singleEvents=true`, a recurring meeting arrives as one series event with an RRULE, and any occurrence someone moved or edited arrives as its own exception event. That's what you want for a copy, since an endless daily standup has no finite list of occurrences to sync. -## Outlook: delta queries over a window +## How does Outlook delta sync work? Microsoft Graph uses delta queries, and for calendars the one that works across a whole range is `calendarView/delta`. Unlike Google, it requires a window up front: @@ -53,7 +53,7 @@ Two more things surprised me. The same event can appear more than once in a roun Expiry looks different too. Depending on the case, Graph answers `410 Gone` or an error code like `SyncStateNotFound`, `SyncStateInvalid` or `resyncRequired`. All of them mean the same thing: start over. -## CalDAV: sync-collection, and the CTags before it +## How do you sync an iCloud or CalDAV calendar? CalDAV is the protocol behind iCloud, Fastmail, Nextcloud, Zoho and many self-hosted servers. It speaks WebDAV over XML, and its sync story has two generations. @@ -75,7 +75,7 @@ There are a few rough edges. A server can truncate the answer, which it signals And one CalDAV resource is a whole series: the master and all its exceptions live in one `.ics` file. When it changes, you have to compare its exceptions with the ones you saw last time to find out which were removed. -## Calendars with no change feed +## What about calendars with no change feed? Not every API has any of this. Calendly and Zoom can't list meetings by when they last changed. The honest approach there is to read every meeting each time and compare it with what you saw last time. To keep that cheap, you store a short hash per event rather than the event itself, so your "cursor" is a list of ids and fingerprints that grows with the calendar. ICS feeds are the same, except the server can save you the download with `304 Not Modified` if you send its `ETag` back. Cal.com sits in between: it can list bookings sorted by when they were updated, so you can read from the last change you saw (with some overlap, because a slow write can land a little out of order). @@ -111,3 +111,25 @@ Under the hood it's exactly the approaches above. Google uses sync tokens over t Save the cursor only after the changes are applied. If your process dies in between, the next run replays them instead of losing them. The [sync guide](/docs/guides/sync) covers paging through a large first sync and how much each provider reads per round. If you want to see the API without connecting a real account, the [quickstart](/docs/quickstart) runs against an in-memory calendar. + +## FAQ + +### What happens when a Google Calendar sync token expires? + +Google answers `410 Gone`. Delete your copy of the calendar and run a full sync again. There's no way to resume from where you were. + +### Can I use timeMin with a Google sync token? + +No. `timeMin`, `timeMax`, `updatedMin`, `q`, `orderBy`, `iCalUID` and the extended property filters can't be combined with `syncToken`, so the first sync has to read the whole calendar. + +### How does Microsoft Graph report deleted events in a delta query? + +As stubs with an `@removed` annotation. The same stub also marks events outside the window that changed, so an event moved out of your window looks like a deletion. + +### Does iCloud support incremental sync? + +Yes. iCloud is a CalDAV server, and CalDAV's `sync-collection` REPORT (RFC 6578) returns the resources that changed or were removed since your last sync token. + +### When should I save the sync cursor? + +Only after the changes are applied to your copy. If your process dies in between, the next run replays the changes instead of losing them. diff --git a/apps/web/content/blog/recurring-events-across-calendar-apis.mdx b/apps/web/content/blog/recurring-events-across-calendar-apis.mdx index f5ec0ed..86e4b01 100644 --- a/apps/web/content/blog/recurring-events-across-calendar-apis.mdx +++ b/apps/web/content/blog/recurring-events-across-calendar-apis.mdx @@ -10,7 +10,7 @@ Here's the event I'll use for the rest of this post: a standup at 9:30 in Berlin I've spent a lot of time with these differences while building [calsdk](/docs), so here is how each provider stores that week, and the places where it caught me out. -## Google: RRULE, plus objects for everything else +## How does Google Calendar store a recurring event? Google is the closest to the standard. The series is one event with a `recurrence` array of RFC 5545 lines: @@ -41,7 +41,7 @@ For reading, `singleEvents=true` expands the series on Google's side and gives y The hard part is "this and following events". Google Calendar's UI offers it, but the API has no such operation. You build it yourself: end the old series just before the chosen occurrence, and create a new series starting there. If the rule has a `COUNT`, you split the count between the two halves. Then you learn that Google resets edited occurrences when a series moves or splits, so Friday's change to 10:00 is lost unless you carry it over. -## Outlook: patterns instead of rules +## How does Outlook store a recurring event? Microsoft Graph doesn't use RRULE at all. A series is a `seriesMaster` with a `recurrence` object made of a pattern and a range: @@ -76,7 +76,7 @@ Instances have a `type` of `occurrence` or `exception` and a `seriesMasterId`. C There's no "this and following" here either. You end the series and create a new one, with the same bookkeeping as on Google. -## CalDAV: the series is a file +## How do iCloud and CalDAV store a recurring event? iCloud, Fastmail, Nextcloud and Zoho all speak CalDAV. CalDAV stores calendars as iCalendar files on a server, so the whole series, with every exception, is one `.ics` resource: @@ -108,7 +108,7 @@ Then you expand the series yourself. CalDAV does let you ask the server to expan Servers vary in small ways too. Zoho, for example, answers a calendar query without the event data, so every event has to be fetched again. -## Putting one API over all three +## How do you put one API over all three? The design choice that made the rest possible was to speak RFC 5545 everywhere. Google and CalDAV already use it, so only Outlook needs translating, in both directions. Every event has a `kind`: `single`, `series`, `occurrence` or `exception`. Occurrences and exceptions carry a `seriesId` and an `originalStart`, which stays put when the occurrence moves. Here's the standup week, which works the same with `microsoft()` or `icloud()` in place of `google()`: @@ -151,3 +151,25 @@ await updateEvent({ The rule I held to is to never save a schedule that's different from the one you asked for. If Outlook can't store a rule exactly, calsdk throws instead of saving something close. If the call can still do what you meant, but something is lost along the way, such as Google resetting edited occurrences when a series moves, the call succeeds and returns a warning that says so. A standup that quietly lands on the wrong days is worse than an error. The [recurring events guide](/docs/guides/recurring-events) has every scope and the limits for each provider. If you want to see it work without connecting an account, the [quickstart](/docs/quickstart) runs against an in-memory calendar. + +## FAQ + +### How does Google Calendar mark a cancelled occurrence? + +In one of two ways: an `EXDATE` line in the series' `recurrence`, or, when the occurrence was deleted through the API, an instance with `status: "cancelled"`. Code that reads Google events has to handle both. + +### Does Microsoft Graph support RRULE? + +No. Graph stores a recurring event as a pattern and a range, with Windows time zone names. Most RRULEs translate, but hourly rules, `RDATE` and some `BYDAY` and `BYMONTHDAY` combinations have no Outlook pattern. + +### How do I find the deleted occurrences of an Outlook recurring event? + +Read `cancelledOccurrences` on the series master. Graph only returns it when you `$select` it on a single event by id, so it costs one request per series. + +### Do the calendar APIs support "this and following events"? + +Google, Outlook and CalDAV don't have that operation. You end the old series before the chosen occurrence and create a new series from it. calsdk does that split for you on Google. + +### How do you edit one occurrence on CalDAV? + +Rewrite the series' `.ics` file: add or change a `VEVENT` with a `RECURRENCE-ID` for that occurrence, then `PUT` the file back with `If-Match` on its ETag.