
Blazor Blog Feature
- 33 installs
- 22 repo stars
- Updated February 19, 2026
- markpitt/claude-skills
Helps with ai & agent building tasks.
About
blazor-blog-feature is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted coding.
- blazor-blog-feature
- AI & Agent Building
- AI-coding skill
Blazor Blog Feature by the numbers
- 33 all-time installs (skills.sh)
- Ranked #8,975 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 2, 2026 (Skillselion catalog sync)
npx skills add https://github.com/markpitt/claude-skills --skill blazor-blog-featureAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 33 |
|---|---|
| repo stars | ★ 22 |
| Last updated | February 19, 2026 |
| Repository | markpitt/claude-skills ↗ |
What it does
Helps with ai & agent building tasks.
Files
Blog Feature Skill for Blazor WASM + Azure Functions
This skill provides a complete, production-ready blog feature for Blazor WASM applications hosted on Azure Static Web Apps with serverless backend processing.
Quick Reference: When to Load Which Resource
| Your Task | Load Resource | Key Concepts |
|---|---|---|
| Understand architecture, prerequisites, shared models | resources/core-architecture.md | 3-layer architecture, project structure, data models |
| Implement backend services, Azure Functions, file share integration | resources/backend-services.md | BlogStorageService, YAML parsing, DI setup |
| Build Blazor components, UI pages, styling | resources/frontend-components.md | Razor components, markdown rendering, responsive design |
| Configure Azure environment, local settings, deployment | resources/azure-configuration.md | Connection strings, file share setup, environment variables |
| Create sample content, test workflow, troubleshoot issues | resources/sample-content-testing.md | Sample markdown, testing checklist, common issues |
Orchestration Protocol
Phase 1: Setup & Understanding
Before writing any code, establish context:
1. Review your current Blazor WASM project structure 2. Confirm you have Azure Functions API project ready 3. Verify Azure Storage account and File Share access 4. Load resources/core-architecture.md to understand 3-layer design
Quick assessment:
- Do you have existing Blazor WASM + Functions project? → YES, proceed
- Do you need to understand what to build? → Load core-architecture.md first
- Are you setting up Azure resources? → Go to azure-configuration.md
Phase 2: Implementation Selection
Choose your implementation path:
| Your Situation | Load This First | Then Load |
|---|---|---|
| Starting from scratch | core-architecture.md | backend-services.md |
| Backend complete, need UI | frontend-components.md | (skip backend-services.md) |
| Just need configuration help | azure-configuration.md | (reference other resources as needed) |
| Debugging or testing | sample-content-testing.md | (target troubleshooting section) |
Phase 3: Execution & Validation
Implementation sequence:
1. Create project structure (core-architecture.md Step 1-2) 2. Add NuGet packages (backend-services.md) 3. Implement BlogStorageService (backend-services.md) 4. Create Azure Functions (backend-services.md) 5. Build Blazor components (frontend-components.md) 6. Configure Azure environment (azure-configuration.md) 7. Test locally (sample-content-testing.md testing workflow) 8. Deploy to Azure (azure-configuration.md deployment section)
Validation checkpoints:
- Backend: Functions respond correctly to test calls
- Frontend: Components load and display posts
- Integration: End-to-end blog viewing works
- Azure: Configuration deployed and accessible
Common Workflow Scenarios
Scenario 1: Fresh Implementation (First Time)
Timeline: 2-3 hours
1. Read core-architecture.md → understand what you're building 2. Follow backend-services.md → implement API layer 3. Follow frontend-components.md → build UI layer 4. Follow azure-configuration.md → configure Azure resources 5. Use sample-content-testing.md → validate with sample posts
Scenario 2: Existing Backend, Need Frontend
Timeline: 1 hour
1. Skip to frontend-components.md 2. Reference core-architecture.md if component questions arise 3. Use sample posts from sample-content-testing.md 4. Deploy following azure-configuration.md
Scenario 3: Update Existing Blog
Timeline: 30 minutes
1. Jump to relevant resource file 2. Reference back to core-architecture.md for context 3. Test changes with sample-content-testing.md checklist
Scenario 4: Troubleshooting Issues
Timeline: As needed
1. Go directly to sample-content-testing.md 2. Find problem in troubleshooting section 3. Reference other resources for context if needed
Architecture Summary
Frontend → Backend → Storage:
- Blazor WASM pages call HTTP endpoints
- Azure Functions retrieve from File Share
- Markdown files with YAML frontmatter contain all content
- No database needed (files are the database)
Key Components:
- BlogStorageService: Abstracts File Share interactions
- BlogFunctions: HTTP endpoints for listing/retrieving posts
- Index/Post Razor Components: Client UI for browsing
- CSS Styling: Responsive design for all screen sizes
Implementation Complexity
| Component | Complexity | Time |
|---|---|---|
| Backend Services | Medium | 45 min |
| Azure Functions | Easy | 30 min |
| Frontend Components | Medium | 60 min |
| Styling | Easy | 30 min |
| Configuration | Easy | 20 min |
| Total | Easy-Medium | ~3 hours |
Prerequisites Checklist
- ✅ Existing Blazor WASM SWA project
- ✅ Azure Functions API project
- ✅ Azure Storage account with File Share
- ✅ .NET 10 SDK (or later)
- ✅ Azure CLI (for deployment)
- ✅ Visual Studio Code or Visual Studio
Resource Files Summary
resources/core-architecture.md (285 lines)
Foundational knowledge about the blog system architecture, project structure, and shared data models needed across frontend and backend.
Load when: Getting started, understanding the design, creating shared models
resources/backend-services.md (425 lines)
Complete implementation of Azure File Share service integration, BlogStorageService class, and Azure Functions for blog operations.
Load when: Building the API layer, implementing backend services
resources/frontend-components.md (610 lines)
Blazor Razor components for blog listing and detail pages, CSS styling for responsive design, navigation integration.
Load when: Building the UI layer, styling components, creating Razor pages
resources/azure-configuration.md (445 lines)
Azure environment setup, local development configuration, File Share structure, deployment guidelines, and security considerations.
Load when: Setting up Azure resources, configuring environments, deploying to production
resources/sample-content-testing.md (395 lines)
Sample markdown formats, complete testing workflow checklist, troubleshooting guide for common issues, and enhancement ideas.
Load when: Creating test data, validating implementation, debugging problems
Best Practices
1. Start with core-architecture.md - Don't skip understanding the design 2. Implement sequentially - Backend first, then frontend, then configuration 3. Test locally - Use Azure Storage Emulator before deploying 4. Use sample content - Test with provided markdown examples 5. Follow naming conventions - Consistent file naming prevents errors
Quick Navigation by Goal
| I want to... | Resource | Section |
|---|---|---|
| Understand the system | core-architecture.md | Architecture Overview |
| Create the backend | backend-services.md | BlogStorageService |
| Build the UI | frontend-components.md | Blog Listing Page |
| Set up Azure | azure-configuration.md | Azure File Share Setup |
| Test everything | sample-content-testing.md | Testing Workflow |
| Fix a problem | sample-content-testing.md | Troubleshooting Guide |
| Deploy to production | azure-configuration.md | Deployment Checklist |
Support & Next Steps
After implementation:
- Add pagination for better performance (recommended)
- Implement search functionality
- Consider caching for frequently-accessed posts
- Monitor Azure Function cold starts
- Optimize featured image sizes for performance
Enhancement opportunities:
- RSS feed generation
- Comment system integration
- Post categories and tagging
- Admin content management interface
- Email newsletter subscription
---
Built with: Blazor WASM, Azure Functions, Azure File Share, Markdown, YAML frontmatter
Skill Type: Feature Implementation (Blazor WASM + Azure)
Difficulty: Easy-Medium (3-4 hours total)
Azure Configuration & Deployment
Local Development Settings
Create or update local.settings.json in your API project:
{
"IsEncrypted": false,
"Values": {
"AzureWebJobsStorage": "UseDevelopmentStorage=true",
"FUNCTIONS_WORKER_RUNTIME": "dotnet-isolated",
"BlogFileShareName": "blog-content"
}
}Configuration Properties:
AzureWebJobsStorage: Connection string for storage (development uses emulator)FUNCTIONS_WORKER_RUNTIME: Must be "dotnet-isolated" for .NET 10BlogFileShareName: Name of the file share containing blog posts
Azure File Share Setup
Step 1: Create Storage Account
1. Go to Azure Portal 2. Create new Storage Account 3. Note the connection string for configuration 4. Choose geographically close to your users for latency
Step 2: Create File Share
1. In Storage Account → File shares 2. Create new file share (name: blog-content or your preference) 3. Record the share name
Step 3: Create Blog Directory Structure
Using Azure Storage Explorer or Azure CLI:
# Create blog-posts directory in file share
# This is where all markdown files will be storedOr programmatically via Azure Storage Explorer: 1. Connect to storage account 2. Navigate to file share 3. Create folder blog-posts 4. Create subfolder blog-posts/images for featured images
Step 4: Get Connection String
1. Go to Storage Account → Access keys 2. Copy connection string (under key1) 3. Store securely (never commit to git)
Azure Static Web App Configuration
Environment Variables
Add these configuration variables to your Azure Static Web App:
# Using Azure CLI
az staticwebapp appsettings set \
--name your-app-name \
--setting-names \
BlogFileShareName=blog-content \
AzureWebJobsStorage="<your-connection-string>"Or via Azure Portal: 1. Go to Static Web App → Configuration 2. Add Application settings:
- Name:
BlogFileShareName - Value:
blog-content - Name:
AzureWebJobsStorage - Value:
<your-connection-string>
staticwebapp.config.json
Optional: Add routing configuration for blog URLs:
{
"routes": [
{
"route": "/blog/*",
"serve": "/index.html",
"statusCode": 200
},
{
"route": "/api/*",
"allowedRoles": ["authenticated", "anonymous"]
}
],
"navigationFallback": {
"rewrite": "/index.html",
"exclude": ["/api/*", "/*.{json,jpg,gif,png,webp,css,js,svg,eot,ttf,woff,woff2}"]
}
}This ensures:
- Blog routes are handled by Blazor SPA (for client-side routing)
- API routes are passed through to Azure Functions
- Static assets are served correctly
Local Development Workflow
Prerequisites
1. Install Azure Storage Emulator or use Azurite 2. Start the emulator/Azurite
Running Locally
# Terminal 1: Start Azure Functions
cd src/YourProject.Api
func start
# Terminal 2: Start Blazor WASM with SWA CLI
swa start http://localhost:3000 --api-location apiThen navigate to: http://localhost:3000/blog
Testing Endpoints
# Test backend API directly
curl http://localhost:7071/api/blog/posts
curl http://localhost:7071/api/blog/posts/your-post-slug
# Test through SWA CLI
curl http://localhost:3000/api/blog/postsFile Share Content Format
Markdown File Structure
Store markdown files in Azure File Share at blog-posts/{slug}.md:
---
title: "Post Title"
slug: "post-slug"
author: "Your Name"
publishedDate: 2025-01-15T10:00:00Z
excerpt: "Brief summary for listings"
tags:
- tag1
- tag2
featuredImage: "/images/blog/featured.jpg"
readTimeMinutes: 5
---
# Post Title
Your markdown content here.
## Section 2
More content...Required YAML Properties:
title: Blog post titleslug: URL-friendly identifier (must match filename without .md)author: Author namepublishedDate: ISO 8601 datetimeexcerpt: Summary for blog listings (50-150 chars)tags: Array of category tagsfeaturedImage: URL to featured imagereadTimeMinutes: Estimated reading time
Optional YAML Properties:
updatedDate: Last modification date
Image Management
1. Upload images to blog-posts/images/ directory 2. Reference in markdown:  3. Use relative paths from web root
Deployment Checklist
Before deploying to production:
- [ ] Connection string configured in Static Web App settings
- [ ] Blog file share created and accessible
- [ ] Sample blog posts uploaded to file share
- [ ] Environment variable
BlogFileShareNameset correctly - [ ] API routes proxied in
staticwebapp.config.json - [ ] Blazor SPA routing configured for
/blog/* - [ ] CORS configured if needed (usually not for same-origin)
- [ ] SSL/TLS certificate valid (Azure SWA handles this)
- [ ] Functions runtime version matches .NET SDK
Monitoring & Troubleshooting
Azure Portal Monitoring
1. Go to Static Web App → Settings → Functions 2. Check function app logs 3. Review Application Insights (if enabled)
Common Issues
Issue: 403 Forbidden accessing file share
- Verify connection string is correct
- Check storage account firewall settings
- Ensure managed identity has access (if using)
Issue: Blog posts not loading (404)
- Verify file share and blog-posts directory exist
- Check markdown file naming (use slug format)
- Verify connection string in configuration
Issue: Markdown not rendering
- Check Markdig package is installed in Client
- Verify markdown syntax is valid
- Look for console errors in browser dev tools
Issue: Featured images not loading
- Verify image URLs are correct
- Check image files exist in
blog-posts/images/ - Use CDN for better performance (future enhancement)
Local Debugging
Use Azure Storage Explorer to: 1. Connect to your storage account 2. Browse file share contents 3. Verify markdown file structure 4. Check frontmatter YAML syntax 5. View file modification times
Security Considerations
Authentication & Authorization
- Blog endpoints are currently anonymous (AuthorizationLevel.Anonymous)
- For admin functions (create/update posts), add higher auth level
- Consider Azure AD for content management UI
Connection String Security
- Never commit connection string to git
- Use Azure Key Vault in production
- Use managed identities for Azure-hosted applications
- Store locally in user secrets (dev)
Content Security
- Sanitize user-generated content if accepting comments
- Validate image uploads if adding image management
- Implement rate limiting on API endpoints
- Use Content Security Policy headers
Performance Optimization
Caching Strategy
Frontend:
- Cache blog list for 5-10 minutes
- Cache individual posts for 1-2 hours
- Invalidate on new post upload
Backend:
- Add response caching headers
- Consider Redis cache layer for frequent posts
- Cache file share directory listing
CDN Optimization
1. Use Azure CDN for static assets (images, CSS, JS) 2. Set appropriate cache headers 3. Enable gzip compression 4. Optimize featured image sizes
Scaling Considerations
Current Architecture:
- Single file share (auto-scales)
- Serverless Azure Functions (auto-scales)
- Static hosting via SWA (auto-scales)
Future Enhancements:
- Add pagination for blog listings
- Implement search functionality
- Add caching layer
- Consider database for metadata (if thousands of posts)
Deployment Pipelines
GitHub Actions Example
name: Deploy Blog Feature
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Build and Deploy
uses: Azure/static-web-apps-deploy@v1
with:
azure_static_web_apps_api_token: ${{ secrets.AZURE_STATIC_WEB_APPS_TOKEN }}
repo_token: ${{ secrets.GITHUB_TOKEN }}
action: "upload"
app_location: "/src/YourProject.Client"
api_location: "src/YourProject.Api"
output_location: "wwwroot"Next Steps
1. Create sample blog posts using sample-content-troubleshooting.md 2. Deploy and validate in staging environment 3. Monitor logs and performance metrics 4. Plan content management workflow
Backend Services & Azure Functions Implementation
Required NuGet Packages
Add these packages to your Api project:
# In the Api project directory
dotnet add package Azure.Storage.Files.Shares
dotnet add package YamlDotNet
dotnet add package MarkdigThese provide:
- Azure.Storage.Files.Shares: Azure File Share client library for C#
- YamlDotNet: Parse YAML frontmatter from markdown files
- Markdig: Markdown processing (optional for backend, required for client)
BlogStorageService: File Share Integration
Create the core service for interacting with Azure File Share:
// Services/BlogStorageService.cs
using Azure.Storage.Files.Shares;
using Azure.Storage.Files.Shares.Models;
using YamlDotNet.Serialization;
using YamlDotNet.Serialization.NamingConventions;
namespace YourProject.Api.Services;
public interface IBlogStorageService
{
Task<List<BlogPostMetadata>> GetAllPostMetadataAsync();
Task<BlogPost?> GetPostBySlugAsync(string slug);
}
public class BlogStorageService : IBlogStorageService
{
private readonly ShareClient _shareClient;
private readonly ILogger<BlogStorageService> _logger;
private const string BlogPostsDirectory = "blog-posts";
public BlogStorageService(string connectionString, string shareName, ILogger<BlogStorageService> logger)
{
_shareClient = new ShareClient(connectionString, shareName);
_logger = logger;
}
public async Task<List<BlogPostMetadata>> GetAllPostMetadataAsync()
{
var posts = new List<BlogPostMetadata>();
var directoryClient = _shareClient.GetDirectoryClient(BlogPostsDirectory);
await foreach (var item in directoryClient.GetFilesAndDirectoriesAsync())
{
if (item.IsDirectory || !item.Name.EndsWith(".md"))
continue;
var fileClient = directoryClient.GetFileClient(item.Name);
var download = await fileClient.DownloadAsync();
using var reader = new StreamReader(download.Value.Content);
var content = await reader.ReadToEndAsync();
var metadata = ParseFrontmatter(content, item.Name);
if (metadata != null)
posts.Add(metadata);
}
return posts.OrderByDescending(p => p.PublishedDate).ToList();
}
public async Task<BlogPost?> GetPostBySlugAsync(string slug)
{
var directoryClient = _shareClient.GetDirectoryClient(BlogPostsDirectory);
var fileName = $"{slug}.md";
var fileClient = directoryClient.GetFileClient(fileName);
try
{
var download = await fileClient.DownloadAsync();
using var reader = new StreamReader(download.Value.Content);
var content = await reader.ReadToEndAsync();
return ParseFullPost(content, fileName);
}
catch (Azure.RequestFailedException ex) when (ex.Status == 404)
{
_logger.LogWarning("Blog post not found: {Slug}", slug);
return null;
}
}
private BlogPostMetadata? ParseFrontmatter(string content, string fileName)
{
var parts = content.Split("---", 3);
if (parts.Length < 3) return null;
var deserializer = new DeserializerBuilder()
.WithNamingConvention(CamelCaseNamingConvention.Instance)
.Build();
var frontmatter = deserializer.Deserialize<BlogPostMetadata>(parts[1].Trim());
frontmatter.Id = Path.GetFileNameWithoutExtension(fileName);
return frontmatter;
}
private BlogPost? ParseFullPost(string content, string fileName)
{
var parts = content.Split("---", 3);
if (parts.Length < 3) return null;
var deserializer = new DeserializerBuilder()
.WithNamingConvention(CamelCaseNamingConvention.Instance)
.Build();
var post = deserializer.Deserialize<BlogPost>(parts[1].Trim());
post.Id = Path.GetFileNameWithoutExtension(fileName);
post.Content = parts[2].Trim();
return post;
}
}Azure Functions Implementation
GetBlogPosts Function
Lists all blog posts with metadata (for blog index page):
[Function("GetBlogPosts")]
public async Task<HttpResponseData> GetBlogPosts(
[HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "blog/posts")] HttpRequestData req)
{
_logger.LogInformation("Getting all blog posts");
try
{
var posts = await _blogService.GetAllPostMetadataAsync();
var response = req.CreateResponse(HttpStatusCode.OK);
await response.WriteAsJsonAsync(posts);
return response;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error getting blog posts");
var response = req.CreateResponse(HttpStatusCode.InternalServerError);
await response.WriteStringAsync("Error retrieving blog posts");
return response;
}
}GetBlogPost Function
Retrieves a single blog post by slug (for blog detail page):
[Function("GetBlogPost")]
public async Task<HttpResponseData> GetBlogPost(
[HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "blog/posts/{slug}")] HttpRequestData req,
string slug)
{
_logger.LogInformation("Getting blog post: {Slug}", slug);
try
{
var post = await _blogService.GetPostBySlugAsync(slug);
if (post == null)
{
var notFoundResponse = req.CreateResponse(HttpStatusCode.NotFound);
await notFoundResponse.WriteStringAsync($"Blog post '{slug}' not found");
return notFoundResponse;
}
var response = req.CreateResponse(HttpStatusCode.OK);
await response.WriteAsJsonAsync(post);
return response;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error getting blog post: {Slug}", slug);
var response = req.CreateResponse(HttpStatusCode.InternalServerError);
await response.WriteStringAsync("Error retrieving blog post");
return response;
}
}Complete BlogFunctions Class
// Functions/BlogFunctions.cs
using Microsoft.Azure.Functions.Worker;
using Microsoft.Azure.Functions.Worker.Http;
using Microsoft.Extensions.Logging;
using System.Net;
namespace YourProject.Api.Functions;
public class BlogFunctions
{
private readonly IBlogStorageService _blogService;
private readonly ILogger<BlogFunctions> _logger;
public BlogFunctions(IBlogStorageService blogService, ILogger<BlogFunctions> logger)
{
_blogService = blogService;
_logger = logger;
}
[Function("GetBlogPosts")]
public async Task<HttpResponseData> GetBlogPosts(
[HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "blog/posts")] HttpRequestData req)
{
_logger.LogInformation("Getting all blog posts");
try
{
var posts = await _blogService.GetAllPostMetadataAsync();
var response = req.CreateResponse(HttpStatusCode.OK);
await response.WriteAsJsonAsync(posts);
return response;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error getting blog posts");
var response = req.CreateResponse(HttpStatusCode.InternalServerError);
await response.WriteStringAsync("Error retrieving blog posts");
return response;
}
}
[Function("GetBlogPost")]
public async Task<HttpResponseData> GetBlogPost(
[HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "blog/posts/{slug}")] HttpRequestData req,
string slug)
{
_logger.LogInformation("Getting blog post: {Slug}", slug);
try
{
var post = await _blogService.GetPostBySlugAsync(slug);
if (post == null)
{
var notFoundResponse = req.CreateResponse(HttpStatusCode.NotFound);
await notFoundResponse.WriteStringAsync($"Blog post '{slug}' not found");
return notFoundResponse;
}
var response = req.CreateResponse(HttpStatusCode.OK);
await response.WriteAsJsonAsync(post);
return response;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error getting blog post: {Slug}", slug);
var response = req.CreateResponse(HttpStatusCode.InternalServerError);
await response.WriteStringAsync("Error retrieving blog post");
return response;
}
}
}Dependency Injection Configuration
Update Program.cs in the API project to register the blog service:
// Program.cs additions
builder.Services.AddSingleton<IBlogStorageService>(sp =>
{
var connectionString = Environment.GetEnvironmentVariable("AzureWebJobsStorage");
var shareName = Environment.GetEnvironmentVariable("BlogFileShareName") ?? "blog-content";
var logger = sp.GetRequiredService<ILogger<BlogStorageService>>();
return new BlogStorageService(connectionString!, shareName, logger);
});Key Implementation Details
Frontmatter Parsing
- Uses YAML frontmatter format:
--- metadata --- content - Properties: title, slug, author, publishedDate, excerpt, tags, featuredImage, readTimeMinutes
- Automatically extracts from markdown between
---delimiters
File Share Integration
- Connects via connection string
- Reads from
blog-posts/directory - Filters
.mdfiles only - Orders by publication date (newest first)
- Handles 404 gracefully when post not found
Error Handling
- Logs all operations with request information
- Returns 404 when post not found
- Returns 500 on unexpected errors
- Provides meaningful error messages to client
Performance Considerations
GetAllPostMetadataAsyncfetches metadata only (smaller payloads)GetPostBySlugAsyncfetches full content on demand- Metadata caching can be added in future (see Next Steps)
- Connection string loaded from configuration once at startup
Testing Locally
Test Azure Functions locally:
# Start the functions runtime
dotnet run --project src/YourProject.Api
# Test GetBlogPosts endpoint
curl http://localhost:7071/api/blog/posts
# Test GetBlogPost endpoint
curl http://localhost:7071/api/blog/posts/getting-startedExpected responses:
GetBlogPosts: Returns array of BlogPostMetadata objectsGetBlogPost: Returns single BlogPost object with full content
Next Steps
1. Implement frontend components in frontend-components.md 2. Configure Azure environment in azure-configuration.md 3. Add sample content using sample-content-troubleshooting.md
Core Architecture & Project Setup
Architecture Overview
The blog feature architecture consists of three main layers:
Frontend (Blazor WASM)
- Static Web App deployed frontend
- Blog listing page (
Pages/Blog/Index.razor) - Blog post detail page (
Pages/Blog/Post.razor) - Markdown rendering using Markdig
- Client-side HTTP calls to backend API
Backend (Azure Functions)
- HTTP-triggered functions for blog operations
GetBlogPostsfunction - list all posts with metadataGetBlogPostfunction - retrieve individual post by slug- Dependency injection for services
- Logging and error handling
Storage (Azure File Share)
- Azure Storage account with File Share
/blog-postsdirectory structure- Markdown files with YAML frontmatter metadata
/blog-posts/imagessubdirectory for featured images- Centralized content management
Prerequisites
Before implementing the blog feature, ensure you have:
Development Environment:
- .NET 10 SDK (or current version)
- VS Code with C# extensions
- Azure Storage Explorer (optional but recommended)
- Git for version control
Azure Resources:
- Azure Storage account created
- File Share created (typically named
blog-content) - Connection string available
- Static Web Apps resource configured
Existing Projects:
- Blazor WASM SWA project (typically
*.Client.csproj) - Azure Functions API project (typically
*.Api.csproj) - Shared models project (or create DTOs)
- Existing routing patterns in Client project
Knowledge:
- Basic Blazor components and pages
- Azure Functions concepts
- Azure Storage basics
- Markdown format understanding
Project Structure After Implementation
The complete blog implementation will have this structure:
YourProject/
├── src/
│ ├── YourProject.Client/
│ │ ├── Pages/
│ │ │ └── Blog/
│ │ │ ├── Index.razor
│ │ │ └── Post.razor
│ │ ├── wwwroot/
│ │ │ └── css/
│ │ │ └── blog.css
│ │ └── Program.cs (with HttpClient config)
│ │
│ ├── YourProject.Api/
│ │ ├── Functions/
│ │ │ └── BlogFunctions.cs
│ │ ├── Services/
│ │ │ ├── IBlogStorageService.cs
│ │ │ └── BlogStorageService.cs
│ │ ├── Models/
│ │ │ └── BlogPost.cs
│ │ ├── Program.cs (with DI setup)
│ │ └── local.settings.json
│ │
│ └── YourProject.Shared/
│ └── Models/
│ ├── BlogPost.cs
│ └── BlogPostMetadata.cs
│
└── staticwebapp.config.json (optional routing config)Shared Data Models
Define DTOs that will be shared between Client and API projects:
BlogPost Model
namespace YourProject.Shared.Models;
public class BlogPost
{
public string Id { get; set; } = string.Empty;
public string Title { get; set; } = string.Empty;
public string Slug { get; set; } = string.Empty;
public string Author { get; set; } = string.Empty;
public DateTime PublishedDate { get; set; }
public DateTime? UpdatedDate { get; set; }
public string Excerpt { get; set; } = string.Empty;
public string Content { get; set; } = string.Empty;
public List<string> Tags { get; set; } = new();
public string FeaturedImage { get; set; } = string.Empty;
public int ReadTimeMinutes { get; set; }
}BlogPostMetadata Model
namespace YourProject.Shared.Models;
public class BlogPostMetadata
{
public string Id { get; set; } = string.Empty;
public string Title { get; set; } = string.Empty;
public string Slug { get; set; } = string.Empty;
public string Author { get; set; } = string.Empty;
public DateTime PublishedDate { get; set; }
public DateTime? UpdatedDate { get; set; }
public string Excerpt { get; set; } = string.Empty;
public List<string> Tags { get; set; } = new();
public string FeaturedImage { get; set; } = string.Empty;
public int ReadTimeMinutes { get; set; }
}Architecture Decision Points
When to use Metadata vs. Full Post:
- Metadata Only: Blog listing page (faster, less data transfer)
- Full Post: Detail page (includes markdown content for rendering)
Why Azure File Share vs. Database:
- Markdown files are self-contained documents
- Easy to version control separately
- Direct image storage alongside content
- Simpler deployment model for SWA
Why Separate Frontend and Backend:
- Static Web App hosting model (frontend static, backend serverless)
- Scalability (frontend via CDN, backend via Functions scale)
- Security (API behind Azure Function authentication)
- Deployment separation (independent CI/CD pipelines)
Technology Stack
| Component | Technology | Version | Purpose |
|---|---|---|---|
| Frontend | Blazor WASM | .NET 10+ | Interactive UI, markdown rendering |
| Backend | Azure Functions | .NET 10+ isolated | HTTP API, File Share integration |
| Storage | Azure File Share | v2023-11 | Content storage for markdown |
| Hosting | Static Web Apps | Latest | Frontend + serverless backend hosting |
| Markdown | Markdig | 0.34+ | Markdown to HTML conversion |
| YAML Parser | YamlDotNet | 13.7+ | Parse frontmatter metadata |
Next Steps
1. Frontend Components: Implement Index.razor and Post.razor pages 2. Backend Services: Create BlogStorageService for File Share integration 3. Azure Functions: Implement GetBlogPosts and GetBlogPost functions 4. Configuration: Set up local.settings.json and environment variables 5. Styling: Apply CSS for responsive blog UI 6. Content: Create sample markdown files in Azure File Share 7. Testing: Validate locally before deployment
See backend-services.md for implementation details.
Frontend Components & UI Implementation
Client-Side NuGet Packages
Add this package to your Client project:
# In the Client project directory
dotnet add package MarkdigThis provides markdown-to-HTML conversion in the browser.
Blog Listing Page Component
Create the blog index page that displays all posts:
@* Pages/Blog/Index.razor *@
@page "/blog"
@inject HttpClient Http
@inject NavigationManager Navigation
<PageTitle>Blog</PageTitle>
<div class="blog-container">
<h1>Blog</h1>
<p class="lead">Latest articles and insights</p>
@if (isLoading)
{
<div class="loading">
<p>Loading posts...</p>
</div>
}
else if (posts == null || !posts.Any())
{
<div class="no-posts">
<p>No blog posts available yet. Check back soon!</p>
</div>
}
else
{
<div class="blog-grid">
@foreach (var post in posts)
{
<article class="blog-card" @onclick="() => NavigateToPost(post.Slug)">
@if (!string.IsNullOrEmpty(post.FeaturedImage))
{
<img src="@post.FeaturedImage" alt="@post.Title" class="featured-image" />
}
<div class="blog-card-content">
<h2>@post.Title</h2>
<div class="post-meta">
<span class="author">@post.Author</span>
<span class="date">@post.PublishedDate.ToString("MMM dd, yyyy")</span>
<span class="read-time">@post.ReadTimeMinutes min read</span>
</div>
<p class="excerpt">@post.Excerpt</p>
<div class="tags">
@foreach (var tag in post.Tags)
{
<span class="tag">@tag</span>
}
</div>
</div>
</article>
}
</div>
}
</div>
@code {
private List<BlogPostMetadata>? posts;
private bool isLoading = true;
protected override async Task OnInitializedAsync()
{
try
{
posts = await Http.GetFromJsonAsync<List<BlogPostMetadata>>("api/blog/posts");
}
catch (Exception ex)
{
Console.WriteLine($"Error loading posts: {ex.Message}");
}
finally
{
isLoading = false;
}
}
private void NavigateToPost(string slug)
{
Navigation.NavigateTo($"/blog/{slug}");
}
}Blog Post Detail Page Component
Create the blog detail page that displays a single post with markdown rendering:
@* Pages/Blog/Post.razor *@
@page "/blog/{slug}"
@inject HttpClient Http
@inject NavigationManager Navigation
@using Markdig
<PageTitle>@(post?.Title ?? "Loading...")</PageTitle>
<div class="blog-post-container">
@if (isLoading)
{
<div class="loading">
<p>Loading post...</p>
</div>
}
else if (post == null)
{
<div class="not-found">
<h1>Post Not Found</h1>
<p>The blog post you're looking for doesn't exist.</p>
<button class="btn-primary" @onclick="NavigateToList">Back to Blog</button>
</div>
}
else
{
<article class="blog-post">
@if (!string.IsNullOrEmpty(post.FeaturedImage))
{
<img src="@post.FeaturedImage" alt="@post.Title" class="hero-image" />
}
<header>
<h1>@post.Title</h1>
<div class="post-meta">
<span class="author">By @post.Author</span>
<span class="date">@post.PublishedDate.ToString("MMMM dd, yyyy")</span>
@if (post.UpdatedDate.HasValue)
{
<span class="updated">Updated: @post.UpdatedDate.Value.ToString("MMM dd, yyyy")</span>
}
<span class="read-time">@post.ReadTimeMinutes min read</span>
</div>
<div class="tags">
@foreach (var tag in post.Tags)
{
<span class="tag">@tag</span>
}
</div>
</header>
<div class="content">
@((MarkupString)renderedContent)
</div>
<footer>
<button class="btn-secondary" @onclick="NavigateToList">← Back to Blog</button>
</footer>
</article>
}
</div>
@code {
[Parameter]
public string Slug { get; set; } = string.Empty;
private BlogPost? post;
private string renderedContent = string.Empty;
private bool isLoading = true;
protected override async Task OnInitializedAsync()
{
await LoadPost();
}
protected override async Task OnParametersSetAsync()
{
await LoadPost();
}
private async Task LoadPost()
{
isLoading = true;
try
{
post = await Http.GetFromJsonAsync<BlogPost>($"api/blog/posts/{Slug}");
if (post != null)
{
var pipeline = new MarkdownPipelineBuilder()
.UseAdvancedExtensions()
.Build();
renderedContent = Markdown.ToHtml(post.Content, pipeline);
}
}
catch (HttpRequestException ex) when (ex.StatusCode == System.Net.HttpStatusCode.NotFound)
{
post = null;
}
catch (Exception ex)
{
Console.WriteLine($"Error loading post: {ex.Message}");
post = null;
}
finally
{
isLoading = false;
}
}
private void NavigateToList()
{
Navigation.NavigateTo("/blog");
}
}Navigation Integration
Add blog link to your main navigation component:
@* Shared/NavMenu.razor or similar *@
<NavLink class="nav-link" href="blog">
<span class="icon">📝</span> Blog
</NavLink>CSS Styling
Create comprehensive styling for the blog feature:
/* wwwroot/css/blog.css */
/* Blog Container & Layout */
.blog-container {
max-width: 1200px;
margin: 0 auto;
padding: 2rem;
}
/* Blog Grid - Listing Page */
.blog-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
gap: 2rem;
margin-top: 2rem;
}
.blog-card {
background: white;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
cursor: pointer;
transition: transform 0.2s, box-shadow 0.2s;
}
.blog-card:hover {
transform: translateY(-4px);
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
}
.blog-card .featured-image {
width: 100%;
height: 200px;
object-fit: cover;
}
.blog-card-content {
padding: 1.5rem;
}
.blog-card h2 {
margin: 0 0 1rem;
font-size: 1.5rem;
color: #333;
}
/* Post Metadata */
.post-meta {
display: flex;
gap: 1rem;
flex-wrap: wrap;
font-size: 0.875rem;
color: #666;
margin-bottom: 1rem;
}
.post-meta span {
display: flex;
align-items: center;
}
.post-meta .author::before {
content: "✍️ ";
margin-right: 0.25rem;
}
.post-meta .date::before {
content: "📅 ";
margin-right: 0.25rem;
}
.post-meta .read-time::before {
content: "⏱️ ";
margin-right: 0.25rem;
}
.post-meta .updated::before {
content: "✏️ ";
margin-right: 0.25rem;
}
/* Excerpt & Tags */
.excerpt {
color: #444;
line-height: 1.6;
margin-bottom: 1rem;
}
.tags {
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
}
.tag {
background: #f0f0f0;
color: #333;
padding: 0.25rem 0.75rem;
border-radius: 16px;
font-size: 0.75rem;
font-weight: 500;
}
/* Blog Post Detail Page */
.blog-post-container {
max-width: 800px;
margin: 0 auto;
padding: 2rem;
}
.blog-post .hero-image {
width: 100%;
height: 400px;
object-fit: cover;
border-radius: 8px;
margin-bottom: 2rem;
}
.blog-post header h1 {
font-size: 2.5rem;
color: #333;
margin-bottom: 1rem;
}
/* Content Rendering */
.blog-post .content {
line-height: 1.8;
font-size: 1.125rem;
color: #333;
}
.blog-post .content h2 {
margin-top: 2rem;
margin-bottom: 1rem;
color: #333;
font-size: 1.75rem;
border-bottom: 2px solid #f0f0f0;
padding-bottom: 0.5rem;
}
.blog-post .content h3 {
margin-top: 1.5rem;
margin-bottom: 0.75rem;
color: #444;
font-size: 1.25rem;
}
.blog-post .content img {
max-width: 100%;
height: auto;
border-radius: 4px;
margin: 1.5rem 0;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.blog-post .content p {
margin-bottom: 1rem;
}
.blog-post .content ul,
.blog-post .content ol {
margin-bottom: 1rem;
margin-left: 1.5rem;
}
.blog-post .content li {
margin-bottom: 0.5rem;
}
.blog-post .content blockquote {
border-left: 4px solid #007bff;
padding-left: 1rem;
margin: 1rem 0;
color: #666;
font-style: italic;
}
/* Code Formatting */
.blog-post .content code {
background: #f5f5f5;
padding: 0.2rem 0.4rem;
border-radius: 3px;
font-family: 'Courier New', monospace;
font-size: 0.9em;
}
.blog-post .content pre {
background: #2d2d2d;
color: #f8f8f8;
padding: 1rem;
border-radius: 4px;
overflow-x: auto;
margin-bottom: 1rem;
}
.blog-post .content pre code {
background: none;
padding: 0;
color: inherit;
}
/* State Messages */
.loading {
text-align: center;
padding: 3rem 1rem;
color: #666;
}
.no-posts {
text-align: center;
padding: 3rem 1rem;
background: #f9f9f9;
border-radius: 8px;
}
.not-found {
text-align: center;
padding: 3rem 1rem;
}
.not-found h1 {
color: #d32f2f;
margin-bottom: 1rem;
}
/* Buttons */
.btn-primary,
.btn-secondary {
padding: 0.75rem 1.5rem;
border: none;
border-radius: 4px;
font-size: 1rem;
cursor: pointer;
transition: background-color 0.2s;
}
.btn-primary {
background-color: #007bff;
color: white;
}
.btn-primary:hover {
background-color: #0056b3;
}
.btn-secondary {
background-color: #6c757d;
color: white;
}
.btn-secondary:hover {
background-color: #545b62;
}
/* Responsive Design */
@media (max-width: 768px) {
.blog-container {
padding: 1rem;
}
.blog-grid {
grid-template-columns: 1fr;
gap: 1.5rem;
}
.blog-post-container {
padding: 1rem;
}
.blog-post header h1 {
font-size: 1.75rem;
}
.blog-post .hero-image {
height: 250px;
}
.post-meta {
flex-direction: column;
gap: 0.25rem;
}
.blog-post .content {
font-size: 1rem;
}
}
/* Print Styles */
@media print {
.blog-container,
.blog-post-container {
max-width: none;
}
.btn-primary,
.btn-secondary {
display: none;
}
.blog-card {
break-inside: avoid;
}
}Key Component Features
Index.razor (Blog Listing)
- Grid layout with responsive design (auto-fill, minmax)
- Loading state with spinner
- Empty state message
- Click navigation to post detail
- Metadata display (author, date, read time)
- Tag display for categorization
Post.razor (Blog Detail)
- Featured image support
- Markdown-to-HTML rendering
- Back navigation
- 404 handling for missing posts
- Related metadata display
- Responsive typography
Styling Highlights
- Mobile-responsive grid (mobile: 1 column, desktop: 3 columns)
- Hover effects for interactivity
- Code block styling for technical content
- Print styles for blog archives
- Accessibility-friendly color contrast
Component Communication Flow
User navigates to /blog
↓
Index.razor OnInitialized
↓
Http.GetFromJsonAsync("api/blog/posts")
↓
Backend: GetBlogPosts function
↓
BlogStorageService: GetAllPostMetadataAsync
↓
Renders blog grid with posts
↓
User clicks post card
↓
NavigateTo("/blog/{slug}")
↓
Post.razor OnInitialized
↓
Http.GetFromJsonAsync("api/blog/posts/{slug}")
↓
Backend: GetBlogPost function
↓
BlogStorageService: GetPostBySlugAsync
↓
Renders full post with markdownNext Steps
1. Configure Azure environment in azure-configuration.md 2. Add sample content using sample-content-troubleshooting.md 3. Test components locally before deployment
Sample Content, Testing & Troubleshooting
Sample Blog Post Format
Create an example markdown file for your blog:
---
title: "Getting Started with Our Blog"
slug: "getting-started"
author: "Your Name"
publishedDate: 2025-01-15T10:00:00Z
excerpt: "Welcome to our blog! Discover the latest insights, tutorials, and updates."
tags:
- welcome
- getting-started
featuredImage: "/images/blog/welcome.jpg"
readTimeMinutes: 5
---
# Getting Started
Welcome to our blog! This is where we share insights, tutorials, and updates.
## What You'll Find Here
- **Technical Tutorials**: Step-by-step guides for developers
- **Industry Insights**: Analysis and trends in technology
- **Product Updates**: Latest features and improvements
- **Best Practices**: Tips and techniques for better development
## Featured Content Series
We organize content into several key areas:
### Blazor Development
Learn building modern web applications with Blazor, from basic components to advanced patterns.
### Azure Services
Deep dives into Azure services including Static Web Apps, Functions, Storage, and more.
### .NET Best Practices
Industry best practices for building robust, scalable .NET applications.
## Getting Started as a Reader
1. Browse blog posts on the [blog main page](/blog)
2. Click any post to read the full article
3. Check the tags to find related articles
4. Share posts with your network
## For Content Creators
To add a new blog post:
1. Create markdown file in `blog-posts/` folder
2. Include required YAML frontmatter
3. Write your content
4. Upload to Azure File Share
5. Posts appear automatically within minutes
## Stay Connected
Subscribe to our updates and follow us for the latest content!
---
**Happy reading!** We hope you find valuable content here.Testing Workflow - Complete Checklist
Phase 1: Local Setup
- [ ] Install .NET 10 SDK
- [ ] Install Azure Functions Core Tools (v4+)
- [ ] Install Azure Static Web Apps CLI
- [ ] Create local test markdown files
- [ ] Start Azure Storage Emulator or Azurite
Phase 2: Backend Testing
# Start Azure Functions locally
cd src/YourProject.Api
func start
# In another terminal, test endpoints
curl http://localhost:7071/api/blog/posts
curl http://localhost:7071/api/blog/posts/getting-started
# Expected response for first call:
# [] (empty array if no posts)
# Expected response for second call:
# 404 error (post doesn't exist yet)Validation:
- [ ] Both endpoints respond
- [ ] Endpoints return correct HTTP status codes
- [ ] Function logs show activity in terminal
Phase 3: File Share Integration Testing
1. Add sample markdown file to local file share emulator 2. Verify filename matches format: {slug}.md 3. Test GetBlogPosts endpoint 4. Test GetBlogPost endpoint with slug
Expected behavior:
- [ ] GetBlogPosts returns array with 1+ posts
- [ ] GetBlogPost returns full post data
- [ ] Frontmatter YAML parsed correctly
- [ ] Markdown content included in response
Phase 4: Frontend Component Testing
# Start Blazor with SWA CLI
swa start http://localhost:3000 --api-location api
# Navigate to http://localhost:3000/blogValidation:
- [ ] Blog listing page loads
- [ ] Posts display in grid
- [ ] Featured images appear
- [ ] Metadata displays correctly
- [ ] Click navigates to detail page
Phase 5: Markdown Rendering Testing
On blog detail page:
- [ ] Headings render correctly
- [ ] Lists format properly
- [ ] Code blocks display with syntax highlighting
- [ ] Links are clickable
- [ ] Images display
- [ ] Tables (if included) render
Phase 6: Error State Testing
Test these scenarios:
- [ ] No posts exist → "No posts available" message
- [ ] Invalid slug → 404 with "Post Not Found"
- [ ] API unavailable → Error message displays
- [ ] Featured image missing → Graceful fallback
- [ ] Malformed frontmatter → Post skipped or error logged
Phase 7: Performance Testing
Measure with browser DevTools:
- [ ] Blog listing page loads in <2s
- [ ] Blog detail page loads in <1s
- [ ] No console errors
- [ ] Network requests have acceptable size
Phase 8: Responsive Testing
Test on multiple screen sizes:
- [ ] Mobile (375px): Single column layout
- [ ] Tablet (768px): 2-3 columns
- [ ] Desktop (1440px): Full grid layout
- [ ] Large (1920px+): Maintains max-width
Troubleshooting Guide
Problem: Blog posts not loading (empty list)
Symptom: GetBlogPosts returns empty array []
Diagnosis Steps: 1. Check file share connection string in local.settings.json 2. Verify markdown files exist in blog-posts/ directory 3. Check file names end with .md 4. Verify YAML frontmatter is valid (between --- markers)
Solution: 1. Open Azure Storage Explorer 2. Connect to local storage emulator 3. Navigate to file share → blog-posts folder 4. Add test markdown file if missing 5. Restart Azure Functions 6. Retest endpoint
Problem: "Post not found" error
Symptom: GetBlogPost returns 404 for valid slug
Diagnosis Steps: 1. Verify filename matches slug: {slug}.md 2. Check slug parameter in URL matches filename 3. Verify file encoding is UTF-8 4. Check for case sensitivity in slug
Example:
- Slug:
getting-started - Expected filename:
getting-started.md✓ - Won't work:
Getting-Started.md✗
Solution: 1. Rename file to match slug exactly 2. Test with simplified slug (e.g., "test") 3. Verify frontmatter contains matching slug property
Problem: Markdown not rendering
Symptom: Blog detail page shows raw markdown text
Diagnosis Steps: 1. Check browser console for errors 2. Verify Markdig package installed in Client 3. Check markdown syntax validity 4. Look for special characters that need escaping
Solution:
// Verify pipeline creation in Post.razor
var pipeline = new MarkdownPipelineBuilder()
.UseAdvancedExtensions()
.Build();
renderedContent = Markdown.ToHtml(post.Content, pipeline);1. Check UseAdvancedExtensions() is called 2. Verify markdown content is being passed correctly 3. Test with simple markdown first (# Heading)
Problem: Featured images not displaying
Symptom: Image placeholders show but images don't load
Diagnosis Steps: 1. Check image URL in frontmatter 2. Verify image file exists at specified path 3. Check browser Network tab for 404 errors 4. Test with absolute vs. relative paths
Solution: 1. Use absolute path: /images/blog/my-image.jpg 2. Verify image uploaded to correct location 3. Check image format is supported (jpg, png, gif, webp) 4. Fallback: Use no featured image for testing
Problem: Components not updating after page navigation
Symptom: Detail page shows old post when navigating between posts
Diagnosis Steps: 1. Check OnParametersSetAsync override in Post.razor 2. Verify slug parameter changes on navigation 3. Check component re-initialization logic
Solution:
// Ensure Post.razor has proper navigation handling
protected override async Task OnParametersSetAsync()
{
await LoadPost(); // Re-load when slug changes
}Problem: YAML frontmatter not parsed
Symptom: Posts missing metadata (author, tags, etc.)
Diagnosis Steps: 1. Verify YAML syntax validity 2. Check property names match expected format 3. Verify date format is ISO 8601 4. Check indentation (YAML is whitespace-sensitive)
Valid YAML Example:
---
title: "Post Title"
slug: "post-slug"
author: "Name"
publishedDate: 2025-01-15T10:00:00Z
excerpt: "Summary"
tags:
- tag1
- tag2
---Common Issues:
- ❌
published-dateshould bepublishedDate(camelCase) - ❌ Missing quotes around strings
- ❌ Inconsistent indentation
- ✓ ISO 8601 dates with Z suffix
Problem: Azure Static Web App deployment fails
Symptom: Deployment pipeline fails or blog not working in production
Diagnosis Steps: 1. Check build logs in Azure Portal 2. Verify environment variables configured 3. Test locally first before deploying 4. Review staticwebapp.config.json syntax
Solution: 1. Ensure all configuration variables set:
BlogFileShareNameAzureWebJobsStorage
2. Verify connection string format 3. Test Functions deployment separately 4. Check SWA configuration for correct API location
Sample Data for Testing
Minimal Post
---
title: "Test Post"
slug: "test-post"
author: "Tester"
publishedDate: 2025-01-15T10:00:00Z
excerpt: "Testing"
tags:
- test
featuredImage: ""
readTimeMinutes: 1
---
# Test Post
This is a test.Comprehensive Post
---
title: "Complete Blazor Blog Implementation"
slug: "blazor-blog-complete"
author: "Jane Developer"
publishedDate: 2025-01-20T14:30:00Z
updatedDate: 2025-01-21T09:00:00Z
excerpt: "A comprehensive guide to implementing a blog feature in Blazor WASM with Azure backend"
tags:
- blazor
- azure
- blog
- tutorial
featuredImage: "/images/blog/blazor-blog.jpg"
readTimeMinutes: 12
---
# Complete Blazor Blog Implementation
## Introduction
Learn how to build a production-ready blog feature for your Blazor application.
## Architecture Overview
The blog uses three key components:
### Frontend
- Blazor WASM components
- Markdown rendering
- Responsive UI
### Backend
- Azure Functions API
- File Share integration
- Metadata parsing
### Storage
- Azure File Share
- Markdown files
- Image management
## Getting Started
1. Set up Azure resources
2. Create Shared models
3. Implement backend services
4. Build frontend components
5. Test and deploy
## Code Example
public class BlogPost { public string Title { get; set; } public string Content { get; set; } }
## Conclusion
This completes the implementation guide!Quick Reference - Common Tasks
Add a New Blog Post
1. Create markdown file: your-slug.md 2. Add YAML frontmatter with required properties 3. Write markdown content 4. Upload to blog-posts/ in file share 5. Wait 30-60 seconds for cache to clear 6. Post appears automatically
Update an Existing Post
1. Edit markdown file in file share 2. Update frontmatter properties as needed 3. Save changes 4. Cache clears automatically within 1 minute 5. Changes appear on site
Debug a Failing Post
1. Check filename matches slug exactly 2. Validate YAML frontmatter syntax 3. Verify content after --- delimiter 4. Look in function logs for parsing errors 5. Test with simpler markdown first
Performance Tuning
1. Resize featured images (< 200KB) 2. Limit posts per page (consider pagination) 3. Add CDN for static assets 4. Cache blog metadata in frontend 5. Consider database for thousands+ posts
Next Steps
1. Deploy to Azure Static Web App staging 2. Test full end-to-end workflow 3. Add additional sample posts 4. Plan content calendar 5. Consider enhancements (search, comments, categories)
Enhancement Ideas for Future
- Pagination: Limit posts per page for performance
- Categories: Organize posts by topic
- Search: Full-text search across posts
- RSS Feed: Generate RSS for subscribers
- Comments: Enable reader discussion
- Related Posts: Show similar articles
- Author Management: Multiple authors per post
- Draft Status: Schedule future posts
- Analytics: Track page views and engagement
- Email Subscription: Newsletter integration