Editor web schema-driven para el contenido gestionado por parroquia-config-api
(el Worker + bucket R2). El formulario no está hardcodeado: se construye
en tiempo de ejecución a partir de un pages.yml que se descarga desde una
URL configurable, así que si el esquema cambia, el editor cambia con él sin
tocar código.
npm install
npm run dev # http://localhost:5173
npm run build # genera docs/.vitepress/dist (estático, se puede subir a cualquier hosting)Al abrir el editor te pedirá:
- Token de escritura del sitio (obligatorio).
- En "Opciones avanzadas": la URL del worker (API), la URL pública de
lectura de datos, y la URL del
pages.yml. Se guardan enlocalStoragepara no tener que repetirlas cada vez (igual de sensibles que el propio token, que también se guarda ahí).
El editor llama a GET /whoami con el token para resolver el :slug
automáticamente — no hace falta que el usuario lo escriba.
El worker original (worker/index.js en tu proyecto) no tenía forma de
resolver token -> slug, así que se le ha añadido un único endpoint
nuevo:
GET /whoami (bearer token) -> { "slug": "..." }
Es exactamente la misma búsqueda que ya hace authorize() en auth.json,
solo que sin comparar contra un slug conocido de antemano. No cambia nada
del modelo de seguridad existente (tokens opacos, invarianza de
no-decodificación en el servidor, etc.).
Como me indicaste que la lectura de ficheros se sirve públicamente desde
data.parroquia.app (o el host que configures), el editor no llama al
worker para leer contenido — solo para /whoami, /sites/:slug/list y el
PUT de escritura. El fichero worker/index.js incluido aquí refleja eso
(sin ninguna ruta GET /sites/:slug/:token, tal y como estaba el original).
El único añadido real es /whoami. Está marcado con EDITOR PATCH en los
comentarios para que sea fácil de localizar y aplicar a tu despliegue.
worker/migrate.js se incluye sin cambios, tal cual lo pasaste, solo como
referencia de la codificación de tokens (el editor la reimplementa en
docs/.vitepress/theme/lib/codec.js, byte a byte idéntica).
docs/.vitepress/theme/
lib/
codec.js encode/validación de filename plano (charset url-safe; / → -; idéntico a migrate.js)
frontmatter.js parseo/serialización YAML frontmatter para .md
schema.js normaliza pages.yml: resuelve `component:`, calcula
valores por defecto, interpola resúmenes collapsible
content-index.js pages.yml + lista de tokens -> lista ordenada de
ficheros editables, índice de referencias, índice
de medios
api.js llamadas HTTP (worker + host público de datos)
store.js estado reactivo global: sesión, fichero abierto,
dirty-tracking, autosave a localStorage, guardar
components/
LoginView.vue pantalla de login
FileBrowser.vue barra lateral, agrupada y ordenada según pages.yml
FieldsGroup.vue pinta una lista de campos contra un objeto
FieldRenderer.vue dispatch de un campo: object / object-list /
block (lista polimórfica) / lista de escalares / hoja
ScalarInput.vue tipos hoja: string, text, rich-text, number,
boolean, date, select, image, reference
SelectField.vue select simple/múltiple, con o sin "creatable"
ImagePickerModal.vue selector de imágenes existentes + subida
EditorApp.vue layout raíz (login o editor)
Cualquier campo o variante de bloque con component: <nombre> hereda
type, options, fields o blocks del componente referenciado en
components:; sus propias claves (label, list, hidden, default, fields...)
siempre tienen prioridad. Esto cubre tanto el uso como "atajo de tipo"
(tags, font, color...) como el uso como "objeto reutilizable"
(hero usado dentro de blocks:).
string, text, rich-text (editor enriquecido básico basado en
contenteditable, no un editor WYSIWYG completo), number, boolean,
date, select (valores string u objetos {value,label}, simple/múltiple,
con o sin creatable), image (simple/múltiple, selector + subida),
object (simple o repetible, con resumen collapsible interpolado tipo
{fields.title}), block (lista polimórfica: cada elemento recuerda su
variante bajo la clave type), reference (simple/múltiple, apuntando a
otra collection por su nombre de fichero decodificado), y listas simples
de escalares (list: true / list: { collapsible: {...} } sobre string,
number, date, etc).
Se sigue estrictamente el orden de content: en pages.yml, nunca el
orden en que el bucket devuelve los tokens. Para type: collection, los
ficheros se ordenan alfabéticamente por su ruta decodificada dentro de esa
carpeta. Al usuario se le muestra siempre el nombre decodificado, nunca el
token (filename plano codificado). Los ficheros de media no aparecen en este listado principal
(solo son accesibles vía el selector de imágenes), y ningún fichero fuera
del esquema se ofrece para editar.
Cada cambio se serializa (JSON con indentado, o YAML frontmatter para
.md) y se guarda en localStorage con un pequeño debounce, bajo una
clave por sitio+fichero. El botón "Guardar cambios" solo se resalta cuando
el contenido serializado difiere del último snapshot remoto conocido. Al
guardar, se hace PUT al worker con el token de escritura y se limpia el
borrador local (queda como nuevo "guardado remoto" de referencia).
Si vuelves a abrir un fichero con cambios sin guardar pendientes, el editor
te avisa y los recupera automáticametne desde localStorage.
reference: la etiqueta mostrada es el nombre de fichero decodificado (sin extensión), no eltitlereal del documento referenciado — cargar eltitlede cada elemento de la colección exigiría descargar todos los ficheros de esa colección solo para construir la lista, así que se ha dejado así por coste. Si quieres el título real, es un cambio localizado enbuildCollectionRefIndex(content-index.js): habría que hacer fetch de cada fichero de la colección y leer su campoprimary.rich-textusadocument.execCommand, que está obsoleto pero ampliamente soportado; es intencionadamente básico (negrita, cursiva, listas, enlace, limpiar formato). Si el HTML resultante necesita ser más controlado, esto es candidato a sustituirse por un editor real (Tiptap, ProseMirror...).- Reordenar listas/bloques usa botones ↑/↓, no drag-and-drop.
- Clave discriminadora de bloques: cada elemento de un campo
blockse guarda como{ type: "<nombre-del-bloque>", ...campos }. Si tu generador estático espera otra convención, es una única constante (BLOCK_TYPE_KEYenlib/schema.js). - El endpoint
/whoamies nuevo: hay que desplegar el worker parcheado (worker/index.js) antes de que el login funcione contra producción. - No se ha podido probar contra el Worker/API reales (no hay red disponible
hacia Cloudflare desde este entorno) — sí se ha verificado que
npm run buildcompila sin errores. Conviene hacer una primera prueba manual con un sitio de test antes de usarlo en producción.