diff --git a/administrator/components/com_config/forms/application.xml b/administrator/components/com_config/forms/application.xml index 3bd48543e20e8..85b4a8e8f51e5 100644 --- a/administrator/components/com_config/forms/application.xml +++ b/administrator/components/com_config/forms/application.xml @@ -717,6 +717,75 @@ +
+ + + + + + + + + + + + + + + + + + + + + + + + +
+
diff --git a/administrator/components/com_config/tmpl/application/default.php b/administrator/components/com_config/tmpl/application/default.php index 1411ed14498c9..699dbb6175cd5 100644 --- a/administrator/components/com_config/tmpl/application/default.php +++ b/administrator/components/com_config/tmpl/application/default.php @@ -67,6 +67,7 @@ loadTemplate('site'); ?> loadTemplate('metadata'); ?> loadTemplate('seo'); ?> + loadTemplate('seo_metadata'); ?> loadTemplate('cookie'); ?>
diff --git a/administrator/components/com_config/tmpl/application/default_seo_metadata.php b/administrator/components/com_config/tmpl/application/default_seo_metadata.php new file mode 100644 index 0000000000000..f26a1874e22e3 --- /dev/null +++ b/administrator/components/com_config/tmpl/application/default_seo_metadata.php @@ -0,0 +1,23 @@ +name = Text::_('COM_CONFIG_SEO_METADATA_SETTINGS'); +$this->fieldsname = 'seo_metadata'; +$this->formclass = 'options-grid-form options-grid-form-full'; +$this->description = Text::_('COM_CONFIG_SEO_METADATA_SETTINGS_DESCRIPTION'); + +echo LayoutHelper::render('joomla.content.options_default', $this); + +// We don't use the description in any other options groups - remove it so the remaining groups don't use it +unset($this->description); diff --git a/administrator/language/en-GB/en-GB.com_config.ini b/administrator/language/en-GB/en-GB.com_config.ini index c87c3a3a89570..f24e7fa7e9f6d 100644 --- a/administrator/language/en-GB/en-GB.com_config.ini +++ b/administrator/language/en-GB/en-GB.com_config.ini @@ -176,6 +176,17 @@ COM_CONFIG_SENDMAIL_SUBJECT="Test mail from {SITENAME}" COM_CONFIG_SENDMAIL_SUCCESS="The email was sent to %s using %s. You should check that you've received the test email." COM_CONFIG_SENDMAIL_SUCCESS_FALLBACK="The email was sent to %s but using %s as fallback. You should check that you've received the test email." COM_CONFIG_SEO_SETTINGS="SEO Settings" +COM_CONFIG_SEO_METADATA_CONTENT_OWNER_LABEL="Type of Content Owner" +COM_CONFIG_SEO_METADATA_ENABLE_LABEL="Enable SEO Metadata" +COM_CONFIG_SEO_METADATA_INDIVIDUAL_NAME="Individual Name" +COM_CONFIG_SEO_METADATA_ORGANISATION_NAME="Company Name" +COM_CONFIG_SEO_METADATA_ORGANISATION_LOGO="Organisation Logo" +COM_CONFIG_SEO_METADATA_OWNER_ORGANISATION="Organisation" +COM_CONFIG_SEO_METADATA_OWNER_INDIVIDUAL="Individual" +COM_CONFIG_SEO_METADATA_INDIVIDUAL_URL="Individual URL" +COM_CONFIG_SEO_METADATA_SETTINGS="SEO MetaData" +; Translation Teams can replace the search engines listed here with local search engines that use schema.org for rankings +COM_CONFIG_SEO_METADATA_SETTINGS_DESCRIPTION="This information is required for many search engines (such as Google, Yahoo etc) and whilst not shown on the screen is public to any search engines indexing your site. Please see this article for more information" COM_CONFIG_SERVER="Server" COM_CONFIG_SERVER_SETTINGS="Server Settings" COM_CONFIG_SESSION_SETTINGS="Session Settings" diff --git a/components/com_contact/View/Contact/HtmlView.php b/components/com_contact/View/Contact/HtmlView.php index 8d0e1b0df71b8..d9da0d16030fa 100644 --- a/components/com_contact/View/Contact/HtmlView.php +++ b/components/com_contact/View/Contact/HtmlView.php @@ -11,6 +11,7 @@ defined('_JEXEC') or die; +use Joomla\CMS\Application\CMSApplicationInterface; use Joomla\CMS\Categories\Categories; use Joomla\CMS\Factory; use Joomla\CMS\Helper\TagsHelper; @@ -20,7 +21,12 @@ use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView; use Joomla\CMS\Plugin\PluginHelper; use Joomla\CMS\Router\Route; +use Joomla\CMS\Uri\Uri; +use Joomla\CMS\User\User; use Joomla\Component\Contact\Site\Helper\Route as ContactHelperRoute; +use Spatie\SchemaOrg\Person; +use Spatie\SchemaOrg\PostalAddress; +use Spatie\SchemaOrg\Schema; /** * HTML Contact View class for the Contact component @@ -107,12 +113,13 @@ class HtmlView extends BaseHtmlView * * @param string $tpl The name of the template file to parse; automatically searches through the template paths. * - * @return mixed A string if successful, otherwise an Error object. + * @return void + * @throws \Exception */ public function display($tpl = null) { $app = Factory::getApplication(); - $user = Factory::getUser(); + $user = $app->getIdentity(); $state = $this->get('State'); $item = $this->get('Item'); $this->form = $this->get('Form'); @@ -181,7 +188,7 @@ public function display($tpl = null) $app->enqueueMessage(Text::_('JERROR_ALERTNOAUTHOR'), 'error'); $app->setHeader('status', 403, true); - return false; + return; } $options['category_id'] = $item->catid; @@ -383,7 +390,7 @@ public function display($tpl = null) $item->text = $item->misc; } - $app->triggerEvent('onContentPrepare', array ('com_contact.contact', &$item, &$this->params, $offset)); + $app->triggerEvent('onContentPrepare', array('com_contact.contact', &$item, &$this->params, $offset)); // Store the events for later $item->event = new \stdClass; @@ -406,7 +413,7 @@ public function display($tpl = null) if ($item->params->get('show_user_custom_fields') && $item->user_id && $contactUser = Factory::getUser($item->user_id)) { $contactUser->text = ''; - $app->triggerEvent('onContentPrepare', array ('com_users.user', &$contactUser, &$item->params, 0)); + $app->triggerEvent('onContentPrepare', array('com_users.user', &$contactUser, &$item->params, 0)); if (!isset($contactUser->jcfields)) { @@ -457,8 +464,9 @@ public function display($tpl = null) } $this->_prepareDocument(); + $this->addJsonSchema(); - return parent::display($tpl); + parent::display($tpl); } /** @@ -574,4 +582,104 @@ protected function _prepareDocument() } } } + + /** + * Prepares the document. + * + * @return void + */ + private function addJsonSchema() + { + // Note we don't display tags here as the keywords property isn't valid for a person + $schema = Schema::person() + ->if( + $this->item->params->get('show_name'), + function (Person $schema) { + $schema->name($this->item->name); + } + ) + ->if( + $this->item->image && $this->item->params->get('show_image'), + function (Person $schema) { + $schema->image(Uri::root() . $this->item->image); + } + ) + ->if( + $this->item->params->get('show_position'), + function (Person $schema) { + $schema->jobTitle($this->item->con_position); + } + ) + ->if( + $this->item->params->get('address_check') > 0, + function (Person $schema) { + $schema->address( + Schema::postalAddress() + ->if( + $this->item->address && $this->params->get('show_street_address'), + function (PostalAddress $schema) { + $schema->streetAddress($this->item->address); + } + ) + ->if( + $this->item->suburb && $this->params->get('show_suburb'), + function (PostalAddress $schema) { + $schema->addressLocality($this->item->suburb); + } + ) + ->if( + $this->item->state && $this->params->get('show_state'), + function (PostalAddress $schema) { + $schema->addressRegion($this->item->state); + } + ) + ->if( + $this->item->postcode && $this->params->get('show_postcode'), + function (PostalAddress $schema) { + $schema->postalCode($this->item->postcode); + } + ) + ->if( + $this->item->country && $this->params->get('show_country'), + function (PostalAddress $schema) { + $schema->addressCountry($this->item->country); + } + ) + ); + } + ) + // TODO: Should we expose the raw email like this? + ->if( + $this->item->params->get('show_email') === '1', + function (Person $schema) { + $schema->email($this->item->email_raw); + } + ) + ->if( + $this->item->telephone && $this->params->get('show_telephone'), + function (Person $schema) { + $schema->telephone($this->item->telephone); + } + ) + ->if( + $this->item->fax && $this->params->get('show_fax'), + function (Person $schema) { + $schema->faxNumber($this->item->fax); + } + ) + ->if( + $this->item->mobile && $this->params->get('show_mobile'), + function (Person $schema) { + $schema->telephone($this->item->mobile); + } + ) + ->if( + $this->item->webpage && $this->params->get('show_webpage'), + function (Person $schema) { + $schema->url($this->item->webpage); + } + ); + + $this->document->addScriptDeclaration(json_encode($schema, JDEBUG ? JSON_PRETTY_PRINT : 0), 'application/ld+json'); + } } diff --git a/components/com_contact/tmpl/contact/default.php b/components/com_contact/tmpl/contact/default.php index c9dd8151a33d8..a8bc37ced2989 100644 --- a/components/com_contact/tmpl/contact/default.php +++ b/components/com_contact/tmpl/contact/default.php @@ -21,7 +21,7 @@ $tparams = $this->item->params; ?> -
+
get('show_page_heading')) : ?>

