Skip to content

Preserve an explicitly requested trailing slash in collection sitemap URLs #3082

Description

@HardyNing

EmDash trailing-slash sitemap policy proposal

Status and scope

This is a local, upstream-ready issue/PR draft based solely on the installed
emdash@0.28.1 source. It proposes no Hikaridia deployment, project sitemap
replacement, node_modules patch, or dependency upgrade.

Problem statement

An Astro site may intentionally make its public canonical and hreflang URLs
end in /. Hikaridia does this through normalizeTrailingSlash() in
src/i18n/ui.ts:636 and uses it for canonical URLs in
src/layouts/Base.astro:87 and locale alternates in src/layouts/Base.astro:78.

EmDash collection sitemaps currently generate the same content URL without a
trailing slash. In installed emdash@0.28.1:

  • src/astro/routes/sitemap-[collection].xml.ts:98-108 resolves every entry
    from its collection url_pattern, then localizes it before emitting <loc>;
  • src/i18n/resolve.ts:49-65 (interpolateUrlPattern) removes a terminal /;
  • src/i18n/resolve.ts:92-98 calls normalizePath; and
  • src/i18n/resolve.ts:184-189 removes a terminal / again.

Consequently, a canonical such as
/en/blog/how-to-verify-lab-grown-diamond-report/ can disagree with the
collection sitemap location /en/blog/how-to-verify-lab-grown-diamond-report.
The mismatch affects sitemap <loc> and sitemap hreflang alternates.

Minimal backward-compatible API/design

Add an explicit optional sitemap URL policy to the EmDash Astro integration:

emdash({
  sitemap: { trailingSlash: "preserve" }, // default remains "never"
});

Semantics:

  • "never" is the default and preserves exactly the current 0.28.1
    no-trailing-slash output.
  • "preserve" retains a terminal slash deliberately supplied by a collection
    urlPattern, including after locale-prefix construction.
  • This setting applies only to collection sitemap content URLs and their
    sitemap hreflang / x-default alternates. It does not change routing,
    canonical generation, redirects, media URLs, robots, or sitemap-index URLs.
  • The collection remains responsible for expressing policy, for example
    /blog/{slug}/; patterns without a terminal slash stay slashless even under
    "preserve".

This makes output explicit, preserves existing installations by default, and
avoids inferring a site-wide routing policy from Astro internals.

File-level upstream patch plan (not applied)

  1. packages/core/src/astro/integration/runtime.ts and the public EmDash
    config types: add and validate sitemap.trailingSlash?: "never" | "preserve";
    default it to "never".
  2. packages/core/src/i18n/resolve.ts: add an explicit resolver option that
    permits preservation of a terminal slash. It must retain the current default
    behavior for all existing callers.
  3. packages/core/src/astro/routes/sitemap-[collection].xml.ts: pass that
    option only while resolving sitemap content URLs and all sibling/x-default
    alternates. Do not apply it to buildSeoImageUrl().
  4. packages/core/src/astro/routes/sitemap.xml.ts: no functional URL change;
    retain child sitemap index URLs without trailing slashes.
  5. EmDash configuration and sitemap documentation: document default behavior,
    the preserve mode, and the requirement that a collection urlPattern
    itself ends in / to opt in.

Test matrix

Run each case with trailingSlash: "never" and "preserve". In preserve
mode, use terminal-slash patterns for the positive cases and a no-slash pattern
for the compatibility case.

Collection / data Locales Assertions
posts, published and indexable ja, en, zh <loc> equals the intended canonical URL; locale prefix rules are retained; all sitemap hreflang values and x-default equal their matching canonical forms.
diamonds, published and indexable ja, en, zh Same URL and alternate assertions; proves the policy is collection-generic.
Future SEO-enabled collection ja, en, zh Discovery through has_seo = 1, URL pattern interpolation, and locale output work without a route-specific allowlist.
Translation group with image and timestamp ja, en, zh Preserve <image:loc> exactly, emit W3C <lastmod>, retain all sibling alternates and x-default.
Excluded rows any Draft, deleted, and seo_no_index = 1 rows remain absent.
Compatibility any Default and explicit never retain no-trailing-slash collection sitemap URLs; preserve mode does not add / when the pattern has none.

Integration assertions:

  • Rendered document canonical/hreflang and matching sitemap entry are byte-equal
    URLs for each representative route.
  • Root sitemap index retains its sitemap-{collection}.xml child URLs.
  • Existing collection eligibility, 50,000-entry cap, image entries, XML
    escaping, error responses, cache headers, and locale-unroutable exclusion
    remain unchanged.

Installed-source evidence

Installed source (emdash@0.28.1) Evidence
src/api/handlers/seo.ts:98-187 Selects every has_seo = 1 collection, retaining only published, non-deleted, indexable rows and returning url_pattern, locale, translation group, image, and timestamps.
src/astro/routes/sitemap-[collection].xml.ts:45-201 Renders per-collection sitemap XML, images, sibling hreflang, and x-default.
src/i18n/resolve.ts:49-65 Interpolates {slug}/{id} and strips terminal slash.
src/i18n/resolve.ts:92-98,184-189 Localizes a path and normalizes it by stripping terminal slash.
src/astro/integration/routes.ts:65-72,793-805 Provides explicit project-route bypass for root sitemap.xml, but unconditionally injects the collection dynamic sitemap route.
package.json:32; package-lock.json:1411 Declares and resolves emdash@0.28.1.

Non-goals

  • No Hikaridia production or preview deployment.
  • No project-owned sitemap replacement or middleware rewrite.
  • No edit to node_modules and no maintained local patch mechanism.
  • No dependency/lockfile upgrade.
  • No changes to routing, redirects, content, D1, Workers, Cloudflare, CMS, or
    EmDash SEO eligibility behavior.

Proposed upstream issue / PR text

Title: Preserve an explicitly requested trailing slash in collection sitemap URLs

Summary: Collection sitemap URLs currently remove terminal slashes in both
interpolateUrlPattern() and localizePath() normalization. This conflicts
with Astro sites whose canonical and hreflang policy deliberately uses trailing
slashes. Please add the backwards-compatible sitemap.trailingSlash policy
described above, defaulting to the current no-slash behavior, and cover it with
collection- and locale-generic sitemap tests.

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions