{"id":13818903,"url":"https://github.com/jasongitmail/super-sitemap","last_synced_at":"2025-05-15T23:08:11.168Z","repository":{"id":194680186,"uuid":"691616056","full_name":"jasongitmail/super-sitemap","owner":"jasongitmail","description":"SvelteKit sitemap focused on ease of use and making it impossible to forget to add your paths.","archived":false,"fork":false,"pushed_at":"2024-12-10T14:55:22.000Z","size":539,"stargazers_count":186,"open_issues_count":2,"forks_count":3,"subscribers_count":2,"default_branch":"main","last_synced_at":"2025-05-07T09:02:39.068Z","etag":null,"topics":["seo","sitemap","sitemap-generator","sitemap-xml","supersitemap","svelte","svelte-library","sveltejs","sveltekit","typescript"],"latest_commit_sha":null,"homepage":"https://npmjs.com/package/super-sitemap","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/jasongitmail.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2023-09-14T14:35:19.000Z","updated_at":"2025-05-03T06:29:37.000Z","dependencies_parsed_at":"2023-12-15T15:42:47.850Z","dependency_job_id":"c975b3db-63b6-4482-b93a-6e075af0cf8d","html_url":"https://github.com/jasongitmail/super-sitemap","commit_stats":{"total_commits":191,"total_committers":4,"mean_commits":47.75,"dds":"0.10994764397905754","last_synced_commit":"8c27554802a11ba08b26c1c038eb32466fc4d94a"},"previous_names":["jasongitmail/sk-sitemap","jasongitmail/super-sitemap"],"tags_count":26,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jasongitmail%2Fsuper-sitemap","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jasongitmail%2Fsuper-sitemap/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jasongitmail%2Fsuper-sitemap/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jasongitmail%2Fsuper-sitemap/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/jasongitmail","download_url":"https://codeload.github.com/jasongitmail/super-sitemap/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":254436949,"owners_count":22070947,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["seo","sitemap","sitemap-generator","sitemap-xml","supersitemap","svelte","svelte-library","sveltejs","sveltekit","typescript"],"created_at":"2024-08-04T08:00:35.123Z","updated_at":"2025-05-15T23:08:06.122Z","avatar_url":"https://github.com/jasongitmail.png","language":"TypeScript","funding_links":[],"categories":["TypeScript"],"sub_categories":[],"readme":"\u003cdiv align=\"center\"\u003e\n  \u003cimg src=\"https://github.com/user-attachments/assets/7d897ca4-a54f-4fba-91a8-549a2e61117b\" alt=\"Svelte Super Sitemap\"\u003e\n\n  \u003cp\u003eSvelteKit sitemap focused on ease of use and \u003cbr\u003emaking it impossible to forget to add your paths.\u003c/p\u003e\n\n  \u003ca href=\"https://github.com/jasongitmail/super-sitemap/blob/main/LICENSE\"\u003e\n    \u003cimg alt=\"license badge\" src=\"https://img.shields.io/npm/l/super-sitemap?color=limegreen\"\u003e\n  \u003c/a\u003e\n  \u003ca href=\"https://www.npmjs.com/package/super-sitemap\"\u003e\n    \u003cimg alt=\"npm badge\" src=\"https://img.shields.io/npm/v/super-sitemap?color=limegreen\"\u003e\n  \u003c/a\u003e\n  \u003ca href=\"https://github.com/jasongitmail/super-sitemap/actions/workflows/ci.yml\"\u003e\n    \u003cimg alt=\"unit tests badge\" src=\"https://img.shields.io/github/actions/workflow/status/jasongitmail/super-sitemap/ci.yml?label=tests\"\u003e\n  \u003c/a\u003e\n\u003c/div\u003e\n\n**\u003ch3\u003ev1.0 is released! 🎉🚀\u003c/h3\u003e The only breaking changes from v0.15.0 are to 1.) rename `priority` to `defaultPriority` and 2.) rename `changefreq` to `defaultChangefreq` in your sitemap config. See [changelog](#changelog).**\n\n## Table of Contents\n\n- [Features](#features)\n- [Installation](#installation)\n- [Usage](#usage)\n  - [Basic example](#basic-example)\n  - [The \"everything\" example](#the-everything-example)\n  - [Sitemap Index](#sitemap-index)\n  - [Param Values](#param-values)\n  - [Optional Params](#optional-params)\n  - [`processPaths()` callback](#processpaths-callback)\n  - [i18n](#i18n)\n  - [Sampled URLs](#sampled-urls)\n  - [Sampled Paths](#sampled-paths)\n- [Robots.txt](#robotstxt)\n- [Playwright test](#playwright-test)\n- [Tip: Querying your database for param values using SQL](#tip-querying-your-database-for-param-values-using-sql)\n- [Example sitemap output](#example-sitemap-output)\n- [Changelog](#changelog)\n\n## Features\n\n- 🤓 Supports any rendering method.\n- 🪄 Automatically collects routes from `/src/routes` using Vite + data for route\n  parameters provided by you.\n- 🧠 Easy maintenance–accidental omission of data for parameterized routes\n  throws an error and requires the developer to either explicitly exclude the\n  route pattern or provide an array of data for that param value.\n- 👻 Exclude specific routes or patterns using regex patterns (e.g.\n  `^/dashboard.*`, paginated URLs, etc).\n- 🚀 Defaults to 1h CDN cache, no browser cache.\n- 💆 Set custom headers to override [default headers](https://github.com/jasongitmail/super-sitemap/blob/main/src/lib/sitemap.ts#L142):\n  `sitemap.response({ headers: {'cache-control: 'max-age=0, s-maxage=60'} })`.\n- 💡 Google, and other modern search engines, [ignore `priority` and\n  `changefreq`](https://developers.google.com/search/docs/crawling-indexing/sitemaps/build-sitemap#xml)\n  and use their own heuristics to determine when to crawl pages on your site. As\n  such, these properties are not included by default to minimize KB size and\n  enable faster crawling. Optionally, you can enable them like so:\n  `sitemap.response({ defaultChangefreq: 'daily', defaultPriority: 0.7 })`.\n- 🗺️ [Sitemap indexes](#sitemap-index)\n- 🌎 [i18n](#i18n)\n- 🧪 Well tested.\n- 🫶 Built with TypeScript.\n\n## Installation\n\n`npm i -D super-sitemap`\n\nor\n\n`bun add -d super-sitemap`\n\nThen see the [Usage](#usage), [Robots.txt](#robotstxt), \u0026 [Playwright Test](#playwright-test) sections.\n\n## Usage\n\n## Basic example\n\nJavaScript:\n\n```js\n// /src/routes/sitemap.xml/+server.js\nimport * as sitemap from 'super-sitemap';\n\nexport const GET = async () =\u003e {\n  return await sitemap.response({\n    origin: 'https://example.com',\n  });\n};\n```\n\nTypeScript:\n\n```ts\n// /src/routes/sitemap.xml/+server.ts\nimport * as sitemap from 'super-sitemap';\nimport type { RequestHandler } from '@sveltejs/kit';\n\nexport const GET: RequestHandler = async () =\u003e {\n  return await sitemap.response({\n    origin: 'https://example.com',\n  });\n};\n```\n\nAlways include the `.xml` extension on your sitemap route name–e.g. `sitemap.xml`. This ensures your web server always sends the correct `application/xml` content type even if you decide to prerender your sitemap to static files.\n\n## The \"everything\" example\n\n_**All aspects of the below example are optional, except for `origin` and\n`paramValues` to provide data for parameterized routes.**_\n\nJavaScript:\n\n```js\n// /src/routes/sitemap.xml/+server.js\nimport * as sitemap from 'super-sitemap';\nimport * as blog from '$lib/data/blog';\n\nexport const prerender = true; // optional\n\nexport const GET = async () =\u003e {\n  // Get data for parameterized routes however you need to; this is only an example.\n  let blogSlugs, blogTags;\n  try {\n    [blogSlugs, blogTags] = await Promise.all([blog.getSlugs(), blog.getTags()]);\n  } catch (err) {\n    throw error(500, 'Could not load data for param values.');\n  }\n\n  return await sitemap.response({\n    origin: 'https://example.com',\n    excludeRoutePatterns: [\n      '^/dashboard.*', // i.e. routes starting with `/dashboard`\n      '.*\\\\[page=integer\\\\].*', // i.e. routes containing `[page=integer]`–e.g. `/blog/2`\n      '.*\\\\(authenticated\\\\).*', // i.e. routes within a group\n    ],\n    paramValues: {\n      // paramValues can be a 1D array of strings\n      '/blog/[slug]': blogSlugs, // e.g. ['hello-world', 'another-post']\n      '/blog/tag/[tag]': blogTags, // e.g. ['red', 'green', 'blue']\n\n      // Or a 2D array of strings\n      '/campsites/[country]/[state]': [\n        ['usa', 'new-york'],\n        ['usa', 'california'],\n        ['canada', 'toronto'],\n      ],\n\n      // Or an array of ParamValue objects\n      '/athlete-rankings/[country]/[state]': [\n        {\n          values: ['usa', 'new-york'], // required\n          lastmod: '2025-01-01T00:00:00Z', // optional\n          changefreq: 'daily', // optional\n          priority: 0.5, // optional\n        },\n        {\n          values: ['usa', 'california'],\n          lastmod: '2025-01-01T00:00:00Z',\n          changefreq: 'daily',\n          priority: 0.5,\n        },\n      ],\n    },\n    headers: {\n      'custom-header': 'foo', // case insensitive; xml content type \u0026 1h CDN cache by default\n    },\n    additionalPaths: [\n      '/foo.pdf', // for example, to a file in your static dir\n    ],\n    defaultChangefreq: 'daily',\n    defaultPriority: 0.7,\n    sort: 'alpha', // default is false; 'alpha' sorts all paths alphabetically.\n    processPaths: (paths) =\u003e {\n      // Optional callback to allow arbitrary processing of your path objects. See the\n      // processPaths() section of the README.\n      return paths;\n    },\n  });\n};\n```\n\nTypeScript:\n\n```ts\n// /src/routes/sitemap.xml/+server.ts\nimport type { RequestHandler } from '@sveltejs/kit';\nimport * as sitemap from 'super-sitemap';\nimport * as blog from '$lib/data/blog';\n\nexport const prerender = true; // optional\n\nexport const GET: RequestHandler = async () =\u003e {\n  // Get data for parameterized routes however you need to; this is only an example.\n  let blogSlugs, blogTags;\n  try {\n    [blogSlugs, blogTags] = await Promise.all([blog.getSlugs(), blog.getTags()]);\n  } catch (err) {\n    throw error(500, 'Could not load data for param values.');\n  }\n\n  return await sitemap.response({\n    origin: 'https://example.com',\n    excludeRoutePatterns: [\n      '^/dashboard.*', // i.e. routes starting with `/dashboard`\n      '.*\\\\[page=integer\\\\].*', // i.e. routes containing `[page=integer]`–e.g. `/blog/2`\n      '.*\\\\(authenticated\\\\).*', // i.e. routes within a group\n    ],\n    paramValues: {\n      // paramValues can be a 1D array of strings\n      '/blog/[slug]': blogSlugs, // e.g. ['hello-world', 'another-post']\n      '/blog/tag/[tag]': blogTags, // e.g. ['red', 'green', 'blue']\n\n      // Or a 2D array of strings\n      '/campsites/[country]/[state]': [\n        ['usa', 'new-york'],\n        ['usa', 'california'],\n        ['canada', 'toronto'],\n      ],\n\n      // Or an array of ParamValue objects\n      '/athlete-rankings/[country]/[state]': [\n        {\n          values: ['usa', 'new-york'], // required\n          lastmod: '2025-01-01T00:00:00Z', // optional\n          changefreq: 'daily', // optional\n          priority: 0.5, // optional\n        },\n        {\n          values: ['usa', 'california'],\n          lastmod: '2025-01-01T00:00:00Z',\n          changefreq: 'daily',\n          priority: 0.5,\n        },\n      ],\n    },\n    headers: {\n      'custom-header': 'foo', // case insensitive; xml content type \u0026 1h CDN cache by default\n    },\n    additionalPaths: [\n      '/foo.pdf', // for example, to a file in your static dir\n    ],\n    defaultChangefreq: 'daily',\n    defaultPriority: 0.7,\n    sort: 'alpha', // default is false; 'alpha' sorts all paths alphabetically.\n    processPaths: (paths: sitemap.PathObj[]) =\u003e {\n      // Optional callback to allow arbitrary processing of your path objects. See the\n      // processPaths() section of the README.\n      return paths;\n    },\n  });\n};\n```\n\n## Sitemap Index\n\n_**You only need to enable or read this if you will have \u003e=50,000 URLs in your sitemap, which is the number\nrecommended by Google.**_\n\nYou can enable sitemap index support with just two changes:\n\n1. Rename your route to `sitemap[[page]].xml`\n2. Pass the page param via your sitemap config\n\nJavaScript:\n\n```js\n// /src/routes/sitemap[[page]].xml/+server.js\nimport * as sitemap from 'super-sitemap';\n\nexport const GET = async ({ params }) =\u003e {\n  return await sitemap.response({\n    origin: 'https://example.com',\n    page: params.page,\n    // maxPerPage: 45_000 // optional; defaults to 50_000\n  });\n};\n```\n\nTypeScript:\n\n```ts\n// /src/routes/sitemap[[page]].xml/+server.ts\nimport * as sitemap from 'super-sitemap';\nimport type { RequestHandler } from '@sveltejs/kit';\n\nexport const GET: RequestHandler = async ({ params }) =\u003e {\n  return await sitemap.response({\n    origin: 'https://example.com',\n    page: params.page,\n    // maxPerPage: 45_000 // optional; defaults to 50_000\n  });\n};\n```\n\n**Feel free to always set up your sitemap as a sitemap index, given it will work optimally whether you\nhave few or many URLs.**\n\nYour `sitemap.xml` route will now return a regular sitemap when your sitemap's total URLs is less than or equal\nto `maxPerPage` (defaults to 50,000 per the [sitemap\nprotocol](https://www.sitemaps.org/protocol.html)) or it will contain a sitemap index when exceeding\n`maxPerPage`.\n\nThe sitemap index will contain links to `sitemap1.xml`, `sitemap2.xml`, etc, which contain your\npaginated URLs automatically.\n\n```xml\n\u003csitemapindex xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\"\u003e\n  \u003csitemap\u003e\n    \u003cloc\u003ehttps://example.com/sitemap1.xml\u003c/loc\u003e\n  \u003c/sitemap\u003e\n  \u003csitemap\u003e\n    \u003cloc\u003ehttps://example.com/sitemap2.xml\u003c/loc\u003e\n  \u003c/sitemap\u003e\n  \u003csitemap\u003e\n    \u003cloc\u003ehttps://example.com/sitemap3.xml\u003c/loc\u003e\n  \u003c/sitemap\u003e\n\u003c/sitemapindex\u003e\n```\n\n## Param Values\n\nWhen specifying values for the params of your parameterized routes,\nyou can use any of the following types:\n`string[]`, `string[][]`, or `ParamValue[]`.\n\nExample:\n\n```ts\nparamValues: {\n  '/blog/[slug]': ['hello-world', 'another-post']\n  '/campsites/[country]/[state]': [\n    ['usa', 'colorado'],\n    ['canada', 'toronto']\n  ],\n  '/athlete-rankings/[country]/[state]': [\n    {\n      values: ['usa', 'new-york'], // required\n      lastmod: '2025-01-01T00:00:00Z', // optional\n      changefreq: 'daily', // optional\n      priority: 0.5, // optional\n    },\n    {\n      values: ['usa', 'california'],\n      lastmod: '2025-01-01T01:16:52Z',\n      changefreq: 'daily',\n      priority: 0.5,\n    },\n  ],\n},\n```\n\nIf any of the optional properties of `ParamValue` are not provided, the sitemap will use the default\nvalue. If a default value is not defined, the property will be excluded from that sitemap entry.\n\n## Optional Params\n\n_**You only need to read this if you want to understand how super sitemap handles optional params and why.**_\n\nSvelteKit allows you to create a route with one or more optional parameters like this:\n\n```text\nsrc/\n  routes/\n    something/\n      [[paramA]]/\n        [[paramB]]/\n          +page.svelte\n          +page.ts\n```\n\nYour app would then respond to HTTP requests for all of the following:\n\n- `/something`\n- `/something/foo`\n- `/something/foo/bar`\n\nConsequently, Super Sitemap will include all such path variations in your\nsitemap and will require you to either exclude these using `excludeRoutePatterns` or\nprovide param values for them using `paramValues`, within your sitemap\nconfig object.\n\n### For example:\n\n- `/something` will exist in your sitemap unless excluded with a pattern of\n  `/something$`.\n- `/something/[[paramA]]` must be either excluded using an `excludeRoutePattern` of\n  `.*/something/\\\\[\\\\[paramA\\\\]\\\\]$` _or_ appear within your config's\n  `paramValues` like this: `'/something/[[paramA]]': ['foo', 'foo2', 'foo3']`.\n- And `/something/[[paramA]]/[[paramB]]` must be either excluded using an\n  `excludeRoutePattern` of `.*/something/\\\\[\\\\[paramA\\\\]\\\\]/\\\\[\\\\[paramB\\\\]\\\\]$` _or_\n  appear within your config's `paramValues` like this: `'/something/[[paramA]]/[[paramB]]':\n[['foo','bar'], ['foo2','bar2'], ['foo3','bar3']]`.\n\nAlternatively, you can exclude ALL versions of this route by providing a single\nregex pattern within `excludeRoutePatterns` that matches all of them, such as\n`/something`; notice this do NOT end with a `$`, thereby allowing this pattern\nto match all 3 versions of this route.\n\nIf you plan to mix and match use of `excludeRoutePatterns` and `paramValues` for a\ngiven route that contains optional params, terminate all of your\n`excludeRoutePatterns` for that route with `$`, to target only the specific desired\nversions of that route.\n\n## processPaths() callback\n\n_**The `processPaths()` callback is powerful, but rarely needed.**_\n\nIt allows you to arbitrarily process the path objects for your site before they become XML, with the\nonly requirement that your callback function must return the expected type of\n[`PathObj[]`](https://github.com/jasongitmail/super-sitemap/blob/main/src/lib/sitemap.ts#L34).\n\nThis can be useful to do something bespoke that would not otherwise be possible. For example:\n\n1. Excluding a specific path, when `excludeRoutePatterns` based on the _route\n   pattern_ would be too broad. (For example, you might want to exclude a path\n   when you have not yet translated its content into one or more of your site’s\n   supported languages; e.g. to exclude only `/zh/about`, but retain all others\n   like `/about`, `/es/about`, etc.)\n2. Adding a trailing slash to URLs (not a recommended style, but possible).\n3. Appending paths from an external sitemap, like from a hosted headless blog\n   backend. However, you can also accomplish this by providing these within the\n   `additionalPaths` array in your super sitemap config, which is a more concise approach.\n\n`processPaths()` runs after all paths have been generated for your site, but prior to de-duplication\nof paths based on unique path names, sorting (if enabled by your config), and creation of XML.\n\nNote that `processPaths()` is intentionally NOT async. This design decision is\nto encourage a consistent pattern within the sitemap request handler where all HTTP\nrequests, including any to fetch param values from a database, [occur\ntogether using `Promise.all()`](\u003chttps://github.com/jasongitmail/super-sitemap/blob/main/src/routes/(public)/%5B%5Blang%5D%5D/sitemap%5B%5Bpage%5D%5D.xml/%2Bserver.ts#L14-L20\u003e), for best performance and consistent code pattern\namong super sitemap users for best DX.\n\n### Example code - to remove specific paths\n\n```ts\nreturn await sitemap.response({\n  // ...\n  processPaths: (paths: sitemap.PathObj[]) =\u003e {\n    const pathsToExclude = ['/zh/about', '/de/team'];\n    return paths.filter(({ path }) =\u003e !pathsToExclude.includes(path));\n  },\n});\n```\n\nNote: If using `excludeRoutePatterns`–which matches again the _route_ pattern–would\nbe sufficient for your needs, you should prefer it for performance reasons. This\nis because a site will have fewer routes than paths, consequently route-based\nexclusions are more performant than path-based exclusions. Although, the\ndifference will be inconsequential in virtually all cases, unless you have a\nvery large number of excluded paths and many millions of generated paths to\nsearch within.\n\n### Example code - to add trailing slashes\n\n```ts\nreturn await sitemap.response({\n  // ...\n  processPaths: (paths: sitemap.PathObj[]) =\u003e {\n    // Add trailing slashes to all paths. (This is just an example and not\n    // actually recommended. Using SvelteKit's default of no trailing slash is\n    // preferable because it provides consistency among all possible paths,\n    // even files like `/foo.pdf`.)\n    return paths.map(({ path, alternates, ...rest }) =\u003e {\n      const rtrn = { path: path === '/' ? path : `${path}/`, ...rest };\n\n      if (alternates) {\n        rtrn.alternates = alternates.map((alternate: sitemap.Alternate) =\u003e ({\n          ...alternate,\n          path: alternate.path === '/' ? alternate.path : `${alternate.path}/`,\n        }));\n      }\n\n      return rtrn;\n    });\n  },\n});\n```\n\n## i18n\n\nSuper Sitemap supports [multilingual site\nannotations](https://developers.google.com/search/blog/2012/05/multilingual-and-multinational-site)\nwithin your sitemap. This allows search engines to be aware of alternate\nlanguage versions of your pages.\n\n### Set up\n\n1. Create a directory named `[[lang]]` at `src/routes/[[lang]]`. Place any\n   routes that you intend to translate inside here.\n\n   - **This parameter must be named `lang`.**\n   - This parameter can specify a [param\n     matcher](https://kit.svelte.dev/docs/advanced-routing#matching), if\n     desired. For example: `src/routes/(public)/[[lang=lang]]`, when you defined\n     a param matcher at `src/params/lang.js`. The param matcher can have any\n     name as long as it uses only lowercase letters.\n   - This directory can be located within a route group, if desired, e.g.\n     `src/routes/(public)/[[lang]]`.\n   - Advanced: If you want to _require_ a language parameter as part of _all_\n     your urls, use single square brackets like `src/routes/[lang]` or\n     `src/routes/[lang=lang]`. Importantly, **if you take this approach, you\n     should redirect your index route (`/`) to one of your language-specific\n     index paths (e.g. `/en`, `/es`, etc)**, because a root url of `/` will not be\n     included in the sitemap when you have _required_ the language param to\n     exist. (The remainder of these docs will assume you are using an\n     _optional_ lang parameter.)\n\n2. Within your `sitemap.xml` route, update your Super Sitemap config object to\n   add a `lang` property specifying your desired languages.\n\n   ```js\n   lang: {\n     default: 'en',           // e.g. /about\n     alternates: ['zh', 'de'] // e.g. /zh/about, /de/about\n   }\n   ```\n\n   The default language will not appear in your URLs (e.g. `/about`). Alternate\n   languages will appear as part of the URLs within your sitemap (e.g.\n   `/zh/about`, `/de/about`).\n\n   These language properties accept any string value, but choose a valid\n   language code. They will appear in two places: 1.) as a slug within your\n   paths (e.g. `/zh/about`), and 2.) as `hreflang` attributes within the sitemap\n   output.\n\n   Note: If you used a _required_ lang param (e.g. `[lang]`), you can set\n   _any_ of your desired languages as the `default` and the rest as the `alternates`; they will _all_ be\n   processed in the same way though.\n\n3. Within your `sitemap.xml` route again, update your Super Sitemap config\n   object's `paramValues` to prepend `/[[lang]]` (or `/[[lang=lang]]`, `[lang]`, etc–whatever you used earlier) onto the property names of all routes you moved\n   into your `/src/routes/[[lang]]` directory, e.g.:\n\n   ```js\n   paramValues: {\n     '/[[lang]]/blog/[slug]': ['hello-world', 'post-2'], // was '/blog/[slug]'\n     '/[[lang]]/campsites/[country]/[state]': [ // was '/campsites/[country]/[state]'\n       ['usa', 'new-york'],\n       ['canada', 'toronto'],\n     ],\n   },\n   ```\n\n### Example\n\n1. Create `/src/routes/[[lang]]/about/+page.svelte` with any content.\n2. Assuming you have a [basic sitemap](#basic-example) set up at\n   `/src/routes/sitemap.xml/+server.ts`, add a `lang` property to your sitemap's\n   config object, as described in Step 2 in the previous section.\n3. Your `sitemap.xml` will then include the following:\n\n```xml\n  ...\n  \u003curl\u003e\n    \u003cloc\u003ehttps://example.com/about\u003c/loc\u003e\n    \u003cxhtml:link rel=\"alternate\" hreflang=\"en\" href=\"https://example.com/about\" /\u003e\n    \u003cxhtml:link rel=\"alternate\" hreflang=\"zh\" href=\"https://example.com/zh/about\" /\u003e\n    \u003cxhtml:link rel=\"alternate\" hreflang=\"de\" href=\"https://example.com/de/about\" /\u003e\n  \u003c/url\u003e\n  \u003curl\u003e\n    \u003cloc\u003ehttps://example.com/de/about\u003c/loc\u003e\n    \u003cxhtml:link rel=\"alternate\" hreflang=\"en\" href=\"https://example.com/about\" /\u003e\n    \u003cxhtml:link rel=\"alternate\" hreflang=\"zh\" href=\"https://example.com/zh/about\" /\u003e\n    \u003cxhtml:link rel=\"alternate\" hreflang=\"de\" href=\"https://example.com/de/about\" /\u003e\n  \u003c/url\u003e\n  \u003curl\u003e\n    \u003cloc\u003ehttps://example.com/zh/about\u003c/loc\u003e\n    \u003cxhtml:link rel=\"alternate\" hreflang=\"en\" href=\"https://example.com/about\" /\u003e\n    \u003cxhtml:link rel=\"alternate\" hreflang=\"zh\" href=\"https://example.com/zh/about\" /\u003e\n    \u003cxhtml:link rel=\"alternate\" hreflang=\"de\" href=\"https://example.com/de/about\" /\u003e\n  \u003c/url\u003e\n  ...\n```\n\n### Note on i18n\n\nSuper Sitemap handles creation of URLs within your sitemap, but it is\n_not_ an i18n library.\n\nYou need a separate i18n library to translate strings within your app. Just\nensure the library you choose allows a similar URL pattern as described here,\nwith a default language (e.g. `/about`) and lang slugs for alternate languages\n(e.g. `/zh/about`, `/de/about`).\n\n### Q\u0026A on i18n\n\n- **What about translated paths like `/about` (English), `/acerca` (Spanish), `/uber` (German)?**\n\n  Realistically, this would break the route patterns and assumptions that Super\n  Sitemap relies on to identify your routes, to know what language to use, and\n  to build the sitemap. \"Never say never\", but there are no plans to support this.\n\n## Sampled URLs\n\n_**`sampledUrls()` is an optional utility to be used in your Playwright tests. You do not need to read this if just getting started.**_\n\nSampled URLs provides a utility to obtain a sample URL for each unique route on your site–i.e.:\n\n1.  the URL for every static route (e.g. `/`, `/about`, `/pricing`, etc.), and\n2.  one URL for each parameterized route (e.g. `/blog/[slug]`)\n\nThis can be helpful for writing functional tests, performing SEO analyses of your public pages, \u0026\nsimilar.\n\nThis data is generated by analyzing your site's `sitemap.xml`, so keep in mind that it will not\ncontain any URLs excluded by `excludeRoutePatterns` in your sitemap config.\n\n```js\nimport { sampledUrls } from 'super-sitemap';\n\nconst urls = await sampledUrls('http://localhost:5173/sitemap.xml');\n// [\n//   'http://localhost:5173/',\n//   'http://localhost:5173/about',\n//   'http://localhost:5173/pricing',\n//   'http://localhost:5173/features',\n//   'http://localhost:5173/login',\n//   'http://localhost:5173/signup',\n//   'http://localhost:5173/blog',\n//   'http://localhost:5173/blog/hello-world',\n//   'http://localhost:5173/blog/tag/red',\n// ]\n```\n\n### Limitations\n\n1. Result URLs will not include any `additionalPaths` from your sitemap config because it's\n   impossible to identify those by a pattern given only your routes and `sitemap.xml` as inputs.\n2. `sampledUrls()` does not distinguish between routes that differ only due to a pattern matcher.\n   For example, `/foo/[foo]` and `/foo/[foo=integer]` will evaluated as `/foo/[foo]` and one sample\n   URL will be returned.\n\n### Designed as a testing utility\n\nBoth `sampledUrls()` and `sampledPaths()` are intended as utilities for use\nwithin your Playwright tests. Their design aims for developer convenience (i.e.\nno need to set up a 2nd sitemap config), not for performance, and they require a\nruntime with access to the file system like Node, to read your `/src/routes`. In\nother words, use for testing, not as a data source for production.\n\nYou can use it in a Playwright test like below, then you'll have `sampledPublicPaths` available to use within your tests in this file.\n\n```js\n// foo.test.js\nimport { expect, test } from '@playwright/test';\nimport { sampledPaths } from 'super-sitemap';\n\nlet sampledPublicPaths = [];\ntry {\n  sampledPublicPaths = await sampledPaths('http://localhost:4173/sitemap.xml');\n} catch (err) {\n  console.error('Error:', err);\n}\n\n// ...\n```\n\n## Sampled Paths\n\nSame as [Sampled URLs](#sampled-urls), except it returns paths.\n\n```js\nimport { sampledPaths } from 'super-sitemap';\n\nconst urls = await sampledPaths('http://localhost:5173/sitemap.xml');\n// [\n//   '/about',\n//   '/pricing',\n//   '/features',\n//   '/login',\n//   '/signup',\n//   '/blog',\n//   '/blog/hello-world',\n//   '/blog/tag/red',\n// ]\n```\n\n## Robots.txt\n\nIt's important to create a `robots.txt` so search engines know where to find your sitemap.\n\nYou can create it at `/static/robots.txt`:\n\n```text\nUser-agent: *\nAllow: /\n\nSitemap: https://example.com/sitemap.xml\n```\n\nOr, at `/src/routes/robots.txt/+server.ts`, if you have defined `PUBLIC_ORIGIN` within your\nproject's `.env` and want to access it:\n\n```ts\nimport * as env from '$env/static/public';\n\nexport const prerender = true;\n\nexport async function GET(): Promise\u003cResponse\u003e {\n  // prettier-ignore\n  const body = [\n    'User-agent: *',\n    'Allow: /',\n    '',\n    `Sitemap: ${env.PUBLIC_ORIGIN}/sitemap.xml`\n  ].join('\\n').trim();\n\n  const headers = {\n    'Content-Type': 'text/plain',\n  };\n\n  return new Response(body, { headers });\n}\n```\n\n## Playwright Test\n\nIt's recommended to add a Playwright test that calls your sitemap.\n\nFor pre-rendered sitemaps, you'll receive an error _at build time_ if your data param values are\nmisconfigured. But for non-prerendered sitemaps, your data is loaded when the sitemap is loaded, and\nconsequently a functional test is more important to confirm you have not misconfigured data for your\nparam values.\n\nFeel free to use or adapt this example test:\n\n```js\n// /src/tests/sitemap.test.js\n\nimport { expect, test } from '@playwright/test';\n\ntest('/sitemap.xml is valid', async ({ page }) =\u003e {\n  const response = await page.goto('/sitemap.xml');\n  expect(response.status()).toBe(200);\n\n  // Ensure XML is valid. Playwright parses the XML here and will error if it\n  // cannot be parsed.\n  const urls = await page.$$eval('url', (urls) =\u003e\n    urls.map((url) =\u003e ({\n      loc: url.querySelector('loc').textContent,\n      // changefreq: url.querySelector('changefreq').textContent, // if you enabled in your sitemap\n      // priority: url.querySelector('priority').textContent,\n    }))\n  );\n\n  // Sanity check\n  expect(urls.length).toBeGreaterThan(5);\n\n  // Ensure entries are in a valid format.\n  for (const url of urls) {\n    expect(url.loc).toBeTruthy();\n    expect(() =\u003e new URL(url.loc)).not.toThrow();\n    // expect(url.changefreq).toBe('daily');\n    // expect(url.priority).toBe('0.7');\n  }\n});\n```\n\n## Tip: Querying your database for param values using SQL\n\nAs a helpful tip, below are a few examples demonstrating how to query an SQL\ndatabase to obtain data to provide as `paramValues` for your routes:\n\n```SQL\n-- Route: /blog/[slug]\nSELECT slug FROM blog_posts WHERE status = 'published';\n\n-- Route: /blog/category/[category]\nSELECT DISTINCT LOWER(category) FROM blog_posts WHERE status = 'published';\n\n-- Route: /campsites/[country]/[state]\nSELECT DISTINCT LOWER(country), LOWER(state) FROM campsites;\n```\n\nUsing `DISTINCT` will prevent duplicates in your result set. Use this when your\ntable could contain multiple rows with the same params, like in the 2nd and 3rd\nexamples. This will be the case for routes that show a list of items.\n\nThen if your result is an array of objects, convert into an array of arrays of\nstring values:\n\n```js\nconst arrayOfArrays = resultFromDB.map((row) =\u003e Object.values(row));\n// [['usa','new-york'],['usa', 'california']]\n```\n\nThat's it.\n\nGoing in the other direction, i.e. when loading data for a component for your\nUI, your database query should typically lowercase both the URL param and value\nin the database during comparison–e.g.:\n\n```sql\n-- Obviously, remember to escape your `params.slug` values to prevent SQL injection.\nSELECT * FROM campsites WHERE LOWER(country) = LOWER(params.country) AND LOWER(state) = LOWER(params.state) LIMIT 10;\n```\n\n\u003cdetails id=\"example-sitemap-output\"\u003e\n  \u003csummary\u003e\u003ch2\u003eExample sitemap output\u003c/h2\u003e\u003c/summary\u003e\n\n```xml\n  \u003curlset\n    xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\"\n    xmlns:xhtml=\"http://www.w3.org/1999/xhtml\"\n  \u003e\n    \u003curl\u003e\n        \u003cloc\u003ehttps://example/\u003c/loc\u003e\n        \u003cchangefreq\u003edaily\u003c/changefreq\u003e\n        \u003cpriority\u003e0.7\u003c/priority\u003e\n    \u003c/url\u003e\n    \u003curl\u003e\n        \u003cloc\u003ehttps://example/about\u003c/loc\u003e\n        \u003cchangefreq\u003edaily\u003c/changefreq\u003e\n        \u003cpriority\u003e0.7\u003c/priority\u003e\n    \u003c/url\u003e\n    \u003curl\u003e\n        \u003cloc\u003ehttps://example/blog\u003c/loc\u003e\n        \u003cchangefreq\u003edaily\u003c/changefreq\u003e\n        \u003cpriority\u003e0.7\u003c/priority\u003e\n    \u003c/url\u003e\n    \u003curl\u003e\n        \u003cloc\u003ehttps://example/login\u003c/loc\u003e\n        \u003cchangefreq\u003edaily\u003c/changefreq\u003e\n        \u003cpriority\u003e0.7\u003c/priority\u003e\n    \u003c/url\u003e\n    \u003curl\u003e\n        \u003cloc\u003ehttps://example/pricing\u003c/loc\u003e\n        \u003cchangefreq\u003edaily\u003c/changefreq\u003e\n        \u003cpriority\u003e0.7\u003c/priority\u003e\n    \u003c/url\u003e\n    \u003curl\u003e\n        \u003cloc\u003ehttps://example/privacy\u003c/loc\u003e\n        \u003cchangefreq\u003edaily\u003c/changefreq\u003e\n        \u003cpriority\u003e0.7\u003c/priority\u003e\n    \u003c/url\u003e\n    \u003curl\u003e\n        \u003cloc\u003ehttps://example/signup\u003c/loc\u003e\n        \u003cchangefreq\u003edaily\u003c/changefreq\u003e\n        \u003cpriority\u003e0.7\u003c/priority\u003e\n    \u003c/url\u003e\n    \u003curl\u003e\n        \u003cloc\u003ehttps://example/support\u003c/loc\u003e\n        \u003cchangefreq\u003edaily\u003c/changefreq\u003e\n        \u003cpriority\u003e0.7\u003c/priority\u003e\n    \u003c/url\u003e\n    \u003curl\u003e\n        \u003cloc\u003ehttps://example/terms\u003c/loc\u003e\n        \u003cchangefreq\u003edaily\u003c/changefreq\u003e\n        \u003cpriority\u003e0.7\u003c/priority\u003e\n    \u003c/url\u003e\n    \u003curl\u003e\n        \u003cloc\u003ehttps://example/blog/hello-world\u003c/loc\u003e\n        \u003cchangefreq\u003edaily\u003c/changefreq\u003e\n        \u003cpriority\u003e0.7\u003c/priority\u003e\n    \u003c/url\u003e\n    \u003curl\u003e\n        \u003cloc\u003ehttps://example/blog/another-post\u003c/loc\u003e\n        \u003cchangefreq\u003edaily\u003c/changefreq\u003e\n        \u003cpriority\u003e0.7\u003c/priority\u003e\n    \u003c/url\u003e\n    \u003curl\u003e\n        \u003cloc\u003ehttps://example/blog/tag/red\u003c/loc\u003e\n        \u003cchangefreq\u003edaily\u003c/changefreq\u003e\n        \u003cpriority\u003e0.7\u003c/priority\u003e\n    \u003c/url\u003e\n    \u003curl\u003e\n        \u003cloc\u003ehttps://example/blog/tag/green\u003c/loc\u003e\n        \u003cchangefreq\u003edaily\u003c/changefreq\u003e\n        \u003cpriority\u003e0.7\u003c/priority\u003e\n    \u003c/url\u003e\n    \u003curl\u003e\n        \u003cloc\u003ehttps://example/blog/tag/blue\u003c/loc\u003e\n        \u003cchangefreq\u003edaily\u003c/changefreq\u003e\n        \u003cpriority\u003e0.7\u003c/priority\u003e\n    \u003c/url\u003e\n    \u003curl\u003e\n        \u003cloc\u003ehttps://example/campsites/usa/new-york\u003c/loc\u003e\n        \u003cchangefreq\u003edaily\u003c/changefreq\u003e\n        \u003cpriority\u003e0.7\u003c/priority\u003e\n    \u003c/url\u003e\n    \u003curl\u003e\n        \u003cloc\u003ehttps://example/campsites/usa/california\u003c/loc\u003e\n        \u003cchangefreq\u003edaily\u003c/changefreq\u003e\n        \u003cpriority\u003e0.7\u003c/priority\u003e\n    \u003c/url\u003e\n    \u003curl\u003e\n        \u003cloc\u003ehttps://example/campsites/canada/toronto\u003c/loc\u003e\n        \u003cchangefreq\u003edaily\u003c/changefreq\u003e\n        \u003cpriority\u003e0.7\u003c/priority\u003e\n    \u003c/url\u003e\n    \u003curl\u003e\n        \u003cloc\u003ehttps://example/foo.pdf\u003c/loc\u003e\n        \u003cchangefreq\u003edaily\u003c/changefreq\u003e\n        \u003cpriority\u003e0.7\u003c/priority\u003e\n    \u003c/url\u003e\n\u003c/urlset\u003e\n```\n\n\u003c/details\u003e\n\n## Changelog\n\n- `1.0.0` - BREAKING: `priority` renamed to `defaultPriority`, and `changefreq` renamed to `defaultChangefreq`. NON-BREAKING: Support for `paramValues` to contain either `string[]`, `string[][]`, or `ParamValueObj[]` values to allow per-path specification of `lastmod`, `changefreq`, and `priority`.\n- `0.15.0` - BREAKING: Rename `excludePatterns` to `excludeRoutePatterns`.\n- `0.14.20` - Adds [processPaths() callback](#processpaths-callback).\n- `0.14.19` - Support `.md` and `.svx` route extensions for msdvex users.\n- `0.14.17` - Support for param matchers (e.g. `[[lang=lang]]`) \u0026\n  required lang params (e.g. `[lang]`). Thanks @JadedBlueEyes \u0026 @epoxide!\n- `0.14.13` - Support route files named to allow [breaking out of a layout](https://kit.svelte.dev/docs/advanced-routing#advanced-layouts-breaking-out-of-layouts).\n- `0.14.12` - Adds [`i18n`](#i18n) support.\n- `0.14.11` - Adds [`optional params`](#optional-params) support.\n- `0.14.0` - Adds [`sitemap index`](#sitemap-index) support.\n- `0.13.0` - Adds [`sampledUrls()`](#sampled-urls) and [`sampledPaths()`](#sampled-paths).\n- `0.12.0` - Adds config option to sort `'alpha'` or `false` (default).\n- `0.11.0` - BREAKING: Rename to `super-sitemap` on npm! 🚀\n- `0.10.0` - Adds ability to use unlimited dynamic params per route! 🎉\n- `0.9.0` - BREAKING: Adds configurable `changefreq` and `priority` and\n  _excludes these by default_. See the README's features list for why.\n- `0.8.0` - Adds ability to specify `additionalPaths` that live outside\n  `/src/routes`, such as `/foo.pdf` located at `/static/foo.pdf`.\n\n## Contributing\n\n```bash\ngit clone https://github.com/jasongitmail/super-sitemap.git\nbun install\n# Then edit files in `/src/lib`\n```\n\n## Publishing\n\nA new version of this npm package is automatically published when the semver\nversion within `package.json` is incremented.\n\n## Credits\n\n- Built by [x.com/@zkjason\\_](https://twitter.com/zkjason_)\n- Made possible by [SvelteKit](https://kit.svelte.dev/) \u0026 [Svelte](https://svelte.dev/).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjasongitmail%2Fsuper-sitemap","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fjasongitmail%2Fsuper-sitemap","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjasongitmail%2Fsuper-sitemap/lists"}