What changed when our institutional site started using analytics
An institutional site feels harmless until it starts loading analytics. At that point it is no longer only a static brochure. It has its own data processing surface, its own privacy copy, and its own navigation obligations. The work is small, but it has to be explicit.
An institutional website can look like the least risky part of an app studio. It has a homepage, a blog, a few links to apps, and a contact email. There is no account system. No checkout. No user-generated content. No gameplay state. Compared with a mobile app that ships ads, purchases, crash reporting, and analytics, the studio site feels almost inert.
That assumption broke the moment we added real analytics IDs to the Sunstone Apps hub.
The IDs themselves were not secrets. A GA4 measurement ID and a Cloudflare Web Analytics token are meant to appear in client-side HTML. The problem was not credential leakage. The problem was what the site became once those scripts were allowed to run: a web property that measures visits, pages, referrers, devices, approximate regions, and technical request context.
That does not mean the site suddenly needed a heavy legal portal. It did mean the visitor deserved a clear privacy policy, and the codebase needed one place where every page could link to it consistently.
The fix ended up touching three areas that are easy to treat separately but should not be separated for long: analytics configuration, legal/SEO behavior, and site chrome.
A static brochure is not the same as a measured website
The first instinct was to treat the institutional site as a brochure.
That is fine while the site only publishes pages and links. The moment it loads analytics, the question changes. The site now has its own disclosure surface, even if it still has no accounts, no forms beyond email contact, and no product state.
The app policies can explain app behavior. They should not be stretched to cover every script on sunstoneapps.com. A visitor reading a blog post on the studio site should not have to infer that site analytics are described somewhere inside a mobile app policy.
The practical rule we settled on was simple:
- if the institutional site has no client-side analytics, a public privacy page may be less urgent;
- if the institutional site loads GA4 or Cloudflare Web Analytics, it should have a short privacy policy for that site;
- that policy should not claim the site collects app data, purchase data, precise location, contacts, camera, microphone, or anything else it does not touch.
The page we added is intentionally narrow. It says the site uses Cloudflare Web Analytics and Google Analytics for traffic measurement, mentions technical delivery data from hosting/CDN/security providers, and points privacy requests to [email protected]. It does not talk about mobile ads, purchases, app saves, or game progress. Those belong to app-specific policies.
That boundary matters. Over-broad privacy copy is not safer. It makes a small site look like it collects more than it does.
Legal pages should be reachable, not indexable
The next decision was SEO behavior.
We had already learned this lesson on the app sites: legal boilerplate does not need to compete for search traffic. Privacy pages, terms pages, and data deletion pages should be accessible, but they do not need to sit in the sitemap or appear as organic landing pages.
So the institutional privacy page follows the same pattern:
- it renders through the normal Astro layout;
- it has canonical and hreflang metadata;
- it is linked from the footer;
- it uses
noIndex={true}; - it is filtered out of the sitemap.
The tempting mistake was to add a Disallow rule to robots.txt for /privacy-policy/. That sounds like the right phrase if the goal is “do not index this.” It is not the best mechanism.
If a crawler is blocked by robots.txt, it may not be allowed to fetch the page and see the noindex meta tag. For a new page that has never been indexed, the practical risk is small. For a page already known to Google, blocking crawl can make cleanup slower or more confusing. The crawler can know the URL exists, but not read the page that tells it to drop the URL.
The cleaner rule is the one we now use across the sites: keep robots.txt open, publish the sitemap, filter legal pages out of the sitemap, and let the legal page itself say noindex.
That leaves robots.txt boring:
User-agent: *
Allow: /
Sitemap: https://sunstoneapps.com/sitemap-index.xml
Boring is the point. robots.txt should not become an SEO control panel.
This is also why we validated the production output, not only the source. A page can look correct in an Astro component while the generated HTML tells a different story. We checked that /privacy-policy/ rendered noindex,nofollow, that privacy-policy did not appear in the generated sitemap, and that the built robots.txt stayed open. That is the same habit described in our Google indexing checklist for Astro sites on Cloudflare Pages: Google indexes output, not intent.
The page exposed a chrome problem
The privacy page was not visually wrong because privacy pages are special. It was wrong because our site chrome had drifted.
The homepage had one footer. The blog index had another footer. Blog posts had their own hardcoded footer arrays. The new legal page copied a pattern from the app sites. The 404 page had yet another local set of header and footer labels, plus a small client-side script for Portuguese text.
Each copy looked reasonable when it was written. Together they created a maintenance bug.
Adding a privacy link to the homepage did not add it to the blog. Adding social links to the homepage did not add them to the legal page. Adjusting the language links in one place did not fix post pages. The pages all looked like they belonged to the same site, but the actual navigation contract was split across components and content files.
The privacy page made that obvious because legal links are not decorative. If a visitor enters through a blog post, that visitor should still be able to find the privacy policy. If a crawler reaches a post directly, footer links should still expose the same important internal routes. A sitemap helps discovery, but it should not be the only path through the site.
The fix was to make site chrome local and shared:
SiteHeader.astrowraps the shared@apps/site-uiHeaderandThemeToggle;SiteFooter.astrowraps the sharedFooter;src/lib/chrome.tsowns the locale-specific navigation, footer columns, legal notice, theme label, skip label, and language-link behavior.
Now the homepage, blog index, blog posts, privacy policy, and 404 page consume the same source.
That is different from moving the whole design system into the site. The reusable UI still belongs in @apps/site-ui. The studio-specific chrome belongs in the site, because its labels and links are content decisions for sunstoneapps.com. The important part is that there is one local source of truth, not five template-local copies.
Markdown was enough
We also corrected the content format.
The first version of the privacy policy used .mdx because the app sites already had legal pages in MDX. But this policy did not render components in the body. It was plain frontmatter plus Markdown. Using MDX would have worked, but it was unnecessary power.
The rule is now narrower: use .md when the legal content does not need JSX; use .mdx only when the content actually renders components. That keeps the content loader flexible without making every legal page look more complex than it is.
This is a small rule, but it prevents future drift. File extensions are part of the maintenance surface. If a future privacy policy needs a custom component, switching one file to MDX is easy. Starting every simple page as MDX teaches the opposite habit.
What we would check next time
The next time an institutional site gets analytics, we will not start with the tracking IDs. We will start with the surface area.
The checklist is now:
- Are analytics scripts enabled, and in which environments?
- Do Cloudflare Web Analytics, Clarity, and Google Analytics render only in production, not on
localhost? - Does the site have a privacy policy that matches the actual data flows?
- Is the privacy contact the right mailbox, not the general contact alias?
- Is the page linked from every public entry point?
- Is it
noindexand filtered out of the sitemap? - Is
robots.txtstill open so crawlers can readnoindex? - Do all page types share the same header and footer source?
- Does the production build prove the intended HTML, sitemap, and robots output?
- Does the local dev console stay clean after a full page reload?
That last point came from the browser, not from the build. Google Analytics was already gated to production, but Cloudflare Web Analytics still loaded on localhost. The request to https://cloudflareinsights.com/cdn-cgi/rum failed CORS in dev because Cloudflare answered for http://localhost, not http://localhost:4173. The fix was to make the shared AnalyticsScripts.astro treat every external analytics provider the same way: no Cloudflare beacon, no Clarity, and no GA script unless import.meta.env.PROD is true.
This keeps local validation honest. A clean console should mean the page is healthy, not that everyone learned to ignore one known analytics error. Production still gets measurement; development keeps fast feedback.
The production check caught one more detail. In the Cloudflare Dashboard, the Web Analytics edit URL contains an ID, but the manual snippet can show a different token inside data-cf-beacon. The page can be configured for sunstoneapps.com and still fail collection if the code uses the edit-page ID instead of the snippet token. The browser symptom was precise: beacon.min.js loaded, but /cdn-cgi/rum failed CORS and the dashboard stayed at zero visits. After switching to the token copied from Install JS Snippet, the same endpoint returned 204 and the console stayed clean.
So the manual-install check is now concrete: copy the token from the snippet textarea, inspect the rendered HTML, and verify the network result. Seeing the dashboard row is not enough.
None of this is glamorous. That is exactly why it is worth writing down.
Small websites accumulate risk through tiny exceptions: one footer array here, one legal page there, one analytics token added because it is “just a public ID.” The fix is not a large compliance framework. It is a short policy, explicit SEO behavior, and one shared chrome source so every page tells the same story.