Por que meu sitemap listava 23 URLs duas vezes: um regex que esperava exatamente dois espaços
Meu nome é Ayumu Kitabayashi e eu desenvolvo o minulo, um app para iPhone. O site do app, minulo.app, é um site estático em 13 idiomas hospedado no GitHub Pages. Toda vez que eu adiciono a versão de uma página em outro idioma, um pequeno script em Python, o tools/sync_langs.py, reescreve as tags hreflang e as entradas do sitemap dessa página.
Em 2026-10-25 eu descobri que esse script tinha deixado 23 URLs listadas duas vezes no sitemap.xml. A causa eram dois espaços fixos no começo de um regex.
O que o script faz
Você passa um ou mais slugs, por exemplo minulo-vs-stillfeed.html. Para cada slug, o script:
- verifica quais pastas de idioma têm esse arquivo
- reescreve as tags
<link rel="alternate" hreflang>e os links de idioma do rodapé em cada uma dessas páginas - apaga as entradas
<url>dessa página no sitemap.xml e acrescenta entradas novas no fim, com os alternates em<xhtml:link>
O problema estava no passo 3. Antes da correção, a remoção era assim:
for lc in locs:
sm=re.sub(r' <url><loc>'+re.escape(lc)+r'</loc>.*?</url>\n','',sm,flags=re.S)
As entradas que ele não conseguia apagar
Esse padrão só casa com um <url> que começa com exatamente dois espaços. O próprio script grava as entradas com dois espaços de indentação, então ele sempre conseguia apagar o que ele mesmo tinha escrito.
Só que o sitemap.xml também tinha entradas antigas, que eu escrevi antes de o script existir, e essas não tinham indentação nenhuma. Na versão anterior à correção, en/minulo-vs-stillfeed.html aparecia na linha 2023 e de novo na linha 9603:
<url><loc>https://minulo.app/en/minulo-vs-stillfeed.html</loc><lastmod>2026-10-20</lastmod></url>
<url><loc>https://minulo.app/en/minulo-vs-stillfeed.html</loc><lastmod>2026-10-24</lastmod>
<xhtml:link rel="alternate" hreflang="ja" href="https://minulo.app/minulo-vs-stillfeed.html"/>
<xhtml:link rel="alternate" hreflang="en" href="https://minulo.app/en/minulo-vs-stillfeed.html"/>
...
A primeira é a entrada antiga: sem indentação, sem alternates, lastmod 2026-10-20. A segunda é a que o script acrescentou, com alternates. A antiga nunca casava com o regex, então ficava lá, e cada execução acrescentava uma entrada nova no fim.
Por que a duplicação importa
Para o mesmo <loc>, o sitemap estava dando duas respostas diferentes:
- dois valores de lastmod, 2026-10-20 e 2026-10-24
- uma entrada com alternates de hreflang e outra sem
Este site declara as versões de idioma com <xhtml:link> no sitemap, além do head da página. Qual das duas entradas um buscador usa é decisão do buscador. Se ele pegar a que não tem alternates, o conjunto de idiomas daquela URL pode não chegar até ele.
Como eu encontrei
Extraí todos os <loc> e pedi ao uniq -d tudo o que aparecesse mais de uma vez:
grep -o '<loc>[^<]*</loc>' sitemap.xml | sort | uniq -d
Eu vinha anotando o número de URLs do sitemap toda vez que adicionava páginas. Esse número não mostra duplicatas: ele só sobe, do mesmo jeito que sobe quando entram páginas novas.
A correção
1. Aceitar qualquer espaço no começo
Os dois espaços do começo viraram [ \t]*, ou seja, zero ou mais espaços ou tabs. Agora o padrão casa com as entradas antigas sem indentação e com as novas indentadas. Ler o arquivo com uma biblioteca de XML e reconstruir tudo também funcionaria. Eu mantive o regex e só troquei o prefixo.
2. Usar a data da execução no lastmod
Lendo o diff, também percebi que as entradas acrescentadas tinham 2026-10-24 escrito direto na f-string. Rodar o script em qualquer outro dia continuaria gravando 2026-10-24. Agora ele usa datetime.date.today().
-import os,re,sys
+import os,re,sys,datetime
+TODAY=datetime.date.today().isoformat()
@@
- sm=re.sub(r' <url><loc>'+re.escape(lc)+r'</loc>.*?</url>\n','',sm,flags=re.S)
+ sm=re.sub(r'[ \t]*<url><loc>'+re.escape(lc)+r'</loc>.*?</url>\n','',sm,flags=re.S)
@@
- add=''.join(f' <url><loc>{url(d,s)}</loc><lastmod>2026-10-24</lastmod>\n{alts} </url>\n' for _,d,_ in have)
+ add=''.join(f' <url><loc>{url(d,s)}</loc><lastmod>{TODAY}</lastmod>\n{alts} </url>\n' for _,d,_ in have)
3. Remover de uma vez as duplicatas que já estavam no arquivo
Corrigir o regex só limpa uma URL na próxima vez que o slug dela for processado. Para limpar o arquivo atual, rodei um script avulso que mantém o último <url> de cada <loc>. O script de sincronização sempre acrescenta no fim, então a última ocorrência é a entrada mais nova.
import re, sys
p = sys.argv[1]
sm = open(p).read()
pat = re.compile(r'[ \t]*<url><loc>(.*?)</loc>.*?</url>\n', re.S)
blocks = list(pat.finditer(sm))
last = {m.group(1): i for i, m in enumerate(blocks)}
drop = [m for i, m in enumerate(blocks) if last[m.group(1)] != i]
for m in reversed(drop):
sm = sm[:m.start()] + sm[m.end():]
open(p, 'w').write(sm)
print("removed", len(drop))
Conferindo o resultado
$ grep -c '<loc>' sitemap.xml
899
$ grep -o '<loc>[^<]*</loc>' sitemap.xml | sort | uniq -d | wc -l
0
$ python3 -c "import xml.etree.ElementTree as ET; print(len(ET.parse('sitemap.xml').getroot()))"
899
899 URLs, 0 duplicatas, e o arquivo é lido como XML sem erro.
O que fica
- Antes de apagar linhas com regex, confira se o arquivo tem linhas escritas num formato mais antigo.
- Depois de reescrever o arquivo, procure valores de
<loc>duplicados, e não só o total. - Não deixe datas fixas no código que gera arquivos.
O minulo é um app para iPhone que oculta os Reels do Instagram e os Shorts do YouTube: https://minulo.app/pt/?ref=dio-pt-sitemap-1025d



