mirror of
https://github.com/Such-Software/such.software.git
synced 2026-10-17 05:32:05 +02:00
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.
106 lines
3.1 KiB
TypeScript
106 lines
3.1 KiB
TypeScript
/**
|
||
* 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` },
|
||
},
|
||
};
|
||
}
|