Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/build-site-preview.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
5 changes: 3 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/<year>`, 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. <br><br>• `development` - Uses **Deferred Static Generation (DSG)** i.e pages built on demand for faster startup. <br>• `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. |

Expand Down Expand Up @@ -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.

Expand Down
9 changes: 6 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
21 changes: 18 additions & 3 deletions gatsby-config.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

const {
DEFAULT_LITE_BUILD_PROFILE,
getBlogYearFilter,
getExcludedCollections,
isFullSiteBuild,
} = require("./src/utils/build-collections");
Expand All @@ -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,
Expand All @@ -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: {
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
73 changes: 72 additions & 1 deletion src/utils/build-collections.js
Original file line number Diff line number Diff line change
@@ -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({
Expand All @@ -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";

Expand Down Expand Up @@ -33,9 +42,71 @@ const getExcludedCollections = ({
).sort();
};

// Year directories (src/collections/blog/<YYYY>) 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,
};
listBlogYears,
};
149 changes: 149 additions & 0 deletions src/utils/build-collections.test.js
Original file line number Diff line number Diff line change
@@ -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)));
});
});