diff --git a/packages/nextjs/app/about/page.tsx b/packages/nextjs/app/about/page.tsx
index 6b0b1666..1a8d81b8 100644
--- a/packages/nextjs/app/about/page.tsx
+++ b/packages/nextjs/app/about/page.tsx
@@ -1,5 +1,294 @@
-import AboutPage from "app/migration/about";
+import { Metadata } from "next";
+
+import Link from "next/link";
+import { BreadIcon } from "../ui/shared-ui";
+import { Breadcrumbs } from "app/components/Breadcrumbs";
+import LocalImage from "app/components/LocalImage";
+import bigPictureImage from "../../../../docs/img/big-picture.png";
+import cacheDiagramImage from "../../../../docs/img/caching-diagram.png";
+
+const DIAGRAM_WIDTH = 700.0;
+const DIAGRAM_HEIGHT = DIAGRAM_WIDTH / 1.86;
+const FASTLY_WIDTH = 700.0;
+const FASTLY_HEIGHT = FASTLY_WIDTH / 2.93;
+
+export const metadata: Metadata = {
+ title: "About",
+ description: "About the Formidable Boulangerie project.",
+};
export default async function Page() {
- return ;
+ return (
+ <>
+
+
+
+
+
+
Learn more about the Formidable Ecommerce demo site
+
+ We don’t really sell bread. The goal of the project is to provide a realistic demonstration of running a
+ highly performant and available e-commerce site with data sourced from Sanity’s headless CMS. The app is
+ powered by
+
+ Next.js
+
+ ,
+
+ Sanity CMS
+
+ , and
+
+ Fastly
+
+ .
+
+
+
+
+
+
+
+
Headless CMS-driven architecture.
+
+ The e-commerce data is stored in a headless CMS (powered by Sanity). The project uses Next.js (deployed on
+ Vercel) to render the site, and Fastly is placed in front of Vercel to cache server-rendered webpages for
+ speed and availability.
+
+
+
+
+
+
+
+
Sanity CMS
+ Sanity is used for storing information about our e-commerce products. The data from Sanity is fetched
+ using
+
+ GROQ
+
+ – a query language, used for fetching data. Formidable built
+
+ Groqd
+
+ – a schema-unaware, runtime and type-safe query builder for GROQ.
+
+
+
+
+
+
+
+
Sanity Studio
+ Sanity Studio is a web interface for Sanity’s headless CMS. It is used for creating and editing the
+ data on the site. The models for Sanity are created in code and tracked in source control. Sanity
+ Studio is integrated into the NextJS application and deployed alongside{" "}
+
+ as a route
+
+ .
+
+
+
+
+
+
+
+
NextJS app
+ To show the CMS data to end-users we created a Next.js web app that server-renders some common
+ e-commerce pages, including a landing page, a Product Listing Page (PLP) with sorting and filtering,
+ and a Product Details Page (PDP). The Next.js app is deployed to{" "}
+
+ Vercel
+
+ via their git pipeline. In a real-world e-commerce app, we expect to experience some heavy loads
+ on pages whose data doesn’t change much between visits, and therefore we can deploy caching strategies
+ to reduce the load on our source server.
+
+
+
+
+
+
The caching story.
+
+
+
+
+
+
+
Fastly CDN and Caching
+
+ In order to enhance the speed of the app, we are utilizing Fastly’s CDN with a high cache-lifetime
+ for server-rendered pages. We are using Fastly to both cache and host the subdomain used for this
+ showcase app. The data flow involved in caching is illustrated below:
+
+
+
+
+
+ To cache our server-rendered pages at the Fastly layer, we use response headers to indicate
+ what/how we want Fastly to cache our responses from the source server. We need to a couple key
+ ingredients:
+
+
+
+
+
+
+ Surrogate-Control response header needs to be added to pages where caching is
+ desired.
+
+ Learn more.
+
+
+
+
+
+
+
+
+ Surrogate-Key response header needs to be added to enable appropriate cache
+ invalidation.
+
+ Learn more.
+
+
+
+
+
+
+
+
+ On the Next.js side we’ll need to include a few primary response headers to then control caching (in
+ our case, we’re setting these headers from middleware on server-rendered pages that
+ we’d like to cache).
+
+
+
+
+
+
+ surrogate-control Fastly-specific header used to set the cache policies.
+
+
+
+
+
+
+
+ surrogate-key Fastly-specific header that allows purging by key. Note: this
+ header is removed by Fastly before sending the response to the client. To see the value of
+ this header, you must include the
+
+ Fastly-Debug
+
+ header in your request.
+
+
+
+
+
+
+
+ cache-control used to indicate to browsers and Vercel to not cache so that we can
+ handle caching solely at the Fastly layer.
+
+
+
+
+
+ With these response headers implemented, Fastly will start caching our responses and give us a path
+ to invalidate our cache when necessary. In our case, we use data items’ slugs as part
+ of our surrogate-key header to indicate what items’ data are used to render a
+ page so that we can invalidate accordingly when any of those items’ data changes.
+
+
+
+
+
+
+
+
+
Cache Invalidation and Purging
+ When CMS data changes, a Sanity webhook is triggered and makes a request to an API endpoint in our
+ Next.js app. The endpoint does some validation on the request (to make sure it’s coming from a trusted
+ Sanity webhook), and then makes a request to Fastly’s API to invalidate/purge our cache accordingly.
+ The Sanity webhook payload contains information (in our case, an item’s
+
+ slug
+
+ about what data changes, and our API endpoint uses that slug to tell Fastly which
+ cache data to invalidate (based on the surrogate-key set in the original response
+ header).
+
+
+
+
+
+
+ >
+ );
}
diff --git a/packages/nextjs/app/categories/page.tsx b/packages/nextjs/app/categories/page.tsx
index 78b9ab2a..851282cc 100644
--- a/packages/nextjs/app/categories/page.tsx
+++ b/packages/nextjs/app/categories/page.tsx
@@ -1,4 +1,7 @@
-import CategoriesPage from "app/migration/categories";
+import { Breadcrumbs } from "app/components/Breadcrumbs";
+import { CategoryList } from "app/components/CategoryList";
+import { Metadata } from "next";
+import { WeDontSellBreadBanner } from "../ui/shared-ui";
import { getAllCategories } from "utils/getAllCategoriesQuery";
import { isString, pluralize } from "utils/pluralize";
@@ -12,8 +15,28 @@ const getData = async () => {
};
};
+export async function generateMetadata(): Promise {
+ const data = await getData();
+
+ return {
+ title: "Categories",
+ description: `Product categories, including ${data.categoryNames}.`,
+ };
+}
+
export default async function Page() {
const data = await getData();
- return ;
+ return (
+
+
+
+
Categories
+
+
+
+
+
+
+ );
}
diff --git a/packages/nextjs/app/components/AnimatedProductDetail.tsx b/packages/nextjs/app/components/AnimatedProductDetail.tsx
new file mode 100644
index 00000000..58768e25
--- /dev/null
+++ b/packages/nextjs/app/components/AnimatedProductDetail.tsx
@@ -0,0 +1,21 @@
+"use client";
+
+import { AnimatePresence } from "framer-motion";
+import { useSearchParams } from "next/navigation";
+import React from "react";
+import { FadeInOut } from "shared-ui";
+
+const AnimatedProductDetail = ({ children }: React.PropsWithChildren) => {
+ const query = useSearchParams();
+ const variant = query?.get("variant");
+
+ return (
+
+
+ {children}
+
+
+ );
+};
+
+export default AnimatedProductDetail;
diff --git a/packages/nextjs/components/Breadcrumbs.tsx b/packages/nextjs/app/components/Breadcrumbs.tsx
similarity index 99%
rename from packages/nextjs/components/Breadcrumbs.tsx
rename to packages/nextjs/app/components/Breadcrumbs.tsx
index d7f666dd..d8854a41 100644
--- a/packages/nextjs/components/Breadcrumbs.tsx
+++ b/packages/nextjs/app/components/Breadcrumbs.tsx
@@ -1,3 +1,5 @@
+"use client";
+
import React from "react";
import { MdOutlineHome } from "react-icons/md";
import { BreadcrumbItem, BreadcrumbsContainer, capitalizeWords } from "shared-ui";
diff --git a/packages/nextjs/components/Card.tsx b/packages/nextjs/app/components/Card.tsx
similarity index 95%
rename from packages/nextjs/components/Card.tsx
rename to packages/nextjs/app/components/Card.tsx
index 6fc27c34..5ca246c4 100644
--- a/packages/nextjs/components/Card.tsx
+++ b/packages/nextjs/app/components/Card.tsx
@@ -2,7 +2,7 @@ import * as React from "react";
import { SanityImageSource } from "@sanity/image-url/lib/types/types";
import Link, { LinkProps } from "next/link";
import { Image } from "./Image";
-import { Card as BaseCard } from "shared-ui";
+import { Card as BaseCard } from "../ui/shared-ui";
export interface CardProps {
title: string;
diff --git a/packages/nextjs/components/CartContext.tsx b/packages/nextjs/app/components/CartContext.tsx
similarity index 100%
rename from packages/nextjs/components/CartContext.tsx
rename to packages/nextjs/app/components/CartContext.tsx
diff --git a/packages/nextjs/components/CategoryList.tsx b/packages/nextjs/app/components/CategoryList.tsx
similarity index 96%
rename from packages/nextjs/components/CategoryList.tsx
rename to packages/nextjs/app/components/CategoryList.tsx
index a86c4ad6..eaf717bf 100644
--- a/packages/nextjs/components/CategoryList.tsx
+++ b/packages/nextjs/app/components/CategoryList.tsx
@@ -1,5 +1,5 @@
import type { GetCategoriesQuery, GetProductsAndCategoriesQuery } from "utils/groqTypes/ProductList";
-import { Card } from "components/Card";
+import { Card } from "app/components/Card";
type CategoryListProps = {
items?: GetCategoriesQuery["categories"] | GetProductsAndCategoriesQuery["categories"];
diff --git a/packages/nextjs/components/FeaturedList.tsx b/packages/nextjs/app/components/FeaturedList.tsx
similarity index 97%
rename from packages/nextjs/components/FeaturedList.tsx
rename to packages/nextjs/app/components/FeaturedList.tsx
index 7ad8853b..c5d2fd24 100644
--- a/packages/nextjs/components/FeaturedList.tsx
+++ b/packages/nextjs/app/components/FeaturedList.tsx
@@ -1,7 +1,7 @@
import type { Categories, Products } from "utils/groqTypes/ProductList";
import * as React from "react";
import classNames from "classnames";
-import { Card, CardProps } from "components/Card";
+import { Card, CardProps } from "app/components/Card";
type Props = {
items?: Products | Categories;
diff --git a/packages/nextjs/components/Header/Header.tsx b/packages/nextjs/app/components/Header/Header.tsx
similarity index 97%
rename from packages/nextjs/components/Header/Header.tsx
rename to packages/nextjs/app/components/Header/Header.tsx
index 49a03b71..fc40390e 100644
--- a/packages/nextjs/components/Header/Header.tsx
+++ b/packages/nextjs/app/components/Header/Header.tsx
@@ -3,7 +3,7 @@
import * as React from "react";
import Link from "next/link";
import { Button, useCart, Header as BaseHeader, useMobileNav } from "shared-ui";
-import { Search } from "components/Search";
+import { Search } from "app/components/Search";
import { usePathname } from "next/navigation";
import { NAV_ITEMS } from "shared-ui";
import { DesktopNavItem } from "shared-ui";
diff --git a/packages/nextjs/components/Image.tsx b/packages/nextjs/app/components/Image.tsx
similarity index 98%
rename from packages/nextjs/components/Image.tsx
rename to packages/nextjs/app/components/Image.tsx
index 0fcb1b42..034ab10f 100644
--- a/packages/nextjs/components/Image.tsx
+++ b/packages/nextjs/app/components/Image.tsx
@@ -1,3 +1,5 @@
+"use client";
+
import type { ImageProps, ImageLoaderProps } from "next/image";
import * as React from "react";
import { SanityImageSource } from "@sanity/image-url/lib/types/types";
diff --git a/packages/nextjs/components/ImageCarousel.tsx b/packages/nextjs/app/components/ImageCarousel.tsx
similarity index 85%
rename from packages/nextjs/components/ImageCarousel.tsx
rename to packages/nextjs/app/components/ImageCarousel.tsx
index 7b746fa2..b8117e5f 100644
--- a/packages/nextjs/components/ImageCarousel.tsx
+++ b/packages/nextjs/app/components/ImageCarousel.tsx
@@ -1,7 +1,7 @@
import { ProductImage } from "utils/groqTypes/ProductList";
import * as React from "react";
-import { Image } from "components/Image";
-import { ImageCarousel as BaseImageCarousel } from "shared-ui";
+import { Image } from "app/components/Image";
+import { ImageCarousel as BaseImageCarousel } from "../ui/shared-ui";
export type ImageCarouselProps = {
productImages: ProductImage[];
diff --git a/packages/nextjs/app/components/LocalImage.tsx b/packages/nextjs/app/components/LocalImage.tsx
new file mode 100644
index 00000000..b8ea602b
--- /dev/null
+++ b/packages/nextjs/app/components/LocalImage.tsx
@@ -0,0 +1,10 @@
+"use client";
+
+import Image from "next/image";
+import { localImageLoader } from "utils/localImageLoader";
+
+const LocalImage = (props: React.ComponentProps) => {
+ return ;
+};
+
+export default LocalImage;
diff --git a/packages/nextjs/views/ModalFiltersMobile.tsx b/packages/nextjs/app/components/ModalFiltersMobile.tsx
similarity index 93%
rename from packages/nextjs/views/ModalFiltersMobile.tsx
rename to packages/nextjs/app/components/ModalFiltersMobile.tsx
index 77a067a8..daebd62b 100644
--- a/packages/nextjs/views/ModalFiltersMobile.tsx
+++ b/packages/nextjs/app/components/ModalFiltersMobile.tsx
@@ -1,6 +1,6 @@
import { Button, Modal } from "shared-ui";
import React from "react";
-import { ProductFilters } from "components/ProductFilters/ProductFilters";
+import { ProductFilters } from "app/components/ProductFilters/ProductFilters";
import { CategoryFilterItem, FlavourFilterItem, StyleFilterItem } from "utils/groqTypes/ProductList";
interface ModalFiltersMobileProps {
diff --git a/packages/nextjs/app/components/ModalFiltersProvider.tsx b/packages/nextjs/app/components/ModalFiltersProvider.tsx
new file mode 100644
index 00000000..d5577613
--- /dev/null
+++ b/packages/nextjs/app/components/ModalFiltersProvider.tsx
@@ -0,0 +1,84 @@
+"use client";
+
+import React from "react";
+import { ModalFiltersMobile } from "app/components/ModalFiltersMobile";
+import { useDeviceSize } from "utils/useDeviceSize";
+import { CategoryFilterItem, FlavourFilterItem, StyleFilterItem } from "utils/groqTypes/ProductList";
+
+// eslint-disable-next-line @typescript-eslint/no-empty-function
+const noop = () => {};
+
+const initialValues = {
+ handleOpenModal: noop,
+ handleCloseModal: noop,
+ isModalOpen: false,
+};
+
+const ModalFiltersContext = React.createContext(initialValues);
+
+export const useModalFilters = () => {
+ const context = React.useContext(ModalFiltersContext);
+
+ if (!context) {
+ throw new Error("useModalFilters must be used within a ModalFiltersContext.Provider");
+ }
+
+ return context;
+};
+
+type Props = {
+ categoryFilters: CategoryFilterItem[];
+ flavourFilters: FlavourFilterItem[];
+ styleFilters: StyleFilterItem[];
+};
+
+const ModalFiltersProvider = ({
+ children,
+ categoryFilters,
+ flavourFilters,
+ styleFilters,
+}: React.PropsWithChildren) => {
+ const { isSm } = useDeviceSize();
+ const [isModalOpen, setIsModalOpen] = React.useState(false);
+
+ const handleOpenModal = React.useCallback(() => {
+ setIsModalOpen(true);
+ }, []);
+
+ const handleCloseModal = React.useCallback(() => {
+ setIsModalOpen(false);
+ }, []);
+
+ React.useEffect(() => {
+ // If modal is open and the window size changes to tablet/desktop viewport,
+ // then closes the modal
+ if (!isSm) {
+ setIsModalOpen(false);
+ }
+ }, [isSm]);
+
+ const value = React.useMemo(
+ () => ({
+ handleOpenModal,
+ handleCloseModal,
+ isModalOpen,
+ }),
+ [handleCloseModal, handleOpenModal, isModalOpen]
+ );
+
+ return (
+
+ {children}
+ {/* Modal UI for filters (mobile only) */}
+
+
+ );
+};
+
+export default ModalFiltersProvider;
diff --git a/packages/nextjs/components/Pagination.tsx b/packages/nextjs/app/components/Pagination.tsx
similarity index 94%
rename from packages/nextjs/components/Pagination.tsx
rename to packages/nextjs/app/components/Pagination.tsx
index b63af415..b8e1d6be 100644
--- a/packages/nextjs/components/Pagination.tsx
+++ b/packages/nextjs/app/components/Pagination.tsx
@@ -1,5 +1,7 @@
+"use client";
+
import * as React from "react";
-import { Pagination as BasePagination } from "shared-ui";
+import { Pagination as BasePagination } from "../ui/shared-ui";
import Link from "next/link";
import classNames from "classnames";
import { usePathname, useSearchParams } from "next/navigation";
diff --git a/packages/nextjs/app/components/PaginationFade.tsx b/packages/nextjs/app/components/PaginationFade.tsx
new file mode 100644
index 00000000..c8d7f9e0
--- /dev/null
+++ b/packages/nextjs/app/components/PaginationFade.tsx
@@ -0,0 +1,35 @@
+"use client";
+
+import classNames from "classnames";
+import { FadeInOut } from "../ui/shared-ui";
+import { useSearchParams } from "next/navigation";
+import { pluralize } from "utils/pluralize";
+import { PLPVariant } from "utils/groqTypes/ProductList";
+
+type Props = {
+ variants: PLPVariant[];
+};
+
+const PaginationFade = ({ children, variants }: React.PropsWithChildren) => {
+ const query = useSearchParams();
+ const productNames = pluralize(variants.map((prod) => prod.name));
+
+ return (
+ 1 && "grid-rows-2"
+ )}
+ key={productNames}
+ >
+ {children}
+ {/* Add padder items when on page > 1 so pagination bar isn't moving around */}
+ {+(query?.get("page") || 1) > 1 &&
+ Array.from({ length: 6 - variants.length })
+ .fill(undefined)
+ .map((_, i) => )}
+
+ );
+};
+
+export default PaginationFade;
diff --git a/packages/nextjs/components/Product.tsx b/packages/nextjs/app/components/Product.tsx
similarity index 95%
rename from packages/nextjs/components/Product.tsx
rename to packages/nextjs/app/components/Product.tsx
index d32514af..f96644d8 100644
--- a/packages/nextjs/components/Product.tsx
+++ b/packages/nextjs/app/components/Product.tsx
@@ -1,5 +1,5 @@
import Link from "next/link";
-import { Price } from "shared-ui";
+import { Price } from "../ui/shared-ui";
import { PLPVariant } from "utils/groqTypes/ProductList";
import { Image } from "./Image";
diff --git a/packages/nextjs/app/components/ProductDetailBody.tsx b/packages/nextjs/app/components/ProductDetailBody.tsx
new file mode 100644
index 00000000..1eca7c0d
--- /dev/null
+++ b/packages/nextjs/app/components/ProductDetailBody.tsx
@@ -0,0 +1,51 @@
+import * as React from "react";
+
+import { BlockContent, Price } from "../ui/shared-ui";
+
+import { ImageCarousel } from "app/components/ImageCarousel";
+import { StyleOptions } from "app/components/ProductPage/StyleOptions";
+import { ProductVariantSelector } from "app/components/ProductPage/ProductVariantSelector";
+import { ProductDetail, ProductDetailVariants } from "utils/groqTypes/ProductDetail";
+import QuantityInput from "./QuantityInput";
+
+export const ProductDetailBody = ({
+ variant,
+ product,
+}: {
+ product?: ProductDetail;
+ variant?: ProductDetailVariants[number];
+}) => {
+ return (
+
+
+ );
+};
+
+export default ProductList;
diff --git a/packages/nextjs/app/components/QuantityInput.tsx b/packages/nextjs/app/components/QuantityInput.tsx
new file mode 100644
index 00000000..bda40866
--- /dev/null
+++ b/packages/nextjs/app/components/QuantityInput.tsx
@@ -0,0 +1,32 @@
+"use client";
+
+import { useState } from "react";
+import { QuantityInput as BaseQuantityInput, useCart } from "shared-ui";
+import { ProductDetailVariants } from "utils/groqTypes/ProductDetail";
+
+type Props = {
+ variant?: ProductDetailVariants[number];
+};
+
+const QuantityInput = ({ variant }: Props) => {
+ const [quantity, setQuantity] = useState("1");
+ const { updateCart, cartItems } = useCart();
+
+ const onAddToCart = () => {
+ if (variant?._id) {
+ // If the item is already in the cart allow user to click add to cart multiple times
+ const existingCartItem = cartItems.find((item) => item._id === variant._id);
+
+ updateCart({
+ _id: variant._id,
+ name: variant.name,
+ price: variant.price,
+ quantity: existingCartItem ? existingCartItem.quantity + Number(quantity) : Number(quantity),
+ });
+ }
+ };
+
+ return ;
+};
+
+export default QuantityInput;
diff --git a/packages/nextjs/components/Search.tsx b/packages/nextjs/app/components/Search.tsx
similarity index 100%
rename from packages/nextjs/components/Search.tsx
rename to packages/nextjs/app/components/Search.tsx
diff --git a/packages/nextjs/views/SortAndFiltersToolbarMobile.tsx b/packages/nextjs/app/components/SortAndFiltersToolbarMobile.tsx
similarity index 64%
rename from packages/nextjs/views/SortAndFiltersToolbarMobile.tsx
rename to packages/nextjs/app/components/SortAndFiltersToolbarMobile.tsx
index b07a3a0d..db2e93e0 100644
--- a/packages/nextjs/views/SortAndFiltersToolbarMobile.tsx
+++ b/packages/nextjs/app/components/SortAndFiltersToolbarMobile.tsx
@@ -1,15 +1,15 @@
-import { Button } from "shared-ui";
+"use client";
+
+import { Button } from "../ui/shared-ui";
import React from "react";
import { MdOutlineFilterList } from "react-icons/md";
-import { ProductSort } from "components/ProductSort";
+import { ProductSort } from "app/components/ProductSort";
import { useGetFiltersCount } from "utils/getFiltersCount";
+import { useModalFilters } from "app/components/ModalFiltersProvider";
-interface SortAndFiltersToolbarMobileProps {
- onFiltersClick?: React.MouseEventHandler;
-}
-
-export const SortAndFiltersToolbarMobile: React.FC = ({ onFiltersClick }) => {
+export const SortAndFiltersToolbarMobile: React.FC = () => {
const total = useGetFiltersCount();
+ const { handleOpenModal } = useModalFilters();
return (
@@ -20,7 +20,7 @@ export const SortAndFiltersToolbarMobile: React.FC}
- onClick={onFiltersClick}
+ onClick={handleOpenModal}
>
Filters {total > 0 ? `(${total})` : ""}
diff --git a/packages/nextjs/app/migration/components.tsx b/packages/nextjs/app/components/page.tsx
similarity index 96%
rename from packages/nextjs/app/migration/components.tsx
rename to packages/nextjs/app/components/page.tsx
index d86ad762..a2bda12d 100644
--- a/packages/nextjs/app/migration/components.tsx
+++ b/packages/nextjs/app/components/page.tsx
@@ -1,9 +1,7 @@
-"use client";
-
import { MdArrowForward } from "react-icons/md";
-import { Button, Input, Pill, Checkbox, Select, LinkText } from "shared-ui";
+import { Button, Input, Pill, Checkbox, Select, LinkText } from "../ui/shared-ui";
-export default function ComponentsPage() {
+export default async function Page() {
return (
Buttons
diff --git a/packages/nextjs/app/layout.tsx b/packages/nextjs/app/layout.tsx
index c4112b4c..ef3a36e1 100644
--- a/packages/nextjs/app/layout.tsx
+++ b/packages/nextjs/app/layout.tsx
@@ -1,14 +1,14 @@
import { Footer, MobileNavProvider } from "./ui/shared-ui";
import "./global.css";
-import { Header } from "components/Header/Header";
+import { Header } from "app/components/Header/Header";
import { Metadata } from "next";
-import { CartProvider } from "components/CartContext";
+import { CartProvider } from "app/components/CartContext";
import { AnimatePresence, MotionConfig } from "./ui/framer";
import localFont from "next/font/local";
export const metadata: Metadata = {
title: "Home",
- description: "Welcome to Next.js",
+ description: "Welcome to Formidable Boulangerie",
};
export const viewport = {
diff --git a/packages/nextjs/app/migration/about.tsx b/packages/nextjs/app/migration/about.tsx
deleted file mode 100644
index a3b89afe..00000000
--- a/packages/nextjs/app/migration/about.tsx
+++ /dev/null
@@ -1,297 +0,0 @@
-"use client";
-
-import * as React from "react";
-import { NextPage } from "next";
-import NextImage from "next/image";
-
-import Link from "next/link";
-import { BreadIcon } from "shared-ui";
-import { localImageLoader } from "utils/localImageLoader";
-import { PageHead } from "components/PageHead";
-import { Breadcrumbs } from "components/Breadcrumbs";
-
-const DIAGRAM_WIDTH = 700.0;
-const DIAGRAM_HEIGHT = DIAGRAM_WIDTH / 1.86;
-const FASTLY_WIDTH = 700.0;
-const FASTLY_HEIGHT = FASTLY_WIDTH / 2.93;
-
-const AboutPage: NextPage = () => {
- return (
- <>
-
-
-
-
-
-
-
Learn more about the Formidable Ecommerce demo site
-
- We don’t really sell bread. The goal of the project is to provide a realistic demonstration of running a
- highly performant and available e-commerce site with data sourced from Sanity’s headless CMS. The app is
- powered by
-
- Next.js
-
- ,
-
- Sanity CMS
-
- , and
-
- Fastly
-
- .
-
-
-
-
-
-
-
-
Headless CMS-driven architecture.
-
- The e-commerce data is stored in a headless CMS (powered by Sanity). The project uses Next.js (deployed on
- Vercel) to render the site, and Fastly is placed in front of Vercel to cache server-rendered webpages for
- speed and availability.
-
-
-
-
-
-
-
-
Sanity CMS
- Sanity is used for storing information about our e-commerce products. The data from Sanity is fetched
- using
-
- GROQ
-
- – a query language, used for fetching data. Formidable built
-
- Groqd
-
- – a schema-unaware, runtime and type-safe query builder for GROQ.
-
-
-
-
-
-
-
-
Sanity Studio
- Sanity Studio is a web interface for Sanity’s headless CMS. It is used for creating and editing the
- data on the site. The models for Sanity are created in code and tracked in source control. Sanity
- Studio is integrated into the NextJS application and deployed alongside{" "}
-
- as a route
-
- .
-
-
-
-
-
-
-
-
NextJS app
- To show the CMS data to end-users we created a Next.js web app that server-renders some common
- e-commerce pages, including a landing page, a Product Listing Page (PLP) with sorting and filtering,
- and a Product Details Page (PDP). The Next.js app is deployed to{" "}
-
- Vercel
-
- via their git pipeline. In a real-world e-commerce app, we expect to experience some heavy loads
- on pages whose data doesn’t change much between visits, and therefore we can deploy caching strategies
- to reduce the load on our source server.
-
-
-
-
-
-
The caching story.
-
-
-
-
-
-
-
Fastly CDN and Caching
-
- In order to enhance the speed of the app, we are utilizing Fastly’s CDN with a high cache-lifetime
- for server-rendered pages. We are using Fastly to both cache and host the subdomain used for this
- showcase app. The data flow involved in caching is illustrated below:
-
-
-
-
-
- To cache our server-rendered pages at the Fastly layer, we use response headers to indicate
- what/how we want Fastly to cache our responses from the source server. We need to a couple key
- ingredients:
-
-
-
-
-
-
- Surrogate-Control response header needs to be added to pages where caching is
- desired.
-
- Learn more.
-
-
-
-
-
-
-
-
- Surrogate-Key response header needs to be added to enable appropriate cache
- invalidation.
-
- Learn more.
-
-
-
-
-
-
-
-
- On the Next.js side we’ll need to include a few primary response headers to then control caching (in
- our case, we’re setting these headers from middleware on server-rendered pages that
- we’d like to cache).
-
-
-
-
-
-
- surrogate-control Fastly-specific header used to set the cache policies.
-
-
-
-
-
-
-
- surrogate-key Fastly-specific header that allows purging by key. Note: this
- header is removed by Fastly before sending the response to the client. To see the value of
- this header, you must include the
-
- Fastly-Debug
-
- header in your request.
-
-
-
-
-
-
-
- cache-control used to indicate to browsers and Vercel to not cache so that we can
- handle caching solely at the Fastly layer.
-
-
-
-
-
- With these response headers implemented, Fastly will start caching our responses and give us a path
- to invalidate our cache when necessary. In our case, we use data items’ slugs as part
- of our surrogate-key header to indicate what items’ data are used to render a
- page so that we can invalidate accordingly when any of those items’ data changes.
-
-
-
-
-
-
-
-
-
Cache Invalidation and Purging
- When CMS data changes, a Sanity webhook is triggered and makes a request to an API endpoint in our
- Next.js app. The endpoint does some validation on the request (to make sure it’s coming from a trusted
- Sanity webhook), and then makes a request to Fastly’s API to invalidate/purge our cache accordingly.
- The Sanity webhook payload contains information (in our case, an item’s
-
- slug
-
- about what data changes, and our API endpoint uses that slug to tell Fastly which
- cache data to invalidate (based on the surrogate-key set in the original response
- header).
-