feat: horário em 3 dias, horário de funcionamento compacto, textura na equipa, estrutura/SEO

- Horário de aulas: cartões mais pequenos e estreitos; a partir de 992px mostra
  o dia anterior, hoje ao centro e o seguinte (a dar a volta à semana), com as
  horas alinhadas e os dias laterais esbatidos. O JPG continua a semana toda.
- Horário de funcionamento: cartões compactos numa <dl>, ordenados por hora.
- Equipa: fundo no cinzento da parede do ginásio com juntas de blocos e grão
  muito discretos (SVG inline).
- Tira o «FitMe 3070» do header e esconde a newsletter.
- Estrutura: um <main> em todas as páginas públicas (skip link aponta-lhe),
  h2 por secção com aria-labelledby, h3 nos itens em vez de h4, sem secções
  aninhadas, alts distintos na galeria e no h1 do CrossFit.
- robots.txt e sitemap.xml gerados com o domínio da instalação; SEO_NOINDEX
  no .env tira uma cópia de staging dos motores de pesquisa.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
andreandClaude Opus 5.5 committed 2026-09-24 21:46:21 +01:00
1 parent 90ee2a93bc
commit 8ebf20f52f
11 files changed
+317 -57

No files matched your search

+2
View File
@@ -40,6 +40,8 @@ RewriteRule ^home/inscricao/?$ site/inscricao.php [L,QSA]
RewriteRule ^home/crossfit/?$ site/crossfit.php [L,QSA] RewriteRule ^home/crossfit/?$ site/crossfit.php [L,QSA]
RewriteRule ^home/privacidade/?$ site/privacidade.php [L,QSA] RewriteRule ^home/privacidade/?$ site/privacidade.php [L,QSA]
RewriteRule ^evento/([a-zA-Z0-9-]+)/?$ site/evento.php?slug=$1 [L,QSA] RewriteRule ^evento/([a-zA-Z0-9-]+)/?$ site/evento.php?slug=$1 [L,QSA]
RewriteRule ^robots\.txt$ site/seo_files.php?f=robots [L]
RewriteRule ^sitemap\.xml$ site/seo_files.php?f=sitemap [L]
# Pass the Authorization header through to PHP # Pass the Authorization header through to PHP
RewriteCond %{HTTP:Authorization} . RewriteCond %{HTTP:Authorization} .
+23
View File
@@ -101,3 +101,26 @@ function site_horario(PDO $pdo, string $local = ''): array {
try { $st = $pdo->prepare($sql); $st->execute($args); return $st->fetchAll(PDO::FETCH_ASSOC); } try { $st = $pdo->prepare($sql); $st->execute($args); return $st->fetchAll(PDO::FETCH_ASSOC); }
catch (Throwable $e) { return []; } catch (Throwable $e) { return []; }
} }
// ─── Indexing ─────────────────────────────────────────────────────────────────
// SEO_NOINDEX=true in .env keeps an install out of search results (meta robots
// + robots.txt "Disallow: /"). For staging copies such as fitme.andmore.pt,
// which serve the same content as the real domain and would otherwise compete
// with it as duplicate content.
function seo_noindex(): bool {
return filter_var(env('SEO_NOINDEX', 'false'), FILTER_VALIDATE_BOOLEAN);
}
// Public, indexable URLs for sitemap.xml: the fixed pages plus any live event
// landing page.
function site_sitemap_urls(PDO $pdo): array {
$urls = [
['loc' => base_url(), 'priority' => '1.0'],
['loc' => base_url('home/crossfit'), 'priority' => '0.8'],
['loc' => base_url('home/privacidade'), 'priority' => '0.2'],
];
foreach (site_active_events($pdo) as $ev) {
$urls[] = ['loc' => base_url('evento/' . rawurlencode($ev['slug'])), 'priority' => '0.6'];
}
return $urls;
}
+3
View File
@@ -13,3 +13,6 @@ $GLOBALS['_view_shared'] = compact('info', 'navbar', 'current_uri_2');
echo view('include'); // renders view('seo') inside <head> echo view('include'); // renders view('seo') inside <head>
echo view('navbar'); echo view('navbar');
echo view('messenger_chat'); echo view('messenger_chat');
// Page content landmark — closed at the top of site/includes/footer.php, so
// every public page gets one <main> (and the skip link a real target).
echo '<main id="conteudo">' . "\n";
+165 -1
View File
@@ -581,9 +581,19 @@ section[id] {
background: var(--fitme-white); background: var(--fitme-white);
} }
/* Team backdrop: the warm grey of the gym's block wall in the header photo
(sampled ≈ rgb(70,66,62)), a touch darker so white names still pop. On top,
very faint mortar joints in running bond (120×60 blocks, alternate rows
offset by half a block) plus a fine concrete grain, both as inline SVG
tiles, and a soft shade at the top and bottom edges. */
.trainers-section { .trainers-section {
background: var(--fitme-dark);
position: relative; position: relative;
background-color: #403d3a;
background-image:
linear-gradient(180deg, rgba(0, 0, 0, .28) 0%, transparent 18%, transparent 82%, rgba(0, 0, 0, .28) 100%),
url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='200' height='200'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.85' numOctaves='2' stitchTiles='stitch'/%3E%3CfeColorMatrix values='0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 .07 0'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E"),
url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='240' height='120'%3E%3Cpath d='M0 1H240M0 61H240M1 0V60M121 0V60M61 60V120M181 60V120' fill='none' stroke='rgba(0,0,0,0.09)' stroke-width='1.5'/%3E%3Cpath d='M0 3.5H240M0 63.5H240' fill='none' stroke='rgba(255,255,255,0.022)' stroke-width='1'/%3E%3C/svg%3E");
background-size: 100% 100%, 200px 200px, 240px 120px;
} }
.trainers-section:before { .trainers-section:before {
@@ -623,6 +633,59 @@ section[id] {
.workhours-section { .workhours-section {
background: var(--fitme-white); background: var(--fitme-white);
padding: 48px 0 44px;
}
.workhours-section .section-title {
margin-bottom: 1.4rem;
}
/* Compact opening-hours cards: dark day label on top, hours underneath,
side by side and close together (wraps to a column on phones). */
.wh-grid {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 12px;
margin: 0;
}
.wh-day {
flex: 0 1 210px;
border-radius: 6px;
overflow: hidden;
background: var(--fitme-surface);
border: 1px solid var(--fitme-surface-2);
text-align: center;
}
.wh-day dt {
background: var(--fitme-dark-3);
color: #fff;
font-size: .9rem;
font-weight: 700;
padding: 8px 10px;
}
.wh-day dd {
margin: 0;
padding: 7px 10px;
font-weight: 700;
font-size: 1rem;
color: var(--fitme-text);
}
.wh-day dd + dd {
border-top: 1px solid var(--fitme-surface-2);
}
.wh-day dd.wh-closed {
font-weight: 600;
color: var(--fitme-text-muted);
}
@media (max-width: 575px) {
.wh-day { flex-basis: 100%; }
} }
.map-section { .map-section {
@@ -698,6 +761,7 @@ section[id] {
mix-blend-mode: multiply; mix-blend-mode: multiply;
} }
.icon-box-item h3,
.icon-box-item h4 { .icon-box-item h4 {
margin-bottom: .75rem; margin-bottom: .75rem;
font-size: 1.15rem; font-size: 1.15rem;
@@ -819,6 +883,7 @@ section[id] {
visibility: hidden; visibility: hidden;
} }
.trainer-item h3,
.trainer-item h4 { .trainer-item h4 {
margin-bottom: .3rem; margin-bottom: .3rem;
} }
@@ -835,6 +900,7 @@ section[id] {
} }
.trainers-section .section-title h2, .trainers-section .section-title h2,
.trainers-section .trainer-item h3,
.trainers-section .trainer-item h4, .trainers-section .trainer-item h4,
.trainers-section .trainer-item>*:last-child { .trainers-section .trainer-item>*:last-child {
color: #fff; color: #fff;
@@ -1136,10 +1202,16 @@ section[id] {
margin-bottom: 2.5rem; margin-bottom: 2.5rem;
} }
/* The form heading is an h3 under the section's h2 — keep the h2 look. */
h3.contact-title {
font-size: 2.25rem;
}
.contact-info-warp { .contact-info-warp {
margin-bottom: 2rem; margin-bottom: 2rem;
} }
.contact-info-warp h3,
.contact-info-warp h4 { .contact-info-warp h4 {
color: var(--fitme-accent); color: var(--fitme-accent);
margin-bottom: .5rem; margin-bottom: .5rem;
@@ -2152,6 +2224,7 @@ section[id] {
max-height: 34px; max-height: 34px;
} }
.icon-box-item h3,
.icon-box-item h4 { .icon-box-item h4 {
font-size: .95rem; font-size: .95rem;
} }
@@ -2551,6 +2624,96 @@ section[id] {
display: none !important; display: none !important;
} }
/* ── Horário: compact cards (on screen, every width) ───────────────────────────
Smaller, narrower cards and type so a day reads at a glance and three days
fit side by side on desktop. The JPG export keeps its own sizing. */
.schedule:not(.schedule-export) .schedule_cell {
gap: 6px;
margin-bottom: .6rem;
}
.schedule:not(.schedule-export) .schedule_cell > .schedule_box {
flex: 0 1 112px;
padding: 6px 3px;
}
.schedule:not(.schedule-export) .schedule_box img {
height: 22px;
margin-bottom: 2px;
}
.schedule:not(.schedule-export) .schedule_box a {
font-size: 10px;
min-height: 2.3em;
}
.schedule:not(.schedule-export) .schedule_info {
margin: auto -3px -6px;
padding: 4px 6px;
font-size: 11.5px;
min-height: 2.4em;
}
.schedule:not(.schedule-export) .schedule_box:hover {
transform: scale(1.05);
}
.schedule:not(.schedule-export) .schedule_dayhead {
margin-bottom: .7rem;
}
.schedule:not(.schedule-export) .rounded_frame {
width: 104px;
height: 32px;
font-size: .95rem;
}
.schedule:not(.schedule-export) .schedule_section_label {
margin: .5rem 0 .8rem;
}
.schedule:not(.schedule-export) .schedule_section_label span {
font-size: .72rem;
}
/* ── Desktop: yesterday · today · tomorrow ─────────────────────────────────────
≥992px, main.js shows three days — the chosen one (today by default) centred
between its neighbours, wrapping round the week — and tags each visible
header/cell with .sched-pos-1/2/3 for its column. Rows stay time-aligned, so
classes at the same hour line up across the three days. The side days are
toned down so the centre one leads. */
@media (min-width: 992px) {
.schedule.is-3day:not(.schedule-export) .schedule_panel {
grid-template-columns: repeat(3, minmax(0, 420px));
column-gap: 28px;
}
.schedule.is-3day:not(.schedule-export) .schedule_section_label {
grid-column: 1 / -1;
}
.schedule.is-3day:not(.schedule-export) .sched-pos-1 { grid-column: 1 !important; }
.schedule.is-3day:not(.schedule-export) .sched-pos-2 { grid-column: 2 !important; }
.schedule.is-3day:not(.schedule-export) .sched-pos-3 { grid-column: 3 !important; }
.schedule.is-3day:not(.schedule-export) .schedule_cell.sched-side {
opacity: .5;
transition: opacity var(--fitme-transition);
}
.schedule.is-3day:not(.schedule-export) .schedule_cell.sched-side:hover {
opacity: 1;
}
.schedule.is-3day:not(.schedule-export) .schedule_dayhead.sched-side .rounded_frame {
background: var(--fitme-surface-2);
color: var(--fitme-text-muted);
}
.schedule.is-3day:not(.schedule-export) .schedule_dayhead.sched-pos-2 .rounded_frame {
background: var(--fitme-accent);
color: var(--fitme-dark);
}
.schedule.is-3day:not(.schedule-export) .schedule_dayhead.sched-side {
cursor: pointer;
}
}
/* ── Aula experimental modal ───────────────────────────────────────────────── */ /* ── Aula experimental modal ───────────────────────────────────────────────── */
.modal-content.trial-modal { .modal-content.trial-modal {
background: #fff !important; background: #fff !important;
@@ -2963,6 +3126,7 @@ section[id] {
/* The name used to be the tile's last child and got its size from the /* The name used to be the tile's last child and got its size from the
`.trainer-item>*:last-child` rule; pin it so it stays the same whether or `.trainer-item>*:last-child` rule; pin it so it stays the same whether or
not a title follows it. */ not a title follows it. */
.trainers-section .trainer-item h3,
.trainers-section .trainer-item h4 { .trainers-section .trainer-item h4 {
font-size: .9rem; font-size: .9rem;
} }
+38 -9
View File
@@ -152,10 +152,11 @@ document.addEventListener('DOMContentLoaded', function () {
}); });
/* ── Schedule day filter ───────────────────────────────────────────────────── /* ── Schedule day filter ─────────────────────────────────────────────────────
The on-screen schedule shows one day at a time; a day-picker bar switches days The on-screen schedule shows the chosen day (phones) or that day centred
(each header + class cell is tagged with data-day). Defaults to today when the between the day before and after (desktop ≥992px); a day-picker bar switches
gym is open that day, otherwise the first day. The JPG export is unaffected — days (each header + class cell is tagged with data-day). Defaults to today
its clone removes every .sched-hide and re-renders the full week. */ when the gym is open that day, otherwise the first day. The JPG export is
unaffected — its clone removes every .sched-hide and re-renders the full week. */
document.addEventListener('DOMContentLoaded', function () { document.addEventListener('DOMContentLoaded', function () {
// Works on the home page (#horario) and the CrossFit page (bare .schedule). // Works on the home page (#horario) and the CrossFit page (bare .schedule).
var scope = document.querySelector('.schedule'); var scope = document.querySelector('.schedule');
@@ -166,19 +167,40 @@ document.addEventListener('DOMContentLoaded', function () {
var buttons = filter.querySelectorAll('.sdf-btn'); var buttons = filter.querySelectorAll('.sdf-btn');
var labels = scope.querySelectorAll('.schedule_section_label'); var labels = scope.querySelectorAll('.schedule_section_label');
// Toggle a class (not inline display) — .sched-hide carries !important so it // Desktop shows three days — the chosen one centred between the day before
// reliably wins, and the export clone simply strips it to show every day. // and the day after (wrapping round the week); phones show just the one.
var wide = window.matchMedia('(min-width: 992px)');
var week = Array.prototype.map.call(buttons, function (b) { return b.getAttribute('data-day'); });
var current = null;
// Toggle classes (not inline display) — .sched-hide carries !important so it
// reliably wins, and the export clone simply strips it (and ignores the
// .sched-pos-* columns) to show every day.
function showDay(day) { function showDay(day) {
current = day;
var i = week.indexOf(day), n = week.length;
var three = wide.matches && n >= 3;
var visible = three ? [week[(i - 1 + n) % n], day, week[(i + 1) % n]] : [day];
scope.classList.toggle('is-3day', three);
cells.forEach(function (c) { cells.forEach(function (c) {
c.classList.toggle('sched-hide', c.getAttribute('data-day') !== day); var pos = visible.indexOf(c.getAttribute('data-day'));
c.classList.toggle('sched-hide', pos === -1);
c.classList.remove('sched-pos-1', 'sched-pos-2', 'sched-pos-3', 'sched-side');
if (pos !== -1) {
var col = three ? pos + 1 : 1;
c.classList.add('sched-pos-' + col);
if (three && col !== 2) c.classList.add('sched-side');
}
}); });
buttons.forEach(function (b) { buttons.forEach(function (b) {
b.classList.toggle('active', b.getAttribute('data-day') === day); b.classList.toggle('active', b.getAttribute('data-day') === day);
}); });
// Show a Manhã / Tarde divider only when the day has a class in that block. // Show a Manhã / Tarde divider only when a visible day has a class in that block.
labels.forEach(function (lbl) { labels.forEach(function (lbl) {
var sec = lbl.getAttribute('data-section'); var sec = lbl.getAttribute('data-section');
var has = !!scope.querySelector('.schedule_cell[data-day="' + day + '"][data-section="' + sec + '"]'); var has = visible.some(function (d) {
return !!scope.querySelector('.schedule_cell[data-day="' + d + '"][data-section="' + sec + '"]');
});
lbl.classList.toggle('sched-hide', !has); lbl.classList.toggle('sched-hide', !has);
}); });
} }
@@ -187,6 +209,13 @@ document.addEventListener('DOMContentLoaded', function () {
var btn = e.target.closest('.sdf-btn'); var btn = e.target.closest('.sdf-btn');
if (btn) showDay(btn.getAttribute('data-day')); if (btn) showDay(btn.getAttribute('data-day'));
}); });
// Clicking a side day's header centres it.
scope.addEventListener('click', function (e) {
var head = e.target.closest('.schedule_dayhead.sched-side');
if (head) showDay(head.getAttribute('data-day'));
});
var reflow = function () { if (current) showDay(current); };
if (wide.addEventListener) wide.addEventListener('change', reflow); else if (wide.addListener) wide.addListener(reflow);
// Pre-select today when the schedule has a button for it (PT day names), // Pre-select today when the schedule has a button for it (PT day names),
// otherwise the first day. // otherwise the first day.
+2
View File
@@ -1,3 +1,5 @@
</main><?php /* opened at the end of site/_open.php */ ?>
<!-- Footer section --> <!-- Footer section -->
<footer class="footer-section set-bg fade-in" <footer class="footer-section set-bg fade-in"
data-setbg="<?= base_url(($current_uri_2 ?? '') == 'crossfit' ? 'site/assets/img/footer-bg-cf.jpg' : 'site/assets/img/footer-bg-gym.jpg') ?>"> data-setbg="<?= base_url(($current_uri_2 ?? '') == 'crossfit' ? 'site/assets/img/footer-bg-cf.jpg' : 'site/assets/img/footer-bg-gym.jpg') ?>">
+2 -2
View File
@@ -17,8 +17,8 @@ if (!$files || !is_array($files)) $files = [];
<div id="primary-slider" class="splide" style="padding:0;"> <div id="primary-slider" class="splide" style="padding:0;">
<div class="splide__track"> <div class="splide__track">
<ul class="splide__list"> <ul class="splide__list">
<?php foreach($files as $f): ?> <?php foreach($files as $gi => $f): /* distinct alt per photo — identical alts read as one image */ ?>
<li class="splide__slide"><img src="<?= base_url($f) ?>" alt="Instalações FitMe 3070"></li> <li class="splide__slide"><img src="<?= base_url($f) ?>" alt="<?= htmlspecialchars(($gallery_h2 ?? 'Instalações') . ' — FitMe 3070, foto ' . ($gi + 1)) ?>"<?= $gi > 0 ? ' loading="lazy"' : '' ?>></li>
<?php endforeach; ?> <?php endforeach; ?>
</ul> </ul>
</div> </div>
+1 -1
View File
@@ -7,7 +7,7 @@
<div class="container h-100"> <div class="container h-100">
<div class="d-flex h-100 text-center align-items-center"> <div class="d-flex h-100 text-center align-items-center">
<div class="w-100 text-white"> <div class="w-100 text-white">
<h1 class="display-3"><img src="<?= base_url('site/assets/img/logos/logo_crossfit_white.svg')?>" alt="CrossFit" style="opacity: .85; max-width: 320px;"></h1> <h1 class="display-3"><img src="<?= base_url('site/assets/img/logos/logo_crossfit_white.svg')?>" alt="CrossFit 3070 — box de CrossFit em Mira" style="opacity: .85; max-width: 320px;"></h1>
<p class="lead mb-0"></p> <p class="lead mb-0"></p>
</div> </div>
</div> </div>
+4 -1
View File
@@ -25,6 +25,9 @@ $canonical = $GLOBALS['_canonical_override'] ?? base_url($page !== '' ? 'home/'
<meta name="theme-color" content="#C2D500"> <meta name="theme-color" content="#C2D500">
<title><?= htmlspecialchars($page_title) ?></title> <title><?= htmlspecialchars($page_title) ?></title>
<link rel="canonical" href="<?= $canonical ?>"> <link rel="canonical" href="<?= $canonical ?>">
<?php if (seo_noindex()): /* staging copies (e.g. fitme.andmore.pt) must not compete with the real domain */ ?>
<meta name="robots" content="noindex, nofollow">
<?php endif; ?>
<!-- Preconnect to external origins --> <!-- Preconnect to external origins -->
<link rel="preconnect" href="https://fonts.googleapis.com" crossorigin> <link rel="preconnect" href="https://fonts.googleapis.com" crossorigin>
@@ -72,4 +75,4 @@ $canonical = $GLOBALS['_canonical_override'] ?? base_url($page !== '' ? 'home/'
<?= view('seo') ?> <?= view('seo') ?>
</head> </head>
<body> <body>
<a href="#home" class="skip-link">Saltar para o conteúdo</a> <a href="#conteudo" class="skip-link">Saltar para o conteúdo</a>
+39 -43
View File
@@ -50,7 +50,6 @@ $reviews = site_reviews(); // null when not configured / nothin
<?php endforeach; ?> <?php endforeach; ?>
</div> </div>
<?php endif; ?> <?php endif; ?>
<span class="hero-brand">FitMe 3070</span>
<h1>Ginásio, Box, Aulas<br><span>&amp; Acompanhamento</span></h1> <h1>Ginásio, Box, Aulas<br><span>&amp; Acompanhamento</span></h1>
<div class="hero-cta-row"> <div class="hero-cta-row">
<a href="?assunto=insc#contactos" class="site-btn">Inscreve-te Já!</a> <a href="?assunto=insc#contactos" class="site-btn">Inscreve-te Já!</a>
@@ -63,13 +62,13 @@ $reviews = site_reviews(); // null when not configured / nothin
// ── Oferta (Feature section) ─────────────────────────────────────────────────── // ── Oferta (Feature section) ───────────────────────────────────────────────────
if ($offer): if ($offer):
?> ?>
<section class="feature-section"> <section id="oferta" class="feature-section" aria-labelledby="oferta-title">
<div class="container"> <div class="container">
<div class="row justify-content-md-center"> <div class="row justify-content-md-center">
<div class="col-lg-8"> <div class="col-lg-8">
<div class="section-title text-center deco"> <div class="section-title text-center deco">
<span class="section-eyebrow">FitMe 3070</span> <span class="section-eyebrow">FitMe 3070</span>
<h2>A Nossa Oferta</h2> <h2 id="oferta-title">A Nossa Oferta</h2>
<p class="section-subtitle">Tudo o que precisas para evoluir, num só espaço.</p> <p class="section-subtitle">Tudo o que precisas para evoluir, num só espaço.</p>
</div> </div>
</div> </div>
@@ -81,7 +80,7 @@ if ($offer):
<div class="ib-icon"> <div class="ib-icon">
<img loading="lazy" src="<?= site_img_url($o['o_img'], 'uploads/website/offer') ?>" alt="<?= htmlspecialchars((string) $o['o_title']) ?>"> <img loading="lazy" src="<?= site_img_url($o['o_img'], 'uploads/website/offer') ?>" alt="<?= htmlspecialchars((string) $o['o_title']) ?>">
</div> </div>
<h4><?= htmlspecialchars((string) $o['o_title']) ?></h4> <h3><?= htmlspecialchars((string) $o['o_title']) ?></h3>
<p><?= htmlspecialchars((string) $o['o_description']) ?></p> <p><?= htmlspecialchars((string) $o['o_description']) ?></p>
</div> </div>
</div> </div>
@@ -107,13 +106,13 @@ $gallery_h2 = 'Conhece as nossas instalações';
// ── Equipa ────────────────────────────────────────────────────────────────────── // ── Equipa ──────────────────────────────────────────────────────────────────────
if ($team): if ($team):
?> ?>
<section id="equipa" class="trainers-section"> <section id="equipa" class="trainers-section" aria-labelledby="equipa-title">
<div class="container"> <div class="container">
<div class="row justify-content-md-center mb-4"> <div class="row justify-content-md-center mb-4">
<div class="col-lg-8"> <div class="col-lg-8">
<div class="section-title text-center deco"> <div class="section-title text-center deco">
<span class="section-eyebrow">Quem somos</span> <span class="section-eyebrow">Quem somos</span>
<h2>Conhece os membros da equipa 3070</h2> <h2 id="equipa-title">Conhece os membros da equipa 3070</h2>
<p class="section-subtitle">Profissionais dedicados a ajudar-te a alcançar os teus objetivos.</p> <p class="section-subtitle">Profissionais dedicados a ajudar-te a alcançar os teus objetivos.</p>
</div> </div>
</div> </div>
@@ -137,7 +136,7 @@ if ($team):
<div class="trainer-pic"> <div class="trainer-pic">
<img loading="lazy" src="<?= $t_img ?>" alt="<?= htmlspecialchars((string) $d['name']) ?>"> <img loading="lazy" src="<?= $t_img ?>" alt="<?= htmlspecialchars((string) $d['name']) ?>">
</div> </div>
<h4><?= htmlspecialchars((string) $d['name']) ?></h4> <h3><?= htmlspecialchars((string) $d['name']) ?></h3>
<?php if ($t_title !== ''): ?><p class="trainer-title"><?= htmlspecialchars($t_title) ?></p><?php endif; ?> <?php if ($t_title !== ''): ?><p class="trainer-title"><?= htmlspecialchars($t_title) ?></p><?php endif; ?>
<?php if ($t_detail): ?> <?php if ($t_detail): ?>
<div class="trainer-detail" id="<?= $t_id ?>"> <div class="trainer-detail" id="<?= $t_id ?>">
@@ -214,14 +213,14 @@ $today_map = [1=>'segunda', 2=>'terça', 3=>'quarta', 4=>'quinta', 5=>'sexta',
$default_day = $today_map[(int) date('N')] ?? null; $default_day = $today_map[(int) date('N')] ?? null;
if (!in_array($default_day, $week_values, true)) { $default_day = $week_values[0]; } if (!in_array($default_day, $week_values, true)) { $default_day = $week_values[0]; }
?> ?>
<section id="horario" class="schedule-section" style="padding:60px 0;"> <section id="horario" class="schedule-section" style="padding:60px 0;" aria-labelledby="horario-title">
<div class="schedule fade-in"> <div class="schedule fade-in">
<div class="container"> <div class="container">
<div class="row justify-content-md-center mb-4"> <div class="row justify-content-md-center mb-4">
<div class="col-lg-8"> <div class="col-lg-8">
<div class="section-title text-center deco"> <div class="section-title text-center deco">
<span class="section-eyebrow">Calendário</span> <span class="section-eyebrow">Calendário</span>
<h2>Horário de Aulas</h2> <h2 id="horario-title">Horário de Aulas</h2>
<p class="section-subtitle">Planeia a tua semana de treinos connosco.</p> <p class="section-subtitle">Planeia a tua semana de treinos connosco.</p>
</div> </div>
<div class="text-center mb-2"> <div class="text-center mb-2">
@@ -426,13 +425,13 @@ if (!in_array($default_day, $week_values, true)) { $default_day = $week_values[0
// the request as a lead and emails the gym). // the request as a lead and emails the gym).
if ($memberships): if ($memberships):
?> ?>
<section id="mensalidades" class="pricing-section"> <section id="mensalidades" class="pricing-section" aria-labelledby="mensalidades-title">
<div class="container"> <div class="container">
<div class="row justify-content-md-center mb-4"> <div class="row justify-content-md-center mb-4">
<div class="col-lg-8"> <div class="col-lg-8">
<div class="section-title text-center deco"> <div class="section-title text-center deco">
<span class="section-eyebrow">Mensalidades</span> <span class="section-eyebrow">Mensalidades</span>
<h2>Escolhe o teu plano</h2> <h2 id="mensalidades-title">Escolhe o teu plano</h2>
<p class="section-subtitle">Encontra a mensalidade que encaixa na tua rotina.</p> <p class="section-subtitle">Encontra a mensalidade que encaixa na tua rotina.</p>
</div> </div>
</div> </div>
@@ -480,13 +479,13 @@ if ($reviews):
return $out; return $out;
}; };
?> ?>
<section id="reviews" class="reviews-section"> <section id="reviews" class="reviews-section" aria-labelledby="reviews-title">
<div class="container"> <div class="container">
<div class="row justify-content-md-center mb-4"> <div class="row justify-content-md-center mb-4">
<div class="col-lg-8"> <div class="col-lg-8">
<div class="section-title text-center deco"> <div class="section-title text-center deco">
<span class="section-eyebrow">Reviews</span> <span class="section-eyebrow">Reviews</span>
<h2>O que dizem de nós</h2> <h2 id="reviews-title">O que dizem de nós</h2>
<?php if ($reviews['rating'] !== null): ?> <?php if ($reviews['rating'] !== null): ?>
<p class="reviews-summary"> <p class="reviews-summary">
<strong><?= number_format((float) $reviews['rating'], 1, ',', '') ?></strong> <strong><?= number_format((float) $reviews['rating'], 1, ',', '') ?></strong>
@@ -539,7 +538,7 @@ if (isset($_GET['assunto'])) {
$session = session(); $session = session();
?> ?>
<?php /* contacts-bg.jpg never existed — outside2.jpg is the current photo in img/contactos/ */ ?> <?php /* contacts-bg.jpg never existed — outside2.jpg is the current photo in img/contactos/ */ ?>
<section id="contactos" class="contact-section set-bg" data-setbg="<?= base_url('site/assets/img/contactos/outside2.jpg') ?>"> <section id="contactos" class="contact-section set-bg" data-setbg="<?= base_url('site/assets/img/contactos/outside2.jpg') ?>" aria-labelledby="contactos-title">
<div class="container"> <div class="container">
<?php if (($_GET['agendar'] ?? '') === 'ok'): ?> <?php if (($_GET['agendar'] ?? '') === 'ok'): ?>
<div class="alert alert-success text-center" role="alert"> <div class="alert alert-success text-center" role="alert">
@@ -552,9 +551,9 @@ $session = session();
<?php endif; ?> <?php endif; ?>
<div class="row"> <div class="row">
<div class="col-lg-4"> <div class="col-lg-4">
<h2 class="contact-title text-center text-md-left">Informações</h2> <h2 id="contactos-title" class="contact-title text-center text-md-left">Informações</h2>
<div class="contact-info-warp text-center text-md-left"> <div class="contact-info-warp text-center text-md-left">
<h4>Localização</h4> <h3>Localização</h3>
<div class="contact-info text-center text-md-left"> <div class="contact-info text-center text-md-left">
<i class="fas fa-map-marker-alt"></i> <i class="fas fa-map-marker-alt"></i>
<div class="cf-text"> <div class="cf-text">
@@ -565,7 +564,7 @@ $session = session();
</div> </div>
</div> </div>
<div class="contact-info-warp text-center text-md-left"> <div class="contact-info-warp text-center text-md-left">
<h4>Telefone</h4> <h3>Telefone</h3>
<div class="contact-info text-center text-md-left"> <div class="contact-info text-center text-md-left">
<i class="fas fa-phone-volume"></i> <i class="fas fa-phone-volume"></i>
<div class="cf-text"> <div class="cf-text">
@@ -581,7 +580,7 @@ $session = session();
</div> </div>
</div> </div>
<div class="contact-info-warp text-center text-md-left"> <div class="contact-info-warp text-center text-md-left">
<h4>E-mail</h4> <h3>E-mail</h3>
<div class="contact-info"> <div class="contact-info">
<i class="fas fa-envelope"></i> <i class="fas fa-envelope"></i>
<div class="cf-text"> <div class="cf-text">
@@ -607,8 +606,8 @@ $session = session();
</div> </div>
</div> </div>
<div class="col-lg-8"> <div class="col-lg-8">
<section id="contact-form"> <div id="contact-form">
<h2 class="contact-title text-center text-md-left">Envia-nos uma mensagem</h2> <h3 class="contact-title text-center text-md-left">Envia-nos uma mensagem</h3>
<?php if (isset($_GET['enviado'])): ?> <?php if (isset($_GET['enviado'])): ?>
<div class="alert alert-success" role="alert"> <div class="alert alert-success" role="alert">
Mensagem enviada com sucesso! Entraremos em contacto brevemente. Mensagem enviada com sucesso! Entraremos em contacto brevemente.
@@ -639,7 +638,7 @@ $session = session();
</label> </label>
<button class="site-btn" id="btn_email" type="submit">Enviar Mensagem</button> <button class="site-btn" id="btn_email" type="submit">Enviar Mensagem</button>
</form> </form>
</section> </div>
</div> </div>
</div> </div>
</div> </div>
@@ -649,31 +648,27 @@ $session = session();
// ── Horário de Funcionamento ────────────────────────────────────────────────── // ── Horário de Funcionamento ──────────────────────────────────────────────────
if ($whours): if ($whours):
$work_hours_days = ['Segunda-Sexta'=>'segunda','Sábado'=>'sabado','Domingo/Feriados'=>'domingo']; $work_hours_days = ['Segunda-Sexta'=>'segunda','Sábado'=>'sabado','Domingo/Feriados'=>'domingo'];
usort($whours, fn($x, $y) => strcmp((string) $x['open'], (string) $y['open'])); // morning before afternoon
?> ?>
<section id="workhours" class="workhours-section" style="padding:40px 0;"> <section id="workhours" class="workhours-section" aria-labelledby="workhours-title">
<div class="container"> <div class="container">
<div class="row justify-content-md-center mb-4"> <div class="section-title text-center deco">
<div class="col-lg-8"> <span class="section-eyebrow">Visita-nos</span>
<div class="section-title text-center deco"> <h2 id="workhours-title">Horário de Funcionamento</h2>
<span class="section-eyebrow">Visita-nos</span>
<h2>Horário de Funcionamento</h2>
<p class="section-subtitle">Estamos abertos quando precisas de treinar.</p>
</div>
</div>
</div> </div>
<div class="row justify-content-center"> <?php // Compact day / hours cards. A <dl> so the pairs read as data
// (label → value) for screen readers and search engines. ?>
<dl class="wh-grid">
<?php foreach ($work_hours_days as $key => $value): ?> <?php foreach ($work_hours_days as $key => $value): ?>
<div class="col-md-4 text-center"> <div class="wh-day">
<a class="schedule_text rounded_frame_medium mt-3 schedule_frame"><?= $key ?></a> <dt><?= htmlspecialchars($key) ?></dt>
<?php foreach($whours as $h): ?> <?php $any = false; foreach ($whours as $h): if ($h['week_day'] == $value): $any = true; ?>
<?php if($h['week_day'] == $value): ?> <dd><?= htmlspecialchars(substr($h['open'], 0, 5) . ' – ' . substr($h['close'], 0, 5)) ?></dd>
<div class="rounded_frame_grey mt-3">
<p><b><?= htmlspecialchars(substr($h['open'], 0, 5) . ' - ' . substr($h['close'], 0, 5)) ?></b></p>
</div>
<?php endif; endforeach; ?> <?php endif; endforeach; ?>
<?php if (!$any): ?><dd class="wh-closed">Encerrado</dd><?php endif; ?>
</div> </div>
<?php endforeach; ?> <?php endforeach; ?>
</div> </dl>
</div> </div>
</section> </section>
<?php endif; ?> <?php endif; ?>
@@ -681,12 +676,12 @@ $work_hours_days = ['Segunda-Sexta'=>'segunda','Sábado'=>'sabado','Domingo/Feri
<?php <?php
// ── Personal Trainer CTA ──────────────────────────────────────────────────────── // ── Personal Trainer CTA ────────────────────────────────────────────────────────
?> ?>
<section class="classes-section" data-setbg=""> <section id="personal-trainer" class="classes-section" aria-labelledby="pt-title">
<div class="container"> <div class="container">
<div class="row"> <div class="row">
<div class="col-lg-6"> <div class="col-lg-6">
<div class="section-title"> <div class="section-title">
<h2>Precisas de um <span>Personal Trainer</span>?</h2> <h2 id="pt-title">Precisas de um <span>Personal Trainer</span>?</h2>
</div> </div>
</div> </div>
<div class="col-lg-6"> <div class="col-lg-6">
@@ -814,6 +809,7 @@ if ($memberships):
<?php endif; ?> <?php endif; ?>
<?php <?php
// ── Newsletter + Footer ─────────────────────────────────────────────────────── // ── Footer ────────────────────────────────────────────────────────────────────
echo view('newsletter'); // Newsletter hidden on request (2026-09); the partial stays in
// site/includes/newsletter.php — re-enable with: echo view('newsletter');
echo view('footer'); echo view('footer');
+38
View File
@@ -0,0 +1,38 @@
<?php
// robots.txt and sitemap.xml, generated so they always carry this install's
// own base URL (the site moves between domains) and honour SEO_NOINDEX.
//
// Reached via .htaccess: ^robots\.txt$ → ?f=robots, ^sitemap\.xml$ → ?f=sitemap
require_once __DIR__ . '/../config/db.php';
require_once __DIR__ . '/../config/functions.php';
$file = (string) ($_GET['f'] ?? '');
if ($file === 'robots') {
header('Content-Type: text/plain; charset=utf-8');
echo "User-agent: *\n";
if (seo_noindex()) {
echo "Disallow: /\n";
exit;
}
// Backoffice, endpoints and internals have nothing for a search engine.
foreach (['/views/', '/api/', '/shared/', '/config/', '/database/', '/includes/', '/templates/', '/cron.php'] as $p) {
echo "Disallow: {$p}\n";
}
echo "\nSitemap: " . base_url('sitemap.xml') . "\n";
exit;
}
if ($file === 'sitemap') {
if (seo_noindex()) { http_response_code(404); exit; }
header('Content-Type: application/xml; charset=utf-8');
echo '<?xml version="1.0" encoding="UTF-8"?>' . "\n";
echo '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">' . "\n";
foreach (site_sitemap_urls($pdo) as $u) {
echo ' <url><loc>' . htmlspecialchars($u['loc'], ENT_XML1) . '</loc><priority>' . $u['priority'] . "</priority></url>\n";
}
echo "</urlset>\n";
exit;
}
http_response_code(404);