Implement a web app where:
- The page loads in the browser as a SwiftWASM app.
- The app automatically requests an image on the home page, article pages, and paginated archive pages.
- A same-session revisit of the same page reuses that page's last returned image from client-side session storage when available.
- The backend persists a stable per-page image key so repeat requests for the same page and request country reuse the existing image instead of generating a new one.
- When the daily generation budget is exhausted, the backend returns a random previously generated image instead of requesting a new one.
- The backend waits for image generation to finish before replying.
- The final image is rendered from a public S3 HTTPS URL.
- The image is generated by the OpenAI image generation API using the fixed model
gpt-image-1.5. - The generation prompt should use the request's origin country when the server can resolve it from the client IP through
country.is, and otherwise fall back to a generic worldwide prompt. - The backend performs
country.islookups and OpenAI image-generation requests with AsyncHTTPClient. - The backend uses a long-lived Hummingbird server.
- Frontend: SwiftWASM app using Parcel for typed HTTP JSON requests in the browser.
- Backend: Hummingbird server exposing a single synchronous
POSTendpoint. - Storage/delivery: A dedicated public S3 bucket exposing generated PNG objects under a dedicated prefix.
- The browser loads the SwiftWASM bundle and page HTML.
- The app reads the page context from the mount element.
- If session storage already contains an image URL for the same page path and page type, the app reuses that URL and skips the API call.
- Otherwise, the app calls
POST <API_URL>with page context. - The server derives the client IP from proxy forwarding headers, preferring
X-Real-IPwhen present and otherwise falling back toX-Forwarded-For, then looks up the origin country withcountry.is. - The server validates input and checks for a stable page-cache key derived from page context and resolved country before considering a new generation.
- If the page-cache key already exists, the server returns that image immediately.
- Otherwise, the server counts generated PNG objects already present under the current UTC day prefix in S3 to decide whether its soft daily generation budget has remaining capacity.
- If budget remains, the server creates a fresh unique dated image key, builds a country-aware prompt when country lookup succeeded, calls OpenAI, uploads the PNG to S3, writes the same image to the stable page-cache key, and returns
200 OK. - If the daily budget is exhausted, the server selects a random existing generated PNG from S3, copies it to the stable page-cache key, and returns
200 OKwith that page-cache image instead. - The app swaps the placeholder image source to the returned or cached URL.
- On successful API responses, the app stores the returned image URL in session storage for future visits to the same page in the current browser session.
- The
BytesizedCafeSwiftWASM package is built into the repo-rootbytesized-cafe-app/directory. - The site generator publishes that directory at
/bytesized-cafe-app/. - Published asset paths must preserve the generated nested package layout, including
/bytesized-cafe-app/platforms/browser.js.
The backend derives the public image origin from GENERATED_IMAGES_BUCKET and AWS_REGION as: https://<generated-images-bucket>.s3.<region>.amazonaws.com
- Keep S3 Object Ownership set to
Bucket owner enforced. - Keep object ACLs disabled.
- Public read comes from bucket policy, not object ACLs.
- Store generated images in a dedicated public S3 bucket separate from the static site bucket.
- Keep generated images under
IMAGE_GEN_PREFIX. - Grant anonymous
s3:GetObjectonarn:aws:s3:::<generated-images-bucket>/<IMAGE_GEN_PREFIX>/*. - Do not upload with
public-readACLs.
- Freshly generated image:
{IMAGE_GEN_PREFIX}/{YYYY}/{MM}/{DD}/{UUID}-{country-slug}.pngwhen the request country is known{IMAGE_GEN_PREFIX}/{YYYY}/{MM}/{DD}/{UUID}.pngwhen the request country is not known
- Stable page-cache image:
{IMAGE_GEN_PREFIX}/page-cache/{pageType}/{normalized-page-path}-{country-slug}.pngwhen the request country is known{IMAGE_GEN_PREFIX}/page-cache/{pageType}/{normalized-page-path}-anywhere.pngwhen the request country is not known
- Random fallback image:
- Prefer an existing PNG under
IMAGE_GEN_PREFIX/whose key ends in the current request's-{country-slug}.png - Fall back to any existing PNG under
IMAGE_GEN_PREFIX/when no country-matching image is available
- Prefer an existing PNG under
Rules:
- Fresh generation keys must not be derived from page context.
- Stable page-cache keys must be derived from page context and resolved country.
- API responses should prefer the stable page-cache key whenever one exists or is created during the request.
When uploading a freshly generated image:
Content-Type: image/pngCache-Control: public, max-age=31536000, immutable
Configure S3 Lifecycle expiration to prevent unbounded storage growth:
- Expire
IMAGE_GEN_PREFIX/after 30 days if regeneration on cache miss is acceptable.
This API uses a single action endpoint:
POST /api/cafe/generatetriggers generation or fallback selection.OPTIONS /api/cafe/generateis handled by Hummingbird CORS middleware.
Enable CORS on the server endpoint for browser access:
- Allowed methods:
POST,OPTIONS - Allowed headers:
Content-Type - Allowed origins: site origin(s) used to host the SwiftWASM app
Request JSON:
{
"context": {
"pagePath": "/posts/example-article",
"pageType": "article"
}
}pageType must be one of:
indexarticlearchive
Response:
- Status:
200 OK - Body:
{
"url": "https://<public-base-domain>/generated/v2/page-cache/article/posts/example-article-france.png"
}Rules:
urlis the final public image URL and must use the generated-images bucket public origin.- The response may return a stable per-page cache key when the page already has an assigned image.
- Return
200only after the image has been uploaded successfully or a random fallback image has been selected successfully. - Invalid input returns
4xx. - If the daily budget is exhausted and no fallback image exists, return
503. - Terminal upstream failures return
5xx.
- Parse and validate input JSON.
- Encapsulate S3 operations behind one
S3ImageStoreclient object that owns the bucket configuration and AWS client lifecycle for image upload and lookup operations. - Resolve the client IP address by preferring
X-Real-IPwhen present and otherwise falling back toX-Forwarded-For. - Look up the request origin country with
https://api.country.is/{ip}and convert the returned region code into an English country name when available. - Derive a stable page-cache key from page context and resolved country, and return it immediately when that object already exists in S3.
- Check the soft daily generation budget by counting PNG objects already present under the current UTC date prefix in S3.
- Build the public
url. - When budget remains:
- Generate a fresh unique image key.
- Build the prompt as a single random dish popular in the request country.
- Include a normalized
-{country-slug}suffix in the generated key when country lookup succeeds. - Instruct the model to prefer specific, visually distinct local dishes over generic national defaults, and to avoid repeatedly defaulting to globally common fast food unless it is genuinely the random choice.
- Fall back to the same prompt structure scoped to somewhere in the world when the client IP or country cannot be resolved.
- Call the OpenAI image generation API with model
gpt-image-1.5. - Upload the PNG to the generated image key used for the dated generation pool.
- Upload the same PNG to the stable page-cache key.
- Return the page-cache
url.
- When budget is exhausted:
- Prefer a random existing generated PNG key from S3 whose key suffix matches the current request country.
- Fall back to a random existing generated PNG key from S3 when no country-matching key is available.
- Copy the selected fallback image to the stable page-cache key without calling OpenAI.
- Return the page-cache
url.
- Show a loading placeholder immediately.
- Read page context from the mount element.
- If session storage contains a URL for the same page path and page type, reuse that URL and skip the API call.
- Otherwise, start a single
POSTrequest to the configured API URL. - When the request succeeds, swap the placeholder image source to the returned
url. - Persist the returned image URL in session storage keyed to the current page so the next same-session visit of that page can reuse it.
GENERATED_IMAGES_BUCKETOPENAI_API_KEYOPENAI_IMAGE_MODELIMAGE_GEN_PREFIXAWS_REGIONAWS_ACCESS_KEY_IDAWS_SECRET_ACCESS_KEYHOSTPORT
Local repo tooling may provide BACKEND_HOST and BACKEND_PORT as aliases for the backend runtime HOST and PORT values.
BYTESIZED_CAFE_API_URL
AWS_S3_BUCKETCLOUDFRONT_DISTRIBUTION_ID
The implementation is considered complete when:
- A same-session revisit of the same page reuses the last returned image URL from session storage without making a new backend request.
- A backend request for a page that already has a stable page-cache object returns that existing image URL without making a new OpenAI request.
- The backend returns
200only after a fresh image upload succeeds or a random fallback image has been selected. - When the daily budget is exhausted, the backend returns a random existing generated image instead of making a new OpenAI request.
- Fresh generations use the request origin country in the prompt when the server can resolve it from the client IP, and otherwise fall back to the generic worldwide prompt.
- Fresh generations include a country slug suffix in the image key when the request country is known.
- When the daily budget is exhausted, fallback selection prefers existing images whose keys match the current request country and otherwise falls back to any existing image.
- The backend persists deterministic per-page cache keys separately from the dated generation pool.
- The backend container image is built from
Backend/using the checked-inBackend/Dockerfile. - The backend container build and runtime stages pin the official
swift:6.3.0-bookwormandswift:6.3.0-bookworm-slimimages. - The checked-in
Backend/railway.tomlcodifies the Railway deploy settings that should live in source control, currently the Dockerfile builder and/healthhealthcheck. - The deployable product is the
Serverexecutable. - Railway builds and runs the production image from GitHub pushes, targeting the backend service with
railway up Backend --ci --path-as-root. - Deployment config changes are validated with
just validate-deployment, which delegates to./Scripts/validate-deployment-config.shto build the Docker image and validate workflow YAML parsing.
- Railway hosts the public backend service, injects runtime environment variables, and exposes a healthchecked HTTPS endpoint for the
Servercontainer. - The Railway service should define a stable custom domain so the static site can build against a fixed
BYTESIZED_CAFE_API_URL. - The GitHub Actions workflow under
.github/workflows/deploy.ymlis the production deployment path and is intended to run on pushes to the primary deployment branch. - The backend deploy job authenticates with a Railway project token, synchronizes the backend runtime variables into Railway, and deploys the
Backend/directory directly to the configured Railway project, environment, and service. - Railway service-level GitHub autodeploy should be disabled when the GitHub Actions workflow is the active deployment path, to avoid duplicate backend deployments from the same push.
- GitHub Actions repository variables and secrets are the source of truth for the backend runtime variables
GENERATED_IMAGES_BUCKET,OPENAI_API_KEY,OPENAI_IMAGE_MODEL,IMAGE_GEN_PREFIX,AWS_REGION,AWS_ACCESS_KEY_ID, andAWS_SECRET_ACCESS_KEY. - The backend deploy workflow sets
HOST=0.0.0.0andPORT=8080in Railway by default, unless the deploy job overridesRAILWAY_RUNTIME_HOSTorRAILWAY_RUNTIME_PORT. - The site deploy job continues to sync
Output/to S3 using a fixedBYTESIZED_CAFE_API_URL. - Paginated archive links use the literal deployed object paths under
/page/<n>/index.htmlbecause the production S3 and CloudFront setup does not rewrite clean directory URLs to nestedindex.htmlobjects. - After the S3 sync completes, the site deploy job invalidates the production CloudFront distribution with
CLOUDFRONT_DISTRIBUTION_IDfor/,/index.html,/page/*,/posts/*,/feed.rss,/bytesized-cafe-app/*,/css/*,/images/*, and/fonts/*.
- A repo-root
justfileprovides the primary entry point for common local tasks such asjust wasm,just site,just site-local,just backend, andjust local, along with deployment-oriented recipes likejust site-release,just site-deploy, andjust validate-deployment. - The repo's Swift package manifests target Swift tools version
6.3, the macOS GitHub Actions job installs Swift6.3.0, and the SwiftWasm site build uses the compatibleswift-6.3-RELEASESDK tag. Scripts/run-local.shprovides a one-command local stack for development and opens the local site in the default browser after the backend and static site server are ready.- The script rebuilds the
BytesizedCafeSwiftWASM bundle, regenerates the site withBYTESIZED_CAFE_API_URLpointed at a localhost backend, prebuilds the backend to avoid counting SwiftPM compilation against the startup timeout, starts the Hummingbird server, and servesOutput/over a local static HTTP server.