migrate apropos data from config/apropos.php to SQLite

- Create apropos_contents table via migration 010
- Add Database methods: getAproposContent(), saveAproposContent(), getAllAproposContents()
- Replace admin/pages.php with admin/contenus.php (renamed header from 'Pages statiques' to 'Contenus')
- Replace admin/pages-edit.php with admin/contenus-edit.php (support editing pages + apropos contents)
- Create admin/actions/apropos.php for saving apropos data (contacts, credits, erg_url)
- Update public/apropos.php to read contacts/credits/erg_url from DB
- Delete config/apropos.php
This commit is contained in:
Pontoporeia
2026-04-16 13:44:06 +02:00
parent 4158c72d08
commit bf30aab0b3
15 changed files with 538 additions and 236 deletions

View File

@@ -0,0 +1,77 @@
<?php
/**
* Save handler for apropos contents (contacts, credits, erg_url).
*/
require_once __DIR__ . "/../../../config/bootstrap.php";
require_once __DIR__ . '/../../../src/AdminAuth.php';
AdminAuth::requireLogin();
// CSRF check
if (!isset($_POST['csrf_token']) || !isset($_SESSION['csrf_token']) ||
!hash_equals($_SESSION['csrf_token'], $_POST['csrf_token'])) {
die("Erreur de sécurité : token invalide.");
}
$allowedKeys = ['contacts', 'credits', 'erg_url'];
$aproposKey = $_POST['apropos_key'] ?? '';
if (!in_array($aproposKey, $allowedKeys)) {
die("Clé invalide.");
}
require_once __DIR__ . '/../../../src/Database.php';
try {
$db = new Database();
if ($aproposKey === 'erg_url') {
$value = trim($_POST['value'] ?? '');
if (strlen($value) > 500) {
die("URL trop longue (max 500 caractères).");
}
$db->saveAproposContent('erg_url', $value);
} else {
$items = $_POST['items'] ?? [];
$cleaned = [];
foreach ($items as $item) {
if ($aproposKey === 'contacts') {
$name = trim($item['name'] ?? '');
if ($name === '') continue; // skip empty rows
$entry = [
'name' => trim($item['name'] ?? ''),
'role' => trim($item['role'] ?? ''),
'email' => trim($item['email'] ?? ''),
];
$url = trim($item['url'] ?? '');
if ($url !== '') {
$entry['url'] = $url;
}
$cleaned[] = $entry;
} else { // credits
$label = trim($item['label'] ?? '');
$val = trim($item['value'] ?? '');
$url = trim($item['url'] ?? '');
if ($label === '' && $val === '') continue;
$entry = [
'label' => $label,
'value' => $val,
];
if ($url !== '') {
$entry['url'] = $url;
}
$cleaned[] = $entry;
}
}
if (empty($cleaned)) {
die("Au moins un élément est requis.");
}
$db->saveAproposContent($aproposKey, $cleaned);
}
App::flash('success', "Contenu « $aproposKey » mis à jour avec succès.");
} catch (Exception $e) {
error_log("Apropos save error: " . $e->getMessage());
die("Erreur lors de la sauvegarde : " . htmlspecialchars($e->getMessage()));
}
header('Location: /admin/contenus.php');
exit;

View File

@@ -1,35 +0,0 @@
<?php
require_once __DIR__ . "/../../../config/bootstrap.php";
require_once __DIR__ . '/../../../src/AdminAuth.php';
AdminAuth::requireLogin();
// CSRF check
if (!isset($_POST['csrf_token']) || !isset($_SESSION['csrf_token']) ||
!hash_equals($_SESSION['csrf_token'], $_POST['csrf_token'])) {
die("Erreur de sécurité : token invalide.");
}
$allowedSlugs = ['about', 'licenses', 'charte', 'contact'];
$slug = $_POST['slug'] ?? '';
if (!in_array($slug, $allowedSlugs)) {
die("Slug invalide.");
}
$content = $_POST['content'] ?? '';
if (strlen($content) > 65535) {
die("Contenu trop long (max 65 535 caractères).");
}
require_once __DIR__ . '/../../../src/Database.php';
try {
$db = new Database();
$db->savePage($slug, $content);
App::flash('success', "Page «" . $slug . "» mise à jour avec succès.");
} catch (Exception $e) {
error_log("Page save error: " . $e->getMessage());
die("Erreur lors de la sauvegarde : " . htmlspecialchars($e->getMessage()));
}
header('Location: /admin/pages.php');
exit;

