diff --git a/source/packages/com_mokoog/sql/install.mysql.sql b/source/packages/com_mokoog/sql/install.mysql.sql index 1bce7f2..86e2c45 100644 --- a/source/packages/com_mokoog/sql/install.mysql.sql +++ b/source/packages/com_mokoog/sql/install.mysql.sql @@ -13,6 +13,8 @@ CREATE TABLE IF NOT EXISTS `#__mokoog_tags` ( `og_image` VARCHAR(512) NOT NULL DEFAULT '', `og_type` VARCHAR(50) NOT NULL DEFAULT 'article', `og_video` VARCHAR(512) NOT NULL DEFAULT '', + `event_data` TEXT NULL, + `recipe_data` TEXT NULL, `seo_title` VARCHAR(70) NOT NULL DEFAULT '', `meta_description` VARCHAR(200) NOT NULL DEFAULT '', `robots` VARCHAR(100) NOT NULL DEFAULT '', diff --git a/source/packages/com_mokoog/sql/updates/mysql/01.04.00.sql b/source/packages/com_mokoog/sql/updates/mysql/01.04.00.sql new file mode 100644 index 0000000..225089e --- /dev/null +++ b/source/packages/com_mokoog/sql/updates/mysql/01.04.00.sql @@ -0,0 +1,6 @@ +-- +-- MokoJoomOpenGraph 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`; diff --git a/source/packages/plg_content_mokoog/forms/mokoog.xml b/source/packages/plg_content_mokoog/forms/mokoog.xml index b5168e3..08635ce 100644 --- a/source/packages/plg_content_mokoog/forms/mokoog.xml +++ b/source/packages/plg_content_mokoog/forms/mokoog.xml @@ -101,5 +101,25 @@ validate="url" /> +
+ diff --git a/source/packages/plg_content_mokoog/language/en-GB/plg_content_mokoog.ini b/source/packages/plg_content_mokoog/language/en-GB/plg_content_mokoog.ini index e5859fa..85fe8f2 100644 --- a/source/packages/plg_content_mokoog/language/en-GB/plg_content_mokoog.ini +++ b/source/packages/plg_content_mokoog/language/en-GB/plg_content_mokoog.ini @@ -29,3 +29,37 @@ PLG_CONTENT_MOKOOG_FIELD_ROBOTS_DESC="Search engine indexing directives for this PLG_CONTENT_MOKOOG_ROBOTS_DEFAULT="- Use default (index, follow) -" 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_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)." diff --git a/source/packages/plg_content_mokoog/language/en-US/plg_content_mokoog.ini b/source/packages/plg_content_mokoog/language/en-US/plg_content_mokoog.ini index e5859fa..9a7634a 100644 --- a/source/packages/plg_content_mokoog/language/en-US/plg_content_mokoog.ini +++ b/source/packages/plg_content_mokoog/language/en-US/plg_content_mokoog.ini @@ -29,3 +29,37 @@ PLG_CONTENT_MOKOOG_FIELD_ROBOTS_DESC="Search engine indexing directives for this PLG_CONTENT_MOKOOG_ROBOTS_DEFAULT="- Use default (index, follow) -" 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_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_MKOOG_FIELD_RECIPE_CUISINE_DESC="Type of cuisine (e.g. Italian, Mexican, American)." diff --git a/source/packages/plg_content_mokoog/src/Extension/MokoOGContent.php b/source/packages/plg_content_mokoog/src/Extension/MokoOGContent.php index b7bac0a..9d93f0e 100644 --- a/source/packages/plg_content_mokoog/src/Extension/MokoOGContent.php +++ b/source/packages/plg_content_mokoog/src/Extension/MokoOGContent.php @@ -98,7 +98,24 @@ final class MokoOGContent extends CMSPlugin implements SubscriberInterface $ogData = $this->loadOgData($contentType, $id, $language); 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]); } } } @@ -195,6 +212,7 @@ final class MokoOGContent extends CMSPlugin implements SubscriberInterface $query = $db->getQuery(true) ->select($db->quoteName([ 'og_title', 'og_description', 'og_image', 'og_type', 'og_video', + 'event_data', 'recipe_data', 'seo_title', 'meta_description', 'robots', 'canonical_url', ])) ->from($db->quoteName('#__mokoog_tags')) @@ -250,6 +268,8 @@ final class MokoOGContent extends CMSPlugin implements SubscriberInterface 'og_image' => trim($ogData['og_image'] ?? ''), 'og_type' => trim($ogData['og_type'] ?? 'article'), 'og_video' => $this->sanitizeUrl($ogData['og_video'] ?? ''), + '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']), 'seo_title' => strip_tags(trim($ogData['seo_title'] ?? '')), 'meta_description' => strip_tags(trim($ogData['meta_description'] ?? '')), 'robots' => trim($robots), @@ -267,6 +287,29 @@ 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) : ''; + } + /** * Sanitize a URL to only allow http/https schemes. * diff --git a/source/packages/plg_system_mokoog/language/en-GB/plg_system_mokoog.ini b/source/packages/plg_system_mokoog/language/en-GB/plg_system_mokoog.ini index 2c8029d..b6df7cb 100644 --- a/source/packages/plg_system_mokoog/language/en-GB/plg_system_mokoog.ini +++ b/source/packages/plg_system_mokoog/language/en-GB/plg_system_mokoog.ini @@ -37,6 +37,10 @@ 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_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_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_DESC="Output BreadcrumbList JSON-LD schema from Joomla's pathway." diff --git a/source/packages/plg_system_mokoog/language/en-US/plg_system_mokoog.ini b/source/packages/plg_system_mokoog/language/en-US/plg_system_mokoog.ini index 2c8029d..b6df7cb 100644 --- a/source/packages/plg_system_mokoog/language/en-US/plg_system_mokoog.ini +++ b/source/packages/plg_system_mokoog/language/en-US/plg_system_mokoog.ini @@ -37,6 +37,10 @@ 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_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_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_DESC="Output BreadcrumbList JSON-LD schema from Joomla's pathway." diff --git a/source/packages/plg_system_mokoog/mokoog.xml b/source/packages/plg_system_mokoog/mokoog.xml index 07d9a89..5a5d246 100644 --- a/source/packages/plg_system_mokoog/mokoog.xml +++ b/source/packages/plg_system_mokoog/mokoog.xml @@ -169,6 +169,28 @@ +]*>.*?<\/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('/