logo hsb.horse
← Retour au blog

Blog

Implémenter un éditeur Live réservé au développement dans les projets Astro

Comment implémenter un éditeur basé sur navigateur limité au mode développement dans Astro. Utiliser le middleware Vite et React pour rendre la gestion de contenu en développement confortable sans affecter les builds de production.

Publié:

Lors de la création de sites statiques avec Astro, la gestion de contenu basée sur Git est la norme. Cependant, pouvoir éditer directement dans le navigateur pendant le développement accélère le flux de travail.

Vous pouvez éditer le contenu sans basculer entre l’éditeur et le navigateur. Les modifications se reflètent immédiatement à l’écran. Les images peuvent être téléchargées via GUI.

Pour le projet PromptLex, j’ai créé un éditeur qui ne fonctionne qu’en mode développement. Il n’a aucun impact sur les builds de production.

Pourquoi créer un éditeur Live

Utiliser la gestion basée sur Git pour la production est évident, mais le développement est différent.

Les tests et la création de contenu sont devenus beaucoup plus faciles. La vitesse d’itération lors du prototypage a augmenté, et la gestion des images est complétée dans la même UI. Vous pouvez facilement créer et supprimer des données fictives pour les tests de développement.

Architecture

Il se compose de trois composants.

Composant React pour l’UI. Middleware Vite pour les points de terminaison API des opérations CRUD. Page Astro comme point d’entrée qui charge l’éditeur uniquement en mode DEV.

Étape 1: Créer la page d’administration

Créez une page Astro qui ne fonctionne qu’en mode développement.

src/pages/admin/index.astro
---
import Base from '../../layouts/Base.astro';
import AdminEditor from '../../components/AdminEditor';
const isDev = import.meta.env.DEV;
---
<Base title="Panneau d'administration">
<meta slot="head" name="robots" content="noindex, nofollow" />
{isDev ? (
<AdminEditor client:only="react" />
) : (
<div>
<h1>Panneau d'administration</h1>
<p>Le panneau d'administration n'est disponible qu'en mode développement.</p>
<p>Exécutez <code>npm run dev</code> pour y accéder.</p>
</div>
)}
</Base>

En spécifiant client:only="react", le composant se charge uniquement côté client.

Étape 2: Construire le middleware API

Créez une intégration Astro qui ajoute des points de terminaison API au serveur de développement.

src/integrations/admin-api.ts
import type { AstroIntegration } from 'astro';
import { readFileSync, writeFileSync, readdirSync } from 'fs';
import { join } from 'path';
export default function adminApi(): AstroIntegration {
return {
name: 'admin-api',
hooks: {
'astro:server:setup': ({ server }) => {
server.middlewares.use(async (req, res, next) => {
const url = req.url || '';
const method = req.method || 'GET';
if (!url.startsWith('/api/')) {
return next();
}
const contentDir = join(process.cwd(), 'src', 'content');
// GET /api/items - Liste de tous les éléments
if (url === '/api/items' && method === 'GET') {
const files = readdirSync(contentDir).filter(f => f.endsWith('.json'));
const items = files.map(file => {
const content = readFileSync(join(contentDir, file), 'utf-8');
return JSON.parse(content);
});
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(items));
return;
}
// POST /api/items - Créer un nouvel élément
if (url === '/api/items' && method === 'POST') {
let body = '';
req.on('data', chunk => body += chunk);
req.on('end', () => {
const data = JSON.parse(body);
const id = generateId(data.text);
const filePath = join(contentDir, `${id}.json`);
const item = {
id,
text: data.text,
createdAt: new Date().toISOString(),
};
writeFileSync(filePath, JSON.stringify(item, null, 2));
res.writeHead(201, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(item));
});
return;
}
res.writeHead(404, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'Not found' }));
});
},
},
};
}
function generateId(text: string): string {
return text.slice(0, 12).replace(/\W/g, '').toLowerCase();
}

