Optimise les images distantes au build et fiabilise la QA

- scripts/fetch-podcast-cover.mjs : pochettes Castopod (flux + épisodes)
  téléchargées et converties en WebP 512px au prebuild (2,3 Mo → 59 Ko)
- scripts/optimize-remote-images.mjs : vignettes PeerTube (640px), avatars
  (96px) et médias Mastodon (800px) optimisés en WebP + mapping
  src/lib/server/image-map.json consommé par mapImage()
- a11y : liens footer soulignés dans les paragraphes, cibles tactiles ≥24px
  sur les liens des posts Mastodon
- image-map.ts : import JSON statique (le createRequire pointait sur le bundle)
- doc : étape prebuild documentée (dépendance ImageMagick)

Lighthouse mobile : 97/100/100/100 — LCP 2,4s, TBT 50ms, CLS 0,
poids accueil 900 Ko (budgets du playbook respectés)
This commit is contained in:
sucupira
2026-07-23 03:37:03 -04:00
parent ffe7f12fb9
commit 6c2df9b1cd
73 changed files with 286 additions and 6 deletions
+3 -1
View File
@@ -6,7 +6,9 @@ Branche `svelte` : le hub est un site **100 % statique** (SvelteKit + adapter-st
```bash
npm ci
npm run build # génère build/ + injecte la CSP par page (scripts/postbuild-csp.mjs)
npm run build # prebuild : optimise les pochettes Castopod (scripts/fetch-podcast-cover.mjs,
# nécessite ImageMagick) et les vignettes/médias distants (scripts/optimize-remote-images.mjs)
# puis génère build/ + injecte la CSP par page (scripts/postbuild-csp.mjs)
npm run preview # vérification locale
```
+18
View File
@@ -15,6 +15,7 @@
"@sveltejs/adapter-static": "^3.0.10",
"@sveltejs/kit": "^2.63.0",
"@sveltejs/vite-plugin-svelte": "^7.1.2",
"@types/node": "^26.1.1",
"svelte": "^5.56.1",
"svelte-check": "^4.6.0",
"typescript": "^6.0.3",
@@ -2614,6 +2615,16 @@
"dev": true,
"license": "MIT"
},
"node_modules/@types/node": {
"version": "26.1.1",
"resolved": "https://registry.npmjs.org/@types/node/-/node-26.1.1.tgz",
"integrity": "sha512-nxAkRSVkN1Y0JC1W8ky/fTfkGsMmcrRsbx+3XoZE+rMOX71kLYTV7fLXpqud1GpbpP5TuffXFqfX7fH2GgZREw==",
"dev": true,
"license": "MIT",
"dependencies": {
"undici-types": "~8.3.0"
}
},
"node_modules/@types/resolve": {
"version": "1.20.2",
"resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz",
@@ -5986,6 +5997,13 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/undici-types": {
"version": "8.3.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz",
"integrity": "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==",
"dev": true,
"license": "MIT"
},
"node_modules/unicode-canonical-property-names-ecmascript": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.1.tgz",
+3 -1
View File
@@ -10,12 +10,14 @@
"prepare": "svelte-kit sync || echo ''",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
"postbuild": "node scripts/postbuild-csp.mjs"
"postbuild": "node scripts/postbuild-csp.mjs",
"prebuild": "node scripts/fetch-podcast-cover.mjs && node scripts/optimize-remote-images.mjs"
},
"devDependencies": {
"@sveltejs/adapter-static": "^3.0.10",
"@sveltejs/kit": "^2.63.0",
"@sveltejs/vite-plugin-svelte": "^7.1.2",
"@types/node": "^26.1.1",
"svelte": "^5.56.1",
"svelte-check": "^4.6.0",
"typescript": "^6.0.3",
+51
View File
@@ -0,0 +1,51 @@
import { writeFileSync, statSync, mkdirSync } from 'node:fs';
import { execFileSync } from 'node:child_process';
import { join, dirname, basename } from 'node:path';
import { fileURLToPath } from 'node:url';
const FEED_URL = 'https://kute.o-k-i.net/@annu_kute_cedric/feed';
const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..');
const EPISODES_DIR = join(ROOT, 'static', 'images', 'episodes');
const CHANNEL_COVER = join(ROOT, 'static', 'images', 'podcast-cover.webp');
mkdirSync(EPISODES_DIR, { recursive: true });
const xml = await (await fetch(FEED_URL)).text();
const urls = new Set();
const channel = xml.split('<item')[0] ?? '';
const channelMatch =
channel.match(/<itunes:image[^>]*href="([^"]+)"/i) ?? channel.match(/<url>([^<]+)<\/url>/i);
if (channelMatch) urls.add(channelMatch[1]);
for (const item of xml.split(/<item[\s>]/i).slice(1)) {
const match = item.match(/<itunes:image[^>]*href="([^"]+)"/i);
if (match) urls.add(match[1]);
}
let total = 0;
for (const url of urls) {
const isChannel = url === channelMatch?.[1];
const out = isChannel
? CHANNEL_COVER
: join(EPISODES_DIR, basename(url).replace(/\.(png|jpe?g|gif|webp)$/i, '.webp'));
try {
const res = await fetch(url);
if (!res.ok) {
console.warn(`[covers] HTTP ${res.status} pour ${url}`);
continue;
}
const source = Buffer.from(await res.arrayBuffer());
const tmp = `/tmp/opencode/cover-${Date.now()}`;
writeFileSync(tmp, source);
execFileSync('convert', [tmp, '-resize', '512x512>', '-quality', '82', '-define', 'webp:method=6', out]);
const size = statSync(out).size;
total += size;
console.log(`[covers] ${basename(out)} : ${Math.round(source.length / 1024)} Ko → ${Math.round(size / 1024)} Ko`);
} catch (err) {
console.warn(`[covers] échec pour ${url}:`, err.message);
}
}
console.log(`[covers] total optimisé : ${Math.round(total / 1024)} Ko`);
+112
View File
@@ -0,0 +1,112 @@
import { createHash } from 'node:crypto';
import { mkdirSync, writeFileSync, statSync } from 'node:fs';
import { execFileSync } from 'node:child_process';
import { join, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..');
const OUT_DIR = join(ROOT, 'static', 'images', 'remote');
const MAP_FILE = join(ROOT, 'src', 'lib', 'server', 'image-map.json');
const PEERTUBE = 'https://gade.o-k-i.net';
const MASTODON = 'https://bokante.o-k-i.net';
const MASTODON_ACCT = 'cedric';
const PRIORITY_CATEGORIES = [11, 15, 4, 9, 10];
mkdirSync(OUT_DIR, { recursive: true });
const urls = new Map();
function localPathFor(remoteUrl, width) {
const hash = createHash('sha1').update(remoteUrl).digest('hex').slice(0, 16);
return { local: `/images/remote/${hash}.webp`, width };
}
async function collectVideos(params, width) {
const query = new URLSearchParams({ isLocal: 'true', ...params });
try {
const res = await fetch(`${PEERTUBE}/api/v1/videos?${query}`);
if (!res.ok) return;
const data = await res.json();
for (const video of data?.data ?? []) {
if (video.previewPath) {
const url = `${PEERTUBE}${video.previewPath}`;
if (!urls.has(url)) urls.set(url, localPathFor(url, width));
}
const avatar = video.channel?.avatars?.[0]?.path;
if (avatar) {
const url = `${PEERTUBE}${avatar}`;
if (!urls.has(url)) urls.set(url, localPathFor(url, 96));
}
}
} catch (err) {
console.warn('[images] collectVideos échoué:', err.message);
}
}
// Pool récent (couvre récentes, shorts et l'essentiel des catégories) + tendances + chaque catégorie prioritaire
await collectVideos({ sort: '-publishedAt', count: '100' }, 640);
await collectVideos({ sort: '-trending', count: '6' }, 640);
for (const id of PRIORITY_CATEGORIES) {
await collectVideos({ categoryOneOf: String(id), sort: '-publishedAt', count: '6' }, 640);
}
// Médias de la timeline Mastodon
try {
const account = await (
await fetch(`${MASTODON}/api/v1/accounts/lookup?acct=${encodeURIComponent(MASTODON_ACCT)}`)
).json();
if (account?.id) {
const statuses = await (
await fetch(`${MASTODON}/api/v1/accounts/${account.id}/statuses?limit=10&exclude_replies=true`)
).json();
for (const status of statuses ?? []) {
const source = status.reblog ?? status;
for (const media of source.media_attachments ?? []) {
const url = media.preview_url ?? media.url;
if (url && !urls.has(url)) urls.set(url, localPathFor(url, 800));
}
}
}
} catch (err) {
console.warn('[images] médias Mastodon échoués:', err.message);
}
console.log(`[images] ${urls.size} image(s) distante(s) à optimiser`);
const map = {};
let total = 0;
let done = 0;
for (const [remote, { local, width }] of urls) {
const out = join(OUT_DIR, local.split('/').pop());
try {
const res = await fetch(remote);
if (!res.ok) {
console.warn(`[images] HTTP ${res.status} pour ${remote}`);
continue;
}
const source = Buffer.from(await res.arrayBuffer());
const tmp = `/tmp/opencode/img-${done}`;
writeFileSync(tmp, source);
execFileSync('convert', [
tmp,
'-auto-orient',
'-resize',
`${width}x${width}>`,
'-quality',
'80',
'-define',
'webp:method=6',
out
]);
map[remote] = local;
total += statSync(out).size;
done++;
} catch (err) {
console.warn(`[images] échec pour ${remote}:`, err.message);
}
}
writeFileSync(MAP_FILE, JSON.stringify(map, null, '\t') + '\n');
console.log(`[images] ${done} optimisée(s), ${Math.round(total / 1024)} Ko au total`);
+2
View File
@@ -231,5 +231,7 @@
.footer-federated a,
.footer-copyright a {
color: var(--accent);
text-decoration: underline;
text-underline-offset: 0.15em;
}
</style>
@@ -79,6 +79,7 @@
.post-content {
font-size: 0.95rem;
line-height: 1.9;
overflow-wrap: anywhere;
}
@@ -86,6 +87,8 @@
color: var(--accent);
text-decoration: underline;
text-underline-offset: 0.15em;
display: inline-block;
padding: 0.35rem 0.15rem;
}
.post-media {
+18 -1
View File
@@ -1,7 +1,20 @@
import { existsSync } from 'node:fs';
import { castopod } from '$lib/config';
import { fetchText } from './http';
import type { Episode } from '$lib/types';
// Pochette du flux optimisée au prebuild (scripts/fetch-podcast-cover.mjs)
const LOCAL_COVER = '/images/podcast-cover.webp';
const hasLocalCover = existsSync(`static${LOCAL_COVER}`);
function localEpisodeCover(remoteUrl: string | null): string | null {
if (!remoteUrl) return null;
const file = remoteUrl.split('/').pop()?.replace(/\.(png|jpe?g|gif|webp)$/i, '.webp');
if (!file) return remoteUrl;
const local = `/images/episodes/${file}`;
return existsSync(`static${local}`) ? local : remoteUrl;
}
function tag(xml: string, name: string): string {
const cdata = new RegExp(`<${name}[^>]*><!\\[CDATA\\[([\\s\\S]*?)\\]\\]></${name}>`, 'i');
const plain = new RegExp(`<${name}[^>]*>([\\s\\S]*?)</${name}>`, 'i');
@@ -53,6 +66,10 @@ async function getEpisodesForSlug(slug: string): Promise<Episode[]> {
const link = tag(body, 'link');
const guid = tag(body, 'guid') || link || `${slug}-${index}`;
const description = stripTags(tag(body, 'description') || tag(body, 'itunes:summary'));
const remoteImage = attr(body, 'itunes:image', 'href') || channelImage || null;
// Les pochettes du flux (souvent lourdes) sont remplacées par leurs versions optimisées au build
const image =
hasLocalCover && remoteImage === channelImage ? LOCAL_COVER : localEpisodeCover(remoteImage);
return {
id: guid,
title,
@@ -61,7 +78,7 @@ async function getEpisodesForSlug(slug: string): Promise<Episode[]> {
audioType: attr(body, 'enclosure', 'type') || 'audio/mpeg',
duration: parseDuration(tag(body, 'itunes:duration')),
date: tag(body, 'pubDate'),
image: attr(body, 'itunes:image', 'href') || channelImage || null,
image,
link,
podcast: slug
};
+61
View File
@@ -0,0 +1,61 @@
{
"https://gade.o-k-i.net/lazy-static/thumbnails/ee8fe93e-62b0-45a3-b200-3b9a7d34ad37.jpg": "/images/remote/8d484efeec42b22e.webp",
"https://gade.o-k-i.net/lazy-static/thumbnails/713279b3-0a30-4e88-a4cd-2bd9015706ff.jpg": "/images/remote/2b57d5382f67e8a6.webp",
"https://gade.o-k-i.net/lazy-static/avatars/b483874d-df89-4403-aabb-684f4eeffce8.png": "/images/remote/b82953cc708a503e.webp",
"https://gade.o-k-i.net/lazy-static/thumbnails/65458991-cc2d-4f88-b04b-1ecb4c831196.jpg": "/images/remote/1e1a6bad9723ecba.webp",
"https://gade.o-k-i.net/lazy-static/thumbnails/19ed28dd-429b-4b86-9f5b-032b0fd8a555.jpg": "/images/remote/a105bc4c5812eb6d.webp",
"https://gade.o-k-i.net/lazy-static/thumbnails/a02a736a-1ca2-48cb-bd04-6adb8ab8bd3f.jpg": "/images/remote/7e49bf87876caf2b.webp",
"https://gade.o-k-i.net/lazy-static/thumbnails/df2d06a4-c5f1-40b2-85c6-bb2cc6f75f0f.jpg": "/images/remote/23a4aee1ddac377d.webp",
"https://gade.o-k-i.net/lazy-static/thumbnails/ac2d53df-9432-419a-bafa-4e92a23a065a.png": "/images/remote/ca91ce3a8c6c048c.webp",
"https://gade.o-k-i.net/lazy-static/avatars/1af26d32-dd03-4ef9-9cb7-e4680630b93a.png": "/images/remote/d3efbaf0d582a4de.webp",
"https://gade.o-k-i.net/lazy-static/thumbnails/7a40594d-6ee8-4a93-a891-acf6a06706e5.jpg": "/images/remote/d668c254603561d5.webp",
"https://gade.o-k-i.net/lazy-static/thumbnails/c433fb22-085b-4b95-840d-a244a16b9d86.jpg": "/images/remote/7f3ee600df8bf0a9.webp",
"https://gade.o-k-i.net/lazy-static/thumbnails/eeee79a2-13a7-41f3-8cf7-c0356e8b4af1.jpg": "/images/remote/8b91a7098be22de6.webp",
"https://gade.o-k-i.net/lazy-static/avatars/68710d9c-f623-4db3-950c-5dcb9519ea89.png": "/images/remote/72f6f94d5459b36c.webp",
"https://gade.o-k-i.net/lazy-static/thumbnails/aa49c8f7-e746-4087-ac8a-b18911ae34bb.jpg": "/images/remote/953bc828f4a909ed.webp",
"https://gade.o-k-i.net/lazy-static/thumbnails/04471380-b2fc-410d-a255-d1ed2c566582.jpg": "/images/remote/dd3dfb419e66a44c.webp",
"https://gade.o-k-i.net/lazy-static/thumbnails/f9dcc845-f6dc-4383-abac-192a74d3ddbb.jpg": "/images/remote/2cd6b3491e675fe9.webp",
"https://gade.o-k-i.net/lazy-static/thumbnails/ee9d0e2b-a266-4e5e-b708-f8a68dd38cd8.jpg": "/images/remote/7794b4aa91e0886e.webp",
"https://gade.o-k-i.net/lazy-static/thumbnails/63aab582-1e95-403f-8fd4-2cd1355e4c48.jpg": "/images/remote/06c7423ee81dbae1.webp",
"https://gade.o-k-i.net/lazy-static/thumbnails/f06d5990-2113-428d-8364-cedc2dd5163f.jpg": "/images/remote/38183e7822943589.webp",
"https://gade.o-k-i.net/lazy-static/thumbnails/e034ba48-4696-4747-8090-36a1ffb7ead8.jpg": "/images/remote/83b50cf812d906f0.webp",
"https://gade.o-k-i.net/lazy-static/thumbnails/8a1726da-b05c-4f4c-9d02-a84aa80089cb.jpg": "/images/remote/ce0279994a8d0c22.webp",
"https://gade.o-k-i.net/lazy-static/thumbnails/eae89876-94f3-4c32-b5bb-d1cd44551ca5.jpg": "/images/remote/626f2aa1c4d7030a.webp",
"https://gade.o-k-i.net/lazy-static/thumbnails/82061a8c-4435-4759-b905-5c8a10eb8a8f.jpg": "/images/remote/efa5aca9038f196e.webp",
"https://gade.o-k-i.net/lazy-static/thumbnails/b1472219-de2b-4a85-8f95-bd544eaf5797.jpg": "/images/remote/940f43beb51345fa.webp",
"https://gade.o-k-i.net/lazy-static/thumbnails/28acfa31-e05d-49c6-89b0-2da42502416e.jpg": "/images/remote/64ba2429385bdd5f.webp",
"https://gade.o-k-i.net/lazy-static/thumbnails/9b9fde1a-be2f-4c7c-8372-f3d65aad73ac.jpg": "/images/remote/674d7ec610419571.webp",
"https://gade.o-k-i.net/lazy-static/avatars/35d753a1-cd9f-4f64-999f-483ef4c8d0f6.jpg": "/images/remote/ff2cc37374f4a362.webp",
"https://gade.o-k-i.net/lazy-static/thumbnails/730fcddd-645e-4377-9b93-f38835818b7d.jpg": "/images/remote/a7ced37c3c927888.webp",
"https://gade.o-k-i.net/lazy-static/thumbnails/66cf4194-3e6c-4c09-adb1-b3ae6591c6a0.jpg": "/images/remote/4ebf139c7ad742c0.webp",
"https://gade.o-k-i.net/lazy-static/avatars/bcdc941d-1be8-4c67-b0c7-d40313ef9aaf.png": "/images/remote/5a4d501bd6d6088c.webp",
"https://gade.o-k-i.net/lazy-static/thumbnails/14fa6efc-9a9a-4c36-a044-02300a92af47.jpg": "/images/remote/461e77261af8212f.webp",
"https://gade.o-k-i.net/lazy-static/thumbnails/7b7768e4-eeac-4024-a59e-052bdfbefe25.jpg": "/images/remote/a652b99264ef113c.webp",
"https://gade.o-k-i.net/lazy-static/thumbnails/f6730ffc-8236-4df3-83f9-bce65b1c5973.jpg": "/images/remote/cc2a19d9f6ac44b1.webp",
"https://gade.o-k-i.net/lazy-static/thumbnails/9fe95de1-ea17-41f9-ae02-d4b93f33f3e9.jpg": "/images/remote/e0d61d23865f0262.webp",
"https://gade.o-k-i.net/lazy-static/thumbnails/a1799f29-ffd4-4c9f-a4a6-7cdfe56cd48c.png": "/images/remote/880d1ba7cf4187a5.webp",
"https://gade.o-k-i.net/lazy-static/avatars/df91692b-df37-4350-a5e4-3b9a1f56af8e.png": "/images/remote/035f2b8e8f5ee0dd.webp",
"https://gade.o-k-i.net/lazy-static/thumbnails/ad988f0b-1fd7-46bc-93b2-0d8b9587b880.jpg": "/images/remote/2713aa6bd6511e7e.webp",
"https://gade.o-k-i.net/lazy-static/thumbnails/f60a9ca6-7a62-44c2-a242-016fdc530577.jpg": "/images/remote/67b3bca007b144c5.webp",
"https://gade.o-k-i.net/lazy-static/thumbnails/3f130e71-7cfd-473b-8ea2-80df832c1241.jpg": "/images/remote/b9e62fd5fa9ce102.webp",
"https://gade.o-k-i.net/lazy-static/thumbnails/61f25602-6dd7-4fe1-9269-af0fe3f73527.jpg": "/images/remote/e7c1aec80a4fc8b6.webp",
"https://gade.o-k-i.net/lazy-static/thumbnails/23fdd0ad-8256-45b2-b8fa-15496fef9d3f.jpg": "/images/remote/d9d7e08baf18edfe.webp",
"https://gade.o-k-i.net/lazy-static/thumbnails/ce2c5680-a787-4028-8768-ca64e8c18fba.jpg": "/images/remote/11f1ace7f75ed731.webp",
"https://gade.o-k-i.net/lazy-static/thumbnails/45e971a5-3131-46bc-8a39-db10d68dbd29.jpg": "/images/remote/09eb4898f46c76db.webp",
"https://gade.o-k-i.net/lazy-static/avatars/60cbad62-3881-4e34-aa3a-726b80b5d334.png": "/images/remote/62c8fe68d5ea8108.webp",
"https://gade.o-k-i.net/lazy-static/thumbnails/bb2aafc3-46bd-4059-a3a7-cd08d4097113.jpg": "/images/remote/4fc5ba257091f2f6.webp",
"https://gade.o-k-i.net/lazy-static/thumbnails/160eda0d-326c-480b-81e4-aabe88b46729.jpg": "/images/remote/8ab957ee76be64c8.webp",
"https://gade.o-k-i.net/lazy-static/thumbnails/d00d12e6-c5c8-4df8-b0fd-5a49f9675bd4.jpg": "/images/remote/61d4b3032d0c87dd.webp",
"https://gade.o-k-i.net/lazy-static/thumbnails/3521d0ed-ae0d-4610-88ff-96c4703d8849.jpg": "/images/remote/d47f4bcf0566064e.webp",
"https://gade.o-k-i.net/lazy-static/thumbnails/05e25c9a-ef93-4ec4-b7a9-5453745d7af5.jpg": "/images/remote/4ceaa3d9b7bf41aa.webp",
"https://gade.o-k-i.net/lazy-static/thumbnails/32220182-8186-4e66-a2d6-03c722fe13af.jpg": "/images/remote/254e260419aa661e.webp",
"https://gade.o-k-i.net/lazy-static/thumbnails/5e7b2bd0-edfe-49ca-abee-fbe21321c3d0.jpg": "/images/remote/57e3480631c3524f.webp",
"https://gade.o-k-i.net/lazy-static/thumbnails/cbe90101-8276-4a39-bce0-4e3b5ade98a7.jpg": "/images/remote/1f6ca697aa450bf5.webp",
"https://gade.o-k-i.net/lazy-static/thumbnails/6ee72db1-d86c-4e6c-8deb-de1672ef40f5.png": "/images/remote/16b11ca906d64dcf.webp",
"https://bokante.o-k-i.net/system/media_attachments/files/116/961/470/179/848/592/small/a1f65d6923b825c6.png": "/images/remote/f0e99c1fd601e8f4.webp",
"https://bokante.o-k-i.net/system/media_attachments/files/116/961/462/013/833/918/small/ab87d35ebbc5b3ff.jpg": "/images/remote/b0f9e20257ed9d70.webp",
"https://bokante.o-k-i.net/system/media_attachments/files/116/961/174/942/983/872/small/9a39261de8c0a287.png": "/images/remote/ac55ca23fae564d8.webp",
"https://bokante.o-k-i.net/system/media_attachments/files/116/944/486/745/892/539/small/fce48c574fa53917.png": "/images/remote/69a1a813f6b9324e.webp",
"https://bokante.o-k-i.net/system/media_attachments/files/116/943/727/051/792/872/small/d7c8bcea86822972.png": "/images/remote/656fc21dfbab689a.webp",
"https://bokante.o-k-i.net/system/media_attachments/files/116/936/109/649/552/039/small/c80ad03182b67097.png": "/images/remote/e925811cec427259.webp"
}
+10
View File
@@ -0,0 +1,10 @@
import { existsSync } from 'node:fs';
import map from './image-map.json' with { type: 'json' };
const imageMap = map as Record<string, string>;
export function mapImage(remoteUrl: string): string {
const local = imageMap[remoteUrl];
if (local && existsSync(`static${local}`)) return local;
return remoteUrl;
}
+2 -1
View File
@@ -1,5 +1,6 @@
import { mastodon } from '$lib/config';
import { fetchJson, sanitizeHtml } from './http';
import { mapImage } from './image-map';
import type { Post } from '$lib/types';
interface MastodonMedia {
@@ -46,7 +47,7 @@ export async function getMastodonPosts(count = mastodon.maxPosts): Promise<Post[
.filter((media) => media.url)
.map((media) => ({
url: media.url as string,
previewUrl: media.preview_url ?? null,
previewUrl: media.preview_url ? mapImage(media.preview_url) : mapImage(media.url as string),
description: media.description ?? ''
})),
repliesCount: status.replies_count ?? 0,
+3 -2
View File
@@ -1,5 +1,6 @@
import { peertube } from '$lib/config';
import { fetchJson, markdownToHtml, sanitizeHtml } from './http';
import { mapImage } from './image-map';
import type {
DownloadOption,
SearchEntry,
@@ -44,11 +45,11 @@ function formatVideos(videos: PtVideo[] = []): VideoSummary[] {
return videos.map((video) => ({
id: video.uuid,
title: video.name,
thumbnail: video.previewPath ? `${BASE}${video.previewPath}` : '/images/logo.png',
thumbnail: video.previewPath ? mapImage(`${BASE}${video.previewPath}`) : '/images/logo.png',
duration: video.duration,
channel: video.channel?.displayName ?? '',
channelAvatar: video.channel?.avatars?.[0]?.path
? `${BASE}${video.channel.avatars[0].path}`
? mapImage(`${BASE}${video.channel.avatars[0].path}`)
: '/images/logo.png',
views: video.views,
date: video.publishedAt,
Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 342 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 586 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 480 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 992 B