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
7 changes: 7 additions & 0 deletions .changeset/astro-marketing-site.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"nostream": minor
---

feat(website): add Astro marketing site with landing film, docs, changelog and community pages

Adds `website/site` (nostream.dev) and updates the landing hero animation to sync NIP count and branding from the repo. Docs, changelog, version label and NIP list are read from the repository at build time.
16 changes: 14 additions & 2 deletions .husky/install.mjs
Original file line number Diff line number Diff line change
@@ -1,18 +1,30 @@
import { existsSync } from 'node:fs';
import { execSync } from 'node:child_process';
import { createRequire } from 'node:module';
import path from 'node:path';
import { fileURLToPath } from 'node:url';

const repoRoot = path.join(path.dirname(fileURLToPath(import.meta.url)), '..');

// Skip Husky installation in environments where hooks are not needed
if (
process.env.NODE_ENV === 'production' ||
process.env.CI === 'true' ||
process.env.HUSKY === '0' ||
!existsSync('.git')
!existsSync(path.join(process.cwd(), '.git'))
) {
process.exit(0);
}

const require = createRequire(path.join(repoRoot, 'package.json'));
try {
require.resolve('husky');
} catch {
process.exit(0);
}

try {
execSync('npx husky install', { stdio: 'ignore' });
execSync('pnpm exec husky install', { cwd: repoRoot, stdio: 'ignore', timeout: 8000 });
} catch {
process.exit(0);
}
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ Install Docker from their [official guide](https://docs.docker.com/engine/instal

## Full Guide

- [Set up a Paid Nostr relay with Nostream and ZEBEDEE](https://docs.zebedee.io/docs/guides/nostr-relay) by [André Neves](https://primal.net/andre) (CTO & Co-Founder at [ZEBEDEE](https://zebedee.io/))
- [Set up a Paid Nostr relay with Nostream and ZEBEDEE](https://docs.zbdpay.com/payments/nostr/nostr-relay) by [André Neves](https://primal.net/andre) (CTO & Co-Founder at [ZEBEDEE](https://zebedee.io/))
- [Set up a Nostr relay in under 5 minutes](https://andreneves.xyz/p/set-up-a-nostr-relay-server-in-under) by [André Neves](https://twitter.com/andreneves) (CTO & Co-Founder at [ZEBEDEE](https://zebedee.io/))

### Accepting Payments
Expand Down Expand Up @@ -119,7 +119,7 @@ Install Docker from their [official guide](https://docs.docker.com/engine/instal
- `payments.processor` to `zebedee`
- `paymentsProcessors.zebedee.callbackBaseURL` to match your Nostream URL (e.g. `https://{YOUR_DOMAIN_HERE}/callbacks/zebedee`)
- Restart Nostream (`nostream stop` followed by `nostream start`)
- Read the in-depth guide for more information: [Set Up a Paid Nostr Relay with ZEBEDEE API](https://docs.zebedee.io/docs/guides/nostr-relay)
- Read the in-depth guide for more information: [Set Up a Paid Nostr Relay with ZEBEDEE API](https://docs.zbdpay.com/payments/nostr/nostr-relay)

3. [Nodeless](https://nodeless.io/?ref=587f477f-ba1c-4bd3-8986-8302c98f6731)
- Complete the step "Before you begin"
Expand Down
17 changes: 13 additions & 4 deletions website/hero-animation/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,17 @@ pnpm run preview

## Export MP4

NIP counts in the film (hero stats, features slide, relay-core checklist) come from the repo root `package.json` → `supportedNips` via `src/repo-meta.ts`. After changing supported NIPs, re-render and refresh site media:

```bash
pnpm run render # → output/nostream-hero.mp4 + output/film.meta.json
pnpm run frames # poster frame for the website
cd ../site && pnpm run media
```

`pnpm run render` writes `output/film.meta.json` (`nipCount`). The Astro site copies that to `public/media/hero.meta.json` and `pnpm build` runs `check-hero-nips.mjs` so a stale hero video fails CI until you re-export.

```bash
pnpm run render # → output/nostream-hero.mp4 (very high bitrate, ~45 MB)
pnpm run render:small # → output/nostream-hero-low.mp4 (~4 MB, for sharing / PR previews)
```

Expand All @@ -44,7 +53,7 @@ pnpm run frames
# → output/frames/f0130.png …
```

## Release film — nostream 3.3.0
## Release film — nostream 3.4.0

A second, operator-focused cut in the same visual dialect: **40 s** at 1920×1080, 30 fps = **1200 frames**, composed in `src/release-film.ts` and rendered headlessly by `src/release-render.ts`.

Expand All @@ -54,8 +63,8 @@ A second, operator-focused cut in the same visual dialect: **40 s** at 1920×108
4. **Close** (760–1200): sign-off over the particle planet

```bash
pnpm run render:release # → output/release/nostream-3.3.0-very_high.mp4 (~14 MB)
pnpm run render:release:small # → output/release/nostream-3.3.0-low.mp4 (~2.3 MB, for sharing)
pnpm run render:release # → output/release/nostream-3.4.0-very_high.mp4 (~14 MB)
pnpm run render:release:small # → output/release/nostream-3.4.0-low.mp4 (~2.3 MB, for sharing)
pnpm run frames:release # → output/release/frames/f0060.png … (one still per scene + the end card)
```

Expand Down
21 changes: 20 additions & 1 deletion website/hero-animation/src/build-assets.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,18 @@
import { mkdir, readFile } from 'node:fs/promises'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
import sharp, { type Sharp } from 'sharp'

const SITE_LOGO = path.join(
path.dirname(fileURLToPath(import.meta.url)),
'..',
'..',
'site',
'src',
'assets',
'logo.png',
)

const RAW = 'assets/icons-raw'
const OUT = 'assets/gen'
const GRAY = '#8a8a8a'
Expand Down Expand Up @@ -66,6 +78,13 @@ async function logos() {
}
}

/** Official nostream mark (same file as the Astro site). */
async function nostreamBrand() {
const fit = { fit: 'contain' as const, background: { r: 0, g: 0, b: 0, alpha: 0 } }
await sharp(SITE_LOGO).resize(512, 512, fit).png().toFile(`${OUT}/nostream.png`)
await sharp(SITE_LOGO).resize(512, 512, fit).grayscale().png().toFile(`${OUT}/nostream-gray.png`)
}

function rng(seed: number) {
return () => {
seed = (seed * 1664525 + 1013904223) % 4294967296
Expand Down Expand Up @@ -187,5 +206,5 @@ async function grain() {
}

await mkdir(OUT, { recursive: true })
await Promise.all([logos(), planet(), glow(), grain(), grid(), sparkles()])
await Promise.all([logos(), nostreamBrand(), planet(), glow(), grain(), grid(), sparkles()])
console.log(`assets written to ${OUT}/`)
80 changes: 34 additions & 46 deletions website/hero-animation/src/film.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { readFileSync } from 'node:fs'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
import { Composition, Layer, LayerAnimation, Vignette } from 'framefields'
import { readSupportedNipCount } from './repo-meta.js'
import {
BG,
BODY,
Expand All @@ -26,9 +26,7 @@ import {

const ROOT = path.join(path.dirname(fileURLToPath(import.meta.url)), '..')
const asset = (name: string) => path.join(ROOT, 'assets', 'gen', `${name}.png`)
const NIP_COUNT = String(
JSON.parse(readFileSync(path.join(ROOT, '..', '..', 'package.json'), 'utf8')).supportedNips.length,
)
const NIP_COUNT = String(readSupportedNipCount())

type Node =
| ReturnType<typeof Layer.box>
Expand Down Expand Up @@ -269,28 +267,9 @@ export async function buildFilm(): Promise<Composition> {

// ── Persistent chrome: nav, footer ──────────────────────────────────────
const always = { start: 0, inF: 12 }
add(rect(120, 45, 16, 16, { background: PINK }), show(45, always))
add(mono('nostream', 146, 37, 22, { fill: FG, fontWeight: 700 }), show(37, always))

const navSize = 14
const navSpacing = 2
let navX = 1800
for (const label of ['GITHUB', 'NIPS', 'DOCS']) {
navX -= monoWidth(label, navSize, navSpacing)
add(mono(label, navX, 42, navSize, { letterSpacing: navSpacing, fill: BODY }), show(42, always))
if (label === 'GITHUB') {
add(img('github', navX - 28, 40, 20), show(40, { ...always, peak: 0.75 }))
navX -= 28
}
navX -= 44
}

const footerRight = 'OPEN SOURCE · MIT LICENSE · GITHUB.COM/CAMERI/NOSTREAM'
add(img('nostream', 118, 41, 24), show(41, always))
add(mono('nostream', 150, 37, 22, { fill: FG, fontWeight: 700 }), show(37, always))
add(mono('nostream', 120, 1022, 18, { fill: FG, fontWeight: 700 }), show(1022, always))
add(
mono(footerRight, 1800 - monoWidth(footerRight, 13, 2), 1025, 13, { letterSpacing: 2, fill: BODY }),
show(1025, always),
)

const hx = 160
const sceneLabel = (text: string, y: number, start: number, end: number) =>
Expand Down Expand Up @@ -533,7 +512,7 @@ export async function buildFilm(): Promise<Composition> {
add(rect(cx - 6, cy - 6, 12, 12, { background: PINK, borderRadius: 6 }), at(iy))
}
if (i === 2) {
add(rect(x + 44, iy + 12, 44, 44, { background: PINK, borderRadius: 2 }), at(iy))
add(img('nostream', x + 44, iy + 12, 44), at(iy))
}
light(i === 0 ? 'nostr' : 'nostream', s, s + 50)
add(sans(title, x + 36, wc.y + 128, 38, { letterSpacing: -1, fill: accent ? PINK : FG }), at(wc.y + 128, 6))
Expand Down Expand Up @@ -570,34 +549,48 @@ export async function buildFilm(): Promise<Composition> {
)
const atF = (start: number, y: number) => show(y, { start, end: fEnd, dy: 16 })

const core = { x: 760, y: 500, w: 400, h: 280 }
const checks = [
'verify id + schnorr signature',
'rate limits · spam rules · PoW',
'paid admission · NIP-42 auth',
'NIP-11 relay information',
`${NIP_COUNT} NIPs supported`,
]
const checkTop = 116
const checkStep = 28
const footerTop = checkTop + checks.length * checkStep + 14
const coreH = footerTop + 38
const core = { x: 760, y: 500, w: 400, h: coreH }
const coreStart = f0 + 18
glow(core.x + core.w / 2, core.y + core.h / 2 - 40, 780, [[coreStart, fEnd]], 0.35)
card(core.x, core.y, core.w, core.h, coreStart, fEnd, true)
add(rect(core.x + 30, core.y + 34, 18, 18, { background: PINK }), atF(coreStart + 4, core.y + 34))
add(img('nostream', core.x + 28, core.y + 28, 24), atF(coreStart + 4, core.y + 28))
add(sans('nostream', core.x + 60, core.y + 20, 36, { letterSpacing: -1 }), atF(coreStart + 4, core.y + 20))
add(
mono('RELAY CORE', core.x + 30, core.y + 72, 13, { letterSpacing: 3, fill: TEAL }),
atF(coreStart + 6, core.y + 72),
)
add(rect(core.x + 30, core.y + 100, core.w - 60, 1, { background: LINE }), atF(coreStart + 6, core.y + 100))
const checks = [
'verify id + schnorr signature',
'rate limits · spam rules · PoW',
'paid admission · NIP-42 auth',
'NIP-11 relay information',
]
checks.forEach((text, i) => {
const y = core.y + 116 + i * 30
const y = core.y + checkTop + i * checkStep
const tick = CYCLES[0] + 24 + i * 6
add(mono('·', core.x + 30, y, 16, { fill: MUTED }), show(y, { start: coreStart + 8, end: tick + 2, outF: 4 }))
add(mono(text, core.x + 56, y, 16, { fill: MUTED }), show(y, { start: coreStart + 8, end: tick + 2, outF: 4 }))
add(check(core.x + 28, y + 4, 13), show(0, { start: tick, end: fEnd, inF: 6 }))
add(mono(text, core.x + 56, y, 16, { fill: FG }), show(y, { start: tick, end: fEnd, inF: 6 }))
})
add(img('typescript', core.x + 30, core.y + 242, 22), atF(coreStart + 10, core.y + 242))
add(img('nodedotjs', core.x + 60, core.y + 242, 22), atF(coreStart + 10, core.y + 242))
add(mono('TypeScript · Node.js', core.x + 94, core.y + 245, 14, { fill: MUTED }), atF(coreStart + 10, core.y + 245))
add(
rect(core.x + 30, core.y + footerTop - 8, core.w - 60, 1, { background: LINE }),
atF(coreStart + 10, core.y + footerTop - 8),
)
const stackIcon = 20
const stackY = core.y + footerTop + 4
add(img('typescript', core.x + 30, stackY, stackIcon), atF(coreStart + 10, stackY))
add(img('nodedotjs', core.x + 30 + stackIcon + 8, stackY, stackIcon), atF(coreStart + 10, stackY))
add(
mono('TypeScript · Node.js', core.x + 30 + (stackIcon + 8) * 2 + 6, stackY + 5, 14, { fill: MUTED }),
atF(coreStart + 10, stackY + 5),
)

// Clients
const clients = [
Expand Down Expand Up @@ -816,7 +809,7 @@ export async function buildFilm(): Promise<Composition> {
]),
)
if (logos.length === 0) {
add(rect(x + 34, y + 38, 40, 40, { background: PINK, borderRadius: 2 }), at(y + 38))
add(img('nostream', x + 34, y + 38, 40), at(y + 38))
}
logos.forEach((name, j) => add(img(name, x + 28 + j * 34, y + 32 + j * 22, j ? 32 : 52), at(y + 32 + j * 22)))
add(sans(title, x + 108, y + 36, 28, { letterSpacing: -0.6 }), at(y + 36, 6))
Expand Down Expand Up @@ -902,13 +895,8 @@ export async function buildFilm(): Promise<Composition> {
const windows = [...lit[name], allOn]
const on = () => LayerAnimation.create().keys('opacity', windowKeys(windows, 1, 8, 10))
add(rect(58, y - 8, 28, 54, { background: BG }), show(y, always))
if (name === 'nostream') {
add(rect(63, y + 3, 20, 20, { background: '#4a4a4a' }), show(y, always))
add(rect(63, y + 3, 20, 20, { background: PINK }), on())
} else {
add(img(`${name}-gray`, 59, y, 27), show(y, { ...always, peak: 0.7 }))
add(img(name, 59, y, 27), on())
}
add(img(`${name}-gray`, 59, y, 27), show(y, { ...always, peak: 0.7 }))
add(img(name, 59, y, 27), on())
add(mono(String(i + 1).padStart(2, '0'), 64, y + 32, 11, { fill: MUTED }), show(y, always))
add(rect(46, y, 3, 27, { background: PINK }), on())
})
Expand Down
9 changes: 8 additions & 1 deletion website/hero-animation/src/render.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import path from 'node:path'
import { fileURLToPath } from 'node:url'
import { HeadlessMediaRenderer, startPreview } from 'framefields'
import { buildFilm } from './film.js'
import { readSupportedNipCount } from './repo-meta.js'

const __dirname = path.dirname(fileURLToPath(import.meta.url))
const OUTPUT = path.join(__dirname, '..', 'output')
Expand Down Expand Up @@ -41,5 +42,11 @@ const result = await film.renderVideo({
quality,
})
await result.cleanup?.().catch(() => {})
console.log(`${result.filePath} (${((Date.now() - started) / 1000).toFixed(1)} s)`)
const meta = {
nipCount: readSupportedNipCount(),
generatedAt: new Date().toISOString(),
output: path.basename(result.filePath),
}
await fs.writeFile(path.join(OUTPUT, 'film.meta.json'), `${JSON.stringify(meta, null, 2)}\n`)
console.log(`${result.filePath} (${((Date.now() - started) / 1000).toFixed(1)} s, ${meta.nipCount} NIPs)`)
process.exit(0)
15 changes: 15 additions & 0 deletions website/hero-animation/src/repo-meta.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { readFileSync } from 'node:fs'
import path from 'node:path'
import { fileURLToPath } from 'node:url'

const REPO_ROOT = path.join(path.dirname(fileURLToPath(import.meta.url)), '..', '..', '..')

/** Same source of truth as the website (`package.json` → `supportedNips`). */
export const readSupportedNipCount = (): number => {
const pkg = JSON.parse(readFileSync(path.join(REPO_ROOT, 'package.json'), 'utf8')) as {
supportedNips: number[]
}
return pkg.supportedNips.length
}

export const REPO_PACKAGE_JSON = path.join(REPO_ROOT, 'package.json')
3 changes: 3 additions & 0 deletions website/site/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
node_modules/
dist/
.astro/
48 changes: 48 additions & 0 deletions website/site/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# nostream website (Astro)

Landing page, docs and changelog for nostream, in the same omp.sh-style dialect as the Framefields films in [`../hero-animation`](../hero-animation).

- `/` framed landing film ([PR #803](https://github.com/cameri/nostream/pull/803) / `film.ts`), sparkles, who it's for, feature pillars, admin & observability, NIPs
- `/get-started` copy-paste build command for the [`run-a-decentralized-social-network-relay`](https://schemaformat.ai/schematics/run-a-decentralized-social-network-relay/SCHEMATIC.md) agent schematic
- `/docs` deploy path chooser plus the repository markdown (README, CLI, CONFIGURATION, deploy docs, Redis, ...) rendered as pages
- `/changelog` rendered from `CHANGELOG.md`
- `/community` maintainer, channels and community guides

Docs, the changelog, the version and the NIP list are read from the repository at build time, so the site never drifts from the code.

## Develop

Needs Node 24+.

```bash
cd website/site
pnpm install
pnpm dev # http://localhost:4321
pnpm build # → dist/
pnpm check
```

## Film media

Media comes from the landing film in `../hero-animation` (needs `ffmpeg` for `pnpm run media`):

| Asset | Source |
|-------|--------|
| `hero.mp4`, `hero-poster.webp`, `hero.meta.json` | Landing film — `src/film.ts` (`pnpm run render`, `pnpm run frames`) |
| `planet.webp` | Particle planet — `src/build-assets.ts` (`pnpm run assets`) |

```bash
cd website/hero-animation
pnpm run assets
pnpm run render && pnpm run frames
cd ../site
pnpm run media
```

## Brand mark

Official logo: `src/assets/logo.png`. Used in nav/footer, favicon (`pnpm run` / build runs `scripts/build-brand.mjs`), terminal chrome, and the landing film (`website/hero-animation` reads the same file on `pnpm run assets`).

## Admin console screenshots

Drop them into `src/assets/screens/` (see the README there). Missing ones render a placeholder.
Loading
Loading