Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

POC StorefrontAPI sitemaps #1922

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
24 changes: 24 additions & 0 deletions templates/hello-world/storefrontapi.generated.d.ts
@@ -0,0 +1,24 @@
/* eslint-disable eslint-comments/disable-enable-pair */
/* eslint-disable eslint-comments/no-unlimited-disable */
/* eslint-disable */
import * as StorefrontAPI from '@shopify/hydrogen/storefront-api-types';

export type LayoutQueryVariables = StorefrontAPI.Exact<{[key: string]: never}>;

export type LayoutQuery = {
shop: Pick<StorefrontAPI.Shop, 'name' | 'description'>;
};

interface GeneratedQueryTypes {
'#graphql\n query layout {\n shop {\n name\n description\n }\n }\n': {
return: LayoutQuery;
variables: LayoutQueryVariables;
};
}

interface GeneratedMutationTypes {}

declare module '@shopify/hydrogen' {
interface StorefrontQueries extends GeneratedQueryTypes {}
interface StorefrontMutations extends GeneratedMutationTypes {}
}
208 changes: 87 additions & 121 deletions templates/skeleton/app/routes/[sitemap.xml].tsx
@@ -1,31 +1,19 @@
import {flattenConnection} from '@shopify/hydrogen';
import type {LoaderFunctionArgs} from '@shopify/remix-oxygen';
import type {SitemapQuery} from 'storefrontapi.generated';
// import type {SitemapQuery} from 'storefrontapi.generated';

/**
* the google limit is 50K, however, the storefront API
* allows querying only 250 resources per pagination page
*/
const MAX_URLS = 250;

type Entry = {
url: string;
lastMod?: string;
changeFreq?: string;
image?: {
url: string;
title?: string;
caption?: string;
};
};

export async function loader({
request,
context: {storefront},
}: LoaderFunctionArgs) {
const data = await storefront.query(SITEMAP_QUERY, {
variables: {
urlLimits: MAX_URLS,
language: storefront.i18n.language,
},
});
Expand All @@ -45,6 +33,54 @@ export async function loader({
});
}

// export async function loader({
// request,
// context: {storefront},
// }: LoaderFunctionArgs) {
// // Hardcoded JSON data
// const data = {
// data: {
// sitemapIndex: {
// sitemaps: [
// {
// __typename: 'SitemapIndexEntry',
// lastmod: '2024-03-20 13:40:05 +0000',
// loc: 'sitemap_products_1.xml?from=1&amp;to=2',
// },
// {
// __typename: 'SitemapIndexEntry',
// lastmod: '2024-03-20 13:40:05 +0000',
// loc: 'sitemap_pages_1.xml',
// },
// {
// __typename: 'SitemapIndexEntry',
// lastmod: '2024-03-20 13:40:05 +0000',
// loc: 'sitemap_collections_1.xml',
// },
// {
// __typename: 'SitemapIndexEntry',
// lastmod: '2024-03-20 13:40:05 +0000',
// loc: 'sitemap_blogs_1.xml',
// },
// ],
// },
// },
// };

// if (!data) {
// throw new Response('No data found', {status: 404});
// }

// const sitemap = generateSitemap({data, baseUrl: new URL(request.url).origin});

// return new Response(sitemap, {
// headers: {
// 'Content-Type': 'application/xml',
// 'Cache-Control': `max-age=${60 * 60 * 24}`,
// },
// });
// }

function xmlEncode(string: string) {
return string.replace(/[&<>'"]/g, (char) => `&#${char.charCodeAt(0)};`);
}
Expand All @@ -53,124 +89,54 @@ function generateSitemap({
data,
baseUrl,
}: {
data: SitemapQuery;
data: Record<string, any>;
baseUrl: string;
}) {
const products = flattenConnection(data.products)
.filter((product) => product.onlineStoreUrl)
.map((product) => {
const url = `${baseUrl}/products/${xmlEncode(product.handle)}`;

const productEntry: Entry = {
url,
lastMod: product.updatedAt,
changeFreq: 'daily',
};

if (product.featuredImage?.url) {
productEntry.image = {
url: xmlEncode(product.featuredImage.url),
};

if (product.title) {
productEntry.image.title = xmlEncode(product.title);
}

if (product.featuredImage.altText) {
productEntry.image.caption = xmlEncode(product.featuredImage.altText);
}
}

return productEntry;
});

const collections = flattenConnection(data.collections)
.filter((collection) => collection.onlineStoreUrl)
.map((collection) => {
const url = `${baseUrl}/collections/${collection.handle}`;

return {
url,
lastMod: collection.updatedAt,
changeFreq: 'daily',
};
});

const pages = flattenConnection(data.pages)
.filter((page) => page.onlineStoreUrl)
.map((page) => {
const url = `${baseUrl}/pages/${page.handle}`;

return {
url,
lastMod: page.updatedAt,
changeFreq: 'weekly',
};
});
const urls = data.sitemapIndex.sitemaps.map((sitemap: any) => {
const url = `${baseUrl}/sitemap${sitemap.loc}`;
const lastMod = sitemap.lastmod;

const urls = [...products, ...collections, ...pages];

return `
<urlset
xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"
xmlns:image="http://www.google.com/schemas/sitemap-image/1.1"
>
${urls.map(renderUrlTag).join('')}
</urlset>`;
}
return {url, lastMod};
});

function renderUrlTag({url, lastMod, changeFreq, image}: Entry) {
const imageTag = image
? `<image:image>
<image:loc>${image.url}</image:loc>
<image:title>${image.title ?? ''}</image:title>
<image:caption>${image.caption ?? ''}</image:caption>
</image:image>`.trim()
: '';
// return `
// <sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
// ${urls
// .map(({url, lastMod}) => {
// return `
// <sitemap>
// <loc>${url}</loc>
// <lastmod>${lastMod}</lastmod>
// </sitemap>
// `;
// })
// .join('')}
// </sitemapindex>
// `;

return `
<url>
<loc>${url}</loc>
<lastmod>${lastMod}</lastmod>
<changefreq>${changeFreq}</changefreq>
${imageTag}
</url>
`.trim();
<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
${urls
.map(({url, lastMod}) => {
return `
<sitemap>
<loc>${url}</loc>
</sitemap>
`;
})
.join('')}
</sitemapindex>
`;
}

const SITEMAP_QUERY = `#graphql
query Sitemap($urlLimits: Int, $language: LanguageCode)
query Sitemap($language: LanguageCode)
@inContext(language: $language) {
products(
first: $urlLimits
query: "published_status:'online_store:visible'"
) {
nodes {
updatedAt
handle
onlineStoreUrl
title
featuredImage {
url
altText
}
}
}
collections(
first: $urlLimits
query: "published_status:'online_store:visible'"
) {
nodes {
updatedAt
handle
onlineStoreUrl
}
}
pages(first: $urlLimits, query: "published_status:'published'") {
nodes {
updatedAt
handle
onlineStoreUrl
sitemapIndex {
sitemaps {
__typename
loc
lastmod
}
}
}
Expand Down