Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 16 additions & 12 deletions scripts/audit-seo-geo.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -245,20 +245,24 @@ for (const [relativePath, html] of pages) {
);
}

if (loop.sourceUrl) {
if (!html.includes(`href="${loop.sourceUrl}"`) || !html.includes("isBasedOn")) {
addFinding(
"high",
"source citations",
"Contributed loop is missing its visible source link or isBasedOn schema.",
relativePath,
);
}
} else if (!pageText.includes(`Contributed by ${loop.author}`)) {
if (!pageText.includes(`Contributed by ${loop.author}`)) {
addFinding(
"medium",
"source citations",
"Original loop is missing visible contributor attribution.",
"contributor attribution",
"Published loop is missing visible contributor attribution.",
relativePath,
);
}

if (
html.includes('class="detail-source-link"') ||
html.includes("isBasedOn") ||
(loop.sourceUrl && html.includes(loop.sourceUrl))
) {
addFinding(
"high",
"source privacy",
"Published loop exposes a source link.",
relativePath,
);
}
Expand Down
109 changes: 99 additions & 10 deletions scripts/build-loop-pages.mjs
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { mkdir, rm, writeFile } from "node:fs/promises";
import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
import { fileURLToPath } from "node:url";
import path from "node:path";

import { escapeJsonForHtmlScript } from "./html-script-utils.mjs";
import { loops, site } from "./loop-data.mjs";
import { getLoopCategory, loops, site } from "./loop-data.mjs";

const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
const outputRoot = path.join(root, "site");
Expand Down Expand Up @@ -39,6 +39,46 @@ function socialImageUrl(loop) {
return `${site.baseUrl}assets/social/${loop.slug}-${site.socialImageVersion}.${site.socialImageExtension}`;
}

async function syncHomepagePublicationDates() {
const homepagePath = path.join(outputRoot, "index.html");
let homepage = await readFile(homepagePath, "utf8");

for (const loop of loops) {
const loopHref = `href="./loops/${loop.slug}/"`;
const hrefIndex = homepage.indexOf(loopHref);
const rowStart = homepage.lastIndexOf("<tr", hrefIndex);
const rowTagEnd = homepage.indexOf(">", rowStart);

if (hrefIndex < 0 || rowStart < 0 || rowTagEnd < 0) {
throw new Error(`Could not find the homepage row for ${loop.slug}.`);
}

const rowTag = homepage.slice(rowStart, rowTagEnd + 1);
const categoryAttribute = `data-category="${getLoopCategory(loop).slug}"`;
const publishedAttribute = `data-published="${loop.published}"`;
const normalizedRowTag = rowTag.replace(
/\n\s*data-published="[^"]*"/,
"",
);

if (!normalizedRowTag.includes(categoryAttribute)) {
throw new Error(`Homepage category drift for ${loop.slug}.`);
}

const updatedRowTag = normalizedRowTag.replace(
categoryAttribute,
`${categoryAttribute}\n ${publishedAttribute}`,
);

homepage =
homepage.slice(0, rowStart) +
updatedRowTag +
homepage.slice(rowTagEnd + 1);
}

await writeFile(homepagePath, homepage);
}

