Shopify / hydrogen-demo-store

This template contains a full-featured setup of components, queries and tooling to get started with Hydrogen. It is deployed to https://hydrogen.shop
97 stars 31 forks source link

Miss blogs and blog > articles in sitemap.xml in demo #39

Open hydropik opened 1 year ago

hydropik commented 1 year ago

What is the location of your example repository?

No response

Which package or tool is having this issue?

Hydrogen

What version of that package or tool are you using?

1.0.4

What version of Remix are you using?

No response

Steps to Reproduce

Use demo-shop

Expected Behavior

I had a look in demo-shop and in [sitemap.xml] load products, collections, pages but not

Thank you

Actual Behavior

sitemap.xml include only collections, pages and products

hydropik commented 1 year ago

Home page is missing too

hydropik commented 1 year ago

I edited my sitemap.xml.tsx like this:

import {flattenConnection} from '@shopify/hydrogen';
import type {LoaderArgs} from '@shopify/remix-oxygen';
import {
  BlogConnection,
  CollectionConnection,
  PageConnection,
  ProductConnection,
  ArticleConnection,
} from '@shopify/hydrogen/storefront-api-types';
import invariant from 'tiny-invariant';

const MAX_URLS = 250; // the google limit is 50K, however, SF API only allow querying for 250 resources each time

interface SitemapQueryData {
  products: ProductConnection;
  collections: CollectionConnection;
  pages: PageConnection;
  blogs: BlogConnection;
  articles: ArticleConnection;
}

interface ProductEntry {
  url: string;
  lastMod: string;
  changeFreq: string;
  image?: {
    url: string;
    title?: string;
    caption?: string;
  };
}

interface ArticleEntry {
  url: string;
  changeFreq: string;
  image?: {
    url: string;
    title?: string;
    caption?: string;
  };
}

export async function loader({request, context: {storefront}}: LoaderArgs) {
  const data = await storefront.query<SitemapQueryData>(SITEMAP_QUERY, {
    variables: {
      urlLimits: MAX_URLS,
      language: storefront.i18n.language,
    },
  });

  invariant(data, 'Sitemap data is missing');

  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}`,
      },
    },
  );
}

function xmlEncode(string: string) {
  return string.replace(/[&<>'"]/g, (char) => `&#${char.charCodeAt(0)};`);
}

function shopSitemap({
  data,
  baseUrl,
}: {
  data: SitemapQueryData;
  baseUrl: string;
}) {
  const productsData = flattenConnection(data.products)
    .filter((product) => product.onlineStoreUrl)
    .map((product) => {
      const url = `${baseUrl}/products/${xmlEncode(product.handle)}`;

      const finalObject: ProductEntry = {
        url,
        lastMod: product.updatedAt,
        changeFreq: 'daily',
      };

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

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

        if (product.featuredImage.altText) {
          finalObject.image.caption = xmlEncode(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 blogsData = flattenConnection(data.blogs)
    .filter((blog) => blog.onlineStoreUrl)
    .map((blog) => {
      const url = `${baseUrl}/blogs/${blog.handle}`;

      return {
        url,
        changeFreq: 'weekly',
      };
    });

  const articlesData = flattenConnection(data.articles)
    .filter((article) => article.onlineStoreUrl)
    .map((article) => {
      const url = `${baseUrl}/blogs/${article.blog.handle}/${article.handle}`;

      const finalObject: ArticleEntry = {
        url,
        changeFreq: 'weekly',
      };

      if (article.image?.url) {
        finalObject.image = {
          url: xmlEncode(article.image.url),
        };

        if (article.title) {
          finalObject.image.title = xmlEncode(article.title);
        }

        if (article.image.altText) {
          finalObject.image.caption = xmlEncode(article.image.altText);
        }
      }
      return finalObject;
      ;
    });

  const urlsDatas = [...productsData, ...collectionsData, ...pagesData, ...blogsData, ...articlesData];

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

function renderUrlTag({
  url, 
  lastMod,
  changeFreq,
  image,
}: {
  url: string;
  lastMod?: string;
  changeFreq?: string;
  image?: {
    url: string;
    title?: string;
    caption?: string;
  };
}) {
  return `
    <url>
      <loc>${url}</loc>
      ${lastMod ? `<lastmod>${lastMod}</lastmod>`: ''}

      <changefreq>${changeFreq}</changefreq>
      ${
        image
          ? `
        <image:image>
          <image:loc>${image.url}</image:loc>
          <image:title>${image.title ?? ''}</image:title>
          <image:caption>${image.caption ?? ''}</image:caption>
        </image:image>`
          : ''
      }

    </url>
  `;
}

const SITEMAP_QUERY = `#graphql
query sitemaps($urlLimits: Int, $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
      }
    }
    blogs(first: $urlLimits, query: "published_status:'published'") {
      nodes {
        handle
        onlineStoreUrl
      }
    }
    articles(first: $urlLimits, query: "published_status:'published'") {
      nodes {
        handle
        onlineStoreUrl
        title
        blog {
          handle
        }
        image {
          url
          altText
        }
      }
    }
  }
`;
blittle commented 1 year ago

Hi! We will be updating the sitemap to be more robust. There are a lot of standard routes that aren't included at the moment.