UNPKG

next-sitemap

Version:
246 lines (245 loc) 9.74 kB
/** * Builder class to generate xml and robots.txt * Returns only string values */ export class SitemapBuilder { /** * Create XML Template * @param content * @returns */ withXMLTemplate(content) { 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>`; } /** * Generates sitemap-index.xml * @param allSitemaps * @returns */ buildSitemapIndexXml(allSitemaps) { return [ '<?xml version="1.0" encoding="UTF-8"?>', '<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">', ...(allSitemaps?.map((x) => `<sitemap><loc>${x}</loc></sitemap>`) ?? []), '</sitemapindex>', ].join('\n'); } /** * Normalize sitemap field keys to stay consistent with <xsd:sequence> order * @link https://www.w3schools.com/xml/el_sequence.asp * @link https://github.com/iamvishnusankar/next-sitemap/issues/345 * @param x * @returns */ normalizeSitemapField(x) { const { loc, lastmod, changefreq, priority, ...restProps } = x; // Return keys in following order return { loc, lastmod, changefreq, priority, ...restProps, }; } /** * Composes YYYY-MM-DDThh:mm:ssTZD date format (with TZ offset) * (ref: https://stackoverflow.com/a/49332027) * @param date * @private */ formatDate(date) { const d = typeof date === 'string' ? new Date(date) : date; const z = (n) => ('0' + n).slice(-2); const zz = (n) => ('00' + n).slice(-3); let off = d.getTimezoneOffset(); const sign = off > 0 ? '-' : '+'; off = Math.abs(off); return (d.getFullYear() + '-' + z(d.getMonth() + 1) + '-' + z(d.getDate()) + 'T' + z(d.getHours()) + ':' + z(d.getMinutes()) + ':' + z(d.getSeconds()) + '.' + zz(d.getMilliseconds()) + sign + z((off / 60) | 0) + ':' + z(off % 60)); } formatBoolean(value) { return value ? 'yes' : 'no'; } escapeHtml(s) { return s.replace(/[^\dA-Za-z ]/g, (c) => '&#' + c.charCodeAt(0) + ';'); } /** * Generates sitemap.xml * @param fields * @returns */ buildSitemapXml(fields) { const content = fields .map((x) => { // Normalize sitemap field keys to stay consistent with <xsd:sequence> order const field = this.normalizeSitemapField(x); // Field array to keep track of properties const fieldArr = []; // Iterate all object keys and key value pair to field-set for (const key of Object.keys(field)) { // Skip reserved keys if (['trailingSlash'].includes(key)) { continue; } if (field[key]) { if (key === 'alternateRefs') { const altRefField = this.buildAlternateRefsXml(field.alternateRefs); fieldArr.push(altRefField); } else if (key === 'news') { if (field.news) { const newsField = this.buildNewsXml(field.news); fieldArr.push(newsField); } } else if (key === 'images') { if (field.images) { for (const image of field.images) { const imageField = this.buildImageXml(image); fieldArr.push(imageField); } } } else if (key === 'videos') { if (field.videos) { for (const video of field.videos) { const videoField = this.buildVideoXml(video); fieldArr.push(videoField); } } } else { fieldArr.push(`<${key}>${field[key]}</${key}>`); } } } // Append previous value and return return `<url>${fieldArr.join('')}</url>\n`; }) .join(''); return this.withXMLTemplate(content); } /** * Generate alternate refs.xml * @param alternateRefs * @returns */ buildAlternateRefsXml(alternateRefs = []) { return alternateRefs .map((alternateRef) => { return `<xhtml:link rel="alternate" hreflang="${alternateRef.hreflang}" href="${alternateRef.href}"/>`; }) .join(''); } /** * Generate Google News sitemap entry * @param news * @returns string */ buildNewsXml(news) { // using array just because it looks more structured return [ `<news:news>`, ...[ `<news:publication>`, ...[ `<news:name>${this.escapeHtml(news.publicationName)}</news:name>`, `<news:language>${news.publicationLanguage}</news:language>`, ], `</news:publication>`, `<news:publication_date>${this.formatDate(news.date)}</news:publication_date>`, `<news:title>${this.escapeHtml(news.title)}</news:title>`, ], `</news:news>`, ] .filter(Boolean) .join(''); } /** * Generate Image sitemap entry * @param image * @returns string */ buildImageXml(image) { // using array just because it looks more structured return [ `<image:image>`, ...[ `<image:loc>${image.loc.href}</image:loc>`, image.caption && `<image:caption>${this.escapeHtml(image.caption)}</image:caption>`, image.title && `<image:title>${this.escapeHtml(image.title)}</image:title>`, image.geoLocation && `<image:geo_location>${this.escapeHtml(image.geoLocation)}</image:geo_location>`, image.license && `<image:license>${image.license.href}</image:license>`, ], `</image:image>`, ] .filter(Boolean) .join(''); } /** * Generate Video sitemap entry * @param video * @returns string */ buildVideoXml(video) { // using array just because it looks more structured return [ `<video:video>`, ...[ `<video:title>${this.escapeHtml(video.title)}</video:title>`, `<video:thumbnail_loc>${video.thumbnailLoc.href}</video:thumbnail_loc>`, `<video:description>${this.escapeHtml(video.description)}</video:description>`, video.contentLoc && `<video:content_loc>${video.contentLoc.href}</video:content_loc>`, video.playerLoc && `<video:player_loc>${video.playerLoc.href}</video:player_loc>`, video.duration && `<video:duration>${video.duration}</video:duration>`, video.viewCount && `<video:view_count>${video.viewCount}</video:view_count>`, video.tag && `<video:tag>${this.escapeHtml(video.tag)}</video:tag>`, video.rating && `<video:rating>${video.rating .toFixed(1) .replace(',', '.')}</video:rating>`, video.expirationDate && `<video:expiration_date>${this.formatDate(video.expirationDate)}</video:expiration_date>`, video.publicationDate && `<video:publication_date>${this.formatDate(video.publicationDate)}</video:publication_date>`, typeof video.familyFriendly !== 'undefined' && `<video:family_friendly>${this.formatBoolean(video.familyFriendly)}</video:family_friendly>`, typeof video.requiresSubscription !== 'undefined' && `<video:requires_subscription>${this.formatBoolean(video.requiresSubscription)}</video:requires_subscription>`, typeof video.live !== 'undefined' && `<video:live>${this.formatBoolean(video.live)}</video:live>`, video.restriction && `<video:restriction relationship="${video.restriction.relationship}">${video.restriction.content}</video:restriction>`, video.platform && `<video:platform relationship="${video.platform.relationship}">${video.platform.content}</video:platform>`, video.uploader && `<video:uploader${video.uploader.info && ` info="${video.uploader.info}"`}>${this.escapeHtml(video.uploader.name)}</video:uploader>`, ], `</video:video>`, ] .filter(Boolean) .join(''); } }