├── 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 |
9 |
10 |
11 |
12 |
13 |
14 |
19 |
20 |
21 |
22 |

23 |
24 |
25 |
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 | [](https://ci.appveyor.com/project/madskristensen/webessentials-aspnetcore-cdntaghelpers)
4 | [](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 | 
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 |
--------------------------------------------------------------------------------
/sample/wwwroot/images/banner4.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------