openvibers/openvibe.tours Commits
The home's FAQ data asks the questions the page shows: one list for both (#37)
Changes
@@@ -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 }))),];}@@@ -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 };@@@ -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, ''')) || shown.includes(q.name), `asked on the page: ${q.name}`);assert.match(r.text, /<main id="main"/);});
@@ -26,11 +26,8 @@ function homeJsonLd(config) {
@@ -26,11 +26,8 @@ function homeJsonLd(config) {
seo.jsonLd.website({ name: SITE_NAME, url: site, description: DESCRIPTION, searchUrl: `${site}/cities?q={q}` }),
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.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.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 }))),
];
];
}
}
@@ -131,6 +131,16 @@ ${safeImage(p.image_url) ? `<img src="${esc(p.image_url)}" alt="${esc(p.name)}"
@@ -131,6 +131,16 @@ ${safeImage(p.image_url) ? `<img src="${esc(p.image_url)}" alt="${esc(p.name)}"
</a></article>`;
</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 = [] } = {}) {
function home({ counts = {}, live = [], recent = [], placeOfDay = null, cities = [], routes = [], stays = [], jobs = [] } = {}) {
const rails = [
const rails = [
// Real first (the owner, 10-31): live tours when someone is live, else the recordings; never an empty rail.
// 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 =
@@ -200,16 +210,7 @@ function home({ counts = {}, live = [], recent = [], placeOfDay = null, cities =
{ label: 'OpenAPI', lang: 'json', code: 'GET /api/v1/openapi.json' },
{ 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
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>`
? `<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() {
@@ -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>`;
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 };
@@ -25,6 +25,13 @@ const { seed } = require('./helpers/seed');
@@ -25,6 +25,13 @@ const { seed } = require('./helpers/seed');
assert.match(r.text, /href="\/routes\/rte_/);
assert.match(r.text, /href="\/routes\/rte_/);
assert.match(r.text, /href="\/stays"/);
assert.match(r.text, /href="\/stays"/);
assert.match(r.text, /TouristAttraction|TouristDestination|FAQPage/);
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, ''')) || shown.includes(q.name), `asked on the page: ${q.name}`);
assert.match(r.text, /<main id="main"/);
assert.match(r.text, /<main id="main"/);
});
});