@forge42/seo-tools
Version:
Framework agnostic set of helpers designed to help you create, maintain and develop your SEO
1 lines • 14.5 kB
Source Map (JSON)
{"version":3,"sources":["../src/sitemap.ts"],"sourcesContent":["import UrlPattern from \"url-pattern\"\n\nexport interface LanguageAlternate {\n\threflang: string\n\thref: string\n}\nexport interface SitemapVideoEntry {\n\turl: string\n\tthumbnailUrl: string\n\ttitle: string\n\tdescription: string\n\tcontentLocation: string\n\tplayerLocation: string\n\tduration?: string\n\texpirationDate?: string\n\trating?: string\n\tviewCount?: string\n\tpublicationDate?: string\n\tfamilyFriendly?: \"yes\" | \"no\"\n\trestriction?: { relationship: \"allow\" | \"disallow\"; value: string }\n\tplatform?: { relationship: \"allow\" | \"disallow\"; value: (\"web\" | \"mobile\" | \"tv\")[] }\n\trequiresSubscription?: \"yes\" | \"no\"\n\tuploader?: { url?: string; name: string }\n\tlive?: \"yes\" | \"no\"\n\ttags?: string[]\n}\n\nexport interface SitemapIndexEntry {\n\turl: string\n\tlastmod: string\n}\n\nexport interface SitemapNewsEntry {\n\tpublication: { name: string; language: string }\n\tpublicationDate: string\n\ttitle: string\n}\n\nexport type SitemapImageEntry = string\n\nexport interface SitemapEntry {\n\troute: string\n\tchangefreq?: \"always\" | \"hourly\" | \"daily\" | \"weekly\" | \"monthly\" | \"yearly\" | \"never\"\n\tpriority?: 0.0 | 0.1 | 0.2 | 0.3 | 0.4 | 0.5 | 0.6 | 0.7 | 0.8 | 0.9 | 1.0\n\tlastmod?: string\n\timages?: string[]\n\tnews?: SitemapNewsEntry[]\n\tvideos?: SitemapVideoEntry[]\n\talternateLinks?: LanguageAlternate[]\n}\n\nexport interface SitemapRoute {\n\turl: string\n\tlastmod?: string\n\tchangefreq?: \"always\" | \"hourly\" | \"daily\" | \"weekly\" | \"monthly\" | \"yearly\" | \"never\"\n\tpriority?: 0.0 | 0.1 | 0.2 | 0.3 | 0.4 | 0.5 | 0.6 | 0.7 | 0.8 | 0.9 | 1.0\n\tsitemapEntries?: SitemapEntry | SitemapEntry[]\n}\n\nconst removeWhitespace = (str: string) =>\n\tstr\n\t\t.replaceAll(/>\\s+</g, \"><\")\n\t\t.replaceAll(/\\s+([^<>\\s][^<>]*[^<>\\s])\\s+/g, \" $1 \")\n\t\t.replaceAll(/(\\s+)/g, \" \")\n\t\t.trim()\n\n/**\n * Helper method used to generate sitemap-index.xml file from an array of sitemaps\n * @param sitemaps Array of sitemaps with url to the sitemaps and lastmod strings\n * @returns Generated sitemap-index xml file as a string\n */\nexport const generateSitemapIndex = (sitemaps: SitemapIndexEntry[]) => {\n\treturn removeWhitespace(`<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n <sitemapindex xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\">\n ${sitemaps\n\t\t\t.map(\n\t\t\t\t(sitemap) => `\n <sitemap>\n <loc>${sitemap.url}</loc>\n <lastmod>${sitemap.lastmod}</lastmod>\n </sitemap>\n `\n\t\t\t)\n\t\t\t.join(\"\\n\")}\n </sitemapindex>`)\n}\n\nexport const generateVideoSitemapData = (videos: SitemapEntry[\"videos\"]) => {\n\tif (!videos) return \"\"\n\treturn videos\n\t\t?.map((video) => {\n\t\t\treturn `\n <video:video>\n\t\t\t\t<video:title>${video.title}</video:title>\n\t\t\t\t<video:description>${video.description}</video:description>\n\t\t\t\t<video:thumbnail_loc>${video.thumbnailUrl}</video:thumbnail_loc>\n\n <video:content_loc>${video.contentLocation}</video:content_loc>\n <video:player_loc>${video.playerLocation}</video:player_loc>\n ${video.duration ? `<video:duration>${video.duration}</video:duration>` : \"\"}\n ${video.expirationDate ? `<video:expiration_date>${video.expirationDate}</video:expiration_date>` : \"\"}\n ${video.rating ? `<video:rating>${video.rating}</video:rating>` : \"\"}\n ${video.viewCount ? `<video:view_count>${video.viewCount}</video:view_count>` : \"\"}\n ${video.publicationDate ? `<video:publication_date>${video.publicationDate}</video:publication_date>` : \"\"}\n ${video.familyFriendly ? `<video:family_friendly>${video.familyFriendly}</video:family_friendly>` : \"\"}\n ${\n\t\t\t\t\tvideo.restriction\n\t\t\t\t\t\t? `<video:restriction relationship=\"${video.restriction.relationship}\">${video.restriction.value}</video:restriction>`\n\t\t\t\t\t\t: \"\"\n\t\t\t\t}\n ${\n\t\t\t\t\tvideo.platform\n\t\t\t\t\t\t? `<video:platform relationship=\"${video.platform.relationship}\">${video.platform.value.join(\n\t\t\t\t\t\t\t\t\" \"\n\t\t\t\t\t\t\t)}</video:platform>`\n\t\t\t\t\t\t: \"\"\n\t\t\t\t}\n ${\n\t\t\t\t\tvideo.requiresSubscription\n\t\t\t\t\t\t? `<video:requires_subscription>${video.requiresSubscription}</video:requires_subscription>`\n\t\t\t\t\t\t: \"\"\n\t\t\t\t}\n ${\n\t\t\t\t\tvideo.uploader\n\t\t\t\t\t\t? `<video:uploader${video.uploader.url ? ` info=\"${video.uploader.url}\"` : \"\"}>${\n\t\t\t\t\t\t\t\tvideo.uploader.name\n\t\t\t\t\t\t\t}</video:uploader>`\n\t\t\t\t\t\t: \"\"\n\t\t\t\t}\n ${video.live ? `<video:live>${video.live}</video:live>` : \"\"}\n ${video.tags ? video.tags.map((tag) => `<video:tag>${tag}</video:tag>`).join(\"\\n\") : \"\"}\n </video:video>\n `\n\t\t})\n\t\t.join(\"\\n\")\n}\n\nexport const generateNewsSitemapData = (news: SitemapEntry[\"news\"]) => {\n\tif (!news) return \"\"\n\treturn news\n\t\t?.map((news) => {\n\t\t\treturn `\n <news:news>\n <news:publication>\n <news:name>${news.publication.name}</news:name>\n <news:language>${news.publication.language}</news:language>\n </news:publication>\n <news:publication_date>${news.publicationDate}</news:publication_date>\n <news:title>${news.title}</news:title>\n </news:news>\n `\n\t\t})\n\t\t.join(\"\\n\")\n}\n\nexport const generateImageSitemapData = (images: SitemapEntry[\"images\"]) => {\n\tif (!images) return \"\"\n\treturn images\n\t\t?.map((image) => {\n\t\t\treturn `\n <image:image>\n <image:loc>${image}</image:loc>\n </image:image>\n `\n\t\t})\n\t\t.join(\"\\n\")\n}\n\nexport const generateAlternateLinks = (links: SitemapEntry[\"alternateLinks\"]) => {\n\tif (!links) return \"\"\n\treturn links\n\t\t.map((link) => {\n\t\t\treturn `<xhtml:link rel=\"alternate\" hreflang=\"${link.hreflang}\" href=\"${link.href}\" />`\n\t\t})\n\t\t.join(\"\\n\")\n}\n\nexport const generateSitemapEntry = (entry: SitemapEntry) => {\n\tconst alternateLinks = generateAlternateLinks(entry.alternateLinks)\n\tconst imageLinks = generateImageSitemapData(entry.images)\n\tconst newsLinks = generateNewsSitemapData(entry.news)\n\tconst videoLinks = generateVideoSitemapData(entry.videos)\n\tif (!entry.route) {\n\t\treturn \"\"\n\t}\n\treturn `\n <url>\n <loc>${entry.route}</loc>\n ${entry.lastmod ? `<lastmod>${entry.lastmod}</lastmod>` : \"\"}\n ${entry.changefreq ? `<changefreq>${entry.changefreq}</changefreq>` : \"\"}\n ${entry.priority ? `<priority>${entry.priority}</priority>` : \"\"}\n ${alternateLinks}\n ${imageLinks}\n ${newsLinks}\n ${videoLinks}\n </url>\n `\n}\n\nconst generateSitemapString = (generatedEntries: string) => {\n\treturn removeWhitespace(`\n <?xml version=\"1.0\" encoding=\"UTF-8\"?>\n <urlset\n xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n xsi:schemaLocation=\"http://www.sitemaps.org/schemas/sitemap/0.9 http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd http://www.google.com/schemas/sitemap-news/0.9 http://www.google.com/schemas/sitemap-news/0.9/sitemap.xsd http://www.google.com/schemas/sitemap-video/1.1 http://www.google.com/schemas/sitemap-video/1.1/sitemap.xsd http://www.google.com/schemas/sitemap-image/1.1 http://www.google.com/schemas/sitemap-image/1.1/sitemap.xsd http://www.w3.org/TR/xhtml11/xhtml11_schema.html http://www.w3.org/2002/08/xhtml/xhtml1-strict.xsd\"\n xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\"\n xmlns:news=\"http://www.google.com/schemas/sitemap-news/0.9\"\n xmlns:video=\"http://www.google.com/schemas/sitemap-video/1.1\"\n xmlns:image=\"http://www.google.com/schemas/sitemap-image/1.1\"\n xmlns:xhtml=\"http://www.w3.org/TR/xhtml11/xhtml11_schema.html\"\n >\n ${generatedEntries}\n </urlset>`)\n}\n\nconst generateSitemapEntriesFromRoutes = ({\n\troutes,\n\tdomain,\n\tignore,\n\turlTransformer,\n}: {\n\tdomain: string\n\troutes: SitemapRoute[]\n\tignore: string[]\n\turlTransformer?: (url: string) => string\n}) => {\n\treturn routes\n\t\t.map((route) => {\n\t\t\t// If the route url doesn't start with / prepend it\n\t\t\tconst finalLocation = route.url.startsWith(\"/\") ? route.url : `/${route.url}`\n\n\t\t\t// If the route matches any ignored pattern ignore it completely\n\t\t\tif (ignore.some((pattern) => new UrlPattern(pattern).match(finalLocation) !== null)) {\n\t\t\t\treturn \"\"\n\t\t\t}\n\n\t\t\t// If the route has sitemap entries, generate them\n\t\t\tif (route.sitemapEntries) {\n\t\t\t\tconst sitemapEntries = Array.isArray(route.sitemapEntries) ? route.sitemapEntries : [route.sitemapEntries]\n\t\t\t\treturn sitemapEntries.map((entry) => generateSitemapEntry(entry)).join(\"\\n\")\n\t\t\t}\n\t\t\t// Allow the user to transform the url before it's added to the sitemap\n\t\t\tconst url = urlTransformer ? urlTransformer(finalLocation) : finalLocation\n\t\t\t// Append the url to the domain\n\t\t\tconst final = domain + url\n\t\t\t// Otherwise, just return the route as a single entry\n\t\t\treturn generateSitemapEntry({\n\t\t\t\troute: final,\n\t\t\t\tlastmod: route.lastmod,\n\t\t\t\tchangefreq: route.changefreq,\n\t\t\t\tpriority: route.priority,\n\t\t\t})\n\t\t})\n\t\t.join(\"\\n\")\n}\n\n/**\n * Helper method used to generate a sitemap from all the provided routes\n *\n * By default ignores all xml and txt files and any route that matches the pattern \"sitemap*\"\n *\n *\n * @param domain - The domain to append the urls to\n * @param routes - All the sitemap routes to generate\n * @param ignore - An array of patterns to ignore (e.g. [\"/status\"])\n * @param urlTransformer - A function to transform the url before adding it to the domain\n * @returns Sitemap returned as a string\n */\nexport const generateSitemap = async ({\n\tdomain,\n\tignore,\n\troutes,\n\turlTransformer,\n}: {\n\t/**\n\t * The domain to append the urls to (e.g. https://example.com)\n\t */\n\tdomain: string\n\t/**\n\t * All the sitemap routes to generate\n\t * @example [{url: \"/page\", lastmod: \"2021-01-01\", changefreq: \"daily\", priority: 0.5}]\n\t */\n\troutes: SitemapRoute[]\n\t/**\n\t * An array of patterns to ignore (e.g. [\"/status\"])\n\t */\n\tignore?: string[]\n\t/**\n\t * A function to transform the url before adding it\n\t * @example (url) => url.replace(\"/page\", \"/new-page\")\n\t * @default undefined\n\t * */\n\turlTransformer?: (url: string) => string\n}) => {\n\tconst defaultIgnore = [\"*.xml\", \"*.txt\", \"sitemap*\", ...(ignore ?? [])]\n\tconst sitemapEntries = generateSitemapEntriesFromRoutes({ domain, ignore: defaultIgnore, routes, urlTransformer })\n\tconst sitemap = generateSitemapString(sitemapEntries)\n\treturn sitemap\n}\n"],"mappings":"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","names":["UrlPattern","removeWhitespace","str","generateSitemapIndex","sitemaps","sitemap","generateVideoSitemapData","videos","video","tag","generateNewsSitemapData","news","generateImageSitemapData","images","image","generateAlternateLinks","links","link","generateSitemapEntry","entry","alternateLinks","imageLinks","newsLinks","videoLinks","generateSitemapString","generatedEntries","generateSitemapEntriesFromRoutes","routes","domain","ignore","urlTransformer","route","finalLocation","pattern","url","final","generateSitemap","defaultIgnore","sitemapEntries"]}