Skip to content

Various smaller fixes in one commit - #561

Merged
patrickdemooij9 merged 1 commit into
dev/mainfrom
feature/variousFixes
Jul 31, 2026
Merged

patrickdemooij9 merged 1 commit into
dev/mainfrom
feature/variousFixes

Conversation

@patrickdemooij9

Copy link
Copy Markdown
Owner

Fixes #554
Fixes #553
Fixes #552
Fixes #550

@qodo-code-review

Copy link
Copy Markdown
Contributor

PR Summary by Qodo

Harden redirects cache refresher and add configurable sitemap index routing

🐞 Bug fix ✨ Enhancement 🧪 Tests ⚙️ Configuration changes 🕐 40+ Minutes

Grey Divider

AI Description

• Make RedirectsCacheRefresher safe to activate when Redirects module is disabled.
• Add configurable sitemap index path/filename routing in SitemapMiddleware.
• Exclude unpublished culture content from sitemap output and expand test coverage.
Diagram

graph TD
  req(["HTTP request"]) --> mw["SitemapMiddleware"] --> cfg["SitemapConfigurationService"] --> gen["Sitemap generation"] --> resp(["XML response"])
  gen --> umb["UmbracoContext/Domains"]
  umb --> gen
  cache["Umbraco cache refresh"] --> rcr["RedirectsCacheRefresher (optional deps)"] --> resp
  subgraph Legend
    direction LR
    _in(["Request/Response"]) ~~~ _comp["Component"]
  end
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Conditionally register the cache refresher only when Redirects is enabled
  • ➕ Avoids constructing the refresher when dependencies are absent
  • ➕ Keeps runtime behavior strictly tied to module enablement
  • ➖ Harder with Umbraco auto-discovery/collection builder behavior
  • ➖ More brittle across Umbraco upgrades and composition patterns
2. Always serve sitemap index at /sitemap.xml in MultiRoot mode
  • ➕ No new configuration option required
  • ➕ Single canonical endpoint for crawlers
  • ➖ Conflicts with current behavior where /sitemap.xml serves default culture sitemap
  • ➖ Less flexible for sites that want both index and single sitemap
3. Use route table/endpoints instead of middleware path matching
  • ➕ Clearer routing semantics and easier to test matching logic
  • ➕ Avoids manual PathString comparisons
  • ➖ More invasive integration change for existing consumers
  • ➖ Potentially larger breaking change surface depending on hosting model

Recommendation: The PR’s approach is appropriate: making RedirectsCacheRefresher dependencies optional is the least invasive way to handle Umbraco’s auto-activation when the module is disabled, and adding SitemapIndexPath provides a backwards-compatible way to host a sitemap index without stealing /sitemap.xml. The chosen solution is localized, test-covered, and avoids broader composition/routing refactors.

Files changed (10) +173 / -16

Enhancement (1) +28 / -1
SitemapMiddleware.csRoute configurable sitemap index endpoint and add .xml pre-guard +28/-1

Route configurable sitemap index endpoint and add .xml pre-guard

• Broadens the middleware guard to only consider .xml requests, then validates either /sitemap.xml or a configured index path/filename before handling. Adds IsSitemapIndexRequest() to support both 'folder/sitemap.xml' and 'file.xml' formats and serves the index at the configured endpoint to keep /sitemap.xml available for the default culture sitemap.

src/SeoToolkit.Umbraco.Sitemap.Core/Middleware/SitemapMiddleware.cs

Bug fix (3) +21 / -12
RedirectsCacheRefresher.csMake RedirectsCacheRefresher tolerate missing module dependencies +14/-9

Make RedirectsCacheRefresher tolerate missing module dependencies

• Updates the constructor to accept optional IRedirectsBloomFilter/IRedirectsRepository parameters. Refresh(Guid) now clears runtime cache as before, but only reads repository and updates bloom filter when both dependencies are present.

src/SeoToolkit.Umbraco.Redirects.Core/Caching/RedirectsCacheRefresher.cs

SitemapGenerator.csExclude culture-unpublished content from sitemap nodes +3/-2

