Cap. 21 · Código
Gerador de sitemaps por tipo
Execução
Rode em 3 comandos
$ python3 sitemap_generator.py --out out/$ ls out/$ cat out/sitemap-news.xmlSaída esperada
[built] sitemap-pages.xml (3 source items)[built] sitemap-posts.xml (2 source items)[built] sitemap-products.xml (2 source items)[built] sitemap-localities.xml (2 source items)[built] sitemap-news.xml (3 source items, 2 within 48h window)[index] sitemap.xml (5 sitemaps)Repare em sitemap-news.xml: dos 3 itens de exemplo, só 2 entram ("2 within 48h window") — o item publicado há 5 dias é filtrado pela regra das 48h do Google News.
Código-fonte
Código do módulo
#!/usr/bin/env python3"""Chapter 21 - Typed multi-sitemap generator with a sitemap index.Generates SEVERAL sitemaps split BY TYPE (pages, posts, products, localities,news) plus a top-level sitemap INDEX that references them all. Two productionconcerns are demonstrated: * Per-type TTL cache: each sitemap type is only rebuilt when its cache TTL has expired. Last-built timestamps live in a small JSON state file. * News sitemap (Google News format): includes ONLY items published within the last 48 hours, using the ``news:news`` namespace.The script ships with inline SAMPLE data so it runs with zero setup (standardlibrary only - no pip install needed).Part of the "living code" companion repository for the book"Aparecer ou Sumir" / "Be Seen or Be Forgotten"."""from __future__ import annotationsimport argparseimport jsonimport osimport sysfrom datetime import datetime, timedelta, timezonefrom pathlib import Pathfrom typing import Dict, List, Optionalfrom xml.sax.saxutils import escape# XML namespaces used across the generated files.NS_SITEMAP = "http://www.sitemaps.org/schemas/sitemap/0.9"NS_NEWS = "http://www.google.com/schemas/sitemap-news/0.9"# Name of the JSON file that records when each type's sitemap was last built.STATE_FILE_NAME = ".sitemap_state.json"# The 48-hour window is a Google News requirement: news sitemaps must only list# articles published in the last two days.NEWS_WINDOW = timedelta(hours=48)# Per-type cache TTLs. A type's sitemap is only rebuilt once its TTL elapses.# The news sitemap gets a short TTL because its content is time-sensitive.DEFAULT_TTL_SECONDS: Dict[str, int] = { "pages": 24 * 3600, # slow-changing: once a day "posts": 6 * 3600, # editorial content: a few times a day "products": 3600, # catalogue: hourly "localities": 24 * 3600, # geo pages: once a day "news": 15 * 60, # news: every 15 minutes}def now_utc() -> datetime: """Return the current time as an aware UTC datetime (easy to mock in tests).""" return datetime.now(timezone.utc)# ---------------------------------------------------------------------------# Sample data source. In a real project each of these would be a DB query.# Every item is a dict with at least "loc" and "lastmod". News items also carry# "title" and "published" (an ISO-8601 timestamp).# ---------------------------------------------------------------------------def fetch_items(item_type: str, base_url: str) -> List[Dict[str, str]]: """Return the URL items for a given sitemap type. TODO: replace this sample implementation with real queries against your CMS / database. The shape of each returned dict must stay the same. """ base = base_url.rstrip("/") today = now_utc().strftime("%Y-%m-%d") recent = now_utc().isoformat() yesterday = (now_utc() - timedelta(hours=12)).isoformat() samples: Dict[str, List[Dict[str, str]]] = { "pages": [ {"loc": f"{base}/", "lastmod": today}, {"loc": f"{base}/sobre", "lastmod": today}, {"loc": f"{base}/contato", "lastmod": today}, ], "posts": [ {"loc": f"{base}/blog/seo-local", "lastmod": today}, {"loc": f"{base}/blog/sitemaps-por-tipo", "lastmod": today}, ], "products": [ {"loc": f"{base}/produtos/plano-pro", "lastmod": today}, {"loc": f"{base}/produtos/plano-basico", "lastmod": today}, ], "localities": [ {"loc": f"{base}/localidades/vila-aurora", "lastmod": today}, {"loc": f"{base}/localidades/porto-sereno", "lastmod": today}, ], "news": [ # Fresh item: published now -> inside the 48h window -> INCLUDED. { "loc": f"{base}/noticias/lancamento", "lastmod": today, "title": "Lancamento da nova ferramenta de SEO", "published": recent, }, # Fresh item: published 12h ago -> inside the 48h window -> INCLUDED. { "loc": f"{base}/noticias/atualizacao", "lastmod": today, "title": "Atualizacao do algoritmo de busca", "published": yesterday, }, # Stale item: published 5 days ago -> outside 48h -> FILTERED OUT. { "loc": f"{base}/noticias/antiga", "lastmod": today, "title": "Noticia antiga", "published": (now_utc() - timedelta(days=5)).isoformat(), }, ], } return samples.get(item_type, [])# ---------------------------------------------------------------------------# State / TTL handling# ---------------------------------------------------------------------------def load_state(state_path: Path) -> Dict[str, float]: """Load {type: last_built_epoch_seconds} from the JSON state file.""" if state_path.exists(): with state_path.open(encoding="utf-8") as handle: return json.load(handle) return {}def save_state(state_path: Path, state: Dict[str, float]) -> None: """Persist {type: last_built_epoch_seconds} to the JSON state file.""" with state_path.open("w", encoding="utf-8") as handle: json.dump(state, handle, indent=2)def is_ttl_expired(state: Dict[str, float], item_type: str, ttl_seconds: int) -> bool: """Return True when a type must be rebuilt because its TTL has expired. TTL LOGIC: we compare "now" against the last-built timestamp. If more than ``ttl_seconds`` have passed (or the type was never built), the cache is considered stale and the sitemap is rebuilt. """ last_built = state.get(item_type) if last_built is None: return True age_seconds = now_utc().timestamp() - last_built return age_seconds >= ttl_seconds# ---------------------------------------------------------------------------# XML builders# ---------------------------------------------------------------------------def build_urlset(items: List[Dict[str, str]]) -> str: """Build a standard <urlset> sitemap document from URL items.""" lines = ['<?xml version="1.0" encoding="UTF-8"?>'] lines.append(f'<urlset xmlns="{NS_SITEMAP}">') for item in items: lines.append(" <url>") lines.append(f" <loc>{escape(item['loc'])}</loc>") if item.get("lastmod"): lines.append(f" <lastmod>{escape(item['lastmod'])}</lastmod>") lines.append(" </url>") lines.append("</urlset>") return "\n".join(lines) + "\n"def build_news_urlset( items: List[Dict[str, str]], publication_name: str, language: str,) -> str: """Build a Google News <urlset> including only items from the last 48h. 48h WINDOW: each item's ``published`` timestamp is parsed and compared against ``now - 48h``. Anything older is skipped, per Google News rules. """ cutoff = now_utc() - NEWS_WINDOW lines = ['<?xml version="1.0" encoding="UTF-8"?>'] lines.append( f'<urlset xmlns="{NS_SITEMAP}" xmlns:news="{NS_NEWS}">' ) for item in items: published = _parse_iso(item.get("published")) if published is None or published < cutoff: # Outside the 48-hour window -> excluded from the news sitemap. continue lines.append(" <url>") lines.append(f" <loc>{escape(item['loc'])}</loc>") lines.append(" <news:news>") lines.append(" <news:publication>") lines.append(f" <news:name>{escape(publication_name)}</news:name>") lines.append(f" <news:language>{escape(language)}</news:language>") lines.append(" </news:publication>") lines.append( f" <news:publication_date>{escape(item['published'])}" "</news:publication_date>" ) lines.append(f" <news:title>{escape(item.get('title', ''))}</news:title>") lines.append(" </news:news>") lines.append(" </url>") lines.append("</urlset>") return "\n".join(lines) + "\n"def build_sitemap_index(sitemap_urls: List[str], lastmod: str) -> str: """Build the top-level <sitemapindex> referencing every type sitemap.""" lines = ['<?xml version="1.0" encoding="UTF-8"?>'] lines.append(f'<sitemapindex xmlns="{NS_SITEMAP}">') for url in sitemap_urls: lines.append(" <sitemap>") lines.append(f" <loc>{escape(url)}</loc>") lines.append(f" <lastmod>{escape(lastmod)}</lastmod>") lines.append(" </sitemap>") lines.append("</sitemapindex>") return "\n".join(lines) + "\n"def _parse_iso(value: Optional[str]) -> Optional[datetime]: """Best-effort ISO-8601 parser that always returns an aware UTC datetime.""" if not value: return None try: parsed = datetime.fromisoformat(value) except ValueError: return None if parsed.tzinfo is None: parsed = parsed.replace(tzinfo=timezone.utc) return parsed.astimezone(timezone.utc)# ---------------------------------------------------------------------------# Orchestration# ---------------------------------------------------------------------------def generate( base_url: str, output_dir: Path, types: List[str], ttl_overrides: Dict[str, int], publication_name: str, language: str, force: bool = False,) -> None: """Generate the per-type sitemaps (honouring TTLs) and the sitemap index.""" output_dir.mkdir(parents=True, exist_ok=True) state_path = output_dir / STATE_FILE_NAME state = load_state(state_path) sitemap_urls: List[str] = [] base = base_url.rstrip("/") for item_type in types: ttl = ttl_overrides.get(item_type, DEFAULT_TTL_SECONDS.get(item_type, 3600)) filename = f"sitemap-{item_type}.xml" out_path = output_dir / filename public_url = f"{base}/{filename}" sitemap_urls.append(public_url) # TTL cache decision: skip rebuild when still fresh (unless forced). if not force and out_path.exists() and not is_ttl_expired(state, item_type, ttl): print(f"[cached] {filename} (TTL not expired)") continue items = fetch_items(item_type, base_url) if item_type == "news": xml = build_news_urlset(items, publication_name, language) kept = xml.count("<url>") print( f"[built] {filename} ({len(items)} source items, " f"{kept} within 48h window)" ) else: xml = build_urlset(items) print(f"[built] {filename} ({len(items)} source items)") out_path.write_text(xml, encoding="utf-8") state[item_type] = now_utc().timestamp() # The index is always rewritten so it references the current set of files. index_xml = build_sitemap_index( sitemap_urls, lastmod=now_utc().strftime("%Y-%m-%d") ) (output_dir / "sitemap.xml").write_text(index_xml, encoding="utf-8") print(f"[index] sitemap.xml ({len(sitemap_urls)} sitemaps)") save_state(state_path, state)def parse_args(argv: List[str]) -> argparse.Namespace: parser = argparse.ArgumentParser( description="Generate typed sitemaps + a sitemap index with TTL cache." ) parser.add_argument( "--base-url", default=os.environ.get("SITE_BASE_URL", "https://www.example.com"), help="Site base URL used to build absolute locations.", ) parser.add_argument( "--out", dest="out", default=os.environ.get("SITEMAP_OUTPUT", "out"), help="Directory to write the sitemap files into.", ) parser.add_argument( "--types", default="pages,posts,products,localities,news", help="Comma-separated list of sitemap types to build.", ) parser.add_argument( "--publication-name", default=os.environ.get("NEWS_PUBLICATION_NAME", "Sua Publicacao"), help="Publication name used in the news sitemap.", ) parser.add_argument( "--language", default=os.environ.get("NEWS_LANGUAGE", "pt"), help="Language code used in the news sitemap.", ) parser.add_argument( "--force", action="store_true", help="Rebuild every sitemap regardless of TTL.", ) return parser.parse_args(argv)def main(argv: List[str]) -> int: args = parse_args(argv) types = [t.strip() for t in args.types.split(",") if t.strip()] generate( base_url=args.base_url, output_dir=Path(args.out), types=types, ttl_overrides={}, # TODO: load per-type TTL overrides here if desired. publication_name=args.publication_name, language=args.language, force=args.force, ) return 0if __name__ == "__main__": raise SystemExit(main(sys.argv[1:]))Dúvidas
Perguntas frequentes
Preciso instalar alguma dependência?
Não. O sitemap_generator.py usa apenas a biblioteca padrão do Python. Basta rodar python3 sitemap_generator.py --out out/ com os dados de exemplo já embutidos.
Como funciona o cache com TTL por tipo?
Cada tipo (pages, posts, products, localities, news) tem um TTL próprio. O horário da última construção fica em out/.sitemap_state.json. Um sitemap só é reconstruído quando seu TTL expira — o de notícias tem TTL curto (15 min) porque é sensível ao tempo. Use --force para reconstruir tudo.
Por que só duas das três notícias aparecem no sitemap?
O sitemap de notícias segue a regra do Google News: só inclui itens publicados nas últimas 48 horas. Dos três itens de exemplo, dois são recentes (entram) e um foi publicado há 5 dias (é filtrado). A saída mostra '3 source items, 2 within 48h window'.
Implementação
Prefere que eu implemente?
Conte o seu caso numa conversa e a gente define o escopo juntos.
Agendar conversa