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 `
`;
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=="],