fix(site): width/height nas imagens da home (auditoria 4.7)
125 imagens na home, 0 com srcset e 122 sem width/height — o browser só sabia o tamanho depois de descarregar, o que causa layout shift. Uploads são redimensionados a um limite de lado maior (functions_images.php), não a uma dimensão fixa (fotos de equipa vão de 389x512 a 512x512, ícones de oferta de 128x128 a 256x256), por isso as dimensões não dão para hardcode: site_img_dims_attr() em functions_site.php lê o ficheiro real com getimagesize() por pedido, tal como o gallery.php já fazia, e devolve o par width="" height="" ou "" se o ficheiro não existir. SVG não passa por getimagesize() (sem decoder embutido) — svg_img_dims_attr() lê os atributos width/height (ícones fitme-*.svg, 48x48 fixo) ou cai para o viewBox (logos do navbar, só têm viewBox). Aplicado a: ícones da oferta, fotos de equipa (card + painel de detalhe), ícone de modalidade em modalidade_icon_html() e no horário (index.php e includes/horario.php), logo do navbar. srcset ficou de fora: as imagens já vêm coartadas para pouco mais do que o maior tamanho a que aparecem (fotos de equipa a 512px cabidas numa caixa de no máx. 200px CSS com object-fit:cover; ícones a 128-256px mostrados a 44px) — é a folga normal para ecrãs retina, não desperdício a resolver com múltiplas resoluções. E o layout é sempre CSS-driven (width/height: 100% + object-fit), por isso um srcset não mudaria o que é descarregado por variação de viewport, só pouparia bytes nalguns casos — gerar 2-3 tamanhos por upload via GD seria trabalho a mais para o ganho real neste site. Fica só o fix de dimensões, que é o que resolve o CLS que a auditoria mediu. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
a42b6d33c0
commit
595a37fd8b
4 files changed
+59
-9
No files matched your search
@@ -104,6 +104,51 @@ function site_img_url($value, $base_dir): string {
|
||||
return base_url(rtrim((string) $base_dir, '/') . '/' . ltrim($value, '/'));
|
||||
}
|
||||
|
||||
// Same value/$base_dir resolution as site_img_url(), but to a filesystem path
|
||||
// (for getimagesize()) instead of a URL. Empty string for a remote http(s)
|
||||
// image or anything that doesn't resolve to a local file.
|
||||
function site_img_path($value, $base_dir): string {
|
||||
$value = (string) $value;
|
||||
if ($value === '' || substr($value, 0, 4) === 'http') return '';
|
||||
$root = dirname(__DIR__);
|
||||
$rel = (substr($value, 0, 8) === 'uploads/')
|
||||
? $value
|
||||
: rtrim((string) $base_dir, '/') . '/' . ltrim($value, '/');
|
||||
return $root . '/' . $rel;
|
||||
}
|
||||
|
||||
// width="" height="" attribute string for an uploaded image, read from the
|
||||
// file itself (uploads aren't resized to one fixed size — see
|
||||
// config/functions_images.php — so this can't be hardcoded). Empty string
|
||||
// when the file is missing/unreadable, so the <img> just renders without the
|
||||
// attributes rather than with bogus 0x0 ones.
|
||||
function site_img_dims_attr($value, $base_dir): string {
|
||||
$path = site_img_path($value, $base_dir);
|
||||
if ($path === '') return '';
|
||||
return svg_img_dims_attr($path);
|
||||
}
|
||||
|
||||
// width="" height="" for any local raster or SVG file. getimagesize() covers
|
||||
// raster; for SVG (which it doesn't decode) this reads the width/height
|
||||
// attributes and falls back to the viewBox aspect ratio when only that is
|
||||
// set (e.g. the site logos). Empty string when the file is missing or has no
|
||||
// size info to go on.
|
||||
function svg_img_dims_attr(string $path): string {
|
||||
if (!is_file($path)) return '';
|
||||
if (strtolower(pathinfo($path, PATHINFO_EXTENSION)) !== 'svg') {
|
||||
$info = @getimagesize($path);
|
||||
return $info ? 'width="' . (int) $info[0] . '" height="' . (int) $info[1] . '"' : '';
|
||||
}
|
||||
$svg = (string) @file_get_contents($path, false, null, 0, 4096);
|
||||
if (preg_match('/\bwidth="([\d.]+)"/', $svg, $mw) && preg_match('/\bheight="([\d.]+)"/', $svg, $mh)) {
|
||||
return 'width="' . (int) round((float) $mw[1]) . '" height="' . (int) round((float) $mh[1]) . '"';
|
||||
}
|
||||
if (preg_match('/\bviewBox="[\d.\-]+\s+[\d.\-]+\s+([\d.]+)\s+([\d.]+)"/', $svg, $mv)) {
|
||||
return 'width="' . (int) round((float) $mv[1]) . '" height="' . (int) round((float) $mv[2]) . '"';
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
// Schedule joined to modalidades (name/colour/icon), optionally filtered by
|
||||
// local (e.g. 'crossfit'). Mirrors Horario_model::getAll().
|
||||
function site_horario(PDO $pdo, string $local = '', ?int $gym_id = null): array {
|
||||
@@ -194,5 +239,7 @@ function modalidade_icon_html(?string $icon): string {
|
||||
return $svg;
|
||||
}
|
||||
}
|
||||
return '<img loading="lazy" decoding="async" draggable="false" src="' . htmlspecialchars(site_img_url($icon, 'uploads/modalidades/icons')) . '" alt="">';
|
||||
$dims = site_img_dims_attr($icon, 'uploads/modalidades/icons');
|
||||
return '<img loading="lazy" decoding="async" draggable="false" ' . $dims
|
||||
. ' src="' . htmlspecialchars(site_img_url($icon, 'uploads/modalidades/icons')) . '" alt="">';
|
||||
}
|
||||
@@ -69,7 +69,7 @@ foreach ($rows as $d) { $grid[$d['dia_semana']][$d['hora_inicio']][] = $d; }
|
||||
<div class="schedule_cell<?= $value === $default_day ? '' : ' sched-hide' ?>" data-day="<?= htmlspecialchars($value) ?>" data-section="<?= ((int) substr($time, 0, 2) < 12) ? 'manha' : 'tarde' ?>" style="grid-column:<?= $col ?>;grid-row:<?= $row_of[$time] ?>;">
|
||||
<?php foreach ($grid[$value][$time] as $d): ?>
|
||||
<div class="schedule_box" style="background-color:<?= htmlspecialchars((string) $d['cor']) ?>">
|
||||
<img src="<?= htmlspecialchars(site_img_url($d['icon'], 'uploads/modalidades/icons')) ?>" alt="<?= htmlspecialchars((string) $d['nome']) ?>" decoding="async">
|
||||
<img src="<?= htmlspecialchars(site_img_url($d['icon'], 'uploads/modalidades/icons')) ?>" alt="<?= htmlspecialchars((string) $d['nome']) ?>" <?= site_img_dims_attr($d['icon'], 'uploads/modalidades/icons') ?> decoding="async">
|
||||
<a><b><?= htmlspecialchars((string) $d['nome']) ?></b></a>
|
||||
<div class="schedule_info">
|
||||
<?= htmlspecialchars((string) $d['hora_inicio']) ?>
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
<?php
|
||||
if($current_uri_2 == 'crossfit'){
|
||||
$logo = base_url('site/assets/img/logos/logo_crossfit.svg');
|
||||
$logo_rel = 'site/assets/img/logos/logo_crossfit.svg';
|
||||
}
|
||||
else
|
||||
{
|
||||
$logo = base_url('site/assets/img/logos/logo_black.svg');
|
||||
$logo_rel = 'site/assets/img/logos/logo_black.svg';
|
||||
}
|
||||
$logo = base_url($logo_rel);
|
||||
$logo_dims = svg_img_dims_attr(dirname(__DIR__, 2) . '/' . $logo_rel);
|
||||
?>
|
||||
<header class="header-section lazy">
|
||||
<nav class="navbar navbar-expand-lg navbar-light" style="padding-left:1rem; padding-top: 0; padding-bottom: 0;padding-right: 0; ">
|
||||
<a class="navbar-brand" href="<?= htmlspecialchars(gym_url(current_gym())) ?>">
|
||||
<img loading="eager" decoding="async" alt="Logo FitMe3070" src="<?= $logo ?>" style="width: 150px;">
|
||||
<img loading="eager" decoding="async" alt="Logo FitMe3070" src="<?= $logo ?>" <?= $logo_dims ?> style="width: 150px;">
|
||||
</a>
|
||||
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation"> <span class="navbar-toggler-icon"></span> </button>
|
||||
<div class="collapse navbar-collapse" id="navbarSupportedContent">
|
||||
|
||||
+5
-4
@@ -98,7 +98,7 @@ if ($offer):
|
||||
<div class="col-6 col-sm-4 col-lg-3">
|
||||
<div class="icon-box-item">
|
||||
<div class="ib-icon">
|
||||
<img loading="lazy" decoding="async" src="<?= htmlspecialchars(site_img_url($o['o_img'], 'uploads/website/offer')) ?>" alt="<?= htmlspecialchars((string) $o['o_title']) ?>">
|
||||
<img loading="lazy" decoding="async" <?= site_img_dims_attr($o['o_img'], 'uploads/website/offer') ?> src="<?= htmlspecialchars(site_img_url($o['o_img'], 'uploads/website/offer')) ?>" alt="<?= htmlspecialchars((string) $o['o_title']) ?>">
|
||||
</div>
|
||||
<h3><?= htmlspecialchars((string) $o['o_title']) ?></h3>
|
||||
<p><?= htmlspecialchars((string) $o['o_description']) ?></p>
|
||||
@@ -202,19 +202,20 @@ if ($team):
|
||||
$t_bio = trim(strip_tags((string) ($d['description'] ?? ''))) !== '' ? clean_richtext((string) $d['description']) : '';
|
||||
$t_detail = $t_skills || $t_certs || $t_bio !== '';
|
||||
$t_img = htmlspecialchars(site_img_url($d['photo'], 'uploads/team/foto'));
|
||||
$t_dims = site_img_dims_attr($d['photo'], 'uploads/team/foto');
|
||||
$t_id = 'td-' . (int) $d['id'];
|
||||
?>
|
||||
<div class="col-6 col-md-4 col-lg-3" style="margin-top: 20px;">
|
||||
<div class="trainer-item<?= $t_detail ? ' has-detail' : '' ?>"<?= $t_detail ? ' tabindex="0" role="button" aria-expanded="false" aria-controls="' . $t_id . '" aria-label="Ver detalhes de ' . htmlspecialchars((string) $d['name']) . '"' : '' ?>>
|
||||
<div class="trainer-pic">
|
||||
<img loading="lazy" decoding="async" src="<?= $t_img ?>" alt="<?= htmlspecialchars((string) $d['name']) ?>">
|
||||
<img loading="lazy" decoding="async" <?= $t_dims ?> src="<?= $t_img ?>" alt="<?= htmlspecialchars((string) $d['name']) ?>">
|
||||
</div>
|
||||
<h3><?= htmlspecialchars((string) $d['name']) ?></h3>
|
||||
<?php if ($t_title !== ''): ?><p class="trainer-title"><?= htmlspecialchars($t_title) ?></p><?php endif; ?>
|
||||
<?php if ($t_detail): ?>
|
||||
<div class="trainer-detail" id="<?= $t_id ?>">
|
||||
<button type="button" class="td-close" aria-label="Fechar">×</button>
|
||||
<div class="td-photo"><img loading="lazy" decoding="async" src="<?= $t_img ?>" alt=""></div>
|
||||
<div class="td-photo"><img loading="lazy" decoding="async" <?= $t_dims ?> src="<?= $t_img ?>" alt=""></div>
|
||||
<div class="td-body">
|
||||
<strong class="td-name"><?= htmlspecialchars((string) $d['name']) ?></strong>
|
||||
<?php if ($t_title !== ''): ?><span class="td-title"><?= htmlspecialchars($t_title) ?></span><?php endif; ?>
|
||||
@@ -333,7 +334,7 @@ if (!in_array($default_day, $week_values, true)) { $default_day = $week_values[0
|
||||
<div class="schedule_cell<?= $value === $default_day ? '' : ' sched-hide' ?>" data-day="<?= htmlspecialchars($value) ?>" data-section="<?= ((int) substr($time, 0, 2) < 12) ? 'manha' : 'tarde' ?>" style="grid-column:<?= $col ?>;grid-row:<?= $row_of[$time] ?>;">
|
||||
<?php foreach ($grid[$value][$time] as $d): ?>
|
||||
<div class="schedule_box" style="background-color:<?= htmlspecialchars((string) $d['cor']) ?>">
|
||||
<img src="<?= htmlspecialchars(site_img_url($d['icon'], 'uploads/modalidades/icons')) ?>" alt="<?= htmlspecialchars((string) $d['nome']) ?>" decoding="async">
|
||||
<img src="<?= htmlspecialchars(site_img_url($d['icon'], 'uploads/modalidades/icons')) ?>" alt="<?= htmlspecialchars((string) $d['nome']) ?>" <?= site_img_dims_attr($d['icon'], 'uploads/modalidades/icons') ?> decoding="async">
|
||||
<a><b><?= htmlspecialchars((string) $d['nome']) ?></b></a>
|
||||
<div class="schedule_info">
|
||||
<?= htmlspecialchars((string) $d['hora_inicio']) ?>
|
||||
|
||||
Reference in new issue
Block a user