> ## 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.

# Nuxt

> Serve images from PixGlory in a Nuxt app with a custom @nuxt/image provider, and upload new files through a Nitro server route.

`@nuxt/image` already knows how to talk to a CDN through a provider. This guide covers both
directions: a custom provider that points `<NuxtImg>` at files already in a PixGlory CDN project,
and a Nitro server route that presigns and confirms new uploads.

## 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 runtime config:

```bash .env theme={null}
NUXT_PIXGLORY_API_KEY=pxg_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
NUXT_PIXGLORY_PROJECT_ID=proj_xxxxxxxxxxxxxxxxxxxxxxxxxxxx
NUXT_PUBLIC_PIXGLORY_BASE_URL=https://cdn.pixglory.com/acme/acme-studio
```

Find your project's id and its `publicUrl` (the base for the third value above) with a request
against `GET /v1/projects`:

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

```ts nuxt.config.ts theme={null}
export default defineNuxtConfig({
  modules: ["@nuxt/image"],
  runtimeConfig: {
    pixgloryApiKey: "",
    pixgloryProjectId: "",
    public: {
      pixgloryBaseUrl: "",
    },
  },
  image: {
    providers: {
      pixglory: {
        provider: "~/providers/pixglory.ts",
      },
    },
  },
});
```

## Serve files that are already in PixGlory

A provider is a pure URL builder: PixGlory has already produced the final bytes, so the provider
just needs to compose the path onto your project's base URL. There is nothing left for `@nuxt/image`
to transform, so the provider declares that up front.

```ts providers/pixglory.ts theme={null}
import { joinURL } from "ufo";
import { defineProvider } from "@nuxt/image";

export default defineProvider({
  getImage(src, { baseURL } = {}) {
    const base =
      baseURL ?? useRuntimeConfig().public.pixgloryBaseUrl;
    return { url: joinURL(base, src) };
  },
  // PixGlory already served the right size and format. There is nothing
  // for @nuxt/image's own resizing to do on top of it.
  supportsAlias: false,
});
```

```vue theme={null}
<template>
  <NuxtImg provider="pixglory" src="/hero.jpg" width="1200" height="630" alt="Product hero shot" />
</template>
```

## Upload new files from a Nitro server route

Presign and confirm from a server route so your API key never reaches the browser. The PUT to
R2 still happens client-side, straight to the presigned URL.

```ts server/api/pixglory/presign.post.ts theme={null}
export default defineEventHandler(async (event) => {
  const { files } = await readBody(event);
  const config = useRuntimeConfig();

  const response = await $fetch("https://pixglory.com/api/v1/uploads", {
    method: "POST",
    headers: { Authorization: `Bearer ${config.pixgloryApiKey}` },
    body: {
      projectId: config.pixgloryProjectId,
      files,
    },
  });

  return response;
});
```

```ts server/api/pixglory/confirm.post.ts theme={null}
export default defineEventHandler(async (event) => {
  const { paths } = await readBody(event);
  const config = useRuntimeConfig();

  const response = await $fetch("https://pixglory.com/api/v1/uploads/confirm", {
    method: "POST",
    headers: { Authorization: `Bearer ${config.pixgloryApiKey}` },
    body: {
      projectId: config.pixgloryProjectId,
      paths,
    },
  });

  return response;
});
```

From the client, presign, PUT the bytes, then confirm:

```ts composables/use-pixglory-upload.ts theme={null}
export async function uploadToPixGlory(file: File): Promise<string> {
  const { uploads } = await $fetch("/api/pixglory/presign", {
    method: "POST",
    body: {
      files: [{ name: file.name, size: file.size, type: file.type }],
    },
  });
  const [upload] = uploads;

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

  const { files: confirmed } = await $fetch("/api/pixglory/confirm", {
    method: "POST",
    body: { paths: [upload.path] },
  });
  return confirmed[0].publicUrl;
}
```

`confirmed[0].publicUrl` is the file's permanent, canonical PixGlory link. Persist it rather than
reconstructing it from the path yourself, since that's 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.