Date : 29/12/2024 Priorite : HAUTE - CETTE SEMAINE Projet : QwikPress - CMS/Générateur de sites modulaire basé sur Qwik
PHP avait amené tout un process intelligent avec le rendu côté serveur. Les SPA (React, Angular) ont été une régression :
Qwik est la révolution : il reprend le meilleur du serveur avec la modernité du frontend.
Principe : Les credentials ne PEUVENT PAS fuiter vers le frontend.
┌─────────────────────────────────────────────┐
│ PLUGIN (seul accès aux credentials) │
│ sharedMap.set('storage', service) │
│ sharedMap.set('connector-xxx', fn) │
└─────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────┐
│ server$ (accès via sharedMap UNIQUEMENT) │
│ const storage = this.sharedMap.get(...) │
└─────────────────────────────────────────────┘
Avantages :
process.env.SUPABASE_* est hors plugin = violationUn connecteur défini via sharedMap.set('connector-xxx', ...) peut être réutilisé dans tous les projets Qwik.
// Dans n'importe quel projet Qwik
const fb = this.sharedMap.get('connector-facebook');
const posts = await fb('/posts', { limit: 10 });
Qwik = ~10kb de JS initial, 0 hydratation
Qwik assure : le code admin n'est jamais envoyé si l'utilisateur n'est pas admin.
Pas de prebundle, pas de code inutile côté client. Si tu n'as pas accès = le code n'existe pas pour toi.
| Composant | Rôle | Pourquoi |
|---|---|---|
| Qwik | Frontend + SSR | Resumability, 0 hydratation |
| Bun | Runtime | Rapidité, légèreté |
| Elysia | API (si besoin) | Performance, DX |
| Supabase | Cloud DB + Auth + Storage | PostgreSQL, tout-en-un |
| Redis | Cache | Performance, sessions |
| Nginx | Reverse Proxy | SSL, routing, cache |
// ❌ INTERDIT
try {
const result = await something();
} catch (e) { /* ... */ }
// ✅ OBLIGATOIRE
const result = await something();
if (!result) {
return { success: false, error: 'Message' };
}
// Code normal...
// plugins/storage.ts - SEUL ENDROIT avec credentials
const client = createClient(process.env.SUPABASE_URL, ...);
sharedMap.set('storage', new SupabaseStorageService(client));
// server$ - JAMAIS de credentials
const storage = this.sharedMap.get('storage');
const pages = await storage.getAllPages();
Avec les bons outils (Qwik + pattern sharedMap), une IA comme Claude peut :
QwikPress offre :
Les connecteurs sont des fonctions injectées via sharedMap qui encapsulent :
// Plugin (une seule fois)
sharedMap.set('connector-facebook', async (endpoint, params) => {
return fetch(`${config.baseUrl}${endpoint}`, {
headers: { Authorization: `Bearer ${token}` }
});
});
// Partout dans le code
const fb = this.sharedMap.get('connector-facebook');
const posts = await fb('/posts', { limit: 10 });
interface StorageService {
readonly mode: 'local' | 'supabase';
getPage(id: string): Promise<Page | null>;
getAllPages(): Promise<Page[]>;
savePage(id: string, page: Page): Promise<StorageResult<Page>>;
// ... toutes les opérations CRUD
}
| Mode | Variable ENV | Description |
|---|---|---|
| Local | STORAGE_MODE=local |
Fichiers JSON dans STORAGE/ |
| Cloud | STORAGE_MODE=supabase |
Supabase (DB + Storage) |
// server$ - Le code ne change PAS selon le mode
const storage = this.sharedMap.get('storage');
const pages = await storage.getAllPages();
// Que ce soit local ou Supabase, même code
// Toujours via server$, jamais direct
export const getPageContent = server$(async function(slug: string) {
const storage = this.sharedMap.get('storage');
return storage.getPageBySlug(slug);
});
process.env.SUPABASE_* hors des pluginsSi on fait bien les choses :
"Les credentials ne doivent JAMAIS être accessibles dans les composants ou server$."
"Un connecteur peut servir dans tout type de projet Qwik très facilement."
"Qwik assure que les fichiers que quelqu'un ne peut pas voir est possible, pas de prebundle."
"Cette stack est bonne pour la planète et l'accès à tous."
"On va prouver que humain et IA avec les bons outils vont très vite et de manière sécurisée."
| Fichier | Rôle |
|---|---|
types/storage.ts |
Interface StorageService |
services/storage/file-storage.service.ts |
Implémentation locale |
services/storage/supabase-storage.service.ts |
Implémentation cloud |
plugins/storage.ts |
Injection sharedMap (SEUL accès credentials) |
CLAUDE.md |
Documentation pour l'IA |
On ne passe JAMAIS src: string pour une image.
C'est TOUJOURS image: MediaRef avec les métadonnées.
// ❌ INTERDIT
interface HeroData { backgroundImage: string; }
// ✅ OBLIGATOIRE
interface HeroData { backgroundImage: MediaRef; }
Le MediaRef contient TOUJOURS : src, width, height, ratio, mimeType, alt.
// ❌ INTERDIT - Le front découvre la taille
<img src="/media/photo.jpg" />
// ✅ OBLIGATOIRE - Dimensions pré-fournies
<img
src="/api/medias/abc123"
width={metadata.dimensions.width}
height={metadata.dimensions.height}
alt={metadata.alt}
style={{ aspectRatio: metadata.ratio }}
/>
// L'API retourne TOUJOURS les métadonnées
{
url: "/api/medias/abc123",
metadata: {
dimensions: { width: 1920, height: 1080 },
ratio: 1.777,
ratioFormatted: "16:9",
mimeType: "image/jpeg",
dominantColor: "#2a3f5f" // Pour placeholder
},
urls: {
thumbnail: "/api/medias/abc123?preset=thumbnail",
medium: "/api/medias/abc123?w=640",
large: "/api/medias/abc123?w=1024"
},
srcset: "...?w=320 320w, ...?w=640 640w, ...?w=1024 1024w"
}
/api/medias/{id} → Original
/api/medias/{id}?w=800 → Largeur 800px
/api/medias/{id}?h=600 → Hauteur 600px
/api/medias/{id}?preset=thumbnail → 150x150
/api/medias/{id}?preset=og → 1200x630 (Open Graph)
/api/medias/{id}?w=800&format=webp → WebP optimisé
Le développeur ajuste la taille via les helpers, le serveur resize + cache automatiquement.
import { getResizedUrl, getResizedDimensions, getResizedMediaRef } from '~/types/media';
// Générer l'URL redimensionnée
const thumbUrl = getResizedUrl(image, { preset: 'thumbnail' });
const mediumUrl = getResizedUrl(image, { width: 640, format: 'webp' });
// Connaître les dimensions finales (pour le HTML)
const dims = getResizedDimensions(image, { width: 800 });
// dims = { width: 800, height: 450 } (ratio préservé)
// Créer un nouveau MediaRef avec les nouvelles dimensions
const resized = getResizedMediaRef(image, { width: 800 });
// resized.width = 800, resized.height = 450, resized.src = "...?w=800"
Côté serveur :
?w=800&format=webp)Ce document est la référence pour la présentation du projet au lancement.