I built SkinRotation, an unofficial Fortnite fan site, to show the daily item shop in German and keep a record of shop appearances. The live German shop is the first release. The data pipeline already handles German, Spanish, French and English, while the public pages currently focus on German.
The implementation uses Astro server-side rendering on Cloudflare Workers. Scheduled jobs fetch the shop, persist its history in a dedicated Supabase schema, publish page data to Workers KV and save the raw response in R2. Page requests read KV. A visitor opening the shop does not start another request to the upstream API.
One important distinction is the identity of an offer. A skin can appear as a standalone purchase and inside a bundle on the same day. I keep offer records and item records separately. On the skin page, a standalone offer takes precedence when choosing the current price. A bundle price is explicitly labelled. Adding every offer price together also needs a clear label because bundles and standalone offers can contain the same item.
The shop changes at 00:00 UTC, but an upstream response can lag behind that boundary. The scheduled job runs at 00:02 UTC, with another attempt at 00:05 and 00:10. It checks both the shop date and its hash. A stale response leaves the last successful snapshot available. Repeating a successful job for the same date and hash skips the write. Job records keep the result of each attempt, and three consecutive failures trigger an email alert.
The shop history starts with snapshots collected in November 2026. That limitation also shapes the UI. A missing record says that a skin has not appeared since our records began. It cannot establish that the skin has never appeared in the game. Consecutive days in the shop count as one appearance in the skin history.
Localisation is a mix of upstream data and small templates. Item names, descriptions, sets and shop sections use the API's translations. Interface labels and generated sentences live in a German string file. I format dates with Intl.DateTimeFormat, store shop dates in UTC and use Europe/Berlin when showing the reset time. Singular values, zero results and browser-rendered search results needed their own checks. Reviewing only the initial server-rendered page missed a singular/plural error in the search results.
Caching produced another practical bug. A cached HTML page could outlive a deployment and reference a CSS file from an older build. The page cache key now contains both the shop date and the deployment version. A new snapshot or a new deployment gets a fresh key. The small stylesheet is also inlined in the HTML.
The skin catalogue contains more outfits than there are standalone skin URLs. Only the initial set of detail pages is linked and included in the sitemap. The remaining outfits are available through the catalogue interface. Search and sorting use local state, which keeps the public URL set manageable while the history grows.
I used AI assistance during implementation and review. The offer-price bug and the client-side localisation mismatch were reminders to verify the rendered pages against the underlying data. A valid JSON response and a passing type check did not catch either issue on their own.
SkinRotation is independent of Epic Games. Shop and cosmetic data comes through fortnite-api.com, and each page displays its source and UTC update timestamp. Next I am watching the scheduled updates and indexing results before releasing another public language.