Zu meinem eigenen Verständnis untersuche ich hier das JOOMLA-eigene Template mit dem Namen Cassiopeia. Im Anschluss (in einem eigenen Beitrag) will ich dann ein eigenes Template erstellen. Wir schauen mal, ob wir den WirrWarr (sieht im ersten Blick so aus) entzerrt bekommen. Wenn jemand Fehler findet oder eine Ergänzung hat, kann mich gerne kontaktieren und ich werde dieses dann einarbeiten.

Warum ich dies veröffentliche?
Leider habe ich keine einzige gute deutsche Dokumentation über die Erstellung von Templates gefunden. Vielleicht bin ich ja nicht der einzigste, den dies brenndend interessiert und schreibe meine Erkenntnisse nicht in lokale Dateien, sondern gebe es der Nachwelt weiter ;-)

VIEL SPAß !!!

<?php
/**
 * @package     Joomla.Site
 * @subpackage  Templates.cassiopeia
 *
 * @copyright   (C) 2017 Open Source Matters, Inc. <https://www.joomla.org>
 * @license     GNU General Public License version 2 or later; see LICENSE.txt
 */

PHP wird eröffnet und einleitend mit Kommentaren zu (c) und Lizenz gefüllt.

defined('_JEXEC') or die;

Hiermit wird verhindert, dass das Script nur von Joomla aufgerufen werden kann.

use Joomla\CMS\Factory;
use Joomla\CMS\HTML\HTMLHelper;
use Joomla\CMS\Language\Text;
use Joomla\CMS\Uri\Uri;
/** @var Joomla\CMS\Document\HtmlDocument $this */

Hiermit werden alle nötigen Klassen importiert. Hierbei ist 'Joomla\CMS\...' der Namespace; 'Factory', HTMLHelper', 'Text' und 'Uri' sind die Klassen.
Der Kommantar '/** @var Joomla\CMS\Document\HtmlDocument $this */' dient als Info und gibt an, dass die Variable '$this' aus der Klasse 'HtmlDocument' im Namespace 'Joomla\CMS\Document' ist.

$app = Factory::getApplication();
$wa  = $this->getWebAssetManager();

So langsam geht´s ans Eingemachte. Nun werden 2 Variablen ($app und $wa) initialisiert.
$app = Wird mit der getApplication-Methode aus der Methode Factory-Klasse belegt, welche verschiedene Systemobjekte Wie DB-Verbindungen und zu Dokumenten abrufen kann.
$wa = Wird mit der WebAssetManager-Klasse belegt. Sie kann styls, skripte und Bilder laden und verwalten.

$this->addHeadLink(HTMLHelper::_('image', 'joomla-favicon.svg', '', [], true, 1), 'icon', 'rel', ['type' => 'image/svg+xml']);
$this->addHeadLink(HTMLHelper::_('image', 'favicon.ico', '', [], true, 1), 'alternate icon', 'rel', ['type' => 'image/vnd.microsoft.icon']);
$this->addHeadLink(HTMLHelper::_('image', 'joomla-favicon-pinned.svg', '', [], true, 1), 'mask-icon', 'rel', ['color' => '#000']);

Hiermit werden die FavIcons geladen und später über die jdoc:include type="metas" in den Header eingebunden.
Es wird die HTMLHelper-Klasse genutzt, um die Grafiken per addHeadLink einzufügen

$option   = $app->input->getCmd('option', '');
$view     = $app->input->getCmd('view', '');
$layout   = $app->input->getCmd('layout', '');
$task     = $app->input->getCmd('task', '');
$itemid   = $app->input->getCmd('Itemid', '');
$sitename = htmlspecialchars($app->get('sitename'), ENT_QUOTES, 'UTF-8');
$menu     = $app->getMenu()->getActive();
$pageclass = $menu !== null ? $menu->getParams()->get('pageclass_sfx', '') : '';

Nun werden einige Variablen belegt.
$option: wird befüllt mit dem Namen der Komponente die ausgeführt werden soll (com_content = Content-Manager)
$view: die Option 'view' wird benutzt, um anzugeben wie was ausgegeben wird
$layout: gibt das zu benutzende Layout an
$task: hier wird angegeben welche Aufgabe oder Aktion statt finden soll (zB edit)
$itemid: gibt an, welche ID des Menüeintrages benutzt wird
$sitename: Hier wird der Seitenname aus Joomla ausgelesen. Dazu wird jedoch die htmlspecialchars verwendet, um SchadCode zu vermeiden
$menu: Hier wird direkt ein ganzes Objekt übergeben, welches das aktive Menü wiederspiegelt
$pageclass: Wird aus dem Menü-Objekt abgefragt. Wenn der Eintrag NICHT Null ist, wird dieser mit dem Inhalt gefüllt, ansonsten ein leerer String

