From 8c346d8601605f9ab7753c3996f5473d6db83ee5 Mon Sep 17 00:00:00 2001 From: big-claude <307551610+vpetersson-bot@users.noreply.github.com> Date: Sun, 25 Oct 2026 11:08:16 +0000 Subject: [PATCH 1/7] fix: link to final URLs instead of redirecting ones Semrush Site Audit reported a redirect chain on every page. The footer and social links pointed at https://sbomify.com/slack, which 302s to join.slack.com, which redirects again to the workspace invite. Link the invite URL directly (single source in data/main.yml, plus the two pricing page links). Also fix the remaining internal links that only resolved via a redirect: a case study link without its trailing slash, the LinkedIn URL without its trailing slash, and two old app.sbomify.com public pages that now redirect (to a login wall, or to the trust center). Co-Authored-By: Claude Opus 5.5 Co-authored-by: multica-agent --- .../posts/2024-10-31-github-action-update-and-attestation.md | 2 +- content/posts/2025-03-31-chris-swan-joins-sbomify.md | 2 +- content/posts/2025-07-04-big-update-to-sbomify.md | 2 +- content/pricing.html | 4 ++-- data/main.yml | 4 ++-- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/content/posts/2024-10-31-github-action-update-and-attestation.md b/content/posts/2024-10-31-github-action-update-and-attestation.md index 8d4ed45b..c3dd5d44 100644 --- a/content/posts/2024-10-31-github-action-update-and-attestation.md +++ b/content/posts/2024-10-31-github-action-update-and-attestation.md @@ -55,7 +55,7 @@ For more details on using the GitHub Action and other CI/CD integrations, see ou What’s great about this is that it removes the need to trust sbomify directly. You can verify the SBOM independently as follows: -- Download the latest SBOM from the GitHub Action [sbomify public page](https://app.sbomify.com/component/Gu9wem8mkX) +- Download the latest SBOM from the GitHub Action [sbomify public page](https://trust.sbomify.com/component/Gu9wem8mkX/) - Install the GitHub CLI if you don't already have it With this done, you can verify the file against GitHub: diff --git a/content/posts/2025-03-31-chris-swan-joins-sbomify.md b/content/posts/2025-03-31-chris-swan-joins-sbomify.md index fd1b01e5..416694ce 100644 --- a/content/posts/2025-03-31-chris-swan-joins-sbomify.md +++ b/content/posts/2025-03-31-chris-swan-joins-sbomify.md @@ -17,7 +17,7 @@ sbomify is excited to announce that Chris Swan has joined its advisory board. Ch Chris currently serves as an Engineer at [Atsign](https://atsign.com/), working on the atPlatform, a privacy-focused open-source networking platform. He previously held leadership roles at DXC Technology, UBS, Cohesive Networks, and Credit Suisse, focusing on cloud, security, and infrastructure innovation. Chris is also known for his insightful writing as an [editor at InfoQ](https://www.infoq.com/profile/Chris-Swan/#allActivity) and as co-host of the [_Tech Debt Burndown_](https://techdebtburndown.com/) podcast. -Chris's involvement with sbomify began during his work at Atsign, where he successfully integrated sbomify into their CI/CD pipeline to automate SBOM generation. This collaboration, detailed in the [Atsign case study](https://sbomify.com/case-studies/atsign), demonstrates Chris's practical approach to integrating security seamlessly into developer workflows. +Chris's involvement with sbomify began during his work at Atsign, where he successfully integrated sbomify into their CI/CD pipeline to automate SBOM generation. This collaboration, detailed in the [Atsign case study](/case-studies/atsign/), demonstrates Chris's practical approach to integrating security seamlessly into developer workflows. Chris's perspective aligns perfectly with sbomify’s mission of developer-friendly, frictionless SBOM management. diff --git a/content/posts/2025-07-04-big-update-to-sbomify.md b/content/posts/2025-07-04-big-update-to-sbomify.md index 49e265ac..3407d26e 100644 --- a/content/posts/2025-07-04-big-update-to-sbomify.md +++ b/content/posts/2025-07-04-big-update-to-sbomify.md @@ -30,6 +30,6 @@ With this new feature, that is now possible. All you need to do is create a new In short, sbomify is evolving into a compliance hub. SBOMs remain important, but they are now just one part of a broader picture. Speaking of SBOMs, we have also exposed our product- and project-level SBOM aggregation download feature to public pages. This means that if you have a project with ten different components – for example, ten different microservices – you can now download an aggregated SBOM that links all of them together using externalReferences in CycloneDX*. -Want a live demo? Check out our [public product page](https://app.sbomify.com/public/product/eP_4dk8ixV/) +Want a live demo? Check out our [public product page](https://trust.sbomify.com/product/sbomify/) _* Note: This is currently available only for CycloneDX, and it assumes your SBOM components are marked as public._ diff --git a/content/pricing.html b/content/pricing.html index 35a24613..12063ba3 100644 --- a/content/pricing.html +++ b/content/pricing.html @@ -109,7 +109,7 @@

Features

@@ -147,7 +147,7 @@

Features