Étape 3: Créer le composant React

Écrivez un composant React qui appelle l’API.

src/components/AdminEditor.tsx
import React, { useState, useEffect } from 'react';
interface Item {
id: string;
text: string;
createdAt: string;
}
export default function AdminEditor() {
const [items, setItems] = useState<Item[]>([]);
const [newText, setNewText] = useState('');
const [loading, setLoading] = useState(true);
useEffect(() => {
fetchItems();
}, []);
const fetchItems = async () => {
const response = await fetch('/api/items');
const data = await response.json();
setItems(data);
setLoading(false);
};
const handleCreate = async (e: React.FormEvent) => {
e.preventDefault();
if (!newText.trim()) return;
const response = await fetch('/api/items', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ text: newText }),
});
const newItem = await response.json();
setItems([newItem, ...items]);
setNewText('');
};
if (loading) return <div>Chargement...</div>;
return (
<div style={{ maxWidth: '1200px', margin: '0 auto', padding: '20px' }}>
<h1>Éditeur de contenu</h1>
<form onSubmit={handleCreate}>
<textarea
value={newText}
onChange={(e) => setNewText(e.target.value)}
placeholder="Entrez le contenu..."
rows={4}
style={{ width: '100%', padding: '10px' }}
/>
<button type="submit">Créer</button>
</form>
<div>
<h2>Éléments ({items.length})</h2>
{items.map(item => (
<div key={item.id} style={{ border: '1px solid #ddd', padding: '10px', margin: '10px 0' }}>
<strong>{item.id}</strong>
<p>{item.text}</p>
<small>{new Date(item.createdAt).toLocaleString()}</small>
</div>
))}
</div>
</div>
);
}

Étape 4: Enregistrer l’intégration

Ajoutez l’intégration API d’administration à astro.config.mjs.

import { defineConfig } from 'astro/config';
import react from '@astrojs/react';
import adminApi from './src/integrations/admin-api';
export default defineConfig({
integrations: [
react(),
adminApi(),
],
});

Fonctionnalités avancées

Support du téléchargement de fichiers

Téléchargez des images avec des données de formulaire multipart.

async function parseMultipart(req) {
return new Promise((resolve) => {
const chunks = [];
req.on('data', chunk => chunks.push(chunk));
req.on('end', () => {
const buffer = Buffer.concat(chunks);
// Analyser la limite et extraire les fichiers
// ...
resolve({ fields, files });
});
});
}

Prévention des doublons

Évitez les doublons avec le hachage de contenu.

import { createHash } from 'crypto';
function generateId(text: string): string {
const hash = createHash('sha256');
hash.update(text);
return hash.digest('hex').substring(0, 12);
}

Fonction de suppression

Affichez une boîte de dialogue de confirmation avant la suppression.

const handleDelete = async (id: string) => {
if (!confirm('Supprimer cet élément?')) return;
await fetch(`/api/items/${id}`, { method: 'DELETE' });
setItems(items.filter(item => item.id !== id));
};

Meilleures pratiques

Vérifiez toujours qu’il s’agit de développement uniquement avec import.meta.env.DEV. Ajoutez la balise meta noindex aux pages d’administration. Rendez les types explicites avec les interfaces TypeScript. Écrivez des messages d’erreur clairs. Validez l’entrée avant l’enregistrement. Utilisez path.join() pour les opérations de fichiers.

Impact sur la production

Aucun impact sur les builds de production est l’avantage de cette approche.

L’éditeur ne fonctionne que pendant astro dev. Les builds de production n’incluent pas de routes supplémentaires. Les points de terminaison API ne sont pas exposés. Les sites statiques restent statiques.

Créer un éditeur Live pour les projets Astro change l’expérience de développement. En utilisant le middleware Vite et React, vous pouvez créer une interface de gestion de contenu qui n’existe que pendant le développement.

L’implémentation complète se trouve dans le dépôt PromptLex.

Références