Azure Functions is an excellent serverless solution that excels in many areas, but it's not the best choice for serving web pages. However, that doesn't mean we can't use it for that purpose. Here are a few reasons why we might want to serve static web pages from Azure Functions:
There's nothing wrong with using Azure Functions to serve static websites. It's fast and robust enough to handle a decent amount of web traffic. My website uwuw.io and the blog you're currently reading are both hosted on Azure Functions using variations of this method!
Note: This tutorial is under the assumption that you know how to get Azure Functions up and running, if you don't, there are plenty of tutorials that'll guide you through that.
To serve static web pages from Azure Functions, we need to alter our configurations. In your host.json, add the extensions property and set the routePrefix to empty. This tells Azure Functions to remove the /api prefix on all your routes.
{
"version": "2.0",
"extensions": {
"http": {
"routePrefix": ""
}
}
}
Next, we're going to create a new folder in the project called wwwroot
and add a file called index.html
.
You can add any HTML you'd like at this point!
In your Azure Functions .csproj
, add the following:
<ItemGroup>
<None Remove="wwwroot\**" />
</ItemGroup>
<ItemGroup>
<Content Include="wwwroot\**">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>
This will set all the files inside wwwroot
to Build Action: Content
and Copy to Output Directory: Copy if newer
so you
don't need to manually set those properties.
Now, we're going to create a new HTTP function to capture the root endpoint:
[FunctionName(nameof(ServeRoot))]
public async Task<IActionResult> ServeRoot(
[HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "{file?}")]
HttpRequest req, string? file)
{
return await GetFile(file);
}
This function will capture any calls to localhost:7071
Lets grab a NuGet package to help sort out Mime types
dotnet add package MimeTypeMapOfficial --version 1.0.17
And add a method to determine the Mime type of a file path
private static string GetMimeType(string filePath)
{
var fileInfo = new FileInfo(filePath);
return MimeTypeMap.GetMimeType(fileInfo.Extension);
}
Now we'll add a method to get a fully qualified file path:
private string GetFilePath(string pathValue)
{
string fullPath = Path.GetFullPath(
Path.Combine(Environment.GetVariable("AzureWebJobsScriptRoot"),"wwwroot", pathValue)
);
if (Directory.Exists(fullPath))
{
fullPath = Path.Combine(fullPath, "index.html");
}
return fullPath;
}
Note: On Azure you need to use $@"{Environment.GetEnvironmentVariable("HOME")}\site\wwwroot";
to get the correct location.
Finally, we'll add the GetFile()
method that does all the work
private Task<IActionResult> GetFile(string? file)
{
var filePath = GetFilePath(file ?? "");
if (File.Exists(filePath))
{
var stream = File.OpenRead(filePath);
return Task.FromResult<IActionResult>(new FileStreamResult(stream, GetMimeType(filePath))
{
LastModified = File.GetLastWriteTime(filePath)
});
}
else
{
return Task.FromResult<IActionResult>(new NotFoundResult());
}
}
This method gets the fully qualified file path, checks if the file exists, gets the MIME type,
and returns the file stream and MIME type in the form of a FileStreamResult
.
From here you have to make some adjustments if you want to have api endpoints or if you want to have sub folders in your wwwroot
folder...
All your api endpoints need to have a prefix or else they'll be gobbled up by your StaticRoot
function. I use api/
but you can use whatever
you like.
If you want to add sub folders to your wwwroot
folder such as wwwroot/css
you need to create another function. I use a catch all, but you can
create individual functions for each folder if you want.
[FunctionName(nameof(ServeContent))]
public async Task<IActionResult> ServeContent(
[HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "content/{folder}/{file}")]
HttpRequest req, string folder, string file)
{
return await GetFile($"{folder}/{file}");
}
If you've reached this point, congrats! You now have an Azure Functions application that serves static web pages. If you're looking for a complete library that does all the work you can clone my Github repo and follow the steps to get you rolling.
Until next time!