Skip to content

Commit

Permalink
feat(sitemap-generator): implement a first basic sitemap generator (b…
Browse files Browse the repository at this point in the history
…eta) (#63)
  • Loading branch information
YoeriNijs committed Jun 9, 2023
1 parent 9832b65 commit 20b89be
Show file tree
Hide file tree
Showing 7 changed files with 247 additions and 0 deletions.
65 changes: 65 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ Check out the [demo application](https://github.com/YoeriNijs/vienna-demo-app).
- [Plugins](#plugins)
- [Logger](#logger)
- [Component testing](#component-testing)
- [Sitemap generator](#sitemap-generator)

## Install

Expand Down Expand Up @@ -1286,6 +1287,70 @@ describe('VComponentFactory', () => {
})
```

## Sitemap generator

Vienna ships a basic sitemap generator to create a sitemap on build time. The generator itself is not part of the core
framework, but a simple utility application that you may call somewhere in your build process.

The sitemap generator accepts Vienna routes and manual locations. Vienna routes are always prioritized with a priority
of 1.0 since we assume that most routes are basic website routes, such as homepage and information pages. The Vienna
route is always excluded from the sitemap when there is a guard configured, because the generator does not have any
logic whether the url with the provided guard should be private or not.

Manual locations are, obviously, custom locations. You can configure priority and changefreq for every custom location,
but you are not forced to.

Please keep in mind that the sitemap generator is pretty dumb. This means: garbage in, garbage out. If you provide a
Vienna route for path x, and you also create a manual location for path x, the sitemap will contain two records for the
same path.

Example usage:

```
import {VSitemapGenerator} from 'vienna-ts/sitemap-generator';
import {MY_VIENNA_ROUTES} from "./routes.ts";
const generator = new VSitemapGenerator();
const config: VSitemapGeneratorConfig = {
routes: MY_VIENNA_ROUTES
}
const xml = generator.generate(config);
// Do something with your xml
...
```

Or, for manual locations:

```
import {VSitemapGenerator} from 'vienna-ts/sitemap-generator';
import {MY_VIENNA_ROUTES} from "./routes.ts";
const generator = new VSitemapGenerator();
const config: VSitemapGeneratorConfig = {
manual: [
{location: '/about-me', priority: 0.8}
]
}
const xml = generator.generate(config);
// Do something with your xml
...
```

If you want to create a xml file right away, just call `generateAndWriteToFile`:

```
import {VSitemapGenerator} from 'vienna-ts/sitemap-generator';
import {MY_VIENNA_ROUTES} from "./routes.ts";
const generator = new VSitemapGenerator();
const config: VSitemapGeneratorConfig = {
routes: MY_VIENNA_ROUTES
}
generator.generateAndWriteToFile(config, 'my/location/sitemap.xml');
```

# Todo

- Add renderer cache to increase rendering performance (e.g. use render event for one component + internal component id
Expand Down
93 changes: 93 additions & 0 deletions src/sitemap-generator/__tests__/v-sitemap-generator.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import {VSitemapGenerator} from "../v-sitemap-generator";
import {VSitemapGeneratorConfig} from "../v-sitemap-generator-config";

describe('VSitemapGenerator', () => {

let generator: VSitemapGenerator;

beforeEach(() => generator = new VSitemapGenerator());

describe('Empty', () => {
it('should generate nothing when config is empty', () => {
const xml = generator.generate({});
expect(xml).toEqual('<?xml version="1.0" encoding="UTF-8"?><urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.sitemaps.org/schemas/sitemap/0.9 http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd"></urlset>');
});

it('should generate nothing when there are no Vienna routes provided', () => {
const xml = generator.generate({routes: []});
expect(xml).toEqual('<?xml version="1.0" encoding="UTF-8"?><urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.sitemaps.org/schemas/sitemap/0.9 http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd"></urlset>');
});

it('should generate nothing when there are no manual items provided', () => {
const xml = generator.generate({manual: []});
expect(xml).toEqual('<?xml version="1.0" encoding="UTF-8"?><urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.sitemaps.org/schemas/sitemap/0.9 http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd"></urlset>');
});
});


describe('Vienna route', () => {
it('should generate with priority 1.0', () => {
const config: VSitemapGeneratorConfig = {
routes: [
{path: '/a', component: {}}
]
}
const xml = generator.generate(config);
expect(xml).toEqual('<?xml version="1.0" encoding="UTF-8"?><urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.sitemaps.org/schemas/sitemap/0.9 http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd"><url><loc>/a</loc><priority>1.0</priority></url></urlset>');
});

it('should not generate it when the route has a guard', () => {
const config: VSitemapGeneratorConfig = {
routes: [
{path: '/a', component: {}, guards: [jest.fn()]}
]
}
const xml = generator.generate(config);
expect(xml).toEqual('<?xml version="1.0" encoding="UTF-8"?><urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.sitemaps.org/schemas/sitemap/0.9 http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd"></urlset>');
});
});

describe('Manual', () => {
it('should generate', () => {
const config: VSitemapGeneratorConfig = {
manual: [
{location: '/b'}
]
}
const xml = generator.generate(config);
expect(xml).toEqual('<?xml version="1.0" encoding="UTF-8"?><urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.sitemaps.org/schemas/sitemap/0.9 http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd"><url><loc>/b</loc></url></urlset>');
});

it('should add changefreq', () => {
const config: VSitemapGeneratorConfig = {
manual: [
{location: '/b', changefreq: 'always'}
]
}
const xml = generator.generate(config);
expect(xml).toEqual('<?xml version="1.0" encoding="UTF-8"?><urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.sitemaps.org/schemas/sitemap/0.9 http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd"><url><loc>/b</loc><changefreq>always</changefreq></url></urlset>');
});

it('should add priority', () => {
const config: VSitemapGeneratorConfig = {
manual: [
{location: '/b', priority: 0.5}
]
}
const xml = generator.generate(config);
expect(xml).toEqual('<?xml version="1.0" encoding="UTF-8"?><urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.sitemaps.org/schemas/sitemap/0.9 http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd"><url><loc>/b</loc><priority>0.5</priority></url></urlset>');
});

it.each([-0.1, 1.1, -10, 10, null, undefined])('should not add priority when value is %s', (priority) => {
const config: VSitemapGeneratorConfig = {
manual: [
{location: '/b', priority}
]
}
const xml = generator.generate(config);
expect(xml).toEqual('<?xml version="1.0" encoding="UTF-8"?><urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.sitemaps.org/schemas/sitemap/0.9 http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd"><url><loc>/b</loc></url></urlset>');
});
});


});
4 changes: 4 additions & 0 deletions src/sitemap-generator/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export * from './v-sitemap-changefreq';
export * from './v-sitemap-generator';
export * from './v-sitemap-generator-config';
export * from './v-sitemap-manual-item';
1 change: 1 addition & 0 deletions src/sitemap-generator/v-sitemap-changefreq.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export type VSitemapChangefreq = 'never' | 'yearly' | 'monthly' | 'weekly' | 'daily' | 'hourly' | 'always';
7 changes: 7 additions & 0 deletions src/sitemap-generator/v-sitemap-generator-config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import {VRoute} from "../core";
import {VSitemapManualItem} from "./v-sitemap-manual-item";

export interface VSitemapGeneratorConfig {
routes?: VRoute[];
manual?: VSitemapManualItem[]
}
70 changes: 70 additions & 0 deletions src/sitemap-generator/v-sitemap-generator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import {VSitemapGeneratorConfig} from "./v-sitemap-generator-config";

const HEAD = `<?xml version="1.0" encoding="UTF-8"?><urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.sitemaps.org/schemas/sitemap/0.9 http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd">`

export class VSitemapGenerator {

/**
* Returns an xml string that contains a sitemap for the provided config.
* @param config
*/
generate(config: VSitemapGeneratorConfig): string | void {
return this.createXmlByConfig(config);
}

/**
* Writes a file to the provided output for the provided config.
* Example output: '/my/location/sitemap.xml'
* @param config
* @param output
*/
generateAndWriteToFile(config: VSitemapGeneratorConfig, output: string): void {
const xml = this.createXmlByConfig(config);
const fs = require("fs");
const writeStream = fs.createWriteStream(output);
writeStream.write(xml);
writeStream.end();
}

private createXmlByConfig(config: VSitemapGeneratorConfig): string {
if (this.isEmptyConfig(config)) {
return `${HEAD}</urlset>`;
}

let xml = HEAD;
if (config.routes) {
xml = config.routes
.filter(route => !(route.guards && route.guards.length > 0))
.reduce((x, route) => {
// Since this are root Vienna routes, we assume these routes contain important pages,
// such as homepage, information pages and so on. Hence, we add the priority 1.0.
// See: https://www.v9digital.com/insights/sitemap-xml-why-changefreq-priority-are-important/
x += `<url><loc>${route.path}</loc><priority>1.0</priority></url>`;
return x;
}, xml);
}

if (config.manual) {
xml = config.manual.reduce((x, item) => {
x += '<url>';
x += `<loc>${item.location}</loc>`;
if (item.changefreq) {
x += `<changefreq>${item.changefreq}</changefreq>`;
}
if (item.priority && item.priority >= 0 && item.priority <= 1) {
x += `<priority>${item.priority.toFixed(1)}</priority>`;
}
x += '</url>';
return x;
}, xml);
}

return `${xml}</urlset>`;
}

private isEmptyConfig(config: VSitemapGeneratorConfig): boolean {
return (!config.routes || config.routes.length < 1)
&& (!config.manual || config.manual.length < 1);
}

}
7 changes: 7 additions & 0 deletions src/sitemap-generator/v-sitemap-manual-item.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import {VSitemapChangefreq} from "./v-sitemap-changefreq";

export interface VSitemapManualItem {
location: string;
changefreq?: VSitemapChangefreq;
priority?: number;
}

0 comments on commit 20b89be

Please sign in to comment.