View File

@@ -0,0 +1,232 @@
<?php
require_once __DIR__ . "/../../config/bootstrap.php";
require_once __DIR__ . '/../../src/AdminAuth.php';
AdminAuth::requireLogin();
require_once __DIR__ . '/../../src/Database.php';
if (empty($_SESSION["csrf_token"])) {
$_SESSION["csrf_token"] = bin2hex(random_bytes(32));
}
$allowedPageSlugs = ["about", "licenses", "charte"];
$allowedApropos = ["contacts", "credits", "erg_url"];
$pageSlug = $_GET["slug"] ?? "";
$aproposKey = $_GET["apropos"] ?? "";
// Exactly one target must be specified
if ($pageSlug && !in_array($pageSlug, $allowedPageSlugs)) {
$pageSlug = "";
}
if ($aproposKey && !in_array($aproposKey, $allowedApropos)) {
$aproposKey = "";
}
if (!$pageSlug && !$aproposKey) {
header("Location: /admin/contenus.php");
exit();
}
try {
$db = new Database();
if ($pageSlug) {
$page = $db->getPage($pageSlug);
if (!$page) {
die("Page introuvable.");
}
$editTitle = $page["title"];
$editType = "page";
} else {
$editType = "apropos";
$value = $db->getAproposContent($aproposKey);
$editTitle = match($aproposKey) {
'contacts' => 'Contacts',
'credits' => 'Crédits',
'erg_url' => 'URL de l\'ERG',
};
}
} catch (Exception $e) {
die("Erreur: " . htmlspecialchars($e->getMessage()));
}
$pageTitle = "Éditer : " . $editTitle;
$extraJs = ["/assets/js/overtype.min.js"];
$extraJsInline = <<<'JS'
var OT = window.OverType.default || window.OverType;
var hidden = document.getElementById('content');
var editor = new OT(document.getElementById('editor'), {
value: hidden.value,
minHeight: '400px',
spellcheck: false,
onChange: function(value) { hidden.value = value; }
});
JS;
$aproposEditorJs = null;
if ($editType === 'apropos' && in_array($aproposKey, ['contacts', 'credits'])) {
// Rich textarea for JSON arrays rendered as structured form
$aproposEditorJs = <<<'JS'
// Auto-format JSON in the hidden field for display purposes
JS;
}
$initialContent = '';
if ($editType === 'page') {
$initialContent = $page["content"] ?? "";
} else {
// For apropos, show structured form
}
?>
<?php
$isAdmin = true;
$bodyClass = "admin-body";
require_once APP_ROOT . "/templates/head.php";
?>
<?php include APP_ROOT . "/templates/header.php"; ?>
<main id="main-content">
<h1>Éditer : <?= htmlspecialchars($editTitle) ?></h1>
<?php if ($editType === 'page'): ?>
<form action="/admin/actions/page.php" method="post" class="admin-form">
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($_SESSION["csrf_token"]) ?>">
<input type="hidden" name="slug" value="<?= htmlspecialchars($pageSlug) ?>">
<label for="editor">Contenu (Markdown) :</label>
<input type="hidden" id="content" name="content"
value="<?= htmlspecialchars($initialContent) ?>">
<div id="editor"></div>
<div class="admin-form-footer">
<button type="submit" class="admin-btn">Enregistrer</button>
<a href="/admin/contenus.php" class="admin-btn-secondary admin-cancel-link">Annuler</a>
</div>
</form>
<?php elseif ($aproposKey === 'erg_url'): ?>
<form action="/admin/actions/apropos.php" method="post" class="admin-form">
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($_SESSION["csrf_token"]) ?>">
<input type="hidden" name="apropos_key" value="erg_url">
<label for="erg_url">URL du site de l'ERG :</label>
<input type="url" id="erg_url" name="value"
value="<?= htmlspecialchars($value) ?>" style="width:100%;max-width:600px;">
<div class="admin-form-footer">
<button type="submit" class="admin-btn">Enregistrer</button>
<a href="/admin/contenus.php" class="admin-btn-secondary admin-cancel-link">Annuler</a>
</div>
</form>
<?php elseif (in_array($aproposKey, ['contacts', 'credits'])): ?>
<?php
$items = is_array($value) ? $value : [];
?>
<form action="/admin/actions/apropos.php" method="post" class="admin-form" id="apropos-form">
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($_SESSION["csrf_token"]) ?>">
<input type="hidden" name="apropos_key" value="<?= htmlspecialchars($aproposKey) ?>">
<?php if ($aproposKey === 'contacts'): ?>
<?php foreach ($items as $i => $item): ?>
<fieldset class="apropos-item">
<legend>Contact <?= $i + 1 ?></legend>
<label for="contact_<?= $i ?>_name">Nom :</label>
<input type="text" id="contact_<?= $i ?>_name"
name="items[<?= $i ?>][name]"
value="<?= htmlspecialchars($item['name'] ?? '') ?>" required>
<label for="contact_<?= $i ?>_role">Rôle :</label>
<input type="text" id="contact_<?= $i ?>_role"
name="items[<?= $i ?>][role]"
value="<?= htmlspecialchars($item['role'] ?? '') ?>">
<label for="contact_<?= $i ?>_email">Email :</label>
<input type="email" id="contact_<?= $i ?>_email"
name="items[<?= $i ?>][email]"
value="<?= htmlspecialchars($item['email'] ?? '') ?>">
<label for="contact_<?= $i ?>_url">Lien (optionnel) :</label>
<input type="url" id="contact_<?= $i ?>_url"
name="items[<?= $i ?>][url]"
value="<?= htmlspecialchars($item['url'] ?? '') ?>">
</fieldset>
<?php endforeach; ?>
<?php else: ?>
<?php foreach ($items as $i => $item): ?>
<fieldset class="apropos-item">
<legend>Crédit <?= $i + 1 ?></legend>
<label for="credit_<?= $i ?>_label">Label :</label>
<input type="text" id="credit_<?= $i ?>_label"
name="items[<?= $i ?>][label]"
value="<?= htmlspecialchars($item['label'] ?? '') ?>">
<label for="credit_<?= $i ?>_value">Valeur :</label>
<input type="text" id="credit_<?= $i ?>_value"
name="items[<?= $i ?>][value]"
value="<?= htmlspecialchars($item['value'] ?? '') ?>">
<label for="credit_<?= $i ?>_url">Lien (optionnel) :</label>
<input type="url" id="credit_<?= $i ?>_url"
name="items[<?= $i ?>][url]"
value="<?= htmlspecialchars($item['url'] ?? '') ?>">
</fieldset>
<?php endforeach; ?>
<?php endif; ?>
<button type="button" class="admin-btn" id="add-item-btn" style="width:auto;">+ Ajouter un élément</button>
<div class="admin-form-footer">
<button type="submit" class="admin-btn">Enregistrer</button>
<a href="/admin/contenus.php" class="admin-btn-secondary admin-cancel-link">Annuler</a>
</div>
<template id="row-template-<?= $aproposKey ?>">
<?php if ($aproposKey === 'contacts'): ?>
<fieldset class="apropos-item">
<legend>Contact {{index}}</legend>
<label for="contact_{{index}}_name">Nom :</label>
<input type="text" id="contact_{{index}}_name"
name="items[{{index}}][name]" required>
<label for="contact_{{index}}_role">Rôle :</label>
<input type="text" id="contact_{{index}}_role"
name="items[{{index}}][role]">
<label for="contact_{{index}}_email">Email :</label>
<input type="email" id="contact_{{index}}_email"
name="items[{{index}}][email]">
<label for="contact_{{index}}_url">Lien (optionnel) :</label>
<input type="url" id="contact_{{index}}_url"
name="items[{{index}}][url]">
</fieldset>
<?php else: ?>
<fieldset class="apropos-item">
<legend>Crédit {{index}}</legend>
<label for="credit_{{index}}_label">Label :</label>
<input type="text" id="credit_{{index}}_label"
name="items[{{index}}][label]">
<label for="credit_{{index}}_value">Valeur :</label>
<input type="text" id="credit_{{index}}_value"
name="items[{{index}}][value]">
<label for="credit_{{index}}_url">Lien (optionnel) :</label>
<input type="url" id="credit_{{index}}_url"
name="items[{{index}}][url]">
</fieldset>
<?php endif; ?>
</template>
</form>
<script>
(function() {
let count = <?= count($items) ?>;
const tpl = document.getElementById('row-template-<?= $aproposKey ?>').innerHTML;
document.getElementById('add-item-btn').addEventListener('click', function() {
count++;
const html = tpl.replaceAll('{{index}}', count);
this.insertAdjacentHTML('beforebegin', html);
});
})();
</script>
<?php endif; ?>
</main>
<?php require_once APP_ROOT . "/templates/admin/footer.php"; ?>

