refactor(docs): use shared @unom/app-ui/footer component
apple / swift (push) Successful in 1m2s
android / android (push) Successful in 4m23s
deb / build-publish (push) Successful in 2m30s
decky / build-publish (push) Successful in 13s
ci / rust (push) Successful in 4m47s
ci / web (push) Successful in 50s
ci / docs-site (push) Successful in 58s
apple / screenshots (push) Successful in 5m16s
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 53s
ci / bench (push) Successful in 4m39s
rpm / build-publish (bazzite, punktfunk-fedora-rpm) (push) Successful in 9m29s
rpm / build-publish (fedora-44, punktfunk-fedora44-rpm) (push) Successful in 9m23s
docker / deploy-docs (push) Successful in 18s

The docs footer was a hand-maintained mirror of the marketing site's. Both now
render the same @unom/app-ui/footer component, so they stay in sync. The shared
view themes itself through @unom/style tokens (which the docs already map onto
their Fumadocs surfaces), and a resolveHref hook rebases root-relative links
onto the marketing-site origin. Footer types now come from the library too.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
enricobuehler
2026-06-28 14:34:45 +00:00
parent 30d0d36efe
commit 1bd60ffb34
5 changed files with 31 additions and 60 deletions
+5
View File
@@ -9,6 +9,7 @@
"@scalar/api-reference-react": "^0.9.47",
"@tanstack/react-router": "^1.121.0",
"@tanstack/react-start": "^1.121.0",
"@unom/app-ui": "^0.1.0",
"@unom/style": "^0.4.4",
"@unom/ui": "^0.8.16",
"fumadocs-core": "^16.10.5",
@@ -231,6 +232,8 @@
"@headlessui/vue": ["@headlessui/vue@1.7.23", "", { "dependencies": { "@tanstack/vue-virtual": "^3.0.0-beta.60" }, "peerDependencies": { "vue": "^3.2.0" } }, "sha512-JzdCNqurrtuu0YW6QaDtR2PIYCKPUWq28csDyMvN4zmGccmE7lz40Is6hc3LA4HFeCI7sekZ/PQMTNmn9I/4Wg=="],
"@icons-pack/react-simple-icons": ["@icons-pack/react-simple-icons@13.13.0", "", { "peerDependencies": { "react": "^16.13 || ^17 || ^18 || ^19" } }, "sha512-B5HhQMIpcSH4z8IZ8HFhD59CboHceKYMpPC9kAwGyKntvPdyJJv26DLu4Z1wAjcCLyrJhf11tMhiQGom9Rxb9g=="],
"@img/colour": ["@img/colour@1.1.0", "", {}, "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ=="],
"@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.2.4" }, "os": "darwin", "cpu": "arm64" }, "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w=="],
@@ -909,6 +912,8 @@
"@unhead/vue": ["@unhead/vue@2.1.15", "", { "dependencies": { "hookable": "^6.0.1", "unhead": "2.1.15" }, "peerDependencies": { "vue": ">=3.5.18" } }, "sha512-SSByXfEjhzPn8gXdEdgpYqpLMPSkLUH2HVE0GxZfOtNsJ0GgOHQs0g9T67ZZ1z0kTELLKdtOtYrzrbv9+ffF7g=="],
"@unom/app-ui": ["@unom/app-ui@0.1.0", "https://git.unom.io/api/packages/unom/npm/%40unom%2Fapp-ui/-/0.1.0/app-ui-0.1.0.tgz", { "dependencies": { "@icons-pack/react-simple-icons": "^13.13.0" }, "peerDependencies": { "@unom/style": "^0.4.4", "react": "^19.0.0" } }, "sha512-znHZOIRWyJDj4va2X/E4GwvxWZsVeWEYpvu7iHTBIa0UXjkX9aoiujJcMyfPpc2Vof53iafl9hIszgSgjQwzhg=="],
"@unom/style": ["@unom/style@0.4.4", "https://git.unom.io/api/packages/unom/npm/%40unom%2Fstyle/-/0.4.4/style-0.4.4.tgz", { "peerDependencies": { "motion": "^12" } }, "sha512-M45nihK+LGyxwy2mmHYRKggaocTt+EKNVFNaMpTvTaIUpozi7bmKIkbM2/enMYS0/UYTaZrBSZs/a0nPXqkAKw=="],
"@unom/ui": ["@unom/ui@0.8.16", "https://git.unom.io/api/packages/unom/npm/%40unom%2Fui/-/0.8.16/ui-0.8.16.tgz", { "dependencies": { "@tanstack/react-router": "^1.170.11", "@tsdown/css": "^0.22.1", "clsx": "^2.1.1", "howler": "^2.2.4", "sonner": "^2.0.7", "tailwind-merge": "^3.6.0" }, "peerDependencies": { "@payloadcms/richtext-lexical": "^3.85.0", "@tanstack/react-virtual": "^3.14.2", "@unom/style": "^0.4.4", "class-variance-authority": "^0.7.1", "embla-carousel-react": "^8.6.0", "lucide-react": "^1.17.0", "motion": "^12.40.0", "radix-ui": "^1.4.3", "react": "^19.2.7", "react-dom": "^19.2.7", "typescript": "^6.0.3", "zod": "^4.4.3" } }, "sha512-ZH7VOyaRDT81VY8nm1hmx8a4CeObykP8egZbnV4Nju6kE8rQ28wdpBo0X+Zsdu8WvTEmHZGwPR53NHWJULyciw=="],
+1
View File
@@ -14,6 +14,7 @@
"@scalar/api-reference-react": "^0.9.47",
"@tanstack/react-router": "^1.121.0",
"@tanstack/react-start": "^1.121.0",
"@unom/app-ui": "^0.1.0",
"@unom/style": "^0.4.4",
"@unom/ui": "^0.8.16",
"fumadocs-core": "^16.10.5",
+15 -39
View File
@@ -1,51 +1,27 @@
import { getRouteApi } from '@tanstack/react-router'
import type { NavigationLink, NavigationSection } from '@/lib/cms'
import { FooterView } from '@unom/app-ui/footer'
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.
// Footer markup is shared with the marketing site via @unom/app-ui so the two
// stay in sync. It themes itself through @unom/style tokens, which the docs map
// onto their Fumadocs surfaces. Root-relative links target the website (the
// docs don't host /legal/* etc.), so rebase them onto its origin.
const SITE_URL = 'https://punktfunk.unom.io'
const resolve = (to?: string | null) =>
to ? (to.startsWith('/') ? `${SITE_URL}${to}` : to) : '#'
const resolveHref = (to: string) =>
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>
<FooterView
sections={footer?.sections}
tagline={footer?.tagline}
socials={footer?.socials}
socialsLabel="Socials"
resolveHref={resolveHref}
className="border-t border-fd-border"
/>
)
}
+9 -21
View File
@@ -1,33 +1,21 @@
// The docs reuse the punktfunk footer from the shared unom CMS (cms.unom.io).
// The CMS is multi-tenant: footer is a per-tenant collection, so scope the read
// to this project's tenant. Read-only GET, so a plain typed fetch rather than
// pulling in the Payload SDK + generated types.
// The footer shape comes from @unom/app-ui/footer so the docs and the marketing
// site share one type. The CMS is multi-tenant: footer is a per-tenant
// collection, so scope the read to this project's tenant. Read-only GET, so a
// plain typed fetch rather than pulling in the Payload SDK + generated types.
import type { FooterData } from '@unom/app-ui/footer'
const CMS_URL = 'https://cms.unom.io'
// This project's tenant in the shared CMS.
const TENANT = 'punktfunk'
export interface NavigationLink {
id?: string | null
label?: string | null
to?: string | null
}
export type { FooterData as Footer } from '@unom/app-ui/footer'
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 | null> {
export async function findFooter(): Promise<FooterData | null> {
const query = `where%5Btenant.slug%5D%5Bequals%5D=${TENANT}&locale=en&depth=1&limit=1`
const res = await fetch(`${CMS_URL}/api/footers?${query}`)
if (!res.ok) throw new Error(`CMS footer request failed: ${res.status}`)
const data = (await res.json()) as { docs?: Footer[] }
const data = (await res.json()) as { docs?: FooterData[] }
return data.docs?.[0] ?? null
}
+1
View File
@@ -7,6 +7,7 @@
design-token system the punktfunk marketing site also builds on. */
@source '../../node_modules/fumadocs-ui/dist/**/*.js';
@source '../../node_modules/@unom/ui/dist/**/*.{js,mjs}';
@source '../../node_modules/@unom/app-ui/dist/**/*.{js,mjs}';
/* ── punktfunk brand ────────────────────────────────────────────────────────
The brand colour is the violet lens mark. (The marketing site's blue is just