Exclude culture-unpublished content from sitemap nodes

• Extends HideFromSitemap resolution to also exclude content that is not published for the requested culture. Prevents culture-specific sitemaps from leaking URLs for unpublished variants.

src/SeoToolkit.Umbraco.Sitemap.Core/Common/SitemapGenerators/SitemapGenerator.cs

SitemapIndexGenerator.csSkip unpublished root nodes when building sitemap index +4/-1

Skip unpublished root nodes when building sitemap index

• Adds a publish-state check for each domain culture before emitting a sitemap entry. Avoids producing index entries pointing at sitemaps for unpublished root content.

src/SeoToolkit.Umbraco.Sitemap.Core/Common/SitemapIndexGenerator/SitemapIndexGenerator.cs

Refactor (1) +0 / -1
SitemapContentRepository.csSimplify delete SQL builder chain +0/-1

Simplify delete SQL builder chain

• Removes a redundant .From<T>() call when building the delete query, keeping the query builder minimal.

src/SeoToolkit.Umbraco.Sitemap.Core/Repositories/SitemapContentRepository.cs

Tests (2) +118 / -1
RedirectTests.csAdd regression test for cache refresher activation without Redirects module +32/-1

Add regression test for cache refresher activation without Redirects module

• Adds a DI-based test that constructs RedirectsCacheRefresher with only core Umbraco services registered. Verifies activation succeeds and Refresh(Guid) is a safe no-op when Redirects dependencies are missing.

src/SeoToolkit.Tests/SeoToolkit.Tests/RedirectTests.cs

SitemapMiddlewareTests.csTest sitemap index routing via configured index path/filename and .xml guard +86/-0

Test sitemap index routing via configured index path/filename and .xml guard

• Adds tests ensuring a configured SitemapIndexPath can be either a folder segment or a root-level .xml filename and still serve the sitemap index. Adds a guard test verifying non-.xml requests short-circuit before loading settings.

src/SeoToolkit.Tests/SeoToolkit.Tests/SitemapMiddlewareTests.cs

Other (3) +6 / -1
SitemapAppSettingsModel.csAdd appsettings model property for SitemapIndexPath +2/-0

Add appsettings model property for SitemapIndexPath

• Introduces SitemapIndexPath with a default empty value, enabling configuration-driven index routing.

src/SeoToolkit.Umbraco.Sitemap.Core/Config/Models/SitemapAppSettingsModel.cs

SitemapConfig.csAdd runtime config property for SitemapIndexPath +2/-0

Add runtime config property for SitemapIndexPath

• Adds SitemapIndexPath to the runtime settings object so middleware and generators can use it.

src/SeoToolkit.Umbraco.Sitemap.Core/Config/Models/SitemapConfig.cs

SitemapConfigurationService.csMap SitemapIndexPath from appsettings into SitemapConfig +2/-1

Map SitemapIndexPath from appsettings into SitemapConfig

• Extends GetSettings() mapping to include the new SitemapIndexPath field, keeping appsettings and runtime config aligned.

src/SeoToolkit.Umbraco.Sitemap.Core/Config/SitemapConfigurationService.cs

@qodo-code-review

Copy link
Copy Markdown
Contributor

Code Review by Qodo

🐞 Bugs (4) 📘 Rule violations (1) 📜 Skill insights (0)

Context used
✅ Compliance rules (platform): 8 rules

Grey Divider


Remediation recommended

1. Trailing-slash XML bypass 🐞 Bug ≡ Correctness
Description
SitemapMiddleware rejects requests like "/sitemap-index.xml/" or "/sitemap-index/sitemap.xml/"
because the new pre-guard checks EndsWith(".xml") without trimming "/", even though
IsSitemapIndexRequest later trims trailing slashes for matching. This makes trailing-slash variants
of sitemap URLs fall through to _next unexpectedly.
Code

src/SeoToolkit.Umbraco.Sitemap.Core/Middleware/SitemapMiddleware.cs[R41-45]

+            if (context.Request.Path.Value?.EndsWith(".xml", StringComparison.OrdinalIgnoreCase) != true)
            {
                await _next.Invoke(context);
                return;
            }
