diff --git a/docs/changelog.adoc b/docs/changelog.adoc
index 13fb6b2..b8d952a 100644
--- a/docs/changelog.adoc
+++ b/docs/changelog.adoc
@@ -2,6 +2,14 @@
A chronological record of all semantic anchors added to the catalog. Community contributors are credited with thanks.
+== 2026-06-11
+
+*Share metadata & descriptions (#601):*
+
+* *Per-page Open Graph / Twitter cards* — every pre-rendered page now carries its own `og:url`, `og:title`, `og:description` and `twitter:*` tags (previously all subpages shipped the home-page values, so sharing an anchor link rendered the generic home preview). German pages get `og:locale=de_DE`.
+* *Accurate home description* — the hard-coded "110+ semantic anchors" claim (real count: 161) is replaced by counts computed from the data files at build time; the static shell texts are now count-free so they cannot go stale.
+* *Normalized snippet lengths* — meta descriptions are capped at ~160 characters on a word boundary; very short anchor extracts get the anchor title prefixed.
+
== 2026-06-10
*Real anchor pages (#597):*
diff --git a/scripts/prerender-routes.js b/scripts/prerender-routes.js
index a0f0828..1876053 100644
--- a/scripts/prerender-routes.js
+++ b/scripts/prerender-routes.js
@@ -218,7 +218,18 @@ const SITE = 'https://llm-coding.github.io/Semantic-Anchors'
* enUrl: string, deUrl: (string|null), lang: string}} meta
* @returns {string} Updated HTML.
*/
+/**
+ * Cap a meta description at ~160 characters on a word boundary — Google
+ * truncates around 160, so longer texts would be cut mid-sentence in SERPs.
+ */
+function capDescription(text) {
+ if (text.length <= 160) return text
+ const cut = text.slice(0, 159)
+ return cut.slice(0, Math.max(cut.lastIndexOf(' '), 100)) + '…'
+}
+
function applyHead(html, { title, description, canonicalUrl, enUrl, deUrl, lang }) {
+ description = capDescription(description)
html = html.replace(/
[\s\S]*?<\/title>/, `${escapeHtml(title)}`)
html = html.replace(
@@ -246,8 +257,45 @@ function applyHead(html, { title, description, canonicalUrl, enUrl, deUrl, lang
.join('\n')
html = html.replace(/(]*>)/, `$1\n${alternates}`)
+ // Per-page social metadata (#601): without this every pre-rendered page
+ // keeps the shell's home-pointing og:/twitter: values, so shared subpage
+ // links render the generic home preview.
+ html = html.replace(
+ //,
+ ``
+ )
+ html = html.replace(
+ //,
+ ``
+ )
+ html = html.replace(
+ //,
+ ``
+ )
+ html = html.replace(
+ //,
+ ``
+ )
+ html = html.replace(
+ //,
+ ``
+ )
+ html = html.replace(
+ //,
+ ``
+ )
+
if (lang === 'de') {
html = html.replace('', '')
+ html = html
+ .replace(
+ //,
+ ''
+ )
+ .replace(
+ //,
+ ''
+ )
}
return html
@@ -462,8 +510,12 @@ function prerenderAnchorPages(shell) {
const hasDe = fs.existsSync(path.join(DIST, fragmentDe))
const enUrl = `${SITE}/anchor/${anchor.id}`
const deUrl = hasDe ? `${SITE}/de/anchor/${anchor.id}` : null
+ // Short Core-Concepts extracts ("Dependencies only point inward") get the
+ // anchor title prefixed so the SERP snippet stands on its own (#601).
+ const withTitle = (extracted) =>
+ extracted && extracted.length < 50 ? `${anchor.title} — ${extracted}` : extracted
const description =
- extractDescription(`docs/anchors/${anchor.id}.adoc`) ||
+ withTitle(extractDescription(`docs/anchors/${anchor.id}.adoc`)) ||
`${anchor.title} — a semantic anchor: an established term that activates a rich, well-defined concept in any modern LLM.`
// The anchor body sits in a [%collapsible] block; expand it on the
@@ -494,7 +546,8 @@ function prerenderAnchorPages(shell) {
fragmentDe,
{
title: `${anchor.title} — Semantic Anchors`,
- description: extractDescription(`docs/anchors/${anchor.id}.de.adoc`) || description,
+ description:
+ withTitle(extractDescription(`docs/anchors/${anchor.id}.de.adoc`)) || description,
canonicalUrl: deUrl,
enUrl,
deUrl,
@@ -530,6 +583,11 @@ function writeHomeVariant(shell, lang) {
${buildCatalogMarkup(lang, tr)}
`
+ // Real counts from the data files, so the description can never go stale
+ // again the way the hard-coded "110+" did (#601).
+ const anchorCount = loadWebsiteJson('public/data/anchors.json').length
+ const contractCount = loadWebsiteJson('public/data/contracts.json').length
+
let html = applyHead(shell, {
title:
lang === 'de'
@@ -537,8 +595,8 @@ function writeHomeVariant(shell, lang) {
: 'Semantic Anchors - Shared Vocabulary for LLM Communication',
description:
lang === 'de'
- ? '110+ semantische Anker und semantische Contracts für präzise LLM-Kommunikation. Kuratierte Methoden, Frameworks und komponierbare Projektkonventionen. Über 10 Modelle evaluiert.'
- : '110+ semantic anchors and semantic contracts for precise LLM communication. Curated methodologies, frameworks, and composable project conventions. Evaluated across 10 models.',
+ ? `${anchorCount} semantische Anker und ${contractCount} semantische Contracts für präzise LLM-Kommunikation — kuratierte Methoden, Frameworks und Projektkonventionen.`
+ : `${anchorCount} semantic anchors and ${contractCount} semantic contracts for precise LLM communication — curated methodologies, frameworks, and composable project conventions.`,
canonicalUrl: lang === 'de' ? `${SITE}/de/` : `${SITE}/`,
enUrl: `${SITE}/`,
deUrl: `${SITE}/de/`,
diff --git a/website/index.html b/website/index.html
index d6b5301..113a636 100644
--- a/website/index.html
+++ b/website/index.html
@@ -8,7 +8,7 @@
Semantic Anchors - Shared Vocabulary for LLM Communication
-
+
@@ -28,7 +28,7 @@
-
+
@@ -40,7 +40,7 @@
-
+
@@ -51,7 +51,7 @@
"name": "Semantic Anchors",
"alternateName": "Semantic Anchors for LLMs",
"url": "https://llm-coding.github.io/Semantic-Anchors/",
- "description": "110+ semantic anchors and semantic contracts for precise communication with Large Language Models. Evaluated across 10 models.",
+ "description": "A curated catalog of semantic anchors and semantic contracts — shared vocabulary for precise communication with Large Language Models.",
"inLanguage": ["en", "de"],
"publisher": { "@id": "https://llm-coding.github.io/Semantic-Anchors/#organization" },
"potentialAction": {
diff --git a/website/src/main.js b/website/src/main.js
index 4550257..dc23444 100644
--- a/website/src/main.js
+++ b/website/src/main.js
@@ -27,7 +27,7 @@ import {
} from './components/onboarding-modal.js'
import { renderContractsPage, initContractsPage } from './components/contracts-page.js'
-const APP_VERSION = '0.6.0'
+const APP_VERSION = '0.6.1'
window.copyAnchorLink = async function copyAnchorLink(anchorId) {
const url = `${window.location.origin}${import.meta.env.BASE_URL}anchor/${anchorId}`