Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Merge pull request #3941 from cephalin/AUX471

Aux471
  • Loading branch information...
commit 71b892021eeb01bfc613f3342cbb6fc1cd2fdbe5 2 parents d2bdcd9 + 819dadb
@tysonn tysonn authored
View
107 articles/cdn-cloud-service-with-cdn.md
@@ -92,7 +92,7 @@ In this section, you will deploy the default ASP.NET MVC application template in
>[WACOM.NOTE] Once your CDN endpoint is created, the Azure portal will show you its URL and the origin domain that it's integrated with. However, it can take awhile for the new CDN endpoint's configuration to be fully propagated to all the CDN node locations.
- Note that the CDN endpoitn is tied to the path **cdn/** of your cloud service. You can either create a **cdn** folder in your **WebRole1** project, or you can use URL rewrite to strip all incoming links of this path. In this tutorial, you will take the latter route.
+ Note that the CDN endpoint is tied to the path **cdn/** of your cloud service. You can either create a **cdn** folder in your **WebRole1** project, or you can use URL rewrite to strip all incoming links of this path. In this tutorial, you will take the latter route.
3. Back in the Azure portal, in the **CDN** tab, click the name of the CDN endpoint you just created.
@@ -150,17 +150,26 @@ You can similarly access any publicly accessible URL at **http://*<serviceNam
- Any controller/action
- If the query string is enabled at your CDN endpoint, any URL with query strings
-In fact, using the above setup, you can host the entire cloud application from **http://*<cdnName>*.vo.msecnd.net/**. If I navigate to **http://az632148.vo.msecnd.net/**, I get the action result from Home/Index.
+In fact, with the above configuration, you can host the entire cloud application from **http://*<cdnName>*.vo.msecnd.net/**. If I navigate to **http://az632148.vo.msecnd.net/**, I get the action result from Home/Index.
![](media/cdn-cloud-service-with-cdn/cdn-2-home-page.PNG)
This does not mean, however, that it's always a good idea (or generally a good idea) to serve an entire cloud application through Azure CDN. Some of the caveats are:
+- This approach requires your entire site to be public, because Azure CDN cannot serve any private content.
- If the CDN endpoint goes offline for any reason, whether scheduled maintenance or user error, your entire cloud application goes offline unless the customers can be redirected to the origin URL **http://*<serviceName>*.cloudapp.net/**.
- Even with the custom Cache-Control settings (see [Configure caching options for static files in your cloud application](#caching)), a CDN endpoint does not improve the performance of highly-dynamic content. If you tried to load the home page from your CDN endpoint as shown above, notice that it took at least 5 seconds to load the default home page the first time, which is a fairly simple page. Imagine what would happen to the client experience if this page contains dynamic content that must update every minute. Serving dynamic content from a CDN endpoint requires short cache expiration, which translates to frequent cache misses at the CDN endpoint. This hurts the performance or your cloud application and defeats the purpose of a CDN.
The alternative is to determine which content to serve from Azure CDN on a case-by-case basis in your cloud application. To that end, you have already seen how to access individual content files from the CDN endpoint. I will show you how to serve a specific controller action through the CDN endpoint in [Serve content from controller actions through Azure CDN](#controller).
+You can specify a more restrictive URL rewrite rule to limit the content accessible through your CDN endpoint. For example, to limit URL rewrite to the *\Scripts* folder, change the above rewrite rule as follows:
+<pre class="prettyprint">
+&lt;rule name=&quot;RewriteIncomingCdnRequest&quot; stopProcessing=&quot;true&quot;&gt;
+ &lt;match url=&quot;^cdn/<mark>Scripts/</mark>(.*)$&quot;/&gt;
+ &lt;action type=&quot;Rewrite&quot; url=&quot;<mark>Scripts/</mark>{R:1}&quot;/&gt;
+&lt;/rule&gt;
+</pre>
+
<a name="caching"></a>
## Configure caching options for static files in your cloud application ##
@@ -206,9 +215,12 @@ Follow the steps above to setup this controller action:
1. In the *\Controllers* folder, create a new .cs file called *MemeGeneratorController.cs* and replace the content with the following code. Be sure to replace the highlighted portion with your CDN name.
<pre class="prettyprint">
+ using System;
+ using System.Collections.Generic;
using System.Diagnostics;
using System.Drawing;
using System.IO;
+ using System.Net;
using System.Web.Hosting;
using System.Web.Mvc;
using System.Web.UI;
@@ -217,58 +229,85 @@ Follow the steps above to setup this controller action:
{
public class MemeGeneratorController : Controller
{
- //
- // GET: /MemeGenerator/
+ static readonly Dictionary<string, Tuple<string ,string>> Memes = new Dictionary<string, Tuple<string, string>>();
+
public ActionResult Index()
{
return View();
}
[HttpPost, ActionName(&quot;Index&quot;)]
- public ActionResult Index_Post(string top, string bottom)
+ public ActionResult Index_Post(string top, string bottom)
+ {
+ var identifier = Guid.NewGuid().ToString();
+ if (!Memes.ContainsKey(identifier))
+ {
+ Memes.Add(identifier, new Tuple&lt;string, string&gt;(top, bottom));
+ }
+
+ return Content(&quot;&lt;a href=\&quot;&quot; + Url.Action(&quot;Show&quot;, new {id = identifier}) + &quot;\&quot;&gt;here&#39;s your meme&lt;/a&gt;&quot;);
+ }
+
+
+ [OutputCache(VaryByParam = &quot;*&quot;, Duration = 1, Location = OutputCacheLocation.Downstream)]
+ public ActionResult Show(string id)
{
+ Tuple<string, string> data = null;
+ if (!Memes.TryGetValue(id, out data))
+ {
+ return new HttpStatusCodeResult(HttpStatusCode.NotFound);
+ }
+
if (Debugger.IsAttached) // Preserve the debug experience
{
- return Redirect(string.Format(&quot;/MemeGenerator/Show?top={0}&amp;bottom={1}&quot;, top, bottom));
+ return Redirect(string.Format(&quot;/MemeGenerator/Generate?top={0}&bottom={1}&quot;, data.Item1, data.Item2));
}
else // Get content from Azure CDN
{
- return Redirect(string.Format(&quot;http://<mark>&lt;cdnName&gt;</mark>.vo.msecnd.net/MemeGenerator/Show?top={0}&amp;bottom={1}&quot;, top, bottom));
+ return Redirect(string.Format(&quot;http://<mark>&lt;cdnName&gt;</mark>.vo.msecnd.net/MemeGenerator/Generate?top={0}&amp;bottom={1}&quot;, data.Item1, data.Item2));
}
}
-
- [OutputCache(VaryByParam = &quot;*&quot;, Duration = 3600, Location = OutputCacheLocation.Downstream)]
- public ActionResult Show(string top, string bottom)
+
+ [OutputCache(VaryByParam = "*", Duration = 3600, Location = OutputCacheLocation.Downstream)]
+ public ActionResult Generate(string top, string bottom)
{
string imageFilePath = HostingEnvironment.MapPath(&quot;~/Content/chuck.bmp&quot;);
Bitmap bitmap = (Bitmap)Image.FromFile(imageFilePath);
- using (Graphics graphics = Graphics.FromImage(bitmap))
+ using (Graphics graphics = Graphics.FromImage(bitmap))
{
- using (Font arialFont = FindBestFitFont(bitmap, graphics, top.ToUpperInvariant()))
+ SizeF size = new SizeF();
+ using (Font arialFont = FindBestFitFont(bitmap, graphics, top.ToUpperInvariant(), new Font("Arial Narrow", 100), out size))
{
- graphics.DrawString(top.ToUpperInvariant(), arialFont, Brushes.White, new PointF(0, 0));
+ graphics.DrawString(top.ToUpperInvariant(), arialFont, Brushes.White, new PointF(((bitmap.Width - size.Width) / 2), 10f));
}
- using (Font arialFont = FindBestFitFont(bitmap, graphics, bottom.ToUpperInvariant()))
+ using (Font arialFont = FindBestFitFont(bitmap, graphics, bottom.ToUpperInvariant(), new Font("Arial Narrow", 100), out size))
{
- graphics.DrawString(bottom.ToUpperInvariant(), arialFont, Brushes.White, new PointF(0, 0));
+ graphics.DrawString(bottom.ToUpperInvariant(), arialFont, Brushes.White, new PointF(((bitmap.Width - size.Width) / 2), bitmap.Height - 10f - arialFont.Height));
}
}
MemoryStream ms = new MemoryStream();
-
bitmap.Save(ms, System.Drawing.Imaging.ImageFormat.Png);
-
- return new FileStreamResult(ms, &quot;image/png&quot;);
+ return File(ms.ToArray(), &quot;image/png&quot;);
}
- private Font FindBestFitFont(Bitmap bitmap, Graphics graphics, string p)
+ private Font FindBestFitFont(Image i, Graphics g, String text, Font font, out SizeF size)
{
- Font f = new Font(FontFamily.GenericSansSerif, 20);
+ // Compute actual size, shrink if needed
+ while (true)
+ {
+ size = g.MeasureString(text, font);
- // Find best fit
+ // It fits, back out
+ if (size.Height < i.Height &&
+ size.Width < i.Width) { return font; }
- return f;
+ // Try a smaller font (90% of old size)
+ Font oldFont = font;
+ font = new Font(font.Name, (float)(font.Size * .9), font.Style);
+ oldFont.Dispose();
+ }
}
}
}
@@ -296,35 +335,41 @@ Follow the steps above to setup this controller action:
5. Publish the cloud application again and navigate to **http://*&lt;serviceName>*.cloudapp.net/MemeGenerator/Index** in your browser.
-When you submit the form values to `/MemeGenerator/Index`, you reach the following controller action:
+When you submit the form values to `/MemeGenerator/Index`, the `Index_Post` action method returns a link to the `Show` action method with the respective input identifier. When you click the link, you reach the following code:
<pre class="prettyprint">
-[HttpPost, ActionName(&quot;Index&quot;)]
-public ActionResult Index_Post(string top, string bottom)
+[OutputCache(VaryByParam = &quot;*&quot;, Duration = 1, Location = OutputCacheLocation.Downstream)]
+public ActionResult Show(string id)
{
+ Tuple<string, string> data = null;
+ if (!Memes.TryGetValue(id, out data))
+ {
+ return new HttpStatusCodeResult(HttpStatusCode.NotFound);
+ }
+
if (Debugger.IsAttached) // Preserve the debug experience
{
- return Redirect(string.Format(&quot;/MemeGenerator/Show?top={0}&amp;bottom={1}&quot;, top, bottom));
+ return Redirect(string.Format(&quot;/MemeGenerator/Generate?top={0}&bottom={1}&quot;, data.Item1, data.Item2));
}
else // Get content from Azure CDN
{
- return Redirect(string.Format(&quot;http://&lt;cdnName&gt;.vo.msecnd.net/MemeGenerator/Show?top={0}&amp;bottom={1}&quot;, top, bottom));
+ return Redirect(string.Format(&quot;http://<mark>&lt;cdnName&gt;</mark>.vo.msecnd.net/MemeGenerator/Generate?top={0}&amp;bottom={1}&quot;, data.Item1, data.Item2));
}
}
</pre>
If your local debugger is attached, then you will get the regular debug experience with a local redirect. If it's running in the cloud service, then it will redirect to:
- http://<cdnName>.vo.msecnd.net/MemeGenerator/Show?top=<formInput>&bottom=<formInput>
+ http://<cdnName>.vo.msecnd.net/MemeGenerator/Generate?top=<formInput>&bottom=<formInput>
Which corresponds to the following origin URL at your CDN endpoint:
- http://cephalinservice.cloudapp.net/cdn/MemeGenerator/Show?top=<formInput>&bottom=<formInput>
+ http://cephalinservice.cloudapp.net/cdn/MemeGenerator/Generate?top=<formInput>&bottom=<formInput>
After URL rewrite rule previously applied, the actual file that gets cached to your CDN endpoint is:
- http://cephalinservice.cloudapp.net/MemeGenerator/Show?top=<formInput>&bottom=<formInput>
+ http://cephalinservice.cloudapp.net/MemeGenerator/Generate?top=<formInput>&bottom=<formInput>
-You can then use the `OutputCacheAttribute` attribute to specify how the action result should be cached, which Azure CDN will honor. The code below specify a cache expiration of 1 hour (3,600 seconds).
+You can then use the `OutputCacheAttribute` attribute on the `Generate` method to specify how the action result should be cached, which Azure CDN will honor. The code below specify a cache expiration of 1 hour (3,600 seconds).
[OutputCache(VaryByParam = "*", Duration = 3600, Location = OutputCacheLocation.Downstream)]
View
171 articles/cdn-serve-content-from-cdn-in-your-web-application.md
@@ -15,7 +15,7 @@ This tutorial shows you how to take advantage of Azure CDN to improve the reach
In this tutorial, you will learn how to do the following:
- [Serve static content from an Azure CDN endpoint](#deploy)
-- [Automating uploading content in your ASP.NET application to your CDN endpoint](#upload)
+- [Automating content upload from your ASP.NET application to your CDN endpoint](#upload)
- [Configure the CDN cache to reflect the desired content update](#update)
- [Serve fresh content immediately using query strings](#query)
@@ -25,7 +25,17 @@ This tutorial has the following prerequisites:
- An active [Microsoft Azure account](http://azure.microsoft.com/en-us/account/). You can sign up for a trial account
- Visual Studio 2013 with [Azure SDK](http://go.microsoft.com/fwlink/p/?linkid=323510&clcid=0x409)
-- A simple ASP.NET MVC application to test CDN URLs. [Automating uploading content in your ASP.NET application to your CDN endpoint](#upload) uses an ASP.NET MVC application as an example.
+- A simple ASP.NET MVC application to test CDN URLs. [Automating content upload from your ASP.NET application to your CDN endpoint](#upload) uses an ASP.NET MVC application as an example.
+- [Azure PowerShell](http://go.microsoft.com/?linkid=9811175&clcid=0x409) (used by [Automating content upload from your ASP.NET application to your CDN endpoint](#upload))
+
+<div class="wa-note">
+ <span class="wa-icon-bulb"></span>
+ <h5><a name="note"></a>You need an Azure account to complete this tutorial:</h5>
+ <ul>
+ <li>You can <a href="http://acom-int.azurewebsites.net/en-us/pricing/free-trial/?WT.mc_id=A261C142F">open an Azure account for free</a> - You get credits you can use to try out paid Azure services, and even after they're used up you can keep the account and use free Azure services, such as Web Sites.</li>
+ <li>You can <a href="http://acom-int.azurewebsites.net/en-us/pricing/member-offers/msdn-benefits-details/?WT.mc_id=A261C142F">activate MSDN subscriber benefits</a> - Your MSDN subscription gives you credits every month that you can use for paid Azure services.</li>
+ <ul>
+</div>
<a name="static"></a>
## Serve static content from an Azure CDN endpoint ##
@@ -127,98 +137,40 @@ Let's get to it. Follow the steps below to start using the Azure CDN:
In this section, you have learned how to create a CDN endpoint, upload content to it, and link to CDN contentfrom any Web page.
<a name="upload"></a>
-## Automating uploading content in your ASP.NET application to your CDN endpoint ##
-
-What if you want to easily upload all of the static content in your ASP.NET Web application to your CDN endpoint? With a little bit of coding, bulk-uploading content from an ASP.NET Web application project to Azure CDN is quite straightforward. [Maarten Balliauw](https://twitter.com/maartenballiauw) has provided an excellent way to do it with ASP.NET MVC in his video [Reducing latency on the web with the Windows Azure CDN](http://channel9.msdn.com/events/TechDays/Techdays-2014-the-Netherlands/Reducing-latency-on-the-web-with-the-Windows-Azure-CDN), which I will simply reproduce here.
-
- >[WACOM.NOTE]These steps use the storage account and CDN that you already created in the previous tutorial.
-
-1. Copy this action method to your Home controller (*Controllers\HomeController.cs*):
- <pre class="prettyprint">
- public ActionResult Synchronize()
- {
- // Connect to your storage account
- var account = CloudStorageAccount.Parse(&quot;DefaultEndpointsProtocol=https;AccountName=<mark>&lt;storageAccountName&gt;</mark>;AccountKey=<mark>&lt;accountKey&gt;</mark>&quot;);
- var client = account.CreateCloudBlobClient();
-
- // Open/create the storage container and set the permissions to Public for all blobs in the container
- var container = client.GetContainerReference(&quot;<mark>&lt;containerName&gt;</mark>&quot;);
- container.CreateIfNotExists();
- container.SetPermissions(
- new BlobContainerPermissions
- {
- PublicAccess = BlobContainerPublicAccessType.Blob
- });
-
- // Discover all files in your \Content and \Scripts folders
- var approot = HostingEnvironment.MapPath(&quot;~/&quot;);
- var files = new List&lt;string&gt;();
- files.AddRange(Directory.EnumerateFiles(
- HostingEnvironment.MapPath(&quot;~/Content&quot;), &quot;*&quot;, SearchOption.AllDirectories));
- files.AddRange(Directory.EnumerateFiles(
- HostingEnvironment.MapPath(&quot;~/Scripts&quot;), &quot;*&quot;, SearchOption.AllDirectories));
-
- // Upload each discovered file to an Azure blob
- foreach (var file in files)
- {
- // Make sure that the content type is set properly for each file.
- // If you have additional content types, add it to the switch statement
- // (Such as &quot;image/jpeg&quot;).
- var contentType = &quot;application/octet-stream&quot;; // Default content type
- switch (Path.GetExtension(file))
- {
- case &quot;.png&quot;:
- contentType = &quot;image/png&quot;;
- break;
- case &quot;.css&quot;:
- contentType = &quot;text/css&quot;;
- break;
- case &quot;.js&quot;:
- contentType = &quot;text/javascript&quot;;
- break;
- }
-
- // Upload the file to a blob
- var blob = container.GetBlockBlobReference(file.Replace(approot, &quot;&quot;));
- blob.Properties.ContentType = contentType;
- blob.UploadFromFile(file, FileMode.OpenOrCreate);
- blob.SetProperties();
- }
-
- // Indicate that the upload has been successful
- return Content(&quot;Content is synchronized with the blob container.&quot;);
- }
- </pre>
-
- This code uploads all files from your *\Content* and *\Scripts* folders to the specified storage account and container. This code has the following advantage:
-
- - Automatically replicate the file structure of your Visual Studio project
- - Automatically create blob containers as needed
- - Reuse the same Azure storage account and CDN endpoint for multiple Web applications, each in a separate blob container
- - Easily update the Azure CDN with new content. For more information on updating content, see [Configure the CDN cache to reflect the desired content update](#update).
-
-2. You'll need to include the following namespaces in *Controllers\HomeController.cs* in order for the code to resolve properly:
-
- using Microsoft.WindowsAzure.Storage;
- using Microsoft.WindowsAzure.Storage.Blob;
- using System.Collections.Generic;
- using System.IO;
- using System.Web.Hosting;
-
-3. You also need to specify the following parameters in the code:
-
- - **&lt;storageAccountName>** The name of your storage account.
- - **&lt;accountKey>** Your account key. You can find the primary or secondary access key by selecting your storage account in the **Storage** tab and click **Manage Access Keys**.
-
- ![](media/cdn-serve-content-from-cdn-in-your-web-application/cdn-mvc-1-accountkey.PNG)
-
- - **&lt;ContainerName>** The name of the blob container. Whereas I used the generic "cdn" as the container name previously, it makes more sense to use the name of your Web app so that all the content for that Web app is organized into the same easily identifiable container.
-
-4. Debug your MVC application by typing `F5`, then navigate to your action method, like `http://localhost:####/Home/Synchronize`.
-
- Once the content has finished uploading, you should see the message "Content is synchronized with the blob container." You can now link to anything in your *\Content* and *\Scripts* folder in your .cshtml files, using `http://<cdnName>.vo.msecnd.net/<containerName>`. Here is an example of something I can use in a Razor view:
-
- <img alt="Mugshot" src="http://az623979.vo.msecnd.net/MyMvcApp/Content/cephas_lin.png" />
+## Automating content upload from your ASP.NET application to your CDN endpoint ##
+
+If you want to easily upload all of the static content in your ASP.NET Web application to your CDN endpoint, or if your deploy your Web application using continuous delivery (for an example, see [Continuous Delivery for Cloud Services in Azure](http://azure.microsoft.com/en-us/documentation/articles/cloud-services-dotnet-continuous-delivery/)), you can use Azure PowerShell to automate the synchronization of the latest content files to Azure blobs every time you deploy your Web application. For example, you can run the script at [Upload Content Files from ASP.NET Application to Azure Blobs](http://gallery.technet.microsoft.com/scriptcenter/Upload-Content-Files-from-41c2142a) upload all the content files in an ASP.NET application. To use this script:
+
+4. From the **Start** menu, run **Windows Azure PowerShell**.
+5. In the Azure PowerShell window, run `Get-AzurePublishSettingsFile` to download a publish settings file for your Azure account.
+6. Once you have downloaded your publish settings file, run the following:
+
+ Import-AzurePublishSettingsFile "<DownloadedFilePath>"
+
+ >[WACOM.NOTE] Once you import your publish settings file, it will be the default Azure account used for all Azure PowerShell sessions. This means that the above steps only need to be done once.
+
+1. Download the script from the [download page]((http://gallery.technet.microsoft.com/scriptcenter/Upload-Content-Files-from-41c2142a)). Save it into your ASP.NET application's project folder.
+2. Right-click the downloaded script and click **Properties**.
+3. Click **Unblock**.
+4. Open a PowerShell window and run the following:
+
+ cd <ProjectFolder>
+ .\UploadContentToAzureBlobs.ps1 -StorageAccount "<StorageAccountName>" -StorageContainer "<ContainerName>"
+
+This script uploads all files from your *\Content* and *\Scripts* folders to the specified storage account and container. It has the following advantages:
+
+- Automatically replicate the file structure of your Visual Studio project
+- Automatically create blob containers as needed
+- Reuse the same Azure storage account and CDN endpoint for multiple Web applications, each in a separate blob container
+- Easily update the Azure CDN with new content. For more information on updating content, see [Configure the CDN cache to reflect the desired content update](#update).
+
+For the `-StorageContainer` parameter, it makes sense to use the name of your Web application, or the Visual Studio project name. Whereas I used the generic "cdn" as the container name previously, using the name of your Web application allows related content to be organized into the same easily identifiable container.
+
+Once the content has finished uploading, you can link to anything in your *\Content* and *\Scripts* folder in your HTML code, such as in your .cshtml files, using `http://<cdnName>.vo.msecnd.net/<containerName>`. Here is an example of something I can use in a Razor view:
+
+ <img alt="Mugshot" src="http://az623979.vo.msecnd.net/MyMvcApp/Content/cephas_lin.png" />
+
+For an example of integrating PowerShell scripts into your continuous delivery configuration, see [Continuous Delivery for Cloud Services in Azure](http://azure.microsoft.com/en-us/documentation/articles/cloud-services-dotnet-continuous-delivery/).
<a name="update"></a>
## Configure the CDN cache to reflect the desired content update ##
@@ -231,18 +183,25 @@ The good news is that you can customize cache expiration. Similar to most browse
![](media/cdn-serve-content-from-cdn-in-your-web-application/cdn-updates-1.PNG)
-You can also do this programmatically to set all blobs' Cache-Control headers. In [Maarten Balliauw](https://twitter.com/maartenballiauw)'s code above, just add the following code before the `blob.UploadFromFile` method is executed.
-
- blob.Properties.CacheControl = "public, max-age=3600";
-
-The complete blob upload code snippet then looks like the following:
-
- // Upload the file to a blob
- var blob = container.GetBlockBlobReference(file.Replace(approot, ""));
- blob.Properties.ContentType = contentType;
- blob.Properties.CacheControl = "public, max-age=3600";
- blob.UploadFromFile(file, FileMode.OpenOrCreate);
- blob.SetProperties();
+You can also do this in your PowerShell script to set all blobs' Cache-Control headers. For the script in [Automating content upload from your ASP.NET application to your CDN endpoint](#upload), find the following code snippet:
+
+ Set-AzureStorageBlobContent `
+ -Container $StorageContainer `
+ -Context $context `
+ -File $file.FullName `
+ -Blob $blobFileName `
+ -Properties @{ContentType=$contentType} `
+ -Force
+
+and modify it as follows:
+
+ Set-AzureStorageBlobContent `
+ -Container $StorageContainer `
+ -Context $context `
+ -File $file.FullName `
+ -Blob $blobFileName `
+ -Properties @{ContentType=$contentType, "CacheControl="public, max-age=3600"} `
+ -Force
You may still need to wait for the full 7-day cached content on your Azure CDN to expire before it pulls the new content, with the new Cache-Control header. This illustrates the fact that custom caching values do not help if you want your content update to go live immediately, such as JavaScript or CSS updates. However, you can work around this issue by versioning your content through query strings. For more information, see [Serve fresh content immediately using query strings](#query).

0 comments on commit 71b8920

Please sign in to comment.
Something went wrong with that request. Please try again.