fix: remove double language prefix from article URLs
generate_articles() was storing url_path with lang prefix (/en/markets/...) but the content blueprint is registered at /<lang>, producing double-prefix URLs like /en/en/markets/italy. Fix: store url_path without prefix, build full_url with prefix for SEO tags (canonical, OG, hreflang, breadcrumbs). Also removes /markets from RESERVED_PREFIXES since article sub-paths under /markets/ are valid pSEO content URLs, not blueprint routes. Subtask 1 of pSEO template improvements. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -69,7 +69,7 @@
|
|||||||
{% for a in scorecard %}
|
{% for a in scorecard %}
|
||||||
<tr>
|
<tr>
|
||||||
<td style="max-width:250px">
|
<td style="max-width:250px">
|
||||||
<a href="{{ a.url_path }}" target="_blank" class="text-sm" title="{{ a.url_path }}">{{ a.title or a.url_path }}</a>
|
<a href="/{{ a.language or 'en' }}{{ a.url_path }}" target="_blank" class="text-sm" title="{{ a.url_path }}">{{ a.title or a.url_path }}</a>
|
||||||
{% if a.template_slug %}
|
{% if a.template_slug %}
|
||||||
<br><span class="text-xs text-slate">{{ a.template_slug }}</span>
|
<br><span class="text-xs text-slate">{{ a.template_slug }}</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
@@ -308,8 +308,8 @@ async def generate_articles(
|
|||||||
# Build render context: row data + language
|
# Build render context: row data + language
|
||||||
ctx = {**row, "language": lang}
|
ctx = {**row, "language": lang}
|
||||||
|
|
||||||
# Render URL pattern
|
# Render URL pattern (no lang prefix — blueprint provides /<lang>)
|
||||||
url_path = f"/{lang}" + _render_pattern(config["url_pattern"], ctx)
|
url_path = _render_pattern(config["url_pattern"], ctx)
|
||||||
if is_reserved_path(url_path):
|
if is_reserved_path(url_path):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
@@ -375,8 +375,8 @@ async def generate_articles(
|
|||||||
# Extract FAQ pairs for structured data
|
# Extract FAQ pairs for structured data
|
||||||
faq_pairs = _extract_faq_pairs(body_md)
|
faq_pairs = _extract_faq_pairs(body_md)
|
||||||
|
|
||||||
# Build SEO metadata
|
# Build SEO metadata (full_url includes lang prefix for canonical/OG)
|
||||||
full_url = base_url + url_path
|
full_url = f"{base_url}/{lang}{url_path}"
|
||||||
publish_dt = datetime(
|
publish_dt = datetime(
|
||||||
publish_date.year, publish_date.month, publish_date.day,
|
publish_date.year, publish_date.month, publish_date.day,
|
||||||
8, 0, 0,
|
8, 0, 0,
|
||||||
@@ -397,7 +397,7 @@ async def generate_articles(
|
|||||||
)
|
)
|
||||||
|
|
||||||
# JSON-LD
|
# JSON-LD
|
||||||
breadcrumbs = _build_breadcrumbs(url_path, base_url)
|
breadcrumbs = _build_breadcrumbs(f"/{lang}{url_path}", base_url)
|
||||||
jsonld_objects = build_jsonld(
|
jsonld_objects = build_jsonld(
|
||||||
config["schema_type"],
|
config["schema_type"],
|
||||||
title=title,
|
title=title,
|
||||||
@@ -499,7 +499,7 @@ async def preview_article(
|
|||||||
|
|
||||||
ctx = {**row, "language": lang}
|
ctx = {**row, "language": lang}
|
||||||
|
|
||||||
url_path = f"/{lang}" + _render_pattern(config["url_pattern"], ctx)
|
url_path = _render_pattern(config["url_pattern"], ctx)
|
||||||
title = _render_pattern(config["title_pattern"], ctx)
|
title = _render_pattern(config["title_pattern"], ctx)
|
||||||
meta_desc = _render_pattern(config["meta_description_pattern"], ctx)
|
meta_desc = _render_pattern(config["meta_description_pattern"], ctx)
|
||||||
|
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ BUILD_DIR = Path("data/content/_build")
|
|||||||
RESERVED_PREFIXES = (
|
RESERVED_PREFIXES = (
|
||||||
"/admin", "/auth", "/planner", "/billing", "/dashboard",
|
"/admin", "/auth", "/planner", "/billing", "/dashboard",
|
||||||
"/directory", "/leads", "/suppliers", "/health",
|
"/directory", "/leads", "/suppliers", "/health",
|
||||||
"/sitemap", "/static", "/markets", "/features", "/feedback",
|
"/sitemap", "/static", "/features", "/feedback",
|
||||||
)
|
)
|
||||||
|
|
||||||
SCENARIO_RE = re.compile(r'\[scenario:([a-z0-9_-]+)(?::([a-z]+))?\]')
|
SCENARIO_RE = re.compile(r'\[scenario:([a-z0-9_-]+)(?::([a-z]+))?\]')
|
||||||
|
|||||||
@@ -322,8 +322,9 @@ class TestReservedPaths:
|
|||||||
def test_planner_reserved(self):
|
def test_planner_reserved(self):
|
||||||
assert is_reserved_path("/planner/") is True
|
assert is_reserved_path("/planner/") is True
|
||||||
|
|
||||||
def test_markets_reserved(self):
|
def test_markets_not_reserved(self):
|
||||||
assert is_reserved_path("/markets") is True
|
# /markets sub-paths are article URLs; explicit /markets route takes priority
|
||||||
|
assert is_reserved_path("/markets/germany/berlin") is False
|
||||||
|
|
||||||
def test_custom_path_allowed(self):
|
def test_custom_path_allowed(self):
|
||||||
assert is_reserved_path("/padel-court-cost-miami") is False
|
assert is_reserved_path("/padel-court-cost-miami") is False
|
||||||
@@ -456,7 +457,7 @@ class TestGenerationPipeline:
|
|||||||
|
|
||||||
miami = await fetch_one("SELECT * FROM articles WHERE slug = 'test-city-en-miami'")
|
miami = await fetch_one("SELECT * FROM articles WHERE slug = 'test-city-en-miami'")
|
||||||
assert miami is not None
|
assert miami is not None
|
||||||
assert miami["url_path"] == "/en/markets/us/miami"
|
assert miami["url_path"] == "/markets/us/miami"
|
||||||
assert miami["title"] == "Padel in Miami"
|
assert miami["title"] == "Padel in Miami"
|
||||||
assert miami["template_slug"] == "test-city"
|
assert miami["template_slug"] == "test-city"
|
||||||
assert miami["language"] == "en"
|
assert miami["language"] == "en"
|
||||||
@@ -761,7 +762,7 @@ class TestPreviewArticle:
|
|||||||
from padelnomics.content import preview_article
|
from padelnomics.content import preview_article
|
||||||
result = await preview_article("test-city", "miami")
|
result = await preview_article("test-city", "miami")
|
||||||
assert result["title"] == "Padel in Miami"
|
assert result["title"] == "Padel in Miami"
|
||||||
assert result["url_path"] == "/en/markets/us/miami"
|
assert result["url_path"] == "/markets/us/miami"
|
||||||
assert result["meta_description"] == "Padel costs in Miami"
|
assert result["meta_description"] == "Padel costs in Miami"
|
||||||
assert "<h1>" in result["html"]
|
assert "<h1>" in result["html"]
|
||||||
|
|
||||||
@@ -773,7 +774,7 @@ class TestPreviewArticle:
|
|||||||
async def test_preview_with_language(self, db, pseo_env):
|
async def test_preview_with_language(self, db, pseo_env):
|
||||||
from padelnomics.content import preview_article
|
from padelnomics.content import preview_article
|
||||||
result = await preview_article("test-city", "miami", lang="de")
|
result = await preview_article("test-city", "miami", lang="de")
|
||||||
assert result["url_path"] == "/de/markets/us/miami"
|
assert result["url_path"] == "/markets/us/miami"
|
||||||
|
|
||||||
async def test_preview_unknown_template_raises(self, db, pseo_env):
|
async def test_preview_unknown_template_raises(self, db, pseo_env):
|
||||||
from padelnomics.content import preview_article
|
from padelnomics.content import preview_article
|
||||||
|
|||||||
Reference in New Issue
Block a user