diff --git a/.gitignore b/.gitignore
index 809e9b4..6285ce2 100644
--- a/.gitignore
+++ b/.gitignore
@@ -16,3 +16,7 @@ content/docs/fabric/resonance/
!.env.example
!.env.*.template
!.env.template
+
+# Local-only docs migration data (contains unlaunched/private product slugs); never commit
+scripts/migration/data/
+content/_unlaunched/
diff --git a/bun.lock b/bun.lock
index dd9b98c..eabea80 100644
--- a/bun.lock
+++ b/bun.lock
@@ -46,6 +46,7 @@
"tw-animate-css": "^1.4.0",
"typescript": "^5.9.3",
"vite-tsconfig-paths": "^6.1.1",
+ "vitest": "^4.1.11",
},
},
},
@@ -646,6 +647,8 @@
"@types/bun": ["@types/bun@1.3.14", "", { "dependencies": { "bun-types": "1.3.14" } }, "sha512-h1hFqFVcvAvD9j9K7ZW7vd82aSA+rTdznZa+5bwvCwqSB1jmmfLcbIWhOLx1/+boy/xmjgCs/OMUL8hRJSmnPw=="],
+ "@types/chai": ["@types/chai@5.2.3", "", { "dependencies": { "@types/deep-eql": "*", "assertion-error": "^2.0.1" } }, "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA=="],
+
"@types/d3-color": ["@types/d3-color@3.1.3", "", {}, "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A=="],
"@types/d3-drag": ["@types/d3-drag@3.0.7", "", { "dependencies": { "@types/d3-selection": "*" } }, "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ=="],
@@ -660,6 +663,8 @@
"@types/debug": ["@types/debug@4.1.12", "", { "dependencies": { "@types/ms": "*" } }, "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ=="],
+ "@types/deep-eql": ["@types/deep-eql@4.0.2", "", {}, "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw=="],
+
"@types/draco3d": ["@types/draco3d@1.4.10", "", {}, "sha512-AX22jp8Y7wwaBgAixaSvkoG4M/+PlAcm3Qs4OW8yT9DM4xUpWKeFhLueTAyZF39pviAdcDdeJoACapiAceqNcw=="],
"@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
@@ -700,6 +705,20 @@
"@vitejs/plugin-react": ["@vitejs/plugin-react@6.0.5", "", { "dependencies": { "@rolldown/pluginutils": "^1.0.1" }, "peerDependencies": { "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", "babel-plugin-react-compiler": "^1.0.0", "vite": "^8.0.0" }, "optionalPeers": ["@rolldown/plugin-babel", "babel-plugin-react-compiler"] }, "sha512-BOVzne/NL162sMdResB25mUv+vWMF5NoAjNf09TeGlE7ZpszZWSD3winycicLJw72yeVsoCn/2kOhEuCvEShMA=="],
+ "@vitest/expect": ["@vitest/expect@4.1.11", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "@types/chai": "^5.2.2", "@vitest/spy": "4.1.11", "@vitest/utils": "4.1.11", "chai": "^6.2.2", "tinyrainbow": "^3.1.0" } }, "sha512-VX2x5vNJXET47KAFzwERI+KRMtTTCSWTfSMKsW7JsUsXV4psq++e3DvZpuTDOpHcxytiDs6p2nhVb2tVDiiUYw=="],
+
+ "@vitest/mocker": ["@vitest/mocker@4.1.11", "", { "dependencies": { "@vitest/spy": "4.1.11", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["msw", "vite"] }, "sha512-2XJVD55d1o5AZous5CCGKS74g/riOj9odEt2bQpCVZeblHyHdnMeFl4jl0XjU21stf4mbjUkew2eXQZt65g5CQ=="],
+
+ "@vitest/pretty-format": ["@vitest/pretty-format@4.1.11", "", { "dependencies": { "tinyrainbow": "^3.1.0" } }, "sha512-yiZzPbGTS9Sr/JpFl8zHrcIkAofNbFV6k21vIgQN/cY/oxZeXhJv5sc/MBJ5jFKWmWs+oJHw0UXLZjmf931+Vw=="],
+
+ "@vitest/runner": ["@vitest/runner@4.1.11", "", { "dependencies": { "@vitest/utils": "4.1.11", "pathe": "^2.0.3" } }, "sha512-LztvUgdwMNJMIkj3hQnnxiC2Xy1zNxq928W/xhjCLaNCzqTZOudjwbQf6v9IntZGPw132i2Lq2rgTRZHD3JHNw=="],
+
+ "@vitest/snapshot": ["@vitest/snapshot@4.1.11", "", { "dependencies": { "@vitest/pretty-format": "4.1.11", "@vitest/utils": "4.1.11", "magic-string": "^0.30.21", "pathe": "^2.0.3" } }, "sha512-pN7ikn1ON7h8ee4gIAp4AzyK+zBtJPzVbqOgu5LCEh4VaJVbPQcgYQYJIMGQPXVeJJq1fnfazis7a5pFNPahog=="],
+
+ "@vitest/spy": ["@vitest/spy@4.1.11", "", {}, "sha512-apNa/prQy2qCeywhnixOHPRCgGNhvg7T4Dapfl1GahLp/R+uhBm5cPyFoNVyqsNd2h1nJxL6BqqdIjiABL60YA=="],
+
+ "@vitest/utils": ["@vitest/utils@4.1.11", "", { "dependencies": { "@vitest/pretty-format": "4.1.11", "convert-source-map": "^2.0.0", "tinyrainbow": "^3.1.0" } }, "sha512-zTCVGpyFsGWBhllOyKlTw/vnr6D9qxsfSDyfbyZmTyjHw5N/VuvzHpHoQjm2ZJzn4RJgx5w4r7V0er69CmLgPQ=="],
+
"@xyflow/react": ["@xyflow/react@12.10.0", "", { "dependencies": { "@xyflow/system": "0.0.74", "classcat": "^5.0.3", "zustand": "^4.4.0" }, "peerDependencies": { "react": ">=17", "react-dom": ">=17" } }, "sha512-eOtz3whDMWrB4KWVatIBrKuxECHqip6PfA8fTpaS2RUGVpiEAe+nqDKsLqkViVWxDGreq0lWX71Xth/SPAzXiw=="],
"@xyflow/system": ["@xyflow/system@0.0.74", "", { "dependencies": { "@types/d3-drag": "^3.0.7", "@types/d3-interpolate": "^3.0.4", "@types/d3-selection": "^3.0.10", "@types/d3-transition": "^3.0.8", "@types/d3-zoom": "^3.0.8", "d3-drag": "^3.0.0", "d3-interpolate": "^3.0.1", "d3-selection": "^3.0.0", "d3-zoom": "^3.0.0" } }, "sha512-7v7B/PkiVrkdZzSbL+inGAo6tkR/WQHHG0/jhSvLQToCsfa8YubOGmBYd1s08tpKpihdHDZFwzQZeR69QSBb4Q=="],
@@ -742,6 +761,8 @@
"aria-hidden": ["aria-hidden@1.2.6", "", { "dependencies": { "tslib": "^2.0.0" } }, "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA=="],
+ "assertion-error": ["assertion-error@2.0.1", "", {}, "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA=="],
+
"astring": ["astring@1.9.0", "", { "bin": { "astring": "bin/astring" } }, "sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg=="],
"babel-dead-code-elimination": ["babel-dead-code-elimination@1.0.12", "", { "dependencies": { "@babel/core": "^7.23.7", "@babel/parser": "^7.23.6", "@babel/traverse": "^7.23.7", "@babel/types": "^7.23.6" } }, "sha512-GERT7L2TiYcYDtYk1IpD+ASAYXjKbLTDPhBtYj7X1NuRMDTMtAx9kyBenub1Ev41lo91OHCKdmP+egTDmfQ7Ig=="],
@@ -768,6 +789,8 @@
"ccount": ["ccount@2.0.1", "", {}, "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg=="],
+ "chai": ["chai@6.2.2", "", {}, "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg=="],
+
"chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="],
"character-entities": ["character-entities@2.0.2", "", {}, "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ=="],
@@ -870,6 +893,8 @@
"entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="],
+ "es-module-lexer": ["es-module-lexer@2.3.2", "", {}, "sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw=="],
+
"esast-util-from-estree": ["esast-util-from-estree@2.0.0", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "devlop": "^1.0.0", "estree-util-visit": "^2.0.0", "unist-util-position-from-estree": "^2.0.0" } }, "sha512-4CyanoAudUSBAn5K13H4JhsMH6L9ZP7XbLVe/dKybkxMO7eDyLsT8UHl9TRNrU2Gr9nz+FovfSIjuXWJ81uVwQ=="],
"esast-util-from-js": ["esast-util-from-js@2.0.1", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "acorn": "^8.0.0", "esast-util-from-estree": "^2.0.0", "vfile-message": "^4.0.0" } }, "sha512-8Ja+rNJ0Lt56Pcf3TAmpBZjmx8ZcK5Ts4cAzIOjsjevg9oSXJnl6SUQ2EevU8tv3h6ZLWmoKL5H4fgWvdvfETw=="],
@@ -898,6 +923,8 @@
"estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="],
+ "expect-type": ["expect-type@1.4.0", "", {}, "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA=="],
+
"exsolve": ["exsolve@1.0.8", "", {}, "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA=="],
"extend": ["extend@3.0.2", "", {}, "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="],
@@ -1050,7 +1077,7 @@
"maath": ["maath@0.10.8", "", { "peerDependencies": { "@types/three": ">=0.134.0", "three": ">=0.134.0" } }, "sha512-tRvbDF0Pgqz+9XUa4jjfgAQ8/aPKmQdWXilFu2tMy4GWj4NOsx99HlULO4IeREfbO3a0sA145DZYyvXPkybm0g=="],
- "magic-string": ["magic-string@1.2.0", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-ptco+HFxTLgjafSLim2LojBSwfg5feBjd+SqyiwdGkzC38UPdZy3zgrHMI2CoTf5fJL38tbHMYWVzIH8BxGqJw=="],
+ "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
"markdown-extensions": ["markdown-extensions@2.0.0", "", {}, "sha512-o5vL7aDWatOTX8LzaS1WMoaoxIiLRQJuIKKe2wAw6IeULDHaqbiqiggmx+pKvZDb1Sj+pE46Sn1T7lCqfFtg1Q=="],
@@ -1182,6 +1209,8 @@
"npm-to-yarn": ["npm-to-yarn@3.2.0", "", {}, "sha512-K1HmQeZT2HrjpsR6KgqbN2FAXL2NrJJmNUSD9ck7HGTVu1JKXox8n9SB+tjbU8m8JGLF4OscrroPepew/L7/Xw=="],
+ "obug": ["obug@2.1.4", "", {}, "sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA=="],
+
"ofetch": ["ofetch@2.0.0-alpha.3", "", {}, "sha512-zpYTCs2byOuft65vI3z43Dd6iSdFbOZZLb9/d21aCpx2rGastVU9dOCv0lu4ykc1Ur1anAYjDi3SUvR0vq50JA=="],
"ohash": ["ohash@2.0.11", "", {}, "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ=="],
@@ -1302,6 +1331,8 @@
"shiki": ["shiki@4.4.3", "", { "dependencies": { "@shikijs/core": "4.4.3", "@shikijs/engine-javascript": "4.4.3", "@shikijs/engine-oniguruma": "4.4.3", "@shikijs/langs": "4.4.3", "@shikijs/themes": "4.4.3", "@shikijs/types": "4.4.3", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.5" } }, "sha512-Mb/GvXPHBAXdgGIcnfU5L3ldpn1XcxrGkPHwqgRx17/I2XRfqlFKk2vGkHWINn1kdXvzJZeuO3is6I9KLPFm0g=="],
+ "siginfo": ["siginfo@2.0.0", "", {}, "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g=="],
+
"smol-toml": ["smol-toml@1.8.0", "", {}, "sha512-kCZr2V3ch9i00x8zXRhjUNVcjG9ijES5dDudkXvUVCT5QlJNQWElSJdZqyPemffHoLNUYwOcou0Fy+ojN0uHSQ=="],
"solid-js": ["solid-js@1.9.10", "", { "dependencies": { "csstype": "^3.1.0", "seroval": "~1.3.0", "seroval-plugins": "~1.3.0" } }, "sha512-Coz956cos/EPDlhs6+jsdTxKuJDPT7B5SVIWgABwROyxjY7Xbr8wkzD68Et+NxnV7DLJ3nJdAC2r9InuV/4Jew=="],
@@ -1314,10 +1345,14 @@
"srvx": ["srvx@0.9.8", "", { "bin": { "srvx": "bin/srvx.mjs" } }, "sha512-RZaxTKJEE/14HYn8COLuUOJAt0U55N9l1Xf6jj+T0GoA01EUH1Xz5JtSUOI+EHn+AEgPCVn7gk6jHJffrr06fQ=="],
+ "stackback": ["stackback@0.0.2", "", {}, "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw=="],
+
"stats-gl": ["stats-gl@2.4.2", "", { "dependencies": { "@types/three": "*", "three": "^0.170.0" } }, "sha512-g5O9B0hm9CvnM36+v7SFl39T7hmAlv541tU81ME8YeSb3i1CIP5/QdDeSB3A0la0bKNHpxpwxOVRo2wFTYEosQ=="],
"stats.js": ["stats.js@0.17.0", "", {}, "sha512-hNKz8phvYLPEcRkeG1rsGmV5ChMjKDAWU7/OJJdDErPBNChQXxCo3WZurGpnWc6gZhAzEPFad1aVgyOANH1sMw=="],
+ "std-env": ["std-env@4.2.0", "", {}, "sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw=="],
+
"string.prototype.codepointat": ["string.prototype.codepointat@0.2.1", "", {}, "sha512-2cBVCj6I4IOvEnjgO/hWqXjqBGsY+zwPmHl12Srk9IXSZ56Jwwmy+66XO5Iut/oQVR7t5ihYdLB0GMa4alEUcg=="],
"stringify-entities": ["stringify-entities@4.0.4", "", { "dependencies": { "character-entities-html4": "^2.0.0", "character-entities-legacy": "^3.0.0" } }, "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg=="],
@@ -1344,10 +1379,14 @@
"tiny-inflate": ["tiny-inflate@1.0.3", "", {}, "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw=="],
+ "tinybench": ["tinybench@2.9.0", "", {}, "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg=="],
+
"tinyexec": ["tinyexec@1.3.0", "", {}, "sha512-QKAl9m8gWWGHV8jZcPeym6j+XULi6tOf1mT83WYJ4Lk2ytW/uwAWkrP0uFsdoYMdueVJ0qs26wZ+23xeB4ibNQ=="],
"tinyglobby": ["tinyglobby@0.2.17", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g=="],
+ "tinyrainbow": ["tinyrainbow@3.1.1", "", {}, "sha512-yau8yJdTt989Mm0Bd/236QnzEiPf2xLLTqUZRUJOo/3CB078LSwzei343DgtJVmfJKJE3TMINY1u42SQsP6mXw=="],
+
"trim-lines": ["trim-lines@3.0.1", "", {}, "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg=="],
"troika-three-text": ["troika-three-text@0.52.5", "", { "dependencies": { "bidi-js": "^1.0.2", "troika-three-utils": "^0.52.5", "troika-worker-utils": "^0.52.0", "webgl-sdf-generator": "1.1.1" }, "peerDependencies": { "three": ">=0.125.0" } }, "sha512-Ry3jRhic9pzcY4JduSvRRyDmVOSqEW19gT4vtK+aCiPNVcDlmkxvGG0YbFd36RTDq1wExOupXnvNF/j1oiHHDA=="],
@@ -1424,6 +1463,8 @@
"vitefu": ["vitefu@1.1.1", "", { "peerDependencies": { "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0-beta.0" }, "optionalPeers": ["vite"] }, "sha512-B/Fegf3i8zh0yFbpzZ21amWzHmuNlLlmJT6n7bu5e+pCHUKQIfXSYokrqOBGEMMe9UG2sostKQF9mml/vYaWJQ=="],
+ "vitest": ["vitest@4.1.11", "", { "dependencies": { "@vitest/expect": "4.1.11", "@vitest/mocker": "4.1.11", "@vitest/pretty-format": "4.1.11", "@vitest/runner": "4.1.11", "@vitest/snapshot": "4.1.11", "@vitest/spy": "4.1.11", "@vitest/utils": "4.1.11", "es-module-lexer": "^2.0.0", "expect-type": "^1.3.0", "magic-string": "^0.30.21", "obug": "^2.1.1", "pathe": "^2.0.3", "picomatch": "^4.0.3", "std-env": "^4.0.0-rc.1", "tinybench": "^2.9.0", "tinyexec": "^1.0.2", "tinyglobby": "^0.2.15", "tinyrainbow": "^3.1.0", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", "@vitest/browser-playwright": "4.1.11", "@vitest/browser-preview": "4.1.11", "@vitest/browser-webdriverio": "4.1.11", "@vitest/coverage-istanbul": "4.1.11", "@vitest/coverage-v8": "4.1.11", "@vitest/ui": "4.1.11", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@opentelemetry/api", "@types/node", "@vitest/browser-playwright", "@vitest/browser-preview", "@vitest/browser-webdriverio", "@vitest/coverage-istanbul", "@vitest/coverage-v8", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "./vitest.mjs" } }, "sha512-fhACrNXUidIbGSBr5FlbuBkO7VWC1ZyLl0DO4CU2DrQoAPxX84Ysxs+HeGQpii5lZWV1Q4gBZTTu49mF+A6Edw=="],
+
"walk-up-path": ["walk-up-path@4.0.0", "", {}, "sha512-3hu+tD8YzSLGuFYtPRb48vdhKMi0KQV5sn+uWr8+7dMEq/2G/dtLrdDinkLjqq5TIbIBjYJ4Ax/n3YiaW7QM8A=="],
"web-namespaces": ["web-namespaces@2.0.1", "", {}, "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ=="],
@@ -1436,6 +1477,8 @@
"which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
+ "why-is-node-running": ["why-is-node-running@2.3.0", "", { "dependencies": { "siginfo": "^2.0.0", "stackback": "0.0.2" }, "bin": { "why-is-node-running": "cli.js" } }, "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w=="],
+
"ws": ["ws@8.18.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg=="],
"xmlbuilder2": ["xmlbuilder2@4.0.3", "", { "dependencies": { "@oozcitak/dom": "^2.0.2", "@oozcitak/infra": "^2.0.2", "@oozcitak/util": "^10.0.0", "js-yaml": "^4.1.1" } }, "sha512-bx8Q1STctnNaaDymWnkfQLKofs0mGNN7rLLapJlGuV3VlvegD7Ls4ggMjE3aUSWItCCzU0PEv45lI87iSigiCA=="],
@@ -1482,8 +1525,6 @@
"@tailwindcss/node/lightningcss": ["lightningcss@1.32.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.32.0", "lightningcss-darwin-arm64": "1.32.0", "lightningcss-darwin-x64": "1.32.0", "lightningcss-freebsd-x64": "1.32.0", "lightningcss-linux-arm-gnueabihf": "1.32.0", "lightningcss-linux-arm64-gnu": "1.32.0", "lightningcss-linux-arm64-musl": "1.32.0", "lightningcss-linux-x64-gnu": "1.32.0", "lightningcss-linux-x64-musl": "1.32.0", "lightningcss-win32-arm64-msvc": "1.32.0", "lightningcss-win32-x64-msvc": "1.32.0" } }, "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ=="],
- "@tailwindcss/node/magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
-
"@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.11.3", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.3", "tslib": "^2.4.0" }, "bundled": true }, "sha512-zLpS5asjEb7lq8jYLq37N6XKaE41DIexlY1rF/z4/tIl3wo13Sqm28fRyfIsKZD+NZ8mM5RoKkpW/rBcuoSZSg=="],
"@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.11.3", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA=="],
@@ -1496,12 +1537,8 @@
"@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
- "@tanstack/devtools-bundler-core/magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
-
"@tanstack/devtools-bundler-core/oxc-parser": ["oxc-parser@0.120.0", "", { "dependencies": { "@oxc-project/types": "^0.120.0" }, "optionalDependencies": { "@oxc-parser/binding-android-arm-eabi": "0.120.0", "@oxc-parser/binding-android-arm64": "0.120.0", "@oxc-parser/binding-darwin-arm64": "0.120.0", "@oxc-parser/binding-darwin-x64": "0.120.0", "@oxc-parser/binding-freebsd-x64": "0.120.0", "@oxc-parser/binding-linux-arm-gnueabihf": "0.120.0", "@oxc-parser/binding-linux-arm-musleabihf": "0.120.0", "@oxc-parser/binding-linux-arm64-gnu": "0.120.0", "@oxc-parser/binding-linux-arm64-musl": "0.120.0", "@oxc-parser/binding-linux-ppc64-gnu": "0.120.0", "@oxc-parser/binding-linux-riscv64-gnu": "0.120.0", "@oxc-parser/binding-linux-riscv64-musl": "0.120.0", "@oxc-parser/binding-linux-s390x-gnu": "0.120.0", "@oxc-parser/binding-linux-x64-gnu": "0.120.0", "@oxc-parser/binding-linux-x64-musl": "0.120.0", "@oxc-parser/binding-openharmony-arm64": "0.120.0", "@oxc-parser/binding-wasm32-wasi": "0.120.0", "@oxc-parser/binding-win32-arm64-msvc": "0.120.0", "@oxc-parser/binding-win32-ia32-msvc": "0.120.0", "@oxc-parser/binding-win32-x64-msvc": "0.120.0" } }, "sha512-WyPWZlcIm+Fkte63FGfgFB8mAAk33aH9h5N9lphXVOHSXEBFFsmYdOBedVKly363aWABjZdaj/m9lBfEY4wt+w=="],
- "@tanstack/router-generator/magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
-
"@tanstack/start-plugin-core/lightningcss": ["lightningcss@1.32.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.32.0", "lightningcss-darwin-arm64": "1.32.0", "lightningcss-darwin-x64": "1.32.0", "lightningcss-freebsd-x64": "1.32.0", "lightningcss-linux-arm-gnueabihf": "1.32.0", "lightningcss-linux-arm64-gnu": "1.32.0", "lightningcss-linux-arm64-musl": "1.32.0", "lightningcss-linux-x64-gnu": "1.32.0", "lightningcss-linux-x64-musl": "1.32.0", "lightningcss-win32-arm64-msvc": "1.32.0", "lightningcss-win32-x64-msvc": "1.32.0" } }, "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ=="],
"@tanstack/start-plugin-core/srvx": ["srvx@0.11.22", "", { "bin": { "srvx": "bin/srvx.mjs" } }, "sha512-LqZxxBDMKuMAZzFzJnDCkFOrs9MZQZr0LvHiO/SuSZVdQaXD7xQ5UWTUxheJrQPve1qk9MG2B/yttUvJxw8egQ=="],
@@ -1512,6 +1549,8 @@
"bun-types/@types/node": ["@types/node@25.9.5", "", { "dependencies": { "undici-types": ">=7.24.0 <7.24.7" } }, "sha512-OScDchr2fwuUmWdf4kZ9h7PcJiYDVInhJizG/biAq3cAvqwYktuy/TYGGdZNMtNTFUP7rnb0NU4TUdm82kt4Rg=="],
+ "fumadocs-mdx/magic-string": ["magic-string@1.2.0", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-ptco+HFxTLgjafSLim2LojBSwfg5feBjd+SqyiwdGkzC38UPdZy3zgrHMI2CoTf5fJL38tbHMYWVzIH8BxGqJw=="],
+
"h3-v2/rou3": ["rou3@0.8.1", "", {}, "sha512-ePa+XGk00/3HuCqrEnK3LxJW7I0SdNg6EFzKUJG73hMAdDcOUC/i/aSz7LSDwLrGr33kal/rqOGydzwl6U7zBA=="],
"h3-v2/srvx": ["srvx@0.11.22", "", { "bin": { "srvx": "bin/srvx.mjs" } }, "sha512-LqZxxBDMKuMAZzFzJnDCkFOrs9MZQZr0LvHiO/SuSZVdQaXD7xQ5UWTUxheJrQPve1qk9MG2B/yttUvJxw8egQ=="],
diff --git a/content/docs/armory/meta.json b/content/docs/armory/meta.json
deleted file mode 100644
index 2c3f9ed..0000000
--- a/content/docs/armory/meta.json
+++ /dev/null
@@ -1,4 +0,0 @@
-{
- "title": "Armory",
- "pages": ["index", "..."]
-}
diff --git a/content/docs/codex/meta.json b/content/docs/codex/meta.json
deleted file mode 100644
index 77519fb..0000000
--- a/content/docs/codex/meta.json
+++ /dev/null
@@ -1,4 +0,0 @@
-{
- "title": "Codex",
- "pages": ["index", "..."]
-}
diff --git a/content/docs/community/contributing/golden.mdx b/content/docs/community/contributing/golden.mdx
index 76b45fb..f989f04 100644
--- a/content/docs/community/contributing/golden.mdx
+++ b/content/docs/community/contributing/golden.mdx
@@ -18,7 +18,7 @@ import app from "@/lib/config/app.config";
**Omni Golden** is the canonical baseline for how Omni code should look, behave, and be styled. It documents style conventions, architectural patterns, and agent rules, representing the source of truth for _how_ code should be written.
- Golden contains only documentation. For config files and project scaffolding, see [📜 Templates](/armory/templates).
+ Golden contains only documentation. For config files and project scaffolding, see [📜 Templates](/realms/armory/templates).
## See Also
diff --git a/content/docs/core/meta.json b/content/docs/core/meta.json
deleted file mode 100644
index e9ca89b..0000000
--- a/content/docs/core/meta.json
+++ /dev/null
@@ -1,4 +0,0 @@
-{
- "title": "Core",
- "pages": ["index", "..."]
-}
diff --git a/content/docs/ethos.mdx b/content/docs/ethos.mdx
index c94ad3e..b2d240e 100644
--- a/content/docs/ethos.mdx
+++ b/content/docs/ethos.mdx
@@ -10,7 +10,7 @@ We believe openness creates better software and stronger communities.
- **Open-source, self-hostable solutions.** Our core tools are open-source. You can inspect the code, contribute improvements, and self-host when you need full control.
- **No vendor lock-in.** We design for portability. Your data and workflows are never trapped.
-- **Community-driven development.** Users influence our roadmap (see [📣 Backfeed](/core/backfeed)). We build what matters to the people who use our tools.
+- **Community-driven development.** Users influence our roadmap (see [📣 Backfeed](/products/backfeed)). We build what matters to the people who use our tools.
## Transparent Operations
diff --git a/content/docs/fabric/meta.json b/content/docs/fabric/meta.json
deleted file mode 100644
index be9e7ea..0000000
--- a/content/docs/fabric/meta.json
+++ /dev/null
@@ -1,4 +0,0 @@
-{
- "title": "Fabric",
- "pages": ["index", "..."]
-}
diff --git a/content/docs/grid/meta.json b/content/docs/grid/meta.json
deleted file mode 100644
index 7eff6a4..0000000
--- a/content/docs/grid/meta.json
+++ /dev/null
@@ -1,4 +0,0 @@
-{
- "title": "Grid",
- "pages": ["index", "..."]
-}
diff --git a/content/docs/help/self-hosting/configuration.mdx b/content/docs/help/self-hosting/configuration.mdx
index 56da273..4a04601 100644
--- a/content/docs/help/self-hosting/configuration.mdx
+++ b/content/docs/help/self-hosting/configuration.mdx
@@ -74,7 +74,7 @@ Set `SELF_HOSTED=true` to disable billing and cloud features:
SELF_HOSTED=true
```
-See the full [Vortex configuration reference](/grid/vortex/configuration) for all available variables.
+See the full [Vortex configuration reference](/products/vortex/configuration) for all available variables.
### Manifold
diff --git a/content/docs/help/self-hosting/docker-compose.mdx b/content/docs/help/self-hosting/docker-compose.mdx
index 9215d30..ddb4e15 100644
--- a/content/docs/help/self-hosting/docker-compose.mdx
+++ b/content/docs/help/self-hosting/docker-compose.mdx
@@ -40,7 +40,7 @@ docker compose --env-file .env.local -f compose.yaml -f compose.dev.yaml up --bu
### Runa
-[Runa](/core/runa) runs three containers:
+[Runa](/products/runa) runs three containers:
| Container | Image | Port |
|-----------|-------|------|
@@ -60,7 +60,7 @@ docker compose --env-file .env.local up -d
### Vortex
-[Vortex](/grid/vortex) runs six containers:
+[Vortex](/products/vortex) runs six containers:
| Container | Image | Port |
|-----------|-------|------|
@@ -97,7 +97,7 @@ docker compose up -d
| `IGGY_USERNAME` | Iggy admin username |
| `IGGY_PASSWORD` | Iggy admin password |
-Optional: `DB_NAME`, `DB_USER`, `DB_PORT`, `API_PORT`, `APP_PORT`, `CACHE_PORT`, and OAuth provider credentials (`GOOGLE_*`, `GITHUB_*`, `OIDC_*`). See the full [Vortex configuration reference](/grid/vortex/configuration).
+Optional: `DB_NAME`, `DB_USER`, `DB_PORT`, `API_PORT`, `APP_PORT`, `CACHE_PORT`, and OAuth provider credentials (`GOOGLE_*`, `GITHUB_*`, `OIDC_*`). See the full [Vortex configuration reference](/products/vortex/configuration).
#### Volumes
@@ -111,7 +111,7 @@ Three named volumes persist data across restarts:
### Manifold
-[Manifold](/armory/manifold) runs three containers:
+[Manifold](/products/manifold) runs three containers:
| Container | Port | Description |
|-----------|------|-------------|
@@ -149,11 +149,11 @@ Manifold supports filesystem or S3-compatible blob storage:
| `BLOB_S3_ENDPOINT` | — | Custom S3 endpoint (for MinIO, R2) |
| `BLOB_S3_PREFIX` | `blobs` | S3 key prefix |
-See the [Manifold Storage guide](/armory/manifold/storage) for details.
+See the [Manifold Storage guide](/products/manifold/storage) for details.
### Synapse
-[Synapse](/grid/synapse) runs five containers:
+[Synapse](/products/synapse) runs five containers:
| Container | Image | Port |
|-----------|-------|------|
@@ -191,7 +191,7 @@ Optional: `AUTH_BASE_URL` (HIDRA Gatekeeper), `BILLING_*` (billing), `CORS_ALLOW
| `db_data` | PostgreSQL | Database files |
| `valkey_data` | Valkey | Rate limit state and cache |
-See the [Synapse self-hosting guide](/grid/synapse/self-hosting) for Helm charts, gateway configuration, and binary installs.
+See the [Synapse self-hosting guide](/products/synapse/self-hosting) for Helm charts, gateway configuration, and binary installs.
## Database Migrations
diff --git a/content/docs/help/self-hosting/index.mdx b/content/docs/help/self-hosting/index.mdx
index 52b71ee..d16e56d 100644
--- a/content/docs/help/self-hosting/index.mdx
+++ b/content/docs/help/self-hosting/index.mdx
@@ -17,38 +17,38 @@ Most Omni products follow a [standard Docker Compose pattern](/help/self-hosting
| Product | Realm | Guide |
|---------|-------|-------|
-| [Runa](/core/runa) | Core | [Docker Compose](/help/self-hosting/docker-compose) |
-| [Backfeed](/core/backfeed) | Core | [Docker Compose](/help/self-hosting/docker-compose) |
-| [Channel](/core/channel) | Core | [Docker Compose](/help/self-hosting/docker-compose) |
-| [Crystal](/core/crystal) | Core | [Docker Compose](/help/self-hosting/docker-compose) |
-| [Mantle](/core/mantle) | Core | [Docker Compose](/help/self-hosting/docker-compose) |
-| [MyFi](/core/myfi) | Core | [Docker Compose](/help/self-hosting/docker-compose) |
-| [Halo](/core/halo) | Core | [Docker Compose](/help/self-hosting/docker-compose) |
-| [Blink](/kindred/blink) | Kindred | [Docker Compose](/help/self-hosting/docker-compose) |
-| [Creator](/fabric/creator) | Fabric | [Docker Compose](/help/self-hosting/docker-compose) |
-| [Gaia](/fabric/gaia) | Fabric | [Docker Compose](/help/self-hosting/docker-compose) |
-| [Trellis](/fabric/trellis) | Fabric | [Docker Compose](/help/self-hosting/docker-compose) |
-| [Beacon](/grid/beacon) | Grid | [Docker Compose](/help/self-hosting/docker-compose) |
-| [Vortex](/grid/vortex) | Grid | [Docker Compose](/help/self-hosting/docker-compose), [Helm](/help/self-hosting/kubernetes) |
-| [Lattice](/grid/lattice) | Grid | [Docker Compose](/help/self-hosting/docker-compose) |
-| [HIDRA](/armory/hidra) | Armory | [Docker Compose](/help/self-hosting/docker-compose) |
-| [Aether](/armory/aether) | Armory | [Docker Compose](/help/self-hosting/docker-compose) |
-| [Arbor](/armory/arbor) | Armory | [Docker Compose](/help/self-hosting/docker-compose) |
-| [Manifold](/armory/manifold) | Armory | [Docker Compose](/help/self-hosting/docker-compose) |
+| [Runa](/products/runa) | Core | [Docker Compose](/help/self-hosting/docker-compose) |
+| [Backfeed](/products/backfeed) | Core | [Docker Compose](/help/self-hosting/docker-compose) |
+| [Channel](/products/channel) | Core | [Docker Compose](/help/self-hosting/docker-compose) |
+| [Crystal](/realms/core/crystal) | Core | [Docker Compose](/help/self-hosting/docker-compose) |
+| [Mantle](/products/mantle) | Core | [Docker Compose](/help/self-hosting/docker-compose) |
+| [MyFi](/products/myfi) | Core | [Docker Compose](/help/self-hosting/docker-compose) |
+| [Halo](/products/halo) | Core | [Docker Compose](/help/self-hosting/docker-compose) |
+| [Blink](/products/blink) | Kindred | [Docker Compose](/help/self-hosting/docker-compose) |
+| [Creator](/products/creator) | Fabric | [Docker Compose](/help/self-hosting/docker-compose) |
+| [Gaia](/products/gaia) | Fabric | [Docker Compose](/help/self-hosting/docker-compose) |
+| [Trellis](/products/trellis) | Fabric | [Docker Compose](/help/self-hosting/docker-compose) |
+| [Beacon](/products/beacon) | Grid | [Docker Compose](/help/self-hosting/docker-compose) |
+| [Vortex](/products/vortex) | Grid | [Docker Compose](/help/self-hosting/docker-compose), [Helm](/help/self-hosting/kubernetes) |
+| [Lattice](/realms/grid/lattice) | Grid | [Docker Compose](/help/self-hosting/docker-compose) |
+| [HIDRA](/products/hidra) | Armory | [Docker Compose](/help/self-hosting/docker-compose) |
+| [Aether](/products/aether) | Armory | [Docker Compose](/help/self-hosting/docker-compose) |
+| [Arbor](/realms/armory/arbor) | Armory | [Docker Compose](/help/self-hosting/docker-compose) |
+| [Manifold](/products/manifold) | Armory | [Docker Compose](/help/self-hosting/docker-compose) |
### Kubernetes
| Product | Guide |
|---------|-------|
-| [Fractal](/grid/fractal) | [Kubernetes](/help/self-hosting/kubernetes) |
-| [Vortex](/grid/vortex) | [Helm chart](/help/self-hosting/kubernetes#vortex-helm-chart) |
+| [Fractal](/products/fractal) | [Kubernetes](/help/self-hosting/kubernetes) |
+| [Vortex](/products/vortex) | [Helm chart](/help/self-hosting/kubernetes#vortex-helm-chart) |
### Binary / Docker
| Product | Methods | Guide |
|---------|---------|-------|
-| [Synapse](/grid/synapse) | Docker Compose, Helm, Binary | [Docker Compose](/help/self-hosting/docker-compose#synapse), [Self-Hosting](/grid/synapse/self-hosting) |
-| [Omni Terminal](/armory/omni-terminal) | Binary, Docker | [Binary installs](#omni-terminal) |
+| [Synapse](/products/synapse) | Docker Compose, Helm, Binary | [Docker Compose](/help/self-hosting/docker-compose#synapse), [Self-Hosting](/products/synapse/self-hosting) |
+| [Omni Terminal](/products/omni-terminal) | Binary, Docker | [Binary installs](#omni-terminal) |
## Binary Installs
@@ -56,7 +56,7 @@ Some products ship as standalone binaries and don't require Docker or Kubernetes
### Synapse
-[Synapse](/grid/synapse) runs as a full stack (gateway + API + dashboard) via Docker Compose, or the gateway alone as a standalone binary:
+[Synapse](/products/synapse) runs as a full stack (gateway + API + dashboard) via Docker Compose, or the gateway alone as a standalone binary:
```bash
# Full stack (Docker Compose)
@@ -75,11 +75,11 @@ cargo build --release
Prebuilt gateway binaries for Linux (x86_64, aarch64) and macOS (Apple Silicon) are available on the [releases page](https://github.com/omnidotdev/synapse-gateway/releases).
-See the [Synapse self-hosting guide](/grid/synapse/self-hosting) for full configuration and environment variable reference.
+See the [Synapse self-hosting guide](/products/synapse/self-hosting) for full configuration and environment variable reference.
### Omni Terminal
-[Omni Terminal](/armory/omni-terminal) runs in the browser via the `serve` subcommand. The WASM frontend is embedded at compile time:
+[Omni Terminal](/products/omni-terminal) runs in the browser via the `serve` subcommand. The WASM frontend is embedded at compile time:
```bash
# Via package manager (Homebrew, AUR, etc.)
@@ -107,11 +107,11 @@ docker build -t omni-terminal-web .
docker run -p 3000:3000 omni-terminal-web
```
-See the [Omni Terminal Web docs](/armory/omni-terminal/web) for full details including TLS configuration and the npm package.
+See the [Omni Terminal Web docs](/products/omni-terminal/web) for full details including TLS configuration and the npm package.
## Self-Hosting Simplification
-[Run](/grid/run) is Omni's tool for simplifying self-hosted deployments. Check its documentation for the latest on streamlining multi-product setups.
+[Run](/products/run) is Omni's tool for simplifying self-hosted deployments. Check its documentation for the latest on streamlining multi-product setups.
diff --git a/content/docs/help/self-hosting/kubernetes.mdx b/content/docs/help/self-hosting/kubernetes.mdx
index 6528305..bd410d5 100644
--- a/content/docs/help/self-hosting/kubernetes.mdx
+++ b/content/docs/help/self-hosting/kubernetes.mdx
@@ -7,7 +7,7 @@ This page covers Kubernetes-based deployments: Fractal for full platform provisi
## Fractal
-[Fractal](/grid/fractal) is Omni's self-hostable application deployment platform built on Kubernetes. It can provision a cluster from scratch or install on an existing one.
+[Fractal](/products/fractal) is Omni's self-hostable application deployment platform built on Kubernetes. It can provision a cluster from scratch or install on an existing one.
### Installation Modes
@@ -106,11 +106,11 @@ Fractal applies default-deny Cilium network policies per project namespace. Each
- DNS resolution
- OTEL collector (observability)
-See the [Fractal documentation](/grid/fractal) for configuration and CLI reference.
+See the [Fractal documentation](/products/fractal) for configuration and CLI reference.
## Vortex Helm Chart
-[Vortex](/grid/vortex) ships a Helm chart for Kubernetes deployments.
+[Vortex](/products/vortex) ships a Helm chart for Kubernetes deployments.
### Prerequisites
diff --git a/content/docs/kindred/meta.json b/content/docs/kindred/meta.json
deleted file mode 100644
index ba9c6f9..0000000
--- a/content/docs/kindred/meta.json
+++ /dev/null
@@ -1,4 +0,0 @@
-{
- "title": "Kindred",
- "pages": ["index", "..."]
-}
diff --git a/content/docs/meta.json b/content/docs/meta.json
index ecbadea..73616a1 100644
--- a/content/docs/meta.json
+++ b/content/docs/meta.json
@@ -10,28 +10,42 @@
"ethos",
"ecosystem",
"open-source",
- "orin",
- "launcher",
- "lens",
- "...",
+ "products",
"---Core---",
- "...core",
- "---Fabric---",
- "...fabric",
- "---Armory---",
- "...armory",
- "---Reality---",
- "...reality",
+ "realms/core/index",
+ "products/backfeed",
+ "products/runa",
"---Kindred---",
- "...kindred",
+ "realms/kindred/index",
+ "---Fabric---",
+ "realms/fabric/index",
"---Grid---",
- "...grid",
+ "realms/grid/index",
+ "products/beacon",
+ "products/fractal",
+ "products/kiln",
+ "products/synapse",
+ "products/vortex",
+ "---Armory---",
+ "realms/armory/index",
+ "products/eden",
+ "products/omni-cli",
+ "products/omni-terminal",
+ "realms/armory/templates",
"---Codex---",
- "...codex",
+ "realms/codex/index",
+ "products/garden",
+ "products/life-json",
+ "products/persona-json",
"---Sigil---",
- "...sigil",
+ "realms/sigil/index",
+ "products/thornberry",
+ "products/zenpetal",
+ "---Reality---",
+ "realms/reality/index",
+ "products/rdk",
"---Worlds---",
- "...worlds",
+ "realms/worlds/index",
"---Community---",
"...community",
"---Help---",
diff --git a/content/docs/core/backfeed/api.mdx b/content/docs/products/backfeed/api.mdx
similarity index 73%
rename from content/docs/core/backfeed/api.mdx
rename to content/docs/products/backfeed/api.mdx
index f798a7f..646c899 100644
--- a/content/docs/core/backfeed/api.mdx
+++ b/content/docs/products/backfeed/api.mdx
@@ -5,7 +5,7 @@ description: Programmatic access and event-driven integration (planned)
**Planned.** A documented public API and webhooks are on the
- [roadmap](/core/backfeed/roadmap). This page describes the direction, not yet
+ [roadmap](/products/backfeed/roadmap). This page describes the direction, not yet
a stable contract.
@@ -16,5 +16,5 @@ existing stack.
In the meantime, Backfeed is already event-driven internally: every change emits
a [CloudEvent](https://cloudevents.io) (`backfeed.post.*`, `backfeed.comment.*`,
`backfeed.vote.*`, and more) so the rest of the Omni ecosystem can react,
-including [closing the loop with Runa](/core/backfeed/runa-loop). See
-[integrations](/core/backfeed/integrations) for what connects today.
+including [closing the loop with Runa](/products/backfeed/runa-loop). See
+[integrations](/products/backfeed/integrations) for what connects today.
diff --git a/content/docs/core/backfeed/board-visibility.mdx b/content/docs/products/backfeed/board-visibility.mdx
similarity index 100%
rename from content/docs/core/backfeed/board-visibility.mdx
rename to content/docs/products/backfeed/board-visibility.mdx
diff --git a/content/docs/core/backfeed/concepts.mdx b/content/docs/products/backfeed/concepts.mdx
similarity index 100%
rename from content/docs/core/backfeed/concepts.mdx
rename to content/docs/products/backfeed/concepts.mdx
diff --git a/content/docs/core/backfeed/features/ai-ingestion.mdx b/content/docs/products/backfeed/features/ai-ingestion.mdx
similarity index 96%
rename from content/docs/core/backfeed/features/ai-ingestion.mdx
rename to content/docs/products/backfeed/features/ai-ingestion.mdx
index 6fe055a..5889e43 100644
--- a/content/docs/core/backfeed/features/ai-ingestion.mdx
+++ b/content/docs/products/backfeed/features/ai-ingestion.mdx
@@ -35,4 +35,4 @@ flow still works.
More connectors are on the way: in-app widgets, social mentions, app-store
reviews, and chat channels (the Signal model already supports these source
-types). Track them on the [roadmap](/core/backfeed/roadmap).
+types). Track them on the [roadmap](/products/backfeed/roadmap).
diff --git a/content/docs/core/backfeed/features/boards-posts.mdx b/content/docs/products/backfeed/features/boards-posts.mdx
similarity index 91%
rename from content/docs/core/backfeed/features/boards-posts.mdx
rename to content/docs/products/backfeed/features/boards-posts.mdx
index 7a8b379..0837918 100644
--- a/content/docs/core/backfeed/features/boards-posts.mdx
+++ b/content/docs/products/backfeed/features/boards-posts.mdx
@@ -12,7 +12,7 @@ A shareable board per project where users submit posts and the team triages
them. Boards are **public by default** (no account required to view), so the
primary use case (collecting feedback from external users) works out of the box.
Posting and voting require signing in. See
-[board visibility](/core/backfeed/board-visibility) for the public/private
+[board visibility](/products/backfeed/board-visibility) for the public/private
toggle.
## Posts
@@ -31,7 +31,7 @@ addressed by a per-project number (`#42`, or `PREFIX-42` when a prefix is set).
Up/down votes on every post surface demand; sort the board by top-voted or most
recent. Human votes are always the prioritization layer, even as
-[AI triage](/core/backfeed/features/ai-ingestion) handles sorting.
+[AI triage](/products/backfeed/features/ai-ingestion) handles sorting.
## Comments & reactions
diff --git a/content/docs/core/backfeed/features/index.mdx b/content/docs/products/backfeed/features/index.mdx
similarity index 54%
rename from content/docs/core/backfeed/features/index.mdx
rename to content/docs/products/backfeed/features/index.mdx
index 4c0955c..29382cb 100644
--- a/content/docs/core/backfeed/features/index.mdx
+++ b/content/docs/products/backfeed/features/index.mdx
@@ -6,18 +6,18 @@ description: A map of what Backfeed does, grouped by what you are trying to get
Backfeed's features fall into a few groups. Start anywhere; everything connects
to the same board.
-- **[Boards, posts & voting](/core/backfeed/features/boards-posts)** — collect
+- **[Boards, posts & voting](/products/backfeed/features/boards-posts)** — collect
feedback: public boards, posts with rich text and attachments, voting,
threaded comments, reactions, and duplicate detection.
-- **[Statuses, roadmap & changelog](/core/backfeed/features/statuses-roadmap)** —
+- **[Statuses, roadmap & changelog](/products/backfeed/features/statuses-roadmap)** —
track and communicate: customizable statuses, a per-post timeline, a public
roadmap, an opt-in changelog, status notifications, and at-a-glance metrics.
-- **[Tags & filtering](/core/backfeed/features/tags-filtering)** — organize:
+- **[Tags & filtering](/products/backfeed/features/tags-filtering)** — organize:
project-scoped tags, search, sorting, and saved views.
-- **[AI & ingestion](/core/backfeed/features/ai-ingestion)** — scale: the Signal
+- **[AI & ingestion](/products/backfeed/features/ai-ingestion)** — scale: the Signal
model, email-to-feedback, optional AI triage, and content moderation.
Two cross-cutting topics have their own pages:
-[board visibility](/core/backfeed/board-visibility) (public vs private) and
-[closing the loop with Runa](/core/backfeed/runa-loop). What is coming next
-lives on the [roadmap](/core/backfeed/roadmap).
+[board visibility](/products/backfeed/board-visibility) (public vs private) and
+[closing the loop with Runa](/products/backfeed/runa-loop). What is coming next
+lives on the [roadmap](/products/backfeed/roadmap).
diff --git a/content/docs/core/backfeed/features/meta.json b/content/docs/products/backfeed/features/meta.json
similarity index 100%
rename from content/docs/core/backfeed/features/meta.json
rename to content/docs/products/backfeed/features/meta.json
diff --git a/content/docs/core/backfeed/features/statuses-roadmap.mdx b/content/docs/products/backfeed/features/statuses-roadmap.mdx
similarity index 100%
rename from content/docs/core/backfeed/features/statuses-roadmap.mdx
rename to content/docs/products/backfeed/features/statuses-roadmap.mdx
diff --git a/content/docs/core/backfeed/features/tags-filtering.mdx b/content/docs/products/backfeed/features/tags-filtering.mdx
similarity index 100%
rename from content/docs/core/backfeed/features/tags-filtering.mdx
rename to content/docs/products/backfeed/features/tags-filtering.mdx
diff --git a/content/docs/core/backfeed/getting-started.mdx b/content/docs/products/backfeed/getting-started.mdx
similarity index 91%
rename from content/docs/core/backfeed/getting-started.mdx
rename to content/docs/products/backfeed/getting-started.mdx
index 01b06ec..f15adb1 100644
--- a/content/docs/core/backfeed/getting-started.mdx
+++ b/content/docs/products/backfeed/getting-started.mdx
@@ -14,4 +14,4 @@ description: Spin up a workspace, create a project, and start collecting feedbac
5. **Review & Prioritize**
Use Backfeed's prioritization tools to filter, sort, and rank posts based on importance and feasibility.
6. **Plan & Execute**
- Move posts through your statuses to build the roadmap, hand accepted work to [Runa](/core/runa), and close the loop with reporters when it ships.
+ Move posts through your statuses to build the roadmap, hand accepted work to [Runa](/products/runa), and close the loop with reporters when it ships.
diff --git a/content/docs/core/backfeed/index.mdx b/content/docs/products/backfeed/index.mdx
similarity index 95%
rename from content/docs/core/backfeed/index.mdx
rename to content/docs/products/backfeed/index.mdx
index 2a36282..ad67bc4 100644
--- a/content/docs/core/backfeed/index.mdx
+++ b/content/docs/products/backfeed/index.mdx
@@ -61,4 +61,4 @@ import {
**Omni Backfeed** is a user feedback reporting platform. Backfeed aggregates user data for your products, services, and experiences, providing a centralized location to iterate on your projects with user-driven insights and planning.
-Backfeed is evolving from a feedback board into an **AI-native feedback brain**: it keeps the public board, voting, and roadmap at its core while learning to ingest feedback from many sources and triage it automatically. The board stays the source of truth, and human votes still set priority. AI handles the manual labor of sorting, not the judgment. See [AI & Ingestion](/core/backfeed/features/ai-ingestion).
+Backfeed is evolving from a feedback board into an **AI-native feedback brain**: it keeps the public board, voting, and roadmap at its core while learning to ingest feedback from many sources and triage it automatically. The board stays the source of truth, and human votes still set priority. AI handles the manual labor of sorting, not the judgment. See [AI & Ingestion](/products/backfeed/features/ai-ingestion).
diff --git a/content/docs/core/backfeed/integrations.mdx b/content/docs/products/backfeed/integrations.mdx
similarity index 64%
rename from content/docs/core/backfeed/integrations.mdx
rename to content/docs/products/backfeed/integrations.mdx
index 47b9d49..9f8d584 100644
--- a/content/docs/core/backfeed/integrations.mdx
+++ b/content/docs/products/backfeed/integrations.mdx
@@ -5,13 +5,13 @@ description: How Backfeed connects across the Omni ecosystem and beyond
Backfeed integrates across the Omni ecosystem and beyond:
-- **Runa** for turning accepted feedback into tracked work (see [Closing the Loop](/core/backfeed/runa-loop))
-- **Additional feedback sources and AI triage** (see [AI & Ingestion](/core/backfeed/features/ai-ingestion))
+- **Runa** for turning accepted feedback into tracked work (see [Closing the Loop](/products/backfeed/runa-loop))
+- **Additional feedback sources and AI triage** (see [AI & Ingestion](/products/backfeed/features/ai-ingestion))
- **GitHub** (planned)
## Ecosystem events
Every change emits a [CloudEvent](https://cloudevents.io) (`backfeed.post.*`,
`backfeed.comment.*`, `backfeed.vote.*`, and more) so the rest of the Omni
-ecosystem can react, including [closing the loop with Runa](/core/backfeed/runa-loop).
-A documented public API and webhooks are [planned](/core/backfeed/api).
+ecosystem can react, including [closing the loop with Runa](/products/backfeed/runa-loop).
+A documented public API and webhooks are [planned](/products/backfeed/api).
diff --git a/content/docs/core/backfeed/meta.json b/content/docs/products/backfeed/meta.json
similarity index 100%
rename from content/docs/core/backfeed/meta.json
rename to content/docs/products/backfeed/meta.json
diff --git a/content/docs/core/backfeed/roadmap.mdx b/content/docs/products/backfeed/roadmap.mdx
similarity index 80%
rename from content/docs/core/backfeed/roadmap.mdx
rename to content/docs/products/backfeed/roadmap.mdx
index 9751224..4c5d239 100644
--- a/content/docs/core/backfeed/roadmap.mdx
+++ b/content/docs/products/backfeed/roadmap.mdx
@@ -8,11 +8,11 @@ the live, voted product roadmap lives in the app.
- **More ingestion sources**: beyond email, capture feedback from in-app
widgets, social mentions, app-store reviews, and chat channels, all funneled
- into the same boards (the [Signal model](/core/backfeed/features/ai-ingestion)
+ into the same boards (the [Signal model](/products/backfeed/features/ai-ingestion)
already supports them; connectors are rolling out).
- **Product reviews**: star-rated reviews alongside feature requests as a
distinct feedback type.
- **Analytics & insights**: deeper dashboards on user needs and engagement
trends.
- **Public API & webhooks**: a documented API and webhooks to integrate Backfeed
- with your existing stack (see [API & Webhooks](/core/backfeed/api)).
+ with your existing stack (see [API & Webhooks](/products/backfeed/api)).
diff --git a/content/docs/core/backfeed/runa-loop.mdx b/content/docs/products/backfeed/runa-loop.mdx
similarity index 73%
rename from content/docs/core/backfeed/runa-loop.mdx
rename to content/docs/products/backfeed/runa-loop.mdx
index 894f0dc..2a372ec 100644
--- a/content/docs/core/backfeed/runa-loop.mdx
+++ b/content/docs/products/backfeed/runa-loop.mdx
@@ -3,6 +3,6 @@ title: Closing the Loop with Runa
description: Hand accepted feedback to Runa and notify reporters when it ships
---
-When a feature request is accepted, Backfeed can hand it to **[Runa](/core/runa)** as a task. When the work ships, the original reporters are notified that their feedback was delivered, the single most effective way to keep feedback flowing.
+When a feature request is accepted, Backfeed can hand it to **[Runa](/products/runa)** as a task. When the work ships, the original reporters are notified that their feedback was delivered, the single most effective way to keep feedback flowing.
Backfeed already does its half of this over CloudEvents: the first time a post reaches a shipped status it emits a `backfeed.post.shipped` event carrying the reporters behind that feedback, with no manual status-syncing required. The Runa-side consumer that turns shipped tasks back into Backfeed updates is rolling out.
diff --git a/content/docs/grid/beacon/authentication.mdx b/content/docs/products/beacon/authentication.mdx
similarity index 98%
rename from content/docs/grid/beacon/authentication.mdx
rename to content/docs/products/beacon/authentication.mdx
index bd64b80..7b556a1 100644
--- a/content/docs/grid/beacon/authentication.mdx
+++ b/content/docs/products/beacon/authentication.mdx
@@ -109,7 +109,7 @@ Only paired devices can connect. This is the most secure mode.
mode = "device_only"
```
-New devices must complete the pairing flow before they can authenticate. See [Device Identity](/grid/beacon/device-identity) for details.
+New devices must complete the pairing flow before they can authenticate. See [Device Identity](/products/beacon/device-identity) for details.
## Local Network Bypass
diff --git a/content/docs/grid/beacon/channels.mdx b/content/docs/products/beacon/channels.mdx
similarity index 98%
rename from content/docs/grid/beacon/channels.mdx
rename to content/docs/products/beacon/channels.mdx
index 4a16881..21e6c86 100644
--- a/content/docs/grid/beacon/channels.mdx
+++ b/content/docs/products/beacon/channels.mdx
@@ -97,4 +97,4 @@ In addition to messaging channels, Beacon supports local voice interaction:
- **Text-to-speech** — OpenAI TTS or ElevenLabs
- **Local audio capture** — Direct microphone input via CPAL
-Voice settings are configured per-persona in the `voice` section of [persona.json](/codex/persona-json).
+Voice settings are configured per-persona in the `voice` section of [persona.json](/products/persona-json).
diff --git a/content/docs/grid/beacon/cloud-features.mdx b/content/docs/products/beacon/cloud-features.mdx
similarity index 100%
rename from content/docs/grid/beacon/cloud-features.mdx
rename to content/docs/products/beacon/cloud-features.mdx
diff --git a/content/docs/grid/beacon/device-identity.mdx b/content/docs/products/beacon/device-identity.mdx
similarity index 100%
rename from content/docs/grid/beacon/device-identity.mdx
rename to content/docs/products/beacon/device-identity.mdx
diff --git a/content/docs/grid/beacon/discovery.mdx b/content/docs/products/beacon/discovery.mdx
similarity index 100%
rename from content/docs/grid/beacon/discovery.mdx
rename to content/docs/products/beacon/discovery.mdx
diff --git a/content/docs/grid/beacon/getting-started.mdx b/content/docs/products/beacon/getting-started.mdx
similarity index 71%
rename from content/docs/grid/beacon/getting-started.mdx
rename to content/docs/products/beacon/getting-started.mdx
index abc2562..01e5fd7 100644
--- a/content/docs/grid/beacon/getting-started.mdx
+++ b/content/docs/products/beacon/getting-started.mdx
@@ -32,7 +32,7 @@ When you run Beacon for the first time:
## Set Up Your AI Provider
-Beacon includes [Synapse](/grid/synapse) as an embedded AI router — no separate service to install or manage. Configure your provider and API key via `beacon setup` or by editing `~/.config/omni/beacon/config.toml` directly.
+Beacon includes [Synapse](/products/synapse) as an embedded AI router — no separate service to install or manage. Configure your provider and API key via `beacon setup` or by editing `~/.config/omni/beacon/config.toml` directly.
### Bring Your Own Key (BYOK)
@@ -47,7 +47,7 @@ Synapse dispatches requests to the appropriate provider based on your config:
### Omni Credits
-Don't want to manage API keys? Connect your Omni account and use pay-per-use credits instead. Go to **Settings → Cloud → Connect Omni Account**. See [Cloud Features](/grid/beacon/cloud-features) for details.
+Don't want to manage API keys? Connect your Omni account and use pay-per-use credits instead. Go to **Settings → Cloud → Connect Omni Account**. See [Cloud Features](/products/beacon/cloud-features) for details.
## Pair Additional Devices
@@ -58,7 +58,7 @@ Add your phone, tablet, or another computer to your gateway:
3. On the new device, open Beacon and enter the code
4. The device is now paired and can chat with your assistant
-You can manage paired devices and revoke access anytime from **Settings → Devices**. See [Device Identity](/grid/beacon/device-identity) for technical details.
+You can manage paired devices and revoke access anytime from **Settings → Devices**. See [Device Identity](/products/beacon/device-identity) for technical details.
## Try Your First Chat
@@ -71,7 +71,7 @@ Type a message in the chat box and press enter. Your assistant responds using wh
3. Ask a question
4. Beacon transcribes your speech, sends it to the AI, and speaks the response back
-See the [Voice guide](/grid/beacon/voice) for more on wake words, provider options, and troubleshooting.
+See the [Voice guide](/products/beacon/voice) for more on wake words, provider options, and troubleshooting.
## Standalone Gateway
@@ -89,11 +89,11 @@ cargo build --release
./target/release/beacon --foreground -v
```
-Clients can discover the gateway automatically via [mDNS](/grid/beacon/discovery), or connect by entering the gateway's IP address manually.
+Clients can discover the gateway automatically via [mDNS](/products/beacon/discovery), or connect by entering the gateway's IP address manually.
## Next Steps
-- [Voice](/grid/beacon/voice) — Set up wake words and voice providers
-- [Personas](/grid/beacon/personas) — Customize your assistant's personality
-- [Channels](/grid/beacon/channels) — Connect Discord, Slack, Telegram, and more
-- [Cloud Features](/grid/beacon/cloud-features) — Memory sync, remote access, and Omni Credits
+- [Voice](/products/beacon/voice) — Set up wake words and voice providers
+- [Personas](/products/beacon/personas) — Customize your assistant's personality
+- [Channels](/products/beacon/channels) — Connect Discord, Slack, Telegram, and more
+- [Cloud Features](/products/beacon/cloud-features) — Memory sync, remote access, and Omni Credits
diff --git a/content/docs/grid/beacon/index.mdx b/content/docs/products/beacon/index.mdx
similarity index 84%
rename from content/docs/grid/beacon/index.mdx
rename to content/docs/products/beacon/index.mdx
index c0257b8..bd1350c 100644
--- a/content/docs/grid/beacon/index.mdx
+++ b/content/docs/products/beacon/index.mdx
@@ -66,7 +66,7 @@ import { FaMicrophone, FaComments, FaServer, FaShieldAlt, FaWifi } from "react-i
]}
/>
-**Omni Beacon** is a local-first AI assistant gateway built in Rust. It ships as a single binary with an embedded AI router ([Synapse](/grid/synapse)) — no separate server needed. It runs on your machine or home server, keeping all your data local while providing seamless multi-device access through device-based identity and mDNS discovery.
+**Omni Beacon** is a local-first AI assistant gateway built in Rust. It ships as a single binary with an embedded AI router ([Synapse](/products/synapse)) — no separate server needed. It runs on your machine or home server, keeping all your data local while providing seamless multi-device access through device-based identity and mDNS discovery.
Beacon goes beyond simple chat — it's a complete platform for running AI assistants across voice, 14+ messaging channels, and a WebSocket API, with agentic tool execution, semantic memory, rule-based routing, attachment processing, and extensible skills.
@@ -82,7 +82,7 @@ Beacon is designed around local-first principles:
## Chat
-Talk to your AI assistant through a familiar chat interface or the WebSocket API. Beacon keeps your conversation history locally in SQLite, with support for sessions, threading, and long-term semantic memory with vector search. Synapse runs in-process by default, routing requests to your configured provider. Or use [Omni Credits](/grid/beacon/cloud-features) for a zero-config experience.
+Talk to your AI assistant through a familiar chat interface or the WebSocket API. Beacon keeps your conversation history locally in SQLite, with support for sessions, threading, and long-term semantic memory with vector search. Synapse runs in-process by default, routing requests to your configured provider. Or use [Omni Credits](/products/beacon/cloud-features) for a zero-config experience.
## Agents
@@ -95,9 +95,9 @@ Beacon supports a multi-agent architecture with pluggable execution backends:
## Voice
-Beacon supports hands-free interaction with wake word detection, speech-to-text, and text-to-speech. Say your persona's wake word, ask a question, and hear the answer spoken back. See the [Voice guide](/grid/beacon/voice) for setup details.
+Beacon supports hands-free interaction with wake word detection, speech-to-text, and text-to-speech. Say your persona's wake word, ask a question, and hear the answer spoken back. See the [Voice guide](/products/beacon/voice) for setup details.
-| Component | Local | Embedded (via [Synapse](/grid/synapse)) |
+| Component | Local | Embedded (via [Synapse](/products/synapse)) |
|-----------|-------|----------------------------------------|
| Wake word detection | Yes | - |
| Speech-to-text | - | Whisper, Deepgram |
@@ -105,11 +105,11 @@ Beacon supports hands-free interaction with wake word detection, speech-to-text,
## Personas
-Beacon uses configurable [personas](/grid/beacon/personas) to shape how your assistant sounds and behaves. The default persona is [Orin](/orin), a friendly otter guide, but you can create your own or install community personas from [Manifold](https://manifold.omni.dev). Each persona has its own wake word, voice, and personality. See the [persona.json spec](/codex/persona-json) for advanced customization.
+Beacon uses configurable [personas](/products/beacon/personas) to shape how your assistant sounds and behaves. The default persona is [Orin](/products/orin), a friendly otter guide, but you can create your own or install community personas from [Manifold](https://manifold.omni.dev). Each persona has its own wake word, voice, and personality. See the [persona.json spec](/products/persona-json) for advanced customization.
## Channels
-Connect Beacon to 14+ messaging platforms. Your assistant works consistently across Discord, Slack, Telegram, Teams, Signal, Gmail, IRC, Twilio (SMS), WhatsApp, Matrix, Google Chat, iMessage, Feishu/Lark, and Line. Each channel declares its own capabilities (reactions, streaming, media, etc.). See [Channels](/grid/beacon/channels) for the full list and setup instructions.
+Connect Beacon to 14+ messaging platforms. Your assistant works consistently across Discord, Slack, Telegram, Teams, Signal, Gmail, IRC, Twilio (SMS), WhatsApp, Matrix, Google Chat, iMessage, Feishu/Lark, and Line. Each channel declares its own capabilities (reactions, streaming, media, etc.). See [Channels](/products/beacon/channels) for the full list and setup instructions.
## Routing
@@ -145,4 +145,4 @@ Semantic memory powered by vector embeddings (via OpenAI or compatible providers
4. **Customize**: Pick a persona, set your wake word, and configure voice
5. **Start talking**: Say your wake word and go
-See the [Getting Started guide](/grid/beacon/getting-started) for a detailed walkthrough.
+See the [Getting Started guide](/products/beacon/getting-started) for a detailed walkthrough.
diff --git a/content/docs/grid/beacon/meta.json b/content/docs/products/beacon/meta.json
similarity index 100%
rename from content/docs/grid/beacon/meta.json
rename to content/docs/products/beacon/meta.json
diff --git a/content/docs/grid/beacon/personas.mdx b/content/docs/products/beacon/personas.mdx
similarity index 87%
rename from content/docs/grid/beacon/personas.mdx
rename to content/docs/products/beacon/personas.mdx
index 079d50f..4c5553b 100644
--- a/content/docs/grid/beacon/personas.mdx
+++ b/content/docs/products/beacon/personas.mdx
@@ -7,7 +7,7 @@ Personas define how your AI assistant sounds, behaves, and responds. Each person
## What Is a Persona?
-A persona is a configuration file ([persona.json](/codex/persona-json)) that tells Beacon:
+A persona is a configuration file ([persona.json](/products/persona-json)) that tells Beacon:
- **Who** the assistant is (name, tagline, icon)
- **How** it sounds (wake words, TTS voice, STT provider)
@@ -15,7 +15,7 @@ A persona is a configuration file ([persona.json](/codex/persona-json)) that tel
## Meet Orin
-[Orin](/orin) is Beacon's default persona — a friendly otter guide. Orin comes pre-installed with every gateway.
+[Orin](/products/orin) is Beacon's default persona — a friendly otter guide. Orin comes pre-installed with every gateway.
```json
{
@@ -84,7 +84,7 @@ Adjust the system prompt, tone, and traits:
## Creating a New Persona
1. Set the `BEACON_PERSONAS_DIR` environment variable to a directory of your choice (e.g. `~/.config/omni/beacon/personas/`)
-2. Create a `.json` file in that directory following the [persona.json spec](/codex/persona-json)
+2. Create a `.json` file in that directory following the [persona.json spec](/products/persona-json)
3. Restart the gateway
Personas are flat JSON files (e.g. `my-persona.json`), not subdirectories.
@@ -99,7 +99,7 @@ Browse and install community-created personas from [Manifold](https://manifold.o
4. The persona is downloaded to your gateway and ready to use
- Manifold access requires an Omni account. See [Cloud Features](/grid/beacon/cloud-features) for setup.
+ Manifold access requires an Omni account. See [Cloud Features](/products/beacon/cloud-features) for setup.
## Persona Storage
@@ -115,4 +115,4 @@ Personas are loaded from these sources, in order of priority:
## Advanced
-For the full persona configuration spec, including all available fields and options, see the [persona.json documentation](/codex/persona-json).
+For the full persona configuration spec, including all available fields and options, see the [persona.json documentation](/products/persona-json).
diff --git a/content/docs/grid/beacon/skills-and-hooks.mdx b/content/docs/products/beacon/skills-and-hooks.mdx
similarity index 100%
rename from content/docs/grid/beacon/skills-and-hooks.mdx
rename to content/docs/products/beacon/skills-and-hooks.mdx
diff --git a/content/docs/grid/beacon/voice.mdx b/content/docs/products/beacon/voice.mdx
similarity index 88%
rename from content/docs/grid/beacon/voice.mdx
rename to content/docs/products/beacon/voice.mdx
index 648fc61..ea72a4b 100644
--- a/content/docs/grid/beacon/voice.mdx
+++ b/content/docs/products/beacon/voice.mdx
@@ -10,8 +10,8 @@ Beacon supports hands-free voice interaction — say a wake word, ask a question
Voice interaction follows three steps:
1. **Wake word** — Beacon listens for a trigger phrase (e.g. "Hey Orin") using local detection
-2. **Speech-to-text** — Your spoken question is transcribed via [Synapse](/grid/synapse)
-3. **Text-to-speech** — The assistant's response is spoken aloud via [Synapse](/grid/synapse)
+2. **Speech-to-text** — Your spoken question is transcribed via [Synapse](/products/synapse)
+3. **Text-to-speech** — The assistant's response is spoken aloud via [Synapse](/products/synapse)
Wake word detection always runs locally for instant response. Transcription and speech synthesis route through Synapse, which dispatches to the configured upstream provider.
@@ -25,7 +25,7 @@ Wake word detection always runs locally for instant response. Transcription and
### 1. Configure Synapse
-Voice requires a running [Synapse](/grid/synapse) instance with at least one STT and one TTS provider configured. The simplest setup is an OpenAI key, which covers both:
+Voice requires a running [Synapse](/products/synapse) instance with at least one STT and one TTS provider configured. The simplest setup is an OpenAI key, which covers both:
| Provider | STT | TTS |
|----------|-----|-----|
@@ -33,7 +33,7 @@ Voice requires a running [Synapse](/grid/synapse) instance with at least one STT
| Deepgram | Yes | - |
| ElevenLabs | - | Yes |
-Configure providers in your Synapse config file. See the [Synapse docs](/grid/synapse) for details.
+Configure providers in your Synapse config file. See the [Synapse docs](/products/synapse) for details.
### 2. Enable voice
@@ -45,7 +45,7 @@ Say your persona's wake word (default: **"Hey Orin"**), then ask a question.
## Wake Words
-Each persona has its own wake words defined in its [persona.json](/codex/persona-json):
+Each persona has its own wake words defined in its [persona.json](/products/persona-json):
```json
{
@@ -59,7 +59,7 @@ Wake word detection runs entirely on-device. No audio is sent anywhere until a w
### Customizing wake words
-Edit your persona's `persona.json` to change or add wake words. See [Personas](/grid/beacon/personas) for details on persona customization.
+Edit your persona's `persona.json` to change or add wake words. See [Personas](/products/beacon/personas) for details on persona customization.
## Voice Settings Per Persona
diff --git a/content/docs/armory/eden.mdx b/content/docs/products/eden.mdx
similarity index 100%
rename from content/docs/armory/eden.mdx
rename to content/docs/products/eden.mdx
diff --git a/content/docs/grid/fractal/architecture.mdx b/content/docs/products/fractal/architecture.mdx
similarity index 87%
rename from content/docs/grid/fractal/architecture.mdx
rename to content/docs/products/fractal/architecture.mdx
index 13f7e34..44474af 100644
--- a/content/docs/grid/fractal/architecture.mdx
+++ b/content/docs/products/fractal/architecture.mdx
@@ -15,7 +15,7 @@ to a running service on a public HTTPS URL.
### Project
A **project** is an isolated workspace that groups related services. Each project
-gets its own [environments](/grid/fractal/environments) (such as production and
+gets its own [environments](/products/fractal/environments) (such as production and
staging), its own resources, and its own team access. Services in one project are
isolated from services in another.
@@ -44,19 +44,19 @@ roll back to any previous revision at any time. Each service tracks its own stat
automatically (from your Dockerfile, or with zero configuration).
3. **Roll out** — the new version is deployed with zero downtime and served
over HTTPS. Certificates are provisioned and renewed automatically, including
- for [custom domains](/grid/fractal/custom-domains).
+ for [custom domains](/products/fractal/custom-domains).
4. **Scale** — services scale on demand, and idle services can sleep and wake
instantly on the next request.
For safer releases, Fractal creates an ephemeral
-[preview environment](/grid/fractal/pr-previews) for every pull request, and
+[preview environment](/products/fractal/pr-previews) for every pull request, and
supports weighted canary rollouts.
## Databases
A database is just a service with the **database** type. Fractal provisions a
managed Postgres database with connection pooling, automatic
-[scheduled backups](/grid/fractal/backups), and one-click restore, so there is no
+[scheduled backups](/products/fractal/backups), and one-click restore, so there is no
database to run or maintain yourself. Caches such as Valkey or Redis run as
ordinary image-based services.
@@ -65,5 +65,5 @@ ordinary image-based services.
- **Dashboard** — a visual workflow for deploying, configuring, and monitoring
services, with real-time logs, metrics, and deploy history
- **CLI** — the `fractal` command-line tool for scripting and CI; see the
- [CLI Reference](/grid/fractal/cli-reference)
+ [CLI Reference](/products/fractal/cli-reference)
- **API** — automate everything programmatically with scoped API tokens
diff --git a/content/docs/grid/fractal/backups.mdx b/content/docs/products/fractal/backups.mdx
similarity index 100%
rename from content/docs/grid/fractal/backups.mdx
rename to content/docs/products/fractal/backups.mdx
diff --git a/content/docs/grid/fractal/billing.mdx b/content/docs/products/fractal/billing.mdx
similarity index 97%
rename from content/docs/grid/fractal/billing.mdx
rename to content/docs/products/fractal/billing.mdx
index 51bb39d..79e32d5 100644
--- a/content/docs/grid/fractal/billing.mdx
+++ b/content/docs/products/fractal/billing.mdx
@@ -52,5 +52,5 @@ it, or we warn you before any deletion.
Prefer to run Fractal on infrastructure you own? **Bring Your Own Cloud** offers a
flat per-cluster price with no per-app compute markup, and is rolling out with design
-partners first. See [self-hosting](/grid/fractal/self-hosting) or
+partners first. See [self-hosting](/products/fractal/self-hosting) or
[apply for early access](https://fractal.omni.dev/early-access).
diff --git a/content/docs/grid/fractal/cli-reference.mdx b/content/docs/products/fractal/cli-reference.mdx
similarity index 100%
rename from content/docs/grid/fractal/cli-reference.mdx
rename to content/docs/products/fractal/cli-reference.mdx
diff --git a/content/docs/grid/fractal/configuration.mdx b/content/docs/products/fractal/configuration.mdx
similarity index 95%
rename from content/docs/grid/fractal/configuration.mdx
rename to content/docs/products/fractal/configuration.mdx
index 00b1d1b..72d93ef 100644
--- a/content/docs/grid/fractal/configuration.mdx
+++ b/content/docs/products/fractal/configuration.mdx
@@ -90,7 +90,7 @@ service and provisions TLS automatically.
| Field | Description |
|-------|-------------|
| `port` | Port the service listens on |
-| `domain` | Custom domain (see [Custom Domains](/grid/fractal/custom-domains)) |
+| `domain` | Custom domain (see [Custom Domains](/products/fractal/custom-domains)) |
| `tls` | Enable TLS |
| `www` | `redirect` to send `www.` to the apex, or `off` |
@@ -115,5 +115,5 @@ fractal env set-secret api DATABASE_URL --secret main-db-app --key uri
```
Backups are enabled by default and managed from the **Backups** tab. See
-[Backups](/grid/fractal/backups) for scheduled backups, on-demand backups, and
+[Backups](/products/fractal/backups) for scheduled backups, on-demand backups, and
restores.
diff --git a/content/docs/grid/fractal/custom-domains.mdx b/content/docs/products/fractal/custom-domains.mdx
similarity index 100%
rename from content/docs/grid/fractal/custom-domains.mdx
rename to content/docs/products/fractal/custom-domains.mdx
diff --git a/content/docs/grid/fractal/environments.mdx b/content/docs/products/fractal/environments.mdx
similarity index 100%
rename from content/docs/grid/fractal/environments.mdx
rename to content/docs/products/fractal/environments.mdx
diff --git a/content/docs/grid/fractal/getting-started.mdx b/content/docs/products/fractal/getting-started.mdx
similarity index 92%
rename from content/docs/grid/fractal/getting-started.mdx
rename to content/docs/products/fractal/getting-started.mdx
index 9e516bf..8bf8325 100644
--- a/content/docs/grid/fractal/getting-started.mdx
+++ b/content/docs/products/fractal/getting-started.mdx
@@ -31,7 +31,7 @@ From the project, choose **New Service** and pick a source:
By default a web service comes up reachable over HTTPS at a generated
`*.up.fractal.omni.dev` URL (toggle this off to keep it internal, or add your own
-domain later; see [Custom Domains](/grid/fractal/custom-domains)).
+domain later; see [Custom Domains](/products/fractal/custom-domains)).
### Private networking
@@ -77,6 +77,6 @@ fractal scale my-api --replicas 3
fractal restart my-api
```
-See the [CLI Reference](/grid/fractal/cli-reference) for the full command set and
-the [Service Configuration](/grid/fractal/configuration) reference for every
+See the [CLI Reference](/products/fractal/cli-reference) for the full command set and
+the [Service Configuration](/products/fractal/configuration) reference for every
setting a service supports.
diff --git a/content/docs/grid/fractal/index.mdx b/content/docs/products/fractal/index.mdx
similarity index 92%
rename from content/docs/grid/fractal/index.mdx
rename to content/docs/products/fractal/index.mdx
index 7883de5..ee2c40d 100644
--- a/content/docs/grid/fractal/index.mdx
+++ b/content/docs/products/fractal/index.mdx
@@ -73,6 +73,6 @@ managed database, autoscaling, and push-to-deploy built in. Work visually in the
## Get started
Deploy your first service in a few minutes with the
-[quickstart](/grid/fractal/getting-started), or explore
-[configuration](/grid/fractal/configuration), [environments](/grid/fractal/environments),
-[custom domains](/grid/fractal/custom-domains), and [backups](/grid/fractal/backups).
+[quickstart](/products/fractal/getting-started), or explore
+[configuration](/products/fractal/configuration), [environments](/products/fractal/environments),
+[custom domains](/products/fractal/custom-domains), and [backups](/products/fractal/backups).
diff --git a/content/docs/grid/fractal/meta.json b/content/docs/products/fractal/meta.json
similarity index 100%
rename from content/docs/grid/fractal/meta.json
rename to content/docs/products/fractal/meta.json
diff --git a/content/docs/grid/fractal/pr-previews.mdx b/content/docs/products/fractal/pr-previews.mdx
similarity index 100%
rename from content/docs/grid/fractal/pr-previews.mdx
rename to content/docs/products/fractal/pr-previews.mdx
diff --git a/content/docs/grid/fractal/self-hosting.mdx b/content/docs/products/fractal/self-hosting.mdx
similarity index 91%
rename from content/docs/grid/fractal/self-hosting.mdx
rename to content/docs/products/fractal/self-hosting.mdx
index 5763cfe..8fba8e2 100644
--- a/content/docs/grid/fractal/self-hosting.mdx
+++ b/content/docs/products/fractal/self-hosting.mdx
@@ -15,7 +15,7 @@ get set up.
## What you need
- A Kubernetes cluster you administer, and a kubeconfig with access to it.
-- The [`fractal` CLI](/grid/fractal/cli-reference), authenticated with
+- The [`fractal` CLI](/products/fractal/cli-reference), authenticated with
`fractal auth login`.
## Connect a cluster
@@ -35,7 +35,7 @@ fractal cluster connect --name my-cluster --registry registry.example.com/myorg
With `--registry`, the operator builds to and pulls runtime images from your
registry instead of Omni's, so image data and credentials never leave your
-environment. See [Bring your own registry](/grid/fractal/cli-reference) in the CLI
+environment. See [Bring your own registry](/products/fractal/cli-reference) in the CLI
reference for details.
After applying the printed manifest, confirm the operator is running:
diff --git a/content/docs/grid/fractal/shell.mdx b/content/docs/products/fractal/shell.mdx
similarity index 100%
rename from content/docs/grid/fractal/shell.mdx
rename to content/docs/products/fractal/shell.mdx
diff --git a/content/docs/codex/garden/decentralized-web.mdx b/content/docs/products/garden/decentralized-web.mdx
similarity index 100%
rename from content/docs/codex/garden/decentralized-web.mdx
rename to content/docs/products/garden/decentralized-web.mdx
diff --git a/content/docs/codex/garden/garden.json b/content/docs/products/garden/garden.json
similarity index 100%
rename from content/docs/codex/garden/garden.json
rename to content/docs/products/garden/garden.json
diff --git a/content/docs/codex/garden/index.mdx b/content/docs/products/garden/index.mdx
similarity index 100%
rename from content/docs/codex/garden/index.mdx
rename to content/docs/products/garden/index.mdx
diff --git a/content/docs/grid/kiln/getting-started.mdx b/content/docs/products/kiln/getting-started.mdx
similarity index 91%
rename from content/docs/grid/kiln/getting-started.mdx
rename to content/docs/products/kiln/getting-started.mdx
index 70a5a6f..88d8113 100644
--- a/content/docs/grid/kiln/getting-started.mdx
+++ b/content/docs/products/kiln/getting-started.mdx
@@ -53,5 +53,5 @@ kiln build --source https://github.com/you/my-app --ref main --dest registry.exa
## What next
- The generated Dockerfile is yours to keep and edit, see
- [how it works](/grid/kiln/how-it-works)
-- Check the [supported languages](/grid/kiln/languages)
+ [how it works](/products/kiln/how-it-works)
+- Check the [supported languages](/products/kiln/languages)
diff --git a/content/docs/grid/kiln/how-it-works.mdx b/content/docs/products/kiln/how-it-works.mdx
similarity index 100%
rename from content/docs/grid/kiln/how-it-works.mdx
rename to content/docs/products/kiln/how-it-works.mdx
diff --git a/content/docs/grid/kiln/index.mdx b/content/docs/products/kiln/index.mdx
similarity index 92%
rename from content/docs/grid/kiln/index.mdx
rename to content/docs/products/kiln/index.mdx
index b4d1583..0dcb872 100644
--- a/content/docs/grid/kiln/index.mdx
+++ b/content/docs/products/kiln/index.mdx
@@ -65,6 +65,6 @@ the output is a standard artifact that works everywhere.
## Next steps
-- [Get started](/grid/kiln/getting-started) and build your first image
-- See the [supported languages](/grid/kiln/languages)
-- Learn [how it works](/grid/kiln/how-it-works)
+- [Get started](/products/kiln/getting-started) and build your first image
+- See the [supported languages](/products/kiln/languages)
+- Learn [how it works](/products/kiln/how-it-works)
diff --git a/content/docs/grid/kiln/languages.mdx b/content/docs/products/kiln/languages.mdx
similarity index 100%
rename from content/docs/grid/kiln/languages.mdx
rename to content/docs/products/kiln/languages.mdx
diff --git a/content/docs/grid/kiln/meta.json b/content/docs/products/kiln/meta.json
similarity index 100%
rename from content/docs/grid/kiln/meta.json
rename to content/docs/products/kiln/meta.json
diff --git a/content/docs/codex/life-json.mdx b/content/docs/products/life-json.mdx
similarity index 90%
rename from content/docs/codex/life-json.mdx
rename to content/docs/products/life-json.mdx
index cbf38cc..48831b1 100644
--- a/content/docs/codex/life-json.mdx
+++ b/content/docs/products/life-json.mdx
@@ -75,7 +75,7 @@ life.json uses a modular slice architecture. All slices are optional—include o
## The Assistants Bridge
-The `assistants` slice is the bridge between life.json and [persona.json](/codex/persona-json). While persona.json defines *what a persona is*, life.json stores *your relationship with it*:
+The `assistants` slice is the bridge between life.json and [persona.json](/products/persona-json). While persona.json defines *what a persona is*, life.json stores *your relationship with it*:
```
persona.json (developer-owned) life.json (user-owned)
@@ -100,13 +100,13 @@ Each assistant entry includes:
| Product | Reads | Writes |
|---------|-------|--------|
-| [Gatekeeper](/armory/hidra/gatekeeper) | identity, preferences, assistants | identity, preferences, assistants (host) |
-| [Beacon](/grid/beacon) | identity, preferences, calendar | assistants |
-| [Blink](/kindred/blink) | identity, social | social |
-| [Moment](/kindred/moment) | relationships | memories, relationships |
-| [Trellis](/fabric/trellis) | — | knowledge |
-| [MyFi](/core/myfi) | — | finances |
-| [Forever](/codex/forever) | identity, accounts, relationships | — |
+| [Gatekeeper](/products/hidra/gatekeeper) | identity, preferences, assistants | identity, preferences, assistants (host) |
+| [Beacon](/products/beacon) | identity, preferences, calendar | assistants |
+| [Blink](/products/blink) | identity, social | social |
+| [Moment](/products/moment) | relationships | memories, relationships |
+| [Trellis](/products/trellis) | — | knowledge |
+| [MyFi](/products/myfi) | — | finances |
+| [Forever](/products/forever) | identity, accounts, relationships | — |
## Examples
diff --git a/content/docs/armory/omni-cli/api.mdx b/content/docs/products/omni-cli/api.mdx
similarity index 100%
rename from content/docs/armory/omni-cli/api.mdx
rename to content/docs/products/omni-cli/api.mdx
diff --git a/content/docs/armory/omni-cli/configuration.mdx b/content/docs/products/omni-cli/configuration.mdx
similarity index 100%
rename from content/docs/armory/omni-cli/configuration.mdx
rename to content/docs/products/omni-cli/configuration.mdx
diff --git a/content/docs/armory/omni-cli/index.mdx b/content/docs/products/omni-cli/index.mdx
similarity index 100%
rename from content/docs/armory/omni-cli/index.mdx
rename to content/docs/products/omni-cli/index.mdx
diff --git a/content/docs/armory/omni-cli/lsp.mdx b/content/docs/products/omni-cli/lsp.mdx
similarity index 100%
rename from content/docs/armory/omni-cli/lsp.mdx
rename to content/docs/products/omni-cli/lsp.mdx
diff --git a/content/docs/armory/omni-cli/providers.mdx b/content/docs/products/omni-cli/providers.mdx
similarity index 93%
rename from content/docs/armory/omni-cli/providers.mdx
rename to content/docs/products/omni-cli/providers.mdx
index 8122618..a56abec 100644
--- a/content/docs/armory/omni-cli/providers.mdx
+++ b/content/docs/products/omni-cli/providers.mdx
@@ -11,7 +11,7 @@ Omni CLI supports multiple LLM providers through a unified interface, allowing y
| Provider | Models | API Key Env |
|----------|--------|-------------|
-| **[Synapse](/grid/synapse)** | All models via unified routing | (none, local) |
+| **[Synapse](/products/synapse)** | All models via unified routing | (none, local) |
| **Anthropic** | Claude 4, Claude 3.5, Claude 3 | `ANTHROPIC_API_KEY` |
| **OpenAI** | GPT-4o, GPT-4, GPT-3.5 | `OPENAI_API_KEY` |
| **Google** | Gemini 2.0, Gemini 1.5 | `GOOGLE_API_KEY` |
@@ -144,7 +144,7 @@ Available models:
### Synapse (Recommended)
-Route through [Synapse](/grid/synapse) for unified provider management. Synapse handles API keys, model routing, and provider dispatch — configure your keys once in Synapse and use any model from any provider:
+Route through [Synapse](/products/synapse) for unified provider management. Synapse handles API keys, model routing, and provider dispatch — configure your keys once in Synapse and use any model from any provider:
```toml
[agent]
diff --git a/content/docs/armory/omni-cli/sessions.mdx b/content/docs/products/omni-cli/sessions.mdx
similarity index 100%
rename from content/docs/armory/omni-cli/sessions.mdx
rename to content/docs/products/omni-cli/sessions.mdx
diff --git a/content/docs/armory/omni-cli/skills.mdx b/content/docs/products/omni-cli/skills.mdx
similarity index 100%
rename from content/docs/armory/omni-cli/skills.mdx
rename to content/docs/products/omni-cli/skills.mdx
diff --git a/content/docs/armory/omni-cli/tools.mdx b/content/docs/products/omni-cli/tools.mdx
similarity index 100%
rename from content/docs/armory/omni-cli/tools.mdx
rename to content/docs/products/omni-cli/tools.mdx
diff --git a/content/docs/armory/omni-terminal/android.mdx b/content/docs/products/omni-terminal/android.mdx
similarity index 96%
rename from content/docs/armory/omni-terminal/android.mdx
rename to content/docs/products/omni-terminal/android.mdx
index a8e0da0..a853a9a 100644
--- a/content/docs/armory/omni-terminal/android.mdx
+++ b/content/docs/products/omni-terminal/android.mdx
@@ -38,7 +38,7 @@ ssh user@example.com
### Remote Connections
-Connect to a remote [Omni Terminal web server](/armory/omni-terminal/web) over secure WebSocket (WSS/TLS). Enter the server URL on the home screen and tap **Connect**.
+Connect to a remote [Omni Terminal web server](/products/omni-terminal/web) over secure WebSocket (WSS/TLS). Enter the server URL on the home screen and tap **Connect**.
URL formats accepted:
diff --git a/content/docs/armory/omni-terminal/configuration.mdx b/content/docs/products/omni-terminal/configuration.mdx
similarity index 100%
rename from content/docs/armory/omni-terminal/configuration.mdx
rename to content/docs/products/omni-terminal/configuration.mdx
diff --git a/content/docs/armory/omni-terminal/index.mdx b/content/docs/products/omni-terminal/index.mdx
similarity index 91%
rename from content/docs/armory/omni-terminal/index.mdx
rename to content/docs/products/omni-terminal/index.mdx
index c6bf865..ce05a57 100644
--- a/content/docs/armory/omni-terminal/index.mdx
+++ b/content/docs/products/omni-terminal/index.mdx
@@ -102,11 +102,11 @@ cargo build --release -p omni-terminal
### Web
-See the [Web](/armory/omni-terminal/web) page for self-hosting the web terminal.
+See the [Web](/products/omni-terminal/web) page for self-hosting the web terminal.
### Android
-See the [Android](/armory/omni-terminal/android) page for building and installing the Android app.
+See the [Android](/products/omni-terminal/android) page for building and installing the Android app.
## Quick Start
@@ -175,9 +175,9 @@ omni-terminal -e htop
| Subcommand | Description |
|------------|-------------|
-| `serve` | Start the [web terminal server](/armory/omni-terminal/web) with embedded WASM frontend |
+| `serve` | Start the [web terminal server](/products/omni-terminal/web) with embedded WASM frontend |
-See [`omni-terminal serve --help`](/armory/omni-terminal/web#options) for server-specific options.
+See [`omni-terminal serve --help`](/products/omni-terminal/web#options) for server-specific options.
## Platforms
@@ -186,7 +186,7 @@ See [`omni-terminal serve --help`](/armory/omni-terminal/web#options) for server
| macOS | Experimental | Native WebGPU |
| Linux (X11/Wayland) | Experimental | Native WebGPU |
| Windows | Experimental | Native WebGPU |
-| [Web (WASM)](/armory/omni-terminal/web) | Experimental | WebGPU in browser + WebSocket PTY |
-| [Android](/armory/omni-terminal/android) | Experimental | JNI + PRoot Arch Linux |
+| [Web (WASM)](/products/omni-terminal/web) | Experimental | WebGPU in browser + WebSocket PTY |
+| [Android](/products/omni-terminal/android) | Experimental | JNI + PRoot Arch Linux |
-See [Keybindings](/armory/omni-terminal/keybindings) for the full keybinding reference and [Configuration](/armory/omni-terminal/configuration) for all config options.
+See [Keybindings](/products/omni-terminal/keybindings) for the full keybinding reference and [Configuration](/products/omni-terminal/configuration) for all config options.
diff --git a/content/docs/armory/omni-terminal/keybindings.mdx b/content/docs/products/omni-terminal/keybindings.mdx
similarity index 98%
rename from content/docs/armory/omni-terminal/keybindings.mdx
rename to content/docs/products/omni-terminal/keybindings.mdx
index 76ca582..6a5590d 100644
--- a/content/docs/armory/omni-terminal/keybindings.mdx
+++ b/content/docs/products/omni-terminal/keybindings.mdx
@@ -5,7 +5,7 @@ description: Default keybindings reference for Omni Terminal
# Keybindings
-Omni Terminal ships with sensible default keybindings for macOS and Linux/Windows. All bindings can be overridden in your [config file](/armory/omni-terminal/configuration#custom-keybindings).
+Omni Terminal ships with sensible default keybindings for macOS and Linux/Windows. All bindings can be overridden in your [config file](/products/omni-terminal/configuration#custom-keybindings).
## macOS
diff --git a/content/docs/armory/omni-terminal/meta.json b/content/docs/products/omni-terminal/meta.json
similarity index 100%
rename from content/docs/armory/omni-terminal/meta.json
rename to content/docs/products/omni-terminal/meta.json
diff --git a/content/docs/armory/omni-terminal/web.mdx b/content/docs/products/omni-terminal/web.mdx
similarity index 94%
rename from content/docs/armory/omni-terminal/web.mdx
rename to content/docs/products/omni-terminal/web.mdx
index e181b82..2a9b57c 100644
--- a/content/docs/armory/omni-terminal/web.mdx
+++ b/content/docs/products/omni-terminal/web.mdx
@@ -84,7 +84,7 @@ just wasm-pack
## Connecting from Android
-The [Android app](/armory/omni-terminal/android) can connect to a self-hosted web server as a remote terminal client. Enter the server URL (e.g. `wss://192.168.1.100:3000/ws`) on the Android home screen and tap **Connect**.
+The [Android app](/products/omni-terminal/android) can connect to a self-hosted web server as a remote terminal client. Enter the server URL (e.g. `wss://192.168.1.100:3000/ws`) on the Android home screen and tap **Connect**.
## Protocol
diff --git a/content/docs/orin.mdx b/content/docs/products/orin.mdx
similarity index 91%
rename from content/docs/orin.mdx
rename to content/docs/products/orin.mdx
index 735e48a..9149ce6 100644
--- a/content/docs/orin.mdx
+++ b/content/docs/products/orin.mdx
@@ -67,12 +67,12 @@ import { FaOtter } from "react-icons/fa6";
Orin is Omni's beloved otter mascot who embodies the playful, intelligent, and helpful spirit of the Omni brand. As a character:
- **Mascot**: Orin serves as the official face of Omni, appearing in branding, documentation, and communications
-- **AI Assistant**: Orin is a general-purpose AI assistant powered by [🗼 Beacon](/grid/beacon), capable of helping with any task while specializing in the Omni ecosystem
+- **AI Assistant**: Orin is a general-purpose AI assistant powered by [🗼 Beacon](/products/beacon), capable of helping with any task while specializing in the Omni ecosystem
- **Ecosystem Expert**: Orin has comprehensive knowledge about all Omni products, services, and their relationships
## Powered by Beacon
-Orin is the flagship persona running on [🗼 Beacon](/grid/beacon), Omni's local-first voice and messaging gateway. This architecture means:
+Orin is the flagship persona running on [🗼 Beacon](/products/beacon), Omni's local-first voice and messaging gateway. This architecture means:
- **Local-First**: Orin runs on your machine - your data stays with you
- **Device Identity**: No accounts needed - devices authenticate with cryptographic keys
@@ -97,4 +97,4 @@ Orin's AI assistant capabilities include:
Detailed documentation about interacting with Orin as an AI assistant will be available soon! Meanwhile:
-- Learn about the underlying platform: [🗼 Beacon](/grid/beacon)
+- Learn about the underlying platform: [🗼 Beacon](/products/beacon)
diff --git a/content/docs/codex/persona-json.mdx b/content/docs/products/persona-json.mdx
similarity index 95%
rename from content/docs/codex/persona-json.mdx
rename to content/docs/products/persona-json.mdx
index ecb5d38..3effcf4 100644
--- a/content/docs/codex/persona-json.mdx
+++ b/content/docs/products/persona-json.mdx
@@ -43,7 +43,7 @@ import { FaRobot, FaLink, FaMask } from "react-icons/fa";
## Relationship to life.json
-persona.json complements [life.json](/codex/life-json) as twin specifications for identity duality:
+persona.json complements [life.json](/products/life-json) as twin specifications for identity duality:
| Spec | Describes | Owned By |
|------|-----------|----------|
@@ -171,8 +171,8 @@ The `capabilities` slice controls what a persona can do across channels:
| Product | Usage |
|---------|-------|
-| [Beacon](/grid/beacon) | Loads persona definitions for AI assistants |
-| [Orin](/orin) | Defined as a persona.json instance |
+| [Beacon](/products/beacon) | Loads persona definitions for AI assistants |
+| [Orin](/products/orin) | Defined as a persona.json instance |
## Getting Started
diff --git a/content/docs/reality/rdk.mdx b/content/docs/products/rdk.mdx
similarity index 100%
rename from content/docs/reality/rdk.mdx
rename to content/docs/products/rdk.mdx
diff --git a/content/docs/core/runa/concepts.mdx b/content/docs/products/runa/concepts.mdx
similarity index 100%
rename from content/docs/core/runa/concepts.mdx
rename to content/docs/products/runa/concepts.mdx
diff --git a/content/docs/core/runa/index.mdx b/content/docs/products/runa/index.mdx
similarity index 100%
rename from content/docs/core/runa/index.mdx
rename to content/docs/products/runa/index.mdx
diff --git a/content/docs/core/runa/permissions.mdx b/content/docs/products/runa/permissions.mdx
similarity index 100%
rename from content/docs/core/runa/permissions.mdx
rename to content/docs/products/runa/permissions.mdx
diff --git a/content/docs/grid/synapse/api.mdx b/content/docs/products/synapse/api.mdx
similarity index 92%
rename from content/docs/grid/synapse/api.mdx
rename to content/docs/products/synapse/api.mdx
index c215348..5dd44ca 100644
--- a/content/docs/grid/synapse/api.mdx
+++ b/content/docs/products/synapse/api.mdx
@@ -140,7 +140,7 @@ Response:
## POST /v1/embeddings
-Generate text embeddings. Requires an [embeddings provider](/grid/synapse/providers#embeddings-providers) configured.
+Generate text embeddings. Requires an [embeddings provider](/products/synapse/providers#embeddings-providers) configured.
```bash
curl http://localhost:6000/v1/embeddings \
@@ -174,7 +174,7 @@ Response:
## POST /v1/images/generations
-Generate images. Requires an [image generation provider](/grid/synapse/providers#image-generation-providers) configured.
+Generate images. Requires an [image generation provider](/products/synapse/providers#image-generation-providers) configured.
```bash
curl http://localhost:6000/v1/images/generations \
@@ -203,7 +203,7 @@ Response:
## POST /v1/audio/transcriptions
-Speech-to-text transcription. Requires an [STT provider](/grid/synapse/providers#stt-providers) configured. Accepts multipart form data.
+Speech-to-text transcription. Requires an [STT provider](/products/synapse/providers#stt-providers) configured. Accepts multipart form data.
```bash
curl http://localhost:6000/v1/audio/transcriptions \
@@ -222,7 +222,7 @@ Response:
## POST /v1/audio/speech
-Text-to-speech synthesis. Requires a [TTS provider](/grid/synapse/providers#tts-providers) configured. Returns audio bytes.
+Text-to-speech synthesis. Requires a [TTS provider](/products/synapse/providers#tts-providers) configured. Returns audio bytes.
```bash
curl http://localhost:6000/v1/audio/speech \
@@ -238,7 +238,7 @@ curl http://localhost:6000/v1/audio/speech \
## POST /mcp/tools/list
-List all tools available from configured [MCP servers](/grid/synapse/mcp).
+List all tools available from configured [MCP servers](/products/synapse/mcp).
```bash
curl http://localhost:6000/mcp/tools/list \
diff --git a/content/docs/grid/synapse/authentication.mdx b/content/docs/products/synapse/authentication.mdx
similarity index 94%
rename from content/docs/grid/synapse/authentication.mdx
rename to content/docs/products/synapse/authentication.mdx
index 5129360..0c47547 100644
--- a/content/docs/grid/synapse/authentication.mdx
+++ b/content/docs/products/synapse/authentication.mdx
@@ -104,12 +104,12 @@ When set, Synapse uses the value of this header as the client identifier, regard
## Security Best Practices
-1. **Use TLS in production** -- configure TLS in the [server section](/grid/synapse/configuration#tls) or terminate TLS at a reverse proxy
+1. **Use TLS in production** -- configure TLS in the [server section](/products/synapse/configuration#tls) or terminate TLS at a reverse proxy
2. **Rotate API keys regularly** -- use the Omni dashboard or API to rotate keys without downtime
3. **Use JWT for multi-tenant deployments** -- JWTs carry identity and scope, reducing key management overhead
4. **Enable CSRF for browser access** -- protect against cross-site request forgery
-5. **Limit CORS origins** -- only allow trusted origins in [CORS configuration](/grid/synapse/configuration#cors)
-6. **Monitor authentication failures** -- use [telemetry](/grid/synapse/telemetry) to track and alert on auth failures
+5. **Limit CORS origins** -- only allow trusted origins in [CORS configuration](/products/synapse/configuration#cors)
+6. **Monitor authentication failures** -- use [telemetry](/products/synapse/telemetry) to track and alert on auth failures
7. **Never log API keys or tokens** -- Synapse redacts sensitive values in logs by default
## Unauthenticated Endpoints
diff --git a/content/docs/grid/synapse/billing.mdx b/content/docs/products/synapse/billing.mdx
similarity index 97%
rename from content/docs/grid/synapse/billing.mdx
rename to content/docs/products/synapse/billing.mdx
index e87fad5..72d6d0a 100644
--- a/content/docs/grid/synapse/billing.mdx
+++ b/content/docs/products/synapse/billing.mdx
@@ -82,4 +82,4 @@ When self-hosting, billing integration is optional. Without Aether configured:
- Usage events are still recorded locally in the database
- Set `BILLING_BASE_URL` to connect to your own Aether instance
-See the [self-hosting guide](/grid/synapse/self-hosting) for configuration details.
+See the [self-hosting guide](/products/synapse/self-hosting) for configuration details.
diff --git a/content/docs/grid/synapse/configuration.mdx b/content/docs/products/synapse/configuration.mdx
similarity index 98%
rename from content/docs/grid/synapse/configuration.mdx
rename to content/docs/products/synapse/configuration.mdx
index d9ab5cb..52172b4 100644
--- a/content/docs/grid/synapse/configuration.mdx
+++ b/content/docs/products/synapse/configuration.mdx
@@ -348,4 +348,4 @@ type = "openai"
api_key = "{{ env.OPENAI_API_KEY }}"
```
-See also: [Routing](/grid/synapse/routing) for model selection configuration, [Telemetry](/grid/synapse/telemetry) for OpenTelemetry setup.
+See also: [Routing](/products/synapse/routing) for model selection configuration, [Telemetry](/products/synapse/telemetry) for OpenTelemetry setup.
diff --git a/content/docs/grid/synapse/getting-started.mdx b/content/docs/products/synapse/getting-started.mdx
similarity index 86%
rename from content/docs/grid/synapse/getting-started.mdx
rename to content/docs/products/synapse/getting-started.mdx
index 4748379..957bc2a 100644
--- a/content/docs/grid/synapse/getting-started.mdx
+++ b/content/docs/products/synapse/getting-started.mdx
@@ -20,7 +20,7 @@ curl https://gateway.synapse.omni.dev/v1/chat/completions \
## Self-Hosted
-Synapse can be self-hosted from source, via Docker, or with prebuilt binaries. See the [Self-Hosting guide](/grid/synapse/self-hosting) for installation options.
+Synapse can be self-hosted from source, via Docker, or with prebuilt binaries. See the [Self-Hosting guide](/products/synapse/self-hosting) for installation options.
## Minimal Configuration
@@ -136,7 +136,7 @@ api_key = "{{ env.OPENAI_API_KEY }}"
## Next Steps
-- [Configuration](/grid/synapse/configuration) -- Full TOML reference with rate limits, failover, and TLS
-- [Routing](/grid/synapse/routing) -- Intelligent model selection strategies
-- [Providers](/grid/synapse/providers) -- All supported providers across every modality
-- [API Reference](/grid/synapse/api) -- Complete endpoint documentation with examples
+- [Configuration](/products/synapse/configuration) -- Full TOML reference with rate limits, failover, and TLS
+- [Routing](/products/synapse/routing) -- Intelligent model selection strategies
+- [Providers](/products/synapse/providers) -- All supported providers across every modality
+- [API Reference](/products/synapse/api) -- Complete endpoint documentation with examples
diff --git a/content/docs/grid/synapse/guardrails.mdx b/content/docs/products/synapse/guardrails.mdx
similarity index 98%
rename from content/docs/grid/synapse/guardrails.mdx
rename to content/docs/products/synapse/guardrails.mdx
index 68ba017..08dbb79 100644
--- a/content/docs/grid/synapse/guardrails.mdx
+++ b/content/docs/products/synapse/guardrails.mdx
@@ -181,4 +181,4 @@ guardrail blocked request rule=prompt-injection reason="matched keyword: jailb
guardrail warning rule=pii-email-warn reason="detected Email pattern"
```
-These events include the rule name and the specific match reason, making them easy to filter in your log aggregator or alerting system. See [Telemetry](/grid/synapse/telemetry) for configuration details.
+These events include the rule name and the specific match reason, making them easy to filter in your log aggregator or alerting system. See [Telemetry](/products/synapse/telemetry) for configuration details.
diff --git a/content/docs/grid/synapse/index.mdx b/content/docs/products/synapse/index.mdx
similarity index 95%
rename from content/docs/grid/synapse/index.mdx
rename to content/docs/products/synapse/index.mdx
index e6d6b52..5b57daa 100644
--- a/content/docs/grid/synapse/index.mdx
+++ b/content/docs/products/synapse/index.mdx
@@ -93,8 +93,8 @@ Without Synapse, each application manages its own provider keys, API clients, an
Synapse is used by:
-- **[Beacon](/grid/beacon)** -- routes all LLM, STT, and TTS traffic through Synapse
-- **[Omni CLI](/armory/omni-cli)** -- uses Synapse as an LLM provider via `provider = "synapse"`
+- **[Beacon](/products/beacon)** -- routes all LLM, STT, and TTS traffic through Synapse
+- **[Omni CLI](/products/omni-cli)** -- uses Synapse as an LLM provider via `provider = "synapse"`
- **Any OpenAI-compatible client** -- point your base URL to Synapse and authenticate with your `synapse_` API key
## Architecture
diff --git a/content/docs/grid/synapse/mcp.mdx b/content/docs/products/synapse/mcp.mdx
similarity index 97%
rename from content/docs/grid/synapse/mcp.mdx
rename to content/docs/products/synapse/mcp.mdx
index e96712c..e14471a 100644
--- a/content/docs/grid/synapse/mcp.mdx
+++ b/content/docs/products/synapse/mcp.mdx
@@ -143,4 +143,4 @@ Synapse exposes three endpoints for MCP tool interaction:
| `POST /mcp/tools/call` | Execute a tool by name |
| `GET /mcp/search` | Search available tools by query |
-See the [API Reference](/grid/synapse/api#post-mcptoolslist) for request and response examples.
+See the [API Reference](/products/synapse/api#post-mcptoolslist) for request and response examples.
diff --git a/content/docs/grid/synapse/meta.json b/content/docs/products/synapse/meta.json
similarity index 100%
rename from content/docs/grid/synapse/meta.json
rename to content/docs/products/synapse/meta.json
diff --git a/content/docs/grid/synapse/providers.mdx b/content/docs/products/synapse/providers.mdx
similarity index 100%
rename from content/docs/grid/synapse/providers.mdx
rename to content/docs/products/synapse/providers.mdx
diff --git a/content/docs/grid/synapse/quickstart.mdx b/content/docs/products/synapse/quickstart.mdx
similarity index 84%
rename from content/docs/grid/synapse/quickstart.mdx
rename to content/docs/products/synapse/quickstart.mdx
index f237fba..e81662c 100644
--- a/content/docs/grid/synapse/quickstart.mdx
+++ b/content/docs/products/synapse/quickstart.mdx
@@ -74,7 +74,7 @@ Use `provider/model` format for explicit routing:
| `openai/gpt-4o-mini` | OpenAI |
| `openai/o3-mini` | OpenAI |
-Or use virtual names for [smart routing](/grid/synapse/routing):
+Or use virtual names for [smart routing](/products/synapse/routing):
| Name | Behavior |
|------|----------|
@@ -85,7 +85,7 @@ Or use virtual names for [smart routing](/grid/synapse/routing):
## Next steps
-- [Configuration](/grid/synapse/configuration) -- full TOML reference
-- [Routing](/grid/synapse/routing) -- customize how requests are routed
-- [Providers](/grid/synapse/providers) -- add your own API keys for BYOK mode
-- [Self-hosting](/grid/synapse/self-hosting) -- run Synapse on your own infrastructure
+- [Configuration](/products/synapse/configuration) -- full TOML reference
+- [Routing](/products/synapse/routing) -- customize how requests are routed
+- [Providers](/products/synapse/providers) -- add your own API keys for BYOK mode
+- [Self-hosting](/products/synapse/self-hosting) -- run Synapse on your own infrastructure
diff --git a/content/docs/grid/synapse/routing.mdx b/content/docs/products/synapse/routing.mdx
similarity index 98%
rename from content/docs/grid/synapse/routing.mdx
rename to content/docs/products/synapse/routing.mdx
index 428abf2..f3ff698 100644
--- a/content/docs/grid/synapse/routing.mdx
+++ b/content/docs/products/synapse/routing.mdx
@@ -189,4 +189,4 @@ curl http://localhost:6000/v1/chat/completions \
If a request specifies a model name that doesn't match any virtual name, Synapse passes it through to the appropriate provider based on model name prefix matching (e.g. `claude-*` routes to Anthropic, `gpt-*` routes to OpenAI) or explicit `provider/model` format (e.g. `anthropic/claude-sonnet-4-20250514`).
-See also: [Providers](/grid/synapse/providers) for configuring the upstream providers that routing selects from.
+See also: [Providers](/products/synapse/providers) for configuring the upstream providers that routing selects from.
diff --git a/content/docs/grid/synapse/self-hosting.mdx b/content/docs/products/synapse/self-hosting.mdx
similarity index 98%
rename from content/docs/grid/synapse/self-hosting.mdx
rename to content/docs/products/synapse/self-hosting.mdx
index fba5dd8..4d51a19 100644
--- a/content/docs/grid/synapse/self-hosting.mdx
+++ b/content/docs/products/synapse/self-hosting.mdx
@@ -133,7 +133,7 @@ kubectl exec -it deploy/synapse-api -- bun db:migrate
The gateway reads its config from `/etc/synapse.toml` (mounted from `config/synapse.toml` in the compose file). Copy `config/synapse.example.toml` to `config/synapse.toml` to customize providers, routing, MCP servers, and failover.
-See [Configuration](/grid/synapse/configuration) for the full TOML reference.
+See [Configuration](/products/synapse/configuration) for the full TOML reference.
## Optional integrations
diff --git a/content/docs/grid/synapse/telemetry.mdx b/content/docs/products/synapse/telemetry.mdx
similarity index 98%
rename from content/docs/grid/synapse/telemetry.mdx
rename to content/docs/products/synapse/telemetry.mdx
index e62e064..39ebbfa 100644
--- a/content/docs/grid/synapse/telemetry.mdx
+++ b/content/docs/products/synapse/telemetry.mdx
@@ -189,4 +189,4 @@ endpoint = "http://loki-otlp:4317"
protocol = "grpc"
```
-See also: [Configuration](/grid/synapse/configuration) for the full TOML reference.
+See also: [Configuration](/products/synapse/configuration) for the full TOML reference.
diff --git a/content/docs/sigil/thornberry.mdx b/content/docs/products/thornberry.mdx
similarity index 88%
rename from content/docs/sigil/thornberry.mdx
rename to content/docs/products/thornberry.mdx
index 86292a0..5cb0803 100644
--- a/content/docs/sigil/thornberry.mdx
+++ b/content/docs/products/thornberry.mdx
@@ -49,7 +49,7 @@ import { SiReact, SiTailwindcss } from "react-icons/si";
]}
/>
-**Omni Sigil Thornberry** is the [🎨 Sigil](/sigil) design system expressed with [Tailwind CSS](https://tailwindcss.com) and [shadcn](https://ui.shadcn.com), built on [Ark UI](https://ark-ui.com) primitives. It ships as a shadcn registry, so you add components straight into your project as editable source instead of pulling in an opaque dependency.
+**Omni Sigil Thornberry** is the [🎨 Sigil](/realms/sigil) design system expressed with [Tailwind CSS](https://tailwindcss.com) and [shadcn](https://ui.shadcn.com), built on [Ark UI](https://ark-ui.com) primitives. It ships as a shadcn registry, so you add components straight into your project as editable source instead of pulling in an opaque dependency.
## Key Features
diff --git a/content/docs/grid/vortex/api.mdx b/content/docs/products/vortex/api.mdx
similarity index 100%
rename from content/docs/grid/vortex/api.mdx
rename to content/docs/products/vortex/api.mdx
diff --git a/content/docs/grid/vortex/cli.mdx b/content/docs/products/vortex/cli.mdx
similarity index 100%
rename from content/docs/grid/vortex/cli.mdx
rename to content/docs/products/vortex/cli.mdx
diff --git a/content/docs/grid/vortex/configuration.mdx b/content/docs/products/vortex/configuration.mdx
similarity index 100%
rename from content/docs/grid/vortex/configuration.mdx
rename to content/docs/products/vortex/configuration.mdx
diff --git a/content/docs/grid/vortex/dsl.mdx b/content/docs/products/vortex/dsl.mdx
similarity index 100%
rename from content/docs/grid/vortex/dsl.mdx
rename to content/docs/products/vortex/dsl.mdx
diff --git a/content/docs/grid/vortex/editor.mdx b/content/docs/products/vortex/editor.mdx
similarity index 100%
rename from content/docs/grid/vortex/editor.mdx
rename to content/docs/products/vortex/editor.mdx
diff --git a/content/docs/grid/vortex/events.mdx b/content/docs/products/vortex/events.mdx
similarity index 98%
rename from content/docs/grid/vortex/events.mdx
rename to content/docs/products/vortex/events.mdx
index 93267ba..b45e63e 100644
--- a/content/docs/grid/vortex/events.mdx
+++ b/content/docs/products/vortex/events.mdx
@@ -169,5 +169,5 @@ Vortex is the event backbone for the entire Omni platform. Every product publish
Any workflow can subscribe to any of these event types via routing rules. Omni product teams can trigger cross-product automation without any direct service coupling.
- For streaming triggers (MQTT, NATS, AMQP, gRPC, WebSocket, Redis), events are normalized to a `NormalizedEvent` envelope before routing. The original protocol-specific metadata is preserved in `metadata.raw`. See [Triggers](/grid/vortex/triggers) for configuration details.
+ For streaming triggers (MQTT, NATS, AMQP, gRPC, WebSocket, Redis), events are normalized to a `NormalizedEvent` envelope before routing. The original protocol-specific metadata is preserved in `metadata.raw`. See [Triggers](/products/vortex/triggers) for configuration details.
diff --git a/content/docs/grid/vortex/execution.mdx b/content/docs/products/vortex/execution.mdx
similarity index 100%
rename from content/docs/grid/vortex/execution.mdx
rename to content/docs/products/vortex/execution.mdx
diff --git a/content/docs/grid/vortex/getting-started.mdx b/content/docs/products/vortex/getting-started.mdx
similarity index 97%
rename from content/docs/grid/vortex/getting-started.mdx
rename to content/docs/products/vortex/getting-started.mdx
index 72c758c..e4824b7 100644
--- a/content/docs/grid/vortex/getting-started.mdx
+++ b/content/docs/products/vortex/getting-started.mdx
@@ -35,7 +35,7 @@ docker compose -f compose.yaml -f compose.dev.yaml up --build
This starts PostgreSQL 18, Valkey 8 (Redis-compatible cache), Apache Iggy (event streaming), and the three Vortex services (API, app, worker). Once healthy, the dashboard is available at `http://localhost:3000` and the API at `http://localhost:4000`.
-See the [Self-Hosting guide](/grid/vortex/self-hosting) for Kubernetes and production deployment options.
+See the [Self-Hosting guide](/products/vortex/self-hosting) for Kubernetes and production deployment options.
## 2. Create a workflow
diff --git a/content/docs/grid/vortex/index.mdx b/content/docs/products/vortex/index.mdx
similarity index 100%
rename from content/docs/grid/vortex/index.mdx
rename to content/docs/products/vortex/index.mdx
diff --git a/content/docs/grid/vortex/integrations.mdx b/content/docs/products/vortex/integrations.mdx
similarity index 94%
rename from content/docs/grid/vortex/integrations.mdx
rename to content/docs/products/vortex/integrations.mdx
index f730a0c..bad4c33 100644
--- a/content/docs/grid/vortex/integrations.mdx
+++ b/content/docs/products/vortex/integrations.mdx
@@ -67,7 +67,7 @@ Once connected, integrations appear as **action** step types in the workflow edi
## Custom integrations
-On the **Pro** plan and above, you can extend Vortex with custom [WebAssembly plugins](/grid/vortex/cli#plugin-development). Plugins run in a sandboxed WASM environment and can define custom step types with their own configuration UI.
+On the **Pro** plan and above, you can extend Vortex with custom [WebAssembly plugins](/products/vortex/cli#plugin-development). Plugins run in a sandboxed WASM environment and can define custom step types with their own configuration UI.
## OAuth providers
diff --git a/content/docs/grid/vortex/meta.json b/content/docs/products/vortex/meta.json
similarity index 100%
rename from content/docs/grid/vortex/meta.json
rename to content/docs/products/vortex/meta.json
diff --git a/content/docs/grid/vortex/sdk.mdx b/content/docs/products/vortex/sdk.mdx
similarity index 100%
rename from content/docs/grid/vortex/sdk.mdx
rename to content/docs/products/vortex/sdk.mdx
diff --git a/content/docs/grid/vortex/self-hosting.mdx b/content/docs/products/vortex/self-hosting.mdx
similarity index 100%
rename from content/docs/grid/vortex/self-hosting.mdx
rename to content/docs/products/vortex/self-hosting.mdx
diff --git a/content/docs/grid/vortex/step-types.mdx b/content/docs/products/vortex/step-types.mdx
similarity index 100%
rename from content/docs/grid/vortex/step-types.mdx
rename to content/docs/products/vortex/step-types.mdx
diff --git a/content/docs/grid/vortex/triggers.mdx b/content/docs/products/vortex/triggers.mdx
similarity index 98%
rename from content/docs/grid/vortex/triggers.mdx
rename to content/docs/products/vortex/triggers.mdx
index 042977c..91d1cec 100644
--- a/content/docs/grid/vortex/triggers.mdx
+++ b/content/docs/products/vortex/triggers.mdx
@@ -58,7 +58,7 @@ Subscribe to the Iggy event stream. A routing rule matches incoming events by ty
}
```
-See [Events](/grid/vortex/events) for routing rule configuration, conditions, transforms, and idempotency.
+See [Events](/products/vortex/events) for routing rule configuration, conditions, transforms, and idempotency.
## Manual
diff --git a/content/docs/grid/vortex/troubleshooting.mdx b/content/docs/products/vortex/troubleshooting.mdx
similarity index 100%
rename from content/docs/grid/vortex/troubleshooting.mdx
rename to content/docs/products/vortex/troubleshooting.mdx
diff --git a/content/docs/sigil/zenpetal.mdx b/content/docs/products/zenpetal.mdx
similarity index 94%
rename from content/docs/sigil/zenpetal.mdx
rename to content/docs/products/zenpetal.mdx
index a8aabd4..1acdf65 100644
--- a/content/docs/sigil/zenpetal.mdx
+++ b/content/docs/products/zenpetal.mdx
@@ -58,7 +58,7 @@ import app from "@/lib/config/app.config";
]}
/>
-**Omni Zenpetal** is the canonical reference implementation of the [🎨 Sigil](/sigil) design system built with [Panda CSS](https://panda-css.com) and [Ark UI](https://ark-ui.com). It provides styled components and design tokens for building modern, accessible interfaces with CSS-in-JS.
+**Omni Zenpetal** is the canonical reference implementation of the [🎨 Sigil](/realms/sigil) design system built with [Panda CSS](https://panda-css.com) and [Ark UI](https://ark-ui.com). It provides styled components and design tokens for building modern, accessible interfaces with CSS-in-JS.
## Key Features
diff --git a/content/docs/reality/meta.json b/content/docs/reality/meta.json
deleted file mode 100644
index cd11f38..0000000
--- a/content/docs/reality/meta.json
+++ /dev/null
@@ -1,4 +0,0 @@
-{
- "title": "Reality",
- "pages": ["index", "..."]
-}
diff --git a/content/docs/armory/index.mdx b/content/docs/realms/armory/index.mdx
similarity index 100%
rename from content/docs/armory/index.mdx
rename to content/docs/realms/armory/index.mdx
diff --git a/content/docs/armory/templates.mdx b/content/docs/realms/armory/templates.mdx
similarity index 100%
rename from content/docs/armory/templates.mdx
rename to content/docs/realms/armory/templates.mdx
diff --git a/content/docs/codex/index.mdx b/content/docs/realms/codex/index.mdx
similarity index 100%
rename from content/docs/codex/index.mdx
rename to content/docs/realms/codex/index.mdx
diff --git a/content/docs/core/index.mdx b/content/docs/realms/core/index.mdx
similarity index 100%
rename from content/docs/core/index.mdx
rename to content/docs/realms/core/index.mdx
diff --git a/content/docs/fabric/index.mdx b/content/docs/realms/fabric/index.mdx
similarity index 100%
rename from content/docs/fabric/index.mdx
rename to content/docs/realms/fabric/index.mdx
diff --git a/content/docs/grid/index.mdx b/content/docs/realms/grid/index.mdx
similarity index 100%
rename from content/docs/grid/index.mdx
rename to content/docs/realms/grid/index.mdx
diff --git a/content/docs/kindred/index.mdx b/content/docs/realms/kindred/index.mdx
similarity index 100%
rename from content/docs/kindred/index.mdx
rename to content/docs/realms/kindred/index.mdx
diff --git a/content/docs/reality/index.mdx b/content/docs/realms/reality/index.mdx
similarity index 100%
rename from content/docs/reality/index.mdx
rename to content/docs/realms/reality/index.mdx
diff --git a/content/docs/sigil/index.mdx b/content/docs/realms/sigil/index.mdx
similarity index 100%
rename from content/docs/sigil/index.mdx
rename to content/docs/realms/sigil/index.mdx
diff --git a/content/docs/worlds/index.mdx b/content/docs/realms/worlds/index.mdx
similarity index 100%
rename from content/docs/worlds/index.mdx
rename to content/docs/realms/worlds/index.mdx
diff --git a/content/docs/sigil/meta.json b/content/docs/sigil/meta.json
deleted file mode 100644
index 98e6a03..0000000
--- a/content/docs/sigil/meta.json
+++ /dev/null
@@ -1,4 +0,0 @@
-{
- "title": "Sigil",
- "pages": ["index", "..."]
-}
diff --git a/content/docs/worlds/meta.json b/content/docs/worlds/meta.json
deleted file mode 100644
index aeac3e7..0000000
--- a/content/docs/worlds/meta.json
+++ /dev/null
@@ -1,4 +0,0 @@
-{
- "title": "Worlds",
- "pages": ["index", "..."]
-}
diff --git a/package.json b/package.json
index b5ac0cb..d0fb00e 100644
--- a/package.json
+++ b/package.json
@@ -55,7 +55,8 @@
"tailwindcss": "^4.3.3",
"tw-animate-css": "^1.4.0",
"typescript": "^5.9.3",
- "vite-tsconfig-paths": "^6.1.1"
+ "vite-tsconfig-paths": "^6.1.1",
+ "vitest": "^4.1.11"
},
"trustedDependencies": [
"@parcel/watcher"
diff --git a/public/robots.txt b/public/robots.txt
new file mode 100644
index 0000000..c4290df
--- /dev/null
+++ b/public/robots.txt
@@ -0,0 +1,4 @@
+User-agent: *
+Allow: /
+
+Sitemap: https://docs.omni.dev/sitemap.xml
diff --git a/realms.json b/realms.json
index 1215257..097bf15 100644
--- a/realms.json
+++ b/realms.json
@@ -6,7 +6,7 @@
"icon": "☄️",
"description": "Omni's unified business operations suite. Project management, CRM, finance, commerce, and everything businesses need to operate.",
"tagline": "Unified business operations",
- "docsUrl": "/core"
+ "docsUrl": "/realms/core"
},
{
"id": "kindred",
@@ -14,7 +14,7 @@
"icon": "🤝",
"description": "Consumer-facing products for creators, communities, and everyday life. Identity, social connection, lifestyle, and meaningful digital relationships.",
"tagline": "Social, consumer, and lifestyle",
- "docsUrl": "/kindred"
+ "docsUrl": "/realms/kindred"
},
{
"id": "fabric",
@@ -22,7 +22,7 @@
"icon": "🎨",
"description": "Tools for content creation, design, and knowledge work. Everything creators need to build, design, and share their ideas.",
"tagline": "Content creation and design",
- "docsUrl": "/fabric"
+ "docsUrl": "/realms/fabric"
},
{
"id": "grid",
@@ -30,7 +30,7 @@
"icon": "🔌",
"description": "Cloud, deployment, infrastructure, and platform tools. The backbone that powers Omni's distributed platform.",
"tagline": "Cloud, deployment, infrastructure",
- "docsUrl": "/grid"
+ "docsUrl": "/realms/grid"
},
{
"id": "armory",
@@ -38,7 +38,7 @@
"icon": "🛠️",
"description": "Developer tools and utilities. Everything engineers need to build, test, deploy, and maintain software.",
"tagline": "Developer tools",
- "docsUrl": "/armory"
+ "docsUrl": "/realms/armory"
},
{
"id": "codex",
@@ -46,7 +46,7 @@
"icon": "📜",
"description": "Specifications, standards, and schemas. The protocols and formats that define how Omni systems communicate and interoperate.",
"tagline": "Specifications and standards",
- "docsUrl": "/codex"
+ "docsUrl": "/realms/codex"
},
{
"id": "sigil",
@@ -55,7 +55,7 @@
"description": "A unified collection of design tokens, components, and utilities for building accessible, consistent, and beautiful user interfaces across the Omni product ecosystem.",
"tagline": "Omni's unified design system",
"websiteUrl": "https://sigil.omni.dev",
- "docsUrl": "/sigil"
+ "docsUrl": "/realms/sigil"
},
{
"id": "reality",
@@ -63,7 +63,7 @@
"icon": "ᯅ",
"description": "Omni's spatial computing realm focused on AR, XR, and real-world interfaces. Open, web-first tools for anchoring digital experiences to physical space.",
"tagline": "Spatial computing and XR",
- "docsUrl": "/reality"
+ "docsUrl": "/realms/reality"
},
{
"id": "worlds",
@@ -71,7 +71,7 @@
"icon": "🌌",
"description": "Immersive experiences, games, and generative worlds. Interactive entertainment and AI-driven environments that blur the line between reality and imagination.",
"tagline": "Games and immersive experiences",
- "docsUrl": "/worlds"
+ "docsUrl": "/realms/worlds"
}
]
}
diff --git a/renovate.json b/renovate.json
index b90b806..08f09c8 100644
--- a/renovate.json
+++ b/renovate.json
@@ -1,7 +1,4 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
- "extends": [
- "github>omnidotdev/.github",
- "customManagers:biomeVersions"
- ]
+ "extends": ["github>omnidotdev/.github", "customManagers:biomeVersions"]
}
diff --git a/scripts/migration/__tests__/rewriteLinkTarget.test.ts b/scripts/migration/__tests__/rewriteLinkTarget.test.ts
new file mode 100644
index 0000000..5c567a4
--- /dev/null
+++ b/scripts/migration/__tests__/rewriteLinkTarget.test.ts
@@ -0,0 +1,69 @@
+/**
+ * Unit tests for rewriteLinkTarget: exact product, sub-page, hub, realm-page,
+ * anchor preservation, and unknown-path passthrough.
+ */
+
+import { describe, expect, test } from "vitest";
+
+import { rewriteLinkTarget } from "../rewriteLinkTarget";
+
+import type { LinkMap } from "../rewriteLinkTarget";
+
+const map: LinkMap = new Map([
+ ["/armory/aether", "/products/aether"],
+ ["/armory/aether/sub", "/products/aether/sub"],
+ ["/armory", "/realms/armory"],
+ ["/codex/organizations", "/realms/codex/organizations"],
+ ["/grid/vortex", "/products/vortex"],
+ ["/grid/vortex/webhooks", "/products/vortex/webhooks"],
+]);
+
+describe("rewriteLinkTarget", () => {
+ test("exact product url", () => {
+ expect(rewriteLinkTarget("/armory/aether", map)).toBe("/products/aether");
+ });
+
+ test("sub-page via exact per-page key", () => {
+ expect(rewriteLinkTarget("/grid/vortex/webhooks", map)).toBe(
+ "/products/vortex/webhooks",
+ );
+ });
+
+ test("realm hub", () => {
+ expect(rewriteLinkTarget("/armory", map)).toBe("/realms/armory");
+ });
+
+ test("realm page", () => {
+ expect(rewriteLinkTarget("/codex/organizations", map)).toBe(
+ "/realms/codex/organizations",
+ );
+ });
+
+ test("trailing anchor preserved", () => {
+ expect(rewriteLinkTarget("/armory/aether/sub#anchor", map)).toBe(
+ "/products/aether/sub#anchor",
+ );
+ });
+
+ test("anchor and query preserved together", () => {
+ expect(rewriteLinkTarget("/grid/vortex/webhooks#a?x=1", map)).toBe(
+ "/products/vortex/webhooks#a?x=1",
+ );
+ });
+
+ test("longest-prefix match for an unmapped deep sub-path", () => {
+ // no exact key for this deep path, so the longest prefix (/grid/vortex) wins
+ expect(rewriteLinkTarget("/grid/vortex/deep/page", map)).toBe(
+ "/products/vortex/deep/page",
+ );
+ });
+
+ test("unknown path unchanged", () => {
+ expect(rewriteLinkTarget("/help/support", map)).toBe("/help/support");
+ });
+
+ test("prefix boundary is respected", () => {
+ // /armory-x must not match the /armory key
+ expect(rewriteLinkTarget("/armory-x/page", map)).toBe("/armory-x/page");
+ });
+});
diff --git a/scripts/migration/buildProductRealms.ts b/scripts/migration/buildProductRealms.ts
new file mode 100644
index 0000000..822a588
--- /dev/null
+++ b/scripts/migration/buildProductRealms.ts
@@ -0,0 +1,137 @@
+/**
+ * Emit the committed public product -> realm map used by the docs sidebar.
+ *
+ * Reads the migration classification (classification.json) and the legacy URL
+ * snapshot (legacy-urls.json), intersects with the set of files tracked at git
+ * HEAD to determine which pages are PUBLIC (a page is public iff its original,
+ * pre-move file is tracked), and writes src/lib/productRealms.generated.ts.
+ *
+ * A product's realm is the FIRST path segment of its ORIGINAL file under
+ * content/docs/ (e.g. content/docs/grid/vortex/index.mdx -> grid). Root-level
+ * product files (no realm folder, e.g. content/docs/orin.mdx) have no derivable
+ * realm and are intentionally omitted; the sidebar resolves such products to a
+ * safe fallback instead.
+ *
+ * Private/unlaunched products are excluded so their slugs never ship in a
+ * committed artifact. Regenerate with `bun run scripts/migration/buildProductRealms.ts`.
+ */
+
+import { execFileSync } from "node:child_process";
+import { resolve } from "node:path";
+
+import realmsData from "../../realms.json";
+
+interface ClassificationEntry {
+ /** Legacy URL. */
+ url: string;
+ /** Original, pre-move file path under content/docs/. */
+ file: string;
+ /** Classification kind. */
+ kind: string;
+ /** New URL, or null. */
+ to: string | null;
+}
+
+interface LegacyUrlEntry {
+ /** Legacy URL. */
+ url: string;
+ /** Original, pre-move file path under content/docs/. */
+ file: string;
+}
+
+const dataDir = resolve(import.meta.dir, "data");
+const outPath = resolve(
+ import.meta.dir,
+ "../../src/lib/productRealms.generated.ts",
+);
+
+const classification: ClassificationEntry[] = await Bun.file(
+ resolve(dataDir, "classification.json"),
+).json();
+const legacyUrls: LegacyUrlEntry[] = await Bun.file(
+ resolve(dataDir, "legacy-urls.json"),
+).json();
+
+// Canonical realm ids (the only valid realm values a product may resolve to).
+const realmIds = new Set(realmsData.realms.map((r) => r.id));
+
+// Files tracked at git HEAD => the authoritative public set. A page is public
+// iff its original pre-move file is tracked; untracked files are private.
+const trackedFiles = new Set(
+ execFileSync(
+ "git",
+ ["ls-tree", "-r", "HEAD", "--name-only", "--", "content/docs"],
+ { encoding: "utf8" },
+ )
+ .split("\n")
+ .map((line) => line.trim())
+ .filter(Boolean),
+);
+
+// url -> original file, from the legacy snapshot (cross-check for classification).
+const fileByUrl = new Map(legacyUrls.map((entry) => [entry.url, entry.file]));
+
+/** Realm of an original file = its first path segment under content/docs/. */
+const realmOfFile = (file: string): string | undefined => {
+ const [, docs, segment] = file.split("/");
+ if (docs !== "docs") return undefined;
+ return realmIds.has(segment) ? segment : undefined;
+};
+
+/** Product id = second segment of the new URL (/products//...). */
+const productIdFromTo = (to: string): string | undefined => {
+ const [, products, id] = to.split("/");
+ return products === "products" ? id : undefined;
+};
+
+const map: Record = {};
+const skippedRootLevel: string[] = [];
+
+for (const entry of classification) {
+ if (entry.kind !== "product" || !entry.to) continue;
+
+ const file = fileByUrl.get(entry.url) ?? entry.file;
+ if (!trackedFiles.has(file)) continue;
+
+ const id = productIdFromTo(entry.to);
+ if (!id) continue;
+
+ const realm = realmOfFile(file);
+ if (!realm) {
+ if (!skippedRootLevel.includes(id)) skippedRootLevel.push(id);
+ continue;
+ }
+
+ map[id] = realm;
+}
+
+const sortedKeys = Object.keys(map).sort((a, b) => a.localeCompare(b));
+
+const body = sortedKeys
+ .map((id) => ` ${JSON.stringify(id)}: ${JSON.stringify(map[id])},`)
+ .join("\n");
+
+const output = `/**
+ * Public product -> realm map for the docs sidebar.
+ *
+ * AUTO-GENERATED by scripts/migration/buildProductRealms.ts. Do not edit by hand.
+ * Private/unlaunched products are intentionally excluded so their slugs never
+ * ship in a committed artifact. Regenerate with
+ * \`bun run scripts/migration/buildProductRealms.ts\`.
+ */
+
+/** Maps a public product id to its realm id. */
+export const PRODUCT_REALMS: Record = {
+${body}
+};
+`;
+
+await Bun.write(outPath, output);
+
+// biome-ignore lint/suspicious/noConsole: migration script output
+console.info(
+ `[product-realms] Wrote ${sortedKeys.length} public product -> realm entries` +
+ (skippedRootLevel.length
+ ? ` (skipped ${skippedRootLevel.length} root-level product(s) with no realm: ${skippedRootLevel.join(", ")})`
+ : ""),
+);
diff --git a/scripts/migration/buildRedirects.ts b/scripts/migration/buildRedirects.ts
new file mode 100644
index 0000000..6ed7fd7
--- /dev/null
+++ b/scripts/migration/buildRedirects.ts
@@ -0,0 +1,121 @@
+/**
+ * Generate the legacy-URL redirect map from the final migration classification.
+ *
+ * Reads scripts/migration/data/classification.json and legacy-urls.json and
+ * emits src/lib/redirects.generated.ts, exporting LEGACY_REDIRECTS: a map from
+ * every PUBLIC legacy URL whose canonical location moved to that canonical
+ * location. Pure excludes (carbon/ember/quantum/vault) get no entry; ecosystem
+ * pages (which do not move) get no entry; the grid/mimic duplicate
+ * (redirect-only) is included when public.
+ *
+ * IMPORTANT: only redirects whose source page is committed at git HEAD are
+ * emitted. Private/unlaunched product redirects are intentionally excluded so
+ * their slugs never ship in the client bundle (the map is imported by a route
+ * and reaches the browser).
+ *
+ * The map powers `throw redirect(301)` at the edge. Keys are sorted for stable
+ * diffs. Do not edit the output by hand; re-run this script instead.
+ */
+
+import { execSync } from "node:child_process";
+import { resolve } from "node:path";
+
+const dir = import.meta.dir;
+
+type Kind =
+ | "product"
+ | "realm-hub"
+ | "realm-page"
+ | "ecosystem"
+ | "exclude"
+ | "redirect-only";
+
+interface Classified {
+ url: string;
+ file: string;
+ kind: Kind;
+ to: string | null;
+}
+
+interface LegacyUrl {
+ url: string;
+ file: string;
+}
+
+const classification: Classified[] = await Bun.file(
+ resolve(dir, "data/classification.json"),
+).json();
+
+const legacyUrls: LegacyUrl[] = await Bun.file(
+ resolve(dir, "data/legacy-urls.json"),
+).json();
+
+// Public = source page file is tracked at git HEAD. Private/unlaunched product
+// pages are untracked, so their slugs must never reach the shipped map. Map
+// each committed content file back to its legacy url to build the public set
+const publicFiles = new Set(
+ execSync("git ls-tree -r HEAD --name-only -- content/docs", {
+ cwd: resolve(dir, "../.."),
+ })
+ .toString()
+ .split("\n")
+ .filter(Boolean),
+);
+
+const publicUrls = new Set(
+ legacyUrls.filter((entry) => publicFiles.has(entry.file)).map((e) => e.url),
+);
+
+// Kinds whose old URL should 301 to a canonical location when it actually moved
+const REDIRECTING_KINDS = new Set([
+ "product",
+ "realm-hub",
+ "realm-page",
+ "redirect-only",
+]);
+
+const redirects: Record = {};
+for (const page of classification) {
+ if (!REDIRECTING_KINDS.has(page.kind)) continue;
+ if (page.to === null || page.to === page.url) continue;
+ // Skip private/unlaunched sources so their slugs never ship in the bundle
+ if (!publicUrls.has(page.url)) continue;
+ redirects[page.url] = page.to;
+}
+
+const sortedKeys = Object.keys(redirects).sort((a, b) => a.localeCompare(b));
+
+const body = sortedKeys
+ .map((key) => ` ${JSON.stringify(key)}: ${JSON.stringify(redirects[key])},`)
+ .join("\n");
+
+const output = `/**
+ * Legacy-URL redirect map (auto-generated).
+ *
+ * DO NOT EDIT BY HAND. Generated by scripts/migration/buildRedirects.ts from
+ * scripts/migration/data/classification.json. Maps each PUBLIC legacy docs URL
+ * that moved under the new URL scheme to its canonical location; feed it to
+ * \`throw redirect(301)\` at the edge. Re-run the builder to regenerate.
+ *
+ * This map is imported by a route and ships to the browser, so it contains ONLY
+ * redirects whose source page is committed at git HEAD. Private/unlaunched
+ * product redirects are intentionally excluded so their slugs never ship in the
+ * client bundle.
+ */
+
+export const LEGACY_REDIRECTS: Record = {
+${body}
+};
+`;
+
+const outPath = resolve(dir, "../../src/lib/redirects.generated.ts");
+await Bun.write(outPath, output);
+
+// Normalize with biome so the committed output matches repo formatting (long
+// entries wrap), keeping the generated file clean under `biome check`
+await Bun.$`bunx biome format --write ${outPath}`.quiet();
+
+// biome-ignore lint/suspicious/noConsole: migration script output
+console.info(
+ `Wrote src/lib/redirects.generated.ts with ${sortedKeys.length} redirects`,
+);
diff --git a/scripts/migration/buildRootMeta.ts b/scripts/migration/buildRootMeta.ts
new file mode 100644
index 0000000..0627120
--- /dev/null
+++ b/scripts/migration/buildRootMeta.ts
@@ -0,0 +1,144 @@
+/**
+ * Regenerate the root docs meta.json, grouping the sidebar by realm.
+ *
+ * Reads the migration classification (classification.json), intersects with the
+ * files tracked at git HEAD to keep only PUBLIC pages, and emits
+ * content/docs/meta.json with:
+ *
+ * - a Welcome section listing the ecosystem root pages still at content/docs/;
+ * - one section per realm (in realms.json order): the realm hub first, then
+ * the realm's public products, then its public realm guide pages;
+ * - the Community and Help ecosystem sections (folder-scoped globs).
+ *
+ * No global `...` catch-all is emitted, so unlisted (private) product pages
+ * never leak into the committed nav. Regenerate with
+ * `bun run scripts/migration/buildRootMeta.ts`.
+ */
+
+import { execFileSync } from "node:child_process";
+import { resolve } from "node:path";
+
+import realmsData from "../../realms.json";
+
+interface ClassificationEntry {
+ /** Legacy URL. */
+ url: string;
+ /** Original, pre-move file path under content/docs/. */
+ file: string;
+ /** Classification kind. */
+ kind: string;
+ /** New URL, or null. */
+ to: string | null;
+}
+
+const dataDir = resolve(import.meta.dir, "data");
+const outPath = resolve(import.meta.dir, "../../content/docs/meta.json");
+
+const classification: ClassificationEntry[] = await Bun.file(
+ resolve(dataDir, "classification.json"),
+).json();
+
+const realmOrder = realmsData.realms.map((r) => r.id);
+const realmIds = new Set(realmOrder);
+
+// Files tracked at git HEAD => the authoritative public set.
+const trackedFiles = new Set(
+ execFileSync(
+ "git",
+ ["ls-tree", "-r", "HEAD", "--name-only", "--", "content/docs"],
+ { encoding: "utf8" },
+ )
+ .split("\n")
+ .map((line) => line.trim())
+ .filter(Boolean),
+);
+
+const isPublic = (entry: ClassificationEntry) => trackedFiles.has(entry.file);
+
+/** Realm of an original file = its first path segment under content/docs/. */
+const realmOfFile = (file: string): string | undefined => {
+ const [, docs, segment] = file.split("/");
+ if (docs !== "docs") return undefined;
+ return realmIds.has(segment) ? segment : undefined;
+};
+
+/** Meta path reference for a page = its path under content/docs/ without extension. */
+const refFromTo = (to: string): string => to.replace(/^\//, "");
+
+/** Title-case a realm id for the section separator label. */
+const sectionLabel = (realmId: string): string =>
+ realmId.charAt(0).toUpperCase() + realmId.slice(1).toLowerCase();
+
+// Per-realm buckets.
+const hubByRealm = new Map();
+const productsByRealm = new Map>();
+const guidesByRealm = new Map>();
+
+for (const realm of realmOrder) {
+ productsByRealm.set(realm, new Set());
+ guidesByRealm.set(realm, new Set());
+}
+
+for (const entry of classification) {
+ if (!entry.to || !isPublic(entry)) continue;
+
+ const realm = realmOfFile(entry.file);
+ if (!realm) continue;
+
+ if (entry.kind === "realm-hub") {
+ // Reference the hub index page as a flat entry (not the folder wrapper).
+ hubByRealm.set(realm, `realms/${realm}/index`);
+ } else if (entry.kind === "product") {
+ // One reference per product (id = second segment of the new URL).
+ const [, , id] = entry.to.split("/");
+ if (id) productsByRealm.get(realm)?.add(`products/${id}`);
+ } else if (entry.kind === "realm-page") {
+ guidesByRealm.get(realm)?.add(refFromTo(entry.to));
+ }
+}
+
+const sorted = (set: Set | undefined): string[] =>
+ [...(set ?? [])].sort((a, b) => a.localeCompare(b));
+
+const pages: string[] = ["---Welcome---"];
+
+// Ecosystem root pages that still live directly under content/docs/.
+const rootEcosystemPages = [
+ "index",
+ "mission",
+ "ethos",
+ "ecosystem",
+ "open-source",
+];
+for (const page of rootEcosystemPages) {
+ if (trackedFiles.has(`content/docs/${page}.mdx`)) pages.push(page);
+}
+
+for (const realm of realmOrder) {
+ pages.push(`---${sectionLabel(realm)}---`);
+
+ const hub = hubByRealm.get(realm);
+ if (hub) pages.push(hub);
+
+ pages.push(...sorted(productsByRealm.get(realm)));
+ pages.push(...sorted(guidesByRealm.get(realm)));
+}
+
+// Ecosystem sections. Folder-scoped globs are safe: they only spread pages
+// within community/ and help/, never unlisted product pages.
+pages.push("---Community---", "...community", "---Help---", "...help");
+
+const meta = {
+ root: true,
+ title: "Welcome to Omni",
+ tagline: "Open-source ecosystem",
+ description: "Open-source ecosystem",
+ pages,
+};
+
+await Bun.write(outPath, `${JSON.stringify(meta, null, 2)}\n`);
+
+// biome-ignore lint/suspicious/noConsole: migration script output
+console.info(
+ `[root-meta] Wrote content/docs/meta.json with ${realmOrder.length} realm sections`,
+);
diff --git a/scripts/migration/classifyPages.ts b/scripts/migration/classifyPages.ts
new file mode 100644
index 0000000..8e5a28e
--- /dev/null
+++ b/scripts/migration/classifyPages.ts
@@ -0,0 +1,376 @@
+/**
+ * Classify every live docs URL into how it moves under the new URL scheme.
+ *
+ * New scheme:
+ * - product page -> /products// (realm stripped from the path)
+ * - realm hub -> /realms/
+ * - realm page -> /realms// (realm-scoped guide, not a product)
+ * - ecosystem -> unchanged (stays at root)
+ * - exclude -> not published, not redirected (to === null)
+ * - redirect-only -> not published, but the old URL still 301s to `to`
+ *
+ * Reads scripts/migration/data/legacy-urls.json (ground truth for what exists)
+ * and scripts/migration/data/overrides.json (human decisions keyed by legacy
+ * url), then writes:
+ * - data/classification.json the FINAL classification for all 267 URLs
+ * - data/ambiguous.json always [] once every page is decided
+ *
+ * The automatic rules below classify the confident pages; the overrides file
+ * carries the human decisions for the rest and wins wherever it is present.
+ * Every one of the 267 URLs must end up with a final classification; the script
+ * fails loudly if any URL is left unaccounted for.
+ *
+ * The omni-api public catalog (product-realms.json) is a PARTIAL cross-check:
+ * where a product is public, we assert the realm we stripped matches the
+ * catalog's realm, and we use catalog membership to sharpen recommendations for
+ * ambiguous codex/sigil pages. Absence means "unknown", never "not a product".
+ */
+
+import { resolve } from "node:path";
+
+const dir = import.meta.dir;
+const read = (p: string) => Bun.file(resolve(dir, p)).json();
+
+interface LegacyUrl {
+ url: string;
+ file: string;
+}
+
+type Kind =
+ | "product"
+ | "realm-hub"
+ | "realm-page"
+ | "ecosystem"
+ | "exclude"
+ | "redirect-only";
+
+interface Classified {
+ url: string;
+ file: string;
+ kind: Kind;
+ /** Canonical destination, or null for pure excludes (no publish, no redirect) */
+ to: string | null;
+}
+
+/** A human decision keyed by legacy url, applied on top of the automatic rules */
+interface Override {
+ kind: Kind;
+ to: string | null;
+}
+
+interface Ambiguous {
+ url: string;
+ file: string;
+ recommendation: string;
+ reason: string;
+}
+
+const legacy: LegacyUrl[] = await read("data/legacy-urls.json");
+const realmsFile: { realms: { id: string }[] } =
+ await read("../../realms.json");
+const productRealms: Record = await read(
+ "data/product-realms.json",
+).catch(() => ({}));
+const overrides: Record = await read("data/overrides.json");
+
+const REALMS = new Set(realmsFile.realms.map((r) => r.id));
+// Ecosystem section roots, per the classification rules
+const ECOSYSTEM_SECTIONS = new Set(["help", "community", "support-omni"]);
+// Single-segment root pages with an obvious classification
+const ECOSYSTEM_SLUGS = new Set([
+ "ecosystem",
+ "ethos",
+ "mission",
+ "open-source",
+]);
+const PRODUCT_SLUGS = new Set(["lens", "frame", "launcher", "orin", "halcyon"]);
+// Non-realm folders that mirror a realm (" Introduction" hubs) but are not
+// among the authoritative 9 realms; their status must be confirmed
+const NEW_REALM_LIKE = new Set(["carbon", "ember", "quantum"]);
+
+const inCatalog = (id: string) => id in productRealms;
+const segsOf = (url: string) => url.split("/").filter(Boolean);
+
+const classified: Classified[] = [];
+const ambiguous: Ambiguous[] = [];
+// Confident product pages whose stripped realm disagrees with the catalog
+const realmDisagreements: {
+ url: string;
+ id: string;
+ pathRealm: string;
+ catalogRealm: string;
+}[] = [];
+
+/** Decide if a realm-nested / root URL is ambiguous, returning the reasoning */
+const asAmbiguous = (
+ url: string,
+ segs: string[],
+): Omit | null => {
+ const top = segs[0]!;
+ const rest = segs.slice(1);
+ const restPath = rest.join("/");
+ const id = rest[0] ?? top;
+
+ // New realm-style groups not in the authoritative 9 realms
+ if (NEW_REALM_LIKE.has(top)) {
+ if (segs.length === 1)
+ return {
+ recommendation: `/realms/${top}`,
+ reason: `New realm-style hub ("${top[0]!.toUpperCase()}${top.slice(1)} Introduction") that is NOT one of the 9 authoritative realms in realms.json. Its children are product-shaped. Confirm whether "${top}" becomes an additional realm (then hub -> /realms/${top}) or is folded elsewhere.`,
+ };
+ return {
+ recommendation: `/products/${restPath}`,
+ reason: `Product-shaped child of the non-authoritative "${top}" group; neither "${top}" (as a realm) nor "${id}" (in the master catalog) is confirmed. Depends on the "${top}" realm decision above.`,
+ };
+ }
+
+ // Fumadocs scaffold / test pages, not real product docs
+ if (top === "vault")
+ return {
+ recommendation: "EXCLUDE (do not migrate)",
+ reason: `Fumadocs vault scaffold/test page; not real product or realm documentation. Recommend removing from the live set rather than migrating.`,
+ };
+
+ // Realm-scoped guides that must NOT become /products/*
+ if (url === "/armory/templates")
+ return {
+ recommendation: "/realms/armory/templates",
+ reason: `Realm-scoped resource page ("Templates: production-ready starting points"), not a product and not in the master catalog. Recommend a realm sub-page, not /products/templates.`,
+ };
+ if (url === "/core/social-media-post")
+ return {
+ recommendation: "/realms/core/social-media-post",
+ reason: `Feature guide of Omni Core ("create, schedule, manage social media content"), not a standalone product and not in the catalog. Recommend a realm sub-page.`,
+ };
+
+ // Codex specs: catalog lists several as products, yet they live in the
+ // "specifications and standards" realm; owner must pick /products vs /realms
+ if (top === "codex" && segs.length >= 2) {
+ if (id === "organizations")
+ return {
+ recommendation: "/realms/codex/organizations",
+ reason: `Cross-product architecture explainer (multi-tenant workspaces), not a product and not in the catalog. Recommend a codex realm sub-page.`,
+ };
+ const cat = inCatalog(id)
+ ? `The public catalog DOES list "${id}" as a product in the codex realm, so it is product-shaped. `
+ : `The public catalog does not expose "${id}" (partial set), though it is in the master product list. `;
+ return {
+ recommendation: `/products/${restPath}`,
+ reason: `Codex specification page. ${cat}Confirm whether codex specs live at /products/${id} or as codex realm pages at /realms/codex/${id}.`,
+ };
+ }
+
+ // Sigil design-system realm: fonts/totem are reference pages; thornberry and
+ // zenpetal are listed as sigil products by the catalog
+ if (top === "sigil" && segs.length >= 2) {
+ if (inCatalog(id))
+ return {
+ recommendation: `/products/${restPath}`,
+ reason: `Sits in the Sigil design-system realm, but the public catalog lists "${id}" as a sigil product, so it is product-shaped. Confirm it belongs at /products/${id} rather than as a design-system page under /realms/sigil.`,
+ };
+ return {
+ recommendation: `/realms/sigil/${restPath}`,
+ reason: `Sigil design-system sub-page (font/theme/token reference "${id}"), not a standalone product and not in the catalog. Recommend a realm sub-page, not /products/*.`,
+ };
+ }
+
+ // ID collision: a GRID product and a WORLDS product are both named "mimic",
+ // so both would claim /products/mimic
+ if (url === "/grid/mimic")
+ return {
+ recommendation: "/products/mimic",
+ reason: `ID COLLISION: this GRID "mimic" (open-source super-emulator engine, libretro) and a DIFFERENT WORLDS "mimic" both map to /products/mimic. Only one can own the slug. Confirm which keeps "mimic" and rename the other.`,
+ };
+ if (top === "worlds" && rest[0] === "mimic")
+ return {
+ recommendation: `/products/${restPath}`,
+ reason: `ID COLLISION: this WORLDS "mimic" and a DIFFERENT GRID "mimic" (super-emulator engine) both map to /products/mimic. Only one can own the slug. Confirm which keeps "mimic" and rename the other.`,
+ };
+
+ return null;
+};
+
+for (const { url, file } of legacy) {
+ const segs = segsOf(url);
+
+ // Rule 1: root
+ if (url === "/") {
+ classified.push({ url, file, kind: "ecosystem", to: url });
+ continue;
+ }
+
+ // Rule 2: realm hub (exactly /)
+ if (segs.length === 1 && REALMS.has(segs[0]!)) {
+ classified.push({ url, file, kind: "realm-hub", to: `/realms/${segs[0]}` });
+ continue;
+ }
+
+ // Ambiguity checks override the generic product/ecosystem defaults
+ const amb = asAmbiguous(url, segs);
+ if (amb) {
+ ambiguous.push({ url, file, ...amb });
+ continue;
+ }
+
+ const top = segs[0]!;
+
+ // Rule 3: ecosystem sections
+ if (ECOSYSTEM_SECTIONS.has(top)) {
+ classified.push({ url, file, kind: "ecosystem", to: url });
+ continue;
+ }
+
+ // worlds/ subpages that survived the ambiguity checks are realm products
+ // Rule 4: nested under a realm -> product (realm stripped)
+ if (REALMS.has(top) && segs.length >= 2) {
+ const rest = segs.slice(1);
+ const id = rest[0]!;
+ if (inCatalog(id) && productRealms[id] !== top)
+ realmDisagreements.push({
+ url,
+ id,
+ pathRealm: top,
+ catalogRealm: productRealms[id]!,
+ });
+ classified.push({
+ url,
+ file,
+ kind: "product",
+ to: `/products/${rest.join("/")}`,
+ });
+ continue;
+ }
+
+ // Rule 5: bare-root single-segment pages
+ if (segs.length === 1) {
+ if (ECOSYSTEM_SLUGS.has(top)) {
+ classified.push({ url, file, kind: "ecosystem", to: url });
+ continue;
+ }
+ if (PRODUCT_SLUGS.has(top)) {
+ if (inCatalog(top) && productRealms[top] !== undefined) {
+ // root products carry no path realm, so nothing to disagree with
+ }
+ classified.push({ url, file, kind: "product", to: `/products/${top}` });
+ continue;
+ }
+ ambiguous.push({
+ url,
+ file,
+ recommendation: `/products/${top}`,
+ reason: `Single-segment root page that is neither a known ecosystem page nor a confirmed product. Confirm whether "${top}" is a product or an ecosystem page.`,
+ });
+ continue;
+ }
+
+ // Anything left is unmodelled; never silently guess
+ ambiguous.push({
+ url,
+ file,
+ recommendation: "(needs review)",
+ reason: `URL shape not covered by the classification rules; manual review required.`,
+ });
+}
+
+// Apply human decisions: the override for a url wins over any automatic result.
+// Build the FINAL classification for every legacy url, failing loudly if any
+// page is left without an automatic classification AND without an override.
+const autoByUrl = new Map(classified.map((c) => [c.url, c]));
+const unresolved: LegacyUrl[] = [];
+const final: Classified[] = [];
+
+for (const { url, file } of legacy) {
+ const ov = overrides[url];
+ if (ov) {
+ final.push({ url, file, kind: ov.kind, to: ov.to });
+ continue;
+ }
+ const auto = autoByUrl.get(url);
+ if (auto) {
+ final.push(auto);
+ continue;
+ }
+ unresolved.push({ url, file });
+}
+
+if (unresolved.length)
+ throw new Error(
+ `unresolved pages (no automatic classification and no override):\n${unresolved
+ .map((u) => ` ${u.url} (${u.file})`)
+ .join("\n")}`,
+ );
+
+// Every override must reference a real legacy url, or a decision is dead
+const legacyUrls = new Set(legacy.map((l) => l.url));
+for (const url of Object.keys(overrides))
+ if (!legacyUrls.has(url))
+ throw new Error(`override for unknown legacy url: ${url}`);
+
+// Invariants on the final set: destinations must match their kind
+for (const c of final) {
+ switch (c.kind) {
+ case "product":
+ if (!c.to?.startsWith("/products/"))
+ throw new Error(`product ${c.url} has non-/products to=${c.to}`);
+ break;
+ case "realm-hub":
+ case "realm-page":
+ if (!c.to?.startsWith("/realms/"))
+ throw new Error(`${c.kind} ${c.url} has non-/realms to=${c.to}`);
+ break;
+ case "ecosystem":
+ if (c.to !== c.url)
+ throw new Error(`ecosystem ${c.url} must be unchanged, got to=${c.to}`);
+ break;
+ case "exclude":
+ if (c.to !== null)
+ throw new Error(`exclude ${c.url} must have to=null, got ${c.to}`);
+ break;
+ case "redirect-only":
+ if (!c.to)
+ throw new Error(`redirect-only ${c.url} must have a non-null to`);
+ break;
+ }
+}
+
+if (final.length !== legacy.length)
+ throw new Error(
+ `coverage mismatch: final ${final.length} != legacy ${legacy.length}`,
+ );
+
+const sortByUrl = (a: T, b: T) =>
+ a.url.localeCompare(b.url);
+final.sort(sortByUrl);
+
+// Every ambiguous page must now be resolved by an override
+const stillAmbiguous = ambiguous.filter((a) => !overrides[a.url]);
+
+await Bun.write(
+ resolve(dir, "data/classification.json"),
+ `${JSON.stringify(final, null, 2)}\n`,
+);
+await Bun.write(
+ resolve(dir, "data/ambiguous.json"),
+ `${JSON.stringify(stillAmbiguous, null, 2)}\n`,
+);
+
+const byKind = final.reduce>((acc, c) => {
+ acc[c.kind] = (acc[c.kind] ?? 0) + 1;
+ return acc;
+}, {});
+
+// biome-ignore lint/suspicious/noConsole: migration script output
+console.info(
+ [
+ `Final classification (${final.length} URLs):`,
+ ` product: ${byKind.product ?? 0}`,
+ ` realm-hub: ${byKind["realm-hub"] ?? 0}`,
+ ` realm-page: ${byKind["realm-page"] ?? 0}`,
+ ` ecosystem: ${byKind.ecosystem ?? 0}`,
+ ` redirect-only: ${byKind["redirect-only"] ?? 0}`,
+ ` exclude: ${byKind.exclude ?? 0}`,
+ `Total: ${final.length} (assert == ${legacy.length}) ${final.length === legacy.length ? "OK" : "FAIL"}`,
+ `Remaining ambiguous: ${stillAmbiguous.length}`,
+ `Realm disagreements (confident products): ${realmDisagreements.length}`,
+ ].join("\n"),
+);
diff --git a/scripts/migration/fetchProductRealms.ts b/scripts/migration/fetchProductRealms.ts
new file mode 100644
index 0000000..f5d52db
--- /dev/null
+++ b/scripts/migration/fetchProductRealms.ts
@@ -0,0 +1,50 @@
+/**
+ * Fetch the omni-api PUBLIC product catalog and emit a productId -> realmId map.
+ *
+ * This is the cross-check set for the URL-restructure migration: the docs cover
+ * ~70 products, but the public catalog only exposes ~21. So this map is a
+ * PARTIAL verifier used to assert that, where a product is public, the realm we
+ * stripped from its docs path matches the realm omni-api assigns it. Absence
+ * from this map means "unknown to the public catalog", never "not a product".
+ *
+ * Mirrors scripts/generateCatalog.ts: fetches via the shared
+ * `@omnidotdev/providers/catalog` client and, if omni-api is unreachable, exits
+ * gracefully while keeping any existing product-realms.json in place.
+ */
+
+import { resolve } from "node:path";
+
+import { fetchPublicCatalog } from "@omnidotdev/providers/catalog";
+
+const outPath = resolve(import.meta.dir, "data/product-realms.json");
+
+const catalog = await fetchPublicCatalog(
+ process.env.OMNI_API_GRAPHQL_URL
+ ? { url: process.env.OMNI_API_GRAPHQL_URL }
+ : {},
+).catch((error) => {
+ // biome-ignore lint/suspicious/noConsole: migration script output
+ console.warn(
+ `[product-realms] could not reach omni-api (${error}); keeping existing product-realms.json`,
+ );
+ return null;
+});
+
+// Keep any committed map when omni-api is unreachable, so the classifier can
+// still run against the last good cross-check set.
+if (!catalog) process.exit(0);
+
+// Deterministic key order so regenerations produce stable diffs.
+const map: Record = {};
+for (const p of [...catalog.products].sort((a, b) =>
+ a.id.localeCompare(b.id),
+)) {
+ if (p.realm) map[p.id] = p.realm;
+}
+
+await Bun.write(outPath, `${JSON.stringify(map, null, 2)}\n`);
+
+// biome-ignore lint/suspicious/noConsole: migration script output
+console.info(
+ `[product-realms] Wrote ${Object.keys(map).length} product -> realm entries`,
+);
diff --git a/scripts/migration/moveContent.ts b/scripts/migration/moveContent.ts
new file mode 100644
index 0000000..6b41bd3
--- /dev/null
+++ b/scripts/migration/moveContent.ts
@@ -0,0 +1,225 @@
+/**
+ * Compute (and optionally execute) the Phase 2 content-move plan.
+ *
+ * Reads scripts/migration/data/classification.json and derives, for every one
+ * of the 267 live docs URLs, a single action:
+ * - product | realm-hub | realm-page -> MOVE file to its new /products or
+ * /realms path (Fumadocs content root is content/docs)
+ * - redirect-only (the grid/mimic duplicate) -> REMOVE (its URL is handled by
+ * the redirect map)
+ * - exclude under carbon|ember|quantum -> MOVE OUT to content/_unlaunched
+ * (preserves the draft, removes it from the served tree)
+ * - exclude vault scaffold -> REMOVE
+ * - ecosystem -> leave in place (no action)
+ *
+ * New file path for a moved page:
+ * content/docs + + (original file ended with /index.mdx ? /index.mdx : .mdx)
+ *
+ * Run with `--execute` to perform the moves via git mv / git rm (tracked) or
+ * plain mv / rm (untracked); without it the plan is printed and asserted only.
+ */
+
+import { execFileSync } from "node:child_process";
+import {
+ existsSync,
+ mkdirSync,
+ readdirSync,
+ renameSync,
+ rmSync,
+} from "node:fs";
+import { dirname, join, resolve } from "node:path";
+
+type Kind =
+ | "product"
+ | "realm-hub"
+ | "realm-page"
+ | "ecosystem"
+ | "exclude"
+ | "redirect-only";
+
+interface Entry {
+ url: string;
+ file: string;
+ kind: Kind;
+ to: string | null;
+}
+
+type ActionType = "move" | "unlaunched" | "remove" | "leave";
+
+interface Action {
+ type: ActionType;
+ from: string;
+ to?: string;
+ kind: Kind;
+}
+
+const repoRoot = resolve(import.meta.dir, "../..");
+const classification: Entry[] = JSON.parse(
+ await Bun.file(
+ join(repoRoot, "scripts/migration/data/classification.json"),
+ ).text(),
+);
+
+const CONTENT_PREFIX = "content/docs/";
+
+/** Derive the new served path for a moved page from its `to` URL and origin */
+const movedPath = (entry: Entry): string => {
+ const isIndex = entry.file.endsWith("/index.mdx");
+ return `content/docs${entry.to}${isIndex ? "/index.mdx" : ".mdx"}`;
+};
+
+const isUnlaunched = (file: string): boolean =>
+ file.startsWith("content/docs/carbon/") ||
+ file.startsWith("content/docs/ember/") ||
+ file.startsWith("content/docs/quantum/");
+
+const planAction = (entry: Entry): Action => {
+ switch (entry.kind) {
+ case "product":
+ case "realm-hub":
+ case "realm-page":
+ return {
+ type: "move",
+ from: entry.file,
+ to: movedPath(entry),
+ kind: entry.kind,
+ };
+ case "redirect-only":
+ return { type: "remove", from: entry.file, kind: entry.kind };
+ case "exclude":
+ if (isUnlaunched(entry.file)) {
+ return {
+ type: "unlaunched",
+ from: entry.file,
+ to: `content/_unlaunched/${entry.file.slice(CONTENT_PREFIX.length)}`,
+ kind: entry.kind,
+ };
+ }
+ return { type: "remove", from: entry.file, kind: entry.kind };
+ case "ecosystem":
+ return { type: "leave", from: entry.file, kind: entry.kind };
+ }
+};
+
+const actions = classification.map(planAction);
+
+// Assertion: no two destinations collide
+const destinations = new Map();
+for (const a of actions) {
+ if (a.type === "move" || a.type === "unlaunched") {
+ const list = destinations.get(a.to as string) ?? [];
+ list.push(a.from);
+ destinations.set(a.to as string, list);
+ }
+}
+const collisions = [...destinations.entries()].filter(
+ ([, sources]) => sources.length > 1,
+);
+
+const counts = {
+ moves: actions.filter((a) => a.type === "move").length,
+ removes: actions.filter((a) => a.type === "remove").length,
+ unlaunched: actions.filter((a) => a.type === "unlaunched").length,
+ ecosystem: actions.filter((a) => a.type === "leave").length,
+};
+const actioned =
+ counts.moves + counts.removes + counts.unlaunched + counts.ecosystem;
+
+console.info("=== Phase 2 move plan ===");
+console.info(counts);
+console.info("total actioned:", actioned, "/ expected 267");
+
+if (collisions.length > 0) {
+ console.error("COLLISIONS DETECTED:");
+ for (const [dest, sources] of collisions) {
+ console.error(` ${dest} <= ${sources.join(", ")}`);
+ }
+ process.exit(1);
+}
+console.info("collisions: none");
+
+if (actioned !== 267) {
+ console.error(`ABORT: actioned ${actioned} != 267`);
+ process.exit(1);
+}
+
+const isTracked = (file: string): boolean => {
+ try {
+ execFileSync("git", ["ls-files", "--error-unmatch", file], {
+ cwd: repoRoot,
+ stdio: "pipe",
+ });
+ return true;
+ } catch {
+ return false;
+ }
+};
+
+const gitMv = (from: string, to: string) => {
+ mkdirSync(dirname(join(repoRoot, to)), { recursive: true });
+ if (isTracked(from)) {
+ execFileSync("git", ["mv", from, to], { cwd: repoRoot, stdio: "pipe" });
+ } else {
+ renameSync(join(repoRoot, from), join(repoRoot, to));
+ }
+};
+
+const remove = (file: string) => {
+ if (isTracked(file)) {
+ execFileSync("git", ["rm", "--quiet", file], {
+ cwd: repoRoot,
+ stdio: "pipe",
+ });
+ } else {
+ rmSync(join(repoRoot, file));
+ }
+};
+
+const execute = process.argv.includes("--execute");
+if (!execute) {
+ console.info("\n(dry run) pass --execute to perform the moves");
+ process.exit(0);
+}
+
+console.info("\n=== executing ===");
+for (const a of actions) {
+ if (a.type === "move" || a.type === "unlaunched") {
+ gitMv(a.from, a.to as string);
+ } else if (a.type === "remove") {
+ remove(a.from);
+ }
+}
+
+// Prune now-empty former realm directories (that held only moved product files)
+const formerRealmDirs = [
+ "armory",
+ "core",
+ "fabric",
+ "grid",
+ "kindred",
+ "codex",
+ "reality",
+ "sigil",
+ "worlds",
+ "carbon",
+ "ember",
+ "quantum",
+ "vault",
+];
+for (const d of formerRealmDirs) {
+ const abs = join(repoRoot, "content/docs", d);
+ // recurse subdirs first, removing any that become empty
+ const recurse = (dir: string) => {
+ if (!existsSync(dir)) return;
+ for (const ent of readdirSync(dir, { withFileTypes: true })) {
+ if (ent.isDirectory()) recurse(join(dir, ent.name));
+ }
+ if (existsSync(dir) && readdirSync(dir).length === 0) {
+ rmSync(dir, { recursive: true, force: true });
+ console.info("pruned empty dir:", dir.slice(repoRoot.length + 1));
+ }
+ };
+ recurse(abs);
+}
+
+console.info("\ndone");
diff --git a/scripts/migration/rewriteLinkTarget.ts b/scripts/migration/rewriteLinkTarget.ts
new file mode 100644
index 0000000..4eba833
--- /dev/null
+++ b/scripts/migration/rewriteLinkTarget.ts
@@ -0,0 +1,30 @@
+/**
+ * Rewrite a single absolute internal link target through the migration map.
+ *
+ * A link target may carry a `#anchor` and/or `?query` suffix; those are split
+ * off, the bare path is matched against the map by longest key that equals the
+ * path or is a path-segment prefix of it, the matched prefix is swapped for its
+ * new value, and the suffix is reattached. Unknown paths pass through unchanged.
+ */
+
+export type LinkMap = Map;
+
+/** Rewrite `target` using `map`; returns it unchanged when nothing matches */
+export const rewriteLinkTarget = (target: string, map: LinkMap): string => {
+ const suffixIndex = target.search(/[#?]/);
+ const path = suffixIndex === -1 ? target : target.slice(0, suffixIndex);
+ const suffix = suffixIndex === -1 ? "" : target.slice(suffixIndex);
+
+ let bestKey: string | null = null;
+ for (const key of map.keys()) {
+ const isMatch = path === key || path.startsWith(`${key}/`);
+ if (isMatch && (bestKey === null || key.length > bestKey.length)) {
+ bestKey = key;
+ }
+ }
+
+ if (bestKey === null) return target;
+
+ const replacement = map.get(bestKey) as string;
+ return `${replacement}${path.slice(bestKey.length)}${suffix}`;
+};
diff --git a/scripts/migration/rewriteLinks.ts b/scripts/migration/rewriteLinks.ts
new file mode 100644
index 0000000..0fe1fcc
--- /dev/null
+++ b/scripts/migration/rewriteLinks.ts
@@ -0,0 +1,94 @@
+/**
+ * Rewrite legacy absolute internal links across all served docs MDX.
+ *
+ * Builds the old-url -> new-url map from classification.json (every product,
+ * realm-hub, and realm-page whose `to` differs from its `url`), then walks every
+ * .mdx under content/docs and rewrites markdown link targets of the form
+ * `](/path#anchor)` via rewriteLinkTarget. Excluded pages (carbon, ember,
+ * quantum, vault) are not in the map; any link that still points at one is left
+ * as-is and reported, since its target no longer exists in the served tree.
+ */
+
+import { readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
+import { join, resolve } from "node:path";
+
+import { rewriteLinkTarget } from "./rewriteLinkTarget";
+
+import type { LinkMap } from "./rewriteLinkTarget";
+
+type Kind =
+ | "product"
+ | "realm-hub"
+ | "realm-page"
+ | "ecosystem"
+ | "exclude"
+ | "redirect-only";
+
+interface Entry {
+ url: string;
+ file: string;
+ kind: Kind;
+ to: string | null;
+}
+
+const repoRoot = resolve(import.meta.dir, "../..");
+const classification: Entry[] = JSON.parse(
+ readFileSync(
+ join(repoRoot, "scripts/migration/data/classification.json"),
+ "utf8",
+ ),
+);
+
+const map: LinkMap = new Map();
+for (const e of classification) {
+ const moved =
+ e.kind === "product" || e.kind === "realm-hub" || e.kind === "realm-page";
+ if (moved && e.to !== null && e.to !== e.url) map.set(e.url, e.to);
+}
+
+// Excluded legacy path prefixes whose targets no longer exist in the served tree
+const excludedPrefixes = ["/carbon", "/ember", "/quantum", "/vault"];
+
+const walk = (dir: string): string[] => {
+ const out: string[] = [];
+ for (const name of readdirSync(dir)) {
+ const p = join(dir, name);
+ if (statSync(p).isDirectory()) out.push(...walk(p));
+ else if (p.endsWith(".mdx")) out.push(p);
+ }
+ return out;
+};
+
+const linkRe = /(\]\()(\/[^)\s]+)(\))/g;
+const contentRoot = join(repoRoot, "content/docs");
+const files = walk(contentRoot);
+
+let rewritten = 0;
+let filesChanged = 0;
+const excludedLinks: { file: string; target: string }[] = [];
+
+for (const file of files) {
+ const original = readFileSync(file, "utf8");
+ let fileHits = 0;
+ const updated = original.replace(linkRe, (_m, open, target, close) => {
+ const path = target.split(/[#?]/)[0];
+ if (excludedPrefixes.some((p) => path === p || path.startsWith(`${p}/`))) {
+ excludedLinks.push({ file: file.slice(repoRoot.length + 1), target });
+ return `${open}${target}${close}`;
+ }
+ const next = rewriteLinkTarget(target, map);
+ if (next !== target) fileHits++;
+ return `${open}${next}${close}`;
+ });
+ if (updated !== original) {
+ writeFileSync(file, updated);
+ rewritten += fileHits;
+ filesChanged++;
+ }
+}
+
+console.info(`files scanned: ${files.length}`);
+console.info(`files changed: ${filesChanged}`);
+console.info(`links rewritten: ${rewritten}`);
+console.info(`links to excluded pages (left as-is): ${excludedLinks.length}`);
+for (const l of excludedLinks) console.info(` ${l.file}: ${l.target}`);
diff --git a/scripts/migration/snapshotUrls.ts b/scripts/migration/snapshotUrls.ts
new file mode 100644
index 0000000..008d979
--- /dev/null
+++ b/scripts/migration/snapshotUrls.ts
@@ -0,0 +1,91 @@
+/**
+ * Snapshot every currently-live docs URL into a stable JSON manifest.
+ *
+ * The docs site is Fumadocs on TanStack Start with `baseUrl: "/"`: content
+ * lives in `content/docs/**\/*.mdx` and a file's URL is its path relative to
+ * `content/docs`, minus the `.mdx` extension. Fumadocs collapses `index.mdx`
+ * onto its parent directory URL (`grid/vortex/index.mdx` -> `/grid/vortex`,
+ * root `index.mdx` -> `/`).
+ *
+ * This walks the content tree, derives each URL, and writes a sorted (by url)
+ * array of `{ url, file }` to `scripts/migration/data/legacy-urls.json`. That
+ * snapshot is the source of truth for the 301 redirect map built in a later
+ * phase, so it must capture EVERY live URL: a missing entry means a dead
+ * inbound link once files move under `/products/`. The output is stable
+ * (deterministic walk + sort) so re-runs produce identical diffs, and the run
+ * fails loudly if two files map to the same URL.
+ */
+
+import { readdirSync } from "node:fs";
+import { relative, resolve } from "node:path";
+
+const repoRoot = resolve(import.meta.dir, "../..");
+const contentRoot = resolve(repoRoot, "content/docs");
+const outPath = resolve(import.meta.dir, "data/legacy-urls.json");
+
+/** Recursively collect absolute paths of every `.mdx` file under a directory. */
+const walk = (dir: string): string[] => {
+ const entries = readdirSync(dir, { withFileTypes: true });
+ const files: string[] = [];
+ for (const entry of entries) {
+ const full = resolve(dir, entry.name);
+ if (entry.isDirectory()) {
+ files.push(...walk(full));
+ } else if (entry.isFile() && entry.name.endsWith(".mdx")) {
+ files.push(full);
+ }
+ }
+ return files;
+};
+
+/**
+ * Derive the live Fumadocs URL for an mdx file, given its path relative to
+ * `content/docs` (POSIX-style, e.g. `grid/vortex/index.mdx`).
+ */
+const toUrl = (relPath: string): string => {
+ const noExt = relPath.replace(/\.mdx$/, "");
+ // Collapse `index` (the parent directory's own page) onto the directory URL
+ const collapsed = noExt === "index" ? "" : noExt.replace(/\/index$/, "");
+ return `/${collapsed}`;
+};
+
+const mdxFiles = walk(contentRoot);
+
+const entries = mdxFiles.map((absPath) => {
+ const relToContent = relative(contentRoot, absPath).split("\\").join("/");
+ return {
+ url: toUrl(relToContent),
+ file: relative(repoRoot, absPath).split("\\").join("/"),
+ };
+});
+
+// Detect collisions: two distinct files resolving to the same URL
+const byUrl = new Map();
+for (const { url, file } of entries) {
+ const bucket = byUrl.get(url) ?? [];
+ bucket.push(file);
+ byUrl.set(url, bucket);
+}
+const collisions = [...byUrl.entries()].filter(([, files]) => files.length > 1);
+
+// Stable sort by url so re-runs produce identical output
+const sorted = [...entries].sort((a, b) => a.url.localeCompare(b.url));
+
+if (collisions.length > 0) {
+ // biome-ignore lint/suspicious/noConsole: script output
+ console.error(`[snapshot] FAIL: ${collisions.length} URL collision(s):`);
+ for (const [url, files] of collisions) {
+ // biome-ignore lint/suspicious/noConsole: script output
+ console.error(` ${url}\n ${files.join("\n ")}`);
+ }
+ process.exit(1);
+}
+
+await Bun.write(outPath, `${JSON.stringify(sorted, null, 2)}\n`);
+
+// biome-ignore lint/suspicious/noConsole: script output
+console.info(
+ `[snapshot] ${mdxFiles.length} mdx files -> ${sorted.length} unique URLs, 0 collisions`,
+);
+// biome-ignore lint/suspicious/noConsole: script output
+console.info(`[snapshot] wrote ${relative(repoRoot, outPath)}`);
diff --git a/src/components/docs/ProductOverview.tsx b/src/components/docs/ProductOverview.tsx
index d5b1647..6e8009d 100644
--- a/src/components/docs/ProductOverview.tsx
+++ b/src/components/docs/ProductOverview.tsx
@@ -1,9 +1,10 @@
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { Badge } from "@/components/ui/badge";
import { cn } from "@/lib/utils";
-import LinkButton, { type Link } from "./LinkButton";
+import LinkButton from "./LinkButton";
import type { ReactNode } from "react";
+import type { Link } from "./LinkButton";
interface ProductOverviewAlert {
/**
diff --git a/src/components/docs/ProductsIndex.tsx b/src/components/docs/ProductsIndex.tsx
new file mode 100644
index 0000000..053323e
--- /dev/null
+++ b/src/components/docs/ProductsIndex.tsx
@@ -0,0 +1,130 @@
+import { useMemo, useState } from "react";
+
+import { Badge } from "@/components/ui/badge";
+import { products as catalog } from "@/lib/catalog/generated/catalog";
+import { PRODUCT_REALMS } from "@/lib/productRealms.generated";
+import { cn } from "@/lib/utils";
+import realmsData from "../../../realms.json";
+
+interface ProductEntry {
+ /** Product id (URL slug). */
+ id: string;
+ /** Display name. */
+ name: string;
+ /** Realm id. */
+ realm: string;
+ /** Canonical docs URL. */
+ url: string;
+}
+
+/** Title case a product id as a display-name fallback. */
+const titleCase = (id: string): string =>
+ id
+ .split("-")
+ .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
+ .join(" ");
+
+/** Every public product that has docs, enriched from the omni-api catalog. */
+const ENTRIES: ProductEntry[] = Object.entries(PRODUCT_REALMS)
+ .map(([id, realm]) => ({
+ id,
+ realm,
+ name: catalog.find((product) => product.id === id)?.name ?? titleCase(id),
+ url: `/products/${id}`,
+ }))
+ .sort((a, b) => a.name.localeCompare(b.name));
+
+/** Realms that actually have at least one product, in realms.json order. */
+const REALM_FILTERS = realmsData.realms.filter((realm) =>
+ ENTRIES.some((entry) => entry.realm === realm.id),
+);
+
+/**
+ * Searchable, realm-filterable index of every product with documentation.
+ *
+ * Reads only committed public data (the generated product to realm map plus the
+ * omni-api catalog), so unlaunched private products never appear and every card
+ * links to a real page.
+ */
+const ProductsIndex: React.FC = () => {
+ const [query, setQuery] = useState("");
+ const [realm, setRealm] = useState(null);
+
+ const filtered = useMemo(() => {
+ const needle = query.trim().toLowerCase();
+
+ return ENTRIES.filter((entry) => {
+ if (realm && entry.realm !== realm) return false;
+ if (!needle) return true;
+
+ return (
+ entry.name.toLowerCase().includes(needle) ||
+ entry.id.toLowerCase().includes(needle)
+ );
+ });
+ }, [query, realm]);
+
+ return (
+