Skip to content

Fix localized API docs SEO URLs#348

Merged
PrzemyslawKlys merged 1 commit intomainfrom
codex/project-apidocs-language-base-url
May 7, 2026
Merged

Fix localized API docs SEO URLs#348
PrzemyslawKlys merged 1 commit intomainfrom
codex/project-apidocs-language-base-url

Conversation

@PrzemyslawKlys
Copy link
Copy Markdown
Member

Summary

  • pass language-specific site base URLs into generated project API docs and suite landing pages
  • preserve file asset/feed URLs when rebasing selected languages to a root host
  • add focused coverage for localized API docs canonicals and language-root file URL rebasing

Validation

  • dotnet test .\PowerForge.Tests\PowerForge.Tests.csproj --filter "FullyQualifiedNameBuild_LanguageRootRebase_PreservesFileAssetUrls|FullyQualifiedNameBuild_WritesRoot404HtmlForNotFoundSlug|FullyQualifiedName~RunPipeline_ProjectApiDocs_LocalizesProjectTabsAndFallsBackToDefaultLanguageForUnsupportedCollections" --no-restore
  • C:\Support\GitHub\PSPublishModule\PowerForge.Web.Cli\bin\Debug\net10.0\PowerForge.Web.Cli.exe pipeline --config .\pipeline.deploy.json --mode dev --only build-evotec-pl,project-apidocs-evotec-pl,seo-smoke-evotec-pl,sitemap-evotec-pl,indexnow-evotec-pl,search-index-compat-evotec-pl,llms-evotec-pl,optimize-evotec-pl,hosting-evotec-pl,agent-ready-evotec-pl,audit-evotec-pl

@claude
Copy link
Copy Markdown

claude Bot commented May 7, 2026

Code Review

Overview

This PR adds localized BaseUrl propagation into generated project API docs and suite landing pages, and fixes a bug where file/asset URLs were having trailing slashes incorrectly appended during language-root rebasing. The scope is focused and the changes are cohesive.

