logo hsb.horse
← Voltar para o índice do blog

Blog

Implementando um Editor Live Exclusivo para Desenvolvimento em Projetos Astro

Como implementar um editor baseado em navegador limitado ao modo de desenvolvimento no Astro. Usando middleware Vite e React para tornar o gerenciamento de conteúdo durante o desenvolvimento confortável sem afetar os builds de produção.

Publicado:

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.

src/pages/admin/index.astro
---
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.

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 - 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.

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>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.

Referências