> ## Documentation Index
> Fetch the complete documentation index at: https://docs.pixglory.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Next.js

> Serve images from PixGlory in a Next.js app, and stop paying Vercel to re-optimise images we already optimised.

If you deploy on Vercel, every image `next/image` serves through its own optimiser is billed
per transformation. When the image already came out of PixGlory at the right size and format,
that second optimisation pass buys you nothing and costs you twice. This guide covers both ways
to use PixGlory from a Next.js app: pointing `next/image` at files already sitting in a CDN
project, and uploading new files from a route handler.

## Before you start

You need a PixGlory API key and a CDN project. See the [Quickstart](/quickstart) to create a key,
and create a project from your [dashboard](https://pixglory.com/pix/cdn) if you don't have one
yet.

Add your key and project id to your environment:

```bash .env.local theme={null}
PIXGLORY_API_KEY=pxg_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
PIXGLORY_PROJECT_ID=proj_xxxxxxxxxxxxxxxxxxxxxxxxxxxx
```

Find your project's id with a quick request against `GET /v1/projects`:

```bash theme={null}
curl https://pixglory.com/api/v1/projects \
  -H "Authorization: Bearer $PIXGLORY_API_KEY"
```

The response lists every project your key can see, each with its `id`, `slug` and `publicUrl`.

## Serve files that are already in PixGlory

Tell Next.js's image component that `cdn.pixglory.com` is a trusted remote host, in
`next.config.ts`:

```ts next.config.ts theme={null}
import type { NextConfig } from "next";

const nextConfig: NextConfig = {
  images: {
    remotePatterns: [
      {
        protocol: "https",
        hostname: "cdn.pixglory.com",
        pathname: "/**",
      },
    ],
  },
};

export default nextConfig;
```

That alone lets `next/image` render a PixGlory URL. The part that saves the Vercel billing is
telling it not to re-optimise what we already optimised. The simplest way is `unoptimized` on
the individual image:

```tsx theme={null}
import Image from "next/image";

export function Hero() {
  return (
    <Image
      src="https://cdn.pixglory.com/acme/acme-studio/hero.jpg"
      width={1200}
      height={630}
      alt="Product hero shot"
      unoptimized
    />
  );
}
```

If most of the images on a page come from PixGlory, a custom loader is less to repeat. A loader
that just returns the URL unchanged skips Next's optimiser for every image that uses it, without
an `unoptimized` prop on each one:

```ts lib/pixglory-loader.ts theme={null}
export default function pixgloryLoader({ src }: { src: string }) {
  // PixGlory already produced the exact bytes at this URL. Returning it
  // unchanged tells next/image there's nothing left for it to do.
  return src;
}
```

```tsx theme={null}
import Image from "next/image";
import pixgloryLoader from "@/lib/pixglory-loader";

<Image
  loader={pixgloryLoader}
  src="https://cdn.pixglory.com/acme/acme-studio/hero.jpg"
  width={1200}
  height={630}
  alt="Product hero shot"
/>;
```

## Upload new files from a route handler

When a user uploads a file through your app, presign and confirm it against PixGlory from a
route handler rather than shipping your API key to the browser. The upload itself (the PUT to
R2) still happens client-side, straight to the presigned URL, so the bytes never pass through
your server.

```ts app/api/pixglory/presign/route.ts theme={null}
import { NextResponse } from "next/server";

const PIXGLORY_API = "https://pixglory.com/api/v1";

export async function POST(request: Request) {
  const { files } = await request.json();

  const response = await fetch(`${PIXGLORY_API}/uploads`, {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.PIXGLORY_API_KEY}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      projectId: process.env.PIXGLORY_PROJECT_ID,
      files,
    }),
  });

  const body = await response.json();
  return NextResponse.json(body, { status: response.status });
}
```

```ts app/api/pixglory/confirm/route.ts theme={null}
import { NextResponse } from "next/server";

const PIXGLORY_API = "https://pixglory.com/api/v1";

export async function POST(request: Request) {
  const { paths } = await request.json();

  const response = await fetch(`${PIXGLORY_API}/uploads/confirm`, {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.PIXGLORY_API_KEY}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      projectId: process.env.PIXGLORY_PROJECT_ID,
      paths,
    }),
  });

  const body = await response.json();
  return NextResponse.json(body, { status: response.status });
}
```

From the client, presign, upload the bytes straight to R2, then confirm:

```ts lib/upload-to-pixglory.ts theme={null}
export async function uploadToPixGlory(file: File): Promise<string> {
  const presignRes = await fetch("/api/pixglory/presign", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({
      files: [{ name: file.name, size: file.size, type: file.type }],
    }),
  });
  const { uploads } = await presignRes.json();
  const [upload] = uploads;

  await fetch(upload.uploadUrl, {
    method: "PUT",
    headers: { "Content-Type": upload.contentType },
    body: file,
  });

  const confirmRes = await fetch("/api/pixglory/confirm", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ paths: [upload.path] }),
  });
  const { files: confirmed } = await confirmRes.json();
  return confirmed[0].publicUrl;
}
```

`confirmed[0].publicUrl` is the file's permanent, canonical PixGlory link. Persist it (in your
database, your CMS, wherever the rest of the record lives) rather than reconstructing it, since
that is the only URL form the API guarantees.

<Note>
  A single presign request can name several files at once (`files: [...]`), and `uploads` comes
  back in the same order. Batch a gallery upload into one call instead of one round trip per
  file.
</Note>

## Handling errors

A `402` response means the organisation's storage quota would be exceeded; a `429` means you've
hit the rate limit and should back off using the `Retry-After` header. Every error body has the
same shape:

```json theme={null}
{ "error": { "message": "...", "code": "CDN_STORAGE_QUOTA_EXCEEDED", "details": { "..." : "..." } } }
```

Branch on `error.code`, not `error.message`, which can change wording without notice. The full
list of status codes and error codes is in the **API Reference** tab.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.