Ao criar sites estáticos com Astro, o gerenciamento de conteúdo baseado em Git é o padrão. No entanto, ter edição direta no navegador durante o desenvolvimento acelera o fluxo de trabalho.
Você pode editar conteúdo sem alternar entre editor e navegador. As alterações refletem imediatamente na tela. Imagens podem ser enviadas via GUI.
Para o projeto PromptLex, construí um editor que funciona apenas no modo de desenvolvimento. Não tem impacto nos builds de produção.
Por que construir um Editor Live
Usar gerenciamento baseado em Git para produção é óbvio, mas desenvolvimento é diferente.
Testes e criação de conteúdo ficaram muito mais fáceis. A velocidade de iteração durante prototipagem aumentou, e o gerenciamento de imagens é concluído na mesma UI. Você pode facilmente criar e excluir dados fictícios para testes de desenvolvimento.
Arquitetura
Consiste em três componentes.
Componente React para a UI. Middleware Vite para endpoints de API de operações CRUD. Página Astro como ponto de entrada que carrega o editor apenas no modo DEV.
Passo 1: Criar Página de Administração
Crie uma página Astro que funciona apenas no modo de desenvolvimento.
---import Base from '../../layouts/Base.astro';import AdminEditor from '../../components/AdminEditor';
const isDev = import.meta.env.DEV;---
<Base title="Painel de Administração"> <meta slot="head" name="robots" content="noindex, nofollow" />
{isDev ? ( <AdminEditor client:only="react" /> ) : ( <div> <h1>Painel de Administração</h1> <p>O painel de administração está disponível apenas no modo de desenvolvimento.</p> <p>Execute <code>npm run dev</code> para acessá-lo.</p> </div> )}</Base>Especificando client:only="react", o componente carrega apenas no lado do cliente.
Passo 2: Construir Middleware API
Crie uma integração Astro que adiciona endpoints de API ao servidor de desenvolvimento.
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 - Listar todos os itens 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 - Criar novo item 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();}Passo 3: Criar Componente React
Escreva um componente React que chama a API.
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>Carregando...</div>;
return ( <div style={{ maxWidth: '1200px', margin: '0 auto', padding: '20px' }}> <h1>Editor de Conteúdo</h1>
<form onSubmit={handleCreate}> <textarea value={newText} onChange={(e) => setNewText(e.target.value)} placeholder="Digite o conteúdo..." rows={4} style={{ width: '100%', padding: '10px' }} /> <button type="submit">Criar</button> </form>
<div> <h2>Itens ({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> );}Passo 4: Registrar Integração
Adicione a integração da API de administração ao 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(), ],});Recursos Avançados
Suporte a Upload de Arquivos
Envie imagens com dados de formulário 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); // Analisar boundary e extrair arquivos // ... resolve({ fields, files }); }); });}Prevenção de Duplicatas
Evite duplicatas com hashing de conteúdo.
import { createHash } from 'crypto';
function generateId(text: string): string { const hash = createHash('sha256'); hash.update(text); return hash.digest('hex').substring(0, 12);}Função de Exclusão
Mostre diálogo de confirmação antes da exclusão.
const handleDelete = async (id: string) => { if (!confirm('Excluir este item?')) return;
await fetch(`/api/items/${id}`, { method: 'DELETE' }); setItems(items.filter(item => item.id !== id));};Melhores Práticas
Sempre verifique que é exclusivo para desenvolvimento com import.meta.env.DEV.
Adicione meta tag noindex às páginas de administração.
Torne os tipos explícitos com interfaces TypeScript.
Escreva mensagens de erro claras.
Valide entrada antes de salvar.
Use path.join() para operações de arquivo.
Impacto na Produção
Nenhum impacto nos builds de produção é a vantagem desta abordagem.
O editor funciona apenas durante astro dev.
Builds de produção não incluem rotas extras.
Endpoints de API não são expostos.
Sites estáticos permanecem estáticos.
Construir um editor Live para projetos Astro muda a experiência de desenvolvimento. Usando middleware Vite e React, você pode criar uma interface de gerenciamento de conteúdo que existe apenas durante o desenvolvimento.
A implementação completa está no repositório PromptLex.
hsb.horse