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)
@@ -6,7 +6,9 @@ Branche `svelte` : le hub est un site **100 % statique** (SvelteKit + adapter-st
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm ci
|
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
|
npm run preview # vérification locale
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
"@sveltejs/adapter-static": "^3.0.10",
|
"@sveltejs/adapter-static": "^3.0.10",
|
||||||
"@sveltejs/kit": "^2.63.0",
|
"@sveltejs/kit": "^2.63.0",
|
||||||
"@sveltejs/vite-plugin-svelte": "^7.1.2",
|
"@sveltejs/vite-plugin-svelte": "^7.1.2",
|
||||||
|
"@types/node": "^26.1.1",
|
||||||
"svelte": "^5.56.1",
|
"svelte": "^5.56.1",
|
||||||
"svelte-check": "^4.6.0",
|
"svelte-check": "^4.6.0",
|
||||||
"typescript": "^6.0.3",
|
"typescript": "^6.0.3",
|
||||||
@@ -2614,6 +2615,16 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/@types/resolve": {
|
||||||
"version": "1.20.2",
|
"version": "1.20.2",
|
||||||
"resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz",
|
"resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz",
|
||||||
@@ -5986,6 +5997,13 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"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": {
|
"node_modules/unicode-canonical-property-names-ecmascript": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.1.tgz",
|
||||||
|
|||||||
@@ -10,12 +10,14 @@
|
|||||||
"prepare": "svelte-kit sync || echo ''",
|
"prepare": "svelte-kit sync || echo ''",
|
||||||
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
||||||
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
"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": {
|
"devDependencies": {
|
||||||
"@sveltejs/adapter-static": "^3.0.10",
|
"@sveltejs/adapter-static": "^3.0.10",
|
||||||
"@sveltejs/kit": "^2.63.0",
|
"@sveltejs/kit": "^2.63.0",
|
||||||
"@sveltejs/vite-plugin-svelte": "^7.1.2",
|
"@sveltejs/vite-plugin-svelte": "^7.1.2",
|
||||||
|
"@types/node": "^26.1.1",
|
||||||
"svelte": "^5.56.1",
|
"svelte": "^5.56.1",
|
||||||
"svelte-check": "^4.6.0",
|
"svelte-check": "^4.6.0",
|
||||||
"typescript": "^6.0.3",
|
"typescript": "^6.0.3",
|
||||||
|
|||||||
@@ -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`);
|
||||||
@@ -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`);
|
||||||
@@ -231,5 +231,7 @@
|
|||||||
.footer-federated a,
|
.footer-federated a,
|
||||||
.footer-copyright a {
|
.footer-copyright a {
|
||||||
color: var(--accent);
|
color: var(--accent);
|
||||||
|
text-decoration: underline;
|
||||||
|
text-underline-offset: 0.15em;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -79,6 +79,7 @@
|
|||||||
|
|
||||||
.post-content {
|
.post-content {
|
||||||
font-size: 0.95rem;
|
font-size: 0.95rem;
|
||||||
|
line-height: 1.9;
|
||||||
overflow-wrap: anywhere;
|
overflow-wrap: anywhere;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -86,6 +87,8 @@
|
|||||||
color: var(--accent);
|
color: var(--accent);
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
text-underline-offset: 0.15em;
|
text-underline-offset: 0.15em;
|
||||||
|
display: inline-block;
|
||||||
|
padding: 0.35rem 0.15rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.post-media {
|
.post-media {
|
||||||
|
|||||||
@@ -1,7 +1,20 @@
|
|||||||
|
import { existsSync } from 'node:fs';
|
||||||
import { castopod } from '$lib/config';
|
import { castopod } from '$lib/config';
|
||||||
import { fetchText } from './http';
|
import { fetchText } from './http';
|
||||||
import type { Episode } from '$lib/types';
|
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 {
|
function tag(xml: string, name: string): string {
|
||||||
const cdata = new RegExp(`<${name}[^>]*><!\\[CDATA\\[([\\s\\S]*?)\\]\\]></${name}>`, 'i');
|
const cdata = new RegExp(`<${name}[^>]*><!\\[CDATA\\[([\\s\\S]*?)\\]\\]></${name}>`, 'i');
|
||||||
const plain = new RegExp(`<${name}[^>]*>([\\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 link = tag(body, 'link');
|
||||||
const guid = tag(body, 'guid') || link || `${slug}-${index}`;
|
const guid = tag(body, 'guid') || link || `${slug}-${index}`;
|
||||||
const description = stripTags(tag(body, 'description') || tag(body, 'itunes:summary'));
|
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 {
|
return {
|
||||||
id: guid,
|
id: guid,
|
||||||
title,
|
title,
|
||||||
@@ -61,7 +78,7 @@ async function getEpisodesForSlug(slug: string): Promise<Episode[]> {
|
|||||||
audioType: attr(body, 'enclosure', 'type') || 'audio/mpeg',
|
audioType: attr(body, 'enclosure', 'type') || 'audio/mpeg',
|
||||||
duration: parseDuration(tag(body, 'itunes:duration')),
|
duration: parseDuration(tag(body, 'itunes:duration')),
|
||||||
date: tag(body, 'pubDate'),
|
date: tag(body, 'pubDate'),
|
||||||
image: attr(body, 'itunes:image', 'href') || channelImage || null,
|
image,
|
||||||
link,
|
link,
|
||||||
podcast: slug
|
podcast: slug
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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"
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import { mastodon } from '$lib/config';
|
import { mastodon } from '$lib/config';
|
||||||
import { fetchJson, sanitizeHtml } from './http';
|
import { fetchJson, sanitizeHtml } from './http';
|
||||||
|
import { mapImage } from './image-map';
|
||||||
import type { Post } from '$lib/types';
|
import type { Post } from '$lib/types';
|
||||||
|
|
||||||
interface MastodonMedia {
|
interface MastodonMedia {
|
||||||
@@ -46,7 +47,7 @@ export async function getMastodonPosts(count = mastodon.maxPosts): Promise<Post[
|
|||||||
.filter((media) => media.url)
|
.filter((media) => media.url)
|
||||||
.map((media) => ({
|
.map((media) => ({
|
||||||
url: media.url as string,
|
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 ?? ''
|
description: media.description ?? ''
|
||||||
})),
|
})),
|
||||||
repliesCount: status.replies_count ?? 0,
|
repliesCount: status.replies_count ?? 0,
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { peertube } from '$lib/config';
|
import { peertube } from '$lib/config';
|
||||||
import { fetchJson, markdownToHtml, sanitizeHtml } from './http';
|
import { fetchJson, markdownToHtml, sanitizeHtml } from './http';
|
||||||
|
import { mapImage } from './image-map';
|
||||||
import type {
|
import type {
|
||||||
DownloadOption,
|
DownloadOption,
|
||||||
SearchEntry,
|
SearchEntry,
|
||||||
@@ -44,11 +45,11 @@ function formatVideos(videos: PtVideo[] = []): VideoSummary[] {
|
|||||||
return videos.map((video) => ({
|
return videos.map((video) => ({
|
||||||
id: video.uuid,
|
id: video.uuid,
|
||||||
title: video.name,
|
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,
|
duration: video.duration,
|
||||||
channel: video.channel?.displayName ?? '',
|
channel: video.channel?.displayName ?? '',
|
||||||
channelAvatar: video.channel?.avatars?.[0]?.path
|
channelAvatar: video.channel?.avatars?.[0]?.path
|
||||||
? `${BASE}${video.channel.avatars[0].path}`
|
? mapImage(`${BASE}${video.channel.avatars[0].path}`)
|
||||||
: '/images/logo.png',
|
: '/images/logo.png',
|
||||||
views: video.views,
|
views: video.views,
|
||||||
date: video.publishedAt,
|
date: video.publishedAt,
|
||||||
|
|||||||
|
After Width: | Height: | Size: 49 KiB |
|
After Width: | Height: | Size: 59 KiB |
|
After Width: | Height: | Size: 342 B |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 27 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 40 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 47 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 52 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 40 KiB |
|
After Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 39 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 31 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 27 KiB |
|
After Width: | Height: | Size: 31 KiB |
|
After Width: | Height: | Size: 586 B |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 53 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 43 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 39 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 48 KiB |
|
After Width: | Height: | Size: 480 B |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 992 B |