Skip to content

Commit

Permalink
feat: sitemaps (#576)
Browse files Browse the repository at this point in the history
  • Loading branch information
alexgoff authored Oct 15, 2024
1 parent 93c68e4 commit c16f3fb
Show file tree
Hide file tree
Showing 8 changed files with 293 additions and 2 deletions.
69 changes: 69 additions & 0 deletions app/[locale]/sitemap.xml/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { NextRequest, NextResponse } from "next/server";
import { XMLBuilder } from "fast-xml-parser";
import {
generateSitemapUrl,
getSitemapData,
getSiteMapNewsData,
} from "@/lib/api/sitemap";

export async function GET(
request: NextRequest,
{ params: { locale } }: LocaleProps
) {
const pages = await getSitemapData(locale);
const pageData = pages.map(({ uri, dateUpdated }) => {
return {
loc: generateSitemapUrl(uri, locale),
lastmod: dateUpdated,
};
});
const { siteTitle, news } = await getSiteMapNewsData(locale);

const today = new Date();
const recentNewsThreshold = new Date(
today.getTime() - 1000 * 60 * 60 * 24 * 2
);

const newsData = news.map(({ uri, dateUpdated, title, date }) => {
const entry = {
loc: generateSitemapUrl(uri, locale),
lastmod: dateUpdated,
};

if (new Date(date) > recentNewsThreshold) {
entry["news:news"] = {
"news:publication": {
"news:name": siteTitle,
"news:language": locale,
},
"news:publication_date": date,
"news:title": title,
};
}

return entry;
});

const data = {
"?xml": {
$version: "1.0",
$encoding: "UTF-8",
},
urlset: {
$xmlns: "http://www.sitemaps.org/schemas/sitemap/0.9",
"$xmlns:news": "http://www.google.com/schemas/sitemap-news/0.9",
url: pageData.concat(newsData),
},
};
const builder = new XMLBuilder({
attributeNamePrefix: "$",
arrayNodeName: "url",
ignoreAttributes: false,
});
const output = builder.build(data);

return new NextResponse(output, {
status: 200,
headers: { "Content-Type": "application/xml; charset=utf-8" },
});
}
30 changes: 30 additions & 0 deletions app/sitemap_index.xml/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { NextResponse } from "next/server";
import { XMLBuilder } from "fast-xml-parser";
import { generateSitemapUrl } from "@/lib/api/sitemap";
import { languages } from "@/lib/i18n/settings";

export async function GET() {
const data = {
"?xml": {
$version: "1.0",
$encoding: "UTF-8",
},
sitemapindex: {
$xmlns: "http://www.sitemaps.org/schemas/sitemap/0.9",
sitemap: languages.map((language) => {
return { loc: generateSitemapUrl("sitemap.xml", language, true) };
}),
},
};
const builder = new XMLBuilder({
attributeNamePrefix: "$",
arrayNodeName: "sitemap",
ignoreAttributes: false,
});
const output = builder.build(data);

return new NextResponse(output, {
status: 200,
headers: { "Content-Type": "application/xml; charset=utf-8" },
});
}
55 changes: 55 additions & 0 deletions cypress/e2e/api/sitemaps.cy.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { XMLValidator, XMLParser } from "fast-xml-parser";
import { languages } from "../../../lib/i18n/settings";

const url = "/sitemap_index.xml";

context("GET /server_index.xml", () => {
it("returns a well formed XML file", () => {
cy.request({
url,
method: "GET",
}).then(({ body, headers }) => {
expect(headers["content-type"]).to.eq("application/xml; charset=utf-8");

const valid = XMLValidator.validate(body);

expect(valid).to.eq(true);
});
});
it("contains a sitemap for each locale", () => {
cy.request({
url,
method: "GET",
}).then(({ body }) => {
const parser = new XMLParser();
const {
sitemapindex: { sitemap },
} = parser.parse(body);

expect(sitemap.length).to.eq(languages.length);
});
});
it("contains valid sitemap links", () => {
cy.request({
url,
method: "GET",
}).then(({ body }) => {
const parser = new XMLParser();
const {
sitemapindex: { sitemap },
} = parser.parse(body);

sitemap.forEach(({ loc }) => {
cy.request({ url: loc, method: "GET" }).then(({ body, headers }) => {
expect(headers["content-type"]).to.eq(
"application/xml; charset=utf-8"
);

const valid = XMLValidator.validate(body);

expect(valid).to.eq(true);
});
});
});
});
});
3 changes: 1 addition & 2 deletions lib/api/homepage/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,7 @@ import {
} from "@/lib/api/fragments/content-blocks";
import { getLinkFields, linkFragment } from "@/lib/api/fragments/link";
import queryAPI from "@/lib/api/client/query";

const CRAFT_HOMEPAGE_URI = "__home__";
import { CRAFT_HOMEPAGE_URI } from "@/lib/constants";

export const getHomepage = async (locale: string, previewToken?: string) => {
const site = getSiteFromLocale(locale);
Expand Down
124 changes: 124 additions & 0 deletions lib/api/sitemap/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import { gql } from "@urql/core";
import { getSiteFromLocale } from "@/lib/helpers/site";
import queryAPI from "@/lib/api/client/query";
import tags from "@/lib/api/client/tags";
import { CRAFT_HOMEPAGE_URI } from "@/lib/constants";
import { fallbackLng } from "@/lib/i18n/settings";

interface PageMetadata {
uri: string;
dateUpdated: string;
}
const baseUrl = process.env.NEXT_PUBLIC_BASE_URL || "";

export const generateSitemapUrl = (
uri: string,
locale: string,
preserveLocale = false
) => {
const segments = uri === CRAFT_HOMEPAGE_URI ? [] : [uri];

if (preserveLocale || locale !== fallbackLng) {
segments.unshift(locale);
}

segments.unshift(baseUrl);

return segments.join("/");
};

export const getSiteMapNewsData = async (locale: string) => {
const site = getSiteFromLocale(locale);

const query = gql`
query NewsSitemapData($site: [String]) {
newsEntries(site: $site) {
... on news_post_Entry {
title
uri
date
dateUpdated
}
}
globalSets(site: $site) {
... on siteInfo_GlobalSet {
siteTitle
}
}
}
`;

const { data } = await queryAPI({
query,
variables: { site },
fetchOptions: {
next: { tags: [tags.globals] },
},
});

const { newsEntries, globalSets } = data;
const { siteTitle } = globalSets.filter((set) =>
Object.hasOwn(set, "siteTitle")
)[0];

return { siteTitle, news: newsEntries };
};

export const getSitemapData = async (
locale: string
): Promise<Array<PageMetadata>> => {
const site = getSiteFromLocale(locale);

const query = gql`
query SitemapData($site: [String]) {
pages: entries(
site: $site
section: ["pages"]
type: ["not", "redirectPage"]
) {
uri
dateUpdated
}
homepage: homepageEntries(site: $site) {
... on homepage_homepage_Entry {
uri
dateUpdated
}
}
staff: staffProfilesEntries(site: $site) {
... on staffProfiles_staffProfiles_Entry {
dateUpdated
uri
}
}
slideshows: slideshowsEntries(site: $site) {
... on slideshows_slideshow_Entry {
dateUpdated
uri
}
}
glossary: glossaryTermsEntries(site: $site) {
... on glossaryTerms_glossaryTerm_Entry {
dateUpdated
uri
}
}
voice: slideshowsEntries(site: $site) {
... on slideshows_slideshow_Entry {
dateUpdated
uri
}
}
}
`;

const { data } = await queryAPI({
query,
variables: { site },
fetchOptions: {
next: { tags: [tags.globals] },
},
});

return Object.values<any>(data).flat();
};
1 change: 1 addition & 0 deletions lib/constants/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const CRAFT_HOMEPAGE_URI = "__home__";
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@
"add": "^2.0.6",
"classnames": "^2.3.1",
"convert": "^4.13.0",
"fast-xml-parser": "^4.5.0",
"feed": "^4.2.2",
"focus-trap": "^7.0.0",
"focus-visible": "^5.1.0",
Expand Down
12 changes: 12 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -6695,6 +6695,13 @@ fast-levenshtein@^2.0.6, fast-levenshtein@~2.0.6:
resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917"
integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==

fast-xml-parser@^4.5.0:
version "4.5.0"
resolved "https://registry.yarnpkg.com/fast-xml-parser/-/fast-xml-parser-4.5.0.tgz#2882b7d01a6825dfdf909638f2de0256351def37"
integrity sha512-/PlTQCI96+fZMAOLMZK4CWG1ItCbfZ/0jx7UIJFChPNrx7tcEgerUgWbeieCM9MfHInUDyK8DWYZ+YrywDJuTg==
dependencies:
strnum "^1.0.5"

fastest-levenshtein@^1.0.16:
version "1.0.16"
resolved "https://registry.yarnpkg.com/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz#210e61b6ff181de91ea9b3d1b84fdedd47e034e5"
Expand Down Expand Up @@ -11613,6 +11620,11 @@ striptags@^3.2.0:
resolved "https://registry.yarnpkg.com/striptags/-/striptags-3.2.0.tgz#cc74a137db2de8b0b9a370006334161f7dd67052"
integrity sha512-g45ZOGzHDMe2bdYMdIvdAfCQkCTDMGBazSw1ypMowwGIee7ZQ5dU0rBJ8Jqgl+jAKIv4dbeE1jscZq9wid1Tkw==

strnum@^1.0.5:
version "1.0.5"
resolved "https://registry.yarnpkg.com/strnum/-/strnum-1.0.5.tgz#5c4e829fe15ad4ff0d20c3db5ac97b73c9b072db"
integrity sha512-J8bbNyKKXl5qYcR36TIO8W3mVGVHrmmxsd5PAItGkmyzwJvybiw2IVq5nqd0i4LSNSkB/sx9VHllbfFdr9k1JA==

style-loader@^3.3.1:
version "3.3.2"
resolved "https://registry.yarnpkg.com/style-loader/-/style-loader-3.3.2.tgz#eaebca714d9e462c19aa1e3599057bc363924899"
Expand Down

0 comments on commit c16f3fb

Please sign in to comment.