This guide will help you add new image CDN providers to the library. It covers implementation details, utility functions, and best practices.
Each provider consists of:
- A TypeScript file containing the implementation
(
src/providers/[provider].ts
) and types for provider-specific operations and options. - A test file (
src/providers/[provider].test.ts
) - Example URLs in
src/demo/examples.json
- Detection domains or paths in
data
if appropriate - Adding to the types and exports in:
src/providers/types.ts
src/types.ts
src/extract.ts
src/transform.ts
src/async.ts
deno.jsonc
The library provides several utilities for URL handling:
// Convert strings or URLs to URL objects (handles relative URLs)
const url = toUrl("https://example.com/image.jpg");
// Convert back to string, preserving relativeness
const urlString = toCanonicalUrlString(url);
// Path manipulation
const cleanPath = stripLeadingSlash("/path/to/image.jpg");
const formattedPath = addTrailingSlash("path/to/image");
The most important utility is createOperationsHandlers
, which creates
standardized parser and generator functions:
const { operationsGenerator, operationsParser } = createOperationsHandlers<
ExampleCdnOperations
>({
// Map standard operation names to provider-specific names
keyMap: {
width: "w",
height: "h",
quality: "q",
format: "fmt",
},
// Set default values
defaults: {
quality: 80,
format: "auto",
},
// Normalize format names
formatMap: {
jpg: "jpeg",
},
// Define parameter formatting
kvSeparator: "=", // key=value
paramSeparator: "&", // param1¶m2
});
Let's create a complete example provider "example-cdn":
// example-cdn.ts
import type {
Operations,
URLExtractor,
URLGenerator,
URLTransformer,
} from "../types.ts";
import {
createExtractAndGenerate,
createOperationsHandlers,
toCanonicalUrlString,
toUrl,
} from "../utils.ts";
// Only add NEW operations specific to your provider
export interface ExampleCdnOperations extends Operations {
// Provider-specific operations
specialCrop?: "smart" | "center";
blur?: number;
// DON'T include these - they're in base Operations
// width?: number; ❌
// height?: number; ❌
// quality?: number; ❌
// format?: string; ❌
}
// Optional provider-specific options
export interface ExampleCdnOptions {
baseUrl?: string;
}
// Different parameter formatting styles:
// Query parameters: ?width=100&height=200
const queryStyle = createOperationsHandlers<ExampleCdnOperations>({
keyMap: {
width: "w",
height: "h",
quality: "q",
format: "fmt",
},
defaults: {
quality: 80,
},
kvSeparator: "=",
paramSeparator: "&",
});
// Path segments: /w_100/h_200/q_80
const pathStyle = createOperationsHandlers<ExampleCdnOperations>({
keyMap: {
width: "w",
height: "h",
quality: "q",
format: "fmt",
},
defaults: {
quality: 80,
},
kvSeparator: "_",
paramSeparator: "/",
});
// You can also disable parameters:
const noHeightStyle = createOperationsHandlers<ExampleCdnOperations>({
keyMap: {
width: "w",
height: false, // Height parameter will be removed
quality: "q",
},
});
// Extract operations from existing URL
export const extract: URLExtractor<"example-cdn"> = (url) => {
const parsedUrl = toUrl(url);
const operations = operationsParser(parsedUrl);
parsedUrl.search = "";
return {
src: toCanonicalUrlString(parsedUrl),
operations,
options: {
baseUrl: parsedUrl.origin,
},
};
};
// Generate new URL with operations
export const generate: URLGenerator<"example-cdn"> = (
src,
operations,
options = {},
) => {
const url = toUrl(src, options.baseUrl);
url.search = operationsGenerator(operations);
return toCanonicalUrlString(url);
};
// Transform existing URL with new operations
export const transform: URLTransformer<"example-cdn"> =
createExtractAndGenerate(extract, generate);
// example-cdn.test.ts
import { assertEquals } from "jsr:@std/assert";
import { extract, generate, transform } from "./example-cdn.ts";
import { assertEqualIgnoringQueryOrder } from "../test-utils.ts";
const img = "https://example-cdn.com/image.jpg";
Deno.test("Example CDN", async (t) => {
// Test extraction
await t.step("should extract operations from URL", () => {
const url = `${img}?w=300&h=200&q=80&fmt=webp&specialCrop=smart`;
const result = extract(url);
assertEquals(result, {
src: img,
operations: {
width: 300,
height: 200,
quality: 80,
format: "webp",
specialCrop: "smart",
},
options: {
baseUrl: "https://example-cdn.com",
},
});
});
// Test URL generation
await t.step("should generate URL with operations", () => {
const result = generate(img, {
width: 400,
height: 300,
quality: 90,
specialCrop: "center",
});
assertEqualIgnoringQueryOrder(
result,
`${img}?w=400&h=300&q=90&specialCrop=center`,
);
});
// Test transformation
await t.step("should transform existing URL", () => {
const url = `${img}?w=300&h=200`;
const result = transform(url, {
width: 500,
blur: 5,
});
assertEqualIgnoringQueryOrder(
result,
`${img}?w=500&h=200&blur=5`,
);
});
// Test error cases
await t.step("should handle invalid URLs", () => {
const result = extract("invalid-url");
assertEquals(result, null);
});
// Test relative URLs
await t.step("should handle relative URLs", () => {
const result = generate("/image.jpg", { width: 300 });
assertEqualIgnoringQueryOrder(
result,
"/image.jpg?w=300",
);
});
});
// types.ts
export interface ProviderOperations {
"example-cdn": ExampleCdnOperations;
// ...
}
export interface ProviderOptions {
"example-cdn": ExampleCdnOptions;
// ...
}
// examples.json
{
"example-cdn": [
"Example CDN",
"https://example-cdn.com/demo-image.jpg"
]
}
Providers use different URL patterns for operations:
// Standard query parameters
// https://example.com/image.jpg?width=100&height=200
{
kvSeparator: "=",
paramSeparator: "&"
}
// Path segments
// https://example.com/image/w_100/h_200/image.jpg
{
kvSeparator: "_",
paramSeparator: "/"
}
// Custom separators
// https://example.com/image:w=100,h=200/image.jpg
{
kvSeparator: "=",
paramSeparator: ","
}
Use the provided utilities when:
- You need standard parameter mapping
- Your provider follows common URL patterns
- You want automatic parameter normalization
Create custom handlers when:
- Your provider has unique URL structures
- Parameters have complex interdependencies
- You need special encoding or encryption
- The provider requires custom protocols
Always handle:
- Invalid URLs
- Missing parameters
- Malformed parameters
- Unsupported formats
- Edge cases
- Define clear interfaces extending
Operations
- Only add provider-specific operations
- Use proper TypeScript generics
- Document supported operations
-
Basic Operations
- Width/height resizing
- Format conversion
- Quality settings
- Provider-specific features
-
URL Handling
- Absolute URLs
- Relative URLs
- URL with existing parameters
- Invalid URLs
-
Parameter Edge Cases
- Missing parameters
- Invalid values
- Parameter combinations
- Default values
-
Common Scenarios
- Standard transformations
- Format conversion
- Quality adjustment
- Size constraints
Before submitting:
- Implementation complete with proper types
- Comprehensive tests covering all features
- Types updated in all files listed above
- Example added to examples.json
- Detection domains or paths added if needed
- All tests passing, including unit tests and E2E tests
This project uses Deno for development. If you haven't already, install Deno from https://deno.com.
Basic commands:
# Run tests
deno test
# Run tests with watch mode
deno test --watch
# Type checking
deno check
# Format code
deno fmt
Tests are written using Deno's built-in test framework. Run them from the project root:
# Run all tests
deno test
# Run tests for a specific provider
deno test src/providers/example-cdn.test.ts
# Run E2E tests. These need network access.
deno test --allow-net e2e.test.ts
When implementing a provider, follow these default behaviors for consistency across CDNs. If the provider does not support a feature then it can be omitted, but these are the defaults to aim for so that users have a consistent experience.
- Enable auto format detection/content negotiation when supported
- When supported, priority order for formats should be. For services that
generate images locally, it is ok to prefer WebP over AVIF for performance
reasons.
- AVIF
- WebP
- Original format
Default to fit=cover
behavior (equivalent to CSS object-fit: cover
). This
means:
- Image should fill requested dimensions
- Maintain aspect ratio
- Crop if necessary
- Avoid distortion
- Never upscale images beyond their original dimensions
- Return largest available size when requested size is too large
- Maintain requested aspect ratio even when size is constrained
The project includes a playground application in the demo
directory for
testing providers visually:
- Start the development server:
cd demo
pnpm install
pnpm dev
The playground is crucial for testing as it:
- Provides real-world testing with actual CDN endpoints
- Allows visual verification of image operations
- Tests responsive image behavior
- Verifies URL generation patterns
When adding a new provider:
- Add an example URL to
demo/src/examples.json
- Ideally use a public sample image from the CDN's documentation
- If unavailable, use any publicly-accessible image on that CDN
- Do not skip this - no provider can be added without an example URL, because otherwise it cannot be tested
- Test comprehensively:
- Verify resizing behavior
- Check that defaults are properly applied
- Test format conversion
- Verify responsive behavior
- Ensure upscaling limits work
- Check aspect ratio handling
The E2E tests in e2e.test.ts
verify that providers work with real CDN
endpoints. They use the images from examples.json
to test real operations:
deno test --allow-net e2e.test.ts
If you need help:
- Review existing provider implementations
- Check test files for patterns
- Open an issue for discussion
- Ask questions in pull requests
Remember that clear, well-tested code is more important than clever solutions. Take time to write comprehensive tests and documentation.