Skip to content

Commit

Permalink
feat(vercel): Add support for image optimization API (#6845)
Browse files Browse the repository at this point in the history
* feat(vercel): Add support for image optimization API

* chore: changeset

* feat: implement image service

* feat: dev service

* feat: full local service

* fix: move assets check to astro:config:done

* feat: update with new settings

* fix: remove unused param

* test: add tsets

* fix: rename to imageService

* docs: add docs

* Apply suggestions from code review

Co-authored-by: Sarah Rainsberger <[email protected]>

* docs(vercel): Add Added In mentions

---------

Co-authored-by: Sarah Rainsberger <[email protected]>
  • Loading branch information
Princesseuh and sarah11918 committed May 2, 2023
1 parent 980246f commit 6063f56
Show file tree
Hide file tree
Showing 16 changed files with 484 additions and 8 deletions.
5 changes: 5 additions & 0 deletions .changeset/wise-geckos-applaud.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@astrojs/vercel': minor
---

Add support for using the Vercel Image Optimization API through `astro:assets`
59 changes: 58 additions & 1 deletion packages/integrations/vercel/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ Learn how to deploy your Astro site in our [Vercel deployment guide](https://doc

## Why Astro Vercel

If you're using Astro as a static site builder — its behavior out of the box — you don't need an adapter.
If you're using Astro as a static site builder — its behavior out of the box — you don't need an adapter.

If you wish to [use server-side rendering (SSR)](https://docs.astro.build/en/guides/server-side-rendering/), Astro requires an adapter that matches your deployment runtime.

Expand Down Expand Up @@ -108,6 +108,63 @@ export default defineConfig({
});
```

### imageConfig

**Type:** `VercelImageConfig`<br>
**Available for:** Edge, Serverless, Static
**Added in:** `@astrojs/[email protected]`

Configuration options for [Vercel's Image Optimization API](https://vercel.com/docs/concepts/image-optimization). See [Vercel's image configuration documentation](https://vercel.com/docs/build-output-api/v3/configuration#images) for a complete list of supported parameters.

```js
// astro.config.mjs
import { defineConfig } from 'astro/config';
import vercel from '@astrojs/vercel/static';

export default defineConfig({
output: 'server',
adapter: vercel({
imageConfig: {
sizes: [320, 640, 1280]
}
})
});
```

### imageService

**Type:** `boolean`<br>
**Available for:** Edge, Serverless, Static
**Added in:** `@astrojs/[email protected]`

When enabled, an [Image Service](https://docs.astro.build/en/reference/image-service-reference/) powered by the Vercel Image Optimization API will be automatically configured and used in production. In development, a built-in Squoosh-based service will be used instead.

```js
// astro.config.mjs
import { defineConfig } from 'astro/config';
import vercel from '@astrojs/vercel/static';

export default defineConfig({
output: 'server',
adapter: vercel({
imageService: true
})
});
```

```astro
---
import { Image } from "astro:assets";
import astroLogo from "../assets/logo.png";
---
<!-- This component -->
<Image src={astroLogo} alt="My super logo!" />
<!-- will become the following HTML -->
<img src="/_vercel/image?url=_astro/logo.hash.png&w=...&q=..." alt="My super logo!" loading="lazy" decoding="async" width="..." height="..." />
```

### includeFiles

**Type:** `string[]`<br>
Expand Down
5 changes: 4 additions & 1 deletion packages/integrations/vercel/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@
"./serverless/entrypoint": "./dist/serverless/entrypoint.js",
"./static": "./dist/static/adapter.js",
"./analytics": "./dist/analytics.js",
"./build-image-service": "./dist/image/build-service.js",
"./dev-image-service": "./dist/image/dev-service.js",
"./package.json": "./package.json"
},
"typesVersions": {
Expand Down Expand Up @@ -60,6 +62,7 @@
"astro": "workspace:*",
"astro-scripts": "workspace:*",
"chai": "^4.3.6",
"mocha": "^9.2.2"
"mocha": "^9.2.2",
"cheerio": "^1.0.0-rc.11"
}
}
17 changes: 16 additions & 1 deletion packages/integrations/vercel/src/edge/adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@ import esbuild from 'esbuild';
import { relative as relativePath } from 'node:path';
import { fileURLToPath } from 'node:url';

import {
defaultImageConfig,
getImageConfig,
throwIfAssetsNotEnabled,
type VercelImageConfig,
} from '../image/shared.js';
import {
copyFilesToFunction,
getFilesFromFolder,
Expand All @@ -26,11 +32,15 @@ function getAdapter(): AstroAdapter {
export interface VercelEdgeConfig {
includeFiles?: string[];
analytics?: boolean;
imageService?: boolean;
imagesConfig?: VercelImageConfig;
}

export default function vercelEdge({
includeFiles = [],
analytics,
imageService,
imagesConfig,
}: VercelEdgeConfig = {}): AstroIntegration {
let _config: AstroConfig;
let buildTempFolder: URL;
Expand All @@ -52,9 +62,11 @@ export default function vercelEdge({
client: new URL('./static/', outDir),
server: new URL('./dist/', config.root),
},
...getImageConfig(imageService, imagesConfig, command),
});
},
'astro:config:done': ({ setAdapter, config }) => {
throwIfAssetsNotEnabled(config, imageService);
setAdapter(getAdapter());
_config = config;
buildTempFolder = config.build.server;
Expand All @@ -64,7 +76,7 @@ export default function vercelEdge({
if (config.output === 'static') {
throw new Error(`
[@astrojs/vercel] \`output: "server"\` is required to use the edge adapter.
`);
}
},
Expand Down Expand Up @@ -135,6 +147,9 @@ export default function vercelEdge({
{ handle: 'filesystem' },
{ src: '/.*', dest: 'render' },
],
...(imageService || imagesConfig
? { images: imagesConfig ? imagesConfig : defaultImageConfig }
: {}),
});
},
},
Expand Down
60 changes: 60 additions & 0 deletions packages/integrations/vercel/src/image/build-service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import type { ExternalImageService } from 'astro';
import { isESMImportedImage, sharedValidateOptions } from './shared';

const service: ExternalImageService = {
validateOptions: (options, serviceOptions) =>
sharedValidateOptions(options, serviceOptions, 'production'),
getHTMLAttributes(options, serviceOptions) {
const { inputtedWidth, ...props } = options;

// If `validateOptions` returned a different width than the one of the image, use it for attributes
if (inputtedWidth) {
props.width = inputtedWidth;
}

let targetWidth = props.width;
let targetHeight = props.height;
if (isESMImportedImage(props.src)) {
const aspectRatio = props.src.width / props.src.height;
if (targetHeight && !targetWidth) {
// If we have a height but no width, use height to calculate the width
targetWidth = Math.round(targetHeight * aspectRatio);
} else if (targetWidth && !targetHeight) {
// If we have a width but no height, use width to calculate the height
targetHeight = Math.round(targetWidth / aspectRatio);
} else if (!targetWidth && !targetHeight) {
// If we have neither width or height, use the original image's dimensions
targetWidth = props.src.width;
targetHeight = props.src.height;
}
}

const { src, width, height, format, quality, ...attributes } = props;

return {
...attributes,
width: targetWidth,
height: targetHeight,
loading: attributes.loading ?? 'lazy',
decoding: attributes.decoding ?? 'async',
};
},
getURL(options, serviceOptions) {
const fileSrc =
typeof options.src === 'string' ? options.src : removeLeadingForwardSlash(options.src.src);

const searchParams = new URLSearchParams();
searchParams.append('url', fileSrc);

options.width && searchParams.append('w', options.width.toString());
options.quality && searchParams.append('q', options.quality.toString());

return '/_vercel/image?' + searchParams;
},
};

function removeLeadingForwardSlash(path: string) {
return path.startsWith('/') ? path.substring(1) : path;
}

export default service;
57 changes: 57 additions & 0 deletions packages/integrations/vercel/src/image/dev-service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import type { LocalImageService } from 'astro';
// @ts-expect-error
import squooshService from 'astro/assets/services/squoosh';
import { sharedValidateOptions } from './shared';

const service: LocalImageService = {
validateOptions: (options, serviceOptions) =>
sharedValidateOptions(options, serviceOptions, 'development'),
getHTMLAttributes(options, serviceOptions) {
const { inputtedWidth, ...props } = options;

// If `validateOptions` returned a different width than the one of the image, use it for attributes
if (inputtedWidth) {
props.width = inputtedWidth;
}

return squooshService.getHTMLAttributes(props, serviceOptions);
},
getURL(options) {
const fileSrc = typeof options.src === 'string' ? options.src : options.src.src;

const searchParams = new URLSearchParams();
searchParams.append('href', fileSrc);

options.width && searchParams.append('w', options.width.toString());
options.quality && searchParams.append('q', options.quality.toString());

return '/_image?' + searchParams;
},
parseURL(url) {
const params = url.searchParams;

if (!params.has('href')) {
return undefined;
}

const transform = {
src: params.get('href')!,
width: params.has('w') ? parseInt(params.get('w')!) : undefined,
quality: params.get('q'),
};

return transform;
},
transform(inputBuffer, transform, serviceOptions) {
// NOTE: Hardcoding webp here isn't accurate to how the Vercel Image Optimization API works, normally what we should
// do is setup a custom endpoint that sniff the user's accept-content header and serve the proper format based on the
// user's Vercel config. However, that's: a lot of work for: not much. The dev service is inaccurate to the prod service
// in many more ways, this is one of the less offending cases and is, imo, okay, erika - 2023-04-27
transform.format = 'webp';

// The base Squoosh service works the same way as the Vercel Image Optimization API, so it's a safe fallback in local
return squooshService.transform(inputBuffer, transform, serviceOptions);
},
};

export default service;
Loading

0 comments on commit 6063f56

Please sign in to comment.