File Structure
Root /
| File | Role |
|---|---|
index.php | Front-end router |
install.php | First-run installer |
config.json | Site settings flat-file store (admin folder name, active theme, language, SEO defaults, canonical_host…) — direct access blocked |
version.json | Current CMS version, checked against the update registry |
plugins.json (plugin activation registry) lives in /plugins/, not the root — see Plugins.
composer.json/package.json at the root are dev-only tooling (PHPUnit, JS/CSS minifiers for the release build) — never shipped, excluded by export-release.sh. There is still no runtime Composer autoloader and no build step for the CMS itself.
Core libraries /core/
Most of these are now thin loaders (a handful of require_once lines, no logic of their own) that pull in focused modules from a matching subdirectory. This split happened without renaming any public function, so every call site elsewhere in the codebase — and everything in this doc — still refers to the loader's filename, not the module it delegates to.
| File | Role |
|---|---|
functions.php | Bootstrap loader — requires core-functions.php, data-functions.php, render/theme-api.php, data-layer.php, plugin-api.php, then lang-cache.php, then the active theme's functions.php (and its child-theme override, if any), then pl_load_active_plugins() |
core-functions.php | Loader → core/content/render-helpers.php, dispatch.php, galleries.php |
data-functions.php | Routing, SEO, category/tag page rendering, parseRequestUri(), getCategoryPath() — not split, still one file |
data-layer.php | Loader → core/data/* (read-only sl_* functions) |
admin-data-layer.php | Loader → core/admin/* (write sl_admin_* functions) |
plugin-api.php | Plugin registry, activation state, admin-menu hook, pl_* hook/filter system — not split |
lang-cache.php | i18n cache and locale management (__t(), _e(), __n(), lang_js_bridge()) |
search.php | Search endpoint |
contact-process.php | Contact form handler |
feed.php | RSS feed |
llms.php / llms-full.php | Serve /llms.txt and /llms-full.txt (LLM indexer files) — loaded standalone, before functions.php, to stay lightweight |
core/content/ — what core-functions.php now delegates to
| File | Role |
|---|---|
render-helpers.php | resolve_admin_dir() and related front-end render helpers |
dispatch.php | processContent() — resolves a request into a rendered item, including the content-generic fallback for custom content types |
galleries.php | getGalleryScripts() and other gallery-layout helpers |
core/data/ — what data-layer.php loads
| File | Covers |
|---|---|
cache.php | _sl_cache_get()/_sl_cache_set() — APCu-or-file cache primitive |
page-cache.php | Full-page HTML cache (sl_page_cache_get/set, _sl_page_cache_file()) — see Security for the cache-key rule |
item-store.php | sl_load_index(), sl_load_item(), sl_load_item_by_slug(), sl_item_path(), sl_load_all_items() |
taxonomy-store.php | Category/tag loading |
type-registry.php | sl_content_types(), sl_content_type(), sl_content_type_exists(), sl_all_type_slugs(), sl_type_dir() — the content-type registry read side, see Data Layer |
misc.php | Bootstrap-adjacent helpers that didn't fit elsewhere |
core/admin/ — what admin-data-layer.php loads
| File | Covers |
|---|---|
bootstrap.php | sl_admin_ensure_dirs() and other setup helpers |
media.php | Media/upload bookkeeping used by the write path |
writer.php | _sl_write_json() — atomic write, temp file (PID-suffixed, see Security) + rename() |
content-types.php | sl_admin_register_content_type(), sl_admin_update_content_type(), sl_admin_unregister_content_type() — the registry write side |
activity-log.php | Admin activity log writes |
items.php | Content save/update entry points |
trash.php | Trash/restore |
revisions.php | Revision snapshots and diffing |
index-store.php | Rewriting a type's _index.json after a write |
Rendering /core/render/
| File | Role |
|---|---|
theme-api.php | Requires the render files below, plus the hook/filter/theme-options system |
tf-page.php | Loader → core/render/page/* |
tf-cards.php | Article/project cards, homepage sections, summaries, related items, custom fields |
tf-shortcodes.php | render_content_html() pipeline and every shortcode renderer |
tf-navigation.php | renderHierarchicalMenu(), renderDefaultMenu() |
tf-markdown.php | Markdown parser, container directives (:::type) |
asset-registry.php | enqueue_css()/enqueue_js()/render_enqueued_assets(), plus sl_content_needs_shortcode_assets()/sl_content_needs_gallery_assets() — see Front-End Asset Registry |
core/render/page/ — what tf-page.php loads
| File | Covers |
|---|---|
identity.php | Site logo/favicon helpers |
meta.php | render_meta_tags() |
assets.php | render_header_scripts(), the highlight.js/collapsibles conditional includes (reads global $pageContent) |
content-pieces.php | Per-item render helpers: title, date, category, tags, gallery |
footer.php | render_footer_content() |
theme-helpers.php | Misc theme-facing helpers |
search.php | render_search_ui(), render_search_icon() |
Data directory /data/
data/
├── content-types.json — custom content-type registry (built-ins are not stored here, see Data Layer)
├── articles/{_index.json, {slug}.json…}
├── pages/{_index.json, {slug}.json…}
├── projects/{_index.json, {slug}.json…}
├── {custom-type}/{_index.json, {slug}.json…} — one directory per registered custom type
├── drafts/{draft_*.json…} — in-progress items before first publish
├── categories.json
└── tags.json
A custom type's storage directory is its identifier exactly as entered (no automatic pluralization) — only the three built-in types get the legacy s-suffixed directory name. Every content item also has its own trash and revisions store, created on demand next to its type folder (see Data Layer).
Cache /cache/
Nothing here is hand-edited; every file rebuilds automatically and is safe to delete.
| Path | Contains |
|---|---|
lang/front/{locale}.php, lang/admin/{locale}.php | Compiled locale strings |
sl_idx_{type}.cache.php | Compiled index caches |
sl_categories.cache.php, sl_tags.cache.php | Compiled taxonomy caches |
media-stats.json | Cached media library stats |
htaccess-check.json | Cached dashboard check for public /data/ exposure on nginx |
Other key directories
| Path | Purpose |
|---|---|
/assets/css/ | search.css, shortcodes.css, gallery-layout.css (galleries and collapsibles/tabs — the name is legacy, see Front-End CSS Tokens), lightbox.css, hljs-theme.css, and the synaptikCSS.php combiner |
/assets/js/ | front-boot.js (data islands, always loaded) plus features/{search,lightbox,collapsibles}.js (search always loaded, the other two conditional — see Front-End Asset Registry) |
/bckps/ | Backups: full-site exports, update safety snapshots, template editor file backups |
/files/ | Media uploads |
/lang/front/, /lang/admin/ | Locale files |
/private/ | Never web-accessible: users.json, contact.secret, theme_preview.secret, contact_rate.json, auth_rate.json, search_rate.json, activity-log.json, audit-log.json |
/theme/ | Installed themes, plus theme/child_theme/{name}/ overrides |
/plugins/ | Installed plugins, plus plugins.json (activation registry) |
Admin panel /admin/
The admin folder name is configurable at install time and read from config.json['admin_dir'] — never hardcoded. Key files:
| File | Role |
|---|---|
index.php | Admin router (?action=), including ?action=plugin_page for plugin admin pages |
content.php | Content save/delete/trash/restore/revision action dispatch — delegates the heavy lifting to admin/includes/content-handlers.php |
manage-users.php | Multi-user account create/update/delete/role-change |
file-manager.php, file-upload.php, get-files.php, image-optimization.php, batch-optimize.php | Media manager |
template-editor.php | Theme file editor with per-file backups |
theme-preview.php | Live theme preview (signed token) — also backs the editor's Preview button (there is no separate preview.php anymore) |
extension-upload.php | ZIP import for themes and plugins |
autosave.php | Draft autosave endpoint |
seo-overview.php, alt-text-assistant.php, sitemap-generator.php | SEO tools |
auth.php, forgot-password.php, reset-password.php, change-password.php | Authentication |
translations-api.php | Translation editor AJAX endpoint |
list-content.php | Server-side pagination/search/sort for the content list |
backup-dl.php | Backup download proxy |
/admin/includes/
admin-functions.php is now a loader → admin/includes/functions/{bootstrap,config,users,content-builder,session-bridge,themes,theme-editor,updates}.php. Alongside it: content-handlers.php (content add/edit/menu-sync/image-upload logic used by content.php), content-purify.php, backup-functions.php, extension-update-functions.php, admin-icons.php, zip-validation.php, htaccess-manager.php, session-config.php, layout.php (admin <head>/topbar/sidebar shell), sidebar.php, template-renderer.php (admin_render_template() — explicit $vars array instead of ambient-scope include, used by every action-dispatch template include in content.php).
/admin/assets/
admin/assets/css/ and admin/assets/js/ hold the admin panel's own stylesheets and scripts (distinct from the front-end /assets/ at the project root) — admin-base.css, admin-components.css, admin-content.css, admin-sidebar.css, editor-layout.css, admin-filemanager.css, and one JS file per admin screen plus shared boot scripts (admin-boot.js, theme-boot.js, common.js, etc.).
/admin/templates/
content-add.php, content-edit.php, content-list.php, content-types-manage.php, trash.php, revision-diff.php, pending-diff.php, activity-log.php, users.php, system-info.php, menu-builder.php, settings-view.php, theme-manager.php, categories-manage.php, tags-manage.php, backup.php, account.php, translations.php, update.php, plugins-manager.php, contact.php. Each is called via admin_render_template() with an explicit variable list, not raw include.
/admin/cache/
extensions-check.json, update-check.json, news-cache.json — all disposable, admin-only, distinct from the root /cache/.
Plugins
plugins/
├── plugins.json — activation registry: { "slug": { "active": bool } }
└── my-plugin/
├── plugin.json — manifest, must set "synaptik_plugin": true
├── my-plugin-init.php — entry file declared in plugin.json
├── data/ — plugin's own JSON store, .htaccess-protected
├── private/ — CSRF secrets, rate-limit stores, .htaccess-protected
├── admin/{admin-page.php, actions.php}
├── assets/
└── lang/
A plugin present on disk but absent (or inactive) in plugins.json never runs — activating it is what loads it, via pl_load_active_plugins() in core/functions.php. See the Plugins category for the hook/filter API and integration points.
Version history of this layout
| Change | Since |
|---|---|
/css/, /js/ → /assets/css/, /assets/js/; /lang/*.json → /lang/front/*.json | 1.2 |
settings.json → config.json; root PHP libraries moved to /core/ and /core/render/; plugin registry moved to /plugins/plugins.json | 1.3.3 |
Autosave drafts moved from /admin/drafts/ into /data/drafts/ (drafts are now real, trashable, revisable content) | 1.4.0 |
/private/users.json (multi-user accounts), /private/activity-log.json, per-item trash and revisions folders added | 1.4.0 |
data/content-types.json (custom content-type registry); admin/css/, admin/js/ moved to admin/assets/css/, admin/assets/js/; admin/preview.php removed (folded into theme-preview.php) | 1.4.6 |
core/data-layer.php, core/admin-data-layer.php, core/core-functions.php, core/render/tf-page.php, and admin/includes/admin-functions.php became thin loaders over core/data/, core/admin/, core/content/, core/render/page/, and admin/includes/functions/ respectively — a maintainability split, no function renamed or moved in behavior | 1.4.6 |
Upgrades migrate all of the above automatically on first load — no manual action needed. Theme code using the standard API (render_header_scripts(), __t(), _e(), etc.) is never affected by internal path changes.
