diff --git a/administrator/language/en-GB/plg_system_schemaorg.ini b/administrator/language/en-GB/plg_system_schemaorg.ini
index 4e13d9795cd..063a4649fa3 100644
--- a/administrator/language/en-GB/plg_system_schemaorg.ini
+++ b/administrator/language/en-GB/plg_system_schemaorg.ini
@@ -8,6 +8,7 @@ PLG_SYSTEM_SCHEMAORG_BASETYPE_DESCRIPTION="Choose whether your website represent
PLG_SYSTEM_SCHEMAORG_BASETYPE_LABEL="Base Type"
PLG_SYSTEM_SCHEMAORG_BASETYPE_OPTION_ORGANIZATION="Organization"
PLG_SYSTEM_SCHEMAORG_BASETYPE_OPTION_PERSON="Person"
+PLG_SYSTEM_SCHEMAORG_CONTACT_LABEL="Select Contact"
PLG_SYSTEM_SCHEMAORG_FIELD_SCHEMA_DESCRIPTION="Structured data is a standardised format for organising and representing information on the web. It provides a way to describe the content and meaning of data in a structured manner, making it easier for search engines and other applications to understand and process the information. More information on schema.org."
PLG_SYSTEM_SCHEMAORG_FIELD_SCHEMA_DESCRIPTION_NOT_CONFIGURED="To use the schema.org functionality, you have to configure the plugin first. Please contact an administrator of the page to get it configured."
PLG_SYSTEM_SCHEMAORG_FIELD_SCHEMA_DESCRIPTION_NOT_CONFIGURED_ADMIN="To use the schema.org functionality, you have to configure the plugin first. Please select this link to open the plugin, configure and save."
@@ -15,6 +16,8 @@ PLG_SYSTEM_SCHEMAORG_FIELD_SCHEMA_EXTEND_JED_DESC="Need more Schema type
PLG_SYSTEM_SCHEMAORG_FIELD_SCHEMA_LABEL="Schema"
PLG_SYSTEM_SCHEMAORG_FIELD_SCHEMA_TYPE_LABEL="Schema Type"
PLG_SYSTEM_SCHEMAORG_IMAGE_LABEL="Image"
+PLG_SYSTEM_SCHEMAORG_INHERIT_CONTACT="Inherited from contact"
+PLG_SYSTEM_SCHEMAORG_INHERIT_DEFAULT_CONTACT="Inherited from default contact"
PLG_SYSTEM_SCHEMAORG_NAME_LABEL="Name"
PLG_SYSTEM_SCHEMAORG_SOCIALMEDIA_LABEL="Social Media Accounts"
PLG_SYSTEM_SCHEMAORG_SOCIALMEDIA_URL_LABEL="URL"
diff --git a/build/media_source/plg_system_schemaorg/joomla.asset.json b/build/media_source/plg_system_schemaorg/joomla.asset.json
new file mode 100644
index 00000000000..8b3d56d01a3
--- /dev/null
+++ b/build/media_source/plg_system_schemaorg/joomla.asset.json
@@ -0,0 +1,20 @@
+{
+ "$schema": "https://developer.joomla.org/schemas/json-schema/web_assets.json",
+ "name": "plg_system_schemaorg",
+ "version": "__DEPLOY_VERSION__",
+ "description": "Joomla CMS",
+ "license": "GPL-2.0-or-later",
+ "assets": [
+ {
+ "name": "plg_system_schemaorg.contact",
+ "type": "script",
+ "uri": "plg_system_schemaorg/schemaorg-contact.js",
+ "dependencies": [
+ "core"
+ ],
+ "attributes": {
+ "type": "module"
+ }
+ }
+ ]
+}
diff --git a/build/media_source/plg_system_schemaorg/js/schemaorg-contact.es6.js b/build/media_source/plg_system_schemaorg/js/schemaorg-contact.es6.js
new file mode 100644
index 00000000000..521fe914444
--- /dev/null
+++ b/build/media_source/plg_system_schemaorg/js/schemaorg-contact.es6.js
@@ -0,0 +1,195 @@
+/**
+ * @copyright (C) 2025 Open Source Matters, Inc.
+ * @license GNU General Public License version 2 or later; see LICENSE.txt
+ */
+/**
+ * schemaorg-contact.js
+ * Populate placeholders from injected initialContact by baseName.
+ */
+((Joomla, document) => {
+ "use strict";
+
+ class SchemaOrgContactHandler {
+ constructor(options) {
+ this.options = Object.assign(
+ {
+ fieldMappings: {
+ name: "name",
+ email: "email_to",
+ url: "webpage",
+ streetAddress: ["address", "street"],
+ addressLocality: ["suburb", "state", "address"],
+ postalCode: "postcode",
+ addressRegion: "state",
+ addressCountry: "country",
+ telephone: "telephone",
+ mobile: "mobile",
+ fax: "fax",
+ misc: "misc",
+ position: "position",
+ },
+ },
+ options
+ );
+
+ // nested fields that must go under [address]
+ this.nestedFields = {
+ streetAddress: "address",
+ postalCode: "address",
+ addressLocality: "address",
+ addressRegion: "address",
+ addressCountry: "address",
+ };
+
+ // read injected contact from Joomla options or global
+ const opt =
+ (Joomla &&
+ Joomla.getOptions &&
+ Joomla.getOptions("plg_system_schemaorg")) ||
+ window.plg_system_schemaorg ||
+ {};
+ this.initialContact = opt.initialContact || null;
+
+ this._initOnce = false;
+ this.init();
+ }
+
+ init() {
+ if (this._initOnce) return;
+ this._initOnce = true;
+
+ if (document.readyState === "loading") {
+ document.addEventListener("DOMContentLoaded", () => this.bindEvents());
+ } else {
+ this.bindEvents();
+ }
+ }
+
+ bindEvents() {
+ // Scope to the current form
+ const form = document.querySelector("form");
+ if (!form) return;
+
+ // Contact title inputs in the form
+ const contactNameFields = form.querySelectorAll(
+ "input.js-input-title[name*='[contact]']"
+ );
+ let isPopulated = false;
+ contactNameFields.forEach((field) => {
+ if (field.value && field.value.trim() && this.initialContact) {
+ this.populatePlaceholders(field, this.initialContact);
+ isPopulated = true;
+ }
+ });
+ if (!isPopulated) {
+ contactNameFields.forEach((field) => {
+ if (this.initialContact) {
+ this.populatePlaceholders(field, this.initialContact);
+ isPopulated = true;
+ }
+ });
+ }
+ }
+
+ populatePlaceholders(contactField, contactData) {
+ if (!contactField || !contactData) return;
+
+ // Normalize simple contact object and trim strings
+ const contact = Object.assign({}, contactData);
+ let isDefaultContact = false;
+ Object.keys(contact).forEach((k) => {
+ if (typeof contact[k] === "string") contact[k] = contact[k].trim();
+ if (k === "isDefaultContact") {
+ isDefaultContact = Boolean(contact[k]);
+ }
+ });
+
+ // find the role container (use nearby grouping)
+ const roleContainer = contactField.closest(
+ ".subform-repeatable-group, fieldset, form"
+ );
+ if (!roleContainer) return;
+
+ // Parse baseName from contact field name
+ const contactName = contactField.name || "";
+ const baseMatch = contactName.match(/^(.*)\[contact\]$/);
+ if (!baseMatch) {
+ console.debug("schemaorg: contact baseName not found for", contactName);
+ return;
+ }
+ const baseName = baseMatch[1]; // e.g. jform[schema][Book][illustrator]
+
+ // For each mapping, compute target name and set placeholder
+ Object.entries(this.options.fieldMappings).forEach(
+ ([schemaField, contactKey]) => {
+ let value = "";
+
+ if (Array.isArray(contactKey)) {
+ for (const k of contactKey) {
+ if (contact[k]) {
+ value = contact[k];
+ break;
+ }
+ }
+ } else {
+ value = contact[contactKey] || "";
+ }
+
+ if (!value) return;
+ const parentKey = this.nestedFields[schemaField] || null;
+ const targetName = parentKey
+ ? `${baseName}[${parentKey}][${schemaField}]`
+ : `${baseName}[${schemaField}]`;
+
+ // find the input within the same roleContainer
+ const selector = `input[name='${targetName}'], textarea[name='${targetName}'], select[name='${targetName}']`;
+ const targetField =
+ roleContainer.querySelector(selector) ||
+ document.querySelector(selector);
+ let newValue = value;
+ const defaultContactText = Joomla.Text._(
+ "PLG_SYSTEM_SCHEMAORG_INHERIT_DEFAULT_CONTACT"
+ );
+
+ const contactText = Joomla.Text._(
+ "PLG_SYSTEM_SCHEMAORG_INHERIT_CONTACT"
+ );
+
+ if (isDefaultContact) {
+ newValue += ` — ${defaultContactText} "${contact.name}"`;
+ } else {
+ newValue += ` — ${contactText} "${contact.name}"`;
+ }
+ if (targetField) {
+ targetField.placeholder = newValue;
+ targetField.classList.add("has-contact-placeholder");
+ } else {
+ // debug if not found
+ console.debug(
+ "schemaorg: could not find target field for",
+ targetName
+ );
+ }
+ }
+ );
+
+ // emit event
+ document.dispatchEvent(
+ new CustomEvent("schemaorg:contact:loaded", {
+ detail: {
+ contactId: contact.id,
+ contactData: contact,
+ container: roleContainer,
+ },
+ })
+ );
+ }
+ }
+
+ // Initialize once
+ if (typeof Joomla !== "undefined") {
+ if (!Joomla.SchemaOrgContactHandler) {
+ Joomla.SchemaOrgContactHandler = new SchemaOrgContactHandler();
+ }
+ }
+})(window.Joomla, document);
diff --git a/plugins/system/schemaorg/schemaorg.xml b/plugins/system/schemaorg/schemaorg.xml
index 326ef254756..b81bd4e2e9e 100644
--- a/plugins/system/schemaorg/schemaorg.xml
+++ b/plugins/system/schemaorg/schemaorg.xml
@@ -20,7 +20,7 @@
-