Skip to content

Commit e0c79cd

Browse files
committed
Add SEO, AEO, and GEO site signals
1 parent a1e2df8 commit e0c79cd

File tree

3 files changed

+382
-4
lines changed

3 files changed

+382
-4
lines changed

public/social-card.svg

Lines changed: 49 additions & 0 deletions
Loading

scripts/build-site.mjs

Lines changed: 250 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,12 @@ const readmePath = path.join(rootDir, "README.md");
1212
const stylesPath = path.join(rootDir, "website", "site.css");
1313
const publicDir = path.join(rootDir, "public");
1414
const distDir = path.join(rootDir, "dist");
15+
const siteUrl = "https://tps.managed-code.com/";
16+
const repoUrl = "https://github.com/managedcode/TPS";
17+
const readmeUrl = `${repoUrl}/blob/main/README.md`;
18+
const licenseUrl = `${repoUrl}/blob/main/LICENSE`;
19+
const socialImageUrl = `${siteUrl}social-card.svg`;
20+
const siteName = "TPS Format Specification";
1521

1622
const readme = await readFile(readmePath, "utf8");
1723
const styles = await readFile(stylesPath, "utf8");
@@ -43,11 +49,29 @@ const sections = extractSections(tokens);
4349
const stats = buildStats(readme, sections);
4450
const articleHtml = md.renderer.render(trimTitle(tokens), md.options, {});
4551
const heroTitle = buildHeroTitle(title);
52+
const quickAnswers = buildQuickAnswers(summary);
53+
const keywords = buildKeywords();
54+
const buildDate = new Date();
55+
const dateModifiedIso = buildDate.toISOString();
56+
const structuredData = buildStructuredData({
57+
dateModifiedIso,
58+
keywords,
59+
licenseUrl,
60+
quickAnswers,
61+
readmeUrl,
62+
repoUrl,
63+
sections,
64+
socialImageUrl,
65+
siteName,
66+
siteUrl,
67+
summary,
68+
title
69+
});
4670
const builtAt = new Intl.DateTimeFormat("en", {
4771
dateStyle: "long",
4872
timeStyle: "short",
4973
timeZone: "UTC"
50-
}).format(new Date());
74+
}).format(buildDate);
5175

5276
await rm(distDir, { recursive: true, force: true });
5377
await mkdir(distDir, { recursive: true });
@@ -60,12 +84,34 @@ const page = `<!DOCTYPE html>
6084
<meta name="viewport" content="width=device-width, initial-scale=1" />
6185
<title>${escapeHtml(title)}</title>
6286
<meta name="description" content="${escapeHtml(summary)}" />
63-
<meta name="theme-color" content="#0f172a" />
87+
<meta name="keywords" content="${escapeHtml(keywords.join(", "))}" />
88+
<meta name="author" content="Managed Code" />
89+
<meta name="publisher" content="Managed Code" />
90+
<meta name="robots" content="index, follow, max-image-preview:large, max-snippet:-1, max-video-preview:-1" />
91+
<meta name="googlebot" content="index, follow, max-image-preview:large, max-snippet:-1, max-video-preview:-1" />
92+
<meta name="theme-color" content="#0d1624" />
93+
<meta name="application-name" content="${escapeHtml(siteName)}" />
94+
<meta name="generator" content="Managed Code TPS static site builder" />
95+
<meta name="referrer" content="strict-origin-when-cross-origin" />
96+
<link rel="canonical" href="${siteUrl}" />
97+
<link rel="alternate" type="text/markdown" title="README source" href="${readmeUrl}" />
98+
<link rel="license" href="${licenseUrl}" />
6499
<meta property="og:title" content="${escapeHtml(title)}" />
65100
<meta property="og:description" content="${escapeHtml(summary)}" />
66101
<meta property="og:type" content="website" />
67-
<meta property="og:url" content="https://tps.managed-code.com/" />
102+
<meta property="og:site_name" content="${escapeHtml(siteName)}" />
103+
<meta property="og:url" content="${siteUrl}" />
104+
<meta property="og:image" content="${socialImageUrl}" />
105+
<meta property="og:image:type" content="image/svg+xml" />
106+
<meta property="og:image:alt" content="TPS Format Specification social preview" />
107+
<meta property="article:modified_time" content="${dateModifiedIso}" />
108+
<meta name="twitter:card" content="summary_large_image" />
109+
<meta name="twitter:title" content="${escapeHtml(title)}" />
110+
<meta name="twitter:description" content="${escapeHtml(summary)}" />
111+
<meta name="twitter:image" content="${socialImageUrl}" />
112+
<meta name="twitter:image:alt" content="TPS Format Specification social preview" />
68113
<link rel="icon" href="./favicon.svg" type="image/svg+xml" />
114+
<script type="application/ld+json">${toJsonLd(structuredData)}</script>
69115
<style>${styles}</style>
70116
</head>
71117
<body>
@@ -93,6 +139,16 @@ const page = `<!DOCTYPE html>
93139
</div>
94140
</header>
95141
142+
<section class="answer-strip" aria-labelledby="answer-strip-title">
143+
<div class="answer-strip-header">
144+
<p class="panel-label">Search Signals</p>
145+
<h2 id="answer-strip-title">Quick Answers for Search, AI, and Humans</h2>
146+
</div>
147+
<div class="answer-grid">
148+
${renderQuickAnswers(quickAnswers)}
149+
</div>
150+
</section>
151+
96152
<main class="layout">
97153
<aside class="toc-card">
98154
<div class="toc-header">
@@ -129,6 +185,13 @@ const page = `<!DOCTYPE html>
129185
</html>`;
130186

