A page-by-page, key-by-key map of every string the public studio.chat site
renders, in render order, so a copywriter (or copywriting agent) can add new
copy variants with confidence. For each variant-enabled section you get the
exact namespace, base key, the existing _vN sibling keys, the current EN/ES
text, and the human-readable variant label registered in the office.
Read docs/website/copy-guide.md first for voice. Short version: lead with podcasts; sharp,
dry, craft-led, allergic to agency fluff; rentals are relationship-gated;
Spanish is a parallel rewrite, never a literal translation. Brand is always
studio.chat (lowercase, with the dot).
Where copy lives (four sources)
locales/en.json/locales/es.json— the bulk of UI/marketing copy, grouped by namespace (home,services,podcasts, …). Pages read it viauseTranslations("ns")(client) orgetTranslations("ns")/getTranslations({ locale, namespace })(server). This is where you add variant keys.src/lib/copy/variants.ts(COPY_PAGES) — the registry of which page/section/fields support variants, plus the human label per variant ("v2 · sharpened generalist"). A_vNkey in the locale JSON is invisible to the office until you also register it here. See the final section.content/*/{en,es}.jsonandcontent/projects/*.mdx— content-file copy, loaded bysrc/lib/content.ts. Covers services list, FAQs, privacy, team, stills captions, and project detail bodies. No variant system — edit the file directly. Listed per page below.- Supabase
itemstable (viasrc/lib/rentals.ts) — rental catalog item names, summaries, manufacturer, MPN, included accessories, category. Not editable from locale JSON or content files; managed in /office inventory. The catalog/item page chrome (labels, pagination, category display names) does live in locale JSON (rentalsCatalog,rentalsItem) and is covered below.
The eyebrow convention (toEyebrow)
Small mono labels above headings render with toEyebrow() (src/lib/eyebrow.ts):
spaces become _ and a trailing _ is appended. The JSON value is natural
text — write "say hello", the page renders say_hello_. Never bake
underscores into a translation value. Example: footer.sayHello = "say hello"
→ renders say_hello_. This applies to every <p className="label"> / eyebrow
slot noted below.
Page title template
The locale layout (src/app/[locale]/layout.tsx) wraps each page's metadata
title as "<title> — studio.chat". Most pages set title to their namespace's
title key (e.g. services.title = "services"). The home page overrides with
site.metaTitle. The launch page sets an absolute title (no template).
home — /
src/app/[locale]/page.tsx. Namespace: home (plus site, projects).
Purpose: the front door. Bold, sparse, visual; a 4-line hero claim, one
featured project, the five-service grid, and a single closing CTA. Reads
variants at request time (dynamic = "force-dynamic").
1. Hero (VARIANT — home.hero, ns home, fields heroLine1..heroLine4)
A site.tagline eyebrow over a 4-line H1; line 4 is dimmed (text-white/70).
The hero is built from four separate keys joined by <br/>, so a variant must
supply all four lines as a set. Background image is hardcoded (not copy).
| element | key | EN | ES |
|---|---|---|---|
| eyebrow | site.tagline | audiovisual | audiovisual |
| line 1 | home.heroLine1 | We craft experiences | Creamos experiencias |
| line 2 | home.heroLine2 | in the form of social content, | en forma de contenido social, |
| line 3 | home.heroLine3 | short films, and live podcasts — | cortometrajes y podcasts en vivo — |
| line 4 (dimmed) | home.heroLine4 | from concept to color and final mix. | del concepto al color y mezcla final. |
Variants (registered labels):
default= "default" → the four base keys above.v1= "v1 · podcasts-led" →heroLine1_v1…heroLine4_v1- EN:
Record the show./Leave with the clips./Live podcast production in Medellín —/multi-cam, clean audio, same-day cuts. - ES:
Graba el programa./Sal con los clips./Producción de podcasts en vivo en Medellín —/multicámara, audio limpio, cortes el mismo día.
- EN:
v2= "v2 · sharpened generalist" →heroLine1_v2…heroLine4_v2- EN:
Make it look as good as it sounded./Social content, short films, and live podcasts,/shot in Medellín —/concept to color, finished in-house. - ES:
Que se vea tan bien como sonó./Contenido social, cortometrajes y podcasts en vivo,/grabados en Medellín —/del concepto al color, terminado en casa.
- EN:
To add v3: add heroLine1_v3…heroLine4_v3 to both locale files, then add
{ id: "v3", label: "v3 · …" } to the home.hero section's variants array.
2. Featured projects (plain)
| element | key / source | EN | ES |
|---|---|---|---|
| eyebrow | home.featured | featured | destacados |
link (top-right → /projects) | home.viewAll | view all projects | ver todos los proyectos |
| card | content file — featured project (see projects section) | — | — |
| fallback if no featured | projects.empty | More work soon. | Pronto más trabajo. |
The featured card itself is rendered by ProjectCard from the content/projects/*.mdx
frontmatter (title / type / deliverables). See Project shared content below.
3. Services (VARIANT — home.services, ns home, field servicesIntro)
A home.servicesTitle eyebrow over a large display servicesIntro line, then a
5-up grid built from content/services/{en,es}.json (NOT locale JSON).
| element | key / source | EN | ES |
|---|---|---|---|
| eyebrow | home.servicesTitle | services | servicios |
| display line | home.servicesIntro (variant base) | End-to-end production for brands, artists, and platforms. | Producción integral para marcas, artistas y plataformas. |
| service items (×5) | content/services/{locale}.json | title + body each | title + body each |
catalog link (on items with href) | home.viewCatalog | view catalog | ver catálogo |
servicesIntro variants:
default= "default" → base above.v1= "v1 · studio day" →home.servicesIntro_v1- EN:
The studio day that ends with a finished episode and a week of clips. - ES:
El día de estudio que termina con un episodio listo y una semana de clips.
- EN:
v2= "v2 · record once" →home.servicesIntro_v2- EN:
Record once. Feed the internet for a week. - ES:
Graba una vez. Alimenta el internet por una semana.
- EN:
Service grid items (content file, edit directly — no variants). The 3rd and 5th
items carry an href so they become links and render the viewCatalog label:
| # | title (EN / ES) | body (EN) | href |
|---|---|---|---|
| 01 | Social content / Contenido social | Vertical and horizontal shorts for brands, artists, and platforms. Brief to delivery in weeks, not months. | — |
| 02 | Short films / Cortometrajes | Narrative and documentary work, from treatment to final deliverable. Crews scaled to scope. | — |
| 03 | Live podcasts / Podcasts en vivo | End-to-end live capture: set design, multi-cam, live audio, same-day social cuts. | /services/podcasts |
| 04 | Post-production / Postproducción | Editorial, color, sound design, and final mix. Standalone or part of a full production. | — |
| 05 | Rentals / Alquiler | A small, curated shelf of our cinema gear … rented to producers we trust between our own shoots. | /services/rentals |
4. CTA (VARIANT — home.cta, ns home, fields ctaTitle, ctaBody, ctaButton)
ctaTitle renders as the eyebrow; ctaBody as the big display line; ctaButton
is the pill button (→ /contact).
| element | key (variant base) | EN | ES |
|---|---|---|---|
| eyebrow | home.ctaTitle | start a project | comencemos |
| display line | home.ctaBody | Tell us what you're making. We'll come back with a plan within two business days. | Cuéntanos qué estás creando. Te respondemos con un plan en dos días hábiles. |
button (→ /contact) | home.ctaButton | Get in touch | Escríbenos |
Variants:
default= "default".v1= "v1 · book a session" →ctaTitle_v1/ctaBody_v1/ctaButton_v1- EN:
book a session/Tell us about the show — format, hosts, episode count. We reply with a plan and a quote within two business days./Book a session - ES:
reserva una sesión/Cuéntanos del programa — formato, presentadores, número de episodios. Te respondemos con un plan y una cotización en dos días hábiles./Reserva una sesión
- EN:
v2= "v2 · start with a pilot" →ctaTitle_v2/ctaBody_v2/ctaButton_v2- EN:
start with a pilot/New show? Book a single pilot session and see the workflow before committing to a season./Start with a pilot - ES:
empieza con un piloto/¿Programa nuevo? Reserva una sesión piloto y conoce el flujo antes de comprometer una temporada./Empieza con un piloto
- EN:
services hub — /services
src/app/[locale]/services/page.tsx. Namespace: services (also reads
cinematography, podcasts, rentals, stills). Purpose: the bookable-
offering index. One intro, then four offering cards that teaser each service.
Dynamic — also resolves the podcasts/rentals hero variants so the teaser taglines
stay in sync.
1. Header (VARIANT — services.intro, ns services, field intro)
SectionHeader with services.title as the big H1 and the variant intro below.
| element | key | EN | ES |
|---|---|---|---|
| H1 / title | services.title | services | servicios |
| intro | services.intro (variant base) | What we offer end-to-end. Bookable as a single show, a season, or a kit-only rental. | Lo que ofrecemos de punta a punta. Reservable como show único, temporada completa o alquiler solo del equipo. |
intro variants:
default= "default".v1= "v1 · kit ladder" →services.intro_v1- EN:
Everything we run, end-to-end. Book a single show, a full season, or just the gear. - ES:
Todo lo que hacemos, de punta a punta. Reserva un show único, una temporada completa o solo el equipo.
- EN:
v2= "v2 · one room" →services.intro_v2- EN:
Crew, studio, and kit under one roof. Take it as a single show, a season, or a rental. - ES:
Gente, estudio y equipo bajo un mismo techo. Tómalo como un show único, una temporada o un alquiler.
- EN:
2. Offering cards (×4) (taglines pull from other namespaces — see notes)
Each card: a label eyebrow, a big tagline headline, and a cta framed link.
The whole card links to href.
| # | href | label key | tagline key | cta key |
|---|---|---|---|---|
| 1 | /contact | cinematography.title (cinematography/cinematografía) | cinematography.tagline | cinematography.cta (request a quote / solicitar cotización) |
| 2 | /services/podcasts | podcasts.title (podcasts) | podcasts.tagline (variant — see podcasts.hero) | podcasts.viewAll (see the offering / ver la oferta) |
| 3 | /services/rentals | rentals.title (rentals/alquiler) | rentals.tagline (variant — see rentals.hero) | rentals.viewAll (see the kit / ver el equipo) |
| 4 | /services/stills | stills.title (stills/fotografía) | stills.tagline | stills.viewAll (view the gallery / ver la galería) |
Notes: Cinematography has no dedicated page by design (held back until staffing
catches up) — its card points at /contact with a "request a quote" CTA.
cinematography.tagline EN = Cinematic capture — DP, lighting, color, final grade.
/ ES = Captura cinematográfica — DP, iluminación, color, gradación final. Cards
2 and 3 reuse the podcasts/rentals hero tagline variants (next two sections),
so editing those there changes this hub too.
podcasts — /services/podcasts
src/app/[locale]/services/podcasts/page.tsx. Namespace: podcasts.
Purpose: the priority offer. Sells the cinematic-lounge live-podcast format,
the inclusions, the kit, season packages, and a closing CTA. Dynamic.
1. Hero (VARIANT — podcasts.hero, ns podcasts, fields tagline, intro)
PageHero: podcasts.title eyebrow, tagline headline, intro paragraph.
| element | key (variant base) | EN | ES |
|---|---|---|---|
| eyebrow | podcasts.title | podcasts | podcasts |
| headline | podcasts.tagline | Conversational podcasts, shot like cinema. | Podcasts conversacionales, con look de cine. |
| intro | podcasts.intro | (long; see locale JSON line 202) | (long; see locale JSON line 202) |
podcasts.hero variants (this set also drives the /services hub card #2 tagline):
default= "default".v1= "v1 · final_final_v3" →tagline_v1/intro_v1- EN tagline:
Your show, not a folder of final_final_v3.mp4. - ES tagline:
Tu show, no una carpeta de final_final_v3.mp4. - intro: long paragraph, EN/ES at locale JSON line 203.
- EN tagline:
v2= "v2 · long conversations" →tagline_v2/intro_v2- EN tagline:
Long conversations, shot the way film is. - ES tagline:
Conversaciones largas, rodadas como se rueda el cine. - intro: long paragraph, EN/ES at locale JSON line 204.
- EN tagline:
2. What you get (plain — array)
| element | key | EN | ES |
|---|---|---|---|
| eyebrow | podcasts.offerTitle | what you get | qué recibes |
| list (×7) | podcasts.offerLines (JSON array) | 7 bullet strings | 7 bullet strings |
offerLines is a 7-item array read via dynamic import (t.raw-style). To reword
inclusions, edit the array in both locale files (same length). Current EN items:
lounge layout; invisible audio capture; up to 6-cam Sony Cinema Line + 3 ProRes
RAW; timecode-locked picture/sound; continuous LED lighting; live program output;
same-day vertical/square social cut. (Full text at locale JSON lines 206–214.)
Not registered for the variant system — no _vN slot.
3. The kit (plain)
| element | key | EN | ES |
|---|---|---|---|
| eyebrow | podcasts.kitTitle | the kit | el equipo |
| body | podcasts.kitBody | Studio-owned, regularly maintained, shot with on our own productions. … | Equipo propio del estudio, mantenido regularmente, … |
framed link (→ /services/rentals) | podcasts.viewCatalog | view rental catalog | ver catálogo de alquiler |
4. Season packages (plain)
| element | key | EN | ES |
|---|---|---|---|
| eyebrow | podcasts.seasonTitle | season packages | paquetes de temporada |
| body | podcasts.seasonBody | Multi-episode bookings get crew familiarity, lighting and audio presets … | Reservas de varios episodios reciben familiaridad de crew, … |
5. CTA (VARIANT — podcasts.cta, ns podcasts, fields ctaTitle, ctaBody, ctaButton)
CtaSection: ctaTitle eyebrow, ctaBody heading, ctaButton pill (→ /contact).
| element | key (variant base) | EN | ES |
|---|---|---|---|
| eyebrow | podcasts.ctaTitle | start a show | empezar un show |
| heading | podcasts.ctaBody | Tell us about your show — format, host count, episodes per season — … | Cuéntanos sobre tu show — formato, cantidad de hosts, … |
| button | podcasts.ctaButton | Get in touch | Escríbenos |
Variants:
default= "default".v1= "v1 · book a session" →ctaTitle_v1(book a session/reserva una sesión),ctaBody_v1,ctaButton_v1(Book a session/Reservar una sesión)v2= "v2 · bring the show" →ctaTitle_v2(bring us the show/tráenos el show),ctaBody_v2,ctaButton_v2(Book a session/Reservar una sesión)
rentals — /services/rentals
src/app/[locale]/services/rentals/page.tsx. Namespace: rentals.
Purpose: the secondary, relationship-gated offer. Curated-shelf positioning,
a catalog jump, 4-step "how it works," rates + eligibility, and a CTA. Dynamic.
1. Hero (VARIANT — rentals.hero, ns rentals, fields tagline, intro)
PageHero: rentals.title eyebrow, tagline headline, intro paragraph.
| element | key (variant base) | EN | ES |
|---|---|---|---|
| eyebrow | rentals.title | rentals | alquiler |
| headline | rentals.tagline | A small shelf of the gear we shoot with. | Un estante pequeño del equipo con el que rodamos. |
| intro | rentals.intro | (long; locale JSON line 170) | (long; locale JSON line 170) |
rentals.hero variants (also drives /services hub card #3 tagline):
default= "default".v1= "v1 · on purpose" →tagline_v1(A small shelf. On purpose./Un estante pequeño. A propósito.) +intro_v1(locale JSON line 171)v2= "v2 · out on loan" →tagline_v2(The kit we shoot with, out on loan./El equipo con el que rodamos, en alquiler.) +intro_v2(locale JSON line 172)
2. Catalog jump (plain)
| element | key | EN | ES |
|---|---|---|---|
framed link (→ /services/rentals/catalog) | rentals.viewAll | see the kit | ver el equipo |
3. How it works (plain — 4 steps)
rentals.howItWorks eyebrow (how it works / cómo funciona), then a 4-step list,
each a title + body:
| step | title key | body key |
|---|---|---|
| 01 | rentals.step1Title (Quote within a day.) | rentals.step1Body |
| 02 | rentals.step2Title (Contract and deposit.) | rentals.step2Body |
| 03 | rentals.step3Title (Pickup from the studio.) | rentals.step3Body |
| 04 | rentals.step4Title (Return and walkthrough.) | rentals.step4Body |
(Full EN/ES bodies at locale JSON lines 174–181.) No variant slots.
4. Rates + eligibility (plain — two columns)
| column | eyebrow key | body key |
|---|---|---|
| rates | rentals.ratesTitle (rates / tarifas) | rentals.ratesBody |
| eligibility | rentals.eligibilityTitle (eligibility / elegibilidad) | rentals.eligibilityBody |
(Full EN/ES at locale JSON lines 182–185.) No variant slots.
5. CTA (VARIANT — rentals.cta, ns rentals, fields ctaTitle, ctaBody, ctaButton)
CtaSection: ctaTitle eyebrow, ctaBody heading, ctaButton pill (→ /contact).
| element | key (variant base) | EN | ES |
|---|---|---|---|
| eyebrow | rentals.ctaTitle | request a quote | solicitar cotización |
| heading | rentals.ctaBody | Tell us what you're shooting, the dates, and the kit you want. … | Cuéntanos qué vas a rodar, las fechas y el equipo que necesitas. … |
| button | rentals.ctaButton | Request a quote | Solicitar cotización |
Variants:
default= "default".v1= "v1 · ask about the shelf" →ctaTitle_v1(ask about the shelf/pregunta por el estante),ctaBody_v1,ctaButton_v1(Ask about the shelf/Pregunta por el estante)v2= "v2 · check availability" →ctaTitle_v2(check availability/consulta disponibilidad),ctaBody_v2,ctaButton_v2(Check availability/Consultar disponibilidad)
Important: the rentals catalog and catalog item pages reuse the
rentals.ctaTitle/ctaBody/ctaButtonbase keys only (NOT the variant picker). So changing a base key there changes the CTA on all three rentals pages, but a chosen_vNonly shows on/services/rentalsitself.
rentals catalog — /services/rentals/catalog
src/app/[locale]/services/rentals/catalog/page.tsx. Namespaces: rentalsCatalog
(+ rentals for the closing CTA). Purpose: browse the shelf. A hero, category
filter chips, a paginated grid of item cards, a per-page control, and the rentals
CTA band. No variant slots anywhere on this page (the hero uses base keys).
1. Hero (plain)
PageHero — uses base rentalsCatalog keys (not variant):
| element | key | EN | ES |
|---|---|---|---|
| eyebrow | rentalsCatalog.title | catalog | catálogo |
| headline | rentalsCatalog.tagline | What we shoot with. | Con qué rodamos. |
| intro | rentalsCatalog.intro | Everything below comes off the same shelf … within a day. | Todo lo de abajo sale del mismo estante … en un día hábil. |
2. Category filter chips (plain)
<nav aria-label> = rentalsCatalog.jumpNav (Catalog filters / Filtros del catálogo). First chip is "all"; the rest come from categoryCounts (only
categories with items appear). Each chip label is
rentalsCatalog.categories.<key> followed by a count.
| element | key | EN | ES |
|---|---|---|---|
| all chip | rentalsCatalog.categories.all | all | todos |
| per-category chips | rentalsCatalog.categories.<category_key> | display name | display name |
Category keys + display names (full map at locale JSON lines 237–268; examples):
camera_body→camera body/cuerpo de cámara, camera_lens→camera lens/lente,
light→light/luz, microphone→microphone/micrófono, grip→grip/grip,
battery→battery/batería, computer→computer/computadora, etc. (29 categories).
3. Grid + pagination (plain)
| element | key | EN | ES |
|---|---|---|---|
| empty state | rentalsCatalog.empty | Nothing in this category yet — try another. | Nada en esta categoría por ahora — prueba otra. |
pagination aria-label | rentalsCatalog.pagination.label | Catalog pagination | Paginación del catálogo |
| prev link | rentalsCatalog.pagination.prev | prev | anterior (rendered as ← prev) |
| next link | rentalsCatalog.pagination.next | next | siguiente (rendered as next →) |
| "of" separator | rentalsCatalog.pagination.of | of | de (shown as NN of NN) |
| per-page label | rentalsCatalog.pagination.perPage | per page | por página |
| card "sold" (sr-only) | rentalsCatalog.status.sold | sold | vendido |
| card "coming soon" (sr-only) | rentalsCatalog.status.comingSoon | coming soon | próximamente |
Each grid cell is a RentalItemCard linking to /services/rentals/catalog/[sku].
The card's manufacturer / name / summary come from the Supabase items table,
not from copy files — not editable here. soldLabel/comingSoonLabel are the two
status.* strings above (screen-reader only; the X overlay is visual).
4. CTA band (reuses rentals base keys — plain)
CtaSection with rentals.ctaTitle / rentals.ctaBody / rentals.ctaButton
(base keys only — see the rentals CTA note above).
catalog item — /services/rentals/catalog/[sku]
src/app/[locale]/services/rentals/catalog/[sku]/page.tsx. Namespaces:
rentalsItem, rentalsCatalog (category label), rentals (CTA). Purpose:
one piece of gear — name, summary, spec rows, image gallery, optional in-use
shots, and the rentals CTA. No variant slots. Item facts (manufacturer / name
/ summary / MPN / accessories / category / quantity) come from Supabase
items — not copy files.
1. Detail header + spec list (chrome from rentalsItem; data from DB)
| element | key / source | EN | ES |
|---|---|---|---|
| manufacturer (eyebrow) | DB item.manufacturer | — | — |
| H1 | DB item.name | — | — |
| summary | DB item.summary | — | — |
| spec: category label | rentalsItem.category | category | categoría |
| spec: availability label | rentalsItem.availability | availability | disponibilidad |
| availability value | rentalsItem.availableCount (ICU {count}) | {count} available | {count, plural, one {# disponible} other {# disponibles}} |
| spec: model label | rentalsItem.mpn | model | modelo |
| spec: accessories label | rentalsItem.included | accessories | accesorios |
| category value | rentalsCatalog.categories.<key> | display name | display name |
back link (in rentalsItem, currently unused on this page) | rentalsItem.back | catalog | catálogo |
2. In use (plain — only if review images exist)
| element | key | EN | ES |
|---|---|---|---|
| eyebrow | rentalsItem.review | in use | en uso |
3. CTA (reuses rentals base keys — plain)
CtaSection with rentals.ctaTitle / ctaBody / ctaButton (base keys).
stills — /services/stills
src/app/[locale]/services/stills/page.tsx. Namespace: stills. Purpose:
the photography practice — a tagline hero, a masonry gallery, and a CTA. No
variant slots. Gallery images + per-image captions come from
content/stills/stills.json (single file, not localized).
1. Hero (plain)
| element | key | EN | ES |
|---|---|---|---|
| eyebrow | stills.title | stills | fotografía |
| headline | stills.tagline | Frame first, gear last. | Encuadre primero, equipo después. |
| intro | stills.intro | Our stills practice runs from street and fashion to travel, portraiture, and still life — … | Nuestra práctica fotográfica va de lo urbano y la moda al viaje, el retrato y el bodegón — … |
2. Gallery (content file — content/stills/stills.json)
Each still has alt (screen-reader description / placeholder label), gear
(caption left), settings (caption right). Numbered overlay (01, 02 …) is
generated. Single shared file — edit content/stills/stills.json. Example
entry: alt: "Side profile portrait, natural light", gear: "Sony A7R V, 70mm",
settings: "ƒ/5, 1/60s, ISO 100".
3. CTA (plain — CtaSection)
| element | key | EN | ES |
|---|---|---|---|
| eyebrow | stills.ctaTitle | start a project | comencemos |
| heading | stills.ctaBody | Need stills for a campaign, a set, or a trip? … | ¿Necesitas fotografía para una campaña, un set o un viaje? … |
button (→ /contact) | stills.ctaButton | Get in touch | Escríbenos |
projects — /projects
src/app/[locale]/projects/page.tsx. Namespace: projects. Purpose: the
work index — a title/intro header and a card grid. No variant slots. Cards
come from content/projects/*.mdx frontmatter.
1. Header (plain)
| element | key | EN | ES |
|---|---|---|---|
| H1 | projects.title | projects | proyectos |
| intro | projects.intro | Selected work, from social shorts to feature-length productions. | Trabajo seleccionado, desde piezas para redes hasta producciones de largo formato. |
| empty state | projects.empty | More work soon. | Pronto más trabajo. |
2. Card grid (content file — see Project shared content)
Each ProjectCard shows the MDX title, an in-image deliverables stamp (falls
back to title), and the type label. Card links to /projects/[slug].
project detail — /projects/[slug]
src/app/[locale]/projects/[slug]/page.tsx. Namespace: projects. Purpose:
a single project — parallax hero, title + description, metadata, body prose,
optional gallery, and a "next project" link. No variant slots. Almost all
copy is per-project MDX frontmatter + body (content/projects/<slug>.<locale>.mdx).
Sections in order
| element | key / source | EN label | ES label |
|---|---|---|---|
| hero image | MDX cover / coverPosition | — | — |
| year (eyebrow) | MDX year | — | — |
| H1 | MDX title | — | — |
| description | MDX description | — | — |
| metadata: timeframe label | projects.timeframe | timeframe | duración |
| metadata: client label | projects.client | client | cliente |
| metadata: role label | projects.role | role | rol |
| metadata values | MDX timeframe / client / role[] | — | — |
| body prose | MDX body (below frontmatter) | — | — |
gallery section label + aria-label | projects.gallery | gallery | galería |
| next-project link | MDX title of next project | — | — |
watch (in JSON, for video link) | projects.watch | watch | ver |
Project shared content (content/projects/*.mdx)
The copywriter edits these .mdx files for project copy. Frontmatter fields
(see src/lib/content.ts Project type): title, year, type (card label,
e.g. "Podcast"), timeframe, deliverables (card stamp, e.g. "12 episodes"),
client, role[], cover, coverPosition, gallery[], description,
videoUrl, featured, order. Body (prose after ---) becomes the detail
paragraphs. Files are per-locale: <slug>.en.mdx and <slug>.es.mdx. Current
slugs: dosnomadas-podcast, lost-in-vegas-bts, monaco-panaderia,
short-film-tropical.
team — /team
src/app/[locale]/team/page.tsx. Namespace: team. Purpose: the small core
team + a recruitment band. No variant slots. Member cards come from
content/team/team.json; the recruitment band is locale JSON.
1. Header (plain)
| element | key | EN | ES |
|---|---|---|---|
| H1 | team.title | team | equipo |
| intro | team.intro | A small core team between Los Angeles and Medellín. We scale up per project … | Un equipo central pequeño entre Los Ángeles y Medellín. … |
2. Member grid (content file — content/team/team.json)
Each member: name, role.{en,es} (rendered as an underscored label, e.g.
Executive_Producer_), bio.{en,es}, photo. Edit content/team/team.json.
Current members: Brandon Dee Zacharie (Executive Producer / Productor Ejecutivo),
Keren Monsalve Cordero (Head of Operations / Directora de Operaciones).
3. Recruitment band (plain — locale JSON)
| element | key | EN | ES |
|---|---|---|---|
| eyebrow | team.recruitStamp | join us | súmate |
| heading | team.recruitTitle | Build a studio with us. | Construyamos un estudio juntos. |
| body | team.recruitBody | We're growing the team. Editors most of all — … | Estamos creciendo el equipo. Sobre todo, edición — … |
| button (mailto) | team.recruitButton | Get in touch | Escríbenos |
| mailto address | team.recruitEmail | hi@studio.chat | hola@studio.chat |
| mailto subject | team.recruitEmailSubject | Joining the team — your name | Sumándome al equipo — tu nombre |
contact — /contact
src/app/[locale]/contact/page.tsx + src/components/ContactForm.tsx.
Namespace: contact (+ site for email). Purpose: low-friction, warm,
direct — make sending the message easy. Header, the form, and an aside with
email + studio address + map. Dynamic (resolves the intro + form variants
server-side and passes them into the client form as props).
1. Header (VARIANT — contact.intro, ns contact, field intro)
SectionHeader: contact.title H1 + variant intro.
| element | key | EN | ES |
|---|---|---|---|
| H1 | contact.title | contact | contacto |
| intro | contact.intro (variant base) | Tell us about your project. We respond within two business days. | Cuéntanos sobre tu proyecto. Respondemos en dos días hábiles. |
contact.intro variants:
default= "default".v1= "v1 · two-day turnaround" →contact.intro_v1- EN:
Tell us what you're making. We read every message and reply within two business days. - ES:
Cuéntanos qué estás creando. Leemos cada mensaje y respondemos en dos días hábiles.
- EN:
v2= "v2 · a few details" →contact.intro_v2- EN:
A few details about the project go a long way. We reply within two business days. - ES:
Unos cuantos detalles del proyecto nos ayudan mucho. Respondemos en dos días hábiles.
- EN:
2. Contact form (VARIANT — contact.form, ns contact, fields message, submit)
The form fields and submit button. Only the message field label and submit
button label are variant-controlled (passed in as messageLabel/submitLabel);
the other field labels are plain. The form has an email/WhatsApp toggle (only one
contact-method field shows at a time).
| element | key | EN | ES |
|---|---|---|---|
| name label | contact.name | Name | Nombre |
| email label / toggle | contact.email | E-mail | Correo |
| whatsapp label / toggle | contact.whatsapp | WhatsApp | WhatsApp |
| company label | contact.company | Company / Project | Empresa / Proyecto |
| budget label | contact.budget | Estimated budget | Presupuesto estimado |
| message label (variant) | contact.message | Tell us about it | Cuéntanos |
| submit button (variant) | contact.submit | Send message | Enviar |
| pending button copy | contact.sending | Sending... | Enviando... |
| success block heading | contact.success | Got it. We'll be in touch soon. | Recibido. Te contactamos pronto. |
| inline submit error | contact.error | Something went wrong. Email us directly at hi@studio.chat. | Algo salió mal. Escríbenos directamente a hola@studio.chat. |
| budget options | contact.budgetOptions.* | Under $5k / $5k–$15k / $15k–$50k / $50k+ / Not sure yet | Menos de $5k / $5k–$15k / $15k–$50k / $50k+ / Aún no sé |
| field errors | contact.errors.* | Required / Check this field / Enter a valid email / Enter a valid number / Tell us a little more | Requerido / Revisa este campo / Ingresa un correo válido / Ingresa un número válido / Cuéntanos un poco más |
contact.form variants (each sets BOTH message and submit):
default= "default" →message/submitabove.v1= "v1 · send the idea" →message_v1(What you have in mind/Lo que tienes en mente),submit_v1(Send the idea/Enviar la idea)v2= "v2 · the project" →message_v2(About the project/Sobre el proyecto),submit_v2(Send it over/Enviar)
3. Aside — email + studio (plain)
| element | key | EN | ES |
|---|---|---|---|
| email eyebrow | contact.or | or write us at | o escríbenos a |
| email value (mailto) | site.email | hi@studio.chat | hola@studio.chat |
| studio eyebrow | contact.studio | studio | estudio |
| address line 1 | contact.addressLine1 | Cra. 34 #7-129, 103 | Cra. 34 #7-129, 103 |
| address line 2 | contact.addressLine2 | Medellín, Antioquia, CO | Medellín, Antioquia, CO |
| directions link (external → Google Maps) | contact.directions | get directions | cómo llegar |
The success state (after submit) re-uses contact.success, contact.or, and
site.email.
faqs — /faqs
src/app/[locale]/faqs/page.tsx. Namespace: faqs. Purpose: quick answers
to common pre-booking questions. A header + an accordion list. Dynamic (intro
variant). FAQ Q&A pairs come from content/faqs/{en,es}.json.
1. Header (VARIANT — faqs.intro, ns faqs, field intro)
SectionHeader: faqs.title H1 + variant intro.
| element | key | EN | ES |
|---|---|---|---|
| H1 | faqs.title | faqs | faqs |
| intro | faqs.intro (variant base) | Quick answers to the questions we hear most. | Respuestas rápidas a las preguntas que más nos hacen. |
faqs.intro variants:
default= "default".v1= "v1 · before the first email" →faqs.intro_v1- EN:
The questions we get before the first email. - ES:
Las preguntas que llegan antes del primer correo.
- EN:
v2= "v2 · what people ask" →faqs.intro_v2- EN:
What people ask before they book a session. - ES:
Lo que la gente pregunta antes de reservar una sesión.
- EN:
2. FAQ accordion (content file — content/faqs/{locale}.json)
Each entry: q (question, the summary line) + a (answer body). Numbered (01,
02 …) automatically. Edit content/faqs/en.json and content/faqs/es.json —
no variant system. Current questions (EN): "Where are you based?", "What's the
typical timeline?", "Do you work in English?", "Can you handle post only?", "How
do we start?".
privacy — /privacy
src/app/[locale]/privacy/page.tsx. Namespace: privacy. Purpose: the
privacy policy. A header + a numbered section list. No variant slots. Section
bodies come from content/privacy/{en,es}.json.
1. Header (plain)
| element | key | EN | ES |
|---|---|---|---|
| H1 | privacy.title | privacy | privacidad |
| intro | privacy.intro | How we handle the information you share with us. | Cómo tratamos la información que compartes con nosotros. |
| "last updated" label | privacy.updated | Last updated | Última actualización |
2. Sections (content file — content/privacy/{locale}.json)
A date plus a sections[] array of { title, body }. Edit
content/privacy/en.json / es.json. Current section titles (EN): Who we are;
What we collect; How we use it; Who we share it with; Error and session
monitoring; Cookies; Your rights; Changes to this policy.
launch gate — /launch
src/app/[locale]/launch/page.tsx + src/components/launch/{LaunchHero, Countdown,LaunchReveal,BypassForm}.tsx. Namespace: launch (+ launch.meta for
metadata). Purpose: the pre-launch holding page (every gated URL is rewritten
here). Full-bleed image, a live countdown, an editorial blurb that flips to a
disguised subscribe form (which secretly accepts @studio.chat insider
credentials to bypass). No variant slots.
Metadata (launch.meta) (plain — link-unfurler only; page is noindex)
| element | key | EN | ES |
|---|---|---|---|
| title (absolute) | launch.meta.title | studio.chat | studio.chat |
| description | launch.meta.description | True audiovisual production is coming to the heart of Medellín. Launching June 26, 2026 — 12:00 Bogotá. | Auténtica producción audiovisual está llegando al corazón de Medellín. Lanzamiento el 26 de junio de 2026 — 12:00 Bogotá. |
| OG image alt | launch.meta.ogAlt | studio.chat — true audiovisual production coming to Medellín | studio.chat — auténtica producción audiovisual llegando a Medellín |
Countdown units (plain — launch.units)
| element | key | EN | ES |
|---|---|---|---|
| days label | launch.units.d | days | días |
| hours label | launch.units.h | hrs | hrs |
| minutes label | launch.units.m | min | min |
| seconds label | launch.units.s | sec | seg |
Blurb reveal (LaunchReveal) (plain)
| element | key | EN | ES |
|---|---|---|---|
blurb (rich, <em> around inner phrase) | launch.blurb | True <em>audiovisual productions</em> are coming to the heart of Medellín. | Auténticas <em>producciones audiovisuales</em> están llegando al corazón de Medellín. |
| reveal button (FramedLink) | launch.learnMore | learn more | conoce más |
Note: launch.eyebrow (until launch / hasta el lanzamiento) exists in JSON
but is not currently rendered by the page; keep it consistent if reworded.
Subscribe / bypass form (BypassForm, labels from launch.subscribe) (plain)
| element | key | EN | ES |
|---|---|---|---|
| form eyebrow | launch.subscribe.eyebrow | subscribe for updates | suscríbete para novedades |
| name label | launch.subscribe.name | name | nombre |
| email label | launch.subscribe.email | email | email |
| password label (insider only) | launch.subscribe.password | password | contraseña |
| submit (subscribe mode) | launch.subscribe.cta | Notify me | Avísame |
| submit (insider/unlock mode) | launch.subscribe.submit | Unlock | Desbloquear |
| in-flight button copy | launch.subscribe.loading | Loading... | Cargando... |
| invalid-credentials error | launch.subscribe.invalid | Invalid | No válido |
| endpoint-failure error | launch.subscribe.unavailable | Unavailable | No disponible |
| subscribed confirmation | launch.subscribe.subscribed | Thanks — we'll send launch updates. | Gracias — te avisaremos del lanzamiento. |
The form POSTs to /api/launch-subscribe (normal) or /api/launch-bypass
(when the email ends in @studio.chat). Submitting an insider email reveals the
password field and switches the button label from cta to submit.
Shared chrome (header, footer, nav)
These render on every public page except /launch (which uses a minimal
header and no footer). No variant slots.
Public nav — src/components/Header.tsx (+ NavLink, ServicesMenu)
Namespace: nav. Desktop shows: Projects · Services (dropdown) · Contact.
The Services dropdown lists Services / Podcasts / Rentals / Stills. On mobile the
parent "services" link is dropped and its children show inline as peers.
| nav item | key | EN | ES | href |
|---|---|---|---|---|
| projects | nav.projects | projects | proyectos | /projects |
| services (parent + landing) | nav.services | services | servicios | /services |
| podcasts | nav.podcasts | podcasts | podcasts | /services/podcasts |
| rentals | nav.rentals | rentals | alquiler | /services/rentals |
| stills | nav.stills | stills | fotografía | /services/stills |
| contact | nav.contact | contact | contacto | /contact |
Other nav.* keys used elsewhere: nav.team (team/equipo, footer),
nav.faqs (faqs/faqs, footer), nav.privacy (privacy/privacidad, footer),
nav.location (location/ubicación, not currently rendered). LocaleSwitch
shows en/es as literal toggles (not translated).
Footer — src/components/Footer.tsx
Namespaces: footer, nav, site. Three eyebrow columns (explore, manage,
say hello) + a legal bar.
| element | key | EN | ES |
|---|---|---|---|
| explore eyebrow | footer.explore | explore | explorar |
| explore links | nav.projects, nav.team, nav.services, nav.podcasts, nav.rentals, nav.stills, nav.faqs, nav.privacy | see nav table | see nav table |
| manage eyebrow (staff/preview only) | footer.manage | manage | administrar |
office link (→ /office) | footer.office | office | oficina |
| say-hello eyebrow | footer.sayHello | say hello | escríbenos |
email link (→ /contact) | site.email | hi@studio.chat | hola@studio.chat |
| social links (Instagram/Vimeo/YouTube/TikTok) | handle studiodotchat (in code) | — | — |
| legal — company | footer.company | STUDIO.CHAT S.A.S. | STUDIO.CHAT S.A.S. |
| legal — made-in | site.madeIn | MADE WITH ♥︎ IN MEDELLÍN | HECHO CON ♥︎ EN MEDELLÍN |
Error / not-found pages (plain)
src/app/[locale]/not-found.tsx (404) and error.tsx (500). Namespace: errors.
| page | element | key | EN | ES |
|---|---|---|---|---|
| 404 | eyebrow | errors.notFound.eyebrow | not found | no encontrado |
| 404 | title | errors.notFound.title | We can't find that page. | No encontramos esa página. |
| 404 | body | errors.notFound.body | The link may be broken, or the page may have moved. … | Puede que el enlace esté roto … sí existe. |
| 404 | home link | errors.notFound.home | back home | volver al inicio (rendered with leading ⮐) |
| 500 | eyebrow | errors.serverError.eyebrow | error | error |
| 500 | title | errors.serverError.title | Something went wrong on our end. | Algo salió mal de nuestro lado. |
| 500 | body | errors.serverError.body | An unexpected error stopped this page from loading. … | Un error inesperado impidió cargar esta página. … |
| 500 | retry button | errors.serverError.retry | try again | reintentar |
| 500 | home link | errors.serverError.home | back home | volver al inicio |
site / brand strings (used across pages) (plain — site ns)
| key | EN | ES |
|---|---|---|
site.name | studio.chat | studio.chat |
site.tagline | audiovisual | audiovisual |
site.metaTitle | STUDIO.CHAT Audiovisual Productions | STUDIO.CHAT Producciones Audiovisuales |
site.description | Audiovisual production in Medellín — social content, short films, and live podcasts, end to end. | Producción audiovisual en Medellín — contenido social, cortometrajes y podcasts en vivo, de principio a fin. |
site.madeIn | MADE WITH ♥︎ IN MEDELLÍN | HECHO CON ♥︎ EN MEDELLÍN |
site.email | hi@studio.chat | hola@studio.chat |
Not covered here: the
portalnamespace (the client reservation portal at/portal, separate from the marketing site) and all of/office(admin). They are not part of the public marketing surface this doc targets.
How variants work + how to add one
Variants let the office (/office/copy) A/B different copy per page section
without code changes. Mechanism (src/lib/copy/variants.ts):
- Key convention. The chosen variant id doubles as the locale-key suffix.
default→ the base key (heroLine1);v1→heroLine1_v1;v2→heroLine1_v2. The helpervariantKey(base, id)does this lookup. So every variant is just more sibling keys in the same namespace. - The registry.
COPY_PAGESlists each page → itssections. A section has:key(e.g.home.hero, thepage.sectionid stored in thecopy_variantstable),label,namespace(which locale namespace the fields live under),fields(the base keys this section controls), andvariants({ id, label }—labelis the human string shown in the office, e.g."v2 · record once"). - Selection. The office writes the chosen id to the
copy_variantstable keyed bypage.section. At request time the page callsgetCopyVariant("page.section")and renderst(variantKey(field, id)). If the stored id isn't in the registry, it falls back to the section's first variant (default).
To add a v3 (or any new variant) to an existing section
- In both
locales/en.jsonandlocales/es.json, add a_v3sibling for every field the section lists inCOPY_PAGES.fields. Example forhome.hero(fieldsheroLine1..heroLine4): addheroLine1_v3,heroLine2_v3,heroLine3_v3,heroLine4_v3in each file. (For a multi-field CTA section, addctaTitle_v3,ctaBody_v3,ctaButton_v3.) Write the Spanish as a parallel rewrite, not a literal translation. - In
src/lib/copy/variants.ts, add{ id: "v3", label: "v3 · <short label>" }to that section'svariantsarray. Without this step the office won't show the new variant even though the keys exist. - That's it — no page code changes. The page already resolves the section via
getCopyVariant+variantKey.
Sections that currently support variants (registry summary)
| section key | namespace | fields | variant ids (labels) |
|---|---|---|---|
home.hero | home | heroLine1, heroLine2, heroLine3, heroLine4 | default, v1 (podcasts-led), v2 (sharpened generalist) |
home.services | home | servicesIntro | default, v1 (studio day), v2 (record once) |
home.cta | home | ctaTitle, ctaBody, ctaButton | default, v1 (book a session), v2 (start with a pilot) |
services.intro | services | intro | default, v1 (kit ladder), v2 (one room) |
podcasts.hero | podcasts | tagline, intro | default, v1 (final_final_v3), v2 (long conversations) |
podcasts.cta | podcasts | ctaTitle, ctaBody, ctaButton | default, v1 (book a session), v2 (bring the show) |
rentals.hero | rentals | tagline, intro | default, v1 (on purpose), v2 (out on loan) |
rentals.cta | rentals | ctaTitle, ctaBody, ctaButton | default, v1 (ask about the shelf), v2 (check availability) |
contact.intro | contact | intro | default, v1 (two-day turnaround), v2 (a few details) |
contact.form | contact | message, submit | default, v1 (send the idea), v2 (the project) |
faqs.intro | faqs | intro | default, v1 (before the first email), v2 (what people ask) |
Everything else on the public site (stills, projects, project detail, team,
privacy, rentals catalog/item, launch, nav, footer, errors, and the non-variant
fields within the pages above) is plain copy — edit the base key in
locales/*.json (or the relevant content/* file) directly; there is no _vN
slot until you create one and register it.
Revalidation note
COPY_PAGE_PATHS in variants.ts maps each page to the public URLs to revalidate
when its copy changes. If you introduce a variant section on a new page, add
that page's EN+ES paths there too (podcasts/rentals also list /services because
their taglines surface on the hub).