Take the article page color from the section banner SVG so the body cannot drift from the hero on current or future posts.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
adilallo
2026-08-19 16:31:10 -06:00
co-authored by Cursor
parent 1a014f30f0
commit 0f16c559c4
5 changed files with 133 additions and 4 deletions
@@ -11,7 +11,7 @@ thumbnail:
vertical: "making-decisions-without-hierarchy-vertical.svg" vertical: "making-decisions-without-hierarchy-vertical.svg"
horizontal: "making-decisions-without-hierarchy-horizontal.svg" horizontal: "making-decisions-without-hierarchy-horizontal.svg"
background: background:
color: "#F3F3F1" color: "#ECC98E"
--- ---
Many groups try to work without bosses, managers, or traditional leadership structures. But when no one's in charge, how do decisions actually get made? Non-hierarchical groups often rely on collective processes that prioritize trust, transparency, and shared responsibility. These approaches can take more time upfront, but they help build stronger, more equitable communities in the long run. Many groups try to work without bosses, managers, or traditional leadership structures. But when no one's in charge, how do decisions actually get made? Non-hierarchical groups often rely on collective processes that prioritize trust, transparency, and shared responsibility. These approaches can take more time upfront, but they help build stronger, more equitable communities in the long run.
+8 -3
View File
@@ -64,7 +64,7 @@ background:
- **related**: Array of article slugs (use filename without .md) - **related**: Array of article slugs (use filename without .md)
- **thumbnail**: Custom images for article thumbnails (optional) - **thumbnail**: Custom images for article thumbnails (optional)
- **banner.horizontal**: Section banner for md+ breakpoints (optional; defaults to `{slug}-section.svg`) - **banner.horizontal**: Section banner for md+ breakpoints (optional; defaults to `{slug}-section.svg`)
- **background.color**: Page background color as a hex code (e.g., `#F4F3F1`) - **background.color**: Fallback page color when there is no `{slug}-section.svg`. If that banner exists, the page uses its 1920×672 rect fill so the body cannot drift from the hero.
### Related Articles ### Related Articles
@@ -107,11 +107,16 @@ If you omit custom SVGs, the site reuses assets from an existing catalog article
## Background Color ## Background Color
Set the content page background using a hex color in frontmatter: The article page background is the fill of the 1920×672 rect in
`public/content/blog/{slug}-section.svg` (the md+ ContentBanner). That keeps
the body the same color as the hero without a second hex to maintain.
`background.color` in frontmatter is only used when that section SVG is
missing:
```yaml ```yaml
background: background:
color: "#F4F3F1" # Use any valid hex code color: "#F4F3F1" # Fallback when there is no {slug}-section.svg
``` ```
## File Naming ## File Naming
+51
View File
@@ -0,0 +1,51 @@
import fs from "fs";
import path from "path";
const SECTION_RECT =
/<rect\b[^>]*\bwidth="1920"[^>]*\bheight="672"[^>]*\bfill="(#[0-9A-Fa-f]{3,8})"/i;
/**
* First 1920×672 rect fill in a Figma Section banner SVG.
* Clip-path rects with fill="white" come later and are ignored.
*/
export function extractSectionBannerFill(svg: string): string | null {
const match = svg.match(SECTION_RECT);
return match?.[1] ?? null;
}
function sectionBannerDiskPath(slug: string, bannerFileName?: string): string {
return path.join(
process.cwd(),
"public/content/blog",
bannerFileName ?? `${slug}-section.svg`,
);
}
/**
* Page background for an article: section banner fill when that SVG exists,
* otherwise the markdown `background.color`.
*/
export function resolveBlogPageBackgroundColor({
slug,
bannerFileName,
frontmatterColor,
}: {
slug: string;
bannerFileName?: string;
frontmatterColor?: string;
}): string | undefined {
try {
const svg = fs.readFileSync(
sectionBannerDiskPath(slug, bannerFileName),
"utf8",
);
const fill = extractSectionBannerFill(svg);
if (fill) {
return fill;
}
} catch {
// No section SVG yet — fall through to frontmatter.
}
return frontmatterColor;
}
+9
View File
@@ -3,6 +3,7 @@ import path from "path";
import matter from "gray-matter"; import matter from "gray-matter";
import { validateBlogPost, sanitizeBlogPost } from "./validation"; import { validateBlogPost, sanitizeBlogPost } from "./validation";
import type { BlogPostFrontmatter } from "./validation"; import type { BlogPostFrontmatter } from "./validation";
import { resolveBlogPageBackgroundColor } from "./blogSectionBackground";
import { logger } from "./logger"; import { logger } from "./logger";
/** /**
@@ -104,6 +105,14 @@ export function parseBlogPost(filePath: string): BlogPost | null {
const sanitizedFrontmatter = sanitizeBlogPost(data); const sanitizedFrontmatter = sanitizeBlogPost(data);
const slug = generateSlug(filePath.replace(/\.mdx?$/, "")); const slug = generateSlug(filePath.replace(/\.mdx?$/, ""));
const pageBackground = resolveBlogPageBackgroundColor({
slug,
bannerFileName: sanitizedFrontmatter.banner?.horizontal,
frontmatterColor: sanitizedFrontmatter.background?.color,
});
if (pageBackground) {
sanitizedFrontmatter.background = { color: pageBackground };
}
return { return {
slug, slug,
+64
View File
@@ -0,0 +1,64 @@
import { describe, expect, it } from "vitest";
import { getAllBlogPosts } from "../../lib/content";
import {
extractSectionBannerFill,
resolveBlogPageBackgroundColor,
} from "../../lib/blogSectionBackground";
describe("extractSectionBannerFill", () => {
it("reads the first 1920×672 rect fill and ignores a later clip-path rect", () => {
const svg = `<svg>
<rect width="1920" height="672" fill="#ECC98E"/>
<defs>
<clipPath id="clip">
<rect width="1920" height="672" fill="white"/>
</clipPath>
</defs>
</svg>`;
expect(extractSectionBannerFill(svg)).toBe("#ECC98E");
});
it("returns null when there is no section rect", () => {
expect(extractSectionBannerFill("<svg></svg>")).toBeNull();
});
});
describe("resolveBlogPageBackgroundColor", () => {
it("uses the section SVG fill when the banner file exists", () => {
expect(
resolveBlogPageBackgroundColor({
slug: "making-decisions-without-hierarchy",
frontmatterColor: "#F3F3F1",
}),
).toBe("#ECC98E");
});
it("falls back to frontmatter when there is no section SVG", () => {
expect(
resolveBlogPageBackgroundColor({
slug: "not-a-real-article",
frontmatterColor: "#F4F3F1",
}),
).toBe("#F4F3F1");
});
});
describe("parsed blog posts", () => {
it("uses each articles section banner fill as the page background", () => {
const posts = getAllBlogPosts();
expect(posts.length).toBeGreaterThan(0);
for (const post of posts) {
const fromBanner = resolveBlogPageBackgroundColor({
slug: post.slug,
bannerFileName: post.frontmatter.banner?.horizontal,
frontmatterColor: "#000000",
});
if (fromBanner === "#000000") {
continue;
}
expect(post.frontmatter.background?.color).toBe(fromBanner);
}
});
});