feat(docs-site): add the site-wide footer, shared with the marketing site
apple / swift (push) Successful in 54s
ci / rust (push) Successful in 1m32s
ci / web (push) Successful in 30s
ci / docs-site (push) Successful in 40s
android / android (push) Successful in 3m14s
deb / build-publish (push) Successful in 3m5s
decky / build-publish (push) Successful in 11s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 5s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 4s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 4s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 4s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 24s
ci / bench (push) Successful in 4m44s
rpm / build-publish (bazzite, punktfunk-fedora-rpm) (push) Successful in 8m55s
rpm / build-publish (fedora-44, punktfunk-fedora44-rpm) (push) Successful in 8m42s
docker / deploy-docs (push) Successful in 19s
apple / swift (push) Successful in 54s
ci / rust (push) Successful in 1m32s
ci / web (push) Successful in 30s
ci / docs-site (push) Successful in 40s
android / android (push) Successful in 3m14s
deb / build-publish (push) Successful in 3m5s
decky / build-publish (push) Successful in 11s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 5s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 4s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 4s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 4s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 24s
ci / bench (push) Successful in 4m44s
rpm / build-publish (bazzite, punktfunk-fedora-rpm) (push) Successful in 8m55s
rpm / build-publish (fedora-44, punktfunk-fedora44-rpm) (push) Successful in 8m42s
docker / deploy-docs (push) Successful in 19s
Pull the same footer from the shared unom CMS global (cms.unom.io) and render it globally under both the home and docs layouts. Read-only typed fetch in a server-side root loader (falls back to null on a CMS hiccup). Root-relative links target the marketing site, so they're resolved against its origin (the docs don't host /legal/* etc.); themed with Fumadocs tokens for light/dark. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,51 @@
|
||||
import { getRouteApi } from '@tanstack/react-router'
|
||||
import type { NavigationLink, NavigationSection } from '@/lib/cms'
|
||||
|
||||
const rootApi = getRouteApi('__root__')
|
||||
|
||||
// The docs share the marketing site's footer (same CMS global). Root-relative
|
||||
// links target the website, so resolve them against its origin — the docs don't
|
||||
// host /legal/* etc. themselves. Mirrors the website Footer, themed for docs.
|
||||
const SITE_URL = 'https://punktfunk.unom.io'
|
||||
const resolve = (to?: string | null) =>
|
||||
to ? (to.startsWith('/') ? `${SITE_URL}${to}` : to) : '#'
|
||||
|
||||
export default function Footer() {
|
||||
const { footer } = rootApi.useLoaderData()
|
||||
const sections: NavigationSection[] = footer?.sections ?? []
|
||||
const tagline = footer?.tagline?.trim()
|
||||
|
||||
if (!sections.length && !tagline) return null
|
||||
|
||||
return (
|
||||
<footer className="border-t border-fd-border bg-fd-card">
|
||||
<div className="mx-auto flex w-full max-w-6xl flex-row flex-wrap gap-12 px-4 py-12 sm:px-6">
|
||||
{sections.map((group, gi) => (
|
||||
<div key={group.id ?? gi}>
|
||||
{group.title && (
|
||||
<h3 className="mb-2 text-sm font-semibold text-fd-foreground">
|
||||
{group.title}
|
||||
</h3>
|
||||
)}
|
||||
<div className="flex flex-col gap-1">
|
||||
{(group.entries ?? []).map((item: NavigationLink, i) => (
|
||||
<a
|
||||
key={item.id ?? `${item.to}-${i}`}
|
||||
href={resolve(item.to)}
|
||||
className="text-sm text-fd-muted-foreground transition-colors hover:text-fd-foreground"
|
||||
>
|
||||
{item.label}
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
{tagline && (
|
||||
<p className="ml-auto self-end text-sm text-fd-muted-foreground">
|
||||
{tagline}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</footer>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
// The docs reuse the punktfunk marketing site's footer — the same Payload CMS
|
||||
// global on the shared unom CMS (cms.unom.io). It's a read-only GET, so a plain
|
||||
// typed fetch rather than pulling in the Payload SDK + generated types.
|
||||
const CMS_URL = 'https://cms.unom.io'
|
||||
|
||||
export interface NavigationLink {
|
||||
id?: string | null
|
||||
label?: string | null
|
||||
to?: string | null
|
||||
}
|
||||
|
||||
export interface NavigationSection {
|
||||
id?: string | null
|
||||
title?: string | null
|
||||
entries?: NavigationLink[] | null
|
||||
}
|
||||
|
||||
export interface Footer {
|
||||
tagline?: string | null
|
||||
sections?: NavigationSection[] | null
|
||||
}
|
||||
|
||||
export async function findFooter(): Promise<Footer> {
|
||||
const res = await fetch(`${CMS_URL}/api/globals/footer?locale=en&depth=1`)
|
||||
if (!res.ok) throw new Error(`CMS footer request failed: ${res.status}`)
|
||||
return res.json() as Promise<Footer>
|
||||
}
|
||||
@@ -1,11 +1,30 @@
|
||||
/// <reference types="vite/client" />
|
||||
import { createRootRoute, HeadContent, Outlet, Scripts } from '@tanstack/react-router'
|
||||
import { createServerFn } from '@tanstack/react-start'
|
||||
import * as React from 'react'
|
||||
import { RootProvider } from 'fumadocs-ui/provider/tanstack'
|
||||
import '@fontsource-variable/geist'
|
||||
import Footer from '@/components/Footer'
|
||||
import { type Footer as FooterData, findFooter } from '@/lib/cms'
|
||||
import appCss from '@/styles/app.css?url'
|
||||
|
||||
// The footer is global and shared with the marketing site (one CMS global).
|
||||
// Fetch it once at the root, server-side, falling back to null so a CMS hiccup
|
||||
// never breaks the page.
|
||||
const getFooter = createServerFn({ method: 'GET' }).handler(
|
||||
async (): Promise<FooterData | null> => {
|
||||
try {
|
||||
return await findFooter()
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
export const Route = createRootRoute({
|
||||
loader: async (): Promise<{ footer: FooterData | null }> => ({
|
||||
footer: await getFooter(),
|
||||
}),
|
||||
head: () => ({
|
||||
meta: [
|
||||
{ charSet: 'utf-8' },
|
||||
@@ -36,7 +55,10 @@ function RootDocument({ children }: { children: React.ReactNode }) {
|
||||
<HeadContent />
|
||||
</head>
|
||||
<body className="flex flex-col min-h-screen">
|
||||
<RootProvider>{children}</RootProvider>
|
||||
<RootProvider>
|
||||
{children}
|
||||
<Footer />
|
||||
</RootProvider>
|
||||
<Scripts />
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user