GitHub Static Web Apps with Azure Images
GitHub Static Web Apps with Azure Images
Section titled “GitHub Static Web Apps with Azure Images”This guide demonstrates how to seamlessly integrate Azure Blob Storage images into your GitHub Static Web Apps, including automated deployment workflows and performance optimizations.
📋 Prerequisites
Section titled “📋 Prerequisites”Before starting, ensure you have:
- GitHub account with a repository
- Azure subscription with an active Storage Account (see Azure Blob Storage for Images)
- Images already uploaded to Azure Blob Storage
- Basic knowledge of HTML, CSS, and JavaScript
🚀 Step 1: Set Up GitHub Static Web Apps
Section titled “🚀 Step 1: Set Up GitHub Static Web Apps”1.1 Create Static Web App Resource
Section titled “1.1 Create Static Web App Resource”Using Azure CLI:
# VariablesRESOURCE_GROUP="rg-static-webapp"WEBAPP_NAME="my-image-gallery"GITHUB_REPO="https://github.com/yourusername/your-repo"LOCATION="East US 2"
# Create resource group if it doesn't existaz group create \ --name $RESOURCE_GROUP \ --location "$LOCATION"
# Create Static Web Appaz staticwebapp create \ --name $WEBAPP_NAME \ --resource-group $RESOURCE_GROUP \ --source $GITHUB_REPO \ --location "$LOCATION" \ --branch main \ --app-location "/" \ --api-location "api" \ --output-location "dist"
1.2 Using Azure Portal
Section titled “1.2 Using Azure Portal”- Go to Azure Portal
- Search for “Static Web Apps” and click Create
- Fill in the details:
- Subscription: Select your subscription
- Resource Group: Create new or select existing
- Name: Choose a unique name
- Plan type: Free (for most use cases)
- Region: Select closest to your users
- Source: GitHub
- Organization: Your GitHub username/org
- Repository: Your repository name
- Branch: main (or your default branch)
🔗 Step 2: Configure Image URLs in Your Application
Section titled “🔗 Step 2: Configure Image URLs in Your Application”2.1 Environment Configuration
Section titled “2.1 Environment Configuration”Create a .env
file in your project root:
# Azure Storage ConfigurationAZURE_STORAGE_ACCOUNT_NAME=myimagestore1234AZURE_STORAGE_CONTAINER_NAME=imagesAZURE_STORAGE_BASE_URL=https://myimagestore1234.blob.core.windows.net/images
2.2 JavaScript Configuration
Section titled “2.2 JavaScript Configuration”Create src/config/azure.js
:
// Azure Storage Configurationconst azureConfig = { storageAccountName: process.env.AZURE_STORAGE_ACCOUNT_NAME || 'myimagestore1234', containerName: process.env.AZURE_STORAGE_CONTAINER_NAME || 'images', baseUrl: process.env.AZURE_STORAGE_BASE_URL || 'https://myimagestore1234.blob.core.windows.net/images'};
// Helper function to generate image URLsexport function getImageUrl(imageName, options = {}) { const { width, height, format = 'webp' } = options;
let url = `${azureConfig.baseUrl}/${imageName}`;
// Add query parameters for image transformations if using Azure CDN const params = new URLSearchParams(); if (width) params.append('w', width.toString()); if (height) params.append('h', height.toString()); if (format !== 'original') params.append('format', format);
if (params.toString()) { url += `?${params.toString()}`; }
return url;}
// Helper for responsive imagesexport function generateResponsiveImageSet(imageName, sizes = [400, 800, 1200]) { return sizes.map(size => ({ src: getImageUrl(imageName, { width: size }), width: size }));}
export default azureConfig;
🖼️ Step 3: Implement Image Components
Section titled “🖼️ Step 3: Implement Image Components”3.1 Basic HTML Implementation
Section titled “3.1 Basic HTML Implementation”<!DOCTYPE html><html lang="en"><head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>My Image Gallery</title></head><body> <div class="image-gallery"> <!-- Single image --> <img src="https://myimagestore1234.blob.core.windows.net/images/hero-image.jpg" alt="Hero Image" class="hero-image">
<!-- Responsive image with srcset --> <img src="https://myimagestore1234.blob.core.windows.net/images/gallery-1.jpg" srcset="https://myimagestore1234.blob.core.windows.net/images/gallery-1.jpg 400w, https://myimagestore1234.blob.core.windows.net/images/gallery-1.jpg 800w, https://myimagestore1234.blob.core.windows.net/images/gallery-1.jpg 1200w" sizes="(max-width: 600px) 400px, (max-width: 1200px) 800px, 1200px" alt="Gallery Image 1"> </div></body></html>
3.2 React Component Implementation
Section titled “3.2 React Component Implementation”import React, { useState, useEffect } from 'react';import { getImageUrl, generateResponsiveImageSet } from '../config/azure';
const ImageGallery = ({ images }) => { const [loading, setLoading] = useState(true); const [imageErrors, setImageErrors] = useState(new Set());
const handleImageError = (imageName) => { setImageErrors(prev => new Set([...prev, imageName])); };
const handleImageLoad = () => { setLoading(false); };
return ( <div className="image-gallery"> {images.map((imageName, index) => { if (imageErrors.has(imageName)) { return ( <div key={index} className="image-placeholder"> <p>Image not available</p> </div> ); }
const responsiveSet = generateResponsiveImageSet(imageName);
return ( <picture key={index} className="gallery-image"> <source srcSet={responsiveSet.map(img => `${img.src} ${img.width}w`).join(', ')} sizes="(max-width: 600px) 400px, (max-width: 1200px) 800px, 1200px" type="image/webp" /> <img src={getImageUrl(imageName)} alt={`Gallery image ${index + 1}`} loading="lazy" onError={() => handleImageError(imageName)} onLoad={handleImageLoad} className="responsive-image" /> </picture> ); })} </div> );};
// Usage componentconst App = () => { const galleryImages = [ 'hero-banner.jpg', 'product-1.jpg', 'product-2.jpg', 'team-photo.jpg' ];
return ( <div className="app"> <h1>My Azure Image Gallery</h1> <ImageGallery images={galleryImages} /> </div> );};
export default App;
3.3 Vue.js Component Implementation
Section titled “3.3 Vue.js Component Implementation”<template> <div class="image-gallery"> <div v-for="(image, index) in images" :key="index" class="gallery-item"> <img :src="getImageUrl(image.name)" :alt="image.alt" :loading="index > 2 ? 'lazy' : 'eager'" @error="handleImageError(image.name)" @load="handleImageLoad" class="gallery-image" /> </div> </div></template>
<script>import { getImageUrl } from '../config/azure.js';
export default { name: 'ImageGallery', props: { images: { type: Array, required: true } }, methods: { getImageUrl(imageName) { return getImageUrl(imageName, { width: 800 }); }, handleImageError(imageName) { console.error(`Failed to load image: ${imageName}`); }, handleImageLoad() { console.log('Image loaded successfully'); } }};</script>
🔄 Step 4: Set Up GitHub Actions Workflow
Section titled “🔄 Step 4: Set Up GitHub Actions Workflow”Create .github/workflows/azure-static-web-apps-deploy.yml
:
name: Azure Static Web Apps CI/CD
on: push: branches: - main pull_request: types: [opened, synchronize, reopened, closed] branches: - main
jobs: build_and_deploy_job: if: github.event_name == 'push' || (github.event_name == 'pull_request' && github.event.action != 'closed') runs-on: ubuntu-latest name: Build and Deploy Job steps: - uses: actions/checkout@v3 with: submodules: true
- name: Setup Node.js uses: actions/setup-node@v3 with: node-version: '18' cache: 'npm'
- name: Install dependencies run: npm install
- name: Build application run: npm run build env: AZURE_STORAGE_ACCOUNT_NAME: ${{ secrets.AZURE_STORAGE_ACCOUNT_NAME }} AZURE_STORAGE_CONTAINER_NAME: ${{ secrets.AZURE_STORAGE_CONTAINER_NAME }} AZURE_STORAGE_BASE_URL: ${{ secrets.AZURE_STORAGE_BASE_URL }}
- name: Build And Deploy id: builddeploy uses: Azure/static-web-apps-deploy@v1 with: azure_static_web_apps_api_token: ${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN }} repo_token: ${{ secrets.GITHUB_TOKEN }} action: "upload" app_location: "/" # App source code path api_location: "api" # Api source code path - optional output_location: "dist" # Built app content directory - optional
close_pull_request_job: if: github.event_name == 'pull_request' && github.event.action == 'closed' runs-on: ubuntu-latest name: Close Pull Request Job steps: - name: Close Pull Request id: closepullrequest uses: Azure/static-web-apps-deploy@v1 with: azure_static_web_apps_api_token: ${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN }} action: "close"
🔐 Step 5: Configure GitHub Secrets
Section titled “🔐 Step 5: Configure GitHub Secrets”5.1 Required Secrets
Section titled “5.1 Required Secrets”In your GitHub repository, go to Settings > Secrets and variables > Actions and add:
-
AZURE_STATIC_WEB_APPS_API_TOKEN
- Get this from Azure Portal > Your Static Web App > Manage deployment token
-
AZURE_STORAGE_ACCOUNT_NAME
- Your Azure Storage Account name
-
AZURE_STORAGE_CONTAINER_NAME
- Container name (e.g., “images”)
-
AZURE_STORAGE_BASE_URL
- Full base URL (e.g., “https://mystorageaccount.blob.core.windows.net/images”)
5.2 Using Azure CLI to Get Deployment Token
Section titled “5.2 Using Azure CLI to Get Deployment Token”# Get the deployment token for your Static Web Appaz staticwebapp secrets list \ --name $WEBAPP_NAME \ --resource-group $RESOURCE_GROUP \ --query "properties.apiKey" \ --output tsv
⚡ Step 6: Performance Optimizations
Section titled “⚡ Step 6: Performance Optimizations”6.1 Enable Azure CDN
Section titled “6.1 Enable Azure CDN”# Create CDN profileaz cdn profile create \ --resource-group $RESOURCE_GROUP \ --name "mycdnprofile" \ --sku Standard_Microsoft
# Create CDN endpoint for storage accountaz cdn endpoint create \ --resource-group $RESOURCE_GROUP \ --profile-name "mycdnprofile" \ --name "mycdn-images" \ --origin myimagestore1234.blob.core.windows.net \ --origin-host-header myimagestore1234.blob.core.windows.net
6.2 Implement Image Lazy Loading
Section titled “6.2 Implement Image Lazy Loading”// Modern lazy loading with Intersection Observerclass LazyImageLoader { constructor() { this.imageObserver = new IntersectionObserver((entries, observer) => { entries.forEach(entry => { if (entry.isIntersecting) { const img = entry.target; img.src = img.dataset.src; img.classList.remove('lazy'); observer.unobserve(img); } }); }); }
observe(selector = 'img[data-src]') { const lazyImages = document.querySelectorAll(selector); lazyImages.forEach(img => { this.imageObserver.observe(img); }); }}
// Initialize lazy loadingdocument.addEventListener('DOMContentLoaded', () => { const lazyLoader = new LazyImageLoader(); lazyLoader.observe();});
6.3 Add Caching Headers
Section titled “6.3 Add Caching Headers”Update your staticwebapp.config.json
:
{ "globalHeaders": { "Cache-Control": "public, max-age=86400" }, "routes": [ { "route": "/images/*", "headers": { "Cache-Control": "public, max-age=31536000, immutable" } } ]}
📊 Step 7: Monitoring and Analytics
Section titled “📊 Step 7: Monitoring and Analytics”7.1 Enable Application Insights
Section titled “7.1 Enable Application Insights”# Create Application Insights instanceaz extension add --name application-insightsaz monitor app-insights component create \ --app "myapp-insights" \ --location "East US" \ --resource-group $RESOURCE_GROUP \ --application-type web
# Link to Static Web Appaz staticwebapp appsettings set \ --name $WEBAPP_NAME \ --resource-group $RESOURCE_GROUP \ --setting-names APPLICATIONINSIGHTS_CONNECTION_STRING \ --setting-values "$(az monitor app-insights component show --app myapp-insights -g $RESOURCE_GROUP --query connectionString -o tsv)"
7.2 Add Performance Monitoring
Section titled “7.2 Add Performance Monitoring”// Add to your main application fileimport { ApplicationInsights } from '@microsoft/applicationinsights-web';
const appInsights = new ApplicationInsights({ config: { connectionString: process.env.APPLICATIONINSIGHTS_CONNECTION_STRING }});
appInsights.loadAppInsights();
// Track image loading performancefunction trackImageLoad(imageName, loadTime) { appInsights.trackMetric({ name: 'Image Load Time', average: loadTime, properties: { imageName: imageName } });}
// Usage in image load handlerconst startTime = performance.now();image.onload = () => { const loadTime = performance.now() - startTime; trackImageLoad(imageName, loadTime);};
🛠️ Step 8: Advanced Features
Section titled “🛠️ Step 8: Advanced Features”8.1 Image Optimization API
Section titled “8.1 Image Optimization API”Create api/optimize-image.js
:
const { BlobServiceClient } = require('@azure/storage-blob');const sharp = require('sharp');
module.exports = async function (context, req) { const { imageName, width, height, format = 'webp', quality = 80 } = req.query;
try { // Connect to Azure Blob Storage const blobServiceClient = BlobServiceClient.fromConnectionString( process.env.AZURE_STORAGE_CONNECTION_STRING );
const containerClient = blobServiceClient.getContainerClient('images'); const blobClient = containerClient.getBlobClient(imageName);
// Download the blob const downloadBlockBlobResponse = await blobClient.download(); const originalBuffer = await streamToBuffer(downloadBlockBlobResponse.readableStreamBody);
// Process the image let processedImage = sharp(originalBuffer);
if (width || height) { processedImage = processedImage.resize(parseInt(width), parseInt(height), { fit: 'cover', withoutEnlargement: true }); }
// Convert format and set quality if (format === 'webp') { processedImage = processedImage.webp({ quality: parseInt(quality) }); } else if (format === 'jpeg') { processedImage = processedImage.jpeg({ quality: parseInt(quality) }); }
const optimizedBuffer = await processedImage.toBuffer();
context.res = { status: 200, headers: { 'Content-Type': `image/${format}`, 'Cache-Control': 'public, max-age=31536000', 'Content-Length': optimizedBuffer.length }, body: optimizedBuffer, isRaw: true }; } catch (error) { context.res = { status: 500, body: { error: 'Failed to process image' } }; }};
async function streamToBuffer(readableStream) { return new Promise((resolve, reject) => { const chunks = []; readableStream.on('data', (data) => { chunks.push(data instanceof Buffer ? data : Buffer.from(data)); }); readableStream.on('end', () => { resolve(Buffer.concat(chunks)); }); readableStream.on('error', reject); });}
8.2 Progressive Image Loading
Section titled “8.2 Progressive Image Loading”/* CSS for progressive loading */.image-container { position: relative; overflow: hidden;}
.image-placeholder { background: linear-gradient(90deg, #f0f0f0 25%, transparent 37%, #f0f0f0 63%); background-size: 400% 100%; animation: shimmer 1.5s ease-in-out infinite;}
@keyframes shimmer { 0% { background-position: 100% 0; } 100% { background-position: -100% 0; }}
.image-loaded { animation: fadeIn 0.3s ease-in-out;}
@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; }}
🎯 Best Practices Summary
Section titled “🎯 Best Practices Summary”Performance
Section titled “Performance”- Use WebP format for modern browsers with fallbacks
- Implement lazy loading for images below the fold
- Enable Azure CDN for global distribution
- Optimize image sizes for different device types
- Set proper caching headers
Security
Section titled “Security”- Use HTTPS everywhere
- Implement SAS tokens for private images
- Configure CORS properly
- Regular security reviews
- Always include alt text
- Use descriptive file names
- Implement structured data for image galleries
- Optimize image loading for Core Web Vitals
🔗 Next Steps
Section titled “🔗 Next Steps”- Implement comprehensive security measures
- Set up automated image processing workflows
- Learn about cost optimization strategies
🐛 Troubleshooting
Section titled “🐛 Troubleshooting”Common Issues
Section titled “Common Issues”Images not loading:
- Check CORS settings on storage account
- Verify container public access level
- Confirm image URLs are correct
Slow loading:
- Enable Azure CDN
- Implement proper caching
- Optimize image sizes
GitHub Actions failing:
- Verify all secrets are set correctly
- Check build output for errors
- Ensure API token is valid
CORS errors:
# Fix CORS issuesaz storage cors add \ --methods GET POST PUT DELETE \ --origins https://yourapp.azurestaticapps.net \ --services b \ --max-age 3600 \ --account-name $STORAGE_ACCOUNT \ --account-key $STORAGE_KEY