From 3f04e6611551681fe084cacb1a3b06b3d5e99335 Mon Sep 17 00:00:00 2001
From: Ayla Croft
Date: Tue, 6 Oct 2026 18:17:54 -0400
Subject: [PATCH] feat(site): the OpenSSF Best Practices badge in the footer
Kotiko holds the OpenSSF silver badge (entry 15259). Every page of
kotiko.org now shows it in the footer, linked to the entry. The site
promises it loads nothing from other hosts, so the badge is drawn in CSS
from one value in src/config.ts (openssf.level) instead of the live
image the README uses; change that value when the level changes.
The link's accessible name is "OpenSSF Best Practices: silver"; the
first version ran the two words together, which the new test caught.
Co-Authored-By: Claude Opus 5.5
Signed-off-by: Ayla Croft
---
site/src/components/Footer.astro | 38 +++++++++++++++++++++++++++++++-
site/src/config.ts | 8 +++++++
site/tests/badge.test.mjs | 29 ++++++++++++++++++++++++
3 files changed, 74 insertions(+), 1 deletion(-)
create mode 100644 site/tests/badge.test.mjs
diff --git a/site/src/components/Footer.astro b/site/src/components/Footer.astro
index 6c597d2..c3f9251 100644
--- a/site/src/components/Footer.astro
+++ b/site/src/components/Footer.astro
@@ -5,7 +5,7 @@
// Starlight's footer (edit link, previous and next) plus the project's own line: the
// license, the trademark note and the privacy promise this site keeps.
import Default from "@astrojs/starlight/components/Footer.astro";
-import { site } from "../config";
+import { openssf, site } from "../config";
---
@@ -18,6 +18,11 @@ import { site } from "../config";
This site sets no cookies, has no analytics and loads nothing from other sites. It is hosted on GitHub Pages
behind Cloudflare, which see visitors' IP addresses, as any web host does.
+
+
+ OpenSSF Best Practices {openssf.level}
+
+
Privacy policy · Source code · Report a problem
@@ -34,4 +39,35 @@ import { site } from "../config";
.kotiko-footer p + p {
margin-top: 0.5rem;
}
+ /* The badge, drawn here rather than loaded from bestpractices.dev (see config.ts). */
+ .kotiko-badge {
+ display: inline-flex;
+ border-radius: 4px;
+ overflow: hidden;
+ font-size: var(--sl-text-xs);
+ line-height: 1.8;
+ text-decoration: none;
+ border: 1px solid var(--sl-color-hairline-light);
+ }
+ .kotiko-badge span {
+ padding: 0 0.5rem;
+ }
+ .kotiko-badge span:first-child {
+ background: var(--sl-color-gray-6);
+ color: var(--sl-color-white);
+ }
+ .kotiko-badge .level {
+ text-transform: capitalize;
+ font-weight: 600;
+ color: #1b1b1b;
+ }
+ .kotiko-badge .level-passing {
+ background: #c9a227;
+ }
+ .kotiko-badge .level-silver {
+ background: #c0c6cc;
+ }
+ .kotiko-badge .level-gold {
+ background: #e2b437;
+ }
diff --git a/site/src/config.ts b/site/src/config.ts
index 454b599..b1bdbbd 100644
--- a/site/src/config.ts
+++ b/site/src/config.ts
@@ -17,3 +17,11 @@ export const site = {
releases: "https://github.com/ScriptKittyOS/kotiko/releases",
issues: "https://github.com/ScriptKittyOS/kotiko/issues",
};
+
+// The OpenSSF Best Practices badge (https://www.bestpractices.dev/projects/15259). The site
+// loads nothing from other hosts, so it shows the level as text instead of the live image
+// the README uses: when the entry reaches a new level, change `level` here.
+export const openssf = {
+ level: "silver" as "passing" | "silver" | "gold",
+ url: "https://www.bestpractices.dev/projects/15259",
+};
diff --git a/site/tests/badge.test.mjs b/site/tests/badge.test.mjs
new file mode 100644
index 0000000..6eb3edb
--- /dev/null
+++ b/site/tests/badge.test.mjs
@@ -0,0 +1,29 @@
+// SPDX-FileCopyrightText: 2026 ScriptKittyOS and the Kotiko contributors
+// SPDX-License-Identifier: Apache-2.0
+
+// The OpenSSF Best Practices badge in the footer: every page links to the entry and shows
+// the level set in src/config.ts, drawn in CSS rather than loaded from another host.
+import { test } from "node:test";
+import assert from "node:assert/strict";
+import fs from "node:fs";
+import path from "node:path";
+import { ensureBuilt, read, text, SITE } from "./helpers.mjs";
+
+const level = fs.readFileSync(path.join(SITE, "src/config.ts"), "utf8").match(/level:\s*"(passing|silver|gold)"/)?.[1];
+
+test("config.ts names a badge level", () => {
+ assert.ok(level, "src/config.ts sets openssf.level");
+});
+
+for (const page of ["index.html", "install/index.html", "privacy/index.html"]) {
+ test(`${page} shows the ${level} badge linked to the entry`, () => {
+ ensureBuilt();
+ const html = read(page);
+ const badge = html.match(/]*href="([^"]+)"[^>]*>([\s\S]*?)<\/a>/);
+ assert.ok(badge, "the footer has the badge");
+ assert.equal(badge[1], "https://www.bestpractices.dev/projects/15259");
+ assert.equal(text(badge[2]), `OpenSSF Best Practices ${level}`);
+ assert.match(badge[0], new RegExp(`aria-label="OpenSSF Best Practices: ${level}"`));
+ assert.doesNotMatch(html, /bestpractices\.dev\/projects\/15259\/badge/, "no image loaded from bestpractices.dev");
+ });
+}