Skip to content

Commit

Permalink
Add channel slug to URLs (#1046)
Browse files Browse the repository at this point in the history
Fixes #1029
  • Loading branch information
typeofweb authored Nov 27, 2023
1 parent dbc8277 commit 5dc219e
Show file tree
Hide file tree
Showing 40 changed files with 211 additions and 129 deletions.
3 changes: 3 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
NEXT_PUBLIC_SALEOR_API_URL=https://storefront1.saleor.cloud/graphql/
# make sure to add it on production for correct canonical URLs
NEXT_PUBLIC_STOREFRONT_URL=http://localhost:3000

# Token used for fetching channels
SALEOR_APP_TOKEN=
File renamed without changes.
File renamed without changes.
File renamed without changes.
Original file line number Diff line number Diff line change
@@ -1,17 +1,16 @@
import { cookies } from "next/headers";
import Image from "next/image";
import Link from "next/link";
import { CheckoutLink } from "./CheckoutLink";
import { DeleteLineButton } from "./DeleteLineButton";
import * as Checkout from "@/lib/checkout";
import { formatMoney, getHrefForVariant } from "@/lib/utils";
import { LinkWithChannel } from "@/ui/atoms/LinkWithChannel";

export const metadata = {
title: "Shopping Cart · Saleor Storefront example",
};

export default async function Page() {
const checkoutId = cookies().get("checkoutId")?.value || "";
export default async function Page({ params }: { params: { channel: string } }) {
const checkoutId = Checkout.getIdFromCookies(params.channel);

const checkout = await Checkout.find(checkoutId);

Expand All @@ -22,12 +21,12 @@ export default async function Page() {
<p className="my-12 text-sm text-neutral-500">
Looks like you haven’t added any items to the cart yet.
</p>
<Link
<LinkWithChannel
href="/products"
className="inline-block max-w-full rounded border border-transparent bg-neutral-900 px-6 py-3 text-center font-medium text-neutral-50 hover:bg-neutral-800 aria-disabled:cursor-not-allowed aria-disabled:bg-neutral-500 sm:px-16"
>
Explore products
</Link>
</LinkWithChannel>
</section>
);
}
Expand Down Expand Up @@ -57,14 +56,14 @@ export default async function Page() {
<div className="relative flex flex-1 flex-col justify-between p-4 py-2">
<div className="flex justify-between justify-items-start gap-4">
<div>
<Link
<LinkWithChannel
href={getHrefForVariant({
productSlug: item.variant.product.slug,
variantId: item.variant.id,
})}
>
<h2 className="font-medium text-neutral-700">{item.variant?.product?.name}</h2>
</Link>
</LinkWithChannel>
<p className="mt-1 text-sm text-neutral-500">{item.variant?.product?.category?.name}</p>
{item.variant.name !== item.variant.id && Boolean(item.variant.name) && (
<p className="mt-1 text-sm text-neutral-500">Variant: {item.variant.name}</p>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,13 @@ import { type ResolvingMetadata, type Metadata } from "next";
import { ProductListByCategoryDocument } from "@/gql/graphql";
import { executeGraphQL } from "@/lib/graphql";
import { ProductList } from "@/ui/components/ProductList";
import { DEFAULT_CHANNEL } from "@/checkout/lib/regions";

export const generateMetadata = async (
{ params }: { params: { slug: string } },
{ params }: { params: { slug: string; channel: string } },
parent: ResolvingMetadata,
): Promise<Metadata> => {
const { category } = await executeGraphQL(ProductListByCategoryDocument, {
variables: { slug: params.slug, channel: DEFAULT_CHANNEL },
variables: { slug: params.slug, channel: params.channel },
revalidate: 60,
});

Expand All @@ -20,9 +19,9 @@ export const generateMetadata = async (
};
};

export default async function Page({ params }: { params: { slug: string } }) {
export default async function Page({ params }: { params: { slug: string; channel: string } }) {
const { category } = await executeGraphQL(ProductListByCategoryDocument, {
variables: { slug: params.slug, channel: DEFAULT_CHANNEL },
variables: { slug: params.slug, channel: params.channel },
revalidate: 60,
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,13 @@ import { type ResolvingMetadata, type Metadata } from "next";
import { ProductListByCollectionDocument } from "@/gql/graphql";
import { executeGraphQL } from "@/lib/graphql";
import { ProductList } from "@/ui/components/ProductList";
import { DEFAULT_CHANNEL } from "@/checkout/lib/regions";

export const generateMetadata = async (
{ params }: { params: { slug: string } },
{ params }: { params: { slug: string; channel: string } },
parent: ResolvingMetadata,
): Promise<Metadata> => {
const { collection } = await executeGraphQL(ProductListByCollectionDocument, {
variables: { slug: params.slug, channel: DEFAULT_CHANNEL },
variables: { slug: params.slug, channel: params.channel },
revalidate: 60,
});

Expand All @@ -21,9 +20,9 @@ export const generateMetadata = async (
};
};

export default async function Page({ params }: { params: { slug: string } }) {
export default async function Page({ params }: { params: { slug: string; channel: string } }) {
const { collection } = await executeGraphQL(ProductListByCollectionDocument, {
variables: { slug: params.slug, channel: DEFAULT_CHANNEL },
variables: { slug: params.slug, channel: params.channel },
revalidate: 60,
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,13 @@ export const metadata = {
description: "Starter pack for building performant e-commerce experiences with Saleor.",
};

export default function RootLayout(props: { children: ReactNode }) {
export default function RootLayout(props: { children: ReactNode; params: { channel: string } }) {
return (
<>
<Header />
<Header channel={props.params.channel} />
<div className="flex min-h-[calc(100dvh-64px)] flex-col">
<main className="flex-1">{props.children}</main>
<Footer />
<Footer channel={props.params.channel} />
</div>
</>
);
Expand Down
File renamed without changes.
File renamed without changes.
5 changes: 2 additions & 3 deletions src/app/(main)/page.tsx → src/app/[channel]/(main)/page.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { DEFAULT_CHANNEL } from "@/checkout/lib/regions";
import { ProductListByCollectionDocument } from "@/gql/graphql";
import { executeGraphQL } from "@/lib/graphql";
import { ProductList } from "@/ui/components/ProductList";
Expand All @@ -9,11 +8,11 @@ export const metadata = {
"Storefront Next.js Example for building performant e-commerce experiences with Saleor - the composable, headless commerce platform for global brands.",
};

export default async function Page() {
export default async function Page({ params }: { params: { channel: string } }) {
const data = await executeGraphQL(ProductListByCollectionDocument, {
variables: {
slug: "featured-products",
channel: DEFAULT_CHANNEL,
channel: params.channel,
},
revalidate: 60,
});
Expand Down
File renamed without changes.
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import edjsHTML from "editorjs-html";
import { revalidatePath } from "next/cache";
import { cookies } from "next/headers";
import { notFound } from "next/navigation";
import { type ResolvingMetadata, type Metadata } from "next";
import xss from "xss";
Expand All @@ -14,25 +13,21 @@ import { formatMoney, formatMoneyRange } from "@/lib/utils";
import { CheckoutAddLineDocument, ProductDetailsDocument, ProductListDocument } from "@/gql/graphql";
import * as Checkout from "@/lib/checkout";
import { AvailabilityMessage } from "@/ui/components/AvailabilityMessage";
import { DEFAULT_CHANNEL } from "@/checkout/lib/regions";

const shouldUseHttps =
process.env.NEXT_PUBLIC_STOREFRONT_URL?.startsWith("https") || !!process.env.NEXT_PUBLIC_VERCEL_URL;

export async function generateMetadata(
{
params,
searchParams,
}: {
params: { slug: string };
params: { slug: string; channel: string };
searchParams: { variant?: string };
},
parent: ResolvingMetadata,
): Promise<Metadata> {
const { product } = await executeGraphQL(ProductDetailsDocument, {
variables: {
slug: decodeURIComponent(params.slug),
channel: DEFAULT_CHANNEL,
channel: params.channel,
},
revalidate: 60,
});
Expand Down Expand Up @@ -66,13 +61,10 @@ export async function generateMetadata(
};
}

// TODO: re-enable when the bug in Next.js is fixed
// https://github.com/vercel/next.js/issues/50658

export async function generateStaticParams() {
export async function generateStaticParams({ params }: { params: { channel: string } }) {
const { products } = await executeGraphQL(ProductListDocument, {
revalidate: 60,
variables: { first: 20, channel: DEFAULT_CHANNEL },
variables: { first: 20, channel: params.channel },
withAuth: false,
});

Expand All @@ -82,13 +74,17 @@ export async function generateStaticParams() {

const parser = edjsHTML();

export default async function Page(props: { params: { slug: string }; searchParams: { variant?: string } }) {
const { params, searchParams } = props;

export default async function Page({
params,
searchParams,
}: {
params: { slug: string; channel: string };
searchParams: { variant?: string };
}) {
const { product } = await executeGraphQL(ProductDetailsDocument, {
variables: {
slug: decodeURIComponent(params.slug),
channel: DEFAULT_CHANNEL,
channel: params.channel,
},
revalidate: 60,
});
Expand All @@ -107,14 +103,13 @@ export default async function Page(props: { params: { slug: string }; searchPara
async function addItem() {
"use server";

const checkout = await Checkout.findOrCreate(cookies().get("checkoutId")?.value);
const checkout = await Checkout.findOrCreate({
checkoutId: Checkout.getIdFromCookies(params.channel),
channel: params.channel,
});
invariant(checkout, "This should never happen");

cookies().set("checkoutId", checkout.id, {
secure: shouldUseHttps,
sameSite: "lax",
httpOnly: true,
});
Checkout.saveIdToCookie(params.channel, checkout.id);

if (!selectedVariantID) {
return;
Expand Down Expand Up @@ -204,7 +199,12 @@ export default async function Page(props: { params: { slug: string }; searchPara
</p>

{variants && (
<VariantSelector selectedVariant={selectedVariant} variants={variants} product={product} />
<VariantSelector
selectedVariant={selectedVariant}
variants={variants}
product={product}
channel={params.channel}
/>
)}
{description && (
<div className="mt-8 space-y-6">
Expand Down
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -3,28 +3,29 @@ import { ProductListPaginatedDocument } from "@/gql/graphql";
import { executeGraphQL } from "@/lib/graphql";
import { Pagination } from "@/ui/components/Pagination";
import { ProductList } from "@/ui/components/ProductList";
import { DEFAULT_CHANNEL } from "@/checkout/lib/regions";
import { ProductsPerPage } from "@/app/config";

export const metadata = {
title: "Products · Saleor Storefront example",
description: "All products in Saleor Storefront example",
};

type Props = {
export default async function Page({
params,
searchParams,
}: {
params: { channel: string };
searchParams: {
cursor: string | string[] | undefined;
};
};

export default async function Page({ searchParams }: Props) {
}) {
const cursor = typeof searchParams.cursor === "string" ? searchParams.cursor : null;

const { products } = await executeGraphQL(ProductListPaginatedDocument, {
variables: {
first: ProductsPerPage,
after: cursor,
channel: DEFAULT_CHANNEL,
channel: params.channel,
},
revalidate: 60,
});
Expand All @@ -44,7 +45,7 @@ export default async function Page({ searchParams }: Props) {
<Pagination
pageInfo={{
...products.pageInfo,
basePathname: "/products",
basePathname: `/products`,
urlSearchParams: newSearchParams,
}}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,20 @@ import { OrderDirection, ProductOrderField, SearchProductsDocument } from "@/gql
import { executeGraphQL } from "@/lib/graphql";
import { Pagination } from "@/ui/components/Pagination";
import { ProductList } from "@/ui/components/ProductList";
import { DEFAULT_CHANNEL } from "@/checkout/lib/regions";
import { ProductsPerPage } from "@/app/config";

export const metadata = {
title: "Search products · Saleor Storefront example",
description: "Search products in Saleor Storefront example",
};

type Props = {
export default async function Page({
searchParams,
params,
}: {
searchParams: Record<"query" | "cursor", string | string[] | undefined>;
};

export default async function Page({ searchParams }: Props) {
params: { channel: string };
}) {
const cursor = typeof searchParams.cursor === "string" ? searchParams.cursor : null;
const searchValue = searchParams.query;

Expand All @@ -38,7 +39,7 @@ export default async function Page({ searchParams }: Props) {
after: cursor,
sortBy: ProductOrderField.Rating,
sortDirection: OrderDirection.Asc,
channel: DEFAULT_CHANNEL,
channel: params.channel,
},
revalidate: 60,
});
Expand All @@ -61,7 +62,7 @@ export default async function Page({ searchParams }: Props) {
<Pagination
pageInfo={{
...products.pageInfo,
basePathname: "/search",
basePathname: `/search`,
urlSearchParams: newSearchParams,
}}
/>
Expand Down
29 changes: 29 additions & 0 deletions src/app/[channel]/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { type ReactNode } from "react";
import { executeGraphQL } from "@/lib/graphql";
import { ChannelsListDocument } from "@/gql/graphql";

export const generateStaticParams = async () => {
// the `channels` query is protected
// you can either hardcode the channels or use an app token to fetch the channel list here

if (process.env.SALEOR_APP_TOKEN) {
const channels = await executeGraphQL(ChannelsListDocument, {
withAuth: false, // disable cookie-based auth for this call
headers: {
// and use app token instead
Authorization: `Bearer ${process.env.SALEOR_APP_TOKEN}`,
},
});
return (
channels.channels
?.filter((channel) => channel.isActive)
.map((channel) => ({ channel: channel.slug })) ?? []
);
} else {
return [{ channel: "default-channel" }];
}
};

export default function ChannelLayout({ children }: { children: ReactNode }) {
return children;
}
5 changes: 5 additions & 0 deletions src/app/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { redirect } from "next/navigation";

export default function EmptyPage() {
redirect("/default-channel");
}
2 changes: 0 additions & 2 deletions src/checkout/lib/regions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,4 @@ export const locales = ["en-US"] as const;

export const DEFAULT_LOCALE = "en-US";

export const DEFAULT_CHANNEL = "default-channel";

export type Locale = (typeof locales)[number];
Loading

0 comments on commit 5dc219e

Please sign in to comment.