openvibers/openvibe.tours Commits

The home's FAQ data asks the questions the page shows: one list for both (#37)

OP OpenVibers committed 11 days ago · fc09fbcdde · 3 files, +21 −16

Changes

ddiff --git a/server/http/discovery.js b/server/http/discovery.js
iindex 2ffc844..4c3815a 100644
---- a/server/http/discovery.js
++++ b/server/http/discovery.js
@@@ -26,11 +26,8 @@ function homeJsonLd(config) {
seo.jsonLd.website({ name: SITE_NAME, url: site, description: DESCRIPTION, searchUrl: `${site}/cities?q={q}` }),
seo.jsonLd.softwareApp({ name: SITE_NAME, url: site, description: DESCRIPTION, category: 'TravelApplication', keywords: 'openvibe tours, travel, places, self-guided routes, live tours, stays, flights, openstreetmap, wikivoyage, wikidata' }),
seo.jsonLd.webPage({ name: SITE_NAME, url: `${site}/`, description: DESCRIPTION, siteUrl: site }),
-- seo.jsonLd.faq([
-- { q: 'Where does OpenVibe.Tours get its data?', a: 'OpenStreetMap (ODbL), Wikivoyage (CC BY-SA), Wikidata (CC0), OpenVibe.Live\'s public stream list and OpenVibe.Rent\'s public listings. Every item carries its source and the time it was fetched.' },
-- { q: 'Are the routes guided tours?', a: 'No. They are self-guided walks generated from Wikivoyage See lists and marked as generated, with a cited note at every stop.' },
-- { q: 'Does OpenVibe.Tours sell flights or stays?', a: 'No. There is no flight API key in v1: the flight form links the official airline-neutral search with no affiliate parameter. Stays link to OpenVibe.Rent; no payment is handled on Tours.' },
-- ]),
++ // The same questions the home shows (the release check: structured data names what is on the page).
++ seo.jsonLd.faq(require('../views').HOME_FAQ.map(([q, a]) => ({ q, a }))),
];
}
ddiff --git a/server/views/index.js b/server/views/index.js
iindex 331813b..58f266f 100644
---- a/server/views/index.js
++++ b/server/views/index.js
@@@ -131,6 +131,16 @@ ${safeImage(p.image_url) ? `<img src="${esc(p.image_url)}" alt="${esc(p.name)}"
</a></article>`;
}
++/** The home's questions: the accordion on the page and the FAQPage data (server/http/discovery.js) are this one list. */
++const HOME_FAQ = [
++ ['Where does the data come from?', 'OpenStreetMap (Overpass, tourism=*) under ODbL, Wikivoyage under CC BY-SA, Wikidata under CC0, OpenVibe.Live\'s public stream list and OpenVibe.Rent\'s public listings. Every city, place, route and source row shows its source and fetch time.'],
++ ['Are the routes real guided tours?', 'No. They are self-guided walks generated from Wikivoyage\'s See lists and marked as generated. Check opening hours and access before you go; the note at each stop is the Wikivoyage text, cited.'],
++ ['Do you sell flights or stays?', 'No. There is no flight API key in v1: the flight form opens the official airline-neutral search with the city pair filled in, with no affiliate parameter. Stays link to the listing on OpenVibe.Rent; Tours never takes a payment.'],
++ ['Is there audio on the stops?', 'Not yet. OpenVibe.AI has no speech-synthesis route today, so every stop says audio is off instead of shipping a broken player. The audio fields are in the schema for when an API exists.'],
++ ['Who can see my trip?', 'Only you, until you switch it to shared. A shared trip is readable by anyone with its link and editable only by you. Delete it at any time; account deletion erases your trips.'],
++ ['Why is a city missing?', 'The v1 catalogue is the top 100 cities by population; every city is fetched from Wikidata and Wikivoyage. A failed source run leaves the previous data and says so in the job receipt rather than inventing a place.'],
++];
++
function home({ counts = {}, live = [], recent = [], placeOfDay = null, cities = [], routes = [], stays = [], jobs = [] } = {}) {
const rails = [
// Real first (the owner, 10-31): live tours when someone is live, else the recordings; never an empty rail.
@@@ -200,16 +210,7 @@ function home({ counts = {}, live = [], recent = [], placeOfDay = null, cities =
{ label: 'OpenAPI', lang: 'json', code: 'GET /api/v1/openapi.json' },
],
},
-- faq: {
-- items: [
-- ['Where does the data come from?', 'OpenStreetMap (Overpass, tourism=*) under ODbL, Wikivoyage under CC BY-SA, Wikidata under CC0, OpenVibe.Live\'s public stream list and OpenVibe.Rent\'s public listings. Every city, place, route and source row shows its source and fetch time.'],
-- ['Are the routes real guided tours?', 'No. They are self-guided walks generated from Wikivoyage\'s See lists and marked as generated. Check opening hours and access before you go; the note at each stop is the Wikivoyage text, cited.'],
-- ['Do you sell flights or stays?', 'No. There is no flight API key in v1: the flight form opens the official airline-neutral search with the city pair filled in, with no affiliate parameter. Stays link to the listing on OpenVibe.Rent; Tours never takes a payment.'],
-- ['Is there audio on the stops?', 'Not yet. OpenVibe.AI has no speech-synthesis route today, so every stop says audio is off instead of shipping a broken player. The audio fields are in the schema for when an API exists.'],
-- ['Who can see my trip?', 'Only you, until you switch it to shared. A shared trip is readable by anyone with its link and editable only by you. Delete it at any time; account deletion erases your trips.'],
-- ['Why is a city missing?', 'The v1 catalogue is the top 100 cities by population; every city is fetched from Wikidata and Wikivoyage. A failed source run leaves the previous data and says so in the job receipt rather than inventing a place.'],
-- ],
-- },
++ faq: { items: HOME_FAQ },
});
const jobsLine = jobs.length
? `<section class="tr-sec tr-sources" aria-labelledby="tr-sources-h"><h2 id="tr-sources-h">Sources and fetch times</h2><p class="tr-muted">${esc(SOURCE.osm.replace(/<[^>]+>/g, ''))} · Wikivoyage CC BY-SA 4.0 · Wikidata CC0 · OpenVibe.Live · OpenVibe.Rent. Last source run ${lastFetch ? when(lastFetch) : 'not yet'}.</p><ul class="tr-jobs">${jobs.map((j) => `<li><b>${esc(j.job)}</b> ${esc(stateLabel(j.status))}${j.finished_at ? ` · ${when(j.finished_at)}` : ''}${j.error ? ` · ${esc(String(j.error).slice(0, 200))}` : ''}</li>`).join('')}</ul></section>`
@@@ -437,4 +438,4 @@ function notFound() {
return `<div class="tr-wrap tr-narrow tr-page-pad" id="tr-not-found"><p class="tr-kicker">404</p><h1>Not here</h1><p class="tr-lede">That city, place, route or trip does not exist, or it is a private trip.</p><p><a class="tr-btn" href="/">Go home</a> <a class="tr-btn tr-btn-quiet" href="/cities">Browse cities</a></p></div>`;
}
--module.exports = { safeImage, home, citiesPage, cityPage, placePage, routePage, livePage, staysPage, flightsPage, signIn, tripNew, tripsPage, tripPage, developersPage, notFound, placeOfDayCard, heroMotif, miniMap, SOURCE };
++module.exports = { HOME_FAQ, safeImage, home, citiesPage, cityPage, placePage, routePage, livePage, staysPage, flightsPage, signIn, tripNew, tripsPage, tripPage, developersPage, notFound, placeOfDayCard, heroMotif, miniMap, SOURCE };
ddiff --git a/test/pages.test.js b/test/pages.test.js
iindex 2d9c456..df05cc9 100644
---- a/test/pages.test.js
++++ b/test/pages.test.js
@@@ -25,6 +25,13 @@ const { seed } = require('./helpers/seed');
assert.match(r.text, /href="\/routes\/rte_/);
assert.match(r.text, /href="\/stays"/);
assert.match(r.text, /TouristAttraction|TouristDestination|FAQPage/);
++ // The FAQPage data asks exactly the questions the page shows (structured data names what is on the page).
++ const ld = [...r.text.matchAll(/<script type="application\/ld\+json">([\s\S]*?)<\/script>/g)].flatMap((m) => [].concat(JSON.parse(m[1])))
++ .flatMap((x) => (x['@graph'] ? x['@graph'] : [x]));
++ const faq = ld.find((x) => x['@type'] === 'FAQPage');
++ assert.ok(faq && faq.mainEntity.length >= 3, 'the FAQPage data');
++ const shown = r.text.replace(/<script[\s\S]*?<\/script>/g, '');
++ for (const q of faq.mainEntity) assert.ok(shown.includes(q.name.replace(/'/g, '&#39;')) || shown.includes(q.name), `asked on the page: ${q.name}`);
assert.match(r.text, /<main id="main"/);
});