$paramsColorName = $this->params->get('colorName', 'colors_standard');
$assetColorName  = 'theme.' . $paramsColorName;
$wa->registerAndUseStyle($assetColorName, 'media/templates/site/cassiopeia/css/global/' . $paramsColorName . '.css');

Jetzt werden Daten zum Farbchema ausgelesen. Diese sind im Template-Backend einzustellen und wurden über die templateDetails eingefügt.
$paramsColorName: Hier wurde über die templateDetails die Auswahlen (colors_standard und colors_alternative) bereit gestellt. Ist nichts eingestellt, wird automatisch colors_standard gewählt.
$assetColorName: Nun wird der CSS-Datei-Name zusammengestellt
$wa->registerAndUseStyle: Hiermit wird Joomla mitgeteilt, welcher Name und welche CSS-Datei bereitgestellt und genutzt werden soll

$paramsFontScheme = $this->params->get('useFontScheme', false);
$fontStyles       = '';

Hier wird geschaut, ob im Template eine bestimmte Schriftart eingetragen wurde und wird dann in die Variable geladen

if ($paramsFontScheme) {
  if (stripos($paramsFontScheme, 'https://') === 0) {
    $this->getPreloadManager()->preconnect('https://fonts.googleapis.com/', ['crossorigin' => 'anonymous']);
    $this->getPreloadManager()->preconnect('https://fonts.gstatic.com/', ['crossorigin' => 'anonymous']);
    $this->getPreloadManager()->preload($paramsFontScheme, ['as' => 'style', 'crossorigin' => 'anonymous']);
    $wa->registerAndUseStyle('fontscheme.current', $paramsFontScheme, [], ['media' => 'print', 'rel' => 'lazy-stylesheet', 'onload' => 'this.media=\'all\'', 'crossorigin' => 'anonymous']);
    if (preg_match_all('/family=([^?:]*):/i', $paramsFontScheme, $matches) > 0) {
      $fontStyles = '--cassiopeia-font-family-body: "' . str_replace('+', ' ', $matches[1][0]) . '", sans-serif;
        --cassiopeia-font-family-headings: "' . str_replace('+', ' ', isset($matches[1][1]) ? $matches[1][1] : $matches[1][0]) . '", sans-serif;
        --cassiopeia-font-weight-normal: 400;
        --cassiopeia-font-weight-headings: 700;';
    }
  } else {
    $wa->registerAndUseStyle('fontscheme.current', $paramsFontScheme, ['version' => 'auto'], ['media' => 'print', 'rel' => 'lazy-stylesheet', 'onload' => 'this.media=\'all\'']);
    $this->getPreloadManager()->preload($wa->getAsset('style', 'fontscheme.current')->getUri() . '?' . $this->getMediaVersion(), ['as' => 'style']);
  }
}

Zuerst wird geprüft, ob die $paramaFontScheme einen Wert hat.
Ist dies der Fall, geht es in eine weitere Abfrage: Ob der Inhalt der Variablen https:// beinhaltet und 0 zurück wirft.
Wenn auch dies der Fall ist, findet ein Preload zu google statt und stellt eine Verbindung her. Anschließend wird die paramsFontScheme unter fontscheme.current registriert und benutzt (im Header geladen).
In der nächsten If-Abfrage wird geprüft, ob in $paramsFontScheme 'family' vorkommt. Sollte dies der Fall sein, wird das Ergebnis in $matches gespeichert. Danach wird in die Variable $fontStyles einige CSS-Variablen geschrieben.
Sollte in der 2ten If-Abfrage kein Treffer sein (sprich: es soll keine externe Schriftart geladen werden), wird im Else-Zweig die zu nutzende Schriftart registriert und benutzt (im Header geladen) und auch vorgeladen.

$wa->usePreset('template.cassiopeia.' . ($this->direction === 'rtl' ? 'rtl' : 'ltr'))
  ->useStyle('template.active.language')
  ->useStyle('template.user')
  ->useScript('template.user')
  ->addInlineStyle(":root {
  --hue: 214;
  --template-bg-light: #f0f4fb;
  --template-text-dark: #495057;
  --template-text-light: #ffffff;
  --template-link-color: #2a69b8;
  --template-special-color: #001B4C;
  $fontStyles
}");

Nun werden alle css, js und InlineStyles gesetzt.

$wa->registerStyle('template.active', '', [], [], ['template.cassiopeia.' . ($this->direction === 'rtl' ? 'rtl' : 'ltr')]);

Jetzt wird die cassiopeia + direction als active registriert.

if ($this->params->get('logoFile')) {
  $logo = '<img src="' . Uri::root(true) . '/' . htmlspecialchars($this->params->get('logoFile'), ENT_QUOTES) . '" alt="' . $sitename . '">';
} elseif ($this->params->get('siteTitle')) {
  $logo = '<span title="' . $sitename . '">' . htmlspecialchars($this->params->get('siteTitle'), ENT_COMPAT, 'UTF-8') . '</span>';
} else {
  $logo = HTMLHelper::_('image', 'logo.svg', $sitename, ['class' => 'logo d-inline-block'], true, 0);
}

