diff --git a/packages/cli/src/plugins/copy/plugin-copy-sitemap.js b/packages/cli/src/plugins/copy/plugin-copy-sitemap.js
new file mode 100644
index 000000000..5e80f9f21
--- /dev/null
+++ b/packages/cli/src/plugins/copy/plugin-copy-sitemap.js
@@ -0,0 +1,36 @@
+import fs from 'fs/promises';
+
+async function writeSitemap(compilation) {
+ try {
+ const { scratchDir, projectDirectory } = compilation.context;
+ const adapterScratchUrl = new URL('./sitemap.xml', scratchDir);
+
+ // Check if module exists
+ const sitemapModule = await import(`${projectDirectory}/src/sitemap.xml.js`);
+ const sitemap = await sitemapModule.generateSitemap(compilation);
+
+ await fs.writeFile(adapterScratchUrl, sitemap);
+ console.info('Wrote sitemap to ./sitemap.xml');
+
+ return adapterScratchUrl;
+ } catch (error) {
+ console.error('Error in sitemapAdapter:', error);
+ }
+}
+
+const greenwoodPluginCopySitemap = [{
+ type: 'copy',
+ name: 'plugin-copy-sitemap',
+ provider: async (compilation) => {
+
+ const { outputDir } = compilation.context;
+ const sitemapScratchUrl = await writeSitemap(compilation);
+
+ return [{
+ from: sitemapScratchUrl,
+ to: new URL('./sitemap.xml', outputDir)
+ }];
+ }
+}];
+
+export { greenwoodPluginCopySitemap };
\ No newline at end of file
diff --git a/packages/cli/src/plugins/resource/plugin-resource-sitemap.js b/packages/cli/src/plugins/resource/plugin-resource-sitemap.js
new file mode 100644
index 000000000..0eb9b6c80
--- /dev/null
+++ b/packages/cli/src/plugins/resource/plugin-resource-sitemap.js
@@ -0,0 +1,37 @@
+import { ResourceInterface } from '@greenwood/cli/src/lib/resource-interface.js';
+
+class SitemapResource extends ResourceInterface {
+ constructor(compilation, options) {
+ super(compilation, options);
+ }
+
+ async shouldServe(url) {
+ return url.pathname.endsWith('sitemap.xml');
+ }
+
+ // eslint-disable-next-line no-unused-vars
+ async serve(url) {
+
+ const { projectDirectory } = this.compilation.context;
+
+ try {
+ const sitemapModule = await import(`${projectDirectory}/src/sitemap.xml.js`);
+ const sitemap = await sitemapModule.generateSitemap(this.compilation);
+ return new Response(sitemap, { headers: { 'Content-Type': 'text/xml' } });
+
+ } catch (error) {
+ console.error('Error loading module: ./sitemap.xml.js Does it exist?', error);
+ return new Response('Sitemap oops.', { headers: { 'Content-Type': 'text/xml' } });
+ }
+
+ }
+
+}
+
+const greenwoodPluginSitemap = {
+ type: 'resource',
+ name: 'plugin-resource-sitemap',
+ provider: (compilation, options) => new SitemapResource(compilation, options)
+};
+
+export { greenwoodPluginSitemap };
\ No newline at end of file
diff --git a/packages/cli/test/cases/build.plugins.copy-sitemap/build.plugins.copy-sitemap.spec.js b/packages/cli/test/cases/build.plugins.copy-sitemap/build.plugins.copy-sitemap.spec.js
new file mode 100644
index 000000000..1d91dbee4
--- /dev/null
+++ b/packages/cli/test/cases/build.plugins.copy-sitemap/build.plugins.copy-sitemap.spec.js
@@ -0,0 +1,84 @@
+/*
+ * Use Case
+ * Run Greenwood with the sitemap adapter plugin.
+ *
+ * User Result
+ * Should generate a static Greenwood build with a sitemap rendered.
+ *
+ * User Command
+ * greenwood build
+ *
+ * User Config
+ * import { greenwoodPluginAdapterSitemap } from '../../../src/index.js';
+*
+* export default {
+* plugins: [
+* greenwoodPluginAdapterSitemap()
+* ]
+* };
+ *
+ * User Workspace
+ * TBD
+ */
+import chai from 'chai';
+import fs from 'fs/promises';
+import path from 'path';
+import { checkResourceExists } from '../../../../cli/src/lib/resource-utils.js';
+import { getSetupFiles } from '../../../../../test/utils.js';
+import { Runner } from 'gallinago';
+import { fileURLToPath } from 'url';
+
+const expect = chai.expect;
+
+describe('Build Greenwood With: ', function() {
+ const LABEL = 'Sitemap Adapter plugin output';
+ const cliPath = path.join(process.cwd(), 'packages/cli/src/index.js');
+ const outputPath = fileURLToPath(new URL('.', import.meta.url));
+ const publicDir = path.join(outputPath, 'public');
+
+ let runner;
+
+ before(function() {
+ this.context = {
+ publicDir: path.join(outputPath, 'public')
+ };
+ runner = new Runner();
+ });
+
+ describe(LABEL, function() {
+ before(function() {
+ runner.setup(outputPath, getSetupFiles(outputPath));
+ runner.runCommand(cliPath, 'build');
+ });
+
+ describe('sitemap.xml', function() {
+ it('should be present', async function() {
+ const sitemapPath = path.join(publicDir, 'sitemap.xml');
+
+ const itExists = await checkResourceExists(new URL(`file://${sitemapPath}`));
+ expect(itExists).to.be.equal(true);
+
+ });
+
+ it('should have the correct first element in the list', async function() {
+ const sitemapPath = path.join(publicDir, 'sitemap.xml');
+ const text = await fs.readFile(sitemapPath, 'utf8');
+
+ const regex = /(http:\/\/www\.example\.com\/about\/)<\/loc>/;
+ const match = text.match(regex);
+
+ expect(match[1]).to.equal('http://www.example.com/about/');
+ });
+
+ });
+
+ });
+
+ after(function() {
+ runner.stopCommand();
+ runner.teardown([
+ path.join(outputPath, '.greenwood')
+ ]);
+ });
+
+});
\ No newline at end of file
diff --git a/packages/cli/test/cases/build.plugins.copy-sitemap/greenwood.config.js b/packages/cli/test/cases/build.plugins.copy-sitemap/greenwood.config.js
new file mode 100644
index 000000000..07b79d186
--- /dev/null
+++ b/packages/cli/test/cases/build.plugins.copy-sitemap/greenwood.config.js
@@ -0,0 +1,3 @@
+export default {
+ plugins: []
+};
\ No newline at end of file
diff --git a/packages/cli/test/cases/build.plugins.copy-sitemap/src/pages/about.md b/packages/cli/test/cases/build.plugins.copy-sitemap/src/pages/about.md
new file mode 100644
index 000000000..4093eb641
--- /dev/null
+++ b/packages/cli/test/cases/build.plugins.copy-sitemap/src/pages/about.md
@@ -0,0 +1,3 @@
+# About Us
+
+Lorem ipsum.
\ No newline at end of file
diff --git a/packages/cli/test/cases/build.plugins.copy-sitemap/src/pages/index.md b/packages/cli/test/cases/build.plugins.copy-sitemap/src/pages/index.md
new file mode 100644
index 000000000..46c5f357e
--- /dev/null
+++ b/packages/cli/test/cases/build.plugins.copy-sitemap/src/pages/index.md
@@ -0,0 +1,3 @@
+## Home Page
+
+Welcome!
\ No newline at end of file
diff --git a/packages/cli/test/cases/build.plugins.copy-sitemap/src/sitemap.xml.js b/packages/cli/test/cases/build.plugins.copy-sitemap/src/sitemap.xml.js
new file mode 100644
index 000000000..7fbac0ba1
--- /dev/null
+++ b/packages/cli/test/cases/build.plugins.copy-sitemap/src/sitemap.xml.js
@@ -0,0 +1,15 @@
+async function generateSitemap(compilation) {
+ const urls = compilation.graph.map((page) => {
+ return `
+ http://www.example.com${page.route}
+ `;
+ });
+ return `
+
+
+${urls.join('\n')}
+
+`;
+}
+
+export { generateSitemap };
\ No newline at end of file
diff --git a/packages/cli/test/cases/develop.plugins.resource-sitemap/develop.plugins.resource-sitemap.spec.js b/packages/cli/test/cases/develop.plugins.resource-sitemap/develop.plugins.resource-sitemap.spec.js
new file mode 100644
index 000000000..3e9efcee1
--- /dev/null
+++ b/packages/cli/test/cases/develop.plugins.resource-sitemap/develop.plugins.resource-sitemap.spec.js
@@ -0,0 +1,73 @@
+
+import path from 'path';
+import { Runner } from 'gallinago';
+import { fileURLToPath } from 'url';
+
+import chai from 'chai';
+const expect = chai.expect;
+
+describe('Develop Sitemap With: ', function() {
+
+ const LABEL = 'Sitemap Resource plugin output';
+
+ const cliPath = path.join(process.cwd(), 'packages/cli/src/index.js');
+ const outputPath = fileURLToPath(new URL('.', import.meta.url));
+ const hostname = 'http://localhost';
+ const port = 1984;
+ let runner;
+
+ before(function() {
+ this.context = {
+ hostname: `${hostname}:${port}`
+ };
+ runner = new Runner();
+ });
+
+ describe(LABEL, function() {
+
+ before(async function() {
+ runner.setup(outputPath);
+
+ return new Promise((resolve) => {
+ setTimeout(() => {
+ resolve();
+ }, 5000);
+
+ runner.runCommand(cliPath, 'develop', { async: true });
+ });
+ });
+
+ describe('Sitemap.xml', function() {
+ let response = {};
+ let text;
+
+ before(async function() {
+ response = await fetch(`${hostname}:${port}/sitemap.xml`);
+ text = await response.text();
+ });
+
+ it('should return a 200', function() {
+ expect(response.status).to.equal(200);
+ });
+
+ it('should return the correct content type', function() {
+ expect(response.headers.get('content-type')).to.equal('text/xml');
+ });
+
+ it('should contain loc element', function() {
+ const regex = /(http:\/\/www\.example\.com\/about\/)<\/loc>/;
+ const match = text.match(regex);
+
+ expect(match[1]).to.equal('http://www.example.com/about/');
+
+ });
+ });
+ });
+
+ after(function() {
+ runner.stopCommand();
+ runner.teardown([
+ path.join(outputPath, '.greenwood')
+ ]);
+ });
+});
\ No newline at end of file
diff --git a/packages/cli/test/cases/develop.plugins.resource-sitemap/greenwood.config.js b/packages/cli/test/cases/develop.plugins.resource-sitemap/greenwood.config.js
new file mode 100644
index 000000000..0e4a08694
--- /dev/null
+++ b/packages/cli/test/cases/develop.plugins.resource-sitemap/greenwood.config.js
@@ -0,0 +1,5 @@
+
+export default {
+ plugins: [
+ ]
+};
\ No newline at end of file
diff --git a/packages/cli/test/cases/develop.plugins.resource-sitemap/src/pages/about.md b/packages/cli/test/cases/develop.plugins.resource-sitemap/src/pages/about.md
new file mode 100644
index 000000000..4093eb641
--- /dev/null
+++ b/packages/cli/test/cases/develop.plugins.resource-sitemap/src/pages/about.md
@@ -0,0 +1,3 @@
+# About Us
+
+Lorem ipsum.
\ No newline at end of file
diff --git a/packages/cli/test/cases/develop.plugins.resource-sitemap/src/pages/index.md b/packages/cli/test/cases/develop.plugins.resource-sitemap/src/pages/index.md
new file mode 100644
index 000000000..46c5f357e
--- /dev/null
+++ b/packages/cli/test/cases/develop.plugins.resource-sitemap/src/pages/index.md
@@ -0,0 +1,3 @@
+## Home Page
+
+Welcome!
\ No newline at end of file
diff --git a/packages/cli/test/cases/develop.plugins.resource-sitemap/src/sitemap.xml.js b/packages/cli/test/cases/develop.plugins.resource-sitemap/src/sitemap.xml.js
new file mode 100644
index 000000000..2b1f8eba0
--- /dev/null
+++ b/packages/cli/test/cases/develop.plugins.resource-sitemap/src/sitemap.xml.js
@@ -0,0 +1,16 @@
+async function generateSitemap(compilation) {
+ const urls = compilation.graph.map((page) => {
+ return `
+ http://www.example.com${page.route}
+ `;
+ });
+
+ return `
+
+
+${urls.join('\n')}
+
+ `;
+}
+
+export { generateSitemap };
\ No newline at end of file