86
public/admin/contenus.php Normal file
View File

@@ -0,0 +1,86 @@
<?php
require_once __DIR__ . "/../../config/bootstrap.php";
require_once __DIR__ . '/../../src/AdminAuth.php';
AdminAuth::requireLogin();
require_once __DIR__ . '/../../src/Database.php';
$pageTitle = "Contenus";
try {
$db = new Database();
$pages = $db->getAllPages();
$aproposKeys = $db->getAllAproposContents();
} catch (Exception $e) {
error_log("Error loading contenus: " . $e->getMessage());
die("Erreur lors du chargement des contenus.");
}
?>
<?php $isAdmin = true; $bodyClass = 'admin-body'; require_once APP_ROOT . '/templates/head.php'; ?>
<?php include APP_ROOT . '/templates/header.php'; ?>
<main id="main-content">
<h1>Contenus</h1>
<?php include APP_ROOT . '/templates/partials/flash-messages.php'; ?>
<h2>Pages statiques</h2>
<table>
<thead>
<tr>
<th scope="col">Slug</th>
<th scope="col">Titre</th>
<th scope="col">Mis à jour</th>
<th scope="col">Action</th>
</tr>
</thead>
<tbody>
<?php foreach ($pages as $p): ?>
<tr>
<td><code><?= htmlspecialchars($p['slug']) ?></code></td>
<td><?= htmlspecialchars($p['title']) ?></td>
<td><?= htmlspecialchars($p['updated_at'] ?? '—') ?></td>
<td>
<a href="/admin/contenus-edit.php?slug=<?= urlencode($p['slug']) ?>"
class="admin-btn admin-btn--sm">Éditer</a>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<h2 style="margin-top:2rem;">À propos</h2>
<table>
<thead>
<tr>
<th scope="col">Clé</th>
<th scope="col">Type</th>
<th scope="col">Mis à jour</th>
<th scope="col">Action</th>
</tr>
</thead>
<tbody>
<?php foreach ($aproposKeys as $a): ?>
<?php
$typeLabel = match($a['key']) {
'contacts' => 'Contacts',
'credits' => 'Crédits',
'erg_url' => 'URL de l\'ERG',
};
?>
<tr>
<td><code><?= htmlspecialchars($a['key']) ?></code></td>
<td><?= htmlspecialchars($typeLabel) ?></td>
<td><?= htmlspecialchars($a['updated_at'] ?? '—') ?></td>
<td>
<a href="/admin/contenus-edit.php?apropos=<?= urlencode($a['key']) ?>"
class="admin-btn admin-btn--sm">Éditer</a>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</main>
<?php require_once APP_ROOT . '/templates/admin/footer.php'; ?>

