Guide to create full featured blog site with Astro

Overview

Infrastructure

For infrastructure I determined following requirements:

  • Azure Static Web Apps for frontend
  • Azure Functions for backend
  • Azure Table Storage for simple tables
  • Custom domain names for both frontend and backend
  • SSL certificates for custom domain names
  • Automated deployment using Azure DevOps
  • Authentication capabilities through Azure AD/Entra

Azure Static Web App

Azure Static Web App can be created in Azure portal. You may also use built-in Azure Functions or so-called managed functions.

If you decide to use managed function, you can get advantage of following:

  • No need to configure CORS as managed functions use <domain>/api endoint. In other words calling function from Javascript will result in same origin call.
  • Deployment pipeline covers both web app and function app.

However, I would not recommend using managed functions, because you will not have full control over your function app. Additionally you will not be able to use table service that would be useful for storing page likes and other small amounts of data. Instead, you can create a separate function app that utilizes consumption plan. Of course, choice is yours.

CORS configuration is not difficult and deployment pipeline can be configured in 10-15 minutes. Further in the article, it is assumed that we will use separate function app.

Azure Function service plan

For Azure Functions I am using consumption plan. It is the most cost-effective plan for functions that are not used frequently. As my objective is to use function for storing and showing page likes, I expect that function will be called only when user clicks on like button. How do I know, maybe nobody will like my blog? ;) In that case, costs will be managed.

Storage Account

Azure Storage Account is used for Azure Functions. It is used to store function app code and configuration. It is also used to store function app logs. You can use Azure Storage Explorer to view logs and other files stored in the storage account. Additionally, you may use table service like “mini-database”.

Custom domain names

Azure Static Web apps and Azure Functions have excellent support for custom domain names. By default, Azure provides you with default domain names for your web app and function app. However, you can add your custom domain names to your web app and function app. Azure is polite enough to provide you with clear instruction how to configure your custom domain names withing domain registrar management console. namecheap DNS configuration

I have used namecheap.com to buy a domain name and configured it using provided instructions. Azure validates your ownership of the domain name and configures applications and functions to use it.

Note: existing default domain still remains operational, so you can use it as a backup.

Certificates

Azure Static Web Apps provides free SSL certificates for your custom domain names. You can use these certificates to secure your web app. SWA Application Insights

Same situation is with Azure Functions, where you can use free SSL certificates to secure your function app. Nothing additional needs to be done. Simply enable it in the Azure portal. SWA Application Insights

Application Insights

Create an Application Insights resource in Azure portal in advance. That way you will be able to attach same resource to both static web app and function app. This will allow you to monitor both applications in one place. Related log analytics workspace will be provisioned for you automatically.

SWA Application Insights

Note: Application Insights applies only to Azure Functions and not to Azure Static Web Apps itself. You must have at least one function in you static web app to be able to attach Application Insights to it.


Backend

I determined following requirements for my backend:

  • No route prefix in API calls
  • No ASP.NET Core integration
  • Ability to work with Azure Table Storage
  • Ability to deploy using Azure Pipelines

Route prefix

As my API is in a separate function app, I selected custom domain name in a form api.<domain>.net, while my app is deployed on www.<domain>.net. This way I do not need to have any route prefix in my API calls. I can simply call
api.<domain>.net/likes to get likes count.

To disable route prefix you need to modify host.json file in your function app. Add following line to the file:

{
  "version": "2.0",
  "extensions": {
    "http": {
      "routePrefix": ""
    }
  }
}

ASP.NET vs Built-in model

First of all you need to take into account following choices:

  • Hosting model. Most likely you will choose isolated model, as it is more secure and scalable.
  • Language that will be used for Azure Functions. I will use C# for my functions.
  • .NET version. I will use .NET 8 for my functions.
  • ASP.NET Core or Built-in model. I will use built-in model for my functions.
  • Deployment method. Tools, managed, or Azure DevOps Pipelines/GitHub Actions. I will use Azure DevOps for my deployment.

Hosting model

