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

# Astro

> Serve remote images from PixGlory in an Astro site, and upload your public/ assets to a CDN project as part of your build.

Astro is static-first, so the honest way to describe this integration is not "PixGlory optimises
your images at build time" (Astro's own `astro:assets` already does that for local files). It's
"PixGlory is your asset host": build-time or ahead-of-time processed images live in a CDN
project, and your site references them by URL instead of shipping the bytes in your own deploy.

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

Set your key and project id as environment variables for the build:

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

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

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

## Serve remote images from PixGlory

Tell Astro's image service to trust `cdn.pixglory.com` as a remote source, in `astro.config.mjs`:

```js astro.config.mjs theme={null}
import { defineConfig } from "astro/config";

export default defineConfig({
  image: {
    domains: ["cdn.pixglory.com"],
  },
});
```

Then use `<Image>` with an explicit `width` and `height`, same as any other remote source. Astro
does not re-process a remote image's pixels, so those two values only reserve layout space; the
bytes PixGlory already produced are what ships:

```astro theme={null}
---
import { Image } from "astro:assets";
---

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

## Upload your public/ assets before you deploy

For a static site, the natural integration point is a build-time script rather than a runtime
route: walk `public/`, upload anything that isn't already in the project, and confirm. Run it in
your build pipeline before the deploy step.

```ts scripts/upload-to-pixglory.ts theme={null}
import { readdir, stat } from "node:fs/promises";
import { extname, join, relative } from "node:path";

const PIXGLORY_API = "https://pixglory.com/api/v1";
const API_KEY = process.env.PIXGLORY_API_KEY;
const PROJECT_ID = process.env.PIXGLORY_PROJECT_ID;
const ASSETS_DIR = "public/images";

const CONTENT_TYPES: Record<string, string> = {
  ".jpg": "image/jpeg",
  ".jpeg": "image/jpeg",
  ".png": "image/png",
  ".webp": "image/webp",
  ".avif": "image/avif",
  ".gif": "image/gif",
  ".svg": "image/svg+xml",
};

async function collectImages(dir: string): Promise<string[]> {
  const entries = await readdir(dir, { withFileTypes: true });
  const files = await Promise.all(
    entries.map(async (entry) => {
      const full = join(dir, entry.name);
      if (entry.isDirectory()) return collectImages(full);
      return extname(entry.name).toLowerCase() in CONTENT_TYPES ? [full] : [];
    }),
  );
  return files.flat();
}

async function main() {
  const paths = await collectImages(ASSETS_DIR);
  if (paths.length === 0) return;

  const files = await Promise.all(
    paths.map(async (path) => {
      const info = await stat(path);
      return {
        path,
        name: relative(ASSETS_DIR, path),
        size: info.size,
        type: CONTENT_TYPES[extname(path).toLowerCase()],
      };
    }),
  );

  const presignRes = await fetch(`${PIXGLORY_API}/uploads`, {
    method: "POST",
    headers: {
      Authorization: `Bearer ${API_KEY}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      projectId: PROJECT_ID,
      files: files.map(({ name, size, type }) => ({ name, size, type })),
    }),
  });
  const { uploads } = await presignRes.json();

  const { readFile } = await import("node:fs/promises");
  await Promise.all(
    uploads.map(async (upload: { uploadUrl: string; contentType: string }, i: number) => {
      const bytes = await readFile(files[i].path);
      await fetch(upload.uploadUrl, {
        method: "PUT",
        headers: { "Content-Type": upload.contentType },
        body: bytes,
      });
    }),
  );

  const confirmRes = await fetch(`${PIXGLORY_API}/uploads/confirm`, {
    method: "POST",
    headers: {
      Authorization: `Bearer ${API_KEY}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      projectId: PROJECT_ID,
      paths: uploads.map((u: { path: string }) => u.path),
    }),
  });
  const { files: confirmed } = await confirmRes.json();

  for (const file of confirmed) {
    console.log(file.path, "->", file.publicUrl);
  }
}

main();
```

Run it with `node --experimental-strip-types scripts/upload-to-pixglory.ts` (or compile it, if
your build already has a TypeScript step) as part of your CI pipeline, before the deploy step
that publishes your static output. Uploads are idempotent per path: a re-run against files
already confirmed skips them rather than erroring.

<Note>
  A single presign request can name several files at once. The script above batches every file
  under `public/images` 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.