nuxt-simple-sitemap
Version:
Powerfully flexible XML Sitemaps that integrate seamlessly, for Nuxt.
229 lines (211 loc) • 10 kB
JavaScript
import { defineEventHandler, getHeader, setHeader } from "h3";
import { getQuery, parseURL, withQuery } from "ufo";
import { useSimpleSitemapRuntimeConfig } from "../utils.mjs";
import { createSitePathResolver, useSiteConfig } from "#imports";
export default defineEventHandler(async (e) => {
setHeader(e, "Content-Type", "application/xslt+xml");
if (!process.dev)
setHeader(e, "Cache-Control", "max-age=600, must-revalidate");
const fixPath = createSitePathResolver(e, { absolute: false, withBase: true });
const { sitemapName: fallbackSitemapName, version, xslColumns, xslTips } = useSimpleSitemapRuntimeConfig();
const svgIcon = `<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="icon" style="margin-right: 4px; font-size: 25px;" width="1em" height="1em" viewBox="0 0 32 32"><path fill="#93c5fd" d="M4 26h4v4H4zm10 0h4v4h-4zm10 0h4v4h-4zm1-10h-8v-2h-2v2H7a2.002 2.002 0 0 0-2 2v6h2v-6h8v6h2v-6h8v6h2v-6a2.002 2.002 0 0 0-2-2zM9 2v10h14V2zm2 2h2v6h-2zm10 6h-6V4h6z"></path></svg>`;
const creditName = `<a href="https://github.com/harlan-zw/nuxt-simple-sitemap" style="color: black; display: flex; align-items: center; font-weight: 500;" target="_blank" rel="noopener">${svgIcon} Nuxt
Simple Sitemap v${version}</a>`;
const { name: siteName, url: siteUrl } = useSiteConfig(e);
const referrer = getHeader(e, "Referer") || "/";
const isNotIndexButHasIndex = referrer !== fixPath("/sitemap.xml") && parseURL(referrer).pathname.endsWith("-sitemap.xml");
const sitemapName = parseURL(referrer).pathname.split("/").pop()?.split("-sitemap")[0] || fallbackSitemapName;
const title = `${siteName}${sitemapName !== "sitemap.xml" ? ` - ${sitemapName === "sitemap_index.xml" ? "index" : sitemapName}` : ""}`.replace(/&/g, "&");
const canonicalQuery = getQuery(referrer).canonical;
const isShowingCanonical = typeof canonicalQuery !== "undefined" && canonicalQuery !== "false";
const conditionalTips = [
'You are looking at a <a href="https://developer.mozilla.org/en-US/docs/Web/XSLT/Transforming_XML_with_XSLT/An_Overview" style="color: #398465" target="_blank">XML stylesheet</a>. Read the <a href="https://nuxtseo.com/sitemap/guides/customising-ui" style="color: #398465" target="_blank">docs</a> to learn how to customize it. View the page source to see the raw XML.',
`URLs missing? Check Nuxt Devtools Sitemap tab (or the <a href="${withQuery("/__sitemap__/debug.json", { sitemap: sitemapName })}" style="color: #398465" target="_blank">debug endpoint</a>).`
];
if (!isShowingCanonical) {
const canonicalPreviewUrl = withQuery(referrer, { canonical: "" });
conditionalTips.push(`Your canonical site URL is <strong>${siteUrl}</strong>.`);
conditionalTips.push(`You can preview your canonical sitemap by visiting <a href="${canonicalPreviewUrl}" style="color: #398465; white-space: nowrap;">${fixPath(canonicalPreviewUrl)}?canonical</a>`);
} else {
conditionalTips.push(`You are viewing the canonical sitemap. You can switch to using the request origin: <a href="${fixPath(referrer)}" style="color: #398465; white-space: nowrap ">${fixPath(referrer)}</a>`);
}
const tips = conditionalTips.map((t) => `<li><p>${t}</p></li>`).join("\n");
const showTips = process.dev && xslTips !== false;
let columns = [...xslColumns];
if (!columns.length) {
columns = [
{ label: "URL", width: "50%" },
{ label: "Images", width: "25%", select: "count(image:image)" },
{ label: "Last Updated", width: "25%", select: "concat(substring(sitemap:lastmod,0,11),concat(' ', substring(sitemap:lastmod,12,5)),concat(' ', substring(sitemap:lastmod,20,6)))" }
];
}
return `
<xsl:stylesheet version="2.0"
xmlns:html="http://www.w3.org/TR/REC-html40"
xmlns:image="http://www.google.com/schemas/sitemap-image/1.1"
xmlns:sitemap="http://www.sitemaps.org/schemas/sitemap/0.9"
xmlns:xhtml="http://www.w3.org/1999/xhtml"
xmlns:news="http://www.google.com/schemas/sitemap-news/0.9"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="html" version="1.0" encoding="UTF-8" indent="yes"/>
<xsl:template match="/">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>XML Sitemap</title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
<style type="text/css">
body {
font-family: Inter, Helvetica, Arial, sans-serif;
font-size: 14px;
color: #333;
}
table {
border: none;
border-collapse: collapse;
}
.bg-yellow-200 {
background-color: #fef9c3;
}
.p-5 {
padding: 1.25rem;
}
.rounded {
border-radius: 4px;
}
.shadow {
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
}
#sitemap tr:nth-child(odd) td {
background-color: #f8f8f8 ;
}
#sitemap tbody tr:hover td {
background-color: #fff;
}
#sitemap tbody tr:hover td, #sitemap tbody tr:hover td a {
color: #000;
}
.expl a {
color: #398465
font-weight: 600;
}
.expl a:visited {
color: #398465
}
a {
color: #000;
text-decoration: none;
}
a:visited {
color: #777;
}
a:hover {
text-decoration: underline;
}
td {
font-size: 12px;
}
.text-2xl {
font-size: 2rem;
font-weight: 600;
line-height: 1.25;
}
th {
text-align: left;
padding-right: 30px;
font-size: 12px;
}
thead th {
border-bottom: 1px solid #000;
}
.fixed { position: fixed; }
.right-2 { right: 2rem; }
.top-2 { top: 2rem; }
.w-30 { width: 30rem; }
p { margin: 0; }
li { padding-bottom: 0.5rem; line-height: 1.5; }
h1 { margin: 0; }
.mb-5 { margin-bottom: 1.25rem; }
.mb-3 { margin-bottom: 0.75rem; }
</style>
</head>
<body>
<div style="grid-template-columns: 1fr 1fr; display: grid; margin: 3rem;">
<div>
<div id="content">
<h1 class="text-2xl mb-3">XML Sitemap</h1>
<h2>${title}</h2>
${isNotIndexButHasIndex ? `<p style="font-size: 12px; margin-bottom: 1rem;"><a href="${fixPath("/sitemap_index.xml")}">${fixPath("/sitemap_index.xml")}</a></p>` : ""}
<xsl:if test="count(sitemap:sitemapindex/sitemap:sitemap) > 0">
<p class="expl" style="margin-bottom: 1rem;">
This XML Sitemap Index file contains
<xsl:value-of select="count(sitemap:sitemapindex/sitemap:sitemap)"/> sitemaps.
</p>
<table id="sitemap" cellpadding="3">
<thead>
<tr>
<th width="75%">Sitemap</th>
<th width="25%">Last Modified</th>
</tr>
</thead>
<tbody>
<xsl:for-each select="sitemap:sitemapindex/sitemap:sitemap">
<xsl:variable name="sitemapURL">
<xsl:value-of select="sitemap:loc"/>
</xsl:variable>
<tr>
<td>
<a href="{$sitemapURL}">
<xsl:value-of select="sitemap:loc"/>
</a>
</td>
<td>
<xsl:value-of
select="concat(substring(sitemap:lastmod,0,11),concat(' ', substring(sitemap:lastmod,12,5)),concat(' ', substring(sitemap:lastmod,20,6)))"/>
</td>
</tr>
</xsl:for-each>
</tbody>
</table>
</xsl:if>
<xsl:if test="count(sitemap:sitemapindex/sitemap:sitemap) < 1">
<p class="expl" style="margin-bottom: 1rem;">
This XML Sitemap contains
<xsl:value-of select="count(sitemap:urlset/sitemap:url)"/> URLs.
</p>
<table id="sitemap" cellpadding="3">
<thead>
<tr>
${columns.map((c) => `<th width="${c.width}">${c.label}</th>`).join("\n")}
</tr>
</thead>
<tbody>
<xsl:variable name="lower" select="'abcdefghijklmnopqrstuvwxyz'"/>
<xsl:variable name="upper" select="'ABCDEFGHIJKLMNOPQRSTUVWXYZ'"/>
<xsl:for-each select="sitemap:urlset/sitemap:url">
<tr>
<td>
<xsl:variable name="itemURL">
<xsl:value-of select="sitemap:loc"/>
</xsl:variable>
<a href="{$itemURL}">
<xsl:value-of select="sitemap:loc"/>
</a>
</td>
${columns.filter((c) => c.label !== "URL").map((c) => `<td>
<xsl:value-of select="${c.select}"/>
</td>`).join("\n")}
</tr>
</xsl:for-each>
</tbody>
</table>
</xsl:if>
</div>
</div>
${showTips ? `<div class="w-30 top-2 shadow rounded p-5 right-2" style="margin: 0 auto;"><p><strong>Sitemap Tips (development only)</strong></p><ul style="margin: 1rem; padding: 0;">${tips}</ul><p style="margin-top: 1rem;">${creditName}</p></div>` : ""}
</div>
</body>
</html>
</xsl:template>
</xsl:stylesheet>
`;
});