Positive Observations

  • Good test coverage: both the canonical URL assertion for localized API docs and the asset URL preservation test cover meaningful behavior.
  • Clean fallback logic: ResolveProjectApiSiteBaseUrl neatly chains language-specific to global BaseUrl with null-coalescing.
  • Existing test corrected: the French baseUrl was set to the default host (https://example.test) instead of the language-specific one (https://fr.example.test) — a real copy-paste bug worth catching.

Issues and Suggestions

1. False-positive risk in LooksLikeFileRoute (potential bug)

Path.GetExtension on URL segments can produce false positives for version-like path components. For example, Path.GetExtension for /api/v1.0 returns .0, so that URL would be treated as a file route and skip trailing-slash normalization even though it is a regular navigable page. A safer heuristic is to check against an explicit allowlist of known file extensions (.css, .js, .png, .woff2, .xml, etc.) rather than treating any non-empty extension as a match.

2. Repeated heap allocation in LooksLikeFileRoute (minor)

new[] containing the query/hash delimiters allocates a new char array on every call. Use path.AsSpan().IndexOfAny or a static readonly field instead.

3. Feed URL coverage gap

The PR description says preserve file asset/feed URLs when rebasing, and FeedSpec is enabled in the new test. However, the test does not assert that feed URLs like /feed.xml are preserved without trailing slashes, leaving that half of the stated fix without a regression test.

4. Inconsistent indentation in test markdown content (cosmetic)

In Build_LanguageRootRebase_PreservesFileAssetUrls, the default-language index.md raw string has the heading and img tag flush-left while the YAML frontmatter is indented. The Polish version is consistent. Won't affect test correctness, but looks like a copy-paste oversight.

Summary

Area Assessment
Correctness Mostly correct; false-positive risk in LooksLikeFileRoute on versioned URL segments
Test coverage Good, but feed URL assertion is missing despite being called out in the PR description
Performance Minor: char array allocation inside LooksLikeFileRoute on each call
Security No concerns
Style Consistent with the rest of the codebase

The core change is solid. The main item worth addressing before merge is the Path.GetExtension false-positive risk for non-file URL segments that contain dots (e.g. version numbers like /api/v1.0).

@PrzemyslawKlys PrzemyslawKlys merged commit 3c671c8 into main May 7, 2026
6 checks passed
@PrzemyslawKlys PrzemyslawKlys deleted the codex/project-apidocs-language-base-url branch May 7, 2026 09:25
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c2addad36b

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

["title"] = $"{name} API Reference",
["out"] = outputPath,
["baseUrl"] = apiBaseUrl,
["siteBaseUrl"] = siteBaseUrl,
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Avoid overwriting parent siteBaseUrl with null

Setting siteBaseUrl on every generated input causes a regression when apiSiteBaseUrl is null: the per-project input now overwrites the parent project-apidocs step value during CreateMergedApiDocsStepDocument merge, so ExecuteApiDocs receives an empty siteBaseUrl and skips the configured/derived base URL fallback. This breaks canonical/OpenGraph absolute URL generation for pipelines that set siteBaseUrl explicitly (or rely on parent-level defaults) when site config cannot provide a language base URL.

Useful? React with 👍 / 👎.

["title"] = $"{name} API Reference",
["out"] = outputPath,
["baseUrl"] = apiBaseUrl,
["siteBaseUrl"] = siteBaseUrl,
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve parent siteBaseUrl when language base URL is missing

project-apidocs now writes siteBaseUrl into each generated child apidocs step even when apiSiteBaseUrl is null, and CreateMergedApiDocsStepDocument lets child properties override parent properties. In pipelines that set siteBaseUrl on the parent step (or rely on parent-level defaults), this clears that value and causes canonical/OpenGraph URLs to fall back to relative paths instead of absolute URLs.

Useful? React with 👍 / 👎.

["title"] = $"{name} API Reference",
["out"] = outputPath,
["baseUrl"] = apiBaseUrl,
["siteBaseUrl"] = siteBaseUrl,
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Do not override explicit project-apidocs siteBaseUrl

Setting siteBaseUrl unconditionally here also overrides any explicit siteBaseUrl configured on the parent project-apidocs step, because merged child inputs win over parent properties. That is a behavior regression: callers can no longer force a deployment-specific canonical host (for example prod/staging host overrides) once a site.json is present, since this line always replaces the parent value.

Useful? React with 👍 / 👎.

Comment on lines +612 to +613
if (LooksLikeFileRoute(publicRoute))
return publicRoute;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Restrict file-route heuristic for dotted directory segments

Using Path.GetExtension to classify file routes treats dotted directory-like routes (for example /docs/v1.0) as files, so trailing-slash normalization is skipped in localization rebasing. This conflicts with existing dotted-route directory behavior in the repo and can emit inconsistent public URLs/canonicals for versioned docs paths when inputs are provided without a trailing slash.

Useful? React with 👍 / 👎.

["title"] = $"{name} API Reference",
["out"] = outputPath,
["baseUrl"] = apiBaseUrl,
["siteBaseUrl"] = siteBaseUrl,
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve parent siteBaseUrl when language base URL is missing

This always writes a siteBaseUrl property into each generated child apidocs input, so when apiSiteBaseUrl resolves to null/empty the merged child step overwrites the parent project-apidocs setting and clears the value that ExecuteApiDocs would otherwise use. In pipelines that provide siteBaseUrl at the parent step (or rely on parent-level defaults), canonical/OpenGraph URLs regress from absolute URLs to relative ones.

Useful? React with 👍 / 👎.

["title"] = $"{name} API Reference",
["out"] = outputPath,
["baseUrl"] = apiBaseUrl,
["siteBaseUrl"] = siteBaseUrl,
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep explicit project-apidocs siteBaseUrl overrides intact

Because child inputs now unconditionally set siteBaseUrl, any explicit siteBaseUrl configured on the parent project-apidocs step is silently replaced by the language-derived value from site.json. That breaks environment-specific host overrides (for example staging/prod canonical host differences) that previously flowed through parent defaults into each generated apidocs run.

Useful? React with 👍 / 👎.

Comment on lines +2513 to +2514
return NormalizeOptionalString(ResolveProjectApiLanguageSpec(siteSpec, targetLanguage)?.BaseUrl) ??
NormalizeOptionalString(siteSpec.BaseUrl);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Align language base URL selection with collection fallback rules

The new helper always prefers the target language BaseUrl without checking whether that language is actually supported for the routed collection, while other project link generation paths explicitly fall back to default-language URLs when a collection is unsupported. This can produce mixed-domain metadata (canonical/social host on one language domain while related project tabs intentionally fall back to default-domain URLs) in partially localized configurations.

Useful? React with 👍 / 👎.

if (queryIndex >= 0)
path = path[..queryIndex];

return !string.IsNullOrWhiteSpace(Path.GetExtension(path));
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Avoid treating dotted route segments as file paths

Using Path.GetExtension as the file/route discriminator classifies dotted directory routes like /docs/v1.0 as files, so trailing-slash normalization is skipped. That conflicts with the repository’s existing dotted-route directory behavior and can generate inconsistent localized/public URLs and canonicals for versioned docs paths when inputs are provided without a trailing slash.

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant