From 8582a3eac563973507a39279d0ed03728ce1aec8 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Mon, 29 Jun 2026 09:35:36 -0500 Subject: [PATCH] fix: resolve release blockers #97 (scalar JSON-LD 500) and #98 (no edit UI) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #97 — Fatal frontend 500 from scalar custom_schema: - MokoOGContent::validateJson() now requires a JSON object/array (rejects scalars like 42/"x"/true that previously passed and were stored) - MokoOG render path guards with is_array($decoded) so already-stored scalar payloads can no longer crash the public page #98 — Missing single-tag create/edit admin UI: - Add Controller/TagController (FormController), View/Tag/HtmlView, tmpl/tag/edit.php - Link OG title in the list to the editor; add New/Edit toolbar buttons - Declare tmpl/tag folder in the component manifest - tag.xml: switch cross-package PLG_CONTENT_* labels to COM_MOKOOG_* keys, make content_type/content_id editable+required (enables New), add language field - Add the new COM_MOKOOG_* strings to en-GB and en-US Also fixes #77 while here: seo_title/meta_description form maxlength now match the DB columns (70/200) instead of 255. --- source/packages/com_mokoog/forms/tag.xml | 36 +++++---- .../com_mokoog/language/en-GB/com_mokoog.ini | 16 ++++ .../com_mokoog/language/en-US/com_mokoog.ini | 16 ++++ source/packages/com_mokoog/mokoog.xml | 1 + .../src/Controller/TagController.php | 31 ++++++++ .../com_mokoog/src/View/Tag/HtmlView.php | 76 +++++++++++++++++++ .../com_mokoog/src/View/Tags/HtmlView.php | 2 + source/packages/com_mokoog/tmpl/tag/edit.php | 41 ++++++++++ .../packages/com_mokoog/tmpl/tags/default.php | 4 +- .../src/Extension/MokoOGContent.php | 9 ++- .../src/Extension/MokoOG.php | 4 +- 11 files changed, 220 insertions(+), 16 deletions(-) create mode 100644 source/packages/com_mokoog/src/Controller/TagController.php create mode 100644 source/packages/com_mokoog/src/View/Tag/HtmlView.php create mode 100644 source/packages/com_mokoog/tmpl/tag/edit.php diff --git a/source/packages/com_mokoog/forms/tag.xml b/source/packages/com_mokoog/forms/tag.xml index 7b4a35f..7799093 100644 --- a/source/packages/com_mokoog/forms/tag.xml +++ b/source/packages/com_mokoog/forms/tag.xml @@ -16,13 +16,15 @@ name="content_type" type="text" label="COM_MOKOOG_FIELD_CONTENT_TYPE" - readonly="true" + description="COM_MOKOOG_FIELD_CONTENT_TYPE_DESC" + required="true" /> JPUBLISHED + + + -
+
diff --git a/source/packages/com_mokoog/language/en-GB/com_mokoog.ini b/source/packages/com_mokoog/language/en-GB/com_mokoog.ini index 4f0d4d3..ae9e73d 100644 --- a/source/packages/com_mokoog/language/en-GB/com_mokoog.ini +++ b/source/packages/com_mokoog/language/en-GB/com_mokoog.ini @@ -66,3 +66,19 @@ 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" + +; Single-tag edit form +COM_MOKOOG_TAG_NEW="MokoSuiteOpenGraph - New OG Tag" +COM_MOKOOG_TAG_EDIT="MokoSuiteOpenGraph - Edit OG Tag" +COM_MOKOOG_TAB_DETAILS="Details" +COM_MOKOOG_FIELDSET_SEO="SEO Meta Tags" +COM_MOKOOG_FIELD_CONTENT_TYPE_DESC="The content type this OG tag applies to (e.g. com_content, menu, com_content.category)." +COM_MOKOOG_FIELD_CONTENT_ID_DESC="The ID of the content item this OG tag applies to." +COM_MOKOOG_FIELD_SEO_TITLE="SEO Title" +COM_MOKOOG_FIELD_SEO_TITLE_DESC="Overrides the page <title> tag (max 70 characters)." +COM_MOKOOG_FIELD_META_DESCRIPTION="Meta Description" +COM_MOKOOG_FIELD_META_DESCRIPTION_DESC="Overrides the page meta description (max 200 characters)." +COM_MOKOOG_FIELD_ROBOTS="Robots" +COM_MOKOOG_FIELD_ROBOTS_DESC="Per-page robots directive, e.g. noindex, nofollow." +COM_MOKOOG_FIELD_CANONICAL_URL="Canonical URL" +COM_MOKOOG_FIELD_CANONICAL_URL_DESC="Overrides the canonical URL for this content item (http/https only)." diff --git a/source/packages/com_mokoog/language/en-US/com_mokoog.ini b/source/packages/com_mokoog/language/en-US/com_mokoog.ini index 4f0d4d3..ae9e73d 100644 --- a/source/packages/com_mokoog/language/en-US/com_mokoog.ini +++ b/source/packages/com_mokoog/language/en-US/com_mokoog.ini @@ -66,3 +66,19 @@ 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" + +; Single-tag edit form +COM_MOKOOG_TAG_NEW="MokoSuiteOpenGraph - New OG Tag" +COM_MOKOOG_TAG_EDIT="MokoSuiteOpenGraph - Edit OG Tag" +COM_MOKOOG_TAB_DETAILS="Details" +COM_MOKOOG_FIELDSET_SEO="SEO Meta Tags" +COM_MOKOOG_FIELD_CONTENT_TYPE_DESC="The content type this OG tag applies to (e.g. com_content, menu, com_content.category)." +COM_MOKOOG_FIELD_CONTENT_ID_DESC="The ID of the content item this OG tag applies to." +COM_MOKOOG_FIELD_SEO_TITLE="SEO Title" +COM_MOKOOG_FIELD_SEO_TITLE_DESC="Overrides the page <title> tag (max 70 characters)." +COM_MOKOOG_FIELD_META_DESCRIPTION="Meta Description" +COM_MOKOOG_FIELD_META_DESCRIPTION_DESC="Overrides the page meta description (max 200 characters)." +COM_MOKOOG_FIELD_ROBOTS="Robots" +COM_MOKOOG_FIELD_ROBOTS_DESC="Per-page robots directive, e.g. noindex, nofollow." +COM_MOKOOG_FIELD_CANONICAL_URL="Canonical URL" +COM_MOKOOG_FIELD_CANONICAL_URL_DESC="Overrides the canonical URL for this content item (http/https only)." diff --git a/source/packages/com_mokoog/mokoog.xml b/source/packages/com_mokoog/mokoog.xml index b182ad0..cd9dc4c 100644 --- a/source/packages/com_mokoog/mokoog.xml +++ b/source/packages/com_mokoog/mokoog.xml @@ -50,6 +50,7 @@ View + tag tags diff --git a/source/packages/com_mokoog/src/Controller/TagController.php b/source/packages/com_mokoog/src/Controller/TagController.php new file mode 100644 index 0000000..c1ce226 --- /dev/null +++ b/source/packages/com_mokoog/src/Controller/TagController.php @@ -0,0 +1,31 @@ + + * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. + * @license GNU General Public License version 3 or later; see LICENSE + */ + +namespace Joomla\Component\MokoOG\Administrator\Controller; + +defined('_JEXEC') or die; + +use Joomla\CMS\MVC\Controller\FormController; + +/** + * Controller for a single OG tag record. + * + * Provides the standard add/edit/save/apply/cancel tasks via FormController, + * backed by the existing TagModel (AdminModel) and TagTable. + */ +class TagController extends FormController +{ + /** + * The list view to redirect to after save/cancel. + * + * @var string + */ + protected $view_list = 'tags'; +} diff --git a/source/packages/com_mokoog/src/View/Tag/HtmlView.php b/source/packages/com_mokoog/src/View/Tag/HtmlView.php new file mode 100644 index 0000000..8b5d31a --- /dev/null +++ b/source/packages/com_mokoog/src/View/Tag/HtmlView.php @@ -0,0 +1,76 @@ + + * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. + * @license GNU General Public License version 3 or later; see LICENSE + */ + +namespace Joomla\Component\MokoOG\Administrator\View\Tag; + +defined('_JEXEC') or die; + +use Joomla\CMS\Factory; +use Joomla\CMS\Language\Text; +use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView; +use Joomla\CMS\Toolbar\ToolbarHelper; + +/** + * Edit view for a single OG tag record. + */ +class HtmlView extends BaseHtmlView +{ + /** + * The edit form. + * + * @var \Joomla\CMS\Form\Form + */ + protected $form; + + /** + * The item being edited. + * + * @var object + */ + protected $item; + + /** + * Display the view. + * + * @param string $tpl Template name + * + * @return void + */ + public function display($tpl = null): void + { + $this->form = $this->get('Form'); + $this->item = $this->get('Item'); + + $this->addToolbar(); + + parent::display($tpl); + } + + /** + * Add the edit toolbar. + * + * @return void + */ + protected function addToolbar(): void + { + Factory::getApplication()->getInput()->set('hidemainmenu', true); + + $isNew = empty($this->item->id); + + ToolbarHelper::title( + Text::_($isNew ? 'COM_MOKOOG_TAG_NEW' : 'COM_MOKOOG_TAG_EDIT'), + 'bookmark' + ); + + ToolbarHelper::apply('tag.apply'); + ToolbarHelper::save('tag.save'); + ToolbarHelper::cancel('tag.cancel', $isNew ? 'JTOOLBAR_CANCEL' : 'JTOOLBAR_CLOSE'); + } +} diff --git a/source/packages/com_mokoog/src/View/Tags/HtmlView.php b/source/packages/com_mokoog/src/View/Tags/HtmlView.php index 9a07a00..e80d0ae 100644 --- a/source/packages/com_mokoog/src/View/Tags/HtmlView.php +++ b/source/packages/com_mokoog/src/View/Tags/HtmlView.php @@ -81,6 +81,8 @@ class HtmlView extends BaseHtmlView protected function addToolbar(): void { ToolbarHelper::title(Text::_('COM_MOKOOG_TAGS_TITLE'), 'bookmark'); + ToolbarHelper::addNew('tag.add'); + ToolbarHelper::editList('tag.edit'); ToolbarHelper::custom('batch.generate', 'refresh', '', 'COM_MOKOOG_TOOLBAR_BATCH_GENERATE', false); ToolbarHelper::custom('importexport.export', 'download', '', 'COM_MOKOOG_TOOLBAR_EXPORT', false); ToolbarHelper::deleteList('JGLOBAL_CONFIRM_DELETE', 'tags.delete'); diff --git a/source/packages/com_mokoog/tmpl/tag/edit.php b/source/packages/com_mokoog/tmpl/tag/edit.php new file mode 100644 index 0000000..63bfe41 --- /dev/null +++ b/source/packages/com_mokoog/tmpl/tag/edit.php @@ -0,0 +1,41 @@ + + * @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\HTML\HTMLHelper; +use Joomla\CMS\Language\Text; +use Joomla\CMS\Router\Route; + +/** @var \Joomla\Component\MokoOG\Administrator\View\Tag\HtmlView $this */ + +HTMLHelper::_('behavior.formvalidator'); +?> +
+
+
+ 'details']); ?> + + + form->renderFieldset('details'); ?> + + + + form->renderFieldset('seo'); ?> + + + +
+
+ + + +
diff --git a/source/packages/com_mokoog/tmpl/tags/default.php b/source/packages/com_mokoog/tmpl/tags/default.php index f3a3ad0..bf6a82b 100644 --- a/source/packages/com_mokoog/tmpl/tags/default.php +++ b/source/packages/com_mokoog/tmpl/tags/default.php @@ -85,7 +85,9 @@ $token = Session::getFormToken(); content_id; ?> - escape($item->og_title ?: '(' . Text::_('COM_MOKOOG_AUTO_GENERATED') . ')'); ?> + + escape($item->og_title ?: '(' . Text::_('COM_MOKOOG_AUTO_GENERATED') . ')'); ?> + og_image) : ?> diff --git a/source/packages/plg_content_mokoog/src/Extension/MokoOGContent.php b/source/packages/plg_content_mokoog/src/Extension/MokoOGContent.php index 91550d7..c7a792b 100644 --- a/source/packages/plg_content_mokoog/src/Extension/MokoOGContent.php +++ b/source/packages/plg_content_mokoog/src/Extension/MokoOGContent.php @@ -322,7 +322,14 @@ final class MokoOGContent extends CMSPlugin implements SubscriberInterface { $json = trim($json); - if ($json === '' || json_decode($json) === null) { + if ($json === '') { + return ''; + } + + // Only accept JSON objects/arrays. Scalars (42, "x", true) decode to a + // non-null value but would crash the frontend renderer when treated as + // an array (writing $decoded['@context'] onto a scalar is a fatal error). + if (!\is_array(json_decode($json, true))) { return ''; } diff --git a/source/packages/plg_system_mokoog/src/Extension/MokoOG.php b/source/packages/plg_system_mokoog/src/Extension/MokoOG.php index b472a9a..fc75984 100644 --- a/source/packages/plg_system_mokoog/src/Extension/MokoOG.php +++ b/source/packages/plg_system_mokoog/src/Extension/MokoOG.php @@ -358,7 +358,9 @@ final class MokoOG extends CMSPlugin implements SubscriberInterface if (!empty($customSchema)) { $decoded = json_decode($customSchema, true); - if ($decoded) { + // Guard against scalar/invalid payloads — only arrays/objects are + // valid JSON-LD. Writing an array offset onto a scalar is fatal. + if (\is_array($decoded) && $decoded !== []) { if (empty($decoded['@context'])) { $decoded['@context'] = 'https://schema.org'; }