You are looking for a way to display contact persons in Joomla 4 with Cassiopeia? I have solved the problem once with a Custom Field[^blog.astrid-guenther.de/joomla-plugins/#fields-felder---indexpluginfields-felder---] and another time with a Content Plugin[^blog.astrid-guenther.de/joomla-plugins/#inhalt-content---indexplugininhalt-content---].

First I, wrote a Custom Field for this purpose, because in the first version was desired that each article is filled with a contact person. In another installation, it was no longer necessary to force the insertion, but a more flexible application was required. After consideration I used a content plugin. The advantage of a content plugin is that you can easily apply it in modules. The custom field is linked to the main content of the display page, usually the post.

Custom Field

A custom field must be installed. For this we need an installation manifest plugins/fields/contact/contact.xml.

// https://codeberg.org/astrid/j/raw/branch/main/pkg_agpledges/j4/pkg_agpledges/src/plugins/fields/contact/contact.xml

<?xml version="1.0" encoding="utf-8" ?>
<extension type="plugin" group="fields" method="upgrade">
	<name>plg_fields_contact</name>
	<author>Astrid Günther</author>
	<creationDate>##DATE##</creationDate>
	<copyright>(C) Astrid Günther. All rights reserved.</copyright>
	<license>GNU General Public License version 2 or later; see LICENSE.txt</license>
	<authorEmail>info@astrid-guenther.de</authorEmail>
	<authorUrl>www.astrid-guenther.de</authorUrl>
	<version>##VERSION##</version>
	<description>PLG_FIELDS_CONTACT_XML_DESCRIPTION</description>
	<files>
		<filename plugin="contact">contact.php</filename>
		<folder>tmpl</folder>
		<folder>fields</folder>
		<folder>language</folder>
	</files>
</extension>

Next, we implement the plugin file. This ensures that the custom field inherits from the Joomla class \Joomla\Component\Fields\Administrator\Plugin\FieldsPlugin. In this way, it offers all the necessary interfaces and is correctly integrated into Joomla.

// https://codeberg.org/astrid/j/raw/branch/main/pkg_agpledges/j4/pkg_agpledges/src/plugins/fields/contact/contact.php

<?php
use Joomla\CMS\Form\Form;

\defined('_JEXEC') or die;

class PlgFieldsContact extends \Joomla\Component\Fields\Administrator\Plugin\FieldsPlugin
{
	public function onCustomFieldsPrepareDom($field, DOMElement $parent, Form $form)
	{
		if ($this->app->isClient('site')) {
			// The user field is not working on the front end
			return;
		}

		return parent::onCustomFieldsPrepareDom($field, $parent, $form);
	}
}

The template file organises the display in the frontend.

// https://codeberg.org/astrid/j/raw/branch/main/pkg_agpledges/j4/pkg_agpledges/src/plugins/fields/contact/tmpl/contact.php

<?php

defined('_JEXEC') or die;

use Joomla\CMS\Factory;
use Joomla\CMS\Component\ComponentHelper;
use Joomla\CMS\String\PunycodeHelper;

$value = $field->value;

if ($value == '') {
	return;
}

$app = Factory::getApplication();
$factory = $app->bootComponent('com_contact')->getMVCFactory();
$cparams = ComponentHelper::getParams('com_contact');
$model = $factory->createModel('Contact', 'Site', ['ignore_request' => true]);
$model->setState('params', $cparams);

try {
	$contact = $model->getItem($value);
} catch (\Exception $e) {
	return "";
}
?>

<div class="plg-loadcontact card mb-3" itemscope itemtype="https://schema.org/Person">
	<div class="row g-0">
			<div class="col-md-4">Ansprechpartner:
			</div>
		<div class="<?php echo $col; ?>">
			<div class="card-body">
				<p class="card-title" itemprop="name"><?php echo $contact->name; ?></p>
				<?php if ($contact->con_position && $cparams->get('show_position')) : ?>
					<p class="contact-position" itemprop="jobTitle"><?php echo $contact->con_position; ?></p>
				<?php endif; ?>
				<?php if ($contact->address || $contact->suburb  || $contact->state || $contact->country || $contact->postcode || $contact->mobile || $contact->telephone || $contact->fax || $contact->email_to || $contact->webpage) : ?>
					<address class="plg-loadcontact__address" itemprop="address" itemscope itemtype="https://schema.org/PostalAddress">
						<?php if ($contact->address && $cparams->get('show_street_address')) : ?>
							<span class="contact-street" itemprop="streetAddress">
								<?php echo nl2br($contact->address, false); ?>
							</span><br />
						<?php endif; ?>

						<?php if ($contact->postcode && $cparams->get('show_postcode')) : ?>
							<span class="contact-postcode" itemprop="postalCode">
								<?php echo $contact->postcode; ?>
							</span>
						<?php endif; ?>

						<?php if ($contact->suburb && $cparams->get('show_suburb')) : ?>
							<span class="contact-suburb" itemprop="addressLocality">
								<?php echo $contact->suburb; ?>
							</span><br />
						<?php endif; ?>

						<?php if ($contact->state && $cparams->get('show_state')) : ?>
							<span class="contact-state" itemprop="addressRegion">
								<?php echo $contact->state; ?>
							</span><br />
						<?php endif; ?>

						<?php if ($contact->country && $cparams->get('show_country')) : ?>
							<span class="contact-country" itemprop="addressCountry">
								<?php echo $contact->country; ?>
							</span><br />
						<?php endif; ?>

						<?php if ($contact->mobile && $cparams->get('show_mobile')) : ?>
							<span class="contact-mobile" itemprop="telephone">
								<?php echo $contact->mobile; ?>
							</span>
						<?php endif; ?>

						<?php if ($contact->telephone && $cparams->get('show_telephone')) : ?>
							<span class="contact-telephone" itemprop="telephone">
								<?php echo $contact->telephone; ?>
							</span><br />
						<?php endif; ?>

						<?php if ($contact->fax && $cparams->get('show_fax')) : ?>
							<span class="contact-fax" itemprop="faxNumber">
								<?php echo $contact->fax; ?>
							</span><br />
						<?php endif; ?>

						<?php if ($contact->email_to && $cparams->get('show_email')) : ?>
							<p class="contact-emailto" itemprop="email">
								<?php echo $contact->email_to; ?>
							</p>
						<?php endif; ?>

						<?php if ($contact->webpage && $cparams->get('show_webpage')) : ?>
							<p class="contact-webpage">
								<a href="<?php echo $contact->webpage; ?>" target="_blank" rel="noopener noreferrer" itemprop="url">
									<?php echo PunycodeHelper::urlToUTF8($contact->webpage); ?>
								</a>
							</p>
						<?php endif; ?>
					</address>
				<?php endif; ?>
			</div>
		</div>
	</div>
</div>

Per XML file implements additional parameters for the backend view.

// https://codeberg.org/astrid/j/raw/branch/main/pkg_agpledges/j4/pkg_agpledges/src/plugins/fields/contact/params/contact.xml

<?xml version="1.0" encoding="utf-8"?>
<form>
	<fields name="params" label="COM_FIELDS_FIELD_BASIC_LABEL">
		<fieldset name="basic">
			<field
				name="show_on"
				type="hidden"
				filter="unset"
			/>
		</fieldset>
	</fields>
</form>

Now we come to the actual field. We are not inventing the wheel. We can extend the SQL field.

// https://codeberg.org/astrid/j/raw/branch/main/pkg_agpledges/j4/pkg_agpledges/src/plugins/fields/contact/fields/contact.php

<?php
defined('JPATH_PLATFORM') or die;

jimport('joomla.form.fields.sqlfield');

class JFormFieldContact extends JFormFieldSQL
{
	public $type = 'Contact';

	public function setup(\SimpleXMLElement $element, $value, $group = null)
	{
		$return = parent::setup($element, $value, $group);

		if ($return) {
			if (empty($this->query)) {
				$query = [];
				$defaults = [];

				$query['select'] = 'id,name';
				$query['from'] = '#__contact_details';
				$query['join'] = '';
				$query['where'] = 'published = 1';
				$query['group'] = '';
				$query['order'] = 'name';

				$this->query = $this->processQuery($query, null, null);
			}

			$this->keyField = 'id';
			$this->valueField = 'name';
			$this->translate = false;
			$this->header = false;
		}

		return $return;
	}
}

Last but not least, we offer files that simplify translation into different languages.

I wrote more information about language files at "Using language files"[^blog.astrid-guenther.de/sprachdateien-nutzen/].

// https://codeberg.org/astrid/j/raw/branch/main/pkg_agpledges/j4/pkg_agpledges/src/plugins/fields/contact/language/en-GB/plg_fields_contact.ini

PLG_FIELDS_CONTACT="Fields - Contact"
PLG_FIELDS_CONTACT_XML_DESCRIPTION="This plugin lets you create new fields of type 'contact' in any extensions where custom fields are supported."

PLG_FIELDS_CONTACT_DEFAULT_VALUE_LABEL="Default Contact"
PLG_FIELDS_CONTACT_LABEL="Contact (%s)"
// https://codeberg.org/astrid/j/raw/branch/main/pkg_agpledges/j4/pkg_agpledges/src/plugins/fields/contact/language/en-GB/plg_fields_contact.sys.ini

PLG_FIELDS_CONTACT="Fields - Contact"
PLG_FIELDS_CONTACT_XML_DESCRIPTION="This plugin lets you create new fields of type 'contact' in any extensions where custom fields are supported."

Now all files are ready. You can pack the files for the Custom Field into an installation file and install them. If you have never done this before, you can also copy the files yourself into a Joomla website and install them via Discover. You can see exactly where you have to copy the files by the path where I saved them when I developed them. This name is always above the source code.

It is important that the Custom Field is activated after the installation.

add a contact person via custom field - activate the plugin

When creating a custom field in the Joomla backend, the types of all activated plugins are offered.

add a contact person via custom field - create custom field

Beim Anlegen eines Artikels kann der Kontakt nun über das Custom Field Formular Feld ausgewählt werden.

Ein Custom Field ist nicht auf die Erweiterung com_content beschränkt. Es kann in jeder Komponente verwendet werden, die Custom Fields unterstützt.

add a contact person via custom field - fill custom field in backend

Das nachfolgende Bild zeigt die Anzeige des Custom Fields im Frontend, falls die Anzeige nicht per Override verändert wurde.

add a contact person via custom field - view of custom field in frontend

Content Plugin

A content plugin must also be installed. Therefore, we also start here with the installation manifest.

// https://codeberg.org/astrid/j/raw/branch/main/pkg_agpledges/j4/pkg_agpledges/src/plugins/content/loadcontact/loadcontact.xml

<?xml version="1.0" encoding="utf-8"?>
<extension type="plugin" group="content" method="upgrade">
    <name>plg_content_loadcontact</name>
	<author>Astrid Günther</author>
	<creationDate>##DATE##</creationDate>
	<copyright>(C) Astrid Günther. All rights reserved.</copyright>
	<license>GNU General Public License version 2 or later; see LICENSE.txt</license>
	<authorEmail>info@astrid-guenther.de</authorEmail>
	<authorUrl>www.astrid-guenther.de</authorUrl>
	<version>##VERSION##</version>
    <description>PLG_CONTENT_LOADCONTACT_XML_DESCRIPTION</description>
    <files>
        <filename plugin="loadcontact">loadcontact.php</filename>
        <folder>tmpl</folder>
    </files>
</extension>
// https://codeberg.org/astrid/j/raw/branch/main/pkg_agpledges/j4/pkg_agpledges/src/plugins/content/loadcontact/loadcontact.php

<?php
use Joomla\CMS\Factory;
use Joomla\CMS\Component\ComponentHelper;
use Joomla\CMS\Plugin\CMSPlugin;
use Joomla\CMS\Plugin\PluginHelper;

\defined('_JEXEC') or die;

class PlgContentLoadcontact extends CMSPlugin
{
	public function onContentPrepare($context, &$article, &$params, $page = 0)
	{
		if ($context === 'com_finder.indexer') {
			return;
		}

		if (strpos($article->text, 'loadcontact') === false) {
			return;
		}

		$regexcontid = '/{loadcontact\s([1-9][0-9]*)}/i';

		preg_match_all($regexcontid, $article->text, $matchescontid, PREG_SET_ORDER);

		if ($matchescontid) {
			foreach ($matchescontid as $match) {
				$id     = trim($match[1]);
				$output = $this->_loadcontid($id);

				if (($start = strpos($article->text, $match[0])) !== false) {
					$article->text = substr_replace($article->text, $output, $start, strlen($match[0]));
				}
			}
		}
	}

	protected function _loadcontid($id)
	{
		$app = Factory::getApplication();
		$factory = $app->bootComponent('com_contact')->getMVCFactory();
		$cparams = ComponentHelper::getParams('com_contact');

		$model = $factory->createModel('Contact', 'Site', ['ignore_request' => true]);

		// Get item
		$model->setState('params', $cparams);
		$contact = $model->getItem($id);

		if ($contact->published == 1) {
			ob_start();
			include PluginHelper::getLayoutPath('content', 'loadcontact');

			return (string)ob_get_clean();
		}
	}
}
// https://codeberg.org/astrid/j/raw/branch/main/pkg_agpledges/j4/pkg_agpledges/src/plugins/content/loadcontact/tmpl/default.php

<?php
use Joomla\CMS\String\PunycodeHelper;

\defined('_JEXEC') or die;
?>
<div class="plg-loadcontact card mb-3" itemscope itemtype="https://schema.org/Person">
	<div class="row g-0">
			<div class="col-md-4">Ansprechpartner:
			</div>
		<div class="<?php echo $col; ?>">
			<div class="card-body">
				<p class="card-title" itemprop="name"><?php echo $contact->name; ?></p>
				<?php if ($contact->con_position && $cparams->get('show_position')) : ?>
					<p class="contact-position" itemprop="jobTitle"><?php echo $contact->con_position; ?></p>
				<?php endif; ?>
				<?php if ($contact->address || $contact->suburb  || $contact->state || $contact->country || $contact->postcode || $contact->mobile || $contact->telephone || $contact->fax || $contact->email_to || $contact->webpage) : ?>
					<address class="plg-loadcontact__address" itemprop="address" itemscope itemtype="https://schema.org/PostalAddress">
						<?php if ($contact->address && $cparams->get('show_street_address')) : ?>
							<span class="contact-street" itemprop="streetAddress">
								<?php echo nl2br($contact->address, false); ?>
							</span><br />
						<?php endif; ?>

						<?php if ($contact->postcode && $cparams->get('show_postcode')) : ?>
							<span class="contact-postcode" itemprop="postalCode">
								<?php echo $contact->postcode; ?>
							</span>
						<?php endif; ?>

						<?php if ($contact->suburb && $cparams->get('show_suburb')) : ?>
							<span class="contact-suburb" itemprop="addressLocality">
								<?php echo $contact->suburb; ?>
							</span><br />
						<?php endif; ?>

						<?php if ($contact->state && $cparams->get('show_state')) : ?>
							<span class="contact-state" itemprop="addressRegion">
								<?php echo $contact->state; ?>
							</span><br />
						<?php endif; ?>

						<?php if ($contact->country && $cparams->get('show_country')) : ?>
							<span class="contact-country" itemprop="addressCountry">
								<?php echo $contact->country; ?>
							</span><br />
						<?php endif; ?>

						<?php if ($contact->mobile && $cparams->get('show_mobile')) : ?>
							<span class="contact-mobile" itemprop="telephone">
								<?php echo $contact->mobile; ?>
							</span>
						<?php endif; ?>

						<?php if ($contact->telephone && $cparams->get('show_telephone')) : ?>
							<span class="contact-telephone" itemprop="telephone">
								<?php echo $contact->telephone; ?>
							</span><br />
						<?php endif; ?>

						<?php if ($contact->fax && $cparams->get('show_fax')) : ?>
							<span class="contact-fax" itemprop="faxNumber">
								<?php echo $contact->fax; ?>
							</span><br />
						<?php endif; ?>

						<?php if ($contact->email_to && $cparams->get('show_email')) : ?>
							<p class="contact-emailto" itemprop="email">
								<?php echo $contact->email_to; ?>
							</p>
						<?php endif; ?>

						<?php if ($contact->webpage && $cparams->get('show_webpage')) : ?>
							<p class="contact-webpage">
								<a href="<?php echo $contact->webpage; ?>" target="_blank" rel="noopener noreferrer" itemprop="url">
									<?php echo PunycodeHelper::urlToUTF8($contact->webpage); ?>
								</a>
							</p>
						<?php endif; ?>
					</address>
				<?php endif; ?>
			</div>
		</div>
	</div>
</div>

Last but not least, we offer files that simplify translation into different languages.

I wrote more information about language files at "Using language files"[^blog.astrid-guenther.de/sprachdateien-nutzen/].

// https://codeberg.org/astrid/j/raw/branch/main/pkg_agpledges/j4/pkg_agpledges/src/plugins/content/loadcontact/language/en-GB/plg_content_loadcontact.ini

PLG_CONTENT_LOADCONTACT="Content - Load Contact"
PLG_CONTENT_LOADCONTACT_XML_DESCRIPTION="Within content this plugin loads a Contact by ID, Syntax: {loadcontact 1}. It display contact information (position, address, phone number, etc.) as defined in the contact component. The default layout is a Bootstrap card, but it can be overwritten by a template override."
// https://codeberg.org/astrid/j/raw/branch/main/pkg_agpledges/j4/pkg_agpledges/src/plugins/content/loadcontact/language/en-GB/plg_content_loadcontact.sys.ini

PLG_CONTENT_LOADCONTACT="Content - Load Contact"
PLG_CONTENT_LOADCONTACT_XML_DESCRIPTION="Within content this plugin loads a Contact by ID, Syntax: {loadcontact 1}. It display contact information (position, address, phone number, etc.) as defined in the contact component. The default layout is a Bootstrap card, but it can be overwritten by a template override."

add a contact person via content plugin - activate the plugin

add a contact person via content plugin - use code in content

add a contact person via content plugin - view in frontend