-
Notifications
You must be signed in to change notification settings - Fork 33
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add support for delegated URLs (#47)
- Loading branch information
Showing
14 changed files
with
322 additions
and
148 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
# Contributing | ||
|
||
:heart: We love contributions of new CDNs, bug fixes, and improvements to the | ||
code. | ||
|
||
To add new domains or subdomains to an existing CDN, add them to `domains.json` | ||
or `subdomains.json` respectively. | ||
|
||
To add a new CDN, add the following: | ||
|
||
- a new source file in `src/transformers`. This should export a `transform` | ||
function that implements the `UrlTransformer` interface, a `parse` function | ||
that implements the `UrlParser` interface and optionally a `generate` function | ||
that implements the `UrlGenerator` interface. | ||
- if the CDN should delegate remote source images to a different CDN where | ||
possible, implement `delegateUrl` and import it in `src/delegate.ts`. This is | ||
likely to apply to all self-hosted image servers. See the `vercel` transformer | ||
for an example. | ||
- a new test file in `src/transformers`. This should test all of the exported | ||
API functions. | ||
- at least one entry in `domains.json`, `subdomains.json` or `paths.json` to | ||
detect the CDN. Do not include paths that are likely to cause false positives. | ||
e.g. `/assets` is too generic, but `/_mycdn` is ok. | ||
- add the new CDN to the types in `src/types.ts` | ||
- import the new source file in `src/transform.ts` and `src/parse.ts` | ||
- add a sample image to `examples.json` in the demo site. Run the site locally | ||
to see that it works. | ||
- ensure tests pass by installing Deno and running `deno test src` | ||
|
||
### Image defaults | ||
|
||
When generating image URLs, we expect transformers to use the following defaults | ||
if supported, to ensure consistent behaviour across all CDNs: | ||
|
||
- Auto format. If the CDN supports it, then it should deliver the best format | ||
for the browser using content negotiation. If supported, the priority order | ||
should be AVIF, WebP, then the original format. | ||
- Fit = cover. The image should fill the requested dimensions, cropping if | ||
necessary and without distortion. This is the equivalent of the CSS | ||
`object-fit: cover` setting. | ||
- No upscaling. The image should not be upscaled if it is smaller than the | ||
requested dimensions. Instead it should return the largest available size, but | ||
maintain the requested aspect ratio. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,81 @@ | ||
import { | ||
assertEquals, | ||
assertExists, | ||
} from "https://deno.land/std@0.172.0/testing/asserts.ts"; | ||
import { getCanonicalCdnForUrl } from "./canonical.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"; | ||
|
||
Deno.test("Canonical", async (t) => { | ||
await t.step("should detect a local image server", () => { | ||
const result = getCanonicalCdnForUrl(nextImgLocal) || undefined; | ||
assertExists(result); | ||
assertEquals( | ||
result?.url, | ||
nextImgLocal, | ||
); | ||
assertEquals( | ||
result?.cdn, | ||
"nextjs", | ||
); | ||
}); | ||
|
||
await t.step( | ||
"should detect a remote image source with a local image server", | ||
() => { | ||
const result = getCanonicalCdnForUrl(nextImgRemote) || undefined; | ||
assertExists(result); | ||
assertEquals( | ||
result?.url, | ||
"https://images.unsplash.com/photo?auto=format&fit=crop&w=200&q=80&h=100", | ||
); | ||
assertEquals( | ||
result?.cdn, | ||
"imgix", | ||
); | ||
}, | ||
); | ||
|
||
await t.step( | ||
"should fall back to the default CDN for unrecognized image domains", | ||
() => { | ||
const result = | ||
getCanonicalCdnForUrl("https://placekitten.com/100", "vercel") || | ||
undefined; | ||
assertExists(result); | ||
assertEquals( | ||
result?.url, | ||
"https://placekitten.com/100", | ||
); | ||
assertEquals( | ||
result?.cdn, | ||
"vercel", | ||
); | ||
}, | ||
); | ||
|
||
await t.step( | ||
"should fall back to the detected local CDN for unrecognized source image domains", | ||
() => { | ||
const unknownDomain = | ||
"https://example.com/_next/image?url=https%3A%2F%2Fplacekitten.com%2F100&w=200&q=75"; | ||
const result = getCanonicalCdnForUrl( | ||
unknownDomain, | ||
) || | ||
undefined; | ||
assertExists(result); | ||
assertEquals( | ||
result?.url, | ||
unknownDomain, | ||
); | ||
assertEquals( | ||
result?.cdn, | ||
"nextjs", | ||
); | ||
}, | ||
); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
import { getImageCdnForUrl } from "./detect.ts"; | ||
import { CanonicalCdnUrl, ImageCdn, ShouldDelegateUrl } from "./types.ts"; | ||
import { delegateUrl as vercel } from "./transformers/vercel.ts"; | ||
import { delegateUrl as nextjs } from "./transformers/nextjs.ts"; | ||
|
||
// Image servers that might delegate to another CDN | ||
const delegators: Partial<Record<ImageCdn, ShouldDelegateUrl>> = { | ||
vercel, | ||
nextjs, | ||
}; | ||
|
||
export function getDelegatedCdn( | ||
url: string | URL, | ||
cdn: ImageCdn, | ||
): CanonicalCdnUrl | false { | ||
// Most CDNs are authoritative for their own URLs | ||
if (!(cdn in delegators)) { | ||
return false; | ||
} | ||
const maybeDelegate = delegators[cdn]; | ||
if (!maybeDelegate) { | ||
return false; | ||
} | ||
return maybeDelegate(url); | ||
} | ||
|
||
/** | ||
* Gets the canonical URL and CDN for a given image URL, recursing into | ||
* the source image if it is hosted on another CDN. | ||
*/ | ||
export function getCanonicalCdnForUrl( | ||
url: string | URL, | ||
defaultCdn?: ImageCdn | false, | ||
): CanonicalCdnUrl | false { | ||
const cdn = getImageCdnForUrl(url) || defaultCdn; | ||
if (!cdn) { | ||
return false; | ||
} | ||
const maybeDelegated = getDelegatedCdn(url, cdn); | ||
if (maybeDelegated) { | ||
return maybeDelegated; | ||
} | ||
return { cdn, url }; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
import { assertEquals } from "https://deno.land/std@0.172.0/testing/asserts.ts"; | ||
import { transformUrl } from "./transform.ts"; | ||
|
||
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("transformer", async (t) => { | ||
await t.step("should format a remote URL", () => { | ||
const result = transformUrl({ | ||
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 = transformUrl({ | ||
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 = transformUrl({ | ||
url: "https://placekitten.com/100", | ||
width: 200, | ||
height: 100, | ||
cdn: "nextjs", | ||
}); | ||
assertEquals( | ||
result?.toString(), | ||
"/_next/image?url=https%3A%2F%2Fplacekitten.com%2F100&w=200&q=75", | ||
); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.