diff --git a/app/[page]/opengraph-image.tsx b/app/[page]/opengraph-image.tsx
index 5f11d802b3..3959388504 100644
--- a/app/[page]/opengraph-image.tsx
+++ b/app/[page]/opengraph-image.tsx
@@ -3,7 +3,7 @@ import { getPage } from "lib/shopify";
export default async function Image({ params }: { params: { page: string } }) {
const page = await getPage(params.page);
- const title = page.seo?.title || page.title;
+ const title = page?.seo?.title || page?.title;
return await OpengraphImage({ title });
}
diff --git a/app/sitemap.ts b/app/sitemap.ts
index b1c199ead6..9e52bb111e 100644
--- a/app/sitemap.ts
+++ b/app/sitemap.ts
@@ -1,5 +1,5 @@
import { getCollections, getPages, getProducts } from "lib/shopify";
-import { baseUrl, validateEnvironmentVariables } from "lib/utils";
+import { baseUrl } from "lib/utils";
import { MetadataRoute } from "next";
type Route = {
@@ -10,8 +10,6 @@ type Route = {
export const dynamic = "force-dynamic";
export default async function sitemap(): Promise {
- validateEnvironmentVariables();
-
const routesMap = [""].map((route) => ({
url: `${baseUrl}${route}`,
lastModified: new Date().toISOString(),
@@ -38,15 +36,9 @@ export default async function sitemap(): Promise {
})),
);
- let fetchedRoutes: Route[] = [];
-
- try {
- fetchedRoutes = (
- await Promise.all([collectionsPromise, productsPromise, pagesPromise])
- ).flat();
- } catch (error) {
- throw JSON.stringify(error, null, 2);
- }
+ const fetchedRoutes: Route[] = (
+ await Promise.all([collectionsPromise, productsPromise, pagesPromise])
+ ).flat();
return [...routesMap, ...fetchedRoutes];
}
diff --git a/components/cart/actions.ts b/components/cart/actions.ts
index c257ac9ecc..5a03af8829 100644
--- a/components/cart/actions.ts
+++ b/components/cart/actions.ts
@@ -14,7 +14,7 @@ import { redirect } from "next/navigation";
export async function addItem(
prevState: any,
- selectedVariantId: string | undefined
+ selectedVariantId: string | undefined,
) {
if (!selectedVariantId) {
return "Error adding item to cart";
@@ -37,7 +37,7 @@ export async function removeItem(prevState: any, merchandiseId: string) {
}
const lineItem = cart.lines.find(
- (line) => line.merchandise.id === merchandiseId
+ (line) => line.merchandise.id === merchandiseId,
);
if (lineItem && lineItem.id) {
@@ -56,7 +56,7 @@ export async function updateItemQuantity(
payload: {
merchandiseId: string;
quantity: number;
- }
+ },
) {
const { merchandiseId, quantity } = payload;
@@ -68,7 +68,7 @@ export async function updateItemQuantity(
}
const lineItem = cart.lines.find(
- (line) => line.merchandise.id === merchandiseId
+ (line) => line.merchandise.id === merchandiseId,
);
if (lineItem && lineItem.id) {
@@ -96,11 +96,25 @@ export async function updateItemQuantity(
}
export async function redirectToCheckout() {
- let cart = await getCart();
- redirect(cart!.checkoutUrl);
+ const cart = await getCart();
+
+ if (!cart?.checkoutUrl) {
+ return;
+ }
+
+ redirect(cart.checkoutUrl);
}
export async function createCartAndSetCookie() {
- let cart = await createCart();
- (await cookies()).set("cartId", cart.id!);
+ try {
+ const cart = await createCart();
+
+ if (!cart.id) {
+ return;
+ }
+
+ (await cookies()).set("cartId", cart.id);
+ } catch (error) {
+ console.error(error);
+ }
}
diff --git a/components/layout/footer.tsx b/components/layout/footer.tsx
index 9f0025df64..9f8bdedb7c 100644
--- a/components/layout/footer.tsx
+++ b/components/layout/footer.tsx
@@ -1,9 +1,9 @@
import Link from "next/link";
+import { Suspense } from "react";
import FooterMenu from "components/layout/footer-menu";
import LogoSquare from "components/logo-square";
import { getMenu } from "lib/shopify";
-import { Suspense } from "react";
const { COMPANY_NAME, SITE_NAME } = process.env;
@@ -47,7 +47,7 @@ export default async function Footer() {
aria-label="Deploy on Vercel"
href="https://vercel.com/templates/next.js/nextjs-commerce"
>
- ▲
+ ▲
Deploy
@@ -68,7 +68,7 @@ export default async function Footer() {
- Created by ▲ Vercel
+ Created by ▲ Vercel
diff --git a/components/welcome-toast.tsx b/components/welcome-toast.tsx
index 414055db9c..c2c4f620ad 100644
--- a/components/welcome-toast.tsx
+++ b/components/welcome-toast.tsx
@@ -5,10 +5,11 @@ import { toast } from "sonner";
export function WelcomeToast() {
useEffect(() => {
- // ignore if screen height is too small
+ // Ignore if screen height is too small.
if (window.innerHeight < 650) return;
+
if (!document.cookie.includes("welcome-toast=2")) {
- toast("🛍️ Welcome to Next.js Commerce!", {
+ toast("\u{1F6CD}\uFE0F Welcome to Next.js Commerce!", {
id: "welcome-toast",
duration: Infinity,
onDismiss: () => {
@@ -22,6 +23,7 @@ export function WelcomeToast() {
href="https://vercel.com/templates/next.js/nextjs-commerce"
className="text-blue-600 hover:underline"
target="_blank"
+ rel="noreferrer"
>
Deploy your own
diff --git a/lib/shopify/index.ts b/lib/shopify/index.ts
index ccd48e32ff..d5e586bb65 100644
--- a/lib/shopify/index.ts
+++ b/lib/shopify/index.ts
@@ -62,12 +62,50 @@ const domain = process.env.SHOPIFY_STORE_DOMAIN
? ensureStartsWith(process.env.SHOPIFY_STORE_DOMAIN, "https://")
: "";
const endpoint = domain ? `${domain}${SHOPIFY_GRAPHQL_API_ENDPOINT}` : "";
-const key = process.env.SHOPIFY_STOREFRONT_ACCESS_TOKEN!;
+const key = process.env.SHOPIFY_STOREFRONT_ACCESS_TOKEN || "";
type ExtractVariables = T extends { variables: object }
? T["variables"]
: never;
+function isShopifyConfigured() {
+ return Boolean(endpoint && key);
+}
+
+function getShopifyErrorMessage(error: unknown) {
+ if (typeof error === "string") {
+ return error;
+ }
+
+ if (error && typeof error === "object" && "message" in error) {
+ return String(error.message);
+ }
+
+ return "Unknown Shopify error";
+}
+
+function logShopifyReadFailure(operation: string, error: unknown) {
+ console.warn(
+ `[shopify] ${operation} failed: ${getShopifyErrorMessage(error)}`,
+ );
+}
+
+function getDefaultCollections(): Collection[] {
+ return [
+ {
+ handle: "",
+ title: "All",
+ description: "All products",
+ seo: {
+ title: "All",
+ description: "All products",
+ },
+ path: "/search",
+ updatedAt: new Date().toISOString(),
+ },
+ ];
+}
+
export async function shopifyFetch({
headers,
query,
@@ -141,7 +179,7 @@ const reshapeCart = (cart: ShopifyCart): Cart => {
};
const reshapeCollection = (
- collection: ShopifyCollection
+ collection: ShopifyCollection,
): Collection | undefined => {
if (!collection) {
return undefined;
@@ -183,7 +221,7 @@ const reshapeImages = (images: Connection, productTitle: string) => {
const reshapeProduct = (
product: ShopifyProduct,
- filterHiddenProducts: boolean = true
+ filterHiddenProducts: boolean = true,
) => {
if (
!product ||
@@ -226,7 +264,7 @@ export async function createCart(): Promise {
}
export async function addToCart(
- lines: { merchandiseId: string; quantity: number }[]
+ lines: { merchandiseId: string; quantity: number }[],
): Promise {
const cartId = (await cookies()).get("cartId")?.value!;
const res = await shopifyFetch({
@@ -253,7 +291,7 @@ export async function removeFromCart(lineIds: string[]): Promise {
}
export async function updateCart(
- lines: { id: string; merchandiseId: string; quantity: number }[]
+ lines: { id: string; merchandiseId: string; quantity: number }[],
): Promise {
const cartId = (await cookies()).get("cartId")?.value!;
const res = await shopifyFetch({
@@ -278,34 +316,48 @@ export async function getCart(): Promise {
return undefined;
}
- const res = await shopifyFetch({
- query: getCartQuery,
- variables: { cartId },
- });
+ try {
+ const res = await shopifyFetch({
+ query: getCartQuery,
+ variables: { cartId },
+ });
+
+ // Old carts becomes `null` when you checkout.
+ if (!res.body.data.cart) {
+ return undefined;
+ }
- // Old carts becomes `null` when you checkout.
- if (!res.body.data.cart) {
+ return reshapeCart(res.body.data.cart);
+ } catch (error) {
+ logShopifyReadFailure("getCart", error);
return undefined;
}
-
- return reshapeCart(res.body.data.cart);
}
export async function getCollection(
- handle: string
+ handle: string,
): Promise {
"use cache";
cacheTag(TAGS.collections);
cacheLife("days");
- const res = await shopifyFetch({
- query: getCollectionQuery,
- variables: {
- handle,
- },
- });
+ if (!isShopifyConfigured()) {
+ return undefined;
+ }
- return reshapeCollection(res.body.data.collection);
+ try {
+ const res = await shopifyFetch({
+ query: getCollectionQuery,
+ variables: {
+ handle,
+ },
+ });
+
+ return reshapeCollection(res.body.data.collection);
+ } catch (error) {
+ logShopifyReadFailure(`getCollection(${handle})`, error);
+ return undefined;
+ }
}
export async function getCollectionProducts({
@@ -321,30 +373,31 @@ export async function getCollectionProducts({
cacheTag(TAGS.collections, TAGS.products);
cacheLife("days");
- if (!endpoint) {
- console.log(
- `Skipping getCollectionProducts for '${collection}' - Shopify not configured`
- );
+ if (!isShopifyConfigured()) {
return [];
}
- const res = await shopifyFetch({
- query: getCollectionProductsQuery,
- variables: {
- handle: collection,
- reverse,
- sortKey: sortKey === "CREATED_AT" ? "CREATED" : sortKey,
- },
- });
+ try {
+ const res = await shopifyFetch({
+ query: getCollectionProductsQuery,
+ variables: {
+ handle: collection,
+ reverse,
+ sortKey: sortKey === "CREATED_AT" ? "CREATED" : sortKey,
+ },
+ });
+
+ if (!res.body.data.collection) {
+ return [];
+ }
- if (!res.body.data.collection) {
- console.log(`No collection found for \`${collection}\``);
+ return reshapeProducts(
+ removeEdgesAndNodes(res.body.data.collection.products),
+ );
+ } catch (error) {
+ logShopifyReadFailure(`getCollectionProducts(${collection})`, error);
return [];
}
-
- return reshapeProducts(
- removeEdgesAndNodes(res.body.data.collection.products)
- );
}
export async function getCollections(): Promise {
@@ -352,47 +405,29 @@ export async function getCollections(): Promise {
cacheTag(TAGS.collections);
cacheLife("days");
- if (!endpoint) {
- console.log("Skipping getCollections - Shopify not configured");
- return [
- {
- handle: "",
- title: "All",
- description: "All products",
- seo: {
- title: "All",
- description: "All products",
- },
- path: "/search",
- updatedAt: new Date().toISOString(),
- },
- ];
+ if (!isShopifyConfigured()) {
+ return getDefaultCollections();
}
- const res = await shopifyFetch({
- query: getCollectionsQuery,
- });
- const shopifyCollections = removeEdgesAndNodes(res.body?.data?.collections);
- const collections = [
- {
- handle: "",
- title: "All",
- description: "All products",
- seo: {
- title: "All",
- description: "All products",
- },
- path: "/search",
- updatedAt: new Date().toISOString(),
- },
- // Filter out the `hidden` collections.
- // Collections that start with `hidden-*` need to be hidden on the search page.
- ...reshapeCollections(shopifyCollections).filter(
- (collection) => !collection.handle.startsWith("hidden")
- ),
- ];
+ try {
+ const res = await shopifyFetch({
+ query: getCollectionsQuery,
+ });
+ const shopifyCollections = removeEdgesAndNodes(res.body?.data?.collections);
+ const collections = [
+ ...getDefaultCollections(),
+ // Filter out the `hidden` collections.
+ // Collections that start with `hidden-*` need to be hidden on the search page.
+ ...reshapeCollections(shopifyCollections).filter(
+ (collection) => !collection.handle.startsWith("hidden"),
+ ),
+ ];
- return collections;
+ return collections;
+ } catch (error) {
+ logShopifyReadFailure("getCollections", error);
+ return getDefaultCollections();
+ }
}
export async function getMenu(handle: string): Promise