diff --git a/main.py b/main.py
index a6922bc..761ea81 100644
--- a/main.py
+++ b/main.py
@@ -2,7 +2,9 @@
import http.server
import random
import shutil
+from datetime import UTC, datetime
from pathlib import Path
+from xml.sax.saxutils import escape
from jinja2 import Environment, FileSystemLoader
from typer import Typer
@@ -26,6 +28,29 @@ def load_settings() -> dict:
return {}
+def write_sitemap(site_url: str) -> None:
+ """Gera dist/sitemap.xml com as páginas renderizadas (referenciado no robots.txt)."""
+ lastmod = datetime.now(UTC).date().isoformat()
+ entries = []
+ for page in sorted(output_folder.glob("*.html")):
+ path = "/" if page.name == "index.html" else f"/{page.name}"
+ priority = "1.0" if path == "/" else "0.8"
+ entries.append(
+ " \n"
+ f" {escape(site_url + path)}\n"
+ f" {lastmod}\n"
+ " weekly\n"
+ f" {priority}\n"
+ " "
+ )
+ (output_folder / "sitemap.xml").write_text(
+ '\n'
+ '\n'
+ + "\n".join(entries)
+ + "\n\n"
+ )
+
+
@cli.command()
def render():
"""Gera os arquivos HTML estáticos na pasta dist."""
@@ -57,8 +82,24 @@ def render():
robots_file.write_text("User-agent: *\nDisallow: /\n")
else:
site_url = settings.get("event", {}).get("url", "").rstrip("/")
- sitemap_line = f"\nSitemap: {site_url}/sitemap.xml" if site_url else ""
- robots_file.write_text(f"User-agent: *\nAllow: /{sitemap_line}\n")
+ # CSS, JS e imagens liberados explicitamente: o Googlebot precisa deles
+ # para renderizar a página e avaliá-la corretamente (mobile-first).
+ robots_lines = [
+ "User-agent: *",
+ "Allow: /",
+ "",
+ "User-agent: Googlebot",
+ "Allow: /",
+ "Allow: /static/",
+ "",
+ "User-agent: Googlebot-Image",
+ "Allow: /static/img/",
+ "",
+ ]
+ if site_url:
+ robots_lines.append(f"Sitemap: {site_url}/sitemap.xml")
+ write_sitemap(site_url)
+ robots_file.write_text("\n".join(robots_lines) + "\n")
# Arquivo .nojekyll para o GitHub Pages servir arquivos estáticos diretamente
(output_folder / ".nojekyll").touch()
diff --git a/templates/base.j2 b/templates/base.j2
index cf98ac0..c3354ec 100644
--- a/templates/base.j2
+++ b/templates/base.j2
@@ -7,6 +7,10 @@
{% set event_desc = event.description if event and event.description else (event_description | default('')) %}
{% set event_url = event.url if event and event.url else ('https://' ~ event_edition ~ '.pythonnordeste.org/') %}
{% set event_preview_image = theme.preview_image if theme and theme.preview_image else (preview_image | default('static/img/logo_pyne2027.png')) %}
+{# Redes sociais (Facebook, LinkedIn, WhatsApp, X) exigem URL absoluta na og:image #}
+{% set event_preview_image_url = event_preview_image if event_preview_image.startswith('http') else (event_url.rstrip('/') ~ '/' ~ event_preview_image.lstrip('/')) %}
+{% set event_preview_image_alt = theme.preview_image_alt if theme and theme.preview_image_alt else ('Logo oficial da ' ~ event_name ~ ' ' ~ event_edition) %}
+{% set event_title = event_name ~ ' ' ~ event_edition ~ ((' | ' ~ event_city ~ ' - ' ~ event_state) if event_city else '') %}
{% set event_favicon = theme.favicon if theme and theme.favicon else (favicon | default('static/img/favicon.png')) %}
{% set is_indexing_allowed = (environment != 'staging') and (allow_indexing | default(true)) %}
@@ -39,22 +43,31 @@
- {{ event_name }} {{ event_edition }}{% if event_city %} | {{ event_city }} - {{ event_state }}{% endif %}
-
-
+ {{ event_title }}
+
+
+
-
-
-
+
+ {% if event_preview_image_url.startswith('https://') %}
+
+ {% endif %}
+ {% if theme and theme.preview_image_width and theme.preview_image_height %}
+
+
+ {% endif %}
+
+
-
+
+
-
+
-
-
+
+
diff --git a/tests/test_site.py b/tests/test_site.py
index 047eff9..1183cc2 100644
--- a/tests/test_site.py
+++ b/tests/test_site.py
@@ -1,5 +1,8 @@
from pathlib import Path
+import pytest
+
+import main
from main import load_settings, output_folder, render, settings_file
@@ -21,7 +24,7 @@ def test_referenced_theme_assets_exist():
for key in ["logo", "favicon", "preview_image"]:
asset_path = theme.get(key)
- if asset_path:
+ if asset_path and not asset_path.startswith("http"):
assert Path(asset_path).exists(), (
f"O arquivo de tema '{key}' apontado para '{asset_path}' não existe."
)
@@ -40,27 +43,103 @@ def test_render_generates_dist_and_index_html():
assert "Pessoas > Tecnologia" in content
-def test_staging_environment_blocks_search_engines():
+@pytest.fixture
+def render_with(monkeypatch):
+ """Renderiza o site sobrescrevendo chaves do variables.yaml, sem alterar o arquivo."""
+
+ def _render(**overrides):
+ settings = {**load_settings(), **overrides}
+ monkeypatch.setattr(main, "load_settings", lambda: settings)
+ render()
+ return settings
+
+ return _render
+
+
+def test_render_generates_github_pages_files():
render()
- robots_file = output_folder / "robots.txt"
- assert robots_file.exists(), "O arquivo dist/robots.txt deve ser gerado."
- assert "Disallow: /" in robots_file.read_text(), (
- "Em staging, o robots.txt deve bloquear todos os robôs de busca."
+ assert (output_folder / "robots.txt").exists(), (
+ "O arquivo dist/robots.txt deve ser gerado."
+ )
+ assert (output_folder / "favicon.ico").exists(), (
+ "O arquivo dist/favicon.ico deve ser copiado."
+ )
+ assert (output_folder / ".nojekyll").exists(), (
+ "O arquivo dist/.nojekyll deve ser gerado para o GitHub Pages."
+ )
+
+
+@pytest.mark.parametrize(
+ "overrides",
+ [
+ {"environment": "staging", "allow_indexing": True},
+ {"environment": "production", "allow_indexing": False},
+ ],
+ ids=["staging", "production-sem-indexacao"],
+)
+def test_indexing_disabled_blocks_search_engines(render_with, overrides):
+ render_with(**overrides)
+
+ assert "Disallow: /" in (output_folder / "robots.txt").read_text(), (
+ "Sem indexação, o robots.txt deve bloquear todos os robôs de busca."
+ )
+ assert not (output_folder / "sitemap.xml").exists(), (
+ "Sem indexação, o sitemap.xml não deve ser gerado."
)
index_html = (output_folder / "index.html").read_text(encoding="utf-8")
- assert 'content="noindex, nofollow' in index_html, (
- "Em staging, a meta tag robots deve ser 'noindex, nofollow'."
+ assert 'name="robots" content="noindex, nofollow' in index_html, (
+ "Sem indexação, a meta tag robots deve ser 'noindex, nofollow'."
)
assert 'name="googlebot" content="noindex, nofollow' in index_html, (
- "Em staging, a meta tag googlebot deve ser 'noindex, nofollow'."
+ "Sem indexação, a meta tag googlebot deve ser 'noindex, nofollow'."
)
+ assert 'rel="canonical"' not in index_html
assert "googletagmanager.com/gtag/js" not in index_html, (
- "Em staging, o Google Analytics não deve ser injetado."
+ "Sem indexação, o Google Analytics não deve ser injetado."
)
- assert (output_folder / "favicon.ico").exists(), (
- "O arquivo dist/favicon.ico deve ser copiado."
+
+
+def test_production_allows_search_engines(render_with):
+ settings = render_with(environment="production", allow_indexing=True)
+ site_url = settings["event"]["url"].rstrip("/")
+
+ robots = (output_folder / "robots.txt").read_text()
+ assert "Disallow: /" not in robots, (
+ "Em produção, o robots.txt não deve bloquear os robôs de busca."
)
- assert (output_folder / ".nojekyll").exists(), (
- "O arquivo dist/.nojekyll deve ser gerado para o GitHub Pages."
+ assert "Allow: /" in robots
+ assert f"Sitemap: {site_url}/sitemap.xml" in robots
+
+ sitemap = output_folder / "sitemap.xml"
+ assert sitemap.exists(), "Em produção, o dist/sitemap.xml deve ser gerado."
+ assert f"{site_url}/" in sitemap.read_text()
+
+ index_html = (output_folder / "index.html").read_text(encoding="utf-8")
+ assert 'name="robots" content="index, follow"' in index_html
+ assert 'name="googlebot" content="index, follow' in index_html
+ assert f'rel="canonical" href="{site_url}/"' in index_html
+ if settings.get("google_tag"):
+ assert f"gtag/js?id={settings['google_tag']}" in index_html, (
+ "Em produção, o Google Analytics deve ser injetado."
+ )
+
+
+def test_social_preview_uses_absolute_image_url():
+ settings = load_settings()
+ render()
+ site_url = settings["event"]["url"].rstrip("/")
+ preview_image = settings["theme"]["preview_image"]
+ expected = (
+ preview_image
+ if preview_image.startswith("http")
+ else f"{site_url}/{preview_image.lstrip('/')}"
+ )
+
+ index_html = (output_folder / "index.html").read_text(encoding="utf-8")
+ assert f'property="og:image" content="{expected}"' in index_html, (
+ "A og:image deve ser uma URL absoluta para os previews em redes sociais."
)
+ assert f'name="twitter:image" content="{expected}"' in index_html
+ assert 'property="og:title"' in index_html
+ assert 'name="twitter:card" content="summary_large_image"' in index_html
diff --git a/variables.yaml b/variables.yaml
index d9a7392..cc165f5 100644
--- a/variables.yaml
+++ b/variables.yaml
@@ -21,7 +21,13 @@ theme:
text_dark: "#2B1B22" # Texto contrastante
logo: "static/img/pyne_emblem.png"
favicon: "static/img/favicon.png"
+ # Imagem do preview ao compartilhar o link em redes sociais (og:image / twitter:image).
+ # Aceita caminho relativo (convertido para URL absoluta usando event.url) ou URL completa.
+ # Recomendado: 1200x630 (ou 1920x1080), JPG/PNG, até 5MB.
preview_image: "static/img/logo_pyne2027.png"
+ preview_image_alt: "Logo oficial da Python Nordeste 2027"
+ preview_image_width: 1920
+ preview_image_height: 1080
# Links e Ações Principais (habilitados conforme a fase do evento)
actions:
@@ -52,6 +58,6 @@ google_tag: "G-Z6HYH6HCL2"
# - Motores de busca (Google, Bing, etc.) são bloqueados com
# - O arquivo robots.txt é gerado com "Disallow: /"
# - O Google Analytics é pausado para não poluir as métricas reais
-environment: "staging" # "staging" ou "production"
-allow_indexing: false # Se false, impede indexação no Google / motores de busca
+environment: "production" # "staging" ou "production"
+allow_indexing: true # Se false, impede indexação no Google / motores de busca