Profullstack Distributed Footer Engine serves one versioned template to every site. Apps fetch the
remote JSON template at render time, pass their own links and optional OpenIcon publisher marks, and
receive sanitized HTML. Agents can use the same small options object from React, Node, or the CLI.
One footer for every Profullstack site: your links, the copyright line, and the
<< Profullstack >> webring nav (previous, ring, next).
Publish once, every site updates. The markup and CSS ship as data
(template.json). At render time each site fetches
https://cdn.jsdelivr.net/npm/@profullstack/footer@latest/template.json
(15 min cache, 1.5 s timeout, the bundled copy as the fallback), so a release reaches
every site within about an hour (sooner on sites without a page cache) with no bump, rebuild or redeploy. Install it as
"@profullstack/footer": "latest" too, so the render code follows on the next install.
It renders on the server: the ring's verifier reads your HTML and never runs
script, so a client-only footer would never count as a ring member.
Install
pnpm add @profullstack/footer@latest
The shared template lives in src/template.js; npm run build produces the public template.json.
Sites and agents fetch that JSON with read-only requests. Only an npm publisher for
@profullstack/footer can publish a version and move the latest tag, so runtime apps never need a
publish key.
Next.js (App Router)
import { Footer } from '@profullstack/footer/react';
<Footer site="https://example.com/" links={[{ label: 'Docs', href: '/docs' }]} since={2025} />
and the ring descriptor:
import { descriptorResponse } from '@profullstack/footer';
export const GET = () => descriptorResponse({ site: 'https://example.com/', name: 'Example' });
Hono / Express / Bun / SvelteKit
import { footerHtml } from '@profullstack/footer';
const footer = await footerHtml({ site: 'https://example.com/', links: [{ label: 'Docs', href: '/docs' }] });
Static HTML (no server)
Print the footer once and keep it in the page (that is what the verifier sees),
then let the embed swap in the @latest one in the browser:
npx @profullstack/footer html --site https://example.com/ --link Docs=/docs
<footer data-profullstack-footer data-site="https://example.com/" data-links='[{"label":"Docs","href":"/docs"}]'>
…output of the command above…
</footer>
<script type="module" src="https://cdn.jsdelivr.net/npm/@profullstack/footer@latest/src/embed.js"></script>
Options
site | required | your homepage; from= on every hop |
links | [] | { label, href, rel? }[], shown before house links |
since | | first year: © 2024–2026 |
owner | Profullstack, Inc. → profullstack.com | { name, url } |
tagline | | after the copyright, e.g. MIT licensed |
ring | { slug: 'profullstack', name: 'Profullstack', host: 'https://rssamplifier.com' } | |
css | true | false drops the <style>; style .pfs-footer* yourself |
houseLinks | true | false hides the shared Terms, Privacy, Media, GitHub, Discord, X, Events, event registration, PairUX and Moshcoding links |
subscribe | true | false hides the newsletter form |
badges | [] | { href, src, alt, width?, height? }[]: optional badges shown only on this site |
pwamartHref | | Opt in to a PwaMart icon badge and set its destination |
builtIndieHref | | Opt in to a built.indie icon badge and set its destination |
nonce | | CSP nonce for the inline <style>; or css: false and style .pfs-footer* yourself |
version | latest | which CDN template to render: '0.6.13' pins one (see below), 'bundled' never fetches |
templateUrl | | full template URL; wins over version |
Pinning the template version
By default every site follows @latest, which is the point of the package, but it also means a bad
or compromised npm publish reaches every site within the cache window. A site that holds secrets
(a secret manager, an admin app) should pin an exact version instead. jsDelivr serves an exact
version immutably, so later publishes cannot change that site's footer:
When using the moving alias, an edge cache that serves a version older than the installed bundle is
ignored; the bundled template stays in use until the edge catches up. Explicit version pins remain
honored.
import { footerHtml } from '@profullstack/footer';
const footer = await footerHtml({ site: 'https://app.example.com/' });
<Footer site="https://app.example.com/" />
Or for a whole deployment without code changes: PROFULLSTACK_FOOTER_VERSION=0.6.13
(PROFULLSTACK_FOOTER_VERSION=bundled renders the copy in the install and never fetches).
latestTemplate({ version }) and templateUrl(version) are exported too; the CLI takes
--template-version 0.6.13. Bump the pin when you want a new footer.
Theme it with --pfs-footer-muted, --pfs-footer-gap and --pfs-footer-size on
.pfs-footer; colors otherwise inherit from the page.
Newsletter form
Every footer carries a small email field and a Subscribe button that posts the site's hostname to
pit.world. pit.world runs the double opt-in (confirmation email, nothing kept until the click) and
records which property each subscriber came from (https://pit.world/admin/subscribers). The
endpoint is fixed in render.js, never in the template.
The email control is labeled “Email” and uses the example address placeholder you@example.com.
Its form and input set autocomplete="off"; the input also includes ignore hints for Bitwarden,
1Password, and LastPass so it is not mistaken for a login field.
Since 0.4.0 a small inline script inside the form (wireSubscribe) sends it as JSON to
https://pit.world/api/subscribe and reports in place: "Email confirmation sent to you@x. Check your
inbox." or "Failed: " (bad address, domain takes no mail, rate limit, mail provider down, or
the request was blocked). With no script it is a plain POST to https://pit.world/subscribe, which
answers with a "check your inbox" page that links back to the site. embed.js wires the form itself.
CSP: pass the page's nonce so the script runs, and allow pit.world:
connect-src 'self' https://pit.world; form-action 'self' https://pit.world. If the request is
blocked anyway, the form says "Failed: could not reach pit.world" instead of doing nothing.
Turn it off with subscribe: false (--no-subscribe). Installs before 0.3.0 cannot render the
form; their footer shows a plain "Newsletter" link to the same page instead, from the @latest template.
CLI
profullstack-footer html --site https://example.com/ [--link Docs=/docs] [--since 2024] [--offline] [--no-css]
profullstack-footer json --site https://example.com/
profullstack-footer descriptor --site https://example.com/
profullstack-footer template
profullstack-footer check https://example.com/
Shared contact and links
Every site gets a CrawlProof Surveys link, plus links to Profullstack's Terms,
Privacy and Media pages, GitHub, Discord and X, the Events calendar on Luma, a
Luma event registration link, and the PairUX and Moshcoding profiles. The
copyright line includes the small Profullstack icon.
Publisher badges are off by default. Publisher and app pages can opt into the OpenIcon HQ SVG marks:
<Footer
site="https://example.com/"
pwamartHref="https://pwamart.com/publishers/example"
builtIndieHref="https://built.indie.pit.world/p/example"
/>
The shared footer keeps the site's navigation, house links, newsletter, webring and copyright. Its compact
layout leaves publisher marks on the pages that opt in. The Profullstack homepage uses the two service
icons for its publisher profiles. The global phone line remains a click-to-call link.
Edit src/template.js, bump the version in it and in package.json, npm run build,
open a PR, merge, and publish a GitHub release vX.Y.Z. The publish workflow puts it
on npm and purges jsDelivr's @latest so sites pick it up on their next cache refresh.
Bump renderer (and RENDERER in render.js) only when the template needs a
placeholder older installs cannot fill; they keep their bundled template until
reinstalled.
Safety: since 0.5.0 the filled template goes through an allowlist sanitizer
(sanitizeHtml), not a regex blacklist. It tokenizes the HTML the way a browser does and
rebuilds it: only footer div nav section aside header p span small strong em b i ul ol li a img br hr h2-h6 survive; attributes are limited to class title role lang dir aria-* data-pfs-*
plus href rel target hreflang on a and src alt width height loading decoding on img;
every value is entity-decoded, checked and re-escaped, so what was checked is what the browser
sees; href accepts http:, https:, mailto:, tel: or relative URLs; src accepts
http:, https:, mailto: or relative URLs, after decoding and removing control characters.
Comments, unknown tags (text kept, escaped), and script/style/iframe/
svg/math/noscript/template/... with their contents are dropped. The only <style> and
<form> in the output are the ones render.js builds. CSS (sanitizeCss) loses markup, escapes,
comments, @import, expression(, behavior:, -moz-binding and any url() that is not
https or relative. Option values are always escaped.
Env: PROFULLSTACK_FOOTER_OFFLINE=1 never fetches; PROFULLSTACK_FOOTER_VERSION pins the
template version; PROFULLSTACK_FOOTER_TEMPLATE_URL overrides the CDN URL.
MIT © Profullstack, Inc.