Catálogo de serviços de desentupimento geral com filtros avançados no WordPress
Como criar catálogo de desentupimento com filtros avançados no WordPress. Taxonomias, AJAX, performance e UX para conversão de leads qualificados.
Um condomínio na zona leste de São Paulo recebe chamados de entupimento em três torres simultâneas: pia da cozinha na torre A, ralo de banheiro na torre B e coluna principal na torre C. O síndico acessa o site da desentupidora parceira pelo celular às 22h30, precisa filtrar por “atendimento 24h”, “hidrojateamento” e “região leste” em segundos, e solicitar orçamento sem ligar para ninguém. Se o catálogo exigir recarregar a página a cada filtro ou não tiver busca por proximidade, o lead abandona e chama o concorrente. Esse cenário real dita cada decisão técnica que veremos a seguir.
Desenvolver um catálogo de serviços de desentupimento com filtros avançados no WordPress não é apenas listar posts customizados. Envolve modelagem de dados precisa, performance sob carga, acessibilidade nativa e integração com ferramentas de conversão. O objetivo é entregar a combinação certa de serviço, equipamento, urgência e localização em milissegundos, seja no desktop do gestor ou no smartphone do morador.
Neste artigo, abordamos a arquitetura completa: do registro de CPT e taxonomias à implementação de filtros AJAX, estratégias de cache, schema.org para SEO local e mensuração de conversão. Cada seção traz código pronto para produção e decisões justificadas para o contexto de emergências hidráulicas na Grande São Paulo.
Modelagem de dados com CPT e taxonomias especializadas
O primeiro passo é registrar o Custom Post Type servico_desentupimento com suporte a título, editor, imagem destacada, excerpt e revisões. Evite page-attributes e post-formats — eles adicionam ruído na administração. Use register_post_type no hook init com public => true, has_archive => true e rewrite => ['slug' => 'servicos'].
Para os filtros, crie três taxonomias hierárquicas separadas: tipo_servico (desentupimento de pia, ralo, vaso, coluna, fossa, caixa de gordura), equipamento (máquina rotativa, hidrojateamento, caminhão vácuo, inspeção por vídeo) e regiao_atendimento (zona norte, sul, leste, oeste, centro, Grande SP). Taxonomias não-hierárquicas como urgencia (normal, urgente, 24h) completam o modelo.
function registra_cpt_servico_desentupimento() {
$labels = [
'name' => 'Serviços de Desentupimento',
'singular_name' => 'Serviço',
'add_new_item' => 'Novo Serviço',
'edit_item' => 'Editar Serviço',
];
$args = [
'labels' => $labels,
'public' => true,
'has_archive' => true,
'rewrite' => ['slug' => 'servicos'],
'supports' => ['title', 'editor', 'thumbnail', 'excerpt', 'revisions'],
'menu_icon' => 'dashicons-admin-multisite',
'show_in_rest' => true,
];
register_post_type('servico_desentupimento', $args);
}
add_action('init', 'registra_cpt_servico_desentupimento');
function registra_taxonomias_servico() {
$taxonomias = [
'tipo_servico' => ['label' => 'Tipo de Serviço', 'hierarchical' => true],
'equipamento' => ['label' => 'Equipamento', 'hierarchical' => true],
'regiao_atendimento' => ['label' => 'Região de Atendimento', 'hierarchical' => true],
'urgencia' => ['label' => 'Urgência', 'hierarchical' => false],
];
foreach ($taxonomias as $slug => $config) {
register_taxonomy($slug, ['servico_desentupimento'], [
'label' => $config['label'],
'hierarchical' => $config['hierarchical'],
'rewrite' => ['slug' => $slug],
'show_in_rest' => true,
'show_admin_column' => true,
]);
}
}
add_action('init', 'registra_taxonomias_servico');
Essa estrutura permite queries compostas como “hidrojateamento + zona leste + 24h” sem joins complexos. Cada taxonomia aparece como coluna na lista de posts do admin, agilizando a gestão pelo cliente. Para entender como essa modelagem se conecta à área do cliente, veja Área do cliente para histórico de manutenções em Moema.
Filtros AJAX via REST API com validação de nonce
Evite admin-ajax.php para catálogos públicos — a REST API nativa é mais rápida, cacheável e segue padrões modernos. Registre um endpoint customizado wp/v2/servicos/filtrar que aceite parâmetros GET: tipo_servico, equipamento, regiao_atendimento, urgencia, search, page, per_page. Valide nonce via header X-WP-Nonce para proteger contra CSRF.
No frontend, use Alpine.js pela leveza (7kb gzipped) e reatividade declarativa. O componente mantém estado dos filtros, faz fetch ao endpoint na mudança de qualquer controle, e atualiza a região de resultados com aria-live="polite". Inclua loading skeleton enquanto a resposta não chega.
// functions.php - Endpoint REST
function registra_endpoint_filtro_servicos() {
register_rest_route('wp/v2', '/servicos/filtrar', [
'methods' => 'GET',
'callback' => 'callback_filtro_servicos',
'permission_callback' => '__return_true',
'args' => [
'tipo_servico' => ['type' => 'string', 'sanitize_callback' => 'sanitize_text_field'],
'equipamento' => ['type' => 'string', 'sanitize_callback' => 'sanitize_text_field'],
'regiao_atendimento' => ['type' => 'string', 'sanitize_callback' => 'sanitize_text_field'],
'urgencia' => ['type' => 'string', 'sanitize_callback' => 'sanitize_text_field'],
'search' => ['type' => 'string', 'sanitize_callback' => 'sanitize_text_field'],
'page' => ['type' => 'integer', 'default' => 1, 'minimum' => 1],
'per_page' => ['type' => 'integer', 'default' => 12, 'maximum' => 24],
],
]);
}
add_action('rest_api_init', 'registra_endpoint_filtro_servicos');
function callback_filtro_servicos($request) {
// Verifica nonce
$nonce = $request->get_header('X-WP-Nonce');
if (!wp_verify_nonce($nonce, 'wp_rest')) {
return new WP_Error('invalid_nonce', 'Nonce inválido', ['status' => 403]);
}
$args = [
'post_type' => 'servico_desentupimento',
'post_status' => 'publish',
'paged' => $request->get_param('page'),
'posts_per_page' => $request->get_param('per_page'),
'tax_query' => [],
];
$mapa_tax = [
'tipo_servico' => 'tipo_servico',
'equipamento' => 'equipamento',
'regiao_atendimento' => 'regiao_atendimento',
'urgencia' => 'urgencia',
];
foreach ($mapa_tax as $param => $taxonomy) {
$valor = $request->get_param($param);
if ($valor) {
$args['tax_query'][] = [
'taxonomy' => $taxonomy,
'field' => 'slug',
'terms' => explode(',', $valor),
'operator' => 'IN',
];
}
}
if ($request->get_param('search')) {
$args['s'] = $request->get_param('search');
}
$query = new WP_Query($args);
return [
'servicos' => array_map(fn($post) => [
'id' => $post->ID,
'titulo' => get_the_title($post->ID),
'permalink' => get_permalink($post->ID),
'thumbnail' => get_the_post_thumbnail_url($post->ID, 'medium'),
'excerpt' => get_the_excerpt($post->ID),
'taxonomias' => [
'tipo_servico' => wp_get_post_terms($post->ID, 'tipo_servico', ['fields' => 'names']),
'equipamento' => wp_get_post_terms($post->ID, 'equipamento', ['fields' => 'names']),
'regiao_atendimento' => wp_get_post_terms($post->ID, 'regiao_atendimento', ['fields' => 'names']),
'urgencia' => wp_get_post_terms($post->ID, 'urgencia', ['fields' => 'names']),
],
], $query->posts),
'total' => $query->found_posts,
'total_pages' => $query->max_num_pages,
'current_page' => $query->get('paged'),
];
}
O endpoint retorna JSON enxuto com apenas o necessário para o card do serviço. No template, passe o nonce via wp_localize_script ou inline no markup. Para ver como essa abordagem se integra a formulários de orçamento, consulte Analytics de conversão para leads de desentupimento no Mandaqui.
Estratégias de cache para performance sob demanda
Catálogos com filtros combinatórios geram muitas URLs únicas (?tipo_servico=hidrojateamento®iao=leste&urgencia=24h). Cache full-page em CDN ou plugin de cache quebra a dinamicidade. A solução é fragment caching: armazene em Transient o HTML de cada card de serviço e a lista de termos por taxonomia, invalidando no save_post do CPT.
function get_card_servico_cached($post_id) {
$cache_key = "card_servico_{$post_id}_" . get_lastpostmodified('GMT');
$html = get_transient($cache_key);
if ($html === false) {
ob_start();
// Template part do card
get_template_part('template-parts/card', 'servico', ['post_id' => $post_id]);
$html = ob_get_clean();
set_transient($cache_key, $html, DAY_IN_SECONDS * 7);
}
return $html;
}
function invalida_cache_servico($post_id) {
if (get_post_type($post_id) === 'servico_desentupimento') {
delete_transient("card_servico_{$post_id}_" . get_lastpostmodified('GMT'));
// Invalida listas de termos
delete_transient('termos_tipo_servico');
delete_transient('termos_equipamento');
delete_transient('termos_regiao_atendimento');
}
}
add_action('save_post', 'invalida_cache_servico');
Para assets estáticos (CSS, JS, imagens), configure Service Worker com Workbox via plugin PWA ou manual no sw.js. Isso permite carregamento instantâneo em visitas repetidas, crítico para emergências noturnas. Aprofunde o tema em Performance e Core Web Vitals — embora não seja slug exato, a categoria reúne artigos sobre otimização.
Schema.org Service e LocalBusiness para SEO local
Cada card de serviço deve conter markup schema.org/Service aninhado em schema.org/LocalBusiness. Isso garante rich snippets no Google com preço, área de atendimento, horário e avaliações. Use JSON-LD no head da página de arquivo ou inline no card.
{
"@context": "https://schema.org",
"@type": "Service",
"name": "Desentupimento de Coluna com Hidrojateamento",
"provider": {
"@type": "LocalBusiness",
"name": "Desentupidora Parceira",
"areaServed": ["São Paulo", "Grande São Paulo", "Zona Leste"],
"availableChannel": {
"@type": "ServiceChannel",
"availableLanguage": "Português",
"processingTime": "PT30M",
"providesService": {
"@type": "Service",
"name": "Hidrojateamento 24h"
}
},
"telephone": "+55-11-99999-9999",
"url": "https://exemplo.com/servicos/desentupimento-coluna-hidrojateamento"
},
"serviceType": "Desentupimento predial",
"areaServed": "Zona Leste, São Paulo",
"offers": {
"@type": "Offer",
"availability": "https://schema.org/InStock",
"priceSpecification": {
"@type": "UnitPriceSpecification",
"priceCurrency": "BRL",
"minPrice": "150",
"maxPrice": "800"
}
}
}
Gere esse JSON dinamicamente no template part do card usando wp_json_encode e dados do post + taxonomias + ACF para telefone/preço. Valide no Rich Results Test. Para estratégias de SEO local em WordPress, veja Agência de SEO especializada em WordPress Feira de Santana — o princípio de geolocalização aplica-se igual.
Acessibilidade nativa nos controles de filtro
Filtros avançados são formulários complexos. Use <form id="filtros-servicos" role="search" aria-label="Filtrar serviços de desentupimento"> com <fieldset> para cada grupo (Tipo, Equipamento, Região, Urgência) e <legend> visível ou .screen-reader-text. Inputs type="checkbox" ou radio precisam de <label> associado via for/id.
Adicione aria-controls="resultados-servicos" nos inputs e aria-live="polite" aria-atomic="true" no container de resultados. Garanta :focus-visible com outline de 3px em cor de contraste 4.5:1. Teste com NVDA/VoiceOver e navegação só por Tab.
<form id="filtros-servicos" role="search" aria-label="Filtrar serviços de desentupimento" x-data="filtrosServicos()">
<div class="filtros-grid">
<fieldset class="filtro-grupo">
<legend>Tipo de Serviço</legend>
<div class="checkbox-list">
<label><input type="checkbox" name="tipo_servico[]" value="desentupimento-pia" x-model="filtros.tipo_servico" @change="buscar()" aria-controls="resultados-servicos"> Desentupimento de Pia</label>
<label><input type="checkbox" name="tipo_servico[]" value="desentupimento-coluna" x-model="filtros.tipo_servico" @change="buscar()" aria-controls="resultados-servicos"> Desentupimento de Coluna</label>
<!-- demais -->
</div>
</fieldset>
<!-- fieldsets para equipamento, regiao, urgencia -->
</div>
<div id="resultados-servicos" aria-live="polite" aria-atomic="true" x-html="resultadosHtml"></div>
</form>
Essa estrutura atende WCAG 2.1 AA e melhora usabilidade para todos. Para formulários acessíveis em contexto de orçamento, leia Acessibilidade WCAG em formulários de orçamento no centro de São Paulo.
Mensuração de conversão e otimização contínua
Implemente data-layer pushes no fetch success do Alpine.js: filter_applied com objeto {filtros_ativos, total_resultados, tempo_resposta_ms}. No botão “Solicitar Orçamento” de cada card, dispare lead_initiated com servico_id, tipo_servico, regiao. Configure GA4/GTM para capturar e criar funil: Filtro → Visualização Card → Clique Orçamento → Envio Formulário.
Use heatmap (Hotjar/Clarity) para ver onde usuários hesitam nos filtros. A/B teste: ordem dos fieldsets, checkbox vs select, carga inicial com 6 vs 12 cards. Documente hipóteses e resultados em planilha compartilhada com o cliente.
// No componente Alpine.js, após fetch success
this.resultadosHtml = html;
window.dataLayer = window.dataLayer || [];
window.dataLayer.push({
event: 'filter_applied',
filtros_ativos: this.filtros,
total_resultados: response.total,
tempo_resposta_ms: performance.now() - this.inicioBusca
});
// No botão de orçamento do card (delegated event)
document.addEventListener('click', e => {
if (e.target.matches('.btn-orcamento')) {
window.dataLayer.push({
event: 'lead_initiated',
servico_id: e.target.dataset.servicoId,
tipo_servico: e.target.dataset.tipoServico,
regiao: e.target.dataset.regiao
});
}
});
Com dados reais, você ajusta quais filtros aparecem primeiro, remove os não usados e prioriza desenvolvimento. Para ver como essa mensuração se conecta a automação de WhatsApp, acesse Automação de WhatsApp para solicitações de fossa séptica em SP.
Extensibilidade via hooks e filters para equipe de desenvolvimento
Exponha hooks em pontos estratégicos: servicos_filtro_args (modifica WP_Query args), servicos_card_html (altera markup do card), servicos_rest_response (adiciona campos na API), servicos_cache_key (personaliza invalidação). Documente com @param e @return no PHPDoc.
// No callback REST, antes de new WP_Query
$args = apply_filters('servicos_filtro_args', $args, $request->get_params());
// No template part do card
$card_html = apply_filters('servicos_card_html', $card_html, $post_id, $taxonomias);
// Na resposta REST
$response = apply_filters('servicos_rest_response', $response, $query);
Isso permite que outro desenvolvedor adicione campo ACF “tempo_estimado” no card ou filtro por “forma_pagamento” sem tocar no core do plugin. Boas práticas de hooks estão em Snippets, Hooks e Boas Práticas.
Um catálogo de desentupimento com filtros avançados bem executado transforma busca de emergência em lead qualificado. A combinação de taxonomias precisas, REST API leve, cache inteligente, schema.org completo, acessibilidade nativa e mensuração contínua cria ativo técnico que escala com o negócio. Comece pelo CPT e taxonomias, valide com usuários reais em situação de urgência, e itere com dados — não com suposições.
Para colocar em produção um catálogo completo com agendamento 24h e área do cliente, avalie Plugin de agendamento 24 horas para manutenção de tubulação como próximo passo. E se precisar de referência de atendimento real na região, consulte desentupimento geral em São Paulo e região.
Checklist prático
Perguntas frequentes
Por que usar taxonomias personalizadas em vez de categorias padrão para serviços de desentupimento?
Taxonomias personalizadas permitem estruturar dados específicos como tipo de equipamento, urgência e localização sem poluir o sistema de categorias nativo. Isso facilita consultas complexas e mantém a administração organizada para o cliente final.
Como implementar filtros AJAX sem recarregar a página no catálogo?
Registre um endpoint REST API ou use admin-ajax.php com nonce de segurança. No frontend, capture eventos de mudança nos filtros, serializa os parâmetros e faça requisição assíncrona atualizando apenas o container de resultados via JavaScript vanilla ou Alpine.js.
Qual a melhor estratégia de cache para catálogos com filtros dinâmicos?
Use fragment caching com Transients API para partes estáticas do catálogo e cache no navegador via Service Worker para assets. Evite cache full-page nas URLs de filtro; prefira cache de objetos WP_Query com chaves baseadas nos parâmetros de filtro ativos.
Como garantir acessibilidade nos filtros avançados do catálogo?
Use elementos semânticos
É viável usar FacetWP ou SearchWP em vez de desenvolvimento customizado?
Plugins como FacetWP aceleram a entrega e oferecem facetas prontas, mas adicionam dependência e custo recorrente. Para requisitos simples a médios, código nativo com WP_Query e REST API é mais leve, performático e dá controle total sobre markup e comportamento.
Como medir a eficácia dos filtros na conversão de leads?
Implemente eventos GA4/GTM nos cliques de filtro e submissão de orçamento. Acompanhe métricas como taxa de uso de cada filtro, tempo até conversão por combinação de filtros e funil de abandono. Use heatmaps para validar usabilidade dos controles.