SiteWeaver_CMS/APP.yaml

1679 lines
90 KiB
YAML
Raw Permalink Normal View History

2026-09-09 17:07:55 +00:00
# APP.yaml
#
# Single source-of-truth specification for the app, organized by feature/domain area.
# Built per .junie/guidelines.md (authoring phase). MILESTONES.yaml is written from this
# file afterward — do not create/update it until asked.
#
# Section conventions (use within any section as needed):
# notes: free-form rationale, considerations, examples (prose or short list)
# decisions_log: append-only list of resolved questions -> {question, decision, rationale}
# open_questions: structured list of items still to clarify -> {question, options?}
# (empty list when none remain)
#
# This is a skeleton layout. Sections below are placeholders to be filled in and adapted
# (added/removed/renamed) as the project's ideas are gathered.
---
app:
name: SiteWeaverCMS
build_path: Milestone
description: >
A new PHP CMS, whose goal is to be modular, with easy to build and understand templates,
built-in module installer (using Zip file uploader),
with an admin interface even a non-tech person finds inviting.
notes:
- Publicly viewable front-end (posts, pages)
- Auth required back-end (Media manager, User manager, Role-based Auth manager, Post/Page manager, Theme manager, Plugin/Module manager)
- URL should be in the form of `https://domain.tld/{page-slug}`
- Security & Reliability:
- Built-in 2FA (TOTP/Email)
- Automatic Basic Security Audits for Plugins:
- no eval
- no exec
- no remote code execution
- Content Management:
- Primary language configurable in Configuration Settings
- Configuration settings to list/check/select which other languages the site supports
- Content Versioning & Rollbacks (History)
- Rich Text Editor (producing clean HTML)
- Built-in multi-language support (i18n)
- i18n Translation Engine:
- Database-backed with `t()` (or t-like method) helper for UI strings.
- Multi-Locale Content Support:
- JSON-per-locale storage for Pages, Posts, and Media.
- Translate for Me Service:
- Service to auto-translate Post/Page/etc to languages selected in Configuration Settings
- Button to trigger Service
- Looking into self-hosting a LibreTranslate instance
- If using LibreTranslate, consider installing jefs42/libretranslate as client
decisions_log:
- question: What language and minimum version should the app target?
decision: PHP 8.4+
rationale: >
8.4 is as far back in PHP versions as we want to support; targeting a recent
minimum keeps the codebase modern instead of carrying compatibility baggage
for older, less-supported PHP versions.
- question: Should the backend use an existing framework (e.g. Laravel, Symfony) or a custom/home-brewed codebase?
decision: Custom/home-brewed backend codebase (no third-party framework).
rationale: >
Almost all other projects have been built on Laravel; going custom here is a
deliberate choice to keep the skill of producing solid custom codebases sharp,
rather than always building on top of someone else's framework.
- question: Which CSS framework should the UI be built on?
decision: Fomantic UI
rationale: >
Fomantic UI is an actively maintained fork of Semantic UI (which is no longer
maintained), and semantic class names are strongly preferred over
structural/utility class names for CSS.
- question: Which JavaScript approach/framework should the frontend use?
decision: >
Vue — but used specifically via its Components feature only (similar in spirit
to how Svelte components are used), not as a full-fledged SPA framework.
rationale: >
General preference for Vue over React or Angular. Svelte would also have been an
acceptable choice for a components-only approach, but Vue was the one picked.
- question: Which tool should manage PHP dependencies?
decision: Composer
rationale: Composer is the de facto standard package manager for PHP.
- question: >
Is SiteWeaverCMS intended to be self-hosted only for v1, or should the
"Multi-tenancy ready" note under `architecture` mean a hosted/SaaS offering
is also on the roadmap?
decision: >
Self-hosted only. Multi-tenancy has been removed from consideration entirely
(see `architecture.decisions_log`), so there is no hosted/SaaS offering on the
roadmap and no tenant-isolation/billing concerns to capture.
rationale: >
Once multi-tenancy was ruled out as an architectural direction, a hosted/SaaS
offering (which would require it) is ruled out along with it.
- question: >
When the "Automatic Basic Security Audits for Plugins" check fails (eval/exec/
remote code execution detected), what happens — is the ZIP upload rejected
outright, or is the plugin installed with an "Install - Error" status (as
implied by the Plugins List Page in `plugins_architecture`) for an Admin to
review?
decision: >
The upload is rejected outright, not installed with an "Install - Error" status.
Flow: the ZIP is unzipped, files are analyzed; if no security issues are found,
the plugin's migrations run and it is marked installed; if security issues are
found, the unzipped plugin files are immediately deleted and a
PluginSecurityException is thrown. The same audit and rejection flow applies to
Themes (throwing a ThemeSecurityException instead) — see
`plugins_architecture.decisions_log` and `themes_architecture.decisions_log`.
rationale: >
Files must be unzipped before they can be scanned, so a full "Install - Error"
list entry is unnecessary — a failed audit is treated as a hard rejection with
no trace left on disk.
- question: How will dependency injection be implemented in the app?
decision: >
A custom DI container will be implemented at the application level using
League/Container, following the service provider pattern. This will allow for
better modularity, testability, and maintainability of the core services.
rationale: >
Using a DI container allows for easier dependency management, testing, and
extension points in the future. The service provider pattern ensures proper
initialization of services and allows for lazy loading where appropriate.
`plugins_architecture.decisions_log` and `themes_architecture.decisions_log`.
- question: What license (if any) will SiteWeaverCMS be released under (e.g. MIT, GPL, proprietary)?
decision: >
No public license for now — SiteWeaverCMS stays sole-ownership/proprietary. It
is being used to build client websites, with the clients themselves administering
the sites built for them; a public release/license may be revisited later.
rationale: >
Matches the current usage model (agency-built client sites) rather than a
publicly redistributable product, so there is no need to pick a license yet.
- question: >
Should a target audience/deployment size be stated up front, since it
influences caching, storage backend, and search decisions made later in the
spec?
decision: >
Yes — the initial target audience is self-employed business owners, small
craft makers, and small new bloggers, with room to grow toward larger sites
and capabilities over time.
rationale: >
Gives later caching/storage/search decisions a concrete initial scale to design
against, while leaving room to grow.
open_questions: []
suggestions: []
glossary:
# Domain-specific terms and their meanings, so everyone uses the same vocabulary.
terms:
- term: $theme variable
definition: >
An object variable exposed to themes, containing the metadata described in
the theme.md (or admin.theme.md) file. This will include Theme Name, Author,
Description, Date, Version. Inside the theme, it is accessed as
$theme->author, $theme->name, etc
- term: 2FA (Two-Factor Authentication)
definition: >
A login security step requiring a second verification factor (e.g., a TOTP
code or an emailed code) in addition to the password.
- term: Admin Theme
definition: >
A theme to alter the look and feel of the Admin section of the CMS. This does
not alter the frontend that visitors to the site will see. This only alters
the backend admin section.
- term: Automated Cache Warming on Publish
definition: >
When a Page or Post is published, the system will pre-cache the Page/Post,
so the visitors will always receive the fastest response (a cached page/post
vs rendered-on-demand page/post)
- term: Blog Post Author Permissions
definition: >
The permissions granted to the author of a blog post (the Author role).
This includes things like Publishing, Editing, Un-Publishing, Deleting,
Saving (prior to publish), Allowing comments on Post
- term: Comments System
definition: >
The functionality of the CMS dedicated to the comment section. This allows
Users to post comments (when the Author has turned comments on), but does
its best to deter spammers, scammers, etc.
- term: Content Versioning & Rollbacks (History)
definition: >
Everytime a Page or Blog Post is saved, a revision is saved in the database.
This way, if the altered changes are not wanted or unapproved, the Page/Post
can be rolled back to a previous iteration easily.
- term: CRUD
definition: >
Create, Read, Update, Delete — the four basic operations for managing a
resource/record.
- term: Faceted Search
definition: >
A search interface combining free-text search with multiple simultaneous
filters (facets) - e.g. categories/tags and date ranges - that narrow
results as they're applied.
- term: Focal Point Selector
definition: >
Selecting the focus point of an image. When an image is cropped or overflow hidden,
this is the "center" of what is shown. It is the focal point.
- term: Frontend Theme
definition: >
A theme to alter the look and feel of the frontend of the CMS. This alters
the frontend that visitors to the site will see. This does not alter the
backend admin section.
- term: Granular Permissions
definition: >
Role-based authorization. Setting the permissions each role has the capability
to perform. Creating, Reading, Updating, Deleting each individual section or
area of the CMS (blog posts, pages, users, media, etc)
- term: Hooks (theme.md)
definition: >
The section of theme.md (the theme metadata file) that tells the theme renderer
what asset files (css, js) need to be loaded and where (header, footer, attached
to partials, etc)
- term: i18n
definition: >
Internationalization — designing/building software so it can be adapted to
different languages/locales without changing the code.
- term: i18n Translation Engine
definition: >
This is the engine that drives the internationalization. When the content on the site
is translated from one language to another, this is the engine that does the translating.
- term: JIT (Just-In-Time) Generation
definition: >
Generating a derived asset (e.g. a resized/cropped image variant) on demand
at request time, instead of pre-generating every possible variant in advance.
- term: Modular DB Migrations
definition: >
Every Plugin/Module is responsible for its own database migrations. What tables
or columns in a table that it relies on to be present in order to run.
- term: Multi-Locale Content Support
definition: >
Storing Pages, Posts, and Media content as JSON-per-locale, so each supported
language/locale has its own independently maintained version of a resource.
- term: Orphaned Media Watcher
definition: >
A background service that scans the entries in the Media Manager, and scans the contents
of Pages and Posts to find when certain media entries are no longer being used. If a media
entry has been orphaned (no longer in use), it is a candidate for deletion, to save space
on the server.
- term: Page Builder Core
definition: >
The CMS is a robust Page Builder. Whether that page is a basic web page (Home, About Us, etc)
a list of Blog Entries, a Calendar, etc, it is all about the Page.
- term: Partial (template partial)
definition: >
A small portion of the overall page. The navbar on the webpage, if separated out into its
own template would be a good example. If you separate the top, bottom, and "main" sections
of a webpage out into their own template files, each of those files would be partials, that
are then rendered into one larger full template file.
- term: PDO
definition: >
PHP Data Objects — PHP's built-in database access layer providing a common
interface across different database engines.
- term: Preview Cache Warming
definition: >
After saving a Page or Post alteration, you are able to Preview how it looks prior to publishing
it to Production. When you click the link to preview it, the system will cache it, so that when you
Publish it, the cache is already present for when your visitors want to view the page. If you alter
the page/post further, before publishing, the cache will be rendered stale, and will be overwritten
by the next Preview Cache Warming or Publish Cache Warming.
- term: Rich Text Editor
definition: >
A WYSIWYG (What You See Is What You Get) editor for formatting text content
that produces clean HTML markup.
- term: Service Provider
definition: >
A class responsible for registering/bootstrapping a plugin's or component's
functionality (routes, migrations, admin sections, etc.) with the core
application.
- term: Site backup
definition: >
This is a process, kicked off in the admin section, to backup the site files and database.
- term: Storage Interface / Storage Abstraction
definition: >
A common interface (`StorageInterface::get($path)`, `put($path, $data)`,
`delete($path)`) that each storage backend (Local Disk, S3, Nextcloud, Linode
Object-Storage) implements. The rest of the CMS (media upload, JIT generation,
etc.) talks only to this interface, making it easy to swap or add storage
providers without touching the calling code.
- term: theme.md / admin.theme.md
definition: >
These are the metadata files for the CMS frontend themes and admin themes. They will contain
information on the theme name, author, date, descriptions, hooks, etc
- term: Theme Editor
definition: >
This is a built-in file editor, in the admin section, that allows an Admin or other authorized
role to make minor modifications to the theme files without the need to change the files
physically and upload them to the server. The theme files are edited in place on the server.
- term: TOTP
definition: >
Time-based One-Time Password — a short-lived numeric code generated from a
shared secret and the current time, used as a second authentication factor.
- term: Translate for Me Service
definition: >
An on-demand, button-triggered service that auto-translates a Post/Page/etc.
into the other languages selected in Configuration Settings, potentially
backed by a self-hosted LibreTranslate instance (e.g. via the
jefs42/libretranslate client).
notes: []
decisions_log: []
open_questions: []
scope:
in_scope:
- Page Builder
- Role Based Auth
- User Management
- Blog plugin
- Media Manager
- Theme/Plugin Editor
- i18n
- Comments
- Backup/Restore
- Search
out_of_scope:
- Multi-tenancy (SaaS)
- Hosted/SaaS offering
- Mobile app
- API-only approach
architecture:
notes:
- Architecture & Scalability:
- Single-site CMS, no multi-tenancy (see decisions_log)
description: >
SiteWeaverCMS is a custom-built PHP CMS with a modular architecture, designed for
self-hosted use. It features a clean separation of concerns between the core system
and plugins/themes, with a focus on developer productivity and extensibility.
decisions_log:
- question: >
Is a dedicated backend_architecture section needed (per the "e.g." placeholder
comment above)?
decision: >
No — a separate backend_architecture section is not needed. Backend detail is
already captured across the domain-specific sections (media_manager_architecture,
plugins_architecture, themes_architecture, auth_requirements), which together
provide sufficient depth on the backend.
rationale: >
Those sections already document the relevant backend concerns for their domain
(storage abstraction, migrations, Service Providers, permissions, etc.), so a
separate umbrella backend_architecture section would just duplicate them.
- question: >
Is a dedicated frontend_architecture section still needed to document how Vue
Components are organized/bundled, and how they integrate with Fomantic UI and
the templating engine?
decision: >
Yes — a `frontend_architecture` section has been added (see below), filled in
with what could be gathered from the rest of this document, with its own
`open_questions` for what couldn't be inferred.
rationale: >
Frontend integration (Vue Components + Fomantic UI + the custom PHP templating
engine) is enough of its own concern to warrant a dedicated section rather than
being folded into `architecture` or `ui`.
- question: >
Given the previous "Multi-tenancy ready" note, should v1's schema/routing
already carry a tenant identifier (even if unused), or should multi-tenancy be
deferred entirely to a future major version with its own migration path?
decision: >
Multi-tenancy has been removed from consideration altogether — not deferred,
not kept "ready for" — SiteWeaverCMS is a single-site, single-tenant CMS with
no planned migration path toward multi-tenancy. The "Multi-tenancy ready" note
has been removed from `notes` above, and the "Multi-tenancy" glossary term has
been removed.
rationale: >
Avoids carrying unused tenant-identifier columns/routing complexity for a
direction the project will not pursue.
- question: How will plugin and theme loading be handled?
decision: >
Plugins and themes will be loaded through service providers that register their
respective services. The plugin manager and theme manager will handle the
discovery, loading, and unloading of these components at runtime.
rationale: >
Using service providers ensures proper dependency injection for plugins and
themes, while also allowing for lazy loading and easy testing of individual
components.
- question: What approach will be used for routing?
decision: >
A custom router with plugin/extension points. The core system will provide
basic routing functionality, but plugins can register their own routes.
rationale: >
This allows for a flexible architecture where plugins can easily add new
routes without modifying the core routing system.
- question: How will configuration be managed?
decision: >
Configuration will be handled through a service that loads from environment
variables and .env files (if available), with support for runtime updates.
rationale: >
Using environment variables for configuration ensures secure handling of
sensitive data while allowing for easy deployment to different environments.
- question: How will services be registered?
decision: >
Services will be registered using a service provider pattern. Core services
will be registered in CoreServiceProvider, application-level services in
AppServiceProvider, and plugin/theme-specific services in their respective
service providers.
rationale: >
The service provider pattern ensures proper initialization of services,
provides lazy loading capabilities, and allows for easy testing and
dependency injection.
- question: How will caching be implemented?
decision: >
A combination of in-memory caching (for frequently accessed data) and file-based
caching (for larger content that doesn't need to be loaded on every request).
rationale: >
This hybrid approach ensures good performance for both frequently accessed
data and larger content while maintaining a reasonable memory footprint.
- question: How will security be implemented?
decision: >
Security will be implemented through a combination of input validation,
authentication, authorization, and secure storage practices. Plugins will be
subject to automated security audits during installation.
rationale: >
A layered security approach ensures that multiple attack vectors are covered,
with special attention to preventing code injection and unauthorized access.
- question: >
`in_scope` and `out_of_scope` were both still empty. What is explicitly IN scope
for v1 and what is explicitly OUT of scope for v1?
decision: >
`in_scope` and `out_of_scope` have been populated (see lists above): Page
Builder, Role Based Auth, User Management, Blog plugin, Media Manager,
Theme/Plugin Editor, i18n, Comments, Backup/Restore, and Search are in scope
for v1; multi-tenant/SaaS deployment, a plugin marketplace, and native mobile
apps are out of scope.
rationale: >
Gives a concrete, at-a-glance v1 boundary to plan milestones against instead of
inferring scope from `features_overview.notes`.
- question: What backup strategy is supported in v1?
decision: Manual trigger only via Admin UI, no automatic or scheduled backups.
rationale: >
The feature notes specify that backups are not automated; they must be triggered
manually by an admin action.
open_questions: []
plugins_architecture:
description: >
The plugin architecture allows for extending the core functionality of SiteWeaverCMS
through modular components. Each plugin can register its own routes, services, and
configuration settings.
notes:
- Plugins/Modules:
- Admin section Plugins List Page:
- Lists each plugin (its name, brief description, and active status):
- Active: Plugin is installed and switched on
- Deactivated: Plugin is installed, but switched off
- Install - Error: Plugin threw an error during Install
- Clicking on Plugin name sends you to Plugin Detail Page for that Plugin
- Plugin Detail Page:
- Lists Plugin metadata details (plugin.md)
- Toggle to switch Plugin Active/Deactivated status
- Service Provider-based architecture
- All plugins, officially included by default or not, reside in the `site/plugins` directory, and are built as a plugin.
- Built-in Plugin Support:
- Blog (included by default):
- URL should be in the form of `https://domain.tld/{published-4-digit-year}/{published-2-digit-month}/{published-2-digit-day}/{post-title-slug}`
- Example https://example.com/2026/08/26/my-new-dog
- The Published Date for the post is always the published date of the original version. Follow up versions keep the same published date.
- Blog Category Pages:
- URL should be in the form of `https://domain.tld/category/{blog_category_slug}`
- Shows a list of Blog Posts that are tagged with the category in the url
- Blog Posts MUST belong to at least 1 category
- A "Uncategorized" blog category will exist
- Blog Posts do not need to have Blog Tags, but may have many
- Blog Category vs Blog Tag:
- Categories:
- Broad, highlevel groupings that organize a blogs content into major topics.
- Affects URL, changes are rare and deliberate
- Names are meant to be stable, descriptive, and representative of major topics (e.g., Travel, Food, Finance).
- Tags:
- Specific, descriptive keywords that indicate particular subjects, themes, or details within a post.
- Changes are easy, and do not affect URLs.
- Names can be more creative or specific (e.g., vegan, budgettravel, 2024updates).
- Blog Category Examples:
- Technology
- Programming
- Front-End
- Vehicles
- Blog Tag Examples:
- ReactJS
- SEO
- Ford Mustang
- Blog Post Comments:
- comments are auto-approved (no manual approval needed), but may be deleted/redacted by Admin or other User Role with proper permissions
- Modular DB Migrations:
- Each plugin/component has a `migrations` folder
- CMS scans and runs them automatically on install
- Plugin table names should be in the form of {plugin_name}_{table_name}
- Example - Blog_Posts, Blog_Post_Versions
- Plugin Upload:
- ZIP directory upload (Admin selects "Plugin" or "Theme" type)
- Uninstall/Remove: Available only when Plugin/Module is Deactivated.
- Runs reverse migrations (regress DB changes).
- Deletes the plugin directory from `plugins/`.
- Configuration Settings:
- If Plugin requires configuration, can add a section to Configuration Settings in Admin
- Admin Section:
- Plugin can add it's own section to Admin section:
- Example: Blog Plugin adds Posts so Author can create Blog Posts on site.
- Front-end:
- Plugin can include it's own template partials for specialized output, or be outputted by installed theme files:
- Example: A calendar Plugin can include it's own calendar.php template file for outputting the calendar
decisions_log:
- question: Should Composer-based installation of modules be supported alongside ZIP upload?
decision: >
No — Composer installation of modules has been removed, at least for v1. The
only installation process now is ZIP file upload.
rationale: >
Keeps a single, admin-friendly install path consistent with the "even a non-tech
person finds inviting" goal in `app`; CLI/Composer access isn't assumed to be
available to site admins.
- question: >
What is the resolution path when the Automatic Basic Security Audit rejects a
plugin — is the upload blocked outright, or installed with an "Install - Error"
status (as implied by the Plugins List Page) for the Admin to inspect further?
decision: >
The upload is blocked outright. Since the upload has to finish before the
system can unzip and inspect its contents, the flow is: the ZIP is unzipped,
files are analyzed; if no security issues are found, the plugin's migration
files run and the plugin is marked installed; if security issues are found,
the unzipped plugin files are immediately deleted and a PluginSecurityException
is thrown. There is no "Install - Error" status resulting from a failed
security audit — that status is reserved for other install-time errors (e.g. a
migration failure).
rationale: >
A plugin that fails the security audit should leave nothing installed or
listed; only errors unrelated to security (e.g. a broken migration) warrant
surfacing an "Install - Error" entry for the Admin to inspect.
- question: >
Is there a plugin dependency/versioning mechanism (e.g. a required core version,
or dependencies between plugins) that needs to be declared in `plugin.md`?
decision: >
Yes — a Core Version dependency is declared in `plugin.md` (added to its
required metadata list in `directory_structure`), mirroring the same field
already present in `theme.md`/`admin.theme.md`.
rationale: >
Lets the install process reject a plugin that requires a newer core version
than what's installed, consistent with how themes already declare a Core
Version.
- question: What approach will be used for plugin installation?
decision: >
Plugins will be installed as ZIP files that are extracted to a plugins directory,
with the plugin's service provider registered automatically.
rationale: >
This approach provides a simple and familiar installation process for users
while ensuring proper service registration through the DI container.
- question: How will plugin dependencies be handled?
decision: >
Plugins can declare their dependencies in their metadata files. The plugin
manager will handle loading plugins in the correct order based on dependencies.
rationale: >
This ensures that required services are available when a plugin is loaded,
preventing runtime errors.
- question: How will plugin configuration be handled?
decision: >
Each plugin can register its own configuration settings, which will be
automatically merged with the main configuration system.
rationale: >
This allows plugins to maintain their own settings while integrating cleanly
with the core configuration system.
open_questions: []
themes_architecture:
description: >
The theme architecture provides a flexible way to customize the look and feel of
SiteWeaverCMS. Themes can be easily installed, switched, and customized through
the admin interface.
notes:
- Themes:
- Default theme (provided by default) supports Light/Dark switching, and set to System by default
- Admin theme supports Light/Dark switching, and set to System by default
- Expressive, easy-to-read templating engine (accessible for non-UI devs)
- Uses `$this->themeUrl($asset)` to produce a url to the asset in the theme directory
- Uses `$this->adminThemeUrl($asset)` to produce a url to the asset in the admin theme directory
- exposes `$theme` variable to templates:
- $theme variable contains all the theme meta data (with lowercase variables) with exception of hooks:
- the hooks section uses it's own set of methods, listed below (in Frontend Theme directory structure and Admin Theme directory structure)
- "Example: `$theme->title` contains the title of the theme as specified in theme.md Title"
- "Example: `$theme->author` contains the author of the theme as specified in theme.md Author"
- Theme Metadata via `theme.md` (frontend theme):
- Title
- Author
- Description
- Date
- Version
- Core Version
- Hooks:
- css:
- ... list of css files to add to theme head (path relative to theme root)
- js:
- header:
- ... list of js files to add to theme head (path relative to theme root)
- footer:
- ... list of js files to add to theme footer (path relative to theme root)
- "{template_name}":
- ... list of js files to add at the top of a template before rendering it (path relative to theme root)
- Theme Metadata via `admin.theme.md` (admin theme):
- same metadata as frontend theme, only file name is `admin.theme.md` instead of `theme.md`
- Simplified architecture (avoiding WordPress-style convolution)
- Theme Upload:
- ZIP directory upload via Admin section
- Uninstall/Remove:
- Available only for themes not currently in use.
- Deletes the theme directory from `themes/`.
- Triggered via button on Theme List page (Delete button per list item)
- Note: Themes should not make DB changes on install.
- Frontend Theme directory folder structure:
- theme.md (required)
- layout.php (required)
- partials:
- optional, unless layout.php uses `$theme->partial()`
- ... (any files called by layout.php using the `$theme->partial($path)` method)
- subdirectories allowed in $path
- "`$theme->partial('footer/contact-block')` would render `partials/footer/contact-block.php`"
- assets:
- css:
- $theme->css() (outputs all the link tags for the list of css files in theme.md hooks/css)
- js:
- $theme->js('header') (outputs all the script tags for the list of js files listed in theme.md hooks/js/header)
- $theme->js('footer') (outputs all the script tags for the list of js files listed in theme.md hooks/js/footer)
- $theme->js($template_name) (outputs all the script tags for the list of js files listed in theme.md hooks/js/{template_name})
- images:
- $theme->imageUrl($image_name) (outputs the url to the image (not the whole img tag, only the url). Subdirectory path allowed)
- Admin Theme directory structure:
- same directory structure as frontend theme, except:
- partials are rendered using `$theme->adminPartial()`, not `$theme->partial()`
- css is output via `$theme->adminCss()`, not `$theme->css()`
- js is output via `$theme->adminJs($section)`, not `$theme->js($section)`
- Admin section Frontend Theme Page:
- Lists each theme (its name and screenshot.jpg):
- If Active:
- shows "Active"
- cannot be deleted (no delete button shown)
- If not Active:
- Shows "Activate" button and "Delete" button
- When Activate button is clicked, Deactivates previous Active theme, Activates current Theme
- Only one Admin Theme may be active at a time.
- Clicking on Theme name sends you to Admin Theme Detail Page for that Theme
- Frontend Theme Detail Page:
- Lists Theme metadata details (theme.md)
- If Active, shows "Active", else provides Activate button
- Admin section Admin Theme Page:
- Lists each theme (its name and screenshot.jpg):
- If Active:
- shows "Active"
- cannot be deleted (no delete button shown)
- If not Active:
- Shows "Activate" button and "Delete" button
- When Activate button is clicked, Deactivates previous Active theme, Activates current Theme
- Only one Admin Theme may be active at a time.
- Clicking on Theme name sends you to Admin Theme Detail Page for that Theme
- Admin Theme Detail Page:
- Lists Theme metadata details (admin.theme.md)
- If Active, shows "Active", else provides Activate button
- Theme Editor File Versioning:
- Each save made through the in-place Theme Editor creates a new version of
the saved file, so an accidental bad edit can be reverted without needing
the full Site Restore feature.
decisions_log:
- question: >
Should the same Automatic Basic Security Audit that applies to Plugins also
apply to uploaded Themes and to any file saved via the in-place Theme Editor,
given that `theme.md`/`layout.php`/partials can contain arbitrary PHP?
decision: >
Yes — the same security audit used for Plugins (no eval, no exec, no remote
code execution) also applies to Themes, both on ZIP upload and on files saved
via the in-place Theme Editor. The only difference from the plugin flow is that
a failed audit throws a `ThemeSecurityException` instead of a
`PluginSecurityException`.
rationale: >
Themes can contain just as much arbitrary PHP as Plugins (`theme.md`,
`layout.php`, partials), so leaving them unaudited would be an inconsistent
security gap.
- question: >
Should theme files be versioned/backed up before each save through the Theme
Editor, so an accidental bad edit can be reverted without needing the full Site
Restore feature?
decision: >
Yes — versioning is implemented on each Theme Editor save (see the "Theme
Editor File Versioning" note above).
rationale: >
Gives a lightweight, per-file undo path for editor mistakes instead of forcing
a full Site Restore for a single bad save.
- question: How will theme assets be handled?
decision: >
Theme assets (CSS, JS, images) will be served directly by the web server,
with support for versioning to prevent caching issues.
rationale: >
Direct serving of assets provides better performance than PHP-based serving
while still allowing for proper version management.
- question: How will theme customization be implemented?
decision: >
Themes will include a built-in editor that allows direct modification of
template files through the admin interface.
rationale: >
This provides an easy way for users to customize their themes without needing
to modify files directly on the server.
open_questions: []
components:
description: >
SiteWeaverCMS consists of several key components that work together to provide
a complete content management solution. These include the core system, plugins,
themes, and various service components.
decisions_log:
- question: How will core components be organized?
decision: >
Core components will be organized under the src/Site/ directory with clear
separation between bootstrap code, router logic, services, and other core
functionality.
rationale: >
This organization provides a clean separation of concerns and makes it easy
to locate specific components within the codebase.
- question: How will service components be structured?
decision: >
Service components will be organized under src/Site/Services/ with each
service having its own class implementing a clear interface.
rationale: >
This structure promotes loose coupling and makes it easy to swap out
implementations for testing or alternative backends.
open_questions: []
notes: []
data_model:
description: >
SiteWeaverCMS uses a relational database model with support for multiple
storage engines. The data model is designed to be extensible through plugins
and themes.
notes:
- Site Settings:
- Site Title/Description
- Global SEO Settings
- Locales supported ('en','es','fr','ru',etc)
- Blog Settings:
- Blog Title
- Inherits locales support from Site Settings
- Configurable Blog "Home" URL segment (Admin/Editor can set the entry point for the blog plugin)
- Database Tables:
- Site_Settings:
- id: int, primary key
- name: string(255), unique, not null
- setting: string(255), null
- created_at: datetime, not null, default=now()
- created_by: int, foreign key (Users.id), not null
- updated_at: datetime, null
- updated_by: int, foreign key (Users.id), null
- Users:
- id: int, primary key
- username: string(255), not null, unique
- password: string(255), not null
- f_name: string(255), not null
- l_name: string(255), null
- email: string(255), not null, unique
- avatar: string(255), null, comment="user account image, uses default if null"
- failed_login_attempts: int, not null, default=0
- locked_until: datetime, null
- created_at: datetime, not null, default=now()
- created_by: int, foreign key (Users.id), not null
- updated_at: datetime, null
- updated_by: int, foreign key (Users.id), null
- User_Totp:
- id: int, primary key
- user_id: int, foreign key (Users.id), not null
- secret: string(32), not null
- enabled: boolean, not null, default=true
- created_at: datetime, not null
- updated_at: datetime, null
- User_password_reset:
- id: int, primary key
- user_id: int, foreign key (Users.id), not null
- token: string(42), not null, unique
- expires_at: datetime, not null
- created_at: datetime, not null
- User_email_verification:
- id: int, primary key
- user_id: int, foreign key (Users.id), not null
- token: string(42), not null, unique
- expires_at: datetime, not null
- created_at: datetime, not null
- Roles (Stores each logical role (e.g., Admin, Editor, Viewer)):
- id: int, primary key
- name: string(255), not null, unique
- description: string(255), null
- created_at: datetime, not null, default=now()
- created_by: int, foreign key (Users.id), not null
- updated_at: datetime, null
- updated_by: int, foreign key (Users.id), null
- Permissions (Stores each distinct permission (e.g., read:document, delete:user)):
- id: int, primary key
- name: string(255), not null, unique
- type: string(255), null
- action: string(255), null
- description: string(255), null
- created_at: datetime, not null, default=now()
- created_by: int, foreign key (Users.id), not null
- updated_at: datetime, null
- updated_by: int, foreign key (Users.id), null
- Role_Permission (Junction table that maps many roles to many permissions):
- role_id: int, foreign key (Roles.id), not null
- permission_id: int, foreign key (Permissions.id), not null
- created_at: datetime, not null, default=now()
- created_by: int, foreign key (Users.id), not null
- updated_at: datetime, null
- updated_by: int, foreign key (Users.id), null
- User_Role (Junction table that maps many users to many roles):
- user_id: int, foreign key (Users.id), not null
- role_id: int, foreign key (Roles.id), not null
- created_at: datetime, not null, default=now()
- created_by: int, foreign key (Users.id), not null
- updated_at: datetime, null
- updated_by: int, foreign key (Users.id), null
- Pages:
- id: int, primary key
- slug: string(255), unique, comment="humanreadable URL part"
- title: string(255)
- created_at: datetime, not null, default=now()
- created_by: int, foreign key (Users.id), not null
- updated_at: datetime, null
- updated_by: int, foreign key (Users.id), null
- published_at: datetime (set once on first publish (first version publish), never updated)
- Page_Versions:
- id: int, primary key
- page_id: int, not null, foreign key (Pages.id)
- version_number: int, not null (incremental, starts at 1)
- locale: string(3), not null
- content: text, not null comment="the full HTML or templating markup"
- meta_json: json/text, null, comment="extra fields like seo_title, meta_description, tags"
- created_at: datetime, not null
- created_by: int, foreign key (Users.id)
- is_published: bool, not null, default=false
- published_at: datetime, null
- Blog_Settings (Blog plugin):
- id: int, primary key
- name: string(255), unique, not null
- setting: string(255), null
- created_at: datetime, not null, default=now()
- created_by: int, foreign key (Users.id), not null
- updated_at: datetime, null
- updated_by: int, foreign key (Users.id), null
- Blog_Posts (Blog plugin):
- id: int, primary key
- slug: string(255), unique, comment="humanreadable URL part"
- title: string(255)
- created_at: datetime, not null, default=now()
- created_by: int, foreign key (Users.id), not null
- updated_at: datetime, null
- updated_by: int, foreign key (Users.id), null
- published_at: datetime (set once on first publish (first version publish), never updated)
- Blog_Post_Versions (Blog plugin):
- id: int, primary key
- post_id: int, not null, foreign key (Blog_Posts.id)
- version_number: int, not null (incremental, starts at 1)
- locale: string(3), not null
- extract: text, null (if null, extract is pulled from content)
- content: text, not null, comment="the full HTML or templating markup"
- meta_json: json/text, null comment="extra fields like seo_title, meta_description, tags"
- created_at: datetime, not null
- created_by: int, foreign key (Users.id)
- is_published: bool, not null, default=false
- published_at: datetime, null
- Blog_Categories (Blog plugin):
- id: int, primary key
- name: string(255), not null, unique
- slug: string(255), not null, unique
- description: text, null
- Blog_Tags (Blog plugin):
- id: int, primary key
- name: string(255), not null, unique
- description: text, null
- Blog_Post_Categories (Blog plugin):
- post_id: int, foreign key (Blog_Posts.id)
- category_id: int, foreign key (Blog_Categories.id)
- created_at: datetime, not null, default=now()
- created_by: int, foreign key (Users.id), not null
- updated_at: datetime, null
- updated_by: int, foreign key (Users.id), null
- Blog_Post_Tags (Blog plugin):
- post_id: int, foreign key (Blog_Posts.id)
- tag_id: int, foreign key (Blog_Tags.id)
- created_at: datetime, not null, default=now()
- created_by: int, foreign key (Users.id), not null
- updated_at: datetime, null
- updated_by: int, foreign key (Users.id), null
- Blog_Comments:
- id: int, primary key
- post_id: int, foreign key (Blog_Posts.id), not null
- user_id: int, foreign key (Users.id), not null
- content: text, not null
- likely_spam: boolean, not null
- created_at: datetime, not null
- updated_at: datetime, null
- updated_by: int, foreign key (Users.id), null, comment="comments can be redacted (content set to "[redacted]") by Admins, and others with the appropriate permissions"
- Media:
- id: int, primary key
- filename: string(255), not null
- path: string(255), not null
- storage_backend: string(255), not null
- mime_type: string(75), not null
- height: int, not null
- width: int, not null
- created_at: datetime, not null, default=now()
- created_by: int, foreign key (Users.id), not null
- updated_at: datetime, null
- updated_by: int, foreign key (Users.id), null
- Media_Metadata:
- id: int, primary key
- media_id: int, foreign key (Media.id), not null
- name: string(255), not null
- alt: text, null, default=null
- locale: string(3), not null
- created_at: datetime, not null, default=now()
- created_by: int, foreign key (Users.id), not null
- updated_at: datetime, null
- updated_by: int, foreign key (Users.id), null
- Plugins:
- id: int, primary key
- name: string(255), not null
- version: string(10), not null
- core_version: string(10), not null
- status: enum('Active', 'Deactivated','Error'), not null, default='Deactivated'
- created_at: datetime, not null, default=now()
- created_by: int, foreign key (Users.id), not null
- updated_at: datetime, null
- updated_by: int, foreign key (Users.id), null
- Frontend_Themes:
- id: int, primary key
- name: string(255), not null
- enabled: boolean, not null, default=false, comment="Only one may be enabled at a time. Currently controlled by app code, not DB constraints"
- created_at: datetime, not null, default=now()
- created_by: int, foreign key (Users.id), not null
- updated_at: datetime, null
- updated_by: int, foreign key (Users.id), null
- Admin_Themes:
- id: int, primary key
- name: string(255), not null
- enabled: boolean, not null, default=false, comment="Only one may be enabled at a time. Currently controlled by app code, not DB constraints"
- created_at: datetime, not null, default=now()
- created_by: int, foreign key (Users.id), not null
- updated_at: datetime, null
- updated_by: int, foreign key (Users.id), null
example_seed_data_for_roles: >
INSERT INTO Roles (name, description) VALUES
('Admin', 'Full access to all CMS features and settings.'),
('Editor', 'Can create, edit, and delete content, but cannot change site settings.'),
('Author', 'Can create and edit their own content; can view drafts.'),
('User', 'Basic frontend readonly access to published content. No backend admin access. Can comment on Blog Posts');
-- 2. Insert a set of permissions that are common to most CMSes
INSERT INTO Permissions (name, type, action, description) VALUES
('manage:site', NULL, 'configure', 'Change sitewide settings (e.g., logo, navigation).'),
('publish:page', 'page', 'publish', 'Publish a page to the public site.'),
('unpublish:page', 'page', 'unpublish', 'Remove a page from public view.'),
('edit:page', 'page', 'edit', 'Modify an existing page.'),
('delete:page', 'page', 'delete', 'Permanently delete a page.'),
('manage:user', NULL, 'admin', 'Create, edit, disable, or delete user accounts.'),
('view:draft', 'draft', 'view', 'Read unpublished drafts.'),
('edit:draft', 'draft', 'edit', 'Edit an unpublished draft.'),
('publish:draft', 'draft', 'publish', 'Publish a draft to the public site.');
-- 3. Assign permissions to the default roles (manytomany via Role_Permission)
-- The composite PK (role_id, permission_id) guarantees no duplicate rows.
INSERT INTO Role_Permission (role_id, permission_id)
SELECT r.id, p.id
FROM Roles r, Permissions p
WHERE r.name = 'Admin' AND p.name IN (SELECT name from Permissions);
INSERT INTO Role_Permission (role_id, permission_id)
SELECT r.id, p.id
FROM Roles r, Permissions p
WHERE r.name = 'Editor' AND p.name IN ('publish:page','unpublish:page', 'edit:page','delete:page', 'view:draft','edit:draft');
INSERT INTO Role_Permission (role_id, permission_id)
SELECT r.id, p.id
FROM Roles r, Permissions p
WHERE r.name = 'Author' AND p.name IN ('publish:page','edit:page', 'edit:draft','publish:draft');
decisions_log:
- question: >
`notes` here only cover Settings; no entities/columns have been sketched yet for
Users, Roles, Permissions, Pages, Posts, Media, Comments, or Content Versions
(History). Should the core schema be drafted in this section before
MILESTONES.yaml is written, given how many features depend on it?
decision: >
Yes — the core schema has been drafted above (`Database Tables`), covering
Settings, Users, Roles, Permissions, Role_Permission, User_Role, Pages,
Page_Versions, Posts, Post_Versions, Media, and Comments.
rationale: >
Gives MILESTONES.yaml concrete table/column definitions to build migrations
from, instead of inferring the schema from feature descriptions alone.
- question: >
Should a cross-cutting convention for audit/timestamp columns (`created_at`/
`updated_at`, and who created/last edited a record) be documented once entities
are sketched out, so every migration follows the same pattern?
decision: >
Partially applied so far: Pages, Page_Versions, Posts, Post_Versions, and
Comments already follow a `created_at`/`created_by`/`updated_at`/`updated_by`
pattern. Users, Roles, Permissions, Role_Permission, User_Role, and Media do
not yet have any `created_at`/`updated_at` columns (see the new suggestion
below flagging this gap).
rationale: >
Records the convention actually in use today, rather than assuming it was
applied uniformly across every table.
- question: >
Should the suggested `created_at`/`created_by`/`updated_at`/`updated_by`
columns be added to Users, Roles, Permissions, Role_Permission, User_Role, and
Media (see `suggestions` below)?
decision: >
Yes, accepted and applied — Users, Roles, Permissions, Role_Permission, and
User_Role already carried the columns above; Media has now also been given the
same `created_at`/`created_by`/`updated_at`/`updated_by` columns, so every
table in the schema now follows the convention. `User_Totp`, `User_password_reset`,
and `User_email_verification` are purposely missing the `created_by` and `updated_by`
fields, as these are system generated and not tied to specific users.
rationale: >
Closes the last gap (Media) so audit/timestamp tracking is consistent across
every table, matching the convention already used on Pages/Page_Versions/
Posts/Post_Versions/Comments.
- question: What database engine will be used?
decision: >
Support for SQLite, MySQL, and PostgreSQL through PDO connections.
rationale: >
Supporting multiple engines provides flexibility for different deployment
scenarios while maintaining a consistent interface through PDO.
- question: How will data migrations be handled?
decision: >
Data migrations will be implemented using Phinx, with each plugin having
its own migration files.
rationale: >
This approach allows plugins to manage their own database schema changes
while providing a centralized migration management system.
open_questions: []
deployment:
description: >
SiteWeaverCMS is designed for self-hosted deployment with support for various
web server configurations and environments.
decisions_log:
- question: What are the deployment requirements?
decision: >
The application requires PHP 8.4+, Composer, and a web server (Apache/Nginx).
Database configuration will be handled through environment variables.
rationale: >
These requirements ensure compatibility with modern PHP features while
providing flexibility in hosting environments.
- question: How will environment-specific configuration be handled?
decision: >
Environment-specific configuration will be managed through .env files (if available) and
environment variables, with a default .env.example file included.
rationale: >
This approach allows for easy deployment to different environments while
keeping sensitive data out of version control.
open_questions: []
notes: []
security:
description: >
SiteWeaverCMS implements security best practices including input validation,
authentication, and secure coding practices.
decisions_log:
- question: What authentication method will be used?
decision: >
Role-based authentication with support for TOTP-based 2FA.
rationale: >
This provides a good balance between security and usability while supporting
enterprise-level security requirements.
- question: How will user permissions be handled?
decision: >
A granular permission system based on roles and capabilities.
rationale: >
This allows for precise control over what actions users can perform while
maintaining a simple and understandable permission model.
open_questions: []
notes: []
performance:
description: >
SiteWeaverCMS is optimized for performance with caching strategies, lazy loading,
and efficient database queries.
decisions_log:
- question: How will caching be implemented?
decision: >
A hybrid caching approach using in-memory caches for frequently accessed data
and file-based caching for larger content.
rationale: >
This approach balances memory usage with performance while maintaining
reasonable disk space requirements.
- question: How will database queries be optimized?
decision: >
Database queries will use prepared statements, proper indexing, and lazy loading
of related data to reduce unnecessary overhead.
rationale: >
These practices ensure efficient database operations while maintaining
security against SQL injection attacks.
open_questions: []
notes: []
secret_management:
notes:
- The project will use a selfhosted Infisical instance to store all runtime secrets.
- If Infisical is unavailable, the app should throw an exception, and tell the viewer to contact the Website Administrator.
- Secrets are injected into the application via environment variables set in Nginx.
- During bootstrap, the app connects to Infisical, pulls all secrets into a local array, and passes it to a new Config instance.
The Config class stores these values internally; subsequent code accesses them via the config service provided by the DI container.
nginx_env_vars:
- INFISICAL_URL
- INFISICAL_ENV ("dev","staging","production")
- INFISICAL_PROJECT_ID
- INFISICAL_SECRET_ID
- INFISICAL_MACHINE_ID
composer_require:
- composer require infisical/php-sdk
php_sdk:
- https://infisical.com/docs/sdks/languages/php
tech_stack:
language: PHP 8.4+
backend_framework: custom/home-brewed, no third-party backend framework
backend_static_analysis:
- PHPStan (for general code quality)
- Psalm (for code security)
backend_code_style_tool: PHP CS Fixer
css_framework: Fomantic UI
js_framework: Vue (using Vue Components)
package_management: Composer
database:
support: multi-support (through PDO)
engines:
- SQLite3
- MySQL/MariaDB
- PostgreSQL
database_migration: Phinx (robmorgan/phinx)
testing_framework: Codeception (codeception/codeception)
notes: []
decisions_log:
- question: >
Which image processing library will back JIT Generation — Glide or Intervention
Image?
decision: Glide
rationale: >
Chosen over Intervention Image as the JIT-generation backend (see
`media_manager_architecture.decisions_log` for the same decision applied there).
- question: >
Should Composer dependency versions be pinned to exact versions, or use caret/
tilde ranges, given the "no compatibility baggage" stance already taken on the
minimum PHP version?
decision: >
Use caret/tilde ranges, but make sure the minimums are set appropriately.
rationale: >
Caret/tilde ranges allow non-breaking dependency updates while still letting the
minimum-version floor for each package be set deliberately.
- question: >
Add a static analysis tool and a code-style tool to the stack to support the
"Quality First" pillar, since none was listed.
decision: >
Added `backend_static_analysis` (PHPStan for general code quality, Psalm for
code security — both are used, for different purposes) and
`backend_code_style_tool` (PHP CS Fixer) above.
rationale: >
PHPStan and Psalm catch different classes of issues (general type/quality bugs
vs. security-focused taint analysis), so both are kept rather than picking one.
open_questions: []
suggestions: []
features_overview:
# High-level list of features/capabilities the app will provide.
features:
- Site Backup
- Site Restore
- Page Builder Core
- Search Functionality
- Comments System
- Media Manager
- Blog plugin
- i18n
- Theme/Plugin Management
- User Management
- Role Based Auth Management
notes:
- Site Backup:
- File and Database backup:
- Not automatic in v1
- Not regular schedule in v1
- Via action triggered in Admin section
- Does `sqldump` (MySQL) or equiv for database, adds to zip archive of CMS folder, offers download of zip archive
- Site Restore:
- Restoring the site from a previous backup
- Page Builder Core:
- The CMS itself is a robust Page Builder
- Blog functionality is a plugin/module interacting with the Page Builder core, Media Manager, and Navigation
- Search Functionality:
- Full-text search for posts and pages
- Category-based filtering (Blog Post Search by tags)
- Faceted Search (Blog Section Only, Left Column, Amazon/eBay style)
- Text search input
- Category/Tag checkboxes
- Date range filters
- Dynamic filtering results
- Comments System:
- Only in Blog posts, not part of Pages
- Author can turn off comments for each blog post
- Author can delete User comments
- Spam protection - Akismet integration + honeypot + rate-limiting
decisions_log:
- question: How should modules be installable by end users?
decision: >
Modules are installable via ZIP file upload to the system.
rationale: >
ZIP upload keeps module installation accessible to admins without CLI or
Composer access.
- question: >
The `features` list itself was empty while all the actual feature content lived
in `notes`. Should `features` be populated with a concise catalog for
at-a-glance scanning?
decision: >
Yes — `features` has been populated. User Management and Role Based Auth
Management were also added to the list above, since they were documented in
`app.notes`/`auth_requirements`/`scope.in_scope` but missing from this catalog.
rationale: >
Keeps `features` a complete, scannable catalog rather than a partial list that
only covers items that happened to also get `notes`.
- question: >
Akismet was the only spam-protection measure named for the Comments System;
should a fallback be added in case Akismet is unavailable or insufficient?
decision: >
Yes — honeypot and rate-limiting have been added alongside Akismet (see
`notes.Comments System` above).
rationale: >
Layers a bot-trap and a submission-frequency cap on top of the third-party
Akismet service so spam protection doesn't have a single point of failure.
open_questions: []
suggestions: []
# Per-domain architecture/requirements sections go here, e.g.:
# auth_requirements:
# Adapt the exact set of sections to the project as it takes shape.
# (A separate backend_architecture section was considered and intentionally omitted —
# see the decisions_log under `architecture` below. A dedicated frontend_architecture
# section was added instead — see the decisions_log under `architecture` and the
# `frontend_architecture` section itself, below.)
frontend_architecture:
notes:
- Vue is used exclusively via its Components feature (not as a full SPA framework),
per the `js_framework` decision in `tech_stack`; individual Vue components are
mounted onto elements within server-rendered PHP theme templates
(`layout.php`/partials), not a client-side-routed app shell.
- Fomantic UI supplies the semantic CSS class vocabulary for markup rendered by the
custom PHP templating engine (`theme.md`/`layout.php`/partials, per
`themes_architecture`); Vue components are expected to reuse Fomantic UI classes
rather than introduce a competing utility-class system.
- Compiled/bundled CSS and JS output lands in `public/build` (per
`directory_structure`); each theme's own `assets/css` and `assets/js` (and any
shared/core frontend source) are the inputs to that build step.
- The PHP templating engine handles server-side rendering of full pages; Vue
Components layer on top only for interactive widgets (e.g. the Theme/Plugin
Editor's split-pane code editor, the Media Manager's Focal Point Selector, the
Faceted Search filter panel).
- Vite is the build tool that bundles the Vue Single-File Components into the
`public/build` output.
- Vue Components are authored as Single-File Components (`.vue` files) living
under each theme's `assets/js/components` directory (see `directory_structure`).
- Each interactive widget gets its own independent Vue instance (no single global
app instance mounting every widget on a page).
- A mounted component is referenced in server-rendered markup as a custom HTML
element tag matching the component name (e.g. `<AlertBox message="Something went
wrong">`), with data passed in as HTML attributes that Vue exposes to the
component as props.
decisions_log:
- question: >
Which build tool bundles the Vue Components and produces the `public/build`
output (e.g. Vite, Webpack, esbuild)?
decision: Vite
rationale: >
Chosen as the build tool despite being new to the author, as a deliberate
opportunity to learn it.
- question: >
How are Vue components mounted into PHP-rendered templates, and how is data
passed from PHP to a mounted component?
decision: >
Components are referenced directly in server-rendered markup as a custom HTML
element tag matching the component name (e.g. `<AlertBox message="Something
went wrong">`); data is passed in as HTML attributes, which Vue exposes to the
component's script block as props.
rationale: >
Matches Vue's own documented Single-File Component usage pattern, keeping the
PHP-side markup declarative rather than needing a separate bootstrap script to
wire up mount points and data.
- question: >
Is there a single global Vue application instance mounting multiple components
per page, or does each interactive widget get its own independent Vue instance?
decision: Each interactive widget gets its own independent Vue instance.
rationale: >
Keeps each widget (Theme/Plugin Editor, Focal Point Selector, Faceted Search
filter panel, etc.) self-contained rather than coordinated through one shared
page-level app instance.
- question: >
Will Vue Components be authored as Single-File Components (`.vue` files needing
a build step) or as plain JS component definitions, and where do their source
files live?
decision: >
Single-File Components (`.vue` files), living under `assets/js/components`
within each theme.
rationale: >
Keeps component source files colocated with the theme that uses them, following
Vue's standard SFC authoring approach.
open_questions: []
directory_structure:
notes:
- Secrets are injected via environment variables set by Nginx from a selfhosted Infisical instance
- Example setting environment variables in nginx config file
- >
```
env INFISICAL_URL="https://your.infisical.instance";
env INFISICAL_TOKEN="YOUR_INFISICAL_TOKEN";
env INFISICAL_SECRET_ID="YOUR_PROJECT_OR_FOLDER_ID";
```
- core:
- admin: (contains all the files/directories for the admin backend dashboard)
- bootstrap
- config: (PHP configuration files; environmentspecific secrets are injected via Nginx from the selfhosted Infisical instance)
- database:
- migrations: (Phinx migrations for core tables - users, roles, permissions, pages, posts, settings, etc.)
- seeds: (Phinx seeds for default data - default roles, default admin theme, default frontend theme, etc.)
- siteweaver.data (file) (if using SQLite3)
- router:
- engine
- (actual routes files)
- services:
- providers: (Service Provider classes that register/bootstrap functionality with the core app, per the Service Provider glossary term)
- (actual service implementation classes live directly under core/services, e.g. TranslationService.php, BackupService.php, StorageService.php)
- public: (web server root, contains front-controller)
- build (compiled/bundled CSS and JS output produced by the front-end build step)
- site:
- admin_themes:
- SiteWeaverAdmin2026: (same internal structure as a Frontend Theme below, using admin.theme.md instead of theme.md)
- plugins:
- blog:
- plugin.md (required; plugin metadata = Title, Author, Description, Date, Version, Core Version)
- migrations (this plugin's own Phinx migrations; scanned and run automatically on install, per Modular DB Migrations)
- (Service Provider class registering the plugin with the core app)
- admin (the plugin's own Admin section pages, e.g. Posts CRUD for the Blog Post Author)
- partials (the plugin's own template partials for front-end output, e.g. post.php, postlist.php)
- themes:
- SiteWeaver2026:
- theme.md (required; theme metadata - Title, Author, Description, Date, Version, Hooks)
- layout.php (required)
- screenshot.jpg
- partials: (optional; rendered via $theme->partial($path), subdirectories allowed)
- assets:
- css:
- js:
- components (Vue Single-File Components (`.vue`), per `frontend_architecture`)
- images:
- storage (outside public/, not directly web-accessible; served through a controller/route instead):
- media: (uploaded files when using the Local Disk storage backend)
- cache: (JIT-generated image variants, and Preview/Publish Cache Warming output for Pages/Posts)
- tests:
- behavioral:
- feature:
- integration:
- unit:
decisions_log:
- question: >
`core/router/engine` is listed as a placeholder — will the router be fully
custom (per the "custom/home-brewed" decision in `app`), or based on a small
existing routing library?
decision: >
Fully custom routing library (`core/router/engine`), not a third-party
package. It passes all set route variables, in order, to the controller's
`__invoke` method, and throws a `RouteConflictException` if more than one
route is registered against the same controller. It must support:
- Middleware — `middleware('auth')`
- Route Variables — `get('page/{id}', PageController::class)`
- Route Variable Typing — `get('page/{id:int}', PageController::class)`
- Optional Route Variables (with or without typing) — `get('page/{?id}', PageController::class)`
- Route Variable Constraints — `get('page/{id:int}', PageController::class)->if('id', '[0-5]')`
- Named Routes — `get('page/{id:int}', PageController::class)->name('page')`
- Name-to-route URL Expansion — `url('home')` outputs the URL for the "home" named route
- Groups — `group('admin')`
rationale: >
Keeps routing consistent with the project-wide "custom/home-brewed backend"
decision in `app`, while explicitly listing the feature set the custom router
must implement so it isn't under-built.
- question: >
No PSR-4 (or equivalent) autoloading/namespace convention has been documented
for `core/`, `site/plugins/*`, and `site/themes/*`. Should that be captured here
before MILESTONES.yaml is written?
decision: >
Yes. PSR-4 namespace mapping:
- `core` => `SiteWeaver\Core`
- `site/plugins` => `SiteWeaver\Plugins`
- `site/themes` => `SiteWeaver\Themes`
- `site/admin_themes` => `SiteWeaver\AdminThemes`
- `tests` => `SiteWeaver\Tests`
rationale: >
Gives every directory tree a predictable, standard PSR-4 namespace so
Composer autoloading and MILESTONES.yaml file paths can be derived consistently.
open_questions: []
suggestions:
- Add a reference to the self-hosted Infisical instance and document the required environment variables for Nginx.
media_manager_architecture:
notes:
- Media Manager:
- Focal Point Selector: Picking focus ensures art direction
- Just-In-Time (JIT) Generation (Glide)
- Preview Cache Warming + Automated Cache Warming on Publish
- Orphaned Media Watcher: Identifies unreferenced files and cached artifacts
- Folder organization
- Storage Abstractions: Support for Local Disk, S3, Nextcloud, and Linode Object-Storage (S3-compatible)
- Define a Storage Interface: <
(`StorageInterface::get($path), put($path,$data), delete($path)`) and have each backend implement it.
The rest of the CMS (media upload, JIT generation) talks only to that interface.
This makes swapping providers painless and lets you add, e.g., Azure Blob Storage later.
- Allowed file-extensions:
- Images:
- .jpg:
- MIME-type: image/jpeg
- Max Upload Size: 10 MB
- .jpeg:
- MIME-type: image/jpeg
- Max Upload Size: 10 MB
- .png:
- MIME-type: image/png
- Max Upload Size: 10 MB
- .gif:
- MIME-type: image/gif
- Max Upload Size: 10 MB
- .bmp:
- MIME-type: image/bmp
- Max Upload Size: 10 MB
- .ico:
- MIME-type: image/vnd.microsoft.icon
- MIME-type: image/x-icon
- Max Upload Size: 10 MB
- .svg:
- MIME-type: image/svg+xml
- Max Upload Size: 10 MB
- Videos:
- .mp4:
- MIME-type: video/mp4
- Max Upload Size: 100 MB
- .mov:
- MIME-type: video/quicktime
- Max Upload Size: 100 MB
- .avi:
- MIME-type: video/x-msvideo
- Max Upload Size: 100 MB
- .wmv:
- MIME-type: video/x-ms-asf
- MIME-type: video/x-ms-wmv
- Max Upload Size: 100 MB
- Upload validation:
- Check MIME type against allowed list.
- Return clear error if max size exceeded or unsupported MIME type.
decisions_log:
- question: >
Which image library backs JIT Generation, Glide or Intervention Image? (Same
question as `tech_stack`; flagging here too since this is where the
"Glide/Intervention" note actually lived.)
decision: Glide
rationale: >
Same decision/rationale as logged in `tech_stack.decisions_log`; kept in sync
across both sections.
- question: >
The glossary's "Storage Interface / Storage Abstraction" term lists GCP as an
example backend, but this section's "Storage Abstractions" list only names Local
Disk, S3, Nextcloud, and Linode Object-Storage. Should GCP be added to the
official supported-backends list here, or removed from the glossary example to
keep the two in sync?
decision: >
GCP has been removed from the glossary example (see `glossary`); it will not be
a supported storage backend at this time.
rationale: >
Keeps the glossary example list in sync with the actual supported backends
named here, rather than implying GCP support that doesn't exist.
open_questions: []
suggestions: []
auth_requirements:
notes:
- User Management:
- Roles & Permissions:
- Admin (created by default)
- Editor (created by default)
- Author (created by default)
- User (created by default)
- other roles can be created by Admin or those with permission granted by Role
- All roles other than Admin can be created/edited/deleted by Admin (including Editor, Author, User)
- User Management CRUD in Backend Admin Section
- Primary Admin Protection: Admin role/account cannot be edited or deleted
- Role Extensibility: All other roles can be modified, and new custom roles can be added (Always by Admin, others if granted by Role)
- Granular Permissions: CRUD-level control (View, Edit, Delete) per resource (Page, Plugin, Theme, Blog Post, User)
- Profile Management: Backend users (Admin, Editor, Author) can update their own profile information (Name, Email, Password).
- User role: Public-facing site viewers who can comment on blog posts, but cannot access the admin section
- Blog Post Author Permissions (Hard-coded/Default):
- Authors can view/edit only their own blog posts in the admin backend by default.
- Access to other authors' posts is restricted unless explicitly permitted by Role.
- Deleting their own "Published" blog posts is a granular permission that can be granted or revoked.
- All Published blog posts remain visible to anyone on the front-end.
- Password Reset: >
A logged-in user changes their own password from their Profile page (see
Profile Management above). This covers changing a known password.
- Forgot Password Recovery: >
A logged-out user who has forgotten their password can request a
self-service reset via an emailed, tokenized reset link (valid for 1
hour), which lets them set a new password directly. Successfully
completing a reset also clears/unlocks any active Login Lockout on that
account.
- Login Lockout Policy: >
After 3 consecutive failed login attempts, the account is temporarily
locked for 2 hours; an Admin can manually override/unlock the lockout at
any time.
- Email Verification: >
The public-facing User (commenter) role must verify their email after
account creation; email verification is not required before submitting a
comment.
- Multiple Admin Accounts: >
A site may have more than one Admin account (e.g. one Admin account
created per client, in addition to the developer's own); those Admins can
create further accounts/roles (Editor, Author, User, etc.) as needed.
decisions_log:
- question: What is the password-reset/forgot-password flow (self-service email link, admin-reset only, or both)?
decision: >
A logged-in user changes their own password from their Profile page (see
"Password Reset" note above); this handles a known/current password change.
rationale: >
Reuses the Profile Management capability already planned for backend users
instead of building a separate flow for a password change.
- question: >
Is there a login rate-limiting/lockout policy (e.g. temporary lockout after N
failed attempts) to complement 2FA?
decision: >
Yes — a temporary 2-hour lockout after 3 failed login attempts, which an Admin
can manually override/unlock (see "Login Lockout Policy" note above).
rationale: >
Slows down brute-force login attempts while giving the Admin an escape hatch
for a legitimately locked-out user.
- question: >
Should the public-facing User (commenter) role require email verification before
their comments are accepted/shown?
decision: >
Email verification is required after account creation, but is not a
precondition for submitting a comment (see "Email Verification" note above).
rationale: >
Confirms the commenter's email address without adding friction/delay to the
act of leaving a comment itself.
- question: Can a site have more than 1 Admin account?
decision: >
Yes — a site can have more than one Admin account (see "Multiple Admin
Accounts" note above).
rationale: >
Matches the intended usage model where the developer holds an Admin account on
every CMS instance alongside a separate Admin account created per client.
- question: >
The Profile-page password change requires being logged in already, so it
doesn't cover a user who has forgotten their password and is locked out. Is a
true "forgot password" recovery path needed (e.g. a self-service emailed
reset link) for that case, or is recovery always Admin-assisted (an Admin
manually resets/unlocks the account)?
decision: >
Yes, a self-service Forgot Password recovery path is needed. It works via an
emailed, tokenized reset link that is valid for 1 hour; the user follows the
link to set a new password directly. Successfully completing a reset also
clears/unlocks any active Login Lockout on that account (see "Login Lockout
Policy" above), since proving ownership of the account's email is treated as
sufficient to lift the lockout.
rationale: >
Covers the case the logged-in Profile-page password change can't handle (a
forgotten password while locked out), without always requiring Admin
involvement. A 1-hour token expiration limits the exposure window for a
leaked/intercepted reset link, and clearing the lockout on successful reset
avoids forcing a legitimate user to wait out the full 2-hour lockout after
already proving account ownership via email.
open_questions: []
ui:
notes:
- Theme/Plugin Editor:
- Built-in file editor for theme/plugin files (CSS, JS, PHP, etc.) within the Admin section.
- Allows small tweaks directly on the server without local development/upload.
- UI Layout:
- '"Theme Editor" header with a dropdown for installed themes in the upper right.'
- Split-pane content area:
- File explorer on the left, code editor on the right.
- File explorer includes a "New File" action button to create new files (Template Partial, CSS, Plugin functionality (PHP), etc.).
- Automatic loading of file content with syntax highlighting upon selection.
- '"Save" and "Cancel" buttons below the split area.'
- Dashboards:
- Every non-User user that logs in is taken to a Dashboard
- Users with only User role are restricted to frontend viewing only (no admin access), and can comment on Blog posts.
- Admin Dashboard:
- Contains a table with number of users broken down by role
- Contains a table with view statistics of top 10 pages/posts (sorted by view count)
- If feasible, shows a table with exception statistics (which exceptions have been thrown and how many times)
- Author Dashboard:
- Contains a table with view statistics of top 10 pages/posts written by them (sorted by view count).
- Contains a table with a list of unpublished pages/posts (Works in Progress)
- Contains a table with the count of comments on each post written by them
- Editor Dashboard:
- To be announced.
- Media Manager:
- Only accessible by those with permissions to create/edit media entries
- Displays a thumbnail of each stored image.
- When a thumbnail is clicked, a modal appears with full size image at the top, and metadata below (height, width, alt text, filename, etc)
- User Management:
- Only accessible by those with permissions to view/edit users
- Shows a table of each user in the system, with username (link), role(s), date created, date last login, status ("Good", "Locked out", etc)
- When the username link is clicked, a modal shows with username as text, email address as text, roles as checkboxes (all roles listed, checked if user contains role), dates as text, action buttons to Save (if role data has changed), Delete, Unlock.
- Has a button near the top to create new users. When button is clicked, a modal appears with form fields for username (text field), email address (text field), roles (list of checkboxes), a save button and a cancel button at the bottom.
- When modal form is submitted, a new user record is added to the database with a temp password, and an email is sent to the user email address with the temp password.
- On first login, the user is required to change their password.
- Role Management:
- Only accessible by those with permissions to view/edit roles
- Shows a table with a list of roles (links), data created, created by
- When a role link is clicked, takes you to a page with a list of permissions, green checkmarks marking the permissions the role has, red x's marking the permissions the role does not have, and an edit button towards the top.
- When the edit button is clicked, the page turns into a form, with checkboxes (checked if role has permission, unchecked if role does not have permission), a save button, and a cancel button
- Page Builder:
- Only accessible by those with permissions to create/edit pages
- A large form to create a page.
- A Page Title text box
- A Checkbox to "Include in Site Navigation"
- A Drop down to choose between custom content or Plugin output.
- If Drop down is custom content, show a WYSIWYG Editor for Page Content (with header buttons (H1-H6), bold, italics, hyperlink, image (include image from media storage), text alignment (applies to entire paragraph (left, middle, right)), switch to Markdown mode, switch to HTML mode)
- If Drop down is Plugin output, show a drop down of active Plugins that have output that can be displayed.
decisions_log:
- question: >
Only the Theme/Plugin Editor's layout is documented here. Should the Admin
dashboard, Media Manager, Page Builder, and User Management screens each get
their own UI notes before milestones are planned, or will that detail be worked
out milestone-by-milestone?
decision: >
The Admin dashboard, Media Manager, User Management, Role Management, and Page
Builder screens are now documented above alongside the Theme/Plugin Editor (see
`notes` above), so no further per-screen UI documentation gap remains at this
time.
rationale: >
Rounds out `ui.notes` so every major screen named in `features_overview` has at
least a first-pass layout description to plan milestones against; any gaps
found later can be raised as new open_questions as they're discovered.
open_questions: []
project_owner:
notes: >
This repository is maintained by a single developer who builds the core,
themes, and plugins for clients. An Admin account is created for the developer initially;
afterwards an Admin account is provisioned for each client, who can then create additional
user accounts/roles as needed.
project_management:
open_questions: []
team_size: 1
budget_range: $100
deployment_strategy:
- ForgeJo Actions CI/CD
- Dev and Prod Environments (no staging)
scope_v1_boundaries:
in_scope_for_launch:
- User registration/login (password + email verification)
- Admin panel with page builder core
- Blog plugin with basic CRUD and comments
- Storage backends:
- Local Disk
- S3
- Nextcloud
- Linode Object-Storage
- Multi-database support:
- MariaDB set as default
- SQLite/PostgreSQL available
- URL-based locale detection
- Faceted search
- Auto-i18n translation features
i18n_translation_workflow:
- UI strings: T-like helper method (`t('key')`) for all admin-facing text
- Content localization: Database-backed (locale-specific rows/JSON columns per resource)
- Translation service: "Translate for Me"
testing:
notes:
- Unit Testing
- Feature Testing
- Integration Testing
- Behavior Testing
- Expected Coverage: 95+% of self-owned code (no need to test code in the vendor directory)
decisions_log:
- question: |
Is there a target CI provider/pipeline where the Codeception suites (behavioral/
feature/integration/unit) will run automatically, and at what stage (on every
PR, pre-merge, nightly)?
decision: >
The repo will be stored on a Forgejo installation. Forgejo Actions will be the target CI provider.
open_questions: []