-
-
+
+
diff --git a/src/data/content-seed.ts b/src/data/content-seed.ts
index a7786d0..d2cdb1a 100644
--- a/src/data/content-seed.ts
+++ b/src/data/content-seed.ts
@@ -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. BALANCE. 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', '
Insanitylab, 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 un’esperienza 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 un’esperienza di allenamento completa e mirata.'),
// --- Home: team (data-driven da team.ts) ---
H('home.team.title', 'Team
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.
Scrivici →'),
- 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.
Scrivici →'),
+ 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),
diff --git a/src/data/program-icons.ts b/src/data/service-icons.ts
similarity index 72%
rename from src/data/program-icons.ts
rename to src/data/service-icons.ts
index 8447cfc..cea44d8 100644
--- a/src/data/program-icons.ts
+++ b/src/data/service-icons.ts
@@ -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
= {
+// Path SVG (stroke, viewBox 0 0 24 24) delle icone servizi, condivisi tra griglia home e sidebar
+export const serviceIcons: Record = {
'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',
diff --git a/src/data/programs.ts b/src/data/services.ts
similarity index 97%
rename from src/data/programs.ts
rename to src/data/services.ts
index 089eec9..1c8bf19 100644
--- a/src/data/programs.ts
+++ b/src/data/services.ts
@@ -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);
}
diff --git a/src/data/site.ts b/src/data/site.ts
index 5b1c6ee..132e491 100644
--- a/src/data/site.ts
+++ b/src/data/site.ts
@@ -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" },
],
};
diff --git a/src/data/trainings.ts b/src/data/trainings.ts
index f87e364..7d9decb 100644
--- a/src/data/trainings.ts
+++ b/src/data/trainings.ts
@@ -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. Un’esperienza 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;
diff --git a/src/lib/db.ts b/src/lib/db.ts
index 2821981..cf8c82c 100644
--- a/src/lib/db.ts
+++ b/src/lib/db.ts
@@ -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 un’esperienza 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. Scrivici →'],
+ ];
+ 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();
diff --git a/src/pages/about.astro b/src/pages/about.astro
index 7a6af3b..54e9146 100644
--- a/src/pages/about.astro
+++ b/src/pages/about.astro
@@ -37,7 +37,7 @@ export const prerender = false;
diff --git a/src/pages/admin/content.astro b/src/pages/admin/content.astro
index bafa84e..41ded06 100644
--- a/src/pages/admin/content.astro
+++ b/src/pages/admin/content.astro
@@ -9,13 +9,13 @@ export const prerender = false;
const GROUP_BY_PREFIX: Record = {
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(
diff --git a/src/pages/index.astro b/src/pages/index.astro
index a6b0c75..196c6fa 100644
--- a/src/pages/index.astro
+++ b/src/pages/index.astro
@@ -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;
-
+
diff --git a/src/pages/programs/[slug].astro b/src/pages/services/[slug].astro
similarity index 78%
rename from src/pages/programs/[slug].astro
rename to src/pages/services/[slug].astro
index 3b11437..ee3b1d9 100644
--- a/src/pages/programs/[slug].astro
+++ b/src/pages/services/[slug].astro
@@ -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'];
---
-
-
+
+
-
{t(`program.${slug}.title`).toUpperCase()}
-
+
{t(`service.${slug}.title`).toUpperCase()}
+
- {program.features.map((_, i) => )}
+ {service.features.map((_, i) => )}
{heroImg && (
-
+
)}
- {program.faq.map((_, i) => (
+ {service.faq.map((_, i) => (
-
-
+
+
))}
- {program.hasPricing && (
+ {service.hasPricing && (
@@ -72,18 +72,18 @@ const tones = ['tan', 'taupe', 'dark'];
{g.open ? '+' : '–'}
{g.plans.length === 0 ? (
-
+
) : (
{g.plans.map((p, i) => (
{p.monthly}€
-
-
{p.entries} {p.entries === 1 ? t('program.pricing.entries-singular') : t('program.pricing.entries-plural')}
+
+
{p.entries} {p.entries === 1 ? t('service.pricing.entries-singular') : t('service.pricing.entries-plural')}
-
{t('program.pricing.buy-cta')}
+
{t('service.pricing.buy-cta')}
))}
diff --git a/src/pages/programs/index.astro b/src/pages/services/index.astro
similarity index 75%
rename from src/pages/programs/index.astro
rename to src/pages/services/index.astro
index 11c4b83..80a4bb0 100644
--- a/src/pages/programs/index.astro
+++ b/src/pages/services/index.astro
@@ -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;
---
-
-
+
+
-
-
+
+
- {programs.map((p, i) => (
+ {services.map((p, i) => (
{imgOf(p.slug) && (
-
-
+
+
)}
))}
diff --git a/src/pages/sitemap.xml.ts b/src/pages/sitemap.xml.ts
index 1f7235d..933578c 100644
--- a/src/pages/sitemap.xml.ts
+++ b/src/pages/sitemap.xml.ts
@@ -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 = `\n
\n${urls.map((u) => ` ${base}${u}`).join('\n')}\n`;
diff --git a/tests/content-db.test.ts b/tests/content-db.test.ts
index de6ec64..facfe9a 100644
--- a/tests/content-db.test.ts
+++ b/tests/content-db.test.ts
@@ -1,4 +1,7 @@
import { describe, it, expect } from 'vitest';
+import { mkdtempSync, rmSync } from 'node:fs';
+import { tmpdir } from 'node:os';
+import { join } from 'node:path';
import { createDb } from '../src/lib/db';
describe('schema content_blocks e ruoli', () => {
@@ -24,6 +27,60 @@ describe('schema content_blocks e ruoli', () => {
expect(row.role).toBe('admin');
});
+ it('migra i tag programmi → servizi preservando i valori modificati dal pannello', () => {
+ const dir = mkdtempSync(join(tmpdir(), 'il-db-test-'));
+ const path = join(dir, 'test.db');
+ try {
+ // Simula un DB di produzione pre-rinomina: tag vecchi con valori modificati dal pannello.
+ const db1 = createDb(path);
+ const old = db1.transaction(() => {
+ db1.prepare("DELETE FROM content_blocks WHERE tag IN ('home.services.title', 'service.rehab.title', 'services.hero.title', 'services.meta.title', 'footer.services.1.label', 'footer.col.services-title')").run();
+ const ins = db1.prepare("INSERT INTO content_blocks (tag, type, value, updated_by) VALUES (?, 'text', ?, 'editor')");
+ ins.run('home.programs.title', 'Titolo modificato');
+ ins.run('program.rehab.title', 'Rehab modificato');
+ ins.run('programs.hero.title', 'Hero modificato');
+ ins.run('footer.programs.1.label', 'Label modificata');
+ ins.run('footer.col.programs-title', 'Colonna modificata');
+ // Riga mai toccata dal pannello: valore identico al vecchio seed → deve prendere il nuovo default.
+ db1.prepare("INSERT INTO content_blocks (tag, type, value) VALUES ('programs.meta.title', 'text', 'Programmi')").run();
+ db1.prepare("UPDATE content_blocks SET value = 'Programmi' WHERE tag = 'global.nav.3.label'").run();
+ });
+ old();
+ db1.close();
+
+ // Riapertura: la migrazione rinomina i tag preservando valore e updated_by.
+ const db2 = createDb(path);
+ const val = (tag: string) =>
+ db2.prepare('SELECT value, updated_by FROM content_blocks WHERE tag = ?').get(tag) as { value: string; updated_by: string | null } | undefined;
+ expect(val('home.services.title')).toMatchObject({ value: 'Titolo modificato', updated_by: 'editor' });
+ expect(val('service.rehab.title')).toMatchObject({ value: 'Rehab modificato', updated_by: 'editor' });
+ expect(val('services.hero.title')).toMatchObject({ value: 'Hero modificato', updated_by: 'editor' });
+ expect(val('footer.services.1.label')).toMatchObject({ value: 'Label modificata', updated_by: 'editor' });
+ expect(val('footer.col.services-title')).toMatchObject({ value: 'Colonna modificata', updated_by: 'editor' });
+
+ // Le righe rimaste al vecchio valore di default prendono il nuovo ("Servizi").
+ expect(val('services.meta.title')).toMatchObject({ value: 'Servizi' });
+ expect(val('global.nav.3.label')).toMatchObject({ value: 'Servizi' });
+ // La riga rinominata ma modificata dal pannello resta invariata (già verificata sopra):
+ // home.services.title = 'Titolo modificato'.
+
+ // Nessun tag vecchio residuo.
+ const residui = db2.prepare(
+ "SELECT count(*) AS c FROM content_blocks WHERE tag LIKE 'program%' OR tag LIKE 'home.programs%' OR tag LIKE 'footer.programs%' OR tag = 'footer.col.programs-title'"
+ ).get() as { c: number };
+ expect(residui.c).toBe(0);
+
+ // Idempotente: una terza apertura non altera i valori.
+ db2.close();
+ const db3 = createDb(path);
+ const again = db3.prepare('SELECT value FROM content_blocks WHERE tag = ?').get('home.services.title') as { value: string };
+ expect(again.value).toBe('Titolo modificato');
+ db3.close();
+ } finally {
+ rmSync(dir, { recursive: true, force: true });
+ }
+ });
+
it('migra un DB esistente senza colonna role', () => {
// simula DB vecchio: createDb due volte sullo stesso path in-memory non è possibile,
// quindi si verifica che la migrazione sia idempotente (doppia esecuzione non lancia)