Relevance

●● Moderate

No prior reviews found on allowing trailing-slash .xml sitemap paths; sitemap routing changes exist
but not this case.

PR-#494

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The middleware pre-guard returns early unless the raw path ends with ".xml", but the later
index-path matcher explicitly trims trailing slashes, so it cannot run for ".xml/" requests.

src/SeoToolkit.Umbraco.Sitemap.Core/Middleware/SitemapMiddleware.cs[41-45]
src/SeoToolkit.Umbraco.Sitemap.Core/Middleware/SitemapMiddleware.cs[110-119]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`SitemapMiddleware` uses `EndsWith(".xml")` as a cheap pre-guard, but it does not normalize trailing slashes. As a result, requests that are effectively XML endpoints but end with `/` never reach the later matching logic (which *does* trim `/`).

## Issue Context
`IsSitemapIndexRequest(...)` already compares using `requestPath.Value?.TrimEnd('/')`, indicating the intention is to treat trailing slashes as equivalent.

## Fix Focus Areas
- src/SeoToolkit.Umbraco.Sitemap.Core/Middleware/SitemapMiddleware.cs[41-57]
- src/SeoToolkit.Umbraco.Sitemap.Core/Middleware/SitemapMiddleware.cs[110-119]

## Suggested change
Normalize once at the start (e.g., `var path = context.Request.Path.Value?.TrimEnd('/')`) and use `path` for:
- the initial `.xml` pre-guard
- `isSitemapRequest`
- `IsSitemapIndexRequest` comparisons (or pass the normalized PathString/value)

## Tests
Add/extend tests to cover trailing-slash variants for both:
- configured index filename (`/sitemap-index.xml/`)
- configured index folder (`/sitemap-index/sitemap.xml/`)

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Un-guarded culture IsPublished 🐞 Bug ☼ Reliability
Description
SitemapIndexGenerator unconditionally calls rootNode.IsPublished(domain.Culture) without checking
for null/whitespace culture, unlike SitemapGenerator which guards IsPublished(culture) behind a
string.IsNullOrWhiteSpace check. This creates inconsistent behavior for empty/unspecified cultures
and relies on IsPublished handling such values safely.
Code

src/SeoToolkit.Umbraco.Sitemap.Core/Common/SitemapIndexGenerator/SitemapIndexGenerator.cs[R33-35]

+                    if (!rootNode.IsPublished(domain.Culture))
+                        continue;
+
Relevance

●● Moderate

No historical evidence about guarding IsPublished(culture); only general null-guard patterns seen.

PR-#405

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
SitemapIndexGenerator adds an IsPublished(culture) call with no culture guard, while
SitemapGenerator shows an explicit culture guard before IsPublished(culture), demonstrating an
inconsistency introduced by this PR change.

src/SeoToolkit.Umbraco.Sitemap.Core/Common/SitemapIndexGenerator/SitemapIndexGenerator.cs[27-35]
src/SeoToolkit.Umbraco.Sitemap.Core/Common/SitemapGenerators/SitemapGenerator.cs[132-136]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`SitemapIndexGenerator.Generate()` calls `rootNode.IsPublished(domain.Culture)` without guarding `domain.Culture` for null/empty/whitespace.

## Issue Context
Elsewhere (SitemapGenerator) the code explicitly avoids calling `IsPublished(culture)` when the culture string is empty, indicating empty cultures are a possibility that should be handled.

## Fix Focus Areas
- src/SeoToolkit.Umbraco.Sitemap.Core/Common/SitemapIndexGenerator/SitemapIndexGenerator.cs[27-39]
- src/SeoToolkit.Umbraco.Sitemap.Core/Common/SitemapGenerators/SitemapGenerator.cs[132-136]

## Suggested change
Update the check to something like:
- `if (!string.IsNullOrWhiteSpace(domain.Culture)) { if (!rootNode.IsPublished(domain.Culture)) continue; }`
- otherwise decide on intended invariant behavior (e.g., `if (!rootNode.IsPublished()) continue;`)

