From ef9120bca07991d5d3d3af055d48c6737ff83a1d Mon Sep 17 00:00:00 2001 From: "Juan P. Prieto" Date: Thu, 31 Oct 2024 10:48:23 -0700 Subject: [PATCH] Add hydrogen sitemap routes --- app/lib/sitemap.ts | 333 ----------------------- app/routes/[sitemap-empty.xml].tsx | 18 -- app/routes/[sitemap.xml].tsx | 10 +- app/routes/sitemap.$type.$page[.xml].tsx | 7 +- 4 files changed, 5 insertions(+), 363 deletions(-) delete mode 100644 app/lib/sitemap.ts delete mode 100644 app/routes/[sitemap-empty.xml].tsx diff --git a/app/lib/sitemap.ts b/app/lib/sitemap.ts deleted file mode 100644 index 6272e5f..0000000 --- a/app/lib/sitemap.ts +++ /dev/null @@ -1,333 +0,0 @@ -import type {LoaderFunctionArgs} from '@shopify/remix-oxygen'; -import type { - CountryCode, - LanguageCode, -} from '@shopify/hydrogen/storefront-api-types'; - -const SITEMAP_INDEX_PREFIX = ` -`; -const SITEMAP_INDEX_SUFFIX = ``; - -const SITEMAP_PREFIX = ` -`; -const SITEMAP_SUFFIX = ``; - -type Locale = `${LanguageCode}-${CountryCode}`; - -type SITEMAP_INDEX_TYPE = - | 'pages' - | 'products' - | 'collections' - | 'blogs' - | 'articles' - | 'metaObjects'; - -/** - * Generate a sitemap index that links to separate sitemaps for each resource type. - */ -export async function getSitemapIndex({ - storefront, - request, - types = ['products', 'pages', 'collections', 'metaObjects', 'articles'], - customUrls = [], -}: { - storefront: LoaderFunctionArgs['context']['storefront']; - request: Request; - types?: SITEMAP_INDEX_TYPE[]; - customUrls?: string[]; -}) { - const data = await storefront.query(SITEMAP_INDEX_QUERY, { - storefrontApiVersion: 'unstable', - }); - - if (!data) { - throw new Response('No data found', {status: 404}); - } - - const baseUrl = new URL(request.url).origin; - - const body = - SITEMAP_INDEX_PREFIX + - types - .map((type) => - getSiteMapLinks(type, data[type].pagesCount.count, baseUrl), - ) - .join('\n') + - customUrls - .map((url) => '' + url + '') - .join('\n') + - SITEMAP_INDEX_SUFFIX; - - return new Response(body, { - headers: { - 'Content-Type': 'application/xml', - 'Cache-Control': `max-age=${60 * 60 * 24}`, - }, - }); -} - -interface GetSiteMapOptions { - /** The params object from Remix */ - params: LoaderFunctionArgs['params']; - /** The Storefront API Client from Hydrogen */ - storefront: LoaderFunctionArgs['context']['storefront']; - /** A Remix Request object */ - request: Request; - /** A function that produces a canonical url for a resource. It is called multiple times for each locale supported by the app. */ - getLink: (options: { - type: string | SITEMAP_INDEX_TYPE; - baseUrl: string; - handle?: string; - locale?: string; - }) => string; - /** An array of locales to generate alternate tags */ - locales: string[]; - /** Optionally customize the changefreq property for each URL */ - getChangeFreq?: (options: { - type: string | SITEMAP_INDEX_TYPE; - handle: string; - }) => string; -} - -/** - * Generate a sitemap for a specific resource type. - */ -export async function getSitemap(options: GetSiteMapOptions) { - const {storefront, request, params, getLink, locales = []} = options; - - if (!params.type || !params.page) - throw new Response('No data found', {status: 404}); - - const type = params.type as keyof typeof QUERIES; - - const query = QUERIES[type]; - - if (!query) throw new Response('Not found', {status: 404}); - - const data = await storefront.query(query, { - variables: { - page: parseInt(params.page, 10), - }, - storefrontApiVersion: 'unstable', - }); - - if (!data?.sitemap?.resources?.items?.length) { - throw new Response('Not found', {status: 404}); - } - - const baseUrl = new URL(request.url).origin; - - const body = - SITEMAP_PREFIX + - data.sitemap.resources.items - .map((item: {handle: string; updatedAt: string; type?: string}) => { - return renderUrlTag({ - getChangeFreq: options.getChangeFreq, - url: getLink({ - type: item.type ?? type, - baseUrl, - handle: item.handle, - }), - type, - getLink, - updatedAt: item.updatedAt, - handle: item.handle, - metaobjectType: item.type, - locales, - baseUrl, - }); - }) - .join('\n') + - SITEMAP_SUFFIX; - - return new Response(body, { - headers: { - 'Content-Type': 'application/xml', - 'Cache-Control': `max-age=${60 * 60 * 24}`, - }, - }); -} - -function getSiteMapLinks(resource: string, count: number, baseUrl: string) { - let links = ``; - - for (let i = 1; i <= count; i++) { - links += `${baseUrl}/sitemap/${resource}/${i}.xml`; - } - return links; -} - -function renderUrlTag({ - url, - updatedAt, - locales, - type, - getLink, - baseUrl, - handle, - getChangeFreq, - metaobjectType, -}: { - type: SITEMAP_INDEX_TYPE; - baseUrl: string; - handle: string; - metaobjectType?: string; - getLink: (options: { - type: string; - baseUrl: string; - handle?: string; - locale?: string; - }) => string; - url: string; - updatedAt: string; - locales: string[]; - getChangeFreq?: (options: {type: string; handle: string}) => string; -}) { - return ` - ${url} - ${updatedAt} - ${ - getChangeFreq - ? getChangeFreq({type: metaobjectType ?? type, handle}) - : 'weekly' - } -${locales - .map((locale) => - renderAlternateTag( - getLink({type: metaobjectType ?? type, baseUrl, handle, locale}), - locale, - ), - ) - .join('\n')} - - `.trim(); -} - -function renderAlternateTag(url: string, locale: string) { - return ` `; -} - -const PRODUCT_SITEMAP_QUERY = `#graphql - query SitemapProducts($page: Int!) { - sitemap(type: PRODUCT) { - resources(page: $page) { - items { - handle - updatedAt - } - } - } - } -` as const; - -const COLLECTION_SITEMAP_QUERY = `#graphql - query SitemapCollections($page: Int!) { - sitemap(type: COLLECTION) { - resources(page: $page) { - items { - handle - updatedAt - } - } - } - } -` as const; - -const ARTICLE_SITEMAP_QUERY = `#graphql - query SitemapArticles($page: Int!) { - sitemap(type: ARTICLE) { - resources(page: $page) { - items { - handle - updatedAt - } - } - } - } -` as const; - -const PAGE_SITEMAP_QUERY = `#graphql - query SitemapPages($page: Int!) { - sitemap(type: PAGE) { - resources(page: $page) { - items { - handle - updatedAt - } - } - } - } -` as const; - -const BLOG_SITEMAP_QUERY = `#graphql - query SitemapBlogs($page: Int!) { - sitemap(type: BLOG) { - resources(page: $page) { - items { - handle - updatedAt - } - } - } - } -` as const; - -const METAOBJECT_SITEMAP_QUERY = `#graphql - query SitemapMetaobjects($page: Int!) { - sitemap(type: METAOBJECT_PAGE) { - resources(page: $page) { - items { - handle - updatedAt - ... on SitemapResourceMetaobject { - type - } - } - } - } - } -` as const; - -const SITEMAP_INDEX_QUERY = `#graphql -query SitemapIndex { - products: sitemap(type: PRODUCT) { - pagesCount { - count - } - } - collections: sitemap(type: COLLECTION) { - pagesCount { - count - } - } - articles: sitemap(type: ARTICLE) { - pagesCount { - count - } - } - pages: sitemap(type: PAGE) { - pagesCount { - count - } - } - blogs: sitemap(type: BLOG) { - pagesCount { - count - } - } - metaObjects: sitemap(type: METAOBJECT_PAGE) { - pagesCount { - count - } - } -} -` as const; - -const QUERIES = { - products: PRODUCT_SITEMAP_QUERY, - articles: ARTICLE_SITEMAP_QUERY, - collections: COLLECTION_SITEMAP_QUERY, - pages: PAGE_SITEMAP_QUERY, - blogs: BLOG_SITEMAP_QUERY, - metaObjects: METAOBJECT_SITEMAP_QUERY, -}; diff --git a/app/routes/[sitemap-empty.xml].tsx b/app/routes/[sitemap-empty.xml].tsx deleted file mode 100644 index 4aab6f2..0000000 --- a/app/routes/[sitemap-empty.xml].tsx +++ /dev/null @@ -1,18 +0,0 @@ -import type {LoaderFunctionArgs} from '@remix-run/server-runtime'; - -export async function loader({request}: LoaderFunctionArgs) { - const url = new URL(request.url); - const baseUrl = url.origin; - - return new Response( - ` - ${baseUrl}/ -`, - { - headers: { - 'Content-Type': 'application/xml', - 'Cache-Control': `max-age=${60 * 60 * 24}`, - }, - }, - ); -} diff --git a/app/routes/[sitemap.xml].tsx b/app/routes/[sitemap.xml].tsx index e5d091a..24e517b 100644 --- a/app/routes/[sitemap.xml].tsx +++ b/app/routes/[sitemap.xml].tsx @@ -1,23 +1,17 @@ import type {LoaderFunctionArgs} from '@shopify/remix-oxygen'; -import {getSitemapIndex} from 'app/lib/sitemap'; +import {getSitemapIndex} from '@shopify/hydrogen'; export async function loader({ request, context: {storefront}, }: LoaderFunctionArgs) { - const url = new URL(request.url); - const baseUrl = url.origin; - const response = await getSitemapIndex({ storefront, request, - types: ['products', 'pages', 'collections', 'articles'], - customUrls: [`${baseUrl}/sitemap-empty.xml`], }); - response.headers.set('Oxygen-Cache-Control', `max-age=${60 * 60 * 24}`); - response.headers.set('Vary', 'Accept-Encoding, Accept-Language'); + response.headers.set('Cache-Control', `max-age=${60 * 60 * 24}`); return response; } diff --git a/app/routes/sitemap.$type.$page[.xml].tsx b/app/routes/sitemap.$type.$page[.xml].tsx index d74424e..4140326 100644 --- a/app/routes/sitemap.$type.$page[.xml].tsx +++ b/app/routes/sitemap.$type.$page[.xml].tsx @@ -1,6 +1,6 @@ import type {LoaderFunctionArgs} from '@shopify/remix-oxygen'; -import {getSitemap} from 'app/lib/sitemap'; +import {getSitemap} from '@shopify/hydrogen'; import {countries} from '~/data/countries'; const locales = Object.keys(countries).filter((k) => k !== 'default'); @@ -18,12 +18,11 @@ export async function loader({ locales, getLink: ({type, baseUrl, handle, locale}) => { if (!locale) return `${baseUrl}/${type}/${handle}`; - return `${baseUrl}${locale}/${type}/${handle}`; + return `${baseUrl}/${locale}/${type}/${handle}`; }, }); - response.headers.set('Oxygen-Cache-Control', `max-age=${60 * 60 * 24}`); - response.headers.set('Vary', 'Accept-Encoding, Accept-Language'); + response.headers.set('Cache-Control', `max-age=${60 * 60 * 24}`); return response; }