Skip to content

Commit d25cc6b

Browse files
authored
feat: Generate sitemap (#215)
* Add sitemap from marketing site * Add sitemap generator * Remove ts dependency for generating pages index * package upgrades
1 parent 3b757a7 commit d25cc6b

File tree

7 files changed

+2010
-1545
lines changed

7 files changed

+2010
-1545
lines changed

.eslintrc.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ module.exports = {
6060
},
6161
overrides: [
6262
{
63-
files: ['.eslintrc.js', 'next.config.js'],
63+
files: ['.eslintrc.js', 'next.config.js', 'index-pages.mjs'],
6464
parserOptions: {
6565
project: null,
6666
},

index-pages.mjs

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import { Dirent } from 'fs'
2+
import { readdir, writeFile } from 'fs/promises'
3+
import path from 'path'
4+
import { fileURLToPath } from 'url'
5+
6+
const __filename = fileURLToPath(import.meta.url)
7+
8+
const __dirname = path.dirname(__filename)
9+
10+
function writeUrl({ location, lastMod, priority = '0.5' }) {
11+
return ` <url>
12+
<loc>${process.env.NEXT_PUBLIC_ROOT_URL}/${location}</loc>
13+
<lastmod>${lastMod}</lastmod>
14+
<changefreq>weekly</changefreq>
15+
<priority>${priority || '0.5'}</priority>
16+
</url>`
17+
}
18+
19+
function wrapSiteMap(content) {
20+
return `<?xml version="1.0" encoding="UTF-8"?>
21+
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
22+
${content}
23+
</urlset>
24+
`
25+
}
26+
27+
const ignore = [
28+
// sitemaps
29+
/^sitemap\.xml.*/,
30+
// _nextjs templates
31+
// .hidden_files
32+
// [nextjs_dynamic_pages]
33+
// 404 and 500 error pages
34+
/^([_.[]|404|500).*$/,
35+
]
36+
37+
const pageFilter = (file) => {
38+
for (const ig of ignore) {
39+
if (file.name.match(ig)) {
40+
return false
41+
}
42+
}
43+
if (file?.isDirectory()) {
44+
return true
45+
}
46+
return file.name.match(/\.(ts|tsx|js|jsx|md|mdoc)$/)
47+
}
48+
49+
const rootDir = __dirname
50+
51+
const PAGES_PATH = '/pages'
52+
53+
async function crawlPages(filePath = '/') {
54+
const fullPath = path.join(rootDir, PAGES_PATH, filePath)
55+
const files = await readdir(fullPath, { withFileTypes: true })
56+
57+
const filteredFiles = files.filter(pageFilter)
58+
59+
const pages = []
60+
for (const file of filteredFiles) {
61+
if (file.isDirectory()) {
62+
pages.push(...(await crawlPages(path.join(filePath, file.name))))
63+
} else {
64+
let pathname = file.name.split('.').slice(0, -1).join('.')
65+
pathname = path.join(filePath, pathname.replace(/(^|\/)index$/g, ''))
66+
pages.push({ path: pathname })
67+
}
68+
}
69+
70+
return pages
71+
}
72+
73+
const pagesObj = await crawlPages()
74+
const pages = JSON.stringify(pagesObj, null, ' ')
75+
76+
writeFile(path.join(rootDir, 'src/generated/pages.json'), pages)
77+
78+
export default {}

package.json

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,10 @@
44
"node": "18.16.0"
55
},
66
"scripts": {
7-
"dev": "concurrently \"next dev\" \"graphql-codegen --watch --config codegen.ts \" ",
8-
"build": "next build",
7+
"dev": "run-s generate:pageindex dev:watch",
8+
"dev:watch": "concurrently \"next dev\" \"yarn graphql:codegen:watch\"",
9+
"build": "run-s generate:pageindex build:next",
10+
"build:next": "next build",
911
"start": "next start",
1012
"lint": "run-p lint:format lint:js lint:css",
1113
"lint:format": "prettier --check --no-error-on-unmatched-pattern ./{src,pages}/**/*.{js,jsx,ts,tsx,graphql,md,mdpart}",
@@ -15,7 +17,9 @@
1517
"fix:format": "prettier --write --no-error-on-unmatched-pattern ./{src,pages}/**/*.{js,jsx,ts,tsx,graphql,md,mdpart}",
1618
"fix:js": "eslint --fix {src,pages} --ext ts,tsx,js,jsx",
1719
"fix:css": "stylelint src/**/*.css --fix",
18-
"graphql:codegen": "graphql-codegen --config codegen.yml"
20+
"graphql:codegen": "graphql-codegen --config codegen.yml",
21+
"graphql:codegen:watch": "graphql-codegen --watch --config codegen.yml",
22+
"generate:pageindex": "node index-pages.mjs"
1923
},
2024
"dependencies": {
2125
"@apollo/client": "3.7.15",
@@ -48,10 +52,10 @@
4852
"js-yaml": "4.1.0",
4953
"lodash": "4.17.21",
5054
"memoize-one": "6.0.0",
51-
"next": "13.4.4",
55+
"next": "13.4.5",
5256
"next-compose-plugins": "2.2.1",
5357
"next-transpile-modules": "10.0.0",
54-
"posthog-js": "1.66.1",
58+
"posthog-js": "1.68.1",
5559
"raw-loader": "4.0.2",
5660
"react": "18.2.0",
5761
"react-dom": "18.2.0",
@@ -72,14 +76,14 @@
7276
"@graphql-codegen/client-preset": "4.0.0",
7377
"@pluralsh/eslint-config-typescript": "2.5.41",
7478
"@pluralsh/stylelint-config": "1.1.3",
75-
"@types/node": "20.2.5",
79+
"@types/node": "20.3.1",
7680
"@types/react": "18.2.12",
7781
"@types/react-dom": "18.2.5",
7882
"@types/styled-components": "5.1.26",
7983
"@typescript-eslint/eslint-plugin": "5.59.11",
80-
"concurrently": "8.1.0",
84+
"concurrently": "8.2.0",
8185
"eslint": "8.42.0",
82-
"eslint-config-next": "13.4.4",
86+
"eslint-config-next": "13.4.5",
8387
"eslint-config-prettier": "8.8.0",
8488
"eslint-import-resolver-typescript": "3.5.5",
8589
"eslint-plugin-import": "2.27.5",

pages/sitemap.xml.ts

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import { until } from '@open-draft/until'
2+
3+
import { getRepos } from '@src/data/getRepos'
4+
5+
import pages from '../src/generated/pages.json'
6+
7+
const S_MAXAGE = 1 * 60 * 60 // 1s * 60s/m * 60m/h = 1 hour
8+
const STALE_WHILE_REVALIDATE = S_MAXAGE * 2
9+
10+
function urlTag({
11+
location,
12+
lastMod,
13+
priority = '0.5',
14+
}: {
15+
location: string
16+
lastMod: string
17+
priority?: string | number
18+
}) {
19+
return ` <url>
20+
<loc>${process.env.NEXT_PUBLIC_ROOT_URL}${location}</loc>
21+
<lastmod>${lastMod}</lastmod>
22+
<changefreq>weekly</changefreq>
23+
<priority>${priority || '0.5'}</priority>
24+
</url>`
25+
}
26+
27+
function wrapSiteMap(content: string) {
28+
return `<?xml version="1.0" encoding="UTF-8"?>
29+
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
30+
${content}
31+
</urlset>
32+
`
33+
}
34+
35+
export default function SiteMap() {
36+
// getServerSideProps will do the heavy lifting
37+
}
38+
39+
async function generateSiteMap({
40+
repos,
41+
}: {
42+
repos: Awaited<ReturnType<typeof getRepos>>
43+
}) {
44+
const lastMod = new Date().toISOString()
45+
46+
// We generate the XML sitemap with the posts data
47+
const sitemap = wrapSiteMap(
48+
`${pages
49+
?.map((page) => urlTag({ location: `${page.path}`, lastMod }))
50+
.join('\n')}
51+
${repos
52+
?.map((repo) => urlTag({ location: `/applications/${repo.name}`, lastMod }))
53+
.join('\n')}`
54+
)
55+
56+
return sitemap
57+
}
58+
59+
let cachedSiteMap: string
60+
61+
export async function getServerSideProps({ res }) {
62+
// We make an API call to gather the URLs for our site
63+
const { data: repos, error: reposError } = await until(() => getRepos())
64+
65+
let sitemap: string = cachedSiteMap
66+
67+
if (!reposError) {
68+
sitemap = await generateSiteMap({ repos })
69+
}
70+
71+
res.setHeader('Content-Type', 'text/xml')
72+
res.setHeader(
73+
'Cache-Control',
74+
`public, s-maxage=${S_MAXAGE}, stale-while-revalidate=${STALE_WHILE_REVALIDATE}`
75+
)
76+
// we send the XML to the browser
77+
res.write(sitemap)
78+
res.end()
79+
80+
return {
81+
props: {},
82+
}
83+
}

0 commit comments

Comments
 (0)