Compare commits
127 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6d4284c6c9 | |||
| 3ac54da149 | |||
| 2af8a72ca3 | |||
| 740fb4e1f6 | |||
| 1413f62476 | |||
| d6fb2816cf | |||
| 464514bc37 | |||
| 2b0a412066 | |||
| 5cf39a5a3a | |||
| 1c8f4e6867 | |||
| 77cf557b71 | |||
| ff2c1a0483 | |||
| 7fb7e38762 | |||
| cedf6808d2 | |||
| 36ce686ae1 | |||
| 5ea422d75e | |||
| e6e525080f | |||
| 42ffb4b46c | |||
| 2907e64641 | |||
| b77054b769 | |||
| 3fb5a87be9 | |||
| 5afbc75f23 | |||
| 7ef082a8de | |||
| c65ef345ef | |||
| 31da0a5980 | |||
| 9991bb3099 | |||
| aa4e254f0b | |||
| 94201082d2 | |||
| 9147790214 | |||
| 3e51d8c439 | |||
| 750f769a13 | |||
| 981464ee4e | |||
| 7afcc8e6b9 | |||
| e47fdf8722 | |||
| 872abec8bc | |||
| d01b39841a | |||
| 6cc08927e7 | |||
| ed715b5db8 | |||
| 5d02db24d5 | |||
| e6ade9033d | |||
| 76845f78f2 | |||
| b68d3f6481 | |||
| 3110d7eb75 | |||
| e285b8e770 | |||
| 0997a875d6 | |||
| baf67e18e6 | |||
| cf6b1286b5 | |||
| c1f560704b | |||
| 52edde00c9 | |||
| 759af6b237 | |||
| e0112d770a | |||
| 5544878cf2 | |||
| bd551bffda | |||
| 48eeb9631f | |||
| 281e742b54 | |||
| 2e3331170f | |||
| 8de243b181 | |||
| 9793bd4031 | |||
| a9fc5d2cf1 | |||
| f1c6eb8f6e | |||
| a578ac3bb3 | |||
| cf783c6b83 | |||
| bc6ce4397f | |||
| 49d644566a | |||
| cbebaecc22 | |||
| e7b0af1fca | |||
| 2088b3f13f | |||
| f649858fcd | |||
| 4699686f26 | |||
| a7fe881d84 | |||
| ab02de34f4 | |||
| 64d9a97db1 | |||
| 7a38025b5e | |||
| e530ca821e | |||
| 872074cd5b | |||
| c871b7d30d | |||
| 641eee753a | |||
| 44d9daf3bc | |||
| 96eea6060f | |||
| aeea65423c | |||
| b4d5b73d15 | |||
| 3ba1c3ead4 | |||
| 4c091805ee | |||
| 0d4e7785a3 | |||
| 6f13a10a34 | |||
| 5f1e44e66b | |||
| 646dd23e81 | |||
| e939e90733 | |||
| d4c22ebdbf | |||
| d4229fd450 | |||
| 5724a1545e | |||
| a04dbfd732 | |||
| bc06710fdd | |||
| 07b296db61 | |||
| 6a0ee812d8 | |||
| fcfa6838e5 | |||
| 908e1d3e1b | |||
| 9539bb44c2 | |||
| 5b29690d34 | |||
| 881bb0a2ae | |||
| e9b34522d3 | |||
| 9aeb588937 | |||
| 9cdc7915a3 | |||
| 72ffaded49 | |||
| 7d1a939b6a | |||
| 23f6fe12a0 | |||
| 4c1d630673 | |||
| 6a3f9c126e | |||
| ddb378a042 | |||
| 560c7458c6 | |||
| e39b617464 | |||
| 20b62b95d8 | |||
| 437a23cec2 | |||
| dac22fdcc4 | |||
| 68eab6fdb2 | |||
| b033cfe4e2 | |||
| e86bb5906b | |||
| b310ddfab2 | |||
| fa12fa5937 | |||
| b52867614c | |||
| b140bc9000 | |||
| 1a16f9ef8e | |||
| 7cdf8b4693 | |||
| d4b24fb57e | |||
| 6169716154 | |||
| 5904bea91d | |||
| 6ef4331f4c |
@@ -156,6 +156,7 @@ vendor/
|
|||||||
composer.lock
|
composer.lock
|
||||||
*.phar
|
*.phar
|
||||||
codeception.phar
|
codeception.phar
|
||||||
|
.phpunit.cache/
|
||||||
.phpunit.result.cache
|
.phpunit.result.cache
|
||||||
.php_cs.cache
|
.php_cs.cache
|
||||||
.php-cs-fixer.cache
|
.php-cs-fixer.cache
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# MokoJoomOpenGraph
|
# MokoSuiteOpenGraph
|
||||||
|
|
||||||
Open Graph, Twitter Card, and social sharing meta tag management for Joomla. Per-article SEO with auto-generation fallback.
|
Open Graph, Twitter Card, and social sharing meta tag management for Joomla. Per-article SEO with auto-generation fallback.
|
||||||
|
|
||||||
@@ -9,7 +9,7 @@ Open Graph, Twitter Card, and social sharing meta tag management for Joomla. Per
|
|||||||
| **Package** | `pkg_mokoog` |
|
| **Package** | `pkg_mokoog` |
|
||||||
| **Language** | PHP 8.1+ |
|
| **Language** | PHP 8.1+ |
|
||||||
| **Branch** | develop on `dev`, merge to `main` (protected) |
|
| **Branch** | develop on `dev`, merge to `main` (protected) |
|
||||||
| **Wiki** | [MokoJoomOpenGraph Wiki](https://git.mokoconsulting.tech/MokoConsulting/MokoJoomOpenGraph/wiki) |
|
| **Wiki** | [MokoSuiteOpenGraph Wiki](https://git.mokoconsulting.tech/MokoConsulting/MokoSuiteOpenGraph/wiki) |
|
||||||
|
|
||||||
## Commands
|
## Commands
|
||||||
|
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ on:
|
|||||||
|
|
||||||
env:
|
env:
|
||||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
||||||
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
|
MOKOGITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
|
|||||||
@@ -10,9 +10,9 @@
|
|||||||
# VERSION: 05.00.00
|
# VERSION: 05.00.00
|
||||||
# BRIEF: Universal build & release � detects platform from manifest.xml
|
# BRIEF: Universal build & release � detects platform from manifest.xml
|
||||||
#
|
#
|
||||||
# +========================================================================+
|
# +=======================================================================+
|
||||||
# | UNIVERSAL BUILD & RELEASE PIPELINE |
|
# | UNIVERSAL BUILD & RELEASE PIPELINE |
|
||||||
# +========================================================================+
|
# +=======================================================================+
|
||||||
# | |
|
# | |
|
||||||
# | Reads manifest.xml (joomla|dolibarr|generic) to branch logic. |
|
# | Reads manifest.xml (joomla|dolibarr|generic) to branch logic. |
|
||||||
# | |
|
# | |
|
||||||
@@ -21,15 +21,24 @@
|
|||||||
# | dolibarr: mod*.class.php, update.txt, dev version reset |
|
# | dolibarr: mod*.class.php, update.txt, dev version reset |
|
||||||
# | generic: README-only, no update stream |
|
# | generic: README-only, no update stream |
|
||||||
# | |
|
# | |
|
||||||
# +========================================================================+
|
# +=======================================================================+
|
||||||
|
|
||||||
name: "Universal: Build & Release"
|
name: "Universal: Build & Release"
|
||||||
|
|
||||||
on:
|
on:
|
||||||
pull_request:
|
pull_request:
|
||||||
types: [opened, closed]
|
types: [opened, synchronize, closed]
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
|
paths-ignore:
|
||||||
|
- '.mokogitea/workflows/**'
|
||||||
|
- '*.md'
|
||||||
|
- 'wiki/**'
|
||||||
|
- '.editorconfig'
|
||||||
|
- '.gitignore'
|
||||||
|
- '.gitattributes'
|
||||||
|
- '.gitmessage'
|
||||||
|
- 'LICENSE'
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
inputs:
|
inputs:
|
||||||
action:
|
action:
|
||||||
@@ -43,7 +52,7 @@ on:
|
|||||||
|
|
||||||
env:
|
env:
|
||||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
||||||
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
|
MOKOGITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
|
||||||
GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }}
|
GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }}
|
||||||
GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }}
|
GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }}
|
||||||
|
|
||||||
@@ -51,12 +60,13 @@ permissions:
|
|||||||
contents: write
|
contents: write
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
# ── PR Opened → Rename branch to RC and build RC release ─────────────────────
|
# ── PR Opened → Rename branch to RC and build RC release ─────────────────────────
|
||||||
promote-rc:
|
promote-rc:
|
||||||
name: Promote to RC
|
name: Promote to RC
|
||||||
runs-on: release
|
runs-on: release
|
||||||
if: >-
|
if: >-
|
||||||
(github.event.action == 'opened' && github.event.pull_request.merged != true) ||
|
(github.event.action == 'opened' && github.event.pull_request.merged != true) ||
|
||||||
|
(github.event.action == 'synchronize' && github.event.pull_request.merged != true) ||
|
||||||
(github.event_name == 'workflow_dispatch' && inputs.action == 'promote-rc')
|
(github.event_name == 'workflow_dispatch' && inputs.action == 'promote-rc')
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
@@ -92,7 +102,7 @@ jobs:
|
|||||||
php ${MOKO_CLI}/branch_rename.php \
|
php ${MOKO_CLI}/branch_rename.php \
|
||||||
--from "${{ github.event.pull_request.head.ref || 'dev' }}" --to rc \
|
--from "${{ github.event.pull_request.head.ref || 'dev' }}" --to rc \
|
||||||
--token "${{ secrets.MOKOGITEA_TOKEN }}" \
|
--token "${{ secrets.MOKOGITEA_TOKEN }}" \
|
||||||
--api-base "${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" \
|
--api-base "${MOKOGITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" \
|
||||||
--pr "${{ github.event.pull_request.number }}"
|
--pr "${{ github.event.pull_request.number }}"
|
||||||
|
|
||||||
- name: Checkout rc and configure git
|
- name: Checkout rc and configure git
|
||||||
@@ -111,7 +121,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Update RC release notes from CHANGELOG.md
|
- name: Update RC release notes from CHANGELOG.md
|
||||||
run: |
|
run: |
|
||||||
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
API_BASE="${MOKOGITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||||
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
|
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
|
||||||
|
|
||||||
# Extract [Unreleased] section from changelog
|
# Extract [Unreleased] section from changelog
|
||||||
@@ -149,7 +159,7 @@ jobs:
|
|||||||
echo "## Promoted to Release Candidate" >> $GITHUB_STEP_SUMMARY
|
echo "## Promoted to Release Candidate" >> $GITHUB_STEP_SUMMARY
|
||||||
echo "Branch renamed to rc, minor bump, RC release built" >> $GITHUB_STEP_SUMMARY
|
echo "Branch renamed to rc, minor bump, RC release built" >> $GITHUB_STEP_SUMMARY
|
||||||
|
|
||||||
# ── Merged PR → Build & Release (or promote RC to stable) ────────────────────
|
# ── Merged PR → Build & Release (or promote RC to stable) ─────────────────────────
|
||||||
release:
|
release:
|
||||||
name: Build & Release Pipeline
|
name: Build & Release Pipeline
|
||||||
runs-on: release
|
runs-on: release
|
||||||
@@ -241,14 +251,50 @@ jobs:
|
|||||||
VERSION=$(echo "$VERSION" | sed 's/-\(dev\|alpha\|beta\|rc\)$//')
|
VERSION=$(echo "$VERSION" | sed 's/-\(dev\|alpha\|beta\|rc\)$//')
|
||||||
[ -z "$VERSION" ] && VERSION="00.00.00" && echo "skip=true" >> "$GITHUB_OUTPUT"
|
[ -z "$VERSION" ] && VERSION="00.00.00" && echo "skip=true" >> "$GITHUB_OUTPUT"
|
||||||
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
|
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
|
||||||
echo "tag=stable" >> "$GITHUB_OUTPUT"
|
PLATFORM="${{ steps.platform.outputs.platform }}"
|
||||||
echo "release_tag=stable" >> "$GITHUB_OUTPUT"
|
if [[ "$PLATFORM" == joomla* ]]; then
|
||||||
|
echo "tag=stable" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "release_tag=stable" >> "$GITHUB_OUTPUT"
|
||||||
|
else
|
||||||
|
echo "tag=v${VERSION}" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "release_tag=v${VERSION}" >> "$GITHUB_OUTPUT"
|
||||||
|
fi
|
||||||
echo "branch=main" >> "$GITHUB_OUTPUT"
|
echo "branch=main" >> "$GITHUB_OUTPUT"
|
||||||
echo "Published version: ${VERSION}"
|
echo "Published version: ${VERSION}"
|
||||||
|
|
||||||
|
- name: "Create semver tag for non-Joomla repos"
|
||||||
|
id: semver
|
||||||
|
if: |
|
||||||
|
steps.version.outputs.skip != 'true' &&
|
||||||
|
!startsWith(steps.platform.outputs.platform, 'joomla')
|
||||||
|
run: |
|
||||||
|
VERSION="${{ steps.version.outputs.version }}"
|
||||||
|
API_BASE="${MOKOGITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||||
|
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
|
||||||
|
SEMVER_TAG="v${VERSION}"
|
||||||
|
|
||||||
|
echo "Creating semver tag: ${SEMVER_TAG}"
|
||||||
|
|
||||||
|
# Create the git tag via API
|
||||||
|
HTTP_CODE=$(curl -sf -o /dev/null -w "%{http_code}" \
|
||||||
|
-X POST -H "Authorization: token ${TOKEN}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
"${API_BASE}/tags" \
|
||||||
|
-d "{\"tag_name\":\"${SEMVER_TAG}\",\"target\":\"main\",\"message\":\"Release ${VERSION}\"}" 2>/dev/null || echo "000")
|
||||||
|
|
||||||
|
if [ "$HTTP_CODE" = "201" ] || [ "$HTTP_CODE" = "200" ]; then
|
||||||
|
echo "Created semver tag: ${SEMVER_TAG}"
|
||||||
|
elif [ "$HTTP_CODE" = "409" ]; then
|
||||||
|
echo "Semver tag ${SEMVER_TAG} already exists (skipped)"
|
||||||
|
else
|
||||||
|
echo "::warning::Failed to create semver tag ${SEMVER_TAG} (HTTP ${HTTP_CODE})"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "semver_tag=${SEMVER_TAG}" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
- name: Update release notes and promote changelog
|
- name: Update release notes and promote changelog
|
||||||
run: |
|
run: |
|
||||||
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
API_BASE="${MOKOGITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||||
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
|
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
|
||||||
|
|
||||||
# Get the stable release info (version and ID)
|
# Get the stable release info (version and ID)
|
||||||
@@ -317,7 +363,7 @@ jobs:
|
|||||||
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
|
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
|
||||||
RELEASE_TAG="${{ steps.version.outputs.release_tag }}"
|
RELEASE_TAG="${{ steps.version.outputs.release_tag }}"
|
||||||
GH_REPO="${{ vars.GH_MIRROR_REPO || github.repository }}"
|
GH_REPO="${{ vars.GH_MIRROR_REPO || github.repository }}"
|
||||||
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
API_BASE="${MOKOGITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||||
php ${MOKO_CLI}/release_mirror.php \
|
php ${MOKO_CLI}/release_mirror.php \
|
||||||
--version "$VERSION" --tag "$RELEASE_TAG" \
|
--version "$VERSION" --tag "$RELEASE_TAG" \
|
||||||
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
|
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
|
||||||
@@ -346,7 +392,7 @@ jobs:
|
|||||||
if: steps.version.outputs.skip != 'true'
|
if: steps.version.outputs.skip != 'true'
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
run: |
|
run: |
|
||||||
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
API_BASE="${MOKOGITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||||
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
|
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
|
||||||
|
|
||||||
# Delete rc branch (ephemeral — created by promote-rc)
|
# Delete rc branch (ephemeral — created by promote-rc)
|
||||||
@@ -370,7 +416,7 @@ jobs:
|
|||||||
if: steps.version.outputs.skip != 'true'
|
if: steps.version.outputs.skip != 'true'
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
run: |
|
run: |
|
||||||
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
API_BASE="${MOKOGITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||||
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
|
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
|
||||||
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
|
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
|
||||||
BRANCH_NAME="version/${VERSION}"
|
BRANCH_NAME="version/${VERSION}"
|
||||||
@@ -391,7 +437,7 @@ jobs:
|
|||||||
if: steps.version.outputs.skip != 'true'
|
if: steps.version.outputs.skip != 'true'
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
run: |
|
run: |
|
||||||
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
API_BASE="${MOKOGITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||||
php ${MOKO_CLI}/version_reset_dev.php \
|
php ${MOKO_CLI}/version_reset_dev.php \
|
||||||
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "${API_BASE}" \
|
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "${API_BASE}" \
|
||||||
--branch dev --path . 2>&1 || true
|
--branch dev --path . 2>&1 || true
|
||||||
@@ -417,5 +463,5 @@ jobs:
|
|||||||
echo "| Version | \`${VERSION}\` |" >> $GITHUB_STEP_SUMMARY
|
echo "| Version | \`${VERSION}\` |" >> $GITHUB_STEP_SUMMARY
|
||||||
echo "| Branch | \`${{ steps.version.outputs.branch }}\` |" >> $GITHUB_STEP_SUMMARY
|
echo "| Branch | \`${{ steps.version.outputs.branch }}\` |" >> $GITHUB_STEP_SUMMARY
|
||||||
echo "| Tag | \`${{ steps.version.outputs.tag }}\` |" >> $GITHUB_STEP_SUMMARY
|
echo "| Tag | \`${{ steps.version.outputs.tag }}\` |" >> $GITHUB_STEP_SUMMARY
|
||||||
echo "| Release | [View](${GITEA_URL}/${GITEA_ORG}/${GITEA_REPO}/releases/tag/${{ steps.version.outputs.tag }}) |" >> $GITHUB_STEP_SUMMARY
|
echo "| Release | [View](${MOKOGITEA_URL}/${GITEA_ORG}/${GITEA_REPO}/releases/tag/${{ steps.version.outputs.tag }}) |" >> $GITHUB_STEP_SUMMARY
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -13,6 +13,12 @@
|
|||||||
name: "Generic: Project CI"
|
name: "Generic: Project CI"
|
||||||
|
|
||||||
on:
|
on:
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
- dev
|
||||||
|
- dev/**
|
||||||
|
- rc/**
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
|
|||||||
@@ -0,0 +1,68 @@
|
|||||||
|
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
#
|
||||||
|
# FILE INFORMATION
|
||||||
|
# DEFGROUP: Gitea.Workflow
|
||||||
|
# INGROUP: mokocli.Universal
|
||||||
|
# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
|
||||||
|
# PATH: /.mokogitea/workflows/ci-issue-reporter.yml
|
||||||
|
# VERSION: 01.00.00
|
||||||
|
# BRIEF: Reusable workflow — creates/updates a Gitea issue when a CI gate fails.
|
||||||
|
# Clones MokoCLI and runs cli/ci_issue_reporter.sh.
|
||||||
|
|
||||||
|
name: "Universal: CI Issue Reporter"
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_call:
|
||||||
|
inputs:
|
||||||
|
gate:
|
||||||
|
description: "CI gate name (e.g. PR Validation, Repository Health)"
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
details:
|
||||||
|
description: "Human-readable failure description"
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
severity:
|
||||||
|
description: "error or warning"
|
||||||
|
required: false
|
||||||
|
type: string
|
||||||
|
default: "error"
|
||||||
|
workflow:
|
||||||
|
description: "Workflow name for the issue title"
|
||||||
|
required: false
|
||||||
|
type: string
|
||||||
|
default: ""
|
||||||
|
secrets:
|
||||||
|
MOKOGITEA_TOKEN:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
env:
|
||||||
|
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
report:
|
||||||
|
name: "Report: ${{ inputs.gate }}"
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Clone MokoCLI
|
||||||
|
env:
|
||||||
|
MOKOGITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||||
|
run: |
|
||||||
|
MOKOGITEA_URL="${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}"
|
||||||
|
git clone --depth 1 --filter=blob:none --sparse "${MOKOGITEA_URL}/MokoConsulting/MokoCLI.git" /tmp/mokocli
|
||||||
|
cd /tmp/mokocli && git sparse-checkout set cli/ci_issue_reporter.sh
|
||||||
|
|
||||||
|
- name: Report CI failure
|
||||||
|
env:
|
||||||
|
MOKOGITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||||
|
MOKOGITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
|
||||||
|
run: |
|
||||||
|
chmod +x /tmp/mokocli/cli/ci_issue_reporter.sh
|
||||||
|
/tmp/mokocli/cli/ci_issue_reporter.sh \
|
||||||
|
--gate "${{ inputs.gate }}" \
|
||||||
|
--details "${{ inputs.details }}" \
|
||||||
|
--severity "${{ inputs.severity }}" \
|
||||||
|
--workflow "${{ inputs.workflow }}"
|
||||||
@@ -4,8 +4,8 @@
|
|||||||
#
|
#
|
||||||
# FILE INFORMATION
|
# FILE INFORMATION
|
||||||
# DEFGROUP: Gitea.Workflow
|
# DEFGROUP: Gitea.Workflow
|
||||||
# INGROUP: moko-platform.Maintenance
|
# INGROUP: MokoStandards.Maintenance
|
||||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards
|
||||||
# PATH: /.gitea/workflows/cleanup.yml
|
# PATH: /.gitea/workflows/cleanup.yml
|
||||||
# VERSION: 01.00.00
|
# VERSION: 01.00.00
|
||||||
# BRIEF: Scheduled cleanup — delete merged branches and old workflow runs
|
# BRIEF: Scheduled cleanup — delete merged branches and old workflow runs
|
||||||
@@ -21,7 +21,7 @@ permissions:
|
|||||||
contents: write
|
contents: write
|
||||||
|
|
||||||
env:
|
env:
|
||||||
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
|
MOKOGITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
cleanup:
|
cleanup:
|
||||||
@@ -37,13 +37,13 @@ jobs:
|
|||||||
|
|
||||||
- name: Delete merged branches
|
- name: Delete merged branches
|
||||||
env:
|
env:
|
||||||
GA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
MOKOGITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||||
run: |
|
run: |
|
||||||
echo "=== Merged Branch Cleanup ==="
|
echo "=== Merged Branch Cleanup ==="
|
||||||
API="${GITEA_URL}/api/v1/repos/${{ github.repository }}"
|
API="${MOKOGITEA_URL}/api/v1/repos/${{ github.repository }}"
|
||||||
|
|
||||||
# List branches via API
|
# List branches via API
|
||||||
BRANCHES=$(curl -sS -H "Authorization: token ${GITEA_TOKEN}" \
|
BRANCHES=$(curl -sS -H "Authorization: token ${MOKOGITEA_TOKEN}" \
|
||||||
"${API}/branches?limit=50" | jq -r '.[].name')
|
"${API}/branches?limit=50" | jq -r '.[].name')
|
||||||
|
|
||||||
DELETED=0
|
DELETED=0
|
||||||
@@ -56,7 +56,7 @@ jobs:
|
|||||||
# Check if branch is merged into main
|
# Check if branch is merged into main
|
||||||
if git merge-base --is-ancestor "origin/${BRANCH}" origin/main 2>/dev/null; then
|
if git merge-base --is-ancestor "origin/${BRANCH}" origin/main 2>/dev/null; then
|
||||||
echo " Deleting merged branch: ${BRANCH}"
|
echo " Deleting merged branch: ${BRANCH}"
|
||||||
curl -sS -X DELETE -H "Authorization: token ${GITEA_TOKEN}" \
|
curl -sS -X DELETE -H "Authorization: token ${MOKOGITEA_TOKEN}" \
|
||||||
"${API}/branches/${BRANCH}" 2>/dev/null || true
|
"${API}/branches/${BRANCH}" 2>/dev/null || true
|
||||||
DELETED=$((DELETED + 1))
|
DELETED=$((DELETED + 1))
|
||||||
fi
|
fi
|
||||||
@@ -66,20 +66,20 @@ jobs:
|
|||||||
|
|
||||||
- name: Clean old workflow runs
|
- name: Clean old workflow runs
|
||||||
env:
|
env:
|
||||||
GA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
MOKOGITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||||
run: |
|
run: |
|
||||||
echo "=== Workflow Run Cleanup ==="
|
echo "=== Workflow Run Cleanup ==="
|
||||||
API="${GITEA_URL}/api/v1/repos/${{ github.repository }}"
|
API="${MOKOGITEA_URL}/api/v1/repos/${{ github.repository }}"
|
||||||
CUTOFF=$(date -d "30 days ago" +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || date -v-30d +%Y-%m-%dT%H:%M:%SZ)
|
CUTOFF=$(date -d "30 days ago" +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || date -v-30d +%Y-%m-%dT%H:%M:%SZ)
|
||||||
|
|
||||||
# Get old completed runs
|
# Get old completed runs
|
||||||
RUNS=$(curl -sS -H "Authorization: token ${GITEA_TOKEN}" \
|
RUNS=$(curl -sS -H "Authorization: token ${MOKOGITEA_TOKEN}" \
|
||||||
"${API}/actions/runs?status=completed&limit=50" | \
|
"${API}/actions/runs?status=completed&limit=50" | \
|
||||||
jq -r ".workflow_runs[] | select(.created_at < \"${CUTOFF}\") | .id" 2>/dev/null)
|
jq -r ".workflow_runs[] | select(.created_at < \"${CUTOFF}\") | .id" 2>/dev/null)
|
||||||
|
|
||||||
DELETED=0
|
DELETED=0
|
||||||
for RUN_ID in $RUNS; do
|
for RUN_ID in $RUNS; do
|
||||||
curl -sS -X DELETE -H "Authorization: token ${GITEA_TOKEN}" \
|
curl -sS -X DELETE -H "Authorization: token ${MOKOGITEA_TOKEN}" \
|
||||||
"${API}/actions/runs/${RUN_ID}" 2>/dev/null || true
|
"${API}/actions/runs/${RUN_ID}" 2>/dev/null || true
|
||||||
DELETED=$((DELETED + 1))
|
DELETED=$((DELETED + 1))
|
||||||
done
|
done
|
||||||
|
|||||||
@@ -1,76 +0,0 @@
|
|||||||
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
|
|
||||||
name: "Publish to Composer"
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
tags:
|
|
||||||
- 'v*'
|
|
||||||
- '[0-9]*.[0-9]*.[0-9]*'
|
|
||||||
release:
|
|
||||||
types: [published]
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
env:
|
|
||||||
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
publish:
|
|
||||||
name: Publish Package
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
if: >-
|
|
||||||
!contains(github.event.head_commit.message, '[skip ci]') &&
|
|
||||||
!contains(github.event.head_commit.message, '[skip publish]')
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Setup PHP
|
|
||||||
run: |
|
|
||||||
if ! command -v php &> /dev/null; then
|
|
||||||
sudo apt-get update -qq
|
|
||||||
sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Install dependencies
|
|
||||||
run: composer install --no-dev --no-interaction --prefer-dist --quiet
|
|
||||||
|
|
||||||
- name: Determine version
|
|
||||||
id: version
|
|
||||||
run: |
|
|
||||||
VERSION=$(php -r "echo json_decode(file_get_contents('composer.json'))->version;")
|
|
||||||
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
|
|
||||||
echo "Package version: ${VERSION}"
|
|
||||||
|
|
||||||
# Gitea Composer Registry — auto-publishes from tags
|
|
||||||
# The tag push itself registers the package at:
|
|
||||||
# https://git.mokoconsulting.tech/api/packages/MokoConsulting/composer
|
|
||||||
- name: Verify Gitea registry
|
|
||||||
run: |
|
|
||||||
echo "Gitea Composer registry auto-publishes from tags."
|
|
||||||
echo "Package available at: ${GITEA_URL}/api/packages/MokoConsulting/composer"
|
|
||||||
echo "Install: composer require mokoconsulting/mokocli"
|
|
||||||
|
|
||||||
# Packagist — notify of new version
|
|
||||||
- name: Notify Packagist
|
|
||||||
if: secrets.PACKAGIST_TOKEN != ''
|
|
||||||
run: |
|
|
||||||
VERSION="${{ steps.version.outputs.version }}"
|
|
||||||
echo "Notifying Packagist of version ${VERSION}..."
|
|
||||||
curl -sf -X POST \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d '{"repository":{"url":"https://git.mokoconsulting.tech/MokoConsulting/mokocli"}}' \
|
|
||||||
"https://packagist.org/api/update-package?username=mokoconsulting&apiToken=${{ secrets.PACKAGIST_TOKEN }}" \
|
|
||||||
&& echo "Packagist notified" \
|
|
||||||
|| echo "::warning::Packagist notification failed (package may not be registered yet)"
|
|
||||||
|
|
||||||
- name: Summary
|
|
||||||
run: |
|
|
||||||
VERSION="${{ steps.version.outputs.version }}"
|
|
||||||
echo "## Composer Package Published" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "| Registry | Status |" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "|----------|--------|" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "| Gitea | \`composer require mokoconsulting/mokocli:${VERSION}\` |" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "| Packagist | \`composer require mokoconsulting/mokocli\` |" >> $GITHUB_STEP_SUMMARY
|
|
||||||
@@ -1,126 +0,0 @@
|
|||||||
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
#
|
|
||||||
# FILE INFORMATION
|
|
||||||
# DEFGROUP: Gitea.Workflow
|
|
||||||
# INGROUP: MokoStandards.Deploy
|
|
||||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards-API
|
|
||||||
# PATH: /templates/workflows/joomla/deploy-manual.yml.template
|
|
||||||
# VERSION: 04.07.00
|
|
||||||
# BRIEF: Manual SFTP deploy to dev server for Joomla repos
|
|
||||||
|
|
||||||
name: "Universal: Deploy to Dev (Manual)"
|
|
||||||
|
|
||||||
on:
|
|
||||||
workflow_dispatch:
|
|
||||||
inputs:
|
|
||||||
clear_remote:
|
|
||||||
description: 'Delete all remote files before uploading'
|
|
||||||
required: false
|
|
||||||
default: 'false'
|
|
||||||
type: boolean
|
|
||||||
|
|
||||||
env:
|
|
||||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
deploy:
|
|
||||||
name: SFTP Deploy to Dev
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout repository
|
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
|
||||||
|
|
||||||
- name: Setup PHP
|
|
||||||
run: |
|
|
||||||
php -v && composer --version
|
|
||||||
|
|
||||||
- name: Setup MokoStandards tools
|
|
||||||
env:
|
|
||||||
GA_TOKEN: ${{ secrets.GA_TOKEN || secrets.GA_TOKEN || github.token }}
|
|
||||||
MOKO_CLONE_TOKEN: ${{ secrets.GA_TOKEN || secrets.GA_TOKEN || github.token }}
|
|
||||||
MOKO_CLONE_HOST: ${{ secrets.GA_TOKEN && 'git.mokoconsulting.tech/MokoConsulting' || 'github.com/mokoconsulting-tech' }}
|
|
||||||
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GA_TOKEN || github.token }}"}}'
|
|
||||||
run: |
|
|
||||||
git clone --depth 1 --branch main --quiet \
|
|
||||||
"https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/MokoStandards-API.git" \
|
|
||||||
/tmp/mokostandards-api 2>/dev/null || true
|
|
||||||
if [ -d "/tmp/mokostandards-api" ] && [ -f "/tmp/mokostandards-api/composer.json" ]; then
|
|
||||||
cd /tmp/mokostandards-api && composer install --no-dev --no-interaction --quiet 2>/dev/null || true
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Check FTP configuration
|
|
||||||
id: check
|
|
||||||
env:
|
|
||||||
HOST: ${{ vars.DEV_FTP_HOST }}
|
|
||||||
PATH_VAR: ${{ vars.DEV_FTP_PATH }}
|
|
||||||
PORT: ${{ vars.DEV_FTP_PORT }}
|
|
||||||
run: |
|
|
||||||
if [ -z "$HOST" ] || [ -z "$PATH_VAR" ]; then
|
|
||||||
echo "DEV_FTP_HOST or DEV_FTP_PATH not configured -- cannot deploy"
|
|
||||||
echo "skip=true" >> "$GITHUB_OUTPUT"
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
echo "skip=false" >> "$GITHUB_OUTPUT"
|
|
||||||
echo "host=$HOST" >> "$GITHUB_OUTPUT"
|
|
||||||
|
|
||||||
REMOTE="${PATH_VAR%/}"
|
|
||||||
echo "remote=$REMOTE" >> "$GITHUB_OUTPUT"
|
|
||||||
|
|
||||||
[ -z "$PORT" ] && PORT="22"
|
|
||||||
echo "port=$PORT" >> "$GITHUB_OUTPUT"
|
|
||||||
|
|
||||||
- name: Deploy via SFTP
|
|
||||||
if: steps.check.outputs.skip != 'true'
|
|
||||||
env:
|
|
||||||
SFTP_KEY: ${{ secrets.DEV_FTP_KEY }}
|
|
||||||
SFTP_PASS: ${{ secrets.DEV_FTP_PASSWORD }}
|
|
||||||
SFTP_USER: ${{ vars.DEV_FTP_USERNAME }}
|
|
||||||
run: |
|
|
||||||
SOURCE_DIR="src"
|
|
||||||
[ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs"
|
|
||||||
[ ! -d "$SOURCE_DIR" ] && { echo "No src/ or htdocs/ -- nothing to deploy"; exit 0; }
|
|
||||||
|
|
||||||
printf '{"host":"%s","port":%s,"username":"%s","remotePath":"%s"' \
|
|
||||||
"${{ steps.check.outputs.host }}" "${{ steps.check.outputs.port }}" "$SFTP_USER" "${{ steps.check.outputs.remote }}" \
|
|
||||||
> /tmp/sftp-config.json
|
|
||||||
|
|
||||||
if [ -n "$SFTP_KEY" ]; then
|
|
||||||
echo "$SFTP_KEY" > /tmp/deploy_key
|
|
||||||
chmod 600 /tmp/deploy_key
|
|
||||||
printf ',"privateKeyPath":"/tmp/deploy_key"}' >> /tmp/sftp-config.json
|
|
||||||
else
|
|
||||||
printf ',"password":"%s"}' "$SFTP_PASS" >> /tmp/sftp-config.json
|
|
||||||
fi
|
|
||||||
|
|
||||||
DEPLOY_ARGS=(--path . --src-dir "$SOURCE_DIR" --config /tmp/sftp-config.json)
|
|
||||||
[ "${{ inputs.clear_remote }}" = "true" ] && DEPLOY_ARGS+=(--clear-remote)
|
|
||||||
|
|
||||||
PLATFORM=$(php /tmp/mokostandards-api/cli/platform_detect.php --path . 2>/dev/null || true)
|
|
||||||
if [ "$PLATFORM" = "waas-component" ] && [ -f "/tmp/mokostandards-api/deploy/deploy-joomla.php" ]; then
|
|
||||||
php /tmp/mokostandards-api/deploy/deploy-joomla.php "${DEPLOY_ARGS[@]}"
|
|
||||||
else
|
|
||||||
php /tmp/mokostandards-api/deploy/deploy-sftp.php "${DEPLOY_ARGS[@]}"
|
|
||||||
fi
|
|
||||||
|
|
||||||
rm -f /tmp/deploy_key /tmp/sftp-config.json
|
|
||||||
|
|
||||||
- name: Summary
|
|
||||||
if: always()
|
|
||||||
run: |
|
|
||||||
if [ "${{ steps.check.outputs.skip }}" = "true" ]; then
|
|
||||||
echo "### Deploy Skipped -- FTP not configured" >> $GITHUB_STEP_SUMMARY
|
|
||||||
else
|
|
||||||
echo "### Manual Dev Deploy Complete" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "| Field | Value |" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "|-------|-------|" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "| Host | \`${{ steps.check.outputs.host }}\` |" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "| Remote | \`${{ steps.check.outputs.remote }}\` |" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "| Clear | ${{ inputs.clear_remote }} |" >> $GITHUB_STEP_SUMMARY
|
|
||||||
fi
|
|
||||||
@@ -4,8 +4,8 @@
|
|||||||
#
|
#
|
||||||
# FILE INFORMATION
|
# FILE INFORMATION
|
||||||
# DEFGROUP: Gitea.Workflow
|
# DEFGROUP: Gitea.Workflow
|
||||||
# INGROUP: moko-platform.Security
|
# INGROUP: MokoStandards.Security
|
||||||
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/moko-platform
|
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/MokoStandards-API
|
||||||
# PATH: /templates/workflows/gitleaks.yml.template
|
# PATH: /templates/workflows/gitleaks.yml.template
|
||||||
# VERSION: 01.00.00
|
# VERSION: 01.00.00
|
||||||
# BRIEF: Secret scanning — detect leaked credentials, API keys, and tokens
|
# BRIEF: Secret scanning — detect leaked credentials, API keys, and tokens
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
# FILE INFORMATION
|
# FILE INFORMATION
|
||||||
# DEFGROUP: Gitea.Workflow
|
# DEFGROUP: Gitea.Workflow
|
||||||
# INGROUP: mokocli.Automation
|
# INGROUP: mokocli.Automation
|
||||||
# VERSION: 01.02.00
|
# VERSION: 01.05.02
|
||||||
# BRIEF: Auto-create feature branch when an issue is opened
|
# BRIEF: Auto-create feature branch when an issue is opened
|
||||||
|
|
||||||
name: "Universal: Issue Branch"
|
name: "Universal: Issue Branch"
|
||||||
@@ -19,7 +19,7 @@ permissions:
|
|||||||
issues: write
|
issues: write
|
||||||
|
|
||||||
env:
|
env:
|
||||||
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
|
MOKOGITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
create-branch:
|
create-branch:
|
||||||
@@ -28,8 +28,8 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Create branch and comment
|
- name: Create branch and comment
|
||||||
run: |
|
run: |
|
||||||
TOKEN="${{ secrets.GA_TOKEN }}"
|
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
|
||||||
API="${GITEA_URL}/api/v1/repos/${{ github.repository }}"
|
API="${MOKOGITEA_URL}/api/v1/repos/${{ github.repository }}"
|
||||||
ISSUE_NUM="${{ github.event.issue.number }}"
|
ISSUE_NUM="${{ github.event.issue.number }}"
|
||||||
ISSUE_TITLE="${{ github.event.issue.title }}"
|
ISSUE_TITLE="${{ github.event.issue.title }}"
|
||||||
|
|
||||||
@@ -58,7 +58,7 @@ jobs:
|
|||||||
echo "Created branch: ${BRANCH}"
|
echo "Created branch: ${BRANCH}"
|
||||||
|
|
||||||
# Comment on issue with branch link
|
# Comment on issue with branch link
|
||||||
REPO_URL="${GITEA_URL}/${{ github.repository }}"
|
REPO_URL="${MOKOGITEA_URL}/${{ github.repository }}"
|
||||||
BODY="Branch created: [\`${BRANCH}\`](${REPO_URL}/src/branch/${BRANCH})\n\n\`\`\`bash\ngit fetch origin\ngit checkout ${BRANCH}\n\`\`\`"
|
BODY="Branch created: [\`${BRANCH}\`](${REPO_URL}/src/branch/${BRANCH})\n\n\`\`\`bash\ngit fetch origin\ngit checkout ${BRANCH}\n\`\`\`"
|
||||||
|
|
||||||
curl -sf -X POST \
|
curl -sf -X POST \
|
||||||
|
|||||||
@@ -4,8 +4,8 @@
|
|||||||
#
|
#
|
||||||
# FILE INFORMATION
|
# FILE INFORMATION
|
||||||
# DEFGROUP: Gitea.Workflow
|
# DEFGROUP: Gitea.Workflow
|
||||||
# INGROUP: moko-platform.Notifications
|
# INGROUP: MokoStandards.Notifications
|
||||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards
|
||||||
# PATH: /.gitea/workflows/notify.yml
|
# PATH: /.gitea/workflows/notify.yml
|
||||||
# VERSION: 01.00.00
|
# VERSION: 01.00.00
|
||||||
# BRIEF: Push notifications via ntfy on release success or workflow failure
|
# BRIEF: Push notifications via ntfy on release success or workflow failure
|
||||||
|
|||||||
@@ -496,39 +496,26 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Trigger RC pre-release
|
- name: Trigger RC pre-release
|
||||||
env:
|
env:
|
||||||
GA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
MOKOGITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||||
REPO: ${{ github.repository }}
|
REPO: ${{ github.repository }}
|
||||||
BRANCH: ${{ github.head_ref }}
|
BRANCH: ${{ github.head_ref }}
|
||||||
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
|
MOKOGITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
|
||||||
run: |
|
run: |
|
||||||
curl -s -X POST "${GITEA_URL}/api/v1/repos/${REPO}/actions/workflows/pre-release.yml/dispatches" -H "Authorization: token ${GITEA_TOKEN}" -H "Content-Type: application/json" -d "{\"ref\":\"${BRANCH}\",\"inputs\":{\"stability\":\"release-candidate\"}}"
|
curl -s -X POST "${MOKOGITEA_URL}/api/v1/repos/${REPO}/actions/workflows/pre-release.yml/dispatches" -H "Authorization: token ${MOKOGITEA_TOKEN}" -H "Content-Type: application/json" -d "{\"ref\":\"${BRANCH}\",\"inputs\":{\"stability\":\"release-candidate\"}}"
|
||||||
echo "### Pre-Release" >> $GITHUB_STEP_SUMMARY
|
echo "### Pre-Release" >> $GITHUB_STEP_SUMMARY
|
||||||
echo "Triggered RC build on branch \`${BRANCH}\`" >> $GITHUB_STEP_SUMMARY
|
echo "Triggered RC build on branch \`${BRANCH}\`" >> $GITHUB_STEP_SUMMARY
|
||||||
|
|
||||||
# ── Issue Reporter ──────────────────────────────────────────────────────
|
# ── Issue Reporter ──────────────────────────────────────────────────────
|
||||||
report-issues:
|
report-issues:
|
||||||
name: Report Issues
|
name: Report Issues
|
||||||
runs-on: ubuntu-latest
|
|
||||||
needs: [branch-policy, validate]
|
needs: [branch-policy, validate]
|
||||||
if: >-
|
if: >-
|
||||||
always() &&
|
always() &&
|
||||||
needs.validate.result == 'failure'
|
needs.validate.result == 'failure'
|
||||||
|
uses: ./.mokogitea/workflows/ci-issue-reporter.yml
|
||||||
steps:
|
with:
|
||||||
- name: Checkout
|
gate: "PR Validation"
|
||||||
uses: actions/checkout@v4
|
workflow: "PR Check"
|
||||||
with:
|
severity: error
|
||||||
sparse-checkout: automation/ci-issue-reporter.sh
|
details: "PR validation failed (syntax, manifest, changelog, or source checks). See the CI run for the specific check that failed."
|
||||||
sparse-checkout-cone-mode: false
|
secrets: inherit
|
||||||
|
|
||||||
- name: "File issue for PR validation failure"
|
|
||||||
env:
|
|
||||||
GITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
|
||||||
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
|
|
||||||
run: |
|
|
||||||
chmod +x automation/ci-issue-reporter.sh
|
|
||||||
./automation/ci-issue-reporter.sh \
|
|
||||||
--gate "PR Validation" \
|
|
||||||
--workflow "PR Check" \
|
|
||||||
--severity error \
|
|
||||||
--details "PR validation failed (syntax, manifest, changelog, or source checks). See the CI run for the specific check that failed."
|
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ permissions:
|
|||||||
contents: read
|
contents: read
|
||||||
|
|
||||||
env:
|
env:
|
||||||
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
|
MOKOGITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
|
||||||
GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }}
|
GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }}
|
||||||
GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }}
|
GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }}
|
||||||
|
|
||||||
@@ -55,14 +55,14 @@ jobs:
|
|||||||
|
|
||||||
- name: Validate metadata against Joomla manifest
|
- name: Validate metadata against Joomla manifest
|
||||||
env:
|
env:
|
||||||
GITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
MOKOGITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||||
run: |
|
run: |
|
||||||
php ${MOKO_CLI}/joomla_metadata_validate.php \
|
php ${MOKO_CLI}/joomla_metadata_validate.php \
|
||||||
--path . \
|
--path . \
|
||||||
--token "${GITEA_TOKEN}" \
|
--token "${MOKOGITEA_TOKEN}" \
|
||||||
--org "${GITEA_ORG}" \
|
--org "${GITEA_ORG}" \
|
||||||
--repo "${GITEA_REPO}" \
|
--repo "${GITEA_REPO}" \
|
||||||
--api-base "${GITEA_URL}/api/v1" \
|
--api-base "${MOKOGITEA_URL}/api/v1" \
|
||||||
--ci
|
--ci
|
||||||
|
|
||||||
if [ $? -ne 0 ]; then
|
if [ $? -ne 0 ]; then
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
# INGROUP: mokocli.Release
|
# INGROUP: mokocli.Release
|
||||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
|
# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
|
||||||
# PATH: /templates/workflows/universal/pre-release.yml.template
|
# PATH: /templates/workflows/universal/pre-release.yml.template
|
||||||
# VERSION: 05.01.00
|
# VERSION: 05.02.00
|
||||||
# BRIEF: Auto pre-release on push to dev/alpha/beta/rc branches
|
# BRIEF: Auto pre-release on push to dev/alpha/beta/rc branches
|
||||||
|
|
||||||
name: "Universal: Pre-Release"
|
name: "Universal: Pre-Release"
|
||||||
@@ -59,6 +59,11 @@ jobs:
|
|||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
token: ${{ secrets.MOKOGITEA_TOKEN }}
|
token: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||||
ref: ${{ github.ref_name }}
|
ref: ${{ github.ref_name }}
|
||||||
|
submodules: recursive
|
||||||
|
|
||||||
|
- name: Update submodules to main
|
||||||
|
run: |
|
||||||
|
git submodule foreach --quiet 'git checkout main && git pull --quiet origin main' 2>/dev/null || true
|
||||||
|
|
||||||
- name: Setup mokocli tools
|
- name: Setup mokocli tools
|
||||||
env:
|
env:
|
||||||
|
|||||||
@@ -29,12 +29,20 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Rename branch
|
- name: Rename branch
|
||||||
|
env:
|
||||||
|
BRANCH: ${{ github.event.pull_request.head.ref }}
|
||||||
|
REPO: ${{ github.repository }}
|
||||||
|
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
|
||||||
|
TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||||
run: |
|
run: |
|
||||||
BRANCH="${{ github.event.pull_request.head.ref }}"
|
set -euo pipefail
|
||||||
|
# BRANCH is attacker-controlled (PR head ref). Strict allowlist before ANY use.
|
||||||
|
if ! printf '%s' "$BRANCH" | grep -Eq '^rc/[A-Za-z0-9._/-]+$'; then
|
||||||
|
echo "::error::Refusing unsafe branch name: $BRANCH"; exit 1
|
||||||
|
fi
|
||||||
SUFFIX="${BRANCH#rc/}"
|
SUFFIX="${BRANCH#rc/}"
|
||||||
DEV_BRANCH="dev/${SUFFIX}"
|
DEV_BRANCH="dev/${SUFFIX}"
|
||||||
API="${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}/api/v1/repos/${{ github.repository }}/branches"
|
API="${GITEA_URL}/api/v1/repos/${REPO}/branches"
|
||||||
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
|
|
||||||
|
|
||||||
# Create dev/ branch from rc/ branch
|
# Create dev/ branch from rc/ branch
|
||||||
STATUS=$(curl -sf -o /dev/null -w "%{http_code}" -X POST \
|
STATUS=$(curl -sf -o /dev/null -w "%{http_code}" -X POST \
|
||||||
@@ -42,25 +50,22 @@ jobs:
|
|||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
-d "{\"new_branch_name\": \"${DEV_BRANCH}\", \"old_branch_name\": \"${BRANCH}\"}" \
|
-d "{\"new_branch_name\": \"${DEV_BRANCH}\", \"old_branch_name\": \"${BRANCH}\"}" \
|
||||||
"${API}" 2>/dev/null || true)
|
"${API}" 2>/dev/null || true)
|
||||||
|
|
||||||
if [ "$STATUS" = "201" ]; then
|
if [ "$STATUS" = "201" ]; then
|
||||||
echo "Created branch: ${DEV_BRANCH}" >> $GITHUB_STEP_SUMMARY
|
echo "Created branch: ${DEV_BRANCH}" >> "$GITHUB_STEP_SUMMARY"
|
||||||
else
|
else
|
||||||
echo "::error::Failed to create ${DEV_BRANCH} from ${BRANCH} (HTTP ${STATUS})"
|
echo "::error::Failed to create ${DEV_BRANCH} from ${BRANCH} (HTTP ${STATUS})"; exit 1
|
||||||
exit 1
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Delete rc/ branch
|
# Read BRANCH from the environment inside PHP (getenv, no string interpolation -> no PHP injection)
|
||||||
ENCODED=$(php -r "echo rawurlencode('${BRANCH}');")
|
ENCODED=$(php -r 'echo rawurlencode(getenv("BRANCH"));')
|
||||||
STATUS=$(curl -sf -o /dev/null -w "%{http_code}" -X DELETE \
|
STATUS=$(curl -sf -o /dev/null -w "%{http_code}" -X DELETE \
|
||||||
-H "Authorization: token ${TOKEN}" \
|
-H "Authorization: token ${TOKEN}" \
|
||||||
"${API}/${ENCODED}" 2>/dev/null || true)
|
"${API}/${ENCODED}" 2>/dev/null || true)
|
||||||
|
|
||||||
if [ "$STATUS" = "204" ]; then
|
if [ "$STATUS" = "204" ]; then
|
||||||
echo "Deleted branch: ${BRANCH}" >> $GITHUB_STEP_SUMMARY
|
echo "Deleted branch: ${BRANCH}" >> "$GITHUB_STEP_SUMMARY"
|
||||||
else
|
else
|
||||||
echo "::warning::Failed to delete ${BRANCH} (HTTP ${STATUS})"
|
echo "::warning::Failed to delete ${BRANCH} (HTTP ${STATUS})"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "### RC Reverted" >> $GITHUB_STEP_SUMMARY
|
echo "### RC Reverted" >> "$GITHUB_STEP_SUMMARY"
|
||||||
echo "${BRANCH} → ${DEV_BRANCH}" >> $GITHUB_STEP_SUMMARY
|
echo "${BRANCH} → ${DEV_BRANCH}" >> "$GITHUB_STEP_SUMMARY"
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ jobs:
|
|||||||
- name: Check actor permission (admin only)
|
- name: Check actor permission (admin only)
|
||||||
id: perm
|
id: perm
|
||||||
env:
|
env:
|
||||||
TOKEN: ${{ secrets.MOKOGITEA_TOKEN || secrets.MOKOGITEA_TOKEN || github.token }}
|
TOKEN: ${{ secrets.MOKOGITEA_TOKEN || github.token }}
|
||||||
REPO: ${{ github.repository }}
|
REPO: ${{ github.repository }}
|
||||||
ACTOR: ${{ github.actor }}
|
ACTOR: ${{ github.actor }}
|
||||||
run: |
|
run: |
|
||||||
@@ -671,42 +671,30 @@ jobs:
|
|||||||
# ═══════════════════════════════════════════════════════════════════════
|
# ═══════════════════════════════════════════════════════════════════════
|
||||||
# Issue Reporter — file issues for failed gates
|
# Issue Reporter — file issues for failed gates
|
||||||
# ═══════════════════════════════════════════════════════════════════════
|
# ═══════════════════════════════════════════════════════════════════════
|
||||||
report-issues:
|
report-scripts:
|
||||||
name: "Report Issues"
|
name: "Report: Scripts Governance"
|
||||||
runs-on: ubuntu-latest
|
needs: [access_check, scripts_governance]
|
||||||
needs: [access_check, scripts_governance, repo_health]
|
|
||||||
if: >-
|
if: >-
|
||||||
always() &&
|
always() &&
|
||||||
(needs.scripts_governance.result == 'failure' ||
|
needs.scripts_governance.result == 'failure'
|
||||||
needs.repo_health.result == 'failure')
|
uses: ./.mokogitea/workflows/ci-issue-reporter.yml
|
||||||
|
with:
|
||||||
|
gate: "Scripts Governance"
|
||||||
|
workflow: "Repo Health"
|
||||||
|
severity: error
|
||||||
|
details: "Scripts directory policy violations detected. Review required and allowed directories."
|
||||||
|
secrets: inherit
|
||||||
|
|
||||||
steps:
|
report-health:
|
||||||
- name: Checkout
|
name: "Report: Repository Health"
|
||||||
uses: actions/checkout@v4
|
needs: [access_check, repo_health]
|
||||||
with:
|
if: >-
|
||||||
sparse-checkout: automation/ci-issue-reporter.sh
|
always() &&
|
||||||
sparse-checkout-cone-mode: false
|
needs.repo_health.result == 'failure'
|
||||||
|
uses: ./.mokogitea/workflows/ci-issue-reporter.yml
|
||||||
- name: "File issues for failed gates"
|
with:
|
||||||
env:
|
gate: "Repository Health"
|
||||||
GITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
workflow: "Repo Health"
|
||||||
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
|
severity: error
|
||||||
run: |
|
details: "Repository health checks failed — missing required artifacts, disallowed files, or content warnings. Check the CI run summary."
|
||||||
chmod +x automation/ci-issue-reporter.sh
|
secrets: inherit
|
||||||
REPORTER="./automation/ci-issue-reporter.sh"
|
|
||||||
WF="Repo Health"
|
|
||||||
|
|
||||||
report_gate() {
|
|
||||||
local gate="$1" result="$2" details="$3"
|
|
||||||
if [ "$result" = "failure" ]; then
|
|
||||||
"$REPORTER" --gate "$gate" --details "$details" --workflow "$WF" --severity error
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
report_gate "Scripts Governance" \
|
|
||||||
"${{ needs.scripts_governance.result }}" \
|
|
||||||
"Scripts directory policy violations detected. Review required and allowed directories."
|
|
||||||
|
|
||||||
report_gate "Repository Health" \
|
|
||||||
"${{ needs.repo_health.result }}" \
|
|
||||||
"Repository health checks failed — missing required artifacts, disallowed files, or content warnings. Check the CI run summary."
|
|
||||||
|
|||||||
@@ -1,98 +0,0 @@
|
|||||||
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
#
|
|
||||||
# FILE INFORMATION
|
|
||||||
# DEFGROUP: Gitea.Workflow
|
|
||||||
# INGROUP: moko-platform.Security
|
|
||||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
|
||||||
# PATH: /.gitea/workflows/security-audit.yml
|
|
||||||
# VERSION: 01.00.00
|
|
||||||
# BRIEF: Dependency vulnerability scanning for composer and npm packages
|
|
||||||
|
|
||||||
name: "Universal: Security Audit"
|
|
||||||
|
|
||||||
on:
|
|
||||||
schedule:
|
|
||||||
- cron: '0 6 * * 1' # Weekly on Monday at 06:00 UTC
|
|
||||||
pull_request:
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
paths:
|
|
||||||
- 'composer.json'
|
|
||||||
- 'composer.lock'
|
|
||||||
- 'package.json'
|
|
||||||
- 'package-lock.json'
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
|
|
||||||
env:
|
|
||||||
NTFY_URL: ${{ vars.NTFY_URL || 'https://ntfy.mokoconsulting.tech' }}
|
|
||||||
NTFY_TOPIC: ${{ vars.NTFY_TOPIC || 'gitea-security' }}
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
audit:
|
|
||||||
name: Dependency Audit
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Composer audit
|
|
||||||
if: hashFiles('composer.lock') != ''
|
|
||||||
run: |
|
|
||||||
echo "=== Composer Security Audit ==="
|
|
||||||
if ! command -v composer &> /dev/null; then
|
|
||||||
sudo apt-get update -qq
|
|
||||||
sudo apt-get install -y -qq php-cli composer >/dev/null 2>&1
|
|
||||||
fi
|
|
||||||
composer audit --format=plain 2>&1 | tee /tmp/composer-audit.txt
|
|
||||||
RESULT=$?
|
|
||||||
if [ $RESULT -ne 0 ]; then
|
|
||||||
echo "::warning::Composer vulnerabilities found"
|
|
||||||
echo "composer_vulnerable=true" >> "$GITHUB_ENV"
|
|
||||||
else
|
|
||||||
echo "No known vulnerabilities in composer dependencies"
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: NPM audit
|
|
||||||
if: hashFiles('package-lock.json') != ''
|
|
||||||
run: |
|
|
||||||
echo "=== NPM Security Audit ==="
|
|
||||||
npm audit --production 2>&1 | tee /tmp/npm-audit.txt || true
|
|
||||||
if npm audit --production 2>&1 | grep -q "found 0 vulnerabilities"; then
|
|
||||||
echo "No known vulnerabilities in npm dependencies"
|
|
||||||
else
|
|
||||||
echo "::warning::NPM vulnerabilities found"
|
|
||||||
echo "npm_vulnerable=true" >> "$GITHUB_ENV"
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Notify on vulnerabilities
|
|
||||||
if: env.composer_vulnerable == 'true' || env.npm_vulnerable == 'true'
|
|
||||||
run: |
|
|
||||||
REPO="${{ github.event.repository.name }}"
|
|
||||||
curl -sS \
|
|
||||||
-H "Title: ${REPO} has vulnerable dependencies" \
|
|
||||||
-H "Tags: lock,warning" \
|
|
||||||
-H "Priority: high" \
|
|
||||||
-d "Security audit found vulnerabilities. Review dependency updates." \
|
|
||||||
"${NTFY_URL}/${NTFY_TOPIC}" || true
|
|
||||||
|
|
||||||
|
|
||||||
- name: Joomla version audit
|
|
||||||
if: always()
|
|
||||||
run: |
|
|
||||||
if [ -f "monitoring/joomla-version-audit.php" ] && [ -n "$JOOMLA_SITES" ]; then
|
|
||||||
echo "$JOOMLA_SITES" > /tmp/sites.json
|
|
||||||
php monitoring/joomla-version-audit.php --sites /tmp/sites.json || true
|
|
||||||
echo "### Joomla Version Audit" >> $GITHUB_STEP_SUMMARY
|
|
||||||
rm -f /tmp/sites.json
|
|
||||||
else
|
|
||||||
echo "Joomla audit skipped (no script or JOOMLA_SITES_JSON not configured)"
|
|
||||||
fi
|
|
||||||
env:
|
|
||||||
JOOMLA_SITES: ${{ vars.JOOMLA_SITES_JSON }}
|
|
||||||
|
|
||||||
@@ -1,312 +0,0 @@
|
|||||||
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
#
|
|
||||||
# FILE INFORMATION
|
|
||||||
# DEFGROUP: Gitea.Workflow
|
|
||||||
# INGROUP: moko-platform.Universal
|
|
||||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
|
||||||
# PATH: /templates/workflows/update-server.yml
|
|
||||||
# VERSION: 05.00.00
|
|
||||||
# BRIEF: Pre-release build + update server XML for dev/alpha/beta/rc branches
|
|
||||||
#
|
|
||||||
# Thin wrapper around moko-platform CLI tools.
|
|
||||||
# Builds packages, updates updates.xml, and optionally deploys via SFTP.
|
|
||||||
#
|
|
||||||
# Joomla filters update entries by the user's "Minimum Stability" setting.
|
|
||||||
|
|
||||||
name: "Update Server"
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- 'dev'
|
|
||||||
- 'dev/**'
|
|
||||||
- 'alpha/**'
|
|
||||||
- 'beta/**'
|
|
||||||
- 'rc/**'
|
|
||||||
paths:
|
|
||||||
- 'src/**'
|
|
||||||
- 'htdocs/**'
|
|
||||||
pull_request:
|
|
||||||
types: [closed]
|
|
||||||
branches:
|
|
||||||
- 'dev'
|
|
||||||
- 'dev/**'
|
|
||||||
- 'alpha/**'
|
|
||||||
- 'beta/**'
|
|
||||||
- 'rc/**'
|
|
||||||
paths:
|
|
||||||
- 'src/**'
|
|
||||||
- 'htdocs/**'
|
|
||||||
workflow_dispatch:
|
|
||||||
inputs:
|
|
||||||
stability:
|
|
||||||
description: 'Stability tag'
|
|
||||||
required: true
|
|
||||||
default: 'development'
|
|
||||||
type: choice
|
|
||||||
options:
|
|
||||||
- development
|
|
||||||
- alpha
|
|
||||||
- beta
|
|
||||||
- rc
|
|
||||||
- stable
|
|
||||||
|
|
||||||
env:
|
|
||||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
|
||||||
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
|
|
||||||
GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }}
|
|
||||||
GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }}
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
update-xml:
|
|
||||||
name: Update Server
|
|
||||||
runs-on: release
|
|
||||||
if: >-
|
|
||||||
github.event.pull_request.merged == true || github.event_name == 'workflow_dispatch' || github.event_name == 'push'
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout repository
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
token: ${{ secrets.MOKOGITEA_TOKEN }}
|
|
||||||
fetch-depth: 0
|
|
||||||
|
|
||||||
- name: Setup moko-platform tools
|
|
||||||
env:
|
|
||||||
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
|
||||||
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
|
|
||||||
COMPOSER_AUTH: '{"http-basic":{"git.mokoconsulting.tech":{"username":"token","password":"${{ secrets.MOKOGITEA_TOKEN }}"}}}'
|
|
||||||
run: |
|
|
||||||
if ! command -v composer &> /dev/null; then
|
|
||||||
sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1
|
|
||||||
fi
|
|
||||||
# Always fetch latest CLI tools — never use stale cache from previous runs
|
|
||||||
rm -rf /tmp/moko-platform
|
|
||||||
git clone --depth 1 --branch main --quiet \
|
|
||||||
"https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git" \
|
|
||||||
/tmp/moko-platform 2>/dev/null || true
|
|
||||||
if [ -d "/tmp/moko-platform" ] && [ -f "/tmp/moko-platform/composer.json" ]; then
|
|
||||||
cd /tmp/moko-platform && composer install --no-dev --no-interaction --quiet 2>/dev/null || true
|
|
||||||
fi
|
|
||||||
echo "MOKO_CLI=/tmp/moko-platform/cli" >> "$GITHUB_ENV"
|
|
||||||
|
|
||||||
- name: Detect platform
|
|
||||||
id: platform
|
|
||||||
run: php ${MOKO_CLI}/manifest_read.php --path . --github-output
|
|
||||||
|
|
||||||
- name: Resolve stability and bump version
|
|
||||||
id: meta
|
|
||||||
run: |
|
|
||||||
BRANCH="${{ github.ref_name }}"
|
|
||||||
|
|
||||||
# Configure git for bot pushes
|
|
||||||
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
|
|
||||||
git config --local user.name "gitea-actions[bot]"
|
|
||||||
git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
|
|
||||||
|
|
||||||
# Auto-bump patch version
|
|
||||||
php ${MOKO_CLI}/version_bump.php --path . 2>/dev/null || true
|
|
||||||
|
|
||||||
VERSION=$(php ${MOKO_CLI}/version_read.php --path . 2>/dev/null || echo "0.0.0")
|
|
||||||
|
|
||||||
# Strip any existing suffix before applying stability
|
|
||||||
VERSION=$(echo "$VERSION" | sed 's/-\(dev\|alpha\|beta\|rc\)$//')
|
|
||||||
|
|
||||||
# Determine stability from branch or manual input
|
|
||||||
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
|
|
||||||
STABILITY="${{ inputs.stability }}"
|
|
||||||
elif [[ "$BRANCH" == rc/* ]]; then
|
|
||||||
STABILITY="rc"
|
|
||||||
elif [[ "$BRANCH" == beta/* ]]; then
|
|
||||||
STABILITY="beta"
|
|
||||||
elif [[ "$BRANCH" == alpha/* ]]; then
|
|
||||||
STABILITY="alpha"
|
|
||||||
else
|
|
||||||
STABILITY="development"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Version suffix per stability stream
|
|
||||||
case "$STABILITY" in
|
|
||||||
development) SUFFIX="-dev"; TAG="development" ;;
|
|
||||||
alpha) SUFFIX="-alpha"; TAG="alpha" ;;
|
|
||||||
beta) SUFFIX="-beta"; TAG="beta" ;;
|
|
||||||
rc) SUFFIX="-rc"; TAG="release-candidate" ;;
|
|
||||||
*) SUFFIX=""; TAG="stable" ;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
# Propagate version with stability suffix to all manifest files
|
|
||||||
php ${MOKO_CLI}/version_set_platform.php \
|
|
||||||
--path . --version "$VERSION" --branch "$BRANCH" --stability "$STABILITY" 2>/dev/null || true
|
|
||||||
php ${MOKO_CLI}/version_check.php --path . --fix 2>/dev/null || true
|
|
||||||
|
|
||||||
# Re-read version (now includes suffix from version_set_platform)
|
|
||||||
if [ -n "$SUFFIX" ]; then
|
|
||||||
VERSION="${VERSION}${SUFFIX}"
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
|
|
||||||
echo "stability=${STABILITY}" >> "$GITHUB_OUTPUT"
|
|
||||||
echo "suffix=${SUFFIX}" >> "$GITHUB_OUTPUT"
|
|
||||||
echo "tag=${TAG}" >> "$GITHUB_OUTPUT"
|
|
||||||
echo "display_version=${VERSION}" >> "$GITHUB_OUTPUT"
|
|
||||||
|
|
||||||
# Commit version bump if changed
|
|
||||||
git add -A
|
|
||||||
git diff --cached --quiet || {
|
|
||||||
git commit -m "chore(version): auto-bump ${VERSION} [skip ci]" \
|
|
||||||
--author="gitea-actions[bot] <gitea-actions[bot]@mokoconsulting.tech>"
|
|
||||||
git push
|
|
||||||
}
|
|
||||||
|
|
||||||
- name: Create release and upload package
|
|
||||||
id: package
|
|
||||||
run: |
|
|
||||||
VERSION="${{ steps.meta.outputs.version }}"
|
|
||||||
TAG="${{ steps.meta.outputs.tag }}"
|
|
||||||
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
|
||||||
|
|
||||||
# Create or update Gitea release
|
|
||||||
php ${MOKO_CLI}/release_create.php \
|
|
||||||
--path . --version "$VERSION" --tag "$TAG" \
|
|
||||||
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
|
|
||||||
--repo "${GITEA_REPO}" --branch "${{ github.ref_name }}" --prerelease
|
|
||||||
|
|
||||||
# Build package and upload
|
|
||||||
php ${MOKO_CLI}/release_package.php \
|
|
||||||
--path . --version "$VERSION" --tag "$TAG" \
|
|
||||||
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
|
|
||||||
--repo "${GITEA_REPO}" --output /tmp || true
|
|
||||||
|
|
||||||
- name: Update updates.xml
|
|
||||||
if: steps.platform.outputs.platform == 'joomla'
|
|
||||||
run: |
|
|
||||||
VERSION="${{ steps.meta.outputs.version }}"
|
|
||||||
STABILITY="${{ steps.meta.outputs.stability }}"
|
|
||||||
SHA256="${{ steps.package.outputs.sha256_zip }}"
|
|
||||||
|
|
||||||
if [ ! -f "updates.xml" ]; then
|
|
||||||
echo "No updates.xml — skipping"
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
SHA_FLAG=""
|
|
||||||
[ -n "$SHA256" ] && SHA_FLAG="--sha ${SHA256}"
|
|
||||||
|
|
||||||
php ${MOKO_CLI}/updates_xml_build.php \
|
|
||||||
--path . --version "${VERSION}" --stability "${STABILITY}" \
|
|
||||||
--gitea-url "${GITEA_URL}" --org "${GITEA_ORG}" --repo "${GITEA_REPO}" \
|
|
||||||
${SHA_FLAG}
|
|
||||||
|
|
||||||
# Commit and push updates.xml
|
|
||||||
git add updates.xml
|
|
||||||
git diff --cached --quiet || {
|
|
||||||
git commit -m "chore: update ${STABILITY} channel ${VERSION} [skip ci]"
|
|
||||||
git push
|
|
||||||
}
|
|
||||||
|
|
||||||
- name: Sync updates.xml to main
|
|
||||||
if: github.ref_name != 'main' && steps.platform.outputs.platform == 'joomla'
|
|
||||||
run: |
|
|
||||||
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
|
||||||
GITEA_TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
|
|
||||||
|
|
||||||
FILE_SHA=$(curl -sf -H "Authorization: token ${GITEA_TOKEN}" \
|
|
||||||
"${API_BASE}/contents/updates.xml?ref=main" | python3 -c "import sys,json; print(json.load(sys.stdin).get('sha',''))" 2>/dev/null || true)
|
|
||||||
|
|
||||||
if [ -n "$FILE_SHA" ] && [ -f "updates.xml" ]; then
|
|
||||||
python3 -c "
|
|
||||||
import base64, json, urllib.request, sys
|
|
||||||
with open('updates.xml', 'rb') as f:
|
|
||||||
content = base64.b64encode(f.read()).decode()
|
|
||||||
payload = json.dumps({
|
|
||||||
'content': content,
|
|
||||||
'sha': '${FILE_SHA}',
|
|
||||||
'message': 'chore: sync updates.xml from ${{ steps.meta.outputs.stability }} [skip ci]',
|
|
||||||
'branch': 'main'
|
|
||||||
}).encode()
|
|
||||||
req = urllib.request.Request(
|
|
||||||
'${API_BASE}/contents/updates.xml',
|
|
||||||
data=payload, method='PUT',
|
|
||||||
headers={
|
|
||||||
'Authorization': 'token ${GITEA_TOKEN}',
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
})
|
|
||||||
try:
|
|
||||||
urllib.request.urlopen(req)
|
|
||||||
print('updates.xml synced to main')
|
|
||||||
except Exception as e:
|
|
||||||
print(f'WARNING: sync to main failed: {e}', file=sys.stderr)
|
|
||||||
"
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: SFTP deploy to dev server
|
|
||||||
if: contains(github.ref, 'dev/') || github.ref == 'refs/heads/dev'
|
|
||||||
env:
|
|
||||||
DEV_HOST: ${{ vars.DEV_FTP_HOST }}
|
|
||||||
DEV_PATH: ${{ vars.DEV_FTP_PATH }}
|
|
||||||
DEV_SUFFIX: ${{ vars.DEV_FTP_SUFFIX }}
|
|
||||||
DEV_USER: ${{ vars.DEV_FTP_USERNAME }}
|
|
||||||
DEV_PORT: ${{ vars.DEV_FTP_PORT }}
|
|
||||||
DEV_KEY: ${{ secrets.DEV_FTP_KEY }}
|
|
||||||
DEV_PASS: ${{ secrets.DEV_FTP_PASSWORD }}
|
|
||||||
run: |
|
|
||||||
# Permission check: admin or maintain role required
|
|
||||||
ACTOR="${{ github.actor }}"
|
|
||||||
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
|
||||||
|
|
||||||
PERMISSION=$(curl -sf -H "Authorization: token ${{ secrets.MOKOGITEA_TOKEN }}" \
|
|
||||||
"${API_BASE}/collaborators/${ACTOR}/permission" 2>/dev/null | \
|
|
||||||
python3 -c "import sys,json; print(json.load(sys.stdin).get('permission','read'))" 2>/dev/null || echo "read")
|
|
||||||
case "$PERMISSION" in
|
|
||||||
admin|maintain|write) ;;
|
|
||||||
*)
|
|
||||||
echo "Deploy denied: ${ACTOR} has '${PERMISSION}' — requires admin, maintain, or write"
|
|
||||||
exit 0
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
[ -z "$DEV_HOST" ] || [ -z "$DEV_PATH" ] && { echo "DEV FTP not configured — skipping SFTP"; exit 0; }
|
|
||||||
|
|
||||||
SOURCE_DIR="src"
|
|
||||||
[ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs"
|
|
||||||
[ ! -d "$SOURCE_DIR" ] && exit 0
|
|
||||||
|
|
||||||
PORT="${DEV_PORT:-22}"
|
|
||||||
REMOTE="${DEV_PATH%/}"
|
|
||||||
[ -n "$DEV_SUFFIX" ] && REMOTE="${REMOTE}/${DEV_SUFFIX#/}"
|
|
||||||
|
|
||||||
printf '{"host":"%s","port":%s,"username":"%s","remotePath":"%s"' \
|
|
||||||
"$DEV_HOST" "$PORT" "$DEV_USER" "$REMOTE" > /tmp/sftp-config.json
|
|
||||||
if [ -n "$DEV_KEY" ]; then
|
|
||||||
echo "$DEV_KEY" > /tmp/deploy_key && chmod 600 /tmp/deploy_key
|
|
||||||
printf ',"privateKeyPath":"/tmp/deploy_key"}' >> /tmp/sftp-config.json
|
|
||||||
else
|
|
||||||
printf ',"password":"%s"}' "$DEV_PASS" >> /tmp/sftp-config.json
|
|
||||||
fi
|
|
||||||
|
|
||||||
PLATFORM=$(php ${MOKO_CLI}/platform_detect.php --path . 2>/dev/null || true)
|
|
||||||
if [ "$PLATFORM" = "waas-component" ] && [ -f "${MOKO_CLI}/../deploy/deploy-joomla.php" ]; then
|
|
||||||
php ${MOKO_CLI}/../deploy/deploy-joomla.php --path . --src-dir "$SOURCE_DIR" --config /tmp/sftp-config.json
|
|
||||||
elif [ -f "${MOKO_CLI}/../deploy/deploy-sftp.php" ]; then
|
|
||||||
php ${MOKO_CLI}/../deploy/deploy-sftp.php --path . --src-dir "$SOURCE_DIR" --config /tmp/sftp-config.json
|
|
||||||
fi
|
|
||||||
rm -f /tmp/deploy_key /tmp/sftp-config.json
|
|
||||||
echo "SFTP deploy to dev complete" >> $GITHUB_STEP_SUMMARY
|
|
||||||
|
|
||||||
- name: Summary
|
|
||||||
if: always()
|
|
||||||
run: |
|
|
||||||
VERSION="${{ steps.meta.outputs.version }}"
|
|
||||||
STABILITY="${{ steps.meta.outputs.stability }}"
|
|
||||||
DISPLAY="${{ steps.meta.outputs.display_version }}"
|
|
||||||
echo "## Update Server" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "| Field | Value |" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "|-------|-------|" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "| Stability | \`${STABILITY}\` |" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "| Version | \`${DISPLAY}\` |" >> $GITHUB_STEP_SUMMARY
|
|
||||||
@@ -0,0 +1,130 @@
|
|||||||
|
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
#
|
||||||
|
# FILE INFORMATION
|
||||||
|
# DEFGROUP: Gitea.Workflow.Template
|
||||||
|
# INGROUP: MokoStandards.CI
|
||||||
|
# REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Joomla
|
||||||
|
# PATH: /.mokogitea/workflows/version-set.yml
|
||||||
|
# VERSION: 01.00.00
|
||||||
|
# BRIEF: Set or reset the extension version across all version-bearing files
|
||||||
|
|
||||||
|
name: "Joomla: Set Version"
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
version:
|
||||||
|
description: "Version number (e.g. 01.00.00)"
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
branch:
|
||||||
|
description: "Branch to update (default: current)"
|
||||||
|
required: false
|
||||||
|
type: string
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
|
||||||
|
env:
|
||||||
|
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
set-version:
|
||||||
|
name: Set Version to ${{ inputs.version }}
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Validate version format
|
||||||
|
run: |
|
||||||
|
VERSION="${{ inputs.version }}"
|
||||||
|
if ! echo "$VERSION" | grep -qP '^\d{2}\.\d{2}\.\d{2}$'; then
|
||||||
|
echo "::error::Invalid version format '${VERSION}' — expected XX.YY.ZZ (e.g. 01.00.00)"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "VERSION=${VERSION}" >> "$GITHUB_ENV"
|
||||||
|
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.MOKOGITEA_TOKEN || github.token }}
|
||||||
|
ref: ${{ inputs.branch || github.ref }}
|
||||||
|
fetch-depth: 1
|
||||||
|
|
||||||
|
- name: Update manifest version
|
||||||
|
run: |
|
||||||
|
MANIFEST=""
|
||||||
|
for XML_FILE in $(find . -maxdepth 3 -name "*.xml" -not -path "./.git/*" -not -path "./vendor/*"); do
|
||||||
|
if grep -q "<extension" "$XML_FILE" 2>/dev/null; then
|
||||||
|
MANIFEST="$XML_FILE"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ -z "$MANIFEST" ]; then
|
||||||
|
echo "::warning::No Joomla extension manifest found — skipping manifest update"
|
||||||
|
else
|
||||||
|
OLD_VER=$(grep -oP '<version>\K[^<]+' "$MANIFEST" | head -1)
|
||||||
|
sed -i "s|<version>${OLD_VER}</version>|<version>${VERSION}</version>|" "$MANIFEST"
|
||||||
|
echo "Manifest: ${OLD_VER} → ${VERSION} (${MANIFEST})"
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Update README.md version
|
||||||
|
run: |
|
||||||
|
if [ -f "README.md" ]; then
|
||||||
|
if grep -qP '^\s*VERSION:\s*\d' README.md; then
|
||||||
|
sed -i -E "s/(VERSION:\s*)[0-9]{2}\.[0-9]{2}\.[0-9]{2}/\1${VERSION}/" README.md
|
||||||
|
echo "README.md version updated to ${VERSION}"
|
||||||
|
else
|
||||||
|
echo "::warning::No VERSION line found in README.md — skipping"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Update CHANGELOG.md
|
||||||
|
run: |
|
||||||
|
if [ -f "CHANGELOG.md" ]; then
|
||||||
|
DATE=$(date +%Y-%m-%d)
|
||||||
|
# Check if this version already has an entry
|
||||||
|
if grep -q "^\#\# \[${VERSION}\]" CHANGELOG.md; then
|
||||||
|
echo "CHANGELOG.md already has entry for ${VERSION} — skipping"
|
||||||
|
else
|
||||||
|
# Insert new version entry after [Unreleased] or at the top after header
|
||||||
|
if grep -q '^\#\# \[Unreleased\]' CHANGELOG.md; then
|
||||||
|
sed -i "/^\#\# \[Unreleased\]/a\\\\n## [${VERSION}] --- ${DATE}" CHANGELOG.md
|
||||||
|
else
|
||||||
|
sed -i "/^\# Changelog/a\\\\n## [Unreleased]\n\n## [${VERSION}] --- ${DATE}" CHANGELOG.md
|
||||||
|
fi
|
||||||
|
echo "CHANGELOG.md: added entry for ${VERSION}"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "::warning::No CHANGELOG.md found — skipping"
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Update FILE INFORMATION blocks
|
||||||
|
run: |
|
||||||
|
# Update VERSION in file header blocks (# VERSION: XX.YY.ZZ)
|
||||||
|
find . -maxdepth 1 -type f \( -name "*.yml" -o -name "*.yaml" -o -name "*.php" -o -name "*.md" \) \
|
||||||
|
-not -path "./.git/*" -not -path "./vendor/*" -print0 2>/dev/null | \
|
||||||
|
while IFS= read -r -d '' FILE; do
|
||||||
|
if head -20 "$FILE" | grep -qP '^\s*#?\s*VERSION:\s*\d{2}\.\d{2}\.\d{2}'; then
|
||||||
|
sed -i -E "s/(#?\s*VERSION:\s*)[0-9]{2}\.[0-9]{2}\.[0-9]{2}/\1${VERSION}/" "$FILE"
|
||||||
|
echo "Updated FILE INFORMATION VERSION in ${FILE}"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
- name: Commit and push
|
||||||
|
run: |
|
||||||
|
git config user.name "Moko Consulting [bot]"
|
||||||
|
git config user.email "hello@mokoconsulting.tech"
|
||||||
|
git add -A
|
||||||
|
if git diff --cached --quiet; then
|
||||||
|
echo "No version changes detected — nothing to commit"
|
||||||
|
else
|
||||||
|
git commit -m "chore: set version to ${VERSION} [skip bump]
|
||||||
|
|
||||||
|
Authored-by: Moko Consulting"
|
||||||
|
git push
|
||||||
|
echo "### Version Set" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "Version updated to \`${VERSION}\` on branch \`${GITHUB_REF_NAME}\`" >> $GITHUB_STEP_SUMMARY
|
||||||
|
fi
|
||||||
@@ -13,6 +13,7 @@
|
|||||||
name: "Universal: Workflow Sync Trigger"
|
name: "Universal: Workflow Sync Trigger"
|
||||||
|
|
||||||
on:
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
pull_request:
|
pull_request:
|
||||||
types: [closed]
|
types: [closed]
|
||||||
branches:
|
branches:
|
||||||
@@ -26,8 +27,9 @@ jobs:
|
|||||||
name: Sync workflows to live repos
|
name: Sync workflows to live repos
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
if: >-
|
if: >-
|
||||||
github.event.pull_request.merged == true &&
|
github.event_name == 'workflow_dispatch' ||
|
||||||
!contains(github.event.pull_request.title, '[skip sync]')
|
(github.event.pull_request.merged == true &&
|
||||||
|
!contains(github.event.pull_request.title, '[skip sync]'))
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Determine platform from repo name
|
- name: Determine platform from repo name
|
||||||
@@ -49,8 +51,14 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
MOKOGITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
MOKOGITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||||
run: |
|
run: |
|
||||||
GITEA_URL="${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}"
|
MOKOGITEA_URL="${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}"
|
||||||
git clone --depth 1 "${GITEA_URL}/MokoConsulting/mokocli.git" /tmp/mokocli
|
git clone --depth 1 "${MOKOGITEA_URL}/MokoConsulting/mokocli.git" /tmp/mokocli
|
||||||
|
|
||||||
|
- name: Install PHP
|
||||||
|
run: |
|
||||||
|
if ! command -v php &> /dev/null; then
|
||||||
|
apt-get update -qq && apt-get install -y -qq php-cli php-json php-curl > /dev/null 2>&1
|
||||||
|
fi
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
+34
-3
@@ -2,16 +2,16 @@
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
## [01.02.00] --- 2026-06-21
|
## [01.05.00] --- 2026-06-28
|
||||||
|
|
||||||
|
|
||||||
<!-- VERSION: 01.02.00 -->
|
<!-- VERSION: 01.05.02 -->
|
||||||
|
|
||||||
All notable changes to MokoSuiteOpenGraph will be documented in this file.
|
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/).
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
||||||
|
|
||||||
## [01.02.00] --- 2026-06-21
|
## [01.05.00] --- 2026-06-28
|
||||||
|
|
||||||
### Security
|
### Security
|
||||||
- Fix JSON-LD XSS vulnerability via `</script>` injection in content data (#34)
|
- Fix JSON-LD XSS vulnerability via `</script>` injection in content data (#34)
|
||||||
@@ -20,6 +20,26 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|||||||
- Fix multilingual data corruption in content plugin load/save (#41)
|
- Fix multilingual data corruption in content plugin load/save (#41)
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
- Fediverse/Mastodon `fediverse:creator` meta tag — first extension on any CMS to support this (#57)
|
||||||
|
- Live character count indicators on OG title, OG description, SEO title, meta description fields with color-coded warnings (#58)
|
||||||
|
- LinkedIn social preview card in article/menu editor alongside Facebook and Twitter/X previews (#61)
|
||||||
|
- `og:video` meta tag support with per-article video URL field, auto-detect MIME type for YouTube/Vimeo/direct files (#59)
|
||||||
|
- Pinterest rich pin tags: `article:tag` from Joomla content tags, `product:availability` from MokoSuiteShop stock (#60)
|
||||||
|
- FAQ JSON-LD schema with auto-detection from article h3/h4 headings (#62)
|
||||||
|
- HowTo JSON-LD schema with auto-detection from ordered lists (#63)
|
||||||
|
- Event JSON-LD schema with per-article event fields (dates, venue, tickets) (#64)
|
||||||
|
- LocalBusiness JSON-LD schema with global plugin configuration (#65)
|
||||||
|
- Recipe JSON-LD schema with per-article fields (times, ingredients, nutrition) (#66)
|
||||||
|
- VideoObject JSON-LD schema for articles with video URLs (#67)
|
||||||
|
- SEO content scoring panel with 7 checks and pass/fail indicators (#68)
|
||||||
|
- Discord, Mastodon, and Slack social preview cards in editor (#69)
|
||||||
|
- Custom JSON-LD schema builder — per-article textarea for any schema.org type (#70)
|
||||||
|
- AI-powered meta tag generation with Claude and OpenAI API support (#71)
|
||||||
|
- XML sitemap generation on article save, respects noindex directives (#72)
|
||||||
|
- OG coverage dashboard in tag manager with coverage percentage (#73)
|
||||||
|
- Per-platform image resizing: Twitter 1200x600, Pinterest 1000x1500, WhatsApp 400x400 (#74)
|
||||||
|
- PHPUnit test suite with 16 unit tests for JsonLdBuilder (#75)
|
||||||
|
- OpenAPI 3.0 specification for REST API (#80)
|
||||||
- Site-wide default OG title and description plugin parameters
|
- Site-wide default OG title and description plugin parameters
|
||||||
- Discord embed color via `theme-color` meta tag (color picker in plugin config)
|
- Discord embed color via `theme-color` meta tag (color picker in plugin config)
|
||||||
- LinkedIn article tags: `article:published_time`, `article:modified_time`, `article:author`
|
- LinkedIn article tags: `article:published_time`, `article:modified_time`, `article:author`
|
||||||
@@ -45,6 +65,17 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|||||||
- Facebook App ID and Telegram channel support
|
- Facebook App ID and Telegram channel support
|
||||||
- Database table `#__mokoog_tags` with multilingual unique key
|
- Database table `#__mokoog_tags` with multilingual unique key
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Fix SQL driver attribute `mysql` → `mysqli` in component manifest preventing fresh installs
|
||||||
|
- Add exception logging to BatchController batch skip (#76)
|
||||||
|
- Align form maxlength attributes with DB schema limits (#77)
|
||||||
|
- Add `strip_tags()` input sanitization on OG text fields (#79)
|
||||||
|
- Only emit `og:video:secure_url` for HTTPS URLs
|
||||||
|
- Only emit `og:video:width/height` for direct files, not embeds
|
||||||
|
- Consolidate duplicate MokoSuiteShop product blocks
|
||||||
|
- Fix stale `com_virtuemart` reference in SQL comment
|
||||||
|
- Use component language keys for og_video field in tag.xml
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
- Consolidated article DB queries into single cached lookup — 5 queries reduced to 1 (#38)
|
- Consolidated article DB queries into single cached lookup — 5 queries reduced to 1 (#38)
|
||||||
- Dynamic `og:image:width`/`og:image:height` from actual image dimensions instead of hardcoded (#39)
|
- Dynamic `og:image:width`/`og:image:height` from actual image dimensions instead of hardcoded (#39)
|
||||||
|
|||||||
+36
-18
@@ -1,28 +1,46 @@
|
|||||||
# Code of Conduct
|
<!-- 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
|
||||||
|
|
||||||
|
This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License (./LICENSE).
|
||||||
|
|
||||||
|
# FILE INFORMATION
|
||||||
|
DEFGROUP: Template-Joomla
|
||||||
|
INGROUP: Template-Joomla.Documentation
|
||||||
|
REPO: https://github.com/mokoconsulting-tech/Template-Joomla/
|
||||||
|
VERSION: 01.05.02
|
||||||
|
PATH: ./CODE_OF_CONDUCT.md
|
||||||
|
BRIEF: Community expectations and enforcement guidelines
|
||||||
|
NOTE: Adapted with attribution from the Contributor Covenant v2.1
|
||||||
|
-->
|
||||||
|
|
||||||
|
# Contributor Covenant Code of Conduct
|
||||||
|
|
||||||
## Our Pledge
|
## Our Pledge
|
||||||
|
We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone.
|
||||||
We pledge to make participation in our project a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation.
|
|
||||||
|
|
||||||
## Our Standards
|
## Our Standards
|
||||||
|
- Be empathetic and kind
|
||||||
|
- Be respectful of differing opinions
|
||||||
|
- Accept constructive feedback
|
||||||
|
- Own mistakes and learn from them
|
||||||
|
|
||||||
Examples of behavior that contributes to a positive environment:
|
Unacceptable behavior includes sexualized language/imagery, trolling, harassment, doxing, and other inappropriate conduct.
|
||||||
|
|
||||||
- Using welcoming and inclusive language
|
|
||||||
- Being respectful of differing viewpoints and experiences
|
|
||||||
- Gracefully accepting constructive criticism
|
|
||||||
- Focusing on what is best for the community
|
|
||||||
|
|
||||||
Examples of unacceptable behavior:
|
|
||||||
|
|
||||||
- Trolling, insulting/derogatory comments, and personal or political attacks
|
|
||||||
- Public or private harassment
|
|
||||||
- Publishing others' private information without explicit permission
|
|
||||||
|
|
||||||
## Enforcement
|
## Enforcement
|
||||||
|
Report incidents to **hello@mokoconsulting.tech** or through GitHub Discussions if you prefer a community-visible approach. Private complaints will be reviewed promptly and fairly.
|
||||||
|
|
||||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the project team at hello@mokoconsulting.tech. All complaints will be reviewed and investigated.
|
## Enforcement Guidelines
|
||||||
|
1. **Correction** — Private warning
|
||||||
|
2. **Warning** — Formal warning and limited interaction
|
||||||
|
3. **Temporary Ban** — Time-boxed exclusion
|
||||||
|
4. **Permanent Ban** — Removal from the community
|
||||||
|
|
||||||
## Attribution
|
## Attribution
|
||||||
|
Adapted from the Contributor Covenant v2.1.
|
||||||
This Code of Conduct is adapted from the [Contributor Covenant](https://www.contributor-covenant.org/), version 2.1.
|
|
||||||
|
|||||||
+149
-22
@@ -1,34 +1,161 @@
|
|||||||
# Contributing to MokoJoomOpenGraph
|
# Contributing to Moko Consulting Projects
|
||||||
|
|
||||||
Thank you for your interest in contributing to MokoJoomOpenGraph.
|
Thank you for your interest in contributing. All Moko Consulting repositories follow this universal workflow and version policy.
|
||||||
|
|
||||||
## Getting Started
|
## Branching Workflow
|
||||||
|
|
||||||
1. Fork the repository on Gitea
|
```
|
||||||
2. Create a feature branch from `dev` (`feature/your-feature`)
|
feature/* ──PR──> dev ──draft PR──> (renamed to rc) ──merge──> main
|
||||||
3. Make your changes following the coding standards below
|
```
|
||||||
4. Submit a pull request targeting `dev`
|
|
||||||
|
|
||||||
## Branch Strategy
|
### Step by step
|
||||||
|
|
||||||
- `main` — stable releases only
|
1. **Create a feature branch** from `dev`:
|
||||||
- `dev` — active development
|
```bash
|
||||||
- `feature/*` — new features (target `dev`)
|
git checkout dev && git pull
|
||||||
- `fix/*` — bug fixes (target `dev`)
|
git checkout -b feature/my-change
|
||||||
- `hotfix/*` — urgent fixes (target `dev` or `main`)
|
```
|
||||||
|
|
||||||
## Coding Standards
|
2. **Work and commit** on your feature branch. Push to origin.
|
||||||
|
|
||||||
- PHP 8.1+ required
|
3. **Open a PR**: `feature/my-change` → `dev`. After review and checks, merge it.
|
||||||
- Follow Joomla coding standards
|
|
||||||
- SPDX license headers on all PHP files
|
4. **When ready for release**, open a **draft PR**: `dev` → `main`.
|
||||||
- Use `SubscriberInterface` for event subscription
|
- This automatically renames the source branch to `rc` (release candidate)
|
||||||
- Use `bind() -> check() -> store()` for Table operations
|
- An RC pre-release is built and uploaded
|
||||||
|
|
||||||
|
5. **Alpha and beta branches** are created by manually renaming the branch before the RC stage:
|
||||||
|
- Rename `dev` to `alpha` for early testing → alpha pre-release is built
|
||||||
|
- Rename `alpha` to `beta` for feature-complete testing → beta pre-release is built
|
||||||
|
- When the draft PR is created, the branch is renamed to `rc`
|
||||||
|
|
||||||
|
6. **Once PR checks pass** on the `rc` branch, mark the PR as ready and merge to `main`.
|
||||||
|
|
||||||
|
7. **Merging to main** triggers the stable release pipeline:
|
||||||
|
- Minor version bump (e.g., `02.09.xx` → `02.10.00`)
|
||||||
|
- Stability suffix stripped (clean version)
|
||||||
|
- Gitea release created with ZIP/tar.gz packages
|
||||||
|
- `updates.xml` updated (Joomla extensions)
|
||||||
|
- `dev` branch recreated from `main`
|
||||||
|
|
||||||
|
### Branch summary
|
||||||
|
|
||||||
|
| Branch | Purpose | Created by |
|
||||||
|
|--------|---------|-----------|
|
||||||
|
| `feature/*` | New features and fixes | Developer |
|
||||||
|
| `dev` | Integration branch | Auto-recreated after release |
|
||||||
|
| `alpha` | Alpha pre-release testing | Manual rename from `dev` |
|
||||||
|
| `beta` | Beta pre-release testing | Manual rename from `alpha` |
|
||||||
|
| `rc` | Release candidate | Auto-renamed on draft PR to main |
|
||||||
|
| `main` | Stable releases | Protected, merge only |
|
||||||
|
| `version/XX.YY.ZZ` | Archived release snapshots | Auto-created by CI |
|
||||||
|
|
||||||
|
### Protected branches
|
||||||
|
|
||||||
|
| Branch | Direct push | Merge via |
|
||||||
|
|--------|------------|-----------|
|
||||||
|
| `main` | Blocked (CI bot whitelisted) | PR merge only |
|
||||||
|
| `dev` | Blocked (CI bot whitelisted) | PR merge from feature/* |
|
||||||
|
| `rc` | Blocked (CI bot whitelisted) | Auto-created on draft PR |
|
||||||
|
| `alpha` | Blocked (CI bot whitelisted) | Manual rename |
|
||||||
|
| `beta` | Blocked (CI bot whitelisted) | Manual rename |
|
||||||
|
| `feature/*` | Open | N/A (source branch) |
|
||||||
|
|
||||||
|
## Version Policy
|
||||||
|
|
||||||
|
### Format
|
||||||
|
|
||||||
|
All versions use `XX.YY.ZZ` — three two-digit segments, zero-padded:
|
||||||
|
|
||||||
|
- **XX** — Major version (breaking changes)
|
||||||
|
- **YY** — Minor version (new features, bumped on release to main)
|
||||||
|
- **ZZ** — Patch version (auto-incremented on every push to dev/feature branches)
|
||||||
|
|
||||||
|
Rollover: patch `99` → `00` increments minor; minor `99` → `00` increments major.
|
||||||
|
|
||||||
|
### Stability suffixes
|
||||||
|
|
||||||
|
Each branch appends a suffix to indicate stability:
|
||||||
|
|
||||||
|
| Branch | Suffix | Example |
|
||||||
|
|--------|--------|---------|
|
||||||
|
| `main` | (none) | `02.09.00` |
|
||||||
|
| `dev` | `-dev` | `02.09.01-dev` |
|
||||||
|
| `feature/*` | `-dev` | `02.09.01-dev` |
|
||||||
|
| `alpha` | `-alpha` | `02.09.01-alpha` |
|
||||||
|
| `beta` | `-beta` | `02.09.01-beta` |
|
||||||
|
| `rc` | `-rc` | `02.09.01-rc` |
|
||||||
|
|
||||||
|
### Auto version bump
|
||||||
|
|
||||||
|
On every push to `dev`, `feature/*`, or `patch/*`:
|
||||||
|
|
||||||
|
1. Patch version incremented
|
||||||
|
2. Stability suffix `-dev` applied
|
||||||
|
3. All version-bearing files updated (manifests, CHANGELOG, PHP headers, etc.)
|
||||||
|
4. Commit created with `[skip ci]` to avoid loops
|
||||||
|
|
||||||
|
### Release version flow
|
||||||
|
|
||||||
|
Version bumps happen at specific release events:
|
||||||
|
|
||||||
|
| Event | Bump | Example |
|
||||||
|
|-------|------|---------|
|
||||||
|
| Feature merged to dev | Patch bump after dev release | `02.09.01-dev` → release → `02.09.02-dev` |
|
||||||
|
| Dev promoted to RC | Minor bump | `02.09.02-dev` → `02.10.00-rc` |
|
||||||
|
| RC merged to main | Minor bump | `02.10.00-rc` → `02.11.00` (stable) |
|
||||||
|
| Dev recreated from main | Patch bump | `02.11.00` → `02.11.01-dev` |
|
||||||
|
|
||||||
|
### Release stream copies
|
||||||
|
|
||||||
|
When a higher-stability release is published, copies are created for all lesser streams with the same base version:
|
||||||
|
|
||||||
|
- **RC `02.10.00-rc`** also creates: `02.10.00-dev`, `02.10.00-alpha`, `02.10.00-beta`
|
||||||
|
- **Stable `02.11.00`** also creates: `02.11.00-dev`, `02.11.00-alpha`, `02.11.00-beta`, `02.11.00-rc`
|
||||||
|
|
||||||
|
This ensures Joomla sites on ANY stability channel see the update (Joomla only shows versions higher than what's installed).
|
||||||
|
|
||||||
|
### Version files
|
||||||
|
|
||||||
|
The version tools update all files containing version stamps:
|
||||||
|
|
||||||
|
- `.mokogitea/manifest.xml` (canonical source)
|
||||||
|
- Joomla XML manifests (`<version>` tag)
|
||||||
|
- `README.md`, `CHANGELOG.md` (`VERSION:` pattern)
|
||||||
|
- `package.json`, `pyproject.toml`
|
||||||
|
- Any text file with a `VERSION: XX.YY.ZZ` label
|
||||||
|
|
||||||
|
Files synced from other repos (with a `# REPO:` header) are not touched.
|
||||||
|
|
||||||
|
## Code Standards
|
||||||
|
|
||||||
|
- **PHP**: PSR-12, tabs for indentation
|
||||||
|
- **Copyright**: all files must include the Moko Consulting copyright header
|
||||||
|
- **License**: SPDX identifier `GPL-3.0-or-later` (or as specified per repo)
|
||||||
|
- **Attribution**: use `Authored-by: Moko Consulting` in commits, not individual names
|
||||||
|
|
||||||
|
## Commit Messages
|
||||||
|
|
||||||
|
Use conventional commit format:
|
||||||
|
|
||||||
|
```
|
||||||
|
type(scope): short description
|
||||||
|
|
||||||
|
Optional body with context.
|
||||||
|
|
||||||
|
Authored-by: Moko Consulting
|
||||||
|
```
|
||||||
|
|
||||||
|
Types: `feat`, `fix`, `chore`, `docs`, `style`, `refactor`, `test`, `ci`
|
||||||
|
|
||||||
|
Special flags in commit messages:
|
||||||
|
- `[skip ci]` — skip all CI workflows
|
||||||
|
- `[skip bump]` — skip auto version bump only
|
||||||
|
|
||||||
## Reporting Issues
|
## Reporting Issues
|
||||||
|
|
||||||
Report bugs and feature requests via [Issues](https://git.mokoconsulting.tech/MokoConsulting/MokoJoomOpenGraph/issues).
|
Use the repository's issue tracker with the appropriate template.
|
||||||
|
|
||||||
## License
|
---
|
||||||
|
|
||||||
By contributing, you agree that your contributions will be licensed under GPL-3.0-or-later.
|
*Moko Consulting <hello@mokoconsulting.tech>*
|
||||||
|
|||||||
+119
@@ -0,0 +1,119 @@
|
|||||||
|
<!--
|
||||||
|
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
|
||||||
|
|
||||||
|
This program is free software; you can redistribute it and/or modify it under the terms of
|
||||||
|
the GNU General Public License as published by the Free Software Foundation; either version 3
|
||||||
|
of the License, or (at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
|
||||||
|
without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
||||||
|
See the GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License (./LICENSE).
|
||||||
|
|
||||||
|
FILE INFORMATION
|
||||||
|
DEFGROUP: mokoconsulting-tech.Template-Joomla
|
||||||
|
INGROUP: MokoStandards.Governance
|
||||||
|
REPO: https://github.com/mokoconsulting-tech/Template-Joomla
|
||||||
|
VERSION: 01.05.02
|
||||||
|
PATH: /GOVERNANCE.md
|
||||||
|
BRIEF: Project governance rules, roles, and decision process for Template-Joomla
|
||||||
|
-->
|
||||||
|
|
||||||
|
[](https://github.com/mokoconsulting-tech/MokoStandards)
|
||||||
|
|
||||||
|
# Project Governance
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
This document defines the governance model for the `Template-Joomla` repository within the
|
||||||
|
`mokoconsulting-tech` organization. It is automatically maintained by
|
||||||
|
[MokoStandards](https://github.com/mokoconsulting-tech/MokoStandards) v04.00.04.
|
||||||
|
|
||||||
|
Full governance policy is defined in the MokoStandards source repository:
|
||||||
|
[docs/policy/GOVERNANCE.md](https://github.com/mokoconsulting-tech/MokoStandards/blob/main/docs/policy/GOVERNANCE.md)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Roles and Responsibilities
|
||||||
|
|
||||||
|
### Maintainer
|
||||||
|
|
||||||
|
**GitHub**: @mokoconsulting-tech
|
||||||
|
|
||||||
|
**Authority**: Final decision-making authority on all matters for this repository.
|
||||||
|
|
||||||
|
**Responsibilities**:
|
||||||
|
- Review and merge pull requests
|
||||||
|
- Maintain code quality and standards compliance
|
||||||
|
- Manage releases and versioning
|
||||||
|
- Respond to issues and security reports
|
||||||
|
|
||||||
|
### Contributors
|
||||||
|
|
||||||
|
**Authority**: Submit changes via pull requests.
|
||||||
|
|
||||||
|
**Requirements**:
|
||||||
|
- Read and accept `CODE_OF_CONDUCT.md`
|
||||||
|
- Follow `CONTRIBUTING.md` guidelines
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Decision-Making
|
||||||
|
|
||||||
|
All changes must be submitted as pull requests. The maintainer (@mokoconsulting-tech)
|
||||||
|
reviews and approves all changes before they are merged.
|
||||||
|
|
||||||
|
### Sole Operator Policy
|
||||||
|
|
||||||
|
This organization operates under a **sole operator** model. The maintainer (@mokoconsulting-tech)
|
||||||
|
is the sole employee and owner and may self-approve pull requests when no second reviewer is
|
||||||
|
available. The following requirements remain mandatory regardless:
|
||||||
|
|
||||||
|
1. **Pull Requests Required** — all changes to protected branches go through a PR.
|
||||||
|
2. **Automated Checks** — all CI checks must pass before merging.
|
||||||
|
3. **Audit Trail** — issues, pull requests, and commit history are preserved.
|
||||||
|
4. **Documentation** — changes are documented in `CHANGELOG.md`.
|
||||||
|
|
||||||
|
See the full policy:
|
||||||
|
[Sole Operator Policy](https://github.com/mokoconsulting-tech/MokoStandards/blob/main/docs/policy/GOVERNANCE.md#sole-operator-policy)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Change Management
|
||||||
|
|
||||||
|
| Change Type | Approval | Process |
|
||||||
|
|-------------|----------|---------|
|
||||||
|
| Routine (docs, bug fixes) | Maintainer | PR → CI pass → merge |
|
||||||
|
| Significant (new features) | Maintainer | PR with description → CI pass → merge |
|
||||||
|
| Major (breaking, architecture) | Maintainer | Issue discussion → PR → CI pass → merge |
|
||||||
|
| Emergency (security) | Maintainer | Labelled `EMERGENCY` → immediate merge → post-mortem |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Reporting Issues
|
||||||
|
|
||||||
|
- **Bugs / Features**: Open a [GitHub Issue](https://github.com/mokoconsulting-tech/Template-Joomla/issues)
|
||||||
|
- **Security vulnerabilities**: See [SECURITY.md](./SECURITY.md)
|
||||||
|
- **Code of Conduct**: See [CODE_OF_CONDUCT.md](./CODE_OF_CONDUCT.md)
|
||||||
|
- **Contact**: dev@mokoconsulting.tech
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Metadata
|
||||||
|
|
||||||
|
| Field | Value |
|
||||||
|
| ------------- | ----------------------------------------------- |
|
||||||
|
| Document Type | Policy |
|
||||||
|
| Domain | Governance |
|
||||||
|
| Applies To | mokoconsulting-tech/Template-Joomla |
|
||||||
|
| Jurisdiction | Tennessee, USA |
|
||||||
|
| Maintainer | @mokoconsulting-tech |
|
||||||
|
| Standards | MokoStandards v04.00.04 |
|
||||||
|
| Repo | https://github.com/mokoconsulting-tech/Template-Joomla |
|
||||||
|
| Path | /GOVERNANCE.md |
|
||||||
|
| Status | Active — auto-maintained by MokoStandards |
|
||||||
@@ -1,203 +0,0 @@
|
|||||||
# Makefile for Joomla Extensions
|
|
||||||
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
#
|
|
||||||
# MokoJoomOpenGraph — Open Graph & social sharing meta tag management
|
|
||||||
|
|
||||||
# ==============================================================================
|
|
||||||
# CONFIGURATION - Customize these for your extension
|
|
||||||
# ==============================================================================
|
|
||||||
|
|
||||||
# Extension Configuration
|
|
||||||
EXTENSION_NAME := mokoog
|
|
||||||
EXTENSION_TYPE := package
|
|
||||||
# Options: module, plugin, component, package, template
|
|
||||||
EXTENSION_VERSION := 1.0.0
|
|
||||||
|
|
||||||
# Module Configuration (for modules only)
|
|
||||||
MODULE_TYPE := site
|
|
||||||
# Options: site, admin
|
|
||||||
|
|
||||||
# Plugin Configuration (for plugins only)
|
|
||||||
PLUGIN_GROUP := system
|
|
||||||
# Options: system, content, user, authentication, etc.
|
|
||||||
|
|
||||||
# Directories
|
|
||||||
SRC_DIR := src
|
|
||||||
BUILD_DIR := build
|
|
||||||
DIST_DIR := dist
|
|
||||||
DOCS_DIR := docs
|
|
||||||
|
|
||||||
# Joomla Installation (for local testing - customize paths)
|
|
||||||
JOOMLA_ROOT := /var/www/html/joomla
|
|
||||||
JOOMLA_VERSION := 4
|
|
||||||
|
|
||||||
# Tools
|
|
||||||
PHP := php
|
|
||||||
COMPOSER := composer
|
|
||||||
NPM := npm
|
|
||||||
PHPCS := vendor/bin/phpcs
|
|
||||||
PHPCBF := vendor/bin/phpcbf
|
|
||||||
PHPUNIT := vendor/bin/phpunit
|
|
||||||
ZIP := zip
|
|
||||||
|
|
||||||
# Coding Standards
|
|
||||||
PHPCS_STANDARD := Joomla
|
|
||||||
|
|
||||||
# Colors for output
|
|
||||||
COLOR_RESET := \033[0m
|
|
||||||
COLOR_GREEN := \033[32m
|
|
||||||
COLOR_YELLOW := \033[33m
|
|
||||||
COLOR_BLUE := \033[34m
|
|
||||||
COLOR_RED := \033[31m
|
|
||||||
|
|
||||||
# ==============================================================================
|
|
||||||
# TARGETS
|
|
||||||
# ==============================================================================
|
|
||||||
|
|
||||||
.PHONY: help
|
|
||||||
help: ## Show this help message
|
|
||||||
@echo "$(COLOR_BLUE)╔════════════════════════════════════════════════════════════╗$(COLOR_RESET)"
|
|
||||||
@echo "$(COLOR_BLUE)║ Joomla Extension Makefile ║$(COLOR_RESET)"
|
|
||||||
@echo "$(COLOR_BLUE)╚════════════════════════════════════════════════════════════╝$(COLOR_RESET)"
|
|
||||||
@echo ""
|
|
||||||
@echo "Extension: $(EXTENSION_NAME) ($(EXTENSION_TYPE)) v$(EXTENSION_VERSION)"
|
|
||||||
@echo ""
|
|
||||||
@echo "$(COLOR_GREEN)Available targets:$(COLOR_RESET)"
|
|
||||||
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf " $(COLOR_BLUE)%-20s$(COLOR_RESET) %s\n", $$1, $$2}'
|
|
||||||
@echo ""
|
|
||||||
|
|
||||||
.PHONY: install-deps
|
|
||||||
install-deps: ## Install all dependencies (Composer + npm)
|
|
||||||
@echo "$(COLOR_BLUE)Installing dependencies...$(COLOR_RESET)"
|
|
||||||
@if [ -f "composer.json" ]; then \
|
|
||||||
$(COMPOSER) install; \
|
|
||||||
echo "$(COLOR_GREEN)✓ Composer dependencies installed$(COLOR_RESET)"; \
|
|
||||||
fi
|
|
||||||
|
|
||||||
.PHONY: lint
|
|
||||||
lint: ## Run PHP linter (syntax check)
|
|
||||||
@echo "$(COLOR_BLUE)Running PHP linter...$(COLOR_RESET)"
|
|
||||||
@find . -name "*.php" ! -path "./vendor/*" ! -path "./node_modules/*" ! -path "./$(BUILD_DIR)/*" \
|
|
||||||
-exec $(PHP) -l {} \; | grep -v "No syntax errors" || true
|
|
||||||
@echo "$(COLOR_GREEN)✓ PHP linting complete$(COLOR_RESET)"
|
|
||||||
|
|
||||||
.PHONY: phpcs
|
|
||||||
phpcs: ## Run PHP CodeSniffer (Joomla standards)
|
|
||||||
@echo "$(COLOR_BLUE)Running PHP CodeSniffer...$(COLOR_RESET)"
|
|
||||||
@if [ -f "$(PHPCS)" ]; then \
|
|
||||||
$(PHPCS) --standard=$(PHPCS_STANDARD) --extensions=php --ignore=vendor,node_modules,$(BUILD_DIR) .; \
|
|
||||||
else \
|
|
||||||
echo "$(COLOR_YELLOW)⚠ PHP CodeSniffer not installed. Run: make install-deps$(COLOR_RESET)"; \
|
|
||||||
fi
|
|
||||||
|
|
||||||
.PHONY: validate
|
|
||||||
validate: lint phpcs ## Run all validation checks
|
|
||||||
@echo "$(COLOR_GREEN)✓ All validation checks passed$(COLOR_RESET)"
|
|
||||||
|
|
||||||
.PHONY: clean
|
|
||||||
clean: ## Clean build artifacts
|
|
||||||
@echo "$(COLOR_BLUE)Cleaning build artifacts...$(COLOR_RESET)"
|
|
||||||
@rm -rf $(BUILD_DIR) $(DIST_DIR)
|
|
||||||
@echo "$(COLOR_GREEN)✓ Build artifacts cleaned$(COLOR_RESET)"
|
|
||||||
|
|
||||||
MOKO_PLATFORM ?= $(or $(wildcard ../moko-platform),$(wildcard $(HOME)/moko-platform),$(wildcard /opt/moko-platform))
|
|
||||||
MINIFY_SCRIPT := $(MOKO_PLATFORM)/build/minify.js
|
|
||||||
|
|
||||||
.PHONY: minify
|
|
||||||
minify: ## Minify CSS/JS assets
|
|
||||||
@echo "Minifying assets..."
|
|
||||||
@if [ -f "$(MINIFY_SCRIPT)" ]; then \
|
|
||||||
node "$(MINIFY_SCRIPT)" $(SRC_DIR); \
|
|
||||||
elif [ -f "scripts/minify.js" ]; then \
|
|
||||||
node scripts/minify.js; \
|
|
||||||
else \
|
|
||||||
echo "No minify script found"; \
|
|
||||||
fi
|
|
||||||
|
|
||||||
.PHONY: build
|
|
||||||
build: clean validate minify ## Build extension package
|
|
||||||
@echo "$(COLOR_BLUE)Building Joomla extension package...$(COLOR_RESET)"
|
|
||||||
@mkdir -p $(DIST_DIR) $(BUILD_DIR)
|
|
||||||
|
|
||||||
# Determine package prefix based on extension type
|
|
||||||
@case "$(EXTENSION_TYPE)" in \
|
|
||||||
module) \
|
|
||||||
PACKAGE_PREFIX="mod_$(EXTENSION_NAME)"; \
|
|
||||||
BUILD_TARGET="$(BUILD_DIR)/$$PACKAGE_PREFIX"; \
|
|
||||||
;; \
|
|
||||||
plugin) \
|
|
||||||
PACKAGE_PREFIX="plg_$(PLUGIN_GROUP)_$(EXTENSION_NAME)"; \
|
|
||||||
BUILD_TARGET="$(BUILD_DIR)/$$PACKAGE_PREFIX"; \
|
|
||||||
;; \
|
|
||||||
component) \
|
|
||||||
PACKAGE_PREFIX="com_$(EXTENSION_NAME)"; \
|
|
||||||
BUILD_TARGET="$(BUILD_DIR)/$$PACKAGE_PREFIX"; \
|
|
||||||
;; \
|
|
||||||
package) \
|
|
||||||
PACKAGE_PREFIX="pkg_$(EXTENSION_NAME)"; \
|
|
||||||
BUILD_TARGET="$(BUILD_DIR)/$$PACKAGE_PREFIX"; \
|
|
||||||
;; \
|
|
||||||
template) \
|
|
||||||
PACKAGE_PREFIX="tpl_$(EXTENSION_NAME)"; \
|
|
||||||
BUILD_TARGET="$(BUILD_DIR)/$$PACKAGE_PREFIX"; \
|
|
||||||
;; \
|
|
||||||
*) \
|
|
||||||
echo "$(COLOR_RED)✗ Unknown extension type: $(EXTENSION_TYPE)$(COLOR_RESET)"; \
|
|
||||||
exit 1; \
|
|
||||||
;; \
|
|
||||||
esac; \
|
|
||||||
\
|
|
||||||
mkdir -p "$$BUILD_TARGET"; \
|
|
||||||
\
|
|
||||||
echo "Building $$PACKAGE_PREFIX..."; \
|
|
||||||
\
|
|
||||||
rsync -av --progress \
|
|
||||||
--exclude='$(BUILD_DIR)' \
|
|
||||||
--exclude='$(DIST_DIR)' \
|
|
||||||
--exclude='.git*' \
|
|
||||||
--exclude='vendor/' \
|
|
||||||
--exclude='node_modules/' \
|
|
||||||
--exclude='tests/' \
|
|
||||||
--exclude='Makefile' \
|
|
||||||
--exclude='composer.json' \
|
|
||||||
--exclude='composer.lock' \
|
|
||||||
--exclude='package.json' \
|
|
||||||
--exclude='package-lock.json' \
|
|
||||||
--exclude='phpunit.xml' \
|
|
||||||
--exclude='*.md' \
|
|
||||||
--exclude='.editorconfig' \
|
|
||||||
. "$$BUILD_TARGET/"; \
|
|
||||||
\
|
|
||||||
cd $(BUILD_DIR) && $(ZIP) -r "../$(DIST_DIR)/$${PACKAGE_PREFIX}-$(EXTENSION_VERSION).zip" "$${PACKAGE_PREFIX}"; \
|
|
||||||
\
|
|
||||||
echo "$(COLOR_GREEN)✓ Package created: $(DIST_DIR)/$${PACKAGE_PREFIX}-$(EXTENSION_VERSION).zip$(COLOR_RESET)"
|
|
||||||
|
|
||||||
.PHONY: package
|
|
||||||
package: build ## Alias for build
|
|
||||||
@echo "$(COLOR_GREEN)✓ Package ready for distribution$(COLOR_RESET)"
|
|
||||||
|
|
||||||
.PHONY: release
|
|
||||||
release: validate build ## Create a release (validate + build)
|
|
||||||
@echo "$(COLOR_GREEN)✓ Release package ready$(COLOR_RESET)"
|
|
||||||
|
|
||||||
.PHONY: version
|
|
||||||
version: ## Display version information
|
|
||||||
@echo "$(COLOR_BLUE)Extension Information:$(COLOR_RESET)"
|
|
||||||
@echo " Name: $(EXTENSION_NAME)"
|
|
||||||
@echo " Type: $(EXTENSION_TYPE)"
|
|
||||||
@echo " Version: $(EXTENSION_VERSION)"
|
|
||||||
|
|
||||||
.PHONY: security-check
|
|
||||||
security-check: ## Run security checks on dependencies
|
|
||||||
@echo "$(COLOR_BLUE)Running security checks...$(COLOR_RESET)"
|
|
||||||
@if [ -f "composer.json" ]; then \
|
|
||||||
$(COMPOSER) audit || echo "$(COLOR_YELLOW)⚠ Vulnerabilities found$(COLOR_RESET)"; \
|
|
||||||
fi
|
|
||||||
|
|
||||||
.PHONY: all
|
|
||||||
all: install-deps validate build ## Run complete build pipeline
|
|
||||||
@echo "$(COLOR_GREEN)✓ Complete build pipeline finished$(COLOR_RESET)"
|
|
||||||
|
|
||||||
# Default target
|
|
||||||
.DEFAULT_GOAL := help
|
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
# MokoSuiteOpenGraph
|
# MokoSuiteOpenGraph
|
||||||
|
|
||||||
<!-- VERSION: 01.02.00 -->
|
<!-- VERSION: 01.05.02 -->
|
||||||
|
|
||||||
Open Graph, Twitter Card, and social sharing meta tag management for Joomla 4/5/6.
|
Open Graph, Twitter Card, and social sharing meta tag management for Joomla 6 and higher.
|
||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
|
|
||||||
@@ -16,6 +16,9 @@ MokoSuiteOpenGraph gives you full control over how your Joomla content appears w
|
|||||||
- **LinkedIn** — `article:published_time`, `article:modified_time`, `article:author`
|
- **LinkedIn** — `article:published_time`, `article:modified_time`, `article:author`
|
||||||
- **Discord** — Custom embed color via `theme-color` meta tag
|
- **Discord** — Custom embed color via `theme-color` meta tag
|
||||||
- **Telegram** — `telegram:channel` for link previews
|
- **Telegram** — `telegram:channel` for link previews
|
||||||
|
- **Mastodon/Fediverse** — `fediverse:creator` for author attribution (first extension on any CMS)
|
||||||
|
- **Pinterest** — Rich pin tags: `article:tag`, `product:availability`, `product:price`
|
||||||
|
- **og:video** — Per-article video URLs with auto MIME type detection (YouTube/Vimeo/direct)
|
||||||
- **Facebook** — `fb:app_id` support, `og:image:width`/`og:image:height` for instant previews
|
- **Facebook** — `fb:app_id` support, `og:image:width`/`og:image:height` for instant previews
|
||||||
|
|
||||||
### Content Management
|
### Content Management
|
||||||
@@ -31,7 +34,8 @@ MokoSuiteOpenGraph gives you full control over how your Joomla content appears w
|
|||||||
- **Meta description** — Per-page meta description control
|
- **Meta description** — Per-page meta description control
|
||||||
- **Robots directive** — Per-page noindex/nofollow settings
|
- **Robots directive** — Per-page noindex/nofollow settings
|
||||||
- **Canonical URL** — Custom canonical URL overrides
|
- **Canonical URL** — Custom canonical URL overrides
|
||||||
- **JSON-LD structured data** — Article, Product, WebPage, BreadcrumbList, Organization schemas
|
- **JSON-LD structured data** — Article, Product, WebPage, BreadcrumbList, Organization, FAQ, HowTo, Event, Recipe, LocalBusiness, VideoObject, and custom schemas
|
||||||
|
- **SEO content scoring** — 7-check analysis panel with pass/fail indicators in the editor
|
||||||
|
|
||||||
### Admin Tools
|
### Admin Tools
|
||||||
- **Tag manager dashboard** — View and manage all OG records centrally
|
- **Tag manager dashboard** — View and manage all OG records centrally
|
||||||
@@ -39,13 +43,20 @@ MokoSuiteOpenGraph gives you full control over how your Joomla content appears w
|
|||||||
- **CSV import/export** — Bulk manage OG data via CSV files
|
- **CSV import/export** — Bulk manage OG data via CSV files
|
||||||
- **SEO health badges** — Visual indicators for missing descriptions, long titles, noindex
|
- **SEO health badges** — Visual indicators for missing descriptions, long titles, noindex
|
||||||
- **Debug links** — Quick links to Facebook Debugger, LinkedIn Inspector, Google Rich Results
|
- **Debug links** — Quick links to Facebook Debugger, LinkedIn Inspector, Google Rich Results
|
||||||
- **Live preview** — Real-time Facebook and Twitter/X card preview in the editor
|
- **Live preview** — Real-time Facebook, Twitter/X, LinkedIn, Discord, Mastodon, and Slack card previews in the editor
|
||||||
|
- **Character count indicators** — Green/yellow/red warnings on OG and SEO text fields
|
||||||
|
- **OG coverage dashboard** — Coverage percentage and missing field counts
|
||||||
|
- **AI meta generation** — Generate OG titles and descriptions with Claude or OpenAI
|
||||||
|
|
||||||
### Developer Features
|
### Developer Features
|
||||||
- **REST API** — Full CRUD via Joomla Web Services (`/api/v1/mokoog/tags`)
|
- **REST API** — Full CRUD via Joomla Web Services (`/api/v1/mokoog/tags`)
|
||||||
- **MokoSuiteShop integration** — Auto-generated OG/JSON-LD for product pages with pricing meta
|
- **MokoSuiteShop integration** — Auto-generated OG/JSON-LD for product pages with pricing meta
|
||||||
- **Plugin event** — `onMokoOGAfterRender` for third-party plugins to add custom social tags
|
- **Plugin event** — `onMokoOGAfterRender` for third-party plugins to add custom social tags
|
||||||
- **OG image generator** — Text overlay on template backgrounds with auto-resize to 1200x630
|
- **OG image generator** — Text overlay on template backgrounds with auto-resize to 1200x630
|
||||||
|
- **Per-platform image resizing** — Twitter 1200x600, Pinterest 1000x1500, WhatsApp 400x400
|
||||||
|
- **XML sitemap** — Auto-generates sitemap.xml on article save, respects noindex
|
||||||
|
- **OpenAPI spec** — Full REST API documentation at `openapi.yaml`
|
||||||
|
- **PHPUnit tests** — 16 unit tests for JsonLdBuilder schema outputs
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
@@ -63,6 +74,11 @@ Navigate to **Extensions → Plugins → System - MokoSuiteOpenGraph** to config
|
|||||||
- Facebook App ID
|
- Facebook App ID
|
||||||
- Discord embed color
|
- Discord embed color
|
||||||
- Telegram channel
|
- Telegram channel
|
||||||
|
- Fediverse/Mastodon creator handle
|
||||||
|
- LocalBusiness schema (address, phone, hours, geo)
|
||||||
|
- XML sitemap generation
|
||||||
|
- AI meta generation (Claude/OpenAI API key)
|
||||||
|
- Per-platform image resizing
|
||||||
- Auto-generation, image resize, JSON-LD, and description length settings
|
- Auto-generation, image resize, JSON-LD, and description length settings
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|||||||
+241
@@ -0,0 +1,241 @@
|
|||||||
|
<!--
|
||||||
|
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
|
||||||
|
|
||||||
|
This program is free software; you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation; either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
# FILE INFORMATION
|
||||||
|
DEFGROUP: Template-Joomla
|
||||||
|
INGROUP: Template-Joomla.Documentation
|
||||||
|
REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Joomla
|
||||||
|
PATH: /SECURITY.md
|
||||||
|
VERSION: 01.05.02
|
||||||
|
BRIEF: Security vulnerability reporting and handling policy
|
||||||
|
-->
|
||||||
|
|
||||||
|
# Security Policy
|
||||||
|
|
||||||
|
## Purpose and Scope
|
||||||
|
|
||||||
|
This document defines the security vulnerability reporting, response, and disclosure policy for this Joomla Plugin template repository. It establishes the authoritative process for responsible disclosure, assessment, remediation, and communication of security issues.
|
||||||
|
|
||||||
|
## Supported Versions
|
||||||
|
|
||||||
|
Security updates are provided for the following versions:
|
||||||
|
|
||||||
|
| Version | Supported |
|
||||||
|
| ------- | ------------------ |
|
||||||
|
| 01.x.x | :white_check_mark: |
|
||||||
|
| < 01.0 | :x: |
|
||||||
|
|
||||||
|
Only the current major version receives security updates. Users should upgrade to the latest supported version to receive security patches.
|
||||||
|
|
||||||
|
## Reporting a Vulnerability
|
||||||
|
|
||||||
|
### Where to Report
|
||||||
|
|
||||||
|
**DO NOT** create public GitHub issues for security vulnerabilities.
|
||||||
|
|
||||||
|
Report security vulnerabilities privately to:
|
||||||
|
|
||||||
|
**Email**: `security@mokoconsulting.tech`
|
||||||
|
|
||||||
|
**Subject Line**: `[SECURITY] Template-Joomla - Brief Description`
|
||||||
|
|
||||||
|
### What to Include
|
||||||
|
|
||||||
|
A complete vulnerability report should include:
|
||||||
|
|
||||||
|
1. **Description**: Clear explanation of the vulnerability
|
||||||
|
2. **Impact**: Potential security impact and severity assessment
|
||||||
|
3. **Affected Versions**: Which versions are vulnerable
|
||||||
|
4. **Reproduction Steps**: Detailed steps to reproduce the issue
|
||||||
|
5. **Proof of Concept**: Code, configuration, or demonstration (if applicable)
|
||||||
|
6. **Suggested Fix**: Proposed remediation (if known)
|
||||||
|
7. **Disclosure Timeline**: Your expectations for public disclosure
|
||||||
|
|
||||||
|
### Response Timeline
|
||||||
|
|
||||||
|
* **Initial Response**: Within 3 business days
|
||||||
|
* **Assessment Complete**: Within 7 business days
|
||||||
|
* **Fix Timeline**: Depends on severity (see below)
|
||||||
|
* **Disclosure**: Coordinated with reporter
|
||||||
|
|
||||||
|
## Severity Classification
|
||||||
|
|
||||||
|
Vulnerabilities are classified using the following severity levels:
|
||||||
|
|
||||||
|
### Critical
|
||||||
|
* Remote code execution
|
||||||
|
* Authentication bypass
|
||||||
|
* Data breach or exposure of sensitive information
|
||||||
|
* **Fix Timeline**: 7 days
|
||||||
|
|
||||||
|
### High
|
||||||
|
* Privilege escalation
|
||||||
|
* SQL injection or command injection
|
||||||
|
* Cross-site scripting (XSS) with significant impact
|
||||||
|
* **Fix Timeline**: 14 days
|
||||||
|
|
||||||
|
### Medium
|
||||||
|
* Information disclosure (limited scope)
|
||||||
|
* Denial of service
|
||||||
|
* Security misconfigurations with moderate impact
|
||||||
|
* **Fix Timeline**: 30 days
|
||||||
|
|
||||||
|
### Low
|
||||||
|
* Security best practice violations
|
||||||
|
* Minor information leaks
|
||||||
|
* Issues requiring user interaction or complex preconditions
|
||||||
|
* **Fix Timeline**: 60 days or next release
|
||||||
|
|
||||||
|
## Remediation Process
|
||||||
|
|
||||||
|
1. **Acknowledgment**: Security team confirms receipt and begins investigation
|
||||||
|
2. **Assessment**: Vulnerability is validated, severity assigned, and impact analyzed
|
||||||
|
3. **Development**: Security patch is developed and tested
|
||||||
|
4. **Review**: Patch undergoes security review and validation
|
||||||
|
5. **Release**: Fixed version is released with security advisory
|
||||||
|
6. **Disclosure**: Public disclosure follows coordinated timeline
|
||||||
|
|
||||||
|
## Security Advisories
|
||||||
|
|
||||||
|
Security advisories are published via:
|
||||||
|
|
||||||
|
* GitHub Security Advisories
|
||||||
|
* Release notes and CHANGELOG.md
|
||||||
|
* Email notification to project users (if mailing list is established)
|
||||||
|
|
||||||
|
Advisories include:
|
||||||
|
|
||||||
|
* CVE identifier (if applicable)
|
||||||
|
* Severity rating
|
||||||
|
* Affected versions
|
||||||
|
* Fixed versions
|
||||||
|
* Mitigation steps
|
||||||
|
* Attribution (with reporter consent)
|
||||||
|
|
||||||
|
## Security Best Practices
|
||||||
|
|
||||||
|
For projects using this template:
|
||||||
|
|
||||||
|
### Required Controls
|
||||||
|
|
||||||
|
* Enable GitHub security features (Dependabot, code scanning)
|
||||||
|
* Implement branch protection on `main`
|
||||||
|
* Require code review for all changes
|
||||||
|
* Enforce signed commits (recommended)
|
||||||
|
* Use secrets management (never commit credentials)
|
||||||
|
* Maintain security documentation
|
||||||
|
* Follow secure coding standards defined in MokoStandards
|
||||||
|
|
||||||
|
### Joomla Plugin Security
|
||||||
|
|
||||||
|
* Follow Joomla security best practices
|
||||||
|
* Validate and sanitize all user input
|
||||||
|
* Use Joomla's database API to prevent SQL injection
|
||||||
|
* Properly escape output to prevent XSS
|
||||||
|
* Implement proper access control checks
|
||||||
|
* Use Joomla's session and authentication APIs
|
||||||
|
* Keep Joomla and dependencies up to date
|
||||||
|
|
||||||
|
### CI/CD Security
|
||||||
|
|
||||||
|
* Validate all inputs
|
||||||
|
* Sanitize outputs
|
||||||
|
* Use least privilege access
|
||||||
|
* Pin dependencies with hash verification
|
||||||
|
* Scan for vulnerabilities in dependencies
|
||||||
|
* Audit third-party actions and tools
|
||||||
|
|
||||||
|
#### Automated Security Scanning
|
||||||
|
|
||||||
|
All repositories SHOULD implement:
|
||||||
|
|
||||||
|
**CodeQL Analysis**:
|
||||||
|
* Enabled for PHP and other supported languages
|
||||||
|
* Runs on: push to main, pull requests, weekly schedule
|
||||||
|
* Query sets: `security-extended` and `security-and-quality`
|
||||||
|
* Configuration: `.github/workflows/codeql-analysis.yml`
|
||||||
|
|
||||||
|
**Dependabot Security Updates**:
|
||||||
|
* Weekly scans for vulnerable dependencies
|
||||||
|
* Automated pull requests for security patches
|
||||||
|
* Configuration: `.github/dependabot.yml`
|
||||||
|
|
||||||
|
**Secret Scanning**:
|
||||||
|
* Enabled by default with push protection
|
||||||
|
* Prevents accidental credential commits
|
||||||
|
|
||||||
|
### Dependency Management
|
||||||
|
|
||||||
|
* Keep dependencies up to date
|
||||||
|
* Monitor security advisories for dependencies
|
||||||
|
* Remove unused dependencies
|
||||||
|
* Audit new dependencies before adoption
|
||||||
|
* Document security-critical dependencies
|
||||||
|
|
||||||
|
## Compliance and Governance
|
||||||
|
|
||||||
|
This security policy is aligned with MokoStandards. Deviations require documented justification.
|
||||||
|
|
||||||
|
Security policies are reviewed and updated at least annually or following significant security incidents.
|
||||||
|
|
||||||
|
## Attribution and Recognition
|
||||||
|
|
||||||
|
We acknowledge and appreciate responsible disclosure. With your permission, we will:
|
||||||
|
|
||||||
|
* Credit you in security advisories
|
||||||
|
* List you in CHANGELOG.md for the fix release
|
||||||
|
* Recognize your contribution publicly (if desired)
|
||||||
|
|
||||||
|
## Contact and Escalation
|
||||||
|
|
||||||
|
* **Security Team**: security@mokoconsulting.tech
|
||||||
|
* **Primary Contact**: hello@mokoconsulting.tech
|
||||||
|
* **Escalation**: For urgent matters requiring immediate attention, contact the maintainer directly via GitHub
|
||||||
|
|
||||||
|
## Out of Scope
|
||||||
|
|
||||||
|
The following are explicitly out of scope:
|
||||||
|
|
||||||
|
* Issues in third-party dependencies (report directly to maintainers)
|
||||||
|
* Social engineering attacks
|
||||||
|
* Physical security issues
|
||||||
|
* Denial of service via resource exhaustion without amplification
|
||||||
|
* Issues requiring physical access to systems
|
||||||
|
* Theoretical vulnerabilities without proof of exploitability
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Metadata
|
||||||
|
|
||||||
|
| Field | Value |
|
||||||
|
| ------------ | ------------------------------------------------------------------------------------------------------------ |
|
||||||
|
| Document | Security Policy |
|
||||||
|
| Path | /SECURITY.md |
|
||||||
|
| Repository | [https://github.com/mokoconsulting-tech/Template-Joomla](https://github.com/mokoconsulting-tech/Template-Joomla) |
|
||||||
|
| Owner | Moko Consulting |
|
||||||
|
| Scope | Security vulnerability handling |
|
||||||
|
| Status | Active |
|
||||||
|
| Effective | 2026-01-16 |
|
||||||
|
|
||||||
|
## Revision History
|
||||||
|
|
||||||
|
| Date | Change Description | Author |
|
||||||
|
| ---------- | ------------------------------------------------- | --------------- |
|
||||||
|
| 2026-01-16 | Initial creation for template repository | Moko Consulting |
|
||||||
@@ -1,237 +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: Automation.CI
|
|
||||||
# INGROUP: moko-platform.Automation
|
|
||||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
|
||||||
# PATH: /automation/ci-issue-reporter.sh
|
|
||||||
# VERSION: 09.23.00
|
|
||||||
# BRIEF: Creates or updates a Gitea issue when a CI gate fails.
|
|
||||||
# Deduplicates by searching open issues with the "ci-auto" label
|
|
||||||
# whose title matches the gate. If a matching issue exists, a comment
|
|
||||||
# is appended instead of opening a duplicate.
|
|
||||||
# ============================================================================
|
|
||||||
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
# ── Defaults ────────────────────────────────────────────────────────────────
|
|
||||||
GITEA_URL="${GITEA_URL:-https://git.mokoconsulting.tech}"
|
|
||||||
GITEA_TOKEN="${GITEA_TOKEN:-}"
|
|
||||||
REPO="${GITHUB_REPOSITORY:-}"
|
|
||||||
RUN_URL="${GITHUB_SERVER_URL:-${GITEA_URL}}/${REPO}/actions/runs/${GITHUB_RUN_ID:-0}"
|
|
||||||
LABEL_NAME="ci-auto"
|
|
||||||
LABEL_COLOR="#e11d48"
|
|
||||||
|
|
||||||
GATE=""
|
|
||||||
DETAILS=""
|
|
||||||
SEVERITY="error"
|
|
||||||
WORKFLOW=""
|
|
||||||
|
|
||||||
# ── Parse arguments ─────────────────────────────────────────────────────────
|
|
||||||
usage() {
|
|
||||||
cat <<EOF
|
|
||||||
Usage: ci-issue-reporter.sh --gate NAME --details TEXT [OPTIONS]
|
|
||||||
|
|
||||||
Required:
|
|
||||||
--gate CI gate name (e.g. "Code Quality", "Self-Health")
|
|
||||||
--details Human-readable failure description
|
|
||||||
|
|
||||||
Optional:
|
|
||||||
--severity "error" (default) or "warning"
|
|
||||||
--workflow Workflow name for the issue title
|
|
||||||
--repo owner/repo (default: \$GITHUB_REPOSITORY)
|
|
||||||
--run-url URL to the CI run (auto-detected from env)
|
|
||||||
--token Gitea API token (default: \$GITEA_TOKEN)
|
|
||||||
--url Gitea base URL (default: \$GITEA_URL)
|
|
||||||
EOF
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
while [[ $# -gt 0 ]]; do
|
|
||||||
case "$1" in
|
|
||||||
--gate) GATE="$2"; shift 2 ;;
|
|
||||||
--details) DETAILS="$2"; shift 2 ;;
|
|
||||||
--severity) SEVERITY="$2"; shift 2 ;;
|
|
||||||
--workflow) WORKFLOW="$2"; shift 2 ;;
|
|
||||||
--repo) REPO="$2"; shift 2 ;;
|
|
||||||
--run-url) RUN_URL="$2"; shift 2 ;;
|
|
||||||
--token) GITEA_TOKEN="$2"; shift 2 ;;
|
|
||||||
--url) GITEA_URL="$2"; shift 2 ;;
|
|
||||||
-h|--help) usage ;;
|
|
||||||
*) echo "Unknown option: $1"; usage ;;
|
|
||||||
esac
|
|
||||||
done
|
|
||||||
|
|
||||||
[[ -z "$GATE" ]] && { echo "ERROR: --gate is required"; usage; }
|
|
||||||
[[ -z "$DETAILS" ]] && { echo "ERROR: --details is required"; usage; }
|
|
||||||
[[ -z "$GITEA_TOKEN" ]] && { echo "ERROR: GITEA_TOKEN not set"; exit 1; }
|
|
||||||
[[ -z "$REPO" ]] && { echo "ERROR: GITHUB_REPOSITORY not set"; exit 1; }
|
|
||||||
|
|
||||||
API="${GITEA_URL}/api/v1/repos/${REPO}"
|
|
||||||
|
|
||||||
# ── Build title ─────────────────────────────────────────────────────────────
|
|
||||||
if [[ -n "$WORKFLOW" ]]; then
|
|
||||||
TITLE="[CI] ${WORKFLOW}: ${GATE} failed"
|
|
||||||
else
|
|
||||||
TITLE="[CI] ${GATE} failed"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# ── Ensure label exists ─────────────────────────────────────────────────────
|
|
||||||
ensure_label() {
|
|
||||||
local exists
|
|
||||||
exists=$(curl -sf -o /dev/null -w '%{http_code}' \
|
|
||||||
-H "Authorization: token ${GITEA_TOKEN}" \
|
|
||||||
"${API}/labels" 2>/dev/null || echo "000")
|
|
||||||
|
|
||||||
if [[ "$exists" == "200" ]]; then
|
|
||||||
# Check if label already exists
|
|
||||||
local found
|
|
||||||
found=$(curl -sf \
|
|
||||||
-H "Authorization: token ${GITEA_TOKEN}" \
|
|
||||||
"${API}/labels" 2>/dev/null \
|
|
||||||
| grep -o "\"name\":\"${LABEL_NAME}\"" || true)
|
|
||||||
|
|
||||||
if [[ -z "$found" ]]; then
|
|
||||||
curl -sf -X POST \
|
|
||||||
-H "Authorization: token ${GITEA_TOKEN}" \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
"${API}/labels" \
|
|
||||||
-d "{\"name\":\"${LABEL_NAME}\",\"color\":\"${LABEL_COLOR}\",\"description\":\"Auto-created by CI issue reporter\"}" \
|
|
||||||
> /dev/null 2>&1 || true
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# ── Search for existing open issue ──────────────────────────────────────────
|
|
||||||
find_existing_issue() {
|
|
||||||
# URL-encode the gate name for the query
|
|
||||||
local query
|
|
||||||
query=$(printf '%s' "[CI] ${GATE}" | sed 's/ /%20/g; s/\[/%5B/g; s/\]/%5D/g')
|
|
||||||
|
|
||||||
local response
|
|
||||||
response=$(curl -sf \
|
|
||||||
-H "Authorization: token ${GITEA_TOKEN}" \
|
|
||||||
"${API}/issues?type=issues&state=open&labels=${LABEL_NAME}&q=${query}&limit=5" \
|
|
||||||
2>/dev/null || echo "[]")
|
|
||||||
|
|
||||||
# Extract the first matching issue number
|
|
||||||
echo "$response" \
|
|
||||||
| grep -oP '"number":\s*\K[0-9]+' \
|
|
||||||
| head -1
|
|
||||||
}
|
|
||||||
|
|
||||||
# ── Build issue body ────────────────────────────────────────────────────────
|
|
||||||
build_body() {
|
|
||||||
local severity_badge
|
|
||||||
if [[ "$SEVERITY" == "error" ]]; then
|
|
||||||
severity_badge="**Severity:** Error"
|
|
||||||
else
|
|
||||||
severity_badge="**Severity:** Warning"
|
|
||||||
fi
|
|
||||||
|
|
||||||
cat <<BODY
|
|
||||||
## CI Gate Failure: ${GATE}
|
|
||||||
|
|
||||||
${severity_badge}
|
|
||||||
**Workflow:** ${WORKFLOW:-unknown}
|
|
||||||
**Branch:** ${GITHUB_REF_NAME:-unknown}
|
|
||||||
**Commit:** \`${GITHUB_SHA:0:8}\`
|
|
||||||
**Run:** [View CI run](${RUN_URL})
|
|
||||||
|
|
||||||
### Details
|
|
||||||
|
|
||||||
${DETAILS}
|
|
||||||
|
|
||||||
### Resolution
|
|
||||||
|
|
||||||
Fix the issue described above and push a new commit. This issue will be closed automatically when the gate passes, or can be closed manually.
|
|
||||||
|
|
||||||
---
|
|
||||||
*Auto-created by [ci-issue-reporter](${GITEA_URL}/${REPO}/src/branch/main/automation/ci-issue-reporter.sh)*
|
|
||||||
BODY
|
|
||||||
}
|
|
||||||
|
|
||||||
# ── Build comment body (for existing issues) ────────────────────────────────
|
|
||||||
build_comment() {
|
|
||||||
cat <<COMMENT
|
|
||||||
### CI failure recurrence
|
|
||||||
|
|
||||||
**Branch:** ${GITHUB_REF_NAME:-unknown}
|
|
||||||
**Commit:** \`${GITHUB_SHA:0:8}\`
|
|
||||||
**Run:** [View CI run](${RUN_URL})
|
|
||||||
|
|
||||||
${DETAILS}
|
|
||||||
COMMENT
|
|
||||||
}
|
|
||||||
|
|
||||||
# ── Main ────────────────────────────────────────────────────────────────────
|
|
||||||
ensure_label
|
|
||||||
|
|
||||||
EXISTING=$(find_existing_issue)
|
|
||||||
|
|
||||||
if [[ -n "$EXISTING" ]]; then
|
|
||||||
# Append comment to existing issue
|
|
||||||
COMMENT_BODY=$(build_comment)
|
|
||||||
COMMENT_JSON=$(printf '%s' "$COMMENT_BODY" | python3 -c "
|
|
||||||
import sys, json
|
|
||||||
print(json.dumps({'body': sys.stdin.read()}))" 2>/dev/null)
|
|
||||||
|
|
||||||
HTTP=$(curl -sf -o /dev/null -w '%{http_code}' -X POST \
|
|
||||||
-H "Authorization: token ${GITEA_TOKEN}" \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
"${API}/issues/${EXISTING}/comments" \
|
|
||||||
-d "${COMMENT_JSON}" 2>/dev/null || echo "000")
|
|
||||||
|
|
||||||
if [[ "$HTTP" == "201" ]]; then
|
|
||||||
echo "Commented on existing issue #${EXISTING}"
|
|
||||||
else
|
|
||||||
echo "WARNING: Failed to comment on issue #${EXISTING} (HTTP ${HTTP})"
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
# Create new issue
|
|
||||||
ISSUE_BODY=$(build_body)
|
|
||||||
ISSUE_JSON=$(python3 -c "
|
|
||||||
import sys, json
|
|
||||||
body = sys.stdin.read()
|
|
||||||
print(json.dumps({
|
|
||||||
'title': sys.argv[1],
|
|
||||||
'body': body,
|
|
||||||
'labels': []
|
|
||||||
}))" "$TITLE" <<< "$ISSUE_BODY" 2>/dev/null)
|
|
||||||
|
|
||||||
# Create the issue
|
|
||||||
RESPONSE=$(curl -sf -X POST \
|
|
||||||
-H "Authorization: token ${GITEA_TOKEN}" \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
"${API}/issues" \
|
|
||||||
-d "${ISSUE_JSON}" 2>/dev/null || echo "{}")
|
|
||||||
|
|
||||||
ISSUE_NUM=$(echo "$RESPONSE" | grep -oP '"number":\s*\K[0-9]+' | head -1)
|
|
||||||
|
|
||||||
if [[ -n "$ISSUE_NUM" ]]; then
|
|
||||||
# Apply label (separate call — more reliable across Gitea versions)
|
|
||||||
LABEL_ID=$(curl -sf \
|
|
||||||
-H "Authorization: token ${GITEA_TOKEN}" \
|
|
||||||
"${API}/labels" 2>/dev/null \
|
|
||||||
| grep -oP "\"id\":\s*\K[0-9]+(?=[^}]*\"name\":\s*\"${LABEL_NAME}\")" \
|
|
||||||
| head -1 || true)
|
|
||||||
|
|
||||||
if [[ -n "$LABEL_ID" ]]; then
|
|
||||||
curl -sf -X POST \
|
|
||||||
-H "Authorization: token ${GITEA_TOKEN}" \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
"${API}/issues/${ISSUE_NUM}/labels" \
|
|
||||||
-d "{\"labels\":[${LABEL_ID}]}" \
|
|
||||||
> /dev/null 2>&1 || true
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "Created issue #${ISSUE_NUM}: ${TITLE}"
|
|
||||||
else
|
|
||||||
echo "WARNING: Failed to create issue"
|
|
||||||
echo "Response: ${RESPONSE}"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
+16
-2
@@ -15,9 +15,23 @@
|
|||||||
"php": ">=8.1"
|
"php": ">=8.1"
|
||||||
},
|
},
|
||||||
"require-dev": {
|
"require-dev": {
|
||||||
"squizlabs/php_codesniffer": "^3.7",
|
"joomla/coding-standards": "^3.0",
|
||||||
"phpstan/phpstan": "^1.10",
|
"phpstan/phpstan": "^1.10",
|
||||||
"joomla/coding-standards": "^3.0"
|
"phpunit/phpunit": "^10.5",
|
||||||
|
"squizlabs/php_codesniffer": "^3.7"
|
||||||
|
},
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"Joomla\\Plugin\\System\\MokoOG\\": "source/packages/plg_system_mokoog/src/",
|
||||||
|
"Joomla\\Plugin\\Content\\MokoOG\\": "source/packages/plg_content_mokoog/src/",
|
||||||
|
"Joomla\\Plugin\\WebServices\\MokoOG\\": "source/packages/plg_webservices_mokoog/src/",
|
||||||
|
"Joomla\\Component\\MokoOG\\Administrator\\": "source/packages/com_mokoog/src/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"autoload-dev": {
|
||||||
|
"psr-4": {
|
||||||
|
"Mokoconsulting\\MokoOG\\Tests\\": "tests/"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"minimum-stability": "alpha",
|
"minimum-stability": "alpha",
|
||||||
"prefer-stable": true,
|
"prefer-stable": true,
|
||||||
|
|||||||
+670
@@ -0,0 +1,670 @@
|
|||||||
|
openapi: 3.0.3
|
||||||
|
|
||||||
|
info:
|
||||||
|
title: MokoSuiteOpenGraph API
|
||||||
|
version: 1.0.0
|
||||||
|
description: |
|
||||||
|
REST API for managing Open Graph, SEO meta, and structured-data tags in
|
||||||
|
Joomla via the MokoSuiteOpenGraph extension.
|
||||||
|
|
||||||
|
**Requires Joomla 6.0 or higher.**
|
||||||
|
|
||||||
|
The API follows Joomla's Web Services conventions and returns responses in
|
||||||
|
[JSON:API](https://jsonapi.org/) format. All endpoints require
|
||||||
|
authentication via a Joomla API token.
|
||||||
|
contact:
|
||||||
|
name: Moko Consulting
|
||||||
|
email: hello@mokoconsulting.tech
|
||||||
|
license:
|
||||||
|
name: GPL-3.0-or-later
|
||||||
|
url: https://www.gnu.org/licenses/gpl-3.0.html
|
||||||
|
|
||||||
|
servers:
|
||||||
|
- url: /api/index.php/v1
|
||||||
|
description: Joomla Web Services API
|
||||||
|
|
||||||
|
security:
|
||||||
|
- apiToken: []
|
||||||
|
|
||||||
|
tags:
|
||||||
|
- name: Tags
|
||||||
|
description: CRUD operations for Open Graph tag records
|
||||||
|
|
||||||
|
paths:
|
||||||
|
/mokoog/tags:
|
||||||
|
get:
|
||||||
|
operationId: listTags
|
||||||
|
summary: List OG tags
|
||||||
|
description: |
|
||||||
|
Returns a paginated collection of OG tag records. Supports filtering
|
||||||
|
by content type, published state, and language.
|
||||||
|
tags: [Tags]
|
||||||
|
parameters:
|
||||||
|
- name: "filter[content_type]"
|
||||||
|
in: query
|
||||||
|
description: Filter by content type (e.g. `com_content`, `menu`, `com_mokoshop`)
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
example: com_content
|
||||||
|
- name: "filter[content_id]"
|
||||||
|
in: query
|
||||||
|
description: Filter by content ID
|
||||||
|
schema:
|
||||||
|
type: integer
|
||||||
|
example: 42
|
||||||
|
- name: "filter[published]"
|
||||||
|
in: query
|
||||||
|
description: Filter by published state
|
||||||
|
schema:
|
||||||
|
type: integer
|
||||||
|
enum: [0, 1]
|
||||||
|
- name: "filter[language]"
|
||||||
|
in: query
|
||||||
|
description: Filter by language tag (e.g. `en-GB`, `*`)
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
example: "*"
|
||||||
|
- name: "filter[search]"
|
||||||
|
in: query
|
||||||
|
description: Free-text search across tag fields
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
- name: "page[offset]"
|
||||||
|
in: query
|
||||||
|
description: Number of records to skip (pagination offset)
|
||||||
|
schema:
|
||||||
|
type: integer
|
||||||
|
minimum: 0
|
||||||
|
default: 0
|
||||||
|
- name: "page[limit]"
|
||||||
|
in: query
|
||||||
|
description: Maximum number of records to return
|
||||||
|
schema:
|
||||||
|
type: integer
|
||||||
|
minimum: 1
|
||||||
|
maximum: 100
|
||||||
|
default: 25
|
||||||
|
- name: "list[fullordering]"
|
||||||
|
in: query
|
||||||
|
description: Sort order for results
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
enum:
|
||||||
|
- a.id ASC
|
||||||
|
- a.id DESC
|
||||||
|
- a.og_title ASC
|
||||||
|
- a.og_title DESC
|
||||||
|
- a.modified ASC
|
||||||
|
- a.modified DESC
|
||||||
|
default: a.modified DESC
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: A JSON:API collection of OG tags
|
||||||
|
content:
|
||||||
|
application/vnd.api+json:
|
||||||
|
schema:
|
||||||
|
$ref: "#/components/schemas/TagCollection"
|
||||||
|
example:
|
||||||
|
links:
|
||||||
|
self: "/api/index.php/v1/mokoog/tags"
|
||||||
|
data:
|
||||||
|
- type: tags
|
||||||
|
id: "1"
|
||||||
|
attributes:
|
||||||
|
content_type: com_content
|
||||||
|
content_id: 42
|
||||||
|
og_title: "My Article Title"
|
||||||
|
og_description: "A brief description for social sharing."
|
||||||
|
og_image: "images/mokoog/og-banner.jpg"
|
||||||
|
og_type: article
|
||||||
|
seo_title: "My Article | Example Site"
|
||||||
|
meta_description: "A brief meta description for search engines."
|
||||||
|
robots: "index, follow"
|
||||||
|
canonical_url: "https://example.com/my-article"
|
||||||
|
language: "*"
|
||||||
|
published: 1
|
||||||
|
created: "2026-06-01T12:00:00+00:00"
|
||||||
|
modified: "2026-06-15T08:30:00+00:00"
|
||||||
|
meta:
|
||||||
|
total-pages: 1
|
||||||
|
"401":
|
||||||
|
$ref: "#/components/responses/Unauthorized"
|
||||||
|
|
||||||
|
post:
|
||||||
|
operationId: createTag
|
||||||
|
summary: Create an OG tag
|
||||||
|
description: |
|
||||||
|
Creates a new OG tag record. The combination of `content_type`,
|
||||||
|
`content_id`, and `language` must be unique.
|
||||||
|
tags: [Tags]
|
||||||
|
requestBody:
|
||||||
|
required: true
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: "#/components/schemas/TagCreateRequest"
|
||||||
|
example:
|
||||||
|
content_type: com_content
|
||||||
|
content_id: 42
|
||||||
|
og_title: "My Article Title"
|
||||||
|
og_description: "A brief description for social sharing."
|
||||||
|
og_image: "images/mokoog/og-banner.jpg"
|
||||||
|
og_type: article
|
||||||
|
language: "*"
|
||||||
|
published: 1
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: The created tag
|
||||||
|
content:
|
||||||
|
application/vnd.api+json:
|
||||||
|
schema:
|
||||||
|
$ref: "#/components/schemas/TagDocument"
|
||||||
|
example:
|
||||||
|
links:
|
||||||
|
self: "/api/index.php/v1/mokoog/tags/1"
|
||||||
|
data:
|
||||||
|
type: tags
|
||||||
|
id: "1"
|
||||||
|
attributes:
|
||||||
|
content_type: com_content
|
||||||
|
content_id: 42
|
||||||
|
og_title: "My Article Title"
|
||||||
|
og_description: "A brief description for social sharing."
|
||||||
|
og_image: "images/mokoog/og-banner.jpg"
|
||||||
|
og_type: article
|
||||||
|
seo_title: ""
|
||||||
|
meta_description: ""
|
||||||
|
robots: ""
|
||||||
|
canonical_url: ""
|
||||||
|
language: "*"
|
||||||
|
published: 1
|
||||||
|
created: "2026-06-23T10:00:00+00:00"
|
||||||
|
modified: "2026-06-23T10:00:00+00:00"
|
||||||
|
"400":
|
||||||
|
$ref: "#/components/responses/BadRequest"
|
||||||
|
"401":
|
||||||
|
$ref: "#/components/responses/Unauthorized"
|
||||||
|
|
||||||
|
/mokoog/tags/{id}:
|
||||||
|
parameters:
|
||||||
|
- $ref: "#/components/parameters/TagId"
|
||||||
|
|
||||||
|
get:
|
||||||
|
operationId: getTag
|
||||||
|
summary: Get a single OG tag
|
||||||
|
tags: [Tags]
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: A single OG tag resource
|
||||||
|
content:
|
||||||
|
application/vnd.api+json:
|
||||||
|
schema:
|
||||||
|
$ref: "#/components/schemas/TagDocument"
|
||||||
|
example:
|
||||||
|
links:
|
||||||
|
self: "/api/index.php/v1/mokoog/tags/1"
|
||||||
|
data:
|
||||||
|
type: tags
|
||||||
|
id: "1"
|
||||||
|
attributes:
|
||||||
|
content_type: com_content
|
||||||
|
content_id: 42
|
||||||
|
og_title: "My Article Title"
|
||||||
|
og_description: "A brief description for social sharing."
|
||||||
|
og_image: "images/mokoog/og-banner.jpg"
|
||||||
|
og_type: article
|
||||||
|
seo_title: "My Article | Example Site"
|
||||||
|
meta_description: "A brief meta description for search engines."
|
||||||
|
robots: "index, follow"
|
||||||
|
canonical_url: "https://example.com/my-article"
|
||||||
|
language: "*"
|
||||||
|
published: 1
|
||||||
|
created: "2026-06-01T12:00:00+00:00"
|
||||||
|
modified: "2026-06-15T08:30:00+00:00"
|
||||||
|
"401":
|
||||||
|
$ref: "#/components/responses/Unauthorized"
|
||||||
|
"404":
|
||||||
|
$ref: "#/components/responses/NotFound"
|
||||||
|
|
||||||
|
patch:
|
||||||
|
operationId: updateTag
|
||||||
|
summary: Update an OG tag
|
||||||
|
description: Partially updates an existing OG tag. Only supplied fields are changed.
|
||||||
|
tags: [Tags]
|
||||||
|
requestBody:
|
||||||
|
required: true
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: "#/components/schemas/TagUpdateRequest"
|
||||||
|
example:
|
||||||
|
og_title: "Updated Title"
|
||||||
|
og_description: "Updated social description."
|
||||||
|
published: 0
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: The updated tag
|
||||||
|
content:
|
||||||
|
application/vnd.api+json:
|
||||||
|
schema:
|
||||||
|
$ref: "#/components/schemas/TagDocument"
|
||||||
|
"400":
|
||||||
|
$ref: "#/components/responses/BadRequest"
|
||||||
|
"401":
|
||||||
|
$ref: "#/components/responses/Unauthorized"
|
||||||
|
"404":
|
||||||
|
$ref: "#/components/responses/NotFound"
|
||||||
|
|
||||||
|
delete:
|
||||||
|
operationId: deleteTag
|
||||||
|
summary: Delete an OG tag
|
||||||
|
tags: [Tags]
|
||||||
|
responses:
|
||||||
|
"204":
|
||||||
|
description: Tag deleted successfully
|
||||||
|
"401":
|
||||||
|
$ref: "#/components/responses/Unauthorized"
|
||||||
|
"404":
|
||||||
|
$ref: "#/components/responses/NotFound"
|
||||||
|
|
||||||
|
/mokoog/lookup/{content_type}/{content_id}:
|
||||||
|
get:
|
||||||
|
operationId: lookupTag
|
||||||
|
summary: Look up an OG tag by content type and content ID
|
||||||
|
description: |
|
||||||
|
Resolves an OG tag by its `content_type` and `content_id` pair and
|
||||||
|
returns the full tag resource. This is a convenience endpoint that
|
||||||
|
avoids the caller needing to know the internal tag ID.
|
||||||
|
tags: [Tags]
|
||||||
|
parameters:
|
||||||
|
- name: content_type
|
||||||
|
in: path
|
||||||
|
required: true
|
||||||
|
description: |
|
||||||
|
The content type identifier (e.g. `com_content`, `menu`,
|
||||||
|
`com_mokoshop`). Must match the pattern `[a-z][a-z0-9_.]*`.
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
pattern: "^[a-z][a-z0-9_.]*$"
|
||||||
|
example: com_content
|
||||||
|
- name: content_id
|
||||||
|
in: path
|
||||||
|
required: true
|
||||||
|
description: The content item ID
|
||||||
|
schema:
|
||||||
|
type: integer
|
||||||
|
minimum: 1
|
||||||
|
example: 42
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: The matching OG tag resource
|
||||||
|
content:
|
||||||
|
application/vnd.api+json:
|
||||||
|
schema:
|
||||||
|
$ref: "#/components/schemas/TagDocument"
|
||||||
|
"400":
|
||||||
|
description: Missing or invalid content_type / content_id
|
||||||
|
content:
|
||||||
|
application/vnd.api+json:
|
||||||
|
schema:
|
||||||
|
$ref: "#/components/schemas/ErrorResponse"
|
||||||
|
example:
|
||||||
|
errors:
|
||||||
|
- title: Bad Request
|
||||||
|
status: 400
|
||||||
|
detail: "content_type and content_id are required"
|
||||||
|
"401":
|
||||||
|
$ref: "#/components/responses/Unauthorized"
|
||||||
|
"404":
|
||||||
|
description: No OG tag found for the given content_type and content_id
|
||||||
|
content:
|
||||||
|
application/vnd.api+json:
|
||||||
|
schema:
|
||||||
|
$ref: "#/components/schemas/ErrorResponse"
|
||||||
|
example:
|
||||||
|
errors:
|
||||||
|
- title: Not Found
|
||||||
|
status: 404
|
||||||
|
detail: "OG tag not found for com_content:42"
|
||||||
|
|
||||||
|
components:
|
||||||
|
securitySchemes:
|
||||||
|
apiToken:
|
||||||
|
type: apiKey
|
||||||
|
name: X-Joomla-Token
|
||||||
|
in: header
|
||||||
|
description: |
|
||||||
|
Joomla API token. Can also be passed as the `api-token` query
|
||||||
|
parameter. Generate a token from the Joomla administrator panel
|
||||||
|
under Users > Manage > [user] > Joomla API Token tab.
|
||||||
|
|
||||||
|
parameters:
|
||||||
|
TagId:
|
||||||
|
name: id
|
||||||
|
in: path
|
||||||
|
required: true
|
||||||
|
description: The OG tag record ID
|
||||||
|
schema:
|
||||||
|
type: integer
|
||||||
|
minimum: 1
|
||||||
|
example: 1
|
||||||
|
|
||||||
|
schemas:
|
||||||
|
TagAttributes:
|
||||||
|
type: object
|
||||||
|
description: Full set of OG tag attributes returned by the API
|
||||||
|
properties:
|
||||||
|
content_type:
|
||||||
|
type: string
|
||||||
|
description: |
|
||||||
|
Content type identifier (e.g. `com_content`, `menu`,
|
||||||
|
`com_mokoshop`). Must match `[a-z][a-z0-9_.]*`.
|
||||||
|
pattern: "^[a-z][a-z0-9_.]*$"
|
||||||
|
maxLength: 100
|
||||||
|
example: com_content
|
||||||
|
content_id:
|
||||||
|
type: integer
|
||||||
|
description: The ID of the associated content item
|
||||||
|
minimum: 1
|
||||||
|
example: 42
|
||||||
|
og_title:
|
||||||
|
type: string
|
||||||
|
description: Open Graph title (`og:title`)
|
||||||
|
maxLength: 255
|
||||||
|
example: "My Article Title"
|
||||||
|
og_description:
|
||||||
|
type: string
|
||||||
|
description: Open Graph description (`og:description`)
|
||||||
|
example: "A brief description for social sharing."
|
||||||
|
og_image:
|
||||||
|
type: string
|
||||||
|
description: Relative path to the Open Graph image (`og:image`)
|
||||||
|
maxLength: 512
|
||||||
|
example: "images/mokoog/og-banner.jpg"
|
||||||
|
og_type:
|
||||||
|
type: string
|
||||||
|
description: Open Graph type (`og:type`)
|
||||||
|
default: article
|
||||||
|
enum:
|
||||||
|
- article
|
||||||
|
- website
|
||||||
|
- product
|
||||||
|
- profile
|
||||||
|
- book
|
||||||
|
- music.song
|
||||||
|
- music.album
|
||||||
|
- video.movie
|
||||||
|
- video.episode
|
||||||
|
- video.other
|
||||||
|
example: article
|
||||||
|
seo_title:
|
||||||
|
type: string
|
||||||
|
description: SEO page title (used in `<title>` tag)
|
||||||
|
maxLength: 70
|
||||||
|
example: "My Article | Example Site"
|
||||||
|
meta_description:
|
||||||
|
type: string
|
||||||
|
description: Meta description for search engines
|
||||||
|
maxLength: 200
|
||||||
|
example: "A brief meta description for search engines."
|
||||||
|
robots:
|
||||||
|
type: string
|
||||||
|
description: |
|
||||||
|
Comma-separated robots directives. Valid directives: `index`,
|
||||||
|
`noindex`, `follow`, `nofollow`, `none`, `noarchive`,
|
||||||
|
`nosnippet`, `noimageindex`, `max-snippet`, `max-image-preview`.
|
||||||
|
maxLength: 100
|
||||||
|
example: "index, follow"
|
||||||
|
canonical_url:
|
||||||
|
type: string
|
||||||
|
format: uri
|
||||||
|
description: Canonical URL for the page
|
||||||
|
maxLength: 512
|
||||||
|
example: "https://example.com/my-article"
|
||||||
|
language:
|
||||||
|
type: string
|
||||||
|
description: Joomla language tag (`*` for all languages)
|
||||||
|
maxLength: 7
|
||||||
|
default: "*"
|
||||||
|
example: "*"
|
||||||
|
published:
|
||||||
|
type: integer
|
||||||
|
description: Published state (1 = published, 0 = unpublished)
|
||||||
|
enum: [0, 1]
|
||||||
|
default: 1
|
||||||
|
example: 1
|
||||||
|
created:
|
||||||
|
type: string
|
||||||
|
format: date-time
|
||||||
|
description: Record creation timestamp (read-only)
|
||||||
|
readOnly: true
|
||||||
|
example: "2026-06-01T12:00:00+00:00"
|
||||||
|
modified:
|
||||||
|
type: string
|
||||||
|
format: date-time
|
||||||
|
description: Last modification timestamp (read-only)
|
||||||
|
readOnly: true
|
||||||
|
example: "2026-06-15T08:30:00+00:00"
|
||||||
|
|
||||||
|
TagResource:
|
||||||
|
type: object
|
||||||
|
description: A single OG tag in JSON:API resource format
|
||||||
|
required: [type, id, attributes]
|
||||||
|
properties:
|
||||||
|
type:
|
||||||
|
type: string
|
||||||
|
enum: [tags]
|
||||||
|
example: tags
|
||||||
|
id:
|
||||||
|
type: string
|
||||||
|
description: The record ID as a string (per JSON:API spec)
|
||||||
|
example: "1"
|
||||||
|
attributes:
|
||||||
|
$ref: "#/components/schemas/TagAttributes"
|
||||||
|
|
||||||
|
TagDocument:
|
||||||
|
type: object
|
||||||
|
description: JSON:API document containing a single tag resource
|
||||||
|
properties:
|
||||||
|
links:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
self:
|
||||||
|
type: string
|
||||||
|
example: "/api/index.php/v1/mokoog/tags/1"
|
||||||
|
data:
|
||||||
|
$ref: "#/components/schemas/TagResource"
|
||||||
|
|
||||||
|
TagCollection:
|
||||||
|
type: object
|
||||||
|
description: JSON:API document containing a collection of tag resources
|
||||||
|
properties:
|
||||||
|
links:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
self:
|
||||||
|
type: string
|
||||||
|
example: "/api/index.php/v1/mokoog/tags"
|
||||||
|
data:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: "#/components/schemas/TagResource"
|
||||||
|
meta:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
total-pages:
|
||||||
|
type: integer
|
||||||
|
description: Total number of pages available
|
||||||
|
example: 1
|
||||||
|
|
||||||
|
TagCreateRequest:
|
||||||
|
type: object
|
||||||
|
description: Request body for creating a new OG tag
|
||||||
|
required:
|
||||||
|
- content_type
|
||||||
|
- content_id
|
||||||
|
properties:
|
||||||
|
content_type:
|
||||||
|
type: string
|
||||||
|
pattern: "^[a-z][a-z0-9_.]*$"
|
||||||
|
maxLength: 100
|
||||||
|
example: com_content
|
||||||
|
content_id:
|
||||||
|
type: integer
|
||||||
|
minimum: 1
|
||||||
|
example: 42
|
||||||
|
og_title:
|
||||||
|
type: string
|
||||||
|
maxLength: 255
|
||||||
|
og_description:
|
||||||
|
type: string
|
||||||
|
og_image:
|
||||||
|
type: string
|
||||||
|
maxLength: 512
|
||||||
|
og_type:
|
||||||
|
type: string
|
||||||
|
default: article
|
||||||
|
enum:
|
||||||
|
- article
|
||||||
|
- website
|
||||||
|
- product
|
||||||
|
- profile
|
||||||
|
- book
|
||||||
|
- music.song
|
||||||
|
- music.album
|
||||||
|
- video.movie
|
||||||
|
- video.episode
|
||||||
|
- video.other
|
||||||
|
og_video:
|
||||||
|
type: string
|
||||||
|
format: uri
|
||||||
|
description: Open Graph video URL (`og:video`)
|
||||||
|
maxLength: 512
|
||||||
|
seo_title:
|
||||||
|
type: string
|
||||||
|
maxLength: 70
|
||||||
|
meta_description:
|
||||||
|
type: string
|
||||||
|
maxLength: 200
|
||||||
|
robots:
|
||||||
|
type: string
|
||||||
|
maxLength: 100
|
||||||
|
canonical_url:
|
||||||
|
type: string
|
||||||
|
format: uri
|
||||||
|
maxLength: 512
|
||||||
|
language:
|
||||||
|
type: string
|
||||||
|
maxLength: 7
|
||||||
|
default: "*"
|
||||||
|
published:
|
||||||
|
type: integer
|
||||||
|
enum: [0, 1]
|
||||||
|
default: 1
|
||||||
|
|
||||||
|
TagUpdateRequest:
|
||||||
|
type: object
|
||||||
|
description: |
|
||||||
|
Request body for updating an OG tag. All fields are optional; only
|
||||||
|
supplied fields are modified.
|
||||||
|
properties:
|
||||||
|
og_title:
|
||||||
|
type: string
|
||||||
|
maxLength: 255
|
||||||
|
og_description:
|
||||||
|
type: string
|
||||||
|
og_image:
|
||||||
|
type: string
|
||||||
|
maxLength: 512
|
||||||
|
og_type:
|
||||||
|
type: string
|
||||||
|
enum:
|
||||||
|
- article
|
||||||
|
- website
|
||||||
|
- product
|
||||||
|
- profile
|
||||||
|
- book
|
||||||
|
- music.song
|
||||||
|
- music.album
|
||||||
|
- video.movie
|
||||||
|
- video.episode
|
||||||
|
- video.other
|
||||||
|
og_video:
|
||||||
|
type: string
|
||||||
|
format: uri
|
||||||
|
maxLength: 512
|
||||||
|
seo_title:
|
||||||
|
type: string
|
||||||
|
maxLength: 70
|
||||||
|
meta_description:
|
||||||
|
type: string
|
||||||
|
maxLength: 200
|
||||||
|
robots:
|
||||||
|
type: string
|
||||||
|
maxLength: 100
|
||||||
|
canonical_url:
|
||||||
|
type: string
|
||||||
|
format: uri
|
||||||
|
maxLength: 512
|
||||||
|
language:
|
||||||
|
type: string
|
||||||
|
maxLength: 7
|
||||||
|
published:
|
||||||
|
type: integer
|
||||||
|
enum: [0, 1]
|
||||||
|
|
||||||
|
ErrorResponse:
|
||||||
|
type: object
|
||||||
|
description: JSON:API error response
|
||||||
|
properties:
|
||||||
|
errors:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
title:
|
||||||
|
type: string
|
||||||
|
example: Not Found
|
||||||
|
status:
|
||||||
|
type: integer
|
||||||
|
example: 404
|
||||||
|
detail:
|
||||||
|
type: string
|
||||||
|
example: "Item not found."
|
||||||
|
|
||||||
|
responses:
|
||||||
|
BadRequest:
|
||||||
|
description: Invalid request data
|
||||||
|
content:
|
||||||
|
application/vnd.api+json:
|
||||||
|
schema:
|
||||||
|
$ref: "#/components/schemas/ErrorResponse"
|
||||||
|
example:
|
||||||
|
errors:
|
||||||
|
- title: Bad Request
|
||||||
|
status: 400
|
||||||
|
detail: "Content type is required."
|
||||||
|
|
||||||
|
Unauthorized:
|
||||||
|
description: Missing or invalid API token
|
||||||
|
content:
|
||||||
|
application/vnd.api+json:
|
||||||
|
schema:
|
||||||
|
$ref: "#/components/schemas/ErrorResponse"
|
||||||
|
example:
|
||||||
|
errors:
|
||||||
|
- title: Forbidden
|
||||||
|
status: 403
|
||||||
|
detail: "You are not authorised to access this resource."
|
||||||
|
|
||||||
|
NotFound:
|
||||||
|
description: Resource not found
|
||||||
|
content:
|
||||||
|
application/vnd.api+json:
|
||||||
|
schema:
|
||||||
|
$ref: "#/components/schemas/ErrorResponse"
|
||||||
|
example:
|
||||||
|
errors:
|
||||||
|
- title: Not Found
|
||||||
|
status: 404
|
||||||
|
detail: "Item not found."
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/10.5/phpunit.xsd"
|
||||||
|
bootstrap="tests/bootstrap.php"
|
||||||
|
colors="true"
|
||||||
|
cacheDirectory=".phpunit.cache">
|
||||||
|
<testsuites>
|
||||||
|
<testsuite name="Unit">
|
||||||
|
<directory>tests/Unit</directory>
|
||||||
|
</testsuite>
|
||||||
|
</testsuites>
|
||||||
|
<source>
|
||||||
|
<include>
|
||||||
|
<directory>source/packages</directory>
|
||||||
|
</include>
|
||||||
|
</source>
|
||||||
|
</phpunit>
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
; MokoJoomOpenGraph - Package System Language File
|
; MokoSuiteOpenGraph - Package System Language File
|
||||||
; Copyright (C) 2026 Moko Consulting. All rights reserved.
|
; Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||||
; License: GPL-3.0-or-later
|
; License: GPL-3.0-or-later
|
||||||
|
|
||||||
PKG_MOKOOG="MokoJoomOpenGraph"
|
PKG_MOKOOG="MokoSuiteOpenGraph"
|
||||||
PKG_MOKOOG_DESCRIPTION="Complete Open Graph, Twitter Card, and social sharing meta tag management for Joomla. Control how every page appears when shared on Facebook, Twitter/X, LinkedIn, WhatsApp, and more."
|
PKG_MOKOOG_DESCRIPTION="Complete Open Graph, Twitter Card, and social sharing meta tag management for Joomla. Control how every page appears when shared on Facebook, Twitter/X, LinkedIn, WhatsApp, and more."
|
||||||
PKG_MOKOOG_PHP_VERSION_ERROR="MokoJoomOpenGraph requires PHP %s or later."
|
PKG_MOKOOG_PHP_VERSION_ERROR="MokoSuiteOpenGraph requires PHP %s or later."
|
||||||
|
PKG_MOKOOG_JOOMLA_VERSION_ERROR="MokoSuiteOpenGraph requires Joomla %s or later."
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
; MokoJoomOpenGraph - Package System Language File
|
; MokoSuiteOpenGraph - Package System Language File
|
||||||
; Copyright (C) 2026 Moko Consulting. All rights reserved.
|
; Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||||
; License: GPL-3.0-or-later
|
; License: GPL-3.0-or-later
|
||||||
|
|
||||||
PKG_MOKOOG="MokoJoomOpenGraph"
|
PKG_MOKOOG="MokoSuiteOpenGraph"
|
||||||
PKG_MOKOOG_DESCRIPTION="Complete Open Graph, Twitter Card, and social sharing meta tag management for Joomla. Control how every page appears when shared on Facebook, Twitter/X, LinkedIn, WhatsApp, and more."
|
PKG_MOKOOG_DESCRIPTION="Complete Open Graph, Twitter Card, and social sharing meta tag management for Joomla. Control how every page appears when shared on Facebook, Twitter/X, LinkedIn, WhatsApp, and more."
|
||||||
PKG_MOKOOG_PHP_VERSION_ERROR="MokoJoomOpenGraph requires PHP %s or later."
|
PKG_MOKOOG_PHP_VERSION_ERROR="MokoSuiteOpenGraph requires PHP %s or later."
|
||||||
|
PKG_MOKOOG_JOOMLA_VERSION_ERROR="MokoSuiteOpenGraph requires Joomla %s or later."
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @package MokoJoomOpenGraph
|
* @package MokoSuiteOpenGraph
|
||||||
* @subpackage com_mokoog.api
|
* @subpackage com_mokoog.api
|
||||||
* @author Moko Consulting <hello@mokoconsulting.tech>
|
* @author Moko Consulting <hello@mokoconsulting.tech>
|
||||||
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||||
@@ -47,7 +47,7 @@ class TagsController extends ApiController
|
|||||||
throw new \RuntimeException('content_type and content_id are required', 400);
|
throw new \RuntimeException('content_type and content_id are required', 400);
|
||||||
}
|
}
|
||||||
|
|
||||||
$db = Factory::getDbo();
|
$db = Factory::getContainer()->get(\Joomla\Database\DatabaseInterface::class);
|
||||||
$query = $db->getQuery(true)
|
$query = $db->getQuery(true)
|
||||||
->select($db->quoteName('id'))
|
->select($db->quoteName('id'))
|
||||||
->from($db->quoteName('#__mokoog_tags'))
|
->from($db->quoteName('#__mokoog_tags'))
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @package MokoJoomOpenGraph
|
* @package MokoSuiteOpenGraph
|
||||||
* @subpackage com_mokoog.api
|
* @subpackage com_mokoog.api
|
||||||
* @author Moko Consulting <hello@mokoconsulting.tech>
|
* @author Moko Consulting <hello@mokoconsulting.tech>
|
||||||
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<!--
|
<!--
|
||||||
* @package MokoJoomOpenGraph
|
* @package MokoSuiteOpenGraph
|
||||||
* @subpackage com_mokoog
|
* @subpackage com_mokoog
|
||||||
* @author Moko Consulting <hello@mokoconsulting.tech>
|
* @author Moko Consulting <hello@mokoconsulting.tech>
|
||||||
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<!--
|
<!--
|
||||||
* @package MokoJoomOpenGraph
|
* @package MokoSuiteOpenGraph
|
||||||
* @subpackage com_mokoog
|
* @subpackage com_mokoog
|
||||||
* @author Moko Consulting <hello@mokoconsulting.tech>
|
* @author Moko Consulting <hello@mokoconsulting.tech>
|
||||||
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||||
@@ -30,7 +30,7 @@
|
|||||||
label="COM_MOKOOG_FIELD_OG_TITLE"
|
label="COM_MOKOOG_FIELD_OG_TITLE"
|
||||||
description="COM_MOKOOG_FIELD_OG_TITLE_DESC"
|
description="COM_MOKOOG_FIELD_OG_TITLE_DESC"
|
||||||
filter="string"
|
filter="string"
|
||||||
maxlength="70"
|
maxlength="255"
|
||||||
/>
|
/>
|
||||||
<field
|
<field
|
||||||
name="og_description"
|
name="og_description"
|
||||||
@@ -39,7 +39,7 @@
|
|||||||
description="COM_MOKOOG_FIELD_OG_DESCRIPTION_DESC"
|
description="COM_MOKOOG_FIELD_OG_DESCRIPTION_DESC"
|
||||||
filter="string"
|
filter="string"
|
||||||
rows="3"
|
rows="3"
|
||||||
maxlength="200"
|
maxlength="512"
|
||||||
/>
|
/>
|
||||||
<field
|
<field
|
||||||
name="og_image"
|
name="og_image"
|
||||||
@@ -60,6 +60,14 @@
|
|||||||
<option value="product">Product</option>
|
<option value="product">Product</option>
|
||||||
<option value="profile">Profile</option>
|
<option value="profile">Profile</option>
|
||||||
</field>
|
</field>
|
||||||
|
<field
|
||||||
|
name="og_video"
|
||||||
|
type="url"
|
||||||
|
label="COM_MOKOOG_FIELD_OG_VIDEO"
|
||||||
|
description="COM_MOKOOG_FIELD_OG_VIDEO_DESC"
|
||||||
|
filter="url"
|
||||||
|
validate="url"
|
||||||
|
/>
|
||||||
<field
|
<field
|
||||||
name="published"
|
name="published"
|
||||||
type="list"
|
type="list"
|
||||||
@@ -77,7 +85,7 @@
|
|||||||
label="PLG_CONTENT_MOKOOG_FIELD_SEO_TITLE"
|
label="PLG_CONTENT_MOKOOG_FIELD_SEO_TITLE"
|
||||||
description="PLG_CONTENT_MOKOOG_FIELD_SEO_TITLE_DESC"
|
description="PLG_CONTENT_MOKOOG_FIELD_SEO_TITLE_DESC"
|
||||||
filter="string"
|
filter="string"
|
||||||
maxlength="70"
|
maxlength="255"
|
||||||
/>
|
/>
|
||||||
<field
|
<field
|
||||||
name="meta_description"
|
name="meta_description"
|
||||||
@@ -86,7 +94,7 @@
|
|||||||
description="PLG_CONTENT_MOKOOG_FIELD_META_DESCRIPTION_DESC"
|
description="PLG_CONTENT_MOKOOG_FIELD_META_DESCRIPTION_DESC"
|
||||||
filter="string"
|
filter="string"
|
||||||
rows="3"
|
rows="3"
|
||||||
maxlength="200"
|
maxlength="255"
|
||||||
/>
|
/>
|
||||||
<field
|
<field
|
||||||
name="robots"
|
name="robots"
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
; MokoJoomOpenGraph - Component Language File
|
; MokoSuiteOpenGraph - Component Language File
|
||||||
; Copyright (C) 2026 Moko Consulting. All rights reserved.
|
; Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||||
; License: GPL-3.0-or-later
|
; License: GPL-3.0-or-later
|
||||||
|
|
||||||
COM_MOKOOG="MokoJoomOpenGraph"
|
COM_MOKOOG="MokoSuiteOpenGraph"
|
||||||
COM_MOKOOG_TAGS_TITLE="MokoJoomOpenGraph - Tag Manager"
|
COM_MOKOOG_TAGS_TITLE="MokoSuiteOpenGraph - Tag Manager"
|
||||||
COM_MOKOOG_SUBMENU_TAGS="Tags"
|
COM_MOKOOG_SUBMENU_TAGS="Tags"
|
||||||
COM_MOKOOG_NO_TAGS="No Open Graph tags have been created yet. Tags are created automatically when you edit articles or menu items."
|
COM_MOKOOG_NO_TAGS="No Open Graph tags have been created yet. Tags are created automatically when you edit articles or menu items."
|
||||||
COM_MOKOOG_TABLE_CAPTION="Table of Open Graph tags"
|
COM_MOKOOG_TABLE_CAPTION="Table of Open Graph tags"
|
||||||
@@ -32,6 +32,8 @@ COM_MOKOOG_FIELD_OG_IMAGE="OG Image"
|
|||||||
COM_MOKOOG_FIELD_OG_IMAGE_DESC="Custom image for social sharing."
|
COM_MOKOOG_FIELD_OG_IMAGE_DESC="Custom image for social sharing."
|
||||||
COM_MOKOOG_FIELD_OG_TYPE="OG Type"
|
COM_MOKOOG_FIELD_OG_TYPE="OG Type"
|
||||||
COM_MOKOOG_FIELD_OG_TYPE_DESC="The Open Graph content type."
|
COM_MOKOOG_FIELD_OG_TYPE_DESC="The Open Graph content type."
|
||||||
|
COM_MOKOOG_FIELD_OG_VIDEO="Video URL"
|
||||||
|
COM_MOKOOG_FIELD_OG_VIDEO_DESC="URL of a video for social sharing previews. Supports direct video URLs and YouTube/Vimeo links."
|
||||||
|
|
||||||
COM_MOKOOG_FILTER_SEARCH="Search OG titles"
|
COM_MOKOOG_FILTER_SEARCH="Search OG titles"
|
||||||
COM_MOKOOG_FILTER_CONTENT_TYPE="Content Type"
|
COM_MOKOOG_FILTER_CONTENT_TYPE="Content Type"
|
||||||
@@ -57,3 +59,10 @@ COM_MOKOOG_IMPORT_INVALID_TYPE="Invalid file type. Please upload a .csv file."
|
|||||||
COM_MOKOOG_IMPORT_FILE_TOO_LARGE="File is too large. Maximum allowed size is %s."
|
COM_MOKOOG_IMPORT_FILE_TOO_LARGE="File is too large. Maximum allowed size is %s."
|
||||||
COM_MOKOOG_IMPORT_READ_ERROR="Could not read the uploaded CSV file."
|
COM_MOKOOG_IMPORT_READ_ERROR="Could not read the uploaded CSV file."
|
||||||
COM_MOKOOG_IMPORT_RESULT="Import complete: %d created, %d updated, %d skipped."
|
COM_MOKOOG_IMPORT_RESULT="Import complete: %d created, %d updated, %d skipped."
|
||||||
|
|
||||||
|
COM_MOKOOG_COVERAGE_TITLE="OG Tag Coverage"
|
||||||
|
COM_MOKOOG_COVERAGE_PERCENT="OG Coverage"
|
||||||
|
COM_MOKOOG_COVERAGE_ARTICLES="%d of %d articles have OG tags"
|
||||||
|
COM_MOKOOG_COVERAGE_MISSING_TITLE="%d tags missing custom title"
|
||||||
|
COM_MOKOOG_COVERAGE_MISSING_DESC="%d tags missing custom description"
|
||||||
|
COM_MOKOOG_COVERAGE_MISSING_IMAGE="%d tags missing custom image"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
; MokoJoomOpenGraph - Component System Language File
|
; MokoSuiteOpenGraph - Component System Language File
|
||||||
; Copyright (C) 2026 Moko Consulting. All rights reserved.
|
; Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||||
; License: GPL-3.0-or-later
|
; License: GPL-3.0-or-later
|
||||||
|
|
||||||
COM_MOKOOG="MokoJoomOpenGraph"
|
COM_MOKOOG="MokoSuiteOpenGraph"
|
||||||
COM_MOKOOG_DESCRIPTION="Manage Open Graph and social sharing tags for all your content. View, edit, and batch-process OG metadata."
|
COM_MOKOOG_DESCRIPTION="Manage Open Graph and social sharing tags for all your content. View, edit, and batch-process OG metadata."
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
; MokoJoomOpenGraph - Component Language File
|
; MokoSuiteOpenGraph - Component Language File
|
||||||
; Copyright (C) 2026 Moko Consulting. All rights reserved.
|
; Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||||
; License: GPL-3.0-or-later
|
; License: GPL-3.0-or-later
|
||||||
|
|
||||||
COM_MOKOOG="MokoJoomOpenGraph"
|
COM_MOKOOG="MokoSuiteOpenGraph"
|
||||||
COM_MOKOOG_TAGS_TITLE="MokoJoomOpenGraph - Tag Manager"
|
COM_MOKOOG_TAGS_TITLE="MokoSuiteOpenGraph - Tag Manager"
|
||||||
COM_MOKOOG_SUBMENU_TAGS="Tags"
|
COM_MOKOOG_SUBMENU_TAGS="Tags"
|
||||||
COM_MOKOOG_NO_TAGS="No Open Graph tags have been created yet. Tags are created automatically when you edit articles or menu items."
|
COM_MOKOOG_NO_TAGS="No Open Graph tags have been created yet. Tags are created automatically when you edit articles or menu items."
|
||||||
COM_MOKOOG_TABLE_CAPTION="Table of Open Graph tags"
|
COM_MOKOOG_TABLE_CAPTION="Table of Open Graph tags"
|
||||||
@@ -32,6 +32,8 @@ COM_MOKOOG_FIELD_OG_IMAGE="OG Image"
|
|||||||
COM_MOKOOG_FIELD_OG_IMAGE_DESC="Custom image for social sharing."
|
COM_MOKOOG_FIELD_OG_IMAGE_DESC="Custom image for social sharing."
|
||||||
COM_MOKOOG_FIELD_OG_TYPE="OG Type"
|
COM_MOKOOG_FIELD_OG_TYPE="OG Type"
|
||||||
COM_MOKOOG_FIELD_OG_TYPE_DESC="The Open Graph content type."
|
COM_MOKOOG_FIELD_OG_TYPE_DESC="The Open Graph content type."
|
||||||
|
COM_MOKOOG_FIELD_OG_VIDEO="Video URL"
|
||||||
|
COM_MOKOOG_FIELD_OG_VIDEO_DESC="URL of a video for social sharing previews. Supports direct video URLs and YouTube/Vimeo links."
|
||||||
|
|
||||||
COM_MOKOOG_FILTER_SEARCH="Search OG titles"
|
COM_MOKOOG_FILTER_SEARCH="Search OG titles"
|
||||||
COM_MOKOOG_FILTER_CONTENT_TYPE="Content Type"
|
COM_MOKOOG_FILTER_CONTENT_TYPE="Content Type"
|
||||||
@@ -57,3 +59,10 @@ COM_MOKOOG_IMPORT_INVALID_TYPE="Invalid file type. Please upload a .csv file."
|
|||||||
COM_MOKOOG_IMPORT_FILE_TOO_LARGE="File is too large. Maximum allowed size is %s."
|
COM_MOKOOG_IMPORT_FILE_TOO_LARGE="File is too large. Maximum allowed size is %s."
|
||||||
COM_MOKOOG_IMPORT_READ_ERROR="Could not read the uploaded CSV file."
|
COM_MOKOOG_IMPORT_READ_ERROR="Could not read the uploaded CSV file."
|
||||||
COM_MOKOOG_IMPORT_RESULT="Import complete: %d created, %d updated, %d skipped."
|
COM_MOKOOG_IMPORT_RESULT="Import complete: %d created, %d updated, %d skipped."
|
||||||
|
|
||||||
|
COM_MOKOOG_COVERAGE_TITLE="OG Tag Coverage"
|
||||||
|
COM_MOKOOG_COVERAGE_PERCENT="OG Coverage"
|
||||||
|
COM_MOKOOG_COVERAGE_ARTICLES="%d of %d articles have OG tags"
|
||||||
|
COM_MOKOOG_COVERAGE_MISSING_TITLE="%d tags missing custom title"
|
||||||
|
COM_MOKOOG_COVERAGE_MISSING_DESC="%d tags missing custom description"
|
||||||
|
COM_MOKOOG_COVERAGE_MISSING_IMAGE="%d tags missing custom image"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
; MokoJoomOpenGraph - Component System Language File
|
; MokoSuiteOpenGraph - Component System Language File
|
||||||
; Copyright (C) 2026 Moko Consulting. All rights reserved.
|
; Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||||
; License: GPL-3.0-or-later
|
; License: GPL-3.0-or-later
|
||||||
|
|
||||||
COM_MOKOOG="MokoJoomOpenGraph"
|
COM_MOKOOG="MokoSuiteOpenGraph"
|
||||||
COM_MOKOOG_DESCRIPTION="Manage Open Graph and social sharing tags for all your content. View, edit, and batch-process OG metadata."
|
COM_MOKOOG_DESCRIPTION="Manage Open Graph and social sharing tags for all your content. View, edit, and batch-process OG metadata."
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<!--
|
<!--
|
||||||
* @package MokoJoomOpenGraph
|
* @package MokoSuiteOpenGraph
|
||||||
* @subpackage com_mokoog
|
* @subpackage com_mokoog
|
||||||
* @author Moko Consulting <hello@mokoconsulting.tech>
|
* @author Moko Consulting <hello@mokoconsulting.tech>
|
||||||
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||||
@@ -8,7 +8,7 @@
|
|||||||
-->
|
-->
|
||||||
<extension type="component" method="upgrade">
|
<extension type="component" method="upgrade">
|
||||||
<name>com_mokoog</name>
|
<name>com_mokoog</name>
|
||||||
<version>01.02.00</version>
|
<version>01.05.02</version>
|
||||||
<creationDate>2026-05-23</creationDate>
|
<creationDate>2026-05-23</creationDate>
|
||||||
<author>Moko Consulting</author>
|
<author>Moko Consulting</author>
|
||||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||||
@@ -23,17 +23,17 @@
|
|||||||
|
|
||||||
<install>
|
<install>
|
||||||
<sql>
|
<sql>
|
||||||
<file driver="mysql" charset="utf8">sql/install.mysql.sql</file>
|
<file driver="mysqli" charset="utf8">sql/install.mysql.sql</file>
|
||||||
</sql>
|
</sql>
|
||||||
</install>
|
</install>
|
||||||
<uninstall>
|
<uninstall>
|
||||||
<sql>
|
<sql>
|
||||||
<file driver="mysql" charset="utf8">sql/uninstall.mysql.sql</file>
|
<file driver="mysqli" charset="utf8">sql/uninstall.mysql.sql</file>
|
||||||
</sql>
|
</sql>
|
||||||
</uninstall>
|
</uninstall>
|
||||||
<update>
|
<update>
|
||||||
<schemas>
|
<schemas>
|
||||||
<schemapath type="mysql">sql/updates/mysql</schemapath>
|
<schemapath type="mysqli">sql/updates/mysql</schemapath>
|
||||||
</schemas>
|
</schemas>
|
||||||
</update>
|
</update>
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @package MokoJoomOpenGraph
|
* @package MokoSuiteOpenGraph
|
||||||
* @subpackage com_mokoog
|
* @subpackage com_mokoog
|
||||||
* @author Moko Consulting <hello@mokoconsulting.tech>
|
* @author Moko Consulting <hello@mokoconsulting.tech>
|
||||||
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||||
@@ -23,7 +23,7 @@ class Com_MokoOGInstallerScript
|
|||||||
*/
|
*/
|
||||||
public function install(InstallerAdapter $parent): void
|
public function install(InstallerAdapter $parent): void
|
||||||
{
|
{
|
||||||
echo '<p>MokoJoomOpenGraph component installed successfully.</p>';
|
echo '<p>MokoSuiteOpenGraph component installed successfully.</p>';
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -35,6 +35,6 @@ class Com_MokoOGInstallerScript
|
|||||||
*/
|
*/
|
||||||
public function update(InstallerAdapter $parent): void
|
public function update(InstallerAdapter $parent): void
|
||||||
{
|
{
|
||||||
echo '<p>MokoJoomOpenGraph component updated successfully.</p>';
|
echo '<p>MokoSuiteOpenGraph component updated successfully.</p>';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @package MokoJoomOpenGraph
|
* @package MokoSuiteOpenGraph
|
||||||
* @subpackage com_mokoog
|
* @subpackage com_mokoog
|
||||||
* @author Moko Consulting <hello@mokoconsulting.tech>
|
* @author Moko Consulting <hello@mokoconsulting.tech>
|
||||||
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||||
|
|||||||
@@ -1,17 +1,21 @@
|
|||||||
--
|
--
|
||||||
-- MokoJoomOpenGraph - Database Schema
|
-- MokoSuiteOpenGraph - Database Schema
|
||||||
-- Copyright (C) 2026 Moko Consulting. All rights reserved.
|
-- Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||||
-- License: GPL-3.0-or-later
|
-- License: GPL-3.0-or-later
|
||||||
--
|
--
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS `#__mokoog_tags` (
|
CREATE TABLE IF NOT EXISTS `#__mokoog_tags` (
|
||||||
`id` INT(11) UNSIGNED NOT NULL AUTO_INCREMENT,
|
`id` INT(11) UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||||
`content_type` VARCHAR(100) NOT NULL DEFAULT '' COMMENT 'e.g. com_content, menu, com_virtuemart',
|
`content_type` VARCHAR(100) NOT NULL DEFAULT '' COMMENT 'e.g. com_content, menu, com_mokoshop',
|
||||||
`content_id` INT(11) UNSIGNED NOT NULL DEFAULT 0,
|
`content_id` INT(11) UNSIGNED NOT NULL DEFAULT 0,
|
||||||
`og_title` VARCHAR(255) NOT NULL DEFAULT '',
|
`og_title` VARCHAR(255) NOT NULL DEFAULT '',
|
||||||
`og_description` TEXT NOT NULL,
|
`og_description` TEXT NOT NULL,
|
||||||
`og_image` VARCHAR(512) NOT NULL DEFAULT '',
|
`og_image` VARCHAR(512) NOT NULL DEFAULT '',
|
||||||
`og_type` VARCHAR(50) NOT NULL DEFAULT 'article',
|
`og_type` VARCHAR(50) NOT NULL DEFAULT 'article',
|
||||||
|
`og_video` VARCHAR(512) NOT NULL DEFAULT '',
|
||||||
|
`event_data` TEXT NULL,
|
||||||
|
`recipe_data` TEXT NULL,
|
||||||
|
`custom_schema` TEXT NULL,
|
||||||
`seo_title` VARCHAR(70) NOT NULL DEFAULT '',
|
`seo_title` VARCHAR(70) NOT NULL DEFAULT '',
|
||||||
`meta_description` VARCHAR(200) NOT NULL DEFAULT '',
|
`meta_description` VARCHAR(200) NOT NULL DEFAULT '',
|
||||||
`robots` VARCHAR(100) NOT NULL DEFAULT '',
|
`robots` VARCHAR(100) NOT NULL DEFAULT '',
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
--
|
--
|
||||||
-- MokoJoomOpenGraph - Uninstall
|
-- MokoSuiteOpenGraph - Uninstall
|
||||||
--
|
--
|
||||||
|
|
||||||
DROP TABLE IF EXISTS `#__mokoog_tags`;
|
DROP TABLE IF EXISTS `#__mokoog_tags`;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
--
|
--
|
||||||
-- MokoJoomOpenGraph 01.01.00 — Add SEO meta management columns
|
-- MokoSuiteOpenGraph 01.01.00 — Add SEO meta management columns
|
||||||
--
|
--
|
||||||
|
|
||||||
ALTER TABLE `#__mokoog_tags`
|
ALTER TABLE `#__mokoog_tags`
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
--
|
--
|
||||||
-- MokoJoomOpenGraph 01.02.00 — Add multilingual OG tag support
|
-- MokoSuiteOpenGraph 01.02.00 — Add multilingual OG tag support
|
||||||
--
|
--
|
||||||
|
|
||||||
ALTER TABLE `#__mokoog_tags`
|
ALTER TABLE `#__mokoog_tags`
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
--
|
||||||
|
-- MokoSuiteOpenGraph 01.03.00 - Add og_video column
|
||||||
|
--
|
||||||
|
|
||||||
|
ALTER TABLE `#__mokoog_tags` ADD COLUMN `og_video` VARCHAR(512) NOT NULL DEFAULT '' AFTER `og_type`;
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
--
|
||||||
|
-- MokoSuiteOpenGraph 01.04.00 - Add event_data and recipe_data columns
|
||||||
|
--
|
||||||
|
|
||||||
|
ALTER TABLE `#__mokoog_tags` ADD COLUMN `event_data` TEXT NULL AFTER `og_video`;
|
||||||
|
ALTER TABLE `#__mokoog_tags` ADD COLUMN `recipe_data` TEXT NULL AFTER `event_data`;
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
/* 01.04.09 — no schema changes */
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
/* 01.04.10 — no schema changes */
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
/* 01.04.11 — no schema changes */
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
/* 01.04.12 — no schema changes */
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
/* 01.04.13 — no schema changes */
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
/* 01.04.14 — no schema changes */
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
/* 01.04.15 — no schema changes */
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
/* 01.04.16 — no schema changes */
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
/* 01.04.17 — no schema changes */
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
/* 01.04.18 — no schema changes */
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE `#__mokoog_tags` ADD COLUMN `custom_schema` TEXT NULL AFTER `canonical_url`;
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
/* 01.05.01 — no schema changes */
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
/* 01.05.02 — no schema changes */
|
||||||
@@ -27,13 +27,13 @@ class BatchController extends BaseController
|
|||||||
*/
|
*/
|
||||||
public function count(): void
|
public function count(): void
|
||||||
{
|
{
|
||||||
Session::checkToken('get') || jexit(Text::_('JINVALID_TOKEN'));
|
Session::checkToken('get') || throw new \RuntimeException(Text::_('JINVALID_TOKEN'), 403);
|
||||||
|
|
||||||
if (!Factory::getApplication()->getIdentity()->authorise('core.create', 'com_mokoog')) {
|
if (!Factory::getApplication()->getIdentity()->authorise('core.create', 'com_mokoog')) {
|
||||||
throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_ACCESS_FORBIDDEN'), 403);
|
throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_ACCESS_FORBIDDEN'), 403);
|
||||||
}
|
}
|
||||||
|
|
||||||
$db = Factory::getDbo();
|
$db = Factory::getContainer()->get(\Joomla\Database\DatabaseInterface::class);
|
||||||
$query = $db->getQuery(true)
|
$query = $db->getQuery(true)
|
||||||
->select('COUNT(*)')
|
->select('COUNT(*)')
|
||||||
->from($db->quoteName('#__content', 'c'))
|
->from($db->quoteName('#__content', 'c'))
|
||||||
@@ -60,7 +60,7 @@ class BatchController extends BaseController
|
|||||||
*/
|
*/
|
||||||
public function process(): void
|
public function process(): void
|
||||||
{
|
{
|
||||||
Session::checkToken('get') || jexit(Text::_('JINVALID_TOKEN'));
|
Session::checkToken('get') || throw new \RuntimeException(Text::_('JINVALID_TOKEN'), 403);
|
||||||
|
|
||||||
if (!Factory::getApplication()->getIdentity()->authorise('core.create', 'com_mokoog')) {
|
if (!Factory::getApplication()->getIdentity()->authorise('core.create', 'com_mokoog')) {
|
||||||
throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_ACCESS_FORBIDDEN'), 403);
|
throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_ACCESS_FORBIDDEN'), 403);
|
||||||
@@ -69,7 +69,7 @@ class BatchController extends BaseController
|
|||||||
$app = Factory::getApplication();
|
$app = Factory::getApplication();
|
||||||
$limit = min($app->getInput()->getInt('limit', 50), 200);
|
$limit = min($app->getInput()->getInt('limit', 50), 200);
|
||||||
|
|
||||||
$db = Factory::getDbo();
|
$db = Factory::getContainer()->get(\Joomla\Database\DatabaseInterface::class);
|
||||||
$query = $db->getQuery(true)
|
$query = $db->getQuery(true)
|
||||||
->select($db->quoteName([
|
->select($db->quoteName([
|
||||||
'c.id', 'c.title', 'c.metadesc', 'c.introtext', 'c.fulltext', 'c.images',
|
'c.id', 'c.title', 'c.metadesc', 'c.introtext', 'c.fulltext', 'c.images',
|
||||||
@@ -120,6 +120,7 @@ class BatchController extends BaseController
|
|||||||
$created++;
|
$created++;
|
||||||
} catch (\RuntimeException $e) {
|
} catch (\RuntimeException $e) {
|
||||||
$skipped++;
|
$skipped++;
|
||||||
|
\Joomla\CMS\Log\Log::add('Batch insert failed for article ' . $article->id . ': ' . $e->getMessage(), \Joomla\CMS\Log\Log::WARNING, 'mokoog');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @package MokoJoomOpenGraph
|
* @package MokoSuiteOpenGraph
|
||||||
* @subpackage com_mokoog
|
* @subpackage com_mokoog
|
||||||
* @author Moko Consulting <hello@mokoconsulting.tech>
|
* @author Moko Consulting <hello@mokoconsulting.tech>
|
||||||
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||||
|
|||||||
@@ -36,14 +36,14 @@ class ImportExportController extends BaseController
|
|||||||
*/
|
*/
|
||||||
public function export(): void
|
public function export(): void
|
||||||
{
|
{
|
||||||
Session::checkToken('get') || jexit(Text::_('JINVALID_TOKEN'));
|
Session::checkToken('get') || throw new \RuntimeException(Text::_('JINVALID_TOKEN'), 403);
|
||||||
|
|
||||||
if (!Factory::getApplication()->getIdentity()->authorise('core.manage', 'com_mokoog')) {
|
if (!Factory::getApplication()->getIdentity()->authorise('core.manage', 'com_mokoog')) {
|
||||||
throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_ACCESS_FORBIDDEN'), 403);
|
throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_ACCESS_FORBIDDEN'), 403);
|
||||||
}
|
}
|
||||||
|
|
||||||
$app = Factory::getApplication();
|
$app = Factory::getApplication();
|
||||||
$db = Factory::getDbo();
|
$db = Factory::getContainer()->get(\Joomla\Database\DatabaseInterface::class);
|
||||||
|
|
||||||
// Join with #__content to get article titles for reference
|
// Join with #__content to get article titles for reference
|
||||||
$query = $db->getQuery(true)
|
$query = $db->getQuery(true)
|
||||||
@@ -102,7 +102,7 @@ class ImportExportController extends BaseController
|
|||||||
*/
|
*/
|
||||||
public function import(): void
|
public function import(): void
|
||||||
{
|
{
|
||||||
Session::checkToken() || jexit(Text::_('JINVALID_TOKEN'));
|
Session::checkToken() || throw new \RuntimeException(Text::_('JINVALID_TOKEN'), 403);
|
||||||
|
|
||||||
$identity = Factory::getApplication()->getIdentity();
|
$identity = Factory::getApplication()->getIdentity();
|
||||||
|
|
||||||
@@ -161,7 +161,7 @@ class ImportExportController extends BaseController
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$db = Factory::getDbo();
|
$db = Factory::getContainer()->get(\Joomla\Database\DatabaseInterface::class);
|
||||||
$header = fgetcsv($handle);
|
$header = fgetcsv($handle);
|
||||||
$created = 0;
|
$created = 0;
|
||||||
$updated = 0;
|
$updated = 0;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @package MokoJoomOpenGraph
|
* @package MokoSuiteOpenGraph
|
||||||
* @subpackage com_mokoog
|
* @subpackage com_mokoog
|
||||||
* @author Moko Consulting <hello@mokoconsulting.tech>
|
* @author Moko Consulting <hello@mokoconsulting.tech>
|
||||||
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @package MokoJoomOpenGraph
|
* @package MokoSuiteOpenGraph
|
||||||
* @subpackage com_mokoog
|
* @subpackage com_mokoog
|
||||||
* @author Moko Consulting <hello@mokoconsulting.tech>
|
* @author Moko Consulting <hello@mokoconsulting.tech>
|
||||||
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @package MokoJoomOpenGraph
|
* @package MokoSuiteOpenGraph
|
||||||
* @subpackage com_mokoog
|
* @subpackage com_mokoog
|
||||||
* @author Moko Consulting <hello@mokoconsulting.tech>
|
* @author Moko Consulting <hello@mokoconsulting.tech>
|
||||||
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @package MokoJoomOpenGraph
|
* @package MokoSuiteOpenGraph
|
||||||
* @subpackage com_mokoog
|
* @subpackage com_mokoog
|
||||||
* @author Moko Consulting <hello@mokoconsulting.tech>
|
* @author Moko Consulting <hello@mokoconsulting.tech>
|
||||||
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||||
|
|||||||
@@ -0,0 +1,58 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @package MokoSuiteOpenGraph
|
||||||
|
* @subpackage com_mokoog
|
||||||
|
* @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
|
||||||
|
*/
|
||||||
|
|
||||||
|
defined('_JEXEC') or die;
|
||||||
|
|
||||||
|
use Joomla\CMS\Factory;
|
||||||
|
use Joomla\CMS\Language\Text;
|
||||||
|
|
||||||
|
$db = Factory::getContainer()->get(\Joomla\Database\DatabaseInterface::class);
|
||||||
|
|
||||||
|
// Total published articles
|
||||||
|
$db->setQuery($db->getQuery(true)->select('COUNT(*)')->from('#__content')->where('state = 1'));
|
||||||
|
$totalArticles = (int) $db->loadResult();
|
||||||
|
|
||||||
|
// Articles with OG tags
|
||||||
|
$db->setQuery($db->getQuery(true)->select('COUNT(DISTINCT content_id)')->from('#__mokoog_tags')->where("content_type = 'com_content'")->where('published = 1'));
|
||||||
|
$articlesWithOg = (int) $db->loadResult();
|
||||||
|
|
||||||
|
// Articles missing OG data fields
|
||||||
|
$db->setQuery($db->getQuery(true)->select('COUNT(*)')->from('#__mokoog_tags')->where("content_type = 'com_content'")->where("og_title = ''")->where('published = 1'));
|
||||||
|
$missingTitle = (int) $db->loadResult();
|
||||||
|
|
||||||
|
$db->setQuery($db->getQuery(true)->select('COUNT(*)')->from('#__mokoog_tags')->where("content_type = 'com_content'")->where("og_description = ''")->where('published = 1'));
|
||||||
|
$missingDesc = (int) $db->loadResult();
|
||||||
|
|
||||||
|
$db->setQuery($db->getQuery(true)->select('COUNT(*)')->from('#__mokoog_tags')->where("content_type = 'com_content'")->where("og_image = ''")->where('published = 1'));
|
||||||
|
$missingImage = (int) $db->loadResult();
|
||||||
|
|
||||||
|
$coverage = $totalArticles > 0 ? round(($articlesWithOg / $totalArticles) * 100) : 0;
|
||||||
|
?>
|
||||||
|
<div class="mokoog-coverage card mb-3">
|
||||||
|
<div class="card-body">
|
||||||
|
<h4 class="card-title"><?php echo Text::_('COM_MOKOOG_COVERAGE_TITLE'); ?></h4>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-3 text-center">
|
||||||
|
<div class="display-4 <?php echo $coverage >= 80 ? 'text-success' : ($coverage >= 50 ? 'text-warning' : 'text-danger'); ?>">
|
||||||
|
<?php echo $coverage; ?>%
|
||||||
|
</div>
|
||||||
|
<small class="text-muted"><?php echo Text::_('COM_MOKOOG_COVERAGE_PERCENT'); ?></small>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-9">
|
||||||
|
<ul class="list-unstyled">
|
||||||
|
<li><?php echo Text::sprintf('COM_MOKOOG_COVERAGE_ARTICLES', $articlesWithOg, $totalArticles); ?></li>
|
||||||
|
<li><?php echo Text::sprintf('COM_MOKOOG_COVERAGE_MISSING_TITLE', $missingTitle); ?></li>
|
||||||
|
<li><?php echo Text::sprintf('COM_MOKOOG_COVERAGE_MISSING_DESC', $missingDesc); ?></li>
|
||||||
|
<li><?php echo Text::sprintf('COM_MOKOOG_COVERAGE_MISSING_IMAGE', $missingImage); ?></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @package MokoJoomOpenGraph
|
* @package MokoSuiteOpenGraph
|
||||||
* @subpackage com_mokoog
|
* @subpackage com_mokoog
|
||||||
* @author Moko Consulting <hello@mokoconsulting.tech>
|
* @author Moko Consulting <hello@mokoconsulting.tech>
|
||||||
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||||
@@ -21,6 +21,7 @@ use Joomla\CMS\Session\Session;
|
|||||||
|
|
||||||
$token = Session::getFormToken();
|
$token = Session::getFormToken();
|
||||||
?>
|
?>
|
||||||
|
<?php include __DIR__ . '/coverage.php'; ?>
|
||||||
<form action="<?php echo Route::_('index.php?option=com_mokoog&view=tags'); ?>" method="post" name="adminForm" id="adminForm">
|
<form action="<?php echo Route::_('index.php?option=com_mokoog&view=tags'); ?>" method="post" name="adminForm" id="adminForm">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-12">
|
<div class="col-md-12">
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<!--
|
<!--
|
||||||
* @package MokoJoomOpenGraph
|
* @package MokoSuiteOpenGraph
|
||||||
* @subpackage plg_content_mokoog
|
* @subpackage plg_content_mokoog
|
||||||
* @author Moko Consulting <hello@mokoconsulting.tech>
|
* @author Moko Consulting <hello@mokoconsulting.tech>
|
||||||
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||||
@@ -16,7 +16,7 @@
|
|||||||
label="PLG_CONTENT_MOKOOG_FIELD_OG_TITLE"
|
label="PLG_CONTENT_MOKOOG_FIELD_OG_TITLE"
|
||||||
description="PLG_CONTENT_MOKOOG_FIELD_OG_TITLE_DESC"
|
description="PLG_CONTENT_MOKOOG_FIELD_OG_TITLE_DESC"
|
||||||
filter="string"
|
filter="string"
|
||||||
maxlength="70"
|
maxlength="255"
|
||||||
/>
|
/>
|
||||||
<field
|
<field
|
||||||
name="og_description"
|
name="og_description"
|
||||||
@@ -25,7 +25,7 @@
|
|||||||
description="PLG_CONTENT_MOKOOG_FIELD_OG_DESCRIPTION_DESC"
|
description="PLG_CONTENT_MOKOOG_FIELD_OG_DESCRIPTION_DESC"
|
||||||
filter="string"
|
filter="string"
|
||||||
rows="3"
|
rows="3"
|
||||||
maxlength="200"
|
maxlength="512"
|
||||||
/>
|
/>
|
||||||
<field
|
<field
|
||||||
name="og_image"
|
name="og_image"
|
||||||
@@ -49,6 +49,14 @@
|
|||||||
<option value="music.song">Music</option>
|
<option value="music.song">Music</option>
|
||||||
<option value="video.other">Video</option>
|
<option value="video.other">Video</option>
|
||||||
</field>
|
</field>
|
||||||
|
<field
|
||||||
|
name="og_video"
|
||||||
|
type="url"
|
||||||
|
label="PLG_CONTENT_MOKOOG_FIELD_OG_VIDEO"
|
||||||
|
description="PLG_CONTENT_MOKOOG_FIELD_OG_VIDEO_DESC"
|
||||||
|
filter="url"
|
||||||
|
validate="url"
|
||||||
|
/>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
<fieldset name="mokoog_seo" label="PLG_CONTENT_MOKOOG_FIELDSET_SEO_LABEL"
|
<fieldset name="mokoog_seo" label="PLG_CONTENT_MOKOOG_FIELDSET_SEO_LABEL"
|
||||||
description="PLG_CONTENT_MOKOOG_FIELDSET_SEO_DESC">
|
description="PLG_CONTENT_MOKOOG_FIELDSET_SEO_DESC">
|
||||||
@@ -58,7 +66,7 @@
|
|||||||
label="PLG_CONTENT_MOKOOG_FIELD_SEO_TITLE"
|
label="PLG_CONTENT_MOKOOG_FIELD_SEO_TITLE"
|
||||||
description="PLG_CONTENT_MOKOOG_FIELD_SEO_TITLE_DESC"
|
description="PLG_CONTENT_MOKOOG_FIELD_SEO_TITLE_DESC"
|
||||||
filter="string"
|
filter="string"
|
||||||
maxlength="70"
|
maxlength="255"
|
||||||
/>
|
/>
|
||||||
<field
|
<field
|
||||||
name="meta_description"
|
name="meta_description"
|
||||||
@@ -67,7 +75,7 @@
|
|||||||
description="PLG_CONTENT_MOKOOG_FIELD_META_DESCRIPTION_DESC"
|
description="PLG_CONTENT_MOKOOG_FIELD_META_DESCRIPTION_DESC"
|
||||||
filter="string"
|
filter="string"
|
||||||
rows="3"
|
rows="3"
|
||||||
maxlength="200"
|
maxlength="255"
|
||||||
/>
|
/>
|
||||||
<field
|
<field
|
||||||
name="robots"
|
name="robots"
|
||||||
@@ -93,5 +101,29 @@
|
|||||||
validate="url"
|
validate="url"
|
||||||
/>
|
/>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
<fieldset name="mokoog_event" label="PLG_CONTENT_MOKOOG_FIELDSET_EVENT_LABEL"
|
||||||
|
description="PLG_CONTENT_MOKOOG_FIELDSET_EVENT_DESC">
|
||||||
|
<field name="event_start" type="calendar" label="PLG_CONTENT_MOKOOG_FIELD_EVENT_START" description="PLG_CONTENT_MOKOOG_FIELD_EVENT_START_DESC" format="%Y-%m-%d %H:%M" filter="string" showtime="true" />
|
||||||
|
<field name="event_end" type="calendar" label="PLG_CONTENT_MOKOOG_FIELD_EVENT_END" description="PLG_CONTENT_MOKOOG_FIELD_EVENT_END_DESC" format="%Y-%m-%d %H:%M" filter="string" showtime="true" />
|
||||||
|
<field name="event_location" type="text" label="PLG_CONTENT_MOKOOG_FIELD_EVENT_LOCATION" description="PLG_CONTENT_MOKOOG_FIELD_EVENT_LOCATION_DESC" filter="string" />
|
||||||
|
<field name="event_address" type="text" label="PLG_CONTENT_MOKOOG_FIELD_EVENT_ADDRESS" description="PLG_CONTENT_MOKOOG_FIELD_EVENT_ADDRESS_DESC" filter="string" />
|
||||||
|
<field name="event_price" type="text" label="PLG_CONTENT_MOKOOG_FIELD_EVENT_PRICE" description="PLG_CONTENT_MOKOOG_FIELD_EVENT_PRICE_DESC" filter="string" />
|
||||||
|
<field name="event_currency" type="text" label="PLG_CONTENT_MOKOOG_FIELD_EVENT_CURRENCY" description="PLG_CONTENT_MOKOOG_FIELD_EVENT_CURRENCY_DESC" filter="string" default="USD" />
|
||||||
|
<field name="event_url" type="url" label="PLG_CONTENT_MOKOOG_FIELD_EVENT_URL" description="PLG_CONTENT_MOKOOG_FIELD_EVENT_URL_DESC" filter="url" />
|
||||||
|
</fieldset>
|
||||||
|
<fieldset name="mokoog_recipe" label="PLG_CONTENT_MOKOOG_FIELDSET_RECIPE_LABEL"
|
||||||
|
description="PLG_CONTENT_MOKOOG_FIELDSET_RECIPE_DESC">
|
||||||
|
<field name="recipe_prep_time" type="text" label="PLG_CONTENT_MOKOOG_FIELD_RECIPE_PREP_TIME" description="PLG_CONTENT_MOKOOG_FIELD_RECIPE_PREP_TIME_DESC" filter="string" hint="PT15M" />
|
||||||
|
<field name="recipe_cook_time" type="text" label="PLG_CONTENT_MOKOOG_FIELD_RECIPE_COOK_TIME" description="PLG_CONTENT_MOKOOG_FIELD_RECIPE_COOK_TIME_DESC" filter="string" hint="PT30M" />
|
||||||
|
<field name="recipe_yield" type="text" label="PLG_CONTENT_MOKOOG_FIELD_RECIPE_YIELD" description="PLG_CONTENT_MOKOOG_FIELD_RECIPE_YIELD_DESC" filter="string" hint="4 servings" />
|
||||||
|
<field name="recipe_calories" type="text" label="PLG_CONTENT_MOKOOG_FIELD_RECIPE_CALORIES" description="PLG_CONTENT_MOKOOG_FIELD_RECIPE_CALORIES_DESC" filter="string" hint="350" />
|
||||||
|
<field name="recipe_ingredients" type="textarea" label="PLG_CONTENT_MOKOOG_FIELD_RECIPE_INGREDIENTS" description="PLG_CONTENT_MOKOOG_FIELD_RECIPE_INGREDIENTS_DESC" filter="string" rows="5" hint="One ingredient per line" />
|
||||||
|
<field name="recipe_category" type="text" label="PLG_CONTENT_MOKOOG_FIELD_RECIPE_CATEGORY" description="PLG_CONTENT_MOKOOG_FIELD_RECIPE_CATEGORY_DESC" filter="string" hint="Dessert" />
|
||||||
|
<field name="recipe_cuisine" type="text" label="PLG_CONTENT_MOKOOG_FIELD_RECIPE_CUISINE" description="PLG_CONTENT_MOKOOG_FIELD_RECIPE_CUISINE_DESC" filter="string" hint="Italian" />
|
||||||
|
</fieldset>
|
||||||
|
<fieldset name="mokoog_custom_schema" label="PLG_CONTENT_MOKOOG_FIELDSET_CUSTOM_SCHEMA_LABEL"
|
||||||
|
description="PLG_CONTENT_MOKOOG_FIELDSET_CUSTOM_SCHEMA_DESC">
|
||||||
|
<field name="custom_schema" type="textarea" label="PLG_CONTENT_MOKOOG_FIELD_CUSTOM_SCHEMA" description="PLG_CONTENT_MOKOOG_FIELD_CUSTOM_SCHEMA_DESC" filter="raw" rows="12" class="input-xxlarge" />
|
||||||
|
</fieldset>
|
||||||
</fields>
|
</fields>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
; MokoJoomOpenGraph - Content Plugin Language File
|
; MokoSuiteOpenGraph - Content Plugin Language File
|
||||||
; Copyright (C) 2026 Moko Consulting. All rights reserved.
|
; Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||||
; License: GPL-3.0-or-later
|
; License: GPL-3.0-or-later
|
||||||
|
|
||||||
@@ -14,6 +14,9 @@ PLG_CONTENT_MOKOOG_FIELD_OG_IMAGE_DESC="Custom image for social sharing. Recomme
|
|||||||
PLG_CONTENT_MOKOOG_FIELD_OG_TYPE="OG Type"
|
PLG_CONTENT_MOKOOG_FIELD_OG_TYPE="OG Type"
|
||||||
PLG_CONTENT_MOKOOG_FIELD_OG_TYPE_DESC="The Open Graph content type for this page."
|
PLG_CONTENT_MOKOOG_FIELD_OG_TYPE_DESC="The Open Graph content type for this page."
|
||||||
|
|
||||||
|
PLG_CONTENT_MOKOOG_FIELD_OG_VIDEO="Video URL"
|
||||||
|
PLG_CONTENT_MOKOOG_FIELD_OG_VIDEO_DESC="URL of a video to embed in social sharing previews. Supports direct video URLs and YouTube/Vimeo links. Outputs og:video meta tags."
|
||||||
|
|
||||||
PLG_CONTENT_MOKOOG_FIELDSET_SEO_LABEL="SEO Meta Tags"
|
PLG_CONTENT_MOKOOG_FIELDSET_SEO_LABEL="SEO Meta Tags"
|
||||||
PLG_CONTENT_MOKOOG_FIELDSET_SEO_DESC="Control search engine meta tags for this page."
|
PLG_CONTENT_MOKOOG_FIELDSET_SEO_DESC="Control search engine meta tags for this page."
|
||||||
|
|
||||||
@@ -26,3 +29,42 @@ PLG_CONTENT_MOKOOG_FIELD_ROBOTS_DESC="Search engine indexing directives for this
|
|||||||
PLG_CONTENT_MOKOOG_ROBOTS_DEFAULT="- Use default (index, follow) -"
|
PLG_CONTENT_MOKOOG_ROBOTS_DEFAULT="- Use default (index, follow) -"
|
||||||
PLG_CONTENT_MOKOOG_FIELD_CANONICAL_URL="Canonical URL"
|
PLG_CONTENT_MOKOOG_FIELD_CANONICAL_URL="Canonical URL"
|
||||||
PLG_CONTENT_MOKOOG_FIELD_CANONICAL_URL_DESC="Override the canonical URL for this page. Leave blank to use the current URL."
|
PLG_CONTENT_MOKOOG_FIELD_CANONICAL_URL_DESC="Override the canonical URL for this page. Leave blank to use the current URL."
|
||||||
|
|
||||||
|
PLG_CONTENT_MOKOOG_FIELDSET_EVENT_LABEL="Event Details"
|
||||||
|
PLG_CONTENT_MOKOOG_FIELDSET_EVENT_DESC="Optional event information for JSON-LD Event schema."
|
||||||
|
PLG_CONTENT_MOKOOG_FIELD_EVENT_START="Start Date/Time"
|
||||||
|
PLG_CONTENT_MOKOOG_FIELD_EVENT_START_DESC="Event start date and time."
|
||||||
|
PLG_CONTENT_MOKOOG_FIELD_EVENT_END="End Date/Time"
|
||||||
|
PLG_CONTENT_MOKOOG_FIELD_EVENT_END_DESC="Event end date and time."
|
||||||
|
PLG_CONTENT_MOKOOG_FIELD_EVENT_LOCATION="Venue Name"
|
||||||
|
PLG_CONTENT_MOKOOG_FIELD_EVENT_LOCATION_DESC="Name of the event venue or location."
|
||||||
|
PLG_CONTENT_MOKOOG_FIELD_EVENT_ADDRESS="Venue Address"
|
||||||
|
PLG_CONTENT_MOKOOG_FIELD_EVENT_ADDRESS_DESC="Full address of the event venue."
|
||||||
|
PLG_CONTENT_MOKOOG_FIELD_EVENT_PRICE="Ticket Price"
|
||||||
|
PLG_CONTENT_MOKOOG_FIELD_EVENT_PRICE_DESC="Ticket price (e.g. 50.00). Leave blank if free."
|
||||||
|
PLG_CONTENT_MOKOOG_FIELD_EVENT_CURRENCY="Currency"
|
||||||
|
PLG_CONTENT_MOKOOG_FIELD_EVENT_CURRENCY_DESC="Currency code for ticket price (e.g. USD, EUR, GBP)."
|
||||||
|
PLG_CONTENT_MOKOOG_FIELD_EVENT_URL="Ticket URL"
|
||||||
|
PLG_CONTENT_MOKOOG_FIELD_EVENT_URL_DESC="URL where tickets can be purchased."
|
||||||
|
|
||||||
|
PLG_CONTENT_MOKOOG_FIELDSET_RECIPE_LABEL="Recipe Details"
|
||||||
|
PLG_CONTENT_MOKOOG_FIELDSET_RECIPE_DESC="Optional recipe information for JSON-LD Recipe schema."
|
||||||
|
PLG_CONTENT_MOKOOG_FIELD_RECIPE_PREP_TIME="Prep Time"
|
||||||
|
PLG_CONTENT_MOKOOG_FIELD_RECIPE_PREP_TIME_DESC="Preparation time in ISO 8601 duration format (e.g. PT15M for 15 minutes)."
|
||||||
|
PLG_CONTENT_MOKOOG_FIELD_RECIPE_COOK_TIME="Cook Time"
|
||||||
|
PLG_CONTENT_MOKOOG_FIELD_RECIPE_COOK_TIME_DESC="Cooking time in ISO 8601 duration format (e.g. PT30M for 30 minutes)."
|
||||||
|
PLG_CONTENT_MOKOOG_FIELD_RECIPE_YIELD="Yield"
|
||||||
|
PLG_CONTENT_MOKOOG_FIELD_RECIPE_YIELD_DESC="Number of servings or yield (e.g. 4 servings, 1 loaf)."
|
||||||
|
PLG_CONTENT_MOKOOG_FIELD_RECIPE_CALORIES="Calories"
|
||||||
|
PLG_CONTENT_MOKOOG_FIELD_RECIPE_CALORIES_DESC="Calories per serving."
|
||||||
|
PLG_CONTENT_MOKOOG_FIELD_RECIPE_INGREDIENTS="Ingredients"
|
||||||
|
PLG_CONTENT_MOKOOG_FIELD_RECIPE_INGREDIENTS_DESC="One ingredient per line."
|
||||||
|
PLG_CONTENT_MOKOOG_FIELD_RECIPE_CATEGORY="Recipe Category"
|
||||||
|
PLG_CONTENT_MOKOOG_FIELD_RECIPE_CATEGORY_DESC="Category (e.g. Dessert, Appetizer, Main course)."
|
||||||
|
PLG_CONTENT_MOKOOG_FIELD_RECIPE_CUISINE="Cuisine"
|
||||||
|
PLG_CONTENT_MOKOOG_FIELD_RECIPE_CUISINE_DESC="Type of cuisine (e.g. Italian, Mexican, American)."
|
||||||
|
|
||||||
|
PLG_CONTENT_MOKOOG_FIELDSET_CUSTOM_SCHEMA_LABEL="Custom Schema"
|
||||||
|
PLG_CONTENT_MOKOOG_FIELDSET_CUSTOM_SCHEMA_DESC="Add custom JSON-LD structured data for this page."
|
||||||
|
PLG_CONTENT_MOKOOG_FIELD_CUSTOM_SCHEMA="Custom JSON-LD"
|
||||||
|
PLG_CONTENT_MOKOOG_FIELD_CUSTOM_SCHEMA_DESC="Enter valid JSON-LD structured data. The @context will be added automatically if missing. Use for schema types not covered by built-in options (e.g. Course, JobPosting, SoftwareApplication)."
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
; MokoJoomOpenGraph - Content Plugin System Language File
|
; MokoSuiteOpenGraph - Content Plugin System Language File
|
||||||
; Copyright (C) 2026 Moko Consulting. All rights reserved.
|
; Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||||
; License: GPL-3.0-or-later
|
; License: GPL-3.0-or-later
|
||||||
|
|
||||||
PLG_CONTENT_MOKOOG="Content - MokoJoomOpenGraph"
|
PLG_CONTENT_MOKOOG="Content - MokoSuiteOpenGraph"
|
||||||
PLG_CONTENT_MOKOOG_DESCRIPTION="Adds Open Graph fields to article and menu item edit forms for per-page social sharing control."
|
PLG_CONTENT_MOKOOG_DESCRIPTION="Adds Open Graph fields to article and menu item edit forms for per-page social sharing control."
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
; MokoJoomOpenGraph - Content Plugin Language File
|
; MokoSuiteOpenGraph - Content Plugin Language File
|
||||||
; Copyright (C) 2026 Moko Consulting. All rights reserved.
|
; Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||||
; License: GPL-3.0-or-later
|
; License: GPL-3.0-or-later
|
||||||
|
|
||||||
@@ -14,6 +14,9 @@ PLG_CONTENT_MOKOOG_FIELD_OG_IMAGE_DESC="Custom image for social sharing. Recomme
|
|||||||
PLG_CONTENT_MOKOOG_FIELD_OG_TYPE="OG Type"
|
PLG_CONTENT_MOKOOG_FIELD_OG_TYPE="OG Type"
|
||||||
PLG_CONTENT_MOKOOG_FIELD_OG_TYPE_DESC="The Open Graph content type for this page."
|
PLG_CONTENT_MOKOOG_FIELD_OG_TYPE_DESC="The Open Graph content type for this page."
|
||||||
|
|
||||||
|
PLG_CONTENT_MOKOOG_FIELD_OG_VIDEO="Video URL"
|
||||||
|
PLG_CONTENT_MOKOOG_FIELD_OG_VIDEO_DESC="URL of a video to embed in social sharing previews. Supports direct video URLs and YouTube/Vimeo links. Outputs og:video meta tags."
|
||||||
|
|
||||||
PLG_CONTENT_MOKOOG_FIELDSET_SEO_LABEL="SEO Meta Tags"
|
PLG_CONTENT_MOKOOG_FIELDSET_SEO_LABEL="SEO Meta Tags"
|
||||||
PLG_CONTENT_MOKOOG_FIELDSET_SEO_DESC="Control search engine meta tags for this page."
|
PLG_CONTENT_MOKOOG_FIELDSET_SEO_DESC="Control search engine meta tags for this page."
|
||||||
|
|
||||||
@@ -26,3 +29,42 @@ PLG_CONTENT_MOKOOG_FIELD_ROBOTS_DESC="Search engine indexing directives for this
|
|||||||
PLG_CONTENT_MOKOOG_ROBOTS_DEFAULT="- Use default (index, follow) -"
|
PLG_CONTENT_MOKOOG_ROBOTS_DEFAULT="- Use default (index, follow) -"
|
||||||
PLG_CONTENT_MOKOOG_FIELD_CANONICAL_URL="Canonical URL"
|
PLG_CONTENT_MOKOOG_FIELD_CANONICAL_URL="Canonical URL"
|
||||||
PLG_CONTENT_MOKOOG_FIELD_CANONICAL_URL_DESC="Override the canonical URL for this page. Leave blank to use the current URL."
|
PLG_CONTENT_MOKOOG_FIELD_CANONICAL_URL_DESC="Override the canonical URL for this page. Leave blank to use the current URL."
|
||||||
|
|
||||||
|
PLG_CONTENT_MOKOOG_FIELDSET_EVENT_LABEL="Event Details"
|
||||||
|
PLG_CONTENT_MOKOOG_FIELDSET_EVENT_DESC="Optional event information for JSON-LD Event schema."
|
||||||
|
PLG_CONTENT_MOKOOG_FIELD_EVENT_START="Start Date/Time"
|
||||||
|
PLG_CONTENT_MOKOOG_FIELD_EVENT_START_DESC="Event start date and time."
|
||||||
|
PLG_CONTENT_MOKOOG_FIELD_EVENT_END="End Date/Time"
|
||||||
|
PLG_CONTENT_MOKOOG_FIELD_EVENT_END_DESC="Event end date and time."
|
||||||
|
PLG_CONTENT_MOKOOG_FIELD_EVENT_LOCATION="Venue Name"
|
||||||
|
PLG_CONTENT_MOKOOG_FIELD_EVENT_LOCATION_DESC="Name of the event venue or location."
|
||||||
|
PLG_CONTENT_MOKOOG_FIELD_EVENT_ADDRESS="Venue Address"
|
||||||
|
PLG_CONTENT_MOKOOG_FIELD_EVENT_ADDRESS_DESC="Full address of the event venue."
|
||||||
|
PLG_CONTENT_MOKOOG_FIELD_EVENT_PRICE="Ticket Price"
|
||||||
|
PLG_CONTENT_MOKOOG_FIELD_EVENT_PRICE_DESC="Ticket price (e.g. 50.00). Leave blank if free."
|
||||||
|
PLG_CONTENT_MOKOOG_FIELD_EVENT_CURRENCY="Currency"
|
||||||
|
PLG_CONTENT_MOKOOG_FIELD_EVENT_CURRENCY_DESC="Currency code for ticket price (e.g. USD, EUR, GBP)."
|
||||||
|
PLG_CONTENT_MOKOOG_FIELD_EVENT_URL="Ticket URL"
|
||||||
|
PLG_CONTENT_MOKOOG_FIELD_EVENT_URL_DESC="URL where tickets can be purchased."
|
||||||
|
|
||||||
|
PLG_CONTENT_MOKOOG_FIELDSET_RECIPE_LABEL="Recipe Details"
|
||||||
|
PLG_CONTENT_MOKOOG_FIELDSET_RECIPE_DESC="Optional recipe information for JSON-LD Recipe schema."
|
||||||
|
PLG_CONTENT_MOKOOG_FIELD_RECIPE_PREP_TIME="Prep Time"
|
||||||
|
PLG_CONTENT_MOKOOG_FIELD_RECIPE_PREP_TIME_DESC="Preparation time in ISO 8601 duration format (e.g. PT15M for 15 minutes)."
|
||||||
|
PLG_CONTENT_MOKOOG_FIELD_RECIPE_COOK_TIME="Cook Time"
|
||||||
|
PLG_CONTENT_MOKOOG_FIELD_RECIPE_COOK_TIME_DESC="Cooking time in ISO 8601 duration format (e.g. PT30M for 30 minutes)."
|
||||||
|
PLG_CONTENT_MOKOOG_FIELD_RECIPE_YIELD="Yield"
|
||||||
|
PLG_CONTENT_MOKOOG_FIELD_RECIPE_YIELD_DESC="Number of servings or yield (e.g. 4 servings, 1 loaf)."
|
||||||
|
PLG_CONTENT_MOKOOG_FIELD_RECIPE_CALORIES="Calories"
|
||||||
|
PLG_CONTENT_MOKOOG_FIELD_RECIPE_CALORIES_DESC="Calories per serving."
|
||||||
|
PLG_CONTENT_MOKOOG_FIELD_RECIPE_INGREDIENTS="Ingredients"
|
||||||
|
PLG_CONTENT_MOKOOG_FIELD_RECIPE_INGREDIENTS_DESC="One ingredient per line."
|
||||||
|
PLG_CONTENT_MOKOOG_FIELD_RECIPE_CATEGORY="Recipe Category"
|
||||||
|
PLG_CONTENT_MOKOOG_FIELD_RECIPE_CATEGORY_DESC="Category (e.g. Dessert, Appetizer, Main course)."
|
||||||
|
PLG_CONTENT_MOKOOG_FIELD_RECIPE_CUISINE="Cuisine"
|
||||||
|
PLG_CONTENT_MOKOOG_FIELD_RECIPE_CUISINE_DESC="Type of cuisine (e.g. Italian, Mexican, American)."
|
||||||
|
|
||||||
|
PLG_CONTENT_MOKOOG_FIELDSET_CUSTOM_SCHEMA_LABEL="Custom Schema"
|
||||||
|
PLG_CONTENT_MOKOOG_FIELDSET_CUSTOM_SCHEMA_DESC="Add custom JSON-LD structured data for this page."
|
||||||
|
PLG_CONTENT_MOKOOG_FIELD_CUSTOM_SCHEMA="Custom JSON-LD"
|
||||||
|
PLG_CONTENT_MOKOOG_FIELD_CUSTOM_SCHEMA_DESC="Enter valid JSON-LD structured data. The @context will be added automatically if missing. Use for schema types not covered by built-in options (e.g. Course, JobPosting, SoftwareApplication)."
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
; MokoJoomOpenGraph - Content Plugin System Language File
|
; MokoSuiteOpenGraph - Content Plugin System Language File
|
||||||
; Copyright (C) 2026 Moko Consulting. All rights reserved.
|
; Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||||
; License: GPL-3.0-or-later
|
; License: GPL-3.0-or-later
|
||||||
|
|
||||||
PLG_CONTENT_MOKOOG="Content - MokoJoomOpenGraph"
|
PLG_CONTENT_MOKOOG="Content - MokoSuiteOpenGraph"
|
||||||
PLG_CONTENT_MOKOOG_DESCRIPTION="Adds Open Graph fields to article and menu item edit forms for per-page social sharing control."
|
PLG_CONTENT_MOKOOG_DESCRIPTION="Adds Open Graph fields to article and menu item edit forms for per-page social sharing control."
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* @package MokoJoomOpenGraph
|
* @package MokoSuiteOpenGraph
|
||||||
* @subpackage plg_content_mokoog
|
* @subpackage plg_content_mokoog
|
||||||
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||||
* @license GPL-3.0-or-later
|
* @license GPL-3.0-or-later
|
||||||
@@ -102,3 +102,154 @@
|
|||||||
color: #536471;
|
color: #536471;
|
||||||
margin-top: 2px;
|
margin-top: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* LinkedIn card */
|
||||||
|
.mokoog-card-li {
|
||||||
|
border: 1px solid #e0dfdc;
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mokoog-card-li .mokoog-card-body {
|
||||||
|
border-top-color: #e0dfdc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mokoog-card-li .mokoog-card-title {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: rgba(0, 0, 0, 0.9);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mokoog-card-li .mokoog-card-domain {
|
||||||
|
font-size: 12px;
|
||||||
|
color: rgba(0, 0, 0, 0.6);
|
||||||
|
text-transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Discord card */
|
||||||
|
.mokoog-card-dc {
|
||||||
|
background: #2b2d31;
|
||||||
|
border-left: 4px solid #5865f2;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mokoog-card-dc .mokoog-card-body {
|
||||||
|
border-top: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mokoog-card-dc .mokoog-card-img {
|
||||||
|
height: 200px;
|
||||||
|
margin: 0 12px 12px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mokoog-card-dc .mokoog-card-title {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #00a8fc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mokoog-card-dc .mokoog-card-desc {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #dbdee1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mokoog-card-dc .mokoog-card-domain {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #b5bac1;
|
||||||
|
text-transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Mastodon card */
|
||||||
|
.mokoog-card-ma {
|
||||||
|
border: 1px solid #c8ccd0;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mokoog-card-ma .mokoog-card-img {
|
||||||
|
border-radius: 8px 8px 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mokoog-card-ma .mokoog-card-body {
|
||||||
|
border-top-color: #c8ccd0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mokoog-card-ma .mokoog-card-title {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1a1a2e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mokoog-card-ma .mokoog-card-desc {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #606984;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mokoog-card-ma .mokoog-card-domain {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #606984;
|
||||||
|
text-transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Slack card */
|
||||||
|
.mokoog-card-sl {
|
||||||
|
border-left: 4px solid #36c5f0;
|
||||||
|
border-radius: 0;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mokoog-card-sl .mokoog-card-body {
|
||||||
|
border-top: none;
|
||||||
|
padding: 8px 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mokoog-card-sl .mokoog-card-title {
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #1264a3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mokoog-card-sl .mokoog-card-desc {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #1d1c1d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mokoog-card-sl .mokoog-card-domain {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #616061;
|
||||||
|
text-transform: none;
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Character count indicators */
|
||||||
|
.mokoog-char-count {
|
||||||
|
display: block;
|
||||||
|
font-size: 12px;
|
||||||
|
margin-top: 4px;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mokoog-char-ok {
|
||||||
|
color: #2e7d32;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mokoog-char-warn {
|
||||||
|
color: #f57c00;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mokoog-char-over {
|
||||||
|
color: #d32f2f;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* SEO scoring panel */
|
||||||
|
.mokoog-seo-score { margin: 15px 0; padding: 15px; background: #f8f9fa; border-radius: 8px; border: 1px solid #dee2e6; }
|
||||||
|
.mokoog-seo-heading { margin: 0 0 10px; font-size: 14px; color: #666; }
|
||||||
|
.mokoog-seo-list { list-style: none; padding: 0; margin: 0 0 10px; }
|
||||||
|
.mokoog-seo-item { padding: 4px 0; font-size: 13px; display: flex; align-items: center; gap: 8px; }
|
||||||
|
.mokoog-seo-dot { width: 10px; height: 10px; border-radius: 50%; flex-shrink: 0; }
|
||||||
|
.mokoog-seo-pass { background: #2e7d32; }
|
||||||
|
.mokoog-seo-fail { background: #d32f2f; }
|
||||||
|
.mokoog-seo-total { font-size: 14px; font-weight: 600; padding-top: 8px; border-top: 1px solid #dee2e6; }
|
||||||
|
.mokoog-seo-total-good { color: #2e7d32; }
|
||||||
|
.mokoog-seo-total-ok { color: #f57c00; }
|
||||||
|
.mokoog-seo-total-bad { color: #d32f2f; }
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
"$schema": "https://developer.joomla.org/schemas/json-schema/web_assets.json",
|
"$schema": "https://developer.joomla.org/schemas/json-schema/web_assets.json",
|
||||||
"name": "plg_content_mokoog",
|
"name": "plg_content_mokoog",
|
||||||
"version": "01.00.00",
|
"version": "01.00.00",
|
||||||
"description": "MokoJoomOpenGraph Content Plugin Assets",
|
"description": "MokoSuiteOpenGraph Content Plugin Assets",
|
||||||
"license": "GPL-3.0-or-later",
|
"license": "GPL-3.0-or-later",
|
||||||
"assets": [
|
"assets": [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* @package MokoJoomOpenGraph
|
* @package MokoSuiteOpenGraph
|
||||||
* @subpackage plg_content_mokoog
|
* @subpackage plg_content_mokoog
|
||||||
* @author Moko Consulting <hello@mokoconsulting.tech>
|
* @author Moko Consulting <hello@mokoconsulting.tech>
|
||||||
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||||
@@ -15,9 +15,87 @@ document.addEventListener('DOMContentLoaded', function () {
|
|||||||
ogDesc: document.getElementById('jform_mokoog_og_description'),
|
ogDesc: document.getElementById('jform_mokoog_og_description'),
|
||||||
ogImage: document.getElementById('jform_mokoog_og_image'),
|
ogImage: document.getElementById('jform_mokoog_og_image'),
|
||||||
articleTitle: document.getElementById('jform_title'),
|
articleTitle: document.getElementById('jform_title'),
|
||||||
metaDesc: document.getElementById('jform_metadesc')
|
metaDesc: document.getElementById('jform_metadesc'),
|
||||||
|
seoTitle: document.getElementById('jform_mokoog_seo_title'),
|
||||||
|
metaDescription: document.getElementById('jform_mokoog_meta_description')
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Character count indicators
|
||||||
|
var charLimits = [
|
||||||
|
{ field: fields.ogTitle, optimal: 60, max: 90 },
|
||||||
|
{ field: fields.ogDesc, optimal: 155, max: 200 },
|
||||||
|
{ field: fields.seoTitle, optimal: 60, max: 70 },
|
||||||
|
{ field: fields.metaDescription, optimal: 155, max: 160 }
|
||||||
|
];
|
||||||
|
|
||||||
|
charLimits.forEach(function (cfg) {
|
||||||
|
if (!cfg.field) return;
|
||||||
|
|
||||||
|
var counter = document.createElement('span');
|
||||||
|
counter.className = 'mokoog-char-count';
|
||||||
|
cfg.field.parentNode.appendChild(counter);
|
||||||
|
|
||||||
|
function refresh() {
|
||||||
|
var len = cfg.field.value.length;
|
||||||
|
counter.textContent = len + '/' + cfg.optimal;
|
||||||
|
|
||||||
|
if (len > cfg.max) {
|
||||||
|
counter.className = 'mokoog-char-count mokoog-char-over';
|
||||||
|
} else if (len > cfg.optimal) {
|
||||||
|
counter.className = 'mokoog-char-count mokoog-char-warn';
|
||||||
|
} else {
|
||||||
|
counter.className = 'mokoog-char-count mokoog-char-ok';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg.field.addEventListener('input', refresh);
|
||||||
|
cfg.field.addEventListener('change', refresh);
|
||||||
|
refresh();
|
||||||
|
});
|
||||||
|
|
||||||
|
// AI Generate buttons
|
||||||
|
['ogTitle', 'ogDesc'].forEach(function(fieldKey) {
|
||||||
|
var field = fields[fieldKey];
|
||||||
|
if (!field) return;
|
||||||
|
|
||||||
|
var btn = document.createElement('button');
|
||||||
|
btn.type = 'button';
|
||||||
|
btn.className = 'btn btn-sm btn-outline-primary mokoog-ai-btn';
|
||||||
|
btn.textContent = 'Generate with AI';
|
||||||
|
btn.dataset.target = fieldKey;
|
||||||
|
field.parentNode.appendChild(btn);
|
||||||
|
|
||||||
|
btn.addEventListener('click', function() {
|
||||||
|
var articleTitle = fields.articleTitle ? fields.articleTitle.value : '';
|
||||||
|
btn.disabled = true;
|
||||||
|
btn.textContent = 'Generating...';
|
||||||
|
|
||||||
|
var formData = new FormData();
|
||||||
|
formData.append('task', 'mokoog.aiGenerate');
|
||||||
|
formData.append('field', fieldKey === 'ogTitle' ? 'title' : 'description');
|
||||||
|
formData.append('article_title', articleTitle);
|
||||||
|
formData.append(Joomla.getOptions('csrf.token'), 1);
|
||||||
|
|
||||||
|
fetch(window.location.origin + '/administrator/index.php?option=com_ajax&plugin=mokoog&group=system&format=json', {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData
|
||||||
|
})
|
||||||
|
.then(function(r) { return r.json(); })
|
||||||
|
.then(function(data) {
|
||||||
|
if (data.data && data.data[0]) {
|
||||||
|
field.value = data.data[0];
|
||||||
|
field.dispatchEvent(new Event('input'));
|
||||||
|
}
|
||||||
|
btn.disabled = false;
|
||||||
|
btn.textContent = 'Generate with AI';
|
||||||
|
})
|
||||||
|
.catch(function() {
|
||||||
|
btn.disabled = false;
|
||||||
|
btn.textContent = 'Generate with AI';
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// Find the mokoog fieldset and insert preview after it
|
// Find the mokoog fieldset and insert preview after it
|
||||||
var fieldset = document.querySelector('[data-showon-id="mokoog"]') ||
|
var fieldset = document.querySelector('[data-showon-id="mokoog"]') ||
|
||||||
document.getElementById('attrib-mokoog') ||
|
document.getElementById('attrib-mokoog') ||
|
||||||
@@ -110,6 +188,137 @@ document.addEventListener('DOMContentLoaded', function () {
|
|||||||
twCard.appendChild(twBody);
|
twCard.appendChild(twBody);
|
||||||
wrapper.appendChild(twCard);
|
wrapper.appendChild(twCard);
|
||||||
|
|
||||||
|
// LinkedIn preview card
|
||||||
|
var liLabel = document.createElement('small');
|
||||||
|
liLabel.className = 'mokoog-platform-label';
|
||||||
|
liLabel.textContent = 'LinkedIn';
|
||||||
|
wrapper.appendChild(liLabel);
|
||||||
|
|
||||||
|
var liCard = document.createElement('div');
|
||||||
|
liCard.className = 'mokoog-card mokoog-card-li';
|
||||||
|
|
||||||
|
var liImg = document.createElement('div');
|
||||||
|
liImg.id = 'mokoog-li-img';
|
||||||
|
liImg.className = 'mokoog-card-img';
|
||||||
|
liCard.appendChild(liImg);
|
||||||
|
|
||||||
|
var liBody = document.createElement('div');
|
||||||
|
liBody.className = 'mokoog-card-body';
|
||||||
|
|
||||||
|
var liTitle = document.createElement('div');
|
||||||
|
liTitle.id = 'mokoog-li-title';
|
||||||
|
liTitle.className = 'mokoog-card-title';
|
||||||
|
liBody.appendChild(liTitle);
|
||||||
|
|
||||||
|
var liDomain = document.createElement('div');
|
||||||
|
liDomain.id = 'mokoog-li-domain';
|
||||||
|
liDomain.className = 'mokoog-card-domain';
|
||||||
|
liBody.appendChild(liDomain);
|
||||||
|
|
||||||
|
liCard.appendChild(liBody);
|
||||||
|
wrapper.appendChild(liCard);
|
||||||
|
|
||||||
|
// Discord preview card
|
||||||
|
var dcLabel = document.createElement('small');
|
||||||
|
dcLabel.className = 'mokoog-platform-label';
|
||||||
|
dcLabel.textContent = 'Discord';
|
||||||
|
wrapper.appendChild(dcLabel);
|
||||||
|
|
||||||
|
var dcCard = document.createElement('div');
|
||||||
|
dcCard.className = 'mokoog-card mokoog-card-dc';
|
||||||
|
|
||||||
|
var dcBody = document.createElement('div');
|
||||||
|
dcBody.className = 'mokoog-card-body';
|
||||||
|
|
||||||
|
var dcTitle = document.createElement('div');
|
||||||
|
dcTitle.id = 'mokoog-dc-title';
|
||||||
|
dcTitle.className = 'mokoog-card-title';
|
||||||
|
dcBody.appendChild(dcTitle);
|
||||||
|
|
||||||
|
var dcDesc = document.createElement('div');
|
||||||
|
dcDesc.id = 'mokoog-dc-desc';
|
||||||
|
dcDesc.className = 'mokoog-card-desc';
|
||||||
|
dcBody.appendChild(dcDesc);
|
||||||
|
|
||||||
|
var dcDomain = document.createElement('div');
|
||||||
|
dcDomain.id = 'mokoog-dc-domain';
|
||||||
|
dcDomain.className = 'mokoog-card-domain';
|
||||||
|
dcBody.appendChild(dcDomain);
|
||||||
|
|
||||||
|
dcCard.appendChild(dcBody);
|
||||||
|
|
||||||
|
var dcImg = document.createElement('div');
|
||||||
|
dcImg.id = 'mokoog-dc-img';
|
||||||
|
dcImg.className = 'mokoog-card-img';
|
||||||
|
dcCard.appendChild(dcImg);
|
||||||
|
|
||||||
|
wrapper.appendChild(dcCard);
|
||||||
|
|
||||||
|
// Mastodon preview card
|
||||||
|
var maLabel = document.createElement('small');
|
||||||
|
maLabel.className = 'mokoog-platform-label';
|
||||||
|
maLabel.textContent = 'Mastodon';
|
||||||
|
wrapper.appendChild(maLabel);
|
||||||
|
|
||||||
|
var maCard = document.createElement('div');
|
||||||
|
maCard.className = 'mokoog-card mokoog-card-ma';
|
||||||
|
|
||||||
|
var maImg = document.createElement('div');
|
||||||
|
maImg.id = 'mokoog-ma-img';
|
||||||
|
maImg.className = 'mokoog-card-img';
|
||||||
|
maCard.appendChild(maImg);
|
||||||
|
|
||||||
|
var maBody = document.createElement('div');
|
||||||
|
maBody.className = 'mokoog-card-body';
|
||||||
|
|
||||||
|
var maTitle = document.createElement('div');
|
||||||
|
maTitle.id = 'mokoog-ma-title';
|
||||||
|
maTitle.className = 'mokoog-card-title';
|
||||||
|
maBody.appendChild(maTitle);
|
||||||
|
|
||||||
|
var maDesc = document.createElement('div');
|
||||||
|
maDesc.id = 'mokoog-ma-desc';
|
||||||
|
maDesc.className = 'mokoog-card-desc';
|
||||||
|
maBody.appendChild(maDesc);
|
||||||
|
|
||||||
|
var maDomain = document.createElement('div');
|
||||||
|
maDomain.id = 'mokoog-ma-domain';
|
||||||
|
maDomain.className = 'mokoog-card-domain';
|
||||||
|
maBody.appendChild(maDomain);
|
||||||
|
|
||||||
|
maCard.appendChild(maBody);
|
||||||
|
wrapper.appendChild(maCard);
|
||||||
|
|
||||||
|
// Slack preview card
|
||||||
|
var slLabel = document.createElement('small');
|
||||||
|
slLabel.className = 'mokoog-platform-label';
|
||||||
|
slLabel.textContent = 'Slack';
|
||||||
|
wrapper.appendChild(slLabel);
|
||||||
|
|
||||||
|
var slCard = document.createElement('div');
|
||||||
|
slCard.className = 'mokoog-card mokoog-card-sl';
|
||||||
|
|
||||||
|
var slBody = document.createElement('div');
|
||||||
|
slBody.className = 'mokoog-card-body';
|
||||||
|
|
||||||
|
var slTitle = document.createElement('div');
|
||||||
|
slTitle.id = 'mokoog-sl-title';
|
||||||
|
slTitle.className = 'mokoog-card-title';
|
||||||
|
slBody.appendChild(slTitle);
|
||||||
|
|
||||||
|
var slDesc = document.createElement('div');
|
||||||
|
slDesc.id = 'mokoog-sl-desc';
|
||||||
|
slDesc.className = 'mokoog-card-desc';
|
||||||
|
slBody.appendChild(slDesc);
|
||||||
|
|
||||||
|
var slDomain = document.createElement('div');
|
||||||
|
slDomain.id = 'mokoog-sl-domain';
|
||||||
|
slDomain.className = 'mokoog-card-domain';
|
||||||
|
slBody.appendChild(slDomain);
|
||||||
|
|
||||||
|
slCard.appendChild(slBody);
|
||||||
|
wrapper.appendChild(slCard);
|
||||||
|
|
||||||
preview.appendChild(wrapper);
|
preview.appendChild(wrapper);
|
||||||
fieldset.parentNode.insertBefore(preview, fieldset.nextSibling);
|
fieldset.parentNode.insertBefore(preview, fieldset.nextSibling);
|
||||||
|
|
||||||
@@ -152,19 +361,139 @@ document.addEventListener('DOMContentLoaded', function () {
|
|||||||
} else {
|
} else {
|
||||||
twImgEl.style.display = 'none';
|
twImgEl.style.display = 'none';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// LinkedIn (shorter truncation: title 70, no description shown in card)
|
||||||
|
var liTitle = title.length > 70 ? title.substring(0, 67) + '...' : title;
|
||||||
|
document.getElementById('mokoog-li-title').textContent = liTitle;
|
||||||
|
document.getElementById('mokoog-li-domain').textContent = domain;
|
||||||
|
var liImgEl = document.getElementById('mokoog-li-img');
|
||||||
|
if (img) {
|
||||||
|
liImgEl.style.backgroundImage = 'url(' + encodeURI(img) + ')';
|
||||||
|
liImgEl.style.display = '';
|
||||||
|
} else {
|
||||||
|
liImgEl.style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Discord (title 256, desc 350)
|
||||||
|
var dcTitle = title.length > 256 ? title.substring(0, 253) + '...' : title;
|
||||||
|
var dcDesc = desc.length > 350 ? desc.substring(0, 347) + '...' : desc;
|
||||||
|
document.getElementById('mokoog-dc-title').textContent = dcTitle;
|
||||||
|
document.getElementById('mokoog-dc-desc').textContent = dcDesc;
|
||||||
|
document.getElementById('mokoog-dc-domain').textContent = domain;
|
||||||
|
var dcImgEl = document.getElementById('mokoog-dc-img');
|
||||||
|
if (img) {
|
||||||
|
dcImgEl.style.backgroundImage = 'url(' + encodeURI(img) + ')';
|
||||||
|
dcImgEl.style.display = '';
|
||||||
|
} else {
|
||||||
|
dcImgEl.style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mastodon (title 70, desc 200)
|
||||||
|
var maTitle = title.length > 70 ? title.substring(0, 67) + '...' : title;
|
||||||
|
var maDesc = desc.length > 200 ? desc.substring(0, 197) + '...' : desc;
|
||||||
|
document.getElementById('mokoog-ma-title').textContent = maTitle;
|
||||||
|
document.getElementById('mokoog-ma-desc').textContent = maDesc;
|
||||||
|
document.getElementById('mokoog-ma-domain').textContent = domain;
|
||||||
|
var maImgEl = document.getElementById('mokoog-ma-img');
|
||||||
|
if (img) {
|
||||||
|
maImgEl.style.backgroundImage = 'url(' + encodeURI(img) + ')';
|
||||||
|
maImgEl.style.display = '';
|
||||||
|
} else {
|
||||||
|
maImgEl.style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Slack (title 70, desc 150, no image)
|
||||||
|
var slTitle = title.length > 70 ? title.substring(0, 67) + '...' : title;
|
||||||
|
var slDesc = desc.length > 150 ? desc.substring(0, 147) + '...' : desc;
|
||||||
|
document.getElementById('mokoog-sl-title').textContent = slTitle;
|
||||||
|
document.getElementById('mokoog-sl-desc').textContent = slDesc;
|
||||||
|
document.getElementById('mokoog-sl-domain').textContent = domain;
|
||||||
|
}
|
||||||
|
|
||||||
|
// SEO scoring panel
|
||||||
|
var seoChecks = [
|
||||||
|
{ id: 'og-title', label: 'OG Title', check: function() { return fields.ogTitle && fields.ogTitle.value.length > 0; }},
|
||||||
|
{ id: 'og-desc', label: 'OG Description', check: function() { return fields.ogDesc && fields.ogDesc.value.length > 0; }},
|
||||||
|
{ id: 'og-image', label: 'OG Image', check: function() { return fields.ogImage && fields.ogImage.value.length > 0; }},
|
||||||
|
{ id: 'seo-title', label: 'SEO Title', check: function() { return fields.seoTitle && fields.seoTitle.value.length > 0; }},
|
||||||
|
{ id: 'meta-desc', label: 'Meta Description', check: function() { return fields.metaDescription && fields.metaDescription.value.length > 0; }},
|
||||||
|
{ id: 'title-length', label: 'Title Length (\u226460)', check: function() {
|
||||||
|
var t = (fields.ogTitle && fields.ogTitle.value) || (fields.articleTitle && fields.articleTitle.value) || '';
|
||||||
|
return t.length > 0 && t.length <= 60;
|
||||||
|
}},
|
||||||
|
{ id: 'desc-length', label: 'Description Length (\u2264160)', check: function() {
|
||||||
|
var d = (fields.ogDesc && fields.ogDesc.value) || (fields.metaDesc && fields.metaDesc.value) || '';
|
||||||
|
return d.length > 0 && d.length <= 160;
|
||||||
|
}}
|
||||||
|
];
|
||||||
|
|
||||||
|
var seoPanel = document.createElement('div');
|
||||||
|
seoPanel.className = 'mokoog-seo-score';
|
||||||
|
|
||||||
|
var seoHeading = document.createElement('h4');
|
||||||
|
seoHeading.className = 'mokoog-seo-heading';
|
||||||
|
seoHeading.textContent = 'SEO Analysis';
|
||||||
|
seoPanel.appendChild(seoHeading);
|
||||||
|
|
||||||
|
var seoList = document.createElement('ul');
|
||||||
|
seoList.className = 'mokoog-seo-list';
|
||||||
|
|
||||||
|
var seoDots = {};
|
||||||
|
seoChecks.forEach(function (chk) {
|
||||||
|
var li = document.createElement('li');
|
||||||
|
li.className = 'mokoog-seo-item';
|
||||||
|
|
||||||
|
var dot = document.createElement('span');
|
||||||
|
dot.className = 'mokoog-seo-dot mokoog-seo-fail';
|
||||||
|
seoDots[chk.id] = dot;
|
||||||
|
li.appendChild(dot);
|
||||||
|
|
||||||
|
var label = document.createElement('span');
|
||||||
|
label.textContent = chk.label;
|
||||||
|
li.appendChild(label);
|
||||||
|
|
||||||
|
seoList.appendChild(li);
|
||||||
|
});
|
||||||
|
|
||||||
|
seoPanel.appendChild(seoList);
|
||||||
|
|
||||||
|
var seoTotal = document.createElement('div');
|
||||||
|
seoTotal.className = 'mokoog-seo-total';
|
||||||
|
seoPanel.appendChild(seoTotal);
|
||||||
|
|
||||||
|
wrapper.parentNode.insertBefore(seoPanel, wrapper.nextSibling);
|
||||||
|
|
||||||
|
function updateSeoScore() {
|
||||||
|
var passed = 0;
|
||||||
|
seoChecks.forEach(function (chk) {
|
||||||
|
var ok = chk.check();
|
||||||
|
if (ok) passed++;
|
||||||
|
seoDots[chk.id].className = 'mokoog-seo-dot ' + (ok ? 'mokoog-seo-pass' : 'mokoog-seo-fail');
|
||||||
|
});
|
||||||
|
|
||||||
|
seoTotal.textContent = passed + '/' + seoChecks.length + ' checks passed';
|
||||||
|
|
||||||
|
if (passed === seoChecks.length) {
|
||||||
|
seoTotal.className = 'mokoog-seo-total mokoog-seo-total-good';
|
||||||
|
} else if (passed >= Math.ceil(seoChecks.length / 2)) {
|
||||||
|
seoTotal.className = 'mokoog-seo-total mokoog-seo-total-ok';
|
||||||
|
} else {
|
||||||
|
seoTotal.className = 'mokoog-seo-total mokoog-seo-total-bad';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Object.values(fields).forEach(function (el) {
|
Object.values(fields).forEach(function (el) {
|
||||||
if (el) {
|
if (el) {
|
||||||
el.addEventListener('input', updatePreview);
|
el.addEventListener('input', function () { updatePreview(); updateSeoScore(); });
|
||||||
el.addEventListener('change', updatePreview);
|
el.addEventListener('change', function () { updatePreview(); updateSeoScore(); });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (fields.ogImage) {
|
if (fields.ogImage) {
|
||||||
var observer = new MutationObserver(updatePreview);
|
var observer = new MutationObserver(function () { updatePreview(); updateSeoScore(); });
|
||||||
observer.observe(fields.ogImage, { attributes: true, attributeFilter: ['value'] });
|
observer.observe(fields.ogImage, { attributes: true, attributeFilter: ['value'] });
|
||||||
}
|
}
|
||||||
|
|
||||||
updatePreview();
|
updatePreview();
|
||||||
|
updateSeoScore();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @package MokoJoomOpenGraph
|
* @package MokoSuiteOpenGraph
|
||||||
* @subpackage plg_content_mokoog
|
* @subpackage plg_content_mokoog
|
||||||
* @author Moko Consulting <hello@mokoconsulting.tech>
|
* @author Moko Consulting <hello@mokoconsulting.tech>
|
||||||
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<!--
|
<!--
|
||||||
* @package MokoJoomOpenGraph
|
* @package MokoSuiteOpenGraph
|
||||||
* @subpackage plg_content_mokoog
|
* @subpackage plg_content_mokoog
|
||||||
* @author Moko Consulting <hello@mokoconsulting.tech>
|
* @author Moko Consulting <hello@mokoconsulting.tech>
|
||||||
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||||
* @license GNU General Public License version 3 or later; see LICENSE
|
* @license GNU General Public License version 3 or later; see LICENSE
|
||||||
-->
|
-->
|
||||||
<extension type="plugin" group="content" method="upgrade">
|
<extension type="plugin" group="content" method="upgrade">
|
||||||
<name>Content - MokoJoomOpenGraph</name>
|
<name>Content - MokoSuiteOpenGraph</name>
|
||||||
<version>01.02.00</version>
|
<version>01.05.02</version>
|
||||||
<creationDate>2026-05-23</creationDate>
|
<creationDate>2026-05-23</creationDate>
|
||||||
<author>Moko Consulting</author>
|
<author>Moko Consulting</author>
|
||||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @package MokoJoomOpenGraph
|
* @package MokoSuiteOpenGraph
|
||||||
* @subpackage plg_content_mokoog
|
* @subpackage plg_content_mokoog
|
||||||
* @author Moko Consulting <hello@mokoconsulting.tech>
|
* @author Moko Consulting <hello@mokoconsulting.tech>
|
||||||
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @package MokoJoomOpenGraph
|
* @package MokoSuiteOpenGraph
|
||||||
* @subpackage plg_content_mokoog
|
* @subpackage plg_content_mokoog
|
||||||
* @author Moko Consulting <hello@mokoconsulting.tech>
|
* @author Moko Consulting <hello@mokoconsulting.tech>
|
||||||
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||||
@@ -98,7 +98,24 @@ final class MokoOGContent extends CMSPlugin implements SubscriberInterface
|
|||||||
$ogData = $this->loadOgData($contentType, $id, $language);
|
$ogData = $this->loadOgData($contentType, $id, $language);
|
||||||
|
|
||||||
if ($ogData) {
|
if ($ogData) {
|
||||||
$form->bind(['mokoog' => (array) $ogData]);
|
$bindData = (array) $ogData;
|
||||||
|
|
||||||
|
// Unpack JSON blob fields into individual form fields
|
||||||
|
foreach (['event_data', 'recipe_data'] as $jsonField) {
|
||||||
|
if (!empty($bindData[$jsonField])) {
|
||||||
|
$decoded = json_decode($bindData[$jsonField], true);
|
||||||
|
|
||||||
|
if (\is_array($decoded)) {
|
||||||
|
foreach ($decoded as $key => $value) {
|
||||||
|
$bindData[$key] = $value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
unset($bindData[$jsonField]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$form->bind(['mokoog' => $bindData]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -170,7 +187,7 @@ final class MokoOGContent extends CMSPlugin implements SubscriberInterface
|
|||||||
$contentType = $supportedContexts[$context];
|
$contentType = $supportedContexts[$context];
|
||||||
$contentId = (int) $article->id;
|
$contentId = (int) $article->id;
|
||||||
|
|
||||||
$db = Factory::getDbo();
|
$db = Factory::getContainer()->get(\Joomla\Database\DatabaseInterface::class);
|
||||||
$query = $db->getQuery(true)
|
$query = $db->getQuery(true)
|
||||||
->delete($db->quoteName('#__mokoog_tags'))
|
->delete($db->quoteName('#__mokoog_tags'))
|
||||||
->where($db->quoteName('content_type') . ' = ' . $db->quote($contentType))
|
->where($db->quoteName('content_type') . ' = ' . $db->quote($contentType))
|
||||||
@@ -191,10 +208,11 @@ final class MokoOGContent extends CMSPlugin implements SubscriberInterface
|
|||||||
*/
|
*/
|
||||||
private function loadOgData(string $contentType, int $contentId, string $language = '*'): ?object
|
private function loadOgData(string $contentType, int $contentId, string $language = '*'): ?object
|
||||||
{
|
{
|
||||||
$db = Factory::getDbo();
|
$db = Factory::getContainer()->get(\Joomla\Database\DatabaseInterface::class);
|
||||||
$query = $db->getQuery(true)
|
$query = $db->getQuery(true)
|
||||||
->select($db->quoteName([
|
->select($db->quoteName([
|
||||||
'og_title', 'og_description', 'og_image', 'og_type',
|
'og_title', 'og_description', 'og_image', 'og_type', 'og_video',
|
||||||
|
'event_data', 'recipe_data', 'custom_schema',
|
||||||
'seo_title', 'meta_description', 'robots', 'canonical_url',
|
'seo_title', 'meta_description', 'robots', 'canonical_url',
|
||||||
]))
|
]))
|
||||||
->from($db->quoteName('#__mokoog_tags'))
|
->from($db->quoteName('#__mokoog_tags'))
|
||||||
@@ -221,7 +239,7 @@ final class MokoOGContent extends CMSPlugin implements SubscriberInterface
|
|||||||
*/
|
*/
|
||||||
private function saveOgData(string $contentType, int $contentId, array $ogData, string $language = '*'): void
|
private function saveOgData(string $contentType, int $contentId, array $ogData, string $language = '*'): void
|
||||||
{
|
{
|
||||||
$db = Factory::getDbo();
|
$db = Factory::getContainer()->get(\Joomla\Database\DatabaseInterface::class);
|
||||||
|
|
||||||
// Check if record exists for this content + language
|
// Check if record exists for this content + language
|
||||||
$query = $db->getQuery(true)
|
$query = $db->getQuery(true)
|
||||||
@@ -245,14 +263,18 @@ final class MokoOGContent extends CMSPlugin implements SubscriberInterface
|
|||||||
'content_type' => $contentType,
|
'content_type' => $contentType,
|
||||||
'content_id' => $contentId,
|
'content_id' => $contentId,
|
||||||
'language' => $language,
|
'language' => $language,
|
||||||
'og_title' => trim($ogData['og_title'] ?? ''),
|
'og_title' => strip_tags(trim($ogData['og_title'] ?? '')),
|
||||||
'og_description' => trim($ogData['og_description'] ?? ''),
|
'og_description' => strip_tags(trim($ogData['og_description'] ?? '')),
|
||||||
'og_image' => trim($ogData['og_image'] ?? ''),
|
'og_image' => trim($ogData['og_image'] ?? ''),
|
||||||
'og_type' => trim($ogData['og_type'] ?? 'article'),
|
'og_type' => trim($ogData['og_type'] ?? 'article'),
|
||||||
'seo_title' => trim($ogData['seo_title'] ?? ''),
|
'og_video' => $this->sanitizeUrl($ogData['og_video'] ?? ''),
|
||||||
'meta_description' => trim($ogData['meta_description'] ?? ''),
|
'event_data' => $this->packJsonFields($ogData, ['event_start', 'event_end', 'event_location', 'event_address', 'event_price', 'event_currency', 'event_url']),
|
||||||
|
'recipe_data' => $this->packJsonFields($ogData, ['recipe_prep_time', 'recipe_cook_time', 'recipe_yield', 'recipe_calories', 'recipe_ingredients', 'recipe_category', 'recipe_cuisine']),
|
||||||
|
'custom_schema' => $this->validateJson($ogData['custom_schema'] ?? ''),
|
||||||
|
'seo_title' => strip_tags(trim($ogData['seo_title'] ?? '')),
|
||||||
|
'meta_description' => strip_tags(trim($ogData['meta_description'] ?? '')),
|
||||||
'robots' => trim($robots),
|
'robots' => trim($robots),
|
||||||
'canonical_url' => trim($ogData['canonical_url'] ?? ''),
|
'canonical_url' => $this->sanitizeUrl($ogData['canonical_url'] ?? ''),
|
||||||
'published' => 1,
|
'published' => 1,
|
||||||
'modified' => Factory::getDate()->toSql(),
|
'modified' => Factory::getDate()->toSql(),
|
||||||
];
|
];
|
||||||
@@ -266,6 +288,69 @@ final class MokoOGContent extends CMSPlugin implements SubscriberInterface
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pack form fields into a JSON string for storage.
|
||||||
|
*
|
||||||
|
* @param array $ogData Form data array
|
||||||
|
* @param array $fields Field names to pack
|
||||||
|
*
|
||||||
|
* @return string JSON string or empty
|
||||||
|
*/
|
||||||
|
private function packJsonFields(array $ogData, array $fields): string
|
||||||
|
{
|
||||||
|
$data = [];
|
||||||
|
|
||||||
|
foreach ($fields as $field) {
|
||||||
|
$val = trim($ogData[$field] ?? '');
|
||||||
|
|
||||||
|
if ($val !== '') {
|
||||||
|
$data[$field] = $val;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return !empty($data) ? json_encode($data) : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate a JSON string — returns trimmed JSON or empty string if invalid.
|
||||||
|
*
|
||||||
|
* @param string $json Raw JSON input
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
private function validateJson(string $json): string
|
||||||
|
{
|
||||||
|
$json = trim($json);
|
||||||
|
|
||||||
|
if ($json === '' || json_decode($json) === null) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return $json;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sanitize a URL to only allow http/https schemes.
|
||||||
|
*
|
||||||
|
* @param string $url Raw URL value
|
||||||
|
*
|
||||||
|
* @return string Sanitized URL or empty string
|
||||||
|
*/
|
||||||
|
private function sanitizeUrl(string $url): string
|
||||||
|
{
|
||||||
|
$url = trim($url);
|
||||||
|
|
||||||
|
if ($url === '') {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!str_starts_with($url, 'http://') && !str_starts_with($url, 'https://')) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return $url;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Extract the language tag from content data.
|
* Extract the language tag from content data.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
; MokoJoomOpenGraph - System Plugin Language File
|
; MokoSuiteOpenGraph - System Plugin Language File
|
||||||
; Copyright (C) 2026 Moko Consulting. All rights reserved.
|
; Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||||
; License: GPL-3.0-or-later
|
; License: GPL-3.0-or-later
|
||||||
|
|
||||||
@@ -23,6 +23,8 @@ PLG_SYSTEM_MOKOOG_FIELD_FB_APP_ID="Facebook App ID"
|
|||||||
PLG_SYSTEM_MOKOOG_FIELD_FB_APP_ID_DESC="Your Facebook App ID for fb:app_id meta tag."
|
PLG_SYSTEM_MOKOOG_FIELD_FB_APP_ID_DESC="Your Facebook App ID for fb:app_id meta tag."
|
||||||
PLG_SYSTEM_MOKOOG_FIELD_DISCORD_COLOR="Discord Embed Color"
|
PLG_SYSTEM_MOKOOG_FIELD_DISCORD_COLOR="Discord Embed Color"
|
||||||
PLG_SYSTEM_MOKOOG_FIELD_DISCORD_COLOR_DESC="The color of the embed sidebar when shared on Discord. Sets the theme-color meta tag. Leave blank to use Discord defaults."
|
PLG_SYSTEM_MOKOOG_FIELD_DISCORD_COLOR_DESC="The color of the embed sidebar when shared on Discord. Sets the theme-color meta tag. Leave blank to use Discord defaults."
|
||||||
|
PLG_SYSTEM_MOKOOG_FIELD_FEDIVERSE_CREATOR="Fediverse Creator"
|
||||||
|
PLG_SYSTEM_MOKOOG_FIELD_FEDIVERSE_CREATOR_DESC="Your Fediverse/Mastodon handle (e.g. @user@mastodon.social). Outputs a fediverse:creator meta tag for author attribution on Mastodon and other Fediverse platforms."
|
||||||
PLG_SYSTEM_MOKOOG_FIELD_AUTO_GENERATE="Auto-generate Tags"
|
PLG_SYSTEM_MOKOOG_FIELD_AUTO_GENERATE="Auto-generate Tags"
|
||||||
PLG_SYSTEM_MOKOOG_FIELD_AUTO_GENERATE_DESC="Automatically generate OG tags from article content when no custom tags are set."
|
PLG_SYSTEM_MOKOOG_FIELD_AUTO_GENERATE_DESC="Automatically generate OG tags from article content when no custom tags are set."
|
||||||
PLG_SYSTEM_MOKOOG_FIELD_STRIP_HTML="Strip HTML from Description"
|
PLG_SYSTEM_MOKOOG_FIELD_STRIP_HTML="Strip HTML from Description"
|
||||||
@@ -35,5 +37,59 @@ PLG_SYSTEM_MOKOOG_FIELD_AUTO_RESIZE="Auto-resize Images"
|
|||||||
PLG_SYSTEM_MOKOOG_FIELD_AUTO_RESIZE_DESC="Automatically resize images to 1200x630px (Facebook recommended) using center crop. Generated images are saved to images/mokoog/generated/."
|
PLG_SYSTEM_MOKOOG_FIELD_AUTO_RESIZE_DESC="Automatically resize images to 1200x630px (Facebook recommended) using center crop. Generated images are saved to images/mokoog/generated/."
|
||||||
PLG_SYSTEM_MOKOOG_FIELD_JSONLD_ENABLED="Enable JSON-LD"
|
PLG_SYSTEM_MOKOOG_FIELD_JSONLD_ENABLED="Enable JSON-LD"
|
||||||
PLG_SYSTEM_MOKOOG_FIELD_JSONLD_ENABLED_DESC="Output JSON-LD structured data (Article, WebPage) for Google rich results."
|
PLG_SYSTEM_MOKOOG_FIELD_JSONLD_ENABLED_DESC="Output JSON-LD structured data (Article, WebPage) for Google rich results."
|
||||||
|
PLG_SYSTEM_MOKOOG_FIELD_JSONLD_FAQ="JSON-LD FAQ Schema"
|
||||||
|
PLG_SYSTEM_MOKOOG_FIELD_JSONLD_FAQ_DESC="Auto-detect FAQ sections from article headings (h3/h4 + paragraphs) and output FAQPage structured data."
|
||||||
|
PLG_SYSTEM_MOKOOG_FIELD_JSONLD_HOWTO="JSON-LD HowTo Schema"
|
||||||
|
PLG_SYSTEM_MOKOOG_FIELD_JSONLD_HOWTO_DESC="Auto-detect step-by-step instructions from ordered lists (ol/li) and output HowTo structured data."
|
||||||
PLG_SYSTEM_MOKOOG_FIELD_JSONLD_BREADCRUMBS="JSON-LD Breadcrumbs"
|
PLG_SYSTEM_MOKOOG_FIELD_JSONLD_BREADCRUMBS="JSON-LD Breadcrumbs"
|
||||||
PLG_SYSTEM_MOKOOG_FIELD_JSONLD_BREADCRUMBS_DESC="Output BreadcrumbList JSON-LD schema from Joomla's pathway."
|
PLG_SYSTEM_MOKOOG_FIELD_JSONLD_BREADCRUMBS_DESC="Output BreadcrumbList JSON-LD schema from Joomla's pathway."
|
||||||
|
|
||||||
|
PLG_SYSTEM_MOKOOG_FIELDSET_LOCALBUSINESS="Local Business"
|
||||||
|
PLG_SYSTEM_MOKOOG_FIELD_LB_ENABLED="Enable LocalBusiness Schema"
|
||||||
|
PLG_SYSTEM_MOKOOG_FIELD_LB_ENABLED_DESC="Output LocalBusiness JSON-LD structured data on all pages."
|
||||||
|
PLG_SYSTEM_MOKOOG_FIELD_LB_NAME="Business Name"
|
||||||
|
PLG_SYSTEM_MOKOOG_FIELD_LB_NAME_DESC="Your business name for structured data."
|
||||||
|
PLG_SYSTEM_MOKOOG_FIELD_LB_TYPE="Business Type"
|
||||||
|
PLG_SYSTEM_MOKOOG_FIELD_LB_TYPE_DESC="Schema.org business type."
|
||||||
|
PLG_SYSTEM_MOKOOG_FIELD_LB_STREET="Street Address"
|
||||||
|
PLG_SYSTEM_MOKOOG_FIELD_LB_STREET_DESC="Street address of your business."
|
||||||
|
PLG_SYSTEM_MOKOOG_FIELD_LB_CITY="City"
|
||||||
|
PLG_SYSTEM_MOKOOG_FIELD_LB_CITY_DESC="City of your business."
|
||||||
|
PLG_SYSTEM_MOKOOG_FIELD_LB_REGION="State/Region"
|
||||||
|
PLG_SYSTEM_MOKOOG_FIELD_LB_REGION_DESC="State or region of your business."
|
||||||
|
PLG_SYSTEM_MOKOOG_FIELD_LB_POSTAL="Postal Code"
|
||||||
|
PLG_SYSTEM_MOKOOG_FIELD_LB_POSTAL_DESC="Postal/ZIP code of your business."
|
||||||
|
PLG_SYSTEM_MOKOOG_FIELD_LB_COUNTRY="Country"
|
||||||
|
PLG_SYSTEM_MOKOOG_FIELD_LB_COUNTRY_DESC="Country code (e.g. US, GB, DE)."
|
||||||
|
PLG_SYSTEM_MOKOOG_FIELD_LB_PHONE="Phone"
|
||||||
|
PLG_SYSTEM_MOKOOG_FIELD_LB_PHONE_DESC="Business phone number."
|
||||||
|
PLG_SYSTEM_MOKOOG_FIELD_LB_EMAIL="Email"
|
||||||
|
PLG_SYSTEM_MOKOOG_FIELD_LB_EMAIL_DESC="Business email address."
|
||||||
|
PLG_SYSTEM_MOKOOG_FIELD_LB_URL="Website URL"
|
||||||
|
PLG_SYSTEM_MOKOOG_FIELD_LB_URL_DESC="Business website URL."
|
||||||
|
PLG_SYSTEM_MOKOOG_FIELD_LB_OPENING_HOURS="Opening Hours"
|
||||||
|
PLG_SYSTEM_MOKOOG_FIELD_LB_OPENING_HOURS_DESC="Opening hours in schema.org format (e.g. Mo-Fr 09:00-17:00)."
|
||||||
|
PLG_SYSTEM_MOKOOG_FIELD_LB_LATITUDE="Latitude"
|
||||||
|
PLG_SYSTEM_MOKOOG_FIELD_LB_LATITUDE_DESC="Geographic latitude of your business."
|
||||||
|
PLG_SYSTEM_MOKOOG_FIELD_LB_LONGITUDE="Longitude"
|
||||||
|
PLG_SYSTEM_MOKOOG_FIELD_LB_LONGITUDE_DESC="Geographic longitude of your business."
|
||||||
|
PLG_SYSTEM_MOKOOG_FIELD_LB_PRICE_RANGE="Price Range"
|
||||||
|
PLG_SYSTEM_MOKOOG_FIELD_LB_PRICE_RANGE_DESC="Price range indicator (e.g. $, $$, $$$)."
|
||||||
|
PLG_SYSTEM_MOKOOG_FIELD_PLATFORM_RESIZE="Per-platform Image Sizes"
|
||||||
|
PLG_SYSTEM_MOKOOG_FIELD_PLATFORM_RESIZE_DESC="Generate platform-specific image sizes (Twitter 1200x600, Pinterest 1000x1500, WhatsApp 400x400) in addition to the default Facebook 1200x630."
|
||||||
|
|
||||||
|
PLG_SYSTEM_MOKOOG_FIELDSET_SITEMAP="XML Sitemap"
|
||||||
|
PLG_SYSTEM_MOKOOG_FIELD_SITEMAP_ENABLED="Enable Sitemap"
|
||||||
|
PLG_SYSTEM_MOKOOG_FIELD_SITEMAP_ENABLED_DESC="Auto-generate sitemap.xml when articles are saved. Respects noindex robots directives."
|
||||||
|
PLG_SYSTEM_MOKOOG_FIELD_SITEMAP_CHANGEFREQ="Change Frequency"
|
||||||
|
PLG_SYSTEM_MOKOOG_FIELD_SITEMAP_CHANGEFREQ_DESC="Default change frequency for sitemap entries."
|
||||||
|
|
||||||
|
PLG_SYSTEM_MOKOOG_FIELDSET_AI="AI Meta Generation"
|
||||||
|
PLG_SYSTEM_MOKOOG_FIELD_AI_ENABLED="Enable AI Generation"
|
||||||
|
PLG_SYSTEM_MOKOOG_FIELD_AI_ENABLED_DESC="Show Generate with AI buttons next to OG title and description fields."
|
||||||
|
PLG_SYSTEM_MOKOOG_FIELD_AI_PROVIDER="AI Provider"
|
||||||
|
PLG_SYSTEM_MOKOOG_FIELD_AI_PROVIDER_DESC="Select the AI API provider."
|
||||||
|
PLG_SYSTEM_MOKOOG_FIELD_AI_API_KEY="API Key"
|
||||||
|
PLG_SYSTEM_MOKOOG_FIELD_AI_API_KEY_DESC="Your AI provider API key."
|
||||||
|
PLG_SYSTEM_MOKOOG_FIELD_AI_MODEL="Model"
|
||||||
|
PLG_SYSTEM_MOKOOG_FIELD_AI_MODEL_DESC="AI model to use for generation."
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
; MokoJoomOpenGraph - System Plugin System Language File
|
; MokoSuiteOpenGraph - System Plugin System Language File
|
||||||
; Copyright (C) 2026 Moko Consulting. All rights reserved.
|
; Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||||
; License: GPL-3.0-or-later
|
; License: GPL-3.0-or-later
|
||||||
|
|
||||||
PLG_SYSTEM_MOKOOG="System - MokoJoomOpenGraph"
|
PLG_SYSTEM_MOKOOG="System - MokoSuiteOpenGraph"
|
||||||
PLG_SYSTEM_MOKOOG_DESCRIPTION="Injects Open Graph and Twitter Card meta tags into every page for optimal social media sharing previews."
|
PLG_SYSTEM_MOKOOG_DESCRIPTION="Injects Open Graph and Twitter Card meta tags into every page for optimal social media sharing previews."
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
; MokoJoomOpenGraph - System Plugin Language File
|
; MokoSuiteOpenGraph - System Plugin Language File
|
||||||
; Copyright (C) 2026 Moko Consulting. All rights reserved.
|
; Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||||
; License: GPL-3.0-or-later
|
; License: GPL-3.0-or-later
|
||||||
|
|
||||||
@@ -23,6 +23,8 @@ PLG_SYSTEM_MOKOOG_FIELD_FB_APP_ID="Facebook App ID"
|
|||||||
PLG_SYSTEM_MOKOOG_FIELD_FB_APP_ID_DESC="Your Facebook App ID for fb:app_id meta tag."
|
PLG_SYSTEM_MOKOOG_FIELD_FB_APP_ID_DESC="Your Facebook App ID for fb:app_id meta tag."
|
||||||
PLG_SYSTEM_MOKOOG_FIELD_DISCORD_COLOR="Discord Embed Color"
|
PLG_SYSTEM_MOKOOG_FIELD_DISCORD_COLOR="Discord Embed Color"
|
||||||
PLG_SYSTEM_MOKOOG_FIELD_DISCORD_COLOR_DESC="The color of the embed sidebar when shared on Discord. Sets the theme-color meta tag. Leave blank to use Discord defaults."
|
PLG_SYSTEM_MOKOOG_FIELD_DISCORD_COLOR_DESC="The color of the embed sidebar when shared on Discord. Sets the theme-color meta tag. Leave blank to use Discord defaults."
|
||||||
|
PLG_SYSTEM_MOKOOG_FIELD_FEDIVERSE_CREATOR="Fediverse Creator"
|
||||||
|
PLG_SYSTEM_MOKOOG_FIELD_FEDIVERSE_CREATOR_DESC="Your Fediverse/Mastodon handle (e.g. @user@mastodon.social). Outputs a fediverse:creator meta tag for author attribution on Mastodon and other Fediverse platforms."
|
||||||
PLG_SYSTEM_MOKOOG_FIELD_AUTO_GENERATE="Auto-generate Tags"
|
PLG_SYSTEM_MOKOOG_FIELD_AUTO_GENERATE="Auto-generate Tags"
|
||||||
PLG_SYSTEM_MOKOOG_FIELD_AUTO_GENERATE_DESC="Automatically generate OG tags from article content when no custom tags are set."
|
PLG_SYSTEM_MOKOOG_FIELD_AUTO_GENERATE_DESC="Automatically generate OG tags from article content when no custom tags are set."
|
||||||
PLG_SYSTEM_MOKOOG_FIELD_STRIP_HTML="Strip HTML from Description"
|
PLG_SYSTEM_MOKOOG_FIELD_STRIP_HTML="Strip HTML from Description"
|
||||||
@@ -35,5 +37,59 @@ PLG_SYSTEM_MOKOOG_FIELD_AUTO_RESIZE="Auto-resize Images"
|
|||||||
PLG_SYSTEM_MOKOOG_FIELD_AUTO_RESIZE_DESC="Automatically resize images to 1200x630px (Facebook recommended) using center crop. Generated images are saved to images/mokoog/generated/."
|
PLG_SYSTEM_MOKOOG_FIELD_AUTO_RESIZE_DESC="Automatically resize images to 1200x630px (Facebook recommended) using center crop. Generated images are saved to images/mokoog/generated/."
|
||||||
PLG_SYSTEM_MOKOOG_FIELD_JSONLD_ENABLED="Enable JSON-LD"
|
PLG_SYSTEM_MOKOOG_FIELD_JSONLD_ENABLED="Enable JSON-LD"
|
||||||
PLG_SYSTEM_MOKOOG_FIELD_JSONLD_ENABLED_DESC="Output JSON-LD structured data (Article, WebPage) for Google rich results."
|
PLG_SYSTEM_MOKOOG_FIELD_JSONLD_ENABLED_DESC="Output JSON-LD structured data (Article, WebPage) for Google rich results."
|
||||||
|
PLG_SYSTEM_MOKOOG_FIELD_JSONLD_FAQ="JSON-LD FAQ Schema"
|
||||||
|
PLG_SYSTEM_MOKOOG_FIELD_JSONLD_FAQ_DESC="Auto-detect FAQ sections from article headings (h3/h4 + paragraphs) and output FAQPage structured data."
|
||||||
|
PLG_SYSTEM_MOKOOG_FIELD_JSONLD_HOWTO="JSON-LD HowTo Schema"
|
||||||
|
PLG_SYSTEM_MOKOOG_FIELD_JSONLD_HOWTO_DESC="Auto-detect step-by-step instructions from ordered lists (ol/li) and output HowTo structured data."
|
||||||
PLG_SYSTEM_MOKOOG_FIELD_JSONLD_BREADCRUMBS="JSON-LD Breadcrumbs"
|
PLG_SYSTEM_MOKOOG_FIELD_JSONLD_BREADCRUMBS="JSON-LD Breadcrumbs"
|
||||||
PLG_SYSTEM_MOKOOG_FIELD_JSONLD_BREADCRUMBS_DESC="Output BreadcrumbList JSON-LD schema from Joomla's pathway."
|
PLG_SYSTEM_MOKOOG_FIELD_JSONLD_BREADCRUMBS_DESC="Output BreadcrumbList JSON-LD schema from Joomla's pathway."
|
||||||
|
|
||||||
|
PLG_SYSTEM_MOKOOG_FIELDSET_LOCALBUSINESS="Local Business"
|
||||||
|
PLG_SYSTEM_MOKOOG_FIELD_LB_ENABLED="Enable LocalBusiness Schema"
|
||||||
|
PLG_SYSTEM_MOKOOG_FIELD_LB_ENABLED_DESC="Output LocalBusiness JSON-LD structured data on all pages."
|
||||||
|
PLG_SYSTEM_MOKOOG_FIELD_LB_NAME="Business Name"
|
||||||
|
PLG_SYSTEM_MOKOOG_FIELD_LB_NAME_DESC="Your business name for structured data."
|
||||||
|
PLG_SYSTEM_MOKOOG_FIELD_LB_TYPE="Business Type"
|
||||||
|
PLG_SYSTEM_MOKOOG_FIELD_LB_TYPE_DESC="Schema.org business type."
|
||||||
|
PLG_SYSTEM_MOKOOG_FIELD_LB_STREET="Street Address"
|
||||||
|
PLG_SYSTEM_MOKOOG_FIELD_LB_STREET_DESC="Street address of your business."
|
||||||
|
PLG_SYSTEM_MOKOOG_FIELD_LB_CITY="City"
|
||||||
|
PLG_SYSTEM_MOKOOG_FIELD_LB_CITY_DESC="City of your business."
|
||||||
|
PLG_SYSTEM_MOKOOG_FIELD_LB_REGION="State/Region"
|
||||||
|
PLG_SYSTEM_MOKOOG_FIELD_LB_REGION_DESC="State or region of your business."
|
||||||
|
PLG_SYSTEM_MOKOOG_FIELD_LB_POSTAL="Postal Code"
|
||||||
|
PLG_SYSTEM_MOKOOG_FIELD_LB_POSTAL_DESC="Postal/ZIP code of your business."
|
||||||
|
PLG_SYSTEM_MOKOOG_FIELD_LB_COUNTRY="Country"
|
||||||
|
PLG_SYSTEM_MOKOOG_FIELD_LB_COUNTRY_DESC="Country code (e.g. US, GB, DE)."
|
||||||
|
PLG_SYSTEM_MOKOOG_FIELD_LB_PHONE="Phone"
|
||||||
|
PLG_SYSTEM_MOKOOG_FIELD_LB_PHONE_DESC="Business phone number."
|
||||||
|
PLG_SYSTEM_MOKOOG_FIELD_LB_EMAIL="Email"
|
||||||
|
PLG_SYSTEM_MOKOOG_FIELD_LB_EMAIL_DESC="Business email address."
|
||||||
|
PLG_SYSTEM_MOKOOG_FIELD_LB_URL="Website URL"
|
||||||
|
PLG_SYSTEM_MOKOOG_FIELD_LB_URL_DESC="Business website URL."
|
||||||
|
PLG_SYSTEM_MOKOOG_FIELD_LB_OPENING_HOURS="Opening Hours"
|
||||||
|
PLG_SYSTEM_MOKOOG_FIELD_LB_OPENING_HOURS_DESC="Opening hours in schema.org format (e.g. Mo-Fr 09:00-17:00)."
|
||||||
|
PLG_SYSTEM_MOKOOG_FIELD_LB_LATITUDE="Latitude"
|
||||||
|
PLG_SYSTEM_MOKOOG_FIELD_LB_LATITUDE_DESC="Geographic latitude of your business."
|
||||||
|
PLG_SYSTEM_MOKOOG_FIELD_LB_LONGITUDE="Longitude"
|
||||||
|
PLG_SYSTEM_MOKOOG_FIELD_LB_LONGITUDE_DESC="Geographic longitude of your business."
|
||||||
|
PLG_SYSTEM_MOKOOG_FIELD_LB_PRICE_RANGE="Price Range"
|
||||||
|
PLG_SYSTEM_MOKOOG_FIELD_LB_PRICE_RANGE_DESC="Price range indicator (e.g. $, $$, $$$)."
|
||||||
|
PLG_SYSTEM_MOKOOG_FIELD_PLATFORM_RESIZE="Per-platform Image Sizes"
|
||||||
|
PLG_SYSTEM_MOKOOG_FIELD_PLATFORM_RESIZE_DESC="Generate platform-specific image sizes (Twitter 1200x600, Pinterest 1000x1500, WhatsApp 400x400) in addition to the default Facebook 1200x630."
|
||||||
|
|
||||||
|
PLG_SYSTEM_MOKOOG_FIELDSET_SITEMAP="XML Sitemap"
|
||||||
|
PLG_SYSTEM_MOKOOG_FIELD_SITEMAP_ENABLED="Enable Sitemap"
|
||||||
|
PLG_SYSTEM_MOKOOG_FIELD_SITEMAP_ENABLED_DESC="Auto-generate sitemap.xml when articles are saved. Respects noindex robots directives."
|
||||||
|
PLG_SYSTEM_MOKOOG_FIELD_SITEMAP_CHANGEFREQ="Change Frequency"
|
||||||
|
PLG_SYSTEM_MOKOOG_FIELD_SITEMAP_CHANGEFREQ_DESC="Default change frequency for sitemap entries."
|
||||||
|
|
||||||
|
PLG_SYSTEM_MOKOOG_FIELDSET_AI="AI Meta Generation"
|
||||||
|
PLG_SYSTEM_MOKOOG_FIELD_AI_ENABLED="Enable AI Generation"
|
||||||
|
PLG_SYSTEM_MOKOOG_FIELD_AI_ENABLED_DESC="Show Generate with AI buttons next to OG title and description fields."
|
||||||
|
PLG_SYSTEM_MOKOOG_FIELD_AI_PROVIDER="AI Provider"
|
||||||
|
PLG_SYSTEM_MOKOOG_FIELD_AI_PROVIDER_DESC="Select the AI API provider."
|
||||||
|
PLG_SYSTEM_MOKOOG_FIELD_AI_API_KEY="API Key"
|
||||||
|
PLG_SYSTEM_MOKOOG_FIELD_AI_API_KEY_DESC="Your AI provider API key."
|
||||||
|
PLG_SYSTEM_MOKOOG_FIELD_AI_MODEL="Model"
|
||||||
|
PLG_SYSTEM_MOKOOG_FIELD_AI_MODEL_DESC="AI model to use for generation."
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
; MokoJoomOpenGraph - System Plugin System Language File
|
; MokoSuiteOpenGraph - System Plugin System Language File
|
||||||
; Copyright (C) 2026 Moko Consulting. All rights reserved.
|
; Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||||
; License: GPL-3.0-or-later
|
; License: GPL-3.0-or-later
|
||||||
|
|
||||||
PLG_SYSTEM_MOKOOG="System - MokoJoomOpenGraph"
|
PLG_SYSTEM_MOKOOG="System - MokoSuiteOpenGraph"
|
||||||
PLG_SYSTEM_MOKOOG_DESCRIPTION="Injects Open Graph and Twitter Card meta tags into every page for optimal social media sharing previews."
|
PLG_SYSTEM_MOKOOG_DESCRIPTION="Injects Open Graph and Twitter Card meta tags into every page for optimal social media sharing previews."
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @package MokoJoomOpenGraph
|
* @package MokoSuiteOpenGraph
|
||||||
* @subpackage plg_system_mokoog
|
* @subpackage plg_system_mokoog
|
||||||
* @author Moko Consulting <hello@mokoconsulting.tech>
|
* @author Moko Consulting <hello@mokoconsulting.tech>
|
||||||
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<!--
|
<!--
|
||||||
* @package MokoJoomOpenGraph
|
* @package MokoSuiteOpenGraph
|
||||||
* @subpackage plg_system_mokoog
|
* @subpackage plg_system_mokoog
|
||||||
* @author Moko Consulting <hello@mokoconsulting.tech>
|
* @author Moko Consulting <hello@mokoconsulting.tech>
|
||||||
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||||
* @license GNU General Public License version 3 or later; see LICENSE
|
* @license GNU General Public License version 3 or later; see LICENSE
|
||||||
-->
|
-->
|
||||||
<extension type="plugin" group="system" method="upgrade">
|
<extension type="plugin" group="system" method="upgrade">
|
||||||
<name>System - MokoJoomOpenGraph</name>
|
<name>System - MokoSuiteOpenGraph</name>
|
||||||
<version>01.02.00</version>
|
<version>01.05.02</version>
|
||||||
<creationDate>2026-05-23</creationDate>
|
<creationDate>2026-05-23</creationDate>
|
||||||
<author>Moko Consulting</author>
|
<author>Moko Consulting</author>
|
||||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||||
@@ -106,6 +106,14 @@
|
|||||||
description="PLG_SYSTEM_MOKOOG_FIELD_DISCORD_COLOR_DESC"
|
description="PLG_SYSTEM_MOKOOG_FIELD_DISCORD_COLOR_DESC"
|
||||||
default=""
|
default=""
|
||||||
/>
|
/>
|
||||||
|
<field
|
||||||
|
name="fediverse_creator"
|
||||||
|
type="text"
|
||||||
|
label="PLG_SYSTEM_MOKOOG_FIELD_FEDIVERSE_CREATOR"
|
||||||
|
description="PLG_SYSTEM_MOKOOG_FIELD_FEDIVERSE_CREATOR_DESC"
|
||||||
|
default=""
|
||||||
|
filter="string"
|
||||||
|
/>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
<fieldset name="advanced" label="PLG_SYSTEM_MOKOOG_FIELDSET_ADVANCED">
|
<fieldset name="advanced" label="PLG_SYSTEM_MOKOOG_FIELDSET_ADVANCED">
|
||||||
<field
|
<field
|
||||||
@@ -150,6 +158,17 @@
|
|||||||
<option value="1">JYES</option>
|
<option value="1">JYES</option>
|
||||||
<option value="0">JNO</option>
|
<option value="0">JNO</option>
|
||||||
</field>
|
</field>
|
||||||
|
<field
|
||||||
|
name="platform_resize"
|
||||||
|
type="radio"
|
||||||
|
label="PLG_SYSTEM_MOKOOG_FIELD_PLATFORM_RESIZE"
|
||||||
|
description="PLG_SYSTEM_MOKOOG_FIELD_PLATFORM_RESIZE_DESC"
|
||||||
|
default="0"
|
||||||
|
class="btn-group"
|
||||||
|
>
|
||||||
|
<option value="1">JYES</option>
|
||||||
|
<option value="0">JNO</option>
|
||||||
|
</field>
|
||||||
<field
|
<field
|
||||||
name="jsonld_enabled"
|
name="jsonld_enabled"
|
||||||
type="radio"
|
type="radio"
|
||||||
@@ -161,6 +180,28 @@
|
|||||||
<option value="1">JYES</option>
|
<option value="1">JYES</option>
|
||||||
<option value="0">JNO</option>
|
<option value="0">JNO</option>
|
||||||
</field>
|
</field>
|
||||||
|
<field
|
||||||
|
name="jsonld_faq"
|
||||||
|
type="radio"
|
||||||
|
label="PLG_SYSTEM_MOKOOG_FIELD_JSONLD_FAQ"
|
||||||
|
description="PLG_SYSTEM_MOKOOG_FIELD_JSONLD_FAQ_DESC"
|
||||||
|
default="1"
|
||||||
|
class="btn-group"
|
||||||
|
>
|
||||||
|
<option value="1">JYES</option>
|
||||||
|
<option value="0">JNO</option>
|
||||||
|
</field>
|
||||||
|
<field
|
||||||
|
name="jsonld_howto"
|
||||||
|
type="radio"
|
||||||
|
label="PLG_SYSTEM_MOKOOG_FIELD_JSONLD_HOWTO"
|
||||||
|
description="PLG_SYSTEM_MOKOOG_FIELD_JSONLD_HOWTO_DESC"
|
||||||
|
default="1"
|
||||||
|
class="btn-group"
|
||||||
|
>
|
||||||
|
<option value="1">JYES</option>
|
||||||
|
<option value="0">JNO</option>
|
||||||
|
</field>
|
||||||
<field
|
<field
|
||||||
name="jsonld_breadcrumbs"
|
name="jsonld_breadcrumbs"
|
||||||
type="radio"
|
type="radio"
|
||||||
@@ -173,6 +214,158 @@
|
|||||||
<option value="0">JNO</option>
|
<option value="0">JNO</option>
|
||||||
</field>
|
</field>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
<fieldset name="localbusiness" label="PLG_SYSTEM_MOKOOG_FIELDSET_LOCALBUSINESS">
|
||||||
|
<field
|
||||||
|
name="lb_enabled"
|
||||||
|
type="radio"
|
||||||
|
label="PLG_SYSTEM_MOKOOG_FIELD_LB_ENABLED"
|
||||||
|
description="PLG_SYSTEM_MOKOOG_FIELD_LB_ENABLED_DESC"
|
||||||
|
default="0"
|
||||||
|
class="btn-group"
|
||||||
|
>
|
||||||
|
<option value="1">JYES</option>
|
||||||
|
<option value="0">JNO</option>
|
||||||
|
</field>
|
||||||
|
<field
|
||||||
|
name="lb_name"
|
||||||
|
type="text"
|
||||||
|
label="PLG_SYSTEM_MOKOOG_FIELD_LB_NAME"
|
||||||
|
description="PLG_SYSTEM_MOKOOG_FIELD_LB_NAME_DESC"
|
||||||
|
default=""
|
||||||
|
filter="string"
|
||||||
|
/>
|
||||||
|
<field
|
||||||
|
name="lb_type"
|
||||||
|
type="list"
|
||||||
|
label="PLG_SYSTEM_MOKOOG_FIELD_LB_TYPE"
|
||||||
|
description="PLG_SYSTEM_MOKOOG_FIELD_LB_TYPE_DESC"
|
||||||
|
default="LocalBusiness"
|
||||||
|
>
|
||||||
|
<option value="LocalBusiness">LocalBusiness</option>
|
||||||
|
<option value="Restaurant">Restaurant</option>
|
||||||
|
<option value="Store">Store</option>
|
||||||
|
<option value="MedicalBusiness">MedicalBusiness</option>
|
||||||
|
<option value="LegalService">LegalService</option>
|
||||||
|
<option value="FinancialService">FinancialService</option>
|
||||||
|
<option value="EducationalOrganization">EducationalOrganization</option>
|
||||||
|
</field>
|
||||||
|
<field
|
||||||
|
name="lb_street"
|
||||||
|
type="text"
|
||||||
|
label="PLG_SYSTEM_MOKOOG_FIELD_LB_STREET"
|
||||||
|
description="PLG_SYSTEM_MOKOOG_FIELD_LB_STREET_DESC"
|
||||||
|
default=""
|
||||||
|
filter="string"
|
||||||
|
/>
|
||||||
|
<field
|
||||||
|
name="lb_city"
|
||||||
|
type="text"
|
||||||
|
label="PLG_SYSTEM_MOKOOG_FIELD_LB_CITY"
|
||||||
|
description="PLG_SYSTEM_MOKOOG_FIELD_LB_CITY_DESC"
|
||||||
|
default=""
|
||||||
|
filter="string"
|
||||||
|
/>
|
||||||
|
<field
|
||||||
|
name="lb_region"
|
||||||
|
type="text"
|
||||||
|
label="PLG_SYSTEM_MOKOOG_FIELD_LB_REGION"
|
||||||
|
description="PLG_SYSTEM_MOKOOG_FIELD_LB_REGION_DESC"
|
||||||
|
default=""
|
||||||
|
filter="string"
|
||||||
|
/>
|
||||||
|
<field
|
||||||
|
name="lb_postal"
|
||||||
|
type="text"
|
||||||
|
label="PLG_SYSTEM_MOKOOG_FIELD_LB_POSTAL"
|
||||||
|
description="PLG_SYSTEM_MOKOOG_FIELD_LB_POSTAL_DESC"
|
||||||
|
default=""
|
||||||
|
filter="string"
|
||||||
|
/>
|
||||||
|
<field
|
||||||
|
name="lb_country"
|
||||||
|
type="text"
|
||||||
|
label="PLG_SYSTEM_MOKOOG_FIELD_LB_COUNTRY"
|
||||||
|
description="PLG_SYSTEM_MOKOOG_FIELD_LB_COUNTRY_DESC"
|
||||||
|
default="US"
|
||||||
|
filter="string"
|
||||||
|
/>
|
||||||
|
<field
|
||||||
|
name="lb_phone"
|
||||||
|
type="tel"
|
||||||
|
label="PLG_SYSTEM_MOKOOG_FIELD_LB_PHONE"
|
||||||
|
description="PLG_SYSTEM_MOKOOG_FIELD_LB_PHONE_DESC"
|
||||||
|
default=""
|
||||||
|
/>
|
||||||
|
<field
|
||||||
|
name="lb_email"
|
||||||
|
type="email"
|
||||||
|
label="PLG_SYSTEM_MOKOOG_FIELD_LB_EMAIL"
|
||||||
|
description="PLG_SYSTEM_MOKOOG_FIELD_LB_EMAIL_DESC"
|
||||||
|
default=""
|
||||||
|
/>
|
||||||
|
<field
|
||||||
|
name="lb_url"
|
||||||
|
type="url"
|
||||||
|
label="PLG_SYSTEM_MOKOOG_FIELD_LB_URL"
|
||||||
|
description="PLG_SYSTEM_MOKOOG_FIELD_LB_URL_DESC"
|
||||||
|
default=""
|
||||||
|
/>
|
||||||
|
<field
|
||||||
|
name="lb_opening_hours"
|
||||||
|
type="text"
|
||||||
|
label="PLG_SYSTEM_MOKOOG_FIELD_LB_OPENING_HOURS"
|
||||||
|
description="PLG_SYSTEM_MOKOOG_FIELD_LB_OPENING_HOURS_DESC"
|
||||||
|
default=""
|
||||||
|
filter="string"
|
||||||
|
/>
|
||||||
|
<field
|
||||||
|
name="lb_latitude"
|
||||||
|
type="text"
|
||||||
|
label="PLG_SYSTEM_MOKOOG_FIELD_LB_LATITUDE"
|
||||||
|
description="PLG_SYSTEM_MOKOOG_FIELD_LB_LATITUDE_DESC"
|
||||||
|
default=""
|
||||||
|
filter="string"
|
||||||
|
/>
|
||||||
|
<field
|
||||||
|
name="lb_longitude"
|
||||||
|
type="text"
|
||||||
|
label="PLG_SYSTEM_MOKOOG_FIELD_LB_LONGITUDE"
|
||||||
|
description="PLG_SYSTEM_MOKOOG_FIELD_LB_LONGITUDE_DESC"
|
||||||
|
default=""
|
||||||
|
filter="string"
|
||||||
|
/>
|
||||||
|
<field
|
||||||
|
name="lb_price_range"
|
||||||
|
type="text"
|
||||||
|
label="PLG_SYSTEM_MOKOOG_FIELD_LB_PRICE_RANGE"
|
||||||
|
description="PLG_SYSTEM_MOKOOG_FIELD_LB_PRICE_RANGE_DESC"
|
||||||
|
default=""
|
||||||
|
filter="string"
|
||||||
|
/>
|
||||||
|
</fieldset>
|
||||||
|
<fieldset name="sitemap" label="PLG_SYSTEM_MOKOOG_FIELDSET_SITEMAP">
|
||||||
|
<field name="sitemap_enabled" type="radio" label="PLG_SYSTEM_MOKOOG_FIELD_SITEMAP_ENABLED" description="PLG_SYSTEM_MOKOOG_FIELD_SITEMAP_ENABLED_DESC" default="0" class="btn-group">
|
||||||
|
<option value="1">JYES</option>
|
||||||
|
<option value="0">JNO</option>
|
||||||
|
</field>
|
||||||
|
<field name="sitemap_changefreq" type="list" label="PLG_SYSTEM_MOKOOG_FIELD_SITEMAP_CHANGEFREQ" description="PLG_SYSTEM_MOKOOG_FIELD_SITEMAP_CHANGEFREQ_DESC" default="weekly">
|
||||||
|
<option value="daily">Daily</option>
|
||||||
|
<option value="weekly">Weekly</option>
|
||||||
|
<option value="monthly">Monthly</option>
|
||||||
|
</field>
|
||||||
|
</fieldset>
|
||||||
|
<fieldset name="ai" label="PLG_SYSTEM_MOKOOG_FIELDSET_AI">
|
||||||
|
<field name="ai_enabled" type="radio" label="PLG_SYSTEM_MOKOOG_FIELD_AI_ENABLED" description="PLG_SYSTEM_MOKOOG_FIELD_AI_ENABLED_DESC" default="0" class="btn-group">
|
||||||
|
<option value="1">JYES</option>
|
||||||
|
<option value="0">JNO</option>
|
||||||
|
</field>
|
||||||
|
<field name="ai_provider" type="list" label="PLG_SYSTEM_MOKOOG_FIELD_AI_PROVIDER" description="PLG_SYSTEM_MOKOOG_FIELD_AI_PROVIDER_DESC" default="claude">
|
||||||
|
<option value="claude">Claude (Anthropic)</option>
|
||||||
|
<option value="openai">OpenAI</option>
|
||||||
|
</field>
|
||||||
|
<field name="ai_api_key" type="password" label="PLG_SYSTEM_MOKOOG_FIELD_AI_API_KEY" description="PLG_SYSTEM_MOKOOG_FIELD_AI_API_KEY_DESC" filter="string" />
|
||||||
|
<field name="ai_model" type="text" label="PLG_SYSTEM_MOKOOG_FIELD_AI_MODEL" description="PLG_SYSTEM_MOKOOG_FIELD_AI_MODEL_DESC" default="claude-haiku-4-5-20251001" filter="string" />
|
||||||
|
</fieldset>
|
||||||
</fields>
|
</fields>
|
||||||
</config>
|
</config>
|
||||||
</extension>
|
</extension>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @package MokoJoomOpenGraph
|
* @package MokoSuiteOpenGraph
|
||||||
* @subpackage plg_system_mokoog
|
* @subpackage plg_system_mokoog
|
||||||
* @author Moko Consulting <hello@mokoconsulting.tech>
|
* @author Moko Consulting <hello@mokoconsulting.tech>
|
||||||
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ use Joomla\Event\Event;
|
|||||||
use Joomla\Event\SubscriberInterface;
|
use Joomla\Event\SubscriberInterface;
|
||||||
use Joomla\Plugin\System\MokoOG\Helper\ImageHelper;
|
use Joomla\Plugin\System\MokoOG\Helper\ImageHelper;
|
||||||
use Joomla\Plugin\System\MokoOG\Helper\JsonLdBuilder;
|
use Joomla\Plugin\System\MokoOG\Helper\JsonLdBuilder;
|
||||||
|
use Joomla\Plugin\System\MokoOG\Helper\SitemapBuilder;
|
||||||
|
|
||||||
final class MokoOG extends CMSPlugin implements SubscriberInterface
|
final class MokoOG extends CMSPlugin implements SubscriberInterface
|
||||||
{
|
{
|
||||||
@@ -37,6 +38,8 @@ final class MokoOG extends CMSPlugin implements SubscriberInterface
|
|||||||
return [
|
return [
|
||||||
'onAfterRoute' => 'onAfterRoute',
|
'onAfterRoute' => 'onAfterRoute',
|
||||||
'onBeforeCompileHead' => 'onBeforeCompileHead',
|
'onBeforeCompileHead' => 'onBeforeCompileHead',
|
||||||
|
'onContentAfterSave' => 'onContentAfterSaveRebuildSitemap',
|
||||||
|
'onAjaxMokoog' => 'onAjaxMokoog',
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -92,7 +95,7 @@ final class MokoOG extends CMSPlugin implements SubscriberInterface
|
|||||||
|
|
||||||
if ($catOg) {
|
if ($catOg) {
|
||||||
// Merge: category fills any gaps in the content-level data
|
// Merge: category fills any gaps in the content-level data
|
||||||
foreach (['og_title', 'og_description', 'og_image', 'og_type', 'seo_title', 'meta_description', 'robots', 'canonical_url'] as $field) {
|
foreach (['og_title', 'og_description', 'og_image', 'og_type', 'og_video', 'seo_title', 'meta_description', 'robots', 'canonical_url'] as $field) {
|
||||||
if (empty($ogData->$field) && !empty($catOg->$field)) {
|
if (empty($ogData->$field) && !empty($catOg->$field)) {
|
||||||
$ogData->$field = $catOg->$field;
|
$ogData->$field = $catOg->$field;
|
||||||
}
|
}
|
||||||
@@ -156,7 +159,10 @@ final class MokoOG extends CMSPlugin implements SubscriberInterface
|
|||||||
$doc->setMetaData('twitter:description', $description);
|
$doc->setMetaData('twitter:description', $description);
|
||||||
|
|
||||||
if ($image) {
|
if ($image) {
|
||||||
$doc->setMetaData('twitter:image', $this->resolveImageUrl($image));
|
$twitterImage = ($this->params->get('auto_resize', 1) && $this->params->get('platform_resize', 0))
|
||||||
|
? ImageHelper::resizeForPlatform($image, 'twitter')
|
||||||
|
: $image;
|
||||||
|
$doc->setMetaData('twitter:image', $this->resolveImageUrl($twitterImage));
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($twitterSite) {
|
if ($twitterSite) {
|
||||||
@@ -177,6 +183,38 @@ final class MokoOG extends CMSPlugin implements SubscriberInterface
|
|||||||
$doc->setMetaData('theme-color', $discordColor);
|
$doc->setMetaData('theme-color', $discordColor);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fediverse/Mastodon creator attribution
|
||||||
|
$fediverseCreator = $this->params->get('fediverse_creator', '');
|
||||||
|
|
||||||
|
if ($fediverseCreator) {
|
||||||
|
$doc->setMetaData('fediverse:creator', $fediverseCreator);
|
||||||
|
}
|
||||||
|
|
||||||
|
// og:video tags
|
||||||
|
$videoUrl = $ogData->og_video ?? '';
|
||||||
|
|
||||||
|
if ($videoUrl) {
|
||||||
|
$doc->setMetaData('og:video', $videoUrl, 'property');
|
||||||
|
|
||||||
|
if (str_starts_with($videoUrl, 'https://')) {
|
||||||
|
$doc->setMetaData('og:video:secure_url', $videoUrl, 'property');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Detect video type from URL — embeds vs direct files
|
||||||
|
$isEmbed = str_contains($videoUrl, 'youtube.com') || str_contains($videoUrl, 'youtu.be')
|
||||||
|
|| str_contains($videoUrl, 'vimeo.com');
|
||||||
|
|
||||||
|
if ($isEmbed) {
|
||||||
|
$doc->setMetaData('og:video:type', 'text/html', 'property');
|
||||||
|
} else {
|
||||||
|
$ext = strtolower(pathinfo(parse_url($videoUrl, PHP_URL_PATH) ?: '', PATHINFO_EXTENSION));
|
||||||
|
$mimeMap = ['mp4' => 'video/mp4', 'webm' => 'video/webm', 'ogg' => 'video/ogg'];
|
||||||
|
$doc->setMetaData('og:video:type', $mimeMap[$ext] ?? 'video/mp4', 'property');
|
||||||
|
$doc->setMetaData('og:video:width', '1280', 'property');
|
||||||
|
$doc->setMetaData('og:video:height', '720', 'property');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// LinkedIn article tags
|
// LinkedIn article tags
|
||||||
if ($option === 'com_content' && $view === 'article' && $id > 0) {
|
if ($option === 'com_content' && $view === 'article' && $id > 0) {
|
||||||
$doc->setMetaData('article:published_time', $this->getArticleDate($id, 'publish_up'), 'property');
|
$doc->setMetaData('article:published_time', $this->getArticleDate($id, 'publish_up'), 'property');
|
||||||
@@ -189,13 +227,34 @@ final class MokoOG extends CMSPlugin implements SubscriberInterface
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MokoSuiteShop product meta tags
|
// MokoSuiteShop product meta tags (pricing + Pinterest availability)
|
||||||
if ($option === 'com_mokoshop' && $view === 'product' && $id > 0) {
|
if ($option === 'com_mokoshop' && $view === 'product' && $id > 0) {
|
||||||
$productData = $this->loadShopProduct($id);
|
$productData = $this->loadShopProduct($id);
|
||||||
|
|
||||||
if ($productData) {
|
if ($productData) {
|
||||||
$doc->setMetaData('product:price:amount', number_format((float) $productData->price, 2, '.', ''), 'property');
|
$doc->setMetaData('product:price:amount', number_format((float) $productData->price, 2, '.', ''), 'property');
|
||||||
$doc->setMetaData('product:price:currency', $productData->currency ?: 'USD', 'property');
|
$doc->setMetaData('product:price:currency', $productData->currency ?: 'USD', 'property');
|
||||||
|
$availability = ((int) ($productData->stock_qty ?? 0) > 0) ? 'instock' : 'outofstock';
|
||||||
|
$doc->setMetaData('product:availability', $availability, 'property');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pinterest article:tag rich pins (from Joomla content tags)
|
||||||
|
if ($option === 'com_content' && $view === 'article' && $id > 0) {
|
||||||
|
$db = Factory::getContainer()->get(\Joomla\Database\DatabaseInterface::class);
|
||||||
|
$tagQuery = $db->getQuery(true)
|
||||||
|
->select($db->quoteName('t.title'))
|
||||||
|
->from($db->quoteName('#__tags', 't'))
|
||||||
|
->join('INNER', $db->quoteName('#__contentitem_tag_map', 'm')
|
||||||
|
. ' ON ' . $db->quoteName('m.tag_id') . ' = ' . $db->quoteName('t.id'))
|
||||||
|
->where($db->quoteName('m.type_alias') . ' = ' . $db->quote('com_content.article'))
|
||||||
|
->where($db->quoteName('m.content_item_id') . ' = ' . $id)
|
||||||
|
->where($db->quoteName('t.published') . ' = 1');
|
||||||
|
$db->setQuery($tagQuery);
|
||||||
|
$tags = $db->loadColumn();
|
||||||
|
|
||||||
|
foreach ($tags as $tag) {
|
||||||
|
$doc->setMetaData('article:tag', $tag, 'property');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -229,6 +288,85 @@ final class MokoOG extends CMSPlugin implements SubscriberInterface
|
|||||||
$doc->addCustomTag(JsonLdBuilder::toScriptTag($schema));
|
$doc->addCustomTag(JsonLdBuilder::toScriptTag($schema));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!empty($ogData->og_video)) {
|
||||||
|
$videoSchema = JsonLdBuilder::buildVideo($ogData->og_video, $title, $description, $imageUrl);
|
||||||
|
|
||||||
|
if ($videoSchema) {
|
||||||
|
$doc->addCustomTag(JsonLdBuilder::toScriptTag($videoSchema));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// FAQ schema (auto-detected from article headings)
|
||||||
|
if ($this->params->get('jsonld_faq', 1) && $option === 'com_content' && $view === 'article' && $id > 0) {
|
||||||
|
$faqItems = $this->extractFaqFromContent($id);
|
||||||
|
|
||||||
|
if (!empty($faqItems)) {
|
||||||
|
$faqSchema = JsonLdBuilder::buildFaq($faqItems);
|
||||||
|
|
||||||
|
if ($faqSchema) {
|
||||||
|
$doc->addCustomTag(JsonLdBuilder::toScriptTag($faqSchema));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// HowTo schema (auto-detected from ordered lists)
|
||||||
|
if ($this->params->get('jsonld_howto', 1) && $option === 'com_content' && $view === 'article' && $id > 0) {
|
||||||
|
$howToSteps = $this->extractHowToFromContent($id);
|
||||||
|
|
||||||
|
if (!empty($howToSteps)) {
|
||||||
|
$howToSchema = JsonLdBuilder::buildHowTo($title, $howToSteps, $imageUrl);
|
||||||
|
|
||||||
|
if ($howToSchema) {
|
||||||
|
$doc->addCustomTag(JsonLdBuilder::toScriptTag($howToSchema));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Event JSON-LD from per-article event data
|
||||||
|
$eventJson = $ogData->event_data ?? '';
|
||||||
|
|
||||||
|
if (!empty($eventJson)) {
|
||||||
|
$eventObj = json_decode($eventJson);
|
||||||
|
|
||||||
|
if ($eventObj && !empty($eventObj->event_start)) {
|
||||||
|
$eventSchema = JsonLdBuilder::buildEvent($title, $description, $imageUrl, $eventObj);
|
||||||
|
|
||||||
|
if ($eventSchema) {
|
||||||
|
$doc->addCustomTag(JsonLdBuilder::toScriptTag($eventSchema));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recipe JSON-LD from per-article recipe data
|
||||||
|
$recipeJson = $ogData->recipe_data ?? '';
|
||||||
|
|
||||||
|
if (!empty($recipeJson)) {
|
||||||
|
$recipeObj = json_decode($recipeJson);
|
||||||
|
|
||||||
|
if ($recipeObj) {
|
||||||
|
$recipeSchema = JsonLdBuilder::buildRecipe($title, $description, $imageUrl, $recipeObj);
|
||||||
|
|
||||||
|
if ($recipeSchema) {
|
||||||
|
$doc->addCustomTag(JsonLdBuilder::toScriptTag($recipeSchema));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Custom JSON-LD schema (user-provided)
|
||||||
|
$customSchema = $ogData->custom_schema ?? '';
|
||||||
|
|
||||||
|
if (!empty($customSchema)) {
|
||||||
|
$decoded = json_decode($customSchema, true);
|
||||||
|
|
||||||
|
if ($decoded) {
|
||||||
|
if (empty($decoded['@context'])) {
|
||||||
|
$decoded['@context'] = 'https://schema.org';
|
||||||
|
}
|
||||||
|
|
||||||
|
$doc->addCustomTag(JsonLdBuilder::toScriptTag($decoded));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if ($this->params->get('jsonld_breadcrumbs', 1)) {
|
if ($this->params->get('jsonld_breadcrumbs', 1)) {
|
||||||
$breadcrumbs = JsonLdBuilder::buildBreadcrumbs();
|
$breadcrumbs = JsonLdBuilder::buildBreadcrumbs();
|
||||||
|
|
||||||
@@ -237,6 +375,15 @@ final class MokoOG extends CMSPlugin implements SubscriberInterface
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// LocalBusiness JSON-LD
|
||||||
|
if ($this->params->get('lb_enabled', 0)) {
|
||||||
|
$lbSchema = JsonLdBuilder::buildLocalBusiness($this->params);
|
||||||
|
|
||||||
|
if ($lbSchema) {
|
||||||
|
$doc->addCustomTag(JsonLdBuilder::toScriptTag($lbSchema));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -299,6 +446,10 @@ final class MokoOG extends CMSPlugin implements SubscriberInterface
|
|||||||
'og_description' => '',
|
'og_description' => '',
|
||||||
'og_image' => '',
|
'og_image' => '',
|
||||||
'og_type' => '',
|
'og_type' => '',
|
||||||
|
'og_video' => '',
|
||||||
|
'event_data' => '',
|
||||||
|
'recipe_data' => '',
|
||||||
|
'custom_schema' => '',
|
||||||
'seo_title' => '',
|
'seo_title' => '',
|
||||||
'meta_description' => '',
|
'meta_description' => '',
|
||||||
'robots' => '',
|
'robots' => '',
|
||||||
@@ -316,7 +467,7 @@ final class MokoOG extends CMSPlugin implements SubscriberInterface
|
|||||||
return $this->loadOgDataByMenu((int) $menuItem->id) ?: $empty;
|
return $this->loadOgDataByMenu((int) $menuItem->id) ?: $empty;
|
||||||
}
|
}
|
||||||
|
|
||||||
$db = Factory::getDbo();
|
$db = Factory::getContainer()->get(\Joomla\Database\DatabaseInterface::class);
|
||||||
$query = $db->getQuery(true)
|
$query = $db->getQuery(true)
|
||||||
->select('*')
|
->select('*')
|
||||||
->from($db->quoteName('#__mokoog_tags'))
|
->from($db->quoteName('#__mokoog_tags'))
|
||||||
@@ -342,7 +493,7 @@ final class MokoOG extends CMSPlugin implements SubscriberInterface
|
|||||||
*/
|
*/
|
||||||
private function loadOgDataByType(string $contentType, int $contentId): ?object
|
private function loadOgDataByType(string $contentType, int $contentId): ?object
|
||||||
{
|
{
|
||||||
$db = Factory::getDbo();
|
$db = Factory::getContainer()->get(\Joomla\Database\DatabaseInterface::class);
|
||||||
$lang = Factory::getLanguage()->getTag();
|
$lang = Factory::getLanguage()->getTag();
|
||||||
|
|
||||||
$query = $db->getQuery(true)
|
$query = $db->getQuery(true)
|
||||||
@@ -369,7 +520,7 @@ final class MokoOG extends CMSPlugin implements SubscriberInterface
|
|||||||
*/
|
*/
|
||||||
private function loadOgDataByMenu(int $menuId): ?object
|
private function loadOgDataByMenu(int $menuId): ?object
|
||||||
{
|
{
|
||||||
$db = Factory::getDbo();
|
$db = Factory::getContainer()->get(\Joomla\Database\DatabaseInterface::class);
|
||||||
$lang = Factory::getLanguage()->getTag();
|
$lang = Factory::getLanguage()->getTag();
|
||||||
|
|
||||||
$query = $db->getQuery(true)
|
$query = $db->getQuery(true)
|
||||||
@@ -464,7 +615,7 @@ final class MokoOG extends CMSPlugin implements SubscriberInterface
|
|||||||
|
|
||||||
// Fallback: check the article's category for an image
|
// Fallback: check the article's category for an image
|
||||||
if ($view === 'article') {
|
if ($view === 'article') {
|
||||||
$db = Factory::getDbo();
|
$db = Factory::getContainer()->get(\Joomla\Database\DatabaseInterface::class);
|
||||||
$catQuery = $db->getQuery(true)
|
$catQuery = $db->getQuery(true)
|
||||||
->select($db->quoteName('cat.params'))
|
->select($db->quoteName('cat.params'))
|
||||||
->from($db->quoteName('#__categories', 'cat'))
|
->from($db->quoteName('#__categories', 'cat'))
|
||||||
@@ -523,7 +674,7 @@ final class MokoOG extends CMSPlugin implements SubscriberInterface
|
|||||||
return $cache[$id];
|
return $cache[$id];
|
||||||
}
|
}
|
||||||
|
|
||||||
$db = Factory::getDbo();
|
$db = Factory::getContainer()->get(\Joomla\Database\DatabaseInterface::class);
|
||||||
$query = $db->getQuery(true)
|
$query = $db->getQuery(true)
|
||||||
->select($db->quoteName([
|
->select($db->quoteName([
|
||||||
'a.title', 'a.introtext', 'a.fulltext', 'a.images',
|
'a.title', 'a.introtext', 'a.fulltext', 'a.images',
|
||||||
@@ -569,6 +720,226 @@ final class MokoOG extends CMSPlugin implements SubscriberInterface
|
|||||||
return $article->author_name ?? '';
|
return $article->author_name ?? '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract FAQ question/answer pairs from article content.
|
||||||
|
*
|
||||||
|
* @param int $articleId Article ID
|
||||||
|
*
|
||||||
|
* @return array Array of ['question' => '...', 'answer' => '...'] pairs
|
||||||
|
*/
|
||||||
|
private function extractFaqFromContent(int $articleId): array
|
||||||
|
{
|
||||||
|
$article = $this->loadArticle($articleId);
|
||||||
|
|
||||||
|
if (!$article) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$content = ($article->introtext ?? '') . ($article->fulltext ?? '');
|
||||||
|
|
||||||
|
if (trim($content) === '') {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$faqItems = [];
|
||||||
|
|
||||||
|
if (preg_match_all('/<h[34][^>]*>(.*?)<\/h[34]>\s*((?:<p[^>]*>.*?<\/p>\s*)+)/si', $content, $matches, PREG_SET_ORDER)) {
|
||||||
|
foreach ($matches as $match) {
|
||||||
|
$question = trim(strip_tags($match[1]));
|
||||||
|
$answer = trim(strip_tags($match[2]));
|
||||||
|
|
||||||
|
if ($question !== '' && $answer !== '') {
|
||||||
|
$faqItems[] = [
|
||||||
|
'question' => $question,
|
||||||
|
'answer' => $answer,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $faqItems;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract HowTo steps from ordered lists in article content.
|
||||||
|
*
|
||||||
|
* @param int $articleId Article ID
|
||||||
|
*
|
||||||
|
* @return array Array of ['name' => '...', 'text' => '...'] pairs
|
||||||
|
*/
|
||||||
|
private function extractHowToFromContent(int $articleId): array
|
||||||
|
{
|
||||||
|
$article = $this->loadArticle($articleId);
|
||||||
|
|
||||||
|
if (!$article) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$content = ($article->introtext ?? '') . ($article->fulltext ?? '');
|
||||||
|
|
||||||
|
if (!preg_match('/<ol[^>]*>(.*?)<\/ol>/si', $content, $olMatch)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!preg_match_all('/<li[^>]*>(.*?)<\/li>/si', $olMatch[1], $liMatches)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$steps = [];
|
||||||
|
|
||||||
|
foreach ($liMatches[1] as $liHtml) {
|
||||||
|
$text = trim(strip_tags($liHtml));
|
||||||
|
|
||||||
|
if ($text === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$name = $text;
|
||||||
|
|
||||||
|
if (preg_match('/<(?:b|strong)[^>]*>(.*?)<\/(?:b|strong)>/si', $liHtml, $boldMatch)) {
|
||||||
|
$name = trim(strip_tags($boldMatch[1]));
|
||||||
|
} elseif (preg_match('/^([^.!?]+[.!?])/', $text, $sentenceMatch)) {
|
||||||
|
$name = trim($sentenceMatch[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$steps[] = [
|
||||||
|
'name' => $name,
|
||||||
|
'text' => $text,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $steps;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rebuild sitemap.xml when article content is saved.
|
||||||
|
*
|
||||||
|
* @param Event $event The event
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function onContentAfterSaveRebuildSitemap(Event $event): void
|
||||||
|
{
|
||||||
|
if (!$this->params->get('sitemap_enabled', 0)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
[$context] = array_values($event->getArguments());
|
||||||
|
|
||||||
|
if ($context !== 'com_content.article') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$changefreq = $this->params->get('sitemap_changefreq', 'weekly');
|
||||||
|
$xml = SitemapBuilder::generate($changefreq);
|
||||||
|
|
||||||
|
if (!SitemapBuilder::writeToFile($xml)) {
|
||||||
|
\Joomla\CMS\Log\Log::add('MokoOG: Failed to write sitemap.xml — check file permissions', \Joomla\CMS\Log\Log::WARNING, 'mokoog');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle AJAX requests for AI meta tag generation.
|
||||||
|
*
|
||||||
|
* @param Event $event The event
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function onAjaxMokoog(Event $event): void
|
||||||
|
{
|
||||||
|
$app = $this->getApplication();
|
||||||
|
|
||||||
|
if (!$app->isClient('administrator')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!\Joomla\CMS\Session\Session::checkToken()) {
|
||||||
|
$event->setArgument('result', ['Invalid Token']);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$this->params->get('ai_enabled', 0)) {
|
||||||
|
$event->setArgument('result', ['AI generation is not enabled']);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$apiKey = $this->params->get('ai_api_key', '');
|
||||||
|
$provider = $this->params->get('ai_provider', 'claude');
|
||||||
|
$model = $this->params->get('ai_model', 'claude-haiku-4-5-20251001');
|
||||||
|
|
||||||
|
if (empty($apiKey)) {
|
||||||
|
$event->setArgument('result', ['API key not configured']);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$input = $app->getInput();
|
||||||
|
$field = $input->getString('field', 'title');
|
||||||
|
$articleTitle = mb_substr(strip_tags($input->getString('article_title', '')), 0, 200);
|
||||||
|
|
||||||
|
$prompt = $field === 'title'
|
||||||
|
? "Generate a concise, engaging social media sharing title (max 60 characters) for an article titled: \"$articleTitle\". Return only the title text, no quotes or explanation."
|
||||||
|
: "Generate a compelling social media sharing description (max 155 characters) for an article titled: \"$articleTitle\". Return only the description text, no quotes or explanation.";
|
||||||
|
|
||||||
|
try {
|
||||||
|
$result = $this->callAiApi($provider, $apiKey, $model, $prompt);
|
||||||
|
$event->setArgument('result', [$result]);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$event->setArgument('result', ['Error: ' . $e->getMessage()]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Call an AI API (Claude or OpenAI) with a prompt.
|
||||||
|
*
|
||||||
|
* @param string $provider Provider name (claude or openai)
|
||||||
|
* @param string $apiKey API key
|
||||||
|
* @param string $model Model name
|
||||||
|
* @param string $prompt Prompt text
|
||||||
|
*
|
||||||
|
* @return string Generated text
|
||||||
|
*/
|
||||||
|
private function callAiApi(string $provider, string $apiKey, string $model, string $prompt): string
|
||||||
|
{
|
||||||
|
$http = \Joomla\CMS\Http\HttpFactory::getHttp();
|
||||||
|
|
||||||
|
if ($provider === 'claude') {
|
||||||
|
$response = $http->post(
|
||||||
|
'https://api.anthropic.com/v1/messages',
|
||||||
|
json_encode([
|
||||||
|
'model' => $model,
|
||||||
|
'max_tokens' => 200,
|
||||||
|
'messages' => [['role' => 'user', 'content' => $prompt]],
|
||||||
|
]),
|
||||||
|
[
|
||||||
|
'Content-Type' => 'application/json',
|
||||||
|
'x-api-key' => $apiKey,
|
||||||
|
'anthropic-version' => '2023-06-01',
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
$data = json_decode($response->body, true);
|
||||||
|
|
||||||
|
return trim($data['content'][0]['text'] ?? '');
|
||||||
|
}
|
||||||
|
|
||||||
|
$response = $http->post(
|
||||||
|
'https://api.openai.com/v1/chat/completions',
|
||||||
|
json_encode([
|
||||||
|
'model' => $model,
|
||||||
|
'max_tokens' => 200,
|
||||||
|
'messages' => [['role' => 'user', 'content' => $prompt]],
|
||||||
|
]),
|
||||||
|
[
|
||||||
|
'Content-Type' => 'application/json',
|
||||||
|
'Authorization' => 'Bearer ' . $apiKey,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
$data = json_decode($response->body, true);
|
||||||
|
|
||||||
|
return trim($data['choices'][0]['message']['content'] ?? '');
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Warn administrators once per session when no license key is configured.
|
* Warn administrators once per session when no license key is configured.
|
||||||
*
|
*
|
||||||
@@ -576,20 +947,20 @@ final class MokoOG extends CMSPlugin implements SubscriberInterface
|
|||||||
*/
|
*/
|
||||||
private function warnMissingLicenseKey(): void
|
private function warnMissingLicenseKey(): void
|
||||||
{
|
{
|
||||||
$session = Factory::getSession();
|
$session = Factory::getApplication()->getSession();
|
||||||
|
|
||||||
if ($session->get('mokoog.license_warned', false)) {
|
if ($session->get('mokoog.license_warned', false)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$user = Factory::getUser();
|
$user = Factory::getApplication()->getIdentity();
|
||||||
|
|
||||||
if ($user->guest || !$user->authorise('core.manage')) {
|
if ($user->guest || !$user->authorise('core.manage')) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$db = Factory::getDbo();
|
$db = Factory::getContainer()->get(\Joomla\Database\DatabaseInterface::class);
|
||||||
|
|
||||||
$query = $db->getQuery(true)
|
$query = $db->getQuery(true)
|
||||||
->select($db->quoteName('extra_query'))
|
->select($db->quoteName('extra_query'))
|
||||||
@@ -640,7 +1011,7 @@ final class MokoOG extends CMSPlugin implements SubscriberInterface
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$db = Factory::getDbo();
|
$db = Factory::getContainer()->get(\Joomla\Database\DatabaseInterface::class);
|
||||||
$query = $db->getQuery(true)
|
$query = $db->getQuery(true)
|
||||||
->select('p.id, p.sku, p.price, p.currency, p.stock_qty')
|
->select('p.id, p.sku, p.price, p.currency, p.stock_qty')
|
||||||
->select('c.title AS name, c.introtext AS description, c.images')
|
->select('c.title AS name, c.introtext AS description, c.images')
|
||||||
|
|||||||
@@ -149,6 +149,137 @@ class ImageHelper
|
|||||||
return $outputRel;
|
return $outputRel;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resize an image for a specific platform.
|
||||||
|
*
|
||||||
|
* @param string $imagePath Relative image path
|
||||||
|
* @param string $platform Platform name (facebook, twitter, pinterest, whatsapp)
|
||||||
|
*
|
||||||
|
* @return string Path to the resized image
|
||||||
|
*/
|
||||||
|
public static function resizeForPlatform(string $imagePath, string $platform): string
|
||||||
|
{
|
||||||
|
$sizes = [
|
||||||
|
'facebook' => ['width' => 1200, 'height' => 630],
|
||||||
|
'twitter' => ['width' => 1200, 'height' => 600],
|
||||||
|
'pinterest' => ['width' => 1000, 'height' => 1500],
|
||||||
|
'whatsapp' => ['width' => 400, 'height' => 400],
|
||||||
|
];
|
||||||
|
|
||||||
|
if (!isset($sizes[$platform])) {
|
||||||
|
return self::resize($imagePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
$size = $sizes[$platform];
|
||||||
|
|
||||||
|
return self::resizeToSize($imagePath, $size['width'], $size['height'], $platform);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resize an image to specific dimensions with a platform-specific subdirectory.
|
||||||
|
*
|
||||||
|
* @param string $imagePath Image path relative to JPATH_ROOT
|
||||||
|
* @param int $width Target width
|
||||||
|
* @param int $height Target height
|
||||||
|
* @param string $subdir Subdirectory name for output (e.g. platform name)
|
||||||
|
*
|
||||||
|
* @return string Path to the output image (relative to JPATH_ROOT)
|
||||||
|
*/
|
||||||
|
private static function resizeToSize(string $imagePath, int $width, int $height, string $subdir = ''): 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 <= $width && $origHeight <= $height) {
|
||||||
|
return $imagePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build output directory with optional subdirectory
|
||||||
|
$outputRelDir = self::OUTPUT_DIR . ($subdir ? '/' . $subdir : '');
|
||||||
|
$outputDir = JPATH_ROOT . '/' . $outputRelDir;
|
||||||
|
|
||||||
|
if (!is_dir($outputDir) && !Folder::create($outputDir)) {
|
||||||
|
Log::add('MokoOG ImageHelper: Cannot create output directory: ' . $outputRelDir, Log::WARNING, 'mokoog');
|
||||||
|
|
||||||
|
return $imagePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate output filename based on source hash + dimensions
|
||||||
|
$hash = md5($imagePath . $width . $height);
|
||||||
|
$outputName = $hash . '.jpg';
|
||||||
|
$outputPath = $outputDir . '/' . $outputName;
|
||||||
|
$outputRel = $outputRelDir . '/' . $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 = $width / $height;
|
||||||
|
$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($width, $height);
|
||||||
|
|
||||||
|
imagecopyresampled(
|
||||||
|
$output,
|
||||||
|
$source,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
$cropX,
|
||||||
|
$cropY,
|
||||||
|
$width,
|
||||||
|
$height,
|
||||||
|
$cropWidth,
|
||||||
|
$cropHeight
|
||||||
|
);
|
||||||
|
|
||||||
|
// Save as JPEG
|
||||||
|
imagejpeg($output, $outputPath, self::JPEG_QUALITY);
|
||||||
|
|
||||||
|
imagedestroy($source);
|
||||||
|
imagedestroy($output);
|
||||||
|
|
||||||
|
return $outputRel;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Remove a generated image file.
|
* Remove a generated image file.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ class JsonLdBuilder
|
|||||||
$article = $cachedArticle;
|
$article = $cachedArticle;
|
||||||
|
|
||||||
if (!$article) {
|
if (!$article) {
|
||||||
$db = Factory::getDbo();
|
$db = Factory::getContainer()->get(\Joomla\Database\DatabaseInterface::class);
|
||||||
$query = $db->getQuery(true)
|
$query = $db->getQuery(true)
|
||||||
->select($db->quoteName([
|
->select($db->quoteName([
|
||||||
'a.created', 'a.modified', 'a.publish_up',
|
'a.created', 'a.modified', 'a.publish_up',
|
||||||
@@ -179,7 +179,7 @@ class JsonLdBuilder
|
|||||||
$product = $cachedProduct;
|
$product = $cachedProduct;
|
||||||
|
|
||||||
if (!$product) {
|
if (!$product) {
|
||||||
$db = Factory::getDbo();
|
$db = Factory::getContainer()->get(\Joomla\Database\DatabaseInterface::class);
|
||||||
$query = $db->getQuery(true)
|
$query = $db->getQuery(true)
|
||||||
->select('p.sku, p.price, p.currency, p.stock_qty')
|
->select('p.sku, p.price, p.currency, p.stock_qty')
|
||||||
->from($db->quoteName('#__mokosuite_crm_products', 'p'))
|
->from($db->quoteName('#__mokosuite_crm_products', 'p'))
|
||||||
@@ -248,6 +248,413 @@ class JsonLdBuilder
|
|||||||
return $schema;
|
return $schema;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build VideoObject schema for pages with a video URL.
|
||||||
|
*
|
||||||
|
* @param string $videoUrl Video URL (e.g. YouTube, Vimeo, or direct)
|
||||||
|
* @param string $title Video title
|
||||||
|
* @param string $description Video description
|
||||||
|
* @param string $imageUrl Thumbnail image URL (absolute)
|
||||||
|
*
|
||||||
|
* @return array|null
|
||||||
|
*/
|
||||||
|
public static function buildVideo(string $videoUrl, string $title, string $description, string $imageUrl): ?array
|
||||||
|
{
|
||||||
|
if (empty($videoUrl)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$schema = [
|
||||||
|
'@context' => 'https://schema.org',
|
||||||
|
'@type' => 'VideoObject',
|
||||||
|
'name' => $title,
|
||||||
|
'description' => $description,
|
||||||
|
'thumbnailUrl' => $imageUrl,
|
||||||
|
'contentUrl' => $videoUrl,
|
||||||
|
'uploadDate' => Factory::getDate()->toISO8601(),
|
||||||
|
];
|
||||||
|
|
||||||
|
// Add embedUrl for YouTube and Vimeo
|
||||||
|
if (preg_match('/youtube\.com|youtu\.be|vimeo\.com/i', $videoUrl)) {
|
||||||
|
$schema['embedUrl'] = $videoUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $schema;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build LocalBusiness schema from plugin parameters.
|
||||||
|
*
|
||||||
|
* @param object $params Plugin parameters object
|
||||||
|
*
|
||||||
|
* @return array|null
|
||||||
|
*/
|
||||||
|
public static function buildLocalBusiness(object $params): ?array
|
||||||
|
{
|
||||||
|
$name = trim((string) $params->get('lb_name', ''));
|
||||||
|
|
||||||
|
if ($name === '') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$schema = [
|
||||||
|
'@context' => 'https://schema.org',
|
||||||
|
'@type' => $params->get('lb_type', 'LocalBusiness'),
|
||||||
|
'name' => $name,
|
||||||
|
];
|
||||||
|
|
||||||
|
// Build PostalAddress
|
||||||
|
$address = [];
|
||||||
|
$street = trim((string) $params->get('lb_street', ''));
|
||||||
|
$city = trim((string) $params->get('lb_city', ''));
|
||||||
|
$region = trim((string) $params->get('lb_region', ''));
|
||||||
|
$postal = trim((string) $params->get('lb_postal', ''));
|
||||||
|
$country = trim((string) $params->get('lb_country', ''));
|
||||||
|
|
||||||
|
if ($street !== '') {
|
||||||
|
$address['streetAddress'] = $street;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($city !== '') {
|
||||||
|
$address['addressLocality'] = $city;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($region !== '') {
|
||||||
|
$address['addressRegion'] = $region;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($postal !== '') {
|
||||||
|
$address['postalCode'] = $postal;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($country !== '') {
|
||||||
|
$address['addressCountry'] = $country;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($address)) {
|
||||||
|
$address['@type'] = 'PostalAddress';
|
||||||
|
$schema['address'] = $address;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Contact properties
|
||||||
|
$phone = trim((string) $params->get('lb_phone', ''));
|
||||||
|
$email = trim((string) $params->get('lb_email', ''));
|
||||||
|
$url = trim((string) $params->get('lb_url', ''));
|
||||||
|
|
||||||
|
if ($phone !== '') {
|
||||||
|
$schema['telephone'] = $phone;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($email !== '') {
|
||||||
|
$schema['email'] = $email;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($url !== '') {
|
||||||
|
$schema['url'] = $url;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Opening hours
|
||||||
|
$openingHours = trim((string) $params->get('lb_opening_hours', ''));
|
||||||
|
|
||||||
|
if ($openingHours !== '') {
|
||||||
|
$schema['openingHours'] = $openingHours;
|
||||||
|
}
|
||||||
|
|
||||||
|
// GeoCoordinates
|
||||||
|
$latitude = trim((string) $params->get('lb_latitude', ''));
|
||||||
|
$longitude = trim((string) $params->get('lb_longitude', ''));
|
||||||
|
|
||||||
|
if ($latitude !== '' && $longitude !== '') {
|
||||||
|
$schema['geo'] = [
|
||||||
|
'@type' => 'GeoCoordinates',
|
||||||
|
'latitude' => $latitude,
|
||||||
|
'longitude' => $longitude,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Price range
|
||||||
|
$priceRange = trim((string) $params->get('lb_price_range', ''));
|
||||||
|
|
||||||
|
if ($priceRange !== '') {
|
||||||
|
$schema['priceRange'] = $priceRange;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $schema;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build FAQPage schema from question/answer pairs.
|
||||||
|
*
|
||||||
|
* @param array $questions Array of ['question' => '...', 'answer' => '...'] pairs
|
||||||
|
*
|
||||||
|
* @return array|null
|
||||||
|
*/
|
||||||
|
public static function buildFaq(array $questions): ?array
|
||||||
|
{
|
||||||
|
if (empty($questions)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$mainEntity = [];
|
||||||
|
|
||||||
|
foreach ($questions as $item) {
|
||||||
|
$question = trim($item['question'] ?? '');
|
||||||
|
$answer = trim($item['answer'] ?? '');
|
||||||
|
|
||||||
|
if ($question === '' || $answer === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$mainEntity[] = [
|
||||||
|
'@type' => 'Question',
|
||||||
|
'name' => $question,
|
||||||
|
'acceptedAnswer' => [
|
||||||
|
'@type' => 'Answer',
|
||||||
|
'text' => $answer,
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($mainEntity)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'@context' => 'https://schema.org',
|
||||||
|
'@type' => 'FAQPage',
|
||||||
|
'mainEntity' => $mainEntity,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build HowTo schema from step-by-step instructions.
|
||||||
|
*
|
||||||
|
* @param string $title HowTo title
|
||||||
|
* @param array $steps Array of ['name' => 'Step title', 'text' => 'Step instructions']
|
||||||
|
* @param string $imageUrl Optional image URL (absolute)
|
||||||
|
*
|
||||||
|
* @return array|null
|
||||||
|
*/
|
||||||
|
public static function buildHowTo(string $title, array $steps, string $imageUrl = ''): ?array
|
||||||
|
{
|
||||||
|
if (empty($steps)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$schema = [
|
||||||
|
'@context' => 'https://schema.org',
|
||||||
|
'@type' => 'HowTo',
|
||||||
|
'name' => $title,
|
||||||
|
];
|
||||||
|
|
||||||
|
if (!empty($imageUrl)) {
|
||||||
|
$schema['image'] = $imageUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
$schema['step'] = [];
|
||||||
|
|
||||||
|
foreach ($steps as $step) {
|
||||||
|
$schema['step'][] = [
|
||||||
|
'@type' => 'HowToStep',
|
||||||
|
'name' => $step['name'],
|
||||||
|
'text' => $step['text'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $schema;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build Event schema from per-article event data.
|
||||||
|
*
|
||||||
|
* @param string $title Event/article title
|
||||||
|
* @param string $description Event description
|
||||||
|
* @param string $imageUrl Image URL (absolute)
|
||||||
|
* @param object $eventData Decoded event_data with event_start, event_end, etc.
|
||||||
|
*
|
||||||
|
* @return array|null
|
||||||
|
*/
|
||||||
|
public static function buildEvent(string $title, string $description, string $imageUrl, object $eventData): ?array
|
||||||
|
{
|
||||||
|
$startDate = $eventData->event_start ?? '';
|
||||||
|
|
||||||
|
if (empty($startDate)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$schema = [
|
||||||
|
'@context' => 'https://schema.org',
|
||||||
|
'@type' => 'Event',
|
||||||
|
'name' => $title,
|
||||||
|
'description' => $description,
|
||||||
|
'startDate' => $startDate,
|
||||||
|
'url' => Uri::getInstance()->toString(),
|
||||||
|
];
|
||||||
|
|
||||||
|
$endDate = $eventData->event_end ?? '';
|
||||||
|
|
||||||
|
if (!empty($endDate)) {
|
||||||
|
$schema['endDate'] = $endDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($imageUrl)) {
|
||||||
|
$schema['image'] = $imageUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
$locationName = $eventData->event_location ?? '';
|
||||||
|
$address = $eventData->event_address ?? '';
|
||||||
|
|
||||||
|
if (!empty($locationName) || !empty($address)) {
|
||||||
|
$location = ['@type' => 'Place'];
|
||||||
|
|
||||||
|
if (!empty($locationName)) {
|
||||||
|
$location['name'] = $locationName;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($address)) {
|
||||||
|
$location['address'] = [
|
||||||
|
'@type' => 'PostalAddress',
|
||||||
|
'streetAddress' => $address,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$schema['location'] = $location;
|
||||||
|
}
|
||||||
|
|
||||||
|
$price = $eventData->event_price ?? '';
|
||||||
|
$currency = $eventData->event_currency ?? 'USD';
|
||||||
|
$ticketUrl = $eventData->event_url ?? '';
|
||||||
|
|
||||||
|
if ($price !== '') {
|
||||||
|
$offer = [
|
||||||
|
'@type' => 'Offer',
|
||||||
|
'price' => number_format((float) $price, 2, '.', ''),
|
||||||
|
'priceCurrency' => $currency ?: 'USD',
|
||||||
|
'availability' => 'https://schema.org/InStock',
|
||||||
|
];
|
||||||
|
|
||||||
|
if (!empty($ticketUrl)) {
|
||||||
|
$offer['url'] = $ticketUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
$schema['offers'] = $offer;
|
||||||
|
} elseif (!empty($ticketUrl)) {
|
||||||
|
$schema['offers'] = [
|
||||||
|
'@type' => 'Offer',
|
||||||
|
'price' => '0.00',
|
||||||
|
'priceCurrency' => $currency ?: 'USD',
|
||||||
|
'availability' => 'https://schema.org/InStock',
|
||||||
|
'url' => $ticketUrl,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $schema;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build Recipe schema from per-article recipe data.
|
||||||
|
*
|
||||||
|
* @param string $title Recipe/article title
|
||||||
|
* @param string $description Recipe/article description
|
||||||
|
* @param string $imageUrl Image URL (absolute)
|
||||||
|
* @param object $recipeData Decoded recipe_data object
|
||||||
|
*
|
||||||
|
* @return array|null
|
||||||
|
*/
|
||||||
|
public static function buildRecipe(string $title, string $description, string $imageUrl, object $recipeData): ?array
|
||||||
|
{
|
||||||
|
$fields = ['recipe_prep_time', 'recipe_cook_time', 'recipe_yield', 'recipe_calories', 'recipe_ingredients', 'recipe_category', 'recipe_cuisine'];
|
||||||
|
$hasData = false;
|
||||||
|
|
||||||
|
foreach ($fields as $field) {
|
||||||
|
if (!empty($recipeData->$field)) {
|
||||||
|
$hasData = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$hasData) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$schema = [
|
||||||
|
'@context' => 'https://schema.org',
|
||||||
|
'@type' => 'Recipe',
|
||||||
|
'name' => $title,
|
||||||
|
'description' => $description,
|
||||||
|
'url' => Uri::getInstance()->toString(),
|
||||||
|
];
|
||||||
|
|
||||||
|
if (!empty($imageUrl)) {
|
||||||
|
$schema['image'] = $imageUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($recipeData->recipe_prep_time)) {
|
||||||
|
$schema['prepTime'] = $recipeData->recipe_prep_time;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($recipeData->recipe_cook_time)) {
|
||||||
|
$schema['cookTime'] = $recipeData->recipe_cook_time;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($recipeData->recipe_prep_time) && !empty($recipeData->recipe_cook_time)) {
|
||||||
|
try {
|
||||||
|
$prep = new \DateInterval($recipeData->recipe_prep_time);
|
||||||
|
$cook = new \DateInterval($recipeData->recipe_cook_time);
|
||||||
|
$totalMinutes = ($prep->h * 60 + $prep->i) + ($cook->h * 60 + $cook->i);
|
||||||
|
$hours = intdiv($totalMinutes, 60);
|
||||||
|
$minutes = $totalMinutes % 60;
|
||||||
|
$totalTime = 'PT';
|
||||||
|
|
||||||
|
if ($hours > 0) {
|
||||||
|
$totalTime .= $hours . 'H';
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($minutes > 0) {
|
||||||
|
$totalTime .= $minutes . 'M';
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($totalTime !== 'PT') {
|
||||||
|
$schema['totalTime'] = $totalTime;
|
||||||
|
}
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
// Invalid duration format
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($recipeData->recipe_yield)) {
|
||||||
|
$schema['recipeYield'] = $recipeData->recipe_yield;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($recipeData->recipe_calories)) {
|
||||||
|
$schema['nutrition'] = [
|
||||||
|
'@type' => 'NutritionInformation',
|
||||||
|
'calories' => $recipeData->recipe_calories . ' calories',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($recipeData->recipe_ingredients)) {
|
||||||
|
$ingredients = array_filter(
|
||||||
|
array_map('trim', preg_split('/\r\n|\r|\n/', $recipeData->recipe_ingredients)),
|
||||||
|
fn($line) => $line !== ''
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!empty($ingredients)) {
|
||||||
|
$schema['recipeIngredient'] = array_values($ingredients);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($recipeData->recipe_category)) {
|
||||||
|
$schema['recipeCategory'] = $recipeData->recipe_category;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($recipeData->recipe_cuisine)) {
|
||||||
|
$schema['recipeCuisine'] = $recipeData->recipe_cuisine;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $schema;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Encode a schema array to a JSON-LD script tag string.
|
* Encode a schema array to a JSON-LD script tag string.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -0,0 +1,110 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @package MokoSuiteOpenGraph
|
||||||
|
* @subpackage plg_system_mokoog
|
||||||
|
* @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 Joomla\Plugin\System\MokoOG\Helper;
|
||||||
|
|
||||||
|
defined('_JEXEC') or die;
|
||||||
|
|
||||||
|
use Joomla\CMS\Factory;
|
||||||
|
use Joomla\CMS\Uri\Uri;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* XML Sitemap builder.
|
||||||
|
*
|
||||||
|
* Generates a sitemap.xml containing all published articles, excluding
|
||||||
|
* those marked with noindex robots directives in the mokoog_tags table.
|
||||||
|
*/
|
||||||
|
class SitemapBuilder
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Generate sitemap XML content.
|
||||||
|
*
|
||||||
|
* @param string $changefreq Default change frequency for entries
|
||||||
|
*
|
||||||
|
* @return string Complete sitemap XML
|
||||||
|
*/
|
||||||
|
public static function generate(string $changefreq = 'weekly'): string
|
||||||
|
{
|
||||||
|
$allowed = ['always', 'hourly', 'daily', 'weekly', 'monthly', 'yearly', 'never'];
|
||||||
|
$changefreq = \in_array($changefreq, $allowed, true) ? $changefreq : 'weekly';
|
||||||
|
|
||||||
|
$db = Factory::getContainer()->get(\Joomla\Database\DatabaseInterface::class);
|
||||||
|
|
||||||
|
// Get all published articles
|
||||||
|
$query = $db->getQuery(true)
|
||||||
|
->select($db->quoteName(['a.id', 'a.alias', 'a.catid', 'a.modified', 'a.language']))
|
||||||
|
->from($db->quoteName('#__content', 'a'))
|
||||||
|
->where($db->quoteName('a.state') . ' = 1');
|
||||||
|
|
||||||
|
$db->setQuery($query);
|
||||||
|
$articles = $db->loadObjectList();
|
||||||
|
|
||||||
|
// Get noindex articles from mokoog_tags
|
||||||
|
$noindexQuery = $db->getQuery(true)
|
||||||
|
->select($db->quoteName('content_id'))
|
||||||
|
->from($db->quoteName('#__mokoog_tags'))
|
||||||
|
->where($db->quoteName('content_type') . ' = ' . $db->quote('com_content'))
|
||||||
|
->where($db->quoteName('robots') . ' LIKE ' . $db->quote('%noindex%'));
|
||||||
|
|
||||||
|
$db->setQuery($noindexQuery);
|
||||||
|
$noindexIds = array_map('intval', $db->loadColumn());
|
||||||
|
|
||||||
|
$root = rtrim(Uri::root(), '/');
|
||||||
|
$xml = '<?xml version="1.0" encoding="UTF-8"?>' . "\n";
|
||||||
|
$xml .= '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">' . "\n";
|
||||||
|
|
||||||
|
// Homepage
|
||||||
|
$xml .= ' <url>' . "\n";
|
||||||
|
$xml .= ' <loc>' . $root . '/</loc>' . "\n";
|
||||||
|
$xml .= ' <changefreq>daily</changefreq>' . "\n";
|
||||||
|
$xml .= ' <priority>1.0</priority>' . "\n";
|
||||||
|
$xml .= ' </url>' . "\n";
|
||||||
|
|
||||||
|
foreach ($articles as $article) {
|
||||||
|
// Skip noindexed
|
||||||
|
if (\in_array((int) $article->id, $noindexIds, true)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$url = $root . '/index.php?option=com_content&view=article&id=' . $article->id;
|
||||||
|
$lastmod = $article->modified && $article->modified !== '0000-00-00 00:00:00'
|
||||||
|
? date('Y-m-d', strtotime($article->modified)) : '';
|
||||||
|
|
||||||
|
$xml .= ' <url>' . "\n";
|
||||||
|
$xml .= ' <loc>' . htmlspecialchars($url, ENT_XML1) . '</loc>' . "\n";
|
||||||
|
|
||||||
|
if ($lastmod) {
|
||||||
|
$xml .= ' <lastmod>' . $lastmod . '</lastmod>' . "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
$xml .= ' <changefreq>' . $changefreq . '</changefreq>' . "\n";
|
||||||
|
$xml .= ' <priority>0.8</priority>' . "\n";
|
||||||
|
$xml .= ' </url>' . "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
$xml .= '</urlset>';
|
||||||
|
|
||||||
|
return $xml;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Write sitemap XML to the site root.
|
||||||
|
*
|
||||||
|
* @param string $xml The sitemap XML content
|
||||||
|
*
|
||||||
|
* @return bool True on success
|
||||||
|
*/
|
||||||
|
public static function writeToFile(string $xml): bool
|
||||||
|
{
|
||||||
|
$path = JPATH_ROOT . '/sitemap.xml';
|
||||||
|
|
||||||
|
return (bool) file_put_contents($path, $xml);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
; MokoJoomOpenGraph - Web Services Plugin Language File
|
; MokoSuiteOpenGraph - Web Services Plugin Language File
|
||||||
; Copyright (C) 2026 Moko Consulting. All rights reserved.
|
; Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||||
; License: GPL-3.0-or-later
|
; License: GPL-3.0-or-later
|
||||||
|
|
||||||
PLG_WEBSERVICES_MOKOOG="Web Services - MokoJoomOpenGraph"
|
PLG_WEBSERVICES_MOKOOG="Web Services - MokoSuiteOpenGraph"
|
||||||
|
|||||||
+3
-3
@@ -1,6 +1,6 @@
|
|||||||
; MokoJoomOpenGraph - Web Services Plugin System Language File
|
; MokoSuiteOpenGraph - Web Services Plugin System Language File
|
||||||
; Copyright (C) 2026 Moko Consulting. All rights reserved.
|
; Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||||
; License: GPL-3.0-or-later
|
; License: GPL-3.0-or-later
|
||||||
|
|
||||||
PLG_WEBSERVICES_MOKOOG="Web Services - MokoJoomOpenGraph"
|
PLG_WEBSERVICES_MOKOOG="Web Services - MokoSuiteOpenGraph"
|
||||||
PLG_WEBSERVICES_MOKOOG_DESCRIPTION="Exposes MokoJoomOpenGraph OG tag data via Joomla's REST API at /api/index.php/v1/mokoog/tags."
|
PLG_WEBSERVICES_MOKOOG_DESCRIPTION="Exposes MokoSuiteOpenGraph OG tag data via Joomla's REST API at /api/index.php/v1/mokoog/tags."
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
; MokoJoomOpenGraph - Web Services Plugin Language File
|
; MokoSuiteOpenGraph - Web Services Plugin Language File
|
||||||
; Copyright (C) 2026 Moko Consulting. All rights reserved.
|
; Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||||
; License: GPL-3.0-or-later
|
; License: GPL-3.0-or-later
|
||||||
|
|
||||||
PLG_WEBSERVICES_MOKOOG="Web Services - MokoJoomOpenGraph"
|
PLG_WEBSERVICES_MOKOOG="Web Services - MokoSuiteOpenGraph"
|
||||||
|
|||||||
+3
-3
@@ -1,6 +1,6 @@
|
|||||||
; MokoJoomOpenGraph - Web Services Plugin System Language File
|
; MokoSuiteOpenGraph - Web Services Plugin System Language File
|
||||||
; Copyright (C) 2026 Moko Consulting. All rights reserved.
|
; Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||||
; License: GPL-3.0-or-later
|
; License: GPL-3.0-or-later
|
||||||
|
|
||||||
PLG_WEBSERVICES_MOKOOG="Web Services - MokoJoomOpenGraph"
|
PLG_WEBSERVICES_MOKOOG="Web Services - MokoSuiteOpenGraph"
|
||||||
PLG_WEBSERVICES_MOKOOG_DESCRIPTION="Exposes MokoJoomOpenGraph OG tag data via Joomla's REST API at /api/index.php/v1/mokoog/tags."
|
PLG_WEBSERVICES_MOKOOG_DESCRIPTION="Exposes MokoSuiteOpenGraph OG tag data via Joomla's REST API at /api/index.php/v1/mokoog/tags."
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user