Server-Rendered Price Tables: Lessons from Two Gold-Pricing Dashboards in Medan
Last year I worked on two company-profile projects for precious-metals businesses in Medan, North Sumatra — PT UNIMETALS INDO RAYA and PT AURUM GRIYA INVESTAMA. Both needed the same thing: a public page that shows gold prices, updated regularly, without the user having to call the store.
This post is about the technical decisions behind those dashboards. Not the marketing side — the data model, the refresh strategy, and the front-end choices that made them work on the devices people in Medan actually use.
The Problem With One Number
Most gold price pages show a single figure: the sell price. That's marketing, not information. The number that actually determines whether a gold purchase ever turns a profit is the spread — the gap between the sell price and the buyback price.
A useful reference is the price page on harga emas, which shows both columns side by side for Antam, UBS, and Galeri 24. That two-column layout became the minimum viable format for both projects.
The Data Model
Both projects ended up with nearly identical models:
PriceEntry {
vendor: "Antam" | "UBS" | "Galeri 24"
weightGram: number // 0.5, 1, 2, 5, 10, 25, 50, 100
sellPrice: number // IDR
buybackPrice: number // IDR
updatedAt: ISO8601 timestamp
source: "manual" | "scraper" | "api"
}
Three rules I'd apply to any similar project:
Every row gets an updatedAt. Price data without a timestamp is a liability.
Sell and buyback are separate fields. Never derive one from the other — they come from different sources and different spreads.
Weight is explicit. A 1 gram Antam and a 1 gram UBS are different products.
Refresh Strategy
Both projects refresh every 15 minutes. Slower than 30 minutes and users start doubting accuracy. Faster than 5 minutes on the server side was unnecessary.
Display layer (cached HTML): every 15 minutes
Server-side data refresh: every 5 minutes
Historical snapshot archive: weekly
The server cron job pulls from the upstream source, writes a JSON file, and the HTML table is regenerated from that JSON. No database query on page load.
The Front-End: Static Shell, Server-Rendered Table
This is the part I'd push back on if a client asked for a SPA. A page that displays a table does not need a JavaScript framework.
What we actually shipped:
Static HTML shell — no client-side rendering for the price table.
Server-rendered table — regenerated every 15 minutes from the cached JSON.
Small inline JS — updates the "last updated X minutes ago" label without re-fetching data.
No external CDN dependency for the table — if the CDN is down, the page still shows stale prices with a warning banner.
The last point matters more than it sounds. A dashboard that disappears when the CDN fails is worse than one that shows clearly-labeled stale data. Always show the timestamp.
Designing for the Actual Device
The dominant browsing profile in Medan is still a mid-range Android on a 3G connection. That constraint drives a lot of decisions:
No web fonts blocking first paint — system fonts only.
No third-party trackers before the price is visible — analytics loads after the table.
Table renders in the initial HTML payload — not fetched after hydration.
Responsive breakpoints at 360px and 480px — not 768px. 360px is what most users actually see.
Time-to-first-price on a throttled 3G connection ended up around 800ms. Not great, not terrible — but the price is visible before the page finishes loading.
What We Deliberately Skipped
Real-time price ticker — WebSocket infrastructure for no behavior change.
Price prediction charts — legally risky, factually dubious.
User accounts / login — nobody needs an account to check a price.
Push notifications — conversion too low to justify the permission prompt.
Editorial Notes
The technical dashboard is half the work. The other half is telling users what the numbers actually mean. Across both projects, the content followed three rules:
Show both sides of the trade. Sell and buyback, always together.
Show the corporate identity. For a commodity business, the entity name and physical address is part of the product. The second project uses emas medan as its public entry point — company profile, service information, and consultation channel in one place.
Explain the spread. One paragraph telling users why the sell price and buyback price differ is worth more than any chart.
Takeaways
Show both sides of the trade. One price is marketing; two prices is a service.
Every number gets a timestamp. Stale data labeled as stale is fine. Stale data presented as current is a bug.
Serve the data server-side. A table doesn't need a framework.
Design for the device your audience actually uses. 360px Android on 3G, not 1440px on fiber.
Fail gracefully. If the CDN is down, keep the last known price visible with a warning.
Both projects are live: unimetalsmedan.com (price page at harga emas) and emas medan. If you've built similar commodity-pricing interfaces for the Indonesian market, I'd be curious how you handled the refresh cadence and the buyback spread display.
Disclosure: this article is part of the company-profile series for both entities. Logo and trademark rights remain with their respective owners. dev.to links are typically nofollow; the anchors here are formatted for reuse in Medium or standalone HTML publications where rel="dofollow" applies.
Top comments (0)