openvibers/openvibe.work Commits
Structured data names list items as the page shows them (the release browser check failed both lists) (#69)
Changes
@@@ -143,7 +143,7 @@ ${facets.type.map((t) => link({ type: t.type }, t.type, filters.type === t.type,description: `Search ${total ? `${n(total)} ` : ''}jobs from open job boards, each with its source and date, plus saved searches, alerts through OpenVibe.Watch and an application tracker.`,jsonLd: [...homeJsonLd(config),-- latest.length ? seo.jsonLd.itemList('Latest remote jobs on OpenVibe.Work', latest.map((row) => ({ name: `${row.title} at ${row.company}`, url: `${base}/jobs/${row.id}` })), { url: `${base}/jobs?remote=true` }) : null,++ latest.length ? seo.jsonLd.itemList('Latest remote jobs on OpenVibe.Work', latest.map((row) => ({ name: row.title, url: `${base}/jobs/${row.id}` })), { url: `${base}/jobs?remote=true` }) : null,FAQ.length ? seo.jsonLd.faq(FAQ.map((f) => ({ q: f.question, a: f.answer }))) : null,],styles: [showcase.STYLESHEET], localStyles: ['css/home.css'],@@@ -343,7 +343,7 @@ ${l.posted_at ? html`<dt>Posted</dt><dd>${time(l.posted_at)}</dd>` : ''}title: q ? `Companies matching “${q}”` : 'Companies hiring',description: `${n(all.length)} companies with open listings on OpenVibe.Work, most openings first, each with its jobs, where they are and which boards list them.`,crumbs: [{ label: 'Companies' }], styles: [showcase.STYLESHEET],-- jsonLd: q ? [] : [seo.jsonLd.itemList('Companies hiring on OpenVibe.Work', shown.slice(0, 50).map((c) => ({ name: `${c.company}: ${c.count} open listing${c.count === 1 ? '' : 's'}`, url: `${base}/companies/${c.slug}` })), { url: `${base}/companies` })],++ jsonLd: q ? [] : [seo.jsonLd.itemList('Companies hiring on OpenVibe.Work', shown.slice(0, 50).map((c) => ({ name: c.company, url: `${base}/companies/${c.slug}` })), { url: `${base}/companies` })],body: html`<h1>Companies hiring</h1><p class="lede">${n(all.length)} companies have open listings on the boards OpenVibe.Work reads, most openings first.</p><form class="ov-form filter-form" method="get" action="/companies" role="search"><label for="cq">Company</label><input id="cq" name="q" type="search" value="${q}" maxlength="80" placeholder="A company's name"><button class="sc-btn" type="submit">Find</button>${q ? html` <a href="/companies">All companies</a>` : ''}</form>@@@ -231,6 +231,17 @@ const first = async (t, path) => (await t.get(path)).json();for (const h of ['Browse by field', 'Jobs by city', 'Companies with the most openings', 'Questions people ask']) assert.ok(r.text.includes(h), `the home has "${h}"`);assert.match(r.text, /<li><a href="\/companies\/[a-z0-9-]+"><b>[^<]+<\/b><span>\d+ open listings?<\/span>/, 'a company card opens its company page, with its count');assert.match(r.text, /"@type":"ItemList"/, 'the latest remote jobs are an ItemList');++ // Structured data states only what the page shows: each list item's name is visible text (the release++ // browser check fails a list whose names are not, 2026-10-23).++ const visible = (html) => html.replace(/<script[\s\S]*?<\/script>/g, ' ').replace(/<[^>]+>/g, ' ').replace(/</g, '<').replace(/>/g, '>').replace(/'/g, "'").replace(/"/g, '"').replace(/&/g, '&');++ const itemNames = (html) => [...html.matchAll(/<script type="application\/ld\+json">([\s\S]*?)<\/script>/g)].map((m) => JSON.parse(m[1]))++ .filter((d) => d['@type'] === 'ItemList').flatMap((d) => d.itemListElement.map((x) => x.name || (x.item && x.item.name)));++ for (const page of ['/', '/companies']) {++ const h = (await t.get(page)).text;++ const names = itemNames(h);++ assert.ok(names.length, `${page} has a list`);++ for (const nm of names) assert.ok(visible(h).includes(nm), `${page}: list item "${nm}" is not on the page`);++ }});await check('search is limited per caller, and past it the API says so with Retry-After', async () => {
@@ -143,7 +143,7 @@ ${facets.type.map((t) => link({ type: t.type }, t.type, filters.type === t.type,
@@ -143,7 +143,7 @@ ${facets.type.map((t) => link({ type: t.type }, t.type, filters.type === t.type,
description: `Search ${total ? `${n(total)} ` : ''}jobs from open job boards, each with its source and date, plus saved searches, alerts through OpenVibe.Watch and an application tracker.`,
description: `Search ${total ? `${n(total)} ` : ''}jobs from open job boards, each with its source and date, plus saved searches, alerts through OpenVibe.Watch and an application tracker.`,
jsonLd: [
jsonLd: [
...homeJsonLd(config),
...homeJsonLd(config),
- latest.length ? seo.jsonLd.itemList('Latest remote jobs on OpenVibe.Work', latest.map((row) => ({ name: `${row.title} at ${row.company}`, url: `${base}/jobs/${row.id}` })), { url: `${base}/jobs?remote=true` }) : null,
+ latest.length ? seo.jsonLd.itemList('Latest remote jobs on OpenVibe.Work', latest.map((row) => ({ name: row.title, url: `${base}/jobs/${row.id}` })), { url: `${base}/jobs?remote=true` }) : null,
FAQ.length ? seo.jsonLd.faq(FAQ.map((f) => ({ q: f.question, a: f.answer }))) : null,
FAQ.length ? seo.jsonLd.faq(FAQ.map((f) => ({ q: f.question, a: f.answer }))) : null,
],
],
styles: [showcase.STYLESHEET], localStyles: ['css/home.css'],
styles: [showcase.STYLESHEET], localStyles: ['css/home.css'],
@@ -343,7 +343,7 @@ ${l.posted_at ? html`<dt>Posted</dt><dd>${time(l.posted_at)}</dd>` : ''}
@@ -343,7 +343,7 @@ ${l.posted_at ? html`<dt>Posted</dt><dd>${time(l.posted_at)}</dd>` : ''}
title: q ? `Companies matching “${q}”` : 'Companies hiring',
title: q ? `Companies matching “${q}”` : 'Companies hiring',
description: `${n(all.length)} companies with open listings on OpenVibe.Work, most openings first, each with its jobs, where they are and which boards list them.`,
description: `${n(all.length)} companies with open listings on OpenVibe.Work, most openings first, each with its jobs, where they are and which boards list them.`,
crumbs: [{ label: 'Companies' }], styles: [showcase.STYLESHEET],
crumbs: [{ label: 'Companies' }], styles: [showcase.STYLESHEET],
- jsonLd: q ? [] : [seo.jsonLd.itemList('Companies hiring on OpenVibe.Work', shown.slice(0, 50).map((c) => ({ name: `${c.company}: ${c.count} open listing${c.count === 1 ? '' : 's'}`, url: `${base}/companies/${c.slug}` })), { url: `${base}/companies` })],
+ jsonLd: q ? [] : [seo.jsonLd.itemList('Companies hiring on OpenVibe.Work', shown.slice(0, 50).map((c) => ({ name: c.company, url: `${base}/companies/${c.slug}` })), { url: `${base}/companies` })],
body: html`<h1>Companies hiring</h1>
body: html`<h1>Companies hiring</h1>
<p class="lede">${n(all.length)} companies have open listings on the boards OpenVibe.Work reads, most openings first.</p>
<p class="lede">${n(all.length)} companies have open listings on the boards OpenVibe.Work reads, most openings first.</p>
<form class="ov-form filter-form" method="get" action="/companies" role="search"><label for="cq">Company</label><input id="cq" name="q" type="search" value="${q}" maxlength="80" placeholder="A company's name"><button class="sc-btn" type="submit">Find</button>${q ? html` <a href="/companies">All companies</a>` : ''}</form>
<form class="ov-form filter-form" method="get" action="/companies" role="search"><label for="cq">Company</label><input id="cq" name="q" type="search" value="${q}" maxlength="80" placeholder="A company's name"><button class="sc-btn" type="submit">Find</button>${q ? html` <a href="/companies">All companies</a>` : ''}</form>
@@ -231,6 +231,17 @@ const first = async (t, path) => (await t.get(path)).json();
@@ -231,6 +231,17 @@ const first = async (t, path) => (await t.get(path)).json();
for (const h of ['Browse by field', 'Jobs by city', 'Companies with the most openings', 'Questions people ask']) assert.ok(r.text.includes(h), `the home has "${h}"`);
for (const h of ['Browse by field', 'Jobs by city', 'Companies with the most openings', 'Questions people ask']) assert.ok(r.text.includes(h), `the home has "${h}"`);
assert.match(r.text, /<li><a href="\/companies\/[a-z0-9-]+"><b>[^<]+<\/b><span>\d+ open listings?<\/span>/, 'a company card opens its company page, with its count');
assert.match(r.text, /<li><a href="\/companies\/[a-z0-9-]+"><b>[^<]+<\/b><span>\d+ open listings?<\/span>/, 'a company card opens its company page, with its count');
assert.match(r.text, /"@type":"ItemList"/, 'the latest remote jobs are an ItemList');
assert.match(r.text, /"@type":"ItemList"/, 'the latest remote jobs are an ItemList');
+ // Structured data states only what the page shows: each list item's name is visible text (the release
+ // browser check fails a list whose names are not, 2026-10-23).
+ const visible = (html) => html.replace(/<script[\s\S]*?<\/script>/g, ' ').replace(/<[^>]+>/g, ' ').replace(/</g, '<').replace(/>/g, '>').replace(/'/g, "'").replace(/"/g, '"').replace(/&/g, '&');
+ const itemNames = (html) => [...html.matchAll(/<script type="application\/ld\+json">([\s\S]*?)<\/script>/g)].map((m) => JSON.parse(m[1]))
+ .filter((d) => d['@type'] === 'ItemList').flatMap((d) => d.itemListElement.map((x) => x.name || (x.item && x.item.name)));
+ for (const page of ['/', '/companies']) {
+ const h = (await t.get(page)).text;
+ const names = itemNames(h);
+ assert.ok(names.length, `${page} has a list`);
+ for (const nm of names) assert.ok(visible(h).includes(nm), `${page}: list item "${nm}" is not on the page`);
+ }
});
});
await check('search is limited per caller, and past it the API says so with Retry-After', async () => {
await check('search is limited per caller, and past it the API says so with Retry-After', async () => {