feat: Schema.org structured data integration#1171
Merged
bcordis merged 51 commits intodevelopmentfrom Mar 19, 2026
Merged
Conversation
…ration ProclaimComponent now implements SchemaorgServiceInterface, enabling Joomla's built-in Schema.org system plugin to recognize Proclaim. Admins get a "Schema" tab on sermon, teacher, and series edit forms where they can configure structured data per item. Contexts registered: - com_proclaim.cwmmessage → Messages (sermons) - com_proclaim.teacher → Teachers - com_proclaim.serie → Series The existing CwmschemaorgHelper auto-generation continues to work as before. Phase 2 will add a schemaorg plugin to auto-populate defaults from item data. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Implement SchemaorgServiceInterface on ProclaimComponent so Joomla's built-in Schema.org system plugin recognizes Proclaim. Adds Schema.org tab to message, teacher, and series edit forms. Key changes: - ProclaimComponent implements SchemaorgServiceInterface with contexts for messages, teachers, and series - Override preprocessForm in all 3 models to import system plugins into the model's local dispatcher (required for third-party components) - Add Schema.org tab rendering to message, teacher, and series edit templates (conditional — only shows when system plugin is enabled) - Add JBS_CMN_SCHEMAORG_TAB language string for translatable tab label Admins can now select a schema type (Article, Event, Person, etc.) per item. Phase 2 will add a Proclaim-specific schemaorg plugin with auto-populated sermon/speaker fields. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Eliminates 15 PHPUnit deprecation warnings about metadata in doc-comments. PHPUnit 12 will require attributes instead. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Creates plg_schemaorg_proclaim that adds "Sermon" as a schema type for messages, teachers, and series. Auto-populates schema fields from item data: - Messages: headline (studytitle), description (studyintro), datePublished (studydate), author (teachername), image - Teachers: name, jobTitle, bio, image - Series: name, description, image Admins can override any auto-populated field. Custom key/value fields allow adding arbitrary schema.org properties. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…data Auto-populates Schema.org fields from message data in loadFormData(): - headline, description, image, datePublished, dateModified - author (all teachers from junction table, comma-separated) - isPartOf (series name), about (translated topics) - genre (message type), locationCreated (location name) - publisher (site name) Topics with language keys are translated via Text::_(). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add hasJoomlaSchema() for future fallback checks. Pretty-print JSON-LD output when JDEBUG is enabled for readable page source. Keep both JSON-LD outputs: our helper provides item-specific CreativeWork schema, the system plugin provides page-level @graph (Organization, WebSite, WebPage). They complement each other. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The sermon view was setting raw studyintro (with HTML tags) as the page meta description. Joomla's schema.org system plugin reads this for the WebPage description in the @graph. Now strips HTML and truncates to 160 chars for clean schema and SEO output. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: Brent Cordis <994259+bcordis@users.noreply.github.com>
…ention Add hasJoomlaSchema check inside inject() so detail views skip standalone JSON-LD when admin has configured per-item schema via Joomla's system plugin. Add buildTeacherList() and buildSeriesList() helpers with ItemList schema output for teacher and series list pages. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Include plg_schemaorg_proclaim in the install action queue so it is auto-installed and enabled on fresh installs and upgrades. Register the plugin manifest in bump.php for version bumping and add the dev symlink entry in proclaim_build.php. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The plugin was only registering "Sermon" in the dropdown for all three contexts. Teachers saw "Sermon" as the only option and data was stored under the wrong key. Now each context gets its own schema type (Sermon, Teacher, Series) with dedicated form fields and correct auto-population: - Teacher → Person schema with name, jobTitle, description, image - Series → CreativeWorkSeries with name, description, image - onSchemaBeforeCompileHead handles Person and CreativeWorkSeries types - Override onSchemaPrepareForm to add only the relevant type per context Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Joomla's system schemaorg plugin returns early from onContentPrepareData when no #__schemaorg row exists, so the plugin's onSchemaPrepareData event never fires for new items. The sermon model already handled this by populating schema data in loadFormData(). Apply the same pattern to teacher (Person) and series (CreativeWorkSeries) models. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add proper Person schema properties to the Teacher type: - url: auto-populated from teacher website field - sameAs: social profile URLs from social_links JSON or legacy fields - worksFor: Organization with site name - Form XML adds editable fields for URL, social profiles, and organization - onSchemaBeforeCompileHead flattens sameAs subform for JSON-LD output Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Adds a one-click bulk sync that populates Joomla's #__schemaorg table for all published messages, teachers, and series with auto-generated structured data: - Dashboard card on Control Panel tab triggers XHR modal - CwmschemaorgHelper::syncAll() iterates all published items - Controller schemaSyncXHR() endpoint with CSRF protection - Bootstrap 5 progress modal with success/error states - Force mode overwrites all existing entries Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Model loadFormData() now checks #__schemaorg before auto-populating: if schema data was already saved (and possibly customized), skip auto-population to preserve manual edits - Schema Sync modal now offers two modes: "Sync New Items Only" — skips items with existing schema entries "Force Update All" — overwrites everything (warns about custom changes) - Remove auto worksFor from model/sync — Organization name may differ from site name, let admins set it manually via the form Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When saving an item with a schema type selected, check for empty recommended fields (headline/name, description, datePublished) and enqueue a notice message listing what's missing. The save still completes — this is informational, not blocking. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Smart Sync uses a _autoHash fingerprint stored inside the schema JSON to detect whether an admin manually edited the schema fields. On sync: - Generate fresh auto-schema from current item data - Load stored schema, compare _autoHash against actual data hash - If hashes match → never manually edited → safe to overwrite - If hashes differ → admin customized → skip with count Three sync modes available from the admin dashboard: - Smart Sync (default): updates unedited items, skips manual edits - New Items Only: only creates entries for items without schema rows - Force Update All: overwrites everything including manual edits Also stamps _autoHash on every save via onSchemaPrepareSave, and strips it from frontend JSON-LD output in onSchemaBeforeCompileHead. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Joomla's form filter strips _autoHash since it's not in the form XML. On save, restore the existing hash from the DB row instead of computing a new one — this preserves the original auto-generated fingerprint so manual edits are correctly detected by Smart Sync. First save (no existing row) stamps a fresh hash. Subsequent saves restore the original hash, so any field changes cause a mismatch. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When saving a message, teacher, or series, the schema.org plugin now applies Smart Sync logic: - If schema was auto-generated (hash matches): silently regenerate schema fields from the updated item data and stamp a new hash - If schema was manually edited (hash mismatch): preserve the admin's customizations and show an info notice pointing to bulk sync Adds generateSchemaFromItem() with buildSermonSchema(), buildTeacherSchema(), buildSeriesSchema() methods that create fresh schema arrays from the Table object during save. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When saving an item with manually customized schema, the info notice now includes a "Refresh from item data" button that force-regenerates the schema for that single item and redirects back to the edit page. - Add CwmadminController::schemaForceRefresh() endpoint - Add CwmschemaorgHelper::syncOne() for single-item force sync - Extract buildSermonSchemaFromRow(), buildTeacherSchemaFromRow(), buildSeriesSchemaFromRow() as reusable static methods - Notice shows inline button with CSRF-protected link Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The hasJoomlaSchema() DB check caused schema auto-population to be skipped when a #__schemaorg row existed, but Joomla's system plugin onContentPrepareData runs AFTER loadFormData and overwrites our defaults with saved data anyway. Reverting to the original pattern: - Always auto-populate schema defaults from item data - Joomla's system plugin loads saved schema data on top if it exists - The Smart Sync hash in onSchemaPrepareSave handles edit detection This fixes messages showing "None" after bulk sync was run. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add an "Organization Name" field in Admin > SEO & Metadata that overrides the site name for Schema.org publisher (sermons) and worksFor (teachers). Falls back to the Joomla site name when blank. - CwmschemaorgHelper::getOrgName() centralizes the lookup - Used in bulk sync builders, model auto-populate, and plugin builders - Per-teacher org override possible via the worksFor schema form field Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add org_name column to #__bsms_teachers so visiting speakers can have their own organization (e.g., "Grace Community Church") in schema.org worksFor output instead of the site default. Three-tier fallback: teacher org_name → admin "Organization Name" setting → Joomla site name. If the teacher field is blank, the site default is used automatically. - SQL migration 10.3.0-20260318 - Field in teacher form Details fieldset - Property on CwmteacherTable - Updated in helper buildTeacherSchemaFromRow(), plugin buildTeacherSchema(), and model loadFormData() auto-populate Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The field was added to the form XML but not rendered in the template. Added it after the Title field in the Details tab. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The Smart Sync regeneration in onSchemaPrepareSave was overwriting manual edits from the Schema tab — it couldn't distinguish "user edited schema field" from "item data changed since last sync." Simplified to: on save, just preserve the original _autoHash from DB (or stamp one on first save). The user's form edits save as-is. The model's loadFormData() provides fresh auto-populated defaults each time the form opens. Bulk Smart Sync still handles the "update unedited items from changed source data" use case. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The system schemaorg plugin listens on onContentPrepareData to load saved schema from #__schemaorg, but preprocessData() only imports the 'content' plugin group by default. The system plugin never heard the event, so saved schema data (including manual edits) was never loaded from the DB — the model's auto-populated defaults always won. Override preprocessData() in all three models to also import system plugins into the dispatcher, matching what preprocessForm() already does for onContentPrepareForm. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Joomla's loadForm() never calls preprocessData() — models must call it explicitly (as com_content's ArticleModel does). Without it, the system schemaorg plugin's onContentPrepareData never fires, so saved schema from #__schemaorg is never loaded. The model's auto-populated defaults always win, overwriting manual edits on every form open. Added $this->preprocessData() call at the end of loadFormData() in all three models, after auto-populate runs. The system plugin then loads saved DB data on top, preserving manual edits. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Compare incoming form data against existing DB data to detect whether the user actually edited the schema tab: Case 1: incoming == existing + hash matches (auto-generated) → User didn't touch schema, regenerate from updated item data → Title change auto-updates headline Case 2: incoming == existing + hash mismatch (previously customized) → User didn't touch schema, preserve their prior customization Case 3: incoming != existing (user actively edited schema tab) → Save user's edits, keep original hash for future detection Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The form POST includes all schema fields with empty values (description ="", image="", noteSermon="") while the DB only stores non-empty values. Direct comparison always detected a "user edit" (Case 3), preventing auto-regeneration when the title changed (Case 1). Added normalizeForCompare() that strips empty values, null values, and note-prefixed fields before comparing incoming vs existing data. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The data comparison approach was unreliable — form POST and DB data have structural differences (empty fields, subform serialization) that made detection impossible. New approach: a hidden _schemaEdited field defaults to 0. JS sets it to 1 when the user changes any schema field. On save: - _schemaEdited=0 + hash matches → regenerate from item data (title change auto-updates headline) - _schemaEdited=0 + hash mismatch → preserve prior manual edits - _schemaEdited=1 → save user's current edits, keep old hash Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Removed the hash check from individual save. Now the logic is simple: - _schemaEdited=0 (user didn't touch schema tab): always regenerate from current item data. Schema stays in sync with title, description, etc. automatically. - _schemaEdited=1 (user actively edited a field): save their edits and stamp a fresh hash. The _autoHash is now only used by bulk Smart Sync to detect items with prior manual edits. Individual save relies purely on the JS flag. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Track which specific schema fields the user edits via JS, not just a whole-schema flag. On save: 1. Regenerate fresh schema from current item data (title change auto-updates headline, date changes update datePublished, etc.) 2. Overlay only the fields the user actively edited in this session Example: user edits description but not headline → headline auto- updates from title, description keeps the user's custom value. Uses a hidden _editedFields JSON array populated by JS change/input listeners on schema form fields. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Adds a "Reset Schema from Item Data" button at the bottom of the Schema.org tab on message, teacher, and series edit forms. Clicking it force-regenerates the schema from item data and redirects back. Also fixes missing Uri and Session use statements in templates. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace session-only _editedFields with persistent _customFields stored in the schema JSON. When a user edits a specific schema field: 1. JS tracks field name in _editedFields (current session) 2. On save, _editedFields merges into _customFields (persistent in DB) 3. Fresh schema regenerates from item data 4. Fields listed in _customFields overlay from form data instead Example flow: - User customizes "description" → _customFields: ["description"] - Next save, title changes → headline auto-updates, description stays - Reset button clears _customFields and regenerates everything _customFields stripped from frontend JSON-LD output alongside _autoHash. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Temporary debug output to trace Smart Sync field tracking: - _editedFields raw value from form - sessionEdited parsed array - existing _customFields from DB - merged customFields - incoming vs fresh headline values TODO: remove debug flag after testing Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The system plugin's event object wasn't persisting our _customFields modifications to the DB. Now our plugin writes directly to #__schemaorg and unsets entry->schemaType to tell the system plugin to skip its own DB write (it checks isset(schemaType) before writing). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
CMSPlugin::getDatabase() is not available in all plugin contexts. Use Factory::getContainer()->get(DatabaseInterface::class) instead for both writeSchemaToDb and loadExistingSchema. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When a user edits a schema field back to the auto-generated value (e.g., sets headline to match the title), that field is removed from _customFields. This means future saves will auto-update it again. Allows users to "un-lock" a field by simply setting it back to what the system would generate. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Joomla's subform widget renders inner fields after DOMContentLoaded, so direct querySelectorAll binding missed them. Switched to event delegation on document with capture phase — catches change/input events from any schema field regardless of when it's rendered. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Joomla's subform web component prevents input/change events from bubbling to document. New approach: 1. Capture original field values after 1.5s delay (subform render) 2. On form submit, compare current values against originals 3. Any changed fields get added to _editedFields Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace all JS-based edit tracking with pure server-side comparison. On every save: 1. Regenerate fresh schema from current item data 2. Compare each submitted field against the auto-generated value 3. If submitted != auto-generated → user customized → preserve 4. If submitted == auto-generated → not customized → use fresh value 5. Setting a field back to the auto value = un-customize No JS, no timing issues, no event bubbling problems. The comparison happens at save time using generateSchemaFromItem() as the baseline. Tracked fields: headline, name, description, jobTitle, datePublished, dateModified, image, url. Complex fields (author, sameAs, worksFor, genericField) are always preserved from submission. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Only track headline/name, description, and jobTitle for custom edit detection. Dates (timezone conversion), images (path format), and URLs cause false positives due to format differences between form POST and raw Table data. These always auto-update from item data now. Also keeps unset(schemaType) to prevent system plugin from overwriting our direct DB write. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Store short hashes (8-char md5) of auto-generated field values instead
of full values — avoids duplicating long descriptions in the DB.
Compare submitted values against PREVIOUS auto-gen hashes (not current):
- submitted hash == previous auto-gen hash → user didn't touch → auto-update
- submitted hash == current auto-gen hash → user set it back → un-customize
- neither match → genuinely custom → preserve
Stored as _fieldHashes: {headline: "a1b2c3d4", description: "e5f6g7h8"}
Stripped from frontend JSON-LD output alongside other internal keys.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add 'url' to per-field hash tracking (headline, name, description, jobTitle, url) - Remove unused _autoValues hidden form field - Clean up stale _autoValues/_editedFields references - Remove debug comments Dates and images excluded from tracking due to format differences (timezone conversion, path normalization) causing false positives. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The model name (cwmteacher/cwmserie) differs from the form name (teacher/serie), creating two different contexts: - Form context: com_proclaim.teacher (for Schema tab rendering) - Content event context: com_proclaim.cwmteacher (for save events) The system plugin's onContentAfterSave uses the content event context, but our isSupported() only matched form contexts. Teacher and series saves were silently skipped — schema data was never processed by our Smart Sync logic. Added content event contexts to getSchemaorgContexts(), CONTEXT_TYPE_MAP, and generateSchemaFromItem(). Messages were unaffected (model name == form name: cwmmessage). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The save event context (com_proclaim.cwmteacher) differs from the form context (com_proclaim.teacher) used when storing/loading schema data. This caused loadExistingSchema and writeSchemaToDb to use different contexts — the load found nothing, the write created a duplicate row. Added CONTEXT_CANONICAL map to normalize content event contexts to form contexts. All DB operations (load, write) now use the canonical form context consistently. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…warnings Add url (via Route::link), datePublished, dateModified, and publisher fields to Series CreativeWorkSeries schema. Wire publisher org name into sermon and series populate methods for form display. Merge fresh auto-generated fields into existing schema records so new fields appear on previously saved items. Fix ProclaimNomenuRules warnings where $view->parent could be false instead of an object. Sync translations. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Full Schema.org structured data integration with Joomla's built-in system, Smart Sync, and per-item edit protection.
Core Integration
SchemaorgServiceInterfaceinProclaimComponent— enables Schema.org tab on sermon, teacher, and series edit formsplg_schemaorg_proclaimplugin with three schema types: Sermon (CreativeWork), Teacher (Person), Series (CreativeWorkSeries)CwmschemaorgHelperon both detail and list views:ItemListschema with collection metadataSmart Sync
_fieldHashes) detects which fields the user has customized vs auto-generatedSchema Type Mapping
Organization Name
admin/forms/admin.xml— SEO & Metadata > Organization Name) overrides site name for schema.org publisher/worksFororg_namefield (admin/forms/teacher.xml) for visiting speakers — three-tier fallback: teacher → admin setting → site name10.3.0-20260318addsorg_namecolumn to#__bsms_teachersContext Normalization
com_proclaim.cwmteacher,com_proclaim.cwmserie) while forms use different contexts (com_proclaim.teacher,com_proclaim.serie). Plugin maps both to canonical form context for consistent DB storageCwmmessageModel,CwmteacherModel, andCwmserieModelupdated to load and process schema data during saveRouter Fix
ProclaimNomenuRuleswarnings where$view->parentcould befalseinstead of an object, causing "Attempt to read property on false" warnings on frontend routes without menu itemsInstallation & Build
$installActionQueue— auto-installs and enables on fresh install/upgradeTests
CmsRuntimeShims.php)Test plan
Route::link('site', ...)respecting menu items<head><head>Related
🤖 Generated with Claude Code