forked from ORGANISATION-KA-INTERNATIONALE/FEDIVERSE-OKI
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:
@@ -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
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user