From 03ea9dda59c1a610a31f8cf9881d121dc80b284d Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 1 Jul 2026 08:27:20 +0000 Subject: [PATCH 1/3] Fix newsletter month seeding so the current month's issue always appears The newsletter dated the 1st of month M reports the *previous* calendar month's content, but getNewsletterMonths seeded candidate months from the month each item was *dated in* rather than the month that *reports* it. A newsletter therefore only appeared if something happened to be dated in its own month, so the July issue (reporting June) would not show on the 1st unless unrelated July-dated content existed. Seed backward-looking content (blog posts, advisories, releases, new pipelines) from month X into the newsletter dated X+1, and events into both their own month and the previous month (matching the forward-looking "upcoming events" section). newsletterMonthHasContent still gates out any candidate whose sections would all be empty. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01XUD4bQJx4nFhMGTtSqiwvK --- sites/main-site/tests/newsletter.spec.ts | 16 ++++++--- sites/main-site/utils/newsletter.ts | 45 +++++++++++++++++------- 2 files changed, 44 insertions(+), 17 deletions(-) diff --git a/sites/main-site/tests/newsletter.spec.ts b/sites/main-site/tests/newsletter.spec.ts index 76aa91c560..eb3d4111e7 100644 --- a/sites/main-site/tests/newsletter.spec.ts +++ b/sites/main-site/tests/newsletter.spec.ts @@ -22,7 +22,7 @@ test("getMonthName maps 1-indexed months to English names", () => { expect(getMonthName(12)).toBe("December"); }); -test("getNewsletterMonths dedupes, sorts newest-first, and drops future months", () => { +test("getNewsletterMonths seeds the reporting newsletter, dedupes, sorts newest-first, drops future months", () => { const future = new Date(); future.setFullYear(future.getFullYear() + 5); @@ -30,13 +30,19 @@ test("getNewsletterMonths dedupes, sorts newest-first, and drops future months", [blog("a", "2020-03-10"), blog("future", future.toISOString())], [evt("e", "2020-05-20")], [], // pipelines - [advisory("adv", "2020-05-02")], // same month as the event -> must dedupe + [advisory("adv", "2020-05-02")], ); - // 2020-05 appears once despite event + advisory; future month excluded. + // Backward-looking content is reported by the *next* month's newsletter: + // the March blog -> April, the May advisory -> June. + expect(months).toContainEqual({ year: 2020, month: 4 }); + expect(months).toContainEqual({ year: 2020, month: 6 }); + // An event seeds its own month (this month) and the previous month (next + // month's "upcoming events"): the May event -> May and April. expect(months).toContainEqual({ year: 2020, month: 5 }); - expect(months).toContainEqual({ year: 2020, month: 3 }); - expect(months.filter((m) => m.year === 2020 && m.month === 5)).toHaveLength(1); + // April is seeded by both the March blog and the May event -> still one entry. + expect(months.filter((m) => m.year === 2020 && m.month === 4)).toHaveLength(1); + // Future months excluded. expect(months.some((m) => m.year === future.getFullYear())).toBe(false); // Sorted strictly newest-first. diff --git a/sites/main-site/utils/newsletter.ts b/sites/main-site/utils/newsletter.ts index f5fd98b80f..dc661433ef 100644 --- a/sites/main-site/utils/newsletter.ts +++ b/sites/main-site/utils/newsletter.ts @@ -95,7 +95,13 @@ function adjacentMonths(year: number, month: number, count: number, step: 1 | -1 // ======================================== /** - * Get all months that have any newsletter content, sorted descending. + * Candidate newsletter months (the 1st-of-month dates a newsletter could be dated), + * sorted newest-first and limited to past/current months. + * + * Seeded from the months that *report* each piece of content rather than the months + * the content is dated in (see the seed helpers below), so the newsletter that renders + * a given item is always in the set. Callers still pass this through + * newsletterMonthHasContent to drop any candidate whose sections would all be empty. */ export function getNewsletterMonths( blogPosts: { data: { pubDate: Date } }[], @@ -104,31 +110,46 @@ export function getNewsletterMonths( advisories: { data: { publishedDate: Date } }[] = [], ): NewsletterMonth[] { const monthSet = new Set(); + const add = (year: number, month: number) => monthSet.add(`${year}-${month}`); + + // A newsletter is *dated* the 1st of month M but reports the content it renders + // from other months, so we must seed the month that *reports* each item — not the + // month the item is dated in — or a newsletter can be missing its own content. + // + // Backward-looking content (blog posts, advisories, releases, new pipelines) from + // month X is reported by the newsletter dated X+1. This mirrors the `contentMonth` + // look-back in getNewsletterContentData / newsletterMonthHasContent. + const seedFromContent = (date: Date) => { + const [{ year, month }] = adjacentMonths(date.getUTCFullYear(), date.getUTCMonth() + 1, 1, 1); + add(year, month); + }; + // Events are forward-looking: an event in month X appears in the newsletters dated + // X (this month) and X-1 (next month's "upcoming events"). + const seedFromEvent = (date: Date) => { + add(date.getUTCFullYear(), date.getUTCMonth() + 1); + const [{ year, month }] = adjacentMonths(date.getUTCFullYear(), date.getUTCMonth() + 1, 1, -1); + add(year, month); + }; for (const post of blogPosts) { - const d = new Date(post.data.pubDate); - monthSet.add(`${d.getUTCFullYear()}-${d.getUTCMonth() + 1}`); + seedFromContent(new Date(post.data.pubDate)); } for (const event of events) { - const d = new Date(event.data.start); - monthSet.add(`${d.getUTCFullYear()}-${d.getUTCMonth() + 1}`); + seedFromEvent(new Date(event.data.start)); } for (const advisory of advisories) { - const d = new Date(advisory.data.publishedDate); - monthSet.add(`${d.getUTCFullYear()}-${d.getUTCMonth() + 1}`); + seedFromContent(new Date(advisory.data.publishedDate)); } for (const pipeline of pipelines) { for (const release of pipeline.releases) { if (release.tag_name === "dev") continue; - const d = new Date(release.published_at); - monthSet.add(`${d.getUTCFullYear()}-${d.getUTCMonth() + 1}`); + seedFromContent(new Date(release.published_at)); } - // Also include month when pipeline was created - const created = new Date(pipeline.created_at); - monthSet.add(`${created.getUTCFullYear()}-${created.getUTCMonth() + 1}`); + // Also seed from the month the pipeline was created (new-pipeline section). + seedFromContent(new Date(pipeline.created_at)); } const months: NewsletterMonth[] = [...monthSet].map((key) => { From b87c8ed64ac7def4f5afef00892269a81a0581f2 Mon Sep 17 00:00:00 2001 From: mashehu Date: Wed, 1 Jul 2026 11:29:29 +0200 Subject: [PATCH 2/3] simplify seed function --- sites/main-site/utils/newsletter.ts | 39 +++++++++++++---------------- 1 file changed, 18 insertions(+), 21 deletions(-) diff --git a/sites/main-site/utils/newsletter.ts b/sites/main-site/utils/newsletter.ts index dc661433ef..d12c8124c8 100644 --- a/sites/main-site/utils/newsletter.ts +++ b/sites/main-site/utils/newsletter.ts @@ -112,44 +112,41 @@ export function getNewsletterMonths( const monthSet = new Set(); const add = (year: number, month: number) => monthSet.add(`${year}-${month}`); - // A newsletter is *dated* the 1st of month M but reports the content it renders - // from other months, so we must seed the month that *reports* each item — not the - // month the item is dated in — or a newsletter can be missing its own content. + // A newsletter is *dated* the 1st of month M but reports content from adjacent months, + // so we seed the newsletter(s) that *report* each item rather than the month of the item. // - // Backward-looking content (blog posts, advisories, releases, new pipelines) from - // month X is reported by the newsletter dated X+1. This mirrors the `contentMonth` - // look-back in getNewsletterContentData / newsletterMonthHasContent. - const seedFromContent = (date: Date) => { - const [{ year, month }] = adjacentMonths(date.getUTCFullYear(), date.getUTCMonth() + 1, 1, 1); - add(year, month); - }; - // Events are forward-looking: an event in month X appears in the newsletters dated - // X (this month) and X-1 (next month's "upcoming events"). - const seedFromEvent = (date: Date) => { - add(date.getUTCFullYear(), date.getUTCMonth() + 1); - const [{ year, month }] = adjacentMonths(date.getUTCFullYear(), date.getUTCMonth() + 1, 1, -1); - add(year, month); + // offsets control which newsletter month(s) to seed relative to the item's month: + // [1] – backward-looking content (blog posts, advisories, releases, new pipelines): + // month X is reported by the newsletter dated X+1. + // [0, -1] – events: an event in month X appears in newsletters dated X (this month) + // and X-1 (next month's "upcoming events"). + const seedFrom = (date: Date, offsets: number[]) => { + const anchor = new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), 1, 12)); + for (const offset of offsets) { + const d = addMonths(anchor, offset); + add(d.getUTCFullYear(), d.getUTCMonth() + 1); + } }; for (const post of blogPosts) { - seedFromContent(new Date(post.data.pubDate)); + seedFrom(new Date(post.data.pubDate), [1]); } for (const event of events) { - seedFromEvent(new Date(event.data.start)); + seedFrom(new Date(event.data.start), [0, -1]); } for (const advisory of advisories) { - seedFromContent(new Date(advisory.data.publishedDate)); + seedFrom(new Date(advisory.data.publishedDate), [1]); } for (const pipeline of pipelines) { for (const release of pipeline.releases) { if (release.tag_name === "dev") continue; - seedFromContent(new Date(release.published_at)); + seedFrom(new Date(release.published_at), [1]); } // Also seed from the month the pipeline was created (new-pipeline section). - seedFromContent(new Date(pipeline.created_at)); + seedFrom(new Date(pipeline.created_at), [1]); } const months: NewsletterMonth[] = [...monthSet].map((key) => { From 7bc51ccdcd33d8977c8472c1a6303c58fe52db50 Mon Sep 17 00:00:00 2001 From: mashehu Date: Wed, 1 Jul 2026 11:30:23 +0200 Subject: [PATCH 3/3] make comments less wordy --- sites/main-site/utils/newsletter.ts | 33 ++++++++++------------------- 1 file changed, 11 insertions(+), 22 deletions(-) diff --git a/sites/main-site/utils/newsletter.ts b/sites/main-site/utils/newsletter.ts index d12c8124c8..c2d5852556 100644 --- a/sites/main-site/utils/newsletter.ts +++ b/sites/main-site/utils/newsletter.ts @@ -95,13 +95,11 @@ function adjacentMonths(year: number, month: number, count: number, step: 1 | -1 // ======================================== /** - * Candidate newsletter months (the 1st-of-month dates a newsletter could be dated), - * sorted newest-first and limited to past/current months. + * Candidate newsletter months, sorted newest-first and limited to past/current months. * - * Seeded from the months that *report* each piece of content rather than the months - * the content is dated in (see the seed helpers below), so the newsletter that renders - * a given item is always in the set. Callers still pass this through - * newsletterMonthHasContent to drop any candidate whose sections would all be empty. + * Seeded from the months that *report* each item, not the months it's dated in, so the + * newsletter that renders an item is always in the set. Callers filter this further with + * newsletterMonthHasContent to drop candidates whose sections would all be empty. */ export function getNewsletterMonths( blogPosts: { data: { pubDate: Date } }[], @@ -112,14 +110,9 @@ export function getNewsletterMonths( const monthSet = new Set(); const add = (year: number, month: number) => monthSet.add(`${year}-${month}`); - // A newsletter is *dated* the 1st of month M but reports content from adjacent months, - // so we seed the newsletter(s) that *report* each item rather than the month of the item. - // - // offsets control which newsletter month(s) to seed relative to the item's month: - // [1] – backward-looking content (blog posts, advisories, releases, new pipelines): - // month X is reported by the newsletter dated X+1. - // [0, -1] – events: an event in month X appears in newsletters dated X (this month) - // and X-1 (next month's "upcoming events"). + // Seeds the newsletter month(s) an item's month is reported in, relative to its own month: + // [1] – content (posts, advisories, releases, new pipelines): month X → newsletter X+1. + // [0, -1] – events: month X → newsletters X (this month) and X-1 (upcoming events). const seedFrom = (date: Date, offsets: number[]) => { const anchor = new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), 1, 12)); for (const offset of offsets) { @@ -170,14 +163,10 @@ export function getNewsletterMonths( /** * Whether the newsletter dated the 1st of (year, month) would render any content. * - * The candidate-month set above is seeded from the months that content lands in, - * but a newsletter looks *back* at the previous calendar month (blog posts, - * advisories, releases, new pipelines, recent events, proposals) and *forward* at - * this + next month (upcoming events). That offset means a seeded month can end up - * with every section empty — e.g. a lone advisory dated in a month whose preceding - * month has nothing. This mirrors the section gating in NewsletterLayout so we only - * keep months where at least one section would actually render, and never publish a - * page that is just the header and footer. + * A newsletter looks *back* a month (posts, advisories, releases, new pipelines, recent + * events, proposals) and *forward* (upcoming events), so a seeded candidate month can still + * end up empty — e.g. a lone advisory whose preceding month has nothing. This mirrors the + * section gating in NewsletterLayout so we never publish a page with just a header/footer. */ export function newsletterMonthHasContent( blogPosts: { id: string; data: { pubDate: Date } }[],