Skip to content

Commit

Permalink
feat(image): throw if alt text is missing (#4511)
Browse files Browse the repository at this point in the history
* feat(image): throw if no `alt` is provided

* chore: add changeset

* docs(image): update README

* updated alt text stuff throughout

* fixing with-mdx test suite

* warn for missing alt text, will throw an error in a future release

* final README tweaks

Co-authored-by: Tony Sullivan <tony.f.sullivan@outlook.com>
Co-authored-by: Sarah Rainsberger <sarah@rainsberger.ca>
  • Loading branch information
3 people committed Sep 1, 2022
1 parent df402dd commit 72c760e
Show file tree
Hide file tree
Showing 25 changed files with 391 additions and 50 deletions.
5 changes: 5 additions & 0 deletions .changeset/polite-pears-hope.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@astrojs/image': minor
---

feat: throw if alt text is missing
60 changes: 41 additions & 19 deletions packages/integrations/image/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ This **[Astro integration][astro-integration]** makes it easy to optimize images

Images play a big role in overall site performance and usability. Serving properly sized images makes all the difference but is often tricky to automate.

This integration provides `<Image />` and `<Picture>` components as well as a basic image transformer powered by [sharp](https://sharp.pixelplumbing.com/), with full support for static sites and server-side rendering. The built-in `sharp` transformer is also replacable, opening the door for future integrations that work with your favorite hosted image service.
This integration provides `<Image />` and `<Picture>` components as well as a basic image transformer powered by [sharp](https://sharp.pixelplumbing.com/), with full support for static sites and server-side rendering. The built-in `sharp` transformer is also replaceable, opening the door for future integrations that work with your favorite hosted image service.

## Installation

Expand Down Expand Up @@ -90,6 +90,10 @@ import { Image, Picture } from '@astrojs/image/components';

The included `sharp` transformer supports resizing images and encoding them to different image formats. Third-party image services will be able to add support for custom transformations as well (ex: `blur`, `filter`, `rotate`, etc).

Astro’s <Image /> and <Picture /> components require the alt attribute which provides descriptive text for images. A warning will be logged if "alt" text is missing, and a future release of the integration will throw an error if no alt text is provided.

If the image is merely decorative (i.e. doesn’t contribute to the understanding of the page), set alt="" so that the image is properly understood and ignored by screen readers.

### `<Image />`

The built-in `<Image />` component is used to create an optimized `<img />` for both remote images hosted on other domains as well as local images imported from your project's `src` directory.
Expand All @@ -108,10 +112,22 @@ Source for the original image file.

For images located in your project's `src`: use the file path relative to the `src` directory. (e.g. `src="../assets/source-pic.png"`)

For images located in your `public` directory: use the URL path relative to the `public` directory. (e.g. `src="/images/public-image.jpg"`)
For images located in your `public` directory: use the URL path relative to the `public` directory. (e.g. `src="/images/public-image.jpg"`)

For remote images, provide the full URL. (e.g. `src="https://astro.build/assets/blog/astro-1-release-update.avif"`)

#### alt

<p>

**Type:** `string`<br>
**Required:** `true`
</p>

Defines an alternative text description of the image.

Set to an empty string (alt="") if the image is not a key part of the content (it's decoration or a tracking pixel).

#### format

<p>
Expand Down Expand Up @@ -186,17 +202,23 @@ A `number` can also be provided, useful when the aspect ratio is calculated at b

Source for the original image file.

For images in your project's repository, use the path relative to the `src` or `public` directory. For remote images, provide the full URL.
For images located in your project's `src`: use the file path relative to the `src` directory. (e.g. `src="../assets/source-pic.png"`)

For images located in your `public` directory: use the URL path relative to the `public` directory. (e.g. `src="/images/public-image.jpg"`)

For remote images, provide the full URL. (e.g. `src="https://astro.build/assets/blog/astro-1-release-update.avif"`)

#### alt

<p>

**Type:** `string`<br>
**Default:** `undefined`
**Required:** `true`
</p>

If provided, the `alt` string will be included on the built `<img />` element.
Defines an alternative text description of the image.

Set to an empty string (alt="") if the image is not a key part of the content (it's decoration or a tracking pixel).

#### sizes

Expand Down Expand Up @@ -266,7 +288,7 @@ const { src } = await getImage('../assets/hero.png');
<html>
<head>
<link rel="preload" as="image" href={src}>
<link rel="preload" as="image" href={src} alt="alt text">
</head>
</html>
```
Expand Down Expand Up @@ -330,19 +352,19 @@ import heroImage from '../assets/hero.png';
---
// optimized image, keeping the original width, height, and image format
<Image src={heroImage} />
<Image src={heroImage} alt="descriptive text" />
// height will be recalculated to match the original aspect ratio
<Image src={heroImage} width={300} />
<Image src={heroImage} width={300} alt="descriptive text" />
// cropping to a specific width and height
<Image src={heroImage} width={300} height={600} />
<Image src={heroImage} width={300} height={600} alt="descriptive text" />
// cropping to a specific aspect ratio and converting to an avif format
<Image src={heroImage} aspectRatio="16:9" format="avif" />
<Image src={heroImage} aspectRatio="16:9" format="avif" alt="descriptive text" />
// image imports can also be inlined directly
<Image src={import('../assets/hero.png')} />
<Image src={import('../assets/hero.png')} alt="descriptive text" />
```

#### Images in `/public`
Expand All @@ -356,11 +378,11 @@ For example, use an image located at `public/social.png` in either static or SSR
```astro title="src/pages/page.astro"
---
import { Image } from '@astrojs/image/components';
import socialImage from '/social.png';
import socialImage from '/social.png';
---
// In static builds: the image will be built and optimized to `/dist`.
// In SSR builds: the image will be optimized by the server when requested by a browser.
<Image src={socialImage} width={1280} aspectRatio="16:9" />
<Image src={socialImage} width={1280} aspectRatio="16:9" alt="descriptive text" />
```

### Remote images
Expand All @@ -375,13 +397,13 @@ const imageUrl = 'https://www.google.com/images/branding/googlelogo/2x/googlelog
---
// cropping to a specific width and height
<Image src={imageUrl} width={544} height={184} />
<Image src={imageUrl} width={544} height={184} alt="descriptive text" />
// height will be recalculated to match the aspect ratio
<Image src={imageUrl} width={300} aspectRatio={16/9} />
<Image src={imageUrl} width={300} aspectRatio={16/9} alt="descriptive text" />
// cropping to a specific height and aspect ratio and converting to an avif format
<Image src={imageUrl} height={200} aspectRatio="16:9" format="avif" />
<Image src={imageUrl} height={200} aspectRatio="16:9" format="avif" alt="descriptive text" />
```

### Responsive pictures
Expand All @@ -401,13 +423,13 @@ const imageUrl = 'https://www.google.com/images/branding/googlelogo/2x/googlelog
---
// Local image with multiple sizes
<Picture src={hero} widths={[200, 400, 800]} sizes="(max-width: 800px) 100vw, 800px" alt="My hero image" />
<Picture src={hero} widths={[200, 400, 800]} sizes="(max-width: 800px) 100vw, 800px" alt="descriptive text" />
// Remote image (aspect ratio is required)
<Picture src={imageUrl} widths={[200, 400, 800]} aspectRatio="4:3" sizes="(max-width: 800px) 100vw, 800px" alt="My hero image" />
<Picture src={imageUrl} widths={[200, 400, 800]} aspectRatio="4:3" sizes="(max-width: 800px) 100vw, 800px" alt="descriptive text" />
// Inlined imports are supported
<Picture src={import("../assets/hero.png")} widths={[200, 400, 800]} sizes="(max-width: 800px) 100vw, 800px" alt="My hero image" />
<Picture src={import("../assets/hero.png")} widths={[200, 400, 800]} sizes="(max-width: 800px) 100vw, 800px" alt="descriptive text" />
```

## Troubleshooting
Expand Down
9 changes: 9 additions & 0 deletions packages/integrations/image/components/Image.astro
Original file line number Diff line number Diff line change
@@ -1,17 +1,22 @@
---
// @ts-ignore
import { getImage } from '../dist/index.js';
import { warnForMissingAlt } from './index.js';
import type { ImgHTMLAttributes } from './index.js';
import type { ImageMetadata, TransformOptions, OutputFormat } from '../dist/index.js';
interface LocalImageProps
extends Omit<TransformOptions, 'src'>,
Omit<ImgHTMLAttributes, 'src' | 'width' | 'height'> {
src: ImageMetadata | Promise<{ default: ImageMetadata }>;
/** Defines an alternative text description of the image. Set to an empty string (alt="") if the image is not a key part of the content (it's decoration or a tracking pixel). */
alt: string;
}
interface RemoteImageProps extends TransformOptions, astroHTML.JSX.ImgHTMLAttributes {
src: string;
/** Defines an alternative text description of the image. Set to an empty string (alt="") if the image is not a key part of the content (it's decoration or a tracking pixel). */
alt: string;
format: OutputFormat;
width: number;
height: number;
Expand All @@ -21,6 +26,10 @@ export type Props = LocalImageProps | RemoteImageProps;
const { loading = 'lazy', decoding = 'async', ...props } = Astro.props as Props;
if (props.alt === undefined || props.alt === null) {
warnForMissingAlt();
}
const attrs = await getImage(props);
---

Expand Down
11 changes: 9 additions & 2 deletions packages/integrations/image/components/Picture.astro
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
---
import { getPicture } from '../dist/index.js';
import { warnForMissingAlt } from './index.js';
import type { ImgHTMLAttributes, HTMLAttributes } from './index.js';
import type { ImageMetadata, OutputFormat, TransformOptions } from '../dist/index.js';
Expand All @@ -8,7 +9,8 @@ interface LocalImageProps
Omit<TransformOptions, 'src'>,
Pick<astroHTML.JSX.ImgHTMLAttributes, 'loading' | 'decoding'> {
src: ImageMetadata | Promise<{ default: ImageMetadata }>;
alt?: string;
/** Defines an alternative text description of the image. Set to an empty string (alt="") if the image is not a key part of the content (it's decoration or a tracking pixel). */
alt: string;
sizes: HTMLImageElement['sizes'];
widths: number[];
formats?: OutputFormat[];
Expand All @@ -19,7 +21,8 @@ interface RemoteImageProps
TransformOptions,
Pick<ImgHTMLAttributes, 'loading' | 'decoding'> {
src: string;
alt?: string;
/** Defines an alternative text description of the image. Set to an empty string (alt="") if the image is not a key part of the content (it's decoration or a tracking pixel). */
alt: string;
sizes: HTMLImageElement['sizes'];
widths: number[];
aspectRatio: TransformOptions['aspectRatio'];
Expand All @@ -40,6 +43,10 @@ const {
...attrs
} = Astro.props as Props;
if (alt === undefined || alt === null) {
warnForMissingAlt();
}
const { image, sources } = await getPicture({ src, widths, formats, aspectRatio });
---

Expand Down
14 changes: 14 additions & 0 deletions packages/integrations/image/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,17 @@ export type HTMLAttributes = Omit<
astroHTML.JSX.HTMLAttributes,
'client:list' | 'set:text' | 'set:html' | 'is:raw'
>;

let altWarningShown = false;

export function warnForMissingAlt() {
if (altWarningShown === true) { return }

altWarningShown = true;

console.warn(`\n[@astrojs/image] "alt" text was not provided for an <Image> or <Picture> component.
A future release of @astrojs/image may throw a build error when "alt" text is missing.
The "alt" attribute holds a text description of the image, which isn't mandatory but is incredibly useful for accessibility. Set to an empty string (alt="") if the image is not a key part of the content (it's decoration or a tracking pixel).\n`);
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,16 @@ import { Image } from '@astrojs/image/components';
<!-- Head Stuff -->
</head>
<body>
<Image id="hero" src="/hero.jpg" width={768} height={414} format="webp" />
<Image id="hero" src="/hero.jpg" width={768} height={414} format="webp" alt="hero" />
<br />
<Image id="spaces" src={introJpg} width={768} height={414} format="webp" />
<Image id="spaces" src={introJpg} width={768} height={414} format="webp" alt="spaces" />
<br />
<Image id="social-jpg" src={socialJpg} width={506} height={253} />
<Image id="social-jpg" src={socialJpg} width={506} height={253} alt="social-jpg" />
<br />
<Image id="google" src="https://www.google.com/images/branding/googlelogo/2x/googlelogo_color_272x92dp.png" width={544} height={184} format="webp" />
<Image id="google" src="https://www.google.com/images/branding/googlelogo/2x/googlelogo_color_272x92dp.png" width={544} height={184} format="webp" alt="Google" />
<br />
<Image id="inline" src={import('../assets/social.jpg')} width={506} />
<Image id="inline" src={import('../assets/social.jpg')} width={506} alt="inline" />
<br />
<Image id="query" src="https://www.google.com/images/branding/googlelogo/2x/googlelogo_color_272x92dp.png?token=abc" width={544} height={184} format="webp" />
<Image id="query" src="https://www.google.com/images/branding/googlelogo/2x/googlelogo_color_272x92dp.png?token=abc" width={544} height={184} format="webp" alt="query" />
</body>
</html>
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { defineConfig } from 'astro/config';
import image from '@astrojs/image';

// https://astro.build/config
export default defineConfig({
site: 'http://localhost:3000',
integrations: [image({ logLevel: 'silent' })]
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"name": "@test/no-alt-text-image",
"version": "0.0.0",
"private": true,
"dependencies": {
"@astrojs/image": "workspace:*",
"@astrojs/node": "workspace:*",
"astro": "workspace:*"
}
}
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { createServer } from 'http';
import fs from 'fs';
import mime from 'mime';
import { handler as ssrHandler } from '../dist/server/entry.mjs';

const clientRoot = new URL('../dist/client/', import.meta.url);

async function handle(req, res) {
ssrHandler(req, res, async (err) => {
if (err) {
res.writeHead(500);
res.end(err.stack);
return;
}

let local = new URL('.' + req.url, clientRoot);
try {
const data = await fs.promises.readFile(local);
res.writeHead(200, {
'Content-Type': mime.getType(req.url),
});
res.end(data);
} catch {
res.writeHead(404);
res.end();
}
});
}

const server = createServer((req, res) => {
handle(req, res).catch((err) => {
console.error(err);
res.writeHead(500, {
'Content-Type': 'text/plain',
});
res.end(err.toString());
});
});

server.listen(8085);
console.log('Serving at http://localhost:8085');

// Silence weird <time> warning
console.error = () => {};
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
import socialJpg from '../assets/social.jpg';
import { Image } from '@astrojs/image/components';
---

<html>
<head>
<!-- Head Stuff -->
</head>
<body>
<Image id="social-jpg" src={socialJpg} width={506} height={253} />
</body>
</html>
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { defineConfig } from 'astro/config';
import image from '@astrojs/image';

// https://astro.build/config
export default defineConfig({
site: 'http://localhost:3000',
integrations: [image({ logLevel: 'silent' })]
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"name": "@test/no-alt-text-picture",
"version": "0.0.0",
"private": true,
"dependencies": {
"@astrojs/image": "workspace:*",
"@astrojs/node": "workspace:*",
"astro": "workspace:*"
}
}
Binary file not shown.

0 comments on commit 72c760e

Please sign in to comment.