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:
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.
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:
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;Ordem das seções
Seção intitulada “Ordem das seções”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:
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)], });}