View File

@@ -1,70 +0,0 @@
<?php
require_once __DIR__ . "/../../config/bootstrap.php";
require_once __DIR__ . "/../../src/AdminAuth.php";
AdminAuth::requireLogin();
if (empty($_SESSION["csrf_token"])) {
$_SESSION["csrf_token"] = bin2hex(random_bytes(32));
}
require_once __DIR__ . "/../../src/Database.php";
$allowedSlugs = ["about", "licenses", "charte", "contact"];
$slug = $_GET["slug"] ?? "";
if (!in_array($slug, $allowedSlugs)) {
header("Location: /admin/pages.php");
exit();
}
try {
$db = new Database();
$page = $db->getPage($slug);
if (!$page) {
die("Page introuvable.");
}
} catch (Exception $e) {
die("Erreur: " . htmlspecialchars($e->getMessage()));
}
$pageTitle = "Éditer : " . htmlspecialchars($page["title"]);
$extraJs = ["/assets/js/overtype.min.js"];
$extraJsInline = <<<'JS'
var OT = window.OverType.default || window.OverType;
var hidden = document.getElementById('content');
var editor = new OT(document.getElementById('editor'), {
value: hidden.value,
minHeight: '400px',
spellcheck: false,
onChange: function(value) { hidden.value = value; }
});
JS;
?>
<?php
$isAdmin = true;
$bodyClass = "admin-body";
require_once APP_ROOT . "/templates/head.php";
?>
<?php include APP_ROOT . "/templates/header.php"; ?>
<main id="main-content">
<h1>Éditer : <?= htmlspecialchars($page["title"]) ?></h1>
<form action="/admin/actions/page.php" method="post" class="admin-form">
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars(
$_SESSION["csrf_token"],
) ?>">
<input type="hidden" name="slug" value="<?= htmlspecialchars($slug) ?>">
<label for="editor">Contenu (Markdown) :</label>
<input type="hidden" id="content" name="content"
value="<?= htmlspecialchars($page["content"] ?? "") ?>">
<div id="editor"></div>
<div class="admin-form-footer">
<button type="submit" class="admin-btn">Enregistrer</button>
<a href="/admin/pages.php" class="admin-btn-secondary admin-cancel-link">Annuler</a>
</div>
</form>
</main>
<?php require_once APP_ROOT . "/templates/admin/footer.php"; ?>

