├── sample ├── wwwroot │ ├── js │ │ ├── site.min.js │ │ └── site.js │ ├── favicon.ico │ ├── css │ │ └── site.css │ └── images │ │ ├── banner2.svg │ │ ├── banner1.svg │ │ ├── banner3.svg │ │ └── banner4.svg ├── Pages │ ├── _ViewStart.cshtml │ ├── _ViewImports.cshtml │ ├── Index.cshtml.cs │ ├── _Layout.cshtml │ └── Index.cshtml ├── appsettings.json ├── WebEssentials.CdnTagHelpers.Sample.csproj ├── Program.cs ├── Properties │ └── launchSettings.json └── Startup.cs ├── art ├── cdn.png └── logo.png ├── LICENSE ├── src ├── CdnHelper.cs ├── CdnifyTagHelper.cs ├── PreConnectTagHelper.cs ├── WebEssentials.AspNetCore.CdnTagHelpers.csproj ├── StyleTagHelper.cs └── CdnTagHelper.cs ├── appveyor.yml ├── WebEssentials.AspNetCore.CdnTagHelpers.sln ├── .gitattributes ├── README.md ├── .editorconfig └── .gitignore /sample/wwwroot/js/site.min.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /sample/wwwroot/js/site.js: -------------------------------------------------------------------------------- 1 | // Write your Javascript code. 2 | -------------------------------------------------------------------------------- /sample/Pages/_ViewStart.cshtml: -------------------------------------------------------------------------------- 1 | @{ 2 | Layout = "_Layout"; 3 | } 4 | -------------------------------------------------------------------------------- /art/cdn.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/madskristensen/WebEssentials.AspNetCore.CdnTagHelpers/HEAD/art/cdn.png -------------------------------------------------------------------------------- /art/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/madskristensen/WebEssentials.AspNetCore.CdnTagHelpers/HEAD/art/logo.png -------------------------------------------------------------------------------- /sample/wwwroot/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/madskristensen/WebEssentials.AspNetCore.CdnTagHelpers/HEAD/sample/wwwroot/favicon.ico -------------------------------------------------------------------------------- /sample/Pages/_ViewImports.cshtml: -------------------------------------------------------------------------------- 1 | @using WebEssentials.CdnTagHelpers.Sample 2 | @namespace WebEssentials.CdnTagHelpers.Sample.Pages 3 | @addTagHelper *, WebEssentials.AspNetCore.CdnTagHelpers 4 | @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers 5 | -------------------------------------------------------------------------------- /sample/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "IncludeScopes": false, 4 | "LogLevel": { 5 | "Default": "Warning" 6 | } 7 | }, 8 | "cdn": { 9 | "url": "https://foo.com", 10 | "prefetch": true 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /sample/Pages/Index.cshtml.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Microsoft.AspNetCore.Mvc; 6 | using Microsoft.AspNetCore.Mvc.RazorPages; 7 | 8 | namespace WebEssentials.CdnTagHelpers.Sample.Pages 9 | { 10 | public class IndexModel : PageModel 11 | { 12 | public void OnGet() 13 | { 14 | 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /sample/Pages/_Layout.cshtml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | @ViewData["Title"] - WebEssentials.CdnTagHelpers.Sample 7 | 8 | 9 | 10 | 11 |
12 | @RenderBody() 13 |
14 | 15 | @RenderSection("Scripts", required: false) 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2017 LigerShark 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. -------------------------------------------------------------------------------- /sample/WebEssentials.CdnTagHelpers.Sample.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | netcoreapp2.0 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /sample/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Threading.Tasks; 6 | using Microsoft.AspNetCore; 7 | using Microsoft.AspNetCore.Hosting; 8 | using Microsoft.Extensions.Configuration; 9 | using Microsoft.Extensions.Logging; 10 | 11 | namespace WebEssentials.CdnTagHelpers.Sample 12 | { 13 | public class Program 14 | { 15 | public static void Main(string[] args) 16 | { 17 | BuildWebHost(args).Run(); 18 | } 19 | 20 | public static IWebHost BuildWebHost(string[] args) => 21 | WebHost.CreateDefaultBuilder(args) 22 | .UseStartup() 23 | .Build(); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /sample/Pages/Index.cshtml: -------------------------------------------------------------------------------- 1 | @page 2 | @model IndexModel 3 | @{ 4 | ViewData["Title"] = "Home page"; 5 | } 6 | 7 |

Home page

8 | Banner 9 | 10 | 11 | 12 | 13 | 14 | 19 | 20 | 21 |
22 | Banner2 23 |
24 | 25 | Pixel 26 | 27 | -------------------------------------------------------------------------------- /sample/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "iisSettings": { 3 | "windowsAuthentication": false, 4 | "anonymousAuthentication": true, 5 | "iisExpress": { 6 | "applicationUrl": "http://localhost:2490/", 7 | "sslPort": 0 8 | } 9 | }, 10 | "profiles": { 11 | "IIS Express": { 12 | "commandName": "IISExpress", 13 | "launchBrowser": true, 14 | "environmentVariables": { 15 | "ASPNETCORE_ENVIRONMENT": "Development" 16 | } 17 | }, 18 | "WebEssentials.CdnTagHelpers.Sample": { 19 | "commandName": "Project", 20 | "launchBrowser": true, 21 | "environmentVariables": { 22 | "ASPNETCORE_ENVIRONMENT": "Development" 23 | }, 24 | "applicationUrl": "http://localhost:2491/" 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /sample/wwwroot/css/site.css: -------------------------------------------------------------------------------- 1 | body { 2 | padding-top: 50px; 3 | padding-bottom: 20px; 4 | } 5 | 6 | /* Wrapping element */ 7 | /* Set some basic padding to keep content from hitting the edges */ 8 | .body-content { 9 | padding-left: 15px; 10 | padding-right: 15px; 11 | } 12 | 13 | /* Carousel */ 14 | .carousel-caption p { 15 | font-size: 20px; 16 | line-height: 1.4; 17 | } 18 | 19 | /* Make .svg files in the carousel display properly in older browsers */ 20 | .carousel-inner .item img[src$=".svg"] { 21 | width: 100%; 22 | } 23 | 24 | /* QR code generator */ 25 | #qrCode { 26 | margin: 15px; 27 | } 28 | 29 | /* Hide/rearrange for smaller screens */ 30 | @media screen and (max-width: 767px) { 31 | /* Hide captions */ 32 | .carousel-caption { 33 | display: none; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/CdnHelper.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using System.Text.RegularExpressions; 4 | 5 | namespace WebEssentials.AspNetCore.CdnTagHelpers 6 | { 7 | public static class CdnHelper 8 | { 9 | public static string CdnifyHtmlImageUrls(this string html, string cdnUrl) 10 | { 11 | string result = html; 12 | MatchCollection matchCollection = Regex.Matches(result, "]+src=\"(?[^\"]+)\"[^>]+>"); 13 | IEnumerable matches = new List(matchCollection.Cast()).ToArray().Reverse(); 14 | 15 | foreach (Match match in matches) 16 | { 17 | Group group = match.Groups["src"]; 18 | string value = group.Value; 19 | 20 | if (value.Contains("://") || value.StartsWith("//") || value.StartsWith("data:")) 21 | continue; 22 | 23 | string sep = value.StartsWith("/") ? "" : "/"; 24 | 25 | result = result.Insert(group.Index, $"{cdnUrl.TrimEnd('/')}{sep}"); 26 | } 27 | 28 | return result; 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | image: Visual Studio 2017 2 | version: 1.0.{build} 3 | 4 | install: 5 | # .NET Core SDK binaries 6 | - ps: $urlCurrent = "https://aka.ms/dotnet-sdk-2.0.0-win-x64" 7 | - ps: $env:DOTNET_INSTALL_DIR = "$pwd\.dotnetsdk" 8 | - ps: mkdir $env:DOTNET_INSTALL_DIR -Force | Out-Null 9 | - ps: $tempFileCurrent = [System.IO.Path]::GetTempFileName() 10 | - ps: (New-Object System.Net.WebClient).DownloadFile($urlCurrent, $tempFileCurrent) 11 | - ps: Add-Type -AssemblyName System.IO.Compression.FileSystem; [System.IO.Compression.ZipFile]::ExtractToDirectory($tempFileCurrent, $env:DOTNET_INSTALL_DIR) 12 | - ps: $env:Path = "$env:DOTNET_INSTALL_DIR;$env:Path" 13 | 14 | build_script: 15 | - dotnet restore -v quiet 16 | - ps: dotnet build /p:configuration=Release /p:Version=$($env:appveyor_build_version) 17 | 18 | test: off 19 | 20 | artifacts: 21 | - path: src\**\bin\release\*.nupkg 22 | name: NuGet package 23 | 24 | deploy: 25 | - provider: NuGet 26 | artifact: /NuGet/ 27 | api_key: 28 | secure: 6xBu/05BWJDPa2PRkxEH3PHU/caLvy2lzf1eWdRBGXwSCTejHtI+6e0V4s2LaVri 29 | on: 30 | branch: master 31 | appveyor_repo_commit_message_extended: /\[release\]/ -------------------------------------------------------------------------------- /src/CdnifyTagHelper.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Microsoft.AspNetCore.Razor.TagHelpers; 3 | using Microsoft.Extensions.Configuration; 4 | 5 | namespace WebEssentials.AspNetCore.CdnTagHelpers 6 | { 7 | [HtmlTargetElement("*", Attributes = _attrName)] 8 | public class CdnifyTagHelper : TagHelper 9 | { 10 | private string _cdnUrl; 11 | private const string _attrName = "cdnify"; 12 | 13 | public CdnifyTagHelper(IConfiguration config) 14 | { 15 | _cdnUrl = config["cdn:url"]; 16 | } 17 | 18 | public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output) 19 | { 20 | output?.Attributes?.RemoveAll(_attrName); 21 | 22 | if (string.IsNullOrWhiteSpace(_cdnUrl) || string.IsNullOrEmpty(output.TagName)) 23 | { 24 | return; 25 | } 26 | 27 | string html = output.Content.IsModified ? output.Content.GetContent() : (await output.GetChildContentAsync()).GetContent(); 28 | string cdnified = html.CdnifyHtmlImageUrls(_cdnUrl); 29 | 30 | output.Content.SetHtmlContent(cdnified); 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/PreConnectTagHelper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.AspNetCore.Html; 3 | using Microsoft.AspNetCore.Razor.TagHelpers; 4 | using Microsoft.Extensions.Configuration; 5 | 6 | namespace WebEssentials.AspNetCore.CdnTagHelpers 7 | { 8 | [HtmlTargetElement("head")] 9 | public class PreConnectTagHelper : TagHelper 10 | { 11 | private string _cdnUrl; 12 | private string _dnsPrefetch; 13 | 14 | public PreConnectTagHelper(IConfiguration config) 15 | { 16 | _cdnUrl = config["cdn:url"]; 17 | _dnsPrefetch = config["cdn:prefetch"]; 18 | } 19 | 20 | public override void Process(TagHelperContext context, TagHelperOutput output) 21 | { 22 | if (_dnsPrefetch == "False" || // opted out manually 23 | string.IsNullOrWhiteSpace(_cdnUrl) || 24 | string.IsNullOrEmpty(output.TagName)) 25 | { 26 | return; 27 | } 28 | 29 | var url = new Uri(_cdnUrl, UriKind.Absolute); 30 | var link = new HtmlString($""); 31 | 32 | output.PreContent.AppendHtml(link); 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/WebEssentials.AspNetCore.CdnTagHelpers.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard1.5 5 | True 6 | cdn, azure, cloud, performance, speed 7 | 1.0.0 8 | Mads Kristensen 9 | Tag Helpers for ASP.NET Core that makes it easy to work with CDNs 10 | Copyright © Mads Kristensen 11 | WebEssentials.AspNetCore.CdnTagHelpers 12 | WebEssentials.AspNetCore.CdnTagHelpers 13 | https://raw.githubusercontent.com/madskristensen/WebEssentials.AspNetCore.CdnTagHelpers/master/art/logo.png 14 | https://github.com/madskristensen/WebEssentials.AspNetCore.CdnTagHelpers 15 | https://github.com/madskristensen/WebEssentials.AspNetCore.CdnTagHelpers/blob/master/LICENSE 16 | https://github.com/madskristensen/WebEssentials.AspNetCore.CdnTagHelpers 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /sample/Startup.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Microsoft.AspNetCore.Builder; 6 | using Microsoft.AspNetCore.Hosting; 7 | using Microsoft.Extensions.Configuration; 8 | using Microsoft.Extensions.DependencyInjection; 9 | 10 | namespace WebEssentials.CdnTagHelpers.Sample 11 | { 12 | public class Startup 13 | { 14 | public Startup(IConfiguration configuration) 15 | { 16 | Configuration = configuration; 17 | } 18 | 19 | public IConfiguration Configuration { get; } 20 | 21 | // This method gets called by the runtime. Use this method to add services to the container. 22 | public void ConfigureServices(IServiceCollection services) 23 | { 24 | services.AddMvc(); 25 | } 26 | 27 | // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. 28 | public void Configure(IApplicationBuilder app, IHostingEnvironment env) 29 | { 30 | if (env.IsDevelopment()) 31 | { 32 | app.UseDeveloperExceptionPage(); 33 | app.UseBrowserLink(); 34 | } 35 | 36 | app.UseStaticFiles(); 37 | 38 | app.UseMvc(routes => 39 | { 40 | routes.MapRoute( 41 | name: "default", 42 | template: "{controller}/{action=Index}/{id?}"); 43 | }); 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/StyleTagHelper.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using System.Text.RegularExpressions; 4 | using System.Threading.Tasks; 5 | using Microsoft.AspNetCore.Razor.TagHelpers; 6 | using Microsoft.Extensions.Configuration; 7 | 8 | namespace WebEssentials.AspNetCore.CdnTagHelpers 9 | { 10 | [HtmlTargetElement("style")] 11 | [HtmlTargetElement("link", Attributes = "inline")] // Support for LigerShark.WebOptimizer 12 | public class StyleTagHelper : TagHelper 13 | { 14 | private string _cdnUrl; 15 | private static readonly Regex _rxUrl = new Regex(@"url\s*\(\s*([""']?)([^:)]+)\1\s*\)", RegexOptions.IgnoreCase | RegexOptions.Compiled); 16 | 17 | public StyleTagHelper(IConfiguration config) 18 | { 19 | _cdnUrl = config["cdn:url"]; 20 | } 21 | 22 | public override int Order => base.Order + 100; 23 | 24 | public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output) 25 | { 26 | if (string.IsNullOrWhiteSpace(_cdnUrl) || output.TagName != "style") 27 | { 28 | return; 29 | } 30 | 31 | TagHelperContent content = await output.GetChildContentAsync(); 32 | string css = content.GetContent(); 33 | 34 | IEnumerable matches = _rxUrl.Matches(content.GetContent()).Cast().Reverse(); 35 | 36 | foreach (Match match in matches) 37 | { 38 | Group group = match.Groups[2]; 39 | string value = group.Value; 40 | 41 | // Ignore references with protocols 42 | if (value.Contains("://") || value.StartsWith("//") || value.StartsWith("data:")) 43 | continue; 44 | 45 | string sep = value.StartsWith("/") ? "" : "/"; 46 | 47 | css = css.Insert(group.Index, $"{_cdnUrl.TrimEnd('/')}{sep}"); 48 | } 49 | 50 | output.Content.SetHtmlContent(css); 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /WebEssentials.AspNetCore.CdnTagHelpers.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | VisualStudioVersion = 15.0.26914.1 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WebEssentials.AspNetCore.CdnTagHelpers", "src\WebEssentials.AspNetCore.CdnTagHelpers.csproj", "{4A9EAAD6-167C-4E0A-A907-574721D0B9C2}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WebEssentials.CdnTagHelpers.Sample", "sample\WebEssentials.CdnTagHelpers.Sample.csproj", "{D6E08A64-26EC-4B95-89BC-DB31F2B8CB78}" 9 | EndProject 10 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{6B8EAFBA-E041-44C7-B584-3D73A5D6E1FD}" 11 | ProjectSection(SolutionItems) = preProject 12 | .editorconfig = .editorconfig 13 | appveyor.yml = appveyor.yml 14 | README.md = README.md 15 | EndProjectSection 16 | EndProject 17 | Global 18 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 19 | Debug|Any CPU = Debug|Any CPU 20 | Release|Any CPU = Release|Any CPU 21 | EndGlobalSection 22 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 23 | {4A9EAAD6-167C-4E0A-A907-574721D0B9C2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 24 | {4A9EAAD6-167C-4E0A-A907-574721D0B9C2}.Debug|Any CPU.Build.0 = Debug|Any CPU 25 | {4A9EAAD6-167C-4E0A-A907-574721D0B9C2}.Release|Any CPU.ActiveCfg = Release|Any CPU 26 | {4A9EAAD6-167C-4E0A-A907-574721D0B9C2}.Release|Any CPU.Build.0 = Release|Any CPU 27 | {D6E08A64-26EC-4B95-89BC-DB31F2B8CB78}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 28 | {D6E08A64-26EC-4B95-89BC-DB31F2B8CB78}.Debug|Any CPU.Build.0 = Debug|Any CPU 29 | {D6E08A64-26EC-4B95-89BC-DB31F2B8CB78}.Release|Any CPU.ActiveCfg = Release|Any CPU 30 | {D6E08A64-26EC-4B95-89BC-DB31F2B8CB78}.Release|Any CPU.Build.0 = Release|Any CPU 31 | EndGlobalSection 32 | GlobalSection(SolutionProperties) = preSolution 33 | HideSolutionNode = FALSE 34 | EndGlobalSection 35 | GlobalSection(ExtensibilityGlobals) = postSolution 36 | SolutionGuid = {05D784C3-0431-4ADB-A300-F7513939DC99} 37 | EndGlobalSection 38 | EndGlobal 39 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=auto 5 | 6 | ############################################################################### 7 | # Set default behavior for command prompt diff. 8 | # 9 | # This is need for earlier builds of msysgit that does not have it on by 10 | # default for csharp files. 11 | # Note: This is only used by command line 12 | ############################################################################### 13 | #*.cs diff=csharp 14 | 15 | ############################################################################### 16 | # Set the merge driver for project and solution files 17 | # 18 | # Merging from the command prompt will add diff markers to the files if there 19 | # are conflicts (Merging from VS is not affected by the settings below, in VS 20 | # the diff markers are never inserted). Diff markers may cause the following 21 | # file extensions to fail to load in VS. An alternative would be to treat 22 | # these files as binary and thus will always conflict and require user 23 | # intervention with every merge. To do so, just uncomment the entries below 24 | ############################################################################### 25 | #*.sln merge=binary 26 | #*.csproj merge=binary 27 | #*.vbproj merge=binary 28 | #*.vcxproj merge=binary 29 | #*.vcproj merge=binary 30 | #*.dbproj merge=binary 31 | #*.fsproj merge=binary 32 | #*.lsproj merge=binary 33 | #*.wixproj merge=binary 34 | #*.modelproj merge=binary 35 | #*.sqlproj merge=binary 36 | #*.wwaproj merge=binary 37 | 38 | ############################################################################### 39 | # behavior for image files 40 | # 41 | # image files are treated as binary by default. 42 | ############################################################################### 43 | #*.jpg binary 44 | #*.png binary 45 | #*.gif binary 46 | 47 | ############################################################################### 48 | # diff behavior for common document formats 49 | # 50 | # Convert binary document formats to text before diffing them. This feature 51 | # is only available from the command line. Turn it on by uncommenting the 52 | # entries below. 53 | ############################################################################### 54 | #*.doc diff=astextplain 55 | #*.DOC diff=astextplain 56 | #*.docx diff=astextplain 57 | #*.DOCX diff=astextplain 58 | #*.dot diff=astextplain 59 | #*.DOT diff=astextplain 60 | #*.pdf diff=astextplain 61 | #*.PDF diff=astextplain 62 | #*.rtf diff=astextplain 63 | #*.RTF diff=astextplain 64 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ASP.NET Core CDN helpers 2 | 3 | [![Build status](https://ci.appveyor.com/api/projects/status/txquc7aq1kgweap7?svg=true)](https://ci.appveyor.com/project/madskristensen/webessentials-aspnetcore-cdntaghelpers) 4 | [![NuGet](https://img.shields.io/nuget/v/WebEssentials.AspNetCore.CdnTagHelpers.svg)](https://nuget.org/packages/WebEssentials.AspNetCore.CdnTagHelpers/) 5 | 6 | This NuGet package makes it painless to use a Content Delivery Network (CDN) to serve static files from any ASP.NET Core web application no matter where it is hosted. 7 | 8 | Using a CDN to serve static resoruces (JS, CSS and image files) can significantly speed up the delivery of content to your users by serving those resources from edge servers located in data centers around the world. This reduces latency by a wide margin. 9 | 10 | ![CDN chart](art/cdn.png) 11 | 12 | Using a CDN has never been cheaper and with this NuGet package it is now super easy to set up. 13 | 14 | Read more about [Content Delivery Networks here](https://docs.microsoft.com/en-us/azure/architecture/best-practices/cdn). 15 | 16 | ## Getting started 17 | It's easy to use a CDN in your ASP.NET Core web application. Here's how to get started. 18 | 19 | ### 1. Setup a CDN 20 | We recommend you use the Azure CDN ([try it for free now](https://azure.microsoft.com/en-us/free/?utm_source=github&utm_medium=referral&utm_campaign=aspnetwt-cdntaghelpers)), but any CDN supporting *reverse proxying* or *origin push* will work. 21 | 22 | > Keep in mind that you don't need to host your website on Azure in order to use the Azure CDN. 23 | 24 | When using the Azure CDN, you will get an endpoint URL that looks something like this: `https://myname.azureedge.net`. You need that URL in step 2. 25 | 26 | ### 2. Register the Tag Helpers 27 | Do that by adding this line to the **_ViewImports.cshtml** file: 28 | 29 | ```csharp 30 | @addTagHelper *, WebEssentials.AspNetCore.CdnTagHelpers 31 | ``` 32 | 33 | Then add he CDN url to the appsettings.json file: 34 | 35 | ```json 36 | { 37 | "cdn": { 38 | "url": "https://myname.azureedge.net" 39 | } 40 | } 41 | ``` 42 | 43 | That's it. Now all your static assets are being requested directly from the CDN instead of locally from your website. 44 | 45 | ### 3. Verify it works 46 | Run the page in the browser and make sure that all JavaScript, CSS and image references have been modified to now point to the CDN. 47 | 48 | You can do that by looking at the HTML source code. There should no longer by any relative file references, like this one: 49 | 50 | ```html 51 | 52 | ``` 53 | 54 | ...but instead it should look like this: 55 | 56 | ```html 57 | 58 | ``` 59 | 60 | ## Dynamic HTML 61 | If HTML is generated from a database or other source where you don't control the markup, you can still cdnify the image elements by using the `cdnify` attribute like so: 62 | 63 | ```html 64 |
65 | @Model.Post 66 |
67 | ``` 68 | 69 | ## Configuration 70 | Use the **appsettings.json** file to store the configuration settings. 71 | 72 | ```json 73 | { 74 | "cdn": { 75 | "url": "https://myname.azureedge.net", 76 | "prefetch": true 77 | } 78 | } 79 | ``` 80 | 81 | That makes it easy to disable CDN locally when developing and only enable it in production by adding the config to the **appsettings.production.json** file. 82 | 83 | **url**: An absolute URL that is used to prefix all static file references with. 84 | 85 | **prefetch**: Will by default inject a DNS prefetch link to the `` of the document to speed up the DNS resolution. Set to `false` to disable. 86 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome:http://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | # Don't use tabs for indentation. 7 | [*] 8 | indent_style = space 9 | end_of_line = crlf 10 | # (Please don't specify an indent_size here; that has too many unintended consequences.) 11 | 12 | # Code files 13 | [*.{cs,csx,vb,vbx}] 14 | indent_size = 4 15 | 16 | # Xml project files 17 | [*.{csproj,vbproj,vcxproj,vcxproj.filters,proj,projitems,shproj}] 18 | indent_size = 2 19 | 20 | # Xml config files 21 | [*.{props,targets,ruleset,config,nuspec,resx,vsixmanifest,vsct}] 22 | indent_size = 2 23 | 24 | # JSON files 25 | [*.json] 26 | indent_size = 2 27 | 28 | # Dotnet code style settings: 29 | [*.{cs,vb}] 30 | # Sort using and Import directives with System.* appearing first 31 | dotnet_sort_system_directives_first = true 32 | 33 | # Avoid "this." and "Me." if not necessary 34 | dotnet_style_qualification_for_field = false : suggestion 35 | dotnet_style_qualification_for_property = false : suggestion 36 | dotnet_style_qualification_for_method = false : suggestion 37 | dotnet_style_qualification_for_event = false : suggestion 38 | 39 | # Use language keywords instead of framework type names for type references 40 | dotnet_style_predefined_type_for_locals_parameters_members = true : suggestion 41 | dotnet_style_predefined_type_for_member_access = true : suggestion 42 | 43 | # Suggest more modern language features when available 44 | dotnet_style_object_initializer = true : suggestion 45 | dotnet_style_collection_initializer = true : suggestion 46 | dotnet_style_coalesce_expression = true : suggestion 47 | dotnet_style_null_propagation = true : suggestion 48 | dotnet_style_explicit_tuple_names = true : suggestion 49 | 50 | # Naming rules - async methods to be prefixed with Async 51 | dotnet_naming_rule.async_methods_must_end_with_async.severity = warning 52 | dotnet_naming_rule.async_methods_must_end_with_async.symbols = method_symbols 53 | dotnet_naming_rule.async_methods_must_end_with_async.style = end_in_async_style 54 | 55 | dotnet_naming_symbols.method_symbols.applicable_kinds = method 56 | dotnet_naming_symbols.method_symbols.required_modifiers = async 57 | 58 | dotnet_naming_style.end_in_async_style.capitalization = pascal_case 59 | dotnet_naming_style.end_in_async_style.required_suffix = Async 60 | 61 | # Naming rules - private fields must start with an underscore 62 | dotnet_naming_rule.field_must_start_with_underscore.severity = warning 63 | dotnet_naming_rule.field_must_start_with_underscore.symbols = private_fields 64 | dotnet_naming_rule.field_must_start_with_underscore.style = start_underscore_style 65 | 66 | dotnet_naming_symbols.private_fields.applicable_kinds = field 67 | dotnet_naming_symbols.private_fields.applicable_accessibilities = private 68 | 69 | dotnet_naming_style.start_underscore_style.capitalization = camel_case 70 | dotnet_naming_style.start_underscore_style.required_prefix = _ 71 | 72 | # CSharp code style settings: 73 | [*.cs] 74 | # Prefer "var" everywhere 75 | csharp_style_var_for_built_in_types = false : suggestion 76 | csharp_style_var_when_type_is_apparent = true : suggestion 77 | csharp_style_var_elsewhere = false : suggestion 78 | 79 | # Prefer method-like constructs to have a block body 80 | csharp_style_expression_bodied_methods = false : none 81 | csharp_style_expression_bodied_constructors = false : none 82 | csharp_style_expression_bodied_operators = false : none 83 | 84 | # Prefer property-like constructs to have an expression-body 85 | csharp_style_expression_bodied_properties = true : none 86 | csharp_style_expression_bodied_indexers = true : none 87 | csharp_style_expression_bodied_accessors = true : none 88 | 89 | # Suggest more modern language features when available 90 | csharp_style_pattern_matching_over_is_with_cast_check = true : suggestion 91 | csharp_style_pattern_matching_over_as_with_null_check = true : suggestion 92 | csharp_style_inlined_variable_declaration = true : suggestion 93 | csharp_style_throw_expression = true : suggestion 94 | csharp_style_conditional_delegate_call = true : suggestion 95 | 96 | # Newline settings 97 | csharp_new_line_before_open_brace = all 98 | csharp_new_line_before_else = true 99 | csharp_new_line_before_catch = true 100 | csharp_new_line_before_finally = true 101 | csharp_new_line_before_members_in_object_initializers = true 102 | csharp_new_line_before_members_in_anonymous_types = true -------------------------------------------------------------------------------- /src/CdnTagHelper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Text.Encodings.Web; 5 | using Microsoft.AspNetCore.Html; 6 | using Microsoft.AspNetCore.Razor.TagHelpers; 7 | using Microsoft.Extensions.Configuration; 8 | 9 | namespace WebEssentials.AspNetCore.CdnTagHelpers 10 | { 11 | [HtmlTargetElement("img")] 12 | [HtmlTargetElement("audio")] 13 | [HtmlTargetElement("video")] 14 | [HtmlTargetElement("track")] 15 | [HtmlTargetElement("source")] 16 | [HtmlTargetElement("link", Attributes = "[rel=stylesheet]")] 17 | [HtmlTargetElement("link", Attributes = "[rel=alternate]")] 18 | [HtmlTargetElement("link", Attributes = "[rel=preload]")] 19 | [HtmlTargetElement("link", Attributes = "[rel$=image]")] 20 | [HtmlTargetElement("link", Attributes = "[rel$=icon]")] 21 | [HtmlTargetElement("link", Attributes = "[rel$=-icon-precomposed]")] 22 | [HtmlTargetElement("meta", Attributes = "[name$=image]")] 23 | [HtmlTargetElement("meta", Attributes = "[property=image]")] 24 | [HtmlTargetElement("script")] 25 | [HtmlTargetElement("*", Attributes = "cdn-prop")] 26 | public class CdnTagHelper : TagHelper 27 | { 28 | private string _cdnUrl; 29 | 30 | private static readonly Dictionary _attributes = new Dictionary(StringComparer.OrdinalIgnoreCase) 31 | { 32 | { "audio", new[] { "src" } }, 33 | { "embed", new[] { "src" } }, 34 | { "img", new[] { "src", "srcset" } }, 35 | { "input", new[] { "src" } }, 36 | { "link", new[] { "href" } }, 37 | { "meta", new[] { "content" } }, 38 | { "menuitem", new[] { "icon" } }, 39 | { "script", new[] { "src" } }, 40 | { "source", new[] { "src", "srcset" } }, 41 | { "track", new[] { "src" } }, 42 | { "video", new[] { "poster", "src" } }, 43 | }; 44 | 45 | public CdnTagHelper(IConfiguration config) 46 | { 47 | _cdnUrl = config["cdn:url"]; 48 | } 49 | 50 | public override int Order => int.MaxValue; 51 | 52 | public override void Process(TagHelperContext context, TagHelperOutput output) 53 | { 54 | if (string.IsNullOrWhiteSpace(_cdnUrl) || string.IsNullOrEmpty(output.TagName)) 55 | { 56 | return; 57 | } 58 | 59 | if (output.Attributes.ContainsName("no-cdn")) 60 | { 61 | output.Attributes.RemoveAll("cdn-prop"); 62 | output.Attributes.RemoveAll("no-cdn"); 63 | return; 64 | } 65 | 66 | if (_attributes.TryGetValue(output.TagName, out string[] attributeNames)) 67 | { 68 | foreach (string attrName in attributeNames) 69 | { 70 | PrependCdnUrl(output, attrName); 71 | } 72 | } 73 | 74 | if (output.Attributes.TryGetAttribute("cdn-prop", out TagHelperAttribute prop)) 75 | { 76 | string targetProp = GetValue("cdn-prop", output); 77 | PrependCdnUrl(output, targetProp); 78 | output.Attributes.RemoveAll("cdn-prop"); 79 | } 80 | } 81 | 82 | private void PrependCdnUrl(TagHelperOutput output, string attrName) 83 | { 84 | string attrValue = GetValue(attrName, output); 85 | 86 | // Don't modify absolute paths 87 | if (string.IsNullOrWhiteSpace(attrName) || string.IsNullOrWhiteSpace(attrValue) || attrValue.Contains("://") || attrValue.StartsWith("//") || attrValue.StartsWith("data:")) 88 | { 89 | return; 90 | } 91 | 92 | string[] values = attrValue.Split(','); 93 | string modifiedValue = null; 94 | 95 | foreach (string value in values) 96 | { 97 | string fullUrl = _cdnUrl.Trim().TrimEnd('/') + "/" + value.Trim().TrimStart('~', '/'); 98 | modifiedValue += fullUrl + ", "; 99 | } 100 | 101 | var result = new HtmlString((modifiedValue ?? attrValue).Trim(',', ' ')); 102 | 103 | output.Attributes.SetAttribute(attrName, result); 104 | } 105 | 106 | public static string GetValue(string attrName, TagHelperOutput output) 107 | { 108 | if (string.IsNullOrEmpty(attrName) || !output.Attributes.TryGetAttribute(attrName, out TagHelperAttribute attr)) 109 | { 110 | return null; 111 | } 112 | 113 | if (attr.Value is string stringValue) 114 | { 115 | return stringValue; 116 | } 117 | else if (attr.Value is IHtmlContent content) 118 | { 119 | if (content is HtmlString htmlString) 120 | { 121 | return htmlString.ToString(); 122 | } 123 | 124 | using (var writer = new StringWriter()) 125 | { 126 | content.WriteTo(writer, HtmlEncoder.Default); 127 | return writer.ToString(); 128 | } 129 | } 130 | 131 | return null; 132 | } 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # User-specific files 5 | *.suo 6 | *.user 7 | *.userosscache 8 | *.sln.docstates 9 | *.pubxml 10 | 11 | # User-specific files (MonoDevelop/Xamarin Studio) 12 | *.userprefs 13 | 14 | # Build results 15 | [Dd]ebug/ 16 | [Dd]ebugPublic/ 17 | [Rr]elease/ 18 | [Rr]eleases/ 19 | x64/ 20 | x86/ 21 | bld/ 22 | [Bb]in/ 23 | [Oo]bj/ 24 | [Ll]og/ 25 | 26 | # Visual Studio 2015 cache/options directory 27 | .vs/ 28 | # Uncomment if you have tasks that create the project's static files in wwwroot 29 | #wwwroot/ 30 | 31 | # MSTest test Results 32 | [Tt]est[Rr]esult*/ 33 | [Bb]uild[Ll]og.* 34 | 35 | # NUNIT 36 | *.VisualState.xml 37 | TestResult.xml 38 | 39 | # Build Results of an ATL Project 40 | [Dd]ebugPS/ 41 | [Rr]eleasePS/ 42 | dlldata.c 43 | 44 | # DNX 45 | project.lock.json 46 | project.fragment.lock.json 47 | artifacts/ 48 | 49 | *_i.c 50 | *_p.c 51 | *_i.h 52 | *.ilk 53 | *.meta 54 | *.obj 55 | *.pch 56 | *.pdb 57 | *.pgc 58 | *.pgd 59 | *.rsp 60 | *.sbr 61 | *.tlb 62 | *.tli 63 | *.tlh 64 | *.tmp 65 | *.tmp_proj 66 | *.log 67 | *.vspscc 68 | *.vssscc 69 | .builds 70 | *.pidb 71 | *.svclog 72 | *.scc 73 | 74 | # Chutzpah Test files 75 | _Chutzpah* 76 | 77 | # Visual C++ cache files 78 | ipch/ 79 | *.aps 80 | *.ncb 81 | *.opendb 82 | *.opensdf 83 | *.sdf 84 | *.cachefile 85 | *.VC.db 86 | *.VC.VC.opendb 87 | 88 | # Visual Studio profiler 89 | *.psess 90 | *.vsp 91 | *.vspx 92 | *.sap 93 | 94 | # TFS 2012 Local Workspace 95 | $tf/ 96 | 97 | # Guidance Automation Toolkit 98 | *.gpState 99 | 100 | # ReSharper is a .NET coding add-in 101 | _ReSharper*/ 102 | *.[Rr]e[Ss]harper 103 | *.DotSettings.user 104 | 105 | # JustCode is a .NET coding add-in 106 | .JustCode 107 | 108 | # TeamCity is a build add-in 109 | _TeamCity* 110 | 111 | # DotCover is a Code Coverage Tool 112 | *.dotCover 113 | 114 | # NCrunch 115 | _NCrunch_* 116 | .*crunch*.local.xml 117 | nCrunchTemp_* 118 | 119 | # MightyMoose 120 | *.mm.* 121 | AutoTest.Net/ 122 | 123 | # Web workbench (sass) 124 | .sass-cache/ 125 | 126 | # Installshield output folder 127 | [Ee]xpress/ 128 | 129 | # DocProject is a documentation generator add-in 130 | DocProject/buildhelp/ 131 | DocProject/Help/*.HxT 132 | DocProject/Help/*.HxC 133 | DocProject/Help/*.hhc 134 | DocProject/Help/*.hhk 135 | DocProject/Help/*.hhp 136 | DocProject/Help/Html2 137 | DocProject/Help/html 138 | 139 | # Click-Once directory 140 | publish/ 141 | 142 | # Publish Web Output 143 | *.[Pp]ublish.xml 144 | *.azurePubxml 145 | # TODO: Comment the next line if you want to checkin your web deploy settings 146 | # but database connection strings (with potential passwords) will be unencrypted 147 | #*.pubxml 148 | *.publishproj 149 | 150 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 151 | # checkin your Azure Web App publish settings, but sensitive information contained 152 | # in these scripts will be unencrypted 153 | PublishScripts/ 154 | 155 | # NuGet Packages 156 | *.nupkg 157 | # The packages folder can be ignored because of Package Restore 158 | **/packages/* 159 | # except build/, which is used as an MSBuild target. 160 | !**/packages/build/ 161 | # Uncomment if necessary however generally it will be regenerated when needed 162 | #!**/packages/repositories.config 163 | # NuGet v3's project.json files produces more ignoreable files 164 | *.nuget.props 165 | *.nuget.targets 166 | 167 | # Microsoft Azure Build Output 168 | csx/ 169 | *.build.csdef 170 | 171 | # Microsoft Azure Emulator 172 | ecf/ 173 | rcf/ 174 | 175 | # Windows Store app package directories and files 176 | AppPackages/ 177 | BundleArtifacts/ 178 | Package.StoreAssociation.xml 179 | _pkginfo.txt 180 | 181 | # Visual Studio cache files 182 | # files ending in .cache can be ignored 183 | *.[Cc]ache 184 | # but keep track of directories ending in .cache 185 | !*.[Cc]ache/ 186 | 187 | # Others 188 | ClientBin/ 189 | ~$* 190 | *~ 191 | *.dbmdl 192 | *.dbproj.schemaview 193 | *.jfm 194 | *.pfx 195 | *.publishsettings 196 | node_modules/ 197 | orleans.codegen.cs 198 | 199 | # Since there are multiple workflows, uncomment next line to ignore bower_components 200 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 201 | #bower_components/ 202 | 203 | # RIA/Silverlight projects 204 | Generated_Code/ 205 | 206 | # Backup & report files from converting an old project file 207 | # to a newer Visual Studio version. Backup files are not needed, 208 | # because we have git ;-) 209 | _UpgradeReport_Files/ 210 | Backup*/ 211 | UpgradeLog*.XML 212 | UpgradeLog*.htm 213 | 214 | # SQL Server files 215 | *.mdf 216 | *.ldf 217 | 218 | # Business Intelligence projects 219 | *.rdl.data 220 | *.bim.layout 221 | *.bim_*.settings 222 | 223 | # Microsoft Fakes 224 | FakesAssemblies/ 225 | 226 | # GhostDoc plugin setting file 227 | *.GhostDoc.xml 228 | 229 | # Node.js Tools for Visual Studio 230 | .ntvs_analysis.dat 231 | 232 | # Visual Studio 6 build log 233 | *.plg 234 | 235 | # Visual Studio 6 workspace options file 236 | *.opt 237 | 238 | # Visual Studio LightSwitch build output 239 | **/*.HTMLClient/GeneratedArtifacts 240 | **/*.DesktopClient/GeneratedArtifacts 241 | **/*.DesktopClient/ModelManifest.xml 242 | **/*.Server/GeneratedArtifacts 243 | **/*.Server/ModelManifest.xml 244 | _Pvt_Extensions 245 | 246 | # Paket dependency manager 247 | .paket/paket.exe 248 | paket-files/ 249 | 250 | # FAKE - F# Make 251 | .fake/ 252 | 253 | # JetBrains Rider 254 | .idea/ 255 | *.sln.iml 256 | 257 | # CodeRush 258 | .cr/ 259 | 260 | # Python Tools for Visual Studio (PTVS) 261 | __pycache__/ 262 | *.pyc -------------------------------------------------------------------------------- /sample/wwwroot/images/banner2.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /sample/wwwroot/images/banner1.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /sample/wwwroot/images/banner3.svg: -------------------------------------------------------------------------------- 1 | banner3b -------------------------------------------------------------------------------- /sample/wwwroot/images/banner4.svg: -------------------------------------------------------------------------------- 1 | --------------------------------------------------------------------------------