openvibers/openvibe.domains Commits
Give every indexable Domains page its canonical URL and share card (#50)
Changes
@@@ -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: namreturn 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() {@@@ -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);
@@ -135,7 +135,7 @@ function totalPicture(r) {
@@ -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>`;
+ `<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
// 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.
// 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)));
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 } = {}) {
@@ -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.
// 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-faq"', 'faq-fold');
body = foldSection(body, 'class="sc-code"', 'dev-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. */
/** 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
@@ -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: '/' });
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 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>
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.' })}`;
${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). */
/** 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) {
@@ -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>`;
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 rows = (t && t.prices) || [];
const name = t ? (t.display || dot(t.tld)) : '';
const name = t ? (t.display || dot(t.tld)) : '';
const best = rows.find((r) => !r.currency_other && r.y3 != null);
const best = rows.find((r) => !r.currency_other && r.y3 != null);
@@ -271,10 +271,10 @@ ${searchBox()}</header>
@@ -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>
<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>
${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>`;
<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 full = name ? (name.display || name.ascii) : '';
const h = history || {};
const h = history || {};
const snaps = Array.isArray(h.snapshots) ? h.snapshots : [];
const snaps = Array.isArray(h.snapshots) ? h.snapshots : [];
@@ -295,11 +295,11 @@ ${status ? `<p class="dm-status ${(STATUS[status] || ['is-unknown'])[0]}"><span
@@ -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>`}
<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>
${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>` : ''}`;
${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. */
/** The API, for people and agents: every endpoint with a call that copies. */
-function developers() {
+function developers({ canonical } = {}) {
const rows = [
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/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).'],
['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() {
@@ -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>
<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]);" }] })}
${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>`;
<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() {
function notFound() {
@@ -132,6 +132,22 @@ function stubs() {
@@ -132,6 +132,22 @@ function stubs() {
assert.ok(missing.text.includes('Not found'));
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 () => {
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');
const r = await t.get('/d/example.com');
assert.strictEqual(r.status, 200);
assert.strictEqual(r.status, 200);