diff --git a/playground/astro.config.mjs b/playground/astro.config.mjs index 2d4fa3b..2aaa7e9 100644 --- a/playground/astro.config.mjs +++ b/playground/astro.config.mjs @@ -111,6 +111,28 @@ export default defineConfig({ style: 'font-size:1.25rem; margin: 0 -0.125rem;', }, }], + subNavBar: [ + { + label: 'Blog', + slug: 'posts', + }, + { + label: 'Talks', + slug: 'talks', + }, + { + label: 'Podcasts', + slug: 'podcasts', + }, + { + label: 'Streams', + slug: 'streams', + }, + { + label: 'Notes', + slug: 'notes', + }, + ], }), ], }) diff --git a/src/components/SubNav.astro b/src/components/SubNav.astro index 55c7947..5d5913e 100644 --- a/src/components/SubNav.astro +++ b/src/components/SubNav.astro @@ -1,20 +1,26 @@ --- -const inactiveStyle = 'opacity-20 hover:opacity-50' -const activeStyle = 'opacity-100 underline' +import { getSubNavbarFromConfig } from '../utils/navigation' +import { slugToLocaleData, urlToSlug } from '../utils/slugs' +import config from 'virtual:vitesse/user-config' -const currentPath = Astro.url.pathname +const url = Astro.url +const slug = urlToSlug(url) +const localeData = slugToLocaleData(slug) +const subNavBar = getSubNavbarFromConfig(config.subNavBar, url.pathname, localeData.locale) ---
- Blog - Talks - - Podcasts - - - Streams - - Notes + { + subNavBar.map((subNavLink) => ( + + {subNavLink.label} + + )) + }
diff --git a/src/schemas/navbar.ts b/src/schemas/navbar.ts index 9d3f256..dd236d6 100644 --- a/src/schemas/navbar.ts +++ b/src/schemas/navbar.ts @@ -7,6 +7,9 @@ const NavBarBaseSchema = z.object({ label: z.string(), /** Translations of the `label` for each supported language. */ translations: z.record(z.string()).default({}), +}) + +const NavBarWithIconSchema = NavBarBaseSchema.extend({ icon: z.string().optional(), hideLabel: z.boolean().default(false), labelClass: z.string().optional().default(''), @@ -24,7 +27,7 @@ export type LinkHTMLAttributes = z.infer export const NavBarLinkItemHTMLAttributesSchema = (): z.ZodDefault => linkHTMLAttributesSchema.default({}) -const NavBarLinkItemSchema = NavBarBaseSchema.extend({ +const NavBarLinkItemSchema = NavBarWithIconSchema.extend({ /** The link to this item’s content. Can be a relative link to local files or the full URL of an external page. */ link: z.string(), /** HTML attributes to add to the link item. */ @@ -32,7 +35,7 @@ const NavBarLinkItemSchema = NavBarBaseSchema.extend({ }).strict() export type SidebarLinkItem = z.infer -const InternalNavBarLinkItemSchema = NavBarBaseSchema.partial({ label: true }).extend({ +const InternalNavBarLinkItemSchema = NavBarWithIconSchema.extend({ /** The link to this item’s content. Must be a slug of a Content Collection entry. */ slug: z.string(), /** HTML attributes to add to the link item. */ @@ -49,3 +52,11 @@ export const NavBarItemSchema = z.union([ InternalNavBarLinkItemShorthandSchema, ]) export type NavBarItem = z.infer + +export const SubNavBarItemSchema = NavBarBaseSchema.extend({ + /** The link to this item’s content. Must be a slug of a Content Collection entry. */ + slug: z.string(), + /** HTML attributes to add to the link item. */ + attrs: NavBarLinkItemHTMLAttributesSchema(), +}) +export type SubNavBarItem = z.infer diff --git a/src/utils/navigation.ts b/src/utils/navigation.ts index c8e142c..ba10cc6 100644 --- a/src/utils/navigation.ts +++ b/src/utils/navigation.ts @@ -3,6 +3,7 @@ import type { LinkHTMLAttributes, NavBarItem, SidebarLinkItem, + SubNavBarItem, } from '../schemas/navbar' import type { VitesseConfig } from './user-config' @@ -176,3 +177,84 @@ export function getNavbarFromConfig( export function getNavBar(pathname: string, locale: string | undefined): NavBarEntry[] { return getNavbarFromConfig(config.navBar, pathname, locale) } + +export type SubNavBarEntry = Pick + +export type SubNavBarLink = SubNavBarEntry + +function makeSubNavLink({ + isCurrent = false, + attrs = {}, + ...opts +}: { + label: string + href: string + isCurrent?: boolean + attrs?: LinkHTMLAttributes | undefined +}): SubNavBarLink { + return { ...opts, isCurrent, attrs } +} + +function makeSubNavBarLink({ currentPathname, href, label, attrs }: { + href: string + label: string + currentPathname: string + attrs?: LinkHTMLAttributes +}): SubNavBarLink { + if (!isAbsolute(href)) { + href = formatPath(href) + } + const isCurrent = pathsMatch(encodeURI(href), currentPathname) + return makeSubNavLink({ label, href, isCurrent, attrs }) +} + +function linkFromInternalSubNavBarLinkItem( + item: SubNavBarItem, + locale: string | undefined, + currentPathname: string, +): SubNavBarLink { + // Astro passes root `index.[md|mdx]` entries with a slug of `index` + const slug = item.slug === 'index' ? '' : item.slug + const localizedSlug = locale ? (slug ? `${locale}/${slug}` : locale) : slug + const entry = routes.find(entry => localizedSlug === entry.slug) + if (!entry) { + const hasExternalSlashes = item.slug.at(0) === '/' || item.slug.at(-1) === '/' + if (hasExternalSlashes) { + throw new AstroError( + `The slug \`"${item.slug}"\` specified in the Vitesse navBar config must not start or end with a slash.`, + `Please try updating \`"${item.slug}"\` to \`"${stripLeadingAndTrailingSlashes(item.slug)}"\`.`, + ) + } + else { + throw new AstroError( + `The slug \`"${item.slug}"\` specified in the Vitesse navBar config does not exist.`, + 'Update the Vitesse config to reference a valid entry slug in the docs content collection.\n' + + 'Learn more about Astro content collection slugs at https://docs.astro.build/en/reference/api-reference/#getentry', + ) + } + } + const label + = pickLang(item.translations, localeToLang(locale)) || item.label || entry.entry.data.title + return makeSubNavBarLink({ + href: entry.slug, + label, + currentPathname, + attrs: item.attrs, + }) +} + +function configItemToEntrySubNavBar( + item: SubNavBarItem, + currentPathname: string, + locale: string | undefined, +): SubNavBarEntry { + return linkFromInternalSubNavBarLinkItem(item, locale, currentPathname) +} + +export function getSubNavbarFromConfig(subNavBarConfig: VitesseConfig['subNavBar'], pathname: string, locale: string | undefined): SubNavBarEntry[] { + if (!subNavBarConfig) { + return [] + } + + return subNavBarConfig.map(subNavBar => configItemToEntrySubNavBar(subNavBar, pathname, locale)) +} diff --git a/src/utils/user-config.ts b/src/utils/user-config.ts index f9df09a..a3a7fd6 100644 --- a/src/utils/user-config.ts +++ b/src/utils/user-config.ts @@ -5,7 +5,7 @@ import { ComponentConfigSchema } from '../schemas/components' import { FaviconSchema } from '../schemas/favicon' import { HeadConfigSchema } from '../schemas/head' import { LogoConfigSchema } from '../schemas/logo' -import { NavBarItemSchema } from '../schemas/navbar' +import { NavBarItemSchema, SubNavBarItemSchema } from '../schemas/navbar' import { TitleConfigSchema, TitleTransformConfigSchema } from '../schemas/site-title' import { SocialLinksSchema } from '../schemas/social' import { BuiltInDefaultLocale } from './i18n' @@ -158,6 +158,7 @@ const UserConfigSchema = z.object({ /** Configure your site’s sidebar navigation items. */ navBar: NavBarItemSchema.array().optional(), + subNavBar: SubNavBarItemSchema.array().optional(), }) export const VitesseConfigSchema = UserConfigSchema.strict()