Refactor/public webroot#46
Merged
Merged
Conversation
Captures the architecture decision to move Scriptor 2.0 to a
`public/`-as-webroot layout (Symfony/Laravel/Drupal-8+ pattern), in
response to discovering that the current flat-root layout exposes
`data/imanager.db`, `.git/`, `vendor/`, `boot/`, `bin/` on any
Caddy/nginx that doesn't ship explicit deny rules (Apache via
`.htaccess` is fine, but `.htaccess` is server-config-specific defense
that doesn't generalise).
The plan covers the final layout, the file-by-file mapping, every
code change site (~10 files), the asset-URL helper strategy
(`themeAssetUrl` / `editorAssetUrl` / `Editor::assetUrl`), Apache /
nginx / Caddy / php-S server config templates, the full test matrix,
deployment + rollback, and the eight decisions (six recommended, all
confirmed):
1. favicon: copies at public/favicon.ico AND public/editor-assets/
2. naming: public/editor-assets/ (explicit, non-clashing with
admin_path URL space)
3. theme-internal vendor/ stays with the theme
4. theme_path config keeps trailing slash
5. bin/ keeps its name
6. no migration tooling (0 external installs)
7. docs/themes.md ships in the same PR
8. same-day live deploy after merge
First step of the public/-webroot refactor: introduce the new webroot with its front controller and Apache-fallback .htaccess. No files have been moved yet; this commit is structural only. public/index.php is the same admin-path delegation pattern the root index.php uses today, but anchored one directory up (`dirname(__DIR__) . '/boot.php'`). The frontend theme root moves from `<root>/site/themes/<theme>` to `<root>/themes/<theme>` to match the planned split where PHP source lives outside public/ and only static assets live under public/themes/. public/.htaccess is drastically simpler than the old root .htaccess: no directory deny list (source / data / configs live outside the webroot now, so there's nothing dangerous to enumerate), just a dotfile rule and the catch-all to index.php. Next commits do the directory moves and code-path updates that make this skeleton functional.
Themes split into two halves to fit the new public/-webroot:
themes/basic/ ← PHP source, OUTSIDE the webroot
_ext.php, template.php, default.php, blog.php, blog-post.php,
contact.php, 404.php, lib/, resources/, vendor/, composer.{json,lock}
public/themes/basic/ ← Static assets, INSIDE the webroot
css/, fonts/, images/, scripts/
The site/themes/ wrapper directory is gone; themes are a top-level
concept. Theme-internal vendor/ stays with the theme — it's
PHP-included only via _ext.php's `require __DIR__ . '/vendor/autoload.php'`
and is now physically unreachable through HTTP because it lives
outside public/.
URL helper methods (Site::themeAssetUrl, Site::editorAssetUrl) and
the template rewrites that consume them land in a follow-up commit.
Right now the basic theme's templates still hardcode `/site/themes/`
URLs and the frontend renders 404s for theme assets — that's expected
mid-refactor and gets fixed when the helpers land.
Git records every file as a rename (R) so blame/log history is
preserved.
The site/ wrapper directory disappears with the public/-webroot refactor. site/themes/ moved out in the previous commit; site/modules/ now moves to modules/ at the repo root. modules/ contains user-installable site modules — PHP code, never web-reachable directly. Living outside public/ matches its role. Currently empty (only empty.txt as a placeholder).
The editor theme — like site themes — splits into two halves:
editor/theme/ ← PHP, included only by editor/index.php
template.php, header.php, summary.php
public/editor-assets/ ← Static, served directly by the web server
css/, fonts/, images/, scripts/ (incl. filepond/ and remarkable/),
favicon.ico
The editor theme's PHP files (template.php, header.php, summary.php)
stay in editor/theme/ — they're only ever included from
editor/index.php, never web-served, and now live outside public/ so
they're physically unreachable through HTTP.
favicon.ico is duplicated: once under public/editor-assets/ (where
the editor theme conceptually owns it) and once at public/favicon.ico
(where browsers implicitly fetch it). Both files are byte-identical
copies of the same source — cheap, no symlink, no 404s in the access
log for the implicit /favicon.ico request.
Asset-URL helper (Editor::assetUrl) and template rewrites land in a
follow-up commit. The editor's CSS/JS will 404 mid-refactor — that's
expected and gets fixed once paths are wired through the helper.
User-uploaded files (FilePond image uploads) move from data/uploads-2.0/ to public/uploads/. They live inside the webroot now — that's the whole point: the web server serves them directly, no Caddy/nginx alias rules needed, no "but allow /data/uploads/*" exception cluttering the deny list (because there's no deny list to clutter). The data/ directory now contains only true runtime state that must NEVER be served (SQLite, cache, logs, configs, backups) — and it's outside the webroot, so it's physically unreachable. .gitignore updated: /data/uploads-2.0/ → /public/uploads/. The existing on-disk uploads were `mv`'d in this commit too, but they're git-ignored so they don't show up in the diff. URL/path constants in boot/ImanagerBootstrap, ImageUrlBuilder, and PagesModule land in a follow-up commit.
…yout
Boot-side path/URL constants now match the new layout:
boot/ImanagerBootstrap.php
uploadsPath: data/uploads-2.0 → public/uploads (filesystem)
uploadsUrl: /data/uploads-2.0 → /uploads (URL)
boot/Frontend/Site.php
themeUrl: $siteUrl/site/themes/<theme> → $siteUrl/themes/<theme>
throw404 lookup: scriptorRoot/site/themes/<theme> → scriptorRoot/themes/<theme>
boot/Editor/Pages/PagesModule.php
FilePond `data-base` for pre-existing images: /data/uploads-2.0 → /uploads
themes/basic/_ext.php
Scriptor root: dirname(__DIR__, 3) → dirname(__DIR__, 2)
(themes/basic/ → ../../ is two `dirname` calls now, not three)
themes/basic/lib/Basic.php
headlineImage()'s path field: 'data/uploads-2.0/...' → 'uploads/...'
(the dict consumed by ImageUrlBuilder)
boot/Frontend/ImageUrlBuilder is now backward-compatible with stored
paths from BOTH the 1.x (`data/uploads/`) and the 2.0-pre-public-webroot
(`data/uploads-2.0/`) shapes. The constructor takes a list of legacy
prefixes instead of a single one, and rewritePath() walks the list.
Effect: existing items in the live DB keep rendering without a path
migration — their stored `path` field is rewritten on read.
Asset-URL helpers and template rewrites land in the next two commits.
The frontend will still 404 on theme CSS/JS/images until then because
the templates hardcode the old `/site/themes/...` URLs.
Three new helpers on the public Site/Editor surface so templates stop
hardcoding asset paths:
Site::themeAssetUrl(string $relative): string
→ <host>/themes/<active-theme>/<relative>
The Frontend's own theme assets (CSS, fonts, images, scripts).
Site::editorAssetUrl(string $relative): string
→ <host>/editor-assets/<relative>
Admin-side assets reused on the frontend (prism syntax-highlighter
CSS in blog posts, the favicon, jQuery for legacy snippets).
Editor::assetUrl(string $relative): string
→ <host>/editor-assets/<relative>
Same target as Site::editorAssetUrl — referenced from inside the
admin shell.
Editor gains a `baseUrl` property (host without admin path) so
themeUrl can be built independent of admin_path. The editor's
themeUrl semantic changes from "<admin>/theme" to "<host>/editor-assets"
because the static assets no longer sit under the admin URL prefix.
Templates aren't yet using the helpers — the next commit converts
the basic theme + editor templates over.
Editor templates (editor/theme/template.php, summary.php) use
\$editor->assetUrl(...) instead of hand-rolling
\$editor->siteUrl . '/theme/...'. The asset URL is now decoupled from
the admin URL prefix — editor assets live at /editor-assets/<rel>,
regardless of where the admin mount-point is.
Basic-theme templates (default, blog, blog-post, contact, _head,
_sidebar-right):
- own theme assets: \$site->themeUrl . '...' → \$site->themeAssetUrl('...')
- editor-side reuse: hand-rolled '/admin_path/theme/...'
→ \$site->editorAssetUrl('...')
- favicon: routed to /favicon.ico (root-level copy lives
at public/favicon.ico) instead of the editor
theme's own copy. One canonical URL, single
round-trip for the browser's implicit request.
After this commit, every URL that the page emits is server-config-
independent — it doesn't care whether admin_path is "editor/" or
"admin/" or anything else; the assets live where they live.
… public/
The web-server entry has moved to public/index.php, and the public/
fallback .htaccess landed two commits ago. The old root-level
index.php and .htaccess are now dead code — they referenced
'site/themes/...' paths that no longer exist and would never be
reached anyway after the webroot moves to public/. Removed.
docker/nginx.conf:
root /var/www/scriptor → /var/www/scriptor/public
- drop /data/ alias + deny (data/ is outside the webroot now)
- drop /imanager/ deny rule (gone since the hygiene PR)
+ dotfile deny rule
+ /uploads/ location with long-cache headers (uploads moved INTO
the webroot; nginx serves them straight from public/uploads/)
+ keep `location ~ \.php$ { return 404; }` as defence-in-depth
against accidental .php under public/themes/<name>/
docker/Dockerfile + docker/entrypoint.sh:
mkdir list: drop data/uploads-2.0/, add public/uploads/.
chown sweeps both data/ and public/uploads/ so php-fpm can write.
docs/themes.md (new) — theme-author guide for the public/-webroot layout: split layout (PHP source in themes/<name>/, static assets in public/themes/<name>/), _ext.php bootstrap protocol, asset-URL helpers (themeAssetUrl, editorAssetUrl, Editor::assetUrl), theme config, composer setup, subscribers, cache, 404 page, and how to test that the PHP half stays out of the webroot. docs/install-shared-hosting.md (new) — two acceptable ways to bridge Scriptor's public/ to a fixed public_html/-webroot host: symlink (preferred, ~one-liner) and physical copy with one-line index.php patch. Includes the curl probe set to verify the install isn't exposing data/ or source. README.md — Quickstart and Project-layout sections rewritten for the public/ webroot. Adds the `php -S … -t public public/index.php` recipe for local dev and links the shared-hosting doc. CHANGELOG.md — new "Unreleased" section enumerating the BREAKING changes (public/ webroot, theme split, editor-assets, uploads move, asset-URL helpers, public/.htaccess, nginx + docker rewiring) and links the refactor plan doc.
`php -S … -t public public/index.php` invokes the router for every request, including static-asset GETs. Without a `return false` for existing files, theme CSS/JS/images all 404'd; with a blanket `return false`, .htaccess would have been served and stray .php files in public/ would have been executed. Branch added at the top of public/index.php hands existing files back to the cli-server EXCEPT: - .php files — only the front controller may run as PHP, - dotfiles (.htaccess, .env, .DS_Store) — never serve their bytes. Branch is a no-op under PHP-FPM (PHP_SAPI != cli-server), so Apache/Caddy/nginx are unaffected — they have their own try_files / location rules. Tested with the full 27-path allow/deny matrix: - 11/11 allow → 200 / 302 / PHP-rendered 404 - 16/16 deny → 4xx (no file content leaked)
Without this, `COPY . .` in docker/Dockerfile pulled in the
maintainer's local SQLite database, the live FilePond uploads,
git history, caches and logs. The first-start `entrypoint.sh`
checks `if [ ! -f data/imanager.db ]` before running schema:migrate
and the demo seeder — a bundled DB short-circuits the seed, so
`docker compose up` reused stale credentials instead of provisioning
fresh ones.
Excluded:
- .git/, .gitignore, .DS_Store, .editorconfig, .claude/, vendor/
- data/imanager.db + sidecar files, data/cache/sections, data/logs,
data/backups, the data.bak.* directories
- public/uploads/
- node_modules, IDE clutter
Tested: docker compose build --no-cache + up -d on a fresh volume now
triggers entrypoint's "no database" branch, applies migrations,
runs seed-demo.php, and admin/scriptor login works against the
freshly-seeded DB.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.