cdn-media-delivery

SKILL.md

CDN Media Delivery

Guidance for configuring CDN delivery, cache management, and secure media access for headless CMS architectures.

When to Use This Skill

  • Configuring CDN for media delivery
  • Implementing cache invalidation strategies
  • Setting up signed/secure URLs
  • Optimizing edge caching
  • Configuring origin shielding

CDN Architecture

Basic CDN Setup

┌─────────────────────────────────────────────────────────────┐
│                         Users                                │
│              (Global, geographically distributed)            │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│                      CDN Edge Network                        │
│  ┌─────────┐  ┌─────────┐  ┌─────────┐  ┌─────────┐        │
│  │ Edge    │  │ Edge    │  │ Edge    │  │ Edge    │        │
│  │ US-West │  │ US-East │  │ Europe  │  │ Asia    │        │
│  └─────────┘  └─────────┘  └─────────┘  └─────────┘        │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│                      Origin Shield                           │
│              (Optional intermediate cache layer)             │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│                      Origin Server                           │
│  ┌───────────────┐  ┌────────────────┐  ┌───────────────┐  │
│  │ Media API     │  │ Blob Storage   │  │ Image         │  │
│  │ (transform)   │  │ (Azure/S3)     │  │ Processor     │  │
│  └───────────────┘  └────────────────┘  └───────────────┘  │
└─────────────────────────────────────────────────────────────┘

CDN Configuration

Azure CDN (Front Door)

// appsettings.json
{
  "Cdn": {
    "Provider": "AzureFrontDoor",
    "Endpoint": "https://media.example.com",
    "OriginHost": "storage.blob.core.windows.net",
    "CacheRules": {
      "Images": {
        "CacheDuration": "365.00:00:00",
        "QueryStringCaching": "IgnoreQueryString"
      },
      "Transforms": {
        "CacheDuration": "30.00:00:00",
        "QueryStringCaching": "UseQueryString"
      }
    }
  }
}

CloudFront Configuration

public class CloudFrontConfiguration
{
    public string DistributionId { get; set; } = string.Empty;
    public string DomainName { get; set; } = string.Empty;
    public string OriginId { get; set; } = string.Empty;

    public CacheBehavior DefaultCacheBehavior { get; set; } = new()
    {
        ViewerProtocolPolicy = "redirect-to-https",
        CachePolicyId = "658327ea-f89d-4fab-a63d-7e88639e58f6", // CachingOptimized
        Compress = true,
        AllowedMethods = new[] { "GET", "HEAD", "OPTIONS" },
        CachedMethods = new[] { "GET", "HEAD" }
    };

    public CacheBehavior[] CacheBehaviors { get; set; } =
    {
        new()
        {
            PathPattern = "/media/transform/*",
            CachePolicyId = "custom-transform-policy",
            QueryStringCaching = QueryStringCaching.All
        }
    };
}

Cloudflare Configuration

public class CloudflareConfiguration
{
    public string ZoneId { get; set; } = string.Empty;
    public string ApiToken { get; set; } = string.Empty;

    public PageRule[] PageRules { get; set; } =
    {
        new()
        {
            Targets = new[] { "*example.com/media/*" },
            Actions = new PageRuleAction
            {
                CacheLevel = "cache_everything",
                EdgeCacheTtl = 2592000, // 30 days
                BrowserCacheTtl = 86400  // 1 day
            }
        }
    };
}

Cache Headers

Setting Cache Headers

public class MediaCacheMiddleware
{
    public async Task InvokeAsync(HttpContext context, RequestDelegate next)
    {
        await next(context);

        if (context.Request.Path.StartsWithSegments("/media"))
        {
            var cacheControl = GetCacheControl(context.Request.Path);
            context.Response.Headers["Cache-Control"] = cacheControl;
            context.Response.Headers["Vary"] = "Accept, Accept-Encoding";
        }
    }

    private string GetCacheControl(PathString path)
    {
        // Original media: cache for 1 year (immutable content)
        if (path.Value?.Contains("/original/") == true)
        {
            return "public, max-age=31536000, immutable";
        }

        // Transformed images: cache for 30 days
        if (path.Value?.Contains("/transform/") == true)
        {
            return "public, max-age=2592000, stale-while-revalidate=86400";
        }

        // Default: 1 day
        return "public, max-age=86400";
    }
}

Cache-Control Directives

Directive Purpose Example
public Allow CDN caching Images, static assets
private Browser only User-specific content
max-age Cache duration (seconds) max-age=86400 (1 day)
immutable Never revalidate Versioned assets
stale-while-revalidate Serve stale while fetching Background refresh
no-cache Always revalidate Dynamic content
no-store Never cache Sensitive data

Cache Invalidation

Invalidation Service

public interface ICdnInvalidationService
{
    Task InvalidatePathAsync(string path);
    Task InvalidatePathsAsync(IEnumerable<string> paths);
    Task InvalidatePrefixAsync(string prefix);
    Task InvalidateAllAsync();
}

// Azure CDN implementation
public class AzureCdnInvalidationService : ICdnInvalidationService
{
    private readonly CdnManagementClient _cdnClient;

    public async Task InvalidatePathAsync(string path)
    {
        await _cdnClient.Endpoints.PurgeContentAsync(
            _resourceGroup,
            _profileName,
            _endpointName,
            new PurgeParameters(new[] { path }));
    }

    public async Task InvalidatePrefixAsync(string prefix)
    {
        await _cdnClient.Endpoints.PurgeContentAsync(
            _resourceGroup,
            _profileName,
            _endpointName,
            new PurgeParameters(new[] { $"{prefix}/*" }));
    }
}

// CloudFront implementation
public class CloudFrontInvalidationService : ICdnInvalidationService
{
    private readonly AmazonCloudFrontClient _client;

    public async Task InvalidatePathAsync(string path)
    {
        var request = new CreateInvalidationRequest
        {
            DistributionId = _distributionId,
            InvalidationBatch = new InvalidationBatch
            {
                CallerReference = Guid.NewGuid().ToString(),
                Paths = new Paths
                {
                    Items = new List<string> { path },
                    Quantity = 1
                }
            }
        };

        await _client.CreateInvalidationAsync(request);
    }
}

Event-Based Invalidation

public class MediaUpdatedHandler : INotificationHandler<MediaUpdatedEvent>
{
    private readonly ICdnInvalidationService _cdn;

    public async Task Handle(MediaUpdatedEvent notification, CancellationToken ct)
    {
        // Invalidate original
        await _cdn.InvalidatePathAsync($"/media/{notification.MediaId}");

        // Invalidate all transformations
        await _cdn.InvalidatePrefixAsync($"/media/transform/{notification.MediaId}");
    }
}

Signed URLs

Signed URL Generation

public class SignedUrlService
{
    public string GenerateSignedUrl(
        string path,
        TimeSpan validity,
        SignedUrlOptions? options = null)
    {
        options ??= new SignedUrlOptions();

        var expiry = DateTime.UtcNow.Add(validity);
        var expiryTimestamp = new DateTimeOffset(expiry).ToUnixTimeSeconds();

        // Build URL with parameters
        var urlBuilder = new UriBuilder($"{_cdnBaseUrl}{path}");
        var query = HttpUtility.ParseQueryString(urlBuilder.Query);

        query["expires"] = expiryTimestamp.ToString();

        if (options.AllowedIp != null)
        {
            query["ip"] = options.AllowedIp;
        }

        // Generate signature
        var signatureData = $"{path}|{expiryTimestamp}|{options.AllowedIp}";
        var signature = ComputeSignature(signatureData);
        query["signature"] = signature;

        urlBuilder.Query = query.ToString();
        return urlBuilder.ToString();
    }

    private string ComputeSignature(string data)
    {
        using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(_signingKey));
        var hash = hmac.ComputeHash(Encoding.UTF8.GetBytes(data));
        return Convert.ToBase64String(hash)
            .Replace("+", "-")
            .Replace("/", "_")
            .TrimEnd('=');
    }
}

public class SignedUrlOptions
{
    public string? AllowedIp { get; set; }
    public string? AllowedCountry { get; set; }
    public int? MaxDownloads { get; set; }
}

Signed URL Validation

public class SignedUrlValidationMiddleware
{
    public async Task InvokeAsync(HttpContext context, RequestDelegate next)
    {
        if (RequiresSignedUrl(context.Request.Path))
        {
            var query = context.Request.Query;

            // Check expiry
            if (!long.TryParse(query["expires"], out var expiry) ||
                DateTimeOffset.UtcNow.ToUnixTimeSeconds() > expiry)
            {
                context.Response.StatusCode = 403;
                await context.Response.WriteAsync("URL expired");
                return;
            }

            // Validate signature
            var expectedSignature = ComputeSignature(
                context.Request.Path,
                expiry,
                query["ip"]);

            if (query["signature"] != expectedSignature)
            {
                context.Response.StatusCode = 403;
                await context.Response.WriteAsync("Invalid signature");
                return;
            }

            // Check IP restriction
            if (!string.IsNullOrEmpty(query["ip"]))
            {
                var clientIp = context.Connection.RemoteIpAddress?.ToString();
                if (clientIp != query["ip"])
                {
                    context.Response.StatusCode = 403;
                    await context.Response.WriteAsync("IP not allowed");
                    return;
                }
            }
        }

        await next(context);
    }
}

Origin Shielding

Shield Configuration

public class OriginShieldConfiguration
{
    public bool Enabled { get; set; } = true;
    public string ShieldRegion { get; set; } = "us-east-1";
    public int ShieldCacheTtl { get; set; } = 3600; // 1 hour
    public int MaxConnectionsToOrigin { get; set; } = 100;
}

Benefits

Feature Without Shield With Shield
Origin requests From each edge From one region
Cache efficiency Per-edge Shared shield cache
Origin load High Reduced 90%+
Latency Variable Predictable

CDN URL Generation

URL Service

public class CdnUrlService
{
    public string GetMediaUrl(MediaItem media, MediaUrlOptions? options = null)
    {
        options ??= new MediaUrlOptions();

        var path = $"/media/{media.StoragePath}";

        // Add transformation query params
        if (options.Width.HasValue || options.Height.HasValue)
        {
            var query = new List<string>();

            if (options.Width.HasValue) query.Add($"w={options.Width}");
            if (options.Height.HasValue) query.Add($"h={options.Height}");
            if (options.Format.HasValue) query.Add($"format={options.Format}");
            if (options.Quality.HasValue) query.Add($"q={options.Quality}");

            path += "?" + string.Join("&", query);
        }

        // Generate signed URL if private
        if (media.IsPrivate || options.RequireSignature)
        {
            return _signedUrlService.GenerateSignedUrl(
                path,
                options.UrlValidity ?? TimeSpan.FromHours(1));
        }

        return $"{_cdnBaseUrl}{path}";
    }
}

public class MediaUrlOptions
{
    public int? Width { get; set; }
    public int? Height { get; set; }
    public ImageFormat? Format { get; set; }
    public int? Quality { get; set; }
    public bool RequireSignature { get; set; }
    public TimeSpan? UrlValidity { get; set; }
}

Performance Monitoring

CDN Metrics

public class CdnMetrics
{
    public long TotalRequests { get; set; }
    public long CacheHits { get; set; }
    public long CacheMisses { get; set; }
    public double CacheHitRatio => (double)CacheHits / TotalRequests;
    public long BandwidthBytes { get; set; }
    public double AverageLatencyMs { get; set; }
    public Dictionary<string, long> RequestsByRegion { get; set; } = new();
    public Dictionary<int, long> StatusCodeCounts { get; set; } = new();
}

Related Skills

  • media-asset-management - Media storage and organization
  • image-optimization - Image processing before CDN
  • headless-api-design - Media API endpoints
Weekly Installs
1
Installed on
trae1