diff --git a/data/main.yml b/data/main.yml index 907a5b49..ebdd5bdf 100644 --- a/data/main.yml +++ b/data/main.yml @@ -14,6 +14,6 @@ nav: social: x: https://x.com/sbomify - linkedIn: https://www.linkedin.com/company/sbomify + linkedIn: https://www.linkedin.com/company/sbomify/ github: https://github.com/sbomify - slack: https://sbomify.com/slack + slack: https://join.slack.com/t/sbomify/shared_invite/zt-3na54pa1f-MXrFWhotmZr0YxXc8sABTw From f98868b32c11dfe7c1e4102eb00f49f63fef90d9 Mon Sep 17 00:00:00 2001 From: big-claude <307551610+vpetersson-bot@users.noreply.github.com> Date: Sun, 25 Oct 2026 11:08:50 +0000 Subject: [PATCH 2/7] fix: keep Cloudflare email obfuscation out of page content Semrush Site Audit reported broken internal links and a 4xx page at /cdn-cgi/l/email-protection. Cloudflare's Email Address Obfuscation rewrites every mailto: link, and every email-shaped string outside code blocks (PURLs such as pkg:pypi/name@1.2.3 in inline code), into a link to that path, which 404s for crawlers and for visitors without JavaScript. The markers only wrapped fenced code blocks. Wrap the whole rendered content instead; the addresses on the site are public role addresses that are also listed in security.txt. Co-Authored-By: Claude Opus 5.5 Co-authored-by: multica-agent --- .../_default/_markup/render-codeblock.html | 6 +--- layouts/partials/content.html | 30 ++++++++++--------- 2 files changed, 17 insertions(+), 19 deletions(-) diff --git a/layouts/_default/_markup/render-codeblock.html b/layouts/_default/_markup/render-codeblock.html index 75c5d931..5d9d805f 100644 --- a/layouts/_default/_markup/render-codeblock.html +++ b/layouts/_default/_markup/render-codeblock.html @@ -1,9 +1,5 @@ {{- $result := transform.HighlightCodeBlock . -}} -{{- /* The figure wrapper is also what partials/content.html matches on to - wrap code blocks in Cloudflare markers, so keep the - class name and this markup in sync with the regexp there. - - The header band carries the language and a copy button, as the app's +{{- /* The header band carries the language and a copy button, as the app's code block does. The button ships hidden and assets/js/main.js reveals it, so there is no dead control without JavaScript. */ -}}
{{ .Type }}
{{ $result.Wrapped }}
diff --git a/layouts/partials/content.html b/layouts/partials/content.html index 8731afde..8cdca40e 100644 --- a/layouts/partials/content.html +++ b/layouts/partials/content.html @@ -4,20 +4,24 @@ written as raw tags in content (Markdown links are handled earlier by layouts/_markup/render-link.html). See CLAUDE.md, "Outbound Links". - 2. Cloudflare's Email Address Obfuscation is switched off around code - blocks only. Cloudflare rewrites anything shaped like an email address, - which mangles package identifiers in samples: express@4.18.2 was served - as "[email protected]" in the JavaScript guide, and the rewritten link - points at /cdn-cgi/l/email-protection, which 404s. The - markers scope the feature out of code blocks while genuine mailto: - addresses in prose stay obfuscated. + 2. Cloudflare's Email Address Obfuscation is switched off for all page + content. Cloudflare rewrites anything shaped like an email address + into a link to /cdn-cgi/l/email-protection, which 404s for anything + that does not run its decoder script. That mangled package + identifiers (express@4.18.2 was served as "[email protected]", both in + code blocks and in inline code such as PURLs), and turned every + mailto: link into a link to that 404 -- which site audits report as + broken internal links and a 4xx page. The addresses on the site are + public role addresses (hello@, privacy@, security@) that are also + published in security.txt, so there is nothing to hide from scrapers. - The markers live here rather than in the code block render hook because - Hugo strips HTML comments emitted by render hooks. They also need + The markers live here rather than in a render hook because Hugo + strips HTML comments emitted by render hooks. They also need minify.tdewolff.html keepComments in hugo.toml, or the minifier removes them, so templates use Go template comments and content HTML files use the comment shortcode -- an HTML comment anywhere else now reaches the - built page. + built page. Cloudflare's markers do not nest, so do not add inner + / pairs inside content. 3. fingerprint-assets.html points src="/assets/images/..." at the fingerprinted copy of the file, so an updated image is not served @@ -26,7 +30,5 @@ Every layout renders page content through this partial, so anything that has to apply site-wide to rendered content belongs here rather than in each layout. */ -}} -{{- partial "nofollow-links.html" .Content - | partial "fingerprint-assets.html" - | replaceRE `(?s)(
.*?
)` `${1}` - | safeHTML -}} +{{- $html := partial "nofollow-links.html" .Content | partial "fingerprint-assets.html" -}} +{{- printf "%s" $html | safeHTML -}} From 170707ef28b6559a2bf4f50aeaf1c1d0e2d8cf74 Mon Sep 17 00:00:00 2001 From: big-claude <307551610+vpetersson-bot@users.noreply.github.com> Date: Sun, 25 Oct 2026 11:09:22 +0000 Subject: [PATCH 3/7] fix: shorten over-long tags with seo_title Semrush Site Audit flagged six pages whose <title> runs past ~70 characters. Set the existing seo_title front matter on them so the <title> fits the search result display limit, while the visible <h1> keeps the full editorial headline. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> --- content/compliance/nist-800-171.md | 1 + content/compliance/nist-800-53.md | 1 + ...ra-explained-cyber-resilience-act-for-device-manufacturers.md | 1 + content/posts/2026-08-20-bsi-tr-03183-four-parts-cra-update.md | 1 + .../posts/2026-09-10-cra-single-reporting-platform-enisa-srp.md | 1 + content/sbomify-action/_index.md | 1 + 6 files changed, 6 insertions(+) diff --git a/content/compliance/nist-800-171.md b/content/compliance/nist-800-171.md index 6df9d070..c4254687 100644 --- a/content/compliance/nist-800-171.md +++ b/content/compliance/nist-800-171.md @@ -2,6 +2,7 @@ url: /compliance/nist-800-171/ title: "NIST SP 800-171 SBOM Requirements: Protecting CUI in Software Supply Chains" +seo_title: "NIST SP 800-171 SBOM Requirements" description: "How NIST SP 800-171 Rev 3 supply chain and component inventory controls relate to SBOM requirements for organizations handling Controlled Unclassified Information. Includes the CMMC phased rollout timeline through 2028." tldr: "NIST SP 800-171 protects Controlled Unclassified Information (CUI) in nonfederal systems and is required for DoD contractors via CMMC. Its supply chain risk management requirements align with SBOM practices for documenting and verifying software components." faq: diff --git a/content/compliance/nist-800-53.md b/content/compliance/nist-800-53.md index ec309b7e..5be3ecce 100644 --- a/content/compliance/nist-800-53.md +++ b/content/compliance/nist-800-53.md @@ -2,6 +2,7 @@ url: /compliance/nist-800-53/ title: "NIST 800-53 and SBOM Requirements: Security Controls for Software Supply Chains" +seo_title: "NIST 800-53 SBOM Requirements" description: "How NIST SP 800-53 Rev 5 security controls relate to SBOM requirements, covering the SA and SR control families for software supply chain risk management." tldr: "NIST SP 800-53 defines security and privacy controls for federal information systems. Its supply chain risk management controls (SR family) and developer security requirements (SA family) increasingly expect organizations to produce and consume SBOMs as evidence of software transparency." faq: diff --git a/content/posts/2026-01-06-cra-explained-cyber-resilience-act-for-device-manufacturers.md b/content/posts/2026-01-06-cra-explained-cyber-resilience-act-for-device-manufacturers.md index 9ea24c42..0753bcc2 100644 --- a/content/posts/2026-01-06-cra-explained-cyber-resilience-act-for-device-manufacturers.md +++ b/content/posts/2026-01-06-cra-explained-cyber-resilience-act-for-device-manufacturers.md @@ -1,6 +1,7 @@ --- title: "CRA Explained: What the Cyber Resilience Act Means for Device Manufacturers" +seo_title: "CRA Explained for Device Manufacturers" description: "Podcast episode with EU CRA expert Sarah Fluchs explaining SBOM requirements, the 5-year support mandate, and vulnerability management for device manufacturers." author: display_name: Viktor Petersson diff --git a/content/posts/2026-08-20-bsi-tr-03183-four-parts-cra-update.md b/content/posts/2026-08-20-bsi-tr-03183-four-parts-cra-update.md index 476e9a2b..0b6374e6 100644 --- a/content/posts/2026-08-20-bsi-tr-03183-four-parts-cra-update.md +++ b/content/posts/2026-08-20-bsi-tr-03183-four-parts-cra-update.md @@ -1,6 +1,7 @@ --- title: "BSI TR-03183 Is Now Four Documents, and the CRA Clock Runs Out in Three Weeks" +seo_title: "BSI TR-03183 Is Now Four Documents: CRA Update" description: "BSI published TR-03183-1 in version 1.0.0 on 31 July 2026, completing a four-part CRA guideline family. Here is what changed across TR-03183, the Commission's July 2026 guidance, and the ENISA Single Reporting Platform before 11 September." keywords: "BSI TR-03183, TR-03183-1, TR-03183-3 security.txt, RFC 9116 CRA, CRA reporting 11 September 2026, Single Reporting Platform, EN 40000, CRA harmonised standards, BSI SBOM requirements, CRA compliance 2026" categories: diff --git a/content/posts/2026-09-10-cra-single-reporting-platform-enisa-srp.md b/content/posts/2026-09-10-cra-single-reporting-platform-enisa-srp.md index ff050199..aada4c3d 100644 --- a/content/posts/2026-09-10-cra-single-reporting-platform-enisa-srp.md +++ b/content/posts/2026-09-10-cra-single-reporting-platform-enisa-srp.md @@ -1,6 +1,7 @@ --- title: "The CRA Single Reporting Platform Opens Tomorrow: What ENISA Actually Requires" +seo_title: "CRA Single Reporting Platform: ENISA Requirements" description: "ENISA's CRA Single Reporting Platform goes live on 11 September 2026 with Article 14 reporting obligations. Registration, Assigned Representative roles, the 24/72-hour clocks, CSIRT selection, and the fields you will be typing." keywords: "CRA Single Reporting Platform, ENISA SRP, CRA SRP registration, CRA reporting obligations, CRA Article 14, actively exploited vulnerability reporting, CRA severe incident, CSIRT designated as coordinator, EU Login SRP, CRA 11 September 2026" categories: diff --git a/content/sbomify-action/_index.md b/content/sbomify-action/_index.md index d0c689ca..d6dd72c3 100644 --- a/content/sbomify-action/_index.md +++ b/content/sbomify-action/_index.md @@ -4,6 +4,7 @@ url: /sbomify-action/ aliases: - /guides/sbomify-action/ title: "sbomify Action: Generate Compliance-Grade SBOMs in Any CI Pipeline" +seo_title: "sbomify Action: SBOMs in Any CI Pipeline" description: "Complete documentation for the sbomify action - a CLI shipped as a container that generates, augments and enriches SBOMs in GitHub Actions, GitLab CI, Bitbucket, Jenkins, CircleCI and any other pipeline." keywords: ["sbomify action", "SBOM CI/CD", "SBOM automation", "CycloneDX", "SPDX", "GitHub Action SBOM", "GitLab SBOM"] tldr: "sbomify-action is a CLI shipped as a container image that turns a lockfile into a compliance-grade SBOM inside your pipeline. It picks the right generator for your ecosystem, adds your business metadata, and enriches every component from package registries. It runs anywhere containers run - GitHub Actions is one runtime among several." From b6bbb105133f21cfa09fcd0041d734016628595d Mon Sep 17 00:00:00 2001 From: big-claude <307551610+vpetersson-bot@users.noreply.github.com> Date: Sun, 25 Oct 2026 11:11:37 +0000 Subject: [PATCH 4/7] fix: render tag and category pages with post cards again Current Hugo resolves term pages (/tags/sbom/) to _default/taxonomy.html ahead of _default/term.html, so term.html was never used: term pages rendered as a bare grid of post titles, with no descriptions and no pagination, even though paginator.html already paginated them for the rel=prev/next links. Semrush Site Audit flagged most of them for low word count and low text to HTML ratio. Render both kinds from taxonomy.html, branching on .Kind, and drop the dead term.html. Term pages also link back to their taxonomy index and the blog. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> --- layouts/_default/taxonomy.html | 25 +++++++++++++++++++++++++ layouts/_default/term.html | 19 ------------------- layouts/partials/paginator.html | 2 +- 3 files changed, 26 insertions(+), 20 deletions(-) delete mode 100644 layouts/_default/term.html diff --git a/layouts/_default/taxonomy.html b/layouts/_default/taxonomy.html index 134f0838..cee2692e 100644 --- a/layouts/_default/taxonomy.html +++ b/layouts/_default/taxonomy.html @@ -1,4 +1,28 @@ +{{- /* Renders both the taxonomy indexes (/tags/, /categories/) and the term + pages (/tags/sbom/). Current Hugo resolves term pages to + _default/taxonomy.html ahead of _default/term.html, so a separate + term.html was silently ignored and term pages rendered as a bare grid of + titles without the post cards. */ -}} {{ define "main" }} +{{ if eq .Kind "term" }} +<div> + <div class="hero"> + <div class="relative px-5 py-16 lg:py-24 lg:px-24 max-w-7xl mx-auto"> + <h1 class="text-ink text-[40px] lg:text-[68px] leading-[48px] lg:leading-[78px] mb-6"> + <span class="capitalize">{{ .Title }}</span> + </h1> + <p class="text-ink-muted text-lg lg:text-xl max-w-4xl leading-relaxed"> + {{ .Data.Singular | title }} · <a href="{{ .Parent.RelPermalink }}" class="link">All {{ .Data.Plural }}</a> · <a href="/blog/" class="link">Blog</a> + </p> + </div> + </div> + <section class="bg-surface"> + <div class="max-w-7xl mx-auto px-6 lg:px-8 py-12 lg:py-20"> + {{ partial "posts.html" (dict "paginate" true "context" . "pages" .Pages) }} + </div> + </section> +</div> +{{ else }} <div> <div class="hero"> <div class="relative px-5 py-16 lg:py-24 lg:px-24 max-w-7xl mx-auto"> @@ -27,3 +51,4 @@ <h2 class="text-2xl font-semibold leading-tight mb-4 group-hover:text-accent-str </section> </div> {{ end }} +{{ end }} diff --git a/layouts/_default/term.html b/layouts/_default/term.html deleted file mode 100644 index 08749054..00000000 --- a/layouts/_default/term.html +++ /dev/null @@ -1,19 +0,0 @@ -{{ define "main" }} -<div> - <div class="hero"> - <div class="relative px-5 py-16 lg:py-24 lg:px-24 max-w-7xl mx-auto"> - <h1 class="text-ink text-[40px] lg:text-[68px] leading-[48px] lg:leading-[78px] mb-6"> - <span class="capitalize">{{ .Title }}</span> - </h1> - <p class="text-ink-muted text-lg lg:text-xl max-w-4xl leading-relaxed"> - {{ .Data.Singular | title }} - </p> - </div> - </div> - <section class="bg-surface"> - <div class="max-w-7xl mx-auto px-6 lg:px-8 py-12 lg:py-20"> - {{ partial "posts.html" (dict "paginate" true "context" . "pages" .Pages) }} - </div> - </section> -</div> -{{ end }} diff --git a/layouts/partials/paginator.html b/layouts/partials/paginator.html index 9b5bcc46..03f06f9c 100644 --- a/layouts/partials/paginator.html +++ b/layouts/partials/paginator.html @@ -1,6 +1,6 @@ {{- /* Returns the page's paginator for paginated list pages, or "" otherwise. Mirrors the .Paginate calls in posts.html (via posts/list.html and - authors/list.html) and term.html: Hugo errors if .Paginate is invoked + authors/list.html) and _default/taxonomy.html (term pages): Hugo errors if .Paginate is invoked with different arguments for the same page, so keep these in sync. */ -}} {{- $paginator := "" -}} {{- if eq .Kind "term" -}} From eea13b7e12652f121c1e69fe392b7ab95de61965 Mon Sep 17 00:00:00 2001 From: big-claude <307551610+vpetersson-bot@users.noreply.github.com> Date: Sun, 25 Oct 2026 11:11:37 +0000 Subject: [PATCH 5/7] feat: add internal links to FAQ, compliance and blog index pages Semrush Site Audit listed pages that only one internal link points to: most FAQ answers (linked only from /faq/), the UK Software Security Code of Practice page (linked only from /compliance/), and tags used by a single post (/tags/ and /categories/ were not linked from anywhere). - FAQ answers list related questions (keyword matches plus the neighbouring questions in FAQ order). - Compliance framework pages list the other frameworks. - The blog index links the category and topic indexes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> --- layouts/_default/single.html | 14 ++++++++++++++ layouts/faq/single.html | 17 +++++++++++++++++ layouts/posts/list.html | 3 +++ 3 files changed, 34 insertions(+) diff --git a/layouts/_default/single.html b/layouts/_default/single.html index 051df0d7..17cdd5b4 100644 --- a/layouts/_default/single.html +++ b/layouts/_default/single.html @@ -24,6 +24,20 @@ <h1 class="text-ink text-[40px] lg:text-[68px] leading-[48px] lg:leading-[78px] </article> {{ partial "toc.html" (dict "page" . "mode" "aside") }} </div> + {{/* Cross-links between compliance frameworks. Most frameworks were + only linked from the /compliance/ index. */}} + {{ if eq .Section "compliance" }} + {{ with where .CurrentSection.RegularPages "RelPermalink" "ne" .RelPermalink }} + <nav class="max-w-4xl mx-auto px-6 lg:px-8 pb-16 lg:pb-24" aria-labelledby="compliance-more-heading"> + <h2 id="compliance-more-heading" class="text-xl font-bold text-ink mb-4">More compliance frameworks</h2> + <ul class="grid gap-3 sm:grid-cols-2"> + {{ range .ByTitle }} + <li><a href="{{ .RelPermalink }}" class="link">{{ .LinkTitle }}</a></li> + {{ end }} + </ul> + </nav> + {{ end }} + {{ end }} </section> </div> {{ end }} diff --git a/layouts/faq/single.html b/layouts/faq/single.html index d61444f8..b737e826 100644 --- a/layouts/faq/single.html +++ b/layouts/faq/single.html @@ -31,6 +31,23 @@ <h1 class="text-ink text-[40px] lg:text-[68px] leading-[48px] lg:leading-[78px] {{ partial "content.html" . }} </article> + {{/* Related questions: the closest matches by keywords, plus the + neighbouring questions in FAQ order so that every answer is linked + from at least two others, not only from the /faq/ index. */}} + {{ $related := first 3 (where (site.RegularPages.Related .) "Section" "faq") }} + {{ with .PrevInSection }}{{ $related = $related | union (slice .) }}{{ end }} + {{ with .NextInSection }}{{ $related = $related | union (slice .) }}{{ end }} + {{ with $related }} + <nav class="mt-12" aria-labelledby="faq-related-heading"> + <h2 id="faq-related-heading" class="text-xl font-bold text-ink mb-4">Related questions</h2> + <ul class="space-y-3"> + {{ range . }} + <li><a href="{{ .RelPermalink }}" class="link">{{ .Title }}</a></li> + {{ end }} + </ul> + </nav> + {{ end }} + {{/* Bottom navigation */}} <div class="mt-12 flex flex-wrap items-center justify-between gap-4"> <a href="/faq/" class="text-ink hover:text-accent-strong font-semibold transition-colors no-underline"> diff --git a/layouts/posts/list.html b/layouts/posts/list.html index 026bb11b..3e423dc2 100644 --- a/layouts/posts/list.html +++ b/layouts/posts/list.html @@ -7,6 +7,9 @@ <h1 class="text-ink text-[40px] lg:text-[68px] leading-[48px] lg:leading-[78px] The sbomify <span class="font-light text-ink-subtle">Blog</span> </h1> <p class="text-ink-muted text-lg lg:text-xl max-w-4xl leading-relaxed">Content for software, product, and digital security experts</p> + {{/* Links the taxonomy indexes, which nothing else links to. Without + them a tag used by a single post has only one internal link. */}} + <p class="text-ink-muted mt-4">Browse by <a href="/categories/" class="link">category</a> or <a href="/tags/" class="link">topic</a>.</p> </div> </div> From e6e78de7ef4d6b3b6fb5454e865af1a8f5e4c235 Mon Sep 17 00:00:00 2001 From: big-claude <307551610+vpetersson-bot@users.noreply.github.com> Date: Sun, 25 Oct 2026 11:12:24 +0000 Subject: [PATCH 6/7] perf: drop Chroma's unstyled per-line and whitespace spans Semrush Site Audit flagged the code-heavy guides and runtime pages for low semantic HTML usage. The pages already use header/main/article/nav, but Chroma wraps each code line in two spans (.line > .cl) and each run of whitespace in a third (.w), which made up over a third of all elements on those pages. None of them are styled, so unwrap them in the code block render hook. This also trims the HTML size of every page with code. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> --- layouts/_default/_markup/render-codeblock.html | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/layouts/_default/_markup/render-codeblock.html b/layouts/_default/_markup/render-codeblock.html index 5d9d805f..957ddd08 100644 --- a/layouts/_default/_markup/render-codeblock.html +++ b/layouts/_default/_markup/render-codeblock.html @@ -1,5 +1,14 @@ {{- $result := transform.HighlightCodeBlock . -}} +{{- /* Chroma wraps every line in two spans (.line > .cl) and every run of + whitespace in another (.w). None of them carry styling here (pre already + keeps the line breaks; .line's flex display changes nothing on its own), + yet on code-heavy guides they were over a third of all elements on the + page, which site audits report as low semantic HTML usage. Unwrap them. + Highlighted lines (hl_lines) use "line hl" and are left alone. */ -}} +{{- $code := $result.Wrapped | string + | replaceRE `<span class="line"><span class="cl">((?:[^<]|<span class="[^"]*">[^<]*</span>)*)</span></span>` "$1" + | replaceRE `<span class="w">([^<]*)</span>` "$1" -}} {{- /* The header band carries the language and a copy button, as the app's code block does. The button ships hidden and assets/js/main.js reveals it, so there is no dead control without JavaScript. */ -}} -<figure class="code-block"><figcaption class="code-block-header"><span class="code-block-lang">{{ .Type }}</span><button type="button" class="code-block-copy" data-code-copy hidden>Copy</button></figcaption>{{ $result.Wrapped }}</figure> +<figure class="code-block"><figcaption class="code-block-header"><span class="code-block-lang">{{ .Type }}</span><button type="button" class="code-block-copy" data-code-copy hidden>Copy</button></figcaption>{{ $code | safeHTML }}</figure> From 6feccc3dd44c20e9ac2c6cf20d3ce704b266f9bf Mon Sep 17 00:00:00 2001 From: big-claude <307551610+vpetersson-bot@users.noreply.github.com> Date: Sun, 25 Oct 2026 11:13:23 +0000 Subject: [PATCH 7/7] fix: give icon-only links visible-to-crawlers anchor text Semrush Site Audit reported links with no anchor text. The footer social links, author bio links and the About page LinkedIn links held only a decorative icon, with the name in aria-label or title, which crawlers do not treat as anchor text. Put the name in an sr-only span instead; screen readers announce the same name as before. assets/css/styles.css is regenerated with bun run build:css, which also picks up sm:grid-cols-2 used by the compliance framework list. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> --- assets/css/styles.css | 17 +++++++++++++++++ content/about.html | 1 + layouts/_default/about.html | 1 + layouts/partials/author-bio-card.html | 18 ++++++++++++------ layouts/partials/footer.html | 9 ++++++--- 5 files changed, 37 insertions(+), 9 deletions(-) diff --git a/assets/css/styles.css b/assets/css/styles.css index 5b447859..61d1355b 100644 --- a/assets/css/styles.css +++ b/assets/css/styles.css @@ -989,6 +989,17 @@ button { .hero > * { position: relative; } +.sr-only { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border-width: 0; +} .pointer-events-none { pointer-events: none; } @@ -2871,6 +2882,9 @@ button { .sm\:w-auto { width: auto; } + .sm\:grid-cols-2 { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } .sm\:grid-cols-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); } @@ -3061,6 +3075,9 @@ button { .lg\:pb-20 { padding-bottom: 5rem; } + .lg\:pb-24 { + padding-bottom: 6rem; + } .lg\:pt-16 { padding-top: 4rem; } diff --git a/content/about.html b/content/about.html index b77065ed..a6478f3c 100644 --- a/content/about.html +++ b/content/about.html @@ -33,6 +33,7 @@ <h2>Leadership</h2> <h3 class="!mt-0 text-2xl font-bold text-ink !mb-0">Viktor Petersson</h3> <a href="https://www.linkedin.com/in/vpetersson/" target="_blank" rel="noopener noreferrer" class="inline-flex items-center text-[#0077b5] hover:text-[#006097] transition-colors"> {{< icon "linkedin" "w-5 h-5" >}} + <span class="sr-only">Viktor Petersson on LinkedIn</span> </a> </div> <p class="text-ink-muted font-semibold mb-6 uppercase tracking-eyebrow text-sm">Founder</p> diff --git a/layouts/_default/about.html b/layouts/_default/about.html index d36829af..0d7edd33 100644 --- a/layouts/_default/about.html +++ b/layouts/_default/about.html @@ -73,6 +73,7 @@ <h3 class="text-2xl font-bold text-ink !mt-0 !mb-0">{{ $advisor.name }}</h3> {{ with $advisor.linkedin }} <a href="{{ . }}"{{ partial "link-attrs.html" (dict "url" . "rel" "noopener noreferrer" "target" "_blank") }} class="inline-flex items-center text-[#0077b5] hover:text-[#006097] transition-colors" title="{{ $advisor.name }} on LinkedIn"> {{ partial "icon.html" (dict "name" "linkedin" "class" "w-5 h-5") }} + <span class="sr-only">{{ $advisor.name }} on LinkedIn</span> </a> {{ end }} </div> diff --git a/layouts/partials/author-bio-card.html b/layouts/partials/author-bio-card.html index 0632537f..8d71a1f6 100644 --- a/layouts/partials/author-bio-card.html +++ b/layouts/partials/author-bio-card.html @@ -35,33 +35,39 @@ <h1 class="text-ink text-[40px] lg:text-[64px] leading-[44px] lg:leading-[68px] {{ if $hasSocial }} <div class="flex flex-wrap items-center justify-center lg:justify-start gap-3"> {{ with $author.linkedin }} - <a href="{{ . }}"{{ partial "link-attrs.html" (dict "url" . "rel" "noopener noreferrer" "target" "_blank") }} class="inline-flex items-center justify-center w-10 h-10 rounded-full bg-surface border border-line text-ink-muted hover:text-ink hover:bg-surface-strong hover:border-ink transition-all duration-200" title="{{ $author.display_name }} on LinkedIn" aria-label="{{ $author.display_name }} on LinkedIn"> + <a href="{{ . }}"{{ partial "link-attrs.html" (dict "url" . "rel" "noopener noreferrer" "target" "_blank") }} class="inline-flex items-center justify-center w-10 h-10 rounded-full bg-surface border border-line text-ink-muted hover:text-ink hover:bg-surface-strong hover:border-ink transition-all duration-200" title="{{ $author.display_name }} on LinkedIn"> {{ partial "icon.html" (dict "name" "linkedin" "class" "w-4 h-4") }} + <span class="sr-only">{{ $author.display_name }} on LinkedIn</span> </a> {{ end }} {{ with $author.twitter }} - <a href="{{ . }}"{{ partial "link-attrs.html" (dict "url" . "rel" "noopener noreferrer" "target" "_blank") }} class="inline-flex items-center justify-center w-10 h-10 rounded-full bg-surface border border-line text-ink-muted hover:text-ink hover:bg-surface-strong hover:border-ink transition-all duration-200" title="{{ $author.display_name }} on X" aria-label="{{ $author.display_name }} on X"> + <a href="{{ . }}"{{ partial "link-attrs.html" (dict "url" . "rel" "noopener noreferrer" "target" "_blank") }} class="inline-flex items-center justify-center w-10 h-10 rounded-full bg-surface border border-line text-ink-muted hover:text-ink hover:bg-surface-strong hover:border-ink transition-all duration-200" title="{{ $author.display_name }} on X"> {{ partial "icon.html" (dict "name" "x-twitter" "class" "w-4 h-4") }} + <span class="sr-only">{{ $author.display_name }} on X</span> </a> {{ end }} {{ with $author.mastodon }} - <a href="{{ . }}"{{ partial "link-attrs.html" (dict "url" . "rel" "me noopener noreferrer" "target" "_blank") }} class="inline-flex items-center justify-center w-10 h-10 rounded-full bg-surface border border-line text-ink-muted hover:text-ink hover:bg-surface-strong hover:border-ink transition-all duration-200" title="{{ $author.display_name }} on Mastodon" aria-label="{{ $author.display_name }} on Mastodon"> + <a href="{{ . }}"{{ partial "link-attrs.html" (dict "url" . "rel" "me noopener noreferrer" "target" "_blank") }} class="inline-flex items-center justify-center w-10 h-10 rounded-full bg-surface border border-line text-ink-muted hover:text-ink hover:bg-surface-strong hover:border-ink transition-all duration-200" title="{{ $author.display_name }} on Mastodon"> {{ partial "icon.html" (dict "name" "mastodon" "class" "w-4 h-4") }} + <span class="sr-only">{{ $author.display_name }} on Mastodon</span> </a> {{ end }} {{ with $author.github }} - <a href="{{ . }}"{{ partial "link-attrs.html" (dict "url" . "rel" "noopener noreferrer" "target" "_blank") }} class="inline-flex items-center justify-center w-10 h-10 rounded-full bg-surface border border-line text-ink-muted hover:text-ink hover:bg-surface-strong hover:border-ink transition-all duration-200" title="{{ $author.display_name }} on GitHub" aria-label="{{ $author.display_name }} on GitHub"> + <a href="{{ . }}"{{ partial "link-attrs.html" (dict "url" . "rel" "noopener noreferrer" "target" "_blank") }} class="inline-flex items-center justify-center w-10 h-10 rounded-full bg-surface border border-line text-ink-muted hover:text-ink hover:bg-surface-strong hover:border-ink transition-all duration-200" title="{{ $author.display_name }} on GitHub"> {{ partial "icon.html" (dict "name" "github" "class" "w-4 h-4") }} + <span class="sr-only">{{ $author.display_name }} on GitHub</span> </a> {{ end }} {{ with $author.website }} - <a href="{{ . }}"{{ partial "link-attrs.html" (dict "url" . "rel" "noopener noreferrer" "target" "_blank") }} class="inline-flex items-center justify-center w-10 h-10 rounded-full bg-surface border border-line text-ink-muted hover:text-ink hover:bg-surface-strong hover:border-ink transition-all duration-200" title="{{ $author.display_name }}'s website" aria-label="{{ $author.display_name }}'s website"> + <a href="{{ . }}"{{ partial "link-attrs.html" (dict "url" . "rel" "noopener noreferrer" "target" "_blank") }} class="inline-flex items-center justify-center w-10 h-10 rounded-full bg-surface border border-line text-ink-muted hover:text-ink hover:bg-surface-strong hover:border-ink transition-all duration-200" title="{{ $author.display_name }}'s website"> {{ partial "icon.html" (dict "name" "globe" "class" "w-4 h-4") }} + <span class="sr-only">{{ $author.display_name }}'s website</span> </a> {{ end }} {{ with $author.podcast }} - <a href="{{ . }}"{{ partial "link-attrs.html" (dict "url" . "rel" "noopener noreferrer" "target" "_blank") }} class="inline-flex items-center justify-center w-10 h-10 rounded-full bg-surface border border-line text-ink-muted hover:text-ink hover:bg-surface-strong hover:border-ink transition-all duration-200" title="{{ $author.display_name }}'s podcast" aria-label="{{ $author.display_name }}'s podcast"> + <a href="{{ . }}"{{ partial "link-attrs.html" (dict "url" . "rel" "noopener noreferrer" "target" "_blank") }} class="inline-flex items-center justify-center w-10 h-10 rounded-full bg-surface border border-line text-ink-muted hover:text-ink hover:bg-surface-strong hover:border-ink transition-all duration-200" title="{{ $author.display_name }}'s podcast"> {{ partial "icon.html" (dict "name" "podcast" "class" "w-4 h-4") }} + <span class="sr-only">{{ $author.display_name }}'s podcast</span> </a> {{ end }} </div> diff --git a/layouts/partials/footer.html b/layouts/partials/footer.html index ca2c488d..6580c4dd 100644 --- a/layouts/partials/footer.html +++ b/layouts/partials/footer.html @@ -74,7 +74,7 @@ <h2 class="eyebrow !text-on-inverse-subtle mb-4">Company</h2> <a href="https://studio.viktopia.io"{{ partial "link-attrs.html" "https://studio.viktopia.io" }} class="hover:text-white transition-colors">A Viktopia Studio project</a> </div> <div class="flex gap-1"> - <a href="{{ site.Data.main.social.github }}"{{ partial "link-attrs.html" (dict "url" site.Data.main.social.github "rel" "noopener noreferrer" "target" "_blank") }} class="inline-flex items-center justify-center w-10 h-10 rounded-full opacity-80 hover:opacity-100 hover:bg-white/10 transition-[opacity,background-color]" aria-label="sbomify on GitHub"> + <a href="{{ site.Data.main.social.github }}"{{ partial "link-attrs.html" (dict "url" site.Data.main.social.github "rel" "noopener noreferrer" "target" "_blank") }} class="inline-flex items-center justify-center w-10 h-10 rounded-full opacity-80 hover:opacity-100 hover:bg-white/10 transition-[opacity,background-color]"> {{ $ghIcon := resources.Get "images/site/github-white.svg" }} {{ if $ghIcon }} {{ $ghIcon = $ghIcon | fingerprint }} @@ -82,8 +82,9 @@ <h2 class="eyebrow !text-on-inverse-subtle mb-4">Company</h2> {{ else }} <img src="/assets/images/site/github-white.svg" alt="" aria-hidden="true" class="h-5 w-5" width="20" height="20" loading="lazy" /> {{ end }} + <span class="sr-only">sbomify on GitHub</span> </a> - <a href="{{ site.Data.main.social.linkedIn }}"{{ partial "link-attrs.html" (dict "url" site.Data.main.social.linkedIn "rel" "noopener noreferrer" "target" "_blank") }} class="inline-flex items-center justify-center w-10 h-10 rounded-full opacity-80 hover:opacity-100 hover:bg-white/10 transition-[opacity,background-color]" aria-label="sbomify on LinkedIn"> + <a href="{{ site.Data.main.social.linkedIn }}"{{ partial "link-attrs.html" (dict "url" site.Data.main.social.linkedIn "rel" "noopener noreferrer" "target" "_blank") }} class="inline-flex items-center justify-center w-10 h-10 rounded-full opacity-80 hover:opacity-100 hover:bg-white/10 transition-[opacity,background-color]"> {{ $liIcon := resources.Get "images/site/linkedin-white.svg" }} {{ if $liIcon }} {{ $liIcon = $liIcon | fingerprint }} @@ -91,8 +92,9 @@ <h2 class="eyebrow !text-on-inverse-subtle mb-4">Company</h2> {{ else }} <img src="/assets/images/site/linkedin-white.svg" alt="" aria-hidden="true" class="h-5 w-5" width="20" height="20" loading="lazy" /> {{ end }} + <span class="sr-only">sbomify on LinkedIn</span> </a> - <a href="{{ site.Data.main.social.slack }}"{{ partial "link-attrs.html" (dict "url" site.Data.main.social.slack "rel" "noopener noreferrer" "target" "_blank") }} class="inline-flex items-center justify-center w-10 h-10 rounded-full opacity-80 hover:opacity-100 hover:bg-white/10 transition-[opacity,background-color]" aria-label="Join the sbomify Slack community"> + <a href="{{ site.Data.main.social.slack }}"{{ partial "link-attrs.html" (dict "url" site.Data.main.social.slack "rel" "noopener noreferrer" "target" "_blank") }} class="inline-flex items-center justify-center w-10 h-10 rounded-full opacity-80 hover:opacity-100 hover:bg-white/10 transition-[opacity,background-color]"> {{ $slIcon := resources.Get "images/site/slack-white.svg" }} {{ if $slIcon }} {{ $slIcon = $slIcon | fingerprint }} @@ -100,6 +102,7 @@ <h2 class="eyebrow !text-on-inverse-subtle mb-4">Company</h2> {{ else }} <img src="/assets/images/site/slack-white.svg" alt="" aria-hidden="true" class="h-5 w-5" width="20" height="20" loading="lazy" /> {{ end }} + <span class="sr-only">Join the sbomify Slack community</span> </a> </div> </div>