From 5f2a2fb9c60d9df6159733d259e824943c6a6057 Mon Sep 17 00:00:00 2001 From: Anthony Ettinger Date: Sat, 10 Oct 2026 09:50:31 +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 --- .../contract/logicsrc-web.contract.test.ts | 7 ++-- apps/logicsrc-web/package.json | 1 + .../logicsrc-web/src/app/[[...slug]]/page.tsx | 4 ++- .../src/components/site-shell.tsx | 36 +++---------------- apps/logicsrc-web/src/lib/page-markup.ts | 19 ++++------ apps/logicsrc-web/src/lib/site-footer.ts | 26 ++++++++++++++ bun.lock | 3 ++ 7 files changed, 50 insertions(+), 46 deletions(-) create mode 100644 apps/logicsrc-web/src/lib/site-footer.ts diff --git a/apps/logicsrc-web/contract/logicsrc-web.contract.test.ts b/apps/logicsrc-web/contract/logicsrc-web.contract.test.ts index e447552f..a5fcfe25 100644 --- a/apps/logicsrc-web/contract/logicsrc-web.contract.test.ts +++ b/apps/logicsrc-web/contract/logicsrc-web.contract.test.ts @@ -4,6 +4,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { proxy } from "../src/proxy"; import { renderPageMarkup } from "@/lib/page-markup"; +import { siteFooterHtml } from "@/lib/site-footer"; import { choosePaymentRail, signSession, verifyCoinPayWebhook, verifySession } from "@/lib/coinpay"; import { POST as coinpayCheckout } from "@/app/api/hire-us/coinpay-checkout/route"; import { POST as projectRequest } from "@/app/api/hire-us/project-request/route"; @@ -91,8 +92,10 @@ describe("server-rendered page markup", () => { expect(markup).toContain('id="project-request-form"'); }); - it("carries the Profullstack webring links (the ring verifier reads the homepage)", () => { - const markup = renderPageMarkup(); + it("carries the Profullstack webring links (the ring verifier reads the homepage)", async () => { + process.env.PROFULLSTACK_FOOTER_OFFLINE = "1"; + const markup = renderPageMarkup("/", await siteFooterHtml()); + expect(markup).toContain("data-pfs-footer"); const from = "from=https%3A%2F%2Flogicsrc.com%2F"; for (const hop of ["previous", "next", "random"]) { expect(markup).toContain(`https://rssamplifier.com/ring/profullstack/${hop}?${from}`); diff --git a/apps/logicsrc-web/package.json b/apps/logicsrc-web/package.json index fbbdd88b..9a143a8a 100644 --- a/apps/logicsrc-web/package.json +++ b/apps/logicsrc-web/package.json @@ -15,6 +15,7 @@ "@logicsrc/openontology": "file:../../packages/openontology", "@logicsrc/openprd": "file:../../packages/openprd", "@profullstack/autoblog": "github:profullstack/autoblog#75e54af", + "@profullstack/footer": "latest", "@profullstack/stack": "^0.1.3", "@profullstack/x402-gateway": "^0.7.0", "@supabase/supabase-js": "^2.105.4", diff --git a/apps/logicsrc-web/src/app/[[...slug]]/page.tsx b/apps/logicsrc-web/src/app/[[...slug]]/page.tsx index 2f09b350..06e28945 100644 --- a/apps/logicsrc-web/src/app/[[...slug]]/page.tsx +++ b/apps/logicsrc-web/src/app/[[...slug]]/page.tsx @@ -4,6 +4,7 @@ import type { ReactNode } from "react"; import { renderPageMarkup } from "@/lib/page-markup"; import { pageMetadata, specMetadata } from "@/lib/page-meta"; import { HomeInteractivity } from "@/components/home-interactivity"; +import { siteFooterHtml } from "@/lib/site-footer"; // The legacy SPA served the same single page for every top-level path and just // scrolled to the matching section. We preserve those URLs (they are canonical @@ -50,9 +51,10 @@ export default async function Page({ } } + const footer = await siteFooterHtml(); return ( <> -
+
); diff --git a/apps/logicsrc-web/src/components/site-shell.tsx b/apps/logicsrc-web/src/components/site-shell.tsx index 5a29fa29..ce6e60de 100644 --- a/apps/logicsrc-web/src/components/site-shell.tsx +++ b/apps/logicsrc-web/src/components/site-shell.tsx @@ -2,6 +2,8 @@ import type { ReactNode } from "react"; import { renderInstallCommand } from "@/lib/install-command"; import { Breadcrumbs } from "@/components/breadcrumbs"; import { SideNav } from "@/components/side-nav"; +import { Footer } from "@profullstack/footer/react"; +import { SITE_FOOTER, SITE_FOOTER_COLOR } from "@/lib/site-footer"; /** * The site chrome for every standalone route: the sidebar that unfolds to @@ -37,37 +39,9 @@ export function SiteShell({
{children} - +
+
+
); diff --git a/apps/logicsrc-web/src/lib/page-markup.ts b/apps/logicsrc-web/src/lib/page-markup.ts index 937509fb..583a1d77 100644 --- a/apps/logicsrc-web/src/lib/page-markup.ts +++ b/apps/logicsrc-web/src/lib/page-markup.ts @@ -111,7 +111,12 @@ const comparisonRows = [ } ]; -export function renderPageMarkup(path = "/"): string { +/** + * `footer` is the site footer's HTML (lib/site-footer.ts, @profullstack/footer), placed + * at the end of the workspace. The page route passes it; it is server-rendered, which + * the Profullstack ring's verifier needs. + */ +export function renderPageMarkup(path = "/", footer = ""): string { return `
- - +
${footer}
`; diff --git a/apps/logicsrc-web/src/lib/site-footer.ts b/apps/logicsrc-web/src/lib/site-footer.ts new file mode 100644 index 00000000..314e378b --- /dev/null +++ b/apps/logicsrc-web/src/lib/site-footer.ts @@ -0,0 +1,26 @@ +import { footerHtml, type FooterOptions } from "@profullstack/footer"; + +/** + * The site footer: @profullstack/footer (links, copyright and the Profullstack + * webring). SiteShell renders it as the package's React server component; the + * homepage, which is the legacy markup string (lib/page-markup.ts), gets the same + * footer as HTML. Both render on the server, which is what the ring's verifier reads. + */ +export const SITE_FOOTER: FooterOptions = { + site: "https://logicsrc.com/", + links: [ + { label: "Specs", href: "/specs" }, + { label: "Docs", href: "/docs" }, + { label: "RSS", href: "/blog/rss.xml" }, + { label: "llms.txt", href: "/llms.txt" }, + { label: "Terms", href: "/terms" }, + { label: "Privacy", href: "/privacy" }, + ], +}; + +/** Muted text, as the old footer had; the package inherits `color`. */ +export const SITE_FOOTER_COLOR = "#5b6b7a"; + +export function siteFooterHtml(): Promise { + return footerHtml(SITE_FOOTER); +} diff --git a/bun.lock b/bun.lock index 856e9a2e..a077f66f 100644 --- a/bun.lock +++ b/bun.lock @@ -57,6 +57,7 @@ "@logicsrc/openontology": "file:../../packages/openontology", "@logicsrc/openprd": "file:../../packages/openprd", "@profullstack/autoblog": "github:profullstack/autoblog#75e54af", + "@profullstack/footer": "latest", "@profullstack/stack": "^0.1.3", "@profullstack/x402-gateway": "^0.7.0", "@supabase/supabase-js": "^2.105.4", @@ -801,6 +802,8 @@ "@profullstack/emailer": ["@profullstack/emailer@1.0.1", "", {}, "sha512-/uhHJJGH+1xSSz3mJn6X+m6aruYjMD3JOaRp/d4R/YWlzpy07H9z0/JUleIyRyBPNmaANSIwjTZ7aVjaukOEpg=="], + "@profullstack/footer": ["@profullstack/footer@0.2.1", "", { "peerDependencies": { "react": ">=18" }, "optionalPeers": ["react"], "bin": { "profullstack-footer": "bin/profullstack-footer.js" } }, "sha512-KHmKVwrlzCBrSAAJ4zobNFCxTg8VF0gvNMeAut00dX3+gbWCtk6eOwIR2AaqEC7smj6Aio6cIexzt/dUnv1McA=="], + "@profullstack/hqtui": ["@profullstack/hqtui@0.5.0", "", { "bin": { "hqtui": "bin/hqtui.mjs" } }, "sha512-jDwILBmdQx8pQA0ao/tzzty8u5nFNS4XhvU9xXhZvdIaMeiF3HBJIVtP6cS6kigS4lXmHR7sztbb9QJpaWwhig=="], "@profullstack/keys": ["@profullstack/keys@0.1.0", "", {}, "sha512-Vys47LvS5ivhdZu4SYOxh9Le3DEmWhjP0+CtrU45RhXAbY0ZFG/2Dk9V1bIIUGDp8UWMJqctvUD21JrNjfh04w=="],