View File

@@ -1,53 +0,0 @@
<?php
require_once __DIR__ . "/../../config/bootstrap.php";
require_once __DIR__ . '/../../src/AdminAuth.php';
AdminAuth::requireLogin();
require_once __DIR__ . '/../../src/Database.php';
$pageTitle = "Pages statiques";
try {
$db = new Database();
$pages = $db->getAllPages();
} catch (Exception $e) {
error_log("Error loading pages: " . $e->getMessage());
die("Erreur lors du chargement des pages.");
}
// Flash messages are consumed by the flash-messages partial below.
?>
<?php $isAdmin = true; $bodyClass = 'admin-body'; require_once APP_ROOT . '/templates/head.php'; ?>
<?php include APP_ROOT . '/templates/header.php'; ?>
<main id="main-content">
<h1>Pages statiques</h1>
<?php include APP_ROOT . '/templates/partials/flash-messages.php'; ?>
<table>
<thead>
<tr>
<th scope="col">Slug</th>
<th scope="col">Titre</th>
<th scope="col">Mis à jour</th>
<th scope="col">Action</th>
</tr>
</thead>
<tbody>
<?php foreach ($pages as $p): ?>
<tr>
<td><code><?= htmlspecialchars($p['slug']) ?></code></td>
<td><?= htmlspecialchars($p['title']) ?></td>
<td><?= htmlspecialchars($p['updated_at'] ?? '—') ?></td>
<td>
<a href="/admin/pages-edit.php?slug=<?= urlencode($p['slug']) ?>"
class="admin-btn admin-btn--sm">Éditer</a>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</main>
<?php require_once APP_ROOT . '/templates/admin/footer.php'; ?>

View File

