Merge branch 'dev' into version/03

This commit is contained in:
2026-04-07 20:16:10 -05:00
13 changed files with 459 additions and 899 deletions

376
.github/CLAUDE.md vendored
View File

@@ -1,3 +1,22 @@
<!--
Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
This file is part of a Moko Consulting project.
SPDX-License-Identifier: GPL-3.0-or-later
# FILE INFORMATION
DEFGROUP: MokoStandards.Templates.GitHub
INGROUP: MokoStandards.Templates
REPO: https://github.com/mokoconsulting-tech/MokoStandards
PATH: /templates/github/copilot-instructions.joomla.md.template
VERSION: XX.YY.ZZ
BRIEF: GitHub Copilot custom instructions template for Joomla/MokoWaaS governed repositories
NOTE: Synced to .github/copilot-instructions.md in all Joomla/WaaS repos via bulk sync.
Tokens replaced at sync time: MokoCassiopeia, https://github.com/mokoconsulting-tech/MokoCassiopeia, {{EXTENSION_NAME}},
{{EXTENSION_TYPE}}, {{EXTENSION_ELEMENT}}
-->
> [!IMPORTANT]
> **🔧 AI Self-Update Required on First Use**
>
@@ -19,135 +38,34 @@
> |---|---|
> | `MokoCassiopeia` | The GitHub repository name (visible in the URL, `README.md` heading, or `git remote -v`) |
> | `https://github.com/mokoconsulting-tech/MokoCassiopeia` | Full GitHub URL, e.g. `https://github.com/mokoconsulting-tech/<repo-name>` |
> | `A modern enhancement layer for Joomlas Cassiopeia template. Moko-Cassiopeia adds Font Awesome 7, Bootstrap 5 helpers, an automatic Table of Contents (TOC) utility, and optional expansions including Google Tag Manager and Google Analytics (GA4) hooks.` | First paragraph of `README.md` body, or the GitHub repo description |
> | `{{EXTENSION_NAME}}` | The `<name>` element in `manifest.xml` at the repository root |
> | `{{EXTENSION_TYPE}}` | The `type` attribute of the `<extension>` tag in `manifest.xml` (`component`, `module`, `plugin`, or `template`) |
> | `{{EXTENSION_ELEMENT}}` | The `<element>` tag in `manifest.xml`, or the filename prefix (e.g. `com_myextension`, `mod_mymodule`) |
>
> ---
# What This Repo Is
# MokoCassiopeia — GitHub Copilot Custom Instructions
**MokoCassiopeia** is a Moko Consulting **MokoWaaS** (Joomla) extension repository.
## What This Repo Is
A modern enhancement layer for Joomlas Cassiopeia template. Moko-Cassiopeia adds Font Awesome 7, Bootstrap 5 helpers, an automatic Table of Contents (TOC) utility, and optional expansions including Google Tag Manager and Google Analytics (GA4) hooks.
This is a **Moko Consulting MokoWaaS** (Joomla) repository governed by [MokoStandards](https://github.com/mokoconsulting-tech/MokoStandards). All coding standards, workflows, and policies are defined there and enforced here via bulk sync.
Repository URL: https://github.com/mokoconsulting-tech/MokoCassiopeia
Extension name: **{{EXTENSION_NAME}}**
Extension type: **{{EXTENSION_TYPE}}** (`{{EXTENSION_ELEMENT}}`)
Repository URL: https://github.com/mokoconsulting-tech/MokoCassiopeia
This repository is governed by [MokoStandards](https://github.com/mokoconsulting-tech/MokoStandards) — the single source of truth for coding standards, file-header policies, GitHub Actions workflows, and Terraform configuration templates across all Moko Consulting repositories.
Platform: **Joomla 4.x / MokoWaaS**
---
# Repo Structure
## Primary Language
```
MokoCassiopeia/
├── manifest.xml # Joomla installer manifest (root — required)
├── update.xml # Update server manifest (root — required)
├── site/ # Frontend (site) code
│ ├── controller.php
│ ├── controllers/
│ ├── models/
│ └── views/
├── admin/ # Backend (admin) code
│ ├── controller.php
│ ├── controllers/
│ ├── models/
│ ├── views/
│ └── sql/
├── language/ # Language INI files
├── media/ # CSS, JS, images
├── docs/ # Technical documentation
├── tests/ # Test suite
├── .github/
│ ├── workflows/ # CI/CD workflows (synced from MokoStandards)
│ ├── copilot-instructions.md
│ └── CLAUDE.md # This file
├── README.md # Version source of truth
├── CHANGELOG.md
├── CONTRIBUTING.md
└── LICENSE # GPL-3.0-or-later
```
**PHP** (≥ 7.4) is the primary language for this Joomla extension. JavaScript may be used for frontend enhancements. YAML uses 2-space indentation. All other text files use tabs per `.editorconfig`.
---
# Primary Language
## File Header — Always Required on New Files
**PHP** (≥ 7.4) is the primary language for this Joomla extension. YAML uses 2-space indentation. All other text files use tabs per `.editorconfig`.
---
# Version Management
**`README.md` is the single source of truth for the repository version.**
- **Bump the patch version on every PR** — increment `XX.YY.ZZ` (e.g. `01.02.03``01.02.04`) in `README.md` before opening the PR; the `sync-version-on-merge` workflow propagates it to all `FILE INFORMATION` headers automatically on merge.
- Version format is zero-padded semver: `XX.YY.ZZ` (e.g. `01.02.03`).
- Never hardcode a version number in body text — use the badge or FILE INFORMATION header only.
### Joomla Version Alignment
Three files must **always have the same version**:
| File | Where the version lives |
|------|------------------------|
| `README.md` | `FILE INFORMATION` block + badge |
| `manifest.xml` | `<version>` tag |
| `update.xml` | `<version>` in the most recent `<update>` block |
The `make release` command / release workflow syncs all three automatically.
---
# update.xml — Required in Repo Root
`update.xml` is the Joomla update server manifest. It allows Joomla installations to check for new versions of this extension via:
```xml
<!-- In manifest.xml -->
<updateservers>
<server type="extension" priority="1" name="{{EXTENSION_NAME}}">
https://github.com/mokoconsulting-tech/MokoCassiopeia/raw/main/update.xml
</server>
</updateservers>
```
**Rules:**
- Every release prepends a new `<update>` block at the top — older entries are preserved.
- `<version>` in `update.xml` must exactly match `<version>` in `manifest.xml` and `README.md`.
- `<downloadurl>` must be a publicly accessible GitHub Releases asset URL.
- `<targetplatform version="4\.[0-9]+">` — backslash is literal (Joomla regex syntax).
Example `update.xml` entry for a new release:
```xml
<updates>
<update>
<name>{{EXTENSION_NAME}}</name>
<description>MokoCassiopeia</description>
<element>{{EXTENSION_ELEMENT}}</element>
<type>{{EXTENSION_TYPE}}</type>
<version>01.02.04</version>
<infourl title="Release Information">https://github.com/mokoconsulting-tech/MokoCassiopeia/releases/tag/01.02.04</infourl>
<downloads>
<downloadurl type="full" format="zip">
https://github.com/mokoconsulting-tech/MokoCassiopeia/releases/download/01.02.04/{{EXTENSION_ELEMENT}}-01.02.04.zip
</downloadurl>
</downloads>
<targetplatform name="joomla" version="4\.[0-9]+" />
<php_minimum>7.4</php_minimum>
<maintainer>Moko Consulting</maintainer>
<maintainerurl>https://mokoconsulting.tech</maintainerurl>
</update>
</updates>
```
---
# File Header Requirements
Every new file **must** have a copyright header as its first content. JSON files, binary files, generated files, and third-party files are exempt.
Every new file needs a copyright header as its first content.
**PHP:**
```php
@@ -162,47 +80,141 @@ Every new file **must** have a copyright header as its first content. JSON files
* DEFGROUP: MokoCassiopeia.{{EXTENSION_TYPE}}
* INGROUP: MokoCassiopeia
* REPO: https://github.com/mokoconsulting-tech/MokoCassiopeia
* PATH: /site/controllers/item.php
* PATH: /path/to/file.php
* VERSION: XX.YY.ZZ
* BRIEF: One-line description of file purpose
* BRIEF: One-line description of purpose
*/
defined('_JEXEC') or die;
```
**Markdown / YAML / Shell / XML:** Use the appropriate comment syntax with the same fields.
**Markdown:**
```markdown
<!--
Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
This file is part of a Moko Consulting project.
SPDX-License-Identifier: GPL-3.0-or-later
# FILE INFORMATION
DEFGROUP: MokoCassiopeia.Documentation
INGROUP: MokoCassiopeia
REPO: https://github.com/mokoconsulting-tech/MokoCassiopeia
PATH: /docs/file.md
VERSION: XX.YY.ZZ
BRIEF: One-line description
-->
```
**YAML / Shell / XML:** Use the appropriate comment syntax with the same fields. JSON files are exempt.
---
# Coding Standards
## Version Management
## Naming Conventions
**`README.md` is the single source of truth for the repository version.**
| Context | Convention | Example |
|---------|-----------|---------|
| PHP class | `PascalCase` | `ItemModel` |
| PHP method / function | `camelCase` | `getItems()` |
| PHP variable | `$snake_case` | `$item_id` |
| PHP constant | `UPPER_SNAKE_CASE` | `MAX_ITEMS` |
| PHP class file | `PascalCase.php` | `ItemModel.php` |
| YAML workflow | `kebab-case.yml` | `ci-joomla.yml` |
| Markdown doc | `kebab-case.md` | `installation-guide.md` |
- **Bump the patch version on every PR** — increment `XX.YY.ZZ` (e.g. `01.02.03``01.02.04`) in `README.md` before opening the PR; the `sync-version-on-merge` workflow propagates it automatically to all badges and `FILE INFORMATION` headers on merge to `main`.
- The `VERSION: XX.YY.ZZ` field in `README.md` governs all other version references.
- Version format is zero-padded semver: `XX.YY.ZZ` (e.g. `01.02.03`).
- Never hardcode a specific version in document body text — use the badge or FILE INFORMATION header only.
## Commit Messages
### Joomla Version Alignment
Format: `<type>(<scope>): <subject>` — imperative, lower-case subject, no trailing period.
The version in `README.md` **must always match** the `<version>` tag in `manifest.xml` and the latest entry in `update.xml`. The `make release` command / release workflow updates all three automatically.
Valid types: `feat` · `fix` · `docs` · `chore` · `ci` · `refactor` · `style` · `test` · `perf` · `revert` · `build`
```xml
<!-- In manifest.xml — must match README.md version -->
<version>01.02.04</version>
## Branch Naming
Format: `<prefix>/<MAJOR.MINOR.PATCH>[/description]`
Approved prefixes: `dev/` · `rc/` · `version/` · `patch/` · `copilot/` · `dependabot/`
<!-- In update.xml — prepend a new <update> block for every release.
Note: the backslash in version="4\.[0-9]+" is a literal backslash character
in the XML attribute value. Joomla's update server treats the value as a
regular expression, so \. matches a literal dot. -->
<updates>
<update>
<name>{{EXTENSION_NAME}}</name>
<version>01.02.04</version>
<downloads>
<downloadurl type="full" format="zip">
https://github.com/mokoconsulting-tech/MokoCassiopeia/releases/download/01.02.04/{{EXTENSION_ELEMENT}}-01.02.04.zip
</downloadurl>
</downloads>
<targetplatform name="joomla" version="4\.[0-9]+" />
</update>
<!-- … older entries preserved below … -->
</updates>
```
---
# GitHub Actions — Token Usage
## Joomla Extension Structure
```
MokoCassiopeia/
├── manifest.xml # Joomla installer manifest (root — required)
├── update.xml # Update server manifest (root — required, see below)
├── site/ # Frontend (site) code
│ ├── controller.php
│ ├── controllers/
│ ├── models/
│ └── views/
├── admin/ # Backend (admin) code
│ ├── controller.php
│ ├── controllers/
│ ├── models/
│ ├── views/
│ └── sql/
├── language/ # Language INI files
├── media/ # CSS, JS, images (deployed to /media/{{EXTENSION_ELEMENT}}/)
├── docs/ # Technical documentation
├── tests/ # Test suite
├── .github/
│ ├── workflows/
│ ├── copilot-instructions.md # This file
│ └── CLAUDE.md
├── README.md # Version source of truth
├── CHANGELOG.md
├── CONTRIBUTING.md
├── LICENSE # GPL-3.0-or-later
└── Makefile # Build automation
```
---
## update.xml — Required in Repo Root
`update.xml` **must exist at the repository root**. It is the Joomla update server manifest that allows Joomla installations to check for new versions of this extension.
The `manifest.xml` must reference it via:
```xml
<updateservers>
<server type="extension" priority="1" name="{{EXTENSION_NAME}}">
https://github.com/mokoconsulting-tech/MokoCassiopeia/raw/main/update.xml
</server>
</updateservers>
```
**Rules:**
- Every release must prepend a new `<update>` block at the top of `update.xml` — old entries must be preserved below.
- The `<version>` in `update.xml` must exactly match `<version>` in `manifest.xml` and the version in `README.md`.
- The `<downloadurl>` must be a publicly accessible direct download link (GitHub Releases asset URL).
- `<targetplatform name="joomla" version="4\.[0-9]+">` — the backslash is a **literal backslash character** in the XML attribute value; Joomla's update-server parser treats the value as a regular expression, so `\.` matches a literal dot and `[0-9]+` matches one or more digits. Do not double-escape it.
---
## manifest.xml Rules
- Lives at the repo root as `manifest.xml` (not inside `site/` or `admin/`).
- `<version>` tag must be kept in sync with `README.md` version and `update.xml`.
- Must include `<updateservers>` block pointing to this repo's `update.xml`.
- Must include `<files folder="site">` and `<administration>` sections.
- Joomla 4.x requires `<namespace path="src">Moko\{{EXTENSION_NAME}}</namespace>` for namespaced extensions.
---
## GitHub Actions — Token Usage
Every workflow must use **`secrets.GH_TOKEN`** (the org-level Personal Access Token).
@@ -217,58 +229,76 @@ env:
```
```yaml
# ❌ Wrong — never use these
# ❌ Wrong — never use these in workflows
token: ${{ github.token }}
token: ${{ secrets.GITHUB_TOKEN }}
```
---
# Keeping Documentation Current
## MokoStandards Reference
| Change type | Documentation to update |
|-------------|------------------------|
| New or renamed PHP class/method | PHPDoc block; `docs/api/` entry |
| New or changed `manifest.xml` | Sync version to `update.xml` and `README.md` |
| New release | Prepend `<update>` to `update.xml`; update `CHANGELOG.md`; bump `README.md` |
| New or changed workflow | `docs/workflows/<workflow-name>.md` |
| Any modified file | Update the `VERSION` field in that file's `FILE INFORMATION` block |
| **Every PR** | **Bump the patch version** — increment `XX.YY.ZZ` in `README.md`; `sync-version-on-merge` propagates it |
---
# What NOT to Do
- **Never commit directly to `main`** — all changes go through a PR.
- **Never hardcode version numbers** in body text — update `README.md` and let automation propagate.
- **Never let `manifest.xml`, `update.xml`, and `README.md` versions diverge.**
- **Never skip the FILE INFORMATION block** on a new source file.
- **Never use bare `catch (\Throwable $e) {}`** — always log or re-throw.
- **Never mix tabs and spaces** within a file — follow `.editorconfig`.
- **Never use `github.token` or `secrets.GITHUB_TOKEN` in workflows** — always use `secrets.GH_TOKEN`.
- **Never remove `defined('_JEXEC') or die;`** from web-accessible PHP files.
---
# PR Checklist
Before opening a PR, verify:
- [ ] Patch version bumped in `README.md` (e.g. `01.02.03``01.02.04`)
- [ ] If this is a release: `manifest.xml` version updated; `update.xml` updated with new entry
- [ ] FILE INFORMATION headers updated in modified files
- [ ] CHANGELOG.md updated
- [ ] Tests pass
---
# Key Policy Documents (MokoStandards)
This repository is governed by [MokoStandards](https://github.com/mokoconsulting-tech/MokoStandards). Authoritative policies:
| Document | Purpose |
|----------|---------|
| [file-header-standards.md](https://github.com/mokoconsulting-tech/MokoStandards/blob/main/docs/policy/file-header-standards.md) | Copyright-header rules for every file type |
| [coding-style-guide.md](https://github.com/mokoconsulting-tech/MokoStandards/blob/main/docs/policy/coding-style-guide.md) | Naming and formatting conventions |
| [branching-strategy.md](https://github.com/mokoconsulting-tech/MokoStandards/blob/main/docs/policy/branching-strategy.md) | Branch naming, hierarchy, and release workflow |
| [merge-strategy.md](https://github.com/mokoconsulting-tech/MokoStandards/blob/main/docs/policy/merge-strategy.md) | Squash-merge policy and PR conventions |
| [merge-strategy.md](https://github.com/mokoconsulting-tech/MokoStandards/blob/main/docs/policy/merge-strategy.md) | Squash-merge policy and PR title/body conventions |
| [changelog-standards.md](https://github.com/mokoconsulting-tech/MokoStandards/blob/main/docs/policy/changelog-standards.md) | How and when to update CHANGELOG.md |
| [joomla-development-guide.md](https://github.com/mokoconsulting-tech/MokoStandards/blob/main/docs/guide/waas/joomla-development-guide.md) | MokoWaaS Joomla extension development guide |
---
## Naming Conventions
| Context | Convention | Example |
|---------|-----------|---------|
| PHP class | `PascalCase` | `MyController` |
| PHP method / function | `camelCase` | `getItems()` |
| PHP variable | `$snake_case` | `$item_id` |
| PHP constant | `UPPER_SNAKE_CASE` | `MAX_ITEMS` |
| PHP class file | `PascalCase.php` | `ItemModel.php` |
| YAML workflow | `kebab-case.yml` | `ci-joomla.yml` |
| Markdown doc | `kebab-case.md` | `installation-guide.md` |
---
## Commit Messages
Format: `<type>(<scope>): <subject>` — imperative, lower-case subject, no trailing period.
Valid types: `feat` · `fix` · `docs` · `chore` · `ci` · `refactor` · `style` · `test` · `perf` · `revert` · `build`
---
## Branch Naming
Format: `<prefix>/<MAJOR.MINOR.PATCH>[/description]`
Approved prefixes: `dev/` · `rc/` · `version/` · `patch/` · `copilot/` · `dependabot/`
---
## Keeping Documentation Current
| Change type | Documentation to update |
|-------------|------------------------|
| New or renamed PHP class/method | PHPDoc block; `docs/api/` entry |
| New or changed manifest.xml | Update `update.xml` version; bump README.md version |
| New release | Prepend `<update>` block to `update.xml`; update CHANGELOG.md; bump README.md version |
| New or changed workflow | `docs/workflows/<workflow-name>.md` |
| Any modified file | Update the `VERSION` field in that file's `FILE INFORMATION` block |
| **Every PR** | **Bump the patch version** — increment `XX.YY.ZZ` in `README.md`; `sync-version-on-merge` propagates it |
---
## Key Constraints
- Never commit directly to `main` — all changes go via PR, squash-merged
- Never skip the FILE INFORMATION block on a new file
- Never add `defined('_JEXEC') or die;` to CLI scripts or model tests — only to web-accessible PHP files
- Never hardcode version numbers in body text — update `README.md` and let automation propagate
- Never use `github.token` or `secrets.GITHUB_TOKEN` in workflows — always use `secrets.GH_TOKEN`
- Never let `manifest.xml` version, `update.xml` version, and `README.md` version go out of sync

View File

@@ -1,3 +1,22 @@
<!--
Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
This file is part of a Moko Consulting project.
SPDX-License-Identifier: GPL-3.0-or-later
# FILE INFORMATION
DEFGROUP: MokoStandards.Templates.GitHub
INGROUP: MokoStandards.Templates
REPO: https://github.com/mokoconsulting-tech/MokoStandards
PATH: /templates/github/copilot-instructions.joomla.md.template
VERSION: XX.YY.ZZ
BRIEF: GitHub Copilot custom instructions template for Joomla/MokoWaaS governed repositories
NOTE: Synced to .github/copilot-instructions.md in all Joomla/WaaS repos via bulk sync.
Tokens replaced at sync time: MokoCassiopeia, https://github.com/mokoconsulting-tech/MokoCassiopeia, {{EXTENSION_NAME}},
{{EXTENSION_TYPE}}, {{EXTENSION_ELEMENT}}
-->
> [!IMPORTANT]
> **🔧 AI Self-Update Required on First Use**
>

View File

@@ -13,13 +13,14 @@
BRIEF: Documentation for MokoCassiopeia template
-->
# README - MokoCassiopeia (VERSION: 03.09.03)
# MokoCassiopeia
**A Modern, Lightweight Joomla Template Based on Cassiopeia**
[![Version](https://img.shields.io/badge/version-03.09.04-green.svg)](https://github.com/mokoconsulting-tech/MokoCassiopeia/releases/tag/v03)
[![License: GPL v3](https://img.shields.io/badge/License-GPLv3-blue.svg)](https://www.gnu.org/licenses/gpl-3.0)
[![Joomla](https://img.shields.io/badge/Joomla-4.4.x%20%7C%205.x-blue.svg)](https://www.joomla.org)
[![PHP](https://img.shields.io/badge/PHP-8.0%2B-blue.svg)](https://www.php.net)
[![Joomla](https://img.shields.io/badge/Joomla-5.x%20%7C%206.x-blue.svg)](https://www.joomla.org)
[![PHP](https://img.shields.io/badge/PHP-8.1%2B-blue.svg)](https://www.php.net)
MokoCassiopeia is a modern, lightweight enhancement layer built on top of Joomla's Cassiopeia template. It adds **Font Awesome 7**, **Bootstrap 5** helpers, an automatic **Table of Contents (TOC)** utility, advanced **Dark Mode** theming, and optional integrations for **Google Tag Manager** and **Google Analytics (GA4)**—all while maintaining minimal core template overrides for maximum upgrade compatibility.

View File

@@ -1,276 +0,0 @@
# Scripts — MokoCassiopeia
This directory contains utility scripts for building, releasing, and managing the MokoCassiopeia Joomla template.
## Available Scripts
### build-release.sh
**Purpose**: Build a release package for MokoCassiopeia template.
**Usage**:
```bash
# Build with auto-detected version from templateDetails.xml
./scripts/build-release.sh
# Build with specific version
./scripts/build-release.sh 03.08.03
```
**What it does**:
1. Creates a `build/` directory
2. Copies template files from `src/`
3. Copies media files from `src/media/` to `media/`
4. Creates a ZIP package: `mokocassiopeia-src-{version}.zip`
5. Generates SHA-256 and MD5 checksums
6. Outputs package location and checksums
**Output**:
- `build/mokocassiopeia-src-{version}.zip` - Installation package
- `build/mokocassiopeia-src-{version}.zip.sha256` - SHA-256 checksum
- `build/mokocassiopeia-src-{version}.zip.md5` - MD5 checksum
**Requirements**:
- `rsync` for file copying
- `zip` for package creation
- `sha256sum` and `md5sum` for checksums
---
## Automated Workflows
The repository includes GitHub Actions workflows that automate the build and release process:
### `.github/workflows/release.yml`
**Purpose**: Automated release creation when tags are pushed.
**Triggers**:
- Push of version tag (e.g., `03.08.03`)
- Manual workflow dispatch with version input
**Process**:
1. Checks out repository
2. Sets up PHP environment
3. Installs dependencies (if composer.json exists)
4. Updates version numbers in manifest files
5. Creates package structure
6. Builds ZIP package
7. Generates checksums
8. Creates GitHub Release with artifacts
**Usage**:
```bash
# Create and push a tag
git tag 03.08.04
git push origin 03.08.04
# Or use GitHub UI to run manually
```
---
### `.github/workflows/auto-update-sha.yml`
**Purpose**: Automatically update SHA-256 hash in `updates.xml` after a release is published.
**Triggers**:
- GitHub Release published
- Manual workflow dispatch with tag input
**Process**:
1. Downloads the release package
2. Calculates SHA-256 hash
3. Updates `updates.xml` with:
- New version number
- Current date
- Download URL
- SHA-256 hash
4. Commits and pushes changes to main branch
**Benefits**:
- Ensures `updates.xml` always has correct SHA-256 hash
- Enables Joomla update server functionality
- Reduces manual update errors
- Automates security verification
---
## Release Process
### Manual Release (Local Build)
1. **Update version numbers**:
```bash
# Update these files manually:
# - src/templateDetails.xml
# - updates.xml
# - CHANGELOG.md
```
2. **Build package**:
```bash
./scripts/build-release.sh 03.08.04
```
3. **Test package**:
- Install ZIP in Joomla test environment
- Verify all features work correctly
4. **Create GitHub Release**:
- Go to GitHub Releases
- Click "Create a new release"
- Upload the ZIP, SHA256, and MD5 files
- Add release notes from CHANGELOG.md
5. **Update updates.xml**:
- Copy SHA-256 hash from `.sha256` file
- Update `updates.xml` with new hash
- Commit and push changes
---
### Automated Release (GitHub Actions)
1. **Update version numbers**:
```bash
# Update these files in a branch:
# - src/templateDetails.xml
# - CHANGELOG.md
git checkout -b release/03.08.04
# Make changes
git commit -m "chore: Prepare release 03.08.04"
git push origin release/03.08.04
```
2. **Create and merge PR**:
- Create PR from release branch
- Review changes
- Merge to main
3. **Create and push tag**:
```bash
git checkout main
git pull
git tag 03.08.04
git push origin 03.08.04
```
4. **Automated process**:
- GitHub Actions builds package automatically
- Creates GitHub Release with artifacts
- `auto-update-sha` workflow updates `updates.xml`
5. **Verify**:
- Check GitHub Release is created
- Verify `updates.xml` has correct SHA-256
- Test Joomla update server
---
## Development Workflow
### Testing Local Builds
```bash
# Build current version
./scripts/build-release.sh
# Install in Joomla
# Navigate to Extensions > Manage > Install > Upload Package File
# Select: build/mokocassiopeia-src-{version}.zip
```
### Pre-Release Checklist
- [ ] All code changes merged to main
- [ ] Version numbers updated:
- [ ] `src/templateDetails.xml`
- [ ] `CHANGELOG.md`
- [ ] CHANGELOG.md updated with release notes
- [ ] Tests passing
- [ ] Documentation updated
- [ ] Local build tested in Joomla
---
## Troubleshooting
### Build Fails
**Problem**: `rsync: command not found`
```bash
# Ubuntu/Debian
sudo apt-get install rsync
# macOS
brew install rsync
```
**Problem**: `zip: command not found`
```bash
# Ubuntu/Debian
sudo apt-get install zip
# macOS (usually pre-installed)
brew install zip
```
### GitHub Actions Fails
**Problem**: Release workflow fails on tag push
Check:
1. Tag format matches pattern: `[0-9][0-9].[0-9][0-9].[0-9][0-9]`
2. Repository has write permissions for GITHUB_TOKEN
3. `src/` and `src/media/` directories exist
**Problem**: auto-update-sha fails
Check:
1. Release package was published successfully
2. Workflow has write permissions
3. Package naming matches expected format
---
## File Structure
```
scripts/
├── README.md # This file
└── build-release.sh # Local build script
.github/workflows/
├── release.yml # Automated release workflow
└── auto-update-sha.yml # SHA hash update workflow
```
---
## Contributing
When adding new scripts:
1. Add GPL-3.0-or-later license header
2. Include FILE INFORMATION block
3. Add usage documentation in this README
4. Make scripts executable: `chmod +x scripts/your-script.sh`
5. Test thoroughly before committing
---
## Support
- **Issues**: [GitHub Issues](https://github.com/mokoconsulting-tech/MokoCassiopeia/issues)
- **Email**: hello@mokoconsulting.tech
- **Documentation**: [docs/](../docs/)
---
## License
All scripts are licensed under GPL-3.0-or-later.
Copyright (C) 2026 Moko Consulting

View File

@@ -1,132 +0,0 @@
#!/usr/bin/env bash
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
# SPDX-License-Identifier: GPL-3.0-or-later
#
# FILE INFORMATION
# DEFGROUP: Build.Scripts
# INGROUP: MokoCassiopeia.Build
# REPO: https://github.com/mokoconsulting-tech/MokoCassiopeia
# PATH: /scripts/build-release.sh
# VERSION: 01.00.00
# BRIEF: Build release package for MokoCassiopeia template
# USAGE: ./scripts/build-release.sh [version]
set -e
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Script directory
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
# Functions
log_info() {
echo -e "${BLUE}[INFO]${NC} $1"
}
log_success() {
echo -e "${GREEN}[SUCCESS]${NC} $1"
}
log_warning() {
echo -e "${YELLOW}[WARNING]${NC} $1"
}
log_error() {
echo -e "${RED}[ERROR]${NC} $1"
}
# Check if version is provided
if [ -z "$1" ]; then
# Try to extract version from templateDetails.xml
if [ -f "${PROJECT_ROOT}/src/templateDetails.xml" ]; then
VERSION=$(grep -oP '<version>\K[^<]+' "${PROJECT_ROOT}/src/templateDetails.xml" | head -1)
log_info "Detected version: ${VERSION}"
else
log_error "Please provide version as argument: ./build-release.sh 03.08.03"
exit 1
fi
else
VERSION="$1"
fi
log_info "Building MokoCassiopeia release package"
log_info "Version: ${VERSION}"
# Change to project root
cd "${PROJECT_ROOT}"
# Create build directory
BUILD_DIR="${PROJECT_ROOT}/build"
PACKAGE_DIR="${BUILD_DIR}/package"
rm -rf "${BUILD_DIR}"
mkdir -p "${PACKAGE_DIR}"
log_info "Creating package structure..."
# Copy template files from src (excluding media directory)
if [ -d "src" ]; then
rsync -av --exclude='.git*' --exclude='media' src/ "${PACKAGE_DIR}/"
else
log_error "src directory not found!"
exit 1
fi
# Copy media files from src/media
if [ -d "src/media" ]; then
mkdir -p "${PACKAGE_DIR}/media"
rsync -av --exclude='.git*' src/media/ "${PACKAGE_DIR}/media/"
else
log_warning "src/media directory not found, skipping media files"
fi
log_info "Package structure created"
# Create ZIP package
cd "${PACKAGE_DIR}"
ZIP_NAME="mokocassiopeia-src-${VERSION}.zip"
log_info "Creating ZIP package: ${ZIP_NAME}"
zip -r "../${ZIP_NAME}" . -q
if [ $? -ne 0 ]; then
log_error "Failed to create ZIP package"
exit 1
fi
cd "${BUILD_DIR}"
log_success "Created: ${ZIP_NAME}"
# Generate checksums
log_info "Generating checksums..."
sha256sum "${ZIP_NAME}" > "${ZIP_NAME}.sha256"
md5sum "${ZIP_NAME}" > "${ZIP_NAME}.md5"
# Extract just the hash
SHA256_HASH=$(sha256sum "${ZIP_NAME}" | cut -d' ' -f1)
log_success "SHA-256: ${SHA256_HASH}"
log_success "MD5: $(md5sum "${ZIP_NAME}" | cut -d' ' -f1)"
# Show file info
FILE_SIZE=$(du -h "${ZIP_NAME}" | cut -f1)
log_info "Package size: ${FILE_SIZE}"
# Summary
echo ""
log_success "Build completed successfully!"
echo ""
echo "Package: ${BUILD_DIR}/${ZIP_NAME}"
echo "SHA-256: ${BUILD_DIR}/${ZIP_NAME}.sha256"
echo "MD5: ${BUILD_DIR}/${ZIP_NAME}.md5"
echo ""
echo "Next steps:"
echo "1. Test the package installation in Joomla"
echo "2. Create a GitHub release with this package"
echo "3. Update updates.xml with the new version and SHA-256 hash"
echo ""

View File

@@ -0,0 +1,73 @@
<?php
/* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
*
* This file is part of a Moko Consulting project.
*
* SPDX-License-Identifier: GPL-3.0-or-later
*
* # FILE INFORMATION
* DEFGROUP: Joomla.Template.Site
* INGROUP: MokoCassiopeia
* PATH: scripts/download-google-fonts.php
* VERSION: 03.09.05
* BRIEF: Download Google Fonts (woff2) for local self-hosting
*/
$fontsDir = __DIR__ . '/../src/media/fonts';
if (!is_dir($fontsDir)) {
fwrite(STDERR, "Error: Fonts directory not found: {$fontsDir}\n");
exit(1);
}
$fonts = [
'Roboto' => 'https://fonts.googleapis.com/css2?family=Roboto:wght@100;300;400;700&display=swap',
'Noto Sans' => 'https://fonts.googleapis.com/css2?family=Noto+Sans:wght@100;300;400;700&display=swap',
'Fira Sans' => 'https://fonts.googleapis.com/css2?family=Fira+Sans:wght@100;300;400;700&display=swap',
];
$userAgent = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36';
echo "Google Fonts Downloader for MokoCassiopeia\n";
echo str_repeat('=', 48) . "\n";
echo "Target: {$fontsDir}\n\n";
foreach ($fonts as $name => $url) {
echo "Downloading {$name}...\n";
$ctx = stream_context_create(['http' => ['header' => "User-Agent: {$userAgent}\r\n"]]);
$css = @file_get_contents($url, false, $ctx);
if ($css === false) {
fwrite(STDERR, " FAIL: could not fetch CSS for {$name}\n");
continue;
}
preg_match_all('#https://fonts\.gstatic\.com[^)]*\.woff2#', $css, $matches);
if (empty($matches[0])) {
fwrite(STDERR, " FAIL: no woff2 URLs found for {$name}\n");
continue;
}
$count = 0;
foreach ($matches[0] as $fontUrl) {
$filename = basename($fontUrl);
$dest = $fontsDir . '/' . $filename;
$data = @file_get_contents($fontUrl, false, $ctx);
if ($data === false) {
fwrite(STDERR, " FAIL: {$filename}\n");
continue;
}
file_put_contents($dest, $data);
$size = round(strlen($data) / 1024, 1);
echo " OK: {$filename} ({$size} KB)\n";
$count++;
}
echo " {$count} file(s) downloaded\n\n";
}
echo "Done.\n";

View File

@@ -1,96 +0,0 @@
#!/bin/bash
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
# SPDX-License-Identifier: GPL-3.0-or-later
#
# Download Google Fonts for self-hosting
# This script downloads Roboto, Noto Sans, and Fira Sans fonts
set -e
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Target directory
FONTS_DIR="../src/media/fonts"
echo -e "${BLUE}╔════════════════════════════════════════════════╗${NC}"
echo -e "${BLUE}║ Google Fonts Downloader for MokoCassiopeia ║${NC}"
echo -e "${BLUE}╚════════════════════════════════════════════════╝${NC}"
echo ""
# Check if fonts directory exists
if [ ! -d "$FONTS_DIR" ]; then
echo -e "${RED}✗ Error: Fonts directory not found: $FONTS_DIR${NC}"
exit 1
fi
cd "$FONTS_DIR"
echo -e "${YELLOW}Target directory: $(pwd)${NC}"
echo ""
# Function to download font CSS and extract font files
download_font() {
local font_name="$1"
local font_url="$2"
local display_name="$3"
echo -e "${GREEN}Downloading $display_name...${NC}"
# Download CSS with user agent for woff2 format
css=$(curl -s -A "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36" "$font_url")
if [ -z "$css" ]; then
echo -e "${RED} ✗ Failed to download CSS${NC}"
return 1
fi
# Extract woff2 URLs
urls=$(echo "$css" | grep -oP 'https://fonts\.gstatic\.com[^\)]*\.woff2' || true)
if [ -z "$urls" ]; then
echo -e "${RED} ✗ No font URLs found in CSS${NC}"
return 1
fi
count=0
while IFS= read -r url; do
if [ -z "$url" ]; then
continue
fi
filename=$(basename "$url")
echo -e " → Downloading ${filename}..."
if curl -s "$url" -o "$filename"; then
size=$(du -h "$filename" | cut -f1)
echo -e "${GREEN} ✓ Downloaded ($size)${NC}"
((count++))
else
echo -e "${RED} ✗ Failed${NC}"
fi
done <<< "$urls"
echo -e "${GREEN} ✓ Downloaded $count font files${NC}"
echo ""
}
# Download fonts
download_font "roboto" "https://fonts.googleapis.com/css2?family=Roboto:wght@100;300;400;700&display=swap" "Roboto"
download_font "noto-sans" "https://fonts.googleapis.com/css2?family=Noto+Sans:wght@100;300;400;700&display=swap" "Noto Sans"
download_font "fira-sans" "https://fonts.googleapis.com/css2?family=Fira+Sans:wght@100;300;400;700&display=swap" "Fira Sans"
echo -e "${GREEN}╔════════════════════════════════════════════╗${NC}"
echo -e "${GREEN}║ ✓ All fonts downloaded successfully! ║${NC}"
echo -e "${GREEN}╚════════════════════════════════════════════╝${NC}"
echo ""
echo -e "Font files saved to: ${BLUE}$(pwd)${NC}"
echo ""
echo "Next steps:"
echo "1. Verify font files are present"
echo "2. Update templateDetails.xml font options (if needed)"
echo "3. Remove Google Fonts CDN preconnect links from PHP templates"

View File

@@ -1,188 +0,0 @@
#!/usr/bin/env node
/* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
*
* This file is part of a Moko Consulting project.
*
* SPDX-License-Identifier: GPL-3.0-or-later
*
* # FILE INFORMATION
* DEFGROUP: Joomla.Template.Site
* INGROUP: MokoCassiopeia
* REPO: https://github.com/mokoconsulting-tech/MokoCassiopeia
* PATH: ./scripts/minify.js
* VERSION: 03.09.03
* BRIEF: Generates .min.css and .min.js files from the Joomla asset manifest
*/
'use strict';
const fs = require('fs');
const path = require('path');
const CleanCSS = require('clean-css');
const { minify: terserMinify } = require('terser');
// ---------------------------------------------------------------------------
// Config
// ---------------------------------------------------------------------------
const ROOT = path.resolve(__dirname, '..');
const SRC_MEDIA = path.join(ROOT, 'src', 'media');
const ASSET_JSON = path.join(ROOT, 'src', 'joomla.asset.json');
// URI prefix used in the manifest — maps to SRC_MEDIA on disk.
// e.g. "media/templates/site/mokocassiopeia/css/template.css"
const URI_PREFIX = 'media/templates/site/mokocassiopeia/';
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
/**
* Resolve a manifest URI to an absolute disk path under src/media/.
*
* @param {string} uri e.g. "media/templates/site/mokocassiopeia/css/foo.css"
* @returns {string|null}
*/
function uriToPath(uri) {
if (!uri.startsWith(URI_PREFIX)) return null;
return path.join(SRC_MEDIA, uri.slice(URI_PREFIX.length));
}
/**
* Return true if the filename looks like an already-minified file or belongs
* to a vendor bundle we don't own.
*/
function isVendorOrUserFile(filePath) {
const rel = filePath.replace(SRC_MEDIA + path.sep, '');
return rel.startsWith('vendor' + path.sep)
|| path.basename(filePath).startsWith('user.');
}
// ---------------------------------------------------------------------------
// Pair detection
// ---------------------------------------------------------------------------
/**
* Read the asset manifest and return an array of { src, dest, type } pairs
* where dest is a minified version of src that doesn't already exist or is
* older than src.
*
* Pairing logic: for every non-.min asset, check whether the manifest also
* contains a corresponding .min asset. If so, that's our pair.
*/
function detectPairs(assets) {
// Build a lookup of all URIs in the manifest.
const uriSet = new Set(assets.map(a => a.uri));
const pairs = [];
for (const asset of assets) {
const { uri, type } = asset;
if (type !== 'style' && type !== 'script') continue;
// Skip already-minified entries.
if (/\.min\.(css|js)$/.test(uri)) continue;
// Derive the expected .min URI.
const minUri = uri.replace(/\.(css|js)$/, '.min.$1');
if (!uriSet.has(minUri)) continue;
const srcPath = uriToPath(uri);
const destPath = uriToPath(minUri);
if (!srcPath || !destPath) continue;
if (isVendorOrUserFile(srcPath)) continue;
if (!fs.existsSync(srcPath)) {
console.warn(` [skip] source missing: ${srcPath}`);
continue;
}
pairs.push({ src: srcPath, dest: destPath, type });
}
return pairs;
}
// ---------------------------------------------------------------------------
// Minifiers
// ---------------------------------------------------------------------------
async function minifyCSS(srcPath, destPath) {
const source = fs.readFileSync(srcPath, 'utf8');
const result = new CleanCSS({ level: 2, returnPromise: true });
const output = await result.minify(source);
if (output.errors && output.errors.length) {
throw new Error(output.errors.join('\n'));
}
fs.mkdirSync(path.dirname(destPath), { recursive: true });
fs.writeFileSync(destPath, output.styles, 'utf8');
const srcSize = Buffer.byteLength(source, 'utf8');
const destSize = Buffer.byteLength(output.styles, 'utf8');
const saving = (100 - (destSize / srcSize * 100)).toFixed(1);
return { srcSize, destSize, saving };
}
async function minifyJS(srcPath, destPath) {
const source = fs.readFileSync(srcPath, 'utf8');
const result = await terserMinify(source, {
compress: { drop_console: false },
mangle: true,
format: { comments: false }
});
if (!result.code) throw new Error('terser returned no output');
fs.mkdirSync(path.dirname(destPath), { recursive: true });
fs.writeFileSync(destPath, result.code, 'utf8');
const srcSize = Buffer.byteLength(source, 'utf8');
const destSize = Buffer.byteLength(result.code, 'utf8');
const saving = (100 - (destSize / srcSize * 100)).toFixed(1);
return { srcSize, destSize, saving };
}
// ---------------------------------------------------------------------------
// Main
// ---------------------------------------------------------------------------
(async () => {
const manifest = JSON.parse(fs.readFileSync(ASSET_JSON, 'utf8'));
const pairs = detectPairs(manifest.assets);
if (pairs.length === 0) {
console.log('No pairs found — nothing to minify.');
return;
}
console.log(`\nMinifying ${pairs.length} file(s)...\n`);
let ok = 0, fail = 0;
for (const { src, dest, type } of pairs) {
const label = path.relative(ROOT, src);
process.stdout.write(` ${label} ... `);
try {
const stats = type === 'style'
? await minifyCSS(src, dest)
: await minifyJS(src, dest);
const kb = n => (n / 1024).toFixed(1) + ' kB';
console.log(`${kb(stats.srcSize)}${kb(stats.destSize)} (${stats.saving}% saved)`);
ok++;
} catch (err) {
console.error(`FAILED\n ${err.message}`);
fail++;
}
}
console.log(`\nDone. ${ok} succeeded, ${fail} failed.\n`);
if (fail > 0) process.exit(1);
})();

View File

@@ -448,7 +448,10 @@ $wa->useScript('user.js'); // js/user.js
</nav>
<?php endif; ?>
<?php if ($this->countModules('search', true)) : ?>
<div class="container-search">
<button class="search-toggler d-lg-none" type="button" data-bs-toggle="collapse" data-bs-target="#headerSearchCollapse" aria-controls="headerSearchCollapse" aria-expanded="false" aria-label="<?php echo Text::_('JSEARCH_FILTER_SUBMIT'); ?>">
<span class="fa-solid fa-magnifying-glass" aria-hidden="true"></span>
</button>
<div class="container-search collapse d-lg-block" id="headerSearchCollapse">
<jdoc:include type="modules" name="search" style="none" />
</div>
<?php endif; ?>

View File

@@ -14477,13 +14477,20 @@ li.current a {
min-width: 12rem;
}
.container-header .navbar-toggler {
.container-header .navbar-toggler,
.container-header .search-toggler {
color: var(--mainmenu-nav-link-color, #fff);
border-color: var(--mainmenu-nav-link-color, #fff);
font-size: 1.25rem;
cursor: pointer;
}
.container-header .search-toggler {
background: none;
border: none;
padding: 0.25rem 0.5rem;
}
.container-header .container-search {
margin-top: 0.75em;
}
@@ -18665,6 +18672,10 @@ nav[data-toggle=toc] .nav-link.active+ul{
margin-top: 0.5rem;
}
.container-header .container-search.collapse:not(.show) {
display: none;
}
.mod-finder__search.input-group {
max-width: 100%;
}
@@ -18683,8 +18694,8 @@ nav[data-toggle=toc] .nav-link.active+ul{
}
.container-header .container-search {
flex: 0 0 25%;
max-width: 25%;
flex: 0 0 15%;
max-width: 15%;
margin-top: 0;
}
}

View File

@@ -36,7 +36,7 @@
</server>
</updateservers>
<name>MokoCassiopeia</name>
<version>03.09.04</version>
<version>03.09.07</version>
<scriptfile>script.php</scriptfile>
<creationDate>2026-03-26</creationDate>
<author>Jonathan Miller || Moko Consulting</author>

View File

@@ -1,22 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<updates>
<update>
<name>MokoCassiopeia</name>
<description>Moko Consulting site template based on Cassiopeia.</description>
<element>mokocassiopeia</element>
<type>template</type>
<version>03.09.04</version>
<client>site</client>
<tags>
<tag>stable</tag>
</tags>
<infourl title="MokoCassiopeia">https://github.com/mokoconsulting-tech/MokoCassiopeia/releases/tag/v03</infourl>
<downloads>
<downloadurl type="full" format="zip">https://github.com/mokoconsulting-tech/MokoCassiopeia/releases/download/v03/mokocassiopeia-03.09.04.zip</downloadurl>
</downloads>
<sha256>da85bf5a34cafadbae26df199ed36c04438e7dab440d046e0e4117d25510feaf</sha256>
<targetplatform name="joomla" version="(5|6)\..*" />
<maintainer>Moko Consulting</maintainer>
<maintainerurl>https://mokoconsulting.tech</maintainerurl>
</update>
</updates>

View File

@@ -10,11 +10,15 @@
INGROUP: MokoCassiopeia
REPO: https://github.com/mokoconsulting-tech/MokoCassiopeia
PATH: ./updates.xml
VERSION: 03.09.04
BRIEF: Update manifest XML file for MokoCassiopeia
VERSION: 03.09.05
BRIEF: Update manifest XML — stable, rc, and development channels
-->
<updates>
<!-- ══════════════════════════════════════════════════════════════
STABLE — production releases from main branch
══════════════════════════════════════════════════════════════ -->
<update>
<name>MokoCassiopeia</name>
<description>Moko Consulting's site template based on Cassiopeia.</description>
@@ -44,4 +48,137 @@
<targetplatform name='joomla' version='(5|6)\..*'/>
</update>
<!-- ══════════════════════════════════════════════════════════════
RC — release candidates for testing and validation
══════════════════════════════════════════════════════════════ -->
<update>
<name>MokoCassiopeia</name>
<description>MokoCassiopeia release candidate — testing only.</description>
<element>mokocassiopeia</element>
<type>template</type>
<client>site</client>
<version>03.09.06</version>
<creationDate>2026-04-07</creationDate>
<author>Jonathan Miller || Moko Consulting</author>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
<copyright>(C)GNU General Public License Version 3 - 2026 Moko Consulting</copyright>
<infourl title='MokoCassiopeia RC'>https://github.com/mokoconsulting-tech/MokoCassiopeia/releases/tag/release-candidate</infourl>
<downloads>
<downloadurl type='full' format='zip'>https://github.com/mokoconsulting-tech/MokoCassiopeia/releases/download/release-candidate/mokocassiopeia-rc.zip</downloadurl>
</downloads>
<sha256>99b868a54466138d22a544d3e489559e7303960922c8eb3a142c504225b0e647</sha256>
<tags>
<tag>rc</tag>
</tags>
<maintainer>Moko Consulting</maintainer>
<maintainerurl>https://mokoconsulting.tech</maintainerurl>
<targetplatform name='joomla' version='(5|6)\..*'/>
</update>
<!-- ══════════════════════════════════════════════════════════════
DEVELOPMENT — latest dev branch builds, not for production
══════════════════════════════════════════════════════════════ -->
<update>
<name>MokoCassiopeia</name>
<description>MokoCassiopeia development build — unstable.</description>
<element>mokocassiopeia</element>
<type>template</type>
<client>site</client>
<version>03.09.07</version>
<creationDate>2026-04-07</creationDate>
<author>Jonathan Miller || Moko Consulting</author>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
<copyright>(C)GNU General Public License Version 3 - 2026 Moko Consulting</copyright>
<infourl title='MokoCassiopeia Dev'>https://github.com/mokoconsulting-tech/MokoCassiopeia/releases/tag/development</infourl>
<downloads>
<downloadurl type='full' format='zip'>https://github.com/mokoconsulting-tech/MokoCassiopeia/releases/download/development/mokocassiopeia-dev.zip</downloadurl>
</downloads>
<sha256>b6403918c5f65f4f52b889fbe4bd217c903f0d9c2fcf90f8eb59df1e7b4b7333</sha256>
<tags>
<tag>development</tag>
</tags>
<maintainer>Moko Consulting</maintainer>
<maintainerurl>https://mokoconsulting.tech</maintainerurl>
<targetplatform name='joomla' version='(5|6)\..*'/>
</update>
<!-- ══════════════════════════════════════════════════════════════
ALPHA — early testing, expect breaking changes
══════════════════════════════════════════════════════════════ -->
<update>
<name>MokoCassiopeia</name>
<description>MokoCassiopeia alpha build — early testing.</description>
<element>mokocassiopeia</element>
<type>template</type>
<client>site</client>
<version>03.09.07</version>
<creationDate>2026-04-07</creationDate>
<author>Jonathan Miller || Moko Consulting</author>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
<copyright>(C)GNU General Public License Version 3 - 2026 Moko Consulting</copyright>
<infourl title='MokoCassiopeia Alpha'>https://github.com/mokoconsulting-tech/MokoCassiopeia/releases/tag/alpha</infourl>
<downloads>
<downloadurl type='full' format='zip'>https://github.com/mokoconsulting-tech/MokoCassiopeia/releases/download/alpha/mokocassiopeia-alpha.zip</downloadurl>
</downloads>
<sha256>b5f6d073d126f0041ddd475e61bc4eaa050e060e9b51f4fe0e8a348f188c40b2</sha256>
<tags>
<tag>alpha</tag>
</tags>
<maintainer>Moko Consulting</maintainer>
<maintainerurl>https://mokoconsulting.tech</maintainerurl>
<targetplatform name='joomla' version='(5|6)\..*'/>
</update>
<!-- ══════════════════════════════════════════════════════════════
BETA — feature complete, testing for stability
══════════════════════════════════════════════════════════════ -->
<update>
<name>MokoCassiopeia</name>
<description>MokoCassiopeia beta build — feature complete, stability testing.</description>
<element>mokocassiopeia</element>
<type>template</type>
<client>site</client>
<version>03.09.07</version>
<creationDate>2026-04-07</creationDate>
<author>Jonathan Miller || Moko Consulting</author>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
<copyright>(C)GNU General Public License Version 3 - 2026 Moko Consulting</copyright>
<infourl title='MokoCassiopeia Beta'>https://github.com/mokoconsulting-tech/MokoCassiopeia/releases/tag/beta</infourl>
<downloads>
<downloadurl type='full' format='zip'>https://github.com/mokoconsulting-tech/MokoCassiopeia/releases/download/beta/mokocassiopeia-beta.zip</downloadurl>
</downloads>
<sha256>b5f6d073d126f0041ddd475e61bc4eaa050e060e9b51f4fe0e8a348f188c40b2</sha256>
<tags>
<tag>beta</tag>
</tags>
<maintainer>Moko Consulting</maintainer>
<maintainerurl>https://mokoconsulting.tech</maintainerurl>
<targetplatform name='joomla' version='(5|6)\..*'/>
</update>
</updates>