openvibers/openvibe.domains Commits

Give every indexable Domains page its canonical URL and share card (#50)

OP OpenVibers committed 12 days ago · 480cb2c76f · 2 files, +26 −10

Changes

ddiff --git a/server/views/index.js b/server/views/index.js
iindex 6a4f7a7..8c89c23 100644
---- a/server/views/index.js
++++ b/server/views/index.js
@@@ -135,7 +135,7 @@ function totalPicture(r) {
+ `<div class="is-total"><small>${esc(r.display || dot(r.tld))} for 3 years</small><b>${money(r.y3, r.currency)}</b></div></div>`;
}
--function home({ endings, lists } = {}) {
++function home({ endings, lists, canonical } = {}) {
// The home's board shows the endings people mean (generic and country codes), not second-level ones like
// augustow.pl or punycode endings, which are on /tld.
const rows = priceRows((endings && endings.tlds) || []).filter((r) => !r.currency_other && r.y3 != null && !String(r.tld).includes('.') && !/^xn--/.test(String(r.tld)));
@@@ -187,7 +187,7 @@ function home({ endings, lists } = {}) {
// the room is one or two screens. Both stay in the page for the crawlers and open where there is room.
body = foldSection(body, 'class="sc-faq"', 'faq-fold');
body = foldSection(body, 'class="sc-code"', 'dev-fold');
-- return layout({ title: null, canonical: undefined, body: `<div class="dm-wrap">${body}</div>`, active: '/', jsonLd: [{ '@context': 'https://schema.org', '@type': 'WebSite', name: NAME, url: 'https://openvibe.domains/', potentialAction: { '@type': 'SearchAction', target: 'https://openvibe.domains/search?q={name}', 'query-input': 'required name=name' } }] });
++ return layout({ title: null, canonical, body: `<div class="dm-wrap">${body}</div>`, active: '/', jsonLd: [{ '@context': 'https://schema.org', '@type': 'WebSite', name: NAME, url: 'https://openvibe.domains/', potentialAction: { '@type': 'SearchAction', target: 'https://openvibe.domains/search?q={name}', 'query-input': 'required name=name' } }] });
}
/** A table of priced rows: ending or registrar first, then the year totals, sorted by 3 years. */
@@@ -227,11 +227,11 @@ ${priceTable(rows, { by: 'ending', label: `${label} in every ending`, check: nam
return layout({ title: `${full}: price and availability`, description: `${full}: whether it is free, and what every ending costs over 1, 3 and 5 years.`, robots: 'noindex, follow', body: `<div class="dm-wrap">${body}</div>`, active: '/' });
}
--function endings({ endings: list } = {}) {
++function endings({ endings: list, canonical } = {}) {
const rows = priceRows((list && list.tlds) || []);
const body = `<header class="dm-page-h"><h1>Every ending</h1><p class="dm-lede">All the endings OpenVibe prices, cheapest to keep over three years first.${latest(rows) ? ` Prices checked ${esc(day(latest(rows)))}.` : ''}</p>${searchBox()}</header>
${rows.length ? priceTable(rows, { by: 'ending', label: 'Every ending' }) : kit.empty({ title: 'No prices yet', text: 'The price lists are being read. Try again in a few minutes.' })}`;
-- return layout({ title: 'Every ending, cheapest to keep', description: 'Every domain ending OpenVibe prices, sorted by what three years really cost.', body: `<div class="dm-wrap">${body}</div>`, active: '/tld' });
++ return layout({ title: 'Every ending, cheapest to keep', description: 'Every domain ending OpenVibe prices, sorted by what three years really cost.', canonical, body: `<div class="dm-wrap">${body}</div>`, active: '/tld' });
}
/** A small history chart: each registrar's first-year price over time, as steps (real changes only). */
@@@ -251,7 +251,7 @@ function historyChart(history, currency) {
return `<figure class="dm-chart"><svg viewBox="0 0 600 160" role="img" aria-label="First-year price over time">${lines}</svg><figcaption>First-year price over time${currency && currency !== 'USD' ? ` (${esc(currency)})` : ''}, each registrar a line.</figcaption></figure>`;
}
--function tld({ tld: t } = {}) {
++function tld({ tld: t, canonical } = {}) {
const rows = (t && t.prices) || [];
const name = t ? (t.display || dot(t.tld)) : '';
const best = rows.find((r) => !r.currency_other && r.y3 != null);
@@@ -271,10 +271,10 @@ ${searchBox()}</header>
<p class="dm-fine">From each registrar's own public price list.${latest(rows) ? ` Checked ${esc(day(latest(rows)))}.` : ''} Affiliate links, where there are any, are marked and never change the order.</p>
${rows.length ? priceTable(rows, { by: 'registrar', label: `Registrars for ${name}` }) : '<p class="dm-fine">No registrar prices this ending yet.</p>'}</section>
<section aria-labelledby="dm-hist-h"><h2 id="dm-hist-h">Price history</h2>${chart || `<p class="dm-fine">No price changes recorded${rows.length && rows[0].first_seen_at ? ` since ${esc(day(rows.map((r) => r.first_seen_at).filter(Boolean).sort()[0]))}` : ''}. Every change from now on is kept here.</p>`}</section>`;
-- return layout({ title: `${name} domain prices: every registrar, 1, 3 and 5 years`, description: `What ${name} costs at every registrar OpenVibe reads: first year, renewal and the real 3- and 5-year totals.`, body: `<div class="dm-wrap">${body}</div>`, active: '/tld' });
++ return layout({ title: `${name} domain prices: every registrar, 1, 3 and 5 years`, description: `What ${name} costs at every registrar OpenVibe reads: first year, renewal and the real 3- and 5-year totals.`, canonical, body: `<div class="dm-wrap">${body}</div>`, active: '/tld' });
}
--function domain({ name, history, prices = [], status = null, check = null } = {}) {
++function domain({ name, history, prices = [], status = null, check = null, canonical } = {}) {
const full = name ? (name.display || name.ascii) : '';
const h = history || {};
const snaps = Array.isArray(h.snapshots) ? h.snapshots : [];
@@@ -295,11 +295,11 @@ ${status ? `<p class="dm-status ${(STATUS[status] || ['is-unknown'])[0]}"><span
<section aria-labelledby="dm-rec-h"><h2 id="dm-rec-h">Public record</h2>${record || `<p class="dm-fine">OpenVibe keeps a domain's history once it has been looked up ${Number(h.min_lookups || 5)} times${h.lookups ? ` (${Number(h.lookups)} so far)` : ''}. <a href="https://whois.openvibe.tools/?q=${encodeURIComponent(name ? name.ascii : '')}">Look it up now on OpenVibe.Tools</a>.</p>`}
${timeline ? `<h3>What changed</h3>${timeline}` : ''}</section>
${prices.length ? `<section aria-labelledby="dm-p-h"><h2 id="dm-p-h">What ${esc(dot(name.tld))} costs</h2>${priceTable(prices, { by: 'ending', label: `${dot(name.tld)} prices` })}</section>` : ''}`;
-- return layout({ title: `${full}: public record and history`, description: `${full}: its registrar, dates and how they changed, from public records only.`, robots: snaps.length ? 'index, follow' : 'noindex, follow', body: `<div class="dm-wrap">${body}</div>` });
++ return layout({ title: `${full}: public record and history`, description: `${full}: its registrar, dates and how they changed, from public records only.`, canonical, robots: snaps.length ? 'index, follow' : 'noindex, follow', body: `<div class="dm-wrap">${body}</div>` });
}
/** The API, for people and agents: every endpoint with a call that copies. */
--function developers() {
++function developers({ canonical } = {}) {
const rows = [
['GET /api/v1/search?q=sushitruck', 'A name: whether it is free (the registry\'s answer) and every ending\'s prices with 1, 3 and 5-year totals, cheapest over three years first.'],
['GET /api/v1/tlds?sort=renew', 'Every ending OpenVibe prices (sort popular, tld, register or renew; q, kind and page narrow it).'],
@@@ -310,7 +310,7 @@ function developers() {
<section aria-labelledby="dm-ep-h"><h2 id="dm-ep-h">Endpoints</h2><dl class="dm-eps">${rows.map(([ep, what]) => `<div><dt><code>${esc(ep)}</code></dt><dd>${esc(what)}</dd></div>`).join('')}</dl></section>
${sc.code({ title: 'Try it', samples: [{ label: 'Price a name', lang: 'bash', code: 'curl "https://openvibe.domains/api/v1/search?q=sushitruck"' }, { label: 'JavaScript', lang: 'js', code: "const r = await fetch('https://openvibe.domains/api/v1/tlds/dev').then((x) => x.json());\nconsole.log(r.tld.prices[0]);" }] })}
<p class="dm-fine">Limits: 120 requests a minute from one address. Errors are problem+json (RFC 9457). <a href="/llms.txt">llms.txt</a> describes the site for agents.</p>`;
-- return layout({ title: 'The Domains API', description: 'Open JSON for domain prices, availability and history: every ending and registrar, with 1, 3 and 5-year totals.', body: `<div class="dm-wrap">${body}</div>`, active: '/developers' });
++ return layout({ title: 'The Domains API', description: 'Open JSON for domain prices, availability and history: every ending and registrar, with 1, 3 and 5-year totals.', canonical, body: `<div class="dm-wrap">${body}</div>`, active: '/developers' });
}
function notFound() {
ddiff --git a/test/routes.test.js b/test/routes.test.js
iindex 0002edb..cfe07e9 100644
---- a/test/routes.test.js
++++ b/test/routes.test.js
@@@ -132,6 +132,22 @@ function stubs() {
assert.ok(missing.text.includes('Not found'));
});
++ await check('every indexable page names its canonical and the site card', async () => {
++ const pages = [
++ ['/', 'https://openvibe.domains/'],
++ ['/tld', 'https://openvibe.domains/tld'],
++ ['/tld/dev', 'https://openvibe.domains/tld/dev'],
++ ['/developers', 'https://openvibe.domains/developers'],
++ ['/d/example.com', 'https://openvibe.domains/d/example.com'],
++ ];
++ for (const [path, canonical] of pages) {
++ const r = await t.get(path);
++ assert.strictEqual(r.status, 200, `${path}: status`);
++ assert.ok(r.text.includes(`<link rel="canonical" href="${canonical}">`), `${path}: canonical ${canonical}`);
++ assert.match(r.text, /<meta property="og:image" content="https:\/\/[^"]+">/, `${path}: og:image`);
++ }
++ });
++
await check('GET /d/example.com: 200 with history and its ending\'s prices; bad domain: 404', async () => {
const r = await t.get('/d/example.com');
assert.strictEqual(r.status, 200);