Skip to content

The interface contract

app:setup builds the interface in Vue or React, and both are the same application: same routes, same screens, same site JSON and same backend calls. This contract is what both honor. If one changes something here, the other changes the same way.

The source is the package's docs/shell-contract.md. This page covers all of it and ends with where the code departs from it.

Where each thing lives

text
stubs/app/common/              backend, shared by both
stubs/app/<ui>/                copied over the application root
  package.json
  vite.config.js
  resources/views/app.blade.php
  resources/<ui>/app/**        the base application
  resources/<ui>/routes.json   placeholder; php artisan route:json rewrites it
resources/<ui>/index.js        the LaraPack module (generated by larapack:import)
resources/<ui>/src/**          its models: users and whatever is generated later
tests/Frontend/<ui>/           interface tests, outside what is copied
  • resources/<ui>/app/** belongs to laravel-setup when copied and to you afterwards.
  • resources/<ui>/index.js and resources/<ui>/src/** belong to LaraPack. The base application imports them and never edits them.

Boot

  1. axios. axios.defaults with withCredentials, withXSRFToken, Accept: application/json and X-Requested-With: XMLHttpRequest. A single axios copy.
  2. Backend routes. setRoutes(routes.json) from innoboxrr-route-resolver. No backend URL is written by hand: they all come from route(name).
  3. Strings. addTranslations with the LaraPack module's and then resources/<ui>/app/lang/*.json, in that order, and setLocale(document.documentElement.lang). The base application's strings use t('English key'), with the translation in lang/es.json.
  4. Styles. innoboxrr-form-core/styles and the module's src/theme.js, if it exists.
  5. Session and options. They load (auth and options) before the router mounts.
  6. Toasts. ToastRegion and ConfirmHost are mounted once, at the root.

Front-end routes

PathNameAccessScreen
/site.homeEveryoneThe site's home page
/privacysite.privacyEveryoneThe privacy page
/termssite.termsEveryoneThe terms page
/contactsite.contactEveryoneThe contact page
/joinsite.joinEveryoneThe join page
/auth/loginauth.loginGuestLog in; honors ?redirect=
/auth/registerauth.registerGuestRegister
/auth/forgot-passwordauth.forgot-passwordGuestRequest the link
/auth/reset-password/:token/:emailauth.reset-passwordGuestNew password; it is the URL in laravel-auth's email
/adminadmin.dashboardSessionAdmin home
/admin/profileadmin.profileSessionProfile: name, email, photo and password
/admin/siteadmin.siteAdminSite editor
/admin/<model>/…The module'sSession, and admin if in adminOnlyThe routes the LaraPack module exports, as children of /admin
Anything elsenot-foundEveryone404

Front-end route names do not clash with backend ones because they live in another registry: the front-end router versus routes.json.

Guards

FlagVueReactEffect
Guestmeta.guesthandle.guestA user with a session goes to /admin.
Sessionmeta.authhandle.authA visitor goes to /auth/login?redirect=<route>. Module routes carry auth: true.
Adminmeta.admin, or the name in adminOnlyhandle.admin, or the id in adminOnlyRequires is_admin; without it, to /admin with a toast.
  • Guards look at the whole route chain: a child inherits what its parent requires.
  • ?redirect= only accepts internal paths: they start with / and not // or /\.

resources/<ui>/app/config.js

Exports adminOnly: the names (Vue) or ids (React) of the module routes only an admin sees. By default, the users one (AdminUsers).

The admin menu

It is built, not written. Every first-level module route with a title (meta.title / handle.title) and no parameters is an entry. Those in adminOnly only show to an admin. Plus:

  • Home (admin.dashboard), always first.
  • "Administration" group, admins only: the adminOnly routes, "Site" (admin.site), "Logs" (/log-viewer, new tab) and "Environment" (/env-editor, new tab).

A model generated later shows up in the menu on its own at build time.

State

auth

  • load(). GET route('auth.get.auth'){ user, authenticated, is_admin, verified, impersonating }. verified is true for a user that does not implement MustVerifyEmail (laravel-auth 6.0.2): the verification notice only shows when there really is an email to verify.
  • login({ email, password, remember }). First GET /sanctum/csrf-cookie (route('sanctum.csrf-cookie') if it is in routes.json), then the auth.login POST, then load().
  • The rest. register, logout, forgotPassword, resetPassword, updatePassword, resendVerification and revertImpersonation use laravel-auth 6's routes, with dotted names:
ActionRoute
registerauth.register
logoutauth.logout
forgotPasswordauth.forgot.password
resetPasswordauth.reset.password
updatePasswordauth.update.password
resendVerificationauth.email.verification.notification
revertImpersonationauth.revert.impersonate
  • revertImpersonation(). POST to auth.revert.impersonate, then load(). laravel-auth 6.1 no longer accepts GET, which another site could trigger with an <img>. It carries the CSRF token like any other POST.
  • ?redirect=. Only accepts internal paths.
  • Errors. A 401 outside load() clears the session and sends to login. A 419 fetches the CSRF cookie again and retries the request once.

options

  • load(). GET route('api.laravel-options.option.index', { paginate: 0 }), which is public. Stores key → value and each option's id. A value that is a JSON object or array is stored decoded; any other text stays text (a site_name of "2024" does not become a number), just like Option::value() in the backend.
  • option(path, default). option('site_name') or option('theme.home'), which walks into the JSON by dots.
  • save(key, value). Calls laravel-options' update (admin only) with option_id, name, key and the value, serialized to JSON when it is not text. If the option does not exist yet, calls its create. Then updates the state.

notifications (laravel-notifications 2.1)

  • Count. The unread ones, when the admin panel mounts, every 60 s and when the tab becomes visible.
  • List. The latest ones, when the bell opens.
  • Marking. Marking one as read navigates to data.action: an internal path with the router, an absolute URL with location. All can be marked at once.
  • Text. data.message is rendered as text, never as HTML.

The admin panel

A layout with fe-shell: a header (site name; menu button on mobile, with a backdrop that closes it; bell; dark mode; user menu) and a sidebar with the menu.

  • Impersonation notice, when impersonating: "You are viewing the account of …" and "Back to my account".
  • Verification notice, when verified === false: resend the email.
  • User menu: Profile and Sign out.
  • Dark mode: data-theme on <html>, remembered in localStorage. With no choice, the system's.
  • Home: a greeting and one card per menu entry.
  • Profile.
    • Name and email through the generated user's update: api.app.user.update, fields user_id, name and email.
    • Photo uploaded with laravel-uploads (lu.upload.file, field file) and saved as the avatar meta through the same update, using the upload's relative uri. Removing it sends avatar: ''.
    • Password through laravel-auth's update-password.
  • Site editor (see below).

Details in The admin panel and Authentication and users.

The site

Pages render from the theme option, seeded by SiteOptionsSeeder. It is the old theme-manager's format, so an older application's content still renders:

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": [] }
}
  • Registry. Each section is looked up by <theme>/<group>/<name>. One that does not exist does not render and logs a warning.
  • display. A section with props.display set to false or "false" does not render; without the key, it does.
  • Title. The tab title is <page title> · <site_name>.
  • Images. Every image is optional: without it the section still looks right, with no broken gaps.
  • Styles. They come from form-core's variables (--fe-*), so the site has dark mode. No Tailwind, no Headless UI, no Heroicons: icons use form-elements' Icon.
  • Links. One that starts with / navigates with the router; anything else is a normal link.

Sections and their props

SectionProps
legacy/header/HeaderOnelogo, nav: [{ label, link }], facebook, twitter, instagram, youtube, whatsapp, linkedin, tiktok. Shows "Sign in" or "Administrator" depending on the session.
legacy/hero/HeroOnebadge, badge_value, badge_link, title, message, primary_button_text, primary_button_link, secondary_button_text, secondary_button_link, videos: [url] (one at random)
legacy/hero/HeroTwoHeroOne's without videos, plus badge_value_link, video, display_play_button, play_button_text, imgs_1, imgs_2, imgs_3: [url]
legacy/hero/HeroThreeHeroOne's without videos
legacy/section/MissionSectiontitle, subtitle, message, button_text, button_link, images: [url] (up to 4)
legacy/section/JoinSectiontitle, subtitle, image, features: [text], 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: { <frequency>: text }, features: [text], most_popular }]
legacy/section/HtmlContentcontent: HTML written by the admin
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. Stores the decision in the cookie_consent cookie (accepted or rejected) and does not show again.

Every prop, with its type and behavior, is in The site and its editor.

The site editor

/admin/site, admins only:

  • General: site_name and site_description.
  • Pages: one tab per page. In each, its sections in order: toggle (display), move up, move down, remove, and add one from the registry.
  • Props: each section's, in a JSON editor (CodeMirror, json). Invalid JSON is flagged and blocks saving.
  • "View page" opens the page in a new tab.
  • Save writes the theme option with options.save and confirms with a toast. A 422 shows the error.

Where the code departs from the contract

In laravel-setup 7.0.1, the contract does not capture these points:

  • Option update in Vue. Vue sends only { option_id, value } and React sends all four fields. It works because in laravel-options key is sometimes and name is nullable.
  • Settings. The contract only names adminOnly. React also exports adminBase, userUpdateRoute, adminTools, notificationsInterval and sitePages from config.js; Vue has those values written in router/index.js, router/menu.js, admin/ProfileView.vue and stores/notifications.js.
  • Behavioral differences. React's error screen, different string keys ("Sign in" / "Log in"), the name of the option that skips the 401 redirect (skipAuthHandling / skipAuthRedirect) and more. They are in Customize and extend.
  • What is not wired. Social login, starting an impersonation, auditing and the S3 file manager have no interface. See What it includes.