From e81a012c80ab6052a1bcfbab68385fe99edd2ac5 Mon Sep 17 00:00:00 2001 From: Lee Calcote Date: Mon, 14 Sep 2026 17:42:06 -0500 Subject: [PATCH 1/2] fix(build): make BLOG_YEAR scope lite builds to blog years again make site-blog advertised "2026 posts only" and passed BLOG_YEAR, but nothing read it: year filtering was added in 8c73741b6 and removed with the rest of that change by its revert, 8d3d41bc7. The blog profile sourced every blog year. - build-collections: getBlogYearFilter turns BLOG_YEAR (one year or a comma-separated list) into ignore globs for the other src/collections/blog/ directories. It applies to lightweight builds that include the blog (content, blog profiles), warns when it cannot apply (full build, blog excluded), and fails on a value that is not a year or has no directory rather than silently building nothing. - gatsby-config: add the globs to the collections source and log the blog years in the build scope. - Makefile: site-blog defaults to the latest year directory instead of a hardcoded 2026 and accepts BLOG_YEAR overrides. - Unit tests with node:test (npm run test:unit), run in Checks CI. - CONTRIBUTING: document the blog profile, BLOG_YEAR, and make site-blog. Signed-off-by: Lee Calcote --- .github/workflows/checks.yml | 3 + CONTRIBUTING.md | 5 +- Makefile | 9 +- gatsby-config.js | 21 +++- package.json | 1 + src/utils/build-collections.js | 73 +++++++++++++- src/utils/build-collections.test.js | 149 ++++++++++++++++++++++++++++ 7 files changed, 252 insertions(+), 9 deletions(-) create mode 100644 src/utils/build-collections.test.js diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index 3fd7e8199af1e4..27ccfaf02fbb1d 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -21,6 +21,9 @@ jobs: - name: Check CONTRIBUTING versions run: npm run check:contributing-versions + - name: Unit tests + run: npm run test:unit + - name: Build run: npm run build diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8a92fa42ff0599..27157c1a0ccb2a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -518,8 +518,9 @@ Environment variables are named values used to configure how an application beha | Variable | Possible Values | Description | | --------------------------- | -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `BUILD_FULL_SITE` | `true`, `false` | When set to `true`, enables a full site build including all collections. If not explicitly set to `true`, the project defaults to a lightweight build. | -| `LITE_BUILD_PROFILE` | `content`, `core` | Selects which collections are excluded when `BUILD_FULL_SITE=false`. `core` is the default for `make site`, `npm start`, and `npm run dev`, while `content` keeps blog, news, events, and resources enabled. | +| `LITE_BUILD_PROFILE` | `core`, `content`, `blog` | Selects which collections are excluded when `BUILD_FULL_SITE=false`. `core` is the default for `make site`, `npm start`, and `npm run dev` and skips blog, news, events, and resources. `content` keeps blog, news, events, and resources enabled. `blog` keeps the blog and skips news, events, and resources; `make site-blog` uses it. | | `BUILD_COLLECTIONS_EXCLUDE` | comma-separated collection names | Adds extra collections to exclude from a lightweight build without editing project files. | +| `BLOG_YEAR` | comma-separated four-digit years | Limits a lightweight build that includes the blog (`content` or `blog` profile) to posts under `src/collections/blog/`, e.g. `BLOG_YEAR=2026` or `BLOG_YEAR=2025,2026`. Posts from other years are not built and return 404. `make site-blog` defaults it to the latest year. A year with no directory, or a value that is not a year, fails the build. Ignored, with a warning, for full builds and for profiles that exclude the blog. | | `NODE_ENV` | `development`, `production` | Determines the build and rendering mode used by Gatsby. This is automatically set by Gatsby.

