A serverless static portal where AI auto-generates content daily and weekly
This document treats this site(moontaeyang.com) itself as an Azure demo and explains which services it uses, how they are composed, and why it was designed this way from the perspective of engineers who want to build it themselves.
moontaeyang.com is a personal learning portal where AI automatically generates and publishes content daily/weekly. On top of one static web hosting service(Static Web Apps), two scheduled AI agents(Container Apps Jobs) generate and attach content. The key themes are serverless · keyless(no secrets) · schedule-based · static-first.
$web accumulated storage). ┌──────────────────────────────────────┐
User(browser) ──HTTPS──▶ │ Azure Static Web Apps (Standard) │
moontaeyang.com │ - Global CDN + free TLS certificate │
│ - Custom domains(moontaeyang.com,www)│
Social login ──/.auth/*──▶ │ - Built-in auth(Google·GitHub·Kakao OIDC)│
│ - Static file serving + SPA fallback │
└───────────────▲──────────────────────┘
│ swa deploy(rebuild entire portal)
┌────────────────────────────────┴────────────────────────────────┐
│ │
┌─────────────┴─────────────┐ ┌─────────────────┴───────────────┐
│ Container Apps Job │ Daily 06:00 KST │ Container Apps Job │ Every Mon 06:00 KST
│ engitdaily-job │ (cron 0 21 * * *) │ azwhatsnew-job │ (cron 0 21 * * 0)
│ 1) Collect HN/RSS │ │ 1) Collect Azure Updates RSS │
│ 2) Azure OpenAI summarize/│◀── Managed Identity(keyless) ──▶ │ 2) Azure OpenAI summarize │
│ generate │ │ │
│ 3) Accumulate in Storage │ │ 3) Accumulate in Storage │
│ $web │ │ $web │
│ 4) Rebuild/deploy portal │ │ 4) Rebuild/deploy portal │
│ 5) Telegram notification │ │ 5) Telegram notification │
└─────────────┬─────────────┘ └─────────────────┬───────────────┘
│ │
┌─────────────▼─────────────┐ ┌─────────────────▼───────────────┐
│ Storage(engit) $web │ Accumulates past daily pages │ Storage(azwn) $web │ Accumulates past weekly pages
│ engitdailyst… │ (permanent "warehouse") │ azwhatsnewst… │ (permanent "warehouse")
└────────────────────────────┘ └─────────────────────────────────┘
Shared: each agent has a dedicated ACR(image) · User-Assigned Managed Identity(UAMI) · Container Apps Environment.
Static sections(/labs, /demos) are included from repository HTML into the portal shell and published with every deployment.
| Service | Role | Notes |
|---|---|---|
| Azure Static Web Apps (Standard) | Public web serving, custom domain, TLS, social login | Standard is required for custom authentication/OIDC |
| Azure Container Apps Job | Generates content as scheduled(cron) batches | 0 instances normally → billed only when running(serverless) |
| Azure OpenAI (gpt-5.4) | Article/summary/vocabulary generation | Keyless: called via Managed Identity + RBAC |
Azure Storage(Blob, $web) |
Accumulated storage for generated pages(warehouse) + md history | Uses the static website container $web |
| Azure Container Registry(ACR) | Stores agent container images | Cloud build via az acr build |
| User-Assigned Managed Identity | ACR pull · Storage read/write · OpenAI call permissions | Core of zero-secret operations |
| Telegram Bot API(external) | Publication completion notifications | Token only as Job environment variable(secret) |
Microsoft Entra ID(app registration) is used to integrate GitHub/Google/Kakao OAuth apps with SWA authentication for social login.
customOpenIdConnectProviders(OIDC discovery)./.auth/* endpoints to be mounted. Without them, everything returns 404.swa deploy replaces the entire site. There is no concept of "append".$web as a permanent accumulated warehouse, and on every deployment
reassemble both warehouses + portal shell + static sections in full before running one swa deploy.
→ Whoever deploys, the "entire portal" is always uploaded, so sections do not overwrite each other.PORTAL_SECTIONS=engit-daily=<$web>;azure-daily=<$web>./engit-daily, /azure-daily): generated by agents → accumulated in Storage $web → mirrored during deployment./labs, /demos): repository HTML is included in the portal shell → carried along unchanged with every deployment(no agent/Storage required).Cognitive Services OpenAI User,
Storage Blob Data Contributor(own storage), Storage Blob Data Reader(for mirroring the other storage), AcrPull.DefaultAzureCredential() → no secrets remain in the repository.0 21 * * * · Weekly(Monday): 0 21 * * 0(Sunday 21:00 UTC = Monday 06:00 KST).publicNetworkAccess=Disabled → this demo uses RBAC protection while keeping public access enabled
(in environments where policy enforces blocking, Private Endpoint + VNet integration is the standard approach. Front Door is for inbound acceleration and is not appropriate for this use case).(A) User request flow
1. Browser requests moontaeyang.com → SWA returns static files from the global CDN(TLS automatic).
2. User clicks login → /.auth/login/<provider> → provider(Google/Kakao/GitHub) authentication → callback → session cookie.
3. SPA fallback: unmatched paths route to shell index.html(except /engit-daily/*,/azure-daily/*,/labs/*,/demos/*,/.auth/*).
(B) Content generation flow(agent, once per day/week)
1. Cron triggers the Job → container starts(image pulled from ACR).
2. Source collection(Hacker News API / Azure Updates RSS).
3. Generate articles, summaries, and vocabulary through Azure OpenAI(keyless).
4. Accumulate results in Storage $web(today's/this week's page + section index updates), and keep md history in Blob.
5. Portal rebuild: copy shell(+static sections labs/demos) + mirror the two dynamic sections from each $web → swa deploy.
6. Send a "publication complete + link" notification through Telegram.
Prices change, so verify actual charges with Azure Pricing/Cost Management(these figures are guidance only).
moontaeyang.com, www) + DNS(A/ALIAS, TXT verification).auth block to SWA staticwebapp.config.json(+ customOIDC for Kakao) → inject client-id/secret into SWA app settings(otherwise /.auth/* returns 404).$web static website enabled) + Azure OpenAI(deployment gpt-5.4).AcrPull · Cognitive Services OpenAI User · own Storage Blob Data Contributor · other Storage Blob Data Reader(mirror).az acr build → update Job(environment variables: PORTAL_SECTIONS, PORTAL_BASE_URL, SWA_DEPLOY_TOKEN(secret), etc.).swa deploy./labs, /demos from Markdown to HTML and include them in the portal shell./.auth/* returns 404 → provider app settings(client-id/secret) are missing. They are mounted after configuration.swa deploy replaces the whole site. Rebuild the entire portal every time(PORTAL_SECTIONS mirror).AuthorizationFailure → Storage publicNetworkAccess=Disabled. If policy enforces this, use Private Endpoint; otherwise public access Enabled + RBAC.