{"id":51001242,"url":"https://github.com/vsima/astro-aeo-image","last_synced_at":"2026-06-20T14:08:12.803Z","repository":{"id":361845351,"uuid":"1256190354","full_name":"vsima/astro-aeo-image","owner":"vsima","description":"Astro image service that embeds \u003cImage\u003e 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.","archived":false,"fork":false,"pushed_at":"2026-06-01T15:02:13.000Z","size":0,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-06-01T15:09:52.949Z","etag":null,"topics":["accessibility","aeo","alt-text","answer-engine-optimization","astro","astro-integration","image-metadata","seo","xmp"],"latest_commit_sha":null,"homepage":null,"language":null,"has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/vsima.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-06-01T14:44:58.000Z","updated_at":"2026-06-01T14:45:40.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/vsima/astro-aeo-image","commit_stats":null,"previous_names":["vsima/astro-aeo-image"],"tags_count":1,"template":false,"template_full_name":null,"purl":"pkg:github/vsima/astro-aeo-image","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/vsima%2Fastro-aeo-image","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/vsima%2Fastro-aeo-image/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/vsima%2Fastro-aeo-image/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/vsima%2Fastro-aeo-image/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/vsima","download_url":"https://codeload.github.com/vsima/astro-aeo-image/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/vsima%2Fastro-aeo-image/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34572475,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-20T02:00:06.407Z","response_time":98,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["accessibility","aeo","alt-text","answer-engine-optimization","astro","astro-integration","image-metadata","seo","xmp"],"created_at":"2026-06-20T14:08:10.319Z","updated_at":"2026-06-20T14:08:12.794Z","avatar_url":"https://github.com/vsima.png","language":null,"funding_links":[],"categories":[],"sub_categories":[],"readme":"# astro-aeo-image\n\n\u003e An Astro image service that embeds your `\u003cImage\u003e` **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.\n\n[![npm](https://img.shields.io/npm/v/astro-aeo-image.svg)](https://www.npmjs.com/package/astro-aeo-image)\n![license](https://img.shields.io/badge/license-MIT-green)\n\nAstro **already requires** an `alt` on every `\u003cImage /\u003e` — 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.\n\n\u003e **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.\n\n## How it's different from `image.service.config.keepMetadata`\n\nThis is an **authoring** layer, not a **preservation** one — and that distinction is the whole point:\n\n| | sharp's `keepMetadata` / `keepExif` | **astro-aeo-image** |\n| --- | --- | --- |\n| Carries through metadata already in the *source* file | ✅ | — |\n| **Authors new descriptive metadata** from your app data (the `alt` you already wrote, captions, keywords) | ❌ | ✅ |\n| Writes `Iptc4xmpCore:AltTextAccessibility` + `dc:description` for AEO/accessibility | ❌ | ✅ |\n\nsharp 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.\n\n## How it works\n\nIt'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.\n\n## Install\n\n```bash\nnpm install astro-aeo-image\n```\n\n`aeo-image` comes along as its only dependency (zero-dependency itself). `astro` is a peer dependency (you already have it).\n\n## Configure\n\nAdd the integration — one line:\n\n```js\n// astro.config.mjs\nimport { defineConfig } from \"astro/config\";\nimport aeoImage from \"astro-aeo-image\";\n\nexport default defineConfig({\n  integrations: [\n    aeoImage(), // or aeoImage({ useAltAsDescription: false })\n  ],\n});\n```\n\n\u003cdetails\u003e\n\u003csummary\u003eAdvanced: use the image service directly (without the integration)\u003c/summary\u003e\n\n```js\n// astro.config.mjs\nimport { defineConfig } from \"astro/config\";\n\nexport default defineConfig({\n  image: {\n    service: {\n      entrypoint: \"astro-aeo-image/service\",\n      config: { useAltAsDescription: true },\n    },\n  },\n});\n```\n\nThe integration is just a thin wrapper that sets this for you and forwards options.\n\u003c/details\u003e\n\nThat's it. Every `\u003cImage /\u003e` you already have now ships its `alt` inside the file:\n\n```astro\n---\nimport { Image } from \"astro:assets\";\nimport barn from \"../assets/barn.jpg\";\n---\n\u003cImage src={barn} alt=\"A weathered red barn under a violet dusk sky in rural Vermont\" width={1200} height={800} /\u003e\n```\n\nThe generated `dist/_astro/barn.*.webp` now contains:\n- `Iptc4xmpCore:AltTextAccessibility` = the alt text\n- `dc:description` = the alt text (unless `useAltAsDescription: false`)\n\n## Optional richer metadata\n\nPass extra props on `\u003cImage\u003e` for fuller AEO signals (distinct SEO description, keywords, title):\n\n```astro\n\u003cImage\n  src={barn}\n  alt=\"A weathered red barn at dusk\"\n  description=\"A restored 1890s dairy barn in Vermont, now a working agrivoltaics site\"\n  keywords={[\"barn\", \"vermont\", \"agrivoltaics\", \"rural\"]}\n  title=\"Vermont Agrivoltaics Barn\"\n  width={1200} height={800}\n/\u003e\n```\n\n| Prop | XMP field |\n| --- | --- |\n| `alt` (required by Astro) | `Iptc4xmpCore:AltTextAccessibility` (+ `dc:description`) |\n| `description` | `dc:description` |\n| `keywords` (array or comma string) | `dc:subject` |\n| `title` | `dc:title` |\n| `creator` | `dc:creator` |\n| `credit` | `photoshop:Credit` |\n| `rights` | `dc:rights` |\n| `copyrightNotice` | `photoshop:Copyright` |\n| `licenseUrl` | `xmpRights:WebStatement` — *Google Licensable* |\n| `licensor` `{url, name?}` (or flat `licensorUrl`/`licensorName`) | IPTC PLUS `plus:Licensor` — *Google \"Get this image\" link* |\n| `digitalSourceType` (full IRI or bare term like `\"trainedAlgorithmicMedia\"`) | `Iptc4xmpExt:DigitalSourceType` — *AI-generated disclosure* |\n| `aiGenerated` (boolean shorthand) | sets `Iptc4xmpExt:DigitalSourceType` to `trainedAlgorithmicMedia` |\n| `ai` `{prompt?, promptWriter?, system?, systemVersion?}` (or flat `aiPrompt`/`aiPromptWriter`/`aiSystem`/`aiSystemVersion`) | IPTC 2025.1 `Iptc4xmpExt:AIPromptInformation` / `AIPromptWriterName` / `AISystemUsed` / `AISystemVersionUsed` |\n\n### Make images Licensable in Google\n\nThe last fields implement what [Google Images reads for the **Licensable** badge](https://developers.google.com/search/docs/appearance/structured-data/image-license-metadata):\n\n```astro\n\u003cImage\n  src={barn}\n  alt=\"A weathered red barn at dusk\"\n  creator=\"Jane Doe\"\n  copyrightNotice=\"© 2026 Example Studio\"\n  licenseUrl=\"https://example.com/license/barn\"\n  licensorUrl=\"https://example.com/buy/barn\"\n  width={1200} height={800}\n/\u003e\n```\n\n### Label AI-generated images (IPTC 2025.1)\n\nFor AI-generated assets, embed the IPTC 2025.1 provenance fields plus the\nDigital Source Type IRI that downstream tools read to label an image as\nAI-generated:\n\n```astro\n\u003cImage\n  src={market}\n  alt=\"A neon-lit street market at night in the rain\"\n  aiGenerated\n  aiPrompt=\"neon street market, rain reflections, cinematic 35mm\"\n  aiPromptWriter=\"Jane Doe\"\n  aiSystem=\"DALL-E via Bing Image Creator\"\n  aiSystemVersion=\"3\"\n  width={1200} height={800}\n/\u003e\n```\n\n`aiGenerated` is shorthand for\n`digitalSourceType=\"trainedAlgorithmicMedia\"`; pass `digitalSourceType`\nexplicitly for other terms (e.g. `\"compositeSynthetic\"` — bare CV terms are\nexpanded to the full IPTC IRI). Per IPTC guidance, leave `creator` off for\nfully AI-generated images — the prompt writer is explicitly not the creator.\n\nFor TypeScript autocomplete on the custom props, augment Astro's image props in `src/env.d.ts`:\n\n```ts\ndeclare namespace Astro {\n  interface CustomImageProps {\n    description?: string;\n    keywords?: string[] | string;\n    title?: string;\n    aiGenerated?: boolean;\n    aiPrompt?: string;\n    aiSystem?: string;\n  }\n}\n```\n\n## Supported formats\n\nWhatever 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**.\n\n## Standards\n\nMetadata 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).\n\n## Verifying it worked\n\nGiven `\u003cImage alt=\"A weathered red barn under a violet dusk sky in rural Vermont\" /\u003e`, the built file gains embedded metadata:\n\n```console\n$ exiftool dist/_astro/barn.abc123.webp | grep -iE \"description|alt\"\n# ── before (default Astro service) ──\n#   (no metadata)\n\n# ── after (astro-aeo-image) ──\nDescription                     : A weathered red barn under a violet dusk sky in rural Vermont\nAlt Text Accessibility          : A weathered red barn under a violet dusk sky in rural Vermont\n```\n\nOr check it programmatically:\n\n```bash\nnode -e \"import('aeo-image').then(async m=\u003e{const {readFileSync}=await import('node:fs');console.log(m.readMetadata(new Uint8Array(readFileSync(process.argv[1]))))})\" dist/_astro/\u003cyour-image\u003e.webp\n# → { description: '…', altText: '…' }\n```\n\nThe pixels are untouched — only a metadata block is added.\n\n## Status \u0026 scope\n\n- The metadata-embedding core is unit-tested against the published `aeo-image`.\n- On **static builds**, Astro passes `\u003cImage\u003e` 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.\n- Wraps `astro/assets/services/sharp`. If you use a different image service, this won't layer on top of it.\n\n## License\n\nMIT\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fvsima%2Fastro-aeo-image","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fvsima%2Fastro-aeo-image","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fvsima%2Fastro-aeo-image/lists"}