For C#, you only will have a choice, if you are using .NET 6 as documented in versions support by runtime. You project will need Worker and Worker.SDK packages. Project file should look something like this:

  <ItemGroup>
    <PackageReference Include="Microsoft.Azure.Functions.Worker" Version="1.21.0" />
    <PackageReference Include="Microsoft.Azure.Functions.Worker.Sdk" Version="1.17.2" />
    <PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.Http" Version="3.1.0" />
    <PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.Tables" Version="1.3.0" />
  </ItemGroup>

For .NET 6 you can choose between isolated and in-process hosting models. Choose isolated and use .NET 8, if possible.

Language

One of excellent features of Azure Functions is that you can use multiple languages to implement your functions. My choice is C#, and much of this article is related to C# functions, but you have support for JavaScript, TypeScript, Python, Java, and PowerShell. For C# you also have an option to use C# script (.csx) files.

.NET version

You should at least target .NET 6. As .NET 8 is latest LTS version and has good support across Azure services, my choice is .NET 8.

ASP.NET Core or Built-in model

To enable ASP.NET Core integration, you need to do 3-4 things:

  1. Add Microsoft.Azure.Functions.Worker.Extensions.Http.AspNetCore package to your project.
  2. Update worker packages to latest versions.
  3. Use specific HostBuilder procedure - use ConfigureFunctionsWebApplication() instead of ConfigureFunctionsWorkerDefaults().
  4. Update HTTP-triggered functions to use the ASP.NET Core types.

You can read the details from the link above. I can tell that ASP.NET Core gives you more flexibility and options. In built-in model you have to do everything yourself. Not a big deal for simple, small amount of functions.

Deployment method

This is about settings in your function’s so called Deployment Center. You can choose between following options:

  1. Deployment managed by App Service.
  2. Deployment using Azure DevOps Pipelines or GitHub actions.
  3. Manual deployment using Visual Studio/VS Code/Azure Functions tools.

Managed deployment integrates with repos and monitor them for changes. When changes are detected, deployment is triggered. Theoretically, you can connect Azure Repos, however, in reality it does not work - you are directed to use Azure DevOps pipelines. In other words only GitHub is supported.

In my case, I want to have control of how my function is being built and deployed. So this suits me well. I will use Azure DevOps pipelines.

ReadyToRun: Improved cold start

When using consumption plan for Azure Functions, you may experience cold start issues. This is because functions are not running all the time. When you call a function, Azure needs to start it first. This may take some time. To improve cold start time, you may use Premium plan for Azure Functions. However, this plan is more expensive than consumption plan.

Instead there is a feature called ReadyToRun. This feature precompiles your functions and reduces cold start time. You can enable this feature by providing R2R (aka ReadyToRun) binaries. It is similar to AOT (Ahead of Time) compilation.

  <PropertyGroup>
    <TargetFramework>net8.0</TargetFramework>
    <AzureFunctionsVersion>v4</AzureFunctionsVersion>
    <RuntimeIdentifier>win-x64</RuntimeIdentifier>
    <PublishReadyToRun>true</PublishReadyToRun>
  </PropertyGroup>	

Build and Deploy using Azure Repos

Ok. So to set up pipelines, you will need to do following:

  1. Create two repos - one for SWA and other for FA. Commit and sync your SWA and FA code respectively.
  2. Create a two pipelines in Azure DevOps - one for SWA and other for FA.
  3. For SWA pipeline, add task to update Node version to 20.x. This is because Astro requires Node 18.14.1.
  4. For SWA pipeline, add build version naming. This is because SWA is sensitive to how you name your build.
  5. For SWA pipeline, add SWA token.
  6. For SWA pipeline, you need to use ubuntu-latest vmImage.

Here is how my SWA pipeline looks like:


name: $(SourceBranchName)_$(Date:yyyyMMdd)$(Rev:r) # customize build name format

pr:
  branches:
    include:
      - main
trigger:
  branches:
    include:
      - main

