Use shared docs layout for unlisted beta guide

This commit is contained in:
Michael Nguyen committed 2026-09-11 00:18:39 -07:00
1 parent a89f246c67
commit 4b94d17fbb
5 files changed
+43 -44

No files matched your search

+2 -2
View File
@@ -59,8 +59,8 @@ The **documentation in this repository** is licensed under [Creative Commons Att
### Unlisted hosted beta guide
`docs/hosted-beta.md` is built at `/hosted-beta/` by `site/build-unlisted.mjs`.
It reuses the article stylesheet without booting the navigation/search SPA.
`docs/hosted-beta.md` is built at `/hosted-beta/` by `site/build-release.mjs`.
It reuses the full docs shell and initializes page tools without adding the page to the navigation/search manifest.
Keep it out of `site/content.json`: it is deliberately absent from navigation,
search, homepage listings, previous/next links, and the sitemap. It has a
`noindex` meta tag; this is a public page, not an access-controlled document.
+4 -2
View File
@@ -10,8 +10,10 @@ try {
const html = await fs.readFile(path.join(out, 'hosted-beta/index.html'), 'utf8');
assert.match(html, /name="robots" content="noindex, follow"/);
assert.equal((html.match(/<h1(?:\s[^>]*)?>/g) || []).length, 1);
assert.ok(html.includes(`href="${base}styles.`));
assert.ok(!html.includes('<script'), 'Unlisted page must not boot the manifest-based SPA');
assert.ok(html.includes(`<base data-seo-base href="${base}"`));
assert.ok(html.includes('data-unlisted-page="hosted-beta"'));
assert.ok(html.includes('id="sb-sections" data-server-rendered="true"'));
assert.ok(html.includes('id="breadcrumb"'));
for (const file of ['content.json', 'index.html', 'sitemap.xml']) {
assert.ok(!(await fs.readFile(path.join(out, file), 'utf8')).includes('hosted-beta'), file);
}
+25 -4
View File
@@ -385,8 +385,8 @@ function findHeadingTarget(article, targetHeading) {
);
}
function decorateHeadings(article, file) {
const page = allPages.find(candidate => candidate.file === file);
function decorateHeadings(article, file, unlistedPage = null) {
const page = unlistedPage || allPages.find(candidate => candidate.file === file);
if (!page) return;
const usedIds = new Set();
@@ -888,6 +888,23 @@ async function init() {
buildMobileDrawer();
initSearch();
const unlistedArticle = document.querySelector('article[data-unlisted-page]');
if (unlistedArticle) {
const page = { file: 'hosted-beta.md', slug: 'hosted-beta', title: 'Hosted beta', sectionTitle: 'Closed beta', navTrail: ['Hosted beta'] };
currentFile = page.file;
showArticleView();
insertMetaRow(unlistedArticle, page);
decorateHeadings(unlistedArticle, page.file, page);
buildToc(unlistedArticle, page.file, page);
hideLoading();
const heading = getCurrentHeadingRoute();
if (heading) {
const target = findHeadingTarget(unlistedArticle, heading);
if (target) focusHeading(target);
}
return;
}
const pathRoute = getPathRoute();
const rawRoute = applyRedirect(pathRoute || getLegacyRoute());
const initialRoute = parseRoute(rawRoute);
@@ -1090,6 +1107,10 @@ function buildFlatList() {
/* ─── Load page ─────────────────────────────────────────────────────────── */
async function loadPage(file, targetHeading = null, historyMode = 'push', options = {}) {
const page = allPages.find(candidate => candidate.file === file);
if (document.querySelector('article[data-unlisted-page]') && page) {
location.assign(targetHeading ? getPageHeadingUrl(page, targetHeading) : getPageUrl(page));
return;
}
currentFile = file;
showArticleView();
setActiveState(file);
@@ -1721,7 +1742,7 @@ function headingLabel(heading) {
return clone.textContent.trim();
}
function buildToc(article, file) {
function buildToc(article, file, unlistedPage = null) {
resetToc();
const metaRow = article.querySelector('.meta-row');
@@ -1732,7 +1753,7 @@ function buildToc(article, file) {
const headings = [...article.querySelectorAll('h2, h3')];
if (headings.length < 2) return;
const page = allPages.find(candidate => candidate.file === file);
const page = unlistedPage || allPages.find(candidate => candidate.file === file);
const CHEVRON_SVG = '<svg viewBox="0 0 12 12" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="m3 4.5 3 3 3-3"/></svg>';
const TOC_SVG = '<svg class="toc-icon" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M3 4h10M3 8h7M3 12h9"/></svg>';
+12 -2
View File
@@ -8,7 +8,6 @@ import { fileURLToPath } from "node:url";
import { createHash } from "node:crypto";
import { transform } from "esbuild";
import { marked } from "marked";
import { buildUnlistedPages } from "./build-unlisted.mjs";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
@@ -1743,7 +1742,18 @@ async function main() {
await copyDirRecursive(screenshotsSourceDir, screenshotTargetDir);
}
await buildUnlistedPages({ repoRoot, outDir: options.outDir, basePath: options.basePath, stylesName });
// Unlisted routes reuse the full docs shell, but never enter the discovery manifest.
const betaMarkdown = await fs.readFile(path.join(docsRoot, "hosted-beta.md"), "utf8");
const betaPage = { file: "hosted-beta.md", slug: "hosted-beta", title: "Hosted beta", navTrail: ["Hosted beta"] };
const betaMetadata = { ...rootMetadata, title: "Welcome to the Paperclip hosted beta", description: markdownDescription(betaMarkdown), url: siteUrlForPath(options.siteUrl, options.basePath, "hosted-beta/"), page: betaPage };
const betaHtml = buildStaticPageHtml(
withStaticChrome(sourceIndex, releaseNav, options.basePath, sectionIconPaths, betaMetadata),
betaMetadata, betaMarkdown, options.basePath, releaseStyles, null, null,
).replace("</head>", '<meta name="robots" content="noindex, follow" /></head>')
.replace('<article id="article">', '<article id="article" data-unlisted-page="hosted-beta">');
await ensureDir(path.join(options.outDir, "hosted-beta"));
await fs.writeFile(path.join(options.outDir, "hosted-beta/index.html"), betaHtml);
await fs.writeFile(path.join(options.outDir, "hosted-beta.md"), betaMarkdown);
const missingNavTargets = [];
for (const { page } of flattenNavPages(releaseNav)) {
-34
View File
@@ -1,34 +0,0 @@
import fs from 'node:fs/promises';
import path from 'node:path';
import { marked } from 'marked';
// Deliberately separate from content.json: no sidebar, search, sitemap,
// homepage listing, or previous/next entry. No SPA boot on these pages.
export async function buildUnlistedPages({ repoRoot, outDir, basePath, stylesName }) {
const base = basePath === 'auto' ? '../' : basePath;
const markdown = await fs.readFile(path.join(repoRoot, 'docs/hosted-beta.md'), 'utf8');
const html = `<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="robots" content="noindex, follow">
<title>Welcome to the Paperclip hosted beta</title>
<link rel="stylesheet" href="${base}${stylesName}">
<style>
body { display: block; overflow: auto; }
.beta-shell { max-width: 824px; margin: 0 auto; padding: 32px; }
.beta-header { display: flex; justify-content: space-between; align-items: center; gap: 16px; padding-bottom: 24px; margin-bottom: 40px; border-bottom: 1px solid var(--stone); font-family: var(--font-body); }
.beta-header a { color: var(--ink); text-decoration: none; }
.beta-header a:first-child { font-weight: 650; font-size: 20px; }
.beta-header a:last-child { font-size: 14px; text-decoration: underline; }
@media (max-width: 600px) { .beta-shell { padding: 24px 20px; } .beta-header { margin-bottom: 28px; } }
</style>
</head>
<body><div class="beta-shell">
<header class="beta-header"><a href="${base}">Paperclip Docs</a><a href="https://my.paperclip.app/">Sign in to Paperclip</a></header>
<main><article id="article">${marked.parse(markdown)}</article></main>
</div></body></html>`;
await fs.mkdir(path.join(outDir, 'hosted-beta'), { recursive: true });
await fs.writeFile(path.join(outDir, 'hosted-beta/index.html'), html);
}