Kümmern wir uns nun um die Einbindung des Logos.
Zuerst wird geprüft, ob im Template-Backend ein logoFile gesetzt ist. Ist dies der Fall, wird ein IMG-Tag zusammen gesetzt und in die Variable $logo geschrieben.
Ist dies nicht der Fall, wird geprüft ob der siteTitle gesetzt ist. Ist dies der Fall, wird ein SPAN-Tag zusammen gesetzt aus sitename und siteTitle und in $logo gespeichert.
Trifft beides nicht zu, wird über den HTMLHelper ein IMAGE-Tag erzeugt welches das logo.svg mit dem alt-tag sitename und einer definierten CSS_Klasse.

$hasClass = '';
if ($this->countModules('sidebar-left', true)) {
  $hasClass .= ' has-sidebar-left';
}
if ($this->countModules('sidebar-right', true)) {
  $hasClass .= ' has-sidebar-right';
}

Hier wird mit 2 If-Abfragen geschaut, ob Module auf den Modul-Positionen (sidebar-left und sidebar-right) gesetzt sind. Wenn dies der Fall ist, wird die $hasClass-Variable erweitert um den Ausdruck 'has-sidebar-...'.

$wrapper = $this->params->get('fluidContainer') ? 'wrapper-fluid' : 'wrapper-static';

Jetzt wird der Wrapper geschrieben. Dazu wird geschaut, ob im Template-Backend die fluidContainer geschaltet ist (true) oder nicht (false). Ist sie true, wird fluid geschrieben, ansonsten static.

$this->setMetaData('viewport', 'width=device-width, initial-scale=1');

Es wird ein META-Tag geschrieben. Hier der viewport.

$stickyHeader = $this->params->get('stickyHeader') ? 'position-sticky sticky-top' : '';

Wie beim Wrapper wird geschaut ob der Header sticky sein soll. Ist im Template-Backend gesetzt, dann werden die zu nutzenden Klassen geschrieben, ansonsten bleibt sie leer.

$wa->getAsset('style', 'fontawesome')->setAttribute('rel', 'lazy-stylesheet');

Bindet (sofern vorhanden) fontawesome als style ein und setzt das Attribute 'rel' auf 'lazy'.

<!DOCTYPE html>
<html lang="<?php echo $this->language; ?>" dir="<?php echo $this->direction; ?>">
  <head>
    <jdoc:include type="metas" />
    <jdoc:include type="styles" />
    <jdoc:include type="scripts" />
  </head>

Wir beginnen den offziellen HTML-Teil mit DOCTYPE und dem html-tag in dem die Sprache aus $this-language und die dir aus $this->direction ausgelesen wird.

<body class="site
  <?php echo $option
    . ' ' . $wrapper
    . ' view-' . $view
    . ($layout ? ' layout-' . $layout : ' no-layout')
    . ($task ? ' task-' . $task : ' no-task')
    . ($itemid ? ' itemid-' . $itemid : '')
    . ($pageclass ? ' ' . $pageclass : '')
    . $hasClass
    . ($this->direction == 'rtl' ? ' rtl' : '');
  ?>">

Für den Body werden folgende Klassen gesetzt:

  • option = siehe oben
  • view = siehe oben
  • layout = siehe oben
  • task = siehe oben
  • itemid = siehe oben
  • pageClass = siehe oben
  • hasClass = siehe oben
  • direction = siehe oben
<header class="header container-header full-width<?php echo $stickyHeader ? ' ' . $stickyHeader : ''; ?>">

Der Header-Bereich wird erstellt, mit den beschriebenen Klassen und ob dieser sticky ist oder nicht.

<?php if ($this->countModules('topbar')) : ?>
  <div class="container-topbar">
    <jdoc:include type="modules" name="topbar" style="none" />
  </div>
<?php endif; ?>

Erste Abfrage ob ein Modul der Modul-Position 'topbar' zugewiesen ist. Wenn ja, wird diese eingebunden und angezeigt.

<?php if ($this->countModules('below-top')) : ?>
  <div class="grid-child container-below-top">
    <jdoc:include type="modules" name="below-top" style="none" />
  </div>
<?php endif; ?>

Nächste Abfrage für die Position 'below-top'.

<?php if ($this->params->get('brand', 1)) : ?>
  <div class="grid-child">
    <div class="navbar-brand">
      <a class="brand-logo" href="/<?php echo $this->baseurl; ?>/">
        <?php echo $logo; ?>
      </a>
      <?php if ($this->params->get('siteDescription')) : ?>
        <div class="site-description"><?php echo htmlspecialchars($this->params->get('siteDescription')); ?></div>
      <?php endif; ?>
    </div>
  </div>
