An open API service indexing awesome lists of open source software.

https://github.com/vsima/astro-aeo-image

Astro image service that embeds <Image> alt text as XMP into your optimized output files — so dist/_astro assets are self-describing for Google Images and AI answer engines. Wraps Astro's sharp service; byte-preserving via aeo-image.
https://github.com/vsima/astro-aeo-image

accessibility aeo alt-text answer-engine-optimization astro astro-integration image-metadata seo xmp

Last synced: about 1 month ago
JSON representation

Astro image service that embeds <Image> alt text as XMP into your optimized output files — so dist/_astro assets are self-describing for Google Images and AI answer engines. Wraps Astro's sharp service; byte-preserving via aeo-image.

Awesome Lists containing this project

README

          

# astro-aeo-image

> An Astro image service that embeds your `` **alt text** (and optional description/keywords/license) as standards **XMP/IPTC** directly into the optimized output files — so your `dist/_astro` assets are **self-describing**: [Google Images reads embedded IPTC metadata and recommends embedding it](https://developers.google.com/search/docs/appearance/structured-data/image-license-metadata), and the description travels with the file for accessibility, attribution, and the AI-search era.

[![npm](https://img.shields.io/npm/v/astro-aeo-image.svg)](https://www.npmjs.com/package/astro-aeo-image)
![license](https://img.shields.io/badge/license-MIT-green)

Astro **already requires** an `alt` on every `` — but that text normally lives only in the HTML attribute. The moment the optimized file is downloaded, hot-linked, or indexed *as a file*, the page context is gone. `astro-aeo-image` writes the description (and attribution/license) **into the image bytes**, where it travels with the file.

> **What's documented:** [Google Images reads embedded IPTC metadata](https://developers.google.com/search/docs/appearance/structured-data/image-license-metadata) (creator/credit/copyright/license) and **recommends embedding it**. For image *ranking*, Google uses the [HTML `alt`](https://developers.google.com/search/docs/appearance/google-images) — so embedding **complements** it (durability, accessibility, attribution), it doesn't replace it or claim a ranking boost. AI engines consuming embedded metadata is forward-looking, not yet a spec.

## How it's different from `image.service.config.keepMetadata`

This is an **authoring** layer, not a **preservation** one — and that distinction is the whole point:

| | sharp's `keepMetadata` / `keepExif` | **astro-aeo-image** |
| --- | --- | --- |
| Carries through metadata already in the *source* file | ✅ | — |
| **Authors new descriptive metadata** from your app data (the `alt` you already wrote, captions, keywords) | ❌ | ✅ |
| Writes `Iptc4xmpCore:AltTextAccessibility` + `dc:description` for AEO/accessibility | ❌ | ✅ |

sharp can preserve a camera's existing EXIF; it can't compose a fresh XMP packet from the `alt` prop in your `.astro` file. That's the gap this fills.

## How it works

It's a thin wrapper around **Astro's own default sharp service**. Everything sharp does is unchanged — same resizing, formats, quality, caching. After sharp encodes each variant, this service splices descriptive XMP into the output buffer via [`aeo-image`](https://www.npmjs.com/package/aeo-image): **byte-preserving, no re-encode** — the compressed pixels are identical; only a metadata block is added.

## Install

```bash
npm install astro-aeo-image
```

`aeo-image` comes along as its only dependency (zero-dependency itself). `astro` is a peer dependency (you already have it).

## Configure

Add the integration — one line:

```js
// astro.config.mjs
import { defineConfig } from "astro/config";
import aeoImage from "astro-aeo-image";

export default defineConfig({
integrations: [
aeoImage(), // or aeoImage({ useAltAsDescription: false })
],
});
```

Advanced: use the image service directly (without the integration)

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

export default defineConfig({
image: {
service: {
entrypoint: "astro-aeo-image/service",
config: { useAltAsDescription: true },
},
},
});
```

The integration is just a thin wrapper that sets this for you and forwards options.

That's it. Every `` you already have now ships its `alt` inside the file:

```astro
---
import { Image } from "astro:assets";
import barn from "../assets/barn.jpg";
---

```

The generated `dist/_astro/barn.*.webp` now contains:
- `Iptc4xmpCore:AltTextAccessibility` = the alt text
- `dc:description` = the alt text (unless `useAltAsDescription: false`)

## Optional richer metadata

Pass extra props on `` for fuller AEO signals (distinct SEO description, keywords, title):

```astro

```

| Prop | XMP field |
| --- | --- |
| `alt` (required by Astro) | `Iptc4xmpCore:AltTextAccessibility` (+ `dc:description`) |
| `description` | `dc:description` |
| `keywords` (array or comma string) | `dc:subject` |
| `title` | `dc:title` |
| `creator` | `dc:creator` |
| `credit` | `photoshop:Credit` |
| `rights` | `dc:rights` |
| `copyrightNotice` | `photoshop:Copyright` |
| `licenseUrl` | `xmpRights:WebStatement` — *Google Licensable* |
| `licensor` `{url, name?}` (or flat `licensorUrl`/`licensorName`) | IPTC PLUS `plus:Licensor` — *Google "Get this image" link* |
| `digitalSourceType` (full IRI or bare term like `"trainedAlgorithmicMedia"`) | `Iptc4xmpExt:DigitalSourceType` — *AI-generated disclosure* |
| `aiGenerated` (boolean shorthand) | sets `Iptc4xmpExt:DigitalSourceType` to `trainedAlgorithmicMedia` |
| `ai` `{prompt?, promptWriter?, system?, systemVersion?}` (or flat `aiPrompt`/`aiPromptWriter`/`aiSystem`/`aiSystemVersion`) | IPTC 2025.1 `Iptc4xmpExt:AIPromptInformation` / `AIPromptWriterName` / `AISystemUsed` / `AISystemVersionUsed` |

### Make images Licensable in Google

The last fields implement what [Google Images reads for the **Licensable** badge](https://developers.google.com/search/docs/appearance/structured-data/image-license-metadata):

```astro

```

### Label AI-generated images (IPTC 2025.1)

For AI-generated assets, embed the IPTC 2025.1 provenance fields plus the
Digital Source Type IRI that downstream tools read to label an image as
AI-generated:

```astro

```

`aiGenerated` is shorthand for
`digitalSourceType="trainedAlgorithmicMedia"`; pass `digitalSourceType`
explicitly for other terms (e.g. `"compositeSynthetic"` — bare CV terms are
expanded to the full IPTC IRI). Per IPTC guidance, leave `creator` off for
fully AI-generated images — the prompt writer is explicitly not the creator.

For TypeScript autocomplete on the custom props, augment Astro's image props in `src/env.d.ts`:

```ts
declare namespace Astro {
interface CustomImageProps {
description?: string;
keywords?: string[] | string;
title?: string;
aiGenerated?: boolean;
aiPrompt?: string;
aiSystem?: string;
}
}
```

## Supported formats

Whatever you output from Astro: **WebP, AVIF, JPEG, PNG** (via `aeo-image`). SVG/GIF and any format `aeo-image` doesn't handle are passed through untouched — the service degrades gracefully and **never fails a build over metadata**.

## Standards

Metadata is written as **Adobe XMP** (not legacy IPTC-IIM), conforming to the [IPTC Photo Metadata Standard 2025.1](https://iptc.org/standards/photo-metadata/iptc-standard/) (descriptive + accessibility + rights/licensing + AI-provenance subset) plus Dublin Core, Adobe, and PLUS namespaces — see [`aeo-image`'s conformance notes](https://github.com/vsima/aeo-image#standards--conformance). This includes the 2025.1 AI-generation provenance properties and `Iptc4xmpExt:DigitalSourceType` (readable by exiftool, named tags from 13.40).

## Verifying it worked

Given ``, the built file gains embedded metadata:

```console
$ exiftool dist/_astro/barn.abc123.webp | grep -iE "description|alt"
# ── before (default Astro service) ──
# (no metadata)

# ── after (astro-aeo-image) ──
Description : A weathered red barn under a violet dusk sky in rural Vermont
Alt Text Accessibility : A weathered red barn under a violet dusk sky in rural Vermont
```

Or check it programmatically:

```bash
node -e "import('aeo-image').then(async m=>{const {readFileSync}=await import('node:fs');console.log(m.readMetadata(new Uint8Array(readFileSync(process.argv[1]))))})" dist/_astro/.webp
# → { description: '…', altText: '…' }
```

The pixels are untouched — only a metadata block is added.

## Status & scope

- The metadata-embedding core is unit-tested against the published `aeo-image`.
- On **static builds**, Astro passes `` props (including `alt`) to the service's `transform`, which is where embedding happens. For **on-demand/SSR** image endpoints, the custom props are included in `propertiesToHash` so they survive into `transform`; if you rely on SSR image optimization, please smoke-test and [open an issue](https://github.com/vsima/astro-aeo-image/issues) with your Astro version if a prop doesn't come through.
- Wraps `astro/assets/services/sharp`. If you use a different image service, this won't layer on top of it.

## License

MIT