Back to 0cron
0cron

Du HTML statique au tableau de bord SvelteKit en une nuit

Comment nous avons converti une page marketing statique en un tableau de bord SvelteKit 2 complet avec runes Svelte 5, store d'auth, client API, et wizard CronBuilder -- en une session.

Thales & Claude | March 30, 2026 8 min 0cron
EN/ FR/ ES
0cronsveltesveltekitdashboardtailwindcssfrontendui

Quand nous avons lancé la première version de 0cron.dev, le « frontend » était un seul fichier HTML statique. Une page marketing avec de jolis dégradés, un tableau de prix, et un bouton d'appel à l'action pointant vers une API qui n'avait pas encore d'interface. Le backend était solide -- 14 endpoints, 8 tables de base de données, 2 852 lignes de Rust -- mais les utilisateurs n'avaient aucun moyen d'interagir avec, en dehors de requêtes HTTP brutes.

Cela a changé en une seule session. Le 11 mars 2026, nous avons converti 0cron-website d'un HTML statique en une application SvelteKit 2 complète avec 13 pages de routes, un store d'authentification réactif, un client API, une mise en page avec barre latérale sombre, et un wizard de création de tâches avec un constructeur d'expressions cron à la cPanel. Voici comment nous l'avons fait, et les décisions architecturales qui l'ont rendu possible.

Pourquoi SvelteKit, pourquoi maintenant

Nous avions une API Rust fonctionnelle. Nous avions besoin d'un tableau de bord. La question n'a jamais été « devrions-nous construire un frontend ? » mais « quel framework offre la productivité maximale pour une équipe de deux personnes dont une est une IA ? »

SvelteKit 2 avec les runes Svelte 5 était le choix évident. Voici pourquoi :

Le routage basé sur les fichiers élimine le boilerplate. Chaque page est un fichier. Chaque layout enveloppe ses enfants automatiquement. Pas de configuration de routeur, pas d'enregistrement de routes, pas de chaîne de middleware à configurer manuellement. Vous créez un fichier, et il devient une route.

Les runes Svelte 5 remplacent le pattern store. Au lieu d'importer des writable stores, de s'abonner, et de gérer le démontage, vous écrivez $state et le compilateur gère la réactivité.

TailwindCSS 4 est livré avec SvelteKit prêt à l'emploi. Pas de configuration PostCSS, pas de setup de purge, pas de réglages de pipeline de build.

La structure des routes

La première décision architecturale concernait les groupes de routes. SvelteKit supporte des noms de répertoires entre parenthèses qui créent des frontières de layout sans affecter l'URL. Nous avons utilisé deux groupes : (auth) pour le flux de connexion et (app) pour le tableau de bord authentifié.

src/routes/
  +page.svelte                          -- Landing (charge static/landing.html)
  (auth)/
    login/+page.svelte                  -- Google Sign-In + e-mail/mot de passe
  (app)/
    +layout.svelte                      -- Auth guard + layout avec barre latérale
    dashboard/+page.svelte              -- Cartes de stats, tâches à venir, exécutions récentes
    jobs/+page.svelte                   -- Liste de tâches avec actions (déclencher/pause/supprimer)
    jobs/new/+page.svelte               -- Wizard de création de tâche (CronBuilder)
    jobs/[id]/+page.svelte              -- Détail de tâche + historique d'exécution + stats
    billing/+page.svelte                -- Affichage du plan, Stripe Checkout/Portal
    settings/+page.svelte               -- Clés API + gestion des secrets
    account/+page.svelte                -- Profil

Cette séparation n'est pas cosmétique. Le fichier (app)/+layout.svelte contient l'auth guard -- si vous n'êtes pas authentifié, vous êtes redirigé vers login. Chaque page dans (app)/ hérite de cette protection automatiquement. Le groupe (auth)/ a son propre layout minimal sans la barre latérale ni la navigation. Et le +page.svelte racine charge la page marketing statique pour les visiteurs non authentifiés.

Le store d'authentification : les runes Svelte 5 en pratique

L'état d'authentification est la colonne vertébrale de toute application de tableau de bord. Il doit être accessible partout, persister entre les rechargements de page, et se synchroniser avec le client API. En Svelte 4, on aurait utilisé un writable store avec un abonnement personnalisé. En Svelte 5, on écrit une classe avec des champs $state :

typescriptclass AuthStore {
    token = $state<string | null>(null);
    user = $state<User | null>(null);

    get isAuthenticated() { return !!this.token; }
    get isAdmin() { return this.user?.is_admin ?? false; }

    login(token: string, user: User) {
        this.token = token;
        this.user = user;
        if (browser) {
            localStorage.setItem('auth_token', token);
            localStorage.setItem('auth_user', JSON.stringify(user));
        }
    }

    logout() {
        this.token = null;
        this.user = null;
        if (browser) {
            localStorage.removeItem('auth_token');
            localStorage.removeItem('auth_user');
        }
    }
}

$state rend les champs de classe réactifs. Quand this.token change, tout composant qui lit authStore.token ou authStore.isAuthenticated se re-rend automatiquement.

