Skip to content

Commit d68f764

Browse files
committed
Implementación de imágenes predescargadas de Directus para eliminar dependencia con el CMS en producción.
1 parent 235bb9e commit d68f764

15 files changed

Lines changed: 221 additions & 19 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,4 @@ node_modules
88
!.env.example
99
vite.config.js.timestamp-*
1010
vite.config.ts.timestamp-*
11+
/static/images/

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44
"private": true,
55
"scripts": {
66
"dev": "vite dev",
7-
"build": "vite build",
7+
"build": "node --env-file-if-exists=.env scripts/prebuild.js && vite build",
8+
"prebuild-images": "node --env-file-if-exists=.env scripts/prebuild.js",
89
"preview": "vite preview",
910
"check": "svelte-kit sync && svelte-check --tsconfig ./jsconfig.json",
1011
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./jsconfig.json --watch",

scripts/prebuild.js

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
// Descarga y convierte a WebP todas las imagenes del CMS antes del build.
2+
// Se ejecuta automaticamente al correr el script de build.
3+
4+
import { writeFileSync, mkdirSync, existsSync } from 'fs';
5+
import { join, dirname } from 'path';
6+
import { fileURLToPath } from 'url';
7+
8+
const __dirname = dirname(fileURLToPath(import.meta.url));
9+
const ROOT = join(__dirname, '..');
10+
const IMAGES_DIR = join(ROOT, 'static', 'images');
11+
const MANIFEST_DIR = join(ROOT, 'src', 'lib', 'generated');
12+
const MANIFEST_PATH = join(MANIFEST_DIR, 'imageManifest.json');
13+
14+
const DIRECTUS_URL = 'https://content.codeandomexico.org';
15+
const SIZES = [400, 800, 1200];
16+
17+
const token = process.env.DIRECTUS_TOKEN;
18+
if (!token) {
19+
console.error('Error: DIRECTUS_TOKEN no encontrado en variables de entorno');
20+
process.exit(1);
21+
}
22+
23+
async function fetchItems(collection, fields) {
24+
const url = `${DIRECTUS_URL}/items/${collection}?fields=${fields.join(',')}&filter[status][_eq]=published&access_token=${token}`;
25+
const res = await fetch(url);
26+
if (!res.ok) throw new Error(`Error al obtener ${collection}: ${res.statusText}`);
27+
const json = await res.json();
28+
return json.data;
29+
}
30+
31+
async function downloadImage(assetId, width) {
32+
const url = `${DIRECTUS_URL}/assets/${assetId}?width=${width}&format=webp&quality=80&access_token=${token}`;
33+
const res = await fetch(url);
34+
if (!res.ok) throw new Error(`Error al descargar ${assetId} (${width}w): ${res.statusText}`);
35+
return Buffer.from(await res.arrayBuffer());
36+
}
37+
38+
async function main() {
39+
mkdirSync(IMAGES_DIR, { recursive: true });
40+
mkdirSync(MANIFEST_DIR, { recursive: true });
41+
42+
const [posts, projects] = await Promise.all([
43+
fetchItems('Blog_Posts', ['post_image']),
44+
fetchItems('projects', ['image'])
45+
]);
46+
47+
const assetIds = [
48+
...new Set([
49+
...posts.map((p) => p.post_image).filter(Boolean),
50+
...projects.map((p) => p.image).filter(Boolean)
51+
])
52+
];
53+
54+
console.log(`Descargando ${assetIds.length} imagenes en ${SIZES.length} tamaños...`);
55+
56+
const manifest = {};
57+
let count = 0;
58+
59+
for (const assetId of assetIds) {
60+
manifest[assetId] = {
61+
sm: `/images/${assetId}-400w.webp`,
62+
md: `/images/${assetId}-800w.webp`,
63+
lg: `/images/${assetId}-1200w.webp`
64+
};
65+
66+
await Promise.all(
67+
SIZES.map(async (size) => {
68+
const buffer = await downloadImage(assetId, size);
69+
writeFileSync(join(IMAGES_DIR, `${assetId}-${size}w.webp`), buffer);
70+
})
71+
);
72+
73+
count++;
74+
console.log(` [${count}/${assetIds.length}] ${assetId}`);
75+
}
76+
77+
writeFileSync(MANIFEST_PATH, JSON.stringify(manifest, null, 2));
78+
console.log(`\nManifiesto guardado: ${MANIFEST_PATH}`);
79+
console.log(`Imagenes guardadas: ${IMAGES_DIR}`);
80+
}
81+
82+
main().catch((err) => {
83+
console.error('Error en prebuild:', err);
84+
process.exit(1);
85+
});

src/components/BlogHero.svelte

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,14 @@
22
import Badge from './Badge.svelte';
33
import ReadingTime from './ReadingTime.svelte';
44
import HumanDate from './HumanDate.svelte';
5+
import CmsImage from '@/components/CmsImage.svelte';
56
/**
67
* @typedef {Object} Props
78
* @property {Array<string>} [tags]
89
* @property {string} [title]
910
* @property {string} [slug]
1011
* @property {string} [date]
11-
* @property {string} [image]
12+
* @property {string} [assetId]
1213
* @property {Array<{authors_id: {name: string}}>} [authors]
1314
* @property {string} [content]
1415
*/
@@ -19,15 +20,15 @@
1920
title = 'Blog',
2021
slug = '',
2122
date = '',
22-
image = '',
23+
assetId = '',
2324
authors = [],
2425
content = ''
2526
} = $props();
2627
</script>
2728