## Tests
Add a unit/integration test that covers a domain with an empty/unspecified culture and verifies the generator does not throw and produces expected output.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Informational

3. Settings read on any XML 🐞 Bug ➹ Performance
Description
SitemapMiddleware now calls GetSettings() for every request ending in ".xml" even when it later
determines the request is neither a sitemap.xml nor the configured index path and forwards to _next.
This introduces extra configuration resolution work on unrelated XML endpoints compared to the
previous sitemap-only guard.
Code

src/SeoToolkit.Umbraco.Sitemap.Core/Middleware/SitemapMiddleware.cs[R41-57]

+            if (context.Request.Path.Value?.EndsWith(".xml", StringComparison.OrdinalIgnoreCase) != true)
            {
                await _next.Invoke(context);
                return;
            }

            var settings = _sitemapConfigurationService.GetSettings();

+            var isSitemapRequest = context.Request.Path.Value.EndsWith("/sitemap.xml", StringComparison.OrdinalIgnoreCase);
+            var isConfiguredIndexRequest = !string.IsNullOrWhiteSpace(settings.SitemapIndexPath)
+                && IsSitemapIndexRequest(context.Request.Path, settings.SitemapIndexPath);
+
+            if (!isSitemapRequest && !isConfiguredIndexRequest)
+            {
+                await _next.Invoke(context);
+                return;
+            }
Relevance

●●● Strong

SitemapMiddleware perf/overhead reductions were accepted before (e.g., PR #494 optimized
high-traffic middleware behavior).

PR-#494

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The code path shows settings are loaded immediately after the .xml suffix check, before the
sitemap/index path checks decide to forward to _next.

src/SeoToolkit.Umbraco.Sitemap.Core/Middleware/SitemapMiddleware.cs[41-57]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`GetSettings()` is invoked before confirming the request is sitemap-related, so all `.xml` requests incur the settings lookup.

## Issue Context
The broadened `.xml` pre-guard is needed to support an index filename like `sitemap-index.xml`, but settings lookup can often be avoided for unrelated `.xml` requests.

## Fix Focus Areas
- src/SeoToolkit.Umbraco.Sitemap.Core/Middleware/SitemapMiddleware.cs[41-57]

## Suggested change
Consider a two-phase guard:
1) Normalize path and check if it ends with `/sitemap.xml`; if not, only then consider the configured index filename/path.
2) Only call `GetSettings()` when the request is either:
  - a `/sitemap.xml` request, or
  - a candidate for the configured index (e.g., ends with `.xml` and could match the configured index filename).

If `GetSettings()` is cheap/cached this is optional; otherwise it reduces unnecessary work on non-sitemap XML endpoints.

## Tests
Add a test for an arbitrary `.xml` path (e.g. `/rss.xml`) verifying `_next` is called and (optionally) settings lookup is not performed.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


4. Url computed when excluded 🐞 Bug ➹ Performance
Description
SitemapGenerator now flags items as HideFromSitemap when !content.IsPublished(culture), but it still
calls content.Url(culture, UrlMode.Absolute) before the hideFromSitemap check prevents adding the
node. This defeats the intent of excluding unpublished variants early and performs URL resolution
work even when the node will be discarded.
Code

src/SeoToolkit.Umbraco.Sitemap.Core/Common/SitemapGenerators/SitemapGenerator.cs[R133-135]

                var hideFromSitemap = contentOverride?.ExcludeFromSitemap == true
-                    || (docTypeSettings?.HideFromSitemap ?? false);
+                    || (docTypeSettings?.HideFromSitemap ?? false)
+                    || (!string.IsNullOrWhiteSpace(culture) && !content.IsPublished(culture));
Relevance

●● Moderate

No historical evidence found about skipping Url() computation for hidden sitemap nodes.

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The new IsPublished(culture) exclusion is part of hideFromSitemap, but SitemapNodeItem is still
constructed using Url(culture) before the code checks hideFromSitemap and decides whether to add the
item.

