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
7 changes: 5 additions & 2 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,11 @@ <h2 class="visually-hidden">Seed stage venture capital firm investing in deep te

<!-- The block below is generated by scripts/build-pages.js from
config/*.js — edit those, not this. Most LLM crawlers do not run
JavaScript, so without it they see an empty terminal div. Browsers
with JS never render it, so the terminal is visually unchanged. -->
JavaScript, so without it they see an empty terminal div. It is
offscreen rather than inside <noscript>: extraction pipelines strip
<noscript> as non-content, and Googlebot indexes the rendered DOM,
which drops it once JS runs. Sighted visitors never see it either
way, so the terminal is visually unchanged. -->
<!-- BEGIN generated-index -->
<!-- END generated-index -->

Expand Down
6 changes: 5 additions & 1 deletion js/ascii-art.js
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,11 @@ function _loadArt(id, ratio, scale, ext, inverse, callback) {
}
);
} else {
div.innerText = `[ Photo: ${document.location.href}images/${id}.${ext} ]`;
// Resolve against the document rather than concatenating onto href, which
// glues the path onto any fragment already there: arriving via a deep
// link like /#whois-avidan produced ".../#whois-avidanimages/avidan.png".
const photo = new URL(`images/${id}.${ext}`, document.baseURI);
div.innerText = `[ Photo: ${photo.href} ]`;
div.dataset.loaded = "true";
if (callback) {
callback();
Expand Down
70 changes: 35 additions & 35 deletions scripts/build-pages.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
//
// config/*.js is the ONLY source of truth. Nothing here is hand-maintained:
// editing a description regenerates its page, both indexes, the sitemap, the
// llms files, and the <noscript> block in index.html.
// llms files, and the crawlable index block in index.html.
//
// Everything is written into dist/, which is the Netlify publish directory and
// is gitignored. Generated files are never committed, so there is nothing that
Expand All @@ -28,8 +28,8 @@ const OG_IMAGE = `${ORIGIN}/images/og-image.png`;

// Sentinel comment pairs in index.html; everything between each pair is
// regenerated from config/*.js on every build.
const NOSCRIPT_BEGIN = "<!-- BEGIN generated-index -->";
const NOSCRIPT_END = "<!-- END generated-index -->";
const INDEX_BEGIN = "<!-- BEGIN generated-index -->";
const INDEX_END = "<!-- END generated-index -->";
const JSONLD_BEGIN = "<!-- BEGIN generated-jsonld -->";
const JSONLD_END = "<!-- END generated-jsonld -->";

Expand Down Expand Up @@ -848,46 +848,51 @@ function renderLlmsFull({ firm, portfolio, team, jobs }) {
return lines.join("\n");
}

// ── index.html <noscript> injection ───────────────────────────────────────────
// ── index.html crawlable index injection ─────────────────────────────────────

// Most LLM crawlers do not execute JavaScript, so on the homepage they see an
// empty <div id="terminal">. This block gives them the whole map. JS-enabled
// visitors never render it, so the terminal is visually untouched.
function renderNoscriptIndex({ firm, portfolio, team }) {
// empty <div id="terminal">. This block gives them the whole map.
//
// It is a visually-hidden div rather than <noscript>, which is what it used to
// be. Two reasons. Extraction pipelines routinely strip <noscript> as
// non-content, which would leave the homepage looking empty to exactly the
// crawlers this exists for. And Googlebot indexes the rendered DOM, which omits
// <noscript> entirely once JS runs. A clipped div is present in both the raw
// HTML and the rendered DOM, and unlike display:none it is not discounted.
// Sighted visitors never see it, so the terminal is visually untouched.
function renderTextIndex({ firm, portfolio, team }) {
const companies = Object.keys(portfolio)
.map(
(slug) =>
` <li><a href="/portfolio/${esc(slug)}/">${esc(portfolio[slug].name)}</a> — ${esc(portfolio[slug].description)}</li>`
` <li><a href="/portfolio/${esc(slug)}/">${esc(portfolio[slug].name)}</a> — ${esc(portfolio[slug].description)}</li>`
)
.join("\n");
const people = Object.keys(team)
.map(
(slug) =>
` <li><a href="/team/${esc(slug)}/">${esc(team[slug].name)}</a> — ${esc(team[slug].title)}</li>`
` <li><a href="/team/${esc(slug)}/">${esc(team[slug].name)}</a> — ${esc(team[slug].title)}</li>`
)
.join("\n");

return `${NOSCRIPT_BEGIN}
<noscript>
<div id="text-version">
<p>${esc(firm.blurb)}</p>
<p>
<a href="/about/">About</a> ·
<a href="/portfolio/">Portfolio</a> ·
<a href="/team/">Team</a> ·
<a href="/jobs/">Jobs</a>
</p>
<h2>Portfolio</h2>
<ul>
return `${INDEX_BEGIN}
<div id="text-version" class="visually-hidden">
<p>${esc(firm.blurb)}</p>
<p>
<a href="/about/">About</a> ·
<a href="/portfolio/">Portfolio</a> ·
<a href="/team/">Team</a> ·
<a href="/jobs/">Jobs</a>
</p>
<h2>Portfolio</h2>
<ul>
${companies}
</ul>
<h2>Team</h2>
<ul>
</ul>
<h2>Team</h2>
<ul>
${people}
</ul>
</div>
</noscript>
${NOSCRIPT_END}`;
</ul>
</div>
${INDEX_END}`;
}

// Every generated page points `isPartOf` at #website and `funder`/`worksFor` at
Expand Down Expand Up @@ -929,12 +934,7 @@ function injectBetween(html, beginMarker, endMarker, block) {
function renderIndexHtml(config) {
let html = readText("index.html");
html = injectBetween(html, JSONLD_BEGIN, JSONLD_END, renderHomeJsonLd(config));
html = injectBetween(
html,
NOSCRIPT_BEGIN,
NOSCRIPT_END,
renderNoscriptIndex(config)
);
html = injectBetween(html, INDEX_BEGIN, INDEX_END, renderTextIndex(config));
return html;
}

Expand Down Expand Up @@ -996,7 +996,7 @@ module.exports = {
outDir,
renderCompany,
renderIndexHtml,
renderNoscriptIndex,
renderTextIndex,
renderPerson,
writePages,
};
Expand Down
25 changes: 21 additions & 4 deletions tests/build-pages.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -643,19 +643,36 @@ describe("index.html", () => {
expect(h1.className).toBe("visually-hidden");
});

it("lists every company and person in the noscript block", () => {
const noscript = html.slice(
it("lists every company and person in the crawlable index block", () => {
const block = html.slice(
html.indexOf("BEGIN generated-index"),
html.indexOf("END generated-index")
);
for (const slug of Object.keys(config.portfolio)) {
expect(noscript).toContain(`href="/portfolio/${slug}/"`);
expect(block).toContain(`href="/portfolio/${slug}/"`);
}
for (const slug of Object.keys(config.team)) {
expect(noscript).toContain(`href="/team/${slug}/"`);
expect(block).toContain(`href="/team/${slug}/"`);
}
});

it("keeps the crawlable index reachable to text extractors", () => {
// Not <noscript>: extraction pipelines strip it as non-content, and
// Googlebot indexes the rendered DOM, which drops it once JS runs. Not
// display:none either, which search engines discount. Offscreen clipping
// survives both while staying invisible to sighted visitors.
const block = html.slice(
html.indexOf("BEGIN generated-index"),
html.indexOf("END generated-index")
);
expect(block).not.toContain("<noscript");
expect(block).not.toMatch(/display:\s*none/);

const textVersion = doc.querySelector("#text-version");
expect(textVersion).not.toBeNull();
expect(textVersion.className).toBe("visually-hidden");
});

it("keeps the terminal markup untouched", () => {
expect(doc.querySelector("#terminal")).not.toBeNull();
expect(doc.querySelector("#aa-all")).not.toBeNull();
Expand Down
Loading