2829
<div class="hero flex gap-2 md:gap-4 flex-col md:flex-row">
2930
<div class="hero_left aspect-16/9 border border-cmxblack border-2">
30-
<img src={image} alt="" class="" />
31+
<CmsImage {assetId} alt="" class="" />
3132
</div>
3233
<div class="hero_right flex flex-col gap-4">
3334
<div></div>

src/components/Cards/ArticleCard.svelte

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
<script>
22
import Badge from '@/components/Badge.svelte';
33
import HumanDate from '../HumanDate.svelte';
4-
let { slug, title, date, image, tags, authors } = $props();
4+
import CmsImage from '@/components/CmsImage.svelte';
5+
let { slug, title, date, assetId, tags, authors } = $props();
56
</script>
67

78
<div class="w-full">
89
<div class="md:aspect-video aspect-square mb-4 border border-cmxblack border-1">
910
<a href="/blog/{slug}">
10-
<img class="object-cover w-full h-full" src={image} alt="Card" />
11+
<CmsImage {assetId} alt="Card" class="object-cover w-full h-full" />
1112
</a>
1213
</div>
1314
<div class="space-y-2 flex flex-col gap-2">

src/components/Cards/ContentCard.svelte

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,16 @@
11
<script>
2+
import CmsImage from "@/components/CmsImage.svelte";
23
let {
34
title,
45
description,
5-
image,
6+
assetId,
67
link
78
} = $props();
89
</script>
910

1011

1112
<div class="Card w-full min-h-[519px] bg-zinc-100 flex-col justify-start items-start inline-flex">
12-
<img class="Image4 self-stretch h-[238px] object-cover" src={image} alt={title} />
13+
<CmsImage {assetId} alt={title} class="Image4 self-stretch h-[238px] object-cover" />
1314
<div class="Frame5 self-stretch min-h-[281px] p-8 flex-col justify-between items-start gap-4 flex">
1415
<div class="Heading self-stretch min-h-[41px] flex-col justify-end items-start gap-8 flex">
1516
<div class="Heading self-stretch text-black text-[31.25px] font-bold font-['Albert Sans'] leading-10">