function shareActions(loop, url) {
const postText = `Try "${loop.title}" from the Loop Library: ${loop.summary}`;

Expand Down Expand Up @@ -72,6 +112,58 @@ function relatedLinks(loop) {
.join("");
}

function playbookList(items) {
return items
.map((item) => ` <li>${escapeHtml(item)}</li>`)
.join("\n");
}

function contributorPlaybook(loop) {
const playbook = loop.contributorPlaybook;

if (!playbook) {
return "";
}

return `
<details class="detail-more contributor-playbook">
<summary>
<span>Contributor playbook</span>
<small>Boundaries, required outputs, implementation guidance, and reviewer handoff</small>
</summary>

<div class="detail-more-body contributor-playbook-body">
<section aria-labelledby="contributor-when-not-to-use">
<h2 id="contributor-when-not-to-use">Do not use this when</h2>
<ul class="contributor-playbook-list">
${playbookList(playbook.whenNotToUse)}
</ul>
</section>

<section aria-labelledby="contributor-expected-outputs">
<h2 id="contributor-expected-outputs">Required outputs</h2>
<ul class="contributor-playbook-list contributor-playbook-list--grid">
${playbookList(playbook.expectedOutputs)}
</ul>
</section>

<section aria-labelledby="contributor-implementation-guidance">
<h2 id="contributor-implementation-guidance">Match the method to the artifact</h2>
<ul class="contributor-playbook-list">
${playbookList(playbook.implementationGuidance)}
</ul>
</section>

<section aria-labelledby="contributor-reviewer-handoff">
<h2 id="contributor-reviewer-handoff">Reviewer handoff</h2>
<ul class="contributor-playbook-list">
${playbookList(playbook.reviewerHandoff)}
</ul>
</section>
</div>
</details>`;
}

function hereNowCredit(assetPath, modifier) {
return `<a
class="here-now-credit here-now-credit--${modifier}"
Expand Down Expand Up @@ -142,7 +234,6 @@ function structuredData(loop) {
dateModified: loop.modified,
articleSection: loop.categoryLabel,
keywords: loop.keywords,
...(loop.sourceUrl ? { isBasedOn: loop.sourceUrl } : {}),
image: {
"@type": "ImageObject",
url: imageUrl,
Expand Down Expand Up @@ -248,11 +339,11 @@ function renderLoopPage(loop) {
<link rel="alternate" type="text/plain" title="${escapeHtml(site.name)} plain-text catalog" href="${escapeHtml(site.baseUrl)}catalog.txt" />
<link rel="help" href="${escapeHtml(site.baseUrl)}agents/" />
<link rel="icon" type="image/png" href="../../assets/favicon.png" />
<link rel="stylesheet" href="../../styles.css?v=20260620-primary-nav" />
<link rel="stylesheet" href="../../styles.css?v=20260620-newest-first" />
<script type="application/ld+json">
${structuredData(loop)}
</script>
<script src="../../script.js?v=20260620-primary-nav" defer></script>
<script src="../../script.js?v=20260620-newest-first" defer></script>
<title>${escapeHtml(loop.seoTitle)}</title>
</head>
<body>
Expand Down Expand Up @@ -313,11 +404,7 @@ ${structuredData(loop)}
<h1>${escapeHtml(loop.title)}</h1>
<p class="detail-lede">${escapeHtml(loop.description)}</p>
<p class="detail-byline">
Contributed by <strong>${escapeHtml(loop.author)}</strong>${
loop.sourceUrl
? ` · <a class="detail-source-link" href="${escapeHtml(loop.sourceUrl)}" target="_blank" rel="noopener noreferrer">Source</a>`
: ""
}
Contributed by <strong>${escapeHtml(loop.author)}</strong>
</p>
${shareActions(loop, url)}
</header>
Expand Down Expand Up @@ -392,6 +479,7 @@ ${relatedLinks(loop)}
</nav>
</div>
</details>
${contributorPlaybook(loop)}
</div>
</article>
</main>
Expand Down Expand Up @@ -484,6 +572,7 @@ ${loops
`;
}

await syncHomepagePublicationDates();
await rm(path.join(outputRoot, "loops"), { recursive: true, force: true });

for (const loop of loops) {
Expand Down
4 changes: 3 additions & 1 deletion scripts/build-skill-catalog.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,9 @@ export function renderCatalogJson() {
steps: loop.steps,
why: loop.why,
implementationNote: loop.note,
...(loop.contributorPlaybook
? { contributorPlaybook: loop.contributorPlaybook }
: {}),
keywords: loop.keywords,
related: loop.related.map((slug) => {
const relatedLoop = loopBySlug.get(slug);
Expand All @@ -123,7 +126,6 @@ export function renderCatalogJson() {
url: `${site.baseUrl}loops/${slug}/`,
};
}),
...(loop.sourceUrl ? { sourceUrl: loop.sourceUrl } : {}),
};
}),
};
Expand Down
55 changes: 35 additions & 20 deletions scripts/check.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -475,6 +475,10 @@ for (const [index, loop] of loops.entries()) {
assert.deepEqual(catalogLoop.steps, loop.steps);
assert.equal(catalogLoop.why, loop.why);
assert.equal(catalogLoop.implementationNote, loop.note);
assert.deepEqual(
catalogLoop.contributorPlaybook,
loop.contributorPlaybook,
);
assert.deepEqual(catalogLoop.keywords, loop.keywords);
assert.deepEqual(
catalogLoop.related.map(({ slug }) => slug),
Expand All @@ -490,7 +494,7 @@ for (const [index, loop] of loops.entries()) {
(relatedSlug) => `${siteMeta.baseUrl}loops/${relatedSlug}/`,
),
);
assert.equal(catalogLoop.sourceUrl, loop.sourceUrl);
assert.equal(catalogLoop.sourceUrl, undefined);
assert(loop.related.every((relatedSlug) => slugs.has(relatedSlug)));
assert(html.includes(loop.title));
assert(normalizedHomepageRow.includes(loop.prompt));
Expand All @@ -501,6 +505,7 @@ for (const [index, loop] of loops.entries()) {
);
assert(!homepageRow.includes('class="cell-number"'));
assert(homepageRow.includes(`data-category="${category.slug}"`));
assert(homepageRow.includes(`data-published="${loop.published}"`));
assert(
homepageRow.includes(
`<span class="loop-category">${category.label}</span>`,
Expand Down Expand Up @@ -551,8 +556,8 @@ for (const [index, loop] of loops.entries()) {
),
);
assert(page.includes(`rel="help" href="${siteMeta.baseUrl}agents/"`));
assert(page.includes("../../styles.css?v=20260620-primary-nav"));
assert(page.includes("../../script.js?v=20260620-primary-nav"));
assert(page.includes("../../styles.css?v=20260620-newest-first"));
assert(page.includes("../../script.js?v=20260620-newest-first"));
assert(page.includes(`<meta property="og:image" content="${imageUrl}"`));
assert(page.includes(`<meta property="og:image:secure_url" content="${imageUrl}"`));
assert(page.includes(`<meta property="og:image:type" content="${siteMeta.socialImageMimeType}"`));
Expand Down Expand Up @@ -616,6 +621,22 @@ for (const [index, loop] of loops.entries()) {
assert(page.includes("How to run it"));
assert(page.includes("Why it works"));
assert(page.includes("Implementation note"));
if (loop.contributorPlaybook) {
assert(page.includes('class="detail-more contributor-playbook"'));
assert(page.includes("Contributor playbook"));
assert(page.includes("Do not use this when"));
assert(page.includes("Required outputs"));
assert(page.includes("Match the method to the artifact"));
assert(page.includes("Reviewer handoff"));
assert(
Object.values(loop.contributorPlaybook)
.flat()
.every((item) => page.includes(escapeHtml(item))),
);
} else {
assert.equal(catalogLoop.contributorPlaybook, undefined);
assert(!page.includes('class="detail-more contributor-playbook"'));
}
assert(!page.includes("<h2>Topics</h2>"));
assert(page.includes("Related loops"));
assert(!page.includes("<dt>Type</dt>"));
Expand Down Expand Up @@ -695,17 +716,9 @@ for (const [index, loop] of loops.entries()) {
escapeHtml(loopBySlug.get(relatedSlug).title),
),
);
if (loop.sourceUrl) {
assert.equal(article.isBasedOn, loop.sourceUrl);
assert(
page.includes(
`<a class="detail-source-link" href="${escapeHtml(loop.sourceUrl)}" target="_blank" rel="noopener noreferrer">Source</a>`,
),
);
} else {
assert.equal(article.isBasedOn, undefined);
assert(!page.includes('class="detail-source-link"'));
}
assert.equal(article.isBasedOn, undefined);
assert(!page.includes('class="detail-source-link"'));
assert(!loop.sourceUrl || !page.includes(loop.sourceUrl));
assert(sitemap.includes(`<loc>${url}</loc>`));
assert(sitemap.includes(`<lastmod>${loop.modified}</lastmod>`));
assert(feed.includes(`<id>${url}</id>`));
Expand Down Expand Up @@ -815,8 +828,10 @@ assert(!html.includes('data-type='));
assert(!html.includes('class="cell-type"'));
assert(!html.includes("type-badge"));
assert(!html.includes('<th scope="col">Type</th>'));
assert(html.includes("./styles.css?v=20260620-primary-nav"));
assert(html.includes("./script.js?v=20260620-primary-nav"));
assert(html.includes("./styles.css?v=20260620-newest-first"));
assert(html.includes("./script.js?v=20260620-newest-first"));
assert(script.includes("const publishedDifference = b.dataset.published.localeCompare("));
assert(script.includes("return loopRowPositions.get(b) - loopRowPositions.get(a);"));
const homepagePostText =
"Find Loops and create your own - Loop Library";
assert(html.includes('class="share-actions" aria-label="Share Loop Library"'));
Expand Down Expand Up @@ -877,8 +892,8 @@ assert.equal(
(learnHtml.match(/href="https:\/\/here\.now\/r\/signals"/g) || []).length,
2,
);
assert(learnHtml.includes("../styles.css?v=20260620-article-layout"));
assert(learnHtml.includes("../script.js?v=20260620-primary-nav"));
assert(learnHtml.includes("../styles.css?v=20260620-newest-first"));
assert(learnHtml.includes("../script.js?v=20260620-newest-first"));
assert(learnHtml.includes("How agent loops work"));
assert(learnHtml.includes('<meta name="robots" content="index, follow"'));
assert(learnHtml.includes("What makes a loop useful"));
Expand Down Expand Up @@ -926,8 +941,8 @@ assert(agentHtml.includes("npx skills add Forward-Future/loop-library --skill lo
assert(agentHtml.includes('<meta name="robots" content="index, follow"'));
assert(agentHtml.includes(`href="${siteMeta.baseUrl}catalog.json"`));
assert(agentHtml.includes(`href="${siteMeta.baseUrl}llms.txt"`));
assert(agentHtml.includes("../styles.css?v=20260620-article-layout"));
assert(agentHtml.includes("../script.js?v=20260620-primary-nav"));
assert(agentHtml.includes("../styles.css?v=20260620-newest-first"));
assert(agentHtml.includes("../script.js?v=20260620-newest-first"));
assert(html.includes("Repeatable AI Agent Workflows"));
assert(html.includes('rel="sitemap"'));
assert(html.includes(`href="${siteMeta.baseUrl}catalog.json"`));
Expand Down
Loading
Loading