openvibers/openvibe.coupons Commits

Coupons: a code's page has its title as the h1 (it had none; the list card's h3 was reused) (#54)

OP OpenVibers committed 29 days ago · 1bf134d6c3 · 3 files, +7 −3

**A coupon code's page has its title as the h1.**

**Found by:** a crawl of every site's linked pages (420 pages). Every openvibe.coupons code page (`/c/cpn_…`) had no `<h1>`: the page reuses the list card, whose title is an `<h3>`. A screen reader's heading list and a search engine saw no main heading. The page started at h3, then jumped to "Status history" at h2.

**Change:**
- **`server/render/pages.js`:** `couponCard(v, { page: true })` renders the title as `<h1 class="coupon-title">`. The code page passes `page: true`, and lists keep their linked `<h3>` cards.
- **`public/css/coupons.css`:** `.coupon .coupon-title` keeps it card-sized (1.35 rem).

**Test:** `pages-discovery.test.js` checks that a code page has exactly one h1, its title. The 14 test files that reach the change pass.

Changes

ddiff --git a/public/css/coupons.css b/public/css/coupons.css
iindex b8f771b..47369c9 100644
---- a/public/css/coupons.css
++++ b/public/css/coupons.css
@@@ -15,6 +15,7 @@ h3 { font-size: 1.1rem; margin: 0 0 .4rem; }
.meta, .empty { color: var(--text-muted, #7386a3); font-size: .9rem; margin: .25rem 0; }
.unknown { font-style: italic; color: var(--text-secondary, #96a7c2); }
.coupon { border: 1px solid var(--border, #1f2d47); border-radius: 10px; padding: 12px 16px; margin: 12px 0; background: var(--bg-card, #131c2e); }
++.coupon .coupon-title { font-size: 1.35rem; line-height: 1.3; margin: 4px 0 10px; }
.code-line { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; margin: .3rem 0; }
.code-line .label { color: var(--text-muted, #7386a3); font-size: .85rem; }
.code { font: 600 1.15rem/1.3 ui-monospace, SFMono-Regular, Menlo, monospace; padding: 4px 10px; border: 1px dashed var(--accent, #3b82f6); border-radius: 6px; user-select: all; -webkit-user-select: all; word-break: break-all; }
ddiff --git a/server/render/pages.js b/server/render/pages.js
iindex 63cf704..f2d93d1 100644
---- a/server/render/pages.js
++++ b/server/render/pages.js
@@@ -88,10 +88,11 @@ function reportForms(v, { viewer, csrf, back }) {
</div>`;
}
--/** One code. v: coupons.view(); opts: { viewer, csrf, back, link } */
++/** One code. v: coupons.view(); opts: { viewer, csrf, back, link, page } (page: its own page, where the title is the h1) */
function couponCard(v, opts = {}) {
++ const title = opts.link ? h`<a href="/c/${v.id}">${v.title}</a>` : v.title;
return h`<article class="coupon status-${v.status}" id="${v.id}">
--<h3>${opts.link ? h`<a href="/c/${v.id}">${v.title}</a>` : v.title}</h3>
++${opts.page ? h`<h1 class="coupon-title">${title}</h1>` : h`<h3>${title}</h3>`}
<p class="code-line"><span class="label">Code</span> <code class="code">${v.code}</code> ${statusBadge(v.status)}</p>
${v.description ? h`<p>${v.description}</p>` : ''}
<p class="meta">${confidenceText(v)}</p>
@@@ -162,7 +163,7 @@ function couponPage({ v, m, history, viewer, csrf, back, flashMsg }) {
return h`${raw(ssr.breadcrumbsHtml([{ name: 'Coupons', url: '/' }, { name: m.name, url: `/m/${m.slug}` }, { name: v.code }]))}
${flash(flashMsg)}
${v.active ? '' : h`<p class="notice">This code is not in active results (${STATUS_LABEL[v.status] ? STATUS_LABEL[v.status].toLowerCase() : v.status}${v.expiry.known && !v.active && v.status !== 'expired' ? ', past its expiry' : ''}).</p>`}
--${couponCard(v, { viewer, csrf, back })}
++${couponCard(v, { viewer, csrf, back, page: true })}
<h2>Status history</h2>
${history.length ? h`<ol class="history">${history.map((x) => h`<li>${utc(new Date(x.created_at).toISOString())}: ${x.from_status ? h`${STATUS_LABEL[x.from_status] || x.from_status} → ` : ''}${STATUS_LABEL[x.to_status] || x.to_status}${x.confidence_after != null ? h`, confidence ${Math.round(x.confidence_after * 100)}%` : ''} <span class="meta">(${x.reason.startsWith('staff') ? 'staff' : x.reason.startsWith('service:') ? 'service' : x.reason})</span></li>`)}</ol>` : h`<p class="empty">No changes yet.</p>`}
<p class="meta"><a href="/c/${v.id}.json">This code as JSON</a></p>`;
ddiff --git a/test/pages-discovery.test.js b/test/pages-discovery.test.js
iindex 64cd21d..1293370 100644
---- a/test/pages-discovery.test.js
++++ b/test/pages-discovery.test.js
@@@ -78,6 +78,8 @@ const { boot, check, done } = require('./helpers/boot');
await check('JSON-LD only from real fields (breadcrumbs, web page); the JSON twin has the page\'s content', async () => {
const page = (await t.get(`/c/${codeId}`)).text;
++ assert.strictEqual((page.match(/<h1\b/g) || []).length, 1, 'the code page has one h1: its title');
++ assert.match(page, /<h1 class="coupon-title">/);
const blocks = [...page.matchAll(/<script type="application\/ld\+json">([\s\S]*?)<\/script>/g)].map((m) => JSON.parse(m[1]));
assert.deepStrictEqual(blocks.map((b) => b['@type']).sort(), ['BreadcrumbList', 'WebPage']);
const json = (await t.get(`/c/${codeId}.json`)).json();