Configuration
TVfolio keeps configuration in src/config/, typed against types/configDataTypes.ts, and the house rule is stated in the section contract: a component reads typed config or receives props, never both for the same data, and never a literal. A string that appears on two pages is a config value; a sentence that appears on one page stays in that page.
That distinction is the thing to hold on to while you work through this chapter. Replacing the demo persona is not one edit — it is a config edit that re-labels the set, plus a prose pass over the handful of files that hold the persona’s writing.
siteData.json.ts
The identity file. Five fields re-label the entire television:
const siteData: SiteDataProps = {
name: "Alex Mercer",
title: "Alex Mercer — Frontend Developer, Bordeaux",
description: "Frontend developer specializing in digital interfaces …",
author: {
name: "Alex Mercer",
email: "[email protected]",
twitter: "alexmercer",
role: "Frontend Developer",
location: "Bordeaux, FR",
bio: "…",
},
// …
};
name is the name in lights — the tube’s <h1> on the home page renders it directly. author.email is the contact form’s default delivery address. author.twitter is the only reason a twitter:creator tag is emitted, and omitting it drops the tag rather than emitting an empty one.
The availability block, and why it has a second half
availability: {
from: "March 2026",
discipline: "Frontend · Realtime UI",
timezone: "UTC+1",
timezoneName: "CET",
responseHours: 48,
},
Those are the facts. Below them, the file exports the drawn forms:
export const availabilityLabels = {
availableFrom: `From ${siteData.availability.from}`,
timezone: `${siteData.availability.timezone} · ${siteData.availability.timezoneName}`,
responseLong: `${siteData.availability.responseHours} hours`,
responseShort: `${siteData.availability.responseHours}h`,
} as const;
This exists because deriving them at the call site was a real bug. /about/ and /cv/ each wrote their own `From ${from}`; /about/ and /contact/ each joined the timezone; and ContactForm re-derived "48 hours" independently of the spec strip that promised it — so a site moving to “2 business days” would have kept a form still saying hours. Note the boundary the file draws: a sentence that embeds a fact (“Booking from March 2026…”) still reads the raw value, and only the repeated standalone forms are derived. That is the difference between a shared fact and a shared sentence.
socials and sameAs — two lists that look the same and are not
socials holds the targets behind the four push buttons on the cabinet. A platform listed here renders a live <a>; one left out stays an inert moulding rather than a dead link. Out of the box it holds four platform homepages as stand-ins.
sameAs feeds the JSON-LD Organization node, and it ships deliberately empty rather than derived from socials. The reasoning is worth understanding because it is easy to get backwards: sameAs asserts “these URLs are the same entity as this site”. Emitting https://x.com would tell crawlers that this site is X. It is a disambiguation claim, not a link list. Once socials holds real profile URLs, sameAs becomes Object.values(socials) — plus any profile that has no cap on the cabinet.
The socials gate
The file throws at build time if a production deploy still carries platform homepages:
if (isProductionDeploy()) {
const placeholders = Object.entries(socials)
.filter(([, url]) => new URL(url).pathname === "/")
.map(([platform]) => platform);
if (placeholders.length > 0) throw new Error(/* … */);
}
The guard sits beside the value it guards, so it cannot be missed by someone editing that value. Local builds and deploy previews are unaffected — isProductionDeploy() reads Netlify’s CONTEXT, Vercel’s VERCEL_ENV and a generic DEPLOY_ENV, none of which a local pnpm build sets.
siteSettings.json.ts
Three values and two switches:
export const siteLang = "en" as const;
export const siteLocale = "en-US" as const;
export const siteSettings = {
useViewTransitions: true,
useAnimations: true,
} satisfies SiteSettingsProps;
siteLang is the <html lang> attribute. siteLocale is the BCP-47 tag driving Intl date formatting, og:locale (converted to the underscore form for OG) and JSON-LD inLanguage.
useViewTransitions mounts Astro’s <ClientRouter />. useAnimations turns the decorative motion layer on and off site-wide. Note the satisfies rather than a type annotation — it checks the shape while preserving the literal true, so consuming code sees a literal rather than a widened boolean.
The site is single-language. The i18n layer was removed, and these two locale values are all that remains of it; the shape to restore is recorded in the theme’s own wiki/subsystems/i18n.md.
navData.json.ts
The navigation file, covered in full in Navigation & Fastext. In summary it holds the six coloured fastext keys, the eleven channels the PAGES drop-up lists, and three helper functions — channelFor, fkeyFor and fkeyLabelFor — that join the two lists rather than letting any page restate the join.
The one rule to remember while editing it: a key or channel with no href renders inert, and only routes that actually exist in src/pages/ may carry one. With trailingSlash: "always", a missing route is a 404 rather than a soft landing, so a key gets its href in the same commit that adds its page.
cvData.json.ts
The career record — revised, a standfirst, and three arrays: careers, education and toolkit. It is the single source for four surfaces at once: the /cv/ page, /cv.txt, /cv.pdf and the career list on /about/. CV & Downloads covers how the three renderings stay in step.
legalData.json.ts
Terms and privacy, each with a title, description, an intro and an array of { heading, body[] } sections. Both render through the same Sections/Legal/LegalArticle.astro, so the two pages cannot drift in layout.
The shipped copy is placeholder text. Have it reviewed before you launch — it is not legal advice, and the theme says so.
SITE_URL and the site gate
site is not in src/config/ at all; it is read from the environment in astro.config.mjs:
const site = process.env.SITE_URL ?? "https://example.com";
if (isProductionDeploy() && site.includes("example.com")) {
throw new Error("SITE_URL is unset or still the placeholder. …");
}
One value feeds six things — canonical URLs, Open Graph, JSON-LD, the sitemap, robots.txt and llms.txt — and none of them looks visibly broken in review when it is wrong. That is why it is gated rather than merely documented. A fresh clone builds on the placeholder so a buyer can see the site before owning a domain; a production deploy refuses to.
SITE_URL is a build-time value, baked into the HTML. Secrets are the opposite — read at request time, set once per host. Mixing the two up is the most common deployment mistake with this theme; Deployment covers both sides.
What config cannot reach
Editing siteData re-labels the set. It does not touch prose, and the demo persona’s prose lives in these files:
| File | What it holds |
|---|---|
src/config/cvData.json.ts |
the whole career record |
src/data/blog/, work/, authors/ |
8 bulletins, 6 transmissions, 1 byline |
src/assets/images/portrait.webp |
the persona’s face, drawn on / and /about/ |
src/pages/about.astro |
the standfirst, three briefing paragraphs (they name a city), the quote |
src/pages/contact.astro |
the standfirst and the four DIRECT CHANNELS notes |
src/pages/index.astro |
the three audio-transmission rows and the test-card caption |
src/pages/cv.astro |
the EXPERIENCE spec |
Sections/Global/StatsStrip.astro |
the three figures — drawn on /, /about/ and /cv/ |
Sections/Contact/ContactForm.astro |
the two field placeholders |
That last row is the one people miss: StatsStrip is a section, not config, and it is drawn on three pages. If your numbers are not the demo’s numbers, that file needs an edit.