Skip to content

Commit fce0baa

Browse files
committed
feat(site): add extended date scheduling on /book and optimize OG cards + social meta for 2026 standards
1 parent 503bf03 commit fce0baa

5 files changed

Lines changed: 497 additions & 160 deletions

File tree

‎apps/site/src/components/SocialMeta.astro‎

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -65,10 +65,16 @@ const cardType =
6565
: imageUrl
6666
? "summary_large_image"
6767
: "summary";
68+
const cleanAuthors = authors
69+
.map((name) => cleanMetaText(name))
70+
.filter((name): name is string => Boolean(name));
71+
const primaryAuthor = cleanAuthors[0] ?? "Alex Patterson";
6872
---
6973

7074
<title>{cleanTitle}</title>
7175
{cleanDescription && <meta name="description" content={cleanDescription} />}
76+
<meta name="author" content={primaryAuthor} />
77+
<meta name="theme-color" content="#7C3AED" />
7278
<link rel="canonical" href={canonicalUrl} />
7379
{noindex && <meta name="robots" content="noindex, nofollow" />}
7480

@@ -82,6 +88,8 @@ const cardType =
8288
imageUrl && (
8389
<>
8490
<meta property="og:image" content={imageUrl} />
91+
<meta property="og:image:secure_url" content={imageUrl} />
92+
<meta property="og:image:type" content="image/png" />
8593
{image?.width && <meta property="og:image:width" content={String(image.width)} />}
8694
{image?.height && <meta property="og:image:height" content={String(image.height)} />}
8795
{imageAlt && <meta property="og:image:alt" content={imageAlt} />}
@@ -91,6 +99,8 @@ const cardType =
9199

92100
<meta name="twitter:card" content={cardType} />
93101
<meta name="twitter:site" content="@CodingCatDev" />
102+
<meta name="twitter:creator" content="@CodingCatDev" />
103+
<meta name="twitter:domain" content="codingcat.dev" />
94104
<meta name="twitter:title" content={cleanOgTitle} />
95105
{cleanOgDescription && <meta name="twitter:description" content={cleanOgDescription} />}
96106
{
@@ -102,6 +112,8 @@ const cardType =
102112
)
103113
}
104114

115+
<meta name="fediverse:creator" content="@codingcatdev@mastodon.social" />
116+
105117
{
106118
type === "article" && (
107119
<>
@@ -111,11 +123,9 @@ const cardType =
111123
{modifiedAt && (
112124
<meta property="article:modified_time" content={stegaClean(modifiedAt)} />
113125
)}
114-
{authors
115-
.filter((name): name is string => Boolean(name))
116-
.map((name) => (
117-
<meta property="article:author" content={stegaClean(name)} />
118-
))}
126+
{cleanAuthors.map((name) => (
127+
<meta property="article:author" content={name} />
128+
))}
119129
</>
120130
)
121131
}

‎apps/site/src/lib/google-calendar.ts‎

Lines changed: 71 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -65,12 +65,32 @@ async function getAccessToken(
6565
return data.access_token ?? null;
6666
}
6767

