openvibers/openvibe.live Commits
Live docs: search titles and descriptions fit (sentences up to 160 characters; long headings without a suffix) (#132)
Changes
@@@ -225,11 +225,20 @@ function publicBase() {return b.replace(/\/+$/, '');}++/** A search-result summary: the text when it fits 160 characters, else its sentences up to that, else cut at a word. */++function summaryOf(text, max = 160) {++ if (text.length <= max) return text;++ const cut = text.slice(0, max);++ const end = Math.max(cut.lastIndexOf('. '), cut.lastIndexOf('! '), cut.lastIndexOf('? '), cut.endsWith('.') ? max - 1 : -1);++ return end >= 60 ? cut.slice(0, end + 1) : `${cut.slice(0, cut.lastIndexOf(' '))}…`;++}++function renderPage({ name, title, html, headings }) {-- const pageTitle = title ? `${title} — OpenVibe.Live Docs` : 'OpenVibe.Live Docs';++ // A long heading is the whole title (search results cut a title near 60 characters, so no suffix after it).++ const pageTitle = title ? (title.length > 45 ? title : `${title} — OpenVibe.Live Docs`) : 'OpenVibe.Live Docs';// The first paragraph is the page's summary for search engines and link previews.const firstP = /<p>([\s\S]*?)<\/p>/.exec(html);-- const description = firstP ? firstP[1].replace(/<[^>]+>/g, '').replace(/&/g, '&').replace(/"/g, '"').replace(/'/g, "'").replace(/</g, '<').replace(/>/g, '>').replace(/\s+/g, ' ').trim().slice(0, 300) : '';++ const description = firstP ? summaryOf(firstP[1].replace(/<[^>]+>/g, '').replace(/&/g, '&').replace(/"/g, '"').replace(/'/g, "'").replace(/</g, '<').replace(/>/g, '>').replace(/\s+/g, ' ').trim()) : '';const canonical = `${publicBase()}/docs${name === 'README' ? '' : `/${name}`}`;const toc = headings.length? `<nav class="toc" aria-label="On this page">${headings.map(h => `<a href="#${h.id}">${escapeHtml(h.text)}</a>`).join('')}</nav>`@@@ -99,4 +99,17 @@ assert.ok(shell.includes('<link rel="icon" type="image/svg+xml" href="/assets/loassert.ok(fs.existsSync(path.join(__dirname, '..', 'public', 'assets', 'logo.svg')), 'the icon file exists');console.log('✅ page shell: canonical and icon');++// Search results: a long heading is the whole <title>, and the description is the first paragraph's sentences up to 160++// characters (the go-live guide's paragraph is 300).++{++ const long = 'A very long first sentence that explains the page in plain words for people who search for it. ' + 'And then more words follow. '.repeat(8);++ const page = renderPage({ name: 'x', title: 'Go live from your browser: no OBS, no downloads, no follower minimum', html: `<p>${long}</p>`, headings: [] });++ assert.ok(page.includes('<title>Go live from your browser: no OBS, no downloads, no follower minimum</title>'), 'a long title has no suffix');++ const d = (/<meta name="description" content="([^"]*)"/.exec(page) || [])[1];++ assert.ok(d.length <= 160 && d.endsWith('.'), `description is whole sentences within 160: ${d}`);++ const short = renderPage({ name: 'y', title: 'WHIP', html: '<p>Short.</p>', headings: [] });++ assert.ok(short.includes('<title>WHIP — OpenVibe.Live Docs</title>') && short.includes('content="Short."'));++ console.log('✅ page shell: search title and description');++}++console.log('\n✅ All docs renderer tests passed');
@@ -225,11 +225,20 @@ function publicBase() {
@@ -225,11 +225,20 @@ function publicBase() {
return b.replace(/\/+$/, '');
return b.replace(/\/+$/, '');
}
}
+/** A search-result summary: the text when it fits 160 characters, else its sentences up to that, else cut at a word. */
+function summaryOf(text, max = 160) {
+ if (text.length <= max) return text;
+ const cut = text.slice(0, max);
+ const end = Math.max(cut.lastIndexOf('. '), cut.lastIndexOf('! '), cut.lastIndexOf('? '), cut.endsWith('.') ? max - 1 : -1);
+ return end >= 60 ? cut.slice(0, end + 1) : `${cut.slice(0, cut.lastIndexOf(' '))}…`;
+}
+
function renderPage({ name, title, html, headings }) {
function renderPage({ name, title, html, headings }) {
- const pageTitle = title ? `${title} — OpenVibe.Live Docs` : 'OpenVibe.Live Docs';
+ // A long heading is the whole title (search results cut a title near 60 characters, so no suffix after it).
+ const pageTitle = title ? (title.length > 45 ? title : `${title} — OpenVibe.Live Docs`) : 'OpenVibe.Live Docs';
// The first paragraph is the page's summary for search engines and link previews.
// The first paragraph is the page's summary for search engines and link previews.
const firstP = /<p>([\s\S]*?)<\/p>/.exec(html);
const firstP = /<p>([\s\S]*?)<\/p>/.exec(html);
- const description = firstP ? firstP[1].replace(/<[^>]+>/g, '').replace(/&/g, '&').replace(/"/g, '"').replace(/'/g, "'").replace(/</g, '<').replace(/>/g, '>').replace(/\s+/g, ' ').trim().slice(0, 300) : '';
+ const description = firstP ? summaryOf(firstP[1].replace(/<[^>]+>/g, '').replace(/&/g, '&').replace(/"/g, '"').replace(/'/g, "'").replace(/</g, '<').replace(/>/g, '>').replace(/\s+/g, ' ').trim()) : '';
const canonical = `${publicBase()}/docs${name === 'README' ? '' : `/${name}`}`;
const canonical = `${publicBase()}/docs${name === 'README' ? '' : `/${name}`}`;
const toc = headings.length
const toc = headings.length
? `<nav class="toc" aria-label="On this page">${headings.map(h => `<a href="#${h.id}">${escapeHtml(h.text)}</a>`).join('')}</nav>`
? `<nav class="toc" aria-label="On this page">${headings.map(h => `<a href="#${h.id}">${escapeHtml(h.text)}</a>`).join('')}</nav>`
@@ -99,4 +99,17 @@ assert.ok(shell.includes('<link rel="icon" type="image/svg+xml" href="/assets/lo
@@ -99,4 +99,17 @@ assert.ok(shell.includes('<link rel="icon" type="image/svg+xml" href="/assets/lo
assert.ok(fs.existsSync(path.join(__dirname, '..', 'public', 'assets', 'logo.svg')), 'the icon file exists');
assert.ok(fs.existsSync(path.join(__dirname, '..', 'public', 'assets', 'logo.svg')), 'the icon file exists');
console.log('✅ page shell: canonical and icon');
console.log('✅ page shell: canonical and icon');
+// Search results: a long heading is the whole <title>, and the description is the first paragraph's sentences up to 160
+// characters (the go-live guide's paragraph is 300).
+{
+ const long = 'A very long first sentence that explains the page in plain words for people who search for it. ' + 'And then more words follow. '.repeat(8);
+ const page = renderPage({ name: 'x', title: 'Go live from your browser: no OBS, no downloads, no follower minimum', html: `<p>${long}</p>`, headings: [] });
+ assert.ok(page.includes('<title>Go live from your browser: no OBS, no downloads, no follower minimum</title>'), 'a long title has no suffix');
+ const d = (/<meta name="description" content="([^"]*)"/.exec(page) || [])[1];
+ assert.ok(d.length <= 160 && d.endsWith('.'), `description is whole sentences within 160: ${d}`);
+ const short = renderPage({ name: 'y', title: 'WHIP', html: '<p>Short.</p>', headings: [] });
+ assert.ok(short.includes('<title>WHIP — OpenVibe.Live Docs</title>') && short.includes('content="Short."'));
+ console.log('✅ page shell: search title and description');
+}
+
console.log('\n✅ All docs renderer tests passed');
console.log('\n✅ All docs renderer tests passed');