<?php endif; ?>

Als erstes wird geprüft, ob im Template-Backend 'brand' auf true gesetzt ist. Wenn ja, wird ein mit a-tag besetztes img (welches in $logo steht) geschrieben.
Im Anschluss wird geprüft, ob 'siteDescription' gesetzt ist, wenn ja, wird dies ebenfalls gesetzt.

<?php if ($this->countModules('menu', true) || $this->countModules('search', true)) : ?>
  <div class="grid-child container-nav">
    <?php if ($this->countModules('menu', true)) : ?>
      <jdoc:include type="modules" name="menu" style="none" />
    <?php endif; ?>
    <?php if ($this->countModules('search', true)) : ?>
      <div class="container-search">
        <jdoc:include type="modules" name="search" style="none" />
      </div>
    <?php endif; ?>
  </div>
<?php endif; ?>
</header>

Bevor nun der Header-Tag geschlossen wird, wird noch geprüft ob 'menu' oder 'search' gesetzt sind. Wenn dies der Fall sein sollte, dann werden vorhandene eingebunden.

<div class="site-grid">
  <?php if ($this->countModules('banner', true)) : ?>
    <div class="container-banner full-width">
      <jdoc:include type="modules" name="banner" style="none" />
    </div>
  <?php endif; ?>
  <?php if ($this->countModules('top-a', true)) : ?>
    <div class="grid-child container-top-a">
      <jdoc:include type="modules" name="top-a" style="card" />
    </div>
  <?php endif; ?>
  <?php if ($this->countModules('top-b', true)) : ?>
    <div class="grid-child container-top-b">
      <jdoc:include type="modules" name="top-b" style="card" />
    </div>
  <?php endif; ?>
  <?php if ($this->countModules('sidebar-left', true)) : ?>
    <div class="grid-child container-sidebar-left">
      <jdoc:include type="modules" name="sidebar-left" style="card" />
    </div>
  <?php endif; ?>

Nun kommen wir solangsam zum Hauptteil der Seite. Hier wird in einem site-grid geprüft, ob 'banner', 'top-a', 'top-b' und 'sidebar-left' einzubinden sind.

<div class="grid-child container-component">
  <jdoc:include type="modules" name="breadcrumbs" style="none" />
  <jdoc:include type="modules" name="main-top" style="card" />
  <jdoc:include type="message" />
  <main>
    <jdoc:include type="component" />
  </main>
  <jdoc:include type="modules" name="main-bottom" style="card" />
</div>

Hier werden nun alle wichtigen Elemente eingebunden: 'breadcrumbs', 'main-top', 'message', 'component' und 'main-bottom'.

  <?php if ($this->countModules('sidebar-right', true)) : ?>
    <div class="grid-child container-sidebar-right">
      <jdoc:include type="modules" name="sidebar-right" style="card" />
    </div>
  <?php endif; ?>
  <?php if ($this->countModules('bottom-a', true)) : ?>
    <div class="grid-child container-bottom-a">
      <jdoc:include type="modules" name="bottom-a" style="card" />
    </div>
  <?php endif; ?>
  <?php if ($this->countModules('bottom-b', true)) : ?>
    <div class="grid-child container-bottom-b">
      <jdoc:include type="modules" name="bottom-b" style="card" />
    </div>
  <?php endif; ?>
</div>
<?php if ($this->countModules('footer', true)) : ?>
  <footer class="container-footer footer full-width">
    <div class="grid-child">
      <jdoc:include type="modules" name="footer" style="none" />
    </div>
  </footer>
<?php endif; ?>

Nun werden unterhalb des Content-Bereiches alle Module eingebunden (sofern belegt) wie: 'sidebar-right', 'bottom-a', 'bottom-b' und 'footer'.

<?php if ($this->params->get('backTop') == 1) : ?>
  <a href="#top" id="back-top" class="back-to-top-link" aria-label="<?php echo Text::_('TPL_CASSIOPEIA_BACKTOTOP'); ?>">
    <span class="icon-arrow-up icon-fw" aria-hidden="true"></span>
  </a>
<?php endif; ?>
<jdoc:include type="modules" name="debug" style="none" />

Als letztes wird noch geprüft, ob der back-to-top-Button noch eingebunden werden soll (kann im Template-Backend ein- und ausgeschaltet werden). Und zu guter Schluss dann auch das debug-Modul.

Jetzt sind wir mit der Beschreibung des Cassiopeia-Templates fertig und es lichtet sich der Urwald ;-)

An dieser Stelle möchte ich darauf hinweisen, dass ich KEINE Rechte oder ähnliches an diesem Template habe und diese alleine bei JOOMLA liegen. Ich habe es nur zum Zweck der eigenen Template-Entwicklung analysiert.