Compare commits

..

49 Commits

Author SHA1 Message Date
gitea-actions[bot] 354eef7011 chore(release): build 01.08.00 [skip ci]
Branch Cleanup / Delete merged branch (pull_request) Has been skipped
RC Revert / Rename rc/ back to dev/ (pull_request) Has been skipped
2026-07-13 09:48:33 +00:00
jmiller 529e5ece3e Merge pull request 'Release: dev -> main' (#127) from dev into main
Sync Workflows to Repos / sync (push) Has been skipped
Cascade Main -> Dev / Cascade main -> dev (push) Successful in 3s
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Universal: PR Check / Require Docs Update (pull_request) Has been skipped
Universal: PR Check / Wiki Update Reminder (pull_request) Has been skipped
Generic: Project CI / Lint & Validate (pull_request) Successful in 12s
Generic: Project CI / Tests (pull_request) Failing after 8s
Universal: PR Check / Secret Scan (pull_request) Successful in 5s
Joomla: Metadata Validation / Validate Joomla Metadata (pull_request) Failing after 8s
Universal: PR Check / Validate PR (pull_request) Successful in 13s
Universal: PR Check / Build RC Package (pull_request) Has been cancelled
Universal: PR Check / Report Issues (pull_request) Has been cancelled
2026-07-13 09:37:51 +00:00
mokogitea-actions[bot] b87d6bca0e chore(version): pre-release bump to 01.07.19-dev [skip ci]
Branch Cleanup / Delete merged branch (pull_request) Has been skipped
RC Revert / Rename rc/ back to dev/ (pull_request) Has been skipped
Universal: Workflow Sync Trigger / Sync workflows to live repos (pull_request) Has been skipped
Universal: Build & Release / Promote to RC (pull_request) Has been skipped
Universal: Build & Release / Build & Release Pipeline (pull_request) Successful in 20s
2026-07-13 09:36:29 +00:00
mokogitea-actions[bot] 69e3fe4996 chore(version): pre-release bump to 01.07.18-dev [skip ci] 2026-07-13 09:36:07 +00:00
jmiller bbbffb79ea chore(sync): CI notify retry + JEXEC whole-file scan from Template-Generic
Universal: Auto Version Bump / Version Bump (push) Successful in 9s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 12s
2026-07-13 09:31:48 +00:00
jmiller 39d8d6c6ed chore(sync): CI notify retry + JEXEC whole-file scan from Template-Generic
Universal: Auto Version Bump / Version Bump (push) Successful in 7s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 11s
2026-07-13 09:31:47 +00:00
jmiller 15ab2b41ae chore: sync pr-check.yml from Template-Generic [skip ci] 2026-07-13 09:28:36 +00:00
jmiller c0667e8728 chore: sync notify.yml from Template-Generic [skip ci] 2026-07-13 09:28:35 +00:00
mokogitea-actions[bot] 9602930d8c chore(version): pre-release bump to 01.07.17-dev [skip ci] 2026-07-13 02:24:01 +00:00
jmiller d024ea7ec1 Merge pull request 'chore: align to Joomla 6 / PHP 8.3' (#126) from fix/joomla6-php83 into dev
Universal: Auto Version Bump / Version Bump (push) Has been skipped
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 10s
2026-07-13 02:23:45 +00:00
mokogitea-actions[bot] c531383a33 chore(version): pre-release bump to 01.07.16-dev [skip ci]
RC Revert / Rename rc/ back to dev/ (pull_request) Has been skipped
Branch Cleanup / Delete merged branch (pull_request) Successful in 2s
2026-07-13 01:44:49 +00:00
mokogitea-actions[bot] 003e9ad564 chore(version): pre-release bump to 01.07.15-dev [skip ci] 2026-07-13 01:44:26 +00:00
mokogitea-actions[bot] 91f1869fb7 chore(version): pre-release bump to 01.07.14-dev [skip ci] 2026-07-13 01:43:47 +00:00
jmiller 647eeb71a7 chore: align composer.json to Joomla 6 / PHP 8.3
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 13s
Generic: Project CI / Tests (pull_request) Failing after 8s
Generic: Project CI / Lint & Validate (pull_request) Successful in 10s
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Universal: PR Check / Require Docs Update (pull_request) Has been skipped
Universal: PR Check / Wiki Update Reminder (pull_request) Has been skipped
Universal: PR Check / Secret Scan (pull_request) Successful in 5s
Universal: PR Check / Validate PR (pull_request) Successful in 10s
Joomla: Metadata Validation / Validate Joomla Metadata (pull_request) Failing after 8s
Universal: PR Check / Build RC Package (pull_request) Has been cancelled
Universal: PR Check / Report Issues (pull_request) Has been cancelled
2026-07-13 01:42:37 +00:00
jmiller 1575454aae chore: align .mokogitea/CLAUDE.md to Joomla 6 / PHP 8.3
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 17s
2026-07-13 01:42:36 +00:00
jmiller fb1e37878b chore: align README.md to Joomla 6 / PHP 8.3
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 12s
2026-07-13 01:42:36 +00:00
mokogitea-actions[bot] d2a1c1a60c chore(version): pre-release bump to 01.07.13-dev [skip ci] 2026-07-12 21:40:49 +00:00
jmiller 37dc6908b3 Merge pull request 'docs(changelog): note admin menu short-name constant' (#125) from jmiller/changelog-short-constant into dev
Universal: Auto Version Bump / Version Bump (push) Has been skipped
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 10s
2026-07-12 21:39:22 +00:00
jmiller 22dee1464f docs(changelog): note admin menu short-name constant (#119)
Generic: Project CI / Lint & Validate (pull_request) Has been cancelled
Generic: Project CI / Tests (pull_request) Has been cancelled
Universal: PR Check / Branch Policy (pull_request) Has been cancelled
Universal: PR Check / Require Docs Update (pull_request) Has been cancelled
Universal: PR Check / Wiki Update Reminder (pull_request) Has been cancelled
Universal: PR Check / Secret Scan (pull_request) Has been cancelled
Universal: PR Check / Validate PR (pull_request) Has been cancelled
Joomla: Metadata Validation / Validate Joomla Metadata (pull_request) Has been cancelled
Branch Cleanup / Delete merged branch (pull_request) Has been cancelled
RC Revert / Rename rc/ back to dev/ (pull_request) Has been cancelled
Universal: PR Check / Build RC Package (pull_request) Has been cancelled
Universal: PR Check / Report Issues (pull_request) Has been cancelled
Claude-Session: https://claude.ai/code/session_01B9aZHSWbiiZykJD88pYx8R
2026-07-12 16:35:08 -05:00
mokogitea-actions[bot] ddfec7b8c2 chore(version): pre-release bump to 01.07.12-dev [skip ci] 2026-07-09 23:23:34 +00:00
jmiller 50d56b2bf6 Merge pull request 'chore(sync): cascade main -> dev (conflict-resolved)' (#124) from chore/cascade-main-to-dev into dev
Universal: Auto Version Bump / Version Bump (push) Has been skipped
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 12s
2026-07-09 23:23:17 +00:00
jmiller 419e333f20 Merge remote-tracking branch 'origin/main' into chore/cascade-main-to-dev
Universal: PR Check / Require Docs Update (pull_request) Has been skipped
Universal: PR Check / Wiki Update Reminder (pull_request) Has been skipped
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Universal: PR Check / Secret Scan (pull_request) Successful in 7s
RC Revert / Rename rc/ back to dev/ (pull_request) Has been skipped
Branch Cleanup / Delete merged branch (pull_request) Successful in 2s
Generic: Project CI / Lint & Validate (pull_request) Successful in 11s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Failing after 5s
Generic: Project CI / Tests (pull_request) Failing after 26s
Universal: PR Check / Validate PR (pull_request) Successful in 32s
Joomla: Metadata Validation / Validate Joomla Metadata (pull_request) Failing after 32s
Universal: PR Check / Build RC Package (pull_request) Has been cancelled
Universal: PR Check / Report Issues (pull_request) Has been cancelled
# Conflicts:
#	.mokogitea/workflows/issue-branch.yml
#	CHANGELOG.md
#	CODE_OF_CONDUCT.md
#	GOVERNANCE.md
#	README.md
#	SECURITY.md
#	source/packages/com_mokoog/mokoog.xml
#	source/packages/plg_content_mokoog/mokoog.xml
#	source/packages/plg_system_mokoog/mokoog.xml
#	source/packages/plg_webservices_mokoog/mokoog.xml
#	source/pkg_mokoog.xml
2026-07-09 18:22:49 -05:00
gitea-actions[bot] b6e9afacd8 chore(version): pre-release bump to 01.07.11-dev [skip ci] 2026-07-09 23:11:21 +00:00
jmiller 1e054c9864 Merge pull request 'test: unit tests for pure JsonLdBuilder methods (#33 partial)' (#113) from test/jsonld-unit-33 into dev
Universal: Auto Version Bump / Version Bump (push) Has been skipped
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 10s
2026-07-09 23:11:11 +00:00
gitea-actions[bot] eac29f3f23 chore(version): pre-release bump to 01.07.10-dev [skip ci] 2026-07-07 10:16:42 +00:00
jmiller 89830842a8 Merge pull request 'feat(menu): admin menu short-name constant COM_MOKOOG_SHORT (#119)' (#123) from fix/short-menu-constant into dev
Universal: Auto Version Bump / Version Bump (push) Has been skipped
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 10s
2026-07-07 10:15:23 +00:00
gitea-actions[bot] a2ba10c562 chore(version): pre-release bump to 01.07.09-dev [skip ci]
RC Revert / Rename rc/ back to dev/ (pull_request) Has been skipped
Branch Cleanup / Delete merged branch (pull_request) Successful in 1s
2026-07-06 22:41:46 +00:00
gitea-actions[bot] e94a950cb0 chore(version): pre-release bump to 01.07.08-dev [skip ci] 2026-07-06 22:40:10 +00:00
gitea-actions[bot] c64c503f5d chore(version): pre-release bump to 01.07.07-dev [skip ci] 2026-07-06 22:39:54 +00:00
jmiller 2b56dc411a feat(menu): use COM_MOKOOG_SHORT for admin menu label (#119)
Universal: PR Check / Branch Policy (pull_request) Successful in 3s
Universal: PR Check / Validate PR (pull_request) Failing after 6s
Generic: Project CI / Lint & Validate (pull_request) Successful in 16s
Universal: PR Check / Secret Scan (pull_request) Successful in 7s
Joomla: Metadata Validation / Validate Joomla Metadata (pull_request) Failing after 10s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 23s
Generic: Project CI / Tests (pull_request) Has been cancelled
Universal: PR Check / Build RC Package (pull_request) Has been cancelled
Universal: PR Check / Report Issues (pull_request) Has been cancelled
Claude-Session: https://claude.ai/code/session_01B9aZHSWbiiZykJD88pYx8R
2026-07-06 22:37:44 +00:00
jmiller 6ef67d9ca7 feat(menu): add COM_MOKOOG_SHORT short-name constant (#119)
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 13s
Claude-Session: https://claude.ai/code/session_01B9aZHSWbiiZykJD88pYx8R
2026-07-06 22:37:10 +00:00
jmiller 79cd2fd9e6 chore: sync pre-release.yml from Template-Generic [skip ci] 2026-07-06 17:04:32 +00:00
jmiller cf744a8bd2 Merge pull request 'fix(installer): gate update() + enqueueMessage for success (og-com tidy)' (#122) from fix/og-installer-tidy into dev
Universal: Auto Version Bump / Version Bump (push) Has been skipped
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 23s
2026-07-06 16:24:41 +00:00
jmiller dbaeb33dc4 fix(installer): gate update() on schema + use enqueueMessage for success
Universal: PR Check / Branch Policy (pull_request) Successful in 2s
RC Revert / Rename rc/ back to dev/ (pull_request) Has been skipped
Branch Cleanup / Delete merged branch (pull_request) Failing after 1s
Universal: PR Check / Validate PR (pull_request) Failing after 5s
Universal: PR Check / Secret Scan (pull_request) Successful in 8s
Joomla: Metadata Validation / Validate Joomla Metadata (pull_request) Failing after 9s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Failing after 5s
Generic: Project CI / Lint & Validate (pull_request) Successful in 27s
Generic: Project CI / Tests (pull_request) Has been cancelled
Universal: PR Check / Build RC Package (pull_request) Has been cancelled
Universal: PR Check / Report Issues (pull_request) Has been cancelled
Mirrors the install() smart-check on update() and replaces echo with a proper
Joomla message for consistency.

Claude-Session: https://claude.ai/code/session_01MrxXW9iyuDda5yDZrjZMbF
2026-07-06 16:24:15 +00:00
jmiller fe38a422be Merge pull request 'fix(installer): gate install-success on real verification' (#120) from fix/installer-success-detection into dev
Universal: Auto Version Bump / Version Bump (push) Has been skipped
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Failing after 5s
2026-07-06 16:15:36 +00:00
gitea-actions[bot] a6fe8fb580 chore(version): pre-release bump to 01.07.05-dev [skip ci]
RC Revert / Rename rc/ back to dev/ (pull_request) Has been skipped
Branch Cleanup / Delete merged branch (pull_request) Successful in 1s
2026-07-06 03:55:02 +00:00
jmiller a6a24f72de fix(installer): gate install-success on real verification
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 22s
Joomla installers keep running after a failed sub-step, so a postflight
"installed successfully" / next-steps message can lie. Gate the success
path in both installer scripts.

Package (source/script.php): postflight now verifies every child extension
declared in the package manifest actually registered in #__extensions
(element + type, plus folder for plugins). If any are missing it enqueues
an error listing them and returns before the licence notice. Download-key
restore and plugin-enable housekeeping still run unconditionally. Check
fails open on any error.

Component (source/packages/com_mokoog/script.php): install() now parses the
installed SQL (JPATH_ADMINISTRATOR/components/com_mokoog/sql/install.mysql.sql)
for CREATE TABLE declarations and verifies each exists (prefix-substituted,
case-insensitive) before echoing the success message. Fails open if the SQL
is unreadable.

Claude-Session: https://claude.ai/code/session_01MrxXW9iyuDda5yDZrjZMbF
2026-07-05 22:54:38 -05:00
jmiller 8738b09d83 chore: sync pr-check.yml from Template-Generic [skip ci] 2026-07-06 03:50:17 +00:00
jmiller 0a82276565 chore: sync ci-generic.yml from Template-Generic [skip ci] 2026-07-06 03:50:14 +00:00
jmiller afc5ef797a chore: sync auto-release.yml from Template-Generic [skip ci] 2026-07-06 03:50:13 +00:00
jmiller d02334b0da Merge chore/normalize-docs: normalize doc/config refs
Cascade Main -> Dev / Cascade main -> dev (push) Successful in 8s
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Universal: PR Check / Require Docs Update (pull_request) Has been skipped
Universal: PR Check / Wiki Update Reminder (pull_request) Has been skipped
Universal: PR Check / Secret Scan (pull_request) Successful in 7s
Universal: PR Check / Validate PR (pull_request) Failing after 25s
Joomla: Metadata Validation / Validate Joomla Metadata (pull_request) Failing after 15s
Generic: Project CI / Lint & Validate (pull_request) Successful in 54s
Generic: Project CI / Tests (pull_request) Has been cancelled
Universal: PR Check / Build RC Package (pull_request) Has been cancelled
Universal: PR Check / Report Issues (pull_request) Has been cancelled
2026-07-05 22:52:10 +00:00
gitea-actions[bot] 84b3d9dc2c chore(version): pre-release bump to 01.07.03-dev [skip ci] 2026-06-29 17:26:56 +00:00
jmiller d6d93bddd9 Merge pull request 'refactor: consolidate ImageHelper resize paths + drop unused validate() (#104)' (#117) from fix/imagehelper-consolidate-104 into dev
Universal: Auto Version Bump / Version Bump (push) Has been skipped
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 9s
2026-06-29 17:26:45 +00:00
gitea-actions[bot] 3e6c8d685b chore(version): pre-release bump to 01.07.02-dev [skip ci]
Branch Cleanup / Delete merged branch (pull_request) Successful in 1s
RC Revert / Rename rc/ back to dev/ (pull_request) Has been skipped
2026-06-29 17:26:29 +00:00
jmiller 236baed80d refactor: consolidate ImageHelper resize paths, drop unused validate() (#104)
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 12s
- resize() is now a thin wrapper over resizeToSize() (added a $quality param),
  removing ~80 lines of duplicated crop/resample/encode logic. resize(path)
  and resizeToSize(path, 1200, 630, '') produce identical output paths/files,
  so behavior is unchanged.
- Removed the unused ImageHelper::validate() method (no callers).

Net: -130/+11 lines. Closes the remaining #104 consolidation items.
2026-06-29 12:26:13 -05:00
gitea-actions[bot] fd28cb8a93 chore(version): pre-release bump to 01.07.01-dev [skip ci] 2026-06-29 17:18:29 +00:00
jmiller cb0e5596ea Merge pull request 'refactor: sitemap throttle + SEF URLs (#100) and batch cursor pagination (#106)' (#116) from feat/sitemap-batch-redesign into dev
Universal: Auto Version Bump / Version Bump (push) Has been skipped
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 14s
2026-06-29 17:18:11 +00:00
jmiller 7532446e46 refactor: sitemap throttle + SEF URLs (#100) and batch cursor pagination (#106)
Generic: Project CI / Lint & Validate (pull_request) Successful in 13s
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Universal: PR Check / Secret Scan (pull_request) Successful in 6s
Universal: PR Check / Validate PR (pull_request) Failing after 5s
Branch Cleanup / Delete merged branch (pull_request) Failing after 1s
RC Revert / Rename rc/ back to dev/ (pull_request) Has been skipped
Joomla: Metadata Validation / Validate Joomla Metadata (pull_request) Failing after 10s
Generic: Project CI / Tests (pull_request) Has been cancelled
Universal: PR Check / Build RC Package (pull_request) Has been cancelled
Universal: PR Check / Report Issues (pull_request) Has been cancelled
#106 — BatchController now paginates by id cursor (WHERE c.id > :lastId)
instead of always querying offset 0. A row that fails to insert falls behind
the cursor and is not re-fetched, so the batch always terminates and reaches
100% even with persistent failures. process() returns examined + last_id; the
editor JS drives the cursor and stops when a chunk examines 0 rows.

#100 — Sitemap:
- Throttle: regenerate at most once per 60s on content save (SITEMAP_MIN_INTERVAL)
  so bulk edits/imports don't rebuild the whole file every save
- SEF URLs: route each article via Route::link('site', ...) with a fallback to
  the non-SEF index.php URL if routing fails (worst case = prior behavior)
(access-level filtering + atomic write were done earlier in the cycle)
2026-06-29 12:17:37 -05:00
jmiller 903d4d37c8 test: add unit tests for pure JsonLdBuilder methods (#33 partial)
Universal: PR Check / Branch Policy (pull_request) Successful in 2s
Universal: PR Check / Validate PR (pull_request) Failing after 4s
Universal: PR Check / Secret Scan (pull_request) Successful in 5s
Generic: Project CI / Lint & Validate (pull_request) Successful in 11s
Joomla: Metadata Validation / Validate Joomla Metadata (pull_request) Failing after 28s
Generic: Project CI / Tests (pull_request) Has been cancelled
Universal: PR Check / Build RC Package (pull_request) Has been cancelled
Universal: PR Check / Report Issues (pull_request) Has been cancelled
RC Revert / Rename rc/ back to dev/ (pull_request) Has been skipped
Branch Cleanup / Delete merged branch (pull_request) Successful in 1s
- JsonLdBuilderLocalBusinessTest: buildLocalBusiness() null/minimal/custom-type/
  partial-address/geo-requires-both-coords cases
- JsonLdScriptTagTest: toScriptTag() wraps in ld+json, escapes </ to prevent
  </script> breakout, and produces valid JSON after unescaping

Covers the pure (Factory/Uri/DB-free) helpers that run under the existing
minimal bootstrap. The remaining #33 targets (ImageHelper, BatchController,
CSV import, content/system plugin integration) need a Joomla test harness.
2026-06-29 10:52:55 -05:00
47 changed files with 481 additions and 186 deletions
+4 -4
View File
@@ -7,7 +7,7 @@ Open Graph, Twitter Card, and social sharing meta tag management for Joomla. Per
| Field | Value |
|---|---|
| **Package** | `pkg_mokoog` |
| **Language** | PHP 8.1+ |
| **Language** | PHP 8.3+ |
| **Branch** | develop on `dev`, merge to `main` (protected) |
| **Wiki** | [MokoSuiteOpenGraph Wiki](https://git.mokoconsulting.tech/MokoConsulting/MokoSuiteOpenGraph/wiki) |
@@ -28,7 +28,7 @@ Joomla **package** with three sub-extensions:
### com_mokoog (Component)
- Admin backend for viewing/managing all OG tag records
- Joomla 4/5 MVC: `Controller/DisplayController`, `Model/TagsModel`, `View/Tags/HtmlView`, `Table/TagTable`
- Joomla 6 MVC: `Controller/DisplayController`, `Model/TagsModel`, `View/Tags/HtmlView`, `Table/TagTable`
- Namespace: `Joomla\Component\MokoOG\Administrator`
### plg_system_mokoog (System Plugin)
@@ -58,8 +58,8 @@ Single table `#__mokoog_tags`:
## Coding Standards
- PHP 8.1+ minimum
- Joomla 4/5 DI container pattern: `services/provider.php` → Extension class
- PHP 8.3+ minimum
- Joomla 6 DI container pattern: `services/provider.php` → Extension class
- Legacy stub `.php` file required for plugin loader but empty
- `SubscriberInterface` for event subscription (not `on*` method naming)
- `bind() → check() → store()` for Table operations (not `save()`)
+1 -1
View File
@@ -382,7 +382,7 @@ jobs:
content = open('CHANGELOG.md').read()
old = '## [Unreleased]'
new = f'## [Unreleased]\n\n## [{version}] --- {date}'
content = content.replace(old, new, 1)
content = content if ('## [' + version + ']') in content else content.replace(old, new, 1)
open('CHANGELOG.md', 'w').write(content)
" "$VERSION" "$DATE"
git add CHANGELOG.md
+5 -4
View File
@@ -131,10 +131,11 @@ jobs:
test:
name: Tests
runs-on: ubuntu-latest
needs: lint
# Run only when lint succeeded; always() forces evaluation so a skipped
# lint (e.g. template repos) skips this job cleanly instead of hanging.
if: ${{ always() && needs.lint.result == 'success' }}
# Independent job (no `needs: lint`): the Gitea Actions scheduler does not
# offer the dependent 2nd job of a needs-chain to runners, so it stalls in
# "waiting" and is reaped by ABANDONED_JOB_TIMEOUT. Guard template repos
# directly (same condition lint uses) instead of gating on lint's result.
if: ${{ !startsWith(github.event.repository.name, 'Template-') }}
steps:
- name: Checkout
+1 -1
View File
@@ -5,7 +5,7 @@
# FILE INFORMATION
# DEFGROUP: MokoGitea.Workflow
# INGROUP: mokocli.Automation
# VERSION: 01.07.04
# VERSION: 01.08.00
# BRIEF: Auto-create feature branch when an issue is opened
name: "Universal: Issue Branch"
+4 -4
View File
@@ -46,13 +46,13 @@ jobs:
WORKFLOW="${{ github.event.workflow_run.name }}"
URL="${{ github.event.workflow_run.html_url }}"
curl -sS \
curl -sS --retry 3 --retry-connrefused --retry-delay 2 --max-time 20 \
-H "Title: ${REPO} released" \
-H "Tags: white_check_mark,package" \
-H "Priority: default" \
-H "Click: ${URL}" \
-d "${WORKFLOW} completed successfully." \
"${NTFY_URL}/${NTFY_TOPIC}"
"${NTFY_URL}/${NTFY_TOPIC}" || echo "::warning::ntfy notification could not be delivered (non-fatal)"
- name: Notify on failure
if: github.event.workflow_run.conclusion == 'failure'
@@ -61,10 +61,10 @@ jobs:
WORKFLOW="${{ github.event.workflow_run.name }}"
URL="${{ github.event.workflow_run.html_url }}"
curl -sS \
curl -sS --retry 3 --retry-connrefused --retry-delay 2 --max-time 20 \
-H "Title: ${REPO} workflow failed" \
-H "Tags: x,warning" \
-H "Priority: high" \
-H "Click: ${URL}" \
-d "${WORKFLOW} failed. Check the run for details." \
"${NTFY_URL}/${NTFY_TOPIC}"
"${NTFY_URL}/${NTFY_TOPIC}" || echo "::warning::ntfy notification could not be delivered (non-fatal)"
+4 -3
View File
@@ -210,7 +210,7 @@ jobs:
- name: Check for merge conflict markers
run: |
CONFLICTS=$(grep -rn '<<<<<<< \|>>>>>>> \|^=======$' --include='*.php' --include='*.xml' --include='*.css' --include='*.js' --include='*.json' --include='*.md' --include='*.yml' --include='*.yaml' --include='*.ini' --include='*.txt' . 2>/dev/null | grep -v '.git/' || true)
CONFLICTS=$(grep -rn '<<<<<<< \|>>>>>>> \|^=======$' --exclude-dir='.git' --exclude-dir='.mokogitea' --include='*.php' --include='*.xml' --include='*.css' --include='*.js' --include='*.json' --include='*.md' --include='*.yml' --include='*.yaml' --include='*.ini' --include='*.txt' . 2>/dev/null | grep -v '.git/' || true)
if [ -n "$CONFLICTS" ]; then
echo "::error::Merge conflict markers found in source files"
echo "## Conflict Markers Found" >> $GITHUB_STEP_SUMMARY
@@ -258,8 +258,9 @@ jobs:
while IFS= read -r -d '' file; do
# Skip vendor, node_modules, and index.html stub files
case "$file" in ./vendor/*|./node_modules/*) continue ;; esac
# Check first 10 lines for JEXEC or JPATH guard
if ! head -20 "$file" | grep -qE "defined\s*\(\s*['\"](_JEXEC|JPATH_BASE|\\\\JPATH_PLATFORM)['\"]"; then
# Scan the whole file for the JEXEC/JPATH guard: it is placed after
# the SPDX/file-header docblock, which commonly runs past 20 lines.
if ! grep -qE "defined\s*\(\s*['\"](_JEXEC|JPATH_BASE|\\\\JPATH_PLATFORM)['\"]" "$file"; then
echo "::error file=${file}::Missing JEXEC guard: ${file}"
ERRORS=$((ERRORS + 1))
fi
+9 -3
View File
@@ -7,7 +7,7 @@
# INGROUP: mokocli.Release
# REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Generic
# PATH: /.mokogitea/workflows/pre-release.yml
# VERSION: 05.02.00
# VERSION: 05.02.01
# BRIEF: Auto pre-release on push to dev/alpha/beta/rc branches
name: "Universal: Pre-Release"
@@ -162,7 +162,13 @@ jobs:
git add -A
git diff --cached --quiet || {
git commit -m "chore(version): pre-release bump to ${VERSION} [skip ci]"
git push origin HEAD 2>&1
# Push the bump commit, but do NOT fail the release if the target branch
# is protected and the release identity is not on the push allowlist.
# The build proceeds from the in-tree bumped version regardless; if the
# push is rejected, the next run simply re-bumps from the same base.
if ! git push origin HEAD 2>&1; then
echo "::warning::Version-bump commit could not be pushed (protected branch?). Building from in-tree version ${VERSION} anyway."
fi
}
# Auto-detect element via manifest_element.php
@@ -274,4 +280,4 @@ jobs:
echo "| Version | \`${VERSION}\` |" >> $GITHUB_STEP_SUMMARY
echo "| Channel | ${STABILITY} |" >> $GITHUB_STEP_SUMMARY
echo "| Package | \`${ZIP_NAME}\` |" >> $GITHUB_STEP_SUMMARY
echo "| SHA-256 | \`${SHA256:-n/a}\` |" >> $GITHUB_STEP_SUMMARY
echo "| SHA-256 | \`${SHA256:-n/a}\` |" >> $GITHUB_STEP_SUMMARY
+3 -4
View File
@@ -2,18 +2,17 @@
## [Unreleased]
## [01.07.00] --- 2026-06-29
All notable changes to MokoSuiteOpenGraph will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
<!-- VERSION: 01.07.04 -->
<!-- VERSION: 01.08.00 -->
## [01.07.00] --- 2026-06-29
## [01.08.00] --- 2026-07-13
### Added
- **Admin menu short-name constant** — the Administrator → Components menu now shows a compact label via `COM_MOKOOG_SHORT` ("OpenGraph"); defined in the admin sys.ini (#119)
- OG coverage **dashboard** as the default admin view — SVG donut gauge, coverage by content type, and a list of articles missing OG tags with a batch-generate shortcut (#94)
- Single OG tag **create/edit screen** in the admin (the tag manager was previously read-only) (#98)
- **CSV import** button and upload form in the tag manager (#103)
+1 -1
View File
@@ -14,7 +14,7 @@
DEFGROUP: Template-Joomla
INGROUP: Template-Joomla.Documentation
REPO: https://github.com/mokoconsulting-tech/Template-Joomla/
VERSION: 01.07.04
VERSION: 01.08.00
PATH: ./CODE_OF_CONDUCT.md
BRIEF: Community expectations and enforcement guidelines
NOTE: Adapted with attribution from the Contributor Covenant v2.1
+1 -1
View File
@@ -19,7 +19,7 @@
DEFGROUP: mokoconsulting-tech.Template-Joomla
INGROUP: MokoStandards.Governance
REPO: https://github.com/mokoconsulting-tech/Template-Joomla
VERSION: 01.07.04
VERSION: 01.08.00
PATH: /GOVERNANCE.md
BRIEF: Project governance rules, roles, and decision process for Template-Joomla
-->
+2 -2
View File
@@ -1,6 +1,6 @@
# MokoSuiteOpenGraph
<!-- VERSION: 01.07.04 -->
<!-- VERSION: 01.08.00 -->
Open Graph, Twitter Card, and social sharing meta tag management for Joomla 6 and higher.
@@ -61,7 +61,7 @@ MokoSuiteOpenGraph gives you full control over how your Joomla content appears w
## Installation
**Requirements:** Joomla 6.0 or higher and PHP 8.2 or higher.
**Requirements:** Joomla 6.0 or higher and PHP 8.3 or higher.
1. Download the latest `pkg_mokoog-*.zip` from [Releases](https://git.mokoconsulting.tech/MokoConsulting/MokoSuiteOpenGraph/releases)
2. In Joomla Administrator → Extensions → Install → Upload Package File
+1 -1
View File
@@ -23,7 +23,7 @@ DEFGROUP: Template-Joomla
INGROUP: Template-Joomla.Documentation
REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Joomla
PATH: /SECURITY.md
VERSION: 01.07.04
VERSION: 01.08.00
BRIEF: Security vulnerability reporting and handling policy
-->
+1 -1
View File
@@ -12,7 +12,7 @@
}
],
"require": {
"php": ">=8.1"
"php": ">=8.3"
},
"require-dev": {
"joomla/coding-standards": "^3.0",
@@ -3,4 +3,5 @@
; License: GPL-3.0-or-later
COM_MOKOOG="MokoSuiteOpenGraph"
COM_MOKOOG_SHORT="OpenGraph"
COM_MOKOOG_DESCRIPTION="Manage Open Graph and social sharing tags for all your content. View, edit, and batch-process OG metadata."
+2 -2
View File
@@ -8,7 +8,7 @@
-->
<extension type="component" method="upgrade">
<name>com_mokoog</name>
<version>01.07.04</version>
<version>01.08.00</version>
<creationDate>2026-05-23</creationDate>
<author>Moko Consulting</author>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
@@ -71,7 +71,7 @@
<filename>access.xml</filename>
<filename>config.xml</filename>
</files>
<menu img="class:bookmark">COM_MOKOOG</menu>
<menu img="class:bookmark">COM_MOKOOG_SHORT</menu>
<submenu>
<menu link="option=com_mokoog&amp;view=dashboard">COM_MOKOOG_SUBMENU_DASHBOARD</menu>
<menu link="option=com_mokoog&amp;view=tags">COM_MOKOOG_SUBMENU_TAGS</menu>
+81 -2
View File
@@ -10,6 +10,7 @@
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\Installer\InstallerAdapter;
class Com_MokoOGInstallerScript
@@ -23,7 +24,26 @@ class Com_MokoOGInstallerScript
*/
public function install(InstallerAdapter $parent): void
{
echo '<p>MokoSuiteOpenGraph component installed successfully.</p>';
// Be smart: verify EVERY table the component's own install SQL declares is
// actually present. If any are missing the install did not complete — say
// so instead of showing "installed successfully".
$missing = $this->missingTables();
if ($missing !== []) {
$detail = count($missing) > 6
? count($missing) . ' expected database tables are missing'
: 'missing tables: ' . htmlspecialchars(implode(', ', $missing), ENT_QUOTES, 'UTF-8');
Factory::getApplication()->enqueueMessage(
'<strong>MokoSuiteOpenGraph did not install correctly.</strong> ' . $detail
. ' — review the errors above, then reinstall.',
'error'
);
return;
}
Factory::getApplication()->enqueueMessage('<h4>MokoSuiteOpenGraph installed successfully!</h4>', 'info');
}
/**
@@ -35,6 +55,65 @@ class Com_MokoOGInstallerScript
*/
public function update(InstallerAdapter $parent): void
{
echo '<p>MokoSuiteOpenGraph component updated successfully.</p>';
// Same smart check on update: only report success if the schema is intact.
$missing = $this->missingTables();
if ($missing !== []) {
$detail = count($missing) > 6
? count($missing) . ' expected database tables are missing'
: 'missing tables: ' . htmlspecialchars(implode(', ', $missing), ENT_QUOTES, 'UTF-8');
Factory::getApplication()->enqueueMessage(
'<strong>MokoSuiteOpenGraph did not update correctly.</strong> ' . $detail
. ' — review the errors above.',
'error'
);
return;
}
Factory::getApplication()->enqueueMessage('MokoSuiteOpenGraph updated successfully.', 'info');
}
/**
* Smart schema check: parse the component's own installed install SQL for every
* CREATE TABLE, then return the ones missing from the database. An empty array
* means the full schema installed. Fails open (returns []) if the SQL can't be
* read, so a good install is never flagged as failed.
*
* @return string[] Names (without the #__ prefix) of missing tables.
*/
private function missingTables(): array
{
try {
$sqlFile = JPATH_ADMINISTRATOR . '/components/com_mokoog/sql/install.mysql.sql';
if (!is_file($sqlFile)) {
return [];
}
$sql = file_get_contents($sqlFile);
if ($sql === false
|| !preg_match_all('/CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?[`"\']?(#__[A-Za-z0-9_]+)/i', $sql, $m)) {
return [];
}
$db = Factory::getDbo();
$prefix = strtolower($db->getPrefix());
$have = array_map('strtolower', $db->getTableList());
$missing = [];
foreach (array_unique($m[1]) as $decl) {
$bare = substr($decl, 3); // strip "#__"
if (!in_array($prefix . strtolower($bare), $have, true)) {
$missing[] = $bare;
}
}
return $missing;
} catch (\Throwable $e) {
return [];
}
}
}
@@ -0,0 +1 @@
/* 01.07.01 — no schema changes */
@@ -0,0 +1 @@
/* 01.07.02 — no schema changes */
@@ -0,0 +1 @@
/* 01.07.03 — no schema changes */
@@ -1 +0,0 @@
/* 01.07.04 — no schema changes */
@@ -0,0 +1 @@
/* 01.07.05 — no schema changes */
@@ -0,0 +1 @@
/* 01.07.07 — no schema changes */
@@ -0,0 +1 @@
/* 01.07.08 — no schema changes */
@@ -0,0 +1 @@
/* 01.07.09 — no schema changes */
@@ -0,0 +1 @@
/* 01.07.10 — no schema changes */
@@ -0,0 +1 @@
/* 01.07.11 — no schema changes */
@@ -0,0 +1 @@
/* 01.07.12 — no schema changes */
@@ -0,0 +1 @@
/* 01.07.13 — no schema changes */
@@ -0,0 +1 @@
/* 01.07.14 — no schema changes */
@@ -0,0 +1 @@
/* 01.07.15 — no schema changes */
@@ -0,0 +1 @@
/* 01.07.16 — no schema changes */
@@ -0,0 +1 @@
/* 01.07.17 — no schema changes */
@@ -0,0 +1 @@
/* 01.07.18 — no schema changes */
@@ -0,0 +1 @@
/* 01.07.19 — no schema changes */
@@ -0,0 +1 @@
/* 01.08.00 — no schema changes */
@@ -73,7 +73,9 @@ class BatchController extends BaseController
}
$app = Factory::getApplication();
$limit = min($app->getInput()->getInt('limit', 50), 200);
$input = $app->getInput();
$limit = min($input->getInt('limit', 50), 200);
$lastId = max(0, $input->getInt('lastid', 0));
$db = Factory::getContainer()->get(\Joomla\Database\DatabaseInterface::class);
$query = $db->getQuery(true)
@@ -88,18 +90,25 @@ class BatchController extends BaseController
)
->where($db->quoteName('c.state') . ' = 1')
->where($db->quoteName('t.id') . ' IS NULL')
->where($db->quoteName('c.id') . ' > ' . $lastId)
->order($db->quoteName('c.id') . ' ASC');
// Always offset=0: processed articles now have #__mokoog_tags rows
// and are excluded by the LEFT JOIN ... IS NULL filter automatically.
// Cursor-based pagination by id: each chunk fetches the next articles whose
// id is greater than the previous chunk's highest id. A row that fails to
// insert is passed over on the next chunk (its id is already behind the
// cursor) instead of being re-fetched forever, so the batch always reaches
// the end. The client stops when a chunk examines 0 rows.
$db->setQuery($query, 0, $limit);
$articles = $db->loadObjectList();
$created = 0;
$skipped = 0;
$now = Factory::getDate()->toSql();
$created = 0;
$skipped = 0;
$lastProcessedId = $lastId;
$now = Factory::getDate()->toSql();
foreach ($articles as $article) {
$lastProcessedId = (int) $article->id;
$ogTitle = $article->title;
$ogDescription = $this->extractDescription($article);
$ogImage = $this->extractImage($article);
@@ -131,7 +140,10 @@ class BatchController extends BaseController
}
echo new JsonResponse([
'created' => $created,
'created' => $created,
'skipped' => $skipped,
'examined' => \count($articles),
'last_id' => $lastProcessedId,
]);
$app->close();
@@ -234,27 +234,31 @@ document.addEventListener('DOMContentLoaded', function() {
return;
}
status.textContent = total + ' <?php echo Text::_('COM_MOKOOG_BATCH_FOUND', true); ?>';
processChunk(0, total, chunkSize, token, bar, status);
processChunk(0, 0, total, chunkSize, token, bar, status);
})
.catch(function(err) {
status.textContent = '<?php echo Text::_('COM_MOKOOG_BATCH_ERROR', true); ?> ' + err.message;
});
}
function processChunk(processed, total, chunkSize, token, bar, status) {
// Always offset=0: processed items are excluded by the IS NULL filter
fetch('index.php?option=com_mokoog&task=batch.process&format=json&limit=' + chunkSize + '&' + token + '=1')
function processChunk(lastId, processed, total, chunkSize, token, bar, status) {
// Cursor-based: pass the highest id seen so far. Failed rows fall behind
// the cursor and are not re-fetched, so the loop always terminates.
fetch('index.php?option=com_mokoog&task=batch.process&format=json&limit=' + chunkSize + '&lastid=' + lastId + '&' + token + '=1')
.then(function(r) { return r.json(); })
.then(function(resp) {
processed += resp.data.created;
var pct = Math.min(100, Math.round((processed / total) * 100));
var examined = resp.data.examined || 0;
processed += examined;
var pct = total > 0 ? Math.min(100, Math.round((processed / total) * 100)) : 100;
bar.style.width = pct + '%';
bar.textContent = pct + '%';
status.textContent = processed + ' / ' + total + ' <?php echo Text::_('COM_MOKOOG_BATCH_PROCESSED', true); ?>';
if (resp.data.created > 0 && processed < total) {
processChunk(processed, total, chunkSize, token, bar, status);
if (examined > 0) {
processChunk(resp.data.last_id, processed, total, chunkSize, token, bar, status);
} else {
bar.style.width = '100%';
bar.textContent = '100%';
bar.classList.remove('progress-bar-animated');
bar.classList.add('bg-success');
status.textContent = '<?php echo Text::_('COM_MOKOOG_BATCH_COMPLETE', true); ?> ' + processed + ' articles.';
@@ -8,7 +8,7 @@
-->
<extension type="plugin" group="content" method="upgrade">
<name>Content - MokoSuiteOpenGraph</name>
<version>01.07.04</version>
<version>01.08.00</version>
<creationDate>2026-05-23</creationDate>
<author>Moko Consulting</author>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
+1 -1
View File
@@ -8,7 +8,7 @@
-->
<extension type="plugin" group="system" method="upgrade">
<name>System - MokoSuiteOpenGraph</name>
<version>01.07.04</version>
<version>01.08.00</version>
<creationDate>2026-05-23</creationDate>
<author>Moko Consulting</author>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
@@ -28,6 +28,11 @@ final class MokoOG extends CMSPlugin implements SubscriberInterface
*/
protected $autoloadLanguage = true;
/**
* Minimum seconds between full sitemap regenerations (save-time throttle).
*/
private const SITEMAP_MIN_INTERVAL = 60;
/**
* Returns the events this plugin subscribes to.
*
@@ -845,6 +850,15 @@ final class MokoOG extends CMSPlugin implements SubscriberInterface
return;
}
// Throttle: rebuilding the whole sitemap on every save does not scale
// (bulk edits/imports). Regenerate at most once per interval — the
// sitemap is eventually consistent within that window.
$path = JPATH_ROOT . '/sitemap.xml';
if (is_file($path) && (time() - filemtime($path)) < self::SITEMAP_MIN_INTERVAL) {
return;
}
$changefreq = $this->params->get('sitemap_changefreq', 'weekly');
$xml = SitemapBuilder::generate($changefreq);
@@ -57,96 +57,8 @@ class ImageHelper
int $targetHeight = self::TARGET_HEIGHT,
int $quality = self::JPEG_QUALITY
): string {
// Resolve absolute path
$absPath = JPATH_ROOT . '/' . ltrim($imagePath, '/');
if (!is_file($absPath)) {
return $imagePath;
}
$imageInfo = getimagesize($absPath);
if (!$imageInfo) {
Log::add('MokoOG ImageHelper: Cannot read image dimensions: ' . basename($absPath), Log::WARNING, 'mokoog');
return $imagePath;
}
[$origWidth, $origHeight, $type] = $imageInfo;
// Skip if already at or below target size
if ($origWidth <= $targetWidth && $origHeight <= $targetHeight) {
return $imagePath;
}
// Ensure output directory exists
$outputDir = JPATH_ROOT . '/' . self::OUTPUT_DIR;
if (!is_dir($outputDir) && !Folder::create($outputDir)) {
Log::add('MokoOG ImageHelper: Cannot create output directory: ' . self::OUTPUT_DIR, Log::WARNING, 'mokoog');
return $imagePath;
}
// Generate output filename based on source hash + dimensions
$hash = md5($imagePath . $targetWidth . $targetHeight);
$outputName = $hash . '.jpg';
$outputPath = $outputDir . '/' . $outputName;
$outputRel = self::OUTPUT_DIR . '/' . $outputName;
// Skip if already generated
if (is_file($outputPath) && filemtime($outputPath) >= filemtime($absPath)) {
return $outputRel;
}
// Load source image
$source = self::loadImage($absPath, $type);
if (!$source) {
return $imagePath;
}
// Calculate crop dimensions (center crop to target aspect ratio)
$targetRatio = $targetWidth / $targetHeight;
$sourceRatio = $origWidth / $origHeight;
if ($sourceRatio > $targetRatio) {
// Source is wider — crop sides
$cropHeight = $origHeight;
$cropWidth = (int) round($origHeight * $targetRatio);
$cropX = (int) round(($origWidth - $cropWidth) / 2);
$cropY = 0;
} else {
// Source is taller — crop top/bottom
$cropWidth = $origWidth;
$cropHeight = (int) round($origWidth / $targetRatio);
$cropX = 0;
$cropY = (int) round(($origHeight - $cropHeight) / 2);
}
// Create output canvas and resample
$output = imagecreatetruecolor($targetWidth, $targetHeight);
imagecopyresampled(
$output,
$source,
0,
0,
$cropX,
$cropY,
$targetWidth,
$targetHeight,
$cropWidth,
$cropHeight
);
// Save as JPEG
imagejpeg($output, $outputPath, $quality);
imagedestroy($source);
imagedestroy($output);
return $outputRel;
// Thin wrapper over the shared implementation (no subdirectory).
return self::resizeToSize($imagePath, $targetWidth, $targetHeight, '', $quality);
}
/**
@@ -182,11 +94,17 @@ class ImageHelper
* @param int $width Target width
* @param int $height Target height
* @param string $subdir Subdirectory name for output (e.g. platform name)
* @param int $quality JPEG quality 1-100
*
* @return string Path to the output image (relative to JPATH_ROOT)
*/
private static function resizeToSize(string $imagePath, int $width, int $height, string $subdir = ''): string
{
private static function resizeToSize(
string $imagePath,
int $width,
int $height,
string $subdir = '',
int $quality = self::JPEG_QUALITY
): string {
// Resolve absolute path
$absPath = JPATH_ROOT . '/' . ltrim($imagePath, '/');
@@ -272,7 +190,7 @@ class ImageHelper
);
// Save as JPEG
imagejpeg($output, $outputPath, self::JPEG_QUALITY);
imagejpeg($output, $outputPath, $quality);
imagedestroy($source);
imagedestroy($output);
@@ -333,43 +251,6 @@ class ImageHelper
}
}
/**
* Check if an image meets minimum OG size requirements.
*
* @param string $imagePath Image path relative to JPATH_ROOT
*
* @return array{valid: bool, width: int, height: int, message: string}
*/
public static function validate(string $imagePath): array
{
$absPath = JPATH_ROOT . '/' . ltrim($imagePath, '/');
if (!is_file($absPath)) {
return ['valid' => false, 'width' => 0, 'height' => 0, 'message' => 'File not found'];
}
$imageInfo = getimagesize($absPath);
if (!$imageInfo) {
return ['valid' => false, 'width' => 0, 'height' => 0, 'message' => 'Not a valid image'];
}
[$width, $height] = $imageInfo;
// Facebook minimum: 200x200, recommended: 1200x630
// WhatsApp minimum: 300x200
if ($width < 200 || $height < 200) {
return [
'valid' => false,
'width' => $width,
'height' => $height,
'message' => "Image too small ({$width}x{$height}). Minimum: 200x200px.",
];
}
return ['valid' => true, 'width' => $width, 'height' => $height, 'message' => 'OK'];
}
/**
* Load an image resource from a file.
*
@@ -81,7 +81,7 @@ class SitemapBuilder
continue;
}
$url = $root . '/index.php?option=com_content&view=article&id=' . $article->id;
$url = self::articleUrl($article, $root);
$lastmod = $article->modified && $article->modified !== '0000-00-00 00:00:00'
? date('Y-m-d', strtotime($article->modified)) : '';
@@ -102,6 +102,45 @@ class SitemapBuilder
return $xml;
}
/**
* Build the SEF/canonical site URL for an article, with a safe fallback.
*
* Routes through the site router so the sitemap matches the canonical URLs
* the plugin emits. If routing fails (or SEF is off), falls back to the
* non-SEF index.php URL — never an empty or broken URL.
*
* @param object $article Row with id, alias, catid, language
* @param string $root Site root without trailing slash
*
* @return string Absolute URL
*/
private static function articleUrl(object $article, string $root): string
{
$fallback = $root . '/index.php?option=com_content&view=article&id=' . (int) $article->id;
$internal = 'index.php?option=com_content&view=article&id=' . (int) $article->id
. (!empty($article->alias) ? ':' . $article->alias : '')
. (!empty($article->catid) ? '&catid=' . (int) $article->catid : '');
try {
$routed = \Joomla\CMS\Router\Route::link(
'site',
$internal,
false,
\Joomla\CMS\Router\Route::TLS_IGNORE,
true
);
if (\is_string($routed) && $routed !== '') {
return $routed;
}
} catch (\Throwable $e) {
// Fall back to the non-SEF URL below.
}
return $fallback;
}
/**
* Write sitemap XML to the site root.
*
@@ -8,7 +8,7 @@
-->
<extension type="plugin" group="webservices" method="upgrade">
<name>Web Services - MokoSuiteOpenGraph</name>
<version>01.07.04</version>
<version>01.08.00</version>
<creationDate>2026-05-23</creationDate>
<author>Moko Consulting</author>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
+1 -1
View File
@@ -8,7 +8,7 @@
<extension type="package" method="upgrade">
<name>Package - MokoSuiteOpenGraph</name>
<packagename>mokoog</packagename>
<version>01.07.04</version>
<version>01.08.00</version>
<creationDate>2026-05-23</creationDate>
<author>Moko Consulting</author>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
+93 -1
View File
@@ -50,9 +50,10 @@ class Pkg_MokoOGInstallerScript
public function postflight(string $type, InstallerAdapter $parent): void
{
// The download key must be restored regardless of the install outcome.
$this->restoreDownloadKey();
$this->warnMissingLicenseKey();
// Housekeeping: ensure the bundled plugins are enabled after a fresh install.
if ($type === 'install')
{
$db = Factory::getContainer()->get(\Joomla\Database\DatabaseInterface::class);
@@ -69,6 +70,97 @@ class Pkg_MokoOGInstallerScript
)->execute();
}
}
// Be smart: only run the success path (the licence notice, which implies a
// completed install) when every child extension declared in the package
// manifest actually registered. If any are missing, the package did NOT
// install successfully — surface that instead of implying success.
$missing = $this->missingChildExtensions($parent);
if ($missing !== [])
{
Factory::getApplication()->enqueueMessage(
'<strong>MokoSuiteOpenGraph did not install completely.</strong> '
. 'The following extension(s) failed to install: ' . htmlspecialchars(implode(', ', $missing), ENT_QUOTES, 'UTF-8') . '. '
. 'Resolve the errors shown above, then uninstall and reinstall the package.',
'error'
);
return;
}
$this->warnMissingLicenseKey();
}
/**
* Compare the child extensions the package manifest declares against what is
* actually registered in #__extensions after the install/update. Returns the
* label of each declared extension that is missing (i.e. failed to install);
* an empty array means every child installed and the package succeeded.
*
* Fails open: any error returns [] so a query problem can never turn a good
* install into a false "did not install completely" warning.
*
* @param InstallerAdapter $parent Installer adapter
*
* @return string[]
*/
private function missingChildExtensions(InstallerAdapter $parent): array
{
$missing = [];
try
{
$manifest = $parent->getParent()->getManifest();
if (!$manifest || !isset($manifest->files) || !isset($manifest->files->file))
{
return [];
}
$db = Factory::getContainer()->get(\Joomla\Database\DatabaseInterface::class);
foreach ($manifest->files->file as $file)
{
$childType = (string) $file['type'];
$element = (string) $file['id'];
$group = (string) $file['group'];
if ($element === '')
{
continue;
}
$query = $db->getQuery(true)
->select('COUNT(*)')
->from($db->quoteName('#__extensions'))
->where($db->quoteName('element') . ' = ' . $db->quote($element));
if ($childType !== '')
{
$query->where($db->quoteName('type') . ' = ' . $db->quote($childType));
}
// Plugins are only unique by element + folder (the manifest "group").
if ($childType === 'plugin' && $group !== '')
{
$query->where($db->quoteName('folder') . ' = ' . $db->quote($group));
}
$db->setQuery($query);
if ((int) $db->loadResult() === 0)
{
$missing[] = $element . ($group !== '' ? ' (' . $group . ')' : '');
}
}
}
catch (\Throwable $e)
{
return [];
}
return $missing;
}
@@ -0,0 +1,93 @@
<?php
/**
* @package MokoSuiteOpenGraph
* @subpackage Tests
* @author Moko Consulting <hello@mokoconsulting.tech>
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE
*/
namespace Mokoconsulting\MokoOG\Tests\Unit\Helper;
use Joomla\Plugin\System\MokoOG\Helper\JsonLdBuilder;
use PHPUnit\Framework\TestCase;
class JsonLdBuilderLocalBusinessTest extends TestCase
{
/**
* Minimal Registry-like stand-in exposing get($key, $default).
*/
private function params(array $data): object
{
return new class ($data) {
private array $data;
public function __construct(array $data)
{
$this->data = $data;
}
public function get($key, $default = null)
{
return $this->data[$key] ?? $default;
}
};
}
public function testReturnsNullWithoutName(): void
{
$this->assertNull(JsonLdBuilder::buildLocalBusiness($this->params([])));
$this->assertNull(JsonLdBuilder::buildLocalBusiness($this->params(['lb_name' => ' '])));
}
public function testMinimalSchemaHasNoOptionalKeys(): void
{
$result = JsonLdBuilder::buildLocalBusiness($this->params(['lb_name' => 'Acme Co']));
$this->assertSame('https://schema.org', $result['@context']);
$this->assertSame('LocalBusiness', $result['@type']);
$this->assertSame('Acme Co', $result['name']);
$this->assertArrayNotHasKey('address', $result);
$this->assertArrayNotHasKey('geo', $result);
$this->assertArrayNotHasKey('telephone', $result);
}
public function testCustomTypeAndPartialAddress(): void
{
$result = JsonLdBuilder::buildLocalBusiness($this->params([
'lb_name' => 'Joe Pizza',
'lb_type' => 'Restaurant',
'lb_street' => '1 Main St',
'lb_city' => 'Springfield',
'lb_country' => 'US',
'lb_phone' => '+1-555-0100',
]));
$this->assertSame('Restaurant', $result['@type']);
$this->assertSame('PostalAddress', $result['address']['@type']);
$this->assertSame('1 Main St', $result['address']['streetAddress']);
$this->assertSame('Springfield', $result['address']['addressLocality']);
$this->assertSame('US', $result['address']['addressCountry']);
$this->assertArrayNotHasKey('postalCode', $result['address']);
$this->assertSame('+1-555-0100', $result['telephone']);
}
public function testGeoRequiresBothCoordinates(): void
{
$partial = JsonLdBuilder::buildLocalBusiness($this->params([
'lb_name' => 'X',
'lb_latitude' => '1.0',
]));
$this->assertArrayNotHasKey('geo', $partial);
$full = JsonLdBuilder::buildLocalBusiness($this->params([
'lb_name' => 'X',
'lb_latitude' => '1.0',
'lb_longitude' => '2.0',
]));
$this->assertSame('GeoCoordinates', $full['geo']['@type']);
$this->assertSame('1.0', $full['geo']['latitude']);
$this->assertSame('2.0', $full['geo']['longitude']);
}
}
+56
View File
@@ -0,0 +1,56 @@
<?php
/**
* @package MokoSuiteOpenGraph
* @subpackage Tests
* @author Moko Consulting <hello@mokoconsulting.tech>
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE
*/
namespace Mokoconsulting\MokoOG\Tests\Unit\Helper;
use Joomla\Plugin\System\MokoOG\Helper\JsonLdBuilder;
use PHPUnit\Framework\TestCase;
class JsonLdScriptTagTest extends TestCase
{
private const OPEN = '<script type="application/ld+json">';
private const CLOSE = '</script>';
private function body(string $output): string
{
return substr($output, \strlen(self::OPEN), -\strlen(self::CLOSE));
}
public function testWrapsInLdJsonScriptTag(): void
{
$out = JsonLdBuilder::toScriptTag(['@type' => 'Thing']);
$this->assertStringStartsWith(self::OPEN, $out);
$this->assertStringEndsWith(self::CLOSE, $out);
}
public function testEscapesClosingScriptInsideData(): void
{
$out = JsonLdBuilder::toScriptTag(['name' => 'evil </script><script>alert(1)</script>']);
$body = $this->body($out);
// No raw "</" may survive inside the body — it would let content break out
// of the <script> block. The builder rewrites every "</" to "<\/".
$this->assertStringNotContainsString('</', $body);
$this->assertStringContainsString('<\\/', $body);
}
public function testBodyIsValidJsonAfterUnescaping(): void
{
$out = JsonLdBuilder::toScriptTag(['@context' => 'https://schema.org', '@type' => 'Article']);
$json = str_replace('<\\/', '</', $this->body($out));
$decoded = json_decode($json, true);
$this->assertIsArray($decoded);
$this->assertSame('Article', $decoded['@type']);
$this->assertSame('https://schema.org', $decoded['@context']);
}
}