diff --git a/docs/assets/badges/dotnet-10.svg b/docs/assets/badges/dotnet-10.svg new file mode 100644 index 0000000..034a27c --- /dev/null +++ b/docs/assets/badges/dotnet-10.svg @@ -0,0 +1 @@ +.NET: 10.NET10 \ No newline at end of file diff --git a/docs/assets/badges/license-mit.svg b/docs/assets/badges/license-mit.svg new file mode 100644 index 0000000..39d2244 --- /dev/null +++ b/docs/assets/badges/license-mit.svg @@ -0,0 +1 @@ +license: MITlicenseMIT \ No newline at end of file diff --git a/docs/assets/badges/nuget.svg b/docs/assets/badges/nuget.svg new file mode 100644 index 0000000..19eccbb --- /dev/null +++ b/docs/assets/badges/nuget.svg @@ -0,0 +1 @@ +NuGet: v4.0.3NuGetv4.0.3 \ No newline at end of file diff --git a/docs/index.md b/docs/index.md index c7585d4..b768fb6 100644 --- a/docs/index.md +++ b/docs/index.md @@ -16,10 +16,9 @@ title: Stratara — CQRS and Event Sourcing for .NET  GitHub
- NuGet - CI - MIT - .NET 10 + Latest release on NuGet + MIT licence + .NET 10
diff --git a/docs/legal/privacy.md b/docs/legal/privacy.md index 47ada53..7ebf2f8 100644 --- a/docs/legal/privacy.md +++ b/docs/legal/privacy.md @@ -71,9 +71,9 @@ necessary storage; the legal basis is our legitimate interest in providing a fun ## Cookies -This site sets no cookies. It embeds no advertising networks, and fonts are served from this site -rather than from an external font CDN. Only the localStorage values described above are stored, and -only the analytics tool described below is loaded — and that one only with your consent. +This site sets no cookies. It embeds no advertising networks. Only the localStorage values +described above are stored, and only the analytics tool described below is loaded — and that one +only with your consent. ## Web analytics (Umami) @@ -92,14 +92,12 @@ any time with effect for the future: -## Status badges from third parties +## No third-party requests before you agree -Some pages — the start page above all — show status badges that are loaded as images from -`img.shields.io` (Shields.io) and from `github.com` (GitHub Actions). Because your browser fetches -these images directly from those providers, your IP address and your browser's request headers -reach them when such a page loads. This happens before any consent, because the badges are page -content rather than analytics; the legal basis is our legitimate interest in showing the current -build and package status (Art. 6 (1) (f) GDPR). No cookies are set by these requests. +Apart from the analytics described above, this site loads nothing from another host. Fonts, icons, +diagrams, the search index and the status badges are all served from this site itself — the badges +are committed images, refreshed when a release is cut, rather than hot-linked from a badge service. +Until you accept the analytics notice, your browser therefore talks to nobody but this site's host. ## External links diff --git a/scripts/refresh-badges.sh b/scripts/refresh-badges.sh new file mode 100755 index 0000000..81688fd --- /dev/null +++ b/scripts/refresh-badges.sh @@ -0,0 +1,43 @@ +#!/usr/bin/env bash +# +# Regenerate the badge images the documentation site serves from docs/assets/badges/. +# +# The site serves them itself rather than hot-linking a badge service, so that a visitor's browser +# makes no third-party request before that visitor has agreed to anything. The cost of that is +# these files going stale, which is what `LandingBadgeTests` fails on and this script fixes. +# +# The version comes from the newest dated section of CHANGELOG.md — the same source llms.txt uses, +# and for the same reason: names the version being worked toward, which stays +# unreleased for the whole cycle. +# +# Usage: ./scripts/refresh-badges.sh + +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +BADGE_DIR="${ROOT_DIR}/docs/assets/badges" + +# BSD and GNU sed disagree about \+ in a basic regex, so the match is grep -E's job and sed +# only trims the brackets off what it found. +VERSION="$(grep -m1 -E '^## \[[0-9]+\.[0-9]+\.[0-9]+\].*[0-9]{4}-[0-9]{2}-[0-9]{2}' \ + "${ROOT_DIR}/CHANGELOG.md" | sed -e 's/^## \[//' -e 's/\].*$//')" +if [[ -z "${VERSION}" ]]; then + echo "Error: no dated release section found in CHANGELOG.md" >&2 + exit 1 +fi + +mkdir -p "${BADGE_DIR}" + +fetch() { + local name="$1" url="$2" + curl -fsS --retry 3 -o "${BADGE_DIR}/${name}.svg" "${url}" + echo " ${name}.svg" +} + +echo "Refreshing badges for ${VERSION}:" +fetch nuget "https://img.shields.io/badge/NuGet-v${VERSION}-007ec6?logo=nuget" +fetch license-mit "https://img.shields.io/badge/license-MIT-blue.svg" +fetch dotnet-10 "https://img.shields.io/badge/.NET-10-512BD4.svg?logo=dotnet" + +echo +echo "Done. These files are committed, not fetched at page load — that is the point." diff --git a/tests/Stratara.Documentation.Tests/LandingBadgeTests.cs b/tests/Stratara.Documentation.Tests/LandingBadgeTests.cs new file mode 100644 index 0000000..32e055a --- /dev/null +++ b/tests/Stratara.Documentation.Tests/LandingBadgeTests.cs @@ -0,0 +1,60 @@ +using System.Text.RegularExpressions; + +namespace Stratara.Documentation.Tests; + +/// +/// The documentation site serves its badges from docs/assets/badges/ rather than from a +/// badge service, so that a visitor's browser makes no third-party request before that visitor has +/// agreed to anything. That property is only worth having if it holds for every page, and a served +/// badge only stays true if something notices when it stops being. +/// +/// The release badge is checked against the newest dated section of the changelog for the same +/// reason llms.txt is: VersionPrefix names the version being worked toward, which +/// stays unreleased for the whole cycle, so comparing against it would advertise a version nobody +/// can install. +/// +/// +public partial class LandingBadgeTests +{ + [GeneratedRegex(@"^##\s*\[(\d+\.\d+\.\d+)\][^\n]*?\d{4}-\d{2}-\d{2}", RegexOptions.Multiline)] + private static partial Regex NewestReleasedVersionPattern(); + + [GeneratedRegex(@"v(\d+\.\d+\.\d+)")] + private static partial Regex BadgeVersionPattern(); + + // Quoted, single-quoted, bare and protocol-relative all reach another host, and so does a + // Markdown image. A guard that only knew the shape we happen to write today would pass the + // first time somebody wrote it differently, which is the one moment it exists for. + [GeneratedRegex("""]+src\s*=\s*["']?(?:https?:)?//|!\[[^\]]*\]\(\s*(?:https?:)?//""")] + private static partial Regex ExternallyHostedImagePattern(); + + [Fact] + public void TheReleaseBadge_NamesTheNewestRelease() + { + var root = RepositoryRoot.Locate(); + var release = NewestReleasedVersionPattern() + .Match(File.ReadAllText(Path.Combine(root, "CHANGELOG.md"))); + var badge = BadgeVersionPattern() + .Match(File.ReadAllText(Path.Combine(root, "docs", "assets", "badges", "nuget.svg"))); + + // Without these, a changelog heading or badge format we stopped recognising would leave + // both sides empty and the comparison would agree that nothing is wrong. + Assert.True(release.Success, "No dated release section was found in CHANGELOG.md."); + Assert.True(badge.Success, "No version was found in docs/assets/badges/nuget.svg."); + Assert.Equal(release.Groups[1].Value, badge.Groups[1].Value); + } + + [Fact] + public void NoDocumentationPage_LoadsAnImageFromAnotherHost() + { + var offenders = DocumentationFiles.Enumerate() + .Where(file => ExternallyHostedImagePattern().IsMatch(File.ReadAllText(file))) + .ToList(); + + Assert.True( + offenders.Count == 0, + $"These pages load an image from another host, which reaches that host before the " + + $"visitor has consented to anything. Serve the image from docs/assets/ instead: " + + $"{string.Join(", ", offenders)}"); + } +}