jobs:
- job: build_and_deploy_job
  displayName: Build and Deploy Job
  condition: or(eq(variables['Build.Reason'], 'Manual'),or(eq(variables['Build.Reason'], 'PullRequest'),eq(variables['Build.Reason'], 'IndividualCI')))
  pool: 
    vmImage: ubuntu-latest # this is needed for SWA and all node tools
  variables:
  - group: Azure-Static-Web-Apps-<TOKEN>-variable-group # TOKEN is your SWA TOKEN
  steps:
  - task: UseNode@1 # required to use Node 20.x
    inputs:
      version: '20.x'
      checklatest: true
  - script: echo '$(Build.BuildNumber)' # outputs customized build number
  - checkout: self
    submodules: true
  - task: AzureStaticWebApp@0
    inputs:
      azure_static_web_apps_api_token: $(AZURE_STATIC_WEB_APPS_API_<TOKEN>) # token is your SWA token
###### Repository/Build Configurations - These values can be configured to match your app requirements. ######
# For more information regarding Static Web App workflow configurations, please visit: https://aka.ms/swaworkflowconfig
      app_location: "/" # App source code path
      api_location: "" # Api source code path - optional
      output_location: "dist" # Built app content directory - dist for Astro
      app_build_command: "npm run build" # The build command for your app
###### End of Repository/Build Configurations ######


Now, let’s see how FA pipeline looks like. I need to adjust following in boilerplate pipeline created by Azure DevOps:

  1. Add .NET Core SDK installation task. SDK dotnet commands will use latest installed version. List of versions can be found in releases index.
  2. When running the pipeline, pay attention. You may be asked for permission to deploy. You need to grant it.

# .NET Core Function App to Windows on Azure
# Build a .NET Core function app and deploy it to Azure as a Windows function App.
# Add steps that analyze code, save build artifacts, deploy, and more:
# https://docs.microsoft.com/en-us/azure/devops/pipelines/languages/dotnet-core

trigger:
- main

variables:
  # Azure Resource Manager connection created during pipeline creation
  azureSubscription: '<SUBSCRIPTION-GUID>' # GUID of your subscription

  # Function app name
  functionAppName: 'fn-devdvsion-api'

  # Agent VM image name
  vmImageName: 'windows-2019' # windows-latest may work as well

  # Working Directory
  workingDirectory: '$(System.DefaultWorkingDirectory)/'

