feat: rinomina sezione Programmi in Servizi (route, tag, migrazione DB)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-05 20:23:48 +02:00
parent c975dcac12
commit 8ceea9b16f
20 changed files with 226 additions and 118 deletions
+3 -3
View File
@@ -19,9 +19,9 @@ const year = new Date().getFullYear();
<T tag="footer.col.training-title" as="h4" />
{site.footerTraining.map((l, i) => <a href={l.href}>{t(`footer.training.${i + 1}.label`)}</a>)}
</nav>
<nav aria-label="programmi">
<T tag="footer.col.programs-title" as="h4" />
{site.footerPrograms.map((l, i) => <a href={l.href}>{t(`footer.programs.${i + 1}.label`)}</a>)}
<nav aria-label="servizi">
<T tag="footer.col.services-title" as="h4" />
{site.footerServices.map((l, i) => <a href={l.href}>{t(`footer.services.${i + 1}.label`)}</a>)}
</nav>
<div>
<T tag="footer.col.contact-title" as="h4" />
+1 -1
View File
@@ -19,7 +19,7 @@ const blocks = [
<div class="feat__text" data-reveal={b.reverse ? 'left' : 'right'}>
<T tag={`home.feature.${b.key}.title`} as="h2" />
<T tag={`home.feature.${b.key}.body`} as="p" />
<a class="btn" href="/programs">{t(`home.feature.${b.key}.cta`)}</a>
<a class="btn" href="/services">{t(`home.feature.${b.key}.cta`)}</a>
</div>
</div>
))}
+1 -1
View File
@@ -13,7 +13,7 @@ const slides = [
<div class="container hero__in">
<div class="hero__text">
<h1>{s.titleTags.map((tag) => <T tag={tag} as="span" class="hero__line" />)}</h1>
<a class="btn" href="/programs">{t('home.hero.cta')}</a>
<a class="btn" href="/services">{t('home.hero.cta')}</a>
</div>
<TImg tag="home.hero.image" src={s.image} alt={s.alt} class="hero__img" widths={[600, 1000, 1400]} sizes="(max-width: 991px) 100vw, 60vw" loading="eager" />
</div>
+1 -1
View File
@@ -15,7 +15,7 @@ const cols = await Promise.all([
---
<section class="info" aria-label="pilastri">
{cols.map((c, i) => (
<a class="info__col" href="/programs" style={`background-image:url(${c.bg});--reveal-delay:${i * 0.15}s`} data-reveal="fade" data-tag={c.imageTag}>
<a class="info__col" href="/services" style={`background-image:url(${c.bg});--reveal-delay:${i * 0.15}s`} data-reveal="fade" data-tag={c.imageTag}>
<T tag={c.titleTag} as="span" class="info__title" />
</a>
))}
@@ -1,20 +1,20 @@
---
import { programs } from '../../data/programs';
import { programIcons as icons } from '../../data/program-icons';
import { services } from '../../data/services';
import { serviceIcons as icons } from '../../data/service-icons';
import T from '../content/T.astro';
---
<section class="section">
<div class="container">
<div class="section-heading" data-reveal>
<T tag="home.programs.title" as="h2" />
<T tag="home.programs.body" as="p" />
<T tag="home.services.title" as="h2" />
<T tag="home.services.body" as="p" />
</div>
<div class="pgrid">
{programs.map((p, i) => (
<a class="pgrid__item" href={`/programs/${p.slug}`} data-reveal style={`--reveal-delay:${(i % 3) * 0.15}s`}>
{services.map((p, i) => (
<a class="pgrid__item" href={`/services/${p.slug}`} data-reveal style={`--reveal-delay:${(i % 3) * 0.15}s`}>
<svg viewBox="0 0 24 24" fill="none" stroke="var(--c-accent-dark)" stroke-width="1.4" width="52" height="52" aria-hidden="true"><path d={icons[p.slug]} /></svg>
<T tag={`program.${p.slug}.title`} as="h3" />
<T tag={`program.${p.slug}.subtitle`} as="p" />
<T tag={`service.${p.slug}.title`} as="h3" />
<T tag={`service.${p.slug}.subtitle`} as="p" />
</a>
))}
</div>
+40 -40
View File
@@ -5,7 +5,7 @@ import { team } from './team';
import { methodIntro, methodExtraNote, methodSteps } from './method';
import { partners } from './partners';
import { disciplines, days, slots } from './agenda';
import { programs } from './programs';
import { services } from './services';
import { performanceClassPricing } from './pricing';
export type ContentType = 'text' | 'html' | 'image';
@@ -40,7 +40,7 @@ export const contentSeed: SeedEntry[] = [
H('footer.tagline', 'PERFORMANCE.&nbsp; BALANCE.&nbsp; LONGEVITY.'),
T('footer.about', 'Promuoviamo il movimento come cura di sé. Abitudini sostenibili per benessere fisico e mentale.'),
T('footer.col.training-title', 'Training'),
T('footer.col.programs-title', 'Programmi'),
T('footer.col.services-title', 'Servizi'),
T('footer.col.contact-title', 'Contact'),
H('footer.copyright', '<strong>Insanitylab</strong>, All Rights Reserved'),
T('footer.social-label', 'Seguici', []),
@@ -48,12 +48,12 @@ export const contentSeed: SeedEntry[] = [
T('footer.training.2.label', site.footerTraining[1].label, []),
T('footer.training.3.label', site.footerTraining[2].label, []),
T('footer.training.4.label', site.footerTraining[3].label, []),
T('footer.programs.1.label', site.footerPrograms[0].label, []),
T('footer.programs.2.label', site.footerPrograms[1].label, []),
T('footer.programs.3.label', site.footerPrograms[2].label, []),
T('footer.programs.4.label', site.footerPrograms[3].label, []),
T('footer.programs.5.label', site.footerPrograms[4].label, []),
T('footer.programs.6.label', site.footerPrograms[5].label, []),
T('footer.services.1.label', site.footerServices[0].label, []),
T('footer.services.2.label', site.footerServices[1].label, []),
T('footer.services.3.label', site.footerServices[2].label, []),
T('footer.services.4.label', site.footerServices[3].label, []),
T('footer.services.5.label', site.footerServices[4].label, []),
T('footer.services.6.label', site.footerServices[5].label, []),
// --- Contact form ---
T('form.field.first-name', 'Nome', []),
@@ -115,9 +115,9 @@ export const contentSeed: SeedEntry[] = [
I(`training.${tr.id}.image`),
]),
// --- Home: programmi ---
T('home.programs.title', 'Programmi'),
T('home.programs.body', 'Questi sono i programmi delle classi di Insanity Lab selezionati per voi. Abbiamo scelto i migliori percorsi per offrirvi unesperienza di allenamento completa e mirata.'),
// --- Home: servizi ---
T('home.services.title', 'Servizi'),
T('home.services.body', 'Questi sono i servizi delle classi di Insanity Lab selezionati per voi. Abbiamo scelto i migliori percorsi per offrirvi unesperienza di allenamento completa e mirata.'),
// --- Home: team (data-driven da team.ts) ---
H('home.team.title', 'Team<br />Trainer'),
@@ -223,40 +223,40 @@ export const contentSeed: SeedEntry[] = [
T('about.timeline.stage2.text', 'Contenuto in arrivo: il cliente fornirà il testo di questa tappa.'),
T('about.timeline.stage3.label', 'INSANITYLAB OGGI', []),
T('about.timeline.stage3.text', 'Contenuto in arrivo: il cliente fornirà il testo di questa tappa.'),
// --- Programs: lista (/programs) ---
T('programs.meta.title', 'Programmi', []),
T('programs.meta.description', 'I programmi InsanityLab: Personal Training, Performance Class, Coaching, Pilates Flow, Rehab e Nutrizione.', []),
T('programs.hero.title', 'Programmi', []),
T('programs.intro.title', 'Programmi'),
T('programs.intro.body', "Questi sono i programmi delle classi di Insanity Lab selezionati per voi. Abbiamo scelto i migliori percorsi per offrirvi un'esperienza di allenamento completa e mirata."),
T('programs.card.more-label', 'Per saperne di più →', []),
// --- Services: lista (/services) ---
T('services.meta.title', 'Servizi', []),
T('services.meta.description', 'I servizi InsanityLab: Personal Training, Performance Class, Coaching, Pilates Flow, Rehab e Nutrizione.', []),
T('services.hero.title', 'Servizi', []),
T('services.intro.title', 'Servizi'),
T('services.intro.body', "Questi sono i servizi delle classi di Insanity Lab selezionati per voi. Abbiamo scelto i migliori percorsi per offrirvi un'esperienza di allenamento completa e mirata."),
T('services.card.more-label', 'Per saperne di più →', []),
// --- Programs: dettaglio (data-driven da programs.ts) ---
...programs.flatMap((p) => [
T(`program.${p.slug}.title`, p.title),
T(`program.${p.slug}.subtitle`, p.subtitle),
T(`program.${p.slug}.excerpt`, p.excerpt),
T(`program.${p.slug}.long-description`, p.longDescription),
...p.features.map((f, i) => T(`program.${p.slug}.feature.${i + 1}`, f)),
// --- Services: dettaglio (data-driven da services.ts) ---
...services.flatMap((p) => [
T(`service.${p.slug}.title`, p.title),
T(`service.${p.slug}.subtitle`, p.subtitle),
T(`service.${p.slug}.excerpt`, p.excerpt),
T(`service.${p.slug}.long-description`, p.longDescription),
...p.features.map((f, i) => T(`service.${p.slug}.feature.${i + 1}`, f)),
...p.faq.flatMap((item, i) => [
T(`program.${p.slug}.faq.${i + 1}.q`, item.q),
T(`program.${p.slug}.faq.${i + 1}.a`, item.a),
T(`service.${p.slug}.faq.${i + 1}.q`, item.q),
T(`service.${p.slug}.faq.${i + 1}.a`, item.a),
]),
I(`program.${p.slug}.image`),
I(`service.${p.slug}.image`),
]),
// --- Programs: dettaglio, template ---
T('program.side.categories-label', 'Categorie'),
T('program.side.category.1', 'Fitness', []),
T('program.side.category.2', 'Salute', []),
T('program.side.category.3', 'Stile di vita', []),
T('program.side.category.4', 'Nutrizione', []),
T('program.side.others-label', 'Altri programmi'),
H('program.pricing.empty-note', 'Contattaci per i dettagli di questo programma. <a href="/contact">Scrivici →</a>'),
T('program.pricing.per', 'al mese, semestrale', []),
T('program.pricing.entries-singular', 'ingresso settimanale', []),
T('program.pricing.entries-plural', 'ingressi settimanali', []),
T('program.pricing.buy-cta', 'Acquista', []),
// --- Services: dettaglio, template ---
T('service.side.categories-label', 'Categorie'),
T('service.side.category.1', 'Fitness', []),
T('service.side.category.2', 'Salute', []),
T('service.side.category.3', 'Stile di vita', []),
T('service.side.category.4', 'Nutrizione', []),
T('service.side.others-label', 'Altri servizi'),
H('service.pricing.empty-note', 'Contattaci per i dettagli di questo servizio. <a href="/contact">Scrivici →</a>'),
T('service.pricing.per', 'al mese, semestrale', []),
T('service.pricing.entries-singular', 'ingresso settimanale', []),
T('service.pricing.entries-plural', 'ingressi settimanali', []),
T('service.pricing.buy-cta', 'Acquista', []),
// --- Pricing performance class (data-driven da pricing.ts; monthly/entries restano nel data file) ---
T('pricing.heading', performanceClassPricing.heading),
@@ -1,5 +1,5 @@
// Path SVG (stroke, viewBox 0 0 24 24) delle icone programmi, condivisi tra griglia home e sidebar
export const programIcons: Record<string, string> = {
// Path SVG (stroke, viewBox 0 0 24 24) delle icone servizi, condivisi tra griglia home e sidebar
export const serviceIcons: Record<string, string> = {
'personal-training': 'M4 12h3m10 0h3M9 8v8m6-8v8M7 10v4m10-4v4',
'performance-class': 'M13 3l-2 7h4l-6 11 2-8H7l4-10z',
'coaching': 'M12 3a4 4 0 110 8 4 4 0 010-8zm-7 18a7 7 0 0114 0',
@@ -1,4 +1,4 @@
export interface Program {
export interface Service {
slug: string;
title: string;
subtitle: string;
@@ -9,7 +9,7 @@ export interface Program {
hasPricing: boolean;
}
export const programs: Program[] = [
export const services: Service[] = [
{
slug: "personal-training",
title: "Personal Training",
@@ -99,6 +99,6 @@ export const programs: Program[] = [
},
];
export function getProgram(slug: string): Program | undefined {
return programs.find((p) => p.slug === slug);
export function getService(slug: string): Service | undefined {
return services.find((p) => p.slug === slug);
}
+8 -8
View File
@@ -13,7 +13,7 @@ export const site = {
navigation: [
{ label: "Insanitylab", href: "/about" },
{ label: "Training", href: "/training" },
{ label: "Programmi", href: "/programs" },
{ label: "Servizi", href: "/services" },
{ label: "Blog & Edugo", href: "/blog" },
{ label: "Contatti", href: "/contact" },
],
@@ -23,12 +23,12 @@ export const site = {
{ label: "Performance class", href: "/training#performance-class" },
{ label: "Fit remote", href: "/training#fit-remote" },
],
footerPrograms: [
{ label: "Personal Training", href: "/programs/personal-training" },
{ label: "Performance Class", href: "/programs/performance-class" },
{ label: "Coaching", href: "/programs/coaching" },
{ label: "Rehab", href: "/programs/rehab" },
{ label: "Pilates Flow", href: "/programs/pilates-flow" },
{ label: "Nutrizione", href: "/programs/nutrition" },
footerServices: [
{ label: "Personal Training", href: "/services/personal-training" },
{ label: "Performance Class", href: "/services/performance-class" },
{ label: "Coaching", href: "/services/coaching" },
{ label: "Rehab", href: "/services/rehab" },
{ label: "Pilates Flow", href: "/services/pilates-flow" },
{ label: "Nutrizione", href: "/services/nutrition" },
],
};
+1 -1
View File
@@ -1,6 +1,6 @@
export const trainings = [
{ id: "one-to-one", title: "One to One", caption: "Allenamento con un trainer dedicato", image: "training-one-to-one", description: "Allenati con un trainer dedicato che costruisce un percorso su misura per i tuoi obiettivi. Ogni sessione one to one è pensata per migliorare performance, tecnica e risultati. Supporto costante, attenzione totale e allenamenti personalizzati per dare il massimo.", cta: { label: "Contattaci", href: "/contact" } },
{ id: "small-groups", title: "Small Groups", caption: "Mini gruppi (max 3 persone) con trainer dedicato", image: "training-small-groups", description: "Allenati in mini gruppi da massimo 3 persone con un trainer dedicato. Unesperienza small group che unisce attenzione personalizzata, motivazione e condivisione. Programmi mirati e supporto costante per raggiungere i tuoi obiettivi insieme.", cta: { label: "Contattaci", href: "/contact" } },
{ id: "performance-class", title: "Performance Class", caption: "Classi fino a 8 persone con trainer certificato", image: "training-performance-class", description: "Allenati in classi fino a 8 persone guidate da trainer certificati. Sessioni dinamiche ad alta intensità pensate per migliorare forza, resistenza e performance. Energia, motivazione e allenamenti strutturati per dare il massimo in ogni workout.", cta: { label: "Scopri", href: "/programs/performance-class" } },
{ id: "performance-class", title: "Performance Class", caption: "Classi fino a 8 persone con trainer certificato", image: "training-performance-class", description: "Allenati in classi fino a 8 persone guidate da trainer certificati. Sessioni dinamiche ad alta intensità pensate per migliorare forza, resistenza e performance. Energia, motivazione e allenamenti strutturati per dare il massimo in ogni workout.", cta: { label: "Scopri", href: "/services/performance-class" } },
{ id: "fit-remote", title: "Fit Remote", caption: "Piano di allenamento su misura, ovunque ti alleni.", image: "training-fit-remote", description: "Con Fit Remote hai un piano di allenamento su misura, ovunque ti alleni. Programmi personalizzati creati sui tuoi obiettivi, da seguire in palestra, a casa o in viaggio. Supporto costante e allenamenti pensati per adattarsi al tuo stile di vita.", cta: { label: "Contattaci", href: "/contact" } },
] as const;
+46
View File
@@ -50,6 +50,52 @@ export function createDb(path?: string): Database.Database {
if (!userCols.some((c) => c.name === 'role')) {
db.exec(`ALTER TABLE users ADD COLUMN role TEXT NOT NULL DEFAULT 'admin'`);
}
// Migrazione: la sezione "Programmi" è diventata "Servizi" → rinomina dei tag esistenti
// preservando le modifiche fatte dal pannello. Ordine: prima i prefissi più specifici.
// UPDATE OR IGNORE salta le righe in conflitto (tag nuovo già presente); la DELETE
// successiva ripulisce le righe vecchie rimaste. Idempotente: al secondo giro nessun match.
const TAG_RENAMES: [string, string][] = [
['home.programs.', 'home.services.'],
['footer.programs.', 'footer.services.'],
['programs.', 'services.'],
['program.', 'service.'],
];
const renameUpd = db.prepare('UPDATE OR IGNORE content_blocks SET tag = ? || substr(tag, ?) WHERE tag LIKE ?');
const renameDel = db.prepare('DELETE FROM content_blocks WHERE tag LIKE ?');
const renameTx = db.transaction(() => {
for (const [oldP, newP] of TAG_RENAMES) {
renameUpd.run(newP, oldP.length + 1, `${oldP}%`);
renameDel.run(`${oldP}%`);
}
// Tag singolo, non coperto dai prefissi: uguaglianza esatta.
db.prepare("UPDATE OR IGNORE content_blocks SET tag = 'footer.col.services-title' WHERE tag = 'footer.col.programs-title'").run();
db.prepare("DELETE FROM content_blocks WHERE tag = 'footer.col.programs-title'").run();
// I valori di default che nominavano la sezione cambiano con la rinomina. Il seed è
// INSERT OR IGNORE e non tocca le righe esistenti, quindi si aggiornano qui — ma solo
// le righe rimaste identiche al valore originale del seed (mai modificate dal pannello):
// il nuovo valore è quello del seed corrente per lo stesso tag.
const OLD_SECTION_DEFAULTS: [string, string][] = [
['global.nav.3.label', 'Programmi'],
['footer.col.services-title', 'Programmi'],
['home.services.title', 'Programmi'],
['home.services.body', 'Questi sono i programmi delle classi di Insanity Lab selezionati per voi. Abbiamo scelto i migliori percorsi per offrirvi unesperienza di allenamento completa e mirata.'],
['services.meta.title', 'Programmi'],
['services.meta.description', 'I programmi InsanityLab: Personal Training, Performance Class, Coaching, Pilates Flow, Rehab e Nutrizione.'],
['services.hero.title', 'Programmi'],
['services.intro.title', 'Programmi'],
["services.intro.body", "Questi sono i programmi delle classi di Insanity Lab selezionati per voi. Abbiamo scelto i migliori percorsi per offrirvi un'esperienza di allenamento completa e mirata."],
['service.side.others-label', 'Altri programmi'],
['service.pricing.empty-note', 'Contattaci per i dettagli di questo programma. <a href="/contact">Scrivici →</a>'],
];
const seedValue = new Map(contentSeed.map((e) => [e.tag, e.value]));
const updValue = db.prepare('UPDATE content_blocks SET value = ? WHERE tag = ? AND value = ?');
for (const [tag, oldValue] of OLD_SECTION_DEFAULTS) {
const next = seedValue.get(tag);
if (next !== undefined) updValue.run(next, tag, oldValue);
}
});
renameTx();
const ins = db.prepare('INSERT OR IGNORE INTO content_blocks (tag, type, value) VALUES (?, ?, ?)');
const syncTx = db.transaction(() => { for (const e of contentSeed) ins.run(e.tag, e.type, e.value); });
syncTx();
+1 -1
View File
@@ -37,7 +37,7 @@ export const prerender = false;
<div>
<T tag="about.direzione.title" as="h2" />
<T tag="about.direzione.body" as="p" />
<a class="btn" href="/programs">{t('about.direzione.cta')}</a>
<a class="btn" href="/services">{t('about.direzione.cta')}</a>
</div>
<div>
<T tag="about.approccio.title" as="h2" />
+2 -2
View File
@@ -9,13 +9,13 @@ export const prerender = false;
const GROUP_BY_PREFIX: Record<string, string> = {
home: 'Home',
about: 'About', team: 'About', method: 'About',
program: 'Programmi', programs: 'Programmi', pricing: 'Programmi',
service: 'Servizi', services: 'Servizi', pricing: 'Servizi',
training: 'Training',
contact: 'Contatti', form: 'Contatti',
global: 'Globali', header: 'Globali', footer: 'Globali',
agenda: 'Globali', partner: 'Globali', notfound: 'Globali',
};
const GROUP_ORDER = ['Home', 'About', 'Programmi', 'Training', 'Contatti', 'Globali', 'Altro'];
const GROUP_ORDER = ['Home', 'About', 'Servizi', 'Training', 'Contatti', 'Globali', 'Altro'];
interface Row { tag: string; type: string; value: string; styles: string; updated_at: string; updated_by: string | null }
const rows = getDb().prepare(
+2 -2
View File
@@ -5,7 +5,7 @@ import InfoColumns from '../components/home/InfoColumns.astro';
import FeatureBlocks from '../components/home/FeatureBlocks.astro';
import QuoteBanner from '../components/home/QuoteBanner.astro';
import TrainingCards from '../components/home/TrainingCards.astro';
import ProgramsGrid from '../components/home/ProgramsGrid.astro';
import ServicesGrid from '../components/home/ServicesGrid.astro';
import Agenda from '../components/Agenda.astro';
import TeamSection from '../components/home/TeamSection.astro';
import MethodSteps from '../components/home/MethodSteps.astro';
@@ -26,7 +26,7 @@ export const prerender = false;
</div>
</section>
<TrainingCards />
<ProgramsGrid />
<ServicesGrid />
<Agenda />
<TeamSection />
<MethodSteps />
@@ -2,66 +2,66 @@
import Base from '../../layouts/Base.astro';
import PageHero from '../../components/PageHero.astro';
import type { ImageMetadata } from 'astro';
import { programs, getProgram } from '../../data/programs';
import { services, getService } from '../../data/services';
import { performanceClassPricing } from '../../data/pricing';
import { programIcons } from '../../data/program-icons';
import { serviceIcons } from '../../data/service-icons';
import T from '../../components/content/T.astro';
import TImg from '../../components/content/TImg.astro';
import { t } from '../../lib/content';
export const prerender = false;
const program = getProgram(Astro.params.slug!);
if (!program) return new Response(null, { status: 404 });
const slug = program.slug;
const others = programs.filter((p) => p.slug !== program.slug);
const service = getService(Astro.params.slug!);
if (!service) return new Response(null, { status: 404 });
const slug = service.slug;
const others = services.filter((p) => p.slug !== service.slug);
const images = import.meta.glob<{ default: ImageMetadata }>('../../assets/img/program-*.jpg', { eager: true });
const imgOf = (slug: string) => Object.entries(images).find(([p]) => p.includes(`program-${slug}.`))?.[1]?.default;
const heroImg = imgOf(program.slug);
// Categorie come da Figma (Performance class.pdf), non quelle del blog → tag program.side.category.1-4
const heroImg = imgOf(service.slug);
// Categorie come da Figma (Performance class.pdf), non quelle del blog → tag service.side.category.1-4
const categoryIndexes = [1, 2, 3, 4];
// Ordine card pricing come da Figma: tan, taupe, dark
const tones = ['tan', 'taupe', 'dark'];
---
<Base title={t(`program.${slug}.title`)} description={t(`program.${slug}.excerpt`)}>
<PageHero title={t(`program.${slug}.title`).toUpperCase()} bgImage={heroImg} />
<Base title={t(`service.${slug}.title`)} description={t(`service.${slug}.excerpt`)}>
<PageHero title={t(`service.${slug}.title`).toUpperCase()} bgImage={heroImg} />
<section class="section">
<div class="container prog">
<div>
<h2>{t(`program.${slug}.title`).toUpperCase()}</h2>
<T tag={`program.${slug}.long-description`} as="p" />
<h2>{t(`service.${slug}.title`).toUpperCase()}</h2>
<T tag={`service.${slug}.long-description`} as="p" />
<ul class="prog__features">
{program.features.map((_, i) => <T tag={`program.${slug}.feature.${i + 1}`} as="li" />)}
{service.features.map((_, i) => <T tag={`service.${slug}.feature.${i + 1}`} as="li" />)}
</ul>
{heroImg && (
<TImg tag={`program.${slug}.image`} src={heroImg} alt={t(`program.${slug}.title`)} class="prog__img" widths={[600, 1000]} sizes="(max-width:991px) 100vw, 60vw" data-reveal="fade" />
<TImg tag={`service.${slug}.image`} src={heroImg} alt={t(`service.${slug}.title`)} class="prog__img" widths={[600, 1000]} sizes="(max-width:991px) 100vw, 60vw" data-reveal="fade" />
)}
<div class="prog__faq">
{program.faq.map((_, i) => (
{service.faq.map((_, i) => (
<details open={i === 0}>
<summary><T tag={`program.${slug}.faq.${i + 1}.q`} /></summary>
<T tag={`program.${slug}.faq.${i + 1}.a`} as="p" />
<summary><T tag={`service.${slug}.faq.${i + 1}.q`} /></summary>
<T tag={`service.${slug}.faq.${i + 1}.a`} as="p" />
</details>
))}
</div>
</div>
<aside class="prog__side">
<T tag="program.side.categories-label" as="h3" />
{categoryIndexes.map((n) => <T tag={`program.side.category.${n}`} as="p" class="prog__cat" />)}
<T tag="program.side.others-label" as="h3" />
<T tag="service.side.categories-label" as="h3" />
{categoryIndexes.map((n) => <T tag={`service.side.category.${n}`} as="p" class="prog__cat" />)}
<T tag="service.side.others-label" as="h3" />
{others.map((o) => (
<a class="prog__other" href={`/programs/${o.slug}`}>
<a class="prog__other" href={`/services/${o.slug}`}>
<span class="prog__icon">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.4" width="26" height="26" aria-hidden="true"><path d={programIcons[o.slug]} /></svg>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.4" width="26" height="26" aria-hidden="true"><path d={serviceIcons[o.slug]} /></svg>
</span>
<T tag={`program.${o.slug}.title`} />
<T tag={`service.${o.slug}.title`} />
</a>
))}
</aside>
</div>
</section>
{program.hasPricing && (
{service.hasPricing && (
<section class="section section--alt">
<div class="container">
<div class="section-heading">
@@ -72,18 +72,18 @@ const tones = ['tan', 'taupe', 'dark'];
<details class="price__group" open={g.open}>
<summary>{g.open ? '+' : ''} <T tag={`pricing.group.${gi + 1}.label`} /></summary>
{g.plans.length === 0 ? (
<T tag="program.pricing.empty-note" as="p" class="price__empty" />
<T tag="service.pricing.empty-note" as="p" class="price__empty" />
) : (
<div class="price__row">
{g.plans.map((p, i) => (
<div class:list={['price__card', `price__card--${tones[i % 3]}`]}>
<p class="price__amount">{p.monthly}€</p>
<T tag="program.pricing.per" as="p" class="price__per" />
<p class="price__entries">{p.entries} {p.entries === 1 ? t('program.pricing.entries-singular') : t('program.pricing.entries-plural')}</p>
<T tag="service.pricing.per" as="p" class="price__per" />
<p class="price__entries">{p.entries} {p.entries === 1 ? t('service.pricing.entries-singular') : t('service.pricing.entries-plural')}</p>
<T tag={`pricing.group.${gi + 1}.plan.${i + 1}.note`} as="p" />
<T tag={`pricing.group.${gi + 1}.plan.${i + 1}.installments`} as="p" />
<T tag={`pricing.group.${gi + 1}.plan.${i + 1}.best`} as="p" class="price__best" />
<a class="btn btn--light" href="/contact">{t('program.pricing.buy-cta')}</a>
<a class="btn btn--light" href="/contact">{t('service.pricing.buy-cta')}</a>
</div>
))}
</div>
@@ -2,7 +2,7 @@
import Base from '../../layouts/Base.astro';
import PageHero from '../../components/PageHero.astro';
import type { ImageMetadata } from 'astro';
import { programs } from '../../data/programs';
import { services } from '../../data/services';
import T from '../../components/content/T.astro';
import TImg from '../../components/content/TImg.astro';
import { t } from '../../lib/content';
@@ -10,27 +10,27 @@ const images = import.meta.glob<{ default: ImageMetadata }>('../../assets/img/pr
const imgOf = (slug: string) => Object.entries(images).find(([p]) => p.includes(`program-${slug}.`))?.[1]?.default;
export const prerender = false;
---
<Base title={t('programs.meta.title')} description={t('programs.meta.description')}>
<PageHero title={t('programs.hero.title')} />
<Base title={t('services.meta.title')} description={t('services.meta.description')}>
<PageHero title={t('services.hero.title')} />
<section class="section">
<div class="container">
<div class="section-heading">
<T tag="programs.intro.title" as="h2" />
<T tag="programs.intro.body" as="p" />
<T tag="services.intro.title" as="h2" />
<T tag="services.intro.body" as="p" />
</div>
<div class="plist">
{programs.map((p, i) => (
{services.map((p, i) => (
<article class="plist__card" data-reveal style={`--reveal-delay:${((i % 3) * 0.15).toFixed(2)}s`}>
{imgOf(p.slug) && (
<a class="plist__photo" href={`/programs/${p.slug}`}>
<TImg tag={`program.${p.slug}.image`} src={imgOf(p.slug)!} alt={t(`program.${p.slug}.title`)} widths={[400, 700]} sizes="(max-width:767px) 100vw, 33vw" />
<a class="plist__photo" href={`/services/${p.slug}`}>
<TImg tag={`service.${p.slug}.image`} src={imgOf(p.slug)!} alt={t(`service.${p.slug}.title`)} widths={[400, 700]} sizes="(max-width:767px) 100vw, 33vw" />
</a>
)}
<div class="plist__body">
<T tag={`program.${p.slug}.title`} as="h3" />
<T tag={`program.${p.slug}.subtitle`} as="p" class="plist__sub" />
<T tag={`program.${p.slug}.excerpt`} as="p" />
<a class="plist__more" href={`/programs/${p.slug}`}>{t('programs.card.more-label')}</a>
<T tag={`service.${p.slug}.title`} as="h3" />
<T tag={`service.${p.slug}.subtitle`} as="p" class="plist__sub" />
<T tag={`service.${p.slug}.excerpt`} as="p" />
<a class="plist__more" href={`/services/${p.slug}`}>{t('services.card.more-label')}</a>
</div>
</article>
))}
+3 -3
View File
@@ -1,16 +1,16 @@
import type { APIRoute } from 'astro';
import { getDb } from '../lib/db';
import { programs } from '../data/programs';
import { services } from '../data/services';
export const prerender = false;
const STATIC = ['/', '/about', '/contact', '/training', '/programs', '/blog'];
const STATIC = ['/', '/about', '/contact', '/training', '/services', '/blog'];
export const GET: APIRoute = ({ site }) => {
const base = (site ?? new URL('https://insanitylab.tielogic.xyz')).toString().replace(/\/$/, '');
const posts = getDb().prepare('SELECT slug FROM posts WHERE draft = 0').all() as { slug: string }[];
const urls = [
...STATIC,
...programs.map((p) => `/programs/${p.slug}`),
...services.map((p) => `/services/${p.slug}`),
...posts.map((p) => `/blog/${p.slug}`),
];
const xml = `<?xml version="1.0" encoding="UTF-8"?>\n<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n${urls.map((u) => ` <url><loc>${base}${u}</loc></url>`).join('\n')}\n</urlset>`;