- Bumping the cli to 3.80.4 (#2956) by @balazsbajorics
-
Migrating to React Router 7 (#2866) by @balazsbajorics
-
Updated dependencies [
e9132d88,e9132d88]:- @shopify/remix-oxygen@3.0.0
- @shopify/hydrogen@2025.5.0
-
Moved the Cursor rules into more generic LLM prompt files. If you were using the Cursor rules, you will find the prompts in the
cookbook/llmsfolder and they can be put into your.cursor/rulesfolder manually. LLM prompt files will be maintained moving forward, while previous Cursor rules will not be updated anymore. (#2936) by @ruggishop -
Bump skeleton @shopify/cli and @shopify/mini-oxygen (#2883) by @juanpprieto
-
Update SFAPI and CAAPI versions to 2025.04 (#2886) by @juanpprieto
-
Updated dependencies [
af23e710,9d8a6644]:- @shopify/hydrogen@2025.4.0
-
Fix an issue with our starter template where duplicate content can exist on URLs that use internationalized handles. For example, if you have a product handle in english of
the-havocand translate it todas-chaosin German, duplicate content exists at both: (#2821) by @blittleWe've changed the starter template to make the second redirect to the first.
-
Added the Cursor rule for the subscriptions recipe. (#2874) by @ruggishop
-
Fix faulty truthiness check for cart quantity (#2855) by @frontsideair
-
Refactor ProductItem into a separate component (#2872) by @juanpprieto
-
Updated dependencies [
f80f3bc7,61ddf924,642bde4f]:- @shopify/hydrogen@2025.1.4
-
Moved the
Layoutcomponent back intoroot.tsxto avoid issues with styled errors. (#2829) by @ruggishop-
If you have a separate
app/layout.tsxfile, delete it and move its default exported component into yourroot.tsx. For example:// /app/root.tsx export function Layout({children}: {children?: React.ReactNode}) { const nonce = useNonce(); const data = useRouteLoaderData<RootLoader>('root'); return ( <html lang="en"> ... ); }
-
- Fixed an issue with the creation of JavaScript projects. (#2818) by @seanparsons
- Updates the
@shopify/cli,@shopify/cli-kitand@shopify/plugin-cloudflaredependencies to 3.77.1. (#2816) by @seanparsons
-
Bump Remix to 2.16.1 and vite to 6.2.0 (#2784) by @wizardlyhel
-
Update skeleton and create-hydrogen cli to 3.75.4 (#2769) by @juanpprieto
-
Fixing typescript compile (#2787) by @balazsbajorics
In tsconfig.json:
"types": [ "@shopify/oxygen-workers-types", - "@remix-run/node", + "@remix-run/server-runtime", "vite/client" ], -
Updates
@shopify/cli-kit,@shopify/cliand@shopify/plugin-cloudflareto3.77.0. (#2810) by @seanparsons -
Support for the Remix future flag
v3_routeConfig. (#2722) by @seanparsonsPlease refer to the Remix documentation for more details on
v3_routeConfigfuture flag: https://remix.run/docs/en/main/start/future-flags#v3_routeconfig-
Update your
vite.config.ts.export default defineConfig({ plugins: [ hydrogen(), oxygen(), remix({ - presets: [hydrogen.preset()], + presets: [hydrogen.v3preset()], future: { v3_fetcherPersist: true, v3_relativeSplatPath: true, v3_throwAbortReason: true, v3_lazyRouteDiscovery: true, v3_singleFetch: true, + v3_routeConfig: true, }, }), tsconfigPaths(), ], -
Update your
package.jsonand install the new packages. Make sure to match the Remix version along with other Remix npm packages and ensure the versions are 2.16.1 or above:"devDependencies": { "@remix-run/dev": "^2.16.1", + "@remix-run/fs-routes": "^2.16.1", + "@remix-run/route-config": "^2.16.1", -
Move the
Layoutcomponent export fromroot.tsxinto its own file. Make sure to supply an<Outlet>so Remix knows where to inject your route content.// /app/layout.tsx import {Outlet} from '@remix-run/react'; export default function Layout() { const nonce = useNonce(); const data = useRouteLoaderData<RootLoader>('root'); return ( <html lang="en"> ... <Outlet /> ... </html> ); } // Remember to remove the Layout export from your root.tsx
-
Add a routes.ts file. This is your new Remix route configuration file.
import {flatRoutes} from '@remix-run/fs-routes'; import {layout, type RouteConfig} from '@remix-run/route-config'; import {hydrogenRoutes} from '@shopify/hydrogen'; export default hydrogenRoutes([ // Your entire app reading from routes folder using Layout from layout.tsx layout('./layout.tsx', await flatRoutes()), ]) satisfies RouteConfig;
-
-
Updated dependencies [
0425e50d,74ef1ba7]:- @shopify/remix-oxygen@2.0.12
- @shopify/hydrogen@2025.1.3
-
Upgrade eslint to version 9 and unify eslint config across all packages (with the exception of the skeleton, which still keeps its own config) (#2716) by @liady
-
Bump remix version (#2740) by @wizardlyhel
-
Turn on Remix
v3_singleFetchfuture flag (#2708) by @wizardlyhelRemix single fetch migration quick guide: https://remix.run/docs/en/main/start/future-flags#v3_singlefetch Remix single fetch migration guide: https://remix.run/docs/en/main/guides/single-fetch
Note: If you have any routes that appends (or looks for) a search param named
_data, make sure to rename it to something else.-
In your
vite.config.ts, add the single fetch future flag.+ declare module "@remix-run/server-runtime" { + interface Future { + v3_singleFetch: true; + } + } export default defineConfig({ plugins: [ hydrogen(), oxygen(), remix({ presets: [hydrogen.preset()], future: { v3_fetcherPersist: true, v3_relativeSplatPath: true, v3_throwAbortReason: true, v3_lazyRouteDiscovery: true, + v3_singleFetch: true, }, }), tsconfigPaths(), ],
-
In your
entry.server.tsx, addnonceto the<RemixServer>.const body = await renderToReadableStream( <NonceProvider> <RemixServer context={remixContext} url={request.url} + nonce={nonce} /> </NonceProvider>, -
Update the
shouldRevalidatefunction inroot.tsx.Defaulting to no revalidation for root loader data to improve performance. When using this feature, you risk your UI getting out of sync with your server. Use with caution. If you are uncomfortable with this optimization, update the
return false;toreturn defaultShouldRevalidate;instead.For more details see: https://remix.run/docs/en/main/route/should-revalidate
export const shouldRevalidate: ShouldRevalidateFunction = ({ formMethod, currentUrl, nextUrl, - defaultShouldRevalidate, }) => { // revalidate when a mutation is performed e.g add to cart, login... if (formMethod && formMethod !== 'GET') return true; // revalidate when manually revalidating via useRevalidator if (currentUrl.toString() === nextUrl.toString()) return true; - return defaultShouldRevalidate; + return false; }; -
Update
cart.tsxto add a headers export and update todataimport usage.import { - json, + data, type LoaderFunctionArgs, type ActionFunctionArgs, type HeadersFunction } from '@shopify/remix-oxygen'; + export const headers: HeadersFunction = ({actionHeaders}) => actionHeaders; export async function action({request, context}: ActionFunctionArgs) { ... - return json( + return data( { cart: cartResult, errors, warnings, analytics: { cartId, }, }, {status, headers}, ); } export async function loader({context}: LoaderFunctionArgs) { const {cart} = context; - return json(await cart.get()); + return await cart.get(); } -
Deprecate
jsonanddeferimport usage from@shopify/remix-oxygen.Remove
json()/defer()in favor of raw objects.Single Fetch supports JSON objects and Promises out of the box, so you can return the raw data from your loader/action functions:
- import {json} from "@shopify/remix-oxygen"; export async function loader({}: LoaderFunctionArgs) { let tasks = await fetchTasks(); - return json(tasks); + return tasks; }
- import {defer} from "@shopify/remix-oxygen"; export async function loader({}: LoaderFunctionArgs) { let lazyStuff = fetchLazyStuff(); let tasks = await fetchTasks(); - return defer({ tasks, lazyStuff }); + return { tasks, lazyStuff }; }
If you were using the second parameter of json/defer to set a custom status or headers on your response, you can continue doing so via the new data API:
- import {json} from "@shopify/remix-oxygen"; + import {data, type HeadersFunction} from "@shopify/remix-oxygen"; + /** + * If your loader or action is returning a response with headers, + * make sure to export a headers function that merges your headers + * on your route. Otherwise, your headers may be lost. + * Remix doc: https://remix.run/docs/en/main/route/headers + **/ + export const headers: HeadersFunction = ({loaderHeaders}) => loaderHeaders; export async function loader({}: LoaderFunctionArgs) { let tasks = await fetchTasks(); - return json(tasks, { + return data(tasks, { headers: { "Cache-Control": "public, max-age=604800" } }); }
-
If you are using legacy customer account flow or multipass, there are a couple more files that requires updating:
In
root.tsxandroutes/account.tsx, add aheadersexport forloaderHeaders.+ export const headers: HeadersFunction = ({loaderHeaders}) => loaderHeaders;In
routes/account_.register.tsx, add aheadersexport foractionHeaders.+ export const headers: HeadersFunction = ({actionHeaders}) => actionHeaders; -
If you are using multipass, in
routes/account_.login.multipass.tsxa. export a
headersexport+ export const headers: HeadersFunction = ({actionHeaders}) => actionHeaders;b. Update all
jsonresponse wrapper toremixDataimport { - json, + data as remixData, } from '@shopify/remix-oxygen'; - return json( + return remixData( ... );
-
-
Updated dependencies [
3af2e453,6bff6b62,cd65685c,8c717570,4e81bd1b,3ea25820]:- @shopify/hydrogen@2025.1.1
- @shopify/remix-oxygen@2.0.11
-
Bump vite, Remix versions and tailwind v4 alpha to beta (#2696) by @wizardlyhel
-
Workaround for "Error: failed to execute 'insertBefore' on 'Node'" that sometimes happen during development. (#2701) by @wizardlyhel
// root.tsx /** * The main and reset stylesheets are added in the Layout component * to prevent a bug in development HMR updates. * * This avoids the "failed to execute 'insertBefore' on 'Node'" error * that occurs after editing and navigating to another page. * * It's a temporary fix until the issue is resolved. * https://github.com/remix-run/remix/issues/9242 */ export function links() { return [ - {rel: 'stylesheet', href: resetStyles}, - {rel: 'stylesheet', href: appStyles}, { rel: 'preconnect', href: 'https://cdn.shopify.com', }, { rel: 'preconnect', href: 'https://shop.app', }, {rel: 'icon', type: 'image/svg+xml', href: favicon}, ]; } ... export function Layout({children}: {children?: React.ReactNode}) { const nonce = useNonce(); const data = useRouteLoaderData<RootLoader>('root'); return ( <html lang="en"> <head> <meta charSet="utf-8" /> <meta name="viewport" content="width=device-width,initial-scale=1" /> + <link rel="stylesheet" href={resetStyles}></link> + <link rel="stylesheet" href={appStyles}></link> -
Turn on future flag
v3_lazyRouteDiscovery(#2702) by @wizardlyhelIn your vite.config.ts, add the following line:
export default defineConfig({ plugins: [ hydrogen(), oxygen(), remix({ presets: [hydrogen.preset()], future: { v3_fetcherPersist: true, v3_relativeSplatPath: true, v3_throwAbortReason: true, + v3_lazyRouteDiscovery: true, }, }), tsconfigPaths(), ],Test your app by running
npm run devand nothing should break -
Fix image size warnings on collections page (#2703) by @wizardlyhel
-
Bump cli version (#2732) by @wizardlyhel
-
Updated dependencies [
fdab06f5,ae6d71f0,650d57b3,064de138]:- @shopify/remix-oxygen@2.0.10
- @shopify/hydrogen@2025.1.0
- Bump cli version (#2694) by @wizardlyhel
- Prevent scroll reset on variant change (#2672) by @scottdixon
-
Remove initial redirect from product display page (#2643) by @scottdixon
-
Optional updates for the product route and product form to handle combined listing and 2000 variant limit. (#2659) by @wizardlyhel
- Update your SFAPI product query to bring in the new query fields:
const PRODUCT_FRAGMENT = `#graphql fragment Product on Product { id title vendor handle descriptionHtml description + encodedVariantExistence + encodedVariantAvailability options { name optionValues { name + firstSelectableVariant { + ...ProductVariant + } + swatch { + color + image { + previewImage { + url + } + } + } } } - selectedVariant: selectedOrFirstAvailableVariant(selectedOptions: $selectedOptions, ignoreUnknownOptions: true, caseInsensitiveMatch: true) { + selectedOrFirstAvailableVariant(selectedOptions: $selectedOptions, ignoreUnknownOptions: true, caseInsensitiveMatch: true) { + ...ProductVariant + } + adjacentVariants (selectedOptions: $selectedOptions) { + ...ProductVariant + } - variants(first: 1) { - nodes { - ...ProductVariant - } - } seo { description title } } ${PRODUCT_VARIANT_FRAGMENT} ` as const;- Update
loadDeferredDatafunction. We no longer need to load in all the variants. You can also removeVARIANTS_QUERYvariable.
function loadDeferredData({context, params}: LoaderFunctionArgs) { + // Put any API calls that is not critical to be available on first page render + // For example: product reviews, product recommendations, social feeds. - // In order to show which variants are available in the UI, we need to query - // all of them. But there might be a *lot*, so instead separate the variants - // into it's own separate query that is deferred. So there's a brief moment - // where variant options might show as available when they're not, but after - // this deferred query resolves, the UI will update. - const variants = context.storefront - .query(VARIANTS_QUERY, { - variables: {handle: params.handle!}, - }) - .catch((error) => { - // Log query errors, but don't throw them so the page can still render - console.error(error); - return null; - }); + return {} - return { - variants, - }; }- Remove the redirect logic in the
loadCriticalDatafunction and completely removeredirectToFirstVariantfunction
async function loadCriticalData({ context, params, request, }: LoaderFunctionArgs) { const {handle} = params; const {storefront} = context; if (!handle) { throw new Error('Expected product handle to be defined'); } const [{product}] = await Promise.all([ storefront.query(PRODUCT_QUERY, { variables: {handle, selectedOptions: getSelectedProductOptions(request)}, }), // Add other queries here, so that they are loaded in parallel ]); if (!product?.id) { throw new Response(null, {status: 404}); } - const firstVariant = product.variants.nodes[0]; - const firstVariantIsDefault = Boolean( - firstVariant.selectedOptions.find( - (option: SelectedOption) => - option.name === 'Title' && option.value === 'Default Title', - ), - ); - if (firstVariantIsDefault) { - product.selectedVariant = firstVariant; - } else { - // if no selected variant was returned from the selected options, - // we redirect to the first variant's url with it's selected options applied - if (!product.selectedVariant) { - throw redirectToFirstVariant({product, request}); - } - } return { product, }; } ... - function redirectToFirstVariant({ - product, - request, - }: { - product: ProductFragment; - request: Request; - }) { - ... - }- Update the
Productcomponent to use the new data fields.
import { getSelectedProductOptions, Analytics, useOptimisticVariant, + getAdjacentAndFirstAvailableVariants, } from '@shopify/hydrogen'; export default function Product() { + const {product} = useLoaderData<typeof loader>(); - const {product, variants} = useLoaderData<typeof loader>(); + // Optimistically selects a variant with given available variant information + const selectedVariant = useOptimisticVariant( + product.selectedOrFirstAvailableVariant, + getAdjacentAndFirstAvailableVariants(product), + ); - const selectedVariant = useOptimisticVariant( - product.selectedVariant, - variants, - );- Handle missing search query param in url from selecting a first variant
import { getSelectedProductOptions, Analytics, useOptimisticVariant, getAdjacentAndFirstAvailableVariants, + useSelectedOptionInUrlParam, } from '@shopify/hydrogen'; export default function Product() { const {product} = useLoaderData<typeof loader>(); // Optimistically selects a variant with given available variant information const selectedVariant = useOptimisticVariant( product.selectedOrFirstAvailableVariant, getAdjacentAndFirstAvailableVariants(product), ); + // Sets the search param to the selected variant without navigation + // only when no search params are set in the url + useSelectedOptionInUrlParam(selectedVariant.selectedOptions);- Get the product options array using
getProductOptions
import { getSelectedProductOptions, Analytics, useOptimisticVariant, + getProductOptions, getAdjacentAndFirstAvailableVariants, useSelectedOptionInUrlParam, } from '@shopify/hydrogen'; export default function Product() { const {product} = useLoaderData<typeof loader>(); // Optimistically selects a variant with given available variant information const selectedVariant = useOptimisticVariant( product.selectedOrFirstAvailableVariant, getAdjacentAndFirstAvailableVariants(product), ); // Sets the search param to the selected variant without navigation // only when no search params are set in the url useSelectedOptionInUrlParam(selectedVariant.selectedOptions); + // Get the product options array + const productOptions = getProductOptions({ + ...product, + selectedOrFirstAvailableVariant: selectedVariant, + });- Remove the
AwaitandSuspensefrom theProductForm. We no longer have any queries that we need to wait for.
export default function Product() { ... return ( ... + <ProductForm + productOptions={productOptions} + selectedVariant={selectedVariant} + /> - <Suspense - fallback={ - <ProductForm - product={product} - selectedVariant={selectedVariant} - variants={[]} - /> - } - > - <Await - errorElement="There was a problem loading product variants" - resolve={variants} - > - {(data) => ( - <ProductForm - product={product} - selectedVariant={selectedVariant} - variants={data?.product?.variants.nodes || []} - /> - )} - </Await> - </Suspense>- Update the
ProductFormcomponent.
import {Link, useNavigate} from '@remix-run/react'; import {type MappedProductOptions} from '@shopify/hydrogen'; import type { Maybe, ProductOptionValueSwatch, } from '@shopify/hydrogen/storefront-api-types'; import {AddToCartButton} from './AddToCartButton'; import {useAside} from './Aside'; import type {ProductFragment} from 'storefrontapi.generated'; export function ProductForm({ productOptions, selectedVariant, }: { productOptions: MappedProductOptions[]; selectedVariant: ProductFragment['selectedOrFirstAvailableVariant']; }) { const navigate = useNavigate(); const {open} = useAside(); return ( <div className="product-form"> {productOptions.map((option) => ( <div className="product-options" key={option.name}> <h5>{option.name}</h5> <div className="product-options-grid"> {option.optionValues.map((value) => { const { name, handle, variantUriQuery, selected, available, exists, isDifferentProduct, swatch, } = value; if (isDifferentProduct) { // SEO // When the variant is a combined listing child product // that leads to a different url, we need to render it // as an anchor tag return ( <Link className="product-options-item" key={option.name + name} prefetch="intent" preventScrollReset replace to={`/products/${handle}?${variantUriQuery}`} style={{ border: selected ? '1px solid black' : '1px solid transparent', opacity: available ? 1 : 0.3, }} > <ProductOptionSwatch swatch={swatch} name={name} /> </Link> ); } else { // SEO // When the variant is an update to the search param, // render it as a button with javascript navigating to // the variant so that SEO bots do not index these as // duplicated links return ( <button type="button" className={`product-options-item${ exists && !selected ? ' link' : '' }`} key={option.name + name} style={{ border: selected ? '1px solid black' : '1px solid transparent', opacity: available ? 1 : 0.3, }} disabled={!exists} onClick={() => { if (!selected) { navigate(`?${variantUriQuery}`, { replace: true, }); } }} > <ProductOptionSwatch swatch={swatch} name={name} /> </button> ); } })} </div> <br /> </div> ))} <AddToCartButton disabled={!selectedVariant || !selectedVariant.availableForSale} onClick={() => { open('cart'); }} lines={ selectedVariant ? [ { merchandiseId: selectedVariant.id, quantity: 1, selectedVariant, }, ] : [] } > {selectedVariant?.availableForSale ? 'Add to cart' : 'Sold out'} </AddToCartButton> </div> ); } function ProductOptionSwatch({ swatch, name, }: { swatch?: Maybe<ProductOptionValueSwatch> | undefined; name: string; }) { const image = swatch?.image?.previewImage?.url; const color = swatch?.color; if (!image && !color) return name; return ( <div aria-label={name} className="product-option-label-swatch" style={{ backgroundColor: color || 'transparent', }} > {!!image && <img src={image} alt={name} />} </div> ); }
- Update
app.css
+ /* + * -------------------------------------------------- + * Non anchor links + * -------------------------------------------------- + */ + .link:hover { + text-decoration: underline; + cursor: pointer; + } ... - .product-options-item { + .product-options-item, + .product-options-item:disabled { + padding: 0.25rem 0.5rem; + background-color: transparent; + font-size: 1rem; + font-family: inherit; + } + .product-option-label-swatch { + width: 1.25rem; + height: 1.25rem; + margin: 0.25rem 0; + } + .product-option-label-swatch img { + width: 100%; + }
- Update
lib/variants.ts
Make
useVariantUrlandgetVariantUrlflexible to supplying a selected option paramexport function useVariantUrl( handle: string, - selectedOptions: SelectedOption[], + selectedOptions?: SelectedOption[], ) { const {pathname} = useLocation(); return useMemo(() => { return getVariantUrl({ handle, pathname, searchParams: new URLSearchParams(), selectedOptions, }); }, [handle, selectedOptions, pathname]); } export function getVariantUrl({ handle, pathname, searchParams, selectedOptions, }: { handle: string; pathname: string; searchParams: URLSearchParams; - selectedOptions: SelectedOption[]; + selectedOptions?: SelectedOption[], }) { const match = /(\/[a-zA-Z]{2}-[a-zA-Z]{2}\/)/g.exec(pathname); const isLocalePathname = match && match.length > 0; const path = isLocalePathname ? `${match![0]}products/${handle}` : `/products/${handle}`; - selectedOptions.forEach((option) => { + selectedOptions?.forEach((option) => { searchParams.set(option.name, option.value); });
- Update
routes/collections.$handle.tsx
We no longer need to query for the variants since product route can efficiently obtain the first available variants. Update the code to reflect that:
const PRODUCT_ITEM_FRAGMENT = `#graphql fragment MoneyProductItem on MoneyV2 { amount currencyCode } fragment ProductItem on Product { id handle title featuredImage { id altText url width height } priceRange { minVariantPrice { ...MoneyProductItem } maxVariantPrice { ...MoneyProductItem } } - variants(first: 1) { - nodes { - selectedOptions { - name - value - } - } - } } ` as const;and remove the variant reference
function ProductItem({ product, loading, }: { product: ProductItemFragment; loading?: 'eager' | 'lazy'; }) { - const variant = product.variants.nodes[0]; - const variantUrl = useVariantUrl(product.handle, variant.selectedOptions); + const variantUrl = useVariantUrl(product.handle); return (- Update
routes/collections.all.tsx
Same reasoning as
collections.$handle.tsxconst PRODUCT_ITEM_FRAGMENT = `#graphql fragment MoneyProductItem on MoneyV2 { amount currencyCode } fragment ProductItem on Product { id handle title featuredImage { id altText url width height } priceRange { minVariantPrice { ...MoneyProductItem } maxVariantPrice { ...MoneyProductItem } } - variants(first: 1) { - nodes { - selectedOptions { - name - value - } - } - } } ` as const;and remove the variant reference
function ProductItem({ product, loading, }: { product: ProductItemFragment; loading?: 'eager' | 'lazy'; }) { - const variant = product.variants.nodes[0]; - const variantUrl = useVariantUrl(product.handle, variant.selectedOptions); + const variantUrl = useVariantUrl(product.handle); return (- Update
routes/search.tsx
Instead of using the first variant, use
selectedOrFirstAvailableVariantconst SEARCH_PRODUCT_FRAGMENT = `#graphql fragment SearchProduct on Product { __typename handle id publishedAt title trackingParameters vendor - variants(first: 1) { - nodes { + selectedOrFirstAvailableVariant( + selectedOptions: [] + ignoreUnknownOptions: true + caseInsensitiveMatch: true + ) { id image { url altText width height } price { amount currencyCode } compareAtPrice { amount currencyCode } selectedOptions { name value } product { handle title } } - } } ` as const;const PREDICTIVE_SEARCH_PRODUCT_FRAGMENT = `#graphql fragment PredictiveProduct on Product { __typename id title handle trackingParameters - variants(first: 1) { - nodes { + selectedOrFirstAvailableVariant( + selectedOptions: [] + ignoreUnknownOptions: true + caseInsensitiveMatch: true + ) { id image { url altText width height } price { amount currencyCode } } - } }- Update
components/SearchResults.tsx
function SearchResultsProducts({ term, products, }: PartialSearchResult<'products'>) { if (!products?.nodes.length) { return null; } return ( <div className="search-result"> <h2>Products</h2> <Pagination connection={products}> {({nodes, isLoading, NextLink, PreviousLink}) => { const ItemsMarkup = nodes.map((product) => { const productUrl = urlWithTrackingParams({ baseUrl: `/products/${product.handle}`, trackingParams: product.trackingParameters, term, }); + const price = product?.selectedOrFirstAvailableVariant?.price; + const image = product?.selectedOrFirstAvailableVariant?.image; return ( <div className="search-results-item" key={product.id}> <Link prefetch="intent" to={productUrl}> - {product.variants.nodes[0].image && ( + {image && ( <Image - data={product.variants.nodes[0].image} + data={image} alt={product.title} width={50} /> )} <div> <p>{product.title}</p> <small> - <Money data={product.variants.nodes[0].price} /> + {price && + <Money data={price} /> + } </small> </div> </Link> </div> ); });- Update
components/SearchResultsPredictive.tsx
function SearchResultsPredictiveProducts({ term, products, closeSearch, }: PartialPredictiveSearchResult<'products'>) { if (!products.length) return null; return ( <div className="predictive-search-result" key="products"> <h5>Products</h5> <ul> {products.map((product) => { const productUrl = urlWithTrackingParams({ baseUrl: `/products/${product.handle}`, trackingParams: product.trackingParameters, term: term.current, }); + const price = product?.selectedOrFirstAvailableVariant?.price; - const image = product?.variants?.nodes?.[0].image; + const image = product?.selectedOrFirstAvailableVariant?.image; return ( <li className="predictive-search-result-item" key={product.id}> <Link to={productUrl} onClick={closeSearch}> {image && ( <Image alt={image.altText ?? ''} src={image.url} width={50} height={50} /> )} <div> <p>{product.title}</p> <small> - {product?.variants?.nodes?.[0].price && ( + {price && ( - <Money data={product.variants.nodes[0].price} /> + <Money data={price} /> )} </small> </div> </Link> </li> ); })} </ul> </div> ); } -
Update
Asideto have an accessible close button label (#2639) by @lb- -
Fix cart route so that it works with no-js (#2665) by @wizardlyhel
-
Bump Shopify cli version (#2667) by @wizardlyhel
-
Updated dependencies [
8f64915e,a57d5267,91d60fd2,23a80f3e]:- @shopify/hydrogen@2024.10.1
- Bump to get new cli package version by @wizardlyhel
-
Stabilize
getSitemap,getSitemapIndexand implement on skeleton (#2589) by @juanpprieto- Update the
getSitemapIndexat/app/routes/[sitemap.xml].tsx
- import {unstable__getSitemapIndex as getSitemapIndex} from '@shopify/hydrogen'; + import {getSitemapIndex} from '@shopify/hydrogen';
- Update the
getSitemapat/app/routes/sitemap.$type.$page[.xml].tsx
- import {unstable__getSitemap as getSitemap} from '@shopify/hydrogen'; + import {getSitemap} from '@shopify/hydrogen';
For a reference implementation please see the skeleton template sitemap routes
- Update the
-
[Breaking change] (#2588) by @wizardlyhel
Set up Customer Privacy without the Shopify's cookie banner by default.
If you are using Shopify's cookie banner to handle user consent in your app, you need to set
withPrivacyBanner: trueto the consent config. Without this update, the Shopify cookie banner will not appear.return defer({ ... consent: { checkoutDomain: env.PUBLIC_CHECKOUT_DOMAIN, storefrontAccessToken: env.PUBLIC_STOREFRONT_API_TOKEN, + withPrivacyBanner: true, // localize the privacy banner country: args.context.storefront.i18n.country, language: args.context.storefront.i18n.language, }, }); -
Update to 2024-10 SFAPI (#2570) by @wizardlyhel
-
[Breaking change] (#2546) by @frandiox
Update
createWithCacheto make it harder to accidentally cache undesired results.requestis now mandatory prop when initializingcreateWithCache.// server.ts export default { async fetch( request: Request, env: Env, executionContext: ExecutionContext, ): Promise<Response> { try { // ... - const withCache = createWithCache({cache, waitUntil}); + const withCache = createWithCache({cache, waitUntil, request});createWithCachenow returns an object with two utility functions:withCache.runandwithCache.fetch. Both have a new propshouldCacheResultthat must be defined.The original
withCachecallback function is nowwithCache.run. This is useful to run multiple fetch calls and merge their responses, or run any arbitrary code. It caches anything you return, but you can throw if you don't want to cache anything.const withCache = createWithCache({cache, waitUntil, request}); const fetchMyCMS = (query) => { - return withCache(['my-cms', query], CacheLong(), async (params) => { + return withCache.run({ + cacheKey: ['my-cms', query], + cacheStrategy: CacheLong(), + // Cache if there are no data errors or a specific data that make this result not suited for caching + shouldCacheResult: (result) => !result?.errors, + }, async(params) => { const response = await fetch('my-cms.com/api', { method: 'POST', body: query, }); if (!response.ok) throw new Error(response.statusText); const {data, error} = await response.json(); if (error || !data) throw new Error(error ?? 'Missing data'); params.addDebugData({displayName: 'My CMS query', response}); return data; }); };New
withCache.fetchis for caching simple fetch requests. This method caches the responses if they are OK responses, and you can passshouldCacheResponse,cacheKey, etc. to modify behavior.datais the consumed body of the response (we need to consume to cache it).const withCache = createWithCache({cache, waitUntil, request}); const {data, response} = await withCache.fetch<{data: T; error: string}>( 'my-cms.com/api', { method: 'POST', headers: {'Content-type': 'application/json'}, body, }, { cacheStrategy: CacheLong(), // Cache if there are no data errors or a specific data that make this result not suited for caching shouldCacheResponse: (result) => !result?.error, cacheKey: ['my-cms', body], displayName: 'My CMS query', }, );
-
[Breaking change] (#2585) by @wizardlyhel
Deprecate usages of
product.options.valuesand useproduct.options.optionValuesinstead.- Update your product graphql query to use the new
optionValuesfield.
const PRODUCT_FRAGMENT = `#graphql fragment Product on Product { id title options { name - values + optionValues { + name + } }- Update your
<VariantSelector>to use the newoptionValuesfield.
<VariantSelector handle={product.handle} - options={product.options.filter((option) => option.values.length > 1)} + options={product.options.filter((option) => option.optionValues.length > 1)} variants={variants} > - Update your product graphql query to use the new
-
Updated dependencies [
d97cd56e,809c9f3d,8c89f298,a253ef97,84a66b1e,227035e7,ac12293c,c7c9f2eb,76cd4f9b,8337e534]:- @shopify/hydrogen@2024.10.0
- @shopify/remix-oxygen@2.0.9
-
Use HTML datalist element for query suggestions for autocomplete experience (#2506) by @frontsideair
-
Bump cli packages version (#2592) by @wizardlyhel
-
Updated dependencies [
e963389d,d08d8c37]:- @shopify/hydrogen@2024.7.9
- Updated dependencies [
39f8f8fd]:- @shopify/hydrogen@2024.7.7
- Updated dependencies [
d0ff37a9]:- @shopify/hydrogen@2024.7.6
-
Update Shopify CLI and cli-kit dependencies to 3.66.1 (#2512) by @frandiox
-
createCartHandler supplies updateGiftCardCodes method (#2298) by @wizardlyhel
-
Fix menu links in side panel not working on mobile devices (#2450) by @wizardlyhel
// /app/components/Header.tsx export function HeaderMenu({ menu, primaryDomainUrl, viewport, publicStoreDomain, }: { menu: HeaderProps['header']['menu']; primaryDomainUrl: HeaderProps['header']['shop']['primaryDomain']['url']; viewport: Viewport; publicStoreDomain: HeaderProps['publicStoreDomain']; }) { const className = `header-menu-${viewport}`; + const {close} = useAside(); - function closeAside(event: React.MouseEvent<HTMLAnchorElement>) { - if (viewport === 'mobile') { - event.preventDefault(); - window.location.href = event.currentTarget.href; - } - } return ( <nav className={className} role="navigation"> {viewport === 'mobile' && ( <NavLink end - onClick={closeAside} + onClick={close} prefetch="intent" style={activeLinkStyle} to="/" > Home </NavLink> )} {(menu || FALLBACK_HEADER_MENU).items.map((item) => { if (!item.url) return null; // if the url is internal, we strip the domain const url = item.url.includes('myshopify.com') || item.url.includes(publicStoreDomain) || item.url.includes(primaryDomainUrl) ? new URL(item.url).pathname : item.url; return ( <NavLink className="header-menu-item" end key={item.id} - onClick={closeAside} + onClick={close} prefetch="intent" style={activeLinkStyle} to={url} > {item.title} </NavLink> ); })} </nav> ); } -
Add localization support to consent privacy banner (#2457) by @juanpprieto
-
Updated dependencies [
d633e49a,1b217cd6,d929b561,664a09d5,0c1e511d,eefa8203]:- @shopify/hydrogen@2024.7.5
- @shopify/remix-oxygen@2.0.7
- Updated dependencies [
b0d3bc06]:- @shopify/hydrogen@2024.7.4
-
Search & Predictive Search improvements (#2363) by @juanpprieto
-
// in app/lib/context import {createHydrogenContext} from '@shopify/hydrogen'; export async function createAppLoadContext( request: Request, env: Env, executionContext: ExecutionContext, ) { const hydrogenContext = createHydrogenContext({ env, request, cache, waitUntil, session, i18n: {language: 'EN', country: 'US'}, cart: { queryFragment: CART_QUERY_FRAGMENT, }, // ensure to overwrite any options that is not using the default values from your server.ts }); return { ...hydrogenContext, // declare additional Remix loader context }; }
- Use
createAppLoadContextmethod in server.ts Ensure to overwrite any options that is not using the default values increateHydrogenContext.
// in server.ts - import { - createCartHandler, - createStorefrontClient, - createCustomerAccountClient, - } from '@shopify/hydrogen'; + import {createAppLoadContext} from '~/lib/context'; export default { async fetch( request: Request, env: Env, executionContext: ExecutionContext, ): Promise<Response> { - const {storefront} = createStorefrontClient( - ... - ); - const customerAccount = createCustomerAccountClient( - ... - ); - const cart = createCartHandler( - ... - ); + const appLoadContext = await createAppLoadContext( + request, + env, + executionContext, + ); /** * Create a Remix request handler and pass * Hydrogen's Storefront client to the loader context. */ const handleRequest = createRequestHandler({ build: remixBuild, mode: process.env.NODE_ENV, - getLoadContext: (): AppLoadContext => ({ - session, - storefront, - customerAccount, - cart, - env, - waitUntil, - }), + getLoadContext: () => appLoadContext, }); }
- Use infer type for AppLoadContext in env.d.ts
// in env.d.ts + import type {createAppLoadContext} from '~/lib/context'; + interface AppLoadContext extends Awaited<ReturnType<typeof createAppLoadContext>> { - interface AppLoadContext { - env: Env; - cart: HydrogenCart; - storefront: Storefront; - customerAccount: CustomerAccount; - session: AppSession; - waitUntil: ExecutionContext['waitUntil']; }
- Use
-
Use type
HydrogenEnvfor all the env.d.ts (#2333) by @michenly// in env.d.ts + import type {HydrogenEnv} from '@shopify/hydrogen'; + interface Env extends HydrogenEnv {} - interface Env { - SESSION_SECRET: string; - PUBLIC_STOREFRONT_API_TOKEN: string; - PRIVATE_STOREFRONT_API_TOKEN: string; - PUBLIC_STORE_DOMAIN: string; - PUBLIC_STOREFRONT_ID: string; - PUBLIC_CUSTOMER_ACCOUNT_API_CLIENT_ID: string; - PUBLIC_CUSTOMER_ACCOUNT_API_URL: string; - PUBLIC_CHECKOUT_DOMAIN: string; - }
-
Add a hydration check for google web cache. This prevents an infinite redirect when viewing the cached version of a hydrogen site on Google. (#2334) by @blittle
Update your entry.server.jsx file to include this check:
+ if (!window.location.origin.includes("webcache.googleusercontent.com")) { startTransition(() => { hydrateRoot( document, <StrictMode> <RemixBrowser /> </StrictMode> ); }); + }
-
Updated dependencies [
a2d9acf9,c0d7d917,b09e9a4c,c204eacf,bf4e3d3c,20a8e63b,6e5d8ea7,7c4f67a6,dfb9be77,31ea19e8]:- @shopify/cli-hydrogen@8.4.0
- @shopify/hydrogen@2024.7.3
- @shopify/remix-oxygen@2.0.6
- Updated dependencies [
150854ed]:- @shopify/hydrogen@2024.7.2
-
Changed the GraphQL config file format to be TS/JS instead of YAML. (#2311) by @frandiox
-
Updated dependencies [
18ea233c,8b2322d7]:- @shopify/cli-hydrogen@8.3.0
-
Update
@shopify/oxygen-workers-typesto fix issues on Windows. (#2252) by @michenly -
[Breaking change] (#2113) by @blittle
Previously the
VariantSelectorcomponent would filter out options that only had one value. This is undesireable for some apps. We've removed that filter, if you'd like to retain the existing functionality, simply filter the options prop before it is passed to theVariantSelectorcomponent:<VariantSelector handle={product.handle} + options={product.options.filter((option) => option.values.length > 1)} - options={product.options} variants={variants}> </VariantSelector>Fixes #1198
-
Update root to use Remix's Layout Export pattern and eliminate the use of
useLoaderDatain root. (#2292) by @michenlyThe diff below showcase how you can make this refactor in existing application.
import { Outlet, - useLoaderData, + useRouteLoaderData, } from '@remix-run/react'; -import {Layout} from '~/components/Layout'; +import {PageLayout} from '~/components/PageLayout'; -export default function App() { +export function Layout({children}: {children?: React.ReactNode}) { const nonce = useNonce(); - const data = useLoaderData<typeof loader>(); + const data = useRouteLoaderData<typeof loader>('root'); return ( <html> ... <body> - <Layout {...data}> - <Outlet /> - </Layout> + {data? ( + <PageLayout {...data}>{children}</PageLayout> + ) : ( + children + )} </body> </html> ); } +export default function App() { + return <Outlet />; +} export function ErrorBoundary() { - const rootData = useLoaderData<typeof loader>(); return ( - <html> - ... - <body> - <Layout {...rootData}> - <div className="route-error"> - <h1>Error</h1> - ... - </div> - </Layout> - </body> - </html> + <div className="route-error"> + <h1>Error</h1> + ... + </div> ); } -
Refactor the cart and product form components (#2132) by @blittle
-
Remove manual setting of session in headers and recommend setting it in server after response is created. (#2137) by @michenly
Step 1: Add
isPendingimplementation in session// in app/lib/session.ts export class AppSession implements HydrogenSession { + public isPending = false; get unset() { + this.isPending = true; return this.#session.unset; } get set() { + this.isPending = true; return this.#session.set; } commit() { + this.isPending = false; return this.#sessionStorage.commitSession(this.#session); } }Step 2: update response header if
session.isPendingis true// in server.ts export default { async fetch(request: Request): Promise<Response> { try { const response = await handleRequest(request); + if (session.isPending) { + response.headers.set('Set-Cookie', await session.commit()); + } return response; } catch (error) { ... } }, };Step 3: remove setting cookie with session.commit() in routes
// in route files export async function loader({context}: LoaderFunctionArgs) { return json({}, - { - headers: { - 'Set-Cookie': await context.session.commit(), - }, }, ); } -
Moved
@shopify/clifromdependenciestodevDependencies. (#2312) by @frandiox -
The
@shopify/clipackage now bundles the@shopify/cli-hydrogenplugin. Therefore, you can now remove the latter from your local dependencies: (#2306) by @frandiox"@shopify/cli": "3.64.0", - "@shopify/cli-hydrogen": "^8.1.1", "@shopify/hydrogen": "2024.7.0", -
Updated dependencies [
a0e84d76,426bb390,4337200c,710625c7,8b9c726d,10a419bf,6a6278bb,66236ca6,dcbd0bbf,a5e03e2a,c2690653,54c2f7ad,4337200c,e96b332b,f3065371,6cd5554b,9eb60d73,e432533e,de3f70be,83cb96f4]:- @shopify/remix-oxygen@2.0.5
- @shopify/cli-hydrogen@8.2.0
- @shopify/hydrogen@2024.7.1
-
<Analytics>anduseAnalyticsare now stable. (#2141) by @wizardlyhel -
Update the skeleton template to use React state in the aside dialogs (#2088) by @blittle
-
Updated dependencies [
fe82119f,32d4c33e,8eea75ec,27e51abf,f29c9085,7b838beb,d702aec2,ca4cf045,5a554b2e,27e51abf,5d6465b3,608389d6,9dfd1cfe,7def3e9f,65239c76,ca7f2888]:- @shopify/hydrogen@2024.4.3
- @shopify/cli-hydrogen@8.1.0
-
Add JSdoc to
getSelectedProductOptionsutility and cleanup the skeleton implementation (#2089) by @juanpprieto -
Updated dependencies [
286589ee,6f5061d9,ae262b61,2c11ca3b,b70f9c2c,17528db1,58ea9bb0]:- @shopify/cli-hydrogen@8.0.4
- @shopify/hydrogen@2024.4.2
-
Update
@shopify/clidependency to avoid React version mismatches in your project: (#2059) by @frandiox"dependencies": { ... - "@shopify/cli": "3.58.0", + "@shopify/cli": "3.59.2", ... } -
Updated dependencies [
d2bc720b]:- @shopify/cli-hydrogen@8.0.3
-
Pin React dependency to 18.2.0 to avoid mismatches. (#2051) by @frandiox
-
Updated dependencies [
9c36c8a5]:- @shopify/cli-hydrogen@8.0.2
-
Stop inlining the favicon in base64 to avoid issues with the Content-Security-Policy. In
vite.config.js: (#2006) by @frandioxexport default defineConfig({ plugins: [ ... ], + build: { + assetsInlineLimit: 0, + }, }); -
To improve HMR in Vite, move the
useRootLoaderDatafunction fromapp/root.tsxto a separate file likeapp/lib/root-data.ts. This change avoids circular imports: (#2014) by @frandiox// app/lib/root-data.ts import {useMatches} from '@remix-run/react'; import type {SerializeFrom} from '@shopify/remix-oxygen'; import type {loader} from '~/root'; /** * Access the result of the root loader from a React component. */ export const useRootLoaderData = () => { const [root] = useMatches(); return root?.data as SerializeFrom<typeof loader>; };
Import this hook from
~/lib/root-datainstead of~/rootin your components. -
Updated dependencies [
b4dfda32,ffa57bdb,ac4e1670,0af624d5,9723eaf3,e842f68c]:- @shopify/cli-hydrogen@8.0.1
- @shopify/hydrogen@2024.4.1
-
Update internal libraries for parsing
.envfiles. (#1946) by @aswamyPlease update the
@shopify/clidependency in your app to avoid duplicated subdependencies:"dependencies": { - "@shopify/cli": "3.56.3", + "@shopify/cli": "3.58.0", } -
Add Adds magic Catalog route (#1967) by @juanpprieto
-
Update Vite plugin imports, and how their options are passed to Remix: (#1935) by @frandiox
-import {hydrogen, oxygen} from '@shopify/cli-hydrogen/experimental-vite'; +import {hydrogen} from '@shopify/hydrogen/vite'; +import {oxygen} from '@shopify/mini-oxygen/vite'; import {vitePlugin as remix} from '@remix-run/dev'; export default defineConfig({ hydrogen(), oxygen(), remix({ - buildDirectory: 'dist', + presets: [hydrogen.preset()], future: {
-
Add
@shopify/mini-oxygenas a dev dependency for local development: (#1891) by @frandiox"devDependencies": { "@remix-run/dev": "^2.8.0", "@remix-run/eslint-config": "^2.8.0", + "@shopify/mini-oxygen": "^3.0.0", "@shopify/oxygen-workers-types": "^4.0.0", ... }, -
Add the
customer-account pushcommand to the Hydrogen CLI. This allows you to push the current--dev-originURL to the Shopify admin to enable secure connection to the Customer Account API for local development. (#1804) by @michenly -
Fix types returned by the
sessionobject. (#1869) by @frandioxIn
remix.env.d.tsorenv.d.ts, add the following types:import type { // ... HydrogenCart, + HydrogenSessionData, } from '@shopify/hydrogen'; // ... declare module '@shopify/remix-oxygen' { // ... + interface SessionData extends HydrogenSessionData {} } -
Codegen dependencies must be now listed explicitly in
package.json: (#1962) by @frandiox{ "devDependencies": { + "@graphql-codegen/cli": "5.0.2", "@remix-run/dev": "^2.8.0", "@remix-run/eslint-config": "^2.8.0", + "@shopify/hydrogen-codegen": "^0.3.0", "@shopify/mini-oxygen": "^2.2.5", "@shopify/oxygen-workers-types": "^4.0.0", ... } } -
Updated dependencies [
4eaec272,14bb5df1,646b78d4,87072950,5f1295fe,3c8a7313,ca1dcbb7,11879b17,f4d6e5b0,788d86b3,ebaf5529,da95bb1c,5bb43304,140e4768,062d6be7,b3323e59,ab0df5a5,ebaf5529,ebaf5529,9e899218,a209019f,d007b7bc,a5511cd7,4afedb4d,34fbae23,e3baaba5,99d72f7a,9351f9f5]:- @shopify/cli-hydrogen@8.0.0
- @shopify/hydrogen@2024.4.0
- @shopify/remix-oxygen@2.0.4
-
Improve performance of predictive search: (#1823) by @frandiox
- Change the request to be GET instead of POST to avoid Remix route revalidations.
- Add Cache-Control headers to the response to get quicker results when typing.
Aside from that, it now shows a loading state when fetching the results instead of "No results found.".
-
Updated dependencies [
351b3c1b,5060cf57,2888014e]:- @shopify/hydrogen@2024.1.4
- @shopify/cli-hydrogen@7.1.2
-
Update the
@shopify/clidependency: (#1786) by @frandiox- "@shopify/cli": "3.52.0", + "@shopify/cli": "3.56.3",
-
Update Remix and associated packages to 2.8.0. (#1781) by @frandiox
"dependencies": { - "@remix-run/react": "^2.6.0", - "@remix-run/server-runtime": "^2.6.0", + "@remix-run/react": "^2.8.0", + "@remix-run/server-runtime": "^2.8.0", //... }, "devDependencies": { - "@remix-run/dev": "^2.6.0", - "@remix-run/eslint-config": "^2.6.0", + "@remix-run/dev": "^2.8.0", + "@remix-run/eslint-config": "^2.8.0", //... }, -
Updated dependencies [
ced1d4cb,fc013401,e641255e,d7e04cb6,eedd9c49]:- @shopify/cli-hydrogen@7.1.1
- @shopify/hydrogen@2024.1.3
-
This is an important fix to a bug with 404 routes and path-based i18n projects where some unknown routes would not properly render a 404. This fixes all new projects, but to fix existing projects, add a
($locale).tsxroute with the following contents: (#1732) by @blittleimport {type LoaderFunctionArgs} from '@remix-run/server-runtime'; export async function loader({params, context}: LoaderFunctionArgs) { const {language, country} = context.storefront.i18n; if ( params.locale && params.locale.toLowerCase() !== `${language}-${country}`.toLowerCase() ) { // If the locale URL param is defined, yet we still are still at the default locale // then the the locale param must be invalid, send to the 404 page throw new Response(null, {status: 404}); } return null; }
-
Add defensive null checks to the default cart implementation in the starter template (#1746) by @blittle
-
🐛 Fix issue where customer login does not persist to checkout (#1719) by @michenly
✨ Add
customerAccountoption tocreateCartHandler. Where a?logged_in=truewill be added to the checkoutUrl for cart query if a customer is logged in. -
Updated dependencies [
faeba9f8,6d585026,fcecfb23,28864d6f,c0ec7714,226cf478,06d9fd91]:- @shopify/cli-hydrogen@7.1.0
- @shopify/hydrogen@2024.1.2
-
♻️
CustomerClienttype is deprecated and replaced byCustomerAccount(#1692) by @michenly -
Updated dependencies [
02798786,52b15df4,a2664362,eee5d927,c7b2017f,06320ee4]:- @shopify/hydrogen@2024.1.1
- @shopify/cli-hydrogen@7.0.1
-
Use new parameters introduced in Storefront API v2024-01 to fix redirection to the product's default variant when there are unknown query params in the URL. (#1642) by @wizardlyhel
- selectedVariant: variantBySelectedOptions(selectedOptions: $selectedOptions) { + selectedVariant: variantBySelectedOptions(selectedOptions: $selectedOptions, ignoreUnknownOptions: true, caseInsensitiveMatch: true) { ...ProductVariant }
-
Update the GraphQL config in
.graphqlrc.ymlto use the more modernprojectsstructure: (#1577) by @frandiox-schema: node_modules/@shopify/hydrogen/storefront.schema.json +projects: + default: + schema: 'node_modules/@shopify/hydrogen/storefront.schema.json'
This allows you to add additional projects to the GraphQL config, such as third party CMS schemas.
Also, you can modify the document paths used for the Storefront API queries. This is useful if you have a large codebase and want to exclude certain files from being used for codegen or other GraphQL utilities:
projects: default: schema: 'node_modules/@shopify/hydrogen/storefront.schema.json' documents: - '!*.d.ts' - '*.{ts,tsx,js,jsx}' - 'app/**/*.{ts,tsx,js,jsx}'
-
Update
@shopify/clidependency inpackage.json: (#1579) by @frandiox- "@shopify/cli": "3.51.0", + "@shopify/cli": "3.52.0",
-
-
Update example and template Remix versions to
^2.5.1(#1639) by @wizardlyhel -
Enable Remix future flags:
-
-
Updated dependencies [
810f48cf,8c477cb5,42ac4138,0241b7d2,6a897586,0ff63bed,6bc1d61c,eb0f4bcc,400bfee6,a69c21ca,970073e7,772118ca,335375a6,335371ce,94509b75,36d6fa2c,3e7b6e8a,cce65795,9e3d88d4,ca1161b2,92840e51,952fedf2,1bc053c9]:- @shopify/hydrogen@2024.1.0
- @shopify/cli-hydrogen@7.0.0
- @shopify/remix-oxygen@2.0.3
-
Sync up environment variable names across all example & type files. (#1542) by @michenly
-
Remove error boundary from robots.txt file in the Skeleton template (#1492) by @andrewcohen
-
Use the worker runtime by default when running the
devorpreviewcommands. (#1525) by @frandioxEnable it in your project by adding the
--workerflag to your package.json scripts:"scripts": { "build": "shopify hydrogen build", - "dev": "shopify hydrogen dev --codegen", + "dev": "shopify hydrogen dev --worker --codegen", - "preview": "npm run build && shopify hydrogen preview", + "preview": "npm run build && shopify hydrogen preview --worker", ... } -
Update to the latest version of
@shopify/oxygen-workers-types. (#1494) by @frandioxIn TypeScript projects, when updating to the latest
@shopify/remix-oxygenadapter release, you should also update to the latest version of@shopify/oxygen-workers-types:"devDependencies": { "@remix-run/dev": "2.1.0", "@remix-run/eslint-config": "2.1.0", - "@shopify/oxygen-workers-types": "^3.17.3", + "@shopify/oxygen-workers-types": "^4.0.0", "@shopify/prettier-config": "^1.1.2", ... }, -
Update internal dependencies for bug resolution. (#1496) by @vincentezw
Update your
@shopify/clidependency to avoid duplicated sub-dependencies:"dependencies": { - "@shopify/cli": "3.50.2", + "@shopify/cli": "3.51.0", } -
Update all Node.js dependencies to version 18. (Not a breaking change, since Node.js 18 is already required by Remix v2.) (#1543) by @michenly
-
Add
@remix-run/server-runtimedependency. (#1489) by @frandioxSince Remix is now a peer dependency of
@shopify/remix-oxygen, you need to add@remix-run/server-runtimeto your dependencies, with the same version as the rest of your Remix dependencies."dependencies": { "@remix-run/react": "2.1.0" + "@remix-run/server-runtime": "2.1.0" ... } -
Updated dependencies [
b2a350a7,9b4f4534,74ea1dba,2be9ce82,a9b8bcde,bca112ed,848c6260,d53b4ed7,961fd8c6,2bff9fc7,c8e8f6fd,8fce70de,f90e4d47,e8cc49fe]:- @shopify/cli-hydrogen@6.1.0
- @shopify/remix-oxygen@2.0.2
- @shopify/hydrogen@2023.10.3
-
The Storefront API 2023-10 now returns menu item URLs that include the
primaryDomainUrl, instead of defaulting to the Shopify store ID URL (example.myshopify.com). The skeleton template requires changes to check for theprimaryDomainUrl: by @blittle- Update the
HeaderMenucomponent to accept aprimaryDomainUrland include it in the internal url check
// app/components/Header.tsx + import type {HeaderQuery} from 'storefrontapi.generated'; export function HeaderMenu({ menu, + primaryDomainUrl, viewport, }: { menu: HeaderProps['header']['menu']; + primaryDomainUrl: HeaderQuery['shop']['primaryDomain']['url']; viewport: Viewport; }) { // ...code // if the url is internal, we strip the domain const url = item.url.includes('myshopify.com') || item.url.includes(publicStoreDomain) || + item.url.includes(primaryDomainUrl) ? new URL(item.url).pathname : item.url; // ...code }
- Update the
FooterMenucomponent to accept aprimaryDomainUrlprop and include it in the internal url check
// app/components/Footer.tsx - import type {FooterQuery} from 'storefrontapi.generated'; + import type {FooterQuery, HeaderQuery} from 'storefrontapi.generated'; function FooterMenu({ menu, + primaryDomainUrl, }: { menu: FooterQuery['menu']; + primaryDomainUrl: HeaderQuery['shop']['primaryDomain']['url']; }) { // code... // if the url is internal, we strip the domain const url = item.url.includes('myshopify.com') || item.url.includes(publicStoreDomain) || + item.url.includes(primaryDomainUrl) ? new URL(item.url).pathname : item.url; // ...code ); }
- Update the
Footercomponent to accept ashopprop
export function Footer({ menu, + shop, }: FooterQuery & {shop: HeaderQuery['shop']}) { return ( <footer className="footer"> - <FooterMenu menu={menu} /> + <FooterMenu menu={menu} primaryDomainUrl={shop.primaryDomain.url} /> </footer> ); }- Update
Layout.tsxto pass theshopprop
export function Layout({ cart, children = null, footer, header, isLoggedIn, }: LayoutProps) { return ( <> <CartAside cart={cart} /> <SearchAside /> <MobileMenuAside menu={header.menu} shop={header.shop} /> <Header header={header} cart={cart} isLoggedIn={isLoggedIn} /> <main>{children}</main> <Suspense> <Await resolve={footer}> - {(footer) => <Footer menu={footer.menu} />} + {(footer) => <Footer menu={footer.menu} shop={header.shop} />} </Await> </Suspense> </> ); } - Update the
-
If you are calling
useMatches()in different places of your app to access the data returned by the root loader, you may want to update it to the following pattern to enhance types: (#1289) by @frandiox// root.tsx import {useMatches} from '@remix-run/react'; import {type SerializeFrom} from '@shopify/remix-oxygen'; export const useRootLoaderData = () => { const [root] = useMatches(); return root?.data as SerializeFrom<typeof loader>; }; export function loader(context) { // ... }
This way, you can import
useRootLoaderData()anywhere in your app and get the correct type for the data returned by the root loader. -
Updated dependencies [
81400439,a6f397b6,3464ec04,7fc088e2,867e0b03,ad45656c,f24e3424,66a48573,0ae7cbe2,8198c1be,ad45656c]:- @shopify/hydrogen@2023.10.0
- @shopify/remix-oxygen@2.0.0
- @shopify/cli-hydrogen@6.0.0