escape($tparams->get('page_heading')); ?> @@ -34,7 +34,7 @@ item->published == 0) : ?> - item->name; ?> + item->name; ?>

@@ -77,14 +77,14 @@ item->image && $tparams->get('show_image')) : ?>
- item->image, htmlspecialchars($this->item->name, ENT_QUOTES, 'UTF-8'), array('itemprop' => 'image')); ?> + item->image, htmlspecialchars($this->item->name, ENT_QUOTES, 'UTF-8')); ?>
item->con_position && $tparams->get('show_position')) : ?>
:
-
+
item->con_position; ?>
diff --git a/components/com_contact/tmpl/contact/default_address.php b/components/com_contact/tmpl/contact/default_address.php index c367653a009e5..8a7c838f6cd5a 100644 --- a/components/com_contact/tmpl/contact/default_address.php +++ b/components/com_contact/tmpl/contact/default_address.php @@ -16,7 +16,7 @@ * jicon-text, jicon-none, jicon-icon */ ?> -
+
params->get('address_check') > 0) && ($this->item->address || $this->item->suburb || $this->item->state || $this->item->country || $this->item->postcode)) : ?>
@@ -27,7 +27,7 @@ item->address && $this->params->get('show_street_address')) : ?>
- + item->address); ?>
@@ -36,7 +36,7 @@ item->suburb && $this->params->get('show_suburb')) : ?>
- + item->suburb; ?>
@@ -44,7 +44,7 @@ item->state && $this->params->get('show_state')) : ?>
- + item->state; ?>
@@ -52,7 +52,7 @@ item->postcode && $this->params->get('show_postcode')) : ?>
- + item->postcode; ?>
@@ -60,7 +60,7 @@ item->country && $this->params->get('show_country')) : ?>
- + item->country; ?>
@@ -70,7 +70,7 @@ item->email_to && $this->params->get('show_email')) : ?>
- + params->get('marker_email')); ?>
@@ -88,7 +88,7 @@
- + item->telephone; ?>
@@ -100,7 +100,7 @@
- + item->fax; ?>
@@ -112,7 +112,7 @@
- + item->mobile; ?>
@@ -124,7 +124,7 @@
- item->webpage); ?>
diff --git a/components/com_content/View/Article/HtmlView.php b/components/com_content/View/Article/HtmlView.php index 3cfcb7094d736..009b5358afee6 100644 --- a/components/com_content/View/Article/HtmlView.php +++ b/components/com_content/View/Article/HtmlView.php @@ -11,9 +11,12 @@ defined('_JEXEC') or die; +use Joomla\CMS\Application\CMSApplicationInterface; use Joomla\CMS\Categories\Categories; +use Joomla\CMS\Event\ContentPrepareJsonSchemaEvent; use Joomla\CMS\Factory; use Joomla\CMS\Helper\TagsHelper; +use Joomla\CMS\HTML\HTMLHelper; use Joomla\CMS\Language\Associations; use Joomla\CMS\Language\Text; use Joomla\CMS\Layout\FileLayout; @@ -24,6 +27,9 @@ use Joomla\CMS\Uri\Uri; use Joomla\Component\Content\Site\Helper\AssociationHelper; use Joomla\Component\Content\Site\Helper\RouteHelper; +use Spatie\SchemaOrg\Article; +use Spatie\SchemaOrg\Person; +use Spatie\SchemaOrg\Schema; /** * HTML Article View class for the Content component @@ -81,13 +87,16 @@ class HtmlView extends BaseHtmlView * * @param string $tpl The name of the template file to parse; automatically searches through the template paths. * - * @return mixed A string if successful, otherwise an Error object. + * @return void + * @throws \Exception */ public function display($tpl = null) { if ($this->getLayout() == 'pagebreak') { - return parent::display($tpl); + parent::display($tpl); + + return; } $app = Factory::getApplication(); @@ -256,6 +265,7 @@ public function display($tpl = null) $this->pageclass_sfx = htmlspecialchars($this->item->params->get('pageclass_sfx')); $this->_prepareDocument(); + $this->addJsonSchema($app); parent::display($tpl); } @@ -389,4 +399,107 @@ protected function _prepareDocument() $this->document->setMetaData('robots', 'noindex, nofollow'); } } + + /** + * Prepares the document. + * + * @param CMSApplicationInterface $app The application object + * + * @return void + */ + private function addJsonSchema($app) + { + if (!$app->get('enable_seo_metadata', 0)) + { + return; + } + + $images = json_decode($this->item->images); + $articleLanguage = ($this->item->language === '*') ? $app->get('language') : $this->item->language; + $authorised = $app->getIdentity()->getAuthorisedViewLevels(); + $keywords = []; + + foreach ($this->item->tags->itemTags as $tag) + { + if (in_array($tag->access, $authorised)) + { + $keywords[] = $this->escape($tag->title); + } + } + + $schema = Schema::article() + ->articleBody($this->item->text) + ->if( + $this->item->params->get('show_title'), + function (Article $schema) { + $schema->headline($this->escape($this->item->title)); + } + ) + ->inLanguage($articleLanguage) + ->dateCreated(HTMLHelper::_('date', $this->item->created, "Y-m-d")) + ->dateModified(HTMLHelper::_('date', $this->item->modified, "Y-m-d")) + ->datePublished(HTMLHelper::_('date', $this->item->publish_up, "Y-m-d")) + ->if( + $this->item->params->get('show_author') === '1' && !empty($this->item->author), + function (Article $schema) { + $schema->author( + Schema::Person() + ->name($this->item->created_by_alias ?: $this->item->author) + ->if( + $this->item->params->get('link_author'), + function (Person $schema) { + $schema->url($this->item->contact_link); + } + ) + ); + } + ); + + if ($app->get('sef_owner') === 0) + { + $schema->publisher( + Schema::Person() + ->name($app->get('sef_individual')) + ->url($app->get('sef_individual_url')) + ); + } + else + { + $schema->publisher( + Schema::Organisation() + ->name($app->get('sef_organisation')) + ->image( + Schema::imageObject() + ->url($app->get('sef_organisation_logo')) + ) + ); + } + + // TODO: Decide whether tags are keywords or whether to use the existing meta-keywords + if (!empty($keywords)) + { + $schema->keywords($keywords); + } + + if (!empty($images->image_fulltext)) + { + $schema->image( + Schema::imageObject() + ->url($images->image_fulltext) + ); + } + + $event = new ContentPrepareJsonSchemaEvent( + 'onContentPrepareJsonSchema', + [ + 'subject' => $this->item, + 'item' => $this->item + ] + ); + + /** @var ContentPrepareJsonSchemaEvent $result */ + $result = $app->triggerEvent('onContentPrepareJsonSchema', $event); + + $this->document->addScriptDeclaration(json_encode($result->getSchema(), JDEBUG ? JSON_PRETTY_PRINT : 0), 'application/ld+json'); + } } diff --git a/components/com_content/tmpl/article/default.php b/components/com_content/tmpl/article/default.php index 5c8a58052d3a3..81ae481ba0d29 100644 --- a/components/com_content/tmpl/article/default.php +++ b/components/com_content/tmpl/article/default.php @@ -25,14 +25,15 @@ $images = json_decode($this->item->images); $urls = json_decode($this->item->urls); $canEdit = $params->get('access-edit'); -$user = Factory::getUser(); +$user = Factory::getApplication()->getIdentity(); $info = $params->get('info_block_position', 0); +$images = json_decode($this->item->images); + // Check if associations are implemented. If they are, define the parameter. $assocParam = (Associations::isEnabled() && $params->get('show_associations')); ?> -
- +
params->get('show_page_heading')) : ?>