Serving Static Web Pages With Azure Functions

12/24/2022
C#, Azure Functions Matthew Andrews

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.

Configuration

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.

Creating Your Function

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}");
}

Summary

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!