@pas7/nextjs-sitemap-hreflang
v0.7.3
Published
Add and validate hreflang alternates + x-default for Next.js sitemaps (App Router / MetadataRoute) with a tiny library + CLI postbuild fixer.
Downloads
707
Maintainers
Readme
@pas7/nextjs-sitemap-hreflang
Routing-agnostic hreflang toolkit for Next.js sitemaps:
- build hreflang alternates for App Router
MetadataRoute.Sitemap - inject/fix hreflang directly in generated XML
- validate sitemap hreflang in CI
- support mixed content pipelines (
.ts,.json,.md/.mdx, CMS)
Install
npm i @pas7/nextjs-sitemap-hreflangMigration guide (unscoped -> scoped)
npm uninstall nextjs-sitemap-hreflang
npm i @pas7/nextjs-sitemap-hreflangUpdate imports:
// before
import { withHreflang } from "nextjs-sitemap-hreflang";
// after
import { withHreflang } from "@pas7/nextjs-sitemap-hreflang";CLI binary stays the same:
npx nextjs-sitemap-hreflang check --fail-on-missingQuick start: App Router
import type { MetadataRoute } from "next";
import {
withHreflangFromRouting,
routingPrefixAsNeeded,
} from "@pas7/nextjs-sitemap-hreflang";
const routing = routingPrefixAsNeeded({
defaultLocale: "en",
locales: ["en", "uk", "de"],
});
export default function sitemap(): MetadataRoute.Sitemap {
const entries: MetadataRoute.Sitemap = [
{ url: "https://example.com/blog" },
{ url: "https://example.com/about" },
];
return withHreflangFromRouting(entries, routing, {
baseUrl: "https://example.com",
ensureXDefault: true,
});
}Next.js App Router + static export (sitemap.xml)
Use library generation + XML validation in CI:
next build
npx nextjs-sitemap-hreflang check --fail-on-missingOptional postbuild fix step:
npx nextjs-sitemap-hreflang inject --out public/sitemap.xmlAuto-detect order when --in is not provided:
public/sitemap.xmlout/sitemap.xmlsitemap.xml
Prefer explicit source when needed:
npx nextjs-sitemap-hreflang check --fail-on-missing --prefer out
npx nextjs-sitemap-hreflang inject --prefer public --out public/sitemap.xmlGenerate machine-readable CI report:
npx nextjs-sitemap-hreflang check --fail-on-missing --prefer out --json > report.jsonNext.js Full SEO Stack (App Router)
app/sitemap.ts:
import type { MetadataRoute } from "next";
import { routingPAS7, withHreflangFromRouting } from "@pas7/nextjs-sitemap-hreflang";
const baseUrl = "https://example.com";
const routing = routingPAS7({
defaultLocale: "en",
locales: ["en", "uk", "de", "it", "hr"],
suffixPaths: ["/blog", "/projects", "/services", "/cases", "/contact", "/about", "/privacy", "/terms"],
detailPathPattern: /^\/(blog|projects|services|cases)\//,
});
export default function sitemap(): MetadataRoute.Sitemap {
return withHreflangFromRouting(
[
{ url: `${baseUrl}/` },
{ url: `${baseUrl}/blog` },
{ url: `${baseUrl}/blog/en/hello-world` },
{ url: `${baseUrl}/contact` },
],
routing,
{ baseUrl, ensureXDefault: true },
);
}app/robots.ts:
import type { MetadataRoute } from "next";
export default function robots(): MetadataRoute.Robots {
return {
rules: { userAgent: "*", allow: "/" },
sitemap: "https://example.com/sitemap.xml",
};
}CI script:
next build
npx nextjs-sitemap-hreflang check --fail-on-missingUniversal manifest helper (.ts / .json / .md)
If your pipeline already outputs slug + locales + date, use:
import { createSitemapEntriesFromManifest } from "@pas7/nextjs-sitemap-hreflang";
const entries = createSitemapEntriesFromManifest(blogManifest, {
baseUrl: "https://pas7.com.ua",
sectionPath: "/blog",
defaultLocale: "en",
routeStyle: "locale-segment", // /blog/en/slug
});If your content keeps images in nested fields (for example hero.cover + section screenshots), pass imagesFor so the sitemap entry includes all image URLs:
const entries = createSitemapEntriesFromManifest(posts, {
baseUrl: "https://pas7.com.ua",
sectionPath: "/blog",
defaultLocale: "en",
routeStyle: "locale-segment",
imagesFor: (post) => [
post.hero.cover.src,
...post.sections.flatMap((section) => section.screenshots?.map((s) => s.src) ?? []),
],
});routingPAS7 with suffixPaths and prefixPaths
import { routingPAS7 } from "@pas7/nextjs-sitemap-hreflang";
const routing = routingPAS7({
defaultLocale: "en",
locales: ["en", "uk", "de"],
// /blog/uk, /contact/uk
suffixPaths: ["/blog", "/projects", "/services", "/cases", "/contact", "/about", "/privacy", "/terms"],
// /uk/about (if needed for some sections)
prefixPaths: ["/about"],
// keeps highest priority for locale-segment detail pages
detailPathPattern: /^\/(blog|projects|services|cases)\//,
});Hybrid recipes:
- Home pages: prefix-as-needed (
/,/uk,/de) - Content hubs and static pages: suffix locale (
/blog/uk,/contact/uk) - Detail pages: locale segment (
/blog/en/slug,/blog/uk/slug) - Optional mixed prefix pages via
prefixPaths(/uk/about)
Routing priority inside routingPAS7:
detailPathPatternsuffixPaths(or legacyhubPaths)prefixPaths- fallback prefix-as-needed
CLI
inject
npx nextjs-sitemap-hreflang inject \
--x-default loc \
--canonical-locale en \
--prefer public \
--order canonical-first \
--trailing-slash nevercheck
npx nextjs-sitemap-hreflang check \
--prefer out \
--origin-policy same \
--fail-on-missingJSON report format
--json output is stable and includes:
okissues[](withcode,entryUrl,message,suggestion)summary.byCodeinputPathtimingMs
Exit codes
0: OK2: validation errors (--fail-on-missing)4: input not found5: invalid XML input
Release and npm publish
Single workflow in .github/workflows/ci.yml:
- add changeset (
npm run changeset) - push to
main - workflow runs
ci -> release - version/tag/release/npm publish are automated
Required secret: NPM_TOKEN.
Contribution policy (required)
- Any user-facing code change must include a changeset (
.changeset/*.md). - Any API/feature behavior change must include
README.mdupdates in the same PR. - CI enforces both rules on pull requests.
Maintained by PAS7 Studio
- Website: https://pas7.com.ua/
- Blog: https://pas7.com.ua/blog
- Contact: https://pas7.com.ua/contact
License
MIT, PAS7 Studio
