.
*
* Why this exists: everything on the site is drawn by React after ~800 KB of JS has
* downloaded and run, so until now a crawler fetching / got a full of correct
* metadata wrapped around an empty div. Search Console shows exactly what that costs —
* the site ranks for the spelling of its domain (chatbit, chatyourbit, keepbit) rather
* than for anything it does, and twelve of the thirteen localized pages have never been
* shown to anyone, because a page whose only difference from the English one is its
*
tags gives Google no reason to swap it in.
*
* So each page is built carrying its own text: the same strings the app renders, from
* the same locales/
.json, in real headings and paragraphs.
*
* It is never shown to a visitor who has JavaScript. The block defaults to display:none
* and only a stylesheet turns it back on, which means a browser that is going
* to run the app never paints it for even one frame — an earlier version left it visible
* until React mounted, and what that produced was a plain wall of English text on screen
* for as long as the bundles took to arrive. Doing this in CSS rather than with a script
* is deliberate: a script would have to be inline to beat first paint, and the page's CSP
* allows no inline script.
*
* What a visitor does see is the mark, and only the mark. Hiding the text left nothing on
* screen at all until React mounted, and "nothing" is not a first paint: Lighthouse put
* First Contentful Paint at 6.3 s on mobile, because the first contentful thing was the
* app itself. The shield below is the same one the header shows, inline so it costs no
* request, and it is contentful the moment the stylesheet lands. React empties the
* container on mount, so it leaves without being told to.
*
* What still reads it: every crawler that does not execute JavaScript, which is Bing,
* Yandex, DuckDuckGo, the social unfurlers and the AI crawlers, plus Google's own first
* pass over the raw HTML before it queues the page for rendering. Google's renderer sees
* the same text a second time anyway, because the app itself draws these sections once
* it mounts. Nothing here is hidden from a reader that is not also shown to them: the
* block is a fallback for clients that cannot run the app, not a second version of the
* page. Styling is inline and self-contained so it is carried away with the markup when
* React empties the container, instead of lingering in the stylesheet.
*/
/** Escape for HTML *text*, not attributes — build-i18n's attr() covers those. */
const esc = (value) =>
String(value)
.replace(/&/g, '&')
.replace(//g, '>');
// Hidden first, shown only under . Everything after that is scoped under
// .sb-pre so nothing here can reach the app, and the whole subtree — rules included —
// is removed when React empties the container. Spacing is logical (-inline-) so the
// Arabic, Hebrew, Farsi and Urdu builds mirror correctly off the dir already on .
const STYLE = `
`;
/**
* Build the block for one locale.
*
* Strings are read through a lookup that falls back to the default locale and then to
* nothing: a locale that gains a key before its translation lands should render one
* English line, not an empty heading or the literal key.
*/
function prerenderShell(site, code, docs = []) {
const ui = site.byCode[code].ui || {};
const fallback = site.byCode[site.defaultLocale].ui || {};
const s = (key) => {
const value = ui[key] !== undefined ? ui[key] : fallback[key];
return typeof value === 'string' ? value : '';
};
const list = (key) => {
const value = ui[key] !== undefined ? ui[key] : fallback[key];
return Array.isArray(value) ? value : [];
};
const out = [];
// Hero. One per page, carrying the product's own claim rather than its name —
// the name is already in , the claim is what a category query matches.
const headline = [s('hero.headlineTop'), s('hero.headlineBottom')].filter(Boolean);
out.push(
' '
);
// What the product is, in its own terms. These five cards are the densest piece of
// vocabulary the site owns — ECDH, DTLS, forward secrecy, packet padding — and the
// only place a category search has anything to match on.
const cards = ['s1', 's2', 's3', 's4', 's5']
.map((id) => {
const title = [s(`unique.${id}.titleTop`), s(`unique.${id}.titleBottom`)].filter(Boolean).join(' ');
const desc = s(`unique.${id}.desc`);
if (!title && !desc) return '';
const tags = list(`unique.${id}.tags`);
return [
' ',
title ? ` ${esc(title)} ` : '',
desc ? ` ${esc(desc)}
` : '',
tags.length
? ` `
: '',
' ',
].filter(Boolean).join('\n');
})
.filter(Boolean);
if (cards.length) {
out.push(
' ',
` ${esc(s('unique.eyebrow'))}
`,
` ${esc(s('unique.heading'))} `,
' ',
' '
);
}
// The roadmap is a genuine timeline, so it is a numbered list and nothing else is.
// Feature bullets are left to the app: thirteen releases of them would triple this
// block's weight to say what the titles already say.
const releases = [];
for (let i = 1; i <= 40; i += 1) {
const title = s(`roadmap.r${i}.title`);
if (!title) break;
const when = s(`roadmap.r${i}.date`);
const sub = s(`roadmap.r${i}.sub`);
releases.push(
[
' ',
` ${esc(title)} `,
when ? ` ${esc(when)} ` : '',
sub ? ` ${esc(sub)}
` : '',
' ',
].filter(Boolean).join('\n')
);
}
if (releases.length) {
out.push(
' ',
` ${esc(s('roadmap.eyebrow'))}
`,
` ${esc(s('roadmap.heading'))} `,
` ${esc(s('roadmap.subheading'))}
`,
' ',
...releases,
' ',
' '
);
}
// Internal links to the documentation. A sitemap tells Google the pages exist; a
// link from the site's most-crawled page is what actually gets them fetched and
// gives them anchor text to be ranked on. The whole block is marked lang="en"
// because the documents are English on every locale — an untagged English list
// inside a German page is a quality signal working against itself.
if (docs.length) {
out.push(
' ',
' Documentation
',
' ',
' '
);
}
// One outbound link, to the repository. It is the site's only real corroboration —
// the thing a reader checks when a privacy claim needs backing.
out.push(
' '
);
// The mark, and nothing else — logo/securebit-mark.svg, the same one the header
// shows, inlined so it costs no request and is contentful the moment the stylesheet
// lands. Its gradient ids are prefixed here: an inline puts them in the page's
// id namespace, and the app has SVGs of its own.
// aria-hidden because it says nothing a screen reader needs; the page it stands in
// for announces itself once it is there.
const boot = ``;
return `${STYLE}
${boot}
`;
}
module.exports = { prerenderShell };