Thanks to visit codestin.com
Credit goes to github.com

Skip to content
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
23 changes: 17 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,19 +107,29 @@ is not auto-detected.
- Storyblok
- Cloudflare
- Kontent.ai
- Next.js/Vercel

## Usage with Next.js

Unpic has special handling for Next.js image URLs. It detects supported image
CDNs, and falls back to `/_next/image` for local and unsupported remote images.

For more information, see the
[Unpic Next.js](https://github.com/ascorbic/unpic/blob/main/nextjs.md)
documentation.

## FAQs

- **What is an image CDN?** An image CDN is a service that provides a URL API
for transforming images. This is often used to resize images on the fly, but
can also be used to apply other transforms such as cropping, rotation,
compression, etc. This includes dedicated image CDNs such as Imgix and
Cloudinary, CMSs such as Contentful, Builder.io and Sanity, general CDNs such as Bunny.net
that provide an image API, but also other service providers such as Shopify.
The CMSs and other service providers often use a dedicated image CDN to
provide the image API, most commonly Imgix. In most cases they support the
same API, but in others they may proxy the image through their own CDN, or use
a different API.
Cloudinary, CMSs such as Contentful, Builder.io and Sanity, general CDNs such
as Bunny.net that provide an image API, but also other service providers such
as Shopify. The CMSs and other service providers often use a dedicated image
CDN to provide the image API, most commonly Imgix. In most cases they support
the same API, but in others they may proxy the image through their own CDN, or
use a different API.
- **Why would I use this instead of the CDN's own SDK?** If you you know that
your images will all come from one CDN, then you probably should use the CDN's
own SDK. This library is designed to work with images from multiple CDNs, and
Expand Down Expand Up @@ -169,3 +179,4 @@ To add a new CDN, add the following:
- add the new CDN to the types in `src/types.ts`, and import the new source file
in `src/transform.ts`
- add a sample image to `examples.json` in the demo site
- ensure tests pass by installing Deno and running `deno test src`
5 changes: 4 additions & 1 deletion data/paths.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
{
"/cdn-cgi/image/": "cloudflare"
"/cdn-cgi/image/": "cloudflare",
"/_next/image": "nextjs",
"/_next/static": "nextjs",
"/_vercel/image": "vercel"
}
4 changes: 4 additions & 0 deletions demo/src/examples.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,5 +38,9 @@
[
"Kontent.ai",
"https://assets-us-01.kc-usercontent.com/b744f382-bfc7-434d-93e7-a65d51249bc7/cc0afdc7-23d7-4fde-be2c-f58ad54d2934/daylight.jpg"
],
[
"Next.js",
"https://netlify-plugin-nextjs-demo.netlify.app/_next/image/?url=https%3A%2F%2Fimages.unsplash.com%2Fphoto-1674255909399-9bcb2cab6489%3Fauto%3Dformat%26fit%3Dcrop%26w%3D200%26q%3D80%26h%3D100&w=384&q=75"
]
]
41 changes: 41 additions & 0 deletions nextjs.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
## Using Unpic with Next.js

Unpic has special support for Next.js, which detects support image CDNs, and
falls back to `/_next/image` for local and unsupported remote images.

When Unpic is passed a Next.js image URL, it will try to parse the URL in the
`url` param, and then apply the transforms to the parsed URL. If the `url` is a
local image, or is not hosted on a supported image CDN then the transforms will
be applied to the Next.js URL itself by modifying the `w` param. This avoid the
need to download the image from the CDN and then transform it with Next.js.
Instead, the image is transformed by the original CDN.

For example:

- Passing `width: 200` and
`https://example.com/_next/image?url=%2Fimages%2Fimage.jpg&w=640&q=75` will be
transformed to
`https://example.com/_next/image?url=%2Fimages%2Fimage.jpg&w=200&q=75&width=640`

- Passing `width: 200` and
`https://example.com/_next/image?url=https%3A%2F%2Fimages.unsplash.com%2Fphoto&w=640&q=75`
will be transformed to
`https://images.unsplash.com/photo?auto=format&fit=crop&w=200`

You can also pass any relative URL or remote URL and manually set the `cdn`
param to `nextjs`, and it will be transformed as above, falling back to a
`/_next/image` URL if it is not from a supported CDN.

For example:

