Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Dynamic Sitemap Copy Plugin (#1232) #1260

Draft
wants to merge 9 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 69 additions & 0 deletions packages/plugin-adapter-sitemap/src/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import fs from 'fs/promises';
import { ResourceInterface } from '@greenwood/cli/src/lib/resource-interface.js';

async function sitemapAdapter(compilation) {
try {
const { outputDir, projectDirectory } = compilation.context;
const adapterOutputUrl = new URL('./sitemap.xml', outputDir);

// Check if module exists
const sitemapModule = await import(`${projectDirectory}/src/sitemap.xml.js`);
const sitemap = await sitemapModule.generateSitemap(compilation);

await fs.writeFile(adapterOutputUrl, sitemap);
console.info('Wrote sitemap to ./sitemap.xml');
} catch (error) {
console.error('Error in sitemapAdapter:', error);
}
}

/*
*
* Sitemap
*
*/

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('<error>Sitemap oops.</error>', { headers: { 'Content-Type': 'text/xml' } });
}

}

}

const greenwoodPluginSitemap = (options = {}) => [{
type: 'adapter',
name: 'plugin-adapter-sitemap',
provider: (compilation) => {
return async () => {
await sitemapAdapter(compilation, options);
};
}
},
{
type: 'resource',
name: 'plugin-sitemap',
provider: (compilation, options) => new SitemapResource(compilation, options)
}];

export { greenwoodPluginSitemap };
Original file line number Diff line number Diff line change
@@ -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 = /<loc>(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')
]);
});

});
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@

import { greenwoodPluginSitemap } from '../../../src/index.js';

export default {
plugins: [
greenwoodPluginSitemap()
]
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# About Us

Lorem ipsum.
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
## Home Page

Welcome!
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
async function generateSitemap(compilation) {
const urls = compilation.graph.map((page) => {
return ` <url>
<loc>http://www.example.com${page.route}</loc>
</url>`;
});
return `
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
${urls.join('\n')}
</urlset>
`;
}

export { generateSitemap };
Original file line number Diff line number Diff line change
@@ -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 = /<loc>(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')
]);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@

import { greenwoodPluginSitemap } from '../../../src/index.js';

export default {
plugins: [
greenwoodPluginSitemap()
]
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# About Us

Lorem ipsum.
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
## Home Page

Welcome!
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
async function generateSitemap(compilation) {
const urls = compilation.graph.map((page) => {
return ` <url>
<loc>http://www.example.com${page.route}</loc>
</url>`;
});

return `
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
${urls.join('\n')}
</urlset>
`;
}

export { generateSitemap };
24 changes: 24 additions & 0 deletions packages/plugin-dynamic-sitemap/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# @greenwood/plugin-dynamic-sitemap

## Overview
Spiders love to spider. To show our love to all the spiders out there, this plugin reads
the graph and renders a sitemap.xml. Currently, it handles up to 10000 content entries, warning
after 9000 content entries.

## Usage
Add this plugin to your _greenwood.config.js_ and spread the `export`.

```javascript
import { greenwoodPluginDynamicExport } from '@greenwood/plugin-dynamic-sitemap';

export default {
...

plugins: [
greenwoodPluginDynamicExport({
"baseUrl": "https://example.com"
})
]
}
```

30 changes: 30 additions & 0 deletions packages/plugin-dynamic-sitemap/src/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import fs from 'fs/promises';

const greenwoodPluginDynamicExport = (options = {}) => [{
type: 'copy',
name: 'plugin-dynamic-sitemap',
provider: async (compilation) => {

const { baseUrl } = options;
const { outputDir } = compilation.context;

let sitemapXML = '<?xml version="1.0" encoding="UTF-8"?>\n';
sitemapXML += '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n';

compilation.graph.forEach(page => {
sitemapXML += `<url><loc>${baseUrl}${page.outputPath}</loc></url>\n`;
});

sitemapXML += '</urlset>';

const sitemapUrl = new URL('./sitemap.xml', outputDir);
await fs.writeFile(sitemapUrl, sitemapXML);

return {
from: sitemapUrl,
to: new URL('./sitemap.xml', outputDir)
};
}
}];

export { greenwoodPluginDynamicExport };
Loading
Loading