@@ -3,29 +3,39 @@ require_once __DIR__ . '/../config/bootstrap.php';
require_once APP_ROOT . '/src/Database.php';
require_once APP_ROOT . '/src/Parsedown.php';
$apropos = require APP_ROOT . '/config/apropos.php';
$currentNav = 'apropos';
// Fallback static content used when DB content is the placeholder
define('APROPOS_STATIC_CONTENT', "Ce site POSTERG a été créé pour répertorier et valoriser les mémoires de l'erg École de Recherches Graphique de Bruxelles.\n\nL'objectif est à la fois d'offrir une vitrine aux projets des anciens étudiantes et de mettre en lumière la diversité des disciplines et des parcours qui façonnent l'histoire de l'école à travers les âges, depuis près de 100 ans.");
define('APROPOS_STATIC_CONTENT', "Ce site POSTERG a été créé pour répertorier et valoriser les mémoires de l'erg École de Recherches Graphique de Bruxelles.\n\nL'objectif est à la fois d'offrir une vitrine aux projets des anciennes étudiantes et de mettre en lumière la diversité des disciplines et des parcours qui façonnent l'histoire de l'école à travers les âges, depuis près de 100 ans.");
try {
$db = Database::getInstance();
// Intro text from pages table
$aboutPage = $db->getPage('about');
$rawContent = $aboutPage ? $aboutPage['content'] : '';
// Use static fallback if content is placeholder
if (empty(trim($rawContent)) || trim($rawContent) === 'Contenu à venir') {
$rawContent = APROPOS_STATIC_CONTENT;
}
// Contacts, credits, erg_url from apropos_contents table
$contacts = $db->getAproposContent('contacts');
$credits = $db->getAproposContent('credits');
$ergUrl = $db->getAproposContent('erg_url') ?: 'https://erg.be';
// Apply defaults if DB returns empty
$contacts = is_array($contacts) && !empty($contacts) ? $contacts : null;
$credits = is_array($credits) && !empty($credits) ? $credits : null;
} catch (Exception $e) {
error_log("Error loading about page: " . $e->getMessage());
$rawContent = APROPOS_STATIC_CONTENT;
$contacts = null;
$credits = null;
$ergUrl = 'https://erg.be';
}
$pd = new Parsedown();
$pd->setSafeMode(true);
$aboutHtml = $pd->text($rawContent);
$pageTitle = 'À Propos Posterg';
$metaDescription = 'À propos de Posterg, le répertoire des mémoires de fin d\'études de l\'erg École de Recherches Graphiques de Bruxelles.';
$ogTags = [
@@ -49,15 +59,15 @@ $bodyClass = 'apropos-body';
<p class="apropos-toc-label">Parties</p>
<ul>
<li><a href="#apropos-intro">À propos</a></li>
<?php if (!empty($apropos['contacts'])): ?>
<?php if (!empty($contacts)): ?>
<li><a href="#apropos-contacts">Contacts</a></li>
<?php endif; ?>
<?php if (!empty($apropos['credits'])): ?>
<?php if (!empty($credits)): ?>
<li><a href="#apropos-credits">Crédits</a></li>
<?php endif; ?>
</ul>
<div class="apropos-toc-erg">
<a href="<?= htmlspecialchars($apropos['erg_url']) ?>" target="_blank" rel="noopener">
<a href="<?= htmlspecialchars($ergUrl) ?>" target="_blank" rel="noopener">
Site de l'erg ↗
</a>
</div>
@@ -73,14 +83,20 @@ $bodyClass = 'apropos-body';
</div>
</section>
<?php if (!empty($apropos['contacts'])): ?>
<?php if (!empty($contacts)): ?>
<!-- Contacts section -->
<section class="apropos-section" id="apropos-contacts">
<h2 class="apropos-section-title">Contacts</h2>
<div class="apropos-contacts-grid">
<?php foreach ($apropos['contacts'] as $contact): ?>
<?php foreach ($contacts as $contact): ?>
<address class="apropos-contact-card">
<strong><?= htmlspecialchars($contact['name']) ?></strong>
<strong>
<?php if (!empty($contact['url'])): ?>
<a href="<?= htmlspecialchars($contact['url']) ?>" target="_blank" rel="noopener"><?= htmlspecialchars($contact['name']) ?></a>
<?php else: ?>
<?= htmlspecialchars($contact['name']) ?>
<?php endif; ?>
</strong>
<span><?= htmlspecialchars($contact['role']) ?></span>
<a href="mailto:<?= htmlspecialchars($contact['email']) ?>"><?= htmlspecialchars($contact['email']) ?></a>
</address>
@@ -89,15 +105,21 @@ $bodyClass = 'apropos-body';
</section>
<?php endif; ?>
<?php if (!empty($apropos['credits'])): ?>
<?php if (!empty($credits)): ?>
<!-- Credits section -->
<section class="apropos-section" id="apropos-credits">
<h2 class="apropos-section-title">Crédits</h2>
<dl class="apropos-credits-list">
<?php foreach ($apropos['credits'] as $credit): ?>
<?php foreach ($credits as $credit): ?>
<div class="apropos-credit-row">
<dt><?= htmlspecialchars($credit['label']) ?></dt>
<dd><?= htmlspecialchars($credit['value']) ?></dd>
<dd>
<?php if (!empty($credit['url'])): ?>
<a href="<?= htmlspecialchars($credit['url']) ?>" target="_blank" rel="noopener"><?= htmlspecialchars($credit['value']) ?></a>
<?php else: ?>
<?= htmlspecialchars($credit['value']) ?>
<?php endif; ?>
</dd>
</div>
<?php endforeach; ?>
</dl>