diff options
Diffstat (limited to 'packages/integrations/sitemap')
43 files changed, 1760 insertions, 0 deletions
diff --git a/packages/integrations/sitemap/CHANGELOG.md b/packages/integrations/sitemap/CHANGELOG.md new file mode 100644 index 000000000..ccb04f05f --- /dev/null +++ b/packages/integrations/sitemap/CHANGELOG.md @@ -0,0 +1,388 @@ +# @astrojs/sitemap + +## 3.4.1 + +### Patch Changes + +- [#13871](https://github.com/withastro/astro/pull/13871) [`8a1e849`](https://github.com/withastro/astro/commit/8a1e8499dbd1ed98e971635e86eb89f910f0ce78) Thanks [@blimmer](https://github.com/blimmer)! - Uncaught errors in the `filter` method will now bubble, causing the astro build to fail. + +## 3.4.0 + +### Minor Changes + +- [#13753](https://github.com/withastro/astro/pull/13753) [`90293de`](https://github.com/withastro/astro/commit/90293de03320da51965f05cfa6923cbe5521f519) Thanks [@mattyoho](https://github.com/mattyoho)! - Customize the filenames of sitemap XML files generated by the `@astro/sitemap` integration by setting `filenameBase` in the integration configuration settings. This may be useful when deploying an Astro site at a path on a domain with preexisting sitemap files. + + Generated sitemap files will appear at `/sitemap-0.xml` and `/sitemap-index.xml` by default, which may conflict with preexisting files. Set `filenameBase` to a custom value to avoid that if so: + + ```js + import { defineConfig } from 'astro/config'; + import sitemap from '@astrojs/sitemap'; + + export default defineConfig({ + site: 'https://example.com', + integrations: [ + sitemap({ + filenameBase: 'astronomy-sitemap', + }), + ], + }); + ``` + + This will yield sitemap and index files as `https://example.com/astronomy-sitemap-0.xml` and `https://example.com/astronomy-sitemap-index.xml`. + +## 3.3.1 + +### Patch Changes + +- [#13591](https://github.com/withastro/astro/pull/13591) [`5dd2d3f`](https://github.com/withastro/astro/commit/5dd2d3fde8a138ed611dedf39ffa5dfeeed315f8) Thanks [@florian-lefebvre](https://github.com/florian-lefebvre)! - Removes unused code + +## 3.3.0 + +### Minor Changes + +- [#13448](https://github.com/withastro/astro/pull/13448) [`91c9503`](https://github.com/withastro/astro/commit/91c95034e0d0bd450170623fd8aab4b56b5b1366) Thanks [@ematipico](https://github.com/ematipico)! - Add support for XSL in sitemap-index.xml + +## 3.2.1 + +### Patch Changes + +- [#12156](https://github.com/withastro/astro/pull/12156) [`07754f5`](https://github.com/withastro/astro/commit/07754f5873b05ab4dae31ded7264fe4056c2dfc8) Thanks [@mingjunlu](https://github.com/mingjunlu)! - Adds missing `xslURL` property to `SitemapOptions` type. + +## 3.2.0 + +### Minor Changes + +- [#11485](https://github.com/withastro/astro/pull/11485) [`fbe1bc5`](https://github.com/withastro/astro/commit/fbe1bc51d89994c4919c12768908658604513bd3) Thanks [@sondr3](https://github.com/sondr3)! - Adds new `xslURL` option to enable styling of sitemaps + +## 3.1.6 + +### Patch Changes + +- [#11263](https://github.com/withastro/astro/pull/11263) [`7d59750`](https://github.com/withastro/astro/commit/7d597506615fa5a34327304e8321be7b9c4b799d) Thanks [@wackbyte](https://github.com/wackbyte)! - Refactor to use Astro's integration logger for logging + +## 3.1.5 + +### Patch Changes + +- [#10779](https://github.com/withastro/astro/pull/10779) [`cefeadf`](https://github.com/withastro/astro/commit/cefeadf0a4a51420130445b6dc5ab1e5b331732b) Thanks [@adrianlyjak](https://github.com/adrianlyjak)! - Fixes false positives for status code routes like `404` and `500` when generating sitemaps. + +## 3.1.4 + +### Patch Changes + +- [#10772](https://github.com/withastro/astro/pull/10772) [`0e22462d1534afc8f7bb6782f86db680c7a5f245`](https://github.com/withastro/astro/commit/0e22462d1534afc8f7bb6782f86db680c7a5f245) Thanks [@gislerro](https://github.com/gislerro)! - Fixes an issue where the root url does not follow the `trailingSlash` config option + +## 3.1.3 + +### Patch Changes + +- [#10795](https://github.com/withastro/astro/pull/10795) [`1ce22881c657becf0397b83ac393fb5d2399104c`](https://github.com/withastro/astro/commit/1ce22881c657becf0397b83ac393fb5d2399104c) Thanks [@bluwy](https://github.com/bluwy)! - Improves performance when generating the sitemap data + +## 3.1.2 + +### Patch Changes + +- [#10557](https://github.com/withastro/astro/pull/10557) [`5f7e9c47e01116f6ec74b33770f480404680956a`](https://github.com/withastro/astro/commit/5f7e9c47e01116f6ec74b33770f480404680956a) Thanks [@mingjunlu](https://github.com/mingjunlu)! - Fixes an issue where the base path is missing in `sitemap-index.xml`. + +## 3.1.1 + +### Patch Changes + +- [#10179](https://github.com/withastro/astro/pull/10179) [`6343f6a438d790fa16a0dd268f4a51def4fa0f33`](https://github.com/withastro/astro/commit/6343f6a438d790fa16a0dd268f4a51def4fa0f33) Thanks [@ematipico](https://github.com/ematipico)! - Revert https://github.com/withastro/astro/pull/9846 + + The feature to customize the file name of the sitemap was reverted due to some internal issues with one of the dependencies. With an non-deterministic behaviour, the sitemap file was sometime emitted with incorrect syntax. + +- [#9975](https://github.com/withastro/astro/pull/9975) [`ec7d2ebbd96b8c2dfdadaf076bbf7953007536ed`](https://github.com/withastro/astro/commit/ec7d2ebbd96b8c2dfdadaf076bbf7953007536ed) Thanks [@moose96](https://github.com/moose96)! - Fixes URL generation for routes that rest parameters and start with `/` + +## 3.1.0 + +### Minor Changes + +- [#9846](https://github.com/withastro/astro/pull/9846) [`9b78c992750cdb99c40a89a00ea2a0d1c00877d7`](https://github.com/withastro/astro/commit/9b78c992750cdb99c40a89a00ea2a0d1c00877d7) Thanks [@ktym4a](https://github.com/ktym4a)! - Adds a new configuration option `prefix` that allows you to change the default `sitemap-*.xml` file name. + + By default, running `astro build` creates both `sitemap-index.xml` and `sitemap-0.xml` in your output directory. + + To change the names of these files (e.g. to `astrosite-index.xml` and `astrosite-0.xml`), set the `prefix` option in your `sitemap` integration configuration: + + ``` + import { defineConfig } from 'astro/config'; + import sitemap from '@astrojs/sitemap'; + export default defineConfig({ + site: 'https://example.com', + integrations: [ + sitemap({ + prefix: 'astrosite-', + }), + ], + }); + ``` + + This option is useful when Google Search Console is unable to fetch your default sitemap files, but can read renamed files. + +## 3.0.5 + +### Patch Changes + +- [#9704](https://github.com/withastro/astro/pull/9704) [`b325fada567892b63ecae87c1ff845c8514457ba`](https://github.com/withastro/astro/commit/b325fada567892b63ecae87c1ff845c8514457ba) Thanks [@andremralves](https://github.com/andremralves)! - Fixes generated URLs when using a `base` with a SSR adapter + +## 3.0.4 + +### Patch Changes + +- [#9479](https://github.com/withastro/astro/pull/9479) [`1baf0b0d3cbd0564954c2366a7278794fad6726e`](https://github.com/withastro/astro/commit/1baf0b0d3cbd0564954c2366a7278794fad6726e) Thanks [@sarah11918](https://github.com/sarah11918)! - Updates README + +## 3.0.3 + +### Patch Changes + +- [#8762](https://github.com/withastro/astro/pull/8762) [`35cd810f0`](https://github.com/withastro/astro/commit/35cd810f0f988010fbb8e6d7ab205de5d816e2b2) Thanks [@evadecker](https://github.com/evadecker)! - Upgrades Zod to 3.22.4 + +## 3.0.2 + +### Patch Changes + +- [#8824](https://github.com/withastro/astro/pull/8824) [`10b103820`](https://github.com/withastro/astro/commit/10b103820e22e51dcfb0592c542cdf2c5eeb2f52) Thanks [@silent1mezzo](https://github.com/silent1mezzo)! - Display output directory in the sitemap build result + +## 3.0.1 + +### Patch Changes + +- [#8737](https://github.com/withastro/astro/pull/8737) [`6f60da805`](https://github.com/withastro/astro/commit/6f60da805e0014bc50dd07bef972e91c73560c3c) Thanks [@ematipico](https://github.com/ematipico)! - Add provenance statement when publishing the library from CI + +## 3.0.0 + +### Major Changes + +- [#8188](https://github.com/withastro/astro/pull/8188) [`d0679a666`](https://github.com/withastro/astro/commit/d0679a666f37da0fca396d42b9b32bbb25d29312) Thanks [@ematipico](https://github.com/ematipico)! - Remove support for Node 16. The lowest supported version by Astro and all integrations is now v18.14.1. As a reminder, Node 16 will be deprecated on the 11th September 2023. + +- [#8179](https://github.com/withastro/astro/pull/8179) [`6011d52d3`](https://github.com/withastro/astro/commit/6011d52d38e43c3e3d52bc3bc41a60e36061b7b7) Thanks [@matthewp](https://github.com/matthewp)! - Astro 3.0 Release Candidate + +## 3.0.0-rc.1 + +### Major Changes + +- [#8179](https://github.com/withastro/astro/pull/8179) [`6011d52d3`](https://github.com/withastro/astro/commit/6011d52d38e43c3e3d52bc3bc41a60e36061b7b7) Thanks [@matthewp](https://github.com/matthewp)! - Astro 3.0 Release Candidate + +## 3.0.0-beta.0 + +### Major Changes + +- [`1eae2e3f7`](https://github.com/withastro/astro/commit/1eae2e3f7d693c9dfe91c8ccfbe606d32bf2fb81) Thanks [@Princesseuh](https://github.com/Princesseuh)! - Remove support for Node 16. The lowest supported version by Astro and all integrations is now v18.14.1. As a reminder, Node 16 will be deprecated on the 11th September 2023. + +## 2.0.2 + +### Patch Changes + +- [#8063](https://github.com/withastro/astro/pull/8063) [`bee284cb7`](https://github.com/withastro/astro/commit/bee284cb7741ee594e8b38b1a618763e9058740b) Thanks [@martrapp](https://github.com/martrapp)! - docs: fix github search link in README.md + +## 2.0.1 + +### Patch Changes + +- [#7722](https://github.com/withastro/astro/pull/7722) [`77ffcc8f8`](https://github.com/withastro/astro/commit/77ffcc8f8b0ca9f8b9da29525f03028e666fd8df) Thanks [@natemoo-re](https://github.com/natemoo-re)! - Ensure nested 404 and 500 pages are always excluded + +## 2.0.0 + +### Major Changes + +- [#7656](https://github.com/withastro/astro/pull/7656) [`dd931a780`](https://github.com/withastro/astro/commit/dd931a78065a9f46ade0588b35dcc2ea7dbed974) Thanks [@natemoo-re](https://github.com/natemoo-re)! - Sitemap only includes `page` routes (generated by `.astro` files) rather than all routes (pages, endpoints, or redirects). This behavior matches our existing documentation, but is a breaking change nonetheless. + +### Patch Changes + +- [#7656](https://github.com/withastro/astro/pull/7656) [`dd931a780`](https://github.com/withastro/astro/commit/dd931a78065a9f46ade0588b35dcc2ea7dbed974) Thanks [@natemoo-re](https://github.com/natemoo-re)! - Ensure trailing slash is only added to page routes + +## 1.4.0 + +### Minor Changes + +- [#7655](https://github.com/withastro/astro/pull/7655) [`c258492b7`](https://github.com/withastro/astro/commit/c258492b7218cc7e5b7be38f48ec1bb1296292d5) Thanks [@natemoo-re](https://github.com/natemoo-re)! - Ensure sitemap only excludes numerical pages matching `/404` and `/500` exactly + +## 1.3.3 + +### Patch Changes + +- [#7263](https://github.com/withastro/astro/pull/7263) [`dff0d0dda`](https://github.com/withastro/astro/commit/dff0d0dda2f20c02901594739a654834d3451c8e) Thanks [@andremralves](https://github.com/andremralves)! - Fix sitemap does not filter pages + +## 1.3.2 + +### Patch Changes + +- [#7028](https://github.com/withastro/astro/pull/7028) [`6ca3b5a9e`](https://github.com/withastro/astro/commit/6ca3b5a9e8b9aa19a9436043f8ead41e7938c32e) Thanks [@alexanderniebuhr](https://github.com/alexanderniebuhr)! - exported enum type to support typescript > 5.0 + +## 1.3.1 + +### Patch Changes + +- [#7029](https://github.com/withastro/astro/pull/7029) [`1b90a7a5d`](https://github.com/withastro/astro/commit/1b90a7a5d5f16e3e1fa0329b509c6c6e76248181) Thanks [@TheOtterlord](https://github.com/TheOtterlord)! - Fix generation for static dynamic routes + +## 1.3.0 + +### Minor Changes + +- [#6534](https://github.com/withastro/astro/pull/6534) [`ad907196c`](https://github.com/withastro/astro/commit/ad907196cb42f21d9540ae0d77aa742bf7adf030) Thanks [@atilafassina](https://github.com/atilafassina)! - Adds support to SSR routes to sitemap generation. + +## 1.2.2 + +### Patch Changes + +- [#6658](https://github.com/withastro/astro/pull/6658) [`1ec1df126`](https://github.com/withastro/astro/commit/1ec1df12641290ec8b3a417a6284fd8d752c02bf) Thanks [@andremralves](https://github.com/andremralves)! - Fix sitemap generation with a base path + +## 1.2.1 + +### Patch Changes + +- [#6494](https://github.com/withastro/astro/pull/6494) [`a13e9d7e3`](https://github.com/withastro/astro/commit/a13e9d7e33baccf51e7d4815f99b481ad174bc57) Thanks [@Yan-Thomas](https://github.com/Yan-Thomas)! - Consistency improvements to several package descriptions + +## 1.2.0 + +### Minor Changes + +- [#6213](https://github.com/withastro/astro/pull/6213) [`afbbc4d5b`](https://github.com/withastro/astro/commit/afbbc4d5bfafc1779bac00b41c2a1cb1c90f2808) Thanks [@Princesseuh](https://github.com/Princesseuh)! - Updated compilation settings to disable downlevelling for Node 14 + +## 1.1.0 + +### Minor Changes + +- [#6262](https://github.com/withastro/astro/pull/6262) [`4fcefa34f`](https://github.com/withastro/astro/commit/4fcefa34f979e23b8c48940b5a5da57fdabc32a4) Thanks [@vic1707](https://github.com/vic1707)! - update `ChangeFreq` to support typescript configurations with string literal or predefined value. + +## 1.0.1 + +### Patch Changes + +- [#5478](https://github.com/withastro/astro/pull/5478) [`1c7eef308`](https://github.com/withastro/astro/commit/1c7eef308e808aa5ed4662b53e67ec8d1b814d1f) Thanks [@nemo0](https://github.com/nemo0)! - Update READMEs for consistency + +## 1.0.0 + +### Major Changes + +- [`04ad44563`](https://github.com/withastro/astro/commit/04ad445632c67bdd60c1704e1e0dcbcaa27b9308) - > Astro v1.0 is out! Read the [official announcement post](https://astro.build/blog/astro-1/). + + **No breaking changes**. This package is now officially stable and compatible with `astro@1.0.0`! + +## 0.3.0 + +### Minor Changes + +- [#4015](https://github.com/withastro/astro/pull/4015) [`6fd161d76`](https://github.com/withastro/astro/commit/6fd161d7691cbf9d3ffa4646e46059dfd0940010) Thanks [@matthewp](https://github.com/matthewp)! - New `output` configuration option + + This change introduces a new "output target" configuration option (`output`). Setting the output target lets you decide the format of your final build, either: + + - `"static"` (default): A static site. Your final build will be a collection of static assets (HTML, CSS, JS) that you can deploy to any static site host. + - `"server"`: A dynamic server application. Your final build will be an application that will run in a hosted server environment, generating HTML dynamically for different requests. + + If `output` is omitted from your config, the default value `"static"` will be used. + + When using the `"server"` output target, you must also include a runtime adapter via the `adapter` configuration. An adapter will _adapt_ your final build to run on the deployed platform of your choice (Netlify, Vercel, Node.js, Deno, etc). + + To migrate: No action is required for most users. If you currently define an `adapter`, you will need to also add `output: 'server'` to your config file to make it explicit that you are building a server. Here is an example of what that change would look like for someone deploying to Netlify: + + ```diff + import { defineConfig } from 'astro/config'; + import netlify from '@astrojs/netlify/functions'; + + export default defineConfig({ + adapter: netlify(), + + output: 'server', + }); + ``` + +### Patch Changes + +- [#3978](https://github.com/withastro/astro/pull/3978) [`b37d7078a`](https://github.com/withastro/astro/commit/b37d7078a009869bf482912397a073dca490d3da) Thanks [@Chrissdroid](https://github.com/Chrissdroid)! - Update README to reflect `@astrojs/sitemap@0.2.0` changes + +* [#4004](https://github.com/withastro/astro/pull/4004) [`ef9c4152b`](https://github.com/withastro/astro/commit/ef9c4152b2b399e25bf4e8aa7b37adcf6d0d8f17) Thanks [@sarah11918](https://github.com/sarah11918)! - [READMEs] removed "experimental" from astro add instructions + +## 0.2.6 + +### Patch Changes + +- [#3885](https://github.com/withastro/astro/pull/3885) [`bf5d1cc1e`](https://github.com/withastro/astro/commit/bf5d1cc1e71da38a14658c615e9481f2145cc6e7) Thanks [@delucis](https://github.com/delucis)! - Integration README fixes + +## 0.2.5 + +### Patch Changes + +- [#3865](https://github.com/withastro/astro/pull/3865) [`1f9e4857`](https://github.com/withastro/astro/commit/1f9e4857ff2b2cb7db89d619618cdf546cd3b3dc) Thanks [@delucis](https://github.com/delucis)! - Small README fixes + +* [#3854](https://github.com/withastro/astro/pull/3854) [`b012ee55`](https://github.com/withastro/astro/commit/b012ee55b107dea0730286263b27d83e530fad5d) Thanks [@bholmesdev](https://github.com/bholmesdev)! - [astro add] Support adapters and third party packages + +## 0.2.4 + +### Patch Changes + +- [#3677](https://github.com/withastro/astro/pull/3677) [`8045c8ad`](https://github.com/withastro/astro/commit/8045c8ade16fe4306448b7f98a4560ef0557d378) Thanks [@Jutanium](https://github.com/Jutanium)! - Update READMEs + +## 0.2.3 + +### Patch Changes + +- [#3723](https://github.com/withastro/astro/pull/3723) [`52f75369`](https://github.com/withastro/astro/commit/52f75369efe5a0a1b320478984c90b6727d52159) Thanks [@alextim](https://github.com/alextim)! - fix: if `serialize` function returns `undefined` for the passed entry, such entry will be excluded from sitemap + +## 0.2.2 + +### Patch Changes + +- [#3689](https://github.com/withastro/astro/pull/3689) [`3f8ee70e`](https://github.com/withastro/astro/commit/3f8ee70e2bc5b49c65a0444d9606232dadbc2fca) Thanks [@bholmesdev](https://github.com/bholmesdev)! - Add warning log for sitemap + SSR adapter, with suggestion to use customPages configuration option + +## 0.2.1 + +### Patch Changes + +- [#3661](https://github.com/withastro/astro/pull/3661) [`2ff11df4`](https://github.com/withastro/astro/commit/2ff11df438a6a901e72d1f1979c79deb0ad199f2) Thanks [@matthewp](https://github.com/matthewp)! - Fixes the last build + +## 0.2.0 + +### Minor Changes + +- [#3579](https://github.com/withastro/astro/pull/3579) [`1031c06f`](https://github.com/withastro/astro/commit/1031c06f9c6794d9ee6fb18c145ca5614e6f0583) Thanks [@alextim](https://github.com/alextim)! - # Key features + + - Split up your large sitemap into multiple sitemaps by custom limit. + - Ability to add sitemap specific attributes such as `lastmod` etc. + - Final output customization via JS function. + - Localization support. + - Reliability: all config options are validated. + + ## Important changes + + The integration always generates at least two files instead of one: + + - `sitemap-index.xml` - index file; + - `sitemap-{i}.xml` - actual sitemap. + +## 0.1.2 + +### Patch Changes + +- [#3563](https://github.com/withastro/astro/pull/3563) [`09803129`](https://github.com/withastro/astro/commit/098031294f4e25619d0ae5a6ffc871c7401d98ae) Thanks [@alextim](https://github.com/alextim)! - Remove unused dependency + +## 0.1.1 + +### Patch Changes + +- [#3553](https://github.com/withastro/astro/pull/3553) [`c601ce59`](https://github.com/withastro/astro/commit/c601ce59b5740e7ff48c6575a6168d6a2408f7a3) Thanks [@caioferrarezi](https://github.com/caioferrarezi)! - Prevent sitemap URLs with trimmed paths + +## 0.1.0 + +### Minor Changes + +- [`e425f896`](https://github.com/withastro/astro/commit/e425f896b668d98033ad3b998b50c1f28bc7f6ee) Thanks [@FredKSchott](https://github.com/FredKSchott)! - Update config options to respect [RFC0019](https://github.com/withastro/rfcs/blob/main/proposals/0019-config-finalization.md) + +### Patch Changes + +- [`e425f896`](https://github.com/withastro/astro/commit/e425f896b668d98033ad3b998b50c1f28bc7f6ee) Thanks [@FredKSchott](https://github.com/FredKSchott)! - Add new sitemap configuration options: + - `filter`: filter pages to include in your sitemap + - `canonicalURL`: override your astro.config `site` with a custom base URL + +## 0.0.2 + +### Patch Changes + +- [#2885](https://github.com/withastro/astro/pull/2885) [`6b004363`](https://github.com/withastro/astro/commit/6b004363f99f27e581d1e2d53a2ebff39d7afb8a) Thanks [@bholmesdev](https://github.com/bholmesdev)! - Add README across Astro built-in integrations + +* [#2847](https://github.com/withastro/astro/pull/2847) [`3b621f7a`](https://github.com/withastro/astro/commit/3b621f7a613b45983b090794fa7c015f23ed6140) Thanks [@tony-sull](https://github.com/tony-sull)! - Adds keywords to the official integrations to support discoverability on Astro's Integrations site + +## 0.0.2-next.0 + +### Patch Changes + +- [#2847](https://github.com/withastro/astro/pull/2847) [`3b621f7a`](https://github.com/withastro/astro/commit/3b621f7a613b45983b090794fa7c015f23ed6140) Thanks [@tony-sull](https://github.com/tony-sull)! - Adds keywords to the official integrations to support discoverability on Astro's Integrations site diff --git a/packages/integrations/sitemap/README.md b/packages/integrations/sitemap/README.md new file mode 100644 index 000000000..4fc5efd97 --- /dev/null +++ b/packages/integrations/sitemap/README.md @@ -0,0 +1,38 @@ +# @astrojs/sitemap 🗺 + +This **[Astro integration][astro-integration]** generates a sitemap based on your pages when you build your Astro project. + +## Documentation + +Read the [`@astrojs/sitemap` docs][docs] + +## Support + +- Get help in the [Astro Discord][discord]. Post questions in our `#support` forum, or visit our dedicated `#dev` channel to discuss current development and more! + +- Check our [Astro Integration Documentation][astro-integration] for more on integrations. + +- Submit bug reports and feature requests as [GitHub issues][issues]. + +## Contributing + +This package is maintained by Astro's Core team. You're welcome to submit an issue or PR! These links will help you get started: + +- [Contributor Manual][contributing] +- [Code of Conduct][coc] +- [Community Guide][community] + +## License + +MIT + +Copyright (c) 2023–present [Astro][astro] + +[astro]: https://astro.build/ +[docs]: https://docs.astro.build/en/guides/integrations-guide/sitemap/ +[contributing]: https://github.com/withastro/astro/blob/main/CONTRIBUTING.md +[coc]: https://github.com/withastro/.github/blob/main/CODE_OF_CONDUCT.md +[community]: https://github.com/withastro/.github/blob/main/COMMUNITY_GUIDE.md +[discord]: https://astro.build/chat/ +[issues]: https://github.com/withastro/astro/issues +[astro-integration]: https://docs.astro.build/en/guides/integrations-guide/ diff --git a/packages/integrations/sitemap/package.json b/packages/integrations/sitemap/package.json new file mode 100644 index 000000000..d7ae0cf34 --- /dev/null +++ b/packages/integrations/sitemap/package.json @@ -0,0 +1,48 @@ +{ + "name": "@astrojs/sitemap", + "description": "Generate a sitemap for your Astro site", + "version": "3.4.1", + "type": "module", + "types": "./dist/index.d.ts", + "author": "withastro", + "license": "MIT", + "repository": { + "type": "git", + "url": "git+https://github.com/withastro/astro.git", + "directory": "packages/integrations/sitemap" + }, + "keywords": [ + "astro-integration", + "astro-component", + "seo", + "sitemap" + ], + "bugs": "https://github.com/withastro/astro/issues", + "homepage": "https://docs.astro.build/en/guides/integrations-guide/sitemap/", + "exports": { + ".": "./dist/index.js", + "./package.json": "./package.json" + }, + "files": [ + "dist" + ], + "scripts": { + "build": "astro-scripts build \"src/**/*.ts\" && tsc", + "build:ci": "astro-scripts build \"src/**/*.ts\"", + "dev": "astro-scripts dev \"src/**/*.ts\"", + "test": "astro-scripts test \"test/**/*.test.js\"" + }, + "dependencies": { + "sitemap": "^8.0.0", + "stream-replace-string": "^2.0.0", + "zod": "^3.24.2" + }, + "devDependencies": { + "astro": "workspace:*", + "astro-scripts": "workspace:*", + "xml2js": "0.6.2" + }, + "publishConfig": { + "provenance": true + } +} diff --git a/packages/integrations/sitemap/src/config-defaults.ts b/packages/integrations/sitemap/src/config-defaults.ts new file mode 100644 index 000000000..8d854c7a9 --- /dev/null +++ b/packages/integrations/sitemap/src/config-defaults.ts @@ -0,0 +1,6 @@ +import type { SitemapOptions } from './index.js'; + +export const SITEMAP_CONFIG_DEFAULTS = { + filenameBase: 'sitemap', + entryLimit: 45000, +} satisfies SitemapOptions; diff --git a/packages/integrations/sitemap/src/generate-sitemap.ts b/packages/integrations/sitemap/src/generate-sitemap.ts new file mode 100644 index 000000000..0fb096cc9 --- /dev/null +++ b/packages/integrations/sitemap/src/generate-sitemap.ts @@ -0,0 +1,77 @@ +import type { EnumChangefreq } from 'sitemap'; +import type { SitemapItem, SitemapOptions } from './index.js'; +import { parseI18nUrl } from './utils/parse-i18n-url.js'; + +/** Construct sitemap.xml given a set of URLs */ +export function generateSitemap(pages: string[], finalSiteUrl: string, opts?: SitemapOptions) { + const { changefreq, priority, lastmod: lastmodSrc, i18n } = opts ?? {}; + // TODO: find way to respect <link rel="canonical"> URLs here + const urls = [...pages]; + urls.sort((a, b) => a.localeCompare(b, 'en', { numeric: true })); // sort alphabetically so sitemap is same each time + + const lastmod = lastmodSrc?.toISOString(); + + // Parse URLs for i18n matching later + const { defaultLocale, locales } = i18n ?? {}; + let getI18nLinks: GetI18nLinks | undefined; + if (defaultLocale && locales) { + getI18nLinks = createGetI18nLinks(urls, defaultLocale, locales, finalSiteUrl); + } + + const urlData: SitemapItem[] = urls.map((url, i) => ({ + url, + links: getI18nLinks?.(i), + lastmod, + priority, + changefreq: changefreq as EnumChangefreq, + })); + + return urlData; +} + +type GetI18nLinks = (urlIndex: number) => SitemapItem['links'] | undefined; + +function createGetI18nLinks( + urls: string[], + defaultLocale: string, + locales: Record<string, string>, + finalSiteUrl: string, +): GetI18nLinks { + // `parsedI18nUrls` will have the same length as `urls`, matching correspondingly + const parsedI18nUrls = urls.map((url) => parseI18nUrl(url, defaultLocale, locales, finalSiteUrl)); + // Cache as multiple i18n URLs with the same path will have the same links + const i18nPathToLinksCache = new Map<string, SitemapItem['links']>(); + + return (urlIndex) => { + const i18nUrl = parsedI18nUrls[urlIndex]; + if (!i18nUrl) { + return undefined; + } + + const cached = i18nPathToLinksCache.get(i18nUrl.path); + if (cached) { + return cached; + } + + // Find all URLs with the same path (without the locale part), e.g. /en/foo and /es/foo + const links: NonNullable<SitemapItem['links']> = []; + for (let i = 0; i < parsedI18nUrls.length; i++) { + const parsed = parsedI18nUrls[i]; + if (parsed?.path === i18nUrl.path) { + links.push({ + url: urls[i], + lang: locales[parsed.locale], + }); + } + } + + // If 0 or 1 (which is itself), return undefined to not create any links. + // We also don't need to cache this as we know there's no other URLs that would've match this. + if (links.length <= 1) { + return undefined; + } + + i18nPathToLinksCache.set(i18nUrl.path, links); + return links; + }; +} diff --git a/packages/integrations/sitemap/src/index.ts b/packages/integrations/sitemap/src/index.ts new file mode 100644 index 000000000..078f78abb --- /dev/null +++ b/packages/integrations/sitemap/src/index.ts @@ -0,0 +1,195 @@ +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import type { AstroConfig, AstroIntegration } from 'astro'; +import type { EnumChangefreq, LinkItem as LinkItemBase, SitemapItemLoose } from 'sitemap'; +import { ZodError } from 'zod'; + +import { generateSitemap } from './generate-sitemap.js'; +import { validateOptions } from './validate-options.js'; +import { writeSitemap } from './write-sitemap.js'; + +export { EnumChangefreq as ChangeFreqEnum } from 'sitemap'; +export type ChangeFreq = `${EnumChangefreq}`; +export type SitemapItem = Pick< + SitemapItemLoose, + 'url' | 'lastmod' | 'changefreq' | 'priority' | 'links' +>; +export type LinkItem = LinkItemBase; + +export type SitemapOptions = + | { + filenameBase?: string; + filter?(page: string): boolean; + customPages?: string[]; + + i18n?: { + defaultLocale: string; + locales: Record<string, string>; + }; + // number of entries per sitemap file + entryLimit?: number; + + // sitemap specific + changefreq?: ChangeFreq; + lastmod?: Date; + priority?: number; + + // called for each sitemap item just before to save them on disk, sync or async + serialize?(item: SitemapItem): SitemapItem | Promise<SitemapItem | undefined> | undefined; + + xslURL?: string; + } + | undefined; + +function formatConfigErrorMessage(err: ZodError) { + const errorList = err.issues.map((issue) => ` ${issue.path.join('.')} ${issue.message + '.'}`); + return errorList.join('\n'); +} + +const PKG_NAME = '@astrojs/sitemap'; +const STATUS_CODE_PAGES = new Set(['404', '500']); + +const isStatusCodePage = (locales: string[]) => { + const statusPathNames = new Set( + locales + .flatMap((locale) => [...STATUS_CODE_PAGES].map((page) => `${locale}/${page}`)) + .concat([...STATUS_CODE_PAGES]), + ); + + return (pathname: string): boolean => { + if (pathname.endsWith('/')) { + pathname = pathname.slice(0, -1); + } + if (pathname.startsWith('/')) { + pathname = pathname.slice(1); + } + return statusPathNames.has(pathname); + }; +}; +const createPlugin = (options?: SitemapOptions): AstroIntegration => { + let config: AstroConfig; + + return { + name: PKG_NAME, + + hooks: { + 'astro:config:done': async ({ config: cfg }) => { + config = cfg; + }, + + 'astro:build:done': async ({ dir, routes, pages, logger }) => { + try { + if (!config.site) { + logger.warn( + 'The Sitemap integration requires the `site` astro.config option. Skipping.', + ); + return; + } + + const opts = validateOptions(config.site, options); + + const { filenameBase, filter, customPages, serialize, entryLimit } = opts; + + const outFile = `${filenameBase}-index.xml`; + const finalSiteUrl = new URL(config.base, config.site); + const shouldIgnoreStatus = isStatusCodePage(Object.keys(opts.i18n?.locales ?? {})); + let pageUrls = pages + .filter((p) => !shouldIgnoreStatus(p.pathname)) + .map((p) => { + if (p.pathname !== '' && !finalSiteUrl.pathname.endsWith('/')) + finalSiteUrl.pathname += '/'; + if (p.pathname.startsWith('/')) p.pathname = p.pathname.slice(1); + const fullPath = finalSiteUrl.pathname + p.pathname; + return new URL(fullPath, finalSiteUrl).href; + }); + + const routeUrls = routes.reduce<string[]>((urls, r) => { + // Only expose pages, not endpoints or redirects + if (r.type !== 'page') return urls; + + /** + * Dynamic URLs have entries with `undefined` pathnames + */ + if (r.pathname) { + if (shouldIgnoreStatus(r.pathname ?? r.route)) return urls; + + // `finalSiteUrl` may end with a trailing slash + // or not because of base paths. + let fullPath = finalSiteUrl.pathname; + if (fullPath.endsWith('/')) fullPath += r.generate(r.pathname).substring(1); + else fullPath += r.generate(r.pathname); + + const newUrl = new URL(fullPath, finalSiteUrl).href; + + if (config.trailingSlash === 'never') { + urls.push(newUrl); + } else if (config.build.format === 'directory' && !newUrl.endsWith('/')) { + urls.push(newUrl + '/'); + } else { + urls.push(newUrl); + } + } + + return urls; + }, []); + + pageUrls = Array.from(new Set([...pageUrls, ...routeUrls, ...(customPages ?? [])])); + + if (filter) { + pageUrls = pageUrls.filter(filter); + } + + if (pageUrls.length === 0) { + logger.warn(`No pages found!\n\`${outFile}\` not created.`); + return; + } + + let urlData = generateSitemap(pageUrls, finalSiteUrl.href, opts); + + if (serialize) { + try { + const serializedUrls: SitemapItem[] = []; + for (const item of urlData) { + const serialized = await Promise.resolve(serialize(item)); + if (serialized) { + serializedUrls.push(serialized); + } + } + if (serializedUrls.length === 0) { + logger.warn('No pages found!'); + return; + } + urlData = serializedUrls; + } catch (err) { + logger.error(`Error serializing pages\n${(err as any).toString()}`); + return; + } + } + const destDir = fileURLToPath(dir); + const xslURL = opts.xslURL ? new URL(opts.xslURL, finalSiteUrl).href : undefined; + await writeSitemap( + { + filenameBase: filenameBase, + hostname: finalSiteUrl.href, + destinationDir: destDir, + publicBasePath: config.base, + sourceData: urlData, + limit: entryLimit, + xslURL: xslURL, + }, + config, + ); + logger.info(`\`${outFile}\` created at \`${path.relative(process.cwd(), destDir)}\``); + } catch (err) { + if (err instanceof ZodError) { + logger.warn(formatConfigErrorMessage(err)); + } else { + throw err; + } + } + }, + }, + }; +}; + +export default createPlugin; diff --git a/packages/integrations/sitemap/src/schema.ts b/packages/integrations/sitemap/src/schema.ts new file mode 100644 index 000000000..0ab9d672d --- /dev/null +++ b/packages/integrations/sitemap/src/schema.ts @@ -0,0 +1,41 @@ +import { EnumChangefreq as ChangeFreq } from 'sitemap'; +import { z } from 'zod'; +import { SITEMAP_CONFIG_DEFAULTS } from './config-defaults.js'; + +const localeKeySchema = z.string().min(1); + +export const SitemapOptionsSchema = z + .object({ + filenameBase: z.string().optional().default(SITEMAP_CONFIG_DEFAULTS.filenameBase), + filter: z.function().args(z.string()).returns(z.boolean()).optional(), + customPages: z.string().url().array().optional(), + canonicalURL: z.string().url().optional(), + xslURL: z.string().optional(), + + i18n: z + .object({ + defaultLocale: localeKeySchema, + locales: z.record( + localeKeySchema, + z + .string() + .min(2) + .regex(/^[a-zA-Z\-]+$/gm, { + message: 'Only English alphabet symbols and hyphen allowed', + }), + ), + }) + .refine((val) => !val || val.locales[val.defaultLocale], { + message: '`defaultLocale` must exist in `locales` keys', + }) + .optional(), + + entryLimit: z.number().nonnegative().optional().default(SITEMAP_CONFIG_DEFAULTS.entryLimit), + serialize: z.function().args(z.any()).returns(z.any()).optional(), + + changefreq: z.nativeEnum(ChangeFreq).optional(), + lastmod: z.date().optional(), + priority: z.number().min(0).max(1).optional(), + }) + .strict() + .default(SITEMAP_CONFIG_DEFAULTS); diff --git a/packages/integrations/sitemap/src/utils/parse-i18n-url.ts b/packages/integrations/sitemap/src/utils/parse-i18n-url.ts new file mode 100644 index 000000000..86221ca9d --- /dev/null +++ b/packages/integrations/sitemap/src/utils/parse-i18n-url.ts @@ -0,0 +1,42 @@ +interface ParsedI18nUrl { + locale: string; + path: string; +} + +// NOTE: The parameters have been schema-validated with Zod +export function parseI18nUrl( + url: string, + defaultLocale: string, + locales: Record<string, string>, + base: string, +): ParsedI18nUrl | undefined { + if (!url.startsWith(base)) { + return undefined; + } + + let s = url.slice(base.length); + + // Handle root URL + if (!s || s === '/') { + return { locale: defaultLocale, path: '/' }; + } + + if (s[0] !== '/') { + s = '/' + s; + } + + // Get locale from path, e.g. + // "/en-US/" -> "en-US" + // "/en-US/foo" -> "en-US" + const locale = s.split('/')[1]; + if (locale in locales) { + // "/en-US/foo" -> "/foo" + let path = s.slice(1 + locale.length); + if (!path) { + path = '/'; + } + return { locale, path }; + } + + return { locale: defaultLocale, path: s }; +} diff --git a/packages/integrations/sitemap/src/validate-options.ts b/packages/integrations/sitemap/src/validate-options.ts new file mode 100644 index 000000000..f51750ff5 --- /dev/null +++ b/packages/integrations/sitemap/src/validate-options.ts @@ -0,0 +1,22 @@ +import { z } from 'zod'; +import type { SitemapOptions } from './index.js'; +import { SitemapOptionsSchema } from './schema.js'; + +// @internal +export const validateOptions = (site: string | undefined, opts: SitemapOptions) => { + const result = SitemapOptionsSchema.parse(opts); + + z.object({ + site: z.string().optional(), // Astro takes care of `site`: how to validate, transform and refine + canonicalURL: z.string().optional(), // `canonicalURL` is already validated in prev step + }) + .refine((options) => options.site || options.canonicalURL, { + message: 'Required `site` astro.config option or `canonicalURL` integration option', + }) + .parse({ + site, + canonicalURL: result.canonicalURL, + }); + + return result; +}; diff --git a/packages/integrations/sitemap/src/write-sitemap.ts b/packages/integrations/sitemap/src/write-sitemap.ts new file mode 100644 index 000000000..939bd91be --- /dev/null +++ b/packages/integrations/sitemap/src/write-sitemap.ts @@ -0,0 +1,75 @@ +import { type WriteStream, createWriteStream } from 'node:fs'; +import { mkdir } from 'node:fs/promises'; +import { normalize, resolve } from 'node:path'; +import { Readable, pipeline } from 'node:stream'; +import { promisify } from 'node:util'; +import replace from 'stream-replace-string'; + +import { SitemapAndIndexStream, SitemapStream } from 'sitemap'; + +import type { AstroConfig } from 'astro'; +import type { SitemapItem } from './index.js'; + +type WriteSitemapConfig = { + filenameBase: string; + hostname: string; + sitemapHostname?: string; + sourceData: SitemapItem[]; + destinationDir: string; + publicBasePath?: string; + limit?: number; + xslURL?: string; +}; + +// adapted from sitemap.js/sitemap-simple +export async function writeSitemap( + { + filenameBase, + hostname, + sitemapHostname = hostname, + sourceData, + destinationDir, + limit = 50000, + publicBasePath = './', + xslURL: xslUrl, + }: WriteSitemapConfig, + astroConfig: AstroConfig, +) { + await mkdir(destinationDir, { recursive: true }); + + const sitemapAndIndexStream = new SitemapAndIndexStream({ + limit, + xslUrl, + getSitemapStream: (i) => { + const sitemapStream = new SitemapStream({ + hostname, + xslUrl, + }); + const path = `./${filenameBase}-${i}.xml`; + const writePath = resolve(destinationDir, path); + if (!publicBasePath.endsWith('/')) { + publicBasePath += '/'; + } + const publicPath = normalize(publicBasePath + path); + + let stream: WriteStream; + if (astroConfig.trailingSlash === 'never' || astroConfig.build.format === 'file') { + // workaround for trailing slash issue in sitemap.js: https://github.com/ekalinin/sitemap.js/issues/403 + const host = hostname.endsWith('/') ? hostname.slice(0, -1) : hostname; + const searchStr = `<loc>${host}/</loc>`; + const replaceStr = `<loc>${host}</loc>`; + stream = sitemapStream + .pipe(replace(searchStr, replaceStr)) + .pipe(createWriteStream(writePath)); + } else { + stream = sitemapStream.pipe(createWriteStream(writePath)); + } + + return [new URL(publicPath, sitemapHostname).toString(), sitemapStream, stream]; + }, + }); + + const src = Readable.from(sourceData); + const indexPath = resolve(destinationDir, `./${filenameBase}-index.xml`); + return promisify(pipeline)(src, sitemapAndIndexStream, createWriteStream(indexPath)); +} diff --git a/packages/integrations/sitemap/test/base-path.test.js b/packages/integrations/sitemap/test/base-path.test.js new file mode 100644 index 000000000..fee031ff4 --- /dev/null +++ b/packages/integrations/sitemap/test/base-path.test.js @@ -0,0 +1,52 @@ +import assert from 'node:assert/strict'; +import { before, describe, it } from 'node:test'; +import { loadFixture, readXML } from './test-utils.js'; + +describe('URLs with base path', () => { + /** @type {import('./test-utils').Fixture} */ + let fixture; + + describe('using node adapter', () => { + before(async () => { + fixture = await loadFixture({ + root: './fixtures/ssr/', + base: '/base', + }); + await fixture.build(); + }); + + it('Base path is concatenated correctly', async () => { + const [sitemapZero, sitemapIndex] = await Promise.all([ + readXML(fixture.readFile('/client/sitemap-0.xml')), + readXML(fixture.readFile('/client/sitemap-index.xml')), + ]); + assert.equal(sitemapZero.urlset.url[0].loc[0], 'http://example.com/base/one/'); + assert.equal( + sitemapIndex.sitemapindex.sitemap[0].loc[0], + 'http://example.com/base/sitemap-0.xml', + ); + }); + }); + + describe('static', () => { + before(async () => { + fixture = await loadFixture({ + root: './fixtures/static/', + base: '/base', + }); + await fixture.build(); + }); + + it('Base path is concatenated correctly', async () => { + const [sitemapZero, sitemapIndex] = await Promise.all([ + readXML(fixture.readFile('/sitemap-0.xml')), + readXML(fixture.readFile('/sitemap-index.xml')), + ]); + assert.equal(sitemapZero.urlset.url[0].loc[0], 'http://example.com/base/123/'); + assert.equal( + sitemapIndex.sitemapindex.sitemap[0].loc[0], + 'http://example.com/base/sitemap-0.xml', + ); + }); + }); +}); diff --git a/packages/integrations/sitemap/test/config.test.js b/packages/integrations/sitemap/test/config.test.js new file mode 100644 index 000000000..f95333876 --- /dev/null +++ b/packages/integrations/sitemap/test/config.test.js @@ -0,0 +1,125 @@ +import assert from 'node:assert/strict'; +import { before, describe, it } from 'node:test'; +import { sitemap } from './fixtures/static/deps.mjs'; +import { loadFixture, readXML } from './test-utils.js'; + +describe('Config', () => { + /** @type {import('./test-utils.js').Fixture} */ + let fixture; + + describe('Static', () => { + before(async () => { + fixture = await loadFixture({ + root: './fixtures/static/', + integrations: [ + sitemap({ + filter: (page) => page === 'http://example.com/one/', + xslURL: '/sitemap.xsl', + }), + ], + }); + await fixture.build(); + }); + + it('filter: Just one page is added', async () => { + const data = await readXML(fixture.readFile('/sitemap-0.xml')); + const urls = data.urlset.url; + assert.equal(urls.length, 1); + }); + + it('xslURL: Includes xml-stylesheet', async () => { + const indexXml = await fixture.readFile('/sitemap-index.xml'); + assert.ok( + indexXml.includes( + '<?xml-stylesheet type="text/xsl" href="http://example.com/sitemap.xsl"?>', + ), + indexXml, + ); + + const xml = await fixture.readFile('/sitemap-0.xml'); + assert.ok( + xml.includes('<?xml-stylesheet type="text/xsl" href="http://example.com/sitemap.xsl"?>'), + xml, + ); + }); + }); + + describe('SSR', () => { + before(async () => { + fixture = await loadFixture({ + root: './fixtures/ssr/', + integrations: [ + sitemap({ + filter: (page) => page === 'http://example.com/one/', + xslURL: '/sitemap.xsl', + }), + ], + }); + await fixture.build(); + }); + + it('filter: Just one page is added', async () => { + const data = await readXML(fixture.readFile('/client/sitemap-0.xml')); + const urls = data.urlset.url; + assert.equal(urls.length, 1); + }); + + it('xslURL: Includes xml-stylesheet', async () => { + const indexXml = await fixture.readFile('/client/sitemap-index.xml'); + assert.ok( + indexXml.includes( + '<?xml-stylesheet type="text/xsl" href="http://example.com/sitemap.xsl"?>', + ), + indexXml, + ); + + const xml = await fixture.readFile('/client/sitemap-0.xml'); + assert.ok( + xml.includes('<?xml-stylesheet type="text/xsl" href="http://example.com/sitemap.xsl"?>'), + xml, + ); + }); + }); + + describe('Configuring the filename', () => { + before(async () => { + fixture = await loadFixture({ + root: './fixtures/static/', + integrations: [ + sitemap({ + filter: (page) => page === 'http://example.com/one/', + filenameBase: 'my-sitemap', + }), + ], + }); + await fixture.build(); + }); + + it('filenameBase: Sets the generated sitemap filename', async () => { + const data = await readXML(fixture.readFile('/my-sitemap-0.xml')); + const urls = data.urlset.url; + assert.equal(urls.length, 1); + + const indexData = await readXML(fixture.readFile('/my-sitemap-index.xml')); + const sitemapUrls = indexData.sitemapindex.sitemap; + assert.equal(sitemapUrls.length, 1); + assert.equal(sitemapUrls[0].loc[0], 'http://example.com/my-sitemap-0.xml'); + }); + }); + + describe('Filtering pages - error handling', () => { + it('filter: uncaught errors are thrown', async () => { + fixture = await loadFixture({ + root: './fixtures/static/', + integrations: [ + sitemap({ + filter: () => { + throw new Error('filter error'); + }, + }), + ], + }); + await assert.rejects(fixture.build(), /^Error: filter error$/); + }); + }); +}); diff --git a/packages/integrations/sitemap/test/dynamic-path.test.js b/packages/integrations/sitemap/test/dynamic-path.test.js new file mode 100644 index 000000000..eab3b912c --- /dev/null +++ b/packages/integrations/sitemap/test/dynamic-path.test.js @@ -0,0 +1,24 @@ +import assert from 'node:assert/strict'; +import { before, describe, it } from 'node:test'; +import { loadFixture, readXML } from './test-utils.js'; + +describe('Dynamic with rest parameter', () => { + /** @type {import('./test-utils.js').Fixture} */ + let fixture; + + before(async () => { + fixture = await loadFixture({ + root: './fixtures/dynamic', + }); + await fixture.build(); + }); + + it('Should generate correct urls', async () => { + const data = await readXML(fixture.readFile('/sitemap-0.xml')); + const urls = data.urlset.url.map((url) => url.loc[0]); + + assert.ok(urls.includes('http://example.com/')); + assert.ok(urls.includes('http://example.com/blog/')); + assert.ok(urls.includes('http://example.com/test/')); + }); +}); diff --git a/packages/integrations/sitemap/test/fixtures/dynamic/astro.config.mjs b/packages/integrations/sitemap/test/fixtures/dynamic/astro.config.mjs new file mode 100644 index 000000000..82d25b854 --- /dev/null +++ b/packages/integrations/sitemap/test/fixtures/dynamic/astro.config.mjs @@ -0,0 +1,7 @@ +import sitemap from '@astrojs/sitemap'; +import { defineConfig } from 'astro/config'; + +export default defineConfig({ + integrations: [sitemap()], + site: 'http://example.com' +}) diff --git a/packages/integrations/sitemap/test/fixtures/dynamic/package.json b/packages/integrations/sitemap/test/fixtures/dynamic/package.json new file mode 100644 index 000000000..1eac19a1b --- /dev/null +++ b/packages/integrations/sitemap/test/fixtures/dynamic/package.json @@ -0,0 +1,9 @@ +{ + "name": "@test/sitemap-dynamic", + "version": "0.0.0", + "private": true, + "dependencies": { + "astro": "workspace:*", + "@astrojs/sitemap": "workspace:*" + } +} diff --git a/packages/integrations/sitemap/test/fixtures/dynamic/src/pages/[...slug].astro b/packages/integrations/sitemap/test/fixtures/dynamic/src/pages/[...slug].astro new file mode 100644 index 000000000..9622cb374 --- /dev/null +++ b/packages/integrations/sitemap/test/fixtures/dynamic/src/pages/[...slug].astro @@ -0,0 +1,21 @@ +--- +export async function getStaticPaths() { + return [ + { + params: { + slug: undefined, + } + }, + { + params: { + slug: '/blog' + } + }, + { + params: { + slug: '/test' + } + } + ]; +} +--- diff --git a/packages/integrations/sitemap/test/fixtures/ssr/astro.config.mjs b/packages/integrations/sitemap/test/fixtures/ssr/astro.config.mjs new file mode 100644 index 000000000..ce84944bf --- /dev/null +++ b/packages/integrations/sitemap/test/fixtures/ssr/astro.config.mjs @@ -0,0 +1,12 @@ +import nodeServer from '@astrojs/node' +import sitemap from '@astrojs/sitemap'; +import { defineConfig } from 'astro/config'; + +export default defineConfig({ + integrations: [sitemap()], + site: 'http://example.com', + output: 'server', + adapter: nodeServer({ + mode: "standalone" + }) +}) diff --git a/packages/integrations/sitemap/test/fixtures/ssr/package.json b/packages/integrations/sitemap/test/fixtures/ssr/package.json new file mode 100644 index 000000000..4b5e6848d --- /dev/null +++ b/packages/integrations/sitemap/test/fixtures/ssr/package.json @@ -0,0 +1,10 @@ +{ + "name": "@test/sitemap-ssr", + "version": "0.0.0", + "private": true, + "dependencies": { + "astro": "workspace:*", + "@astrojs/node": "workspace:*", + "@astrojs/sitemap": "workspace:*" + } +} diff --git a/packages/integrations/sitemap/test/fixtures/ssr/src/pages/one.astro b/packages/integrations/sitemap/test/fixtures/ssr/src/pages/one.astro new file mode 100644 index 000000000..0c7fb90a7 --- /dev/null +++ b/packages/integrations/sitemap/test/fixtures/ssr/src/pages/one.astro @@ -0,0 +1,8 @@ +<html> + <head> + <title>One</title> + </head> + <body> + <h1>One</h1> + </body> +</html> diff --git a/packages/integrations/sitemap/test/fixtures/ssr/src/pages/two.astro b/packages/integrations/sitemap/test/fixtures/ssr/src/pages/two.astro new file mode 100644 index 000000000..e7ba9910e --- /dev/null +++ b/packages/integrations/sitemap/test/fixtures/ssr/src/pages/two.astro @@ -0,0 +1,8 @@ +<html> + <head> + <title>Two</title> + </head> + <body> + <h1>Two</h1> + </body> +</html> diff --git a/packages/integrations/sitemap/test/fixtures/static/astro.config.mjs b/packages/integrations/sitemap/test/fixtures/static/astro.config.mjs new file mode 100644 index 000000000..ae53a9342 --- /dev/null +++ b/packages/integrations/sitemap/test/fixtures/static/astro.config.mjs @@ -0,0 +1,18 @@ +import sitemap from '@astrojs/sitemap'; +import { defineConfig } from 'astro/config'; + +export default defineConfig({ + integrations: [sitemap({ + i18n: { + defaultLocale: 'it', + locales: { + it: 'it-IT', + de: 'de-DE', + } + } + })], + site: 'http://example.com', + redirects: { + '/redirect': '/' + }, +}) diff --git a/packages/integrations/sitemap/test/fixtures/static/deps.mjs b/packages/integrations/sitemap/test/fixtures/static/deps.mjs new file mode 100644 index 000000000..b24f26189 --- /dev/null +++ b/packages/integrations/sitemap/test/fixtures/static/deps.mjs @@ -0,0 +1 @@ +export { default as sitemap } from '@astrojs/sitemap'; diff --git a/packages/integrations/sitemap/test/fixtures/static/package.json b/packages/integrations/sitemap/test/fixtures/static/package.json new file mode 100644 index 000000000..ed5f3670b --- /dev/null +++ b/packages/integrations/sitemap/test/fixtures/static/package.json @@ -0,0 +1,9 @@ +{ + "name": "@test/sitemap-static", + "version": "0.0.0", + "private": true, + "dependencies": { + "astro": "workspace:*", + "@astrojs/sitemap": "workspace:*" + } +} diff --git a/packages/integrations/sitemap/test/fixtures/static/src/pages/123.astro b/packages/integrations/sitemap/test/fixtures/static/src/pages/123.astro new file mode 100644 index 000000000..115292de9 --- /dev/null +++ b/packages/integrations/sitemap/test/fixtures/static/src/pages/123.astro @@ -0,0 +1,8 @@ +<html> + <head> + <title>123</title> + </head> + <body> + <h1>123</h1> + </body> +</html> diff --git a/packages/integrations/sitemap/test/fixtures/static/src/pages/404.astro b/packages/integrations/sitemap/test/fixtures/static/src/pages/404.astro new file mode 100644 index 000000000..9e307c5c2 --- /dev/null +++ b/packages/integrations/sitemap/test/fixtures/static/src/pages/404.astro @@ -0,0 +1,8 @@ +<html> + <head> + <title>404</title> + </head> + <body> + <h1>404</h1> + </body> +</html> diff --git a/packages/integrations/sitemap/test/fixtures/static/src/pages/[lang]/manifest.ts b/packages/integrations/sitemap/test/fixtures/static/src/pages/[lang]/manifest.ts new file mode 100644 index 000000000..907b94a21 --- /dev/null +++ b/packages/integrations/sitemap/test/fixtures/static/src/pages/[lang]/manifest.ts @@ -0,0 +1,15 @@ +export const GET: APIRoute = async ({ params }) => { + const { lang } = params; + + return new Response(`I'm a route in the "${lang}" language.`); +}; + +export async function getStaticPaths() { + return ['it', 'en'].map((language) => { + return { + params: { + lang: language, + }, + }; + }); +} diff --git a/packages/integrations/sitemap/test/fixtures/static/src/pages/[slug].astro b/packages/integrations/sitemap/test/fixtures/static/src/pages/[slug].astro new file mode 100644 index 000000000..205633c76 --- /dev/null +++ b/packages/integrations/sitemap/test/fixtures/static/src/pages/[slug].astro @@ -0,0 +1,17 @@ +--- +export function getStaticPaths() { + return [ + { params: { slug: 'one' }, props: { title: 'One' } }, + { params: { slug: 'two' }, props: { title: 'Two' } }, + ] +} +--- + +<html> + <head> + <title>{Astro.props.title}</title> + </head> + <body> + <h1>{Astro.props.title}</h1> + </body> +</html> diff --git a/packages/integrations/sitemap/test/fixtures/static/src/pages/de/404.astro b/packages/integrations/sitemap/test/fixtures/static/src/pages/de/404.astro new file mode 100644 index 000000000..9e307c5c2 --- /dev/null +++ b/packages/integrations/sitemap/test/fixtures/static/src/pages/de/404.astro @@ -0,0 +1,8 @@ +<html> + <head> + <title>404</title> + </head> + <body> + <h1>404</h1> + </body> +</html> diff --git a/packages/integrations/sitemap/test/fixtures/static/src/pages/endpoint.json.ts b/packages/integrations/sitemap/test/fixtures/static/src/pages/endpoint.json.ts new file mode 100644 index 000000000..2e088376f --- /dev/null +++ b/packages/integrations/sitemap/test/fixtures/static/src/pages/endpoint.json.ts @@ -0,0 +1,6 @@ +export async function GET({}) { + return Response.json({ + name: 'Astro', + url: 'https://astro.build/', + }); +} diff --git a/packages/integrations/sitemap/test/fixtures/static/src/pages/products-by-id/[id].astro b/packages/integrations/sitemap/test/fixtures/static/src/pages/products-by-id/[id].astro new file mode 100644 index 000000000..b10438a68 --- /dev/null +++ b/packages/integrations/sitemap/test/fixtures/static/src/pages/products-by-id/[id].astro @@ -0,0 +1,11 @@ +--- +export async function getStaticPaths() { + return [ + { params: { id: '404' } }, + { params: { id: '405' } }, + ] +} + +const { id } = Astro.params +--- +<!DOCTYPE html><html> <head><title>Product #{id}</title></head> <body> <h1>Product #404</h1> <p>This is a product that just happens to have an ID of {id}. It is found!</p></body></html>
\ No newline at end of file diff --git a/packages/integrations/sitemap/test/fixtures/trailing-slash/astro.config.mjs b/packages/integrations/sitemap/test/fixtures/trailing-slash/astro.config.mjs new file mode 100644 index 000000000..82d25b854 --- /dev/null +++ b/packages/integrations/sitemap/test/fixtures/trailing-slash/astro.config.mjs @@ -0,0 +1,7 @@ +import sitemap from '@astrojs/sitemap'; +import { defineConfig } from 'astro/config'; + +export default defineConfig({ + integrations: [sitemap()], + site: 'http://example.com' +}) diff --git a/packages/integrations/sitemap/test/fixtures/trailing-slash/package.json b/packages/integrations/sitemap/test/fixtures/trailing-slash/package.json new file mode 100644 index 000000000..980e02e73 --- /dev/null +++ b/packages/integrations/sitemap/test/fixtures/trailing-slash/package.json @@ -0,0 +1,9 @@ +{ + "name": "@test/sitemap-trailing-slash", + "version": "0.0.0", + "private": true, + "dependencies": { + "astro": "workspace:*", + "@astrojs/sitemap": "workspace:*" + } +} diff --git a/packages/integrations/sitemap/test/fixtures/trailing-slash/src/pages/index.astro b/packages/integrations/sitemap/test/fixtures/trailing-slash/src/pages/index.astro new file mode 100644 index 000000000..5a29cbdbe --- /dev/null +++ b/packages/integrations/sitemap/test/fixtures/trailing-slash/src/pages/index.astro @@ -0,0 +1,8 @@ +<html> + <head> + <title>Index</title> + </head> + <body> + <h1>Index</h1> + </body> +</html>
\ No newline at end of file diff --git a/packages/integrations/sitemap/test/fixtures/trailing-slash/src/pages/one.astro b/packages/integrations/sitemap/test/fixtures/trailing-slash/src/pages/one.astro new file mode 100644 index 000000000..0c7fb90a7 --- /dev/null +++ b/packages/integrations/sitemap/test/fixtures/trailing-slash/src/pages/one.astro @@ -0,0 +1,8 @@ +<html> + <head> + <title>One</title> + </head> + <body> + <h1>One</h1> + </body> +</html> diff --git a/packages/integrations/sitemap/test/fixtures/trailing-slash/src/pages/two.astro b/packages/integrations/sitemap/test/fixtures/trailing-slash/src/pages/two.astro new file mode 100644 index 000000000..e7ba9910e --- /dev/null +++ b/packages/integrations/sitemap/test/fixtures/trailing-slash/src/pages/two.astro @@ -0,0 +1,8 @@ +<html> + <head> + <title>Two</title> + </head> + <body> + <h1>Two</h1> + </body> +</html> diff --git a/packages/integrations/sitemap/test/routes.test.js b/packages/integrations/sitemap/test/routes.test.js new file mode 100644 index 000000000..00d6ccde3 --- /dev/null +++ b/packages/integrations/sitemap/test/routes.test.js @@ -0,0 +1,27 @@ +import assert from 'node:assert/strict'; +import { before, describe, it } from 'node:test'; +import { loadFixture, readXML } from './test-utils.js'; + +describe('routes', () => { + /** @type {import('./test-utils.js').Fixture} */ + let fixture; + /** @type {string[]} */ + let urls; + + before(async () => { + fixture = await loadFixture({ + root: './fixtures/static/', + }); + await fixture.build(); + const data = await readXML(fixture.readFile('/sitemap-0.xml')); + urls = data.urlset.url.map((url) => url.loc[0]); + }); + + it('does not include endpoints', async () => { + assert.equal(urls.includes('http://example.com/endpoint.json'), false); + }); + + it('does not include redirects', async () => { + assert.equal(urls.includes('http://example.com/redirect'), false); + }); +}); diff --git a/packages/integrations/sitemap/test/smoke.test.js b/packages/integrations/sitemap/test/smoke.test.js new file mode 100644 index 000000000..d24c191ec --- /dev/null +++ b/packages/integrations/sitemap/test/smoke.test.js @@ -0,0 +1,3 @@ +import '../dist/index.js'; + +// Just a smoke test, this would fail if there's a problem. diff --git a/packages/integrations/sitemap/test/ssr.test.js b/packages/integrations/sitemap/test/ssr.test.js new file mode 100644 index 000000000..b5c92698b --- /dev/null +++ b/packages/integrations/sitemap/test/ssr.test.js @@ -0,0 +1,23 @@ +import assert from 'node:assert/strict'; +import { before, describe, it } from 'node:test'; +import { loadFixture, readXML } from './test-utils.js'; + +describe('SSR support', () => { + /** @type {import('./test-utils.js').Fixture} */ + let fixture; + + before(async () => { + fixture = await loadFixture({ + root: './fixtures/ssr/', + }); + await fixture.build(); + }); + + it('SSR pages require zero config', async () => { + const data = await readXML(fixture.readFile('/client/sitemap-0.xml')); + const urls = data.urlset.url; + + assert.equal(urls[0].loc[0], 'http://example.com/one/'); + assert.equal(urls[1].loc[0], 'http://example.com/two/'); + }); +}); diff --git a/packages/integrations/sitemap/test/staticPaths.test.js b/packages/integrations/sitemap/test/staticPaths.test.js new file mode 100644 index 000000000..7df9d5cb6 --- /dev/null +++ b/packages/integrations/sitemap/test/staticPaths.test.js @@ -0,0 +1,48 @@ +import assert from 'node:assert/strict'; +import { before, describe, it } from 'node:test'; +import { loadFixture, readXML } from './test-utils.js'; + +describe('getStaticPaths support', () => { + /** @type {import('./test-utils.js').Fixture} */ + let fixture; + /** @type {string[]} */ + let urls; + + before(async () => { + fixture = await loadFixture({ + root: './fixtures/static/', + trailingSlash: 'always', + }); + await fixture.build(); + + const data = await readXML(fixture.readFile('/sitemap-0.xml')); + urls = data.urlset.url.map((url) => url.loc[0]); + }); + + it('requires zero config for getStaticPaths', async () => { + assert.equal(urls.includes('http://example.com/one/'), true); + assert.equal(urls.includes('http://example.com/two/'), true); + }); + + it('does not include 404 pages', () => { + assert.equal(urls.includes('http://example.com/404/'), false); + }); + + it('does not include nested 404 pages', () => { + assert.equal(urls.includes('http://example.com/de/404/'), false); + }); + + it('includes numerical pages', () => { + assert.equal(urls.includes('http://example.com/123/'), true); + }); + + it('includes numerical 404 pages if not for i18n', () => { + assert.equal(urls.includes('http://example.com/products-by-id/405/'), true); + assert.equal(urls.includes('http://example.com/products-by-id/404/'), true); + }); + + it('should render the endpoint', async () => { + const page = await fixture.readFile('./it/manifest'); + assert.match(page, /I'm a route in the "it" language./); + }); +}); diff --git a/packages/integrations/sitemap/test/test-utils.js b/packages/integrations/sitemap/test/test-utils.js new file mode 100644 index 000000000..74bba6a44 --- /dev/null +++ b/packages/integrations/sitemap/test/test-utils.js @@ -0,0 +1,29 @@ +import * as xml2js from 'xml2js'; +import { loadFixture as baseLoadFixture } from '../../../astro/test/test-utils.js'; + +/** + * @typedef {import('../../../astro/test/test-utils').Fixture} Fixture + */ + +export function loadFixture(inlineConfig) { + if (!inlineConfig?.root) throw new Error("Must provide { root: './fixtures/...' }"); + + // resolve the relative root (i.e. "./fixtures/tailwindcss") to a full filepath + // without this, the main `loadFixture` helper will resolve relative to `packages/astro/test` + return baseLoadFixture({ + ...inlineConfig, + root: new URL(inlineConfig.root, import.meta.url).toString(), + }); +} + +export function readXML(fileOrPromise) { + const parseString = xml2js.parseString; + return Promise.resolve(fileOrPromise).then((xml) => { + return new Promise((resolve, reject) => { + parseString(xml, function (err, result) { + if (err) return reject(err); + resolve(result); + }); + }); + }); +} diff --git a/packages/integrations/sitemap/test/trailing-slash.test.js b/packages/integrations/sitemap/test/trailing-slash.test.js new file mode 100644 index 000000000..181f0def5 --- /dev/null +++ b/packages/integrations/sitemap/test/trailing-slash.test.js @@ -0,0 +1,127 @@ +import assert from 'node:assert/strict'; +import { before, describe, it } from 'node:test'; +import { loadFixture, readXML } from './test-utils.js'; + +describe('Trailing slash', () => { + /** @type {import('./test-utils').Fixture} */ + let fixture; + + describe('trailingSlash: ignore', () => { + describe('build.format: directory', () => { + before(async () => { + fixture = await loadFixture({ + root: './fixtures/trailing-slash/', + trailingSlash: 'ignore', + build: { + format: 'directory', + }, + }); + await fixture.build(); + }); + + it('URLs end with trailing slash', async () => { + const data = await readXML(fixture.readFile('/sitemap-0.xml')); + const urls = data.urlset.url; + + assert.equal(urls[0].loc[0], 'http://example.com/'); + assert.equal(urls[1].loc[0], 'http://example.com/one/'); + assert.equal(urls[2].loc[0], 'http://example.com/two/'); + }); + }); + + describe('build.format: file', () => { + before(async () => { + fixture = await loadFixture({ + root: './fixtures/trailing-slash/', + trailingSlash: 'ignore', + build: { + format: 'file', + }, + }); + await fixture.build(); + }); + + it('URLs do not end with trailing slash', async () => { + const data = await readXML(fixture.readFile('/sitemap-0.xml')); + const urls = data.urlset.url; + + assert.equal(urls[0].loc[0], 'http://example.com'); + assert.equal(urls[1].loc[0], 'http://example.com/one'); + assert.equal(urls[2].loc[0], 'http://example.com/two'); + }); + }); + }); + + describe('trailingSlash: never', () => { + before(async () => { + fixture = await loadFixture({ + root: './fixtures/trailing-slash/', + trailingSlash: 'never', + }); + await fixture.build(); + }); + + it('URLs do not end with trailing slash', async () => { + const data = await readXML(fixture.readFile('/sitemap-0.xml')); + const urls = data.urlset.url; + + assert.equal(urls[0].loc[0], 'http://example.com'); + assert.equal(urls[1].loc[0], 'http://example.com/one'); + assert.equal(urls[2].loc[0], 'http://example.com/two'); + }); + describe('with base path', () => { + before(async () => { + fixture = await loadFixture({ + root: './fixtures/trailing-slash/', + trailingSlash: 'never', + base: '/base', + }); + await fixture.build(); + }); + + it('URLs do not end with trailing slash', async () => { + const data = await readXML(fixture.readFile('/sitemap-0.xml')); + const urls = data.urlset.url; + assert.equal(urls[0].loc[0], 'http://example.com/base'); + assert.equal(urls[1].loc[0], 'http://example.com/base/one'); + assert.equal(urls[2].loc[0], 'http://example.com/base/two'); + }); + }); + }); + + describe('trailingSlash: always', () => { + before(async () => { + fixture = await loadFixture({ + root: './fixtures/trailing-slash/', + trailingSlash: 'always', + }); + await fixture.build(); + }); + + it('URLs end with trailing slash', async () => { + const data = await readXML(fixture.readFile('/sitemap-0.xml')); + const urls = data.urlset.url; + assert.equal(urls[0].loc[0], 'http://example.com/'); + assert.equal(urls[1].loc[0], 'http://example.com/one/'); + assert.equal(urls[2].loc[0], 'http://example.com/two/'); + }); + describe('with base path', () => { + before(async () => { + fixture = await loadFixture({ + root: './fixtures/trailing-slash/', + trailingSlash: 'always', + base: '/base', + }); + await fixture.build(); + }); + + it('URLs end with trailing slash', async () => { + const data = await readXML(fixture.readFile('/sitemap-0.xml')); + const urls = data.urlset.url; + assert.equal(urls[0].loc[0], 'http://example.com/base/'); + assert.equal(urls[1].loc[0], 'http://example.com/base/one/'); + assert.equal(urls[2].loc[0], 'http://example.com/base/two/'); + }); + }); + }); +}); diff --git a/packages/integrations/sitemap/test/units/generate-sitemap.test.js b/packages/integrations/sitemap/test/units/generate-sitemap.test.js new file mode 100644 index 000000000..fbf4e7858 --- /dev/null +++ b/packages/integrations/sitemap/test/units/generate-sitemap.test.js @@ -0,0 +1,147 @@ +import assert from 'node:assert/strict'; +import { describe, it } from 'node:test'; +import { generateSitemap } from '../../dist/generate-sitemap.js'; + +const site = 'http://example.com'; + +describe('generateSitemap', () => { + describe('basic', () => { + it('works', () => { + const items = generateSitemap( + [ + // All pages + `${site}/a`, + `${site}/b`, + `${site}/c`, + ], + site, + ); + assert.equal(items.length, 3); + assert.equal(items[0].url, `${site}/a`); + assert.equal(items[1].url, `${site}/b`); + assert.equal(items[2].url, `${site}/c`); + }); + + it('sorts the items', () => { + const items = generateSitemap( + [ + // All pages + `${site}/c`, + `${site}/a`, + `${site}/b`, + ], + site, + ); + assert.equal(items.length, 3); + assert.equal(items[0].url, `${site}/a`); + assert.equal(items[1].url, `${site}/b`); + assert.equal(items[2].url, `${site}/c`); + }); + + it('sitemap props are passed to items', () => { + const now = new Date(); + const items = generateSitemap( + [ + // All pages + `${site}/a`, + `${site}/b`, + `${site}/c`, + ], + site, + { + changefreq: 'monthly', + lastmod: now, + priority: 0.5, + }, + ); + + assert.equal(items.length, 3); + + assert.equal(items[0].url, `${site}/a`); + assert.equal(items[0].changefreq, 'monthly'); + assert.equal(items[0].lastmod, now.toISOString()); + assert.equal(items[0].priority, 0.5); + + assert.equal(items[1].url, `${site}/b`); + assert.equal(items[1].changefreq, 'monthly'); + assert.equal(items[1].lastmod, now.toISOString()); + assert.equal(items[1].priority, 0.5); + + assert.equal(items[2].url, `${site}/c`); + assert.equal(items[2].changefreq, 'monthly'); + assert.equal(items[2].lastmod, now.toISOString()); + assert.equal(items[2].priority, 0.5); + }); + }); + + describe('i18n', () => { + it('works', () => { + const items = generateSitemap( + [ + // All pages + `${site}/a`, + `${site}/b`, + `${site}/c`, + `${site}/es/a`, + `${site}/es/b`, + `${site}/es/c`, + `${site}/fr/a`, + `${site}/fr/b`, + // `${site}/fr-CA/c`, (intentionally missing for testing) + ], + site, + { + i18n: { + defaultLocale: 'en', + locales: { + en: 'en-US', + es: 'es-ES', + fr: 'fr-CA', + }, + }, + }, + ); + + assert.equal(items.length, 8); + + const aLinks = [ + { url: `${site}/a`, lang: 'en-US' }, + { url: `${site}/es/a`, lang: 'es-ES' }, + { url: `${site}/fr/a`, lang: 'fr-CA' }, + ]; + const bLinks = [ + { url: `${site}/b`, lang: 'en-US' }, + { url: `${site}/es/b`, lang: 'es-ES' }, + { url: `${site}/fr/b`, lang: 'fr-CA' }, + ]; + const cLinks = [ + { url: `${site}/c`, lang: 'en-US' }, + { url: `${site}/es/c`, lang: 'es-ES' }, + ]; + + assert.equal(items[0].url, `${site}/a`); + assert.deepEqual(items[0].links, aLinks); + + assert.equal(items[1].url, `${site}/b`); + assert.deepEqual(items[1].links, bLinks); + + assert.equal(items[2].url, `${site}/c`); + assert.deepEqual(items[2].links, cLinks); + + assert.equal(items[3].url, `${site}/es/a`); + assert.deepEqual(items[3].links, aLinks); + + assert.equal(items[4].url, `${site}/es/b`); + assert.deepEqual(items[4].links, bLinks); + + assert.equal(items[5].url, `${site}/es/c`); + assert.deepEqual(items[5].links, cLinks); + + assert.equal(items[6].url, `${site}/fr/a`); + assert.deepEqual(items[6].links, aLinks); + + assert.equal(items[7].url, `${site}/fr/b`); + assert.deepEqual(items[7].links, bLinks); + }); + }); +}); diff --git a/packages/integrations/sitemap/tsconfig.json b/packages/integrations/sitemap/tsconfig.json new file mode 100644 index 000000000..1504b4b6d --- /dev/null +++ b/packages/integrations/sitemap/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../../../tsconfig.base.json", + "include": ["src"], + "compilerOptions": { + "outDir": "./dist" + } +} |