src/SeoToolkit.Umbraco.Sitemap.Core/Common/SitemapGenerators/SitemapGenerator.cs[132-147]
src/SeoToolkit.Umbraco.Sitemap.Core/Common/SitemapGenerators/SitemapGenerator.cs[143-187]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Even when `hideFromSitemap` is true (including the newly added `!content.IsPublished(culture)` case), the code still constructs `SitemapNodeItem` with `content.Url(culture, UrlMode.Absolute)`.

## Issue Context
The item is only added inside `if (!hideFromSitemap)`, so URL resolution and object creation for hidden nodes is unnecessary.

## Fix Focus Areas
- src/SeoToolkit.Umbraco.Sitemap.Core/Common/SitemapGenerators/SitemapGenerator.cs[132-187]

## Suggested change
Move URL resolution and `SitemapNodeItem` construction inside the `if (!hideFromSitemap)` block, e.g.:
- compute `hideFromSitemap`
- `if (hideFromSitemap) { /* optionally skip recursion depending on desired behavior */ } else { var url = content.Url(...); var item = new SitemapNodeItem(url) { ... }; items.Add(item); }`

## Tests
Add a test case for a culture where a node is not published ensuring the generator does not attempt URL generation for that node (if test harness can observe calls/behavior).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


5. Tests outside SeoToolkit.Tests/ 📘 Rule violation ▣ Testability
Description
The modified test files are located under src/SeoToolkit.Tests/... instead of a repository root
SeoToolkit.Tests/ directory, violating the required test code placement convention. This makes
test code organization inconsistent with the compliance checklist and can complicate
discovery/standardization of test projects.
Code

src/SeoToolkit.Tests/SeoToolkit.Tests/RedirectTests.cs[R237-263]

+        [Test]
+        public void RedirectsCacheRefresher_CanBeActivated_WhenModuleIsDisabled()
+        {
+            // Arrange - simulate a disabled Redirects module: only the core Umbraco services are
+            // registered, IRedirectsBloomFilter/IRedirectsRepository are not. Umbraco still auto-discovers
+            // and activates the refresher, registering it type-to-type (see CollectionBuilderBase).
+            var services = new ServiceCollection();
+            services.AddSingleton(AppCaches.Disabled);
+            services.AddSingleton(new Mock<IEventAggregator>().Object);
+
+            var notificationFactory = new Mock<ICacheRefresherNotificationFactory>();
+            notificationFactory
+                .Setup(it => it.Create<RedirectsCacheRefresherNotification>(It.IsAny<object>(), It.IsAny<MessageType>()))
+                .Returns((object msg, MessageType type) => new RedirectsCacheRefresherNotification(msg, type));
+            services.AddSingleton(notificationFactory.Object);
+
+            services.AddTransient<RedirectsCacheRefresher>();
+
+            using var provider = services.BuildServiceProvider();
+
+            // Act
+            var refresher = provider.GetRequiredService<RedirectsCacheRefresher>();
+
+            // Assert - activation succeeds and refreshing is a safe no-op without the module dependencies.
+            Assert.IsNotNull(refresher);
+            Assert.DoesNotThrow(() => refresher.Refresh(Guid.NewGuid()));
+        }
Relevance

● Weak