Les getters deviennent de l'état dérivé. isAuthenticated et isAdmin sont de simples getters JavaScript, mais parce qu'ils lisent des champs $state, Svelte les suit comme des dépendances.

Le guard browser est essentiel. SvelteKit exécute le code côté serveur et client. localStorage n'existe pas sur le serveur. L'import browser de $app/environment limite les effets de bord à l'exécution côté client uniquement.

Le client API : injection du Bearer token

Chaque requête authentifiée a besoin du token JWT dans l'en-tête Authorization. Plutôt que de passer le token manuellement à chaque appel fetch, nous avons construit un wrapper client léger :

typescriptclass ApiClient {
    private baseUrl: string;

    constructor(baseUrl: string) {
        this.baseUrl = baseUrl;
    }

    private async request<T>(path: string, options: RequestInit = {}): Promise<T> {
        const headers: Record<string, string> = {
            'Content-Type': 'application/json',
            ...options.headers as Record<string, string>,
        };

        if (authStore.token) {
            headers['Authorization'] = `Bearer ${authStore.token}`;
        }

        const response = await fetch(`${this.baseUrl}${path}`, {
            ...options,
            headers,
        });

        if (response.status === 401) {
            authStore.logout();
            window.location.href = '/login';
            throw new Error('Unauthorized');
        }

        if (!response.ok) {
            const error = await response.json().catch(() => ({}));
            throw new Error(error.detail || `Request failed: ${response.status}`);
        }

        return response.json();
    }

    get<T>(path: string) { return this.request<T>(path); }
    post<T>(path: string, body: unknown) {
        return this.request<T>(path, { method: 'POST', body: JSON.stringify(body) });
    }
    // put, delete...
}

export const api = new ApiClient('/api/v1');

La redirection auto 401 est un petit mais important détail. Si le JWT expire pendant que l'utilisateur est sur le tableau de bord, le prochain appel API recevra un 401. Plutôt que de montrer une erreur cryptique, le client efface l'état d'auth et redirige vers la page de connexion.

Le wizard de création de tâche

Le composant le plus complexe du tableau de bord est la page de création de tâche. Le coeur en est le CronBuilder -- un constructeur d'expressions cron à la cPanel qui permet aux utilisateurs de construire des planifications visuellement :

typescriptconst schedulePresets = [
    { label: 'Every minute', cron: '* * * * *' },
    { label: 'Every 5 minutes', cron: '*/5 * * * *' },
    { label: 'Every 15 minutes', cron: '*/15 * * * *' },
    { label: 'Every day at midnight', cron: '0 0 * * *' },
    { label: 'Every day at 9am', cron: '0 9 * * *' },
    { label: 'Every Monday at 9am', cron: '0 9 * * 1' },
    { label: 'Every weekday at 9am', cron: '0 9 * * 1-5' },
    // ... 18 presets total
];

Dix-huit presets couvrent la grande majorité des besoins de planification du monde réel. Les utilisateurs qui n'ont jamais écrit d'expression cron peuvent cliquer sur « Every weekday at 9am » et obtenir 0 9 <em> </em> 1-5 sans comprendre le format à cinq champs. Les utilisateurs avancés peuvent basculer vers la saisie manuelle et taper des expressions brutes.

Leçons apprises

Les groupes de routes sont sous-utilisés dans SvelteKit. Le pattern (auth) / (app) résout proprement le problème « certaines pages ont besoin d'une barre latérale, d'autres non ».

Les runes Svelte 5 rendent les classes viables pour la gestion d'état. L'ancien pattern store de Svelte fonctionnait mais semblait étranger. Les runes permettent d'écrire des classes normales avec des méthodes normales, et le compilateur les rend réactives.

L'UX pilotée par les presets réduit la charge cognitive. Les 18 presets de planification dans le CronBuilder éliminent le besoin pour les utilisateurs de comprendre la syntaxe cron.

Garder ce qui fonctionne. La page marketing statique était bien. Nous ne l'avons pas réécrite en Svelte. Dans une session unique où chaque minute compte, savoir quoi ne pas construire est aussi important que savoir quoi construire.

Le tableau de bord a donné un visage à 0cron. L'API a toujours été le produit, mais sans interface, le produit était invisible.


Ceci est l'article 7 de 10 dans la série « Comment nous avons construit 0cron ».

  1. Pourquoi le monde a besoin d'un service cron à 2 $
  2. 4 agents, 1 produit : construire 0cron en une seule session
  3. Construire un moteur de planification cron en Rust
  4. "Tous les jours à 9 h" : parsing de planification en langage naturel
  5. Notifications multi-canaux : e-mail, Slack, Discord, Telegram, webhooks
  6. Intégration Stripe pour un SaaS à 1,99 $/mois
  7. Du HTML statique au tableau de bord SvelteKit en une nuit (vous êtes ici)
  8. Monitoring heartbeat : quand votre tâche devrait vous pinguer
  9. Secrets chiffrés, clés API, et sécurité
  10. D'Abidjan à la production : lancement de 0cron.dev
Share this article:

Responses

Write a response
0/2000
Loading responses...

Related Articles