stages:
- stage: Build
  displayName: Build stage

  jobs:
  - job: Build
    displayName: Build
    pool:
      vmImage: $(vmImageName)

    steps:
    - task: UseDotNet@2
      displayName: 'Install .NET Core SDK'
      inputs:
        packageType: 'sdk'
        version: '8.0.203' # .NET 8 LTS version 
        installationPath: $(Agent.ToolsDirectory)/dotnet
    - task: DotNetCoreCLI@2
      displayName: Build
      inputs:
        command: 'build'
        projects: |
          $(workingDirectory)/*.csproj
        arguments: --output $(System.DefaultWorkingDirectory)/publish_output --configuration Release

    - task: ArchiveFiles@2
      displayName: 'Archive files'
      inputs:
        rootFolderOrFile: '$(System.DefaultWorkingDirectory)/publish_output'
        includeRootFolder: false
        archiveType: zip
        archiveFile: $(Build.ArtifactStagingDirectory)/$(Build.BuildId).zip
        replaceExistingArchive: true

    - publish: $(Build.ArtifactStagingDirectory)/$(Build.BuildId).zip
      artifact: drop

- stage: Deploy
  displayName: Deploy stage
  dependsOn: Build
  condition: succeeded()

  jobs:
  - deployment: Deploy
    displayName: Deploy
    environment: 'development'
    pool:
      vmImage: $(vmImageName)

    strategy:
      runOnce:
        deploy:

          steps:
          - task: AzureFunctionApp@1
            displayName: 'Azure functions app deploy'
            inputs:
              azureSubscription: '$(azureSubscription)'
              appType: functionApp
              appName: $(functionAppName)
              package: '$(Pipeline.Workspace)/drop/$(Build.BuildId).zip'

Now you can use Azure DevOps for free. However, what you will need is a build agent. You can use Microsoft-hosted agents, but they are not free.

To get free build agent, you can have to ask for free grant. You can do it by navigating to Azure DevOps | Organization Settings | Billing and clicking MS Hosted CI/CD link. Here you will be presented with note explaining how to request free grant. In couple of business days you may get your free agent. Free agent gives you 1800 minutes of agent time per month.

GitHub has the similar story. In the mean time you can always publish apps using your tools.


Development

Frontend and Astro

Static web apps

Headers on Azure Static Web Apps

Response caching

Azure Functions

Headers on Azure Functions

I am adding response headers before returning response from my function. I am returning two headers: Cache-Control and Content-Type.


        [Function(nameof(Likes))]
        public async Task<HttpResponseData> Run(
            [HttpTrigger(
                AuthorizationLevel.Anonymous,
                "get", "post",
                Route = "likes"
            )] HttpRequestData req)
        {

            //......
            // function content ommitted for brevity
            //......

            // Return the likes count and add headers
            var _response = req.CreateResponse(HttpStatusCode.OK);
            _response.Headers.Add("Cache-Control", "no-cache");
            _response.Headers.Add("X-Content-Type-Options", "nosniff");
            await _response.WriteAsJsonAsync(new LikesResponse { Likes = likes });

            return _response;
        }

WriteAsJsonAsync writes the specified value as JSON to the response body. The response content-type will be set
to "application/json; charset=utf-8".

CORS configuration

CORS configuration is controlled in Azure Portal. You can add allowed origins. You can also enable/disable credentials.

SWA CORS configuration


Tools

Here is the toolset that I used for development:

ToolTypePurpose
Visual Studio CodeCode editorUsed to develop Astro files, CSS and
JavaScript, Azure Functions C# code.
Note: Visual Studio is much better for C#.
AzuriteAzure Storage emulatorDebugging Azure Functions locally.
Azure Storage ExplorerAzure Storage management toolAnalyze storage emulator tables data.
Static Web Apps CLIStatic Web Apps management toolInitialize, configure and debug SWA.
Azure Functions Core ToolsAzure Functions management toolInitialize, configure and debug functions.
C# extension for Visual Studio CodeC# language supportDevelop Azure Functions.
YAML extension for Visual Studio CodeYAML supportUsed to write Azure DevOps pipelines.
XML extension for Visual Studio CodeXML supportUsed to write Azure Functions project files.
MDX supportMarkdown supportUsed to write blog posts.
Azure extension for Visual Studio CodeAzure management toolManage Azure resources.
Azure DevOpsCI/CD toolBuild and deploy SWA and Functions.
Paint.netImage editing toolCreate AVIF images. Better than JPG.
Microsoft Edge Dev ToolsWeb issues analysisConsole and problems is very useful in
analyzing header and HTML tags issues.

All tools are free to use.


Conclusion

Combination of SWA+FA+Astro is a powerful toolset for building modern and cost effective blog site. As a result you will get automated and easy to maintain blog site on secure platform.

Here is my evaluation on various features that I used, their status and implemention difficulty. This is my experience - it is not about any of technologies that I used:

FeatureImplementation statusDifficulty
Blog articles listDoneMedium-High
Centralized navigation headerDoneMedium-High
Centralized footerDoneMedium
Latest articlesDoneLow
Side bar linksDoneMedium-High
Likes counterDoneHigh
Article tagsDoneMedium-High
Support for MarkdownDoneLow
Dynamic metadata in frontmatterDoneMedium
Support for MDXDoneLow
Support for MD imagesDoneLow. Built-in Astro implementation is poor.
Support for MD tablesDoneOOTB
Support for MD HTMLDoneOOTB. Lowercase should be used.
Responsive imagesDoneJust use AVIF and responsive layout.
Image zoomIn progress
Custom componentsDoneOOTB
Custom layoutsDoneOOTB
Custom CSSDoneMedium
Custom scriptsDoneLow
Custom fontsDoneLow
Syntax highlightingDoneBased on Shiki library, which is not the best, but ok.
Copy code buttonDoneMedium
Support for SEOIn progress
Article table of contentsIn progress
CORS configurationDoneMedium
SWA cache controlIn progressLow
Function app cache controlDoneLow

Overall I am satisfied with the result and will stay with this setup for some time. I will continue to improve my blog site and will share my experience with you.