open-design/apps/landing-page/app/_components/sub-page-layout.astro
Joey-nexu e3a848a33a
feat(landing-page): replace Ø wordmark with PNG logo across nav/footer/favicon (#1449)
* feat(landing-page): replace Ø wordmark with PNG logo across nav/footer/favicon

Switches the brand mark from the Unicode 'Ø' glyph to the new circular
gradient paper-plane PNG. Header nav and footer share the same image, and
the browser tab + iOS home screen icons are regenerated from the same
500x500 source.

- public/logo.png (500x500, brand source)
- public/favicon.png (32x32, replaces favicon.svg)
- public/apple-touch-icon.png (180x180, regenerated)
- header.tsx + page.tsx footer: <span>Ø</span> -> <img src=/logo.png />
- globals.css: simplify .brand-mark (drop Ø-era border/font, add object-fit
  contain on child img)
- index.astro: link rel=icon now points at favicon.png

* fix(landing-page): apply logo + favicon swap to sub-page layout too

Review on #1449 caught two cross-page consistency issues:

- P1: sub-page-layout.astro still linked /favicon.svg, which this PR
  deletes — every Skills/Systems/Templates/Craft page would request a
  missing asset. Updated to /favicon.png to match index.astro.
- P2: sub-page-layout.astro still rendered the Ø wordmark in its footer
  brand block, leaving the public site with mixed brand marks. Replaced
  with the same <img src=/logo.png /> wrapper pattern used on the
  homepage header and footer.

Repo-wide grep now shows 0 favicon.svg references and 0 Ø brand-mark
spans. typecheck still 25 files / 0 errors / 0 warnings.

---------

Co-authored-by: Joey-nexu <236967869+joeylee12629-star@users.noreply.github.com>
2026-05-13 12:30:32 +08:00

169 lines
6.3 KiB
Text

---
/*
* Shared shell for every sub-page outside of `/` (Skills, Systems,
* Craft, Templates and their detail pages).
*
* The homepage (`/`) intentionally does NOT use this layout — its
* chrome (rails, topbar, full hero, mega-word footer) stays in
* lockstep with `skills/open-design-landing/example.html`. This
* layout is the lighter sibling: same Atelier Zero tokens, same
* sticky nav, but no editorial side rails or hero, and a compact
* footer focused on the site map.
*
* Every sub-page passes `title`, `description`, `active` (nav
* highlight) and an optional `jsonLd`. Catalog counts are read from
* `getCatalogCounts()` so the nav badges stay live.
*/
import '../globals.css';
import '../sub-pages.css';
import { createElement } from 'react';
import { renderToStaticMarkup } from 'react-dom/server';
import { Header, type HeaderProps } from './header';
import { heroImage } from '../image-assets';
import { getCatalogCounts } from '../_lib/catalog';
export interface Props {
title: string;
description: string;
active?: HeaderProps['active'];
ogImage?: string;
jsonLd?: Record<string, unknown> | Array<Record<string, unknown>>;
}
const { title, description, active = 'home', ogImage, jsonLd } = Astro.props;
const canonical = new URL(Astro.url.pathname, Astro.site).toString();
const og = ogImage ?? heroImage;
const counts = await getCatalogCounts();
const headerHtml = renderToStaticMarkup(
Header({ active, counts, brandHref: '/' }) as ReturnType<typeof createElement>,
);
const ldArray = jsonLd ? (Array.isArray(jsonLd) ? jsonLd : [jsonLd]) : [];
const REPO = 'https://github.com/nexu-io/open-design';
---
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#efe7d2" />
<title>{title}</title>
<meta name="description" content={description} />
<link rel="canonical" href={canonical} />
<link rel="icon" type="image/png" sizes="32x32" href="/favicon.png" />
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
<meta property="og:type" content="website" />
<meta property="og:site_name" content="Open Design" />
<meta property="og:title" content={title} />
<meta property="og:description" content={description} />
<meta property="og:url" content={canonical} />
<meta property="og:image" content={og} />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content={title} />
<meta name="twitter:description" content={description} />
<meta name="twitter:image" content={og} />
{ldArray.map((data) => (
<script is:inline type="application/ld+json" set:html={JSON.stringify(data)} />
))}
</head>
<body class="sub-page">
<div class="shell">
{/* Same React-rendered Header used by the homepage. SSR'd here
* so we have one nav implementation that handles active state. */}
<Fragment set:html={headerHtml} />
<main class="sub-main container">
<slot />
</main>
<footer class="sub-footer" data-od-id="sub-footer">
<div class="container sub-footer-inner">
<div class="sub-footer-grid">
<div class="sub-footer-brand">
<a href="/" class="brand">
<span class="brand-mark">
<img src="/logo.png" alt="" width="36" height="36" />
</span>
<span>Open Design</span>
</a>
<p>
The open-source alternative to Claude Design. Apache-2.0,
local-first, BYOK at every layer.
</p>
</div>
<div class="sub-footer-col">
<h5>Catalog</h5>
<ul>
<li><a href="/skills/">{counts.skills} Skills</a></li>
<li><a href="/systems/">{counts.systems} Systems</a></li>
<li><a href="/templates/">{counts.templates} Templates</a></li>
<li><a href="/craft/">{counts.craft} Craft principles</a></li>
</ul>
</div>
<div class="sub-footer-col">
<h5>Connect</h5>
<ul>
<li><a href={REPO} target="_blank" rel="noopener">GitHub</a></li>
<li><a href={`${REPO}/issues`} target="_blank" rel="noopener">Issues</a></li>
<li><a href={`${REPO}/releases`} target="_blank" rel="noopener">Releases</a></li>
<li><a href="/#contact">Contact</a></li>
</ul>
</div>
</div>
<div class="sub-footer-bottom">
<span>● Open Design · Apache-2.0 · 2026 / Volume 01 / Issue Nº 26</span>
<span>Berlin / Open / Earth · 52.5200° N · 13.4050° E</span>
</div>
</div>
</footer>
</div>
<script is:inline>
(() => {
// Headroom-style hide-on-scroll, mirrors the homepage
// enhancement so nav behavior is consistent across pages.
const nav = document.querySelector('[data-nav-headroom]');
if (nav) {
let lastY = window.scrollY;
const showTopThreshold = 100;
const scrollDelta = 6;
window.addEventListener(
'scroll',
() => {
const y = window.scrollY;
const delta = y - lastY;
if (y <= showTopThreshold) nav.classList.remove('is-hidden');
else if (delta > scrollDelta) nav.classList.add('is-hidden');
else if (delta < -scrollDelta) nav.classList.remove('is-hidden');
lastY = y;
},
{ passive: true },
);
}
const stars = document.querySelector('[data-github-stars]');
if (stars) {
fetch('https://api.github.com/repos/nexu-io/open-design', {
headers: { Accept: 'application/vnd.github+json' },
})
.then((r) => (r.ok ? r.json() : Promise.reject(new Error('http error'))))
.then((data) => {
if (typeof data?.stargazers_count === 'number') {
const n = data.stargazers_count;
stars.textContent =
n < 1000
? String(n)
: `${(n / 1000).toFixed(1).replace(/\.0$/, '')}K`;
}
})
.catch(() => {});
}
})();
</script>
</body>
</html>