Repo keeps tests under src/SeoToolkit.Tests (new tests added there in PRs #492, #494).

PR-#492
PR-#494

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
PR Compliance ID 1560502 requires test code to reside under a root-level SeoToolkit.Tests/
directory. The PR adds/changes NUnit tests in files located at
src/SeoToolkit.Tests/SeoToolkit.Tests/..., demonstrating test code is not placed in the required
directory.

Rule 1560502: Place test code in SeoToolkit.Tests directory
src/SeoToolkit.Tests/SeoToolkit.Tests/RedirectTests.cs[237-263]
src/SeoToolkit.Tests/SeoToolkit.Tests/SitemapMiddlewareTests.cs[295-357]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Compliance requires all automated test source files/projects to live under a root-level `SeoToolkit.Tests/` directory, but this PR modifies tests under `src/SeoToolkit.Tests/...`.

## Issue Context
The affected files contain NUnit tests (`[Test]`) and currently reside in `src/SeoToolkit.Tests/SeoToolkit.Tests/`, which does not match the mandated root-level `SeoToolkit.Tests/` location.

## Fix Focus Areas
- src/SeoToolkit.Tests/SeoToolkit.Tests/RedirectTests.cs[237-263]
- src/SeoToolkit.Tests/SeoToolkit.Tests/SitemapMiddlewareTests.cs[295-357]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

To customize comments, go to the Qodo configuration screen, or learn more in the docs.

Qodo Logo

Comment on lines +41 to 45
if (context.Request.Path.Value?.EndsWith(".xml", StringComparison.OrdinalIgnoreCase) != true)
{
await _next.Invoke(context);
return;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Remediation recommended

2. Trailing-slash xml bypass 🐞 Bug ≡ Correctness

SitemapMiddleware rejects requests like "/sitemap-index.xml/" or "/sitemap-index/sitemap.xml/"
because the new pre-guard checks EndsWith(".xml") without trimming "/", even though
IsSitemapIndexRequest later trims trailing slashes for matching. This makes trailing-slash variants
of sitemap URLs fall through to _next unexpectedly.
Agent Prompt
## Issue description
`SitemapMiddleware` uses `EndsWith(".xml")` as a cheap pre-guard, but it does not normalize trailing slashes. As a result, requests that are effectively XML endpoints but end with `/` never reach the later matching logic (which *does* trim `/`).

## Issue Context
`IsSitemapIndexRequest(...)` already compares using `requestPath.Value?.TrimEnd('/')`, indicating the intention is to treat trailing slashes as equivalent.

## Fix Focus Areas
- src/SeoToolkit.Umbraco.Sitemap.Core/Middleware/SitemapMiddleware.cs[41-57]
- src/SeoToolkit.Umbraco.Sitemap.Core/Middleware/SitemapMiddleware.cs[110-119]

## Suggested change
Normalize once at the start (e.g., `var path = context.Request.Path.Value?.TrimEnd('/')`) and use `path` for:
- the initial `.xml` pre-guard
- `isSitemapRequest`
- `IsSitemapIndexRequest` comparisons (or pass the normalized PathString/value)

## Tests
Add/extend tests to cover trailing-slash variants for both:
- configured index filename (`/sitemap-index.xml/`)
- configured index folder (`/sitemap-index/sitemap.xml/`)

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +41 to +57
if (context.Request.Path.Value?.EndsWith(".xml", StringComparison.OrdinalIgnoreCase) != true)
{
await _next.Invoke(context);
return;
}

var settings = _sitemapConfigurationService.GetSettings();

var isSitemapRequest = context.Request.Path.Value.EndsWith("/sitemap.xml", StringComparison.OrdinalIgnoreCase);
var isConfiguredIndexRequest = !string.IsNullOrWhiteSpace(settings.SitemapIndexPath)
&& IsSitemapIndexRequest(context.Request.Path, settings.SitemapIndexPath);

if (!isSitemapRequest && !isConfiguredIndexRequest)
{
await _next.Invoke(context);
return;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Informational

3. Settings read on any xml 🐞 Bug ➹ Performance

SitemapMiddleware now calls GetSettings() for every request ending in ".xml" even when it later
determines the request is neither a sitemap.xml nor the configured index path and forwards to _next.
This introduces extra configuration resolution work on unrelated XML endpoints compared to the
previous sitemap-only guard.
Agent Prompt
## Issue description
`GetSettings()` is invoked before confirming the request is sitemap-related, so all `.xml` requests incur the settings lookup.

## Issue Context
The broadened `.xml` pre-guard is needed to support an index filename like `sitemap-index.xml`, but settings lookup can often be avoided for unrelated `.xml` requests.

## Fix Focus Areas
- src/SeoToolkit.Umbraco.Sitemap.Core/Middleware/SitemapMiddleware.cs[41-57]

## Suggested change
Consider a two-phase guard:
1) Normalize path and check if it ends with `/sitemap.xml`; if not, only then consider the configured index filename/path.
2) Only call `GetSettings()` when the request is either:
   - a `/sitemap.xml` request, or
   - a candidate for the configured index (e.g., ends with `.xml` and could match the configured index filename).

If `GetSettings()` is cheap/cached this is optional; otherwise it reduces unnecessary work on non-sitemap XML endpoints.

## Tests
Add a test for an arbitrary `.xml` path (e.g. `/rss.xml`) verifying `_next` is called and (optionally) settings lookup is not performed.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +33 to +35
if (!rootNode.IsPublished(domain.Culture))
continue;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Remediation recommended

4. Un-guarded culture ispublished 🐞 Bug ☼ Reliability

SitemapIndexGenerator unconditionally calls rootNode.IsPublished(domain.Culture) without checking
for null/whitespace culture, unlike SitemapGenerator which guards IsPublished(culture) behind a
string.IsNullOrWhiteSpace check. This creates inconsistent behavior for empty/unspecified cultures
and relies on IsPublished handling such values safely.
Agent Prompt
## Issue description
`SitemapIndexGenerator.Generate()` calls `rootNode.IsPublished(domain.Culture)` without guarding `domain.Culture` for null/empty/whitespace.

## Issue Context
Elsewhere (SitemapGenerator) the code explicitly avoids calling `IsPublished(culture)` when the culture string is empty, indicating empty cultures are a possibility that should be handled.

## Fix Focus Areas
- src/SeoToolkit.Umbraco.Sitemap.Core/Common/SitemapIndexGenerator/SitemapIndexGenerator.cs[27-39]
- src/SeoToolkit.Umbraco.Sitemap.Core/Common/SitemapGenerators/SitemapGenerator.cs[132-136]

## Suggested change
Update the check to something like:
- `if (!string.IsNullOrWhiteSpace(domain.Culture)) { if (!rootNode.IsPublished(domain.Culture)) continue; }`
- otherwise decide on intended invariant behavior (e.g., `if (!rootNode.IsPublished()) continue;`)

## Tests
Add a unit/integration test that covers a domain with an empty/unspecified culture and verifies the generator does not throw and produces expected output.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines 133 to +135
var hideFromSitemap = contentOverride?.ExcludeFromSitemap == true
|| (docTypeSettings?.HideFromSitemap ?? false);
|| (docTypeSettings?.HideFromSitemap ?? false)
|| (!string.IsNullOrWhiteSpace(culture) && !content.IsPublished(culture));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Informational

5. Url computed when excluded 🐞 Bug ➹ Performance

SitemapGenerator now flags items as HideFromSitemap when !content.IsPublished(culture), but it still
calls content.Url(culture, UrlMode.Absolute) before the hideFromSitemap check prevents adding the
node. This defeats the intent of excluding unpublished variants early and performs URL resolution
work even when the node will be discarded.
Agent Prompt
## Issue description
Even when `hideFromSitemap` is true (including the newly added `!content.IsPublished(culture)` case), the code still constructs `SitemapNodeItem` with `content.Url(culture, UrlMode.Absolute)`.

## Issue Context
The item is only added inside `if (!hideFromSitemap)`, so URL resolution and object creation for hidden nodes is unnecessary.

## Fix Focus Areas
- src/SeoToolkit.Umbraco.Sitemap.Core/Common/SitemapGenerators/SitemapGenerator.cs[132-187]

## Suggested change
Move URL resolution and `SitemapNodeItem` construction inside the `if (!hideFromSitemap)` block, e.g.:
- compute `hideFromSitemap`
- `if (hideFromSitemap) { /* optionally skip recursion depending on desired behavior */ } else { var url = content.Url(...); var item = new SitemapNodeItem(url) { ... }; items.Add(item); }`

## Tests
Add a test case for a culture where a node is not published ensuring the generator does not attempt URL generation for that node (if test harness can observe calls/behavior).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

@patrickdemooij9
patrickdemooij9 merged commit fb26f79 into dev/main Jul 31, 2026
1 check passed
@patrickdemooij9
patrickdemooij9 deleted the feature/variousFixes branch July 31, 2026 20:33
patrickdemooij9 added a commit that referenced this pull request Aug 15, 2026
Co-authored-by: patrickdemooij9 <11466511+patrickdemooij9@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant