Contributing to Unpic
ascorbic/unpicThis guide will help you add new image CDN providers to Unpic. It covers implementation details, utility functions, and best practices.
Overview
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
Core Concepts and Utilities
URL Manipulation
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 relativenessconst urlString = toCanonicalUrlString(url);
// Path manipulationconst cleanPath = stripLeadingSlash("/path/to/image.jpg");const formattedPath = addTrailingSlash("path/to/image");
Operations Handlers
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 });
Step-by-Step Implementation
Let’s create a complete example provider “example-cdn”:
1. Define Operations Interface
import type { Operations, URLExtractor, URLGenerator, URLTransformer,} from "../types.ts";import { createExtractAndGenerate, createOperationsHandlers, toCanonicalUrlString, toUrl,} from "../utils.ts";
// Only add NEW operations specific to your providerexport 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 optionsexport interface ExampleCdnOptions { baseUrl?: string;}
2. Configure Operations Handlers
// Different parameter formatting styles:
// Query parameters: ?width=100&height=200const queryStyle = createOperationsHandlers<ExampleCdnOperations>({ keyMap: { width: "w", height: "h", quality: "q", format: "fmt", }, defaults: { quality: 80, }, kvSeparator: "=", paramSeparator: "&",});
// Path segments: /w_100/h_200/q_80const 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", },});
3. Implement Core Functions
// Extract operations from existing URLexport 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 operationsexport 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 operationsexport const transform: URLTransformer<"example-cdn"> = createExtractAndGenerate(extract, generate);
4. Add Comprehensive Tests
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"); });});
5. Update Types and Add Examples
export interface ProviderOperations { "example-cdn": ExampleCdnOperations; // ...}
export interface ProviderOptions { "example-cdn": ExampleCdnOptions; // ...}
{ "example-cdn": ["Example CDN", "https://example-cdn.com/demo-image.jpg"]}
Parameter Handling Patterns
Query Parameters vs Path Segments
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: ","}
Best Practices
When to Use Utilities
Use the provided utilities when:
- You need standard parameter mapping
- Your provider follows common URL patterns
- You want automatic parameter normalization
When to Create Custom Solutions
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
Error Handling
Always handle:
- Invalid URLs
- Missing parameters
- Malformed parameters
- Unsupported formats
- Edge cases
Type Safety
- Define clear interfaces extending
Operations
- Only add provider-specific operations
- Use proper TypeScript generics
- Document supported operations
Testing Requirements
-
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
Final Checklist
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
Development Environment
Deno Setup
This project uses Deno for development. If you haven’t already, install Deno from https://deno.com.
Basic commands:
# Run testsdeno test
# Run tests with watch modedeno test --watch
# Type checkingdeno check
# Format codedeno fmt
Running Tests
Tests are written using Deno’s built-in test framework. Run them from the project root:
# Run all testsdeno test
# Run tests for a specific providerdeno test src/providers/example-cdn.test.ts
# Run E2E tests. These need network access.deno test --allow-net e2e.test.ts
Image Defaults
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.
Format Handling
- 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
Image Fitting
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
Size Handling
- 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
Local Development Server
The project includes a playground application in the demo
directory for
testing providers visually:
- Start the development server:
cd demopnpm installpnpm 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
End-to-End Testing
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
Getting Help
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.