Vector Pola — loja de pisos
Loja virtual de pisos em operação — laminado, SPC e vinílico rígido, tábua de engenharia e tábua de parquê — para um varejista com dois showrooms, em Moscou e Krasnogorsk. O catálogo tem cerca de 7 600 produtos ativos: 3 300 itens de laminado, 3 900 de SPC e vinílico rígido, além de tábua de engenharia e de parquê. A vitrine é escrita em PHP puro, sem CMS nem frameworks, roda em hospedagem compartilhada barata e não exige nenhuma etapa de build. A decisão de engenharia central é um armazenamento em duas camadas: um JSON canônico de 17,5 MB e um cache SQLite derivado, de onde a vitrine realmente lê. Os pedidos chegam como solicitações no Telegram, e não como pagamento online — é assim que funciona o processo de vendas do cliente.
O que era precisoO cliente tem duas lojas físicas e um catálogo de 7.600 itens. Era preciso colocá-lo online sem que o site caísse sob o próprio peso nem exigisse um servidor caro.
O que ficou prontoA loja roda em hospedagem comum e barata, os produtos são atualizados por arquivo em vez de na mão, e não há mensalidade de plataforma.
Arquitetura de dados
A ideia central: products.json é a fonte da verdade e catalog.sqlite é um read-cache derivado. A vitrine não decodifica mais 17,5 MB de JSON a cada requisição — ela consulta um banco pronto via PDO.
- O painel escreve no JSON e o banco é reconstruído uma única vez por requisição, via register_shutdown_function — essencial para o importador, que salva produtos em laço e, de outro modo, dispararia uma reconstrução por linha.
- A reconstrução é atômica: escreve em catalog.sqlite.tmp e troca o arquivo com rename. Em caso de erro, o temporário é removido e o banco antigo continua vivo — nenhuma requisição pega um banco escrito pela metade.
- Duas tabelas: products (22 colunas, flags active/in_stock/popular/promo e a coluna search_text = lower(name + brand) para busca sem distinção de caixa em cirílico) e product_facets, com os valores de filtro expandidos por produto. Cinco índices em produtos, três em facetas.
- Resistência à dessincronização de schema: as consultas às colunas novas ficam em try/catch. Se o PHP subir antes da reconstrução do banco, nem a vitrine nem o painel caem em 500 — degradam de forma suave, com bloco vazio ou fallback para o JSON.
Catálogo e filtros
Cerca de 7 600 produtos ativos em 4 das 9 categorias; as demais mostram o estado “seção em preenchimento” em vez de uma página vazia.
- Filtros facetados configurados a partir das especificações reais de cada categoria: marca, classe de abrasão, espessura, tipo de instalação, espécie de madeira e acabamento. A lógica é OU dentro da faceta e E entre facetas, com contador de produtos em cada valor.
- Faixa de preço que sugere os limites reais da categoria, filtro “somente em estoque”, ordenação por preço e paginação no servidor de 24 produtos por página.
- Os filtros têm auto-submit: no desktop, na hora do clique; na gaveta mobile, pelo botão “Mostrar”; no preço, pelo Enter.
- Busca por nome e marca com consultas de múltiplas palavras: cada palavra precisa aparecer no search_text normalizado.
- Os dados do catálogo estão normalizados: sem preços zerados, sem slugs duplicados, marcas preenchidas e chaves de especificação canonizadas.
Página do produto e pedido
O piso tem uma peculiaridade própria: vende-se por pacote, mas o comprador pensa em metros quadrados.
- Calculadora de área: você informa os metros quadrados e ela calcula a quantidade de pacotes (arredondando para cima), a área efetiva e o total final. O primeiro cálculo é renderizado no servidor, então a página continua fazendo sentido sem JavaScript.
- Galeria com troca de fotos, especificações em tabela, botões “Ao carrinho” e “Comprar em um clique”, links de compartilhamento no WhatsApp e Telegram e cópia do endereço.
- Carrinho no cliente (localStorage) que recalcula os pacotes de cada linha; o checkout envia uma solicitação, não um pagamento — a composição do pedido e o total vão para o Telegram em texto.
- Placeholder de imagem em duas camadas: images vazio cai no fallback do servidor, link quebrado cai no onerror do cliente.
Solicitações e notificações
- Todos os formulários — consultoria, compra em um clique, carrinho, entrega e parceria com designers — passam por um único handler até um bot do Telegram, marcados com a origem da solicitação.
- Timeouts separados para a conexão e para a operação inteira (5 s e 15 s). Se a API falhar, a solicitação é registrada no arquivo fechado data/leads-failed.log — o lead sobrevive mesmo com o Telegram fora do ar.
- A assinatura de promoções é gravada em JSON sob flock, com deduplicação por e-mail: reenviar atualiza nome, telefone e data em vez de criar outra linha.
- No cliente, uma única inicialização de formulários lê o endpoint e a mensagem de sucesso de data-attributes; a máscara de telefone e a validação são reaproveitadas por todos os formulários.
Painel administrativo próprio
Totalmente feito à mão, atrás de login com sessões PHP.
- Catálogo: CRUD de produtos, busca e paginação no servidor e abas “Lista”, “Populares” e “Em promoção” — as marcações no produto alimentam os carrosséis da home, e uma seção vazia simplesmente não é renderizada.
- Importação e exportação XLSX com leitor e escritor próprios, sobre ZipArchive e XML manual — sem Composer e sem bibliotecas externas. A exportação transforma cada chave de especificação em uma coluna, o modelo vem com uma linha de exemplo e as colunas fixas e adicionais têm cores diferentes.
- A importação faz upsert: primeiro por slug, depois por SKU; corrige SKUs do tipo “1.0” que o Excel e o Google Sheets geram a partir de inteiros e consegue baixar fotos por URL.
- Upload de imagens: crop centralizado para um quadrado de 800×800 e conversão para WebP no servidor (GD), aceitando arquivo e URL, com verificação de MIME.
- Assinaturas: lista com os mais recentes no topo, exclusão por POST com confirmação e redirect PRG, e exportação em CSV (com BOM para o Excel) e XLSX.
- Proteção do login: captcha aritmético, bloqueio de 5 minutos após 5 tentativas erradas e recuperação de senha por Telegram e e-mail. A reconstrução manual do banco fica em uma página própria.
SEO
- sitemap.xml dinâmico direto do SQLite: páginas estáticas, apenas as categorias não vazias e todos os produtos ativos com lastmod vindo da data de atualização — hoje, cerca de 7 600 URLs.
- Schema.org / JSON-LD: Product + Offer com priceValidUntil, itemCondition e disponibilidade dinâmica nas páginas de produto, e BreadcrumbList nos produtos, nas categorias e no índice do catálogo.
- Controle consciente da indexação: a paginação limpa é indexável, com self-canonical e rel prev/next, enquanto as combinações de filtros e ordenação vão para noindex,follow, para não gerar duplicatas.
- Title e description de SEO são definidos por produto no painel, com fallback para um modelo montado a partir do nome. URLs legíveis /catalog/{categoria}/{slug}/ via mod_rewrite, com transliteração do cirílico para o slug e unicidade garantida.
- A busca interna e o carrinho estão fechados para indexação, e o robots.txt e o sitemap estão de acordo entre si.
Performance e hospedagem
A velocidade vem da arquitetura de armazenamento, e não da infraestrutura: sem CDN, sem camada de cache, sem daemons em segundo plano — hospedagem compartilhada barata na Beget e deploy por rsync.
- Abandonar o parse de 17,5 MB de JSON em cada requisição em favor de consultas SQL indexadas é o maior ganho no tempo de resposta.
- Todas as ~7 800 fotos de produto foram padronizadas em WebP 800×800 e os logotipos das marcas em WebP 320×180. O pipeline de preparação são scripts locais em Python/PIL que cortam as bordas brancas e limitam o upscale.
- Preload e fetchpriority na imagem do hero (LCP), lazy-load nas demais e width/height explícitos — zero deslocamento de layout.
- Gzip e cabeçalhos de cache para estáticos no .htaccess, com cache-busting dos assets por ?v=N.
- Analytics: Yandex.Metrica com webvisor e clickmap.
Segurança e robustez
- A pasta /data/, com o JSON dos produtos e a base de inscritos, está fechada tanto no .htaccess (uma RewriteRule com [F]) quanto no robots.txt; o config.php com o token do bot tem o acesso direto negado.
- Em /uploads/ a execução de PHP está desativada e só imagens são servidas; os arquivos internos do painel são bloqueados por FilesMatch e cada um ainda verifica se não foi chamado diretamente.
- 404 com sentido para produtos e categorias inexistentes: a checagem cobre não só o slug, mas também se a categoria da URL corresponde à categoria do próprio produto.
Por que o projeto é interessante
- Um catálogo de 7 600 produtos rodando em hospedagem compartilhada, sem CMS, sem MySQL, sem npm e sem Composer — graças a um armazenamento bem pensado, e não a capacidade de máquina.
- JSON como fonte da verdade e SQLite como read-cache derivado: escritas raras, leituras frequentes — um trade-off que encaixou melhor neste projeto do que um SGBD completo.
- Os detalhes de engenharia que só aparecem em produção: reconstrução atômica do banco, uma reconstrução por requisição em vez de milhares durante uma importação, degradação suave na dessincronização de schema e log de fallback para solicitações perdidas.
- Leitor e escritor de XLSX próprios em vez de uma biblioteca pesada — o cliente mantém o mix de produtos no Excel, e isso funciona sem nenhuma dependência externa.
- Ciclo completo por uma única pessoa: arquitetura de dados → back-end → front-end → painel → pipeline de imagens → SEO → deploy.