diff --git a/bun.lock b/bun.lock index d4ba3c6..1d7f5f5 100644 --- a/bun.lock +++ b/bun.lock @@ -6,6 +6,7 @@ "name": "@profullstack/readm3", "dependencies": { "@modelcontextprotocol/sdk": "^1.30.0", + "@profullstack/footer": "^0.2.4", "@profullstack/hqtui": "^0.5.0", "@profullstack/synconfig": "^0.1.3", "highlight.js": "^11.12.0", @@ -140,6 +141,8 @@ "@playwright/test": ["@playwright/test@1.63.0", "", { "dependencies": { "playwright": "1.63.0" }, "bin": { "playwright": "cli.js" } }, "sha512-oxMK4vllB9RK5NQ2l1pq1IfOf2AvnEuj/vYGDj0H2nMtmtZpKtCwt/l00GEO6xjGfpBNAvjovvYdCm50dRQkpQ=="], + "@profullstack/footer": ["@profullstack/footer@0.2.4", "", { "peerDependencies": { "react": ">=18" }, "optionalPeers": ["react"], "bin": { "profullstack-footer": "bin/profullstack-footer.js" } }, "sha512-2ErIzHJCDA2baP8aK6f7rCHohW4EX+wnp7D1mrVMQTUxRcyZwKzmho96hhEFJndXRBGgxT3H/78BHOeBfLxTzg=="], + "@profullstack/hqtui": ["@profullstack/hqtui@0.5.0", "", { "bin": { "hqtui": "bin/hqtui.mjs" } }, "sha512-jDwILBmdQx8pQA0ao/tzzty8u5nFNS4XhvU9xXhZvdIaMeiF3HBJIVtP6cS6kigS4lXmHR7sztbb9QJpaWwhig=="], "@profullstack/synconfig": ["@profullstack/synconfig@0.1.3", "", {}, "sha512-Ty/90ibgu2DVlablDeoFdItcobK+c8cMQFvuTfJOUlCfyvQZWoPjgt0JHPwTvq+ynOQ4i1ivBQ3K3PY/S4rObA=="], diff --git a/desktop/package.json b/desktop/package.json index 634e01f..93229aa 100644 --- a/desktop/package.json +++ b/desktop/package.json @@ -1,7 +1,7 @@ { "name": "readm3-desktop", "productName": "readm3", - "version": "0.13.1", + "version": "0.13.2", "private": true, "description": "readm3 as a desktop app: a plain, fast text editor for Markdown and code.", "author": "Profullstack, Inc. ", diff --git a/package.json b/package.json index 6bc622f..58bca00 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@profullstack/readm3", - "version": "0.13.1", + "version": "0.13.2", "description": "Markdown editing and sharing for people and teams. Web/PWA, CLI, API and MCP with version history, view/edit links, and a terminal reader.", "type": "module", "license": "MIT", @@ -43,6 +43,7 @@ }, "dependencies": { "@modelcontextprotocol/sdk": "^1.30.0", + "@profullstack/footer": "latest", "@profullstack/hqtui": "^0.5.0", "@profullstack/synconfig": "^0.1.3", "highlight.js": "^11.12.0", diff --git a/site/assets/styles.css b/site/assets/styles.css index 5760c8a..7303a61 100644 --- a/site/assets/styles.css +++ b/site/assets/styles.css @@ -362,6 +362,8 @@ pre.tree { color: var(--fg); } .site-foot nav { display: flex; flex-wrap: wrap; gap: 0.4rem 1.1rem; } .site-foot a { color: var(--page-dim); text-decoration: none; } .site-foot a:hover { color: var(--page-fg); text-decoration: underline; } +/* More specific than the footer package's own .pfs-footer rules, which come later in the page. */ +footer.pfs-footer { --pfs-footer-muted: var(--page-dim); border-top-color: var(--page-line); } /* --------------------------------------------------------------- narrow -- */ diff --git a/site/build.ts b/site/build.ts index 948a05a..65e0bce 100644 --- a/site/build.ts +++ b/site/build.ts @@ -16,6 +16,7 @@ import { escape, toHtml } from "./html.ts"; import { createHash } from "node:crypto"; import { flatten, type Entry } from "../src/tree.ts"; import { VERSION } from "../src/cli.ts"; +import { footerHtml } from "@profullstack/footer"; import { LANGUAGES } from "../src/code.ts"; const here = dirname(fileURLToPath(import.meta.url)); @@ -30,11 +31,15 @@ const CP_PROJECT = "528372c9-c79a-425f-9f80-f9fe44c5865e"; const CP_SLOT = "b918cc2d-4bd9-45e4-9b9e-52955b4e2e54"; /* - * The site's own footer: one line. The network links (search, ring, publish) - * come from the pit.world global footer, which site/server.ts appends to every - * page at request time. + * The site's own footer: one line, then the shared Profullstack footer + * (@profullstack/footer: copyright and the webring), baked in here and + * re-rendered from the package's @latest template on each request by + * site/server.ts (from dist/pfs-footer.json). The pit.world global footer is + * appended after both at request time. */ -const FOOTER = ``; +const FOOTER_OPTIONS = { site: "https://readm3.com/", links: [] }; +const PFS_FOOTER = await footerHtml(FOOTER_OPTIONS); +const FOOTER = `\n${PFS_FOOTER}`; /* -------------------------------------------------------------- colors --- */ @@ -485,6 +490,7 @@ write("robots.txt", robots); write("sitemap.xml", sitemap); write("llms.txt", llms); write("favicon.svg", favicon); +write("pfs-footer.json", JSON.stringify(FOOTER_OPTIONS)); write(".well-known/openwebring.json", JSON.stringify({ openwebring: "0.1", site: { url: "https://readm3.com/", name: "readm3" }, diff --git a/site/server.ts b/site/server.ts index 7f9add1..e51b101 100644 --- a/site/server.ts +++ b/site/server.ts @@ -4,7 +4,8 @@ * Public pages stay static. Account APIs use verified email identities and * sessions persisted on the mounted database volume. */ -import { existsSync, statSync } from "node:fs"; +import { existsSync, readFileSync, statSync } from "node:fs"; +import { footerHtml } from "@profullstack/footer"; import { dirname, join, normalize, sep } from "node:path"; import { fileURLToPath } from "node:url"; import { Accounts } from "../server/accounts.ts"; @@ -67,14 +68,36 @@ async function pitFooterHtml(): Promise { return pitFooter.html; } +/* + * The shared Profullstack footer (@profullstack/footer). site/build.ts bakes it + * into each page and writes its options to dist/pfs-footer.json; here the baked + * copy is swapped for one rendered from the package's @latest template (cached + * an hour inside the package). Any failure keeps the baked copy. + */ +const footerOptionsFile = join(dist, "pfs-footer.json"); +const footerOptions = existsSync(footerOptionsFile) + ? (JSON.parse(readFileSync(footerOptionsFile, "utf8")) as Parameters[0]) + : null; +const BAKED_FOOTER = /