src/components/Cards/ProjectCard.svelte

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
<script>
22
import Badge from "../Badge.svelte";
3+
import CmsImage from "@/components/CmsImage.svelte";
34
/**
45
* @typedef {Object} Props
56
* @property {any} title
67
* @property {any} description
7-
* @property {any} image
8+
* @property {any} assetId
89
* @property {any} website
910
* @property {any} repository
1011
* @property {any} [tags]
@@ -15,7 +16,7 @@
1516
let {
1617
title,
1718
description,
18-
image,
19+
assetId,
1920
website,
2021
repository,
2122
tags = [],
@@ -25,7 +26,7 @@
2526

2627

2728
<div class="Card w-full min-h-[519px] flex-col justify-start items-start inline-flex">
28-
<img class="Image4 object-cover h-[200px] w-full" src={image} alt={title} />
29+
<CmsImage {assetId} alt={title} class="Image4 object-cover h-[200px] w-full" />
2930
<div class="Frame5 self-stretch min-h-[269px] h-full flex-col justify-start pt-3 items-start gap-4 flex">
3031
<div class="Heading self-stretch min-h-[84px] flex-col justify-start items-start gap-8 flex">
3132
<div class="Heading self-stretch text-black text-[31.25px] font-bold font-['Albert Sans'] leading-10">

src/components/CmsImage.svelte

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<script>
2+
import { getAssetSrcset } from '$lib/imageUtils.js';
3+
4+
let { assetId, alt = '', ...rest } = $props();
5+
const imageData = $derived(getAssetSrcset(assetId));
6+
</script>
7+
8+
<picture class="contents">
9+
{#if imageData.srcset}
10+
<source
11+
srcset={imageData.srcset}
12+
type="image/webp"
13+
sizes="(max-width: 640px) 400px, (max-width: 1024px) 800px, 1200px"
14+
/>
15+
{/if}
16+
<img src={imageData.src} {alt} loading="lazy" {...rest} />
17+
</picture>
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
{
2+
"2d886bf0-b4d9-4dce-a580-d750b421c052": {
3+
"sm": "/images/2d886bf0-b4d9-4dce-a580-d750b421c052-400w.webp",
4+
"md": "/images/2d886bf0-b4d9-4dce-a580-d750b421c052-800w.webp",
5+
"lg": "/images/2d886bf0-b4d9-4dce-a580-d750b421c052-1200w.webp"
6+
},
7+
"34bc3ece-c623-4a73-927d-0c9b21ee157e": {
8+
"sm": "/images/34bc3ece-c623-4a73-927d-0c9b21ee157e-400w.webp",
9+
"md": "/images/34bc3ece-c623-4a73-927d-0c9b21ee157e-800w.webp",
10+
"lg": "/images/34bc3ece-c623-4a73-927d-0c9b21ee157e-1200w.webp"
11+
},
12+
"74d37ce6-441c-4372-b5fc-d2f26cf3c173": {
13+
"sm": "/images/74d37ce6-441c-4372-b5fc-d2f26cf3c173-400w.webp",
14+
"md": "/images/74d37ce6-441c-4372-b5fc-d2f26cf3c173-800w.webp",
15+
"lg": "/images/74d37ce6-441c-4372-b5fc-d2f26cf3c173-1200w.webp"
16+
},
17+
"d3191ea4-95ec-4356-8a51-a2b2b737914f": {
18+
"sm": "/images/d3191ea4-95ec-4356-8a51-a2b2b737914f-400w.webp",
19+
"md": "/images/d3191ea4-95ec-4356-8a51-a2b2b737914f-800w.webp",
20+
"lg": "/images/d3191ea4-95ec-4356-8a51-a2b2b737914f-1200w.webp"
21+
},
22+
"775c1500-1246-49f1-8e65-7e9e50152d76": {
23+
"sm": "/images/775c1500-1246-49f1-8e65-7e9e50152d76-400w.webp",
24+
"md": "/images/775c1500-1246-49f1-8e65-7e9e50152d76-800w.webp",
25+
"lg": "/images/775c1500-1246-49f1-8e65-7e9e50152d76-1200w.webp"
26+
},
27+
"640ac441-3d37-499f-bc4c-ef18c340c78d": {
28+
"sm": "/images/640ac441-3d37-499f-bc4c-ef18c340c78d-400w.webp",
29+
"md": "/images/640ac441-3d37-499f-bc4c-ef18c340c78d-800w.webp",
30+
"lg": "/images/640ac441-3d37-499f-bc4c-ef18c340c78d-1200w.webp"
31+
},
32+
"1419e77d-9427-42de-ae39-0f43b5bad21d": {
33+
"sm": "/images/1419e77d-9427-42de-ae39-0f43b5bad21d-400w.webp",
34+
"md": "/images/1419e77d-9427-42de-ae39-0f43b5bad21d-800w.webp",
35+
"lg": "/images/1419e77d-9427-42de-ae39-0f43b5bad21d-1200w.webp"
36+
},
37+
"45b72f8b-beb7-44db-810c-c8af26363aad": {
38+
"sm": "/images/45b72f8b-beb7-44db-810c-c8af26363aad-400w.webp",
39+
"md": "/images/45b72f8b-beb7-44db-810c-c8af26363aad-800w.webp",
40+
"lg": "/images/45b72f8b-beb7-44db-810c-c8af26363aad-1200w.webp"
41+
},
42+
"378ce012-d63b-439c-9521-e8bfa804c9db": {
43+
"sm": "/images/378ce012-d63b-439c-9521-e8bfa804c9db-400w.webp",
44+
"md": "/images/378ce012-d63b-439c-9521-e8bfa804c9db-800w.webp",
45+
"lg": "/images/378ce012-d63b-439c-9521-e8bfa804c9db-1200w.webp"
46+
},
47+
"7353dc1d-f74b-48a5-a71e-a22b31e21e94": {
48+
"sm": "/images/7353dc1d-f74b-48a5-a71e-a22b31e21e94-400w.webp",
49+
"md": "/images/7353dc1d-f74b-48a5-a71e-a22b31e21e94-800w.webp",
50+
"lg": "/images/7353dc1d-f74b-48a5-a71e-a22b31e21e94-1200w.webp"
51+
},
52+
"935e3fdf-c1b5-43c9-bf01-de122e8e966a": {
53+
"sm": "/images/935e3fdf-c1b5-43c9-bf01-de122e8e966a-400w.webp",
54+
"md": "/images/935e3fdf-c1b5-43c9-bf01-de122e8e966a-800w.webp",
55+
"lg": "/images/935e3fdf-c1b5-43c9-bf01-de122e8e966a-1200w.webp"
56+
},
57+
"fb39ef2b-35d4-4483-b4e1-6e76ae2e3224": {
58+
"sm": "/images/fb39ef2b-35d4-4483-b4e1-6e76ae2e3224-400w.webp",
59+
"md": "/images/fb39ef2b-35d4-4483-b4e1-6e76ae2e3224-800w.webp",
60+
"lg": "/images/fb39ef2b-35d4-4483-b4e1-6e76ae2e3224-1200w.webp"
61+
}
62+
}

src/lib/imageUtils.js

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
// Resuelve IDs de assets de Directus a rutas locales WebP.
2+
// Si el manifiesto no tiene el asset (ej. en dev sin prebuild), retorna la URL de Directus.
3+
4+
import manifestJson from './generated/imageManifest.json';
5+
6+
/** @type {Record<string, { sm: string, md: string, lg: string }>} */
7+
const manifest = manifestJson;
8+
9+
const DIRECTUS_URL = 'https://content.codeandomexico.org';
10+
11+
/**
12+
* @param {string | null | undefined} assetId
13+
* @returns {{ src: string, srcset: string | null }}
14+
*/
15+
export function getAssetSrcset(assetId) {
16+
if (!assetId) return { src: '', srcset: null };
17+
18+
const entry = manifest[assetId];
19+
if (entry) {
20+
return {
21+
src: entry.lg,
22+
srcset: `${entry.sm} 400w, ${entry.md} 800w, ${entry.lg} 1200w`
23+
};
24+
}
25+
26+
// Fallback para desarrollo local sin haber ejecutado el prebuild
27+
return {
28+
src: `${DIRECTUS_URL}/assets/${assetId}`,
29+
srcset: null
30+
};
31+
}

0 commit comments

Comments
 (0)