From 5cc59b2a1e00bac6bbf1c329b1d475947a23328e Mon Sep 17 00:00:00 2001 From: Anthony Ettinger Date: Sat, 10 Oct 2026 10:11:17 +0000 Subject: [PATCH] feat(footer): use @profullstack/footer Replaces the hand-written footer / ring nav with the shared @profullstack/footer, rendered on the server so the ring verifier sees it and a package release updates the footer without a redeploy. Co-Authored-By: Claude Opus 5.5 --- package.json | 1 + pnpm-lock.yaml | 17 ++++++++++ src/app/invite-only/layout.tsx | 10 +++--- src/app/layout.tsx | 6 +++- src/components/layout/main-layout.tsx | 26 ++++----------- src/components/layout/site-footer-context.tsx | 17 ++++++++++ src/components/layout/site-footer.tsx | 24 ++++++++++++++ src/components/layout/webring.tsx | 33 ------------------- 8 files changed, 75 insertions(+), 59 deletions(-) create mode 100644 src/components/layout/site-footer-context.tsx create mode 100644 src/components/layout/site-footer.tsx delete mode 100644 src/components/layout/webring.tsx diff --git a/package.json b/package.json index cb37f0f5..46b9fcd7 100644 --- a/package.json +++ b/package.json @@ -40,6 +40,7 @@ "@noriginmedia/norigin-spatial-navigation": "^3.3.0", "@profullstack/autoblog": "https://codeload.github.com/profullstack/autoblog/tar.gz/75e54af", "@profullstack/emailer": "^1.0.3", + "@profullstack/footer": "latest", "@profullstack/notifications": "^0.1.4", "@profullstack/player": "0.13.2", "@profullstack/referrals": "^0.1.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9680f4a8..2d0d924c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -28,6 +28,9 @@ importers: '@profullstack/emailer': specifier: ^1.0.3 version: 1.0.3 + '@profullstack/footer': + specifier: latest + version: 0.2.1(react@19.3.0) '@profullstack/notifications': specifier: ^0.1.4 version: 0.1.4 @@ -1273,6 +1276,16 @@ packages: '@profullstack/emailer@1.0.3': resolution: {integrity: sha512-VyAtCcfnSI6LOwJvWogCr3ZNmEsaZw+iuWrqnosLsdcKsbRyCrCmXigMNZJZEjlgYlR+lK//PwNPG+xtCFvWAw==} + '@profullstack/footer@0.2.1': + resolution: {integrity: sha512-KHmKVwrlzCBrSAAJ4zobNFCxTg8VF0gvNMeAut00dX3+gbWCtk6eOwIR2AaqEC7smj6Aio6cIexzt/dUnv1McA==} + engines: {node: '>=18'} + hasBin: true + peerDependencies: + react: '>=18' + peerDependenciesMeta: + react: + optional: true + '@profullstack/form-guard@0.1.1': resolution: {integrity: sha512-RGLGfQhjq40f+JqEhSmsJnS0I5sFWjYdrYx4VYtkUqz8xvioN2yyR+6uFK0mnDAStKrYytlngg3meUsrChm/OQ==} engines: {node: '>=20'} @@ -6116,6 +6129,10 @@ snapshots: '@profullstack/emailer@1.0.3': {} + '@profullstack/footer@0.2.1(react@19.3.0)': + optionalDependencies: + react: 19.3.0 + '@profullstack/form-guard@0.1.1': {} '@profullstack/keys@0.1.0': {} diff --git a/src/app/invite-only/layout.tsx b/src/app/invite-only/layout.tsx index d9921e02..c752c790 100644 --- a/src/app/invite-only/layout.tsx +++ b/src/app/invite-only/layout.tsx @@ -1,19 +1,19 @@ import { inviteMetadata } from '@/lib/invite-metadata'; -import { Webring } from '@/components/layout/webring'; +import { SiteFooter } from '@/components/layout/site-footer'; export const metadata = inviteMetadata('Invite only'); /** * While the site is invite only, `/` answers 307 here, so this screen is the homepage - * the Profullstack ring's verifier sees: it carries the ring links, server-rendered. + * the Profullstack ring's verifier sees: it carries the site footer and its ring links, server-rendered. */ export default function InviteOnlyLayout({ children }: { children: React.ReactNode }): React.ReactNode { return ( <> {children} -
- -
+
+ +
); } diff --git a/src/app/layout.tsx b/src/app/layout.tsx index b11ed333..0b6b02bb 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -5,6 +5,8 @@ import Script from 'next/script'; import './globals.css'; import { TvLayoutProvider } from '@/components/layout'; import { Providers } from '@/components/providers'; +import { SiteFooter } from '@/components/layout/site-footer'; +import { SiteFooterProvider } from '@/components/layout/site-footer-context'; const inter = Inter({ subsets: ['latin'], @@ -112,7 +114,9 @@ export default function RootLayout({ children }: RootLayoutProps): React.ReactEl - {children} + + }>{children} + diff --git a/src/components/layout/main-layout.tsx b/src/components/layout/main-layout.tsx index f4e684e1..b2c37a19 100644 --- a/src/components/layout/main-layout.tsx +++ b/src/components/layout/main-layout.tsx @@ -14,7 +14,7 @@ import { cn } from '@/lib/utils'; import { Sidebar } from './sidebar'; import { Header } from './header'; import { useAuth } from '@/hooks/use-auth'; -import { Webring } from '@/components/layout/webring'; +import { useSiteFooter } from '@/components/layout/site-footer-context'; interface MainLayoutProps { children: React.ReactNode; @@ -23,6 +23,7 @@ interface MainLayoutProps { export function MainLayout({ children, className }: MainLayoutProps): React.ReactElement { const router = useRouter(); + const siteFooter = useSiteFooter(); const pathname = usePathname(); const { isLoggedIn, isPremium, user, clearAuth, needsProfileSelection, isLoading, activeProfile } = useAuth(); @@ -72,26 +73,11 @@ export function MainLayout({ children, className }: MainLayoutProps): React.Reac {/* Footer */} - + ); diff --git a/src/components/layout/site-footer-context.tsx b/src/components/layout/site-footer-context.tsx new file mode 100644 index 00000000..2e0ea3d1 --- /dev/null +++ b/src/components/layout/site-footer-context.tsx @@ -0,0 +1,17 @@ +'use client'; + +import { createContext, useContext } from 'react'; + +/** + * Carries the server-rendered SiteFooter (site-footer.tsx) from the root layout to + * client layouts such as MainLayout, which cannot render an async server component. + */ +const SiteFooterContext = createContext(null); + +export function SiteFooterProvider({ footer, children }: { footer: React.ReactNode; children: React.ReactNode }): React.ReactNode { + return {children}; +} + +export function useSiteFooter(): React.ReactNode { + return useContext(SiteFooterContext); +} diff --git a/src/components/layout/site-footer.tsx b/src/components/layout/site-footer.tsx new file mode 100644 index 00000000..2369966c --- /dev/null +++ b/src/components/layout/site-footer.tsx @@ -0,0 +1,24 @@ +import { Footer } from '@profullstack/footer/react'; + +/** + * The site footer: @profullstack/footer (links, copyright and the Profullstack + * webring). An async server component: it renders on the server, where the ring's + * verifier reads it, and fetches the package's @latest template so a footer release + * reaches the site without a redeploy. + * + * MainLayout is a client component, so it cannot render this itself: the root + * layout renders it once and hands it down through SiteFooterProvider. + */ +export function SiteFooter(): React.ReactNode { + return ( +