Build an Online Shop with Next.js #5: Order Confirmation and Deployment

Last time we wired up the checkout form, a Server Action, and the payment simulation — completing the path that turns a cart into an order. In this final post, we close the user flow with an order confirmation page, deploy to Vercel to put the shop on the real internet, and wrap up with a look back at the whole series and the question “what would need fixing before this could be a production service?”

/orders/[orderId] — the order confirmation page #

This is where the client lands right after checkout, via router.push('/orders/${orderId}'). It looks up the order in the in-memory store by ID and displays it.

src/app/orders/[orderId]/page.js:

src/app/orders/[orderId]/page.js
import Link from 'next/link';
import { notFound } from 'next/navigation';
import { getOrder } from '../../lib/orders';
import { formatPrice } from '../../lib/format';

export const metadata = {
  title: 'Order Complete',
  description: 'Your order has been received.',
  robots: { index: false, follow: false },
};

export default async function OrderConfirmationPage({ params }) {
  const { orderId } = await params;
  const order = getOrder(orderId);

  if (!order) notFound();

  return (
    <main className="mx-auto max-w-2xl px-4 py-8">
      <section className="rounded-lg bg-green-50 p-6 text-center">
        <h1 className="text-2xl font-bold text-green-800">Your order is complete</h1>
        <p className="mt-2 text-sm text-green-700">
          Order number <code className="font-mono">{order.id}</code>
        </p>
      </section>

      <section className="mt-6">
        <h2 className="text-lg font-semibold">Order details</h2>
        <ul className="mt-2 divide-y border-y text-sm">
          {order.items.map((it) => (
            <li key={it.slug} className="flex justify-between py-2">
              <span>{it.name} × {it.quantity}</span>
              <span>{formatPrice(it.price * it.quantity)}</span>
            </li>
          ))}
        </ul>
        <div className="mt-3 flex justify-between text-lg font-semibold">
          <span>Total</span>
          <span>{formatPrice(order.total)}</span>
        </div>
      </section>

      <section className="mt-6">
        <h2 className="text-lg font-semibold">Shipping info</h2>
        <dl className="mt-2 grid grid-cols-3 gap-y-1 text-sm">
          <dt className="text-gray-500">Recipient</dt>
          <dd className="col-span-2">{order.shipping.name}</dd>
          <dt className="text-gray-500">Phone</dt>
          <dd className="col-span-2">{order.shipping.phone}</dd>
          <dt className="text-gray-500">Address</dt>
          <dd className="col-span-2">{order.shipping.address}</dd>
        </dl>
      </section>

      <div className="mt-8 flex gap-3">
        <Link
          href="/products"
          className="rounded-md border px-4 py-2 text-sm"
        >
          Continue shopping
        </Link>
        <Link
          href="/"
          className="rounded-md bg-black px-4 py-2 text-sm text-white"
        >
          Back to home
        </Link>
      </div>
    </main>
  );
}

Design details:

  • robots: { index: false, follow: false } — there’s no reason for search engines to index an order confirmation page. It contains personal information (name, phone, address), so we block any chance of it showing up in search from the start.
  • Invalid IDs hit notFound() — if someone guesses another user’s order ID and the order isn’t in memory (server restart, etc.), they get a 404.
  • Two next actions — “Continue shopping” and “Back to home.” Right after payment, users shouldn’t be stuck in a dead end; we point them to where to go next.

A quick note on order page security #

The current implementation means anyone who knows an order ID can view that order. Fine for a learning demo, but a production service needs these reinforcements:

  • Make order IDs long, unguessable random strings (the current timestamp + 6 characters is partially guessable)
  • For guest checkout, issue a separate one-time token for order lookup (attached to an email link)
  • For logged-in users, match orders against the session so users can only see their own

We’ll come back to this in the final section.

Tidying up site metadata #

Finally, let’s polish the metadata to cover the SEO basics. The pattern is similar to #5 of the blog series, adjusted for an e-commerce site.

src/app/layout.js (metadata section):

src/app/layout.js — updated metadata
export const metadata = {
  metadataBase: new URL(process.env.NEXT_PUBLIC_SITE_URL ?? 'http://localhost:3000'),
  title: {
    default: 'My Shop',
    template: '%s | My Shop',
  },
  description: 'A learning e-commerce build — discover great products.',
  openGraph: {
    type: 'website',
    locale: 'en_US',
    siteName: 'My Shop',
  },
};

Refining the product detail page’s OG as well makes social previews richer.

src/app/products/[slug]/page.js (enhanced generateMetadata):

src/app/products/[slug]/page.js — enhanced generateMetadata
export async function generateMetadata({ params }) {
  const { slug } = await params;
  const product = getProductBySlug(slug);
  if (!product) return {};
  return {
    title: product.name,
    description: product.description,
    openGraph: {
      title: product.name,
      description: product.description,
      type: 'website',
      images: [{ url: product.image }],
    },
  };
}

We won’t go as far as automatic OG image generation (ImageResponse). Exposing a static image as the OG image is enough here; fancier OG setups are a topic for another day.

Environment variables and deploy prep #

The domain changes in the deployed environment, so we extract the site URL into an environment variable.

