Add crawlable docs SEO outputs

Add sitemap, robots, Cloudflare routing/header files, route-specific SEO metadata, JSON-LD, and static crawlable docs page HTML.
This commit is contained in:
Dotta authored and GitHub committed 2026-07-01 14:03:14 -05:00
1 parent 1bba6f6601
commit 28a1c40933
3 files changed
+342 -114

No files matched your search

+97 -3
View File
@@ -31,6 +31,9 @@ let tocScrollHandler = null;
let tocResizeHandler = null;
let tocDocumentClickHandler = null;
let tocKeydownHandler = null;
const SEO_SITE_NAME = 'Paperclip Docs';
const SEO_DEFAULT_TITLE = 'Paperclip Docs';
const SEO_DEFAULT_DESCRIPTION = 'Guides, references, and walkthroughs for running Paperclip, an AI company operating system for agent teams, governance, budgets, and workflows.';
const APP_DIR_NAME = 'site';
const APP_BASE_PATH = (() => {
const marker = `/${APP_DIR_NAME}`;
@@ -130,7 +133,7 @@ function buildRouteValue(page, headingId = null) {
function getRouteUrl(routeValue) {
const normalized = normalizeRouteKey(routeValue);
const basePath = APP_BASE_URL.pathname.replace(/\/$/, '');
return normalized ? `${basePath}/${normalized}` : `${basePath}/`;
return normalized ? `${basePath}/${normalized}/` : `${basePath}/`;
}
function getPageUrl(page) {
@@ -138,16 +141,96 @@ function getPageUrl(page) {
}
function getPageHeadingUrl(page, headingId) {
return getRouteUrl(buildRouteValue(page, headingId));
return `${getPageUrl(page)}#${encodeURIComponent(headingId)}`;
}
function getAbsoluteUrl(path) {
return new URL(path, window.location.origin).toString();
}
function getAbsolutePageUrl(page) {
return getAbsoluteUrl(page ? getPageUrl(page) : getRouteUrl(''));
}
function setHeadElement(selector, tagName, attrs) {
let el = document.head.querySelector(selector);
if (!el) {
el = document.createElement(tagName);
el.dataset.seoManaged = '';
document.head.appendChild(el);
}
for (const [name, value] of Object.entries(attrs)) {
el.setAttribute(name, value);
}
return el;
}
function setSeoMetadata({ title, description, url, type = 'website' }) {
const cleanTitle = title || SEO_DEFAULT_TITLE;
const cleanDescription = description || SEO_DEFAULT_DESCRIPTION;
const cleanUrl = url || getAbsolutePageUrl(null);
document.title = cleanTitle;
setHeadElement('meta[name="description"]', 'meta', { name: 'description', content: cleanDescription });
setHeadElement('meta[name="robots"]', 'meta', {
name: 'robots',
content: 'index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1',
});
setHeadElement('link[rel="canonical"]', 'link', { rel: 'canonical', href: cleanUrl });
setHeadElement('meta[property="og:type"]', 'meta', { property: 'og:type', content: type });
setHeadElement('meta[property="og:site_name"]', 'meta', { property: 'og:site_name', content: SEO_SITE_NAME });
setHeadElement('meta[property="og:title"]', 'meta', { property: 'og:title', content: cleanTitle });
setHeadElement('meta[property="og:description"]', 'meta', { property: 'og:description', content: cleanDescription });
setHeadElement('meta[property="og:url"]', 'meta', { property: 'og:url', content: cleanUrl });
setHeadElement('meta[name="twitter:card"]', 'meta', { name: 'twitter:card', content: 'summary' });
setHeadElement('meta[name="twitter:title"]', 'meta', { name: 'twitter:title', content: cleanTitle });
setHeadElement('meta[name="twitter:description"]', 'meta', { name: 'twitter:description', content: cleanDescription });
}
function markdownToDescription(md) {
const plain = stripFrontmatter(md)
.replace(/```[\s\S]*?```/g, ' ')
.split(/\n{2,}/)
.map(block => block.trim())
.find(block =>
block &&
!block.startsWith('#') &&
!block.startsWith('![') &&
!block.startsWith('|') &&
!block.startsWith('---')
);
if (!plain) return SEO_DEFAULT_DESCRIPTION;
return plain
.replace(/\[([^\]]+)\]\([^)]+\)/g, '$1')
.replace(/[*_`>#-]/g, ' ')
.replace(/\s+/g, ' ')
.trim()
.slice(0, 220);
}
function updateLandingSeo() {
setSeoMetadata({
title: SEO_DEFAULT_TITLE,
description: SEO_DEFAULT_DESCRIPTION,
url: getAbsolutePageUrl(null),
type: 'website',
});
}
function updatePageSeo(page, md) {
if (!page) return;
const title = `${page.title} | Paperclip Docs`;
setSeoMetadata({
title,
description: markdownToDescription(md),
url: getAbsolutePageUrl(page),
type: 'article',
});
}
function getLegacyRoute() {
const url = new URL(window.location.href);
return url.searchParams.get('page') || location.hash.slice(1);
const hash = location.hash.startsWith('#/') ? location.hash.slice(2) : location.hash.slice(1);
return url.searchParams.get('page') || hash;
}
function getPathRoute() {
@@ -160,6 +243,12 @@ function getCurrentRoute() {
return getPathRoute() || getLegacyRoute();
}
function getCurrentHeadingRoute() {
const pathRoute = getPathRoute();
if (!pathRoute || !location.hash || location.hash.startsWith('#/')) return null;
return normalizeRouteKey(decodeURIComponent(location.hash.slice(1)));
}
function slugifyHeadingText(text) {
return text
.trim()
@@ -400,6 +489,7 @@ function showLanding() {
document.getElementById('breadcrumb').innerHTML = '';
const basePath = APP_BASE_URL.pathname.replace(/\/$/, '');
history.replaceState(null, '', `${basePath}/`);
updateLandingSeo();
}
function showArticleView() {
document.getElementById('landing').classList.remove('is-active');
@@ -663,6 +753,7 @@ async function init() {
const pathRoute = getPathRoute();
const rawRoute = applyRedirect(pathRoute || getLegacyRoute());
const initialRoute = parseRoute(rawRoute);
if (initialRoute.page && pathRoute) initialRoute.headingId = getCurrentHeadingRoute();
const normalizedRaw = normalizeRouteKey(decodeURIComponent((rawRoute || '').trim()));
if (initialRoute.page) {
@@ -858,6 +949,7 @@ async function loadPage(file, targetHeading = null, historyMode = 'push') {
if (!res.ok) throw new Error(`HTTP ${res.status}`);
md = await res.text();
currentMarkdown = md;
updatePageSeo(page, md);
} catch (e) {
showError(`Could not load: ${file}`, e.message);
return;
@@ -1571,6 +1663,7 @@ function escapeAttr(s) {
}
window.addEventListener('hashchange', () => {
if (getPathRoute() && !location.hash.startsWith('#/')) return;
const route = parseRoute(applyRedirect(location.hash.slice(1)));
if (route.page) {
loadPage(route.page.file, route.headingId, 'replace');
@@ -1582,6 +1675,7 @@ window.addEventListener('hashchange', () => {
window.addEventListener('popstate', () => {
const route = parseRoute(applyRedirect(getCurrentRoute()));
if (route.page && getPathRoute()) route.headingId = getCurrentHeadingRoute();
if (route.page) {
loadPage(route.page.file, route.headingId, 'replace');
return;
+234 -111
View File
@@ -13,7 +13,8 @@ const sourceStylesPath = path.join(__dirname, "styles.css");
const sourceAppJsPath = path.join(__dirname, "app.js");
const sourceNavPath = path.join(__dirname, "content.json");
const screenshotsSourceDir = path.join(docsRoot, "user-guides", "screenshots");
const defaultCanonicalOrigin = "https://docs.paperclip.ing";
const defaultSiteUrl = "https://docs.paperclip.ing";
const defaultSeoDescription = "Guides, references, and walkthroughs for running Paperclip, an AI company operating system for agent teams, governance, budgets, and workflows.";
function printUsage() {
console.log(`Usage: node site/build-release.mjs [options]
@@ -22,6 +23,8 @@ Options:
--base-path <path> Public URL base path for the uploaded docs bundle.
Examples: /, /docs/, /random/paperclip-docs/, auto
Default: auto (explicit paths are recommended for deployment)
--site-url <url> Absolute public origin used for canonical URLs and sitemaps.
Default: ${defaultSiteUrl}
--out-dir <path> Output directory for the release bundle.
Default: site/release
--help Show this help text.`);
@@ -30,6 +33,7 @@ Options:
function parseArgs(argv) {
const options = {
basePath: "auto",
siteUrl: defaultSiteUrl,
outDir: path.join(__dirname, "release"),
};
@@ -57,6 +61,15 @@ function parseArgs(argv) {
index += 1;
continue;
}
if (arg === "--site-url") {
const value = argv[index + 1];
if (!value) {
throw new Error("--site-url requires a value.");
}
options.siteUrl = normalizeSiteUrl(value);
index += 1;
continue;
}
throw new Error(`Unknown argument: ${arg}`);
}
@@ -71,6 +84,16 @@ function normalizeBasePath(value) {
return withLeadingSlash.endsWith("/") ? withLeadingSlash : `${withLeadingSlash}/`;
}
function normalizeSiteUrl(value) {
const trimmed = value.trim().replace(/\/+$/, "");
if (!trimmed) throw new Error("--site-url must not be empty.");
const parsed = new URL(trimmed);
if (parsed.protocol !== "https:" && parsed.protocol !== "http:") {
throw new Error("--site-url must be an http(s) URL.");
}
return parsed.toString().replace(/\/+$/, "");
}
function toPosixPath(value) {
return value.split(path.sep).join("/");
}
@@ -416,6 +439,184 @@ function getDeploymentBasePath(basePath) {
return basePath === "auto" ? "/paperclip-docs/" : basePath;
}
function escapeHtml(value) {
return String(value)
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;");
}
function escapeXml(value) {
return String(value)
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&apos;");
}
function markdownToPlainText(markdown) {
return markdown
.replace(/```[\s\S]*?```/g, " ")
.replace(/`([^`]+)`/g, "$1")
.replace(/!\[([^\]]*)\]\([^)]+\)/g, "$1")
.replace(/\[([^\]]+)\]\([^)]+\)/g, "$1")
.replace(/<[^>]+>/g, " ")
.replace(/[*_>#|-]/g, " ")
.replace(/\s+/g, " ")
.trim();
}
function markdownDescription(markdown) {
const block = markdown
.split(/\n{2,}/)
.map((item) => item.trim())
.find((item) =>
item &&
!item.startsWith("#") &&
!item.startsWith("![") &&
!item.startsWith("|") &&
!item.startsWith("---")
);
const description = block ? markdownToPlainText(block) : defaultSeoDescription;
return description.slice(0, 220);
}
function siteUrlForPath(siteUrl, basePath, routePath = "") {
const publicBasePath = basePath === "auto" ? "/" : basePath;
const base = new URL(publicBasePath, `${siteUrl}/`);
return new URL(routePath.replace(/^\/+/, ""), base).toString();
}
function routeUrlForPage(siteUrl, basePath, page) {
return siteUrlForPath(siteUrl, basePath, `${page.slug}/`);
}
function buildJsonLd(metadata) {
const graph = metadata.page
? {
"@context": "https://schema.org",
"@type": "TechArticle",
headline: metadata.title.replace(/ \| Paperclip Docs$/, ""),
description: metadata.description,
url: metadata.url,
isPartOf: {
"@type": "WebSite",
name: "Paperclip Docs",
url: siteUrlForPath(metadata.siteUrl, metadata.basePath),
},
}
: {
"@context": "https://schema.org",
"@type": "WebSite",
name: "Paperclip Docs",
description: metadata.description,
url: metadata.url,
};
return JSON.stringify(graph);
}
function escapeScriptContent(value) {
return String(value).replace(/<\//g, "<\\/");
}
function injectSeo(html, metadata, { baseHref = null } = {}) {
const title = escapeHtml(metadata.title);
let output = html.replace(/<title>[\s\S]*?<\/title>/, `<title>${title}</title>`);
output = output.replace(/\n\s*<(?:meta|link)\b[^>]*data-seo-managed[^>]*>/g, "");
output = output.replace(/\n\s*<script\b[^>]*data-seo-managed[^>]*>[\s\S]*?<\/script>/g, "");
output = output.replace(/\n\s*<base\b[^>]*data-seo-base[^>]*>/g, "");
const tags = [
...(baseHref ? [`<base data-seo-base href="${escapeHtml(baseHref)}" />`] : []),
`<meta name="description" data-seo-managed content="${escapeHtml(metadata.description)}" />`,
`<meta name="robots" data-seo-managed content="index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1" />`,
`<link rel="canonical" data-seo-managed href="${escapeHtml(metadata.url)}" />`,
`<meta property="og:type" data-seo-managed content="${metadata.page ? "article" : "website"}" />`,
`<meta property="og:site_name" data-seo-managed content="Paperclip Docs" />`,
`<meta property="og:title" data-seo-managed content="${title}" />`,
`<meta property="og:description" data-seo-managed content="${escapeHtml(metadata.description)}" />`,
`<meta property="og:url" data-seo-managed content="${escapeHtml(metadata.url)}" />`,
`<meta name="twitter:card" data-seo-managed content="summary" />`,
`<meta name="twitter:title" data-seo-managed content="${title}" />`,
`<meta name="twitter:description" data-seo-managed content="${escapeHtml(metadata.description)}" />`,
`<script type="application/ld+json" data-seo-managed>${escapeScriptContent(buildJsonLd(metadata))}</script>`,
];
return output.replace(/(<title>[\s\S]*?<\/title>)/, `$1\n ${tags.join("\n ")}`);
}
async function pageMetadataForNav(nav, outDir, siteUrl, basePath) {
const pages = [];
for (const { page, section } of flattenNavPages(nav)) {
const releaseMarkdownPath = path.join(outDir, page.file);
const markdown = await fs.readFile(releaseMarkdownPath, "utf8");
const stats = await fs.stat(releaseMarkdownPath);
const h1 = markdown.match(/^#\s+(.+)$/m)?.[1]?.trim();
const pageTitle = page.title || h1 || "Paperclip Docs";
pages.push({
page,
sectionTitle: section.title,
title: `${pageTitle} | Paperclip Docs`,
description: markdownDescription(markdown),
url: routeUrlForPage(siteUrl, basePath, page),
lastmod: stats.mtime.toISOString().slice(0, 10),
siteUrl,
basePath,
});
}
return pages;
}
function buildSitemap({ siteUrl, basePath, pages }) {
const rootLastmod = pages
.map((page) => page.lastmod)
.sort()
.at(-1) || new Date().toISOString().slice(0, 10);
const entries = [
{ loc: siteUrlForPath(siteUrl, basePath), lastmod: rootLastmod, priority: "1.0" },
...pages.map((page) => ({ loc: page.url, lastmod: page.lastmod, priority: "0.8" })),
];
return `<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
${entries.map((entry) => ` <url>
<loc>${escapeXml(entry.loc)}</loc>
<lastmod>${escapeXml(entry.lastmod)}</lastmod>
<changefreq>weekly</changefreq>
<priority>${entry.priority}</priority>
</url>`).join("\n")}
</urlset>
`;
}
function buildRobots({ siteUrl, basePath }) {
return `User-agent: *
Allow: /
Sitemap: ${siteUrlForPath(siteUrl, basePath, "sitemap.xml")}
`;
}
function buildCloudflareRedirects() {
return `# Serve generated files first; fall back to the SPA shell for client routes.
/* /index.html 200
`;
}
function buildCloudflareHeaders() {
return `/*
X-Robots-Tag: index, follow
Referrer-Policy: strict-origin-when-cross-origin
/sitemap.xml
Content-Type: application/xml; charset=utf-8
/robots.txt
Content-Type: text/plain; charset=utf-8
`;
}
function collectMarkdownLinks(markdown) {
const links = [];
const markdownLinkRegex = /\[[^\]]+\]\(([^)\s]+(?:\s+\"[^\"]*\")?)\)/g;
@@ -530,102 +731,24 @@ location ${deploymentBasePath} {
`;
}
function getPublicBasePath(basePath) {
return getDeploymentBasePath(basePath);
}
function getPublicAssetPath(basePath, fileName) {
const publicBasePath = getPublicBasePath(basePath);
return `${publicBasePath.replace(/\/$/, "")}/${fileName}`;
}
function getCanonicalPath(basePath, route = "") {
const publicBasePath = getPublicBasePath(basePath);
const base = publicBasePath.replace(/\/$/, "");
const normalizedRoute = normalizeRouteKey(route);
return normalizedRoute ? `${base}/${normalizedRoute}` : `${base || "/"}`;
}
function getCanonicalUrl(basePath, route = "") {
return new URL(getCanonicalPath(basePath, route), defaultCanonicalOrigin).toString();
}
function escapeHtml(value) {
return String(value)
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;");
}
function stripMarkdownForDescription(markdown) {
return markdown
.replace(/```[\s\S]*?```/g, " ")
.replace(/`([^`]+)`/g, "$1")
.replace(/!\[[^\]]*\]\([^)]*\)/g, " ")
.replace(/\[([^\]]+)\]\([^)]*\)/g, "$1")
.replace(/^#{1,6}\s+/gm, "")
.replace(/^>\s?/gm, "")
.replace(/[*_~#>|]/g, " ")
.replace(/<[^>]+>/g, " ")
.replace(/\s+/g, " ")
.trim();
}
function getPageDescription(markdown) {
const paragraphs = markdown
.replace(/```[\s\S]*?```/g, "")
.split(/\n{2,}/)
.filter((paragraph) => !paragraph.trim().startsWith("#"))
.map((paragraph) => stripMarkdownForDescription(paragraph))
.filter(Boolean);
const firstParagraph = paragraphs.find((paragraph) => !paragraph.startsWith("paperclip_version:"));
return (firstParagraph || "Paperclip documentation.").slice(0, 180);
}
function getMarkdownTitle(markdown, fallbackTitle) {
const heading = markdown.match(/^#\s+(.+)$/m);
return stripMarkdownForDescription(heading?.[1] || fallbackTitle || "Paperclip Docs");
}
function rewriteIndexAssetUrls(source, basePath) {
return source
.replace('href="styles.css"', `href="${getPublicAssetPath(basePath, "styles.css")}"`)
.replace('src="app.js"', `src="${getPublicAssetPath(basePath, "app.js")}"`);
}
function setHeadMetadata(html, { title, description, canonicalUrl }) {
const metadata = [
`<title>${escapeHtml(title)}</title>`,
`<meta name="description" content="${escapeHtml(description)}" />`,
`<link rel="canonical" href="${escapeHtml(canonicalUrl)}" />`,
].join("\n ");
return html.replace(/<title>[\s\S]*?<\/title>/, metadata);
}
function renderStaticMarkdown(markdown) {
marked.setOptions({ gfm: true, breaks: false });
return marked.parse(markdown);
}
function buildStaticPageHtml(sourceIndex, page, markdown, basePath) {
const description = getPageDescription(markdown);
const title = `${getMarkdownTitle(markdown, page.title)} | Paperclip Docs`;
const canonicalUrl = getCanonicalUrl(basePath, page.slug);
function buildStaticPageHtml(sourceIndex, metadata, markdown, basePath) {
const articleHtml = renderStaticMarkdown(markdown);
return setHeadMetadata(rewriteIndexAssetUrls(sourceIndex, basePath), {
title,
description,
canonicalUrl,
})
const routeBaseHref = basePath === "auto" ? "/" : basePath;
return injectSeo(sourceIndex, metadata, { baseHref: routeBaseHref })
.replace('<section id="landing">', '<section id="landing">')
.replace('<div id="article-view">', '<div id="article-view" class="is-active">')
.replace('<div id="loading">', '<div id="loading" style="display:none">')
.replace('<article id="article" style="display:none"></article>', `<article id="article">${articleHtml}</article>`);
}
async function writeStaticRoutePages({ outDir, sourceIndex, releaseNav, markdownBodiesByFile, basePath }) {
for (const { page } of flattenNavPages(releaseNav)) {
async function writeStaticRoutePages({ outDir, sourceIndex, pages, markdownBodiesByFile, basePath }) {
for (const metadata of pages) {
const { page } = metadata;
const markdown = markdownBodiesByFile.get(page.file);
if (!markdown) continue;
const routePath = path.join(outDir, ...page.slug.split("/"), "index.html");
@@ -633,30 +756,10 @@ async function writeStaticRoutePages({ outDir, sourceIndex, releaseNav, markdown
throw new Error(`Refusing to write route outside release directory: ${page.slug}`);
}
await ensureDir(path.dirname(routePath));
await fs.writeFile(routePath, buildStaticPageHtml(sourceIndex, page, markdown, basePath));
await fs.writeFile(routePath, buildStaticPageHtml(sourceIndex, metadata, markdown, basePath));
}
}
function buildSitemap(releaseNav, basePath) {
const urls = [getCanonicalUrl(basePath)];
for (const { page } of flattenNavPages(releaseNav)) {
urls.push(getCanonicalUrl(basePath, page.slug));
}
return `<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
${urls.map((url) => ` <url><loc>${escapeHtml(url)}</loc></url>`).join("\n")}
</urlset>
`;
}
function buildRobots(basePath) {
return `User-agent: *
Allow: /
Sitemap: ${getCanonicalUrl(basePath, "sitemap.xml")}
`;
}
function buildDeployGuide(basePath) {
const deploymentBasePath = getDeploymentBasePath(basePath);
const basePathGuidance = basePath === "auto"
@@ -711,15 +814,13 @@ async function main() {
const sourceStyles = await fs.readFile(sourceStylesPath, "utf8");
const sourceAppJs = await fs.readFile(sourceAppJsPath, "utf8");
const releaseAppJs = rewriteAppJs(sourceAppJs, options.basePath);
const releaseIndex = rewriteIndexAssetUrls(sourceIndex, options.basePath);
await fs.writeFile(path.join(options.outDir, "index.html"), releaseIndex);
await fs.writeFile(path.join(options.outDir, "styles.css"), sourceStyles);
await fs.writeFile(path.join(options.outDir, "app.js"), releaseAppJs);
await fs.writeFile(path.join(options.outDir, ".htaccess"), buildHtaccess(options.basePath));
await fs.writeFile(path.join(options.outDir, "nginx.conf.example"), buildNginxConfig(options.basePath));
await fs.writeFile(path.join(options.outDir, "DEPLOY.md"), buildDeployGuide(options.basePath));
await fs.writeFile(path.join(options.outDir, "sitemap.xml"), buildSitemap(releaseNav, options.basePath));
await fs.writeFile(path.join(options.outDir, "robots.txt"), buildRobots(options.basePath));
await fs.writeFile(path.join(options.outDir, "_redirects"), buildCloudflareRedirects());
await fs.writeFile(path.join(options.outDir, "_headers"), buildCloudflareHeaders());
// Copy markdown files, stripping YAML frontmatter, and collect per-file
// frontmatter to surface via content.json (keyed by repo-relative path).
@@ -744,14 +845,34 @@ async function main() {
if (fm) page.frontmatter = fm;
}
await fs.writeFile(path.join(options.outDir, "content.json"), `${JSON.stringify(releaseNav, null, 2)}\n`);
const pageMetadata = await pageMetadataForNav(releaseNav, options.outDir, options.siteUrl, options.basePath);
const rootMetadata = {
title: "Paperclip Docs",
description: defaultSeoDescription,
url: siteUrlForPath(options.siteUrl, options.basePath),
siteUrl: options.siteUrl,
basePath: options.basePath,
};
await fs.writeFile(path.join(options.outDir, "index.html"), injectSeo(sourceIndex, rootMetadata));
await writeStaticRoutePages({
outDir: options.outDir,
sourceIndex,
releaseNav,
pages: pageMetadata,
markdownBodiesByFile,
basePath: options.basePath,
});
await fs.writeFile(path.join(options.outDir, "sitemap.xml"), buildSitemap({
siteUrl: options.siteUrl,
basePath: options.basePath,
pages: pageMetadata,
}));
await fs.writeFile(path.join(options.outDir, "robots.txt"), buildRobots({
siteUrl: options.siteUrl,
basePath: options.basePath,
}));
if (await pathExists(screenshotsSourceDir)) {
const screenshotTargetDir = path.join(options.outDir, "user-guides", "screenshots");
await copyDirRecursive(screenshotsSourceDir, screenshotTargetDir);
@@ -771,7 +892,9 @@ async function main() {
console.log(`Release bundle written to ${path.relative(process.cwd(), options.outDir)}`);
console.log(`Base path: ${options.basePath}`);
console.log(`Site URL: ${options.siteUrl}`);
console.log(`Copied ${sortedMarkdownFiles.length} markdown files.`);
console.log(`Generated ${pageMetadata.length} crawlable route pages plus sitemap.xml and robots.txt.`);
if (await pathExists(screenshotsSourceDir)) {
console.log("Copied screenshot assets.");
}
+11
View File
@@ -4,6 +4,17 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<title>Paperclip Docs</title>
<meta name="description" data-seo-managed content="Guides, references, and walkthroughs for running Paperclip, an AI company operating system for agent teams, governance, budgets, and workflows." />
<meta name="robots" data-seo-managed content="index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1" />
<link rel="canonical" data-seo-managed href="https://docs.paperclip.ing/" />
<meta property="og:type" data-seo-managed content="website" />
<meta property="og:site_name" data-seo-managed content="Paperclip Docs" />
<meta property="og:title" data-seo-managed content="Paperclip Docs" />
<meta property="og:description" data-seo-managed content="Guides, references, and walkthroughs for running Paperclip, an AI company operating system for agent teams, governance, budgets, and workflows." />
<meta property="og:url" data-seo-managed content="https://docs.paperclip.ing/" />
<meta name="twitter:card" data-seo-managed content="summary" />
<meta name="twitter:title" data-seo-managed content="Paperclip Docs" />
<meta name="twitter:description" data-seo-managed content="Guides, references, and walkthroughs for running Paperclip, an AI company operating system for agent teams, governance, budgets, and workflows." />
<!-- Google tag (gtag.js) -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-0JGE5X0HMM"></script>
<script>