A humble PHP framework & CMS
The Humblee admin interface is a hybrid architecture: the server renders HTML pages, and individual admin tools are modern JavaScript single-page applications (SPAs) built with Svelte and TypeScript, embedded in those pages. The public-facing side of the site uses whatever HTML and CSS your view templates produce — no frontend framework is prescribed.
Each tool in the admin panel that requires rich interactivity is a dedicated Svelte application. The current tools are:
| App | Path | Purpose |
|---|---|---|
admin-home |
admin/ |
Dashboard — activity overview |
blocks |
admin/blocks |
Content block type management |
media-manager |
admin/media |
File upload, organization, and access control |
page-editor |
admin/pages/{id} |
Inline content editing with draft/publish workflow |
page-manager |
admin/pages |
Page list, reordering, slug management |
personalization |
admin/personalization |
Audience segment rules for content variants |
session-monitor |
(admin layout) | Watches session expiry and warns the user |
templates |
admin/templates |
Template and block slot configuration |
toolbar |
(public pages) | Inline editing toolbar shown to authenticated editors |
tools |
admin/tools |
Developer utilities |
user-manager |
admin/users |
User account and role management |
Each app is fully isolated — it does not share code with other apps at runtime.
/package.json ← Root workspace: exposes npm run build/dev commands
/public/package.json ← Bulma CSS and CSS utilities (web-accessible, never bundled)
/frontend/package.json ← Vite, Svelte, TypeScript (build-time only)
└── apps/ ← npm workspace: one project per admin tool
├── media-manager/
├── page-editor/
└── ...
The two npm contexts (public/ and frontend/) are intentionally separate. Bulma must remain in public/node_modules/ where the PHP templates can reference it by path. Hoisting it into a shared node_modules/ would break those references.
npm run setup # Install all deps — run once after cloning
npm run build # Rebuild all admin apps
npm run build:media-manager # Rebuild one app
npm run dev:media-manager # Vite dev server with hot reload for one app
npm run dev:page-editor # Vite dev server for the page editor
Replace the app name after build: or dev: with any directory name under frontend/apps/.
Each app compiles to a fixed-name pair of files:
public/humblee/js/admin/{app-name}/
├── index.js ← Compiled Svelte + TypeScript (ES module)
└── index.css ← Scoped component styles
These files are committed to git. There are no content-hash suffixes — filenames are always index.js and index.css. After making any change to Svelte source files, rebuild the affected app and commit both the source change and the new index.js/index.css.
The PHP admin controller sets the $this->extra_head_code property in the relevant action method. The admin layout template outputs this inside <head>, so the module loads before the page body becomes interactive.
// In humblee/src/Controller/Admin.php:
public function media(): void
{
$this->page_title = 'Media';
$this->extra_head_code = '<link rel="stylesheet" href="' . _app_path . 'humblee/js/admin/media-manager/index.css">';
$this->extra_head_code .= '<script type="module" src="' . _app_path . 'humblee/js/admin/media-manager/index.js"></script>';
}
The corresponding view template also includes a <div id="app"></div> mount point and a <script> block that sets the configuration global (see below).
PHP passes runtime values to the Svelte app through a global object set before the module loads. There is no separate configuration file or environment variable — all context comes from PHP at page render time.
<script>
window.__MEDIA_CONFIG__ = {
XHR_PATH: "<?php echo _app_path ?>core-request/",
WEB_ROOT: "<?php echo _app_path ?>",
hasAdminRole: <?php echo json_encode((bool) Core::auth(['admin', 'developer'])) ?>,
hasMediaRole: <?php echo json_encode((bool) Core::auth('media')) ?>,
csrf: <?php echo json_encode(\Humblee\Model\Crypto::get_hmac_pair()) ?>
};
</script>
<div id="app"></div>
Rules for config injection:
json_encode() for all values — it handles escaping and PHP→JS type coercion(bool) $valuecsrf) so the first POST is immediately ready<script lang="ts">
import MediaManager from './lib/MediaManager.svelte'
const config = (window as any).__MEDIA_CONFIG__
const xhrPath: string = config.XHR_PATH
const hasAdminRole: boolean = config.hasAdminRole
let csrf = config.csrf // mutable — refresh after each successful POST
</script>
<MediaManager {xhrPath} {hasAdminRole} bind:csrf />
Read window.__* only in App.svelte. Pass values as typed props to child components — never let child components reach into window directly.
| App | Global |
|---|---|
media-manager |
window.__MEDIA_CONFIG__ |
page-editor |
window.__PAGE_EDITOR_CONFIG__ |
templates |
window.__TEMPLATES_CONFIG__ |
page-manager |
window.__PAGE_MANAGER_CONFIG__ |
admin-home |
window.__ADMIN_HOME_CONFIG__ |
Pattern: window.__[APP_NAME_UPPER_SNAKE]_CONFIG__
Every POST to an XHR endpoint requires a valid HMAC pair. After each successful response, the server returns a fresh pair. The Svelte app must update its stored csrf before the next request.
// src/lib/services/myApi.ts
export async function saveItem(
xhrPath: string,
csrf: { key: string; token: string },
formData: FormData
) {
formData.append('hmac_key', csrf.key)
formData.append('hmac_token', csrf.token)
const res = await fetch(xhrPath + 'my_group/save', { method: 'POST', body: formData })
const data = await res.json()
return data // caller updates csrf from data.csrf
}
<script lang="ts">
export let csrf: { key: string; token: string }
async function handleSave() {
const fd = new FormData()
fd.append('name', itemName)
const data = await saveItem(xhrPath, csrf, fd)
if (data.csrf) csrf = data.csrf // refresh for next call
}
</script>
The bind:csrf in App.svelte propagates the update up so all child components share the refreshed token.
See Extending Humblee for the full step-by-step walkthrough. The short version:
frontend/apps/ and rename itvite.config.ts with the new app name in outDirbuild:new-app and dev:new-app script to the root package.jsonnpm run build:new-app) to create the public/humblee/js/admin/new-app/ output$this->extra_head_code<div id="app"></div>