Skip to content
Closed
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
2 changes: 2 additions & 0 deletions packages/starpod/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
105 changes: 65 additions & 40 deletions packages/starpod/src/components/Breadcrumbs.astro
Original file line number Diff line number Diff line change
Expand Up @@ -9,55 +9,80 @@ 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()
}))
};
---

<Schema item={breadcrumbSchema as any} />

<nav class="flex" aria-label="Breadcrumb">
<ol role="list" class="flex items-center space-x-4 text-sm">
<li>
<div>
<a href="/" class="text-gray-400 hover:text-gray-500"> Home </a>
</div>
</li>
<li>
<div class="flex items-center">
<svg
class="h-4 w-4 shrink-0 text-gray-400"
viewBox="0 0 20 20"
fill="currentColor"
aria-hidden="true"
>
<path
fill-rule="evenodd"
d="M7.21 14.77a.75.75 0 01.02-1.06L11.168 10 7.23 6.29a.75.75 0 111.04-1.08l4.5 4.25a.75.75 0 010 1.08l-4.5 4.25a.75.75 0 01-1.06-.02z"
clip-rule="evenodd"></path>
</svg>
<a
href={url}
class="ml-4 line-clamp-1 font-medium text-gray-500 hover:text-gray-700"
>
{title}
</a>
</div>
</li>
{
breadcrumbItems.map((item, index) => (
<li>
{index === 0 ? (
<div>
<a href={item.href} class="text-gray-400 hover:text-gray-500">
{item.name}
</a>
</div>
) : (
<div class="flex items-center">
<svg
class="h-4 w-4 shrink-0 text-gray-400"
viewBox="0 0 20 20"
fill="currentColor"
aria-hidden="true"
>
<path
fill-rule="evenodd"
d="M7.21 14.77a.75.75 0 01.02-1.06L11.168 10 7.23 6.29a.75.75 0 111.04-1.08l4.5 4.25a.75.75 0 010 1.08l-4.5 4.25a.75.75 0 01-1.06-.02z"
clip-rule="evenodd"
/>
</svg>
<a
href={item.href}
class="ml-4 line-clamp-1 font-medium text-gray-500 hover:text-gray-700"
aria-current={
index === breadcrumbItems.length - 1 ? 'page' : undefined
}
>
{item.name}
</a>
</div>
)}
</li>
))
}
</ol>
</nav>
11 changes: 9 additions & 2 deletions packages/starpod/src/layouts/Layout.astro
Original file line number Diff line number Diff line change
Expand Up @@ -67,8 +67,15 @@ const description = Astro.props.description ?? starpodConfig.description;
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" />
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" />
<link rel="manifest" href="/site.webmanifest" />
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="#5bbad5" />
<meta name="msapplication-TileColor" content="#da532c" />
<link
rel="mask-icon"
href="/safari-pinned-tab.svg"
color={starpodConfig.brandColor ?? '#5bbad5'}
/>
<meta
name="msapplication-TileColor"
content={starpodConfig.brandColor ?? '#da532c'}
/>
<meta
name="theme-color"
content="#ffffff"
Expand Down
14 changes: 13 additions & 1 deletion packages/starpod/src/utils/config.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {
array,
hexColor,
minLength,
nonEmpty,
object,
Expand Down Expand Up @@ -83,6 +84,11 @@ export type StarpodConfig = {
* store, or custom pages your site adds (like a sponsor page).
*/
links?: Array<NavLink>;
/**
* 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) =>
Expand Down Expand Up @@ -132,7 +138,13 @@ const StarpodConfigSchema: GenericSchema<StarpodConfig> = 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')
)
)
});

/**
Expand Down
11 changes: 11 additions & 0 deletions tests/unit/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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' })
Expand Down
Loading