From 8388251918c9cabd34acd2912eaf7750362c569a Mon Sep 17 00:00:00 2001 From: Robert Wagner Date: Sat, 29 Aug 2026 20:33:02 -0400 Subject: [PATCH] Generalize breadcrumbs to nested paths; add brandColor config Breadcrumbs now build one crumb per path segment, so a custom nested page like /collections/[slug] gets a linked intermediate crumb (named by humanizing the segment) instead of jumping straight from Home to the page title. The trail also feeds the BreadcrumbList schema, and the last crumb gets aria-current="page". Injected core routes are all single-segment, so their rendering is unchanged. New optional brandColor config field (validated as a hex color) sets the Safari pinned-tab icon color and Windows tile color, which were hardcoded to the favicon-generator template values. Both restore customizations whiskey.fm carried in its fork before converting to the npm package. Co-Authored-By: Claude Fable 5 --- packages/starpod/README.md | 2 + .../starpod/src/components/Breadcrumbs.astro | 105 +++++++++++------- packages/starpod/src/layouts/Layout.astro | 11 +- packages/starpod/src/utils/config.ts | 14 ++- tests/unit/config.test.ts | 11 ++ 5 files changed, 100 insertions(+), 43 deletions(-) diff --git a/packages/starpod/README.md b/packages/starpod/README.md index 1dc8426e..b85655a8 100644 --- a/packages/starpod/README.md +++ b/packages/starpod/README.md @@ -90,6 +90,8 @@ validates your config and lists every problem with its field path. - `rssFeed` — the feed your episodes are read from at build time - `links` (optional) — extra navigation links after About and Contact, e.g. `[{ label: 'Store', href: 'https://example.com/store' }]` +- `brandColor` (optional) — hex accent color for the Safari pinned-tab icon + and Windows tile, e.g. `'#531b3c'` ## Integration options diff --git a/packages/starpod/src/components/Breadcrumbs.astro b/packages/starpod/src/components/Breadcrumbs.astro index 5424bba2..8a8a2de4 100644 --- a/packages/starpod/src/components/Breadcrumbs.astro +++ b/packages/starpod/src/components/Breadcrumbs.astro @@ -9,23 +9,37 @@ export interface Props { const { title } = Astro.props; +// Multi-segment paths (e.g. a custom /collections/[slug] page) get an +// intermediate crumb per ancestor segment, named by humanizing the segment. +// The injected core routes are all single-segment, so those keep the +// classic Home > title trail. +function humanize(segment: string): string { + return segment + .split('-') + .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) + .join(' '); +} + +const segments = url.pathname.split('/').filter(Boolean); + +const breadcrumbItems: Array<{ name: string; href: string }> = [ + { name: 'Home', href: '/' }, + ...segments.slice(0, -1).map((segment, index) => ({ + name: humanize(segment), + href: '/' + segments.slice(0, index + 1).join('/') + })), + ...(segments.length > 0 ? [{ name: title, href: url.pathname }] : []) +]; + const breadcrumbSchema = { '@context': 'https://schema.org', '@type': 'BreadcrumbList', - itemListElement: [ - { - '@type': 'ListItem', - position: 1, - name: 'Home', - item: Astro.site?.toString() || '/' - }, - { - '@type': 'ListItem', - position: 2, - name: title, - item: new URL(url, Astro.site).toString() - } - ] + itemListElement: breadcrumbItems.map((item, index) => ({ + '@type': 'ListItem', + position: index + 1, + name: item.name, + item: new URL(item.href, Astro.site).toString() + })) }; --- @@ -33,31 +47,42 @@ const breadcrumbSchema = { diff --git a/packages/starpod/src/layouts/Layout.astro b/packages/starpod/src/layouts/Layout.astro index ba36cc08..3e9f07f8 100644 --- a/packages/starpod/src/layouts/Layout.astro +++ b/packages/starpod/src/layouts/Layout.astro @@ -67,8 +67,15 @@ const description = Astro.props.description ?? starpodConfig.description; - - + + ; + /** + * Hex accent color used for the Safari pinned-tab icon and the Windows + * tile, e.g. "#531b3c". Defaults to the favicon-generator template colors. + */ + brandColor?: string; }; const urlField = (name: string) => @@ -132,7 +138,13 @@ const StarpodConfigSchema: GenericSchema = object({ 'platforms is required (an empty object is fine)' ), rssFeed: urlField('rssFeed'), - links: optional(array(NavLinkSchema, 'links must be an array')) + links: optional(array(NavLinkSchema, 'links must be an array')), + brandColor: optional( + pipe( + string('brandColor must be a string'), + hexColor('brandColor must be a hex color like #531b3c') + ) + ) }); /** diff --git a/tests/unit/config.test.ts b/tests/unit/config.test.ts index a31af246..71175e3b 100644 --- a/tests/unit/config.test.ts +++ b/tests/unit/config.test.ts @@ -28,6 +28,17 @@ describe('defineStarpodConfig', () => { expect(config.links).toHaveLength(1); }); + it('accepts a hex brandColor and rejects a non-hex one', () => { + const config = defineStarpodConfig({ + ...validConfig, + brandColor: '#531b3c' + }); + expect(config.brandColor).toBe('#531b3c'); + expect(() => + validateStarpodConfig({ ...validConfig, brandColor: 'maroon' }) + ).toThrow(/brandColor must be a hex color/); + }); + it('rejects a non-URL rssFeed and names the field', () => { expect(() => validateStarpodConfig({ ...validConfig, rssFeed: 'not a url' })