Pular para o conteúdo

Tema, textos e seções

Depois de slash create, o projeto tem quatro arquivos para editar, todos dentro da pasta do projeto:

  • src/theme.ts: cores, fontes e raio.
  • src/data/content.ts: os textos e as props de cada seção.
  • src/data/site.ts: nome, WhatsApp, checkout, GTM e SEO.
  • src/pages/home.ts: a ordem das seções.

Não edite src/ui/. É a cópia dos componentes, com a versão no arquivo src/ui/VERSION. Para outra seção ou outra variante, peça ao fornecedor uma versão nova dessa pasta.

O tema vem de defineTheme, em src/ui/theme/theme.ts. Em src/theme.ts você passa só o que muda, e o resto continua como no tema base. Listas (como as fontes) são trocadas inteiras, nunca somadas às da base.

O tema base tem cinco tons (light, gray, dark, accent e photo). Cada tom tem onze cores, os tokens bg, fg, muted, soft, line, ph, phl, card, bb, bf e inp. Os botões principais usam bb como fundo e bf como texto; o campo de formulário usa inp na borda.

A base usa IBM Plex Sans nos pesos 400, 500, 600 e 700, para títulos e textos. Os arquivos ficam em public/fonts/. Raio de 8 px, títulos sem caixa alta, botões sólidos e cartões com linha de 1 px.

A chave fonts tem duas listas, heading e body. Cada item tem family, weight e file, e o file é o caminho servido a partir de public/, como /fonts/ibm-plex-sans-400.woff2. Para trocar a família, coloque os arquivos .woff2 em public/fonts/ e passe a lista inteira em src/theme.ts, por exemplo fonts: { heading: [{ family: "Nome", weight: 400, file: "/fonts/nome-400.woff2" }], body: [...] }. Sem fallbacks, a família cai na pilha de fontes do sistema.

Este exemplo muda o raio, deixa os títulos em maiúsculas e põe o contorno em gradiente nos botões principais e nos cartões:

src/theme.ts
import { defineTheme } from "./ui/theme/theme";
/** Overrides over the base theme: rounder corners, upper-case headings and gradient rings on buttons and cards. */
export const theme = defineTheme({
radius: "12px",
headingCase: "upper",
buttons: "outline-gradient",
cardBorder: "gradient",
});

Para trocar a cor da marca, mude bb nos tons que têm botão principal. O slash check confere o contraste: texto com pelo menos 4,5:1 sobre o fundo e o cartão, texto do botão principal (bf sobre bb) com 4,5:1, e fundo do botão e borda de campo com 3:1. Este exemplo põe o azul #0B5CAD nos tons light e gray; o texto branco sobre ele fica em 6,7:1.

src/theme.ts
import { defineTheme } from "./ui/theme/theme";
/** Brand color on the primary buttons of the light and gray tones; the other tones keep the base colors. */
export const theme = defineTheme({
tones: {
light: { bb: "#0B5CAD" },
gray: { bb: "#0B5CAD" },
},
});

Cor escrita no código (hex, rgb(), oklch()) só fica em src/theme.ts e nos arquivos de tema de src/ui/. O teste de arquitetura do projeto acusa cor em qualquer outro arquivo de src/, exceto os testes.

Com bun run dev aberto, o servidor regenera public/theme.css a cada mudança em src/theme.ts.

Em src/data/content.ts há um objeto por seção, com as props que o componente recebe. Um valor pode ter marcador:

  • claude(...): texto escrito pelo Claude.
  • fictitious(...): conteúdo de exemplo, como um nome ou um depoimento inventado.
  • pending(...): conteúdo que a agência ainda precisa fornecer.

Um valor sem marcador veio da agência. Os componentes recebem o texto sem marcador, pela função plain.

O slash check lista o que falta. Um item pendente bloqueia a publicação. Um item fictício pede slash approve, que mostra cada item com o marcador, pede confirmação e grava content-release.json. O approve não libera pending e pede o git config user.name preenchido. Com bun run build:prod, o gate roda antes do build.

Este exemplo troca o título do hero por um texto da agência, sem marcador, e mantém o subtítulo como texto do Claude, à espera de aprovação:

src/data/content.ts
import { claude, fictitious } from "./origin";
import { site } from "./site";
/** Static placeholders served from public/placeholder (no image pipeline here: this module must stay importable by plain bun). */
const photo = (alt: string, name: "photo" | "wide" | "portrait" | "icon", width: number, height: number) =>
({ src: `/placeholder/${name}.svg`, alt, width, height }) as const;
const OFFER = "#oferta";
/**
* Props of each section, in page order. Values written by Claude or invented are marked (`claude`, `fictitious`,
* `pending`); an unmarked value came from the agency. `plain(content)` strips the markers for the components.
*/
export const content = {
hero: {
tone: "dark",
eyebrow: claude("Curso online de confeitaria"),
title: "Da sua cozinha para um negócio que vende todo dia",
subtitle: claude("Aprenda receitas, preço e vendas em 30 dias, no seu ritmo, direto do celular."),
cta: { kind: "link", label: claude("Quero ver a aula de apresentação"), href: OFFER },
video: {
href: "https://exemplo.com.br/aula-de-apresentacao",
poster: photo("Capa da aula de apresentação do curso", "wide", 1280, 720),
},
rating: fictitious({ value: 4.9, count: 1280 }),
},
features: {
tone: "light",
title: claude("Tudo para começar a vender já na primeira semana"),
intro: claude("Quatro pilares que tiram você do improviso e colocam pedidos na sua agenda."),
items: [
{ title: claude("Receitas testadas"), text: claude("Mais de 40 receitas com rendimento e custo calculados."), icon: photo("Colher", "icon", 44, 44) },
{ title: claude("Preço sem achismo"), text: claude("Planilha pronta para saber quanto cobrar em cada doce."), icon: photo("Calculadora", "icon", 44, 44) },
{ title: claude("Vendas pelo WhatsApp"), text: claude("Modelos de mensagem e cardápio para fechar encomendas."), icon: photo("Balão de conversa", "icon", 44, 44) },
{ title: claude("Suporte de verdade"), text: claude("Tire dúvidas com a professora em dias úteis."), icon: photo("Fone de ouvido", "icon", 44, 44) },
],
},
featureSplit: {
tone: "gray",
eyebrow: claude("Como funciona"),
title: claude("Do primeiro bolo à primeira encomenda"),
text: claude("Cada módulo termina com uma tarefa prática, para você aplicar o que aprendeu antes de seguir."),
points: [
claude("Aulas curtas, de até 12 minutos"),
claude("Passo a passo com fotos e vídeos"),
claude("Certificado ao concluir"),
],
image: photo("Bancada com bolos decorados", "photo", 640, 480),
},
about: {
tone: "light",
title: claude("Quem ensina"),
paragraphs: [
fictitious("Sou a Marina, confeiteira há 12 anos. Comecei vendendo bolos para a vizinhança e hoje tenho um ateliê com equipe própria."),
fictitious("Criei este curso para encurtar o caminho que levei anos para percorrer."),
],
image: photo("Marina decorando um bolo no ateliê", "photo", 640, 480),
numbers: [
fictitious({ value: "12 anos", label: "de confeitaria" }),
fictitious({ value: "3.400", label: "alunas formadas" }),
fictitious({ value: "4,9", label: "nota média das avaliações" }),
fictitious({ value: "27", label: "estados com alunas ativas" }),
],
},
testimonials: {
tone: "gray",
title: claude("Quem fez o curso conta como foi"),
items: [
fictitious({
quote: "Na terceira semana já fechei minha primeira encomenda de aniversário.",
name: "Camila Rocha",
role: "Confeiteira · Recife",
photo: photo("Retrato de Camila Rocha", "portrait", 48, 48),
rating: 5 as const,
verified: false,
}),
fictitious({
quote: "A planilha de preços mudou minha margem. Eu vendia no prejuízo e nem sabia.",
name: "Renata Lima",
role: "Doceira · Belo Horizonte",
photo: photo("Retrato de Renata Lima", "portrait", 48, 48),
rating: 5 as const,
verified: false,
}),
fictitious({
quote: "Aulas curtas, que cabem na rotina de quem tem filhos pequenos.",
name: "Patrícia Souza",
role: "Mãe e empreendedora · Curitiba",
photo: photo("Retrato de Patrícia Souza", "portrait", 48, 48),
rating: 4 as const,
verified: false,
}),
],
},
pricing: {
tone: "light",
id: "oferta",
title: claude("Um preço simples, sem surpresas"),
intro: claude("Pague uma vez e tenha acesso para sempre."),
plans: [
{
name: claude("Curso completo"),
price: fictitious({ amount: 497, currency: "BRL", period: "once" } as const),
note: fictitious("ou 12x de R$ 51,40 no cartão"),
features: [
claude("Acesso vitalício às aulas"),
claude("Planilhas e modelos prontos"),
claude("Comunidade de alunas"),
claude("Certificado de conclusão"),
],
cta: { kind: "checkout", label: claude("Quero garantir minha vaga"), href: site.checkoutUrl },
},
],
guarantee: { title: claude("Garantia de 7 dias"), text: claude("Se não gostar, devolvemos o valor integral.") },
},
faq: {
tone: "gray",
title: claude("Perguntas frequentes"),
items: [
{ question: claude("Preciso de experiência?"), answer: claude("Não. O curso começa do zero e avança passo a passo.") },
{ question: claude("Por quanto tempo tenho acesso?"), answer: claude("Para sempre, com todas as atualizações futuras.") },
{ question: claude("Posso pagar no cartão?"), answer: claude("Sim, em até 12 vezes, além de Pix e boleto.") },
{ question: claude("E se eu não gostar?"), answer: claude("Você tem 7 dias para pedir o reembolso, sem burocracia.") },
],
},
cta: {
tone: "dark",
title: claude("Pronta para vender seus primeiros doces?"),
text: claude("Garantia de 7 dias: se não gostar, devolvemos o valor."),
cta: { kind: "checkout", label: claude("Quero garantir minha vaga"), href: site.checkoutUrl },
},
footer: {
tone: "light",
links: [
{ label: "Termos de uso", href: "#termos" },
{ label: "Política de privacidade", href: "#privacidade" },
],
year: 2026,
cookiePreferences: "Preferências de cookies",
},
consent: {
text: claude("Usamos cookies para medir o uso da página e melhorar a oferta. Você escolhe se aceita."),
policyHref: "#privacidade",
},
} as const;

