Skip to content

El contrato de las interfaces ​

app:setup monta la interfaz en Vue o en React, y las dos son la misma aplicación: mismas rutas, mismas pantallas, mismo JSON del sitio y mismas llamadas al backend. Este contrato es lo que cumplen ambas. Si una cambia algo de aquí, la otra cambia igual.

La fuente es docs/shell-contract.md del paquete. Esta página lo cuenta entero y añade, al final, dónde el código se aparta de él.

Dónde vive cada cosa ​

text
stubs/app/common/              backend, igual para las dos
stubs/app/<ui>/                se copia sobre la raíz de la aplicación
  package.json
  vite.config.js
  resources/views/app.blade.php
  resources/<ui>/app/**        la aplicación base
  resources/<ui>/routes.json   provisional; php artisan route:json lo reescribe
resources/<ui>/index.js        el módulo de LaraPack (lo genera larapack:import)
resources/<ui>/src/**          sus modelos: usuarios y lo que se genere después
tests/Frontend/<ui>/           tests de la interfaz, fuera de lo que se copia
  • resources/<ui>/app/** es de laravel-setup cuando se copia y tuyo después.
  • resources/<ui>/index.js y resources/<ui>/src/** son de LaraPack. La aplicación base los importa y nunca los edita.

Arranque ​

  1. axios. axios.defaults con withCredentials, withXSRFToken, Accept: application/json y X-Requested-With: XMLHttpRequest. Una sola copia de axios.
  2. Rutas del backend. setRoutes(routes.json) de innoboxrr-route-resolver. Ninguna URL del backend se escribe a mano: todas salen de route(nombre).
  3. Textos. addTranslations con las del módulo de LaraPack y las de resources/<ui>/app/lang/*.json, en ese orden, y setLocale(document.documentElement.lang). Los textos de la aplicación base van con t('English key') y su traducción en lang/es.json.
  4. Estilos. innoboxrr-form-core/styles y el src/theme.js del módulo, si existe.
  5. Sesión y opciones. Se cargan (auth y options) antes de montar el router.
  6. Avisos. ToastRegion y ConfirmHost se montan una vez, en la raíz.

Rutas del front ​

RutaNombreAccesoPantalla
/site.homeTodosPágina home del sitio
/privacysite.privacyTodosPágina privacy
/termssite.termsTodosPágina terms
/contactsite.contactTodosPágina contact
/joinsite.joinTodosPágina join
/auth/loginauth.loginInvitadoIniciar sesión; respeta ?redirect=
/auth/registerauth.registerInvitadoRegistro
/auth/forgot-passwordauth.forgot-passwordInvitadoPedir el enlace
/auth/reset-password/:token/:emailauth.reset-passwordInvitadoContraseña nueva; es la URL del correo de laravel-auth
/adminadmin.dashboardSesiónInicio del administrador
/admin/profileadmin.profileSesiónPerfil: nombre, correo, foto y contraseña
/admin/siteadmin.siteAdministradorEditor del sitio
/admin/<modelo>/…Las del móduloSesión, y administrador si está en adminOnlyLas rutas que exporta el módulo de LaraPack, hijas de /admin
Cualquier otranot-foundTodos404

Los nombres de ruta del front no chocan con los del backend porque viven en otro registro: el router del front frente a routes.json.

Guardas ​

MarcaVueReactEfecto
Invitadometa.guesthandle.guestQuien ya tiene sesión va a /admin.
Sesiónmeta.authhandle.authQuien no la tiene va a /auth/login?redirect=<ruta>. Las rutas del módulo traen auth: true.
Administradormeta.admin, o el nombre en adminOnlyhandle.admin, o el id en adminOnlyExige is_admin; sin él, a /admin con un aviso.
  • Las guardas miran toda la cadena de rutas: una hija hereda lo que exige su padre.
  • ?redirect= sólo acepta rutas internas: empiezan por / y no por // ni /\.

resources/<ui>/app/config.js ​

Exporta adminOnly: los nombres (Vue) o ids (React) de las rutas del módulo que sólo ve un administrador. Por omisión, la de usuarios (AdminUsers).

El menú del administrador ​

Se construye, no se escribe. Cada ruta de primer nivel del módulo con título (meta.title / handle.title) y sin parámetros es una entrada. Las de adminOnly sólo se muestran a un administrador. Además:

  • Inicio (admin.dashboard), siempre primero.
  • Grupo «Administración», sólo para administradores: las rutas de adminOnly, «Sitio» (admin.site), «Registros» (/log-viewer, en otra pestaña) y «Entorno» (/env-editor, en otra pestaña).

Un modelo que se genere después aparece solo en el menú al compilar.

Estado ​

auth ​

  • load(). GET route('auth.get.auth') → { user, authenticated, is_admin, verified, impersonating }. verified es true para un usuario que no implementa MustVerifyEmail (laravel-auth 6.0.2): el aviso de verificación sólo sale cuando de verdad hay un correo que verificar.
  • login({ email, password, remember }). Primero GET /sanctum/csrf-cookie (route('sanctum.csrf-cookie') si está en routes.json), luego el POST de auth.login, luego load().
  • Las demás. register, logout, forgotPassword, resetPassword, updatePassword, resendVerification y revertImpersonation usan las rutas de laravel-auth 6, con nombres con punto:
AcciónRuta
registerauth.register
logoutauth.logout
forgotPasswordauth.forgot.password
resetPasswordauth.reset.password
updatePasswordauth.update.password
resendVerificationauth.email.verification.notification
revertImpersonationauth.revert.impersonate
  • revertImpersonation(). POST de auth.revert.impersonate y después load(). laravel-auth 6.1 ya no acepta GET, que otro sitio podía disparar con un <img>. Pasa por el token CSRF como cualquier otro POST.
  • ?redirect=. Sólo acepta rutas internas.
  • Errores. Un 401 fuera de load() limpia la sesión y manda al login. Un 419 pide otra vez la cookie CSRF y repite la petición una vez.

options ​

  • load(). GET route('api.laravel-options.option.index', { paginate: 0 }), que es público. Guarda key → value y el id de cada opción. Un value que es un objeto o un arreglo JSON se guarda decodificado; cualquier otro texto se queda como texto (un site_name "2024" no se vuelve número), igual que Option::value() en el backend.
  • option(path, default). option('site_name') u option('theme.home'), que entra en el JSON por puntos.
  • save(key, value). Llama al update de laravel-options (sólo administrador) con option_id, name, key y el value, serializado a JSON si no es texto. Si la opción todavía no existe, llama a su create. Después actualiza el estado.

notifications (laravel-notifications 2.1) ​

  • Contador. Las no leídas, al montar el administrador, cada 60 s y al volver a la pestaña.
  • Lista. Las últimas, al abrir la campana.
  • Marcar. Marcar una como leída navega a data.action: una ruta interna con el router, una URL absoluta con location. También se pueden marcar todas.
  • Texto. data.message se pinta como texto, nunca como HTML.

El administrador ​

Layout con fe-shell: cabecera (nombre del sitio; botón del menú en móvil, con un fondo que lo cierra; campana; modo oscuro; menú de usuario) y barra lateral con el menú.

  • Aviso de suplantación, cuando impersonating: «Estás viendo la cuenta de …» y «Volver a mi cuenta».
  • Aviso de verificación, cuando verified === false: reenviar el correo.
  • Menú de usuario: Perfil y Cerrar sesión.
  • Modo oscuro: data-theme en <html>, recordado en localStorage. Sin elección, el del sistema.
  • Inicio: saludo y una tarjeta por entrada del menú.
  • Perfil.
    • Nombre y correo con el update del usuario generado: api.app.user.update, campos user_id, name y email.
    • Foto subida con laravel-uploads (lu.upload.file, campo file) y guardada como meta avatar con el mismo update, usando el uri relativo de la subida. Quitarla envía avatar: ''.
    • Contraseña con el update-password de laravel-auth.
  • Editor del sitio (ver abajo).

Detalle en El administrador y Acceso y usuarios.

El sitio ​

Las páginas se pintan desde la opción theme, que siembra SiteOptionsSeeder. Es el formato del theme-manager de siempre, así que el contenido de una aplicación antigua se sigue viendo:

json
{
    "home": {
        "title": "Inicio",
        "sections": [
            { "theme": "legacy", "group": "hero", "name": "HeroOne", "props": { "display": true, "title": "..." } }
        ]
    },
    "privacy": { "title": "...", "sections": [] },
    "terms": { "title": "...", "sections": [] },
    "contact": { "title": "...", "sections": [] },
    "join": { "title": "...", "sections": [] }
}
  • Registro. Cada sección se busca por <theme>/<group>/<name>. Una que no existe no se pinta y avisa en consola.
  • display. Una sección con props.display en false o "false" no se pinta; sin la clave, sí.
  • Título. El de la pestaña es <título de la página> · <site_name>.
  • Imágenes. Toda imagen es opcional: sin ella la sección se ve bien, sin huecos rotos.
  • Estilos. Salen de las variables de form-core (--fe-*), así que el sitio tiene modo oscuro. Sin Tailwind, sin Headless UI, sin Heroicons: los iconos son el Icon de form-elements.
  • Enlaces. Uno que empieza por / navega con el router; cualquier otro es un enlace normal.

Las secciones y sus props ​

SecciónProps
legacy/header/HeaderOnelogo, nav: [{ label, link }], facebook, twitter, instagram, youtube, whatsapp, linkedin, tiktok. Muestra «Entrar» o «Administrador» según la sesión.
legacy/hero/HeroOnebadge, badge_value, badge_link, title, message, primary_button_text, primary_button_link, secondary_button_text, secondary_button_link, videos: [url] (uno al azar)
legacy/hero/HeroTwoLas de HeroOne sin videos, más badge_value_link, video, display_play_button, play_button_text, imgs_1, imgs_2, imgs_3: [url]
legacy/hero/HeroThreeLas de HeroOne sin videos
legacy/section/MissionSectiontitle, subtitle, message, button_text, button_link, images: [url] (hasta 4)
legacy/section/JoinSectiontitle, subtitle, image, features: [texto], button_text, button_link
legacy/section/FaqSectiontitle, subtitle, items: [{ question, answer }]
legacy/section/PartnersSectiontitle, items: [{ name, logo, link }]
legacy/section/TestimonialsSectiontitle, subtitle, feature: { body, author: { name, handle, image } }, items: [{ body, author: { name, handle, image } }]
legacy/section/PlansSectiontitle, subtitle, frequencies: [{ value, label, price_suffix }], tiers: [{ id, name, href, description, price: { <frecuencia>: texto }, features: [texto], most_popular }]
legacy/section/HtmlContentcontent: HTML que escribe el administrador
legacy/footer/FooterOnelogo, description, cols: [{ title, items: [{ name, link }] }], newsletter: { title, subtitle, button_text, button_link }, social_links: { facebook, instagram, twitter, github, youtube, linkedin, tiktok, whatsapp }
legacy/cookie-consent/CookieConsentOnemessage, accept_text, reject_text, policy_link. Guarda la decisión en la cookie cookie_consent (accepted o rejected) y no vuelve a salir.

Cada prop, con su tipo y su comportamiento, en El sitio y su editor.

El editor del sitio ​

/admin/site, sólo administrador:

  • General: site_name y site_description.
  • Páginas: una pestaña por página. En cada una, sus secciones en orden: activar o desactivar (display), subir, bajar, quitar y añadir una del registro.
  • Props: las de cada sección, en un editor JSON (CodeMirror, json). Un JSON inválido se marca y no deja guardar.
  • «Ver página» abre la página en otra pestaña.
  • Guardar escribe la opción theme con options.save y avisa con un toast. Un 422 enseña el error.

Dónde el código se aparta del contrato ​

En laravel-setup 7.0.1, el contrato no recoge estos puntos:

  • Update de opciones en Vue. Vue envía sólo { option_id, value } y React envía los cuatro campos. Funciona porque en laravel-options key es sometimes y name es nullable.
  • Ajustes. El contrato sólo nombra adminOnly. React exporta además adminBase, userUpdateRoute, adminTools, notificationsInterval y sitePages desde config.js; Vue tiene esos valores escritos en router/index.js, router/menu.js, admin/ProfileView.vue y stores/notifications.js.
  • Diferencias de comportamiento. Pantalla de error en React, claves de texto distintas («Sign in» / «Log in»), el nombre de la opción que evita la redirección en un 401 (skipAuthHandling / skipAuthRedirect) y otras. Están en Personalizar y ampliar.
  • Lo que no está conectado. Login social, empezar una suplantación, auditoría y gestor de archivos S3 no tienen interfaz. Ver Qué trae.