Skip to content

Azure Blob Static Site Hosting

This document describes the DCS architecture for hosting customer sites using Azure Blob Storage with static website hosting, fronted by Azure Front Door.

Overview

Starting January 2026, new customer sites are deployed to Azure Blob Storage accounts with static website hosting enabled, replacing Azure Static Web Apps (SWA). This architecture provides:

  • Immediate content updates without full site rebuilds
  • Cost efficiency via shared Front Door profile
  • Direct blob editing for premium tier customers
  • Simplified infrastructure with built-in index.html fallback

Architecture

┌─────────────────────────────────────────────────────────────────────┐
│                        Azure Front Door                              │
│                    (afd-dcs-internal profile)                        │
│  ┌─────────────┬─────────────┬─────────────┬─────────────┐          │
│  │ endpoint1   │ endpoint2   │ endpoint3   │ sitegroup01 │          │
│  │ (customer)  │ (customer)  │ (customer)  │ (demos)     │          │
│  └──────┬──────┴──────┬──────┴──────┬──────┴──────┬──────┘          │
└─────────┼─────────────┼─────────────┼─────────────┼──────────────────┘
          │             │             │             │
          ▼             ▼             ▼             ▼
    ┌──────────┐  ┌──────────┐  ┌──────────┐  ┌──────────┐
    │ Storage  │  │ Storage  │  │ Storage  │  │ Storage  │
    │ Account  │  │ Account  │  │ Account  │  │ Accounts │
    │ ($web)   │  │ ($web)   │  │ ($web)   │  │ (demos)  │
    └──────────┘  └──────────┘  └──────────┘  └──────────┘

⚠️ Standard Tier Constraint: The diagram above shows per-customer endpoints for clarity, but Front Door Standard tier is limited to 10 endpoints per profile (7 currently used). In practice, customer sites should share endpoints using origin-group-per-customer + rule-set routing (the sitegroup01 pattern), NOT endpoint-per-customer. See the repo-local .docs/front-door-audit-2026-02-15.md audit for current usage.

Key Components

ComponentResourcePurpose
Storage AccountPer-site GPv2Hosts static website files in $web container
Static WebsiteBuilt-in featureServes index.html, handles SPA routing
Front Door Profileafd-dcs-internalCDN, SSL, custom domains, cache control
Origin GroupPer-siteRoutes traffic to storage static website endpoint

Demo Sites

Five demo sites showcase different frameworks and CMS integration patterns:

SiteFrameworkThemeStorage AccountCustom Domain
demoVitePressCloudSync Docsstdemositeonedemo.duffcloudservices.com
demo-2VueMaple Street Bakerystdemositetwodemo-2.duffcloudservices.com
demo-3AstroPixel Forge Creativestdemositethreedemo-3.duffcloudservices.com
demo-4ReactTechVenture Labsstdemositefourdemo-4.duffcloudservices.com
demo-5AngularFitnessPro Gymstdemositefivedemo-5.duffcloudservices.com

Resource Group

All demo site storage accounts are in:

  • Resource Group: rg-dcs-demosites
  • Region: northcentralus

Content Update Modes

Mode 1: Build-Time Injection (All Tiers)

Content is injected at build time via the @duffcloudservices/cms Vite plugin:

  1. GitHub Actions runs on content commit
  2. dcsContentPlugin() reads .dcs/content.yaml
  3. Content is embedded as __DCS_CONTENT__ global
  4. Site is built and deployed to blob storage

Latency: 2-5 minutes

Mode 2: Runtime Injection (Premium Tier)

Content is fetched from blob storage at runtime:

  1. Portal editor saves changes
  2. Server writes updated .dcs/content.yaml to blob storage
  3. Front Door cache is purged
  4. Site fetches fresh content on load

Latency: <10 seconds

Database Schema

Site records in Table Storage include:

json
{
  "storageAccountName": "stdemositeone",
  "storageAccountResourceId": "/subscriptions/.../storageAccounts/stdemositeone",
  "storageContainerName": "$web",
  "hostingProvider": "blob-storage"
}

Server Integration

SAS Token Generation

The server generates SAS tokens for direct blob uploads:

go
// server/internal/services/portal/sitehosting.go
func (s *Service) getBlobStorageDeploymentTarget(ctx context.Context, site *site.Row) (*BlobTarget, error) {
    // Uses site.StorageAccountName to generate SAS token
    // Container is always "$web" for static website hosting
}

Quick Publish Flow

go
// QuickPublishContent updates content directly in blob storage
func (s *Service) QuickPublishContent(ctx context.Context, siteSlug string, content []byte) error {
    // 1. Get site's storage account
    // 2. Generate SAS token for $web container
    // 3. Upload updated content.yaml to .dcs/content.yaml
    // 4. Purge Front Door cache for /.dcs/* paths
}

CMS Packages

@duffcloudservices/cms-core

Shared utilities used by all framework packages:

  • ContentConfiguration / SeoConfiguration types
  • resolveTextKey() - Content resolution with page/global fallback
  • resolveSeoForPage() - SEO metadata resolution
  • loadContentYaml() / loadSeoYaml() - YAML parsing
  • fetchRuntimeContent() - Runtime API fetching

Framework-Specific Packages

PackageFrameworkFeatures
@duffcloudservices/cmsVue 3useTextContent, useSEO composables + Vite plugins
@duffcloudservices/cms-reactReactHooks + Vite plugins
@duffcloudservices/cms-angularAngularServices + post-build injection
@duffcloudservices/cms-astroAstroAstro integration

Visual Page Editor Integration

The portal's visual page editor requires properly annotated HTML for section detection. See Visual Editor Annotations for details.

Required HTML Attributes

Sites must include these data attributes for the page editor to detect editable content:

html
<section data-section="hero" data-section-label="Hero Banner">
  <h1 data-text-key="hero.heading">{{ t('hero.heading') }}</h1>
  <p data-text-key="hero.description">{{ t('hero.description') }}</p>
</section>

Snapshot Capture

The deployment workflow captures page snapshots that include:

  • Full-page screenshot
  • Section boundaries (detected from data-section elements)
  • Text keys (detected from data-text-key elements)
  • Current text values for each key

Deployment Pipeline

GitHub Actions Workflow

yaml
# .github/workflows/deploy.yml
jobs:
  deploy:
    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v2
      - run: pnpm install
      - run: pnpm build
      
      # Upload to blob storage
      - uses: azure/CLI@v1
        with:
          inlineScript: |
            az storage blob upload-batch \
              --account-name ${{ secrets.STORAGE_ACCOUNT }} \
              --destination '$web' \
              --source ./dist \
              --overwrite
      
      # Capture snapshots and upload
      - run: node scripts/capture-snapshots.js
      
      # Purge Front Door cache
      - run: |
          az afd endpoint purge \
            --resource-group rg-dcs-internal \
            --profile-name afd-dcs-internal \
            --endpoint-name ${{ secrets.FRONTDOOR_ENDPOINT }} \
            --content-paths '/*'

Migration from SWA

Sites migrating from Azure Static Web Apps need:

  1. New storage account with static website enabled
  2. Front Door configuration (origin group, endpoint, route)
  3. Updated site record in Table Storage
  4. GitHub Actions workflow updated from swa deploy to blob upload
  5. HTML annotations added for visual editor support

Duff Cloud Services Documentation