How I Turned Cloudflare Analytics Into a Public Portfolio Feature
How I exposed aggregate Cloudflare traffic and field performance through a server-only Next.js boundary, then shaped it into a privacy-conscious dashboard with global reach, clear chart semantics, and country-level interaction.
- Cloudflare
- Next.js
- Data Visualization
- Web Vitals
- Privacy
- System Design
Cloudflare already knew how this portfolio moved across the web. The problem was that the useful part of that story lived inside a private provider dashboard.
I wanted visitors to see the same aggregate signals—traffic, global reach, caching, bandwidth, and real-user performance—without exposing a provider token, request logs, IP addresses, or another generic analytics embed.
The work looked like a charting task at first. It became a small product system: a server-only data boundary, bounded query shapes, normalized contracts, privacy thresholds, interactive geographic exploration, and a public route that had to feel like part of the Portfolio rather than a copied operations screen.
The goal was not to copy a dashboard
The Cloudflare dashboard was the starting reference because it made several useful signals easy to scan:
| Signal | What it explains |
|---|---|
| Unique visitors | Approximate audience reach for the selected period |
| Total requests | All edge requests served for the zone |
| Cache efficiency | How much transferred data Cloudflare served from cache |
| Global reach | Where aggregate requests arrived from |
| Web Vitals | How the site behaved in measured real browsers |
But copying the provider screen would have copied the wrong product context. Cloudflare is an operational console. This page belongs to a public editorial portfolio.
The useful constraint was therefore: preserve the information architecture, not the chrome.
Architecture at a glance
The browser never calls Cloudflare. It asks the Portfolio route for a page, and the server performs both the edge-traffic and browser-experience queries while rendering that page.
That decision keeps the integration small and makes the security boundary obvious: credentials stop at the server module; only aggregate numbers reach the interactive components.
Keep the token behind the rendering boundary
The provider integration reads server-only Cloudflare credentials and identifiers. None uses a public environment prefix, none is serialized into page data, and none appears in a client bundle.
The token is deliberately narrow. It can read analytics for the configured account and zone, but it cannot edit DNS, deployments, or account administration. A public analytics page should reveal the minimum useful result, not the credentials or raw events used to calculate it.
The traffic contract contains only:
- a fixed range and granularity;
- start, end, and generation timestamps;
- aggregate chart buckets;
- aggregate totals and country-level request summaries.
The browser-experience contract contains P75 measurements and quality distributions for LCP, INP, CLS, FCP, and TTFB. There are no visitor identities, IP addresses, paths, user agents, or request-level records in either contract.
Three ranges required bounded query shapes
The interface offers 24 Hours, 7 Days, and 30 Days. Treating those as one query with a different start date would be simple, but it would produce the wrong grain and run into hourly-query duration limits.
The 24-hour view uses hourly groups. Its end is rounded down to the latest complete UTC hour, and its start is exactly 24 hours earlier. That avoids presenting a partial current-hour bucket beside complete hours as though they were comparable.
The 7-day and 30-day views use daily groups. They include the current UTC date and request only the number of buckets the interface can display.
Invalid or repeated query parameters do not create arbitrary provider queries. The parser chooses the first value, accepts only the three known ranges, and otherwise falls back to 24 hours. That fixed input set bounds the cache and provider workload at the same time.
Normalize provider data once
Cloudflare returns hourly and daily groups with different dimension names. The chart layer should not need to know whether a point came from datetime or date.
The server maps both responses into one traffic point shape:
bucket, visitors, requests, cachedPercent, bytes, cachedBytes
It also keeps the provider's aggregate total for unique visitors instead of adding unique counts across time buckets. The same visitor can appear in several hours or days, so those bucket values are not safely additive.
Cache percentage is derived from transferred bytes:
cached bytes / total bytes × 100
That matches the bandwidth story shown beside total data served and data cached. A zero-byte period returns zero rather than an invalid percentage.
The Web Vital response needs another normalization step. Cloudflare returns duration quantiles in microseconds, so the server converts them to milliseconds once. CLS remains unitless. The UI receives one stable P75 value per metric and does not reproduce provider-specific conversions.
P75 makes field performance honest
A single average can hide a slow tail. The experience section therefore uses the 75th percentile: three out of four measured visits were this fast or faster, while the slowest quarter took longer.
The dashboard exposes five signals:
- Largest Contentful Paint for when the main content becomes visible;
- Interaction to Next Paint for responsiveness after interaction;
- Cumulative Layout Shift for visual stability;
- First Contentful Paint for the first visible content;
- Time to First Byte for when the server starts responding.
Each metric is classified as good, needs improvement, or poor against an explicit threshold. Visitors can select one metric at a time to inspect its trend and measured experience mix rather than reading five competing lines in one chart.
This data is field data, not a synthetic lab run. Missing measurements stay missing, and the interface says when Cloudflare is still collecting enough evidence.
Global reach without a surveillance table
Edge traffic also provides aggregate country totals for requests, bytes, and threat signals. A world map is a better public explanation of reach than a long ranked list, but it still needs useful interaction.
Hovering or focusing a country shows a compact high-contrast tooltip with requests, data served, and threat signals. Selecting a country updates an animated inspector beside the map with:
- request volume and share of total traffic;
- data served and average response size;
- threat-request count;
- available country-level Web Vitals.
Country-level browser performance is withheld until Cloudflare has at least ten measured visits for that country. That threshold avoids presenting an unstable tiny sample as a meaningful performance result.
The map is keyboard-operable, its countries expose descriptive labels, and selection is not encoded by colour alone.
Make different quantities look different
The first chart pass used two lines on one plot with separate left and right axes. It saved space, but it also made the reader decode two scales and two similar shapes before understanding the story.
The revised traffic card uses aligned small multiples instead:
- requests are coral bars on their own scale;
- unique visitors are a blue area on a separate scale;
- both plots share the same time order;
- direct labels state that each value is measured per interval.
The delivery card uses a stacked area for cached and uncached bandwidth. The shape now communicates composition, not just two unrelated trends.
This removed the unexplained right-hand axis and made every chart answer one question. It also made the encodings intentionally different: bars for volume, area for audience trend, and stacked area for delivery composition.
Tooltips are part of the information design
The early tooltip used the same muted blue as one of the series. Blue text over a blue surface looked consistent in isolation but lost contrast exactly when the reader needed precision.
All analytics tooltips now use a cream surface, dark text, a muted blue border, and a restrained offset shadow. The treatment is shared by traffic charts, delivery charts, the map, and Web Vital charts.
The tooltip reports a date plus the relevant formatted value. It does not force the reader to infer units from colour or from a distant axis label.
Cache the provider read without hiding freshness
Analytics do not need to hit Cloudflare on every page view. Each fixed range is cached on the server for 15 minutes.
The cache reduces provider traffic, keeps the page responsive, and creates only three bounded entries. The interface shows when the snapshot was generated and labels times in UTC, so the optimization does not pretend the data is real-time to the second.
This is a useful distinction: fresh enough for a public trend view is not the same requirement as fresh enough for an incident console.
Make failure safe and boring
Provider integrations fail. A token can be missing, Cloudflare can return an error, or the response can omit a usable result.
Those cases return a small internal failure reason and render a calm unavailable or collecting state. The public page does not expose provider messages, configuration details, or stack traces. The rest of the Portfolio remains usable.
The server logs only the response status or error type needed for diagnosis. It does not log credentials or provider payloads.
Traffic and browser-experience reads also fail independently. A temporary RUM problem should not erase otherwise valid traffic analytics.
Turn operational data into Portfolio UI
The first implementation followed the Cloudflare reference too literally: dark charcoal panels, pale blue charts, and rounded dashboard cards. The data was correct, but the page felt borrowed.
I kept the scan pattern and changed the visual language around it:
- cream paper for the editorial frame and tooltip surfaces;
- a pale sky field for the data section;
- black rules instead of floating dark cards;
- signal coral for selection, indices, bars, and cached data;
- muted blue for visitor and uncached bandwidth areas;
- the Portfolio serif for large totals and headings;
- the existing sans face for controls, axes, and metadata.
Recharts handles the traffic and Web Vital charts. react-simple-maps and world-atlas power the geographic view. Decorative chart animations are disabled, while the selected-country panel uses restrained motion to make the state change understandable.
On small screens, cards and the country inspector stack into one column. The range control spans the available width, labels stay attached to their plots, and the page avoids horizontal overflow.
Navigation made it a product surface
A route can be public and still be practically invisible.
The first release had /analytics, metadata, sitemap coverage, and a link from the GitHub activity section, but it was missing from primary navigation. That made the feature feel secondary even though the page itself was complete.
Analytics now joins the Portfolio navigation on the homepage, every editorial subpage, and the mobile menu. The route mapper owns /analytics, and the current-page state works like the other destinations.
Adding another mobile destination also exposed an existing layout limit: long notes collided with the large serif labels. The menu grid now places each note below its label. Fixing the feature meant fixing the surrounding system it stressed.
Ship with evidence, not a screenshot
The implementation was validated at several layers:
- unit tests for range parsing, query windows, cache percentage, byte formatting, RUM duration conversion, Web Vital ratings, route mapping, and duplicate-free navigation;
- strict TypeScript and lint checks;
- architecture, identity, SEO, service-role, source-health, dead-code, and documentation checks;
- the production build and remote quality workflow;
- browser checks for all three ranges, chart tooltips, map selection, country detail, Web Vital selection, mobile layout, and console errors;
- preview and production deployments tied to exact Git commits;
- canonical-domain smoke tests after the production deployment became ready.
The final verification was not “a deployment started.” It was that the live page rendered aggregate Cloudflare data, the map and selectors responded, the chart scales were understandable, tooltips remained readable, and the console stayed clean.
The result
The Portfolio now has a public analytics page that explains traffic, delivery, geographic reach, security signals, and real-user experience without exposing the private machinery behind them.
The interesting part was not drawing graphs. It was deciding where the token stops, how time ranges become bounded queries, which totals can be safely aggregated, when a sample is too small to publish, how stale or missing data is communicated, and how a provider-shaped dashboard becomes a product-owned experience.
That is the difference between displaying data and shipping a data feature.
View the live analytics page or read how the Portfolio CMS keeps one source of truth.