feat: add privacy policy; wire GA4 and Umami at build time
CI / ci (push) Successful in 2m13s

- /privacy/ page covering the contact form (Formspree), Umami, GA4 and
  server logs; linked from the footer
- Analytics.astro: GA4 alongside Umami, both limited to the production
  host so staging and local builds are not counted
- Dockerfile ARGs + CI build args from repo Actions variables
  GA_MEASUREMENT_ID and UMAMI_WEBSITE_ID (non-secret)
This commit is contained in:
bmr_bluecap committed 2026-10-23 13:49:56 -04:00
1 parent 375ee48ab1
commit 0c2b4a6ac6
7 files changed
+154 -8

No files matched your search

+3 -1
View File
@@ -1,7 +1,9 @@
PUBLIC_SITE_URL=https://www.bluecapstrategies.com
PUBLIC_CONTACT_FORM_ENDPOINT=
# Analytics — side-by-side trial (ADR-0005). Leave blank to disable each.
# Analytics — leave blank to disable each. Production values come from CI (Dockerfile ARGs).
# Google Analytics 4:
PUBLIC_GA_MEASUREMENT_ID=
# PostHog (Cloud free tier recommended for the trial):
PUBLIC_POSTHOG_KEY=
PUBLIC_POSTHOG_HOST=https://us.i.posthog.com
+4
View File
@@ -98,9 +98,13 @@ jobs:
# deploy time. CONTACT_FORM_ENDPOINT is the Formspree URL; set it as a repo Actions
# secret. If it is empty the build still succeeds and the contact form degrades to a
# mailto draft, so a missing secret is survivable but should be caught in review.
# GA_MEASUREMENT_ID / UMAMI_WEBSITE_ID are non-secret repo Actions *variables*
# (Settings → Actions → Variables); empty means that tracker is simply left out.
docker build \
--build-arg PUBLIC_CONTACT_FORM_ENDPOINT="${{ secrets.CONTACT_FORM_ENDPOINT }}" \
--build-arg PUBLIC_SITE_URL="https://www.bluecapstrategies.com" \
--build-arg PUBLIC_GA_MEASUREMENT_ID="${{ vars.GA_MEASUREMENT_ID }}" \
--build-arg PUBLIC_UMAMI_WEBSITE_ID="${{ vars.UMAMI_WEBSITE_ID }}" \
-t "$PUSH_IMAGE:${{ steps.tag.outputs.sha }}" -t "$PUSH_IMAGE:latest" .
# Registry pushes can time out on a slow blob; docker push is resumable, so retry.
push_retry() {
+7 -1
View File
@@ -12,8 +12,14 @@ WORKDIR /app
# .gitea/workflows/ci.yml.
ARG PUBLIC_CONTACT_FORM_ENDPOINT=""
ARG PUBLIC_SITE_URL="https://www.bluecapstrategies.com"
ARG PUBLIC_GA_MEASUREMENT_ID=""
ARG PUBLIC_UMAMI_SRC="https://stats.bluecapstrategies.com/script.js"
ARG PUBLIC_UMAMI_WEBSITE_ID=""
ENV PUBLIC_CONTACT_FORM_ENDPOINT=$PUBLIC_CONTACT_FORM_ENDPOINT \
PUBLIC_SITE_URL=$PUBLIC_SITE_URL
PUBLIC_SITE_URL=$PUBLIC_SITE_URL \
PUBLIC_GA_MEASUREMENT_ID=$PUBLIC_GA_MEASUREMENT_ID \
PUBLIC_UMAMI_SRC=$PUBLIC_UMAMI_SRC \
PUBLIC_UMAMI_WEBSITE_ID=$PUBLIC_UMAMI_WEBSITE_ID
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build
+26 -5
View File
@@ -1,15 +1,19 @@
---
/**
* Side-by-side visitor analytics (ADR-0005): PostHog + Umami, each gated by env vars so this is a
* no-op until configured. Set the PUBLIC_* vars at build time to enable either or both.
* Side-by-side visitor analytics, each gated by env vars so this is a no-op until configured.
* Set the PUBLIC_* vars at build time (Dockerfile ARGs, fed by CI) to enable any of them.
* - Umami: PUBLIC_UMAMI_SRC (script URL), PUBLIC_UMAMI_WEBSITE_ID — self-hosted, cookieless
* - GA4: PUBLIC_GA_MEASUREMENT_ID (G-XXXXXXXXXX)
* - PostHog: PUBLIC_POSTHOG_KEY, PUBLIC_POSTHOG_HOST (default https://us.i.posthog.com)
* - Umami: PUBLIC_UMAMI_SRC (script URL), PUBLIC_UMAMI_WEBSITE_ID
* Track the same event set in both to compare (see docs/seo-aeo-optimization.md §6b).
* Umami and GA4 only fire on the production host, so staging/LAN/local visits are not counted.
* Anything enabled here must be disclosed on /privacy/.
*/
const posthogKey = import.meta.env.PUBLIC_POSTHOG_KEY;
const posthogHost = import.meta.env.PUBLIC_POSTHOG_HOST ?? "https://us.i.posthog.com";
const umamiSrc = import.meta.env.PUBLIC_UMAMI_SRC;
const umamiWebsiteId = import.meta.env.PUBLIC_UMAMI_WEBSITE_ID;
const gaId = import.meta.env.PUBLIC_GA_MEASUREMENT_ID;
const prodHost = new URL(import.meta.env.PUBLIC_SITE_URL ?? "https://www.bluecapstrategies.com").hostname;
---
{
@@ -23,6 +27,23 @@ const umamiWebsiteId = import.meta.env.PUBLIC_UMAMI_WEBSITE_ID;
{
umamiSrc && umamiWebsiteId && (
<script is:inline defer src={umamiSrc} data-website-id={umamiWebsiteId} />
<script is:inline defer src={umamiSrc} data-website-id={umamiWebsiteId} data-domains={prodHost} />
)
}
{
gaId && (
<script is:inline define:vars={{ gaId, prodHost }}>
if (location.hostname === prodHost) {
window.dataLayer = window.dataLayer || [];
window.gtag = function () { window.dataLayer.push(arguments); };
window.gtag("js", new Date());
window.gtag("config", gaId);
var s = document.createElement("script");
s.async = true;
s.src = "https://www.googletagmanager.com/gtag/js?id=" + encodeURIComponent(gaId);
document.head.appendChild(s);
}
</script>
)
}
+1 -1
View File
@@ -28,6 +28,6 @@ import { nav, serviceNav, site } from "@data/site";
</section>
</div>
<div class="footer-bottom">
<p>(c) 2026 BlueCap Strategies, LLC. All rights reserved.</p>
<p>(c) 2026 BlueCap Strategies, LLC. All rights reserved. · <a href="/privacy/">Privacy Policy</a></p>
</div>
</footer>
+108
View File
@@ -0,0 +1,108 @@
---
import BaseLayout from "@layouts/BaseLayout.astro";
import { site } from "@data/site";
import { pageSchema } from "@utils/schema";
const description =
"How BlueCap Strategies collects, uses, and protects information when you visit this website or contact us.";
const effectiveDate = "October 23, 2026";
---
<BaseLayout
title="Privacy Policy"
description={description}
pathname="/privacy/"
jsonLd={[pageSchema({ title: "Privacy Policy", description, pathname: "/privacy/" })]}
>
<section class="insights-shell">
<article class="note-card privacy-policy">
<p class="eyebrow">Effective {effectiveDate}</p>
<h1>Privacy Policy</h1>
<p>
This policy explains what information BlueCap Strategies, LLC ("BlueCap Strategies," "we,"
"us") collects through www.bluecapstrategies.com, how we use it, and the choices you have.
</p>
<h2>Information you give us</h2>
<p>
When you use our contact form, we collect the details you enter: your name, email address,
phone number, organization, and message. The form is delivered to us by Formspree, a
form-processing service that receives the submission and forwards it to our inbox. We use
this information only to respond to your inquiry and to follow up about the work you asked
about. You can also reach us directly by email or phone instead of using the form.
</p>
<h2>Website analytics</h2>
<p>We use two analytics tools to understand how the site is used and to improve it.</p>
<ul>
<li>
<strong>Umami</strong>, which we host on our own infrastructure. It does not use cookies,
does not track you across other websites, and does not store your IP address. It records
aggregate information such as the pages viewed, the referring website, browser and device
type, and approximate country.
</li>
<li>
<strong>Google Analytics</strong>, provided by Google LLC. It uses cookies to distinguish
visitors and measure visits, and the data it collects is processed by Google under
<a href="https://policies.google.com/privacy">Google's Privacy Policy</a>. You can opt out
by installing the
<a href="https://tools.google.com/dlpage/gaoptout">Google Analytics opt-out browser
add-on</a> or by blocking cookies in your browser settings.
</li>
</ul>
<h2>Server and security logs</h2>
<p>
Like most websites, our servers automatically record technical details of each request,
such as IP address, browser type, the page requested, and the time of the request. We use
these logs only to keep the site secure and running, for example to block abusive traffic,
and keep them for a limited time.
</p>
<h2>What we do not do</h2>
<ul>
<li>We do not sell or rent your personal information.</li>
<li>We do not use your information for advertising, and we do not show ads on this site.</li>
<li>We do not knowingly collect information from children under 13.</li>
</ul>
<h2>Sharing</h2>
<p>
We share information only with the service providers named above that help us operate
this site, or when required by law.
</p>
<h2>Your choices</h2>
<p>
You may ask us to tell you what information we hold about you from a contact form
submission, or to correct or delete it, by emailing
<a href={site.contactEmailHref}>{site.email}</a>.
</p>
<h2>Changes to this policy</h2>
<p>
If we change how we handle information, we will update this page and its effective date.
</p>
<h2>Contact</h2>
<p>
Questions about this policy can be sent to <a href={site.contactEmailHref}>{site.email}</a>
or by phone at <a href={`tel:${site.phone.replaceAll("-", "")}`}>{site.phone}</a>.
</p>
</article>
</section>
</BaseLayout>
<style>
.privacy-policy {
max-width: 52rem;
margin-inline: auto;
}
.privacy-policy h2 {
font-size: 1.4rem;
margin: 2rem 0 0.5rem;
}
.privacy-policy li + li {
margin-top: 0.5rem;
}
</style>
+5
View File
@@ -1149,6 +1149,11 @@ textarea:focus {
font-size: 0.92rem;
}
.footer-bottom a {
color: inherit;
text-decoration: underline;
}
.insights-shell {
padding: 4rem 0;
display: grid;