.env.local:

.env.local
NEXT_PUBLIC_SITE_URL=http://localhost:3000

In the Vercel project’s Environment Variables, register the real domain:

Vercel env vars
NEXT_PUBLIC_SITE_URL=https://your-shop.vercel.app

metadataBase uses this value to build absolute URLs for OG images. Dev, preview, and production each automatically get their own domain.

Deploying to Vercel #

Same flow as the blog series.

  1. Push the code to a GitHub repository
  2. On vercel.com, “New Project” → pick the repo
  3. Framework Preset: Next.js (auto-detected)
  4. Add NEXT_PUBLIC_SITE_URL under Environment Variables
  5. Deploy

When the build finishes, you get a domain like https://your-shop.vercel.app. Go in, browse the catalog, add something to the cart, and run a checkout end to end.

The limits of in-memory storage show up after deploy #

Once you actually use the deployed shop, two problems that were invisible during learning surface.

1. 404 on the order page right after checkout #

Checkout succeeds, but /orders/[orderId] sometimes returns a 404. The cause is Vercel’s separate serverless function instances.

  • The checkout request (checkoutAction) runs on instance A, so the order lands in A’s in-memory Map
  • The follow-up order lookup (the Server Component for /orders/[orderId]) runs on instance B
  • B’s memory doesn’t have that order → notFound()

In-memory storage only means anything within a single process. To share data across multiple instances, that data has to live somewhere external — and a database is the standard answer.

2. Redeploying wipes all orders #

When a new deploy goes live, the serverless functions start as fresh instances. The module-scope Map starts out empty again. Every past order evaporates.

A fine trade-off for a learning demo — and a great teaching moment, because it shows precisely where a production service would have to break.

What needs fixing for a production service #

Let’s revisit the areas this series deliberately simplified.

AreaLearning implementationProduction recommendation
Product storageJSON fileDB (PostgreSQL, MySQL, SQLite) or headless CMS
Order storageModule-scope in-memory MapDB. Can’t assume a single instance
Price validationSum prices sent by the clientServer re-fetches prices from the product master, then sums
InventoryStatic stock field, no decrementDecrement stock in a transaction at checkout + concurrency handling
PaymentSimulation (Math.random)Integrate a PG like Toss Payments / Stripe / Iamport + webhooks
AuthenticationNone; anyone can check outNextAuth.js / Clerk / Lucia, etc.
Order access controlAnyone with the order IDLogged-in users see only their own orders / one-time token for guests
Order statusFixed paidA pending / paid / preparing / shipped / delivered / canceled state machine
Email notificationsNoneMail at each payment/shipping step via Resend / SendGrid, etc.
Admin pagesNoneOrder list / inventory management / refund handling
SecurityHTML5 required + server guard+ CSRF tokens / rate limiting / input sanitization

Each row could fill a series of its own. But the skeleton this series built — catalog → cart → checkout → order confirmation — survives intact. The items above are flesh you add to that skeleton over time.

Series retrospective — Build an Online Shop with Next.js #

Across this series, we went from an empty project to a demo shop with a closed user flow.

#Added featureKey patterns/tools
1Design, data model, setupApp Router, JSON seed, lib/ separation
2Catalog (list / detail / filters)Server Components, searchParams, generateStaticParams, notFound, next/image
3Shopping cartClient Context, localStorage persistence, hydration-safe pattern
4Checkout + paymentServer Actions, useActionState, useFormStatus, payment simulation
5Order confirmation + deploymentmetadata API, robots, Vercel, limits of in-memory storage

Same five-post length as the blog series, but different territory. The blog centered on reading and comments; the shop centered on user state (cart) → transactions (orders). Following both builds gives you practical patterns for both read-centric sites and state/transaction-centric apps.

Where to go from here #

You now have an even sturdier base for starting your own projects. Possible directions:

If you made this shop real #

  • Add a DB — start with Prisma + PostgreSQL or Drizzle + SQLite. Keep the call signatures in products.js / orders.js and swap only the internals, and the page code barely needs to change
  • Integrate a real payment provider — open the payment window and receive webhooks with Toss Payments (Korea) or Stripe (global)
  • Add authentication — email/social login with NextAuth.js, users viewing their own orders
  • Admin pages — order list / adding products / inventory management

Other builds #

  • Social app — boards, follows, notifications — real-time and many-to-many relationships
  • Dashboard — charts, filters, data visualization — external data source integration
  • A tool for your own domain — that small tool you’ve always wanted. The choice with the biggest learning payoff

Wrap-up #

Thanks for following along this far. We started from a single small catalog page and tied cart, payment, and order confirmation into one continuous flow. There are learning-grade simplifications throughout, but the exact points where those simplifications break are the signals that tell you which tools a production service needs — the moment in-memory storage splits across instances, you need a DB; the moment client-sent prices start to worry you, you need stronger server-side validation.

The React ecosystem moves fast, but the mental model this series covered — “where does this code run?” — doesn’t change. Once the division of labor between Server Components, Client Components, and Server Actions is in your hands, the same intuition carries over to any other tool. DB, auth, and payment providers just stack on top over time. Knowing the essence, every new tool that comes along quickly finds its place.

X