A ordem está em src/pages/home.ts, na lista de seções dentro de landingPage. Para mudar a ordem, mova a linha da seção. Cada seção pede as props do seu bloco em content.ts; se faltar uma prop, o bun run typecheck acusa o erro.

O exemplo depende de src/data/content.ts (os props de cada seção), de src/data/origin.ts (o plain), de src/data/site.ts (o site), de src/ui-host.ts (o uiHost) e dos componentes de src/ui/ (exportados por src/ui/index.ts). Este exemplo põe os depoimentos logo depois das características:

src/pages/home.ts
import type { SafeHtml } from "@_bashell/slash/ssr";
import { content } from "../data/content";
import { plain } from "../data/origin";
import { site } from "../data/site";
import {
About,
ConsentBanner,
CtaSection,
Faq,
FeatureSplit,
Features,
Footer,
type FooterProps,
faqSchema,
footerSchema,
gtmBody,
Hero,
html,
jsonLd,
landingPage,
Pricing,
pricingSchema,
Testimonials,
testimonialsSchema,
trackMarker,
utmMarker,
WhatsappFab,
whatsappHref,
} from "../ui";
import { uiHost } from "../ui-host";
const c = plain(content);
const s = plain(site);
const footer: FooterProps = { ...c.footer, business: s.business };
/** Id of the GTM container, shared with the head (routes.ts). */
export const gtm = { id: s.gtmId } as const;
/** JSON-LD graph of the page, built from the same content the sections render. */
export const structuredData = jsonLd(
faqSchema(c.faq),
pricingSchema(c.pricing, s.name),
testimonialsSchema(c.testimonials, s.name),
footerSchema(footer, s.baseUrl),
);
/** The sales page, sections in page order. */
export function home(): SafeHtml {
return landingPage({
before: [gtmBody(gtm)],
main: html`${[
Hero(c.hero, uiHost),
Features(c.features, uiHost),
Testimonials(c.testimonials, uiHost),
FeatureSplit(c.featureSplit, uiHost),
About(c.about, uiHost),
Pricing(c.pricing, uiHost),
Faq(c.faq, uiHost),
CtaSection(c.cta, uiHost),
]}`,
footer: Footer(footer, uiHost),
fab: WhatsappFab({ href: whatsappHref(s.whatsapp.number, s.whatsapp.message), label: "Falar no WhatsApp" }, uiHost),
after: [ConsentBanner(c.consent, uiHost), utmMarker(uiHost), trackMarker(uiHost)],
});
}