Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(vercel): Add support for image optimization API #6845

Merged
merged 19 commits into from
May 2, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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]`

Princesseuh marked this conversation as resolved.
Show resolved Hide resolved
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