From cb3b35d14a3f9fcc6948aba941fa3fc167e0cdc2 Mon Sep 17 00:00:00 2001 From: Fran Dios Date: Fri, 30 Sep 2022 19:03:34 +0900 Subject: [PATCH] Add sitemap.xml --- app/data/index.ts | 61 +++++++++++++++ app/routes/[sitemap.xml].tsx | 139 +++++++++++++++++++++++++++++++++++ 2 files changed, 200 insertions(+) create mode 100644 app/routes/[sitemap.xml].tsx diff --git a/app/data/index.ts b/app/data/index.ts index 2823fee57e..b09ecca414 100644 --- a/app/data/index.ts +++ b/app/data/index.ts @@ -13,6 +13,7 @@ import type { ProductConnection, ProductVariant, SelectedOptionInput, + PageConnection, Shop, } from "@shopify/hydrogen-ui-alpha/storefront-api-types"; import { @@ -791,3 +792,63 @@ export async function getTopProducts({ count = 4 }: { count?: number } = {}) { return data.products; } + +const SITEMAP_QUERY = `#graphql + query sitemaps($urlLimits: Int, $language: LanguageCode) + @inContext(language: $language) { + products( + first: $urlLimits + query: "published_status:'online_store:visible'" + ) { + edges { + node { + updatedAt + handle + onlineStoreUrl + title + featuredImage { + url + altText + } + } + } + } + collections( + first: $urlLimits + query: "published_status:'online_store:visible'" + ) { + edges { + node { + updatedAt + handle + onlineStoreUrl + } + } + } + pages(first: $urlLimits, query: "published_status:'published'") { + edges { + node { + updatedAt + handle + onlineStoreUrl + } + } + } + } +`; + +interface SitemapQueryData { + products: ProductConnection; + collections: CollectionConnection; + pages: PageConnection; +} + +export async function getSitemap(variables: { + language: string; + urlLimits: number; +}) { + return getStorefrontData({ + query: SITEMAP_QUERY, + variables, + }); +} diff --git a/app/routes/[sitemap.xml].tsx b/app/routes/[sitemap.xml].tsx new file mode 100644 index 0000000000..c2ac98a48e --- /dev/null +++ b/app/routes/[sitemap.xml].tsx @@ -0,0 +1,139 @@ +import type { LoaderArgs } from "@remix-run/cloudflare"; +import { flattenConnection } from "@shopify/hydrogen-ui-alpha"; +import { getSitemap } from "~/data"; + +const MAX_URLS = 250; // the google limit is 50K, however, SF API only allow querying for 250 resources each time + +export async function loader({ request }: LoaderArgs) { + const data = await getSitemap({ + language: "EN", + urlLimits: MAX_URLS, + }); + + return new Response( + shopSitemap({ data, baseUrl: new URL(request.url).origin }), + { + headers: { + "content-type": "application/xml", + // Cache for 24 hours + "cache-control": `max-age=${60 * 60 * 24}`, + }, + } + ); +} + +interface ProductEntry { + url: string; + lastMod: string; + changeFreq: string; + image?: { + url: string; + title?: string; + caption?: string; + }; +} + +function shopSitemap({ + data, + baseUrl, +}: { + data: Awaited>; + baseUrl: string; +}) { + const productsData = flattenConnection(data.products) + .filter((product) => product.onlineStoreUrl) + .map((product) => { + const url = `${baseUrl}/products/${product.handle}`; + + const finalObject: ProductEntry = { + url, + lastMod: product.updatedAt!, + changeFreq: "daily", + }; + + if (product.featuredImage?.url) { + finalObject.image = { + url: product.featuredImage!.url, + }; + + if (product.title) { + finalObject.image.title = product.title; + } + + if (product.featuredImage!.altText) { + finalObject.image.caption = product.featuredImage!.altText; + } + } + + return finalObject; + }); + + const collectionsData = flattenConnection(data.collections) + .filter((collection) => collection.onlineStoreUrl) + .map((collection) => { + const url = `${baseUrl}/collections/${collection.handle}`; + + return { + url, + lastMod: collection.updatedAt, + changeFreq: "daily", + }; + }); + + const pagesData = flattenConnection(data.pages) + .filter((page) => page.onlineStoreUrl) + .map((page) => { + const url = `${baseUrl}/pages/${page.handle}`; + + return { + url, + lastMod: page.updatedAt, + changeFreq: "weekly", + }; + }); + + const urlsDatas = [...productsData, ...collectionsData, ...pagesData]; + + return ` + + ${urlsDatas.map((url) => renderUrlTag(url!)).join("")} + `; +} + +function renderUrlTag({ + url, + lastMod, + changeFreq, + image, +}: { + url: string; + lastMod?: string; + changeFreq?: string; + image?: { + url: string; + title?: string; + caption?: string; + }; +}) { + return ` + + ${url} + ${lastMod} + ${changeFreq} + ${ + image + ? ` + + ${image.url} + ${image.title ?? ""} + ${image.caption ?? ""} + ` + : "" + } + + + `; +}