• `development` - Uses **Deferred Static Generation (DSG)** i.e pages built on demand for faster startup.
• `production` - Uses **Server-Side Rendering (SSR)** i.e pages rendered on each request for fresh content. | | `CI` | `true`, `false` | Indicates that the build is running in a **Continuous Integration (CI)** environment (e.g., GitHub Actions). When set to `true`, special logic is applied to page paths and redirects for GitHub Pages compatibility. This is typically set automatically by the CI system and does not need to be configured manually. | @@ -579,7 +580,7 @@ make setup make site ``` -This will run a local webserver with "live reload" conveniently enabled. +This will run a local webserver with "live reload" conveniently enabled. `make site` skips the blog, news, events, and resources collections to keep builds fast. To work on a blog post, run `make site-blog`, which builds only the latest year's blog posts; choose other years with `BLOG_YEAR=2025,2026 make site-blog`. Run `make` to list every target. **11.** Track your changes. diff --git a/Makefile b/Makefile index 5097d496b51ded..6b45bbbe244755 100644 --- a/Makefile +++ b/Makefile @@ -25,10 +25,13 @@ site: @echo " Use LITE_BUILD_PROFILE=content make site to include content collections while still skipping the heaviest routes." @npx cross-env BUILD_FULL_SITE=false LITE_BUILD_PROFILE=$(or $(LITE_BUILD_PROFILE),core) BLOG_YEAR=$(or $(BLOG_YEAR),) GATSBY_CPU_COUNT=4 SHARP_CONCURRENCY=4 UV_THREADPOOL_SIZE=4 NODE_OPTIONS=--max-old-space-size=8192 env-cmd -f .env.development gatsby develop -## Run blog-only dev server (2026 posts only, much faster builds). +# Latest year directory under src/collections/blog, e.g. 2026. +LATEST_BLOG_YEAR := $(shell ls src/collections/blog | grep -E '^[0-9]{4}$$' | sort | tail -n 1) + +## Run blog-only dev server scoped to the latest blog year (override: BLOG_YEAR=2025,2026). site-blog: - @echo "🏗️ Building lightweight site version with blog collection only..." - LITE_BUILD_PROFILE=blog BLOG_YEAR=2026 $(MAKE) site + @echo "🏗️ Building lightweight site version with blog posts from $(or $(BLOG_YEAR),$(LATEST_BLOG_YEAR)) only..." + LITE_BUILD_PROFILE=blog BLOG_YEAR=$(or $(BLOG_YEAR),$(LATEST_BLOG_YEAR)) $(MAKE) site # "make site-full" forces the dev server to include every collection. ## Run a full build of layer5.io on your local machine. diff --git a/gatsby-config.js b/gatsby-config.js index 19c2baad345d46..07f6294a33e6c1 100644 --- a/gatsby-config.js +++ b/gatsby-config.js @@ -2,6 +2,7 @@ const { DEFAULT_LITE_BUILD_PROFILE, + getBlogYearFilter, getExcludedCollections, isFullSiteBuild, } = require("./src/utils/build-collections"); @@ -25,9 +26,14 @@ const isLiteDevBuild = isDevelopment && !shouldBuildFullSite; const excludedCollections = getExcludedCollections({ isFullSiteBuild: shouldBuildFullSite, }); -const collectionIgnoreGlobs = excludedCollections.map( - (name) => `**/${name}/**`, -); +const blogYearFilter = getBlogYearFilter({ + isFullSiteBuild: shouldBuildFullSite, + excludedCollections, +}); +const collectionIgnoreGlobs = [ + ...excludedCollections.map((name) => `**/${name}/**`), + ...blogYearFilter.ignoreGlobs, +]; const devFlags = isDevelopment ? { PARALLEL_SOURCING: false, @@ -40,6 +46,15 @@ collectionIgnoreGlobs.length > 0 `Build Scope excludes (${process.env.LITE_BUILD_PROFILE || DEFAULT_LITE_BUILD_PROFILE}): ${excludedCollections.join(", ")}`, ) : console.info("Build Scope includes all collections"); +if (blogYearFilter.years.length > 0) { + console.info( + `Build Scope blog years (BLOG_YEAR): ${blogYearFilter.years.join(", ")}`, + ); +} else if (blogYearFilter.inactiveReason) { + console.warn( + `BLOG_YEAR=${process.env.BLOG_YEAR} ignored: ${blogYearFilter.inactiveReason}`, + ); +} module.exports = { ...(pathPrefix != null ? { pathPrefix } : {}), siteMetadata: { diff --git a/package.json b/package.json index 32a63f2222b0c6..b3118597685ce6 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ "lint": "eslint --fix .", "checklint": "eslint .", "check:contributing-versions": "node .github/scripts/check-contributing-versions.cjs", + "test:unit": "node --test src/utils/build-collections.test.js", "pretest": "eslint --ignore-path .gitignore .", "preload-fonts": "gatsby-preload-fonts", "deploy": "gatsby build && gh-pages -d public -b master", diff --git a/src/utils/build-collections.js b/src/utils/build-collections.js index dad3133afcd4fd..ee332f90da73ba 100644 --- a/src/utils/build-collections.js +++ b/src/utils/build-collections.js @@ -1,3 +1,8 @@ +/* eslint-env node */ + +const fs = require("fs"); +const path = require("path"); + const DEFAULT_LITE_BUILD_PROFILE = "core"; const LITE_BUILD_PROFILES = Object.freeze({ @@ -6,6 +11,10 @@ const LITE_BUILD_PROFILES = Object.freeze({ core: ["members", "integrations", "blog", "news", "events", "resources"], }); +const BLOG_COLLECTION = "blog"; +const BLOG_COLLECTION_DIR = path.join(__dirname, "..", "collections", BLOG_COLLECTION); +const BLOG_YEAR_PATTERN = /^\d{4}$/; + const isFullSiteBuild = (buildFullSite = process.env.BUILD_FULL_SITE) => buildFullSite === "true"; @@ -33,9 +42,71 @@ const getExcludedCollections = ({ ).sort(); }; +// Year directories (src/collections/blog/) that currently hold posts. +const listBlogYears = (blogDir = BLOG_COLLECTION_DIR) => + fs + .readdirSync(blogDir, { withFileTypes: true }) + .filter((entry) => entry.isDirectory() && BLOG_YEAR_PATTERN.test(entry.name)) + .map((entry) => entry.name) + .sort(); + +const parseBlogYears = (value = "") => { + const years = parseCsv(value); + const invalid = years.filter((year) => !BLOG_YEAR_PATTERN.test(year)); + if (invalid.length > 0) { + throw new Error( + `BLOG_YEAR must be a comma-separated list of four-digit years (e.g. "2026" or "2025,2026"); invalid: ${invalid.join(", ")}`, + ); + } + return Array.from(new Set(years)).sort(); +}; + +// Narrows a lightweight build that includes the blog collection to the posts +// of the years listed in BLOG_YEAR, by ignoring every other year directory. +// Returns the years kept, the ignore globs for the collections source, and, +// when BLOG_YEAR is set but cannot apply, why it was not applied. +const getBlogYearFilter = ({ + blogYear = process.env.BLOG_YEAR, + isFullSiteBuild: shouldBuildFullSite = isFullSiteBuild(), + excludedCollections = getExcludedCollections({ + isFullSiteBuild: shouldBuildFullSite, + }), + availableYears = listBlogYears(), +} = {}) => { + const years = parseBlogYears(blogYear); + const inactive = (reason) => ({ years: [], ignoreGlobs: [], inactiveReason: reason }); + + if (years.length === 0) { + return inactive(null); + } + if (shouldBuildFullSite) { + return inactive("BUILD_FULL_SITE=true builds every blog year"); + } + if (excludedCollections.includes(BLOG_COLLECTION)) { + return inactive("the blog collection is excluded from this build"); + } + + const missing = years.filter((year) => !availableYears.includes(year)); + if (missing.length > 0) { + throw new Error( + `BLOG_YEAR ${missing.join(", ")} has no directory under src/collections/blog; available years: ${availableYears.join(", ")}`, + ); + } + + return { + years, + ignoreGlobs: availableYears + .filter((year) => !years.includes(year)) + .map((year) => `**/collections/${BLOG_COLLECTION}/${year}/**`), + inactiveReason: null, + }; +}; + module.exports = { DEFAULT_LITE_BUILD_PROFILE, LITE_BUILD_PROFILES, + getBlogYearFilter, getExcludedCollections, isFullSiteBuild, -}; \ No newline at end of file + listBlogYears, +}; diff --git a/src/utils/build-collections.test.js b/src/utils/build-collections.test.js new file mode 100644 index 00000000000000..7b393aacc0d2b6 --- /dev/null +++ b/src/utils/build-collections.test.js @@ -0,0 +1,149 @@ +/* eslint-env node */ + +const { describe, it } = require("node:test"); +const assert = require("node:assert/strict"); +const fs = require("fs"); +const os = require("os"); +const path = require("path"); + +const { + getBlogYearFilter, + getExcludedCollections, + listBlogYears, +} = require("./build-collections"); + +const availableYears = ["2024", "2025", "2026"]; +const blogProfileExclusions = getExcludedCollections({ + isFullSiteBuild: false, + liteBuildProfile: "blog", + buildCollectionsExclude: "", +}); + +describe("getExcludedCollections", () => { + it("excludes nothing for a full site build", () => { + assert.deepEqual(getExcludedCollections({ isFullSiteBuild: true }), []); + }); + + it("keeps the blog collection in the blog profile", () => { + assert.ok(!blogProfileExclusions.includes("blog")); + }); + + it("falls back to the core profile for an unknown profile", () => { + assert.deepEqual( + getExcludedCollections({ + isFullSiteBuild: false, + liteBuildProfile: "nope", + buildCollectionsExclude: "", + }), + getExcludedCollections({ + isFullSiteBuild: false, + liteBuildProfile: "core", + buildCollectionsExclude: "", + }), + ); + }); + + it("adds BUILD_COLLECTIONS_EXCLUDE entries without duplicates", () => { + assert.deepEqual( + getExcludedCollections({ + isFullSiteBuild: false, + liteBuildProfile: "content", + buildCollectionsExclude: " workshops, members ", + }), + ["integrations", "members", "workshops"], + ); + }); +}); + +describe("getBlogYearFilter", () => { + const filter = (overrides) => + getBlogYearFilter({ + isFullSiteBuild: false, + excludedCollections: blogProfileExclusions, + availableYears, + ...overrides, + }); + + it("is inactive without BLOG_YEAR", () => { + for (const blogYear of [undefined, "", " , "]) { + assert.deepEqual(filter({ blogYear }), { + years: [], + ignoreGlobs: [], + inactiveReason: null, + }); + } + }); + + it("ignores every other year directory for a single year", () => { + assert.deepEqual(filter({ blogYear: "2026" }), { + years: ["2026"], + ignoreGlobs: [ + "**/collections/blog/2024/**", + "**/collections/blog/2025/**", + ], + inactiveReason: null, + }); + }); + + it("accepts a comma-separated list, trimmed, deduplicated, and sorted", () => { + const result = filter({ blogYear: " 2026,2025 ,2026" }); + assert.deepEqual(result.years, ["2025", "2026"]); + assert.deepEqual(result.ignoreGlobs, ["**/collections/blog/2024/**"]); + }); + + it("does not apply to a full site build, and says why", () => { + const result = filter({ blogYear: "2026", isFullSiteBuild: true }); + assert.deepEqual(result.ignoreGlobs, []); + assert.match(result.inactiveReason, /BUILD_FULL_SITE/); + }); + + it("does not apply when the blog collection is excluded, and says why", () => { + const result = filter({ + blogYear: "2026", + excludedCollections: ["blog", "news"], + }); + assert.deepEqual(result.ignoreGlobs, []); + assert.match(result.inactiveReason, /blog collection is excluded/); + }); + + it("rejects values that are not four-digit years", () => { + assert.throws(() => filter({ blogYear: "2026,26" }), /invalid: 26/); + assert.throws(() => filter({ blogYear: "latest" }), /invalid: latest/); + }); + + it("rejects years without a blog directory instead of building nothing", () => { + assert.throws( + () => filter({ blogYear: "2019" }), + /BLOG_YEAR 2019 has no directory.*available years: 2024, 2025, 2026/, + ); + }); + + it("validates BLOG_YEAR even when the filter cannot apply", () => { + assert.throws( + () => filter({ blogYear: "abc", isFullSiteBuild: true }), + /invalid: abc/, + ); + }); +}); + +describe("listBlogYears", () => { + it("lists only four-digit year directories, sorted", () => { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), "blog-years-")); + try { + for (const name of ["2026", "2019", "blog-template"]) { + fs.mkdirSync(path.join(dir, name)); + } + fs.writeFileSync(path.join(dir, "Blog.style.js"), ""); + fs.writeFileSync(path.join(dir, "2020"), ""); + assert.deepEqual(listBlogYears(dir), ["2019", "2026"]); + } finally { + fs.rmSync(dir, { recursive: true, force: true }); + } + }); + + it("finds the repository's blog years", () => { + const years = listBlogYears(); + assert.ok(years.length > 0); + assert.ok(years.every((year) => /^\d{4}$/.test(year))); + }); +}); From d5e2232ebb61d3041bae154f7270d45ded48449c Mon Sep 17 00:00:00 2001 From: Lee Calcote Date: Mon, 14 Sep 2026 17:47:28 -0500 Subject: [PATCH 2/2] ci: run build-collections unit tests in the PR preview build The Checks workflow has been disabled since December 2025, so the unit test step added there never runs. Build PR Preview runs on every PR; run the tests right after install, before the long site build. Signed-off-by: Lee Calcote --- .github/workflows/build-site-preview.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/build-site-preview.yaml b/.github/workflows/build-site-preview.yaml index 102b91669d6002..c8cf5814c9ecfa 100644 --- a/.github/workflows/build-site-preview.yaml +++ b/.github/workflows/build-site-preview.yaml @@ -50,6 +50,9 @@ jobs: - name: Install dependencies run: npm ci + - name: Unit tests + run: npm run test:unit + - name: Build PR preview env: GATSBY_PREVIEW: "true"