diff --git a/bun.lock b/bun.lock
index 7c81fe0..0df0d49 100644
--- a/bun.lock
+++ b/bun.lock
@@ -27,6 +27,7 @@
"@mdx-js/rollup": "^3.1.1",
"@radix-ui/react-checkbox": "^1.3.3",
"@radix-ui/react-slot": "^1.2.4",
+ "@rollup/plugin-yaml": "^4.1.2",
"@tailwindcss/postcss": "^4.2.2",
"@tailwindcss/typography": "^0.5.19",
"@types/node": "^24.10.1",
@@ -47,6 +48,7 @@
"typescript": "^5.9.3",
"vite": "^7.2.6",
"wrangler": "^4.51.0",
+ "yaml": "^2.8.3",
},
},
},
@@ -375,6 +377,8 @@
"@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.47", "", {}, "sha512-8QagwMH3kNCuzD8EWL8R2YPW5e4OrHNSAHRFDdmFqEwEaD/KcNKjVoumo+gP2vW5eKB2UPbM6vTYiGZX0ixLnw=="],
+ "@rollup/plugin-yaml": ["@rollup/plugin-yaml@4.1.2", "", { "dependencies": { "@rollup/pluginutils": "^5.0.1", "js-yaml": "^4.1.0", "tosource": "^2.0.0-alpha.3" }, "peerDependencies": { "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-RpupciIeZMUqhgFE97ba0s98mOFS7CWzN3EJNhJkqSv9XLlWYtwVdtE6cDw6ASOF/sZVFS7kRJXftaqM2Vakdw=="],
+
"@rollup/pluginutils": ["@rollup/pluginutils@5.3.0", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-walker": "^2.0.2", "picomatch": "^4.0.2" }, "peerDependencies": { "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q=="],
"@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.53.3", "", { "os": "android", "cpu": "arm" }, "sha512-mRSi+4cBjrRLoaal2PnqH82Wqyb+d3HsPUN/W+WslCXsZsyHa9ZeQQX/pQsZaVIWDkPcpV6jJ+3KLbTbgnwv8w=="],
@@ -605,6 +609,8 @@
"acorn-walk": ["acorn-walk@8.3.2", "", {}, "sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A=="],
+ "argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="],
+
"aria-hidden": ["aria-hidden@1.2.6", "", { "dependencies": { "tslib": "^2.0.0" } }, "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA=="],
"astring": ["astring@1.9.0", "", { "bin": { "astring": "bin/astring" } }, "sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg=="],
@@ -771,6 +777,8 @@
"js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
+ "js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="],
+
"jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="],
"json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="],
@@ -1045,6 +1053,8 @@
"tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="],
+ "tosource": ["tosource@2.0.0-alpha.3", "", {}, "sha512-KAB2lrSS48y91MzFPFuDg4hLbvDiyTjOVgaK7Erw+5AmZXNq4sFRVn8r6yxSLuNs15PaokrDRpS61ERY9uZOug=="],
+
"trim-lines": ["trim-lines@3.0.1", "", {}, "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg=="],
"trough": ["trough@2.2.0", "", {}, "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw=="],
@@ -1099,6 +1109,8 @@
"yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="],
+ "yaml": ["yaml@2.8.3", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg=="],
+
"youch": ["youch@4.1.0-beta.10", "", { "dependencies": { "@poppinss/colors": "^4.1.5", "@poppinss/dumper": "^0.6.4", "@speed-highlight/core": "^1.2.7", "cookie": "^1.0.2", "youch-core": "^0.3.3" } }, "sha512-rLfVLB4FgQneDr0dv1oddCVZmKjcJ6yX6mS4pU82Mq/Dt9a3cLZQ62pDBL4AUO+uVrCvtWz3ZFUL2HFAFJ/BXQ=="],
"youch-core": ["youch-core@0.3.3", "", { "dependencies": { "@poppinss/exception": "^1.2.2", "error-stack-parser-es": "^1.0.5" } }, "sha512-ho7XuGjLaJ2hWHoK8yFnsUGy2Y5uDpqSTq1FkHLK4/oqKtyUU1AFbOOxY4IpC9f0fTLjwYbslUz0Po5BpD1wrA=="],
diff --git a/components/FeaturedProjects.tsx b/components/FeaturedProjects.tsx
new file mode 100644
index 0000000..55175e0
--- /dev/null
+++ b/components/FeaturedProjects.tsx
@@ -0,0 +1,68 @@
+import { getFeaturedProjects } from "@/src/lib/featuredProjects"
+import type { FeaturedProject } from "@/src/types/featured"
+import { Check, Github } from "lucide-react"
+import { useState } from "react"
+
+function FeaturedAvatar({ src, title }: { src: string; title: string }) {
+ const [ok, setOk] = useState(true)
+ if (!ok) {
+ return (
+
+
+
+ )
+ }
+ return (
+
setOk(false)}
+ title={title}
+ />
+ )
+}
+
+function FeaturedTile({ project, onOpenRepoUrl }: { project: FeaturedProject; onOpenRepoUrl: (url: string) => void }) {
+ return (
+
+ )
+}
+
+export function FeaturedProjects({ onOpenRepoUrl }: { onOpenRepoUrl: (url: string) => void }) {
+ const projects = getFeaturedProjects()
+ if (!projects.length) return null
+
+ return (
+
+
Featured projects
+
+ {projects.map(p => (
+
+ ))}
+
+
+ )
+}
diff --git a/package.json b/package.json
index b477df1..cb6e3fa 100644
--- a/package.json
+++ b/package.json
@@ -12,6 +12,7 @@
"scripts": {
"generate:versions": "node scripts/generate-versions.js && biome format src/constants/versions.ts --write",
"generate:architecture": "node scripts/generate-architecture-hierarchy.js",
+ "sync:featured": "bun scripts/sync-featured-logos.ts",
"dev": "vite",
"build": "vite build",
"preview": "vite preview",
@@ -44,6 +45,7 @@
"@mdx-js/rollup": "^3.1.1",
"@radix-ui/react-checkbox": "^1.3.3",
"@radix-ui/react-slot": "^1.2.4",
+ "@rollup/plugin-yaml": "^4.1.2",
"@tailwindcss/postcss": "^4.2.2",
"@tailwindcss/typography": "^0.5.19",
"@types/node": "^24.10.1",
@@ -63,7 +65,8 @@
"tailwindcss-animate": "^1.0.7",
"typescript": "^5.9.3",
"vite": "^7.2.6",
- "wrangler": "^4.51.0"
+ "wrangler": "^4.51.0",
+ "yaml": "^2.8.3"
},
"type": "module"
}
diff --git a/projects.yaml b/projects.yaml
new file mode 100644
index 0000000..71d8b44
--- /dev/null
+++ b/projects.yaml
@@ -0,0 +1,21 @@
+projects:
+ - title: Lotato
+ subtitle: 100% on-device Potato Mesh ingestor for MeshCore.
+ url: https://github.com/MeshEnvy/MeshCore-lotato
+ highlighted: false
+ logo: https://avatars.githubusercontent.com/u/237130814?v=4&s=128
+ - title: Mesh64
+ subtitle: A 64-hop Meshtastic prototype by Baymesh.
+ url: https://github.com/RCGV1/firmware-Fork/tree/baymesh-refactor
+ highlighted: true
+ logo: https://avatars.githubusercontent.com/u/119711889?v=4&s=128
+ - title: MeshCore
+ subtitle: Open mesh stack and device firmware for LoRa-class radios—companion nodes, repeaters, and app-friendly transports.
+ url: https://github.com/meshcore-dev/MeshCore
+ highlighted: false
+ logo: https://avatars.githubusercontent.com/u/210668307?v=4&s=128
+ - title: Meshtastic
+ subtitle: Official open-source device firmware—encrypted text, position, and telemetry over a decentralized LoRa mesh.
+ url: https://github.com/meshtastic/firmware
+ highlighted: false
+ logo: https://avatars.githubusercontent.com/u/61627050?v=4&s=128
diff --git a/scripts/sync-featured-logos.ts b/scripts/sync-featured-logos.ts
new file mode 100644
index 0000000..6dbc8b3
--- /dev/null
+++ b/scripts/sync-featured-logos.ts
@@ -0,0 +1,45 @@
+import { readFileSync, writeFileSync } from "node:fs"
+import path from "node:path"
+import { fileURLToPath } from "node:url"
+import { parse, stringify } from "yaml"
+import { parseGithubUrl } from "../src/lib/parseGithubUrl"
+
+const __dirname = path.dirname(fileURLToPath(import.meta.url))
+const root = path.resolve(__dirname, "..")
+const yamlPath = path.join(root, "projects.yaml")
+
+type Row = {
+ title: string
+ url: string
+ logo?: string
+ subtitle?: string
+ highlighted?: boolean
+}
+
+const text = readFileSync(yamlPath, "utf8")
+const doc = parse(text) as { projects: Row[] }
+
+if (!doc?.projects || !Array.isArray(doc.projects)) {
+ console.error("projects.yaml: expected top-level `projects` array")
+ process.exit(1)
+}
+
+for (const p of doc.projects) {
+ const parsed = parseGithubUrl(p.url)
+ if (!parsed) {
+ console.error("Invalid url:", p.url)
+ process.exit(1)
+ }
+ const res = await fetch(`https://api.github.com/repos/${parsed.owner}/${parsed.repo}`)
+ if (!res.ok) {
+ console.error(`${parsed.owner}/${parsed.repo}:`, res.status, await res.text())
+ process.exit(1)
+ }
+ const json = (await res.json()) as { owner: { avatar_url: string } }
+ const base = json.owner.avatar_url
+ const sep = base.includes("?") ? "&" : "?"
+ p.logo = `${base}${sep}s=128`
+}
+
+writeFileSync(yamlPath, stringify(doc, { lineWidth: 120, indent: 2 }) + "\n", "utf8")
+console.log("Wrote logos to", yamlPath)
diff --git a/src/index.css b/src/index.css
index 37bbcf8..73d554f 100644
--- a/src/index.css
+++ b/src/index.css
@@ -75,9 +75,14 @@
}
.prose.prose-invert a {
- color: oklch(0.488 0.243 264.376);
+ color: oklch(0.82 0.12 195);
font-weight: 500;
text-decoration: underline;
+ text-underline-offset: 0.15em;
+}
+
+.prose.prose-invert a:hover {
+ color: oklch(0.88 0.1 195);
}
.prose.prose-invert code {
diff --git a/src/lib/featuredProjects.ts b/src/lib/featuredProjects.ts
new file mode 100644
index 0000000..d97cf5b
--- /dev/null
+++ b/src/lib/featuredProjects.ts
@@ -0,0 +1,8 @@
+import type { FeaturedProjectsFile } from "@/src/types/featured"
+import rawFeatured from "../../projects.yaml"
+
+const data = rawFeatured as FeaturedProjectsFile
+
+export function getFeaturedProjects(): FeaturedProjectsFile["projects"] {
+ return data.projects
+}
diff --git a/src/pages/HomePage.tsx b/src/pages/HomePage.tsx
index a3b374e..1cd1015 100644
--- a/src/pages/HomePage.tsx
+++ b/src/pages/HomePage.tsx
@@ -1,4 +1,5 @@
import logo from "@/assets/logo.png"
+import { FeaturedProjects } from "@/components/FeaturedProjects"
import { Button } from "@/components/ui/button"
import { useState } from "react"
import { useNavigate } from "react-router-dom"
@@ -13,8 +14,10 @@ export default function HomePage() {
const [input, setInput] = useState("")
const [error, setError] = useState(null)
- const go = () => {
- const parsed = parseGithubUrl(input)
+ const openRepoUrl = (raw: string) => {
+ const trimmed = raw.trim()
+ setInput(trimmed)
+ const parsed = parseGithubUrl(trimmed)
if (!parsed) {
setError("Paste a GitHub URL like https://github.com/owner/repo or owner/repo")
return
@@ -30,8 +33,10 @@ export default function HomePage() {
}
}
+ const go = () => openRepoUrl(input)
+
return (
-
+
@@ -49,6 +54,8 @@ export default function HomePage() {
+
+
{owner}
-
+
{repo}
{!ghAboutHomepage ? (
diff --git a/src/types/featured.ts b/src/types/featured.ts
new file mode 100644
index 0000000..3efbaa9
--- /dev/null
+++ b/src/types/featured.ts
@@ -0,0 +1,11 @@
+export type FeaturedProject = {
+ title: string
+ subtitle?: string
+ url: string
+ highlighted?: boolean
+ logo: string
+}
+
+export type FeaturedProjectsFile = {
+ projects: FeaturedProject[]
+}
diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts
index b5b4546..26dcfa8 100644
--- a/src/vite-env.d.ts
+++ b/src/vite-env.d.ts
@@ -1,6 +1,11 @@
///
-declare module '*.md?raw' {
+declare module "*.md?raw" {
const src: string
export default src
}
+
+declare module "*.yaml" {
+ const data: unknown
+ export default data
+}
diff --git a/vendor/meshtastic-web b/vendor/meshtastic-web
new file mode 160000
index 0000000..6535c96
--- /dev/null
+++ b/vendor/meshtastic-web
@@ -0,0 +1 @@
+Subproject commit 6535c96e8116f4d3006dca18b9e3b57d0e42a51e
diff --git a/vite.config.ts b/vite.config.ts
index 0fb7dc3..a8e375b 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -1,10 +1,11 @@
+import yaml from "@rollup/plugin-yaml"
import react from "@vitejs/plugin-react"
import path from "node:path"
import { defineConfig } from "vite"
/** Tailwind runs via PostCSS (`postcss.config.mjs`), not `@tailwindcss/vite` — the Vite plugin was stalling builds (no stdout) on large workspaces. */
export default defineConfig({
- plugins: [react()],
+ plugins: [react(), yaml()],
logLevel: "info",
resolve: {
alias: {