131187
await writeFile(path.join(distDir, "index.html"), page, "utf8");
188+
await writeFile(path.join(distDir, "sitemap.xml"), buildSitemapXml(siteUrl, dateModifiedIso), "utf8");
189+
await writeFile(path.join(distDir, "robots.txt"), buildRobotsTxt(siteUrl), "utf8");
190+
await writeFile(
191+
path.join(distDir, "llms.txt"),
192+
buildLlmsTxt({ licenseUrl, quickAnswers, readmeUrl, repoUrl, siteUrl, stats, summary, title }),
193+
"utf8"
194+
);
132195

133196
function slugifyHeading(value) {
134197
return value
@@ -232,6 +295,186 @@ function buildHeroTitle(value) {
232295
return `<h1><span class="hero-title-mark">${escapeHtml(lead)}</span><span class="hero-title-main">${escapeHtml(titleTail)}</span></h1><p class="hero-title-sub">${escapeHtml(subtitle)}</p>`;
233296
}
234297

298+
function buildKeywords() {
299+
return [
300+
"TPS",
301+
"TelePrompterScript",
302+
"teleprompter format",
303+
"markdown teleprompter",
304+
"teleprompter script specification",
305+
"RSVP script format",
306+
"actor reading profile",
307+
"speech pacing metadata",
308+
"teleprompter markdown",
309+
"script timing metadata"
310+
];
311+
}
312+
313+
function buildQuickAnswers(summary) {
314+
return [
315+
{
316+
question: "What is TPS?",
317+
answer: `${summary} TPS keeps authoring human-readable while giving teleprompter software structured cues for timing, pacing, emotion, and styling.`
318+
},
319+
{
320+
question: "Who is TPS for?",
321+
answer: "TPS is designed for script authors, teleprompter app developers, and production teams that need readable source files with structured playback guidance."
322+
},
323+
{
324+
question: "What makes TPS different?",
325+
answer: "Unlike plain markdown, SubRip, or WebVTT, TPS is built for teleprompter delivery: it adds hierarchical segments, inline pacing markers, emotion tags, edit points, and profile-aware rendering rules."
326+
}
327+
];
328+
}
329+
330+
function buildStructuredData({
331+
dateModifiedIso,
332+
keywords,
333+
licenseUrl,
334+
quickAnswers,
335+
readmeUrl,
336+
repoUrl,
337+
sections,
338+
socialImageUrl,
339+
siteName,
340+
siteUrl,
341+
summary,
342+
title
343+
}) {
344+
const primarySections = sections
345+
.filter((section) => section.depth === 2)
346+
.map((section) => section.title);
347+
348+
return {
349+
"@context": "https://schema.org",
350+
"@graph": [
351+
{
352+
"@type": "WebSite",
353+
"@id": `${siteUrl}#website`,
354+
url: siteUrl,
355+
name: siteName,
356+
description: summary,
357+
inLanguage: "en",
358+
publisher: {
359+
"@type": "Organization",
360+
name: "Managed Code",
361+
url: "https://managed-code.com/"
362+
}
363+
},
364+
{
365+
"@type": "TechArticle",
366+
"@id": `${siteUrl}#article`,
367+
headline: title,
368+
description: summary,
369+
url: siteUrl,
370+
mainEntityOfPage: siteUrl,
371+
isPartOf: {
372+
"@id": `${siteUrl}#website`
373+
},
374+
author: {
375+
"@type": "Organization",
376+
name: "Managed Code"
377+
},
378+
publisher: {
379+
"@type": "Organization",
380+
name: "Managed Code",
381+
url: "https://managed-code.com/"
382+
},
383+
about: [
384+
"Teleprompter scripts",
385+
"Markdown specification",
386+
"Speech pacing metadata",
387+
"RSVP reading",
388+
"Actor delivery"
389+
],
390+
articleSection: primarySections,
391+
keywords,
392+
license: licenseUrl,
393+
dateModified: dateModifiedIso,
394+
inLanguage: "en",
395+
image: socialImageUrl,
396+
sameAs: [repoUrl, readmeUrl],
397+
speakable: {
398+
"@type": "SpeakableSpecification",
399+
cssSelector: [".hero-summary", ".answer-answer"]
400+
}
401+
},
402+
{
403+
"@type": "FAQPage",
404+
"@id": `${siteUrl}#faq`,
405+
mainEntity: quickAnswers.map((entry, index) => ({
406+
"@type": "Question",
407+
"@id": `${siteUrl}#quick-answer-${index + 1}`,
408+
name: entry.question,
409+
acceptedAnswer: {
410+
"@type": "Answer",
411+
text: entry.answer
412+
}
413+
}))
414+
}
415+
]
416+
};
417+
}
418+
419+
function renderQuickAnswers(entries) {
420+
return entries
421+
.map(
422+
(entry, index) => `<article class="answer-card" id="quick-answer-${index + 1}">
423+
<p class="answer-label">AEO / GEO</p>
424+
<h3>${escapeHtml(entry.question)}</h3>
425+
<p class="answer-answer">${escapeHtml(entry.answer)}</p>
426+
</article>`
427+
)
428+
.join("");
429+
}
430+
431+
function buildSitemapXml(siteUrl, dateModifiedIso) {
432+
return `<?xml version="1.0" encoding="UTF-8"?>
433+
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
434+
<url>
435+
<loc>${siteUrl}</loc>
436+
<lastmod>${dateModifiedIso}</lastmod>
437+
<changefreq>weekly</changefreq>
438+
<priority>1.0</priority>
439+
</url>
440+
</urlset>
441+
`;
442+
}
443+
444+
function buildRobotsTxt(siteUrl) {
445+
return `User-agent: *
446+
Allow: /
447+
448+
Sitemap: ${siteUrl}sitemap.xml
449+
`;
450+
}
451+
452+
function buildLlmsTxt({ licenseUrl, quickAnswers, readmeUrl, repoUrl, siteUrl, stats, summary, title }) {
453+
return `# ${title}
454+
455+
> ${summary}
456+
457+
Canonical: ${siteUrl}
458+
Repository: ${repoUrl}
459+
Source of truth: ${readmeUrl}
460+
License: ${licenseUrl}
461+
462+
## Key Facts
463+
- ${stats.sectionCount} major sections
464+
- ${stats.subsectionCount} subsections
465+
- ${stats.wordCount.toLocaleString("en-US")} words in the current specification
466+
- Audience: script authors, teleprompter app developers, and production teams
467+
468+
## Quick Answers
469+
${quickAnswers.map((entry) => `- ${entry.question} ${entry.answer}`).join("\n")}
470+
471+
## Retrieval Guidance
472+
- Prefer the canonical site for the polished reader experience.
473+
- Use the GitHub README as the editable source of truth.
474+
- Cite TPS as a markdown-based teleprompter specification with pacing, timing, emotion, and styling metadata.
475+
`;
476+
}
477+
235478
function renderSections(sectionList) {
236479
return sectionList
237480
.map((section) => {
@@ -241,6 +484,10 @@ function renderSections(sectionList) {
241484
.join("");
242485
}
243486

487+
function toJsonLd(value) {
488+
return JSON.stringify(value).replaceAll("<", "\\u003c");
489+
}
490+
244491
function escapeHtml(value) {
245492
return value
246493
.replaceAll("&", "&amp;")

0 commit comments

Comments
 (0)