Add $VOICEBOX token page, blog, and cloud/pricing pages

- /token: dedicated page with on-chain transparency (liquidity lock +
  buyback/burns), holder utility, official-token clarity, and FAQ. The
  landing now shows a teaser linking to it; navbar + footer repointed
  from pump.fun to /token.
- /blog: file-based markdown blog (gray-matter + marked) with per-post
  Open Graph images; first post "Why Voicebox has a token".
- /cloud: end-to-end encrypted backup & sync product page.
- /pricing: Local / Cloud / Studio tiers with a monthly/annual toggle;
  $VOICEBOX holders get Cloud free.
- Add Testimonials section to the landing.
- Navbar: remove API/Download, add Pricing/Blog, fix center-nav spacing.
- docker-compose: host port 17493->17600 for local dev coexistence.
This commit is contained in:
James Pine
2026-06-27 17:34:53 -07:00
parent c6a59f4477
commit 7d9a384ee4
20 changed files with 1675 additions and 25 deletions
+39 -6
View File
@@ -17,7 +17,7 @@
},
"app": {
"name": "@voicebox/app",
"version": "0.4.2",
"version": "0.5.0",
"dependencies": {
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/sortable": "^10.0.0",
@@ -57,6 +57,7 @@
"react-dom": "^18.3.0",
"react-hook-form": "^7.53.0",
"react-i18next": "^17.0.4",
"react-qr-code": "^2.0.18",
"react-sound-visualizer": "^1.4.0",
"tailwind-merge": "^2.5.4",
"wavesurfer.js": "^7.0.0",
@@ -75,7 +76,7 @@
},
"landing": {
"name": "@voicebox/landing",
"version": "0.4.2",
"version": "0.5.0",
"dependencies": {
"@fontsource/space-grotesk": "^5.2.10",
"@icons-pack/react-simple-icons": "^13.13.0",
@@ -85,7 +86,9 @@
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"framer-motion": "^12.36.0",
"gray-matter": "^4.0.3",
"lucide-react": "^0.316.0",
"marked": "^18.0.5",
"next": "^16.1.3",
"postcss": "^8.4.33",
"react": "^18.2.0",
@@ -104,7 +107,7 @@
},
"tauri": {
"name": "@voicebox/tauri",
"version": "0.4.2",
"version": "0.5.0",
"dependencies": {
"@tauri-apps/api": "^2.0.0",
"@tauri-apps/plugin-dialog": "^2.0.0",
@@ -127,7 +130,7 @@
},
"web": {
"name": "@voicebox/web",
"version": "0.4.2",
"version": "0.5.0",
"dependencies": {
"@tanstack/react-query": "^5.0.0",
"react": "^18.3.0",
@@ -663,7 +666,7 @@
"arg": ["[email protected]", "", {}, "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg=="],
"argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="],
"argparse": ["argparse@1.0.10", "", { "dependencies": { "sprintf-js": "~1.0.2" } }, "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg=="],
"aria-hidden": ["[email protected]", "", { "dependencies": { "tslib": "^2.0.0" } }, "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA=="],
@@ -759,6 +762,8 @@
"espree": ["[email protected]", "", { "dependencies": { "acorn": "^8.9.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^3.4.1" } }, "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ=="],
"esprima": ["[email protected]", "", { "bin": { "esparse": "./bin/esparse.js", "esvalidate": "./bin/esvalidate.js" } }, "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="],
"esquery": ["[email protected]", "", { "dependencies": { "estraverse": "^5.1.0" } }, "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g=="],
"esrecurse": ["[email protected]", "", { "dependencies": { "estraverse": "^5.2.0" } }, "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag=="],
@@ -767,6 +772,8 @@
"esutils": ["[email protected]", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="],
"extend-shallow": ["[email protected]", "", { "dependencies": { "is-extendable": "^0.1.0" } }, "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug=="],
"fast-deep-equal": ["[email protected]", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
"fast-glob": ["[email protected]", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="],
@@ -815,6 +822,8 @@
"graphemer": ["[email protected]", "", {}, "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag=="],
"gray-matter": ["[email protected]", "", { "dependencies": { "js-yaml": "^3.13.1", "kind-of": "^6.0.2", "section-matter": "^1.0.0", "strip-bom-string": "^1.0.0" } }, "sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q=="],
"has-flag": ["[email protected]", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="],
"hasown": ["[email protected]", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],
@@ -839,6 +848,8 @@
"is-core-module": ["[email protected]", "", { "dependencies": { "hasown": "^2.0.2" } }, "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w=="],
"is-extendable": ["[email protected]", "", {}, "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw=="],
"is-extglob": ["[email protected]", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="],
"is-glob": ["[email protected]", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="],
@@ -855,7 +866,7 @@
"js-tokens": ["[email protected]", "", {}, "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=="],
"js-yaml": ["js-yaml@3.15.0", "", { "dependencies": { "argparse": "^1.0.7", "esprima": "^4.0.0" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-ttBQIIQPDeLjpPOohtUdXuXUVoA2uIB6fEH9HyJ7234s5mBJ5wTx20njxplLZQgLaOfpmPQA7X2t5AX6tIPbog=="],
"jsesc": ["[email protected]", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="],
@@ -869,6 +880,8 @@
"keyv": ["[email protected]", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="],
"kind-of": ["[email protected]", "", {}, "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw=="],
"levn": ["[email protected]", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="],
"lightningcss": ["[email protected]", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.30.2", "lightningcss-darwin-arm64": "1.30.2", "lightningcss-darwin-x64": "1.30.2", "lightningcss-freebsd-x64": "1.30.2", "lightningcss-linux-arm-gnueabihf": "1.30.2", "lightningcss-linux-arm64-gnu": "1.30.2", "lightningcss-linux-arm64-musl": "1.30.2", "lightningcss-linux-x64-gnu": "1.30.2", "lightningcss-linux-x64-musl": "1.30.2", "lightningcss-win32-arm64-msvc": "1.30.2", "lightningcss-win32-x64-msvc": "1.30.2" } }, "sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ=="],
@@ -913,6 +926,8 @@
"magic-string": ["[email protected]", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
"marked": ["[email protected]", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-S6GcvALHg6K4ohtu4E7x0a1AqhAjp6cV8KhLSyN9qVapnzJkusVBxZRcIU9AeYsbe6P1hKDusSbEOzGyyuce6w=="],
"merge2": ["[email protected]", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="],
"micromatch": ["[email protected]", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="],
@@ -991,6 +1006,8 @@
"punycode": ["[email protected]", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
"qr.js": ["[email protected]", "", {}, "sha512-c4iYnWb+k2E+vYpRimHqSu575b1/wKl4XFeJGpFmrJQz5I88v9aY2czh7s0w36srfCM1sXgC/xpoJz5dJfq+OQ=="],
"queue-microtask": ["[email protected]", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="],
"react": ["[email protected]", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ=="],
@@ -1005,6 +1022,8 @@
"react-loaders": ["[email protected]", "", { "dependencies": { "classnames": "^2.2.3" }, "peerDependencies": { "prop-types": ">=15.6.0", "react": ">=15" } }, "sha512-4igMNqs9Fb3d4Z+0UHIGQNJsw/37gX0nUO8QxupnEKRn1dtyYC1LGwk5GuaoDciMQCQc/MmPwb4Fn6ZfdoX1FQ=="],
"react-qr-code": ["[email protected]", "", { "dependencies": { "prop-types": "^15.8.1", "qr.js": "0.0.0" }, "peerDependencies": { "react": "*" } }, "sha512-v1Jqz7urLMhkO6jkgJuBYhnqvXagzceg3qJUWayuCK/c6LTIonpWbwxR1f1APGd4xrW/QcQEovNrAojbUz65Tg=="],
"react-refresh": ["[email protected]", "", {}, "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ=="],
"react-remove-scroll": ["[email protected]", "", { "dependencies": { "react-remove-scroll-bar": "^2.3.7", "react-style-singleton": "^2.2.3", "tslib": "^2.1.0", "use-callback-ref": "^1.3.3", "use-sidecar": "^1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q=="],
@@ -1033,6 +1052,8 @@
"scheduler": ["[email protected]", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ=="],
"section-matter": ["[email protected]", "", { "dependencies": { "extend-shallow": "^2.0.1", "kind-of": "^6.0.0" } }, "sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA=="],
"semver": ["[email protected]", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
"seroval": ["[email protected]", "", {}, "sha512-OE4cvmJ1uSPrKorFIH9/w/Qwuvi/IMcGbv5RKgcJ/zjA/IohDLU6SVaxFN9FwajbP7nsX0dQqMDes1whk3y+yw=="],
@@ -1051,8 +1072,12 @@
"source-map-js": ["[email protected]", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
"sprintf-js": ["[email protected]", "", {}, "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g=="],
"strip-ansi": ["[email protected]", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
"strip-bom-string": ["[email protected]", "", {}, "sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g=="],
"strip-json-comments": ["[email protected]", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="],
"styled-jsx": ["[email protected]", "", { "dependencies": { "client-only": "0.0.1" }, "peerDependencies": { "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" } }, "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA=="],
@@ -1131,6 +1156,8 @@
"zustand": ["[email protected]", "", { "dependencies": { "use-sync-external-store": "^1.2.2" }, "peerDependencies": { "@types/react": ">=16.8", "immer": ">=9.0.6", "react": ">=16.8" }, "optionalPeers": ["@types/react", "immer", "react"] }, "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw=="],
"@eslint/eslintrc/js-yaml": ["[email protected]", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="],
"@radix-ui/react-alert-dialog/@radix-ui/react-slot": ["@radix-ui/[email protected]", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
"@radix-ui/react-avatar/@radix-ui/react-context": ["@radix-ui/[email protected]", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-ieIFACdMpYfMEjF0rEf5KLvfVyIkOz6PDGyNnP+u+4xQ6jny3VCgA4OgXOwNx2aUkxn8zx9fiVcM8CfFYv9Lxw=="],
@@ -1185,6 +1212,8 @@
"chokidar/glob-parent": ["[email protected]", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
"eslint/js-yaml": ["[email protected]", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="],
"fast-glob/glob-parent": ["[email protected]", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
"motion/framer-motion": ["[email protected]", "", { "dependencies": { "motion-dom": "^12.29.0", "motion-utils": "^12.27.2", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-1gEFGXHYV2BD42ZPTFmSU9buehppU+bCuOnHU0AD18DKh9j4DuTx47MvqY5ax+NNWRtK32qIcJf1UxKo1WwjWg=="],
@@ -1195,8 +1224,12 @@
"tinyglobby/picomatch": ["[email protected]", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="],
"@eslint/eslintrc/js-yaml/argparse": ["[email protected]", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="],
"@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["[email protected]", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="],
"eslint/js-yaml/argparse": ["[email protected]", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="],
"motion/framer-motion/motion-dom": ["[email protected]", "", { "dependencies": { "motion-utils": "^12.27.2" } }, "sha512-3eiz9bb32yvY8Q6XNM4AwkSOBPgU//EIKTZwsSWgA9uzbPBhZJeScCVcBuwwYVqhfamewpv7ZNmVKTGp5qnzkA=="],
"motion/framer-motion/motion-utils": ["[email protected]", "", {}, "sha512-B55gcoL85Mcdt2IEStY5EEAsrMSVE2sI14xQ/uAdPL+mfQxhKKFaEag9JmfxedJOR4vZpBGoPeC/Gm13I/4g5Q=="],
+3 -2
View File
@@ -5,8 +5,9 @@ services:
restart: unless-stopped
ports:
# Bind to localhost only for security
- "127.0.0.1:17493:17493"
# Host-side moved to 17600 so the dev/installed Voicebox can keep 17493.
# Container still listens on its native port internally.
- "127.0.0.1:17600:17493"
volumes:
# Bind-mount for generated audio (customize the host path as needed)
+2
View File
@@ -17,7 +17,9 @@
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"framer-motion": "^12.36.0",
"gray-matter": "^4.0.3",
"lucide-react": "^0.316.0",
"marked": "^18.0.5",
"next": "^16.1.3",
"postcss": "^8.4.33",
"react": "^18.2.0",
@@ -0,0 +1,104 @@
import {readFileSync} from "node:fs";
import {join} from "node:path";
import {ImageResponse} from "next/og";
import {formatDate, getPost, loadAllPosts} from "@/lib/blog";
// Per-post Open Graph image, generated with Satori at build time (static export
// of each post route) and served as PNG. Note: this runs in the Satori renderer,
// which only understands inline styles + flexbox and a subset of CSS — no
// Tailwind classes, no `filter: blur()`. Glows are done with radial gradients.
export const size = {width: 1200, height: 630};
export const contentType = "image/png";
export const alt = "Voicebox Blog";
// Pre-build an image for every post route (mirrors the page's static params).
export function generateStaticParams() {
return loadAllPosts().map((post) => ({slug: post.slug}));
}
// 8-bit PNG decodes reliably in Satori; the 1024px logos are 16-bit and don't.
const logo = `data:image/png;base64,${readFileSync(
join(process.cwd(), "public/apple-touch-icon.png"),
).toString("base64")}`;
function titleFontSize(title: string): number {
if (title.length <= 38) return 76;
if (title.length <= 64) return 60;
return 48;
}
export default async function OgImage({
params,
}: {
params: Promise<{slug: string}>;
}) {
const {slug} = await params;
const post = getPost(slug);
const title = post?.title ?? "Voicebox Blog";
const meta = post
? `${post.author} · ${formatDate(post.date)}`
: "Open source voice cloning. Local-first.";
return new ImageResponse(
(
<div
style={{
width: "100%",
height: "100%",
display: "flex",
flexDirection: "column",
justifyContent: "space-between",
padding: 80,
background:
"radial-gradient(ellipse 80% 70% at 30% 30%, hsla(43,60%,50%,0.14) 0%, hsla(43,60%,50%,0.04) 40%, transparent 70%), linear-gradient(180deg, hsl(30,4%,6%) 0%, hsl(30,4%,4%) 100%)",
}}
>
{/* Top: logo + eyebrow */}
<div style={{display: "flex", alignItems: "center", gap: 24}}>
{/* biome-ignore lint/performance/noImgElement: Satori only renders <img> */}
<img src={logo} width={88} height={88} alt="" />
<div
style={{
display: "flex",
fontSize: 26,
letterSpacing: 6,
fontWeight: 600,
textTransform: "uppercase",
color: "hsl(43, 60%, 58%)",
}}
>
Voicebox Blog
</div>
</div>
{/* Title */}
<div
style={{
display: "flex",
fontSize: titleFontSize(title),
lineHeight: 1.1,
fontWeight: 700,
letterSpacing: -1,
color: "hsl(30, 10%, 94%)",
maxWidth: 1000,
}}
>
{title}
</div>
{/* Footer meta */}
<div
style={{
display: "flex",
fontSize: 28,
color: "hsl(30, 5%, 55%)",
}}
>
{meta}
</div>
</div>
),
size,
);
}
+92
View File
@@ -0,0 +1,92 @@
import type {Metadata} from "next";
import Link from "next/link";
import {notFound} from "next/navigation";
import {Footer} from "@/components/Footer";
import {Navbar} from "@/components/Navbar";
import {formatDate, getPost, loadAllPosts} from "@/lib/blog";
export function generateStaticParams() {
return loadAllPosts().map((post) => ({slug: post.slug}));
}
export async function generateMetadata({
params,
}: {
params: Promise<{slug: string}>;
}): Promise<Metadata> {
const {slug} = await params;
const post = getPost(slug);
if (!post) return {title: "Post not found — Voicebox"};
return {
title: `${post.title} — Voicebox`,
description: post.excerpt,
openGraph: {
title: post.title,
description: post.excerpt,
type: "article",
url: `https://voicebox.sh/blog/${post.slug}`,
// og:image / twitter:image come from the colocated opengraph-image.tsx
},
twitter: {
card: "summary_large_image",
title: post.title,
description: post.excerpt,
},
};
}
export default async function BlogPostPage({
params,
}: {
params: Promise<{slug: string}>;
}) {
const {slug} = await params;
const post = getPost(slug);
if (!post) notFound();
return (
<>
<Navbar />
<main className="mx-auto w-full max-w-3xl px-6 pt-32 pb-20">
<Link
href="/blog"
className="font-mono text-sm text-muted-foreground underline-offset-4 transition-colors hover:text-foreground hover:underline"
>
Back to blog
</Link>
<header className="mt-8 border-b border-border pb-10">
{post.tags.length > 0 ? (
<div className="mb-5 flex flex-wrap gap-2">
{post.tags.map((tag) => (
<span
key={tag}
className="rounded-full border border-border/60 bg-card/40 px-2.5 py-0.5 text-[11px] font-medium uppercase tracking-wider text-muted-foreground"
>
{tag}
</span>
))}
</div>
) : null}
<h1 className="text-4xl md:text-5xl font-bold tracking-tighter text-foreground">
{post.title}
</h1>
<p className="mt-5 font-mono text-sm text-muted-foreground">
by <span className="text-foreground">{post.author}</span> ·{" "}
{formatDate(post.date)} · {post.readingMinutes} min read
</p>
</header>
<article
className="blog-prose mt-10"
// Content is authored markdown from this repo, not user input.
// biome-ignore lint/security/noDangerouslySetInnerHtml: trusted local markdown
dangerouslySetInnerHTML={{__html: post.html}}
/>
</main>
<Footer />
</>
);
}
+86
View File
@@ -0,0 +1,86 @@
import type {Metadata} from "next";
import Link from "next/link";
import {Footer} from "@/components/Footer";
import {Navbar} from "@/components/Navbar";
import {formatDate, listPosts} from "@/lib/blog";
export const metadata: Metadata = {
title: "Blog — Voicebox",
description: "Notes from building Voicebox — the open-source AI voice studio.",
openGraph: {
title: "Voicebox Blog",
description: "Notes from building Voicebox — the open-source AI voice studio.",
type: "website",
url: "https://voicebox.sh/blog",
images: [{url: "/og.webp", width: 1200, height: 630}],
},
};
export default function BlogIndexPage() {
const posts = listPosts();
return (
<>
<Navbar />
<main className="mx-auto w-full max-w-3xl px-6 pt-32 pb-20">
<div className="text-[11px] font-semibold uppercase tracking-[0.22em] text-accent mb-4">
Blog
</div>
<h1 className="text-4xl md:text-5xl font-bold tracking-tighter text-foreground">
Notes from building Voicebox.
</h1>
<p className="mt-5 max-w-2xl text-lg text-muted-foreground">
The story behind the project, what's shipping next, and the occasional
look under the hood.
</p>
{posts.length === 0 ? (
<p className="mt-16 border-t border-border pt-10 text-muted-foreground">
Nothing published yet.
</p>
) : (
<ul className="mt-16 border-t border-border">
{posts.map((post) => (
<li key={post.slug}>
<Link
href={`/blog/${post.slug}`}
className="group grid gap-4 border-b border-border py-10 md:grid-cols-[11rem_1fr] md:gap-10"
>
<div className="font-mono text-sm text-muted-foreground md:pt-1.5">
<p>{formatDate(post.date)}</p>
<p className="mt-1">{post.readingMinutes} min read</p>
</div>
<div className="max-w-2xl">
<h2 className="text-2xl md:text-3xl font-semibold tracking-tight text-foreground transition-colors group-hover:text-accent">
{post.title}
</h2>
{post.excerpt ? (
<p className="mt-3 leading-7 text-muted-foreground">
{post.excerpt}
</p>
) : null}
{post.tags.length > 0 ? (
<div className="mt-5 flex flex-wrap gap-2">
{post.tags.map((tag) => (
<span
key={tag}
className="rounded-full border border-border/60 bg-card/40 px-2.5 py-0.5 text-[11px] font-medium uppercase tracking-wider text-muted-foreground"
>
{tag}
</span>
))}
</div>
) : null}
</div>
</Link>
</li>
))}
</ul>
)}
</main>
<Footer />
</>
);
}
+188
View File
@@ -0,0 +1,188 @@
import {ArrowRight, Cloud, KeyRound, Lock, ShieldCheck} from "lucide-react";
import type {Metadata} from "next";
import Link from "next/link";
import {Footer} from "@/components/Footer";
import {Navbar} from "@/components/Navbar";
import {CLOUD_FEATURES, CLOUD_NOTIFY_URL} from "@/lib/pricing";
export const metadata: Metadata = {
title: "Cloud Backup & Sync — Voicebox",
description:
"End-to-end encrypted backup and sync for your Voicebox library. We can't read your data — only your devices can. Optional, local-first, free for $VOICEBOX holders.",
openGraph: {
title: "Voicebox Cloud — encrypted backup & sync",
description:
"End-to-end encrypted backup and sync across desktop and mobile. The server is blind — only your devices can decrypt.",
type: "website",
url: "https://voicebox.sh/cloud",
images: [{url: "/og.webp", width: 1200, height: 630}],
},
};
const STEPS = [
{
icon: Lock,
title: "Encrypted on your device",
body: "Profiles, generations, and captures are encrypted locally with keys only you hold — before anything is uploaded.",
},
{
icon: Cloud,
title: "Stored as opaque blobs",
body: "The server keeps your encrypted objects and a sync feed. It can route and store them, but never decrypt them.",
},
{
icon: KeyRound,
title: "Only your devices decrypt",
body: "Each device unwraps your master key on pairing. A recovery phrase you control lets you restore everything to a new one.",
},
];
export default function CloudPage() {
return (
<>
<Navbar />
{/* ── Hero ─────────────────────────────────────────────────── */}
<section className="relative pt-32 pb-16">
<div className="hero-glow hero-glow-fade pointer-events-none absolute inset-0 -top-32">
<div className="absolute left-1/2 top-0 -translate-x-1/2 w-[900px] h-[500px] rounded-full bg-accent/12 blur-[140px]" />
</div>
<div className="relative mx-auto max-w-4xl px-6 text-center">
<div className="fade-in mb-6 inline-flex items-center gap-2 rounded-full border border-border/60 bg-card/40 px-3 py-1">
<Cloud className="h-3.5 w-3.5 text-accent" />
<span className="text-[11px] font-semibold uppercase tracking-[0.18em] text-muted-foreground">
Voicebox Cloud · coming soon
</span>
</div>
<h1 className="fade-in text-5xl font-bold tracking-tighter leading-[0.95] text-foreground md:text-6xl lg:text-7xl">
Your studio, backed up and in sync.
</h1>
<p className="fade-in mx-auto mt-6 max-w-2xl text-lg text-muted-foreground md:text-xl">
Optional, end-to-end encrypted backup and sync for your entire
Voicebox library. We can't read a byte of it — only your devices
can. Free for{" "}
<Link href="/token" className="text-foreground underline-offset-4 hover:underline">
$VOICEBOX
</Link>{" "}
holders.
</p>
<div className="fade-in mt-10 flex flex-row items-center justify-center gap-3 sm:gap-4">
<Link
href="/pricing"
className="rounded-full bg-accent px-8 py-3.5 text-sm font-semibold uppercase tracking-wider text-white shadow-[0_4px_20px_hsl(43_60%_50%/0.3),inset_0_2px_0_rgba(255,255,255,0.2),inset_0_-2px_0_rgba(0,0,0,0.1)] transition-all hover:bg-accent-faint"
>
See pricing
</Link>
<a
href={CLOUD_NOTIFY_URL}
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-2 rounded-full border border-border/60 bg-card/40 backdrop-blur-sm px-6 py-3 text-sm font-medium text-muted-foreground transition-colors hover:text-foreground hover:border-border"
>
Get notified
<ArrowRight className="h-4 w-4" />
</a>
</div>
</div>
</section>
{/* ── Features ─────────────────────────────────────────────── */}
<section className="border-t border-border py-20">
<div className="mx-auto max-w-5xl px-6">
<div className="grid gap-4 md:grid-cols-3">
{CLOUD_FEATURES.map((f) => (
<div
key={f.title}
className="rounded-xl border border-border bg-card/40 backdrop-blur-sm p-6"
>
<h3 className="text-[15px] font-semibold text-foreground mb-2">
{f.title}
</h3>
<p className="text-sm leading-relaxed text-muted-foreground">
{f.body}
</p>
</div>
))}
</div>
</div>
</section>
{/* ── How it works ─────────────────────────────────────────── */}
<section className="border-t border-border py-20">
<div className="mx-auto max-w-4xl px-6">
<div className="text-center mb-12">
<div className="text-[11px] font-semibold uppercase tracking-[0.22em] text-accent mb-4">
How it works
</div>
<h2 className="text-3xl md:text-4xl font-semibold tracking-tight text-foreground">
Zero-knowledge by design.
</h2>
</div>
<div className="grid gap-4 md:grid-cols-3">
{STEPS.map((step, i) => {
const Icon = step.icon;
return (
<div
key={step.title}
className="rounded-xl border border-border bg-card/40 backdrop-blur-sm p-6"
>
<div className="flex items-center gap-3 mb-3">
<Icon className="h-5 w-5 text-accent" />
<span className="font-mono text-xs text-muted-foreground/60">
0{i + 1}
</span>
</div>
<h3 className="text-[15px] font-semibold text-foreground mb-2">
{step.title}
</h3>
<p className="text-sm leading-relaxed text-muted-foreground">
{step.body}
</p>
</div>
);
})}
</div>
</div>
</section>
{/* ── Trust callout ────────────────────────────────────────── */}
<section className="border-t border-border py-20">
<div className="mx-auto max-w-3xl px-6">
<div className="rounded-2xl border-2 border-accent/40 bg-card/60 backdrop-blur-sm p-8 md:p-10 text-center shadow-[0_8px_40px_hsl(43_60%_50%/0.08)]">
<ShieldCheck className="h-7 w-7 text-accent mx-auto mb-4" />
<h2 className="text-2xl md:text-3xl font-semibold tracking-tight text-foreground mb-3">
We can't see your data. That's the point.
</h2>
<p className="text-muted-foreground leading-relaxed max-w-2xl mx-auto">
Voicebox is local-first and privacy-first. The cloud keeps that
promise: your library is encrypted before it leaves your device,
the server stores only ciphertext, and the keys never leave your
control. Same philosophy as the app just backed up.
</p>
<div className="mt-8 flex flex-row items-center justify-center gap-3">
<Link
href="/pricing"
className="rounded-full bg-accent px-6 py-3 text-sm font-semibold text-white shadow-[0_4px_20px_hsl(43_60%_50%/0.3)] transition-all hover:bg-accent-faint"
>
See pricing
</Link>
<Link
href="/token"
className="rounded-full border border-border/60 bg-card/40 px-6 py-3 text-sm font-medium text-muted-foreground transition-colors hover:text-foreground hover:border-border"
>
Free for holders
</Link>
</div>
</div>
</div>
</section>
<Footer />
</>
);
}
+93
View File
@@ -142,6 +142,99 @@
will-change: transform;
} */
/* Blog post typography (rendered markdown via marked) */
.blog-prose {
color: hsl(var(--muted-foreground));
font-size: 1.0625rem;
line-height: 1.75;
}
.blog-prose > * + * {
margin-top: 1.25em;
}
.blog-prose h2 {
margin-top: 2.25em;
margin-bottom: 0.75em;
font-size: 1.6rem;
font-weight: 600;
letter-spacing: -0.02em;
color: hsl(var(--foreground));
}
.blog-prose h3 {
margin-top: 1.75em;
margin-bottom: 0.5em;
font-size: 1.25rem;
font-weight: 600;
color: hsl(var(--foreground));
}
.blog-prose p,
.blog-prose ul,
.blog-prose ol,
.blog-prose blockquote {
color: hsl(var(--muted-foreground));
}
.blog-prose strong {
color: hsl(var(--foreground));
font-weight: 600;
}
.blog-prose a {
color: hsl(var(--foreground));
text-decoration: underline;
text-underline-offset: 3px;
text-decoration-color: hsl(var(--accent) / 0.5);
transition: color 0.15s;
}
.blog-prose a:hover {
color: hsl(var(--accent));
}
.blog-prose ul,
.blog-prose ol {
padding-left: 1.4em;
}
.blog-prose ul {
list-style: disc;
}
.blog-prose ol {
list-style: decimal;
}
.blog-prose li + li {
margin-top: 0.4em;
}
.blog-prose blockquote {
border-left: 2px solid hsl(var(--accent) / 0.5);
padding-left: 1.25em;
font-style: italic;
}
.blog-prose code {
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
font-size: 0.875em;
background: hsl(var(--muted));
color: hsl(var(--foreground));
padding: 0.15em 0.4em;
border-radius: 0.3rem;
}
.blog-prose pre {
background: hsl(var(--card));
border: 1px solid hsl(var(--border));
border-radius: 0.75rem;
padding: 1.1em 1.25em;
overflow-x: auto;
}
.blog-prose pre code {
background: transparent;
padding: 0;
font-size: 0.875rem;
color: hsl(var(--foreground));
}
.blog-prose hr {
border: none;
border-top: 1px solid hsl(var(--border));
margin: 2.5em 0;
}
.blog-prose img {
border-radius: 0.75rem;
border: 1px solid hsl(var(--border));
}
/* Scrollbar hiding */
::-webkit-scrollbar {
display: none;
+8 -4
View File
@@ -12,7 +12,8 @@ import {Navbar} from "@/components/Navbar";
import {Personalities} from "@/components/Personalities";
import {AppleIcon, LinuxIcon, WindowsIcon} from "@/components/PlatformIcons";
import {SupportedModels} from "@/components/SupportedModels";
import {TokenSection} from "@/components/TokenSection";
import {Testimonials} from "@/components/Testimonials";
import {TokenTeaser} from "@/components/TokenTeaser";
import {TutorialsSection} from "@/components/TutorialsSection";
import {VoiceCreator} from "@/components/VoiceCreator";
import {GITHUB_REPO} from "@/lib/constants";
@@ -131,9 +132,6 @@ export default function Home() {
</div>
</section>
{/* ── $VOICEBOX token ──────────────────────────────────────── */}
<TokenSection />
{/* ── Features ─────────────────────────────────────────────── */}
<Features />
@@ -158,6 +156,9 @@ export default function Home() {
{/* ── Supported models ─────────────────────────────────────── */}
<SupportedModels />
{/* ── Testimonials ─────────────────────────────────────────── */}
<Testimonials />
{/* ── Download Section ─────────────────────────────────────── */}
<section id="download" className="border-t border-border py-24">
<div className="mx-auto max-w-4xl px-6">
@@ -241,6 +242,9 @@ export default function Home() {
</div>
</section>
{/* ── $VOICEBOX token (teaser → /token) ─────────────────────── */}
<TokenTeaser />
{/* ── Footer ───────────────────────────────────────────────── */}
<Footer />
</>
+131
View File
@@ -0,0 +1,131 @@
import {Coins} from "lucide-react";
import type {Metadata} from "next";
import Link from "next/link";
import {Footer} from "@/components/Footer";
import {Navbar} from "@/components/Navbar";
import {PricingTiers} from "@/components/PricingTiers";
export const metadata: Metadata = {
title: "Pricing — Voicebox",
description:
"Voicebox is free and open source forever. Optional, end-to-end encrypted cloud backup & sync — free for $VOICEBOX holders.",
openGraph: {
title: "Voicebox Pricing",
description:
"The app is free forever. Cloud backup & sync is an optional add-on — free for $VOICEBOX holders.",
type: "website",
url: "https://voicebox.sh/pricing",
images: [{url: "/og.webp", width: 1200, height: 630}],
},
};
const FAQ = [
{
q: "Is the app really free?",
a: "Yes — Voicebox is free and open source, forever. Cloning, dictation, every TTS engine, MCP, personalities: all of it runs locally with no account. The paid plans only add optional cloud backup & sync.",
},
{
q: "What's encrypted in the cloud?",
a: "Everything. Your profiles, generations, and captures are end-to-end encrypted on your device before upload. The server stores only ciphertext and can never read your data.",
},
{
q: "Do $VOICEBOX holders really get Cloud free?",
a: "Yes. Holding the token unlocks the Cloud tier at no cost. The app itself is free regardless — the token is an optional way to support the project.",
},
{
q: "What counts toward storage?",
a: "Your encrypted objects — generated audio, the original audio kept with each capture, and profile data. Plans differ mainly on storage, device count, and version-history length.",
},
{
q: "Can I cancel anytime?",
a: "Yes. Cloud is a subscription you can cancel whenever you like; your local library always stays on your machine and keeps working.",
},
];
export default function PricingPage() {
return (
<>
<Navbar />
{/* ── Hero ─────────────────────────────────────────────────── */}
<section className="relative pt-32 pb-12">
<div className="hero-glow hero-glow-fade pointer-events-none absolute inset-0 -top-32">
<div className="absolute left-1/2 top-0 -translate-x-1/2 w-[900px] h-[460px] rounded-full bg-accent/12 blur-[140px]" />
</div>
<div className="relative mx-auto max-w-4xl px-6 text-center">
<div className="fade-in mb-4 text-[11px] font-semibold uppercase tracking-[0.22em] text-accent">
Pricing
</div>
<h1 className="fade-in text-5xl font-bold tracking-tighter text-foreground md:text-6xl">
The app is free. Forever.
</h1>
<p className="fade-in mx-auto mt-6 max-w-2xl text-lg text-muted-foreground">
Everything that makes Voicebox great runs locally at no cost. Pay
only if you want optional, encrypted cloud backup & sync and
holders get that free.
</p>
</div>
</section>
{/* ── Tiers (with monthly/annual toggle) ───────────────────── */}
<section className="pb-8">
<PricingTiers />
</section>
{/* ── Holder callout ───────────────────────────────────────── */}
<section className="py-12">
<div className="mx-auto max-w-3xl px-6">
<Link
href="/token"
className="group flex flex-col items-center gap-3 rounded-2xl border border-accent/30 bg-card/40 backdrop-blur-sm px-6 py-8 text-center transition-colors hover:border-accent/50"
>
<Coins className="h-6 w-6 text-accent" />
<h2 className="text-xl md:text-2xl font-semibold tracking-tight text-foreground">
Hold $VOICEBOX, get Cloud free.
</h2>
<p className="max-w-xl text-sm text-muted-foreground">
The token is an optional way to back the project and holders get
the Cloud tier at no cost. Learn how it works and verify everything
on-chain.
</p>
<span className="mt-1 text-sm font-medium text-accent group-hover:underline underline-offset-4">
View the token
</span>
</Link>
</div>
</section>
{/* ── FAQ ──────────────────────────────────────────────────── */}
<section className="border-t border-border py-20">
<div className="mx-auto max-w-3xl px-6">
<div className="text-center mb-12">
<h2 className="text-3xl md:text-4xl font-semibold tracking-tight text-foreground">
Questions
</h2>
</div>
<div className="grid gap-4 sm:grid-cols-2">
{FAQ.map((item) => (
<div
key={item.q}
className="rounded-xl border border-border bg-card/40 backdrop-blur-sm p-6"
>
<h3 className="text-[15px] font-semibold text-foreground mb-2">
{item.q}
</h3>
<p className="text-sm leading-relaxed text-muted-foreground">
{item.a}
</p>
</div>
))}
</div>
<p className="text-center text-xs text-muted-foreground/70 mt-10 max-w-2xl mx-auto">
Cloud pricing and limits are not final they'll be confirmed at
launch.
</p>
</div>
</section>
<Footer />
</>
);
}
+281
View File
@@ -0,0 +1,281 @@
import {
ArrowUpRight,
Check,
Cloud,
Flame,
Heart,
Lock,
Rocket,
ShieldCheck,
} from "lucide-react";
import type {Metadata} from "next";
import {Footer} from "@/components/Footer";
import {Navbar} from "@/components/Navbar";
import {TokenSection} from "@/components/TokenSection";
import {
TOKEN_PROOFS,
TOKEN_SOLSCAN_URL,
TOKEN_TICKER,
} from "@/lib/constants";
export const metadata: Metadata = {
title: `${TOKEN_TICKER} — The official Voicebox token`,
description: `${TOKEN_TICKER} is the official community token for Voicebox on Solana. Entirely optional — Voicebox is, and always will be, free and open source.`,
openGraph: {
title: `${TOKEN_TICKER} on Solana`,
description: `The official community token for Voicebox. Optional, just for fun — Voicebox stays free and open source.`,
type: "website",
url: "https://voicebox.sh/token",
images: [{url: "/og.webp", width: 1200, height: 630}],
},
};
const USE_OF_FUNDS = [
{
icon: Rocket,
title: "Full-time development",
body: "The token is the equivalent of a salary — it lets me work on Voicebox every day instead of squeezing it around other work.",
},
{
icon: Cloud,
title: "Mobile + cloud backup & sync",
body: "Shipping the mobile app and encrypted cloud backup/sync so your generations and captures are safe and available anywhere.",
},
{
icon: Heart,
title: "More engines, more hardware",
body: "Adding TTS engines and broadening GPU / OS support so Voicebox runs great on whatever you've got.",
},
];
const FAQ = [
{
q: "Do I need the token to use Voicebox?",
a: "No. Voicebox is free and open source, and every feature works without ever touching the token. It exists purely for supporters who want to back the project and have some fun.",
},
{
q: "Is this an investment?",
a: "No. $VOICEBOX is a community token, not a security or a promise of returns. There is no roadmap of financial milestones, and nothing here is financial advice. Only spend what you're comfortable with.",
},
{
q: "How do I buy it?",
a: "Copy the contract address above, then buy on pump.fun with a Solana wallet. Always verify the address matches the one on this page — impersonators are common.",
},
{
q: "Does buying it fund development?",
a: "Yes — going full-time on Voicebox is funded by the token, alongside donations. The surest way to support the project either way is to use it, star the repo, and tell people about it.",
},
];
export default function TokenPage() {
return (
<>
<Navbar />
{/* Top padding clears the fixed navbar; TokenSection carries the
header, contract address, and buy CTA. */}
<main className="pt-16">
<TokenSection />
{/* ── Why a token ──────────────────────────────────────────── */}
<section className="border-t border-border py-20">
<div className="mx-auto max-w-3xl px-6">
<div className="text-center mb-12">
<div className="text-[11px] font-semibold uppercase tracking-[0.22em] text-accent mb-4">
Why a token
</div>
<h2 className="text-3xl md:text-4xl font-semibold tracking-tight text-foreground">
So I can build this full-time.
</h2>
<p className="text-muted-foreground max-w-2xl mx-auto mt-4">
Voicebox grew to over a million downloads with zero marketing
but donations alone never made full-time work sustainable.{" "}
{TOKEN_TICKER} changed that overnight, and it's already
accelerating everything below. The app stays{" "}
<b className="text-foreground">free, open source, and local-first</b>{" "}
— forever.
</p>
</div>
<div className="grid gap-4 sm:grid-cols-3">
{USE_OF_FUNDS.map((item) => {
const Icon = item.icon;
return (
<div
key={item.title}
className="rounded-xl border border-border bg-card/40 backdrop-blur-sm p-6"
>
<Icon className="h-5 w-5 text-accent mb-3" />
<h3 className="text-[15px] font-semibold text-foreground mb-2">
{item.title}
</h3>
<p className="text-sm leading-relaxed text-muted-foreground">
{item.body}
</p>
</div>
);
})}
</div>
</div>
</section>
{/* ── Holder utility ───────────────────────────────────────── */}
<section className="border-t border-border py-20">
<div className="mx-auto max-w-3xl px-6">
<div className="rounded-2xl border-2 border-accent/40 bg-card/60 backdrop-blur-sm p-8 md:p-10 shadow-[0_8px_40px_hsl(43_60%_50%/0.08)]">
<div className="flex items-center gap-2 mb-4">
<Cloud className="h-5 w-5 text-accent" />
<span className="text-[11px] font-semibold uppercase tracking-[0.22em] text-accent">
Holder perk · coming soon
</span>
</div>
<h2 className="text-2xl md:text-3xl font-semibold tracking-tight text-foreground mb-3">
Cloud backup & sync — free for holders.
</h2>
<p className="text-muted-foreground leading-relaxed">
Encrypted cloud backup and sync (and the mobile cloud) will be a
paid service — roughly{" "}
<b className="text-foreground">$12/year</b> for everyone else, and{" "}
<b className="text-foreground">free for {TOKEN_TICKER} holders</b>.
Generate on the go, keep your captures and generations safe, and
pick up on any device.
</p>
</div>
</div>
</section>
{/* ── On-chain transparency ────────────────────────────────── */}
<section className="border-t border-border py-20">
<div className="mx-auto max-w-4xl px-6">
<div className="text-center mb-12">
<div className="text-[11px] font-semibold uppercase tracking-[0.22em] text-accent mb-4">
On-chain transparency
</div>
<h2 className="text-3xl md:text-4xl font-semibold tracking-tight text-foreground">
Don't trust verify.
</h2>
<p className="text-muted-foreground max-w-2xl mx-auto mt-4">
Liquidity is locked and supply is reduced through ongoing
buyback &amp; burns. Every action is on-chain and linked here, so
you never have to take my word for it.
</p>
</div>
<div className="grid gap-4 sm:grid-cols-3">
{TOKEN_PROOFS.map((proof, i) => {
const Icon = proof.kind === "lock" ? Lock : Flame;
return (
<div
key={`${proof.label}-${i}`}
className="flex flex-col rounded-xl border border-border bg-card/40 backdrop-blur-sm p-6"
>
<Icon className="h-5 w-5 text-accent mb-3" />
<h3 className="text-[15px] font-semibold text-foreground mb-2">
{proof.label}
</h3>
<p className="text-sm leading-relaxed text-muted-foreground flex-1">
{proof.detail}
</p>
{proof.txUrl ? (
<a
href={proof.txUrl}
target="_blank"
rel="noopener noreferrer"
className="mt-4 inline-flex items-center gap-1.5 text-sm font-medium text-foreground/80 hover:text-foreground transition-colors"
>
View on Solscan
<ArrowUpRight className="h-3.5 w-3.5" />
</a>
) : (
<span className="mt-4 inline-flex items-center gap-1.5 text-xs font-medium text-muted-foreground/60">
Proof link pending
</span>
)}
</div>
);
})}
</div>
<div className="mt-6 text-center">
<a
href={TOKEN_SOLSCAN_URL}
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-2 text-sm text-muted-foreground hover:text-foreground transition-colors"
>
Inspect supply &amp; holders on Solscan
<ArrowUpRight className="h-4 w-4" />
</a>
</div>
</div>
</section>
{/* ── Official vs community ────────────────────────────────── */}
<section className="border-t border-border py-20">
<div className="mx-auto max-w-3xl px-6">
<div className="rounded-2xl border border-border bg-card/40 backdrop-blur-sm p-8">
<div className="flex items-center gap-2 mb-4">
<ShieldCheck className="h-5 w-5 text-accent" />
<h2 className="text-xl md:text-2xl font-semibold tracking-tight text-foreground">
One official token. Accept no substitutes.
</h2>
</div>
<ul className="space-y-3">
<ProofRow text={`${TOKEN_TICKER} is the only official Voicebox token. The mint address on this page is the single source of truth — always verify it.`} />
<ProofRow text="My other projects (including Spacedrive) will never have an official token. This is the only one I'll ever make." />
<ProofRow text="I deployed it myself so liquidity can be locked and the trajectory controlled — and I no longer claim fees on any other community tokens." />
</ul>
</div>
</div>
</section>
{/* ── Good to know ─────────────────────────────────────────── */}
<section className="border-t border-border py-20">
<div className="mx-auto max-w-3xl px-6">
<div className="text-center mb-12">
<div className="text-[11px] font-semibold uppercase tracking-[0.22em] text-accent mb-4">
Good to know
</div>
<h2 className="text-3xl md:text-4xl font-semibold tracking-tight text-foreground">
Optional, just for fun.
</h2>
</div>
<div className="grid gap-4 sm:grid-cols-2">
{FAQ.map((item) => (
<div
key={item.q}
className="rounded-xl border border-border bg-card/40 backdrop-blur-sm p-6"
>
<h3 className="text-[15px] font-semibold text-foreground mb-2">
{item.q}
</h3>
<p className="text-sm leading-relaxed text-muted-foreground">
{item.a}
</p>
</div>
))}
</div>
<p className="text-center text-xs text-muted-foreground/70 mt-10 max-w-2xl mx-auto">
{TOKEN_TICKER} is a community token with no affiliation to any
exchange or financial product. Nothing on this page is financial
advice. Verify the contract address before buying.
</p>
</div>
</section>
</main>
<Footer />
</>
);
}
function ProofRow({text}: {text: string}) {
return (
<li className="flex items-start gap-3 text-sm text-foreground/90">
<Check className="h-5 w-5 shrink-0 text-accent mt-px" />
<span>{text}</span>
</li>
);
}
+24 -7
View File
@@ -6,7 +6,6 @@ import {
DONATE_URL,
GITHUB_REPO,
TOKEN_CONTRACT_ADDRESS,
TOKEN_PUMP_URL,
TOKEN_TICKER,
} from '@/lib/constants';
@@ -71,6 +70,16 @@ export function Footer() {
API
</a>
</li>
<li>
<a href="/cloud" className="hover:text-foreground transition-colors">
Cloud
</a>
</li>
<li>
<a href="/pricing" className="hover:text-foreground transition-colors">
Pricing
</a>
</li>
<li>
<a href="/download" className="hover:text-foreground transition-colors">
Download
@@ -83,6 +92,11 @@ export function Footer() {
<div>
<h4 className="text-sm font-semibold mb-3">Resources</h4>
<ul className="space-y-2 text-sm text-muted-foreground">
<li>
<a href="/blog" className="hover:text-foreground transition-colors">
Blog
</a>
</li>
<li>
<Link
href="https://docs.voicebox.sh"
@@ -123,6 +137,11 @@ export function Footer() {
Issues
</Link>
</li>
<li>
<a href="/sponsors" className="hover:text-foreground transition-colors">
VIP Sponsor
</a>
</li>
</ul>
</div>
@@ -163,15 +182,13 @@ export function Footer() {
<span className="text-xs text-muted-foreground/60">Solana</span>
</div>
<CopyAddress address={TOKEN_CONTRACT_ADDRESS} />
<a
href={TOKEN_PUMP_URL}
target="_blank"
rel="noopener noreferrer"
<Link
href="/token"
className="inline-flex items-center gap-1.5 hover:text-foreground transition-colors"
>
Buy on pump.fun
Token details
<ArrowUpRight className="h-3.5 w-3.5" />
</a>
</Link>
</div>
</div>
</div>
+16 -6
View File
@@ -3,7 +3,7 @@
import { Coffee, Coins, Github } from 'lucide-react';
import Image from 'next/image';
import { useEffect, useState } from 'react';
import { DONATE_URL, GITHUB_REPO, TOKEN_PUMP_URL, TOKEN_TICKER } from '@/lib/constants';
import { DONATE_URL, GITHUB_REPO, TOKEN_TICKER } from '@/lib/constants';
function formatStarCount(count: number): string {
if (count >= 1000) {
@@ -32,7 +32,7 @@ export function Navbar() {
return (
<nav className="fixed inset-x-0 top-0 z-50 border-b border-border/50 bg-background/80 backdrop-blur-xl">
<div className="mx-auto flex max-w-7xl items-center justify-between px-6 py-3 sm:grid sm:grid-cols-3">
<div className="mx-auto flex max-w-7xl items-center justify-between px-6 py-3 sm:grid sm:grid-cols-[1fr_auto_1fr] sm:gap-x-6">
{/* Logo + wordmark */}
<a href="/" className="flex items-center gap-2.5 justify-self-start">
<Image
@@ -74,6 +74,18 @@ export function Navbar() {
>
Models
</a>
<a
href="/pricing"
className="rounded-md px-3 py-1.5 text-sm font-medium text-muted-foreground transition-colors hover:text-foreground"
>
Pricing
</a>
<a
href="/blog"
className="rounded-md px-3 py-1.5 text-sm font-medium text-muted-foreground transition-colors hover:text-foreground"
>
Blog
</a>
<a
href="https://docs.voicebox.sh"
target="_blank"
@@ -87,11 +99,9 @@ export function Navbar() {
{/* Token + Donate + GitHub star buttons */}
<div className="flex items-center gap-2 justify-self-end">
<a
href={TOKEN_PUMP_URL}
target="_blank"
rel="noopener noreferrer"
href="/token"
className="hidden sm:flex items-center gap-2 rounded-lg border border-border/60 bg-card/60 px-3 py-1.5 text-sm text-muted-foreground transition-colors hover:text-foreground hover:border-accent/40"
aria-label={`${TOKEN_TICKER} token on pump.fun`}
aria-label={`${TOKEN_TICKER} token`}
>
<Coins className="h-4 w-4 text-accent" />
<span className="text-[13px] font-semibold tracking-wide text-foreground">
+157
View File
@@ -0,0 +1,157 @@
"use client";
import {Check} from "lucide-react";
import {useState} from "react";
import {
annualSavingsPercent,
type BillingPeriod,
PRICING_TIERS,
} from "@/lib/pricing";
const MAX_ANNUAL_SAVINGS = Math.max(
...PRICING_TIERS.map((t) => annualSavingsPercent(t)),
);
export function PricingTiers() {
const [period, setPeriod] = useState<BillingPeriod>("annual");
return (
<div className="mx-auto max-w-6xl px-6">
{/* Billing toggle */}
<div className="mb-10 flex items-center justify-center">
<div className="inline-flex items-center gap-1 rounded-full border border-border bg-card/40 p-1">
<ToggleButton
active={period === "monthly"}
onClick={() => setPeriod("monthly")}
>
Monthly
</ToggleButton>
<ToggleButton
active={period === "annual"}
onClick={() => setPeriod("annual")}
>
Annual
{MAX_ANNUAL_SAVINGS > 0 ? (
<span
className={`ml-1.5 rounded-full px-1.5 py-0.5 text-[10px] font-semibold transition-colors ${
period === "annual"
? "bg-white/25 text-white"
: "bg-accent/15 text-accent"
}`}
>
Save {MAX_ANNUAL_SAVINGS}%
</span>
) : null}
</ToggleButton>
</div>
</div>
<div className="grid items-start gap-4 md:grid-cols-3">
{PRICING_TIERS.map((tier) => {
const isFree = tier.monthly === 0 && tier.annual === 0;
const amount = period === "monthly" ? tier.monthly : tier.annual;
const unit = period === "monthly" ? "/month" : "/year";
const isExternal = tier.cta.href.startsWith("http");
return (
<div
key={tier.id}
className={`flex flex-col rounded-2xl border bg-card/50 backdrop-blur-sm p-7 ${
tier.highlighted
? "border-2 border-accent/50 shadow-[0_8px_40px_hsl(43_60%_50%/0.1)] md:-mt-2"
: "border-border"
}`}
>
<div className="mb-1 flex items-center justify-between gap-2">
<h2 className="text-lg font-semibold text-foreground">
{tier.name}
</h2>
{tier.badge ? (
<span
className={`rounded-full px-2.5 py-0.5 text-[10px] font-semibold uppercase tracking-wider ${
tier.highlighted
? "bg-accent/15 text-accent"
: "border border-border/60 text-muted-foreground"
}`}
>
{tier.badge}
</span>
) : null}
</div>
<p className="mb-5 min-h-[2.5rem] text-sm text-muted-foreground">
{tier.tagline}
</p>
<div className="mb-1 flex items-baseline gap-1">
<span className="text-4xl font-bold tracking-tight text-foreground">
${amount}
</span>
{!isFree ? (
<span className="text-sm text-muted-foreground">{unit}</span>
) : null}
</div>
<p className="mb-6 min-h-[1rem] text-xs text-muted-foreground/70">
{isFree
? tier.priceNote
: period === "annual"
? tier.priceNote
: `Billed monthly · ${tier.priceNote ?? ""}`}
</p>
<a
href={tier.cta.href}
{...(isExternal
? {target: "_blank", rel: "noopener noreferrer"}
: {})}
className={`mb-7 inline-flex items-center justify-center rounded-full px-5 py-3 text-sm font-semibold transition-all ${
tier.highlighted
? "bg-accent text-white shadow-[0_4px_20px_hsl(43_60%_50%/0.3)] hover:bg-accent-faint"
: "border border-border/60 bg-card/40 text-foreground hover:border-accent/40"
}`}
>
{tier.cta.label}
</a>
<ul className="space-y-3">
{tier.features.map((feature) => (
<li
key={feature}
className="flex items-start gap-3 text-sm text-foreground/90"
>
<Check className="h-4 w-4 shrink-0 text-accent mt-0.5" />
<span>{feature}</span>
</li>
))}
</ul>
</div>
);
})}
</div>
</div>
);
}
function ToggleButton({
active,
onClick,
children,
}: {
active: boolean;
onClick: () => void;
children: React.ReactNode;
}) {
return (
<button
type="button"
onClick={onClick}
className={`flex items-center rounded-full px-4 py-1.5 text-sm font-medium transition-colors ${
active
? "bg-accent text-white"
: "text-muted-foreground hover:text-foreground"
}`}
>
{children}
</button>
);
}
+109
View File
@@ -0,0 +1,109 @@
"use client";
import {Heart, Quote} from "lucide-react";
import {DONATE_URL} from "@/lib/constants";
type Testimonial = {
quote: string;
author: string;
};
/** Verbatim supporter messages from Buy Me a Coffee. */
const TESTIMONIALS: Testimonial[] = [
{
quote:
"Cloning my own voice was a snap — and now I can hear my reminders and to-dos from my digital doppelgänger. Very cool.",
author: "jimzip",
},
{
quote: "It's better than most other paid services.",
author: "Peiming Pai",
},
{
quote:
"I'm using this great tool for my multimedia course project — you made it into the classrooms!",
author: "theanoma.ly",
},
{
quote:
"This app is fantastic! I use it to learn languages and for my learning materials. Congratulations, it's great!",
author: "Kevin Serrano",
},
{
quote:
"Absolutely amazing! The learning curve was very short. Thank you for a great program and for making it free.",
author: "DJWhy",
},
{
quote: "First engine I tried, zero config. It worked! Amazing.",
author: "Fitz",
},
{
quote:
"This is great for people who are uncomfortable with advocating for themselves in public. Thanks for making it.",
author: "creativeaction.ca",
},
{
quote: "Thanks for this. It's a life-saver!",
author: "The Cowboy Movie Channel",
},
{
quote: "Fantastic open-source app!",
author: "Mitja",
},
];
export function Testimonials() {
return (
<section id="testimonials" className="border-t border-border py-24">
<div className="mx-auto max-w-6xl px-6">
{/* Header */}
<div className="text-center mb-14">
<div className="inline-flex items-center gap-2 rounded-full border border-border/60 bg-card/40 backdrop-blur-sm px-3 py-1 mb-4">
<Heart className="h-3 w-3 text-accent" />
<span className="text-[11px] font-medium uppercase tracking-wider text-muted-foreground">
Loved by users
</span>
</div>
<h2 className="text-3xl font-semibold tracking-tight text-foreground md:text-4xl mb-4">
What people are saying
</h2>
<p className="text-muted-foreground max-w-2xl mx-auto">
Voicebox has passed 1M+ downloads. Here's a handful of notes from
the people using it every day.
</p>
</div>
{/* Masonry-style columns so cards flow naturally regardless of length */}
<div className="columns-1 gap-4 sm:columns-2 lg:columns-3 [&>*]:mb-4">
{TESTIMONIALS.map((t) => (
<figure
key={t.author}
className="break-inside-avoid rounded-xl border border-border bg-card/60 backdrop-blur-sm p-5 transition-colors hover:border-accent/30"
>
<Quote className="h-4 w-4 text-accent/60 mb-3" />
<blockquote className="text-sm leading-relaxed text-foreground/90">
{t.quote}
</blockquote>
<figcaption className="mt-4 text-xs font-medium text-muted-foreground">
{t.author}
</figcaption>
</figure>
))}
</div>
{/* Attribution + soft CTA */}
<div className="mt-10 text-center">
<a
href={DONATE_URL}
target="_blank"
rel="noopener noreferrer"
className="text-xs text-muted-foreground/70 hover:text-foreground transition-colors"
>
From supporters on Buy Me a Coffee
</a>
</div>
</div>
</section>
);
}
+48
View File
@@ -0,0 +1,48 @@
import {ArrowUpRight, Coins} from "lucide-react";
import {TOKEN_TICKER} from "@/lib/constants";
/**
* Compact teaser shown near the bottom of the landing page. The full token
* details (contract address, buy CTA, disclaimers) live on the dedicated
* /token page this just points there.
*/
export function TokenTeaser() {
return (
<section className="border-t border-border py-20">
<div className="mx-auto max-w-4xl px-6">
<div className="relative flex flex-col items-center gap-5 overflow-hidden rounded-2xl border border-border bg-card/40 backdrop-blur-sm px-6 py-10 text-center">
{/* Subtle accent glow */}
<div className="pointer-events-none absolute inset-0 -z-10 flex justify-center">
<div className="h-[200px] w-[420px] rounded-full bg-accent/10 blur-[130px]" />
</div>
<div className="inline-flex items-center gap-2 rounded-full border border-border/60 bg-card/40 px-3 py-1">
<Coins className="h-3 w-3 text-accent" />
<span className="text-[11px] font-medium uppercase tracking-wider text-muted-foreground">
Official token
</span>
</div>
<h2 className="text-2xl font-semibold tracking-tight text-foreground md:text-3xl">
{TOKEN_TICKER} on Solana
</h2>
<p className="max-w-xl text-muted-foreground">
An optional way to back the project and have some fun. Voicebox is and
always will be{" "}
<b className="text-foreground">free and open source</b> the token is
not required to use anything here.
</p>
<a
href="/token"
className="inline-flex items-center gap-2 rounded-full bg-accent px-6 py-3 text-sm font-semibold text-white shadow-[0_4px_20px_hsl(43_60%_50%/0.3),inset_0_2px_0_rgba(255,255,255,0.2),inset_0_-2px_0_rgba(0,0,0,0.1)] transition-all hover:bg-accent-faint"
>
Learn about {TOKEN_TICKER}
<ArrowUpRight className="h-4 w-4" />
</a>
</div>
</div>
</section>
);
}
+77
View File
@@ -0,0 +1,77 @@
import {readdirSync, readFileSync} from "node:fs";
import {join} from "node:path";
import matter from "gray-matter";
import {marked} from "marked";
// Posts are authored as markdown under src/posts. This module uses the Node fs
// API and must only be imported from server components / server code — never
// from a "use client" file, or the markdown libraries leak into the bundle.
export type BlogPost = {
slug: string;
title: string;
author: string;
date: string;
tags: string[];
excerpt: string;
readingMinutes: number;
html: string;
};
export type BlogPostSummary = Omit<BlogPost, "html">;
const POSTS_DIR = join(process.cwd(), "src/posts");
function slugFromFile(file: string): string {
return file.replace(/\.md$/, "");
}
function parsePost(file: string, raw: string): BlogPost {
const {data, content} = matter(raw);
const words = content.trim().split(/\s+/).filter(Boolean).length;
return {
slug: slugFromFile(file),
title: String(data.title ?? "Untitled"),
author: String(data.author ?? "Jamie Pine"),
date:
data.date instanceof Date
? data.date.toISOString().slice(0, 10)
: String(data.date ?? ""),
tags: Array.isArray(data.tags) ? data.tags.map(String) : [],
excerpt: String(data.excerpt ?? ""),
readingMinutes: Math.max(1, Math.ceil(words / 220)),
html: marked.parse(content, {async: false}) as string,
};
}
export function loadAllPosts(): BlogPost[] {
let files: string[] = [];
try {
files = readdirSync(POSTS_DIR).filter((f) => f.endsWith(".md"));
} catch {
return []; // posts dir doesn't exist yet — treat as empty
}
return files
.map((file) => parsePost(file, readFileSync(join(POSTS_DIR, file), "utf8")))
.sort((a, b) => (a.date < b.date ? 1 : -1));
}
export function listPosts(): BlogPostSummary[] {
return loadAllPosts().map(({html: _html, ...summary}) => summary);
}
export function getPost(slug: string): BlogPost | null {
return loadAllPosts().find((post) => post.slug === slug) ?? null;
}
export function formatDate(date: string): string {
if (!date) return "";
const d = new Date(`${date}T00:00:00Z`);
if (Number.isNaN(d.getTime())) return date;
return d.toLocaleDateString("en-US", {
year: "numeric",
month: "long",
day: "numeric",
timeZone: "UTC",
});
}
+42
View File
@@ -5,11 +5,53 @@ export const LATEST_VERSION = 'v0.1.0';
export const GITHUB_REPO = 'https://github.com/jamiepine/voicebox';
export const GITHUB_RELEASES_PAGE = `${GITHUB_REPO}/releases`;
export const DONATE_URL = 'https://buymeacoffee.com/jamiepine';
export const SPONSOR_CHECKOUT_URL = 'https://buy.stripe.com/eVqdRad3n16ubcqf201Jm00';
export const SPONSOR_CONTACT_EMAIL = '[email protected]';
// $VOICEBOX — the official community token on Solana
export const TOKEN_TICKER = '$VOICEBOX';
export const TOKEN_CONTRACT_ADDRESS = 'FpzZHtp5tbvz6xndEtoJHoGEWcT7cFEuscdCh9RApump';
export const TOKEN_PUMP_URL = `https://pump.fun/coin/${TOKEN_CONTRACT_ADDRESS}`;
// Solscan token page — lets anyone inspect supply, holders, and history.
export const TOKEN_SOLSCAN_URL = `https://solscan.io/token/${TOKEN_CONTRACT_ADDRESS}`;
export const TOKEN_TOTAL_SUPPLY = '1B';
// On-chain transparency log — locks and buyback+burns.
// Add a new entry every time a lock or burn happens; set `txUrl` to its Solscan
// link to make the card a live, verifiable proof. Entries without a txUrl render
// as "proof link pending" — fill them in as soon as the hash is available.
export interface TokenProof {
kind: 'lock' | 'burn';
label: string;
detail: string;
/** Solscan (or locker) URL proving the action. Empty = pending, shown as such. */
txUrl: string;
}
export const TOKEN_PROOFS: TokenProof[] = [
{
kind: 'lock',
label: 'Launch liquidity lock',
detail:
'Liquidity and a portion of dev holdings were locked at launch (~6.6% top holder), so the supply can be verified on-chain from day one.',
txUrl: '', // TODO(jamie): add the Solscan/locker link for the launch lock
},
{
kind: 'burn',
label: 'Buyback & burn',
detail:
'Bought $VOICEBOX back from the open market and burned it to a dead address, permanently removing it from supply.',
txUrl:
'https://solscan.io/tx/5MjK4CYMBKAewLcjdD6QkM8ctkeG2bjyQhpjNgEumkDbDtoKCVmzKcWwLWsd4QJov8hs5zbGLt3g5vVCp4CBmze5',
},
{
kind: 'burn',
label: 'Buyback & burn',
detail:
'A second buyback and burn — part of an ongoing commitment to keep buying back and reducing supply over time.',
txUrl: '', // TODO(jamie): add the Solscan link for the second burn
},
];
export const DOWNLOAD_LINKS = {
macArm: GITHUB_RELEASES_PAGE,
+126
View File
@@ -0,0 +1,126 @@
// Pricing + cloud data — single source of truth for /pricing and /cloud.
//
// DRAFT: the cloud service is not live yet and the pricing model is not final.
// $12/yr is a launch/intro figure; tier limits below are placeholders to shape
// the page — tune them once the model is decided. Keeping it all here means the
// pages update by editing this file only.
export const CLOUD_STATUS = "coming-soon" as const; // → flips to "live" at launch
export const CLOUD_PRICE_YEARLY = 12; // launch price for the Cloud tier (USD/yr)
/** Where "get notified" CTAs point until there's a real signup flow. */
export const CLOUD_NOTIFY_URL = "https://x.com/VoiceboxAI";
export type BillingPeriod = "monthly" | "annual";
export interface PricingTier {
id: string;
name: string;
tagline: string;
/** USD per month. 0 = free. */
monthly: number;
/** USD per year. 0 = free. Set below 12×monthly to reward annual. */
annual: number;
priceNote?: string;
/** Visually emphasize this tier (the headline plan). */
highlighted?: boolean;
/** Status pill, e.g. "Available now" / "Coming soon". */
badge?: string;
cta: {label: string; href: string};
features: string[];
}
export const PRICING_TIERS: PricingTier[] = [
{
id: "local",
name: "Local",
tagline: "The full app, free forever.",
monthly: 0,
annual: 0,
priceNote: "No account required",
badge: "Available now",
cta: {label: "Download Voicebox", href: "/download"},
features: [
"Voice cloning across every TTS engine",
"Dictation & Capture (audio kept alongside transcript)",
"MCP / agent integration & personalities",
"Unlimited local generations & captures",
"100% open source, runs entirely on your machine",
],
},
{
id: "cloud",
name: "Cloud",
tagline: "Backup & sync for everything you make.",
monthly: 2, // placeholder
annual: CLOUD_PRICE_YEARLY, // $12 launch price (≈50% off monthly)
priceNote: "Launch price · free for $VOICEBOX holders",
highlighted: true,
badge: "Coming soon",
cta: {label: "Get notified", href: CLOUD_NOTIFY_URL},
features: [
"Everything in Local",
"End-to-end encrypted backup — we can't read it",
"Sync across desktop & mobile",
"25 GB encrypted storage", // placeholder
"Up to 5 devices", // placeholder
"30-day version history", // placeholder
],
},
{
id: "studio",
name: "Studio",
tagline: "For power users and professionals.",
monthly: 6, // placeholder
annual: 48, // placeholder
priceNote: "Placeholder — pricing TBD",
badge: "Coming soon",
cta: {label: "Get notified", href: CLOUD_NOTIFY_URL},
features: [
"Everything in Cloud",
"250 GB encrypted storage", // placeholder
"Unlimited devices", // placeholder
"1-year version history", // placeholder
"Priority support",
],
},
];
/** Annual savings vs paying 12× the monthly price, as a whole percent (0 if none). */
export function annualSavingsPercent(tier: PricingTier): number {
if (tier.monthly <= 0 || tier.annual <= 0) return 0;
const full = tier.monthly * 12;
return Math.max(0, Math.round((1 - tier.annual / full) * 100));
}
export interface CloudFeature {
title: string;
body: string;
}
export const CLOUD_FEATURES: CloudFeature[] = [
{
title: "End-to-end encrypted",
body: "Everything is encrypted on your device before it leaves it. The server only ever stores opaque blobs — it literally cannot read your voices, generations, or captures.",
},
{
title: "Back up everything",
body: "Voice profiles, generations, and captures — including the original audio Voicebox keeps alongside each transcript — safe off your machine.",
},
{
title: "Sync across devices",
body: "Pick up on desktop or mobile. Your library follows you, encrypted in transit and at rest, with a per-device key.",
},
{
title: "Generate on the go",
body: "The mobile app pairs with your library so you can dictate and generate anywhere, then find it all waiting back at your desk.",
},
{
title: "You hold the keys",
body: "A recovery phrase you control is the root of your encryption. Lose your devices and you can still restore — but no one else, including us, ever can.",
},
{
title: "Optional & local-first",
body: "Voicebox works fully offline without an account. Cloud is an add-on for when you want backup and sync — never a requirement.",
},
];
@@ -0,0 +1,49 @@
---
title: "Why Voicebox has a token"
author: Jamie Pine
date: 2026-06-27
tags: [Token, Transparency]
excerpt: "Voicebox grew to over a million downloads with zero marketing — but donations never made full-time work sustainable. Here's the honest reasoning behind $VOICEBOX, and the commitments that come with it."
---
Voicebox started as a one-day experiment. The Qwen3-TTS model dropped, I wanted to try it, so I built a small CLI to load voice profiles and generate speech. As a designer I already had the interface in my head — profiles as cards, a floating generation box, a player spanning the bottom of the app, a gold accent and a microphone logo to make it feel like a real studio. First working version in a day. Open sourced in three.
I did no marketing. None, to this day. But Reddit found it, creators started making tutorials, and the "ElevenLabs just lost its moat" posts began. It crossed a million downloads and is closing in on Spacedrive's GitHub star count — entirely organically. Along the way it became two things at once: a free alternative to ElevenLabs for voice cloning, and a free alternative to Wispr Flow for dictation.
So a fair question keeps coming up: if it's this successful, why a token? Why not just put it behind a subscription, or turn on GitHub Sponsors and call it a day?
## The honest version
I could have built this as a cloud app with a monthly subscription and probably done well. I chose open source instead, and I'd make the same call again. I don't think it would have grown like this behind a paywall — people trust it more when they can read the code and run it entirely on their own machine, it earns a ton of organic exposure for free, and I get the community's help making it work across the endless combinations of GPUs and operating systems I could never test alone.
But open source doesn't pay rent. The donation button on the site brings in roughly $200300 a month. I didn't expect GitHub Sponsors to move that number much. The old sponsor program made almost nothing. I love this project and I want to work on it full-time — and for a while I couldn't justify it.
The token changed that overnight. It's the closest thing I've had to a salary in a long time, and it let me go full-time on Voicebox immediately. That's not a hypothetical — it's already happening, and you'll see it in the commits, the releases, and the posts on [@VoiceboxAI](https://x.com/VoiceboxAI).
## What the token is — and what it isn't
**$VOICEBOX is entirely optional.** Voicebox is, and always will be, free, open source, and local-first. Every feature works without ever touching the token. It exists for supporters who want to back the project and have some fun — nothing here is gated behind it, and nothing here is financial advice.
It's also the **only** token I will ever make. My other projects, including Spacedrive, will never have an official token. I thought carefully about this: the community was clearly most interested in Voicebox because of its existing traction, and if I'm going to have a token at all, I'd rather deploy it myself so I can lock liquidity and be accountable for its trajectory — rather than have an anonymous community coin I can't control. As of now I no longer claim fees on any other community tokens, either.
## Don't trust — verify
A token only earns trust through actions you can check on-chain. So:
- Liquidity and a portion of dev holdings were **locked at launch**, visible from day one.
- I've done **buyback and burns** — buying $VOICEBOX back from the market and burning it to a dead address, permanently. I've done this more than once, and I'll keep doing it.
- The plan is a balanced mix: lock more for trust, burn periodically, but keep enough flexibility to fund real expenses and add liquidity when it helps.
Every one of these is linked on the [token page](/token), so you never have to take my word for it. Verify the contract address there before you do anything — impersonators are common.
## What the money actually builds
Going full-time means the roadmap moves faster. The near-term priorities:
- **The mobile app.** I use the prototype every day for dictation on the go. It does both cloning and dictation, and it's nearly ready to ship.
- **Encrypted cloud backup & sync.** Generate on the go, keep your captures and generations safe, pick up on any device. This will be a paid service — around $12/year — and **free for token holders**.
- **More engines and broader hardware support.** More TTS engines, better GPU and OS coverage, so Voicebox runs great on whatever you've got.
That's the whole pitch. The app stays free forever, the token is an optional way to support it and unlock the cloud service, and the proof of good faith is on-chain and in the changelog. If you want to back the project, the [token page](/token) is here — but starring the repo and telling a friend helps just as much.
Thanks for being here. Now back to shipping.