# 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, high‑level groupings that organize a blog’s 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, budget‑travel, 2024‑updates). - 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="human‑readable 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="human‑readable 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 read‑only 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 site‑wide 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 (many‑to‑many 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 self‑hosted 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. ``), 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. ``); 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 self‑hosted 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; environment‑specific secrets are injected via Nginx from the self‑hosted 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: []