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:
@@ -11,7 +11,7 @@ thumbnail:
|
||||
vertical: "making-decisions-without-hierarchy-vertical.svg"
|
||||
horizontal: "making-decisions-without-hierarchy-horizontal.svg"
|
||||
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.
|
||||
|
||||
@@ -64,7 +64,7 @@ background:
|
||||
- **related**: Array of article slugs (use filename without .md)
|
||||
- **thumbnail**: Custom images for article thumbnails (optional)
|
||||
- **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
|
||||
|
||||
@@ -107,11 +107,16 @@ If you omit custom SVGs, the site reuses assets from an existing catalog article
|
||||
|
||||
## 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
|
||||
background:
|
||||
color: "#F4F3F1" # Use any valid hex code
|
||||
color: "#F4F3F1" # Fallback when there is no {slug}-section.svg
|
||||
```
|
||||
|
||||
## File Naming
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import path from "path";
|
||||
import matter from "gray-matter";
|
||||
import { validateBlogPost, sanitizeBlogPost } from "./validation";
|
||||
import type { BlogPostFrontmatter } from "./validation";
|
||||
import { resolveBlogPageBackgroundColor } from "./blogSectionBackground";
|
||||
import { logger } from "./logger";
|
||||
|
||||
/**
|
||||
@@ -104,6 +105,14 @@ export function parseBlogPost(filePath: string): BlogPost | null {
|
||||
|
||||
const sanitizedFrontmatter = sanitizeBlogPost(data);
|
||||
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 {
|
||||
slug,
|
||||
|
||||
@@ -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 article’s 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);
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user