Compare commits
24 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5f1e44e66b | |||
| 646dd23e81 | |||
| d4229fd450 | |||
| 5724a1545e | |||
| a04dbfd732 | |||
| bc06710fdd | |||
| 07b296db61 | |||
| 6a0ee812d8 | |||
| fcfa6838e5 | |||
| 908e1d3e1b | |||
| 9539bb44c2 | |||
| 5b29690d34 | |||
| 881bb0a2ae | |||
| e9b34522d3 | |||
| 9aeb588937 | |||
| 9cdc7915a3 | |||
| 72ffaded49 | |||
| 7d1a939b6a | |||
| 23f6fe12a0 | |||
| 4c1d630673 | |||
| 6a3f9c126e | |||
| ddb378a042 | |||
| 20b62b95d8 | |||
| 437a23cec2 |
@@ -30,6 +30,15 @@ on:
|
||||
types: [opened, closed]
|
||||
branches:
|
||||
- main
|
||||
paths-ignore:
|
||||
- '.mokogitea/workflows/**'
|
||||
- '*.md'
|
||||
- 'wiki/**'
|
||||
- '.editorconfig'
|
||||
- '.gitignore'
|
||||
- '.gitattributes'
|
||||
- '.gitmessage'
|
||||
- 'LICENSE'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
action:
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
# FILE INFORMATION
|
||||
# DEFGROUP: Gitea.Workflow
|
||||
# INGROUP: mokocli.Automation
|
||||
# VERSION: 01.03.00
|
||||
# VERSION: 01.04.00
|
||||
# BRIEF: Auto-create feature branch when an issue is opened
|
||||
|
||||
name: "Universal: Issue Branch"
|
||||
|
||||
+8
-3
@@ -2,16 +2,16 @@
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [01.03.00] --- 2026-06-21
|
||||
## [01.04.00] --- 2026-06-23
|
||||
|
||||
|
||||
<!-- VERSION: 01.03.00 -->
|
||||
<!-- VERSION: 01.04.00 -->
|
||||
|
||||
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/).
|
||||
|
||||
## [01.03.00] --- 2026-06-21
|
||||
## [01.04.00] --- 2026-06-23
|
||||
|
||||
### Security
|
||||
- Fix JSON-LD XSS vulnerability via `</script>` injection in content data (#34)
|
||||
@@ -20,6 +20,11 @@ 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)
|
||||
|
||||
### 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)
|
||||
- Site-wide default OG title and description plugin parameters
|
||||
- Discord embed color via `theme-color` meta tag (color picker in plugin config)
|
||||
- LinkedIn article tags: `article:published_time`, `article:modified_time`, `article:author`
|
||||
|
||||
@@ -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,6 +1,6 @@
|
||||
# MokoSuiteOpenGraph
|
||||
|
||||
<!-- VERSION: 01.03.00 -->
|
||||
<!-- VERSION: 01.04.00 -->
|
||||
|
||||
Open Graph, Twitter Card, and social sharing meta tag management for Joomla 4/5/6.
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -60,6 +60,14 @@
|
||||
<option value="product">Product</option>
|
||||
<option value="profile">Profile</option>
|
||||
</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
|
||||
name="published"
|
||||
type="list"
|
||||
|
||||
@@ -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_TYPE="OG 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_CONTENT_TYPE="Content Type"
|
||||
|
||||
@@ -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_TYPE="OG 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_CONTENT_TYPE="Content Type"
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
-->
|
||||
<extension type="component" method="upgrade">
|
||||
<name>com_mokoog</name>
|
||||
<version>01.03.00</version>
|
||||
<version>01.04.00</version>
|
||||
<creationDate>2026-05-23</creationDate>
|
||||
<author>Moko Consulting</author>
|
||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||
|
||||
@@ -6,12 +6,13 @@
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `#__mokoog_tags` (
|
||||
`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,
|
||||
`og_title` VARCHAR(255) NOT NULL DEFAULT '',
|
||||
`og_description` TEXT NOT NULL,
|
||||
`og_image` VARCHAR(512) NOT NULL DEFAULT '',
|
||||
`og_type` VARCHAR(50) NOT NULL DEFAULT 'article',
|
||||
`og_video` VARCHAR(512) NOT NULL DEFAULT '',
|
||||
`seo_title` VARCHAR(70) NOT NULL DEFAULT '',
|
||||
`meta_description` VARCHAR(200) NOT NULL DEFAULT '',
|
||||
`robots` VARCHAR(100) NOT NULL DEFAULT '',
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
--
|
||||
-- MokoJoomOpenGraph 01.03.00 - Add og_video column
|
||||
--
|
||||
|
||||
ALTER TABLE `#__mokoog_tags` ADD COLUMN `og_video` VARCHAR(512) NOT NULL DEFAULT '' AFTER `og_type`;
|
||||
@@ -49,6 +49,14 @@
|
||||
<option value="music.song">Music</option>
|
||||
<option value="video.other">Video</option>
|
||||
</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 name="mokoog_seo" label="PLG_CONTENT_MOKOOG_FIELDSET_SEO_LABEL"
|
||||
description="PLG_CONTENT_MOKOOG_FIELDSET_SEO_DESC">
|
||||
|
||||
@@ -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_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_DESC="Control search engine meta tags for this page."
|
||||
|
||||
|
||||
@@ -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_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_DESC="Control search engine meta tags for this page."
|
||||
|
||||
|
||||
@@ -102,3 +102,46 @@
|
||||
color: #536471;
|
||||
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;
|
||||
}
|
||||
|
||||
/* 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;
|
||||
}
|
||||
|
||||
@@ -15,9 +15,44 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||
ogDesc: document.getElementById('jform_mokoog_og_description'),
|
||||
ogImage: document.getElementById('jform_mokoog_og_image'),
|
||||
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();
|
||||
});
|
||||
|
||||
// Find the mokoog fieldset and insert preview after it
|
||||
var fieldset = document.querySelector('[data-showon-id="mokoog"]') ||
|
||||
document.getElementById('attrib-mokoog') ||
|
||||
@@ -110,6 +145,36 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||
twCard.appendChild(twBody);
|
||||
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);
|
||||
|
||||
preview.appendChild(wrapper);
|
||||
fieldset.parentNode.insertBefore(preview, fieldset.nextSibling);
|
||||
|
||||
@@ -152,6 +217,18 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||
} else {
|
||||
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';
|
||||
}
|
||||
}
|
||||
|
||||
Object.values(fields).forEach(function (el) {
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
-->
|
||||
<extension type="plugin" group="content" method="upgrade">
|
||||
<name>Content - MokoJoomOpenGraph</name>
|
||||
<version>01.03.00</version>
|
||||
<version>01.04.00</version>
|
||||
<creationDate>2026-05-23</creationDate>
|
||||
<author>Moko Consulting</author>
|
||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||
|
||||
@@ -194,7 +194,7 @@ final class MokoOGContent extends CMSPlugin implements SubscriberInterface
|
||||
$db = Factory::getDbo();
|
||||
$query = $db->getQuery(true)
|
||||
->select($db->quoteName([
|
||||
'og_title', 'og_description', 'og_image', 'og_type',
|
||||
'og_title', 'og_description', 'og_image', 'og_type', 'og_video',
|
||||
'seo_title', 'meta_description', 'robots', 'canonical_url',
|
||||
]))
|
||||
->from($db->quoteName('#__mokoog_tags'))
|
||||
@@ -249,6 +249,7 @@ final class MokoOGContent extends CMSPlugin implements SubscriberInterface
|
||||
'og_description' => trim($ogData['og_description'] ?? ''),
|
||||
'og_image' => trim($ogData['og_image'] ?? ''),
|
||||
'og_type' => trim($ogData['og_type'] ?? 'article'),
|
||||
'og_video' => $this->sanitizeUrl($ogData['og_video'] ?? ''),
|
||||
'seo_title' => trim($ogData['seo_title'] ?? ''),
|
||||
'meta_description' => trim($ogData['meta_description'] ?? ''),
|
||||
'robots' => trim($robots),
|
||||
@@ -266,6 +267,28 @@ final class MokoOGContent extends CMSPlugin implements SubscriberInterface
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
|
||||
@@ -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_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_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_DESC="Automatically generate OG tags from article content when no custom tags are set."
|
||||
PLG_SYSTEM_MOKOOG_FIELD_STRIP_HTML="Strip HTML from Description"
|
||||
|
||||
@@ -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_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_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_DESC="Automatically generate OG tags from article content when no custom tags are set."
|
||||
PLG_SYSTEM_MOKOOG_FIELD_STRIP_HTML="Strip HTML from Description"
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
-->
|
||||
<extension type="plugin" group="system" method="upgrade">
|
||||
<name>System - MokoJoomOpenGraph</name>
|
||||
<version>01.03.00</version>
|
||||
<version>01.04.00</version>
|
||||
<creationDate>2026-05-23</creationDate>
|
||||
<author>Moko Consulting</author>
|
||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||
@@ -106,6 +106,14 @@
|
||||
description="PLG_SYSTEM_MOKOOG_FIELD_DISCORD_COLOR_DESC"
|
||||
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 name="advanced" label="PLG_SYSTEM_MOKOOG_FIELDSET_ADVANCED">
|
||||
<field
|
||||
|
||||
@@ -92,7 +92,7 @@ final class MokoOG extends CMSPlugin implements SubscriberInterface
|
||||
|
||||
if ($catOg) {
|
||||
// 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)) {
|
||||
$ogData->$field = $catOg->$field;
|
||||
}
|
||||
@@ -177,6 +177,38 @@ final class MokoOG extends CMSPlugin implements SubscriberInterface
|
||||
$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
|
||||
if ($option === 'com_content' && $view === 'article' && $id > 0) {
|
||||
$doc->setMetaData('article:published_time', $this->getArticleDate($id, 'publish_up'), 'property');
|
||||
@@ -189,13 +221,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) {
|
||||
$productData = $this->loadShopProduct($id);
|
||||
|
||||
if ($productData) {
|
||||
$doc->setMetaData('product:price:amount', number_format((float) $productData->price, 2, '.', ''), '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::getDbo();
|
||||
$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');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -299,6 +352,7 @@ final class MokoOG extends CMSPlugin implements SubscriberInterface
|
||||
'og_description' => '',
|
||||
'og_image' => '',
|
||||
'og_type' => '',
|
||||
'og_video' => '',
|
||||
'seo_title' => '',
|
||||
'meta_description' => '',
|
||||
'robots' => '',
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
-->
|
||||
<extension type="plugin" group="webservices" method="upgrade">
|
||||
<name>Web Services - MokoJoomOpenGraph</name>
|
||||
<version>01.03.00</version>
|
||||
<version>01.04.00</version>
|
||||
<creationDate>2026-05-23</creationDate>
|
||||
<author>Moko Consulting</author>
|
||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<extension type="package" method="upgrade">
|
||||
<name>Package - MokoSuiteOpenGraph</name>
|
||||
<packagename>mokoog</packagename>
|
||||
<version>01.03.00</version>
|
||||
<version>01.04.00</version>
|
||||
<creationDate>2026-05-23</creationDate>
|
||||
<author>Moko Consulting</author>
|
||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||
|
||||
Reference in New Issue
Block a user