diff --git a/apps/web/src/layouts/Layout.astro b/apps/web/src/layouts/Layout.astro
index 8bd1718..da67b0b 100644
--- a/apps/web/src/layouts/Layout.astro
+++ b/apps/web/src/layouts/Layout.astro
@@ -2,6 +2,14 @@
import { Font } from 'astro:assets';
import '../styles/global.css';
import Header from '../components/Header.astro';
+import { NETH_MARK_FILE } from '../lib/nethMark.ts';
+import {
+ SITE_TITLE,
+ SOCIAL_IMAGE_HEIGHT,
+ SOCIAL_IMAGE_WIDTH,
+ socialImageUrl,
+ withBase,
+} from '../lib/site.ts';
interface Props {
title: string;
@@ -11,6 +19,8 @@ interface Props {
const { title, description, surface = 'docs' } = Astro.props;
const canonical = new URL(Astro.url.pathname, Astro.site);
+const faviconHref = withBase(NETH_MARK_FILE);
+const socialImage = socialImageUrl();
---
@@ -21,10 +31,22 @@ const canonical = new URL(Astro.url.pathname, Astro.site);
{title}
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web/src/lib/nethMark.ts b/apps/web/src/lib/nethMark.ts
index 2adf200..99c2cff 100644
--- a/apps/web/src/lib/nethMark.ts
+++ b/apps/web/src/lib/nethMark.ts
@@ -1,20 +1,10 @@
-import { SITE_BASE, SITE_ORIGIN } from './site.ts';
+import { siteAssetUrl } from './site.ts';
export const NETH_MARK_VIEWBOX = '186 117 878 878';
export const NETH_MARK_PATH =
'M 628 117 L 393 544 L 608 995 L 858 552 Z M 628 255 L 689 410 L 627 643 L 737 489 L 769 551 L 611 843 L 476 546 Z';
export const NETH_MARK_FILE = 'neth.svg';
-function envString(key: string): string | undefined {
- const env = import.meta.env as Record | undefined;
- const value = env?.[key];
- return typeof value === 'string' && value.trim() ? value.trim() : undefined;
-}
-
export function nethMarkUrl(): string {
- const originRaw = envString('SITE') ?? SITE_ORIGIN;
- const origin = originRaw.endsWith('/') ? originRaw : `${originRaw}/`;
- const baseRaw = envString('BASE_URL') ?? SITE_BASE;
- const base = baseRaw.endsWith('/') ? baseRaw : `${baseRaw}/`;
- return new URL(`${base}${NETH_MARK_FILE}`, origin).href;
+ return siteAssetUrl(NETH_MARK_FILE);
}
diff --git a/apps/web/src/lib/site.test.ts b/apps/web/src/lib/site.test.ts
new file mode 100644
index 0000000..a872891
--- /dev/null
+++ b/apps/web/src/lib/site.test.ts
@@ -0,0 +1,48 @@
+import assert from 'node:assert/strict';
+import { existsSync, readFileSync } from 'node:fs';
+import { dirname, join } from 'node:path';
+import { describe, it } from 'node:test';
+import { fileURLToPath } from 'node:url';
+import { NETH_MARK_FILE } from './nethMark.ts';
+import {
+ SOCIAL_IMAGE_FILE,
+ SOCIAL_IMAGE_HEIGHT,
+ SOCIAL_IMAGE_WIDTH,
+ siteAssetUrl,
+ socialImageUrl,
+ withBase,
+} from './site.ts';
+
+const here = dirname(fileURLToPath(import.meta.url));
+const publicDir = join(here, '../../public');
+const layoutPath = join(here, '../layouts/Layout.astro');
+
+describe('site assets', () => {
+ it('publishes hero.png as the social preview image', () => {
+ const file = join(publicDir, SOCIAL_IMAGE_FILE);
+ assert.equal(existsSync(file), true, SOCIAL_IMAGE_FILE);
+ const png = readFileSync(file);
+ assert.equal(png.subarray(0, 8).equals(Buffer.from([137, 80, 78, 71, 13, 10, 26, 10])), true);
+ assert.equal(png.readUInt32BE(16), SOCIAL_IMAGE_WIDTH);
+ assert.equal(png.readUInt32BE(20), SOCIAL_IMAGE_HEIGHT);
+ assert.equal(socialImageUrl(), 'https://netherprotocol.xyz/hero.png');
+ });
+
+ it('publishes the NETH mark as the favicon', () => {
+ assert.equal(existsSync(join(publicDir, NETH_MARK_FILE)), true, NETH_MARK_FILE);
+ assert.equal(siteAssetUrl(NETH_MARK_FILE), 'https://netherprotocol.xyz/neth.svg');
+ assert.equal(withBase(NETH_MARK_FILE), '/neth.svg');
+ });
+
+ it('wires favicon and social image tags in the document head', () => {
+ const layout = readFileSync(layoutPath, 'utf8');
+ assert.ok(layout.includes('rel="icon"'));
+ assert.ok(layout.includes('type="image/svg+xml"'));
+ assert.ok(layout.includes('withBase(NETH_MARK_FILE)'));
+ assert.ok(layout.includes('property="og:image"'));
+ assert.ok(layout.includes('name="twitter:card"'));
+ assert.ok(layout.includes('summary_large_image'));
+ assert.ok(layout.includes('name="twitter:image"'));
+ assert.ok(layout.includes('socialImageUrl()'));
+ });
+});
diff --git a/apps/web/src/lib/site.ts b/apps/web/src/lib/site.ts
index 55d6890..4c3ce76 100644
--- a/apps/web/src/lib/site.ts
+++ b/apps/web/src/lib/site.ts
@@ -58,15 +58,37 @@ export const RISK_WARNING_DETAILS = [
'Capital is at high risk. Use Nether entirely at your own risk.',
] as const;
-function githubRepoUrl(): string {
+export const SOCIAL_IMAGE_FILE = 'hero.png';
+export const SOCIAL_IMAGE_WIDTH = 1122;
+export const SOCIAL_IMAGE_HEIGHT = 1402;
+
+function envString(key: string): string | undefined {
const env = import.meta.env as Record | undefined;
- const fromEnv = env?.PUBLIC_GITHUB_REPO;
- if (typeof fromEnv === 'string' && fromEnv.trim()) {
- return fromEnv.trim().replace(/\/+$/, '');
+ const value = env?.[key];
+ return typeof value === 'string' && value.trim() ? value.trim() : undefined;
+}
+
+function githubRepoUrl(): string {
+ const fromEnv = envString('PUBLIC_GITHUB_REPO');
+ if (fromEnv) {
+ return fromEnv.replace(/\/+$/, '');
}
return GITHUB_REPO_FALLBACK;
}
+export function siteAssetUrl(pathname: string): string {
+ const originRaw = envString('SITE') ?? SITE_ORIGIN;
+ const origin = originRaw.endsWith('/') ? originRaw : `${originRaw}/`;
+ const baseRaw = envString('BASE_URL') ?? SITE_BASE;
+ const base = baseRaw.endsWith('/') ? baseRaw : `${baseRaw}/`;
+ const trimmed = pathname.replace(/^\/+/, '');
+ return new URL(`${base}${trimmed}`, origin).href;
+}
+
+export function socialImageUrl(): string {
+ return siteAssetUrl(SOCIAL_IMAGE_FILE);
+}
+
export function withBase(pathname = ''): string {
const env = import.meta.env as Record | undefined;
const base = (typeof env?.BASE_URL === 'string' && env.BASE_URL) || SITE_BASE;
diff --git a/apps/web/src/pages/index.astro b/apps/web/src/pages/index.astro
index 9488da1..c935d18 100644
--- a/apps/web/src/pages/index.astro
+++ b/apps/web/src/pages/index.astro
@@ -12,11 +12,14 @@ import {
SECONDARY_CTA,
SITE_DESCRIPTION,
SITE_TITLE,
+ SOCIAL_IMAGE_FILE,
+ SOCIAL_IMAGE_HEIGHT,
+ SOCIAL_IMAGE_WIDTH,
withBase,
} from '../lib/site';
const learnHref = withBase('learn');
-const heroSrc = withBase('hero.png');
+const heroSrc = withBase(SOCIAL_IMAGE_FILE);
---
@@ -54,8 +57,8 @@ const heroSrc = withBase('hero.png');