- Passing `width: 200`, `cdn: nextjs` and `/profile.png` will be transformed to
`/_next/image?url=%2Fiprofile.png&w=200`
- Passing `width: 200`, `cdn: nextjs` and
`https://images.unsplash.com/photo-1674255909399-9bcb2cab6489` will be
transformed to
`https://images.unsplash.com/photo-1674255909399-9bcb2cab6489?auto=format&fit=crop&w=200`

## Usage

See [unpic-img](https://github.com/ascorbic/unpic-img) for a React component to
use with Next.js
5 changes: 4 additions & 1 deletion src/parse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ import { parse as cloudflare } from "./transformers/cloudflare.ts";
import { parse as bunny } from "./transformers/bunny.ts";
import { parse as storyblok } from "./transformers/storyblok.ts";
import { parse as kontentai } from "./transformers/kontentai.ts";

import { parse as vercel } from "./transformers/vercel.ts";
import { parse as nextjs } from "./transformers/nextjs.ts";
import { ImageCdn, ParsedUrl, SupportedImageCdn, UrlParser } from "./types.ts";

export const parsers = {
Expand All @@ -23,6 +24,8 @@ export const parsers = {
bunny,
storyblok,
"kontent.ai": kontentai,
vercel,
nextjs,
};

export const cdnIsSupportedForParse = (
Expand Down
20 changes: 10 additions & 10 deletions src/transform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,11 @@ import { transform as cloudflare } from "./transformers/cloudflare.ts";
import { transform as bunny } from "./transformers/bunny.ts";
import { transform as storyblok } from "./transformers/storyblok.ts";
import { transform as kontentai } from "./transformers/kontentai.ts";
import { ImageCdn, SupportedImageCdn, UrlTransformer } from "./types.ts";
import { transform as vercel } from "./transformers/vercel.ts";
import { transform as nextjs } from "./transformers/nextjs.ts";
import { ImageCdn, UrlTransformer } from "./types.ts";

export const transformers = {
export const getTransformer = (cdn: ImageCdn) => ({
imgix,
contentful,
"builder.io": builder,
Expand All @@ -21,12 +23,10 @@ export const transformers = {
bunny,
storyblok,
cloudflare,
"kontent.ai": kontentai
};

export const cdnIsSupportedForTransform = (
cdn: ImageCdn | false,
): cdn is SupportedImageCdn => cdn && cdn in transformers;
vercel,
nextjs,
"kontent.ai": kontentai,
}[cdn]);

/**
* Returns a transformer function if the given URL is from a known image CDN
Expand All @@ -41,10 +41,10 @@ export const getTransformerForUrl = (
export const getTransformerForCdn = (
cdn: ImageCdn | false | undefined,
): UrlTransformer | undefined => {
if (!cdn || !cdnIsSupportedForTransform(cdn)) {
if (!cdn) {
return undefined;
}
return transformers[cdn];
return getTransformer(cdn);
};

/**
Expand Down
96 changes: 96 additions & 0 deletions src/transformers/nextjs.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import { assertEquals } from "https://deno.land/[email protected]/testing/asserts.ts";
import { transform } from "./nextjs.ts";

const nextImgLocal =
"https://netlify-plugin-nextjs-demo.netlify.app/_next/image/?url=%2F_next%2Fstatic%2Fmedia%2Funsplash.9a14a3b9.jpg&w=3840&q=75";

const nextImgRemote =
"https://netlify-plugin-nextjs-demo.netlify.app/_next/image/?url=https%3A%2F%2Fimages.unsplash.com%2Fphoto%3Fauto%3Dformat%26fit%3Dcrop%26w%3D200%26q%3D80%26h%3D100&w=384&q=75";

const nextLocal = "/_next/static/image.jpg";

Deno.test("Next.js", async (t) => {
await t.step("should format a local next/image URL", () => {
const result = transform({
url: nextImgLocal,
width: 200,
height: 100,
});
assertEquals(
result?.toString(),
"https://netlify-plugin-nextjs-demo.netlify.app/_next/image/?url=%2F_next%2Fstatic%2Fmedia%2Funsplash.9a14a3b9.jpg&w=200&q=75",
);
});

await t.step("should format a remote next/image URL", () => {
const result = transform({
url: nextImgRemote,
width: 200,
height: 100,
});
assertEquals(
result?.toString(),
"https://images.unsplash.com/photo?auto=format&fit=crop&w=200&q=80&h=100",
);
});

await t.step("should format a local file with next/image", () => {
const result = transform({
url: nextLocal,
width: 200,
height: 100,
});
assertEquals(
result?.toString(),
"/_next/image?url=%2F_next%2Fstatic%2Fimage.jpg&w=200",
);
});

await t.step("should format a local, arbitrary file", () => {
const result = transform({
url: "/profile.png",
width: 200,
height: 100,
});
assertEquals(
result?.toString(),
"/_next/image?url=%2Fprofile.png&w=200",
);
});

await t.step("should format a remote CDN URL", () => {
const result = transform({
url: "https://images.unsplash.com/photo",
width: 200,
height: 100,
});
assertEquals(
result?.toString(),
"https://images.unsplash.com/photo?w=200&h=100&fit=min&auto=format",
);
});

await t.step("should format a remote, non-CDN image next/image", () => {
const result = transform({
url: "https://placekitten.com/100",
width: 200,
height: 100,
});
assertEquals(
result?.toString(),
"/_next/image?url=https%3A%2F%2Fplacekitten.com%2F100&w=200",
);
});

await t.step("should round non-integer dimensions", () => {
const result = transform({
url: nextImgLocal,
width: 200.6,
height: 100.2,
});
assertEquals(
result?.toString(),
"https://netlify-plugin-nextjs-demo.netlify.app/_next/image/?url=%2F_next%2Fstatic%2Fmedia%2Funsplash.9a14a3b9.jpg&w=201&q=75",
);
});
});
12 changes: 12 additions & 0 deletions src/transformers/nextjs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { UrlParser, UrlTransformer } from "../types.ts";
import {
parse as vercelParse,
transform as vercelTransform,
} from "./vercel.ts";
export const parse: UrlParser = (
url,
) => ({ ...vercelParse(url), cdn: "nextjs" });

export const transform: UrlTransformer = (
params,
) => vercelTransform({ ...params, cdn: "nextjs" });
70 changes: 70 additions & 0 deletions src/transformers/vercel.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { assertEquals } from "https://deno.land/[email protected]/testing/asserts.ts";
import { transform } from "./vercel.ts";

const img =
"https://netlify-plugin-nextjs-demo.netlify.app/_vercel/image/?url=%2F_next%2Fstatic%2Fmedia%2Funsplash.9a14a3b9.jpg&w=3840&q=75";

const imgRemote =
"https://netlify-plugin-nextjs-demo.netlify.app/_vercel/image/?url=https%3A%2F%2Fimages.unsplash.com%2Fphoto%3Fauto%3Dformat%26fit%3Dcrop%26w%3D200%26q%3D80%26h%3D100&w=384&q=75";

Deno.test("vercel", async (t) => {
await t.step("should format a local URL", () => {
const result = transform({
url: img,
width: 200,
height: 100,
});
assertEquals(
result?.toString(),
"https://netlify-plugin-nextjs-demo.netlify.app/_vercel/image/?url=%2F_next%2Fstatic%2Fmedia%2Funsplash.9a14a3b9.jpg&w=200&q=75",
);
});

await t.step("should format a remote URL", () => {
const result = transform({
url: imgRemote,
width: 200,
height: 100,
});
assertEquals(
result?.toString(),
"https://images.unsplash.com/photo?auto=format&fit=crop&w=200&q=80&h=100",
);
});

await t.step("should format a remote CDN URL", () => {
const result = transform({
url: "https://images.unsplash.com/photo",
width: 200,
height: 100,
});
assertEquals(
result?.toString(),
"https://images.unsplash.com/photo?w=200&h=100&fit=min&auto=format",
);
});

await t.step("should format a remote, non-CDN image next/image", () => {
const result = transform({
url: "https://placekitten.com/100",
width: 200,
height: 100,
});
assertEquals(
result?.toString(),
"/_vercel/image?url=https%3A%2F%2Fplacekitten.com%2F100&w=200",
);
});

await t.step("should round non-integer dimensions", () => {
const result = transform({
url: img,
width: 200.6,
height: 100.2,
});
assertEquals(
result?.toString(),
"https://netlify-plugin-nextjs-demo.netlify.app/_vercel/image/?url=%2F_next%2Fstatic%2Fmedia%2Funsplash.9a14a3b9.jpg&w=201&q=75",
);
});
});
Loading