diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d11ce5e..e9cfa41 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -123,5 +123,8 @@ jobs: env: MDBASE_SPEC_DIR: .sources/mdbase-spec + - name: Build search index and Markdown copies + run: pnpm index:site + - name: Check local links run: pnpm check:links diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 5e378ad..36c695b 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -119,6 +119,9 @@ jobs: env: MDBASE_SPEC_DIR: .sources/mdbase-spec + - name: Build search index and Markdown copies + run: pnpm index:site + - name: Check local links run: pnpm check:links diff --git a/DESIGN.md b/DESIGN.md index a4bad0e..e051f0c 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -33,14 +33,14 @@ synchronised by the build. The site overrides only the canvas, in The mark is the square-bar icon (`src/components/BrandMark.astro`). It sits beside the wordmark in the header, and a hover ripple runs through the bars in reading order, matching the apps. The favicon and the social card -(`public/og-card.png`, 1200×630) use the same mark. +(`public/og-card.jpg`, 1200×630) use the same mark. The homepage pairs a moving sky (`public/mdbase-atmosphere.js`) with one field of 361 record particles (`public/mdbase-murmuration.js`). The field begins as a murmuration and becomes concrete diagrams as the reader scrolls: files, apps, -editing, sync, Cloud copy and Private, permissions, and sharing. Canvas -surfaces, labels, edges and particles use the page's colour roles. Dashed boxes -mark copies that are offline, locked or removed; they keep a solid surface so +sync, apps reaching the collection through mdbase, permissions, and sharing. +Canvas surfaces, labels, edges and particles use the page's colour roles. +Dashed boxes mark copies that are offline, switched off or removed; they keep a solid surface so they read against the sky. ## Typography diff --git a/README.md b/README.md index 732f70f..27f5c30 100644 --- a/README.md +++ b/README.md @@ -32,9 +32,17 @@ pnpm check pnpm build (cd ../mdbase-spec/site && npm ci && npm run build) pnpm import:spec +pnpm index:site pnpm check:links ``` +`pnpm index:site` builds the Pagefind index for the header's search dialog, +then writes a Markdown copy of each page (`index.md` beside its `index.html`), +`/llms.txt` and `/llms-full.txt` (`scripts/build-llms.mjs`). It runs after the +specification import so the specification is included. Each page's `
` is +used; headers, footers and documentation sidebars are excluded, as are pages +marked `noIndex` and the archived specification. + The website repository owns the ecosystem pages and deployment cadence. The normative specification is built by `mdbase-spec` and imported under `/spec/`. The import preserves its content and section anchors, inserting them into the @@ -81,9 +89,9 @@ pnpm deploy:dev ``` The command builds the current working tree for -`https://mdbase-dev.pages.dev`, imports the specification, checks local links, -and deploys the `main` branch of the standalone `mdbase-dev` Cloudflare Pages -project. It verifies the live homepage, specification, canonical URLs, and +`https://mdbase-dev.pages.dev`, imports the specification, builds the search +index and Markdown copies, checks local links, and deploys the `main` branch of +the standalone `mdbase-dev` Cloudflare Pages project. It verifies the live homepage, specification, canonical URLs, and indexing controls after deployment. The production GitHub Pages deployment and `mdbase.dev` domain are unchanged. diff --git a/package.json b/package.json index c9ddb19..1cb494c 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "sync:mcp-tools": "node scripts/sync-mcp-tools.mjs", "sync:contracts": "node scripts/sync-contracts.mjs", "import:spec": "node scripts/import-spec.mjs", + "index:site": "pagefind --site dist && node scripts/build-llms.mjs", "check:links": "node scripts/check-links.mjs", "check:release": "node scripts/check-connect-release.mjs", "check:release-order": "node scripts/check-connect-release-order.mjs", @@ -21,7 +22,7 @@ "test:home": "node --test scripts/home-animation.test.mjs", "test:docs": "node --test scripts/app-docs.test.mjs", "test:spec": "node --test scripts/spec-page.test.mjs", - "test": "pnpm test:spec && pnpm test:home && pnpm test:docs && pnpm test:deploy:dev && pnpm test:release-update && pnpm check:release && pnpm check && pnpm build && pnpm import:spec && pnpm check:links" + "test": "pnpm test:spec && pnpm test:home && pnpm test:docs && pnpm test:deploy:dev && pnpm test:release-update && pnpm check:release && pnpm check && pnpm build && pnpm import:spec && pnpm index:site && pnpm check:links" }, "dependencies": { "@astrojs/sitemap": "^3.7.3", @@ -32,6 +33,8 @@ }, "devDependencies": { "@astrojs/check": "^0.9.9", + "pagefind": "1.5.2", + "turndown": "7.2.4", "typescript": "^6.0.3" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8a77e35..c109d08 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -27,6 +27,12 @@ importers: '@astrojs/check': specifier: ^0.9.9 version: 0.9.9(prettier@3.9.6)(typescript@6.0.3) + pagefind: + specifier: 1.5.2 + version: 1.5.2 + turndown: + specifier: 7.2.4 + version: 7.2.4 typescript: specifier: ^6.0.3 version: 6.0.3 @@ -56,24 +62,28 @@ packages: engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] + libc: [glibc] '@astrojs/compiler-binding-linux-arm64-musl@0.3.1': resolution: {integrity: sha512-xqE8BVbDoBueK/B47w30PtkVofUWJKGkwoMVE+EOMLf11rnoANxIAdA9FPqY+rng4oNI5ndHGsri1yPj2k8vZQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] + libc: [musl] '@astrojs/compiler-binding-linux-x64-gnu@0.3.1': resolution: {integrity: sha512-1y0StU1qiCuDFH3rmbRJXcxdfHxFPrES1Rd+RLffosvUR7I2cH5SF5SFnBN9vXpzpkmyElZm3Yr47iJBPN7vVA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] + libc: [glibc] '@astrojs/compiler-binding-linux-x64-musl@0.3.1': resolution: {integrity: sha512-16q0fYf7kpbmdObZEeZJEup8hQv/whgNwVjrSvT8umrKwLDSnNIWiQpm09lQQu6bweZB0XyIvHwlPitvJhC+hg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] + libc: [musl] '@astrojs/compiler-binding-wasm32-wasi@0.3.1': resolution: {integrity: sha512-cB456shIwDv/PrVT+2QG7LFndpHkVge5HjqADKZgGaAc9JHVktCtjSrcdkRQ+3tbkPazNKaTLRjXLIiz2NIx9g==} @@ -166,21 +176,25 @@ packages: resolution: {integrity: sha512-u51id17uJwNEMK9nBlICsq6U31c+XVqQueVBkwRIzZG+gMpS8TOJctt5h5Wz33Z8xnMdTd+adtACVz0yHgGuOA==} cpu: [arm64] os: [linux] + libc: [glibc] '@bruits/satteri-linux-arm64-musl@0.9.5': resolution: {integrity: sha512-v39HxiwGC5Rqm01HksP6+5Y+xKLPlsuVFgIgpEAo+SiQ22c+mJVhS3u7Z6ePAKdhL5NJoK1xq70kLz3L13AhpQ==} cpu: [arm64] os: [linux] + libc: [musl] '@bruits/satteri-linux-x64-gnu@0.9.5': resolution: {integrity: sha512-F3uO8uFp3pAP5ZGXttwvh57GS7s0lL953tnNdyI2gRyP4kOOkp6pyGojNJzCjkDvWI2Cvb9iNrKok3aqQPauAw==} cpu: [x64] os: [linux] + libc: [glibc] '@bruits/satteri-linux-x64-musl@0.9.5': resolution: {integrity: sha512-bicEqglLlz++mWyADaZoP0JY20s4vDfLjaPYgQqC+NI4zZLTOOg1T4GB8aqtc822Pqji8SQBmSrTb7CrP8i08Q==} cpu: [x64] os: [linux] + libc: [musl] '@bruits/satteri-wasm32-wasi@0.9.5': resolution: {integrity: sha512-zauAuMwfPnKPUkd4AFixRFpXdgKwP2mKgxrIIo2gJzW0/ZneF9dbHnLkojSpaBnCCp7VUL1hIi5WWZvB1CqmAQ==} @@ -439,89 +453,105 @@ packages: resolution: {integrity: sha512-dqVSFynCox4C/J8kT16V7SIFAns0IjgLwkvYT7p8LQVmJ5OS5b6tI9IGflxTeuBS//zXeFIUbwt5dwxyZ17cnA==} cpu: [arm64] os: [linux] + libc: [glibc] '@img/sharp-libvips-linux-arm@1.3.2': resolution: {integrity: sha512-1eMLzy92I4J6rmi4mAT8yC3HxOtniyGELlzGbNMLLeqe052ahFQ0h6LFq+lh5DsDIdYViIDst08abvSbcEdLXQ==} cpu: [arm] os: [linux] + libc: [glibc] '@img/sharp-libvips-linux-ppc64@1.3.2': resolution: {integrity: sha512-3z0NHDxD6n5I9gc05U1eW1AyRm+Gznzq3naMrthPNqE6oYykcogW0l/jfpJdjYnuNl8R7yI9pNbE1XiUeyq0Aw==} cpu: [ppc64] os: [linux] + libc: [glibc] '@img/sharp-libvips-linux-riscv64@1.3.2': resolution: {integrity: sha512-bsb4rI+NldGOsXuej2r8OdSS8+zXDVaCWxyWrcv6kneTOlgAHtZABRzBBCwdsPiD90J4myNJuHpg6kA20ImW/w==} cpu: [riscv64] os: [linux] + libc: [glibc] '@img/sharp-libvips-linux-s390x@1.3.2': resolution: {integrity: sha512-/ABshyj8gCpyIrNXnHn4LorDJ0HHm1VhXPBlxZ8zAtfVPAaSafXPGn+sUSIRiwaSBy0mmFjSjiXI5mkcwdChKQ==} cpu: [s390x] os: [linux] + libc: [glibc] '@img/sharp-libvips-linux-x64@1.3.2': resolution: {integrity: sha512-ITPEtgffGJ0S6G9dRyw/366tJQqFRcHWPHhC+Stpg3Z8AEMrDrTr2lhdz4f/Y/HMbRh//7Z5mBzEpVdi62Oc3w==} cpu: [x64] os: [linux] + libc: [glibc] '@img/sharp-libvips-linuxmusl-arm64@1.3.2': resolution: {integrity: sha512-zE9EdiUzUmg5mDT5a1rk5fYJ6GWPloTwWBYDS14naqHsL+EaMpDj1AWnpLgh3u0YCORv2Tt50wrcrpYqkP97Kw==} cpu: [arm64] os: [linux] + libc: [musl] '@img/sharp-libvips-linuxmusl-x64@1.3.2': resolution: {integrity: sha512-m0lrLiUt+lBYnCFr8qV/65yMR4E/c7/wf78I5eKTdkEakFAlZ9QlzEM3QIhhAwVeUhLAHLcCq7a7Vszq/oFNZQ==} cpu: [x64] os: [linux] + libc: [musl] '@img/sharp-linux-arm64@0.35.3': resolution: {integrity: sha512-QgKDspHPnrU+GQ55XPhGwyhC8acLVOOSyAvo1oVfFmrIXLkDNmGWzAfDZ4xK8oSA1qBQrALcHX0G5UZni/SuFQ==} engines: {node: '>=20.9.0'} cpu: [arm64] os: [linux] + libc: [glibc] '@img/sharp-linux-arm@0.35.3': resolution: {integrity: sha512-affVWCTLooy8TSxbDx2qkzuDeaWLNVBA+P//FNBirHsXpP2fuBhk5AuboYUnrDnzoXes8GFjpTx0SBFOCRg+FA==} engines: {node: '>=20.9.0'} cpu: [arm] os: [linux] + libc: [glibc] '@img/sharp-linux-ppc64@0.35.3': resolution: {integrity: sha512-sMd8rDxmpLOwv/7N44klFjOD5DUO7FLdjiXDI0hoxYaf7Ar262dQIEkosE98bps+5HPLtp/EvNqeqQtOycP/IA==} engines: {node: '>=20.9.0'} cpu: [ppc64] os: [linux] + libc: [glibc] '@img/sharp-linux-riscv64@0.35.3': resolution: {integrity: sha512-0Eob78yjlYPfL5vMNWAW55l3R9Y6BQS/gOfe0ZcP9mEz9ohhKSt4im1hayiknXgf8AWrFqMvJcKIdmLmEe7yeQ==} engines: {node: '>=20.9.0'} cpu: [riscv64] os: [linux] + libc: [glibc] '@img/sharp-linux-s390x@0.35.3': resolution: {integrity: sha512-KgAxQ0DxpNOq1rG2t5cgTgShJFGSuU7XO45cqC+1NVOuZnP6tlgZRuSYOfNupGkHID0o3cJOsw4DVeJpMovcGw==} engines: {node: '>=20.9.0'} cpu: [s390x] os: [linux] + libc: [glibc] '@img/sharp-linux-x64@0.35.3': resolution: {integrity: sha512-8pqvxubL2PGdhlPy6GLqzDYMUjyRmKAwKHYKixpdJYBUK7PJ0C029XdsnpFIdgRZG68fZiGdHVWcKPvtiPB4cA==} engines: {node: '>=20.9.0'} cpu: [x64] os: [linux] + libc: [glibc] '@img/sharp-linuxmusl-arm64@0.35.3': resolution: {integrity: sha512-Vz0iQjzzcSX3HCbfwFfCSG/9SCIqyO0mH2sXyiHaAYfBk0cRsCWXRyQYX0ovCK/PAQBbTzQ0dsPQHh5MAFL59w==} engines: {node: '>=20.9.0'} cpu: [arm64] os: [linux] + libc: [musl] '@img/sharp-linuxmusl-x64@0.35.3': resolution: {integrity: sha512-6O1NPKcDVj9QEdg7Hx549EX8U0rp6yXQERqru6yRN7fGBn32UvIRJUlWnk+8xDCiG76hXVBbX82NZ/ZKr0euIg==} engines: {node: '>=20.9.0'} cpu: [x64] os: [linux] + libc: [musl] '@img/sharp-wasm32@0.35.3': resolution: {integrity: sha512-cZ0XkcYGpHZkqW6iCkqTcmUC0CD9DhD5d/qeZlZkfRBn6GnHniZXLUo5+9xw8Iv76YE6LQFN9YNBlKREcCG76w==} @@ -553,6 +583,9 @@ packages: '@jridgewell/sourcemap-codec@1.5.5': resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + '@mixmark-io/domino@2.2.0': + resolution: {integrity: sha512-Y28PR25bHXUg88kCV7nivXrP2Nj2RueZ3/l/jdx6J9f8J4nsEGcgX0Qe6lt7Pa+J79+kPiJU3LguR6O/6zrLOw==} + '@napi-rs/wasm-runtime@1.1.6': resolution: {integrity: sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==} peerDependencies: @@ -565,6 +598,41 @@ packages: '@oxc-project/types@0.139.0': resolution: {integrity: sha512-r9gHphtCs+1M7J0pw6Sn/hh/Wpa/iQrOOkrNAlVLF/gHq+/CJmHIWKKUUhdWjcD6CIa8idarspCsASiXCXvFUw==} + '@pagefind/darwin-arm64@1.5.2': + resolution: {integrity: sha512-MXpI+7HsAdPkvJ0gk9xj9g541BCqBZOBbdwj9g6lB5LCj6kSV6nqDSjzcAJwvOsfu0fjwvC8hQU+ecfhp+MpiQ==} + cpu: [arm64] + os: [darwin] + + '@pagefind/darwin-x64@1.5.2': + resolution: {integrity: sha512-IojxFWMEJe0RQ7PQ3KXQsPIImNsbpPYpoZ+QUDrL8fAl/O27IX+LVLs74/UzEZy5uA2LD8Nz1AiwKr72vrkZQw==} + cpu: [x64] + os: [darwin] + + '@pagefind/freebsd-x64@1.5.2': + resolution: {integrity: sha512-7EVzo9+0w+2cbe671BtMj10UlNo83I+HrLVLfRxO731svHRJKUfJ/mo05gU14pe9PCfpKNQT8FS3Xc/oDN6pOA==} + cpu: [x64] + os: [freebsd] + + '@pagefind/linux-arm64@1.5.2': + resolution: {integrity: sha512-Ovt9+K35sqzn8H3ZMXGwls4TD/wMJuvRtShHIsmUQREmaxjrDEX7gHckRCrwYJ4XE1H1p6HkLz3wukrAnsfXQw==} + cpu: [arm64] + os: [linux] + + '@pagefind/linux-x64@1.5.2': + resolution: {integrity: sha512-V+tFqHKXhQKq/WqPBD67AFy7scn1/aZID00ws4fSDd+1daSi5UHR9VVlRrOUYKxn3VuFQYRD7lYXdZK1WED1YA==} + cpu: [x64] + os: [linux] + + '@pagefind/windows-arm64@1.5.2': + resolution: {integrity: sha512-hN9Nh90fNW61nNRCW9ZyQrAj/mD0eRvmJ8NlTUzkbuW8kIzGJUi3cxjFkEcMZ5h/8FsKWD/VcouZl4yo1F7B6g==} + cpu: [arm64] + os: [win32] + + '@pagefind/windows-x64@1.5.2': + resolution: {integrity: sha512-Fa2Iyw7kaDRzGMfNYNUXNW2zbL5FQVDgSOcbDHdzBrDEdpqOqg8TcZ68F22ol6NJ9IGzvUdmeyZypLW5dyhqsg==} + cpu: [x64] + os: [win32] + '@rolldown/binding-android-arm64@1.1.5': resolution: {integrity: sha512-lZg8fqIv2v7FF237bwMgzGZEJvGL79/s5knJ/i6FmsGF4XXlzccZ4jb+TrFIxtSSxFtIpdsgrPZeMk1I9AFcyQ==} engines: {node: ^20.19.0 || >=22.12.0} @@ -600,36 +668,42 @@ packages: engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] + libc: [glibc] '@rolldown/binding-linux-arm64-musl@1.1.5': resolution: {integrity: sha512-lAryqH7IteztmCXQXk0etKj4wBQ7Gx5S6LjKhsgp9zb8I5bsuvU/2llH1hDQcjsFeqIsovMVN339/8pUDDBXxA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] + libc: [musl] '@rolldown/binding-linux-ppc64-gnu@1.1.5': resolution: {integrity: sha512-fsK/sNBnxzBlL4O1JNrZakVQxPspqpED5dLtNsZS9oOKmtSpdNIzxH2kkol5HYTWJN47sE20ztMJPxfZ89qGOg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] + libc: [glibc] '@rolldown/binding-linux-s390x-gnu@1.1.5': resolution: {integrity: sha512-gLYb4BIadlfTOYT5gO503n8zQjXflgzpD0FcyKh0Mzx3rqCZKnHoJWV9xe1KXUJ5lx2JfcSHr/mhzS0PC/McAA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] + libc: [glibc] '@rolldown/binding-linux-x64-gnu@1.1.5': resolution: {integrity: sha512-FjcpEKUyJygHgs1o50VYNvkt5+7Le/VEdYt0AkRpkL33MnyQfwr8l5mXwMmfmTbyMPr5vJLC+8/Gd9gXnwU1QQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] + libc: [glibc] '@rolldown/binding-linux-x64-musl@1.1.5': resolution: {integrity: sha512-Me+PfPI2TMeOQk0gYWfLQZtTktrmzbr8cDboqX83XKc7UrgAi55gF+2dUkWdxd19n55Essp2yeca+O9N5rBxHg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] + libc: [musl] '@rolldown/binding-openharmony-arm64@1.1.5': resolution: {integrity: sha512-yc5WrLzXks6zCQfn9Oxr8pORKyl/pF+QjHmW/Qx3qu0oyrrNC+y2JLTU1E2rcWYAmzlnqngWXHQjy51VzW70Vw==} @@ -1116,24 +1190,28 @@ packages: engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] + libc: [glibc] lightningcss-linux-arm64-musl@1.33.0: resolution: {integrity: sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] + libc: [musl] lightningcss-linux-x64-gnu@1.33.0: resolution: {integrity: sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] + libc: [glibc] lightningcss-linux-x64-musl@1.33.0: resolution: {integrity: sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] + libc: [musl] lightningcss-win32-arm64-msvc@1.33.0: resolution: {integrity: sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==} @@ -1248,6 +1326,10 @@ packages: package-manager-detector@1.8.0: resolution: {integrity: sha512-yQA4H19AmPEoMUeavPMDIe1higySl/gH/yaQrkT/s07Qp+7pp2hYz30N3z2l5BkjVkF9Ow6o0wjJamm2y7Sn0A==} + pagefind@1.5.2: + resolution: {integrity: sha512-XTUaK0hXMCu2jszWE584JGQT7y284TmMV9l/HX3rnG5uo3rHI/uHU56XTyyyPFjeWEBxECbAi0CaFDJOONtG0Q==} + hasBin: true + parse5@7.3.0: resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==} @@ -1420,6 +1502,10 @@ packages: tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + turndown@7.2.4: + resolution: {integrity: sha512-I8yFsfRzmzK0WV1pNNOA4A7y4RDfFxPRxb3t+e3ui14qSGOxGtiSP6GjeX+Y6CHb7HYaFj7ECUD7VE5kQMZWGQ==} + engines: {node: '>=18', npm: '>=9'} + typesafe-path@0.2.2: resolution: {integrity: sha512-OJabfkAg1WLZSqJAJ0Z6Sdt3utnbzr/jh+NAHoyWHJe8CMSy79Gm085094M9nvTPy22KzTVn5Zq5mbapCI/hPA==} @@ -2167,6 +2253,8 @@ snapshots: '@jridgewell/sourcemap-codec@1.5.5': {} + '@mixmark-io/domino@2.2.0': {} + '@napi-rs/wasm-runtime@1.1.6(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)': dependencies: '@emnapi/core': 1.11.1 @@ -2178,6 +2266,27 @@ snapshots: '@oxc-project/types@0.139.0': {} + '@pagefind/darwin-arm64@1.5.2': + optional: true + + '@pagefind/darwin-x64@1.5.2': + optional: true + + '@pagefind/freebsd-x64@1.5.2': + optional: true + + '@pagefind/linux-arm64@1.5.2': + optional: true + + '@pagefind/linux-x64@1.5.2': + optional: true + + '@pagefind/windows-arm64@1.5.2': + optional: true + + '@pagefind/windows-x64@1.5.2': + optional: true + '@rolldown/binding-android-arm64@1.1.5': optional: true @@ -2922,6 +3031,16 @@ snapshots: package-manager-detector@1.8.0: {} + pagefind@1.5.2: + optionalDependencies: + '@pagefind/darwin-arm64': 1.5.2 + '@pagefind/darwin-x64': 1.5.2 + '@pagefind/freebsd-x64': 1.5.2 + '@pagefind/linux-arm64': 1.5.2 + '@pagefind/linux-x64': 1.5.2 + '@pagefind/windows-arm64': 1.5.2 + '@pagefind/windows-x64': 1.5.2 + parse5@7.3.0: dependencies: entities: 6.0.1 @@ -3129,6 +3248,10 @@ snapshots: tslib@2.8.1: optional: true + turndown@7.2.4: + dependencies: + '@mixmark-io/domino': 2.2.0 + typesafe-path@0.2.2: {} typescript-auto-import-cache@0.3.6: diff --git a/public/images/apps/editor-dark.webp b/public/images/apps/editor-dark.webp new file mode 100644 index 0000000..b72aa9a Binary files /dev/null and b/public/images/apps/editor-dark.webp differ diff --git a/public/images/apps/editor-light.webp b/public/images/apps/editor-light.webp new file mode 100644 index 0000000..4165072 Binary files /dev/null and b/public/images/apps/editor-light.webp differ diff --git a/public/images/apps/reader-dark.webp b/public/images/apps/reader-dark.webp new file mode 100644 index 0000000..94e4ab2 Binary files /dev/null and b/public/images/apps/reader-dark.webp differ diff --git a/public/images/apps/reader-light.webp b/public/images/apps/reader-light.webp new file mode 100644 index 0000000..da1edea Binary files /dev/null and b/public/images/apps/reader-light.webp differ diff --git a/public/images/apps/tasknotes-dark.webp b/public/images/apps/tasknotes-dark.webp new file mode 100644 index 0000000..e577208 Binary files /dev/null and b/public/images/apps/tasknotes-dark.webp differ diff --git a/public/images/apps/tasknotes-light.webp b/public/images/apps/tasknotes-light.webp new file mode 100644 index 0000000..875f20d Binary files /dev/null and b/public/images/apps/tasknotes-light.webp differ diff --git a/public/images/apps/writer-dark.webp b/public/images/apps/writer-dark.webp new file mode 100644 index 0000000..1b159c7 Binary files /dev/null and b/public/images/apps/writer-dark.webp differ diff --git a/public/images/apps/writer-light.webp b/public/images/apps/writer-light.webp new file mode 100644 index 0000000..cb2788c Binary files /dev/null and b/public/images/apps/writer-light.webp differ diff --git a/public/mdbase-atmosphere.js b/public/mdbase-atmosphere.js index 6279a82..20c3574 100644 --- a/public/mdbase-atmosphere.js +++ b/public/mdbase-atmosphere.js @@ -131,7 +131,7 @@ const currentDark = () => root.dataset.theme ? root.dataset.theme === 'dark' : systemDark.matches; function sync() { dark = currentDark(); - grain.style.backgroundImage = `url("/textures/${dark ? 'dark' : 'light'}-2.png")`; + grain.style.backgroundImage = `url("/textures/${dark ? 'dark' : 'light'}-2.webp")`; if (pauseControl) { pauseControl.textContent = paused ? 'Play motion' : 'Pause motion'; pauseControl.hidden = reducedMotion.matches; diff --git a/public/mdbase-murmuration.js b/public/mdbase-murmuration.js index 510b7d6..4f196a7 100644 --- a/public/mdbase-murmuration.js +++ b/public/mdbase-murmuration.js @@ -365,37 +365,6 @@ } } - function layoutMerge(bounds) { - const compact = bounds.w < 480; - const laptop = addCard("merge-laptop", "Laptop · text editor", [ - { text: "status: active", tone: "remove" }, - { text: "status: done", tone: "add" } - ], compact ? rect(bounds, 0.00, 0.00, 0.48, 0.36) : rect(bounds, 0.00, 0.06, 0.40, 0.28)); - const phone = addCard("merge-phone", "Phone · TaskNotes", [ - { text: "due: 2026-11-02", tone: "add" } - ], compact ? rect(bounds, 0.52, 0.00, 0.48, 0.36) : rect(bounds, 0.00, 0.66, 0.40, 0.28)); - const result = addCard("merge-result", "website-redesign.md", [ - { text: "---", tone: "faint" }, - "title: Website redesign", - { text: "status: done", tone: "add" }, - 'owner: "[[people/sam.md]]"', - { text: "due: 2026-11-02", tone: "add" }, - { text: "---", tone: "faint" } - ], compact ? rect(bounds, 0.14, 0.50, 0.72, 0.50) : rect(bounds, 0.53, 0.25, 0.47, 0.50)); - const fromLaptop = compact - ? [pointOnNode(laptop, "bottom"), pointOnNode(result, "top", 0.3)] - : [pointOnNode(laptop, "right"), pointOnNode(result, "left", 0.3)]; - const fromPhone = compact - ? [pointOnNode(phone, "bottom"), pointOnNode(result, "top", 0.7)] - : [pointOnNode(phone, "right"), pointOnNode(result, "left", 0.7)]; - addEdge(fromLaptop, null, "one field", true); - addEdge(fromPhone, null, "another field", true); - if (!compact) addNote("both changes kept", result.box.x + result.box.w / 2, result.box.y + result.box.h + 26); - pathParticles(0, 10, fromLaptop); - pathParticles(10, 10, fromPhone); - ambient(20); - } - function lockedLines(count) { return Array.from({ length: count }, (_, index) => ({ text: "▒▒▒▒▒▒▒▒ locked", tone: index === count - 1 ? "add" : "faint" })); @@ -454,36 +423,33 @@ ambient(next); } - function layoutOptions(bounds) { + // Apps on any device reach the collection through mdbase's copy, which + // stays in sync with your devices and answers while they are off. + function layoutServe(bounds) { const compact = bounds.w < 480; - const panels = [ - ["cloud", "Cloud copy", 0.00], - ["private", "Private", 0.53] - ]; - let next = 0; - panels.forEach(([id, label, x]) => { - addNode(`${id}-panel`, label, "", rect(bounds, x, 0.02, 0.47, 0.96), "boundary"); - const devices = addNode(`${id}-devices`, "your devices", "", rect(bounds, x + 0.04, 0.14, 0.39, 0.22), "standard"); - const log = addCard(`${id}-log`, "mdbase sync", [{ text: "▒▒▒▒▒▒ locked", tone: "faint" }], - rect(bounds, x + 0.04, 0.47, 0.39, 0.15), "log"); - const cloud = id === "cloud" - ? addNode(`${id}-copy`, compact ? "readable copy" : "readable copy · apps work while devices are off", "", rect(bounds, x + 0.04, 0.70, 0.39, 0.24), "accent") - : addCard(`${id}-copy`, compact ? "locked" : "locked · only your devices", [{ text: "apps go through", tone: "faint" }, { text: "your devices", tone: "faint" }], - rect(bounds, x + 0.04, 0.70, 0.39, 0.24), "replica"); - const up = [pointOnNode(devices, "bottom"), pointOnNode(log, "top")]; - addEdge(up, null, "", false).relationship = true; - gridParticles(next, 90, devices); - next += 90; - pathParticles(next, 6, up); + const laptop = addNode("serve-laptop", "Laptop", "", rect(bounds, 0.00, 0.06, 0.27, 0.36), "standard"); + const desktop = addNode("serve-desktop", compact ? "Desktop · off" : "Desktop · switched off", "", + rect(bounds, 0.00, 0.58, 0.27, 0.36), "replica"); + const hosted = addNode("serve-hosted", "mdbase", "", rect(bounds, 0.37, 0.18, 0.26, 0.64), "accent"); + const apps = [ + ["serve-app-0", "TaskNotes", "on a phone"], + ["serve-app-1", "Editor", "in a browser"], + ["serve-app-2", "Reader", "in a browser"] + ].map(([id, label, line], index) => addCard(id, label, [{ text: compact ? line.split(" ").pop() : line, tone: "faint" }], + rect(bounds, 0.73, 0.04 + index * 0.33, 0.27, 0.26), "app")); + const sync = [pointOnNode(laptop, "right"), pointOnNode(hosted, "left", 0.25)]; + addEdge(sync, null, "", false).relationship = true; + addEdge([pointOnNode(desktop, "right"), pointOnNode(hosted, "left", 0.75)], null, "", true).relationship = true; + gridParticles(0, 80, laptop); + gridParticles(80, 80, desktop, "replica"); + gridParticles(160, 120, hosted, "metadata"); + pathParticles(280, 6, sync); + let next = 286; + apps.forEach((app, index) => { + const path = [pointOnNode(app, "left"), pointOnNode(hosted, "right", 0.2 + index * 0.3)]; + addEdge(path, null, "", true); + pathParticles(next, 6, index % 2 ? path.slice().reverse() : path); next += 6; - if (id === "cloud") { - const down = [pointOnNode(log, "bottom"), pointOnNode(cloud, "top")]; - addEdge(down, null, "", false).relationship = true; - gridParticles(next, 90, cloud, "metadata"); - next += 90; - pathParticles(next, 6, down); - next += 6; - } }); ambient(next); } @@ -545,8 +511,8 @@ const layouts = { intro: layoutIntro, collection: layoutCollection, structure: layoutStructure, - apps: layoutApps, merge: layoutMerge, sync: layoutSync, options: layoutOptions, - grant: layoutGrant, sharing: layoutSharing + apps: layoutApps, sync: layoutSync, + grant: layoutGrant, sharing: layoutSharing, serve: layoutServe }; (layouts[name] || layoutIntro)(bounds); diff --git a/public/og-card.jpg b/public/og-card.jpg new file mode 100644 index 0000000..6f9792c Binary files /dev/null and b/public/og-card.jpg differ diff --git a/public/og-card.png b/public/og-card.png deleted file mode 100644 index 044e1a8..0000000 Binary files a/public/og-card.png and /dev/null differ diff --git a/public/textures/dark-2.png b/public/textures/dark-2.png deleted file mode 100644 index 358235a..0000000 Binary files a/public/textures/dark-2.png and /dev/null differ diff --git a/public/textures/dark-2.webp b/public/textures/dark-2.webp new file mode 100644 index 0000000..d1834db Binary files /dev/null and b/public/textures/dark-2.webp differ diff --git a/public/textures/light-2.png b/public/textures/light-2.png deleted file mode 100644 index 7884d22..0000000 Binary files a/public/textures/light-2.png and /dev/null differ diff --git a/public/textures/light-2.webp b/public/textures/light-2.webp new file mode 100644 index 0000000..8b8ade4 Binary files /dev/null and b/public/textures/light-2.webp differ diff --git a/scripts/app-docs.test.mjs b/scripts/app-docs.test.mjs index 294dfb9..6355690 100644 --- a/scripts/app-docs.test.mjs +++ b/scripts/app-docs.test.mjs @@ -20,8 +20,6 @@ test("Reader and Writer are listed with live applications and local documentatio assert.ok(catalogue.includes(`https://${app}.mdbase.dev/`)); assert.ok(catalogue.includes(`/apps/${app}/`)); } - assert.ok(catalogue.includes("https://reader.mdbase.dev/?preview=1")); - assert.ok(catalogue.includes("https://lab.mdbase-writer.pages.dev/?demo")); }); test("Every documentation navigation entry resolves to a site page", () => { diff --git a/scripts/build-llms.mjs b/scripts/build-llms.mjs new file mode 100644 index 0000000..be294fd --- /dev/null +++ b/scripts/build-llms.mjs @@ -0,0 +1,153 @@ +// Writes Markdown copies of the built pages for language models and other +// text tools: an index.md beside each searchable page, /llms.txt listing them, +// and /llms-full.txt with every page in one file. +// +// A page is included when its
carries data-pagefind-body, the same rule +// the site search uses, so noIndex pages and the archived specification stay out. +import { existsSync, readFileSync, readdirSync, writeFileSync } from "node:fs"; +import { dirname, join, relative, resolve, sep } from "node:path"; +import { fileURLToPath } from "node:url"; +import TurndownService from "turndown"; + +const root = resolve(fileURLToPath(new URL("../dist", import.meta.url))); +if (!existsSync(root)) throw new Error(`Build output is missing: ${root}`); + +const origin = (process.env.MDBASE_SITE_ORIGIN ?? "https://mdbase.dev").replace(/\/$/, ""); + +// Sections of /llms.txt, matched by path prefix in order. The first match wins. +const sections = [ + { title: "Product", match: (path) => ["/", "/connect/", "/downloads/", "/beta/"].includes(path) }, + { title: "Applications", match: (path) => path.startsWith("/apps/") }, + { title: "SDK", match: (path) => path.startsWith("/sdk/") }, + { + title: "Specification, runtime and contracts", + match: (path) => ["/spec/", "/runtime/", "/contracts/"].some((prefix) => path.startsWith(prefix)) + }, + { title: "Policies and support", match: () => true } +]; + +export function pageToMarkdown(html, turndown = createTurndown()) { + const main = html.match(/]*)>([\s\S]*?)<\/main>/); + if (!main || !/\bdata-pagefind-body\b/.test(main[1])) return null; + const title = decode(html.match(/([\s\S]*?)<\/title>/)?.[1] ?? "").replace(/ \| mdbase$/, ""); + const description = decode(html.match(/<meta name="description" content="([^"]*)"/)?.[1] ?? ""); + const body = turndown.turndown(main[2]).replace(/\n{3,}/g, "\n\n").trim(); + return { title, description, body }; +} + +export function createTurndown() { + const turndown = new TurndownService({ + headingStyle: "atx", + codeBlockStyle: "fenced", + bulletListMarker: "-", + emDelimiter: "*" + }); + turndown.remove(["script", "style", "canvas", "svg", "button", "video", "noscript"]); + turndown.addRule("ignored", { + filter: (node) => node.getAttribute?.("data-pagefind-ignore") != null || node.getAttribute?.("aria-hidden") === "true", + replacement: () => "" + }); + // The docs title block repeats the title and description given above the body, + // and a code block's caption only labels the copy button's target. + turndown.addRule("repeatedChrome", { + filter: (node) => + (node.nodeName === "HEADER" && node.classList?.contains("docs-title")) + || (node.nodeName === "FIGCAPTION" && node.parentNode?.classList?.contains("code-block")), + replacement: () => "" + }); + turndown.addRule("fencedLanguage", { + filter: (node) => node.nodeName === "PRE" && node.firstChild?.nodeName === "CODE", + replacement: (_content, node) => { + const code = node.firstChild; + const language = (code.getAttribute("class") ?? "").match(/language-(\S+)/)?.[1] ?? ""; + const text = code.textContent.replace(/\n$/, ""); + const fence = text.includes("```") ? "~~~~" : "```"; + return `\n\n${fence}${language === "text" ? "" : language}\n${text}\n${fence}\n\n`; + } + }); + // Site-relative links become absolute, so the Markdown works outside the site. + turndown.addRule("absoluteLinks", { + filter: (node) => node.nodeName === "A" && node.getAttribute("href"), + replacement: (content, node) => { + const href = node.getAttribute("href"); + // Heading permalinks ("#") add nothing once the page is Markdown. + if (!content.trim() || content.trim() === "#") return ""; + const absolute = href.startsWith("/") ? `${origin}${href}` : href; + // Links laid out in a row, as in the app catalogue, need a separator. + const separator = node.nextElementSibling?.nodeName === "A" ? " · " : ""; + return `[${content}](${absolute})${separator}`; + } + }); + turndown.addRule("images", { + filter: "img", + replacement: (_content, node) => { + const alt = node.getAttribute("alt"); + return alt ? `\n\n*Image: ${alt}*\n\n` : ""; + } + }); + return turndown; +} + +function decode(text) { + return text + .replace(/&/g, "&") + .replace(/</g, "<") + .replace(/>/g, ">") + .replace(/"/g, '"') + .replace(/'|'/g, "'") + .trim(); +} + +function walk(directory) { + return readdirSync(directory, { withFileTypes: true }).flatMap((entry) => { + const path = join(directory, entry.name); + if (entry.isDirectory()) return entry.name === "pagefind" ? [] : walk(path); + return entry.name === "index.html" ? [path] : []; + }); +} + +function sitePath(file) { + const directory = relative(root, dirname(file)).split(sep).join("/"); + return directory ? `/${directory}/` : "/"; +} + +if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) { + const turndown = createTurndown(); + const pages = walk(root) + .map((file) => ({ file, path: sitePath(file), page: pageToMarkdown(readFileSync(file, "utf8"), turndown) })) + .filter(({ page }) => page) + .sort((a, b) => a.path.localeCompare(b.path)); + + for (const { file, path, page } of pages) { + const markdown = `# ${page.title}\n\n> ${page.description}\n\nSource: ${origin}${path}\n\n${page.body.replace(/^# .*\n+/, "")}\n`; + writeFileSync(join(dirname(file), "index.md"), markdown); + } + + const grouped = sections.map((section) => ({ ...section, pages: [] })); + for (const entry of pages) grouped.find((section) => section.match(entry.path)).pages.push(entry); + + const index = [ + "# mdbase", + "", + "> mdbase keeps structured data in ordinary Markdown files. A collection is a folder of Markdown records plus type definitions; apps built on mdbase Connect read and change those files with the permissions you grant, and Connect syncs and shares them across devices.", + "", + "Every page below is also available as Markdown at the linked index.md. The normative specification is at /spec/. All pages in one file: " + `${origin}/llms-full.txt`, + "" + ]; + for (const section of grouped) { + if (section.pages.length === 0) continue; + index.push(`## ${section.title}`, ""); + for (const { path, page } of section.pages) { + index.push(`- [${page.title}](${origin}${path}index.md): ${page.description}`); + } + index.push(""); + } + writeFileSync(join(root, "llms.txt"), index.join("\n")); + + const full = pages + .map(({ path, page }) => `# ${page.title}\n\nSource: ${origin}${path}\n\n${page.body.replace(/^# .*\n+/, "")}`) + .join("\n\n---\n\n"); + writeFileSync(join(root, "llms-full.txt"), `${full}\n`); + + console.log(`Wrote Markdown for ${pages.length} pages, llms.txt and llms-full.txt`); +} diff --git a/scripts/deploy-pages-dev.mjs b/scripts/deploy-pages-dev.mjs index 3e95b63..e16cf3a 100644 --- a/scripts/deploy-pages-dev.mjs +++ b/scripts/deploy-pages-dev.mjs @@ -37,6 +37,7 @@ export async function deployDevelopmentSite( await dependencies.run(pnpm, ["build"], deploymentEnvironment); await dependencies.run(pnpm, ["import:spec"], deploymentEnvironment); + await dependencies.run(pnpm, ["index:site"], deploymentEnvironment); await dependencies.prepareBuild(); await dependencies.run(pnpm, ["check:links"], deploymentEnvironment); await dependencies.verifyBuild(); diff --git a/scripts/deploy-pages-dev.test.mjs b/scripts/deploy-pages-dev.test.mjs index b6782e0..13aa8cb 100644 --- a/scripts/deploy-pages-dev.test.mjs +++ b/scripts/deploy-pages-dev.test.mjs @@ -42,16 +42,16 @@ test("builds, checks, and deploys only the Pages production branch", async () => assert.equal(buildVerified, true); assert.equal(deploymentVerified, true); assert.deepEqual( - calls.slice(0, 3).map(({ arguments_ }) => arguments_), - [["build"], ["import:spec"], ["check:links"]], + calls.slice(0, 4).map(({ arguments_ }) => arguments_), + [["build"], ["import:spec"], ["index:site"], ["check:links"]], ); assert.ok( - calls[3].arguments_.includes( + calls[4].arguments_.includes( `wrangler@${developmentDeployment.wranglerVersion}`, ), ); - assert.ok(calls[3].arguments_.includes("--project-name=mdbase-dev")); - assert.ok(calls[3].arguments_.includes("--branch=main")); + assert.ok(calls[4].arguments_.includes("--project-name=mdbase-dev")); + assert.ok(calls[4].arguments_.includes("--branch=main")); assert.equal( calls.every( ({ environment }) => diff --git a/scripts/spec-page.mjs b/scripts/spec-page.mjs index 1924bc8..c9ee377 100644 --- a/scripts/spec-page.mjs +++ b/scripts/spec-page.mjs @@ -12,7 +12,8 @@ export function renderSpecPage(shell, source, archive) { .replace(/<!-- spec-navigation:start -->[\s\S]*?<!-- spec-navigation:end -->/, () => `<!-- spec-navigation:start -->${navigation.replace(/<div class="spec-mobile-links"[^>]*>[\s\S]*?<\/div>/, "")}<!-- spec-navigation:end -->`) .replace(/<!-- spec-content:start -->[\s\S]*?<!-- spec-content:end -->/, () => - `<!-- spec-content:start -->${content}<!-- spec-content:end -->`) + // Heading permalinks read as "#" in search results, so search skips them. + `<!-- spec-content:start -->${content.replaceAll('<a class="anchor" ', '<a class="anchor" data-pagefind-ignore ')}<!-- spec-content:end -->`) .replace(/<meta name="mdbase-spec-channel"[^>]*>/g, "") .replace("</head>", `<meta name="mdbase-spec-channel" content="${archive ? "v0.2-archive" : "v0.3-current"}"></head>`); } diff --git a/src/components/AppShot.astro b/src/components/AppShot.astro new file mode 100644 index 0000000..41b9a36 --- /dev/null +++ b/src/components/AppShot.astro @@ -0,0 +1,110 @@ +--- +// A screenshot of a real app in a quiet window frame. Each app is captured in +// light and dark (public/images/apps/<name>-light|dark.webp, 1600×1000 from a +// 1120×700 window at 2×), and the frame shows the capture matching the site's +// theme. The hidden capture is display: none, so the browser never loads it. +interface Props { + name: "tasknotes" | "editor" | "reader" | "writer"; + alt: string; + eager?: boolean; +} + +const hosts = { + tasknotes: "app.tasknotes.dev", + editor: "editor.mdbase.dev", + reader: "reader.mdbase.dev", + writer: "writer.mdbase.dev" +} as const; + +const { name, alt, eager = false } = Astro.props; +const hasCaption = Astro.slots.has("caption"); +const loading = eager ? "eager" : "lazy"; +--- + +<figure class="app-shot"> + <div class="app-shot__window"> + <div class="app-shot__bar" aria-hidden="true"><span>{hosts[name]}</span></div> + <img + class="app-shot__image app-shot__image--light" + src={`/images/apps/${name}-light.webp`} + alt={alt} + width="1600" + height="1000" + loading={loading} + decoding="async" + /> + <img + class="app-shot__image app-shot__image--dark" + src={`/images/apps/${name}-dark.webp`} + alt={alt} + width="1600" + height="1000" + loading={loading} + decoding="async" + /> + </div> + {hasCaption && <figcaption><slot name="caption" /></figcaption>} +</figure> + +<style is:global> + .app-shot { + --app-shot-shadow: + 0 1px 2px oklch(25% 0.02 250 / 0.06), + 0 8px 18px -10px oklch(25% 0.02 250 / 0.18), + 0 28px 56px -28px oklch(25% 0.03 250 / 0.32); + margin: 0; + } + + .app-shot__window { + overflow: hidden; + background: var(--color-surface, var(--color-canvas)); + border: 1px solid var(--color-border-strong, var(--color-border)); + border-radius: 8px; + box-shadow: var(--app-shot-shadow); + } + + .app-shot__bar { + display: flex; + height: 28px; + align-items: center; + justify-content: center; + color: var(--color-text-muted); + background: var(--color-surface-subtle, var(--color-canvas)); + border-bottom: 1px solid var(--color-border); + font: 400 0.68rem/1 var(--mono, monospace); + letter-spacing: 0.01em; + } + + .app-shot__image { + display: block; + width: 100%; + height: auto; + } + + .app-shot__image--dark { display: none; } + + :root[data-theme="dark"] .app-shot__image--light { display: none; } + :root[data-theme="dark"] .app-shot__image--dark { display: block; } + + :root[data-theme="dark"] .app-shot { + --app-shot-shadow: + 0 1px 2px oklch(0% 0 0 / 0.3), + 0 28px 56px -24px oklch(0% 0 0 / 0.6); + } + + @media (prefers-color-scheme: dark) { + :root:not([data-theme]) .app-shot__image--light { display: none; } + :root:not([data-theme]) .app-shot__image--dark { display: block; } + :root:not([data-theme]) .app-shot { + --app-shot-shadow: + 0 1px 2px oklch(0% 0 0 / 0.3), + 0 28px 56px -24px oklch(0% 0 0 / 0.6); + } + } + + .app-shot figcaption { + margin-top: 0.75rem; + color: var(--color-text-soft); + font-size: 0.85em; + } +</style> diff --git a/src/components/DocsLayout.astro b/src/components/DocsLayout.astro index b68af51..4b5df82 100644 --- a/src/components/DocsLayout.astro +++ b/src/components/DocsLayout.astro @@ -14,9 +14,9 @@ const { title, description, active, section = "sdk", headings = [] } = Astro.pro const { label, navLabel, headerCurrent, sourcePath, meta, groups: docGroups } = docsSections[section]; --- -<BaseLayout title={title} description={description} current={headerCurrent} wide> +<BaseLayout title={`${title} · ${label}`} description={description} current={headerCurrent} wide> <div class="docs-shell"> - <aside class="docs-nav"> + <aside class="docs-nav" data-pagefind-ignore="all"> <details open data-docs-nav> <summary>{label}</summary> <nav aria-label={navLabel}> @@ -41,14 +41,14 @@ const { label, navLabel, headerCurrent, sourcePath, meta, groups: docGroups } = <strong>{meta.value}</strong> </div> </aside> - <article class="docs-article"> + <article class="docs-article" data-pagefind-meta={`title:${title} · ${label}`}> <header class="docs-title"> <p>{label}</p> <h1>{title}</h1> <span>{description}</span> </header> <slot /> - <footer class="docs-edit"> + <footer class="docs-edit" data-pagefind-ignore="all"> <a href={`https://github.com/mdbase-dev/mdbase.dev/tree/main/${sourcePath}`}> Edit this page on GitHub </a> @@ -56,7 +56,7 @@ const { label, navLabel, headerCurrent, sourcePath, meta, groups: docGroups } = </article> { headings.length > 0 && ( - <aside class="docs-toc"> + <aside class="docs-toc" data-pagefind-ignore="all"> <strong>On this page</strong> <nav aria-label="On this page"> {headings.map((heading) => <a href={heading.href}>{heading.label}</a>)} diff --git a/src/components/SiteFooter.astro b/src/components/SiteFooter.astro index d20a4f7..91dca0c 100644 --- a/src/components/SiteFooter.astro +++ b/src/components/SiteFooter.astro @@ -1,8 +1,8 @@ -<footer class="site-footer"> +<footer class="site-footer" data-pagefind-ignore="all"> <div class="site-footer__inner"> <div class="site-footer__identity"> <strong>mdbase</strong> - <p>Typed Markdown collections.</p> + <p>Structured data in ordinary Markdown files.</p> </div> <nav aria-label="Footer navigation" class="site-footer__nav"> <div> diff --git a/src/components/SiteHeader.astro b/src/components/SiteHeader.astro index 5dcc454..88c9977 100644 --- a/src/components/SiteHeader.astro +++ b/src/components/SiteHeader.astro @@ -1,20 +1,23 @@ --- import BrandMark from "./BrandMark.astro"; +import SiteSearch from "./SiteSearch.astro"; interface Props { current?: "home" | "apps" | "downloads" | "connect" | "sdk" | "runtime" | "spec"; } const { current } = Astro.props; +const section = current === "sdk" || current === "runtime" || current === "spec" ? "developers" : current; const links = [ { href: "/", label: "Home", key: "home" }, { href: "/connect/", label: "Connect", key: "connect" }, { href: "/downloads/", label: "Downloads", key: "downloads" }, - { href: "/apps/", label: "Apps", key: "apps" } + { href: "/apps/", label: "Apps", key: "apps" }, + { href: "/sdk/", label: "Developers", key: "developers" } ] as const; --- -<header class="site-header"> +<header class="site-header" data-pagefind-ignore="all"> <div class="site-header__inner"> <div class="site-header__identity"> <a class="wordmark" href="/" aria-label="mdbase home"><BrandMark /><span>mdbase</span></a> @@ -23,11 +26,12 @@ const links = [ <nav class="site-nav" aria-label="Primary navigation"> { links.map((link) => ( - <a href={link.href} aria-current={current === link.key ? "page" : undefined}> + <a href={link.href} aria-current={section === link.key ? "page" : undefined}> {link.label} </a> )) } + <SiteSearch /> <button class="theme-toggle" type="button" aria-label="Use dark mode" title="Use dark mode" hidden> <svg class="theme-toggle__icon" viewBox="0 0 16 16" aria-hidden="true"> <circle cx="8" cy="8" r="5.5"></circle> diff --git a/src/components/SiteSearch.astro b/src/components/SiteSearch.astro new file mode 100644 index 0000000..7184519 --- /dev/null +++ b/src/components/SiteSearch.astro @@ -0,0 +1,174 @@ +<button class="site-search__open" type="button" data-search-open aria-haspopup="dialog" aria-keyshortcuts="/ Control+K Meta+K"> + <svg viewBox="0 0 16 16" aria-hidden="true"><circle cx="7" cy="7" r="4.5"></circle><path d="m10.4 10.4 3.6 3.6"></path></svg> + <span>Search</span> +</button> + +<dialog class="site-search" aria-label="Search mdbase.dev" data-search-dialog> + <div class="site-search__bar"> + <strong>Search mdbase.dev</strong> + <button type="button" class="site-search__close" data-search-close>Close</button> + </div> + <div id="site-search-ui" class="site-search__ui"></div> + <p class="site-search__status" data-search-status hidden></p> +</dialog> + +<script is:inline> + (() => { + const dialog = document.querySelector("[data-search-dialog]"); + const opener = document.querySelector("[data-search-open]"); + if (!(dialog instanceof HTMLDialogElement) || !opener) return; + const status = dialog.querySelector("[data-search-status]"); + let loading; + + const load = () => { + loading ??= new Promise((resolve, reject) => { + const style = document.createElement("link"); + style.rel = "stylesheet"; + style.href = "/pagefind/pagefind-ui.css"; + document.head.append(style); + const script = document.createElement("script"); + script.src = "/pagefind/pagefind-ui.js"; + script.onload = () => { + // eslint-disable-next-line no-undef + new PagefindUI({ + element: "#site-search-ui", + showSubResults: true, + showImages: false, + resetStyles: false, + autofocus: true, + translations: { placeholder: "Search the docs, apps and specification" } + }); + resolve(); + }; + script.onerror = reject; + document.head.append(script); + }); + return loading; + }; + + const open = async () => { + if (dialog.open) return; + dialog.showModal(); + try { + await load(); + status.hidden = true; + dialog.querySelector("input")?.focus(); + } catch { + loading = undefined; + status.textContent = "Search isn’t available in this build. Run a production build to create the index."; + status.hidden = false; + } + }; + + opener.addEventListener("click", open); + dialog.querySelector("[data-search-close]")?.addEventListener("click", () => dialog.close()); + dialog.addEventListener("click", (event) => { + if (event.target === dialog) dialog.close(); + }); + document.addEventListener("keydown", (event) => { + const target = event.target; + const typing = target instanceof HTMLElement + && (target.isContentEditable || /^(INPUT|TEXTAREA|SELECT)$/.test(target.tagName)); + if ((event.key === "k" && (event.metaKey || event.ctrlKey)) || (event.key === "/" && !typing)) { + event.preventDefault(); + open(); + } + }); + })(); +</script> + +<style is:global> + .site-search__open { + display: inline-flex; + margin-left: auto; + padding: 0; + align-items: center; + gap: 6px; + color: var(--color-text); + background: transparent; + border: 0; + font: inherit; + cursor: pointer; + } + + .site-search__open:hover { color: var(--color-text-soft); } + + @media (max-width: 35rem) { + .site-search__open span { + position: absolute; + width: 1px; + height: 1px; + overflow: hidden; + clip-path: inset(50%); + white-space: nowrap; + } + } + + .site-search__open:focus-visible, + .site-search__close:focus-visible { + outline: 2px solid currentColor; + outline-offset: 3px; + } + + .site-search__open svg { + width: 14px; + height: 14px; + fill: none; + stroke: currentColor; + stroke-width: 1.4; + stroke-linecap: round; + } + + .site-search { + width: min(calc(100% - 32px), 44rem); + max-height: min(80vh, 46rem); + margin: 10vh auto auto; + padding: 1rem 1.25rem 1.25rem; + color: var(--color-text); + background: var(--color-canvas); + border: 1px solid var(--color-text); + border-radius: 4px; + } + + .site-search::backdrop { background: var(--color-scrim, rgb(0 0 0 / 0.35)); } + + .site-search__bar { + display: flex; + margin-bottom: 0.75rem; + align-items: baseline; + justify-content: space-between; + gap: 1rem; + } + + .site-search__bar strong { font-weight: 400; } + + .site-search__close { + padding: 0; + color: var(--color-text); + background: transparent; + border: 0; + font: inherit; + text-decoration: underline; + text-underline-offset: 0.2em; + cursor: pointer; + } + + .site-search__status { color: var(--color-text-soft); } + + .site-search .pagefind-ui { + --pagefind-ui-scale: 0.8; + --pagefind-ui-primary: var(--color-accent); + --pagefind-ui-text: var(--color-text); + --pagefind-ui-background: var(--color-canvas); + --pagefind-ui-border: var(--color-border-strong); + --pagefind-ui-tag: var(--color-surface-subtle); + --pagefind-ui-border-width: 1px; + --pagefind-ui-border-radius: 4px; + --pagefind-ui-font: var(--sans, inherit); + } + + .site-search .pagefind-ui mark { + color: inherit; + background: color-mix(in oklch, var(--color-accent) 22%, transparent); + } +</style> diff --git a/src/data/docs-sections.ts b/src/data/docs-sections.ts index 325d81c..c79f672 100644 --- a/src/data/docs-sections.ts +++ b/src/data/docs-sections.ts @@ -18,7 +18,7 @@ export const docsSections: Record<DocsSectionId, DocsSection> = { navLabel: "SDK documentation", headerCurrent: "sdk", sourcePath: "src/pages/sdk", - meta: { label: "@mdbase-dev/sdk", value: "Not yet published" }, + meta: { label: "@mdbase-dev/sdk", value: "Beta on npm" }, groups: [ { label: "Start", diff --git a/src/layouts/BaseLayout.astro b/src/layouts/BaseLayout.astro index 364d9c1..d66f719 100644 --- a/src/layouts/BaseLayout.astro +++ b/src/layouts/BaseLayout.astro @@ -13,10 +13,12 @@ interface Props { description: string; current?: "home" | "apps" | "downloads" | "connect" | "sdk" | "runtime" | "spec"; noIndex?: boolean; + /** Leave the page out of site search, as for an archived copy of current content. */ + searchable?: boolean; wide?: boolean; } -const { title, description, current, noIndex = false, wide = false } = Astro.props; +const { title, description, current, noIndex = false, searchable = true, wide = false } = Astro.props; const canonical = new URL(Astro.url.pathname, Astro.site); const fullTitle = title === "mdbase" ? title : `${title} | mdbase`; --- @@ -29,6 +31,7 @@ const fullTitle = title === "mdbase" ? title : `${title} | mdbase`; <title>{fullTitle} + {searchable && !noIndex && } @@ -38,7 +41,7 @@ const fullTitle = title === "mdbase" ? title : `${title} | mdbase`; - + @@ -49,7 +52,7 @@ const fullTitle = title === "mdbase" ? title : `${title} | mdbase`; -
+
diff --git a/src/layouts/HomeLayout.astro b/src/layouts/HomeLayout.astro index 877bea6..3503382 100644 --- a/src/layouts/HomeLayout.astro +++ b/src/layouts/HomeLayout.astro @@ -25,6 +25,7 @@ const canonical = new URL(Astro.url.pathname, Astro.site); {title} + @@ -34,7 +35,7 @@ const canonical = new URL(Astro.url.pathname, Astro.site); - + diff --git a/src/pages/apps/index.astro b/src/pages/apps/index.astro index 2870d30..9d9891c 100644 --- a/src/pages/apps/index.astro +++ b/src/pages/apps/index.astro @@ -1,5 +1,6 @@ --- import BaseLayout from "../../layouts/BaseLayout.astro"; +import AppShot from "../../components/AppShot.astro"; import "../../styles/apps.css"; type CatalogueEntry = { @@ -9,6 +10,7 @@ type CatalogueEntry = { runsOn: string; collections: string; links: Array<{ label: string; href: string }>; + shot?: { name: "tasknotes" | "editor" | "reader" | "writer"; alt: string }; }; const applications: CatalogueEntry[] = [ @@ -20,9 +22,12 @@ const applications: CatalogueEntry[] = [ collections: "Cloud copy, or Private through one of your devices", links: [ { label: "Open TaskNotes", href: "https://app.tasknotes.dev/" }, - { label: "Try the demo", href: "https://app.tasknotes.dev/?demo=30" }, { label: "Source", href: "https://github.com/callumalpass/tasknotes-app" } - ] + ], + shot: { + name: "tasknotes", + alt: "TaskNotes showing today's tasks, with priorities, due dates and projects, and an overdue section." + } }, { name: "mdbase Editor", @@ -34,7 +39,11 @@ const applications: CatalogueEntry[] = [ { label: "Open Editor", href: "https://editor.mdbase.dev/" }, { label: "Documentation", href: "/apps/editor/" }, { label: "Source", href: "https://github.com/mdbase-dev/mdbase-connect/tree/main/apps/editor" } - ] + ], + shot: { + name: "editor", + alt: "mdbase Editor showing a collection's folders and notes beside an open note with a diagram." + } }, { name: "mdbase Reader", @@ -45,9 +54,12 @@ const applications: CatalogueEntry[] = [ links: [ { label: "Open Reader", href: "https://reader.mdbase.dev/" }, { label: "Documentation", href: "/apps/reader/" }, - { label: "Try the preview", href: "https://reader.mdbase.dev/?preview=1" }, { label: "Chrome extension", href: "https://chromewebstore.google.com/detail/mdbase-reader/kimdfjefhbfgfecconmaiaaindjccidp" } - ] + ], + shot: { + name: "reader", + alt: "mdbase Reader showing a chapter of The Principles of Psychology with two highlighted passages." + } }, { name: "mdbase writer", @@ -57,9 +69,12 @@ const applications: CatalogueEntry[] = [ collections: "Cloud copy, or Private through one of your devices", links: [ { label: "Open writer", href: "https://writer.mdbase.dev/" }, - { label: "Documentation", href: "/apps/writer/" }, - { label: "Try the demo", href: "https://lab.mdbase-writer.pages.dev/?demo" } - ] + { label: "Documentation", href: "/apps/writer/" } + ], + shot: { + name: "writer", + alt: "mdbase writer showing a paper's Markdown source, with citations, beside its typeset article preview." + } }, { name: "mdbase for Obsidian", @@ -97,22 +112,12 @@ const applications: CatalogueEntry[] = [

-
-

How apps reach a collection

-

- Web apps can use a Cloud copy collection at any time. They reach a Private - collection through one of your devices, so one of them must be online, - such as a computer running the desktop app. - Several apps can use one collection at once, each within the permission - you gave it. How permissions work. -

-
-
{ applications.map((app) => ( -
+
+ {app.shot && }

{app.name}

{app.description}

diff --git a/src/pages/apps/reader/index.astro b/src/pages/apps/reader/index.astro index 87469be..f5bcd84 100644 --- a/src/pages/apps/reader/index.astro +++ b/src/pages/apps/reader/index.astro @@ -15,7 +15,6 @@ import DocVideo from "../../../components/DocVideo.astro"; { href: "#open", label: "Open a collection" }, { href: "#setup", label: "Reader setup" }, { href: "#workspace", label: "The workspace" }, - { href: "#preview", label: "Try the preview" }, { href: "#switch", label: "Switch collections" } ]} > @@ -107,13 +106,6 @@ import DocVideo from "../../../components/DocVideo.astro"; to search sources and run any command.

-

Try the preview

-

- Open the interface preview - to explore sample sources. It runs in memory and creates no collection - records or files. Changes to the sample library are not kept. -

-

Switch collections

Open the collection-name menu in the header to choose another connected diff --git a/src/pages/apps/writer/data/index.astro b/src/pages/apps/writer/data/index.astro index d0eb652..74c6736 100644 --- a/src/pages/apps/writer/data/index.astro +++ b/src/pages/apps/writer/data/index.astro @@ -26,7 +26,7 @@ A claim supported by a source [@smith2024, p. 12].`; { href: "#records", label: "Manuscript records" }, { href: "#saving", label: "Autosave" }, { href: "#conflicts", label: "Conflicts" }, - { href: "#device", label: "Browser state and demo" }, + { href: "#device", label: "Browser state" }, { href: "#access", label: "Access and availability" } ]} > @@ -67,17 +67,12 @@ A claim supported by a source [@smith2024, p. 12].`; chapter belongs to that chapter's record.

-

Browser state and demo

+

Browser state

Layout, zoom, theme and the last-used export format are remembered in the browser. These preferences are separate from the manuscript settings stored in the collection. Browser state is not a backup of your writing.

-

- The demo collection - runs in memory. It is separate from the collections you authorise through - Connect. Do not use it for work you need to keep. -

Access and availability

diff --git a/src/pages/apps/writer/index.astro b/src/pages/apps/writer/index.astro index b26cb18..2de4b55 100644 --- a/src/pages/apps/writer/index.astro +++ b/src/pages/apps/writer/index.astro @@ -21,9 +21,7 @@ import DocVideo from "../../../components/DocVideo.astro"; Writer is prerelease software.

Keep backups of important work and check the save status before closing or - reloading. The demo - uses an in-memory sample collection. Demo changes are not kept and do not - change your own collection. + reloading.

diff --git a/src/pages/index.astro b/src/pages/index.astro index 176e4a1..05ad2c9 100644 --- a/src/pages/index.astro +++ b/src/pages/index.astro @@ -1,91 +1,97 @@ --- import HomeLayout from "../layouts/HomeLayout.astro"; +import AppShot from "../components/AppShot.astro"; ---

-
-
+
+

Structured data in ordinary Markdown files

-

Keep your notes, tasks, and projects as plain Markdown files that you own. Apps can understand them, and mdbase keeps them in sync across your devices.

- +

Keep notes, tasks and projects as Markdown files. mdbase syncs them between your devices and serves them to the apps you use, on any device, with the permissions you give each app.

+ +

mdbase is in beta. Keep independent backups of important collections.

-
-

A folder of Markdown, with structure

-

A collection is a folder of Markdown records, plus type definitions that say what their fields mean. Frontmatter holds fields that apps can validate and query, while the body stays prose.

-
---
-type: project
-title: Website redesign
-status: active
-owner: "[[people/sam.md]]"
+
+

The same task in an app and on disk

+

A collection is a folder of Markdown records. Each record’s frontmatter holds fields that apps can validate and query, and its body stays ordinary prose.

+ +
tasks/research-summary-for-rowan.md
+---
+title: Send the research summary to Rowan
+status: open
+priority: high
+due: 2026-11-10
 ---
 
-The new site should make mdbase
-easier to understand.
-

Because they’re plain files, they work with git, backups, and any text editor. If you stop using mdbase, you still have a folder of Markdown.

+Include the interview notes from +the field visit.
+

When you tick the task in TaskNotes, the app sets status: done in this file. Because records are plain files, the folder works with git, backups and any text editor, and it stays yours if you stop using mdbase.

-
+

One collection, many apps

-

Apps built on mdbase work directly with these files instead of importing them into their own databases. Use TaskNotes for tasks, Reader for papers and web pages, Writer for long documents, or the Editor for any collection.

-

Several apps can use the same collection at once, so nothing has to be exported from one app to another.

-
- -
-

Edit from anywhere

-

Open the same record in a text editor on your laptop and in an app on your phone. mdbase works out what each edit changed, field by field.

-

Edits to different fields are combined, so one copy of the file never silently replaces another.

+

Apps built on mdbase work on these files directly. Several can open the same collection at once, so nothing is exported from one app to another.

+
+
TaskNotes
Tasks, calendars and reminders, on the web, iOS and Android.
+
Editor
Any collection: records, frontmatter, links and types.
+
Reader
PDFs, EPUBs and web pages, with each highlight kept as a record.
+
Writer
Papers and books in Markdown, with citations from Reader.
+
+

All apps

-
-

Every device has the whole collection

-

Turn on sync and your laptop, phone, and other devices each keep their own complete copy as ordinary files. Each one keeps working without a connection and catches up when it’s back online.

-

No device is the main one, so you can switch any of them off, or lose one, and still have your whole collection.

-

Your changes are locked on your device before they leave it. mdbase passes them between your devices without being able to read them.

+
+

Every device keeps the whole collection

+

With the desktop app, your computers keep a complete copy of the collection as ordinary files. Each copy works offline and catches up when it reconnects.

+

When the same record is edited in two places, mdbase works out what each edit changed, field by field, and combines edits to different fields.

+

Changes are encrypted and signed on the device that made them before they are sent.

-
-

Choose what mdbase can see

-

There are two ways to sync a collection. They differ in whether mdbase can read it, which decides how your devices and apps reach it.

-

Cloud copy, the default, also keeps a readable copy with mdbase, so web apps keep working while your devices are off, and a new device joins when you sign in.

-

Private is end-to-end encrypted: only your devices can unlock it, and mdbase never holds the key. Apps reach the collection through one of your devices. To add a device, you sign in and unlock it with your encryption password or recovery key.

-

Both sync the same way.

+
+

Open it from any device

+

mdbase also keeps a copy of the collection and serves it to your apps. TaskNotes on your phone or the Editor in a browser opens the collection through mdbase, and keeps working while your computers are switched off.

+

A new device joins when you sign in. A new account starts with a small collection that opens in the Editor straight away, before you install anything.

+

The security page sets out what mdbase stores and can read, and the option to sync a collection that mdbase can’t read.

-
+

Apps ask first

-

An application asks for access to one collection and lists the operations it needs. You might let a project planner find, create, and edit project records while leaving deletion blocked.

-

The permission travels with the collection, so every copy checks it, whether on your devices or in the cloud copy.

+

An app asks for access to one collection and lists the operations it needs. You might let a project planner find, create and edit project records while leaving deletion blocked.

+

You approve each app in Connect, and you can narrow or revoke its access at any time. Revoking it ends the app’s sessions.

-
-

Share it, and take it back

-

Invite viewers who can read, or editors who can read and change records. Their devices keep a full copy too, and each person can connect their own applications within the role you gave them. Cloud copy and Private collections can both be shared; in a Private collection, an owner or editor approves a new member’s first device by comparing a six-digit code with them.

-

You can change a member’s role or remove them at any time. A removed member stops receiving changes but keeps the files they already had.

-

Applications work the same way: narrow or revoke any permission. Removing an application never removes the collection, and other approved applications keep using it.

+
+

Share a collection

+

Invite viewers, who can read, or editors, who can also change records. Each person uses their own apps, within the role you gave them.

+

You can change a member’s role or remove them at any time. A removed member stops receiving changes and keeps the files they already had.

-
+ +

Get started

-

Sign up for mdbase Connect, the account that lets apps use your collections, and syncs and shares them. Your account starts with a small Cloud copy collection that you can open in any app. To sync a folder on your computer, as Cloud copy or Private, use the desktop app.

- -

Building an app? Any app can work with an mdbase collection. Read the SDK documentation and the specification. The specification, Connect, Reader and Writer are open source under the MIT licence, on GitHub.

+

Create an account to open your first collection in the Editor. To sync a folder on your computer, install the desktop app. Apps receive no access until you approve them.

+ +
+
Status
Connect is in beta and has no availability guarantee. Service incidents are summarised on the incidents page.
+
Source
The specification, Connect, Reader and Writer are open source under the MIT licence, on GitHub.
+
Developers
Any app can work with a collection. Read the SDK documentation and the specification.
+
diff --git a/src/pages/sdk/connect-quickstart/index.astro b/src/pages/sdk/connect-quickstart/index.astro index 7f9393f..357a164 100644 --- a/src/pages/sdk/connect-quickstart/index.astro +++ b/src/pages/sdk/connect-quickstart/index.astro @@ -80,12 +80,11 @@ const operate = `async function startApp(db) { ]} >
- The SDK is not on npm yet. + Test without an account first.

- Signing in to real collections needs a packaged SDK release, and none is - published yet. Until then, write and test your app against - {" "}createApplicationFixture(). - This page will say when a release is out. + You can write and test the whole flow on this page against + {" "}createApplicationFixture() before + signing in to a real collection.

@@ -99,7 +98,7 @@ const operate = `async function startApp(db) {

2. Install

- The package is not on npm yet; see the + The SDK is in beta; pin an exact version in an app you ship. See the {" "}quickstart.

diff --git a/src/pages/sdk/index.astro b/src/pages/sdk/index.astro index 75e70b0..54398fd 100644 --- a/src/pages/sdk/index.astro +++ b/src/pages/sdk/index.astro @@ -63,8 +63,10 @@ export async function listOpenTasks(db: MdbaseClient) {
Release status

- @mdbase-dev/sdk is not on npm yet. Until it is, the install - commands in these guides fail. + @mdbase-dev/sdk is in beta on + {" "}npm. New + betas are published as Connect changes, and the API can change between + them, so pin an exact version in an app you ship.

diff --git a/src/pages/sdk/notifications/index.astro b/src/pages/sdk/notifications/index.astro index af4bccb..0eafb46 100644 --- a/src/pages/sdk/notifications/index.astro +++ b/src/pages/sdk/notifications/index.astro @@ -76,8 +76,7 @@ if (await deliveries.claim(event.delivery_id)) { Timer access comes from the app connection.

connection.timers is on the connection you get from - {" "}createMdbase() once a collection is ready. The SDK is - not on npm yet. See + {" "}createMdbase() once a collection is ready. See {" "}Connect a browser app.

diff --git a/src/pages/sdk/quickstart/index.astro b/src/pages/sdk/quickstart/index.astro index a65ab85..2516cad 100644 --- a/src/pages/sdk/quickstart/index.astro +++ b/src/pages/sdk/quickstart/index.astro @@ -100,11 +100,12 @@ Open tasks: 1`;

1. Install the SDK

- The package is not on npm yet. + The SDK is in beta.

- Until @mdbase-dev/sdk is published, - {" "}npm install fails. This page will give the exact version - once it is published. + npm install @mdbase-dev/sdk installs the latest beta from + {" "}npm. A new + beta is published as Connect changes, and the API can change between + betas, so pin an exact version in an app you ship.

diff --git a/src/pages/spec/[...version].astro b/src/pages/spec/[...version].astro index 5dcb920..bf2265c 100644 --- a/src/pages/spec/[...version].astro +++ b/src/pages/spec/[...version].astro @@ -13,9 +13,9 @@ const { archive } = Astro.props; const title = archive ? "Specification v0.2 archive" : "Specification v0.3"; --- - +
-