openvibers/openvibe.community Commits
Screenshot cards: a 320/640 WebP from Media, eager only above the fold (#284)
Changes
@@@ -71,6 +71,16 @@ const cache = require('openvibe-shared/cache-policy');const PUBLIC_DIR = path.join(__dirname, '..', 'public');const SLUG_RE = /^[A-Za-z0-9_-]{1,80}$/;++// A card asks the screenshot proxy for one of Media's two card widths (?w=320|640&fmt=webp). The++// format is in the URL, never negotiated: a shared cache stores by URL and ignores Vary.++const SHOT_CARD_WIDTHS = new Set([320, 640]);++function screenshotVariant(query = {}) {++ if (query.w === undefined && query.fmt === undefined) return null;++ const w = Number(query.w);++ const fmt = String(query.fmt || 'webp').toLowerCase();++ if (!SHOT_CARD_WIDTHS.has(w) || fmt !== 'webp') return { error: 'w must be 320 or 640 and fmt webp' };++ return { w, fmt };++}const pasteRef = (slug) => ({ service: 'community', type: 'paste', id: String(slug) });const VERSION = require('../package.json').version;// The date /.well-known/security.txt must be refreshed by: one year on, a person re-reads and re-commits it.@@@ -188,18 +198,21 @@ async function createApp(opts = {}) {return { key: o.id, url: o.url, size: o.size_bytes, mime, media_ref: o.id, private: visibility === 'private' };},/** A paste's screenshot bytes, for the proxy (a private object through a signed download). */-- async bytes(row) {++ async bytes(row, variant = null) {++ // A card's ?w=&fmt= rides on the same URL Media answered: public objects take it openly, a++ // private one through the signature already in the URL (Media checks both before resizing).++ const withVariant = (url) => (!variant ? url : `${url}${url.includes('?') ? '&' : '?'}w=${variant.w}&fmt=${variant.fmt}`);const ref = String(row.media_ref || '');const id = /^med_[0-9A-HJKMNP-TV-Z]{26}$/.test(ref) ? ref : (String(row.screenshot_url || '').match(/\/o\/(med_[0-9A-HJKMNP-TV-Z]{26})/) || [])[1];if (id && mediaObjects.configured) {try {-- const url = await mediaObjects.downloadUrl(id);++ const url = withVariant(await mediaObjects.downloadUrl(id));const res = await (opts.fetchImpl || globalThis.fetch)(url, { signal: AbortSignal.timeout(30_000) });if (res.ok) return { body: Buffer.from(await res.arrayBuffer()), type: res.headers.get('content-type') || 'application/octet-stream' };} catch { /* an unlisted object still answers its stored URL below */ }}if (!row.screenshot_url) return null;-- const res = await (opts.fetchImpl || globalThis.fetch)(row.screenshot_url, { signal: AbortSignal.timeout(30_000) });++ const res = await (opts.fetchImpl || globalThis.fetch)(withVariant(row.screenshot_url), { signal: AbortSignal.timeout(30_000) });if (!res.ok) return null;return { body: Buffer.from(await res.arrayBuffer()), type: res.headers.get('content-type') || 'application/octet-stream' };},@@@ -631,10 +644,12 @@ async function createApp(opts = {}) {// token. The Media object's own URL is never handed to a browser.app.get('/p/:slug/screenshot', withUser, async (req, res) => {if (!SLUG_RE.test(req.params.slug)) return notFound(res);++ const variant = screenshotVariant(req.query);++ if (variant && variant.error) return res.status(400).type('text/plain').set('X-Content-Type-Options', 'nosniff').send(variant.error);try {await app.locals.pastes.screenshotUrl(req.viewer, req.params.slug);const row = await db.prepare('SELECT media_ref, screenshot_url, visibility, burn_after_read, locked FROM pastes WHERE slug = ? AND deleted_at IS NULL').get(req.params.slug);-- const got = row ? await app.locals.pasteMedia.bytes(row) : null;++ const got = row ? await app.locals.pasteMedia.bytes(row, variant) : null;if (!got) return notFound(res);// A public paste's screenshot never changes under this URL, so it is cached like any other// content-addressed asset: the home page shows six of them and each is several hundred KB,@@@ -36,6 +36,13 @@ function mediaW(u, w) {}/** A screenshot's address: Community's checked proxy, never the Media object's own URL. */function screenshotSrc(p) { return `/p/${encodeURIComponent(p.slug)}/screenshot`; }++/** A card-sized screenshot through the same proxy: Media's ladder has 320 and 640, webp by URL. */++function screenshotVariant(p, w) { return `${screenshotSrc(p)}?w=${w}&fmt=webp`; }++/** A card screenshot: two widths, the card's shape in width/height (no shift), lazy unless it is the LCP. */++function screenshotImg(p, { eager = false, sizes = '(max-width: 620px) calc(100vw - 40px), 300px' } = {}) {++ const srcset = [320, 640].map((w) => `${screenshotVariant(p, w)} ${w}w`).join(', ');++ return `<img src="${esc(screenshotVariant(p, 640))}" srcset="${esc(srcset)}" sizes="${esc(sizes)}" width="640" height="360" alt="${esc(p.title || 'Screenshot')}"${eager ? ' fetchpriority="high"' : ' loading="lazy"'} decoding="async">`;++}function timeAgo(v) {const iso = ld.isoDate(v);@@@ -107,13 +114,13 @@ function pagerHref(state, page) {}// ── Paste card (grid item) ───────────────────────────────────--function pasteCard(p) {++function pasteCard(p, { eager = false } = {}) {const isShot = p.type === 'screenshot';const locked = !!p.locked;const preview = locked? `<div class="card-locked"><i class="fa-solid fa-lock" aria-hidden="true"></i> Locked — OpenVibe can’t read what’s inside</div>`: isShot-- ? (p.screenshot_url ? `<div class="card-shot"><img src="${esc(screenshotSrc(p))}" alt="${esc(p.title || 'Screenshot')}" loading="lazy"></div>` : '')++ ? (p.screenshot_url ? `<div class="card-shot">${screenshotImg(p, { eager })}</div>` : ''): `<pre class="card-code" aria-hidden="true">${esc(String(p.content || '').slice(0, 220))}${String(p.content || '').length > 220 ? '…' : ''}</pre>`;const flags = `${p.pinned ? '<i class="fa-solid fa-thumbtack" title="Pinned"></i> ' : ''}${p.burn_after_read ? '<i class="fa-solid fa-fire" title="Burns after reading"></i> ' : ''}${locked ? '<i class="fa-solid fa-lock" title="Locked"></i> ' : ''}${p.is_nsfw ? '<span class="badge badge-nsfw">NSFW</span> ' : ''}`;return `<article class="card${p.is_nsfw ? ' card-nsfw' : ''}${locked ? ' card-is-locked' : ''}">@@@ -129,7 +136,9 @@ function pasteCard(p) {}function cardGrid(pastes, empty = 'Nothing here yet.') {if (!pastes || !pastes.length) return `<p class="empty">${esc(empty)}</p>`;-- return `<div class="grid">${pastes.map(pasteCard).join('\n')}</div>`;++ // The first picture in the grid is the one above the fold: it loads at high priority, the rest stay lazy.++ const firstShot = pastes.findIndex((p) => p.type === 'screenshot' && p.screenshot_url && !p.locked);++ return `<div class="grid">${pastes.map((p, i) => pasteCard(p, { eager: i === firstShot })).join('\n')}</div>`;}// ── Home ─────────────────────────────────────────────────────@@@ -174,10 +183,10 @@ function homeStats(c = {}) {* says NSFW), else its first lines in the media (kit.card preview; the lists carry 300 characters) with its language in* the corner; who, views and when under the title.*/--function pasteKitCard(p) {++function pasteKitCard(p, { eager = false } = {}) {const isShot = p.type === 'screenshot';-- const image = isShot && !p.is_nsfw ? screenshotSrc(p) : '';-- return kit.card({++ const image = isShot && !p.is_nsfw ? screenshotVariant(p, 640) : '';++ const card = kit.card({href: `/p/${p.slug}`, title: p.title || 'Untitled', type: 'post', ratio: '16/9', image: image || '',badge: p.is_nsfw ? 'NSFW' : p.origin === 'ai' ? 'AI' : p.pinned ? 'Pinned' : '',corner: isShot ? '' : languageLabel(p.language || 'text'),@@@ -185,6 +194,9 @@ function pasteKitCard(p) {who: authorName(p), avatar: avatarUrl(p, 68) || '',count: [Number(p.views) || 0, Number(p.views) === 1 ? 'view' : 'views'], when: p.created_at || null,});++ // kit.card's own picture is one plain <img> before the avatar; give it both card widths, the++ // shape the card draws and the LCP hint. Its markup is pinned with the shared version.++ return image ? card.replace(/<img\b[^>]*>/, screenshotImg(p, { eager, sizes: '(max-width: 620px) 80vw, 300px' })) : card;}/** A section's head in the rails' look (room.css): its heading and the one link to the whole list. */@@@ -201,7 +213,10 @@ function kindChips(languages) {}function latestShelf(latest, languages) {-- const rail = kit.rail({ id: 'latest', title: 'Latest pastes', more: { href: '/pastes', label: 'See all' }, items: (latest || []).map(pasteKitCard) });++ const list = latest || [];++ // The newest screenshot in the first rail is the home's LCP picture on a phone: load it at high priority.++ const firstShot = list.findIndex((p) => p.type === 'screenshot' && p.screenshot_url && !p.is_nsfw && !p.locked);++ const rail = kit.rail({ id: 'latest', title: 'Latest pastes', more: { href: '/pastes', label: 'See all' }, items: list.map((p, i) => pasteKitCard(p, { eager: i === firstShot })) });const none = `<section id="latest" aria-labelledby="latest-h">${shelfHead('latest-h', 'Latest pastes', { href: '/pastes', label: 'See all' })}${kit.empty({ title: 'No pastes yet', text: 'Be the first: paste something and share the link.', action: { href: '/new', label: 'Start a paste' } })}</section>`;return `<div class="cm-sec">${rail || none}${languages && languages.length ? kindChips(languages) : ''}</div>`;}@@@ -21,11 +21,17 @@ function start({ publicPem, issuer }) {req.on('end', () => {const json = (status, obj) => { res.writeHead(status, { 'Content-Type': 'application/json' }); res.end(JSON.stringify(obj)); };if (req.method === 'GET' && req.url === '/healthz') return json(200, { ok: true, service: 'mock-media' });-- // A public object's bytes (a private one is reached through /download, below).-- const open = req.url.match(/^\/o\/(med_[0-9A-Z]+)$/);++ // A public object's bytes (a private one is reached through /download, below). ?w=&fmt= asks++ // for Media's resized variant: answer webp with the width in the body, as Media would.++ const open = req.url.match(/^\/o\/(med_[0-9A-Z]+)(\?[^#]*)?$/);if (open && req.method === 'GET') {const o = uploads.find((u) => u.key === open[1]);if (!o) return json(404, { error: 'not found' });++ const q = new URL(req.url, 'http://mock-media').searchParams;++ if (q.get('w')) {++ res.writeHead(200, { 'Content-Type': 'image/webp' });++ return res.end(Buffer.concat([Buffer.from(`WEBP-${q.get('w')}-${q.get('fmt') || 'webp'}`), o.bytes]));++ }res.writeHead(200, { 'Content-Type': o.mime }); return res.end(o.bytes);}const del = req.url.match(/^\/api\/v2\/community\/objects\/(med_[0-9A-Z]+)$/);@@@ -354,6 +354,31 @@ const pastesApiModule = require('../server/pastes/api');assert.strictEqual(got.headers.get('cache-control'), 'public, max-age=31536000, immutable', 'a public screenshot is an immutable asset');});++ await check('a card asks for a 320/640 webp variant through the same proxy, and the first above the fold is eager', async () => {++ const jpeg = Buffer.concat([Buffer.from([0xff, 0xd8]), Buffer.from([0xff, 0xda, 0x00, 0x04, 0x00, 0x00]), Buffer.from('CARD-PIXELS'), Buffer.from([0xff, 0xd9])]);++ const fd = new FormData();++ fd.append('title', 'Card shot');++ fd.append('audience', 'public');++ fd.append('screenshot', new Blob([jpeg], { type: 'image/jpeg' }), 'c.jpg');++ const made = await t.get('/api/pastes/screenshot', { method: 'POST', cookies: [`ov_token=${alexJwt}`], body: fd });++ assert.strictEqual(made.status, 201, made.text);++ const slug = made.json().slug;++ const home = await call('/');++ assert.strictEqual(home.status, 200);++ assert.match(home.text, new RegExp(`srcset="[^"]*/p/${slug}/screenshot\\?w=320&fmt=webp 320w, [^"]*/p/${slug}/screenshot\\?w=640&fmt=webp 640w"`), 'both card widths');++ assert.match(home.text, /sizes="[^"]*"/, 'sizes is set');++ assert.match(home.text, /width="640" height="360"/, 'the card shape is declared');++ assert.match(home.text, /fetchpriority="high"/, 'the first picture is not lazy');++ const mine = await call('/my', { cookie: alexJwt });++ assert.match(mine.text, new RegExp(`<img src="/p/${slug}/screenshot\\?w=640&fmt=webp"`), 'a grid card uses the 640 picture');++ const at320 = await call(`/p/${slug}/screenshot?w=320&fmt=webp`);++ assert.strictEqual(at320.status, 200);++ assert.strictEqual(at320.headers.get('content-type'), 'image/webp');++ assert.strictEqual(at320.headers.get('cache-control'), 'public, max-age=31536000, immutable', 'a public card variant is immutable too');++ assert.ok(at320.text.includes('WEBP-320-webp'), 'the proxy asked Media for the 320 webp variant');++ assert.strictEqual((await call(`/p/${slug}/screenshot?w=1000&fmt=webp`)).status, 400, 'only the two card widths are accepted');++ });++await check('the move script re-hosts, deletes the old object, and retries a failed delete; --keep-old keeps it', async () => {const { createMediaObjects } = require('../server/media/objects');const { moveScreenshotsPrivate } = require('../scripts/move-screenshots-private');
@@ -71,6 +71,16 @@ const cache = require('openvibe-shared/cache-policy');
@@ -71,6 +71,16 @@ const cache = require('openvibe-shared/cache-policy');
const PUBLIC_DIR = path.join(__dirname, '..', 'public');
const PUBLIC_DIR = path.join(__dirname, '..', 'public');
const SLUG_RE = /^[A-Za-z0-9_-]{1,80}$/;
const SLUG_RE = /^[A-Za-z0-9_-]{1,80}$/;
+// A card asks the screenshot proxy for one of Media's two card widths (?w=320|640&fmt=webp). The
+// format is in the URL, never negotiated: a shared cache stores by URL and ignores Vary.
+const SHOT_CARD_WIDTHS = new Set([320, 640]);
+function screenshotVariant(query = {}) {
+ if (query.w === undefined && query.fmt === undefined) return null;
+ const w = Number(query.w);
+ const fmt = String(query.fmt || 'webp').toLowerCase();
+ if (!SHOT_CARD_WIDTHS.has(w) || fmt !== 'webp') return { error: 'w must be 320 or 640 and fmt webp' };
+ return { w, fmt };
+}
const pasteRef = (slug) => ({ service: 'community', type: 'paste', id: String(slug) });
const pasteRef = (slug) => ({ service: 'community', type: 'paste', id: String(slug) });
const VERSION = require('../package.json').version;
const VERSION = require('../package.json').version;
// The date /.well-known/security.txt must be refreshed by: one year on, a person re-reads and re-commits it.
// The date /.well-known/security.txt must be refreshed by: one year on, a person re-reads and re-commits it.
@@ -188,18 +198,21 @@ async function createApp(opts = {}) {
@@ -188,18 +198,21 @@ async function createApp(opts = {}) {
return { key: o.id, url: o.url, size: o.size_bytes, mime, media_ref: o.id, private: visibility === 'private' };
return { key: o.id, url: o.url, size: o.size_bytes, mime, media_ref: o.id, private: visibility === 'private' };
},
},
/** A paste's screenshot bytes, for the proxy (a private object through a signed download). */
/** A paste's screenshot bytes, for the proxy (a private object through a signed download). */
- async bytes(row) {
+ async bytes(row, variant = null) {
+ // A card's ?w=&fmt= rides on the same URL Media answered: public objects take it openly, a
+ // private one through the signature already in the URL (Media checks both before resizing).
+ const withVariant = (url) => (!variant ? url : `${url}${url.includes('?') ? '&' : '?'}w=${variant.w}&fmt=${variant.fmt}`);
const ref = String(row.media_ref || '');
const ref = String(row.media_ref || '');
const id = /^med_[0-9A-HJKMNP-TV-Z]{26}$/.test(ref) ? ref : (String(row.screenshot_url || '').match(/\/o\/(med_[0-9A-HJKMNP-TV-Z]{26})/) || [])[1];
const id = /^med_[0-9A-HJKMNP-TV-Z]{26}$/.test(ref) ? ref : (String(row.screenshot_url || '').match(/\/o\/(med_[0-9A-HJKMNP-TV-Z]{26})/) || [])[1];
if (id && mediaObjects.configured) {
if (id && mediaObjects.configured) {
try {
try {
- const url = await mediaObjects.downloadUrl(id);
+ const url = withVariant(await mediaObjects.downloadUrl(id));
const res = await (opts.fetchImpl || globalThis.fetch)(url, { signal: AbortSignal.timeout(30_000) });
const res = await (opts.fetchImpl || globalThis.fetch)(url, { signal: AbortSignal.timeout(30_000) });
if (res.ok) return { body: Buffer.from(await res.arrayBuffer()), type: res.headers.get('content-type') || 'application/octet-stream' };
if (res.ok) return { body: Buffer.from(await res.arrayBuffer()), type: res.headers.get('content-type') || 'application/octet-stream' };
} catch { /* an unlisted object still answers its stored URL below */ }
} catch { /* an unlisted object still answers its stored URL below */ }
}
}
if (!row.screenshot_url) return null;
if (!row.screenshot_url) return null;
- const res = await (opts.fetchImpl || globalThis.fetch)(row.screenshot_url, { signal: AbortSignal.timeout(30_000) });
+ const res = await (opts.fetchImpl || globalThis.fetch)(withVariant(row.screenshot_url), { signal: AbortSignal.timeout(30_000) });
if (!res.ok) return null;
if (!res.ok) return null;
return { body: Buffer.from(await res.arrayBuffer()), type: res.headers.get('content-type') || 'application/octet-stream' };
return { body: Buffer.from(await res.arrayBuffer()), type: res.headers.get('content-type') || 'application/octet-stream' };
},
},
@@ -631,10 +644,12 @@ async function createApp(opts = {}) {
@@ -631,10 +644,12 @@ async function createApp(opts = {}) {
// token. The Media object's own URL is never handed to a browser.
// token. The Media object's own URL is never handed to a browser.
app.get('/p/:slug/screenshot', withUser, async (req, res) => {
app.get('/p/:slug/screenshot', withUser, async (req, res) => {
if (!SLUG_RE.test(req.params.slug)) return notFound(res);
if (!SLUG_RE.test(req.params.slug)) return notFound(res);
+ const variant = screenshotVariant(req.query);
+ if (variant && variant.error) return res.status(400).type('text/plain').set('X-Content-Type-Options', 'nosniff').send(variant.error);
try {
try {
await app.locals.pastes.screenshotUrl(req.viewer, req.params.slug);
await app.locals.pastes.screenshotUrl(req.viewer, req.params.slug);
const row = await db.prepare('SELECT media_ref, screenshot_url, visibility, burn_after_read, locked FROM pastes WHERE slug = ? AND deleted_at IS NULL').get(req.params.slug);
const row = await db.prepare('SELECT media_ref, screenshot_url, visibility, burn_after_read, locked FROM pastes WHERE slug = ? AND deleted_at IS NULL').get(req.params.slug);
- const got = row ? await app.locals.pasteMedia.bytes(row) : null;
+ const got = row ? await app.locals.pasteMedia.bytes(row, variant) : null;
if (!got) return notFound(res);
if (!got) return notFound(res);
// A public paste's screenshot never changes under this URL, so it is cached like any other
// A public paste's screenshot never changes under this URL, so it is cached like any other
// content-addressed asset: the home page shows six of them and each is several hundred KB,
// content-addressed asset: the home page shows six of them and each is several hundred KB,
@@ -36,6 +36,13 @@ function mediaW(u, w) {
@@ -36,6 +36,13 @@ function mediaW(u, w) {
}
}
/** A screenshot's address: Community's checked proxy, never the Media object's own URL. */
/** A screenshot's address: Community's checked proxy, never the Media object's own URL. */
function screenshotSrc(p) { return `/p/${encodeURIComponent(p.slug)}/screenshot`; }
function screenshotSrc(p) { return `/p/${encodeURIComponent(p.slug)}/screenshot`; }
+/** A card-sized screenshot through the same proxy: Media's ladder has 320 and 640, webp by URL. */
+function screenshotVariant(p, w) { return `${screenshotSrc(p)}?w=${w}&fmt=webp`; }
+/** A card screenshot: two widths, the card's shape in width/height (no shift), lazy unless it is the LCP. */
+function screenshotImg(p, { eager = false, sizes = '(max-width: 620px) calc(100vw - 40px), 300px' } = {}) {
+ const srcset = [320, 640].map((w) => `${screenshotVariant(p, w)} ${w}w`).join(', ');
+ return `<img src="${esc(screenshotVariant(p, 640))}" srcset="${esc(srcset)}" sizes="${esc(sizes)}" width="640" height="360" alt="${esc(p.title || 'Screenshot')}"${eager ? ' fetchpriority="high"' : ' loading="lazy"'} decoding="async">`;
+}
function timeAgo(v) {
function timeAgo(v) {
const iso = ld.isoDate(v);
const iso = ld.isoDate(v);
@@ -107,13 +114,13 @@ function pagerHref(state, page) {
@@ -107,13 +114,13 @@ function pagerHref(state, page) {
}
}
// ── Paste card (grid item) ───────────────────────────────────
// ── Paste card (grid item) ───────────────────────────────────
-function pasteCard(p) {
+function pasteCard(p, { eager = false } = {}) {
const isShot = p.type === 'screenshot';
const isShot = p.type === 'screenshot';
const locked = !!p.locked;
const locked = !!p.locked;
const preview = locked
const preview = locked
? `<div class="card-locked"><i class="fa-solid fa-lock" aria-hidden="true"></i> Locked — OpenVibe can’t read what’s inside</div>`
? `<div class="card-locked"><i class="fa-solid fa-lock" aria-hidden="true"></i> Locked — OpenVibe can’t read what’s inside</div>`
: isShot
: isShot
- ? (p.screenshot_url ? `<div class="card-shot"><img src="${esc(screenshotSrc(p))}" alt="${esc(p.title || 'Screenshot')}" loading="lazy"></div>` : '')
+ ? (p.screenshot_url ? `<div class="card-shot">${screenshotImg(p, { eager })}</div>` : '')
: `<pre class="card-code" aria-hidden="true">${esc(String(p.content || '').slice(0, 220))}${String(p.content || '').length > 220 ? '…' : ''}</pre>`;
: `<pre class="card-code" aria-hidden="true">${esc(String(p.content || '').slice(0, 220))}${String(p.content || '').length > 220 ? '…' : ''}</pre>`;
const flags = `${p.pinned ? '<i class="fa-solid fa-thumbtack" title="Pinned"></i> ' : ''}${p.burn_after_read ? '<i class="fa-solid fa-fire" title="Burns after reading"></i> ' : ''}${locked ? '<i class="fa-solid fa-lock" title="Locked"></i> ' : ''}${p.is_nsfw ? '<span class="badge badge-nsfw">NSFW</span> ' : ''}`;
const flags = `${p.pinned ? '<i class="fa-solid fa-thumbtack" title="Pinned"></i> ' : ''}${p.burn_after_read ? '<i class="fa-solid fa-fire" title="Burns after reading"></i> ' : ''}${locked ? '<i class="fa-solid fa-lock" title="Locked"></i> ' : ''}${p.is_nsfw ? '<span class="badge badge-nsfw">NSFW</span> ' : ''}`;
return `<article class="card${p.is_nsfw ? ' card-nsfw' : ''}${locked ? ' card-is-locked' : ''}">
return `<article class="card${p.is_nsfw ? ' card-nsfw' : ''}${locked ? ' card-is-locked' : ''}">
@@ -129,7 +136,9 @@ function pasteCard(p) {
@@ -129,7 +136,9 @@ function pasteCard(p) {
}
}
function cardGrid(pastes, empty = 'Nothing here yet.') {
function cardGrid(pastes, empty = 'Nothing here yet.') {
if (!pastes || !pastes.length) return `<p class="empty">${esc(empty)}</p>`;
if (!pastes || !pastes.length) return `<p class="empty">${esc(empty)}</p>`;
- return `<div class="grid">${pastes.map(pasteCard).join('\n')}</div>`;
+ // The first picture in the grid is the one above the fold: it loads at high priority, the rest stay lazy.
+ const firstShot = pastes.findIndex((p) => p.type === 'screenshot' && p.screenshot_url && !p.locked);
+ return `<div class="grid">${pastes.map((p, i) => pasteCard(p, { eager: i === firstShot })).join('\n')}</div>`;
}
}
// ── Home ─────────────────────────────────────────────────────
// ── Home ─────────────────────────────────────────────────────
@@ -174,10 +183,10 @@ function homeStats(c = {}) {
@@ -174,10 +183,10 @@ function homeStats(c = {}) {
* says NSFW), else its first lines in the media (kit.card preview; the lists carry 300 characters) with its language in
* says NSFW), else its first lines in the media (kit.card preview; the lists carry 300 characters) with its language in
* the corner; who, views and when under the title.
* the corner; who, views and when under the title.
*/
*/
-function pasteKitCard(p) {
+function pasteKitCard(p, { eager = false } = {}) {
const isShot = p.type === 'screenshot';
const isShot = p.type === 'screenshot';
- const image = isShot && !p.is_nsfw ? screenshotSrc(p) : '';
- return kit.card({
+ const image = isShot && !p.is_nsfw ? screenshotVariant(p, 640) : '';
+ const card = kit.card({
href: `/p/${p.slug}`, title: p.title || 'Untitled', type: 'post', ratio: '16/9', image: image || '',
href: `/p/${p.slug}`, title: p.title || 'Untitled', type: 'post', ratio: '16/9', image: image || '',
badge: p.is_nsfw ? 'NSFW' : p.origin === 'ai' ? 'AI' : p.pinned ? 'Pinned' : '',
badge: p.is_nsfw ? 'NSFW' : p.origin === 'ai' ? 'AI' : p.pinned ? 'Pinned' : '',
corner: isShot ? '' : languageLabel(p.language || 'text'),
corner: isShot ? '' : languageLabel(p.language || 'text'),
@@ -185,6 +194,9 @@ function pasteKitCard(p) {
@@ -185,6 +194,9 @@ function pasteKitCard(p) {
who: authorName(p), avatar: avatarUrl(p, 68) || '',
who: authorName(p), avatar: avatarUrl(p, 68) || '',
count: [Number(p.views) || 0, Number(p.views) === 1 ? 'view' : 'views'], when: p.created_at || null,
count: [Number(p.views) || 0, Number(p.views) === 1 ? 'view' : 'views'], when: p.created_at || null,
});
});
+ // kit.card's own picture is one plain <img> before the avatar; give it both card widths, the
+ // shape the card draws and the LCP hint. Its markup is pinned with the shared version.
+ return image ? card.replace(/<img\b[^>]*>/, screenshotImg(p, { eager, sizes: '(max-width: 620px) 80vw, 300px' })) : card;
}
}
/** A section's head in the rails' look (room.css): its heading and the one link to the whole list. */
/** A section's head in the rails' look (room.css): its heading and the one link to the whole list. */
@@ -201,7 +213,10 @@ function kindChips(languages) {
@@ -201,7 +213,10 @@ function kindChips(languages) {
}
}
function latestShelf(latest, languages) {
function latestShelf(latest, languages) {
- const rail = kit.rail({ id: 'latest', title: 'Latest pastes', more: { href: '/pastes', label: 'See all' }, items: (latest || []).map(pasteKitCard) });
+ const list = latest || [];
+ // The newest screenshot in the first rail is the home's LCP picture on a phone: load it at high priority.
+ const firstShot = list.findIndex((p) => p.type === 'screenshot' && p.screenshot_url && !p.is_nsfw && !p.locked);
+ const rail = kit.rail({ id: 'latest', title: 'Latest pastes', more: { href: '/pastes', label: 'See all' }, items: list.map((p, i) => pasteKitCard(p, { eager: i === firstShot })) });
const none = `<section id="latest" aria-labelledby="latest-h">${shelfHead('latest-h', 'Latest pastes', { href: '/pastes', label: 'See all' })}${kit.empty({ title: 'No pastes yet', text: 'Be the first: paste something and share the link.', action: { href: '/new', label: 'Start a paste' } })}</section>`;
const none = `<section id="latest" aria-labelledby="latest-h">${shelfHead('latest-h', 'Latest pastes', { href: '/pastes', label: 'See all' })}${kit.empty({ title: 'No pastes yet', text: 'Be the first: paste something and share the link.', action: { href: '/new', label: 'Start a paste' } })}</section>`;
return `<div class="cm-sec">${rail || none}${languages && languages.length ? kindChips(languages) : ''}</div>`;
return `<div class="cm-sec">${rail || none}${languages && languages.length ? kindChips(languages) : ''}</div>`;
}
}
@@ -21,11 +21,17 @@ function start({ publicPem, issuer }) {
@@ -21,11 +21,17 @@ function start({ publicPem, issuer }) {
req.on('end', () => {
req.on('end', () => {
const json = (status, obj) => { res.writeHead(status, { 'Content-Type': 'application/json' }); res.end(JSON.stringify(obj)); };
const json = (status, obj) => { res.writeHead(status, { 'Content-Type': 'application/json' }); res.end(JSON.stringify(obj)); };
if (req.method === 'GET' && req.url === '/healthz') return json(200, { ok: true, service: 'mock-media' });
if (req.method === 'GET' && req.url === '/healthz') return json(200, { ok: true, service: 'mock-media' });
- // A public object's bytes (a private one is reached through /download, below).
- const open = req.url.match(/^\/o\/(med_[0-9A-Z]+)$/);
+ // A public object's bytes (a private one is reached through /download, below). ?w=&fmt= asks
+ // for Media's resized variant: answer webp with the width in the body, as Media would.
+ const open = req.url.match(/^\/o\/(med_[0-9A-Z]+)(\?[^#]*)?$/);
if (open && req.method === 'GET') {
if (open && req.method === 'GET') {
const o = uploads.find((u) => u.key === open[1]);
const o = uploads.find((u) => u.key === open[1]);
if (!o) return json(404, { error: 'not found' });
if (!o) return json(404, { error: 'not found' });
+ const q = new URL(req.url, 'http://mock-media').searchParams;
+ if (q.get('w')) {
+ res.writeHead(200, { 'Content-Type': 'image/webp' });
+ return res.end(Buffer.concat([Buffer.from(`WEBP-${q.get('w')}-${q.get('fmt') || 'webp'}`), o.bytes]));
+ }
res.writeHead(200, { 'Content-Type': o.mime }); return res.end(o.bytes);
res.writeHead(200, { 'Content-Type': o.mime }); return res.end(o.bytes);
}
}
const del = req.url.match(/^\/api\/v2\/community\/objects\/(med_[0-9A-Z]+)$/);
const del = req.url.match(/^\/api\/v2\/community\/objects\/(med_[0-9A-Z]+)$/);
@@ -354,6 +354,31 @@ const pastesApiModule = require('../server/pastes/api');
@@ -354,6 +354,31 @@ const pastesApiModule = require('../server/pastes/api');
assert.strictEqual(got.headers.get('cache-control'), 'public, max-age=31536000, immutable', 'a public screenshot is an immutable asset');
assert.strictEqual(got.headers.get('cache-control'), 'public, max-age=31536000, immutable', 'a public screenshot is an immutable asset');
});
});
+ await check('a card asks for a 320/640 webp variant through the same proxy, and the first above the fold is eager', async () => {
+ const jpeg = Buffer.concat([Buffer.from([0xff, 0xd8]), Buffer.from([0xff, 0xda, 0x00, 0x04, 0x00, 0x00]), Buffer.from('CARD-PIXELS'), Buffer.from([0xff, 0xd9])]);
+ const fd = new FormData();
+ fd.append('title', 'Card shot');
+ fd.append('audience', 'public');
+ fd.append('screenshot', new Blob([jpeg], { type: 'image/jpeg' }), 'c.jpg');
+ const made = await t.get('/api/pastes/screenshot', { method: 'POST', cookies: [`ov_token=${alexJwt}`], body: fd });
+ assert.strictEqual(made.status, 201, made.text);
+ const slug = made.json().slug;
+ const home = await call('/');
+ assert.strictEqual(home.status, 200);
+ assert.match(home.text, new RegExp(`srcset="[^"]*/p/${slug}/screenshot\\?w=320&fmt=webp 320w, [^"]*/p/${slug}/screenshot\\?w=640&fmt=webp 640w"`), 'both card widths');
+ assert.match(home.text, /sizes="[^"]*"/, 'sizes is set');
+ assert.match(home.text, /width="640" height="360"/, 'the card shape is declared');
+ assert.match(home.text, /fetchpriority="high"/, 'the first picture is not lazy');
+ const mine = await call('/my', { cookie: alexJwt });
+ assert.match(mine.text, new RegExp(`<img src="/p/${slug}/screenshot\\?w=640&fmt=webp"`), 'a grid card uses the 640 picture');
+ const at320 = await call(`/p/${slug}/screenshot?w=320&fmt=webp`);
+ assert.strictEqual(at320.status, 200);
+ assert.strictEqual(at320.headers.get('content-type'), 'image/webp');
+ assert.strictEqual(at320.headers.get('cache-control'), 'public, max-age=31536000, immutable', 'a public card variant is immutable too');
+ assert.ok(at320.text.includes('WEBP-320-webp'), 'the proxy asked Media for the 320 webp variant');
+ assert.strictEqual((await call(`/p/${slug}/screenshot?w=1000&fmt=webp`)).status, 400, 'only the two card widths are accepted');
+ });
+
await check('the move script re-hosts, deletes the old object, and retries a failed delete; --keep-old keeps it', async () => {
await check('the move script re-hosts, deletes the old object, and retries a failed delete; --keep-old keeps it', async () => {
const { createMediaObjects } = require('../server/media/objects');
const { createMediaObjects } = require('../server/media/objects');
const { moveScreenshotsPrivate } = require('../scripts/move-screenshots-private');
const { moveScreenshotsPrivate } = require('../scripts/move-screenshots-private');