Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion docs/assurance.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ A claim is *evidenced* when its layer runs in CI, *planned* when a plan phase sc

## Summary

The register holds 246 claims: 227 evidenced, 0 planned, and 19 not verified. It maps 91 guidelines from 5 guides; 70 list claims and 21 are exempt.
The register holds 246 claims: 227 evidenced, 0 planned, and 19 not verified. It maps 92 guidelines from 5 guides; 70 list claims and 22 are exempt.

| Layer | Evidenced | Planned | Not verified |
| --- | ---: | ---: | ---: |
Expand Down Expand Up @@ -76,6 +76,7 @@ These guidelines have no claim in the register, and no automated check covers th
| `website/AGENTS.md` | Keep every product… | Editorial accuracy rule for website copy; no automated check covers it. |
| `website/AGENTS.md` | Keep canonical metadata,… | Search-metadata presentation rule; it states no safety or integrity property. |
| `website/AGENTS.md` | Keep ordinary reference… | Presentation rule for website guides and editorial images; it states no safety or integrity property. |
| `website/AGENTS.md` | The HTML 404 renders… | Presentation rule for the informational website; it states no safety or integrity property. |
| `website/AGENTS.md` | Preserve semantic headings,… | Accessibility presentation rule; it states no safety or integrity property. |
| `website/AGENTS.md` | Public copy (page… | Public-copy style rule; it states no safety or integrity property. |
| `website/AGENTS.md` | The one-line description… | Public-copy consistency rule that the website build enforces; it states no safety or integrity property. |
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -682,7 +682,7 @@
"@hraness/desktop-foundation": "https://github.com/hraness/desktop-foundation/releases/download/v0.7.0/hraness-desktop-foundation-0.7.0.tgz"
},
"devDependencies": {
"@hraness/design-kit": "github:hraness/design-kit#v0.19.0",
"@hraness/design-kit": "github:hraness/design-kit#v0.21.0",
"@hraness/direct": "https://github.com/hraness/direct/releases/download/v0.7.21/hraness-direct-0.7.21.tgz",
"@hraness/site-footer": "github:hraness/site-footer#v0.19.2",
"@hraness/ui": "github:hraness/ui#v0.5.18",
Expand Down
6 changes: 6 additions & 0 deletions verification/claims.json
Original file line number Diff line number Diff line change
Expand Up @@ -897,6 +897,12 @@
"digest": "565a9105cb81bd4643ec63da534bf4efebbb4506d1a519638a05f56797818b63",
"exempt": "Presentation rule for website guides and editorial images; it states no safety or integrity property."
},
{
"guide": "website/AGENTS.md",
"anchor": "The HTML 404 renders",
"digest": "c34425a70895c13e96541c1283f008b887acf1aeb5c5b848209ca84c603964d9",
"exempt": "Presentation rule for the informational website; it states no safety or integrity property."
},
{
"guide": "website/AGENTS.md",
"anchor": "Public content pages",
Expand Down
1 change: 1 addition & 0 deletions website/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
- Require a production Git checkout with a resolvable `HEAD`; never substitute a deployment environment SHA or exclude `.git` through `.vercelignore`. For v0.16.13 and later, production verifies immutable GitHub metadata, exact five descriptors, bot/source receipt, manifest/archive digests, HEAD/tag, and Latest. It trusts the authenticated canonical release admission; independent promotion CI runs the cryptographic Sigstore verifier. Do not claim Vercel verifies Sigstore locally. Historical releases retain their prior npm manifest check.
- Keep canonical metadata, robots, sitemap, llms.txt, Open Graph, X metadata, and the linked JSON-LD graph aligned to `https://ghostget.com/`.
- Keep ordinary reference and task guides typographic and preserve the real getting-started video and poster. For the small set of live editorial argument or comparison pages, treat `editorial-images.ts` as the single image registry and use the `editorial-image-seo` skill. For new imagery, use a reviewed Slopcamera build from its [source-install guide](https://github.com/hraness/slopcamera/blob/main/docs/how-to/use-current-source.md), retain its exact source commit, invoke `bun "$SLOPCAMERA_SOURCE_ROOT/apps/desktop/dist/cli/main.js"` inside the intended Vercel environment, and never retry an ambiguous paid result. Preserve historical Atet credits and completed provenance bytes; extend the registry truthfully before accepting a new generator. For each new image, review the original and card thumbnail, then keep that provenance-bound 1536-pixel original canonical for social, schema, and sitemap use while serving locally derived 384- and 768-pixel WebP files through `srcset` in visible figures and cards. Synchronize the visible figure and credit, homepage card, Open Graph and X metadata, `TechArticle.image`, and image sitemap. Retain prompt, completed job, and receipt under `editorial-provenance/<slug>/` without exposing those internal paths publicly. Do not register a draft route, including the PayPal/GrapheneOS page, until that route is live. Use semantic editorial metaphors rather than generated factual, capability, or security diagrams.
- The HTML 404 renders the shared design-kit status page through `renderGhostgetStatusPage` in `build.ts`: the homepage primary action, at most three next links, and every public page and listed post as "Did you mean" routes. `source/status-page.ts` bundles `attachStatusPage` as its only enhancement. Keep 404 presentation in design-kit, not in `styles.css`.
- Public content pages keep one URL for HTML and Markdown. Honor `Accept` q-values, set `Vary: Accept`, and return `406` only when no owned representation remains. Unknown paths stay HTTP 404 and point agents at llms.txt, the sitemap, and getting started.
- Keep analytics canonical-host-only, cookieless, personless, memory-only, DNT-aware, query-free, and restricted to page lifecycle plus the two explicit GitHub links.
- Do not add private packages, private assets, cookies, replay, identity, feature flags, broad autocapture, console capture, or application behavior.
Expand Down
4 changes: 1 addition & 3 deletions website/appearance.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,7 @@ test("one final header menu preserves content and precedes first stylesheet for
expect(html).toContain('data-ready="false"');
expect(html).toContain('aria-label="Appearance: System" disabled');
}
const recovery = addDocumentAppearance(head + '<main class="route-state"><h1>Missing</h1></main></body></html>', asset);
expect(recovery).toContain('<main class="route-state" id="main" tabindex="-1">');
expect(recovery.match(/data-hraness-appearance-menu/gu)).toHaveLength(1);
expect(() => addDocumentAppearance(head + '<main><h1>Missing</h1></main></body></html>', asset)).toThrow("ordinary header");
});

test("every ordinary authored template has one accepted composition; preview remains inert", async () => {
Expand Down
4 changes: 0 additions & 4 deletions website/appearance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,6 @@ export function addDocumentAppearance(html: string, asset: string, format: "html
? existing.replace(actions, `$1\n${menu}\n$2`)
: existing.replace("</header>", `${menu}\n</header>`);
result = result.replace(existing, amended);
} else if (headers === null && /<main class="route-state">/u.test(result)) {
result = result.replace("<body>", `<body>\n<a class="skip-link" href="#main">Skip to content</a>
<header class="topbar guide-topbar"><a class="wordmark" href="/" aria-label="Ghostget home">Ghostget</a><nav aria-label="Primary"><a href="/docs/tutorials/getting-started/">Install</a><a href="/docs/">Docs</a></nav>${menu}</header>`);
result = result.replace('<main class="route-state">', '<main class="route-state" id="main">');
} else throw new Error("Appearance requires exactly one ordinary header.");
const targets = result.match(/<main\b[^>]*\bid="main"[^>]*>/gu);
if (targets?.length !== 1 || /\btabindex=/iu.test(targets[0]!)) throw new Error("Expected one owned skip-link target.");
Expand Down
82 changes: 79 additions & 3 deletions website/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import { ghostgetSupportProfile } from "../src/support-profile";

import { AskAiAboutThis } from "./ask-ai-runtime.js";
import { product, type PortfolioProductId } from "@hraness/design-kit/portfolio";
import { renderStatusPageHtml, type StatusPageLink } from "@hraness/design-kit";
import {
EDITORIAL_ARTICLE_IMAGE_SIZES,
EDITORIAL_CARD_IMAGE_SIZES,
Expand All @@ -50,6 +51,7 @@ import {
renderBlogIndexMain,
renderBlogLlmsEntries,
renderBlogPostMain,
type BlogPost,
type BlogSite,
} from "./blog";
import type { SitemapPath } from "@hraness/web-discovery";
Expand Down Expand Up @@ -964,6 +966,67 @@ function renderTemplate(
return addDocumentAppearance(rendered, options.appearanceAsset, format);
}

/**
* The shared design-kit 404 body. The primary action matches the homepage
* hero; the three next links cover what Ghostget is, the install guide, and
* the provider reference. `routes` feeds "Did you mean" and is never listed.
*/
export function renderGhostgetStatusPage(routes: readonly StatusPageLink[]): string {
return renderStatusPageHtml({
agentIndexHref: "/llms.txt",
next: [
{
description: "Install the CLI, read your first page, and watch the recorded demo.",
href: "/docs/tutorials/getting-started/",
label: "Getting started",
},
{
description: "Every site and named action your agent can call, and how each one runs.",
href: "/docs/reference/provider-capabilities/",
label: "Supported sites and actions",
},
{
description: "What the CLI and SDK do, and what they leave to your agent.",
href: "/about/",
label: "About Ghostget",
},
],
primaryAction: { href: "/#start", label: "Install Ghostget" },
rootElement: "div",
routes,
siteName: "Ghostget",
});
}

const STATUS_ROUTE_LABEL_LIMIT = 48;

/** A page title cut to the lead clause the "Did you mean" hint can show. */
export function statusRouteLabel(title: string): string {
const lead = title.split(":")[0]!.split(", and ")[0]!.trim();
if (lead.length <= STATUS_ROUTE_LABEL_LIMIT) return lead;
const cut = lead.slice(0, STATUS_ROUTE_LABEL_LIMIT - 1);
return `${cut.slice(0, cut.lastIndexOf(" ")).trimEnd()}…`;
}

/** Known pages for "Did you mean": every public page, the blog, and each listed post. */
export function statusPageRoutes(
pages: readonly Pick<PublicPage, "canonicalPath" | "title">[],
posts: readonly Pick<BlogPost, "slug" | "title">[],
): StatusPageLink[] {
return [
...pages.map((page) => {
// WebMCP registry pages carry long registry titles; the domain reads better.
const provider = /^\/providers\/([^/]+)\/$/u.exec(page.canonicalPath)?.[1];
return {
href: page.canonicalPath,
label: statusRouteLabel(provider === undefined ? page.title : `${provider} on Ghostget`),
};
}),
{ href: BLOG_PATH, label: statusRouteLabel(BLOG_TITLE) },
...posts.map((post) => ({ href: blogPostPath(post.slug), label: statusRouteLabel(post.title) })),
];
}

export function renderPreview(template: string, cssAsset: string): string {
const rendered = replaceRequired(template, "{{CSS_ASSET}}", escapeHtml(cssAsset));
if (/\{\{[A-Z0-9_]+\}\}/u.test(rendered)) {
Expand Down Expand Up @@ -1128,6 +1191,7 @@ export async function buildWebsite(
designKitProductMarketingCss,
designKitPlainSiteCss,
designKitPlainPublicationCss,
designKitStatusPageCss,
hranessSiteFooterCss,
blogShell,
blogFragments,
Expand Down Expand Up @@ -1156,6 +1220,7 @@ export async function buildWebsite(
})),
readFile(designKitPlainSiteStylesPath, "utf8"),
readFile(designKitPlainPublicationStylesPath, "utf8"),
readFile(fileURLToPath(import.meta.resolve("@hraness/design-kit/status-page.css")), "utf8"),
readFile(
fileURLToPath(import.meta.resolve("@hraness/site-footer/stylex.css")),
"utf8",
Expand All @@ -1175,6 +1240,7 @@ export async function buildWebsite(
join(sourceRoot, "foil.ts"),
join(sourceRoot, "ghostget-field.ts"),
join(sourceRoot, "appearance.ts"),
join(sourceRoot, "status-page.ts"),
],
format: "esm",
minify: true,
Expand All @@ -1192,7 +1258,7 @@ export async function buildWebsite(
);
const webmcpIndexValues = webmcpIndexTemplateValues(webmcpSnapshot);
const allPages: readonly PublicPage[] = [...PUBLIC_PAGES, ...webmcpPages];
if (!browserBuild.success || browserBuild.outputs.length !== 5) {
if (!browserBuild.success || browserBuild.outputs.length !== 6) {
const messages = browserBuild.logs.map((log) => log.message).join("\n");
throw new Error(`Browser script build failed: ${messages || "no browser output"}`);
}
Expand All @@ -1206,6 +1272,7 @@ export async function buildWebsite(
const skillInstall = new Uint8Array(await scriptAsset("skill-install-command"));
const foil = new Uint8Array(await scriptAsset("foil"));
const field = new Uint8Array(await scriptAsset("ghostget-field"));
const statusPage = new Uint8Array(await scriptAsset("status-page"));
// The blocking head script is a classic script: wrap the shared ESM output in
// one strict-mode IIFE so it never leaks a top-level binding.
const appearance = new TextEncoder().encode(`(() => { "use strict";\n${new TextDecoder().decode(await scriptAsset("appearance"))}\n})();`);
Expand All @@ -1221,13 +1288,14 @@ export async function buildWebsite(
const lanternCss = lanternMaterial.files.get("lantern-material.css");
const lanternLicense = lanternMaterial.files.get("LICENSE");
if (lanternCss === undefined || lanternLicense === undefined) throw new Error("The complete Lantern build snapshot is required.");
const compiledCss = `${uiCss}\n\n${designKitFontsCss.trim()}\n\n${designKitTypographyCss.trim()}\n\n${designKitProductMarketingCss.trim()}\n\n${designKitPlainSiteCss.trim()}\n\n${designKitPlainPublicationCss.trim()}\n\n${hranessSiteFooterCss.trim()}\n\n${paperThemeCss.trim()}\n\n${paletteSystemCss.trim()}\n\n${paletteBridgeCss.replace('@import "./palette-system.css";', "").trim()}\n\n${appearanceCss.trim()}\n\n${css.trimEnd()}\n\n${marketingPreset.files.get("product-marketing-preset.css")!.toString("utf8")}\n\n${lanternCss.toString("utf8")}\n`;
const compiledCss = `${uiCss}\n\n${designKitFontsCss.trim()}\n\n${designKitTypographyCss.trim()}\n\n${designKitProductMarketingCss.trim()}\n\n${designKitPlainSiteCss.trim()}\n\n${designKitPlainPublicationCss.trim()}\n\n${designKitStatusPageCss.trim()}\n\n${hranessSiteFooterCss.trim()}\n\n${paperThemeCss.trim()}\n\n${paletteSystemCss.trim()}\n\n${paletteBridgeCss.replace('@import "./palette-system.css";', "").trim()}\n\n${appearanceCss.trim()}\n\n${css.trimEnd()}\n\n${marketingPreset.files.get("product-marketing-preset.css")!.toString("utf8")}\n\n${lanternCss.toString("utf8")}\n`;
const cssAsset = `/assets/styles-${contentHash(compiledCss)}.css`;
const analyticsAsset = `/assets/analytics-${contentHash(analytics)}.js`;
const appearanceAsset = `/assets/appearance-${contentHash(appearance)}.js`;
const skillInstallAsset = `/assets/skill-install-${contentHash(skillInstall)}.js`;
const foilAsset = `/assets/foil-${contentHash(foil)}.js`;
const fieldAsset = `/assets/field-${contentHash(field)}.js`;
const statusPageAsset = `/assets/status-page-${contentHash(statusPage)}.js`;
const providerDirectory = createProviderDirectory(attestation);
const beeperFacts = createBeeperPresentationFacts(providerDirectory);
const whatsappFacts = createWhatsAppPresentationFacts(providerDirectory, attestation);
Expand Down Expand Up @@ -1313,7 +1381,14 @@ export async function buildWebsite(
)),
writeFile(join(outputRoot, BLOG_FEED_PATH.slice(1)), renderBlogAtomFeed(BLOG_SITE)),
writeFile(join(outputRoot, "preview/index.html"), renderPreview(previewTemplate, cssAsset)),
writeFile(join(outputRoot, "404.html"), renderTemplate(notFoundTemplate, renderOptions)),
writeFile(join(outputRoot, "404.html"), renderTemplate(
// A function replacement keeps "$" in route labels literal.
replaceHtmlRequired(notFoundTemplate, "{{STATUS_PAGE_ASSET}}", escapeHtml(statusPageAsset)).replace(
"{{STATUS_PAGE}}",
() => renderGhostgetStatusPage(statusPageRoutes(allPages, BLOG_POSTS.filter(isIndexablePost))),
),
renderOptions,
)),
writeFile(join(outputRoot, "404.md"), renderTemplate(notFoundMarkdown, renderOptions, undefined, "text")),
writeFile(join(outputRoot, "llms.txt"), renderTemplate(
replaceRequired(llmsTemplate, "{{BLOG_LLMS_ENTRIES}}", renderBlogLlmsEntries(BLOG_SITE)),
Expand All @@ -1327,6 +1402,7 @@ export async function buildWebsite(
writeFile(join(outputRoot, skillInstallAsset.slice(1)), skillInstall),
writeFile(join(outputRoot, foilAsset.slice(1)), foil),
writeFile(join(outputRoot, fieldAsset.slice(1)), field),
writeFile(join(outputRoot, statusPageAsset.slice(1)), statusPage),
writeFile(
join(outputRoot, "robots.txt"),
`User-agent: *\nAllow: /\n\nSitemap: ${SITE_ORIGIN}/sitemap.xml\n`,
Expand Down
Loading
Loading