Multi-page catálogo
O modelo catalogo do template multi-page transforma o site em um catálogo. Ele tem uma lista de itens com foto e preço, uma página para cada item, uma página para cada categoria com pelo menos dois itens e busca na lista. Não tem as páginas de serviços e de equipe do modelo clássico. Mantém início, sobre, contato e a página 404.
Criar o projeto
Seção intitulada “Criar o projeto”Rode slash create <pasta> --template multi-page --model catalogo na pasta onde o projeto deve ficar. A saída é esta:
Projeto criado em <projeto>.
Próximos passos, dentro da pasta:
slash check confere conteúdo, contraste e SEO
slash approve libera o conteúdo fictício aprovado
bun run dev servidor local
slash verify --update-baseline grava as bases visuais e verifica tudo
[saída: 0]
Páginas e rotas
Seção intitulada “Páginas e rotas”| Caminho | Página |
|---|---|
/ | Início |
/sobre/ | Sobre |
/catalogo/ | Lista de todos os itens, com busca |
/catalogo/<categoria>/ | Uma página por categoria com pelo menos 2 itens |
/catalogo/<slug>/ | Uma página por item |
/contato/ | Contato |
/404 | Página de erro 404 |
Os caminhos terminam com barra, exceto /404. Uma categoria não pode ter o mesmo slug de um item, senão o build para e diz qual é a coleção e o slug.
Os itens ficam em src/data/collections/catalog.ts. Cada item tem:
slug: o endereço da página do item. Só letras minúsculas sem acento, números e hífens.title,summaryedescription: o nome, o texto do cartão e o parágrafo da página do item.seoDescription: a descrição da página para o Google.category: o nome que aparece no filtro. Não há lista separada de categorias.price: o preço em reais, como número. Semprice, o texto depriceNote(por exemplo, «Sob orçamento») aparece no lugar.highlights: as linhas de destaque da página do item.photos: pelo menos uma foto. A primeira vai para o cartão. Um item sem foto faz o build parar.kind:servicefaz a página e os dados estruturados de um serviço, em vez de um produto.
Dois arquivos de dado ajudam a ler a coleção:
origin.tsdefine os marcadores de origem do texto:claude(...)marca o que o Claude escreveu,fictitious(...)marca o que foi inventado para o exemplo epending(...)marca o que a agência ainda precisa fornecer.plain(...)tira as marcas para as páginas. Os valores marcados precisam de revisão antes de publicar, como em Multi-page clássico.site.tsdefinephoto, que monta uma imagem de placeholder (src, texto alternativo e tamanho). Os itens usamphotopor meio da função localshot, que acrescenta a legenda.
O exemplo abaixo é o arquivo completo da coleção, com os oito itens do projeto e as funções que montam os cartões. Como ler:
catalogItems(linhas 43 a 197) são os oito itens, no mesmo molde:slug,title,category,summary,description,seoDescription,priceoupriceNote,highlightsephotos. Só o serviçopaisagismo-de-varandatemkind: "service"epriceNoteno lugar deprice.catalog(linha 200) é a coleção: o caminho/catalogoe os itens.priceLabel(linha 208) escreve o preço em reais, ou opriceNotequando não há preço.cardFor(linha 213) monta o cartão de um item, com a primeira foto.cardsBySlugecatalogCards(linhas 225 e 228) guardam os cartões de todos os itens, na ordem do catálogo.cardOf(linha 231) devolve o cartão de um slug, para a home escolher itens à mão.
import type { CollectionItem } from "../../site/collection";import { defineCollection } from "../../site/collection";import type { UiImage } from "../../ui";import { claude, fictitious, plain } from "../origin";import { photo } from "../site";
/** One photo of an item: the carousel of its page shows them all, the first one is the one of the cards. */export interface CatalogPhoto { readonly image: UiImage; readonly caption?: string;}
/** One entry of the catalog: a card in the lists and a page of its own under /catalogo. */export interface CatalogItem extends CollectionItem { /** `product` by default; `service` makes the page and the JSON-LD a service. */ readonly kind?: "product" | "service"; /** Text of the card. */ readonly summary: string; /** Paragraph of the page. */ readonly description: string; /** Meta description of the page. */ readonly seoDescription: string; /** Price in reais (89.9 shows as "R$ 89,90" and feeds the Offer); no price, no Offer. */ readonly price?: number; /** What shows where the price would be, for an item with no price ("Sob orçamento"). */ readonly priceNote?: string; /** What to know before asking, one line each. */ readonly highlights: readonly string[]; readonly photos: readonly CatalogPhoto[];}
const shot = (alt: string, caption?: string): CatalogPhoto => ({ image: photo(alt, "photo", 640, 480), ...(caption === undefined ? {} : { caption }),});
/** * The items of the catalog, marked by origin for the content gate (`plain` strips the markers for the pages). The slugs * are the addresses of the pages: lowercase, no accents, and never equal to the slug of a category. Prices are * invented, so they are `fictitious`. Three categories: Vasos (3), Plantas (4) and Serviços (1, which is below * `categoryMin` and so has no page and no filter of its own). */export const catalogItems = [ { slug: "vaso-de-ceramica-esmaltado", title: claude("Vaso de cerâmica esmaltado"), category: claude("Vasos"), summary: claude("Cerâmica esmaltada por dentro, com furo de drenagem e prato combinando."), description: claude( "Um vaso médio para plantas de interior, feito à mão e esmaltado em tom verde-musgo. Acompanha o prato e serve para jiboia, zamioculca e samambaia.", ), seoDescription: claude( "Vaso de cerâmica esmaltado com prato, feito à mão, para plantas de interior. Peça pelo WhatsApp e retire na loja Verde Casa, em Curitiba.", ), price: fictitious(89), highlights: [ claude("Furo de drenagem e prato incluído"), claude("Cerâmica esmaltada por dentro e por fora"), fictitious("Altura de 24 cm e boca de 22 cm"), ], photos: [shot("Vaso de cerâmica verde-musgo sobre uma mesa de madeira", "Vista de frente"), shot("Vaso de cerâmica visto de cima, com terra e uma muda")], }, { slug: "cachepo-de-fibra-natural", title: claude("Cachepô de fibra natural"), category: claude("Vasos"), summary: claude("Trançado à mão, esconde o vaso de plástico e deixa a planta mais bonita."), description: claude( "Cachepô de fibra trançada para esconder o vaso de cultivo. Combina com ambientes claros e aguenta o peso de plantas médias.", ), seoDescription: claude( "Cachepô de fibra natural trançado à mão para esconder o vaso de cultivo. Escolha no catálogo da Verde Casa e peça pelo WhatsApp.", ), price: fictitious(59), highlights: [ claude("Trançado à mão em fibra natural"), claude("Fundo reforçado para plantas médias"), fictitious("Cabe um vaso de até 20 cm de boca"), ], photos: [shot("Cachepô de fibra trançada com uma planta verde dentro", "Com uma jiboia"), shot("Detalhe do trançado da fibra natural")], }, { slug: "jardineira-de-concreto", title: claude("Jardineira de concreto"), category: claude("Vasos"), summary: claude("Comprida e resistente, boa para ervas e temperos na janela ou na varanda."), description: claude( "Jardineira de concreto leve, própria para uma fileira de ervas, temperos ou suculentas. Resiste ao sol e à chuva.", ), seoDescription: claude( "Jardineira de concreto leve para ervas, temperos e suculentas, resistente ao sol e à chuva. Veja no catálogo e peça pelo WhatsApp.", ), price: fictitious(129), highlights: [ claude("Concreto leve, resistente ao sol e à chuva"), claude("Furos de drenagem no fundo"), fictitious("Comprimento de 60 cm"), ], photos: [shot("Jardineira de concreto com ervas na varanda", "Com ervas na varanda"), shot("Jardineira de concreto vazia, vista de lado")], }, { slug: "jiboia-no-vaso", title: claude("Jiboia no vaso"), category: claude("Plantas"), summary: claude("Folhagem pendente, fácil de cuidar e boa para quem está começando."), description: claude( "A jiboia cresce bem com pouca luz e aceita rega espaçada. Vai em vaso de cultivo, pronta para colocar no cachepô de sua preferência.", ), seoDescription: claude( "Jiboia no vaso, folhagem pendente fácil de cuidar, boa para quem está começando. Peça pelo WhatsApp e retire na loja Verde Casa.", ), price: fictitious(35), highlights: [ claude("Cresce bem com pouca luz"), claude("Rega espaçada, uma vez por semana"), fictitious("Entregue em vaso de cultivo de 14 cm"), ], photos: [shot("Jiboia pendente em um vaso sobre a estante", "Na estante"), shot("Folhas de jiboia em close")], }, { slug: "espada-de-sao-jorge", title: claude("Espada-de-são-jorge"), category: claude("Plantas"), summary: claude("Resistente e de folhas altas, aguenta esquecimento e ambientes claros."), description: claude( "Planta de folhas altas e rígidas, que aguenta bem dias sem rega. Boa para cantos da sala e entradas de casa.", ), seoDescription: claude( "Espada-de-são-jorge, planta resistente de folhas altas, boa para cantos da sala e entradas. Escolha no catálogo e peça pelo WhatsApp.", ), price: fictitious(45), highlights: [ claude("Aguenta bem dias sem rega"), claude("Boa para cantos da sala e entradas"), fictitious("Altura de cerca de 50 cm"), ], photos: [shot("Espada-de-são-jorge em um vaso no canto da sala", "No canto da sala"), shot("Folhas altas da espada-de-são-jorge")], }, { slug: "zamioculca", title: claude("Zamioculca"), category: claude("Plantas"), summary: claude("Folhas brilhantes e crescimento calmo, ideal para escritórios e salas."), description: claude( "A zamioculca pede pouca água e dispensa sol direto. É uma boa escolha para escritórios, salas e quartos.", ), seoDescription: claude( "Zamioculca de folhas brilhantes e crescimento calmo, ideal para escritórios e salas. Veja no catálogo e peça pelo WhatsApp.", ), price: fictitious(79), highlights: [ claude("Dispensa sol direto"), claude("Pede pouca água"), fictitious("Entregue em vaso de cultivo de 17 cm"), ], photos: [shot("Zamioculca em um vaso de cerâmica na sala", "Na sala"), shot("Folhas brilhantes da zamioculca")], }, { slug: "kit-de-suculentas", title: claude("Kit com cinco suculentas"), category: claude("Plantas"), summary: claude("Cinco mudas variadas em vasos pequenos, boas para a janela ou a mesa."), description: claude( "Um kit com mudas variadas de suculentas em vasos pequenos. Gostam de sol e de rega bem espaçada.", ), seoDescription: claude( "Kit de suculentas com mudas variadas em vasos pequenos, boas para a janela ou a mesa. Peça pelo WhatsApp e retire na loja.", ), price: fictitious(69), highlights: [ claude("Mudas variadas em vasos pequenos"), claude("Gostam de sol e de rega espaçada"), fictitious("Cinco vasos de 7 cm cada"), ], photos: [shot("Cinco suculentas em vasos pequenos sobre o parapeito", "No parapeito da janela"), shot("Suculenta em close, com folhas carnudas")], }, { slug: "paisagismo-de-varanda", kind: "service", title: claude("Projeto de paisagismo para varanda"), category: claude("Serviços"), summary: claude("Visitamos a varanda, medimos a luz e montamos a composição de plantas e vasos."), description: claude( "Uma visita à sua varanda para medir luz, espaço e vento, seguida de um projeto com as plantas e os vasos certos para o ambiente. O valor depende do tamanho da varanda.", ), seoDescription: claude( "Projeto de paisagismo para varanda: visita, medição da luz e composição de plantas e vasos. Peça um orçamento pelo WhatsApp.", ), priceNote: claude("Sob orçamento"), highlights: [ claude("Visita para medir luz, espaço e vento"), claude("Projeto com a lista de plantas e vasos"), fictitious("Plantio no local, se você quiser"), ], photos: [shot("Varanda com vasos e plantas organizados em duas alturas", "Varanda depois do projeto"), shot("Planta sendo posicionada em uma varanda")], },] as const;
/** The collection the routes and the pages read: lists, cards, categories and one page per item. */export const catalog = defineCollection<CatalogItem>({ basePath: "/catalogo", items: plain(catalogItems),});
const money = new Intl.NumberFormat("pt-BR", { style: "currency", currency: "BRL" });
/** The price as people read it: the number in reais with cents, else the note, else nothing. */export function priceLabel(item: Pick<CatalogItem, "price" | "priceNote">): string { return item.price === undefined ? (item.priceNote ?? "") : money.format(item.price);}
/** The card of an item: title, text, price as shown, first photo and page link. A photo is required: it fails naming the item. */export function cardFor(item: CatalogItem) { const first = item.photos[0]; if (first === undefined) throw new Error(`Catálogo: o item "${item.slug}" precisa de ao menos uma foto.`); return { title: item.title, text: item.summary, price: priceLabel(item), media: { kind: "image" as const, image: first.image }, href: catalog.href(item), };}
const cardsBySlug = new Map<string, ReturnType<typeof cardFor>>(plain(catalogItems).map((item) => [item.slug, cardFor(item)]));
/** The cards of the lists (home, categories, related items), in catalog order. */export const catalogCards = [...cardsBySlug.values()];
/** The card of one item, for the lists that pick items by hand (the home); an unknown slug fails naming it. */export function cardOf(slug: string): (typeof catalogCards)[number] { const card = cardsBySlug.get(slug); if (card === undefined) throw new Error(`Catálogo: nenhum cartão para o slug "${slug}".`); return card;}Para adicionar um item, copie um dos existentes em catalogItems e troque os campos. A página, o cartão, o filtro e a entrada no sitemap saem sozinhos. Os quatro itens em destaque na home ficam em FEATURED_SLUGS, em src/data/pages/home.ts.
Categorias
Seção intitulada “Categorias”Cada categoria vira uma página e um filtro quando tem pelo menos dois itens. Esse mínimo é o categoryMin de defineCollection, cujo padrão é 2. Uma categoria abaixo do mínimo não tem página nem filtro: os itens dela continuam na lista e na busca.
No projeto de exemplo, «Vasos» (3 itens) e «Plantas» (4 itens) têm página. «Serviços», com um item só, não tem.
Nomes que geram o mesmo endereço, como «Estética» e «estetica», fazem o build falhar.
O campo «Buscar» e o botão «Mostrar mais» só aparecem com JavaScript: o HTML os traz escondidos (hidden, ui/sections/search/search.ts:92 e :96). A busca olha o nome, o texto do cartão e a categoria, sem diferença de maiúsculas e sem acento. Com JavaScript, o botão revela os itens 12 a 12, e quando nada bate aparece «Nenhum item encontrado».
Sem JavaScript, a lista mostra todos os itens (ui/sections/search/search.ts:95), e os filtros de categoria são links para as páginas das categorias (ui/sections/search/search.ts:75-76). Os dois funcionam sem JavaScript.
O site escreve o preço sozinho, em reais, no cartão e na página do item: 89 aparece como «R$ 89,00». Um item sem price mostra só o texto de priceNote.
Nos dados estruturados, um item com preço vira um Product com Offer em reais. Um serviço vira Service, oferecido pelo negócio do site. Sem preço, não há Offer. O site não conhece o estoque, então o Offer não informa disponibilidade: confirme a disponibilidade pelo WhatsApp.
Mensagem do WhatsApp
Seção intitulada “Mensagem do WhatsApp”O botão de WhatsApp da página do item abre a conversa com «Olá! Tenho interesse em» e o nome do item.
Para o layout das páginas de texto, veja Multi-page clássico. Para a página de uma só rolagem, veja One-page.