68+
function resolveBaseStartDate(startDateStr?: string): Date {
69+
const tomorrow = new Date(Date.now() + 86400_000);
70+
if (!startDateStr) {
71+
return tomorrow;
72+
}
73+
const parsed = new Date(`${startDateStr}T12:00:00Z`);
74+
if (Number.isNaN(parsed.getTime())) {
75+
return tomorrow;
76+
}
77+
const maxFuture = new Date(Date.now() + 180 * 86400_000);
78+
if (parsed < tomorrow) {
79+
return tomorrow;
80+
}
81+
if (parsed > maxFuture) {
82+
return maxFuture;
83+
}
84+
return parsed;
85+
}
86+
6887
function generateDefaultCandidateSlots(
69-
daysAhead = 14,
88+
daysAhead = 21,
7089
durationMinutes = 45,
90+
startDateStr?: string,
7191
): AvailableCalendarSlot[] {
7292
const slots: AvailableCalendarSlot[] = [];
73-
const now = new Date();
93+
const baseDate = resolveBaseStartDate(startDateStr);
7494

7595
// Candidate slots in overlapping US PT (8:00 AM - 2:00 PM PT) & ET (11:00 AM - 5:00 PM ET) work hours:
7696
// 15:00 UTC (8:00 AM PDT / 11:00 AM EDT), 16:00 UTC (9:00 AM PDT / 12:00 PM EDT),
@@ -84,8 +104,8 @@ function generateDefaultCandidateSlots(
84104
{ hour: 20, minute: 0, label: "1:00 PM PT / 4:00 PM ET" },
85105
];
86106

87-
for (let d = 1; d <= daysAhead; d++) {
88-
const dayDate = new Date(now.getTime() + d * 86400_000);
107+
for (let d = 0; d < daysAhead; d++) {
108+
const dayDate = new Date(baseDate.getTime() + d * 86400_000);
89109
const dayOfWeek = dayDate.getUTCDay();
90110
// Weekdays Monday (1) through Friday (5)
91111
if (dayOfWeek === 0 || dayOfWeek === 6) {
@@ -273,20 +293,60 @@ async function fetchBusyRanges(
273293
}
274294
}
275295

296+
function spreadSlotsAcrossDays(
297+
slots: AvailableCalendarSlot[],
298+
maxSlots: number,
299+
maxPerDay = 2,
300+
): AvailableCalendarSlot[] {
301+
const selected: AvailableCalendarSlot[] = [];
302+
const perDayCount = new Map<string, number>();
303+
304+
for (const slot of slots) {
305+
const dayKey = slot.startAt.slice(0, 10);
306+
const current = perDayCount.get(dayKey) ?? 0;
307+
if (current < maxPerDay) {
308+
selected.push(slot);
309+
perDayCount.set(dayKey, current + 1);
310+
if (selected.length >= maxSlots) {
311+
return selected;
312+
}
313+
}
314+
}
315+
316+
if (selected.length < maxSlots) {
317+
for (const slot of slots) {
318+
if (!selected.some((s) => s.id === slot.id)) {
319+
selected.push(slot);
320+
if (selected.length >= maxSlots) {
321+
break;
322+
}
323+
}
324+
}
325+
}
326+
327+
return selected.sort(
328+
(a, b) => new Date(a.startAt).getTime() - new Date(b.startAt).getTime(),
329+
);
330+
}
331+
276332
/**
277333
* Queries Google Calendar FreeBusy API across Alex Patterson's calendars
278-
* (codingcat.dev, flipfeeds.com, alex.patterson@gmail.com, sanity.io, and Oliver family calendars)
279334
* and returns conflict-free slots during 8am-5pm PT/ET work hours.
280335
*/
281336
export async function getMultiCalendarAvailableSlots(
282337
maxSlots = 9,
283338
durationMinutes = 45,
339+
startDate?: string,
284340
): Promise<{
285341
slots: AvailableCalendarSlot[];
286342
liveVerified: boolean;
287343
calendarStatuses: CalendarCheckStatus[];
288344
}> {
289-
const candidates = generateDefaultCandidateSlots(14, durationMinutes);
345+
const candidates = generateDefaultCandidateSlots(
346+
21,
347+
durationMinutes,
348+
startDate,
349+
);
290350
if (candidates.length === 0) {
291351
return { slots: [], liveVerified: false, calendarStatuses: [] };
292352
}
@@ -300,7 +360,7 @@ export async function getMultiCalendarAvailableSlots(
300360

301361
if (!liveVerified) {
302362
return {
303-
slots: candidates.slice(0, maxSlots),
363+
slots: spreadSlotsAcrossDays(candidates, maxSlots),
304364
liveVerified: false,
305365
calendarStatuses,
306366
};
@@ -313,7 +373,10 @@ export async function getMultiCalendarAvailableSlots(
313373
});
314374

315375
return {
316-
slots: (available.length > 0 ? available : candidates).slice(0, maxSlots),
376+
slots: spreadSlotsAcrossDays(
377+
available.length > 0 ? available : candidates,
378+
maxSlots,
379+
),
317380
liveVerified: true,
318381
calendarStatuses,
319382
};

0 commit comments

Comments
 (0)