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
1 change: 1 addition & 0 deletions public/home/use-case/conferencing.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions public/home/use-case/contribution.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions public/home/use-case/distribution.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions public/home/use-case/robotics.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
File renamed without changes
10 changes: 10 additions & 0 deletions public/layout/underline.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
10 changes: 1 addition & 9 deletions src/components/author.astro
Original file line number Diff line number Diff line change
@@ -1,14 +1,6 @@
---
// The sign-off at the bottom of every post: avatar plus wherever you can find me.
// The icons in public/social/ are placeholders waiting on hand-drawn versions,
// same as the ones in public/layout/.
const socials = [
{ name: "Email", icon: "email", href: "mailto:me@kixel.me" },
{ name: "X", icon: "x", href: "https://x.com/kixelated" },
{ name: "Bluesky", icon: "bluesky", href: "https://bsky.app/profile/kixel.me" },
{ name: "LinkedIn", icon: "linkedin", href: "https://www.linkedin.com/in/luke-curley-635a457/" },
{ name: "Discord", icon: "discord", href: "https://discord.moq.dev" },
];
import { socials } from "@/socials";
---

<aside class="not-prose mt-12 flex items-center gap-4">
Expand Down
30 changes: 20 additions & 10 deletions src/components/feature.astro
Original file line number Diff line number Diff line change
Expand Up @@ -6,29 +6,39 @@ type Feature =
| "scale"
| "native"
| "discover"
| "efficient";
| "efficient"
| "distribution"
| "contribution"
| "robotics"
| "conferencing";

interface Props {
icon: Feature;
}

const { icon } = Astro.props;

// These are Kixel's original hand-drawn icons from moq.pro.
// The first set are Kixel's original hand-drawn icons from moq.pro. The
// use-case icons in public/home/use-case/ are placeholders waiting on
// hand-drawn versions, same as public/social/.
const artwork: Record<Feature, string> = {
source: "box",
web: "globe",
realtime: "rocket",
scale: "stonk",
native: "puzzle",
discover: "link",
efficient: "battery",
source: "https://moq.pro/icons/box.svg",
web: "https://moq.pro/icons/globe.svg",
realtime: "https://moq.pro/icons/rocket.svg",
scale: "https://moq.pro/icons/stonk.svg",
native: "https://moq.pro/icons/puzzle.svg",
discover: "https://moq.pro/icons/link.svg",
efficient: "https://moq.pro/icons/battery.svg",
distribution: "/home/use-case/distribution.svg",
contribution: "/home/use-case/contribution.svg",
robotics: "/home/use-case/robotics.svg",
conferencing: "/home/use-case/conferencing.svg",
};
---

<li class="flex items-start gap-3">
<span class="mt-0.5 flex h-9 w-11 shrink-0 items-center justify-center" aria-hidden="true">
<img src={`https://moq.pro/icons/${artwork[icon]}.svg`} class="h-9 w-11 object-contain" alt="" />
<img src={artwork[icon]} class="h-9 w-11 object-contain" alt="" />
</span>
<span><slot /></span>
</li>
22 changes: 22 additions & 0 deletions src/components/nav-link.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
---
// One entry in the sidebar: a hand-drawn icon that wiggles on hover, the
// matching hand-drawn word, and optionally a line of small print underneath.
// Both images live in public/layout/ as <icon>-icon.svg and <icon>-word.svg.
interface Props {
href: string;
icon: string;
word: string;
}

const { href, icon, word } = Astro.props;
---

<a href={href} class="group flex items-center gap-2 w-32">
<span class="flex w-9 justify-center shrink-0">
<img src={`/layout/${icon}-icon.svg`} class="h-9 transition-transform group-hover:-rotate-6" alt="" />
</span>
<span class="flex flex-col items-start">
<img src={`/layout/${icon}-word.svg`} class="h-9 w-auto" alt={word} />
<slot />
</span>
</a>
56 changes: 20 additions & 36 deletions src/layouts/global.astro
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import "./global.css";
// Imported after global.css so the theme's .hljs-* color rules land later in
// the cascade and beat the prose plugin's .markdown :where(pre code) { color: inherit }.
import "highlight.js/styles/atom-one-dark.css";
import NavLink from "@/components/nav-link.astro";

// NOTE: This is magically used as the type for Astro.props
interface Props {
Expand Down Expand Up @@ -76,42 +77,19 @@ const proUrl = import.meta.env.MODE === "staging" ? "https://moq.wtf" : "https:/
</a>
</div>
<div class="flex md:flex-col flex-row flex-wrap items-center justify-center gap-4 md:w-32 w-1/2">
<a href="/demo" class="group flex items-center gap-2 w-32">
<span class="flex w-9 justify-center shrink-0">
<img src="/layout/demo-icon.svg" class="h-9 transition-transform group-hover:-rotate-6" alt="" />
</span>
<img src="/layout/demo-word.svg" class="h-9 w-auto" alt="Demo" />
</a>
<a href="/blog" class="group flex items-center gap-2 w-32">
<span class="flex w-9 justify-center shrink-0">
<img src="/layout/blog-icon.svg" class="h-9 transition-transform group-hover:-rotate-6" alt="" />
</span>
<img src="/layout/blog-word.svg" class="h-9 w-auto" alt="Blog" />
</a>
<a href="https://doc.moq.dev" class="group flex items-center gap-2 w-32">
<span class="flex w-9 justify-center shrink-0">
<img src="/layout/docs-icon.svg" class="h-9 transition-transform group-hover:-rotate-6" alt="" />
</span>
<img src="/layout/docs-word.svg" class="h-9 w-auto" alt="Docs" />
</a>
<a href="https://github.com/moq-dev/moq" class="group flex items-center gap-2 w-32">
<span class="flex w-9 justify-center shrink-0">
<img src="/layout/github-icon.svg" class="h-9 transition-transform group-hover:-rotate-6" alt="" />
</span>
<img src="/layout/github-word.svg" class="h-9 w-auto" alt="GitHub" />
</a>
<a href="https://discord.moq.dev" class="group flex items-center gap-2 w-32">
<span class="flex w-9 justify-center shrink-0">
<img src="/layout/discord-icon.svg" class="h-9 transition-transform group-hover:-rotate-6" alt="" />
</span>
<img src="/layout/discord-word.svg" class="h-9 w-auto" alt="Discord" />
</a>
<a href={proUrl} class="group flex items-center gap-2 w-32">
<span class="flex w-9 justify-center shrink-0">
<img src="/layout/pro-icon.svg" class="h-9 transition-transform group-hover:-rotate-6" alt="" />
</span>
<img src="/layout/pro.svg" class="h-9 w-auto" alt="Pro" />
</a>
<NavLink href="/demo" icon="demo" word="Demo" />
<NavLink href="/blog" icon="blog" word="Blog" />
<NavLink href="https://doc.moq.dev" icon="docs" word="Docs" />
<NavLink href={proUrl} icon="pro" word="Pro" />

<img src="/layout/underline.svg" class="hidden md:block w-32" alt="" />

<NavLink href="https://github.com/moq-dev/moq" icon="github" word="GitHub">
<span data-stat="stars" class="text-xs font-bold text-green-500 -mt-1 whitespace-nowrap"></span>
</NavLink>
<NavLink href="https://discord.moq.dev" icon="discord" word="Discord">
<span data-stat="chatters" class="text-xs font-bold text-green-500 -mt-1 whitespace-nowrap"></span>
</NavLink>
</div>
</nav>
<article class="markdown p-4 flex-1 min-w-0">
Expand All @@ -127,3 +105,9 @@ const proUrl = import.meta.env.MODE === "staging" ? "https://moq.wtf" : "https:/
</div>
</body>
</html>

<script>
import { renderStats } from "@/stats";

renderStats();
</script>
11 changes: 7 additions & 4 deletions src/pages/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,13 @@ Supports any **latency**, any **scale**, and any **content** (not just media!).
</ul>

### Use Cases
- [Distribution](/blog/replacing-hls-dash): Scalable and on-demand, plus [HLS compatibility](https://github.com/moq-dev/moq/tree/main/rs/moq-hls). Think HTTP but live.
- **Contribution**: Dynamic origins and failover, plus [TS/SRT compatibility](https://github.com/moq-dev/moq/tree/main/rs/moq-srt).
- [Robotics](/blog/on-a-boat): Real-time teleoperation with custom telemetry, plus [WebRTC compatibility](https://github.com/moq-dev/moq/tree/main/rs/moq-rtc).
- [Conferencing](/blog/first-app): Web support and dynamic peer discovery. [P2P](https://www.iroh.computer/) for native apps.

<ul class="list-none pl-0">
<Feature icon="distribution">**[Distribution](/blog/replacing-hls-dash)**: Scalable and on-demand, plus [HLS compatibility](https://github.com/moq-dev/moq/tree/main/rs/moq-hls). Think HTTP but live.</Feature>
<Feature icon="contribution">**Contribution**: Dynamic origins and failover, plus [TS/SRT compatibility](https://github.com/moq-dev/moq/tree/main/rs/moq-srt).</Feature>
<Feature icon="robotics">**[Robotics](/blog/on-a-boat)**: Real-time teleoperation with custom telemetry, plus [WebRTC compatibility](https://github.com/moq-dev/moq/tree/main/rs/moq-rtc).</Feature>
<Feature icon="conferencing">**[Conferencing](/blog/first-app)**: Web support and dynamic peer discovery. [P2P](https://www.iroh.computer/) for native apps.</Feature>
</ul>

Enough pondering, <a href="/demo">try the demos already</a>. There's even a gameboy emulator. Don't pretend like you're not excited.

Expand Down
10 changes: 10 additions & 0 deletions src/socials.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// Wherever you can find me. Rendered in the <Author /> sign-off at the bottom
// of each post (and the homepage). The icons in public/social/ are placeholders
// waiting on hand-drawn versions.
export const socials = [
{ name: "Email", icon: "email", href: "mailto:me@kixel.me" },
{ name: "X", icon: "x", href: "https://x.com/kixelated" },
{ name: "Bluesky", icon: "bluesky", href: "https://bsky.app/profile/kixel.me" },
{ name: "LinkedIn", icon: "linkedin", href: "https://www.linkedin.com/in/luke-curley-635a457/" },
{ name: "Discord", icon: "discord", href: "https://discord.moq.dev" },
];
87 changes: 87 additions & 0 deletions src/stats.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
// Live community numbers for the nav: GitHub stars and Discord members.
//
// Fetched in the browser rather than at build time because deploys are manual
// and infrequent, so a baked-in count would be weeks stale. Both APIs allow
// anonymous CORS requests; GitHub's limit is 60/hour per IP, which is why the
// result is cached in localStorage for an hour instead of refetched per page.

const GITHUB = "https://api.github.com/repos/moq-dev/moq";
const DISCORD = "https://discord.com/api/v10/invites/FCYF3p99mr?with_counts=true";

const CACHE_KEY = "moq.stats";
const CACHE_TTL = 60 * 60 * 1000;

interface Stats {
stars?: number;
chatters?: number;
}

interface Cached extends Stats {
at: number;
}

function asCount(value: unknown): number | undefined {
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
}

function readCache(): Stats | undefined {
try {
const raw = localStorage.getItem(CACHE_KEY);
if (!raw) return;
const cached = JSON.parse(raw) as Cached;
Comment thread
kixelated marked this conversation as resolved.
if (typeof cached.at !== "number" || Date.now() - cached.at > CACHE_TTL) return;
const stars = asCount(cached.stars);
const chatters = asCount(cached.chatters);
if (stars === undefined && chatters === undefined) return;
return { stars, chatters };
} catch {
return;
}
}

function writeCache(stats: Stats) {
try {
localStorage.setItem(CACHE_KEY, JSON.stringify({ ...stats, at: Date.now() } satisfies Cached));
} catch {
// Private mode, quota, etc. Not worth surfacing.
}
}

async function fetchNumber(url: string, key: string): Promise<number | undefined> {
try {
// Discord echoes the requesting origin in Access-Control-Allow-Origin but
// marks the response cacheable without `Vary: Origin`, so a response cached
// for moq.dev fails the CORS check on doc.moq.dev. Skip the HTTP cache;
// localStorage above is the cache.
const res = await fetch(url, { cache: "no-store" });
if (!res.ok) return;
const json = await res.json();
const value = json[key];
return typeof value === "number" ? value : undefined;
} catch {
return;
}
}

async function fetchStats(): Promise<Stats> {
const [stars, chatters] = await Promise.all([
fetchNumber(GITHUB, "stargazers_count"),
fetchNumber(DISCORD, "approximate_member_count"),
]);
return { stars, chatters };
}

// Fills every element with a `data-stat="stars"` / `data-stat="chatters"`
// attribute, e.g. "1,515 stars". Elements stay empty if a fetch fails; the
// links around them still work.
export async function renderStats() {
const cached = readCache();
const stats = cached ?? (await fetchStats());
if (!cached && (stats.stars !== undefined || stats.chatters !== undefined)) writeCache(stats);

for (const el of document.querySelectorAll<HTMLElement>("[data-stat]")) {
const key = el.dataset.stat as keyof Stats;
const value = stats[key];
if (typeof value === "number") el.textContent = `${value.toLocaleString()} ${key}`;
}
}
Loading