Skip to content

Commit

Permalink
Merge pull request #89 from iamvishnusankar/dynamic-sitemap-support
Browse files Browse the repository at this point in the history
Dynamic sitemap support
  • Loading branch information
iamvishnusankar committed Jan 10, 2021
2 parents 4baf1b2 + 348e066 commit 2a7463f
Show file tree
Hide file tree
Showing 17 changed files with 224 additions and 97 deletions.
61 changes: 60 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# next-sitemap

Sitemap generator for next.js. Generate sitemap(s) and robots.txt for all static/pre-rendered pages.
Sitemap generator for next.js. Generate sitemap(s) and robots.txt for all static/pre-rendered/dynamic/server-side pages.

## Table of contents

Expand All @@ -12,6 +12,7 @@ Sitemap generator for next.js. Generate sitemap(s) and robots.txt for all static
- [Configuration Options](#next-sitemapjs-options)
- [Custom transformation function](#custom-transformation-function)
- [Full configuration example](#full-configuration-example)
- [Generating dynamic/server-side sitemaps](#generating-dynamicserver-side-sitemaps)

## Getting started

Expand Down Expand Up @@ -193,6 +194,64 @@ Sitemap: https://example.com/my-custom-sitemap-2.xml
Sitemap: https://example.com/my-custom-sitemap-3.xml
```

## Generating dynamic/server-side sitemaps

`next-sitemap` now provides a simple API to generate server side sitemaps. This will help to dynamically generate sitemaps by sourcing data from CMS or custom source.

Here's a sample script to generate sitemaps on server side. Create `pages/server-sitemap.xml/index.tsx` page and add the following content.

```ts
// pages/server-sitemap.xml/index.tsx

import { getServerSideSitemap } from 'next-sitemap'
import { GetServerSideProps } from 'next'

export const getServerSideProps: GetServerSideProps = async (ctx) => {
// Method to source urls from cms
// const urls = await fetch('https//example.com/api')

const fields = [
{
loc: 'https://example.com', // Absolute url
lastmod: new Date().toISOString(),
// changefreq
// priority
},
{
loc: 'https://example.com/dynamic-path-2', // Absolute url
lastmod: new Date().toISOString(),
// changefreq
// priority
},
]

return getServerSideSitemap(ctx, fields)
}

// Default export to prevent next.js errors
export default () => {}
```

Now, `next.js` is serving the dynamic sitemap from `http://localhost:3000/server-sitemap.xml`.

List the dynamic sitemap page in `robotTxtOptions.additionalSitemaps` and exclude this path from static sitemap list.

```js
// next-sitemap.js
module.exports = {
siteUrl: 'https://example.com',
generateRobotsTxt: true,
exclude: ['/server-sitemap.xml'], // <= exclude here
robotsTxtOptions: {
additionalSitemaps: [
'https://example.com/server-sitemap.xml', // <==== Add here
],
},
}
```

In this way, `next-sitemap` will manage the sitemaps for all your static pages and your dynamic sitemap will be listed on robots.txt.

## Contribution

All PRs are welcome :)
2 changes: 1 addition & 1 deletion azure-pipeline.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
name: 1.3$(rev:.r)
name: 1.4$(rev:.r)
trigger:
branches:
include:
Expand Down
1 change: 1 addition & 0 deletions example/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
"postbuild": "next-sitemap"
},
"dependencies": {
"@types/react-dom": "^17.0.0",
"next": "^10.0.4",
"react": "^17.0.1",
"react-dom": "^17.0.1"
Expand Down
27 changes: 27 additions & 0 deletions example/pages/server-sitemap.xml/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
/* eslint-disable @typescript-eslint/no-empty-function */
import { getServerSideSitemap } from 'next-sitemap'
import { GetServerSideProps } from 'next'

export const getServerSideProps: GetServerSideProps = async (ctx) => {
// Method to source urls from cms
// const urls = await fetch('https//example.com/api')

return getServerSideSitemap(ctx, [
{
loc: 'https://example.com',
lastmod: new Date().toISOString(),
// changefreq
// priority
},
{
loc: 'https://example.com/dynamic-path-2',
lastmod: new Date().toISOString(),
// changefreq
// priority
},
])
}

// Default export to prevent next.js errors
export default () => {}
2 changes: 1 addition & 1 deletion packages/next-sitemap/bin/next-sitemap
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
#!/usr/bin/env node
require('../dist/cjs')
require('../dist/cjs/cli')
10 changes: 7 additions & 3 deletions packages/next-sitemap/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,12 @@
"version": "1.0.0",
"description": "Sitemap generator for next.js",
"main": "dist/cjs/index.js",
"module": "dist/esnext/index.js",
"module": "dist/esm/index.js",
"types": "dist/@types/index.d.ts",
"repository": "https://github.com/iamvishnusankar/next-sitemap.git",
"author": "Vishnu Sankar (@iamvishnusankar)",
"license": "MIT",
"sideEffects": false,
"publishConfig": {
"access": "public"
},
Expand All @@ -16,12 +17,15 @@
},
"scripts": {
"lint": "tsc --noEmit --declaration",
"build": "tsc && yarn build:esnext",
"build:esnext": "tsc --module esnext --outDir dist/esnext"
"build": "tsc && yarn build:esm",
"build:esm": "tsc --module es2015 --outDir dist/esm"
},
"dependencies": {
"@corex/deepmerge": "^2.5.3",
"matcher": "^3.0.0",
"minimist": "^1.2.5"
},
"peerDependencies": {
"next": "*"
}
}
52 changes: 52 additions & 0 deletions packages/next-sitemap/src/cli.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/* eslint-disable @typescript-eslint/no-non-null-assertion */
import { loadConfig, getRuntimeConfig, updateConfig } from './config'
import { loadManifest } from './manifest'
import { createUrlSet, generateUrl } from './url'
import { generateSitemap } from './sitemap/generateSitemap'
import { toChunks } from './array'
import {
resolveSitemapChunks,
getRuntimePaths,
getConfigFilePath,
} from './path'
import { exportRobotsTxt } from './robots-txt'

// Get config file path
const configFilePath = getConfigFilePath()

// Load next-sitemap.js
let config = loadConfig(configFilePath)

// Get runtime paths
const runtimePaths = getRuntimePaths(config)

// get runtime config
const runtimeConfig = getRuntimeConfig(runtimePaths)

// Update config with runtime config
config = updateConfig(config, runtimeConfig)

// Load next.js manifest files
const manifest = loadManifest(runtimePaths)

// Create url-set based on config and manifest
const urlSet = createUrlSet(config, manifest)

// Split sitemap into multiple files
const chunks = toChunks(urlSet, config.sitemapSize!)
const sitemapChunks = resolveSitemapChunks(runtimePaths.SITEMAP_FILE, chunks)

// All sitemaps array to keep track of generated sitemap files.
// Later to be added on robots.txt
const allSitemaps: string[] = []

// Generate sitemaps from chunks
sitemapChunks.forEach((chunk) => {
generateSitemap(chunk)
allSitemaps.push(generateUrl(config.siteUrl, `/${chunk.filename}`))
})

// Generate robots.txt
if (config.generateRobotsTxt) {
exportRobotsTxt(runtimePaths, config, allSitemaps)
}
6 changes: 3 additions & 3 deletions packages/next-sitemap/src/config/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,9 @@ export const transformSitemap = (
): ISitemapFiled => {
return {
loc: url,
changefreq: config.changefreq,
priority: config.priority,
lastmod: config.autoLastmod ? new Date().toISOString() : undefined,
changefreq: config?.changefreq,
priority: config?.priority,
lastmod: config?.autoLastmod ? new Date().toISOString() : undefined,
}
}

Expand Down
28 changes: 28 additions & 0 deletions packages/next-sitemap/src/dynamic-sitemap/getServerSideSitemap.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
import { ISitemapFiled } from '../interface'
import { buildSitemapXml } from '../sitemap/buildSitemapXml'

export const getServerSideSitemap = async (
context: import('next').GetServerSidePropsContext,
fields: ISitemapFiled[]
) => {
const sitemapContent = buildSitemapXml(fields)

if (context && context.res) {
const { res } = context

// Set header
res.setHeader('Content-Type', 'text/xml')

// Write the sitemap context to resonse
res.write(sitemapContent)

// End response
res.end()
}

// Empty props
return {
props: {},
}
}
1 change: 1 addition & 0 deletions packages/next-sitemap/src/dynamic-sitemap/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './getServerSideSitemap'
54 changes: 2 additions & 52 deletions packages/next-sitemap/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,52 +1,2 @@
/* eslint-disable @typescript-eslint/no-non-null-assertion */
import { loadConfig, getRuntimeConfig, updateConfig } from './config'
import { loadManifest } from './manifest'
import { createUrlSet, generateUrl } from './url'
import { generateSitemap } from './sitemap'
import { toChunks } from './array'
import {
resolveSitemapChunks,
getRuntimePaths,
getConfigFilePath,
} from './path'
import { exportRobotsTxt } from './robots-txt'

// Get config file path
const configFilePath = getConfigFilePath()

// Load next-sitemap.js
let config = loadConfig(configFilePath)

// Get runtime paths
const runtimePaths = getRuntimePaths(config)

// get runtime config
const runtimeConfig = getRuntimeConfig(runtimePaths)

// Update config with runtime config
config = updateConfig(config, runtimeConfig)

// Load next.js manifest files
const manifest = loadManifest(runtimePaths)

// Create url-set based on config and manifest
const urlSet = createUrlSet(config, manifest)

// Split sitemap into multiple files
const chunks = toChunks(urlSet, config.sitemapSize!)
const sitemapChunks = resolveSitemapChunks(runtimePaths.SITEMAP_FILE, chunks)

// All sitemaps array to keep track of generated sitemap files.
// Later to be added on robots.txt
const allSitemaps: string[] = []

// Generate sitemaps from chunks
sitemapChunks.forEach((chunk) => {
generateSitemap(config, chunk.path, chunk.fields)
allSitemaps.push(generateUrl(config.siteUrl, `/${chunk.filename}`))
})

// Generate robots.txt
if (config.generateRobotsTxt) {
exportRobotsTxt(runtimePaths, config, allSitemaps)
}
export * from './sitemap/buildSitemapXml'
export * from './dynamic-sitemap'
16 changes: 16 additions & 0 deletions packages/next-sitemap/src/sitemap/buildSitemapXml.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { ISitemapFiled } from '../interface'
import { withXMLTemplate } from './withXMLTemplate'

export const buildSitemapXml = (fields: ISitemapFiled[]): string => {
const content = fields.reduce((prev, curr) => {
let field = ''
for (const key of Object.keys(curr)) {
field += `<${key}>${curr[key]}</${key}>`
}

// Append previous value and return
return `${prev}<url>${field}</url>\n`
}, '')

return withXMLTemplate(content)
}
8 changes: 8 additions & 0 deletions packages/next-sitemap/src/sitemap/generateSitemap.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { ISitemapChunk } from '../interface'
import { exportFile } from '../file'
import { buildSitemapXml } from './buildSitemapXml'

export const generateSitemap = (chunk: ISitemapChunk): void => {
const sitemapXml = buildSitemapXml(chunk.fields)
exportFile(chunk.path, sitemapXml)
}
33 changes: 0 additions & 33 deletions packages/next-sitemap/src/sitemap/index.ts

This file was deleted.

5 changes: 5 additions & 0 deletions packages/next-sitemap/src/sitemap/withXMLTemplate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
/* eslint-disable @typescript-eslint/no-non-null-assertion */

export const withXMLTemplate = (content: string): string => {
return `<?xml version="1.0" encoding="UTF-8"?>\n<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:news="http://www.google.com/schemas/sitemap-news/0.9" xmlns:xhtml="http://www.w3.org/1999/xhtml" xmlns:mobile="http://www.google.com/schemas/sitemap-mobile/1.0" xmlns:image="http://www.google.com/schemas/sitemap-image/1.1" xmlns:video="http://www.google.com/schemas/sitemap-video/1.1">\n${content}</urlset>`
}
6 changes: 4 additions & 2 deletions packages/next-sitemap/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@
"rootDir": "src",
"outDir": "dist/cjs",
"declarationDir": "dist/@types",
"module": "CommonJS"
"module": "CommonJS",
"target": "ES2015"
},
"include": ["src"]
"include": ["src"],
"exclude": ["node_modules"]
}

0 comments on commit 2a7463f

Please sign in to comment.