feat(seo): add per-page title, canonical, sitemap and RSS feed - #10
Open
Guajir0-code wants to merge 3 commits into
Open
feat(seo): add per-page title, canonical, sitemap and RSS feed#10Guajir0-code wants to merge 3 commits into
Guajir0-code wants to merge 3 commits into
Conversation
The app points Eloquent at an existing WordPress database, so no migration creates wp_posts, wp_users, wp_terms and friends. Any test that touched the database therefore could not run, and the single feature test in the repo (GET / asserting 200) failed on a missing database. Add a schema builder for the wp_* tables the app reads, small row builders, and smoke coverage for all seven routes in routes/web.php, including the highlighted-post exclusion, search, and the published/scheduled filtering the global scope is responsible for. withoutVite() keeps the suite independent of `npm run build`. ExampleTest is dropped: RoutesTest covers GET / properly. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Three separate per-record queries were issued while rendering any listing:
* terms was never eager loaded anywhere, yet WpPostResource::getCategories()
reads it for every post
* getThumbnail() called $this->metadata() (the relation method, bypassing the
eager loaded collection) and then looked up the attachment guid, costing two
queries per post
* author was missing from the home, highlights and related-posts queries
WpPostResource also chose its shape from $request->routeIs('post'), so the
related posts rendered on a post page received the full detail treatment,
including running EmbedProcessorService over content the "Veja tambem" cards
never display. Replaced with an explicit ->detailed() opt-in from the
controller.
Featured images are now a HasOneThrough relation via WpAttachment, a model on
wp_posts without WpPostScope, so they can be eager loaded in one query.
preventLazyLoading is enabled outside production so this cannot silently
regress.
Measured with 10 posts:
home 45 -> 7 queries
category 36 -> 6 queries
post 21 -> 11 queries
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Every page of the site served the same <title>: app.blade.php emitted
config('app.name') and no page component ever set one. The og:title was already
correct, because seo.blade.php computes a proper title for all seven
components — it just was not being used for the title tag.
Moving <title> into that partial fixes it in two lines and, unlike setting it
from the Vue pages, works whether or not the SSR process is running.
Also in this change:
* canonical url, without the query string, so ?page= and ?s= variants do not
compete with the page itself
* noindex on search result pages
* Article JSON-LD on post pages: headline, image, publish date, author and
publisher
* og:description falls back to the body when post_excerpt is empty, which is
the common case in WordPress
* /sitemap.xml covering posts, categories, tags and the privacy page
* /feed, at the address WordPress served, so existing subscribers keep working
* robots.txt points at the sitemap
* pagination renders real hrefs, so crawlers can reach page 2 and beyond
The pagination links also carry the current query string. Navigating with only
{ page } dropped ?s=, which silently turned page 2 of a search into the plain
home listing.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problema
Todas as páginas do site servem o mesmo
<title>. Não há canonical, não há dados estruturados, não há sitemap, não há feed RSS, e a paginação não é rastreável.Evidência
Verificado em
https://beta.sourcevortex.com.br/no dia da abertura deste PR:Idênticos. Mas o
og:titleda mesma página de post está correto:Ou seja: o título certo já é calculado, só não estava sendo usado na tag
<title>.Também confirmado na mesma verificação: nenhum
rel="canonical"e nenhumapplication/ld+jsonem nenhuma página.Causa
app.blade.phpemitia um título fixo:O atributo
inertiaexiste para que o Inertia substitua a tag no cliente — mas nenhuma página Vue usa<Head><title>. O único<Head>do projeto, emMainTemplate.vue, só injeta um<link>de fonte.Enquanto isso,
seo.blade.phpjá computava$ogTitlecorretamente para os sete componentes.Solução
Título: mover a tag para onde o valor já existe
Duas linhas. E, por rodar no Blade e não no Vue, funciona com ou sem SSR — o que importa, já que o processo de SSR não é garantido neste projeto.
Canonical
Sem query string, para que as variantes
?page=e?s=não concorram com a página em si.noindexem resultados de buscaPáginas de busca não têm o que fazer num índice, e o site aceita qualquer
?s=. Elas passam a emitirnoindex, follow.Dados estruturados
JSON-LD do tipo
Articlenas páginas de post: headline, imagem, data de publicação, autor com link e publisher. É o que permite ao buscador exibir o resultado como rich result em vez de link simples. Não é emitido em listagens.og:descriptioncom fallbackpost_excerptcostuma vir vazio no WordPress, e o site herdou esse dado. Quando vazio, a descrição passa a ser extraída do corpo do post, limitada a 160 caracteres./sitemap.xmle/feedNovo
FeedController, com as views correspondentes. O sitemap cobre posts, categorias, tags e a página de política; o feed traz os 20 posts mais recentes.O feed fica em
/feed, o mesmo endereço que o WordPress servia — pode haver assinantes ativos herdados do site anterior.Ambos são cacheados (6h e 1h) e declarados antes das rotas de arquivo, para não serem capturados por elas.
robots.txtpassa a apontar o sitemap.Paginação rastreável
PostCardListPagination.vuenavegava porrouter.get()em resposta a clique, sem nenhum<a href>. Nenhum rastreador alcançava a página 2.Os itens passam a renderizar
hrefreal, mantendo a navegação via Inertia no clique.Isso corrige de quebra um bug de comportamento: a navegação anterior usava
window.location.pathnamee montava a query do zero, descartando o?s=. Numa busca, clicar na página 2 levava para/?page=2e devolvia a listagem normal da home como se fossem resultados. No servidor,->withQueryString()completa a correção.Como validar
A cobertura inclui: títulos distintos entre home, post e categoria; exatamente uma tag
<title>por página; canonical sem query string;noindexapenas em busca; JSON-LD com os campos corretos e ausente em listagens; sitemap contendo post, categoria e tag; feed no endereço certo com o content-type certo; XML bem formado nos dois, validado comsimplexml_load_string; e o fallback de descrição.Verificado nos dois sentidos: contra o código anterior, 4 falham — título, canonical, noindex e JSON-LD.
Impacto
<head>, XML novo e oshrefda paginação./sitemap.xmle/feed.url(), então respeitaAPP_URL.Uma verificação que precisa ser feita com dados reais
O
robots.txtreferenciahttps://sourcevortex.com.br/sitemap.xml. Se o V2 for ao ar em outro domínio, esse endereço precisa acompanhar.Vale também conferir uma amostra de URLs já indexadas antes do corte de V1 para V2. As rotas atuais (
/{year}/{month}/{slug},/category/,/tag/,/author/) coincidem com o permalink padrão do WordPress, e o.htaccessjá faz 301 removendo a barra final — mas isso merece confirmação empírica, porque é o único risco irreversível desta série.Fora de escopo
rel="prev"/rel="next"nas páginas de listagem.