Files
such.software/apps/web/src/components/seo/json-ld.tsx
T
jwinterm c73b9f1c04 feat(brand): regenerate every mark and animation from the such-graphics pack
All branding and animation assets now come from packs/such-software/pack.json
(such-graphics feat/brand-full-blast) instead of hand-maintained files:

- anim1_pulse / anim3_matrix / anim_type_v4 (both themes): regenerated with
  the corrected U bowl, measured pulse timing (the hand-authored node delays
  ran ~17% fast), and CSS animation with real prefers-reduced-motion support.
- clean marks, lockups (clean_*_w_text), favicons (svg + 192px png), and the
  OG banner: regenerated; the OG card is the pack's 1200x630 projection.
- splash-such: rebuilt from the pack -- the old one carried the divergent U,
  an off-palette cyan frame, and the pre-correction radius. Now theme-aware:
  two inlined data URIs (hero-splash shows the light palette on light, the
  dark palette on dark) since the pack's dark hues are tuned for near-black.
- new assets available: games_pulse (the launch arch with the rocket, wired
  into /apps), morph, ambient_{atom,mesh,leaf}, favicon_dots, favicon_live,
  icon-192/512 + maskable-512 (wired into manifest.json), /favicon.ico.
- animation embeds switched <img> -> <object>: Chrome freezes CSS animations
  inside SVG-as-img (the old files animated because they were SMIL); an
  object hosts a real SVG document where CSS animation and the
  reduced-motion query both work. Verified live in Chrome.
2026-08-15 23:18:44 -04:00

106 lines
3.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* JSON-LD helpers. `JsonLd` renders a structured-data <script>; the builders return
* schema.org objects so pages can stay declarative. Server components only (no client JS).
*/
const SITE = "https://such.software";
export function JsonLd({ data }: { data: object }) {
return (
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(data) }}
/>
);
}
/** Breadcrumb trail for a deep page, e.g. Home › Products › Neroswap. */
export function breadcrumbLd(items: { name: string; path: string }[]) {
return {
"@context": "https://schema.org",
"@type": "BreadcrumbList",
itemListElement: items.map((it, i) => ({
"@type": "ListItem",
position: i + 1,
name: it.name,
item: `${SITE}${it.path}`,
})),
};
}
type SoftwareAppOpts = {
name: string;
path: string;
description: string;
/** schema.org applicationCategory, e.g. "GameApplication", "FinanceApplication". */
category: string;
operatingSystem: string;
/** Price as a string; "0" for free. */
price?: string;
/** External play/download URL the offer points at (defaults to the on-site path). */
offerUrl?: string;
/** Optional store rating, rendered as AggregateRating when provided. */
rating?: { value: string; count: string };
};
/** SoftwareApplication node: drives app/game rich results (and star ratings when rated). */
export function softwareAppLd(opts: SoftwareAppOpts) {
const data: Record<string, unknown> = {
"@context": "https://schema.org",
"@type": "SoftwareApplication",
name: opts.name,
url: `${SITE}${opts.path}`,
description: opts.description,
applicationCategory: opts.category,
operatingSystem: opts.operatingSystem,
offers: {
"@type": "Offer",
price: opts.price ?? "0",
priceCurrency: "USD",
url: opts.offerUrl ?? `${SITE}${opts.path}`,
},
publisher: { "@type": "Organization", name: "Such Software" },
};
if (opts.rating) {
data.aggregateRating = {
"@type": "AggregateRating",
ratingValue: opts.rating.value,
ratingCount: opts.rating.count,
};
}
return data;
}
/** FAQPage node. Pair it with the SAME questions/answers rendered visibly on the page. */
export function faqLd(items: { q: string; a: string }[]) {
return {
"@context": "https://schema.org",
"@type": "FAQPage",
mainEntity: items.map((it) => ({
"@type": "Question",
name: it.q,
acceptedAnswer: { "@type": "Answer", text: it.a },
})),
};
}
/** BlogPosting node for an article page. */
export function articleLd(opts: { title: string; description: string; slug: string; date: string }) {
const url = `${SITE}/blog/${opts.slug}`;
return {
"@context": "https://schema.org",
"@type": "BlogPosting",
headline: opts.title,
description: opts.description,
url,
mainEntityOfPage: url,
datePublished: opts.date,
dateModified: opts.date,
author: { "@type": "Organization", name: "Such Software" },
publisher: {
"@type": "Organization",
name: "Such Software",
logo: { "@type": "ImageObject", url: `${SITE}/images/branding/icon-512.png` },
},
};
}