Skip to content
Open
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/autorender-cdn-provider.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@responsive-image/cdn': minor
---

Add Autorender image CDN provider
1 change: 1 addition & 0 deletions apps/docs/.vitepress/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ export default defineConfig({
link: '/',
base: '/cdn',
items: [
{ text: 'Autorender', link: '/autorender' },
{ text: 'Cloudinary', link: '/cloudinary' },
{ text: 'Fastly', link: '/fastly' },
{ text: 'Imgix', link: '/imgix' },
Expand Down
170 changes: 170 additions & 0 deletions apps/docs/src/cdn/autorender.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
---
outline: [2, 3]
---

# Autorender

The image processing capabilities of the [Autorender](https://autorender.io) image CDN are supported by a helper function provided to you by this library.

## Setup

Make sure you have the `@responsive-image/cdn` package installed:

::: code-group

```bash [npm]
npm install @responsive-image/cdn
```

```bash [yarn]
yarn add @responsive-image/cdn
```

```bash [pnpm]
pnpm add @responsive-image/cdn
```

:::

You need to specify your Autorender `domain` and `workspace` in your configuration, which you can set up in your application (e.g. `app.js`). The workspace ID is the public routing identifier in your delivery URLs and is not a secret:

```js
import { setConfig } from '@responsive-image/core';

setConfig('cdn', {
autorender: {
domain: 'assets.autorender.io',
workspace: 'LOKVTtKVGb',
},
});
```

## Usage

> [!IMPORTANT]
> Please make sure you have read the section on [remote images](../usage/remote-images.md) first.

Use the autorender provider function passing the source path of the image inside your workspace, and pass the return value to the [image component](../usage/component.md):

::: code-group

```gjs [Ember .gjs]
import { ResponsiveImage } from '@responsive-image/ember';
import { autorender } from '@responsive-image/cdn';

<template>
<ResponsiveImage @src={{autorender 'products/chair.jpg'}} />
</template>
```

```hbs [Ember .hbs]
<ResponsiveImage @src={{responsive-image-autorender 'products/chair.jpg'}} />
```

```ts [Lit]
import { LitElement, html } from 'lit';
import { customElement } from 'lit/decorators.js';
import { autorender } from '@responsive-image/cdn';
import '@responsive-image/wc';

@customElement('my-app')
export class MyApp extends LitElement {
render() {
return html`<responsive-image
.src=${autorender('products/chair.jpg')}
></responsive-image>`;
}
}
```

```tsx [React]
import { ResponsiveImage } from '@responsive-image/react';
import { autorender } from '@responsive-image/cdn';

export default function MyApp() {
return <ResponsiveImage src={autorender('products/chair.jpg')} />;
}
```

```tsx [Solid]
import { ResponsiveImage } from '@responsive-image/solid';
import { autorender } from '@responsive-image/cdn';

export default function MyApp() {
return <ResponsiveImage src={autorender('products/chair.jpg')} />;
}
```

```svelte [Svelte]
<script>
import { ResponsiveImage } from '@responsive-image/svelte';
import { autorender } from '@responsive-image/cdn';
</script>

<ResponsiveImage src={autorender('products/chair.jpg')} />
```

```vue [Vue]
<script setup>
import { ResponsiveImage } from '@responsive-image/vue';
import { autorender } from '@responsive-image/cdn';
</script>

<template>
<ResponsiveImage :src="autorender('products/chair.jpg')" />
</template>
```

:::

### Aspect Ratio

For the image component to be able to render `width` and `height` attributes to prevent layout shifts after loading has completed, it needs to know the aspect ratio of the source image. Unlike [local images](../usage/local-images.md) it cannot know this upfront for remote images, that's why it is recommended to supply the `aspectRatio` parameter if possible:

```ts [Lit]
autorender('products/chair.jpg', {
aspectRatio: 1.5,
});
```

### Quality

Use the `quality` parameter to pass a custom quality setting (`1`–`100`) instead of Autorender's per-format default:

```ts [Lit]
autorender('products/chair.jpg', {
quality: 50,
});
```

### Image formats

By default the component lets Autorender select the format from the request `Accept` header.

If you want a `picture` tag with one or more specific formats as `source` tags you can specify them using the `formats` argument. Autorender supports `avif`, `webp`, `jpeg`, `png`, `gif`, and `tiff`:

```ts [Lit]
autorender('products/chair.jpg', {
formats: ['avif', 'webp'],
});
```

### Remote images

Besides source paths inside your workspace, you can pass a full `http(s)` URL. Autorender fetches the remote image, then optimizes and delivers it through the CDN — useful when your originals live elsewhere and aren't uploaded to the workspace:

```ts [Lit]
autorender('https://images.example.com/products/chair.jpg', {
formats: ['avif', 'webp'],
});
```

### Custom transforms

Besides the resizing and format tokens the library adds implicitly, you can append any additional [Autorender transform tokens](https://autorender.io/docs/transformations/introduction) verbatim by passing a `transforms` array:

```ts [Lit]
autorender('products/chair.jpg', {
transforms: ['e_sharpen', 'r_16'],
});
```
1 change: 1 addition & 0 deletions apps/docs/src/cdn/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ With image CDNs the image processing is offloaded to the Cloud. This allows for

The following image CDNs are supported by the `@responsive-image/cdn` package out of the box:

- [Autorender](./autorender.md)
- [Cloudinary](./cloudinary.md)
- [Fastly](./fastly.md)
- [Imgix](./imgix.md)
Expand Down
101 changes: 101 additions & 0 deletions packages/cdn/src/autorender.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import { assert, getConfig } from '@responsive-image/core';

import type { Config, CoreOptions } from './types';
import type { ImageData, ImageUrlForType } from '@responsive-image/core';

export interface AutorenderConfig {
/**
* Delivery domain that serves your transformed images,
* e.g. `assets.autorender.io`.
*/
domain: string;
/**
* Public workspace ID that routes the request, e.g. `LOKVTtKVGb`.
* It is part of the delivery URL and is not a secret.
*/
workspace: string;
}

export interface AutorenderOptions extends CoreOptions {
/**
* Extra Autorender transform tokens applied verbatim, e.g.
* `['e_sharpen', 'r_16']`. See the transformation reference for the
* full token vocabulary.
*/
transforms?: string[];
}

const ABSOLUTE_URL_RE = /^https?:\/\//i;

function normalizeSrc(src: string): string {
return src[0] === '/' ? src.slice(1) : src;

Check warning on line 31 in packages/cdn/src/autorender.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use 'String#startsWith' method instead.

See more on https://sonarcloud.io/project/issues?id=simonihmig_responsive-image&issues=AZ_D75CJ1vMCp7SkDuO9&open=AZ_D75CJ1vMCp7SkDuO9&pullRequest=2584
}

/**
* Escape the structural characters of a remote fetch URL so it survives as a
* single transform token: `%` (round-trip safety), `,` (token delimiter),
* `?` and `#` (which would otherwise terminate the path). Slashes and colons
* stay literal; the delivery backend percent-decodes path segments.
*/
function escapeFetchPayload(url: string): string {
return url
.replace(/%/g, '%25')

Check warning on line 42 in packages/cdn/src/autorender.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer `String#replaceAll()` over `String#replace()`.

See more on https://sonarcloud.io/project/issues?id=simonihmig_responsive-image&issues=AZ_D75CJ1vMCp7SkDuO-&open=AZ_D75CJ1vMCp7SkDuO-&pullRequest=2584
.replace(/,/g, '%2C')

Check warning on line 43 in packages/cdn/src/autorender.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer `String#replaceAll()` over `String#replace()`.

See more on https://sonarcloud.io/project/issues?id=simonihmig_responsive-image&issues=AZ_D75CJ1vMCp7SkDuO_&open=AZ_D75CJ1vMCp7SkDuO_&pullRequest=2584
.replace(/\?/g, '%3F')

Check warning on line 44 in packages/cdn/src/autorender.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer `String#replaceAll()` over `String#replace()`.

See more on https://sonarcloud.io/project/issues?id=simonihmig_responsive-image&issues=AZ_D75CJ1vMCp7SkDuPA&open=AZ_D75CJ1vMCp7SkDuPA&pullRequest=2584
.replace(/#/g, '%23');

Check warning on line 45 in packages/cdn/src/autorender.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer `String#replaceAll()` over `String#replace()`.

See more on https://sonarcloud.io/project/issues?id=simonihmig_responsive-image&issues=AZ_D75CJ1vMCp7SkDuPB&open=AZ_D75CJ1vMCp7SkDuPB&pullRequest=2584
}

export function autorender(
image: string,
options: AutorenderOptions = {},
): ImageData {
const config = getConfig<Config>('cdn')?.autorender;
const domain = config?.domain;
const workspace = config?.workspace;
assert(
'domain must be set for the autorender provider!',
typeof domain === 'string',
);
assert(
'workspace must be set for the autorender provider!',
typeof workspace === 'string',
);

const isRemote = ABSOLUTE_URL_RE.test(image.trim());
const src = isRemote ? image.trim() : normalizeSrc(image);

const imageData: ImageData = {
imageTypes: options.formats ?? 'auto',
imageUrlFor(width: number, type: ImageUrlForType = 'jpeg'): string {
const tokens = [`w_${width}`];

// Autorender accepts `f_jpeg` directly, so the format name maps 1:1.
// `auto` emits `f_auto`, letting Autorender negotiate from the
// Accept header instead of pinning a single format.
tokens.push(`f_${type}`);

if (options.quality) {
tokens.push(`q_${options.quality}`);
}

if (options.transforms) {
tokens.push(...options.transforms);
}

// Remote sources become a `fetch_<url>` transform token (not a path
// segment); the workspace-relative path otherwise trails the tokens.
if (isRemote) {
tokens.push(`fetch_${escapeFetchPayload(src)}`);
return `https://${domain}/${workspace}/${tokens.join(',')}`;
}

return `https://${domain}/${workspace}/${tokens.join(',')}/${src}`;
},
};

if (options.aspectRatio) {
imageData.aspectRatio = options.aspectRatio;
}

return imageData;
}
1 change: 1 addition & 0 deletions packages/cdn/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './autorender.ts';
export * from './cloudinary.ts';
export * from './fastly.ts';
export * from './imgix.ts';
Expand Down
2 changes: 2 additions & 0 deletions packages/cdn/src/types.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import type { AutorenderConfig } from './autorender';
import type { CloudinaryConfig } from './cloudinary';
import type { FastlyConfig } from './fastly';
import type { ImgixConfig } from './imgix';
import type { NetlifyConfig } from './netlify';
import type { ImageTypeAuto, ImageType } from '@responsive-image/core';

export interface Config {
autorender?: AutorenderConfig;
imgix?: ImgixConfig;
fastly?: FastlyConfig;
cloudinary?: CloudinaryConfig;
Expand Down
Loading