+ {{ title }} +
++ {{ excerpt }} +
++ {{ excerpt }} +
++ + {{ segment.text }} + {{ segment.text }} + +
+ + +No comments yet.
+ + Reply on Bluesky + +
+
+### Code Blocks
+
+Pre-formatted code blocks are used for writing about programming or
+markup source code. Rather than forming normal paragraphs, the lines
+of a code block are interpreted literally. Markdown wraps a code block
+in both `` and `` tags.
+
+To produce a code block in Markdown, simply indent every line of the
+block by at least 4 spaces or 1 tab.
+
+This is a normal paragraph:
+
+ This is a code block.
+
+Here is an example of AppleScript:
+
+ tell application "Foo"
+ beep
+ end tell
+
+A code block continues until it reaches a line that is not indented
+(or the end of the article).
+
+Within a code block, ampersands (`&`) and angle brackets (`<` and `>`)
+are automatically converted into HTML entities. This makes it very
+easy to include example HTML source code using Markdown -- just paste
+it and indent it, and Markdown will handle the hassle of encoding the
+ampersands and angle brackets. For example, this:
+
+
+
+Regular Markdown syntax is not processed within code blocks. E.g.,
+asterisks are just literal asterisks within a code block. This means
+it's also easy to use Markdown to write about Markdown's own syntax.
+
+```
+tell application "Foo"
+ beep
+end tell
+```
+
+## Span Elements
+
+### Links
+
+Markdown supports two style of links: _inline_ and _reference_.
+
+In both styles, the link text is delimited by [square brackets].
+
+To create an inline link, use a set of regular parentheses immediately
+after the link text's closing square bracket. Inside the parentheses,
+put the URL where you want the link to point, along with an _optional_
+title for the link, surrounded in quotes. For example:
+
+This is [an example](http://example.com/) inline link.
+
+[This link](http://example.net/) has no title attribute.
+
+### Emphasis
+
+Markdown treats asterisks (`*`) and underscores (`_`) as indicators of
+emphasis. Text wrapped with one `*` or `_` will be wrapped with an
+HTML `` tag; double `*`'s or `_`'s will be wrapped with an HTML
+`` tag. E.g., this input:
+
+_single asterisks_
+
+_single underscores_
+
+**double asterisks**
+
+**double underscores**
+
+### Code
+
+To indicate a span of code, wrap it with backtick quotes (`` ` ``).
+Unlike a pre-formatted code block, a code span indicates code within a
+normal paragraph. For example:
+
+Use the `printf()` function.
+
+
diff --git a/app/pages/blog/index.vue b/app/pages/blog/index.vue
new file mode 100644
index 000000000..3bed70129
--- /dev/null
+++ b/app/pages/blog/index.vue
@@ -0,0 +1,156 @@
+
+
+
+
+
+
+
+ {{ $t('blog.heading') }}
+
+
+ {{ $t('tagline') }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ No posts found.
+
+
+
+
+
diff --git a/app/plugins/blog-wrapper.ts b/app/plugins/blog-wrapper.ts
new file mode 100644
index 000000000..fff0ff571
--- /dev/null
+++ b/app/plugins/blog-wrapper.ts
@@ -0,0 +1,5 @@
+import BlogPostWrapper from '~/components/BlogPostWrapper.vue'
+
+export default defineNuxtPlugin(nuxtApp => {
+ nuxtApp.vueApp.component('BlogPostWrapper', BlogPostWrapper)
+})
diff --git a/app/utils/bluesky.ts b/app/utils/bluesky.ts
new file mode 100644
index 000000000..5c41e9896
--- /dev/null
+++ b/app/utils/bluesky.ts
@@ -0,0 +1,8 @@
+export function atUriToWebUrl(atUri: string): string | null {
+ // Convert AT URI to bsky.app URL
+ // at://did:plc:xxx/app.bsky.feed.post/rkey -> https://bsky.app/profile/did:plc:xxx/post/rkey
+ const match = atUri.match(AT_URI_REGEX)
+ if (!match) return null
+ const [, did, rkey] = match
+ return `https://bsky.app/profile/${did}/post/${rkey}`
+}
diff --git a/i18n/locales/en.json b/i18n/locales/en.json
index ef71c6845..875e9aa04 100644
--- a/i18n/locales/en.json
+++ b/i18n/locales/en.json
@@ -13,6 +13,7 @@
"trademark_disclaimer": "npm is a registered trademark of npm, Inc. This site is not affiliated with npm, Inc.",
"footer": {
"about": "about",
+ "blog": "blog",
"docs": "docs",
"source": "source",
"social": "social",
@@ -76,6 +77,18 @@
"links": "Links",
"tap_to_search": "Tap to search"
},
+ "blog": {
+ "title": "Blog",
+ "heading": "blog",
+ "meta_description": "Insights and updates from the npmx community",
+ "author": {
+ "view_profile": "View {name}'s profile on Bluesky"
+ },
+ "atproto": {
+ "loading_bluesky_post": "Loading Bluesky post...",
+ "view_on_bluesky": "View this post on Bluesky"
+ }
+ },
"settings": {
"title": "settings",
"tagline": "customize your npmx experience",
diff --git a/i18n/locales/fr-FR.json b/i18n/locales/fr-FR.json
index c2e92b19d..727a8e84a 100644
--- a/i18n/locales/fr-FR.json
+++ b/i18n/locales/fr-FR.json
@@ -76,6 +76,10 @@
"links": "Liens",
"tap_to_search": "Toucher pour rechercher"
},
+ "blog": {
+ "title": "Blog",
+ "author": {}
+ },
"settings": {
"title": "paramètres",
"tagline": "personnalisez votre expérience npmx",
diff --git a/lexicons.json b/lexicons.json
index c13112e7a..af3d3f232 100644
--- a/lexicons.json
+++ b/lexicons.json
@@ -10,6 +10,7 @@
"app.bsky.feed.getPostThread",
"app.bsky.feed.getPosts",
"app.bsky.feed.post",
+ "com.atproto.identity.resolveHandle",
"com.bad-example.identity.resolveMiniDoc",
"site.standard.document"
],
@@ -98,6 +99,10 @@
"uri": "at://did:plc:4v4y5r3lwsbtmsxhile2ljac/com.atproto.lexicon.schema/app.bsky.richtext.facet",
"cid": "bafyreidg56eo7zynf6ihz4xb627vwoqf5idnevkmwp7sxc4tijg6xngbu4"
},
+ "com.atproto.identity.resolveHandle": {
+ "uri": "at://did:plc:6msi3pj7krzih5qxqtryxlzw/com.atproto.lexicon.schema/com.atproto.identity.resolveHandle",
+ "cid": "bafyreigckmqtt3jrtzd7tvigjatnz6ajqyafs26h5pwcudwag2anedxnmu"
+ },
"com.atproto.label.defs": {
"uri": "at://did:plc:6msi3pj7krzih5qxqtryxlzw/com.atproto.lexicon.schema/com.atproto.label.defs",
"cid": "bafyreig4hmnb2xkecyg4aaqfhr2rrcxxb3gsr4xks4rqb7rscrycalbrji"
diff --git a/lexicons/com/atproto/identity/resolveHandle.json b/lexicons/com/atproto/identity/resolveHandle.json
new file mode 100644
index 000000000..3b7bff82b
--- /dev/null
+++ b/lexicons/com/atproto/identity/resolveHandle.json
@@ -0,0 +1,41 @@
+{
+ "id": "com.atproto.identity.resolveHandle",
+ "defs": {
+ "main": {
+ "type": "query",
+ "errors": [
+ {
+ "name": "HandleNotFound",
+ "description": "The resolution process confirmed that the handle does not resolve to any DID."
+ }
+ ],
+ "output": {
+ "schema": {
+ "type": "object",
+ "required": ["did"],
+ "properties": {
+ "did": {
+ "type": "string",
+ "format": "did"
+ }
+ }
+ },
+ "encoding": "application/json"
+ },
+ "parameters": {
+ "type": "params",
+ "required": ["handle"],
+ "properties": {
+ "handle": {
+ "type": "string",
+ "format": "handle",
+ "description": "The handle to resolve."
+ }
+ }
+ },
+ "description": "Resolves an atproto handle (hostname) to a DID. Does not necessarily bi-directionally verify against the the DID document."
+ }
+ },
+ "$type": "com.atproto.lexicon.schema",
+ "lexicon": 1
+}
diff --git a/lunaria/files/en-GB.json b/lunaria/files/en-GB.json
index d6b2fefca..5aee67c20 100644
--- a/lunaria/files/en-GB.json
+++ b/lunaria/files/en-GB.json
@@ -12,6 +12,7 @@
"trademark_disclaimer": "npm is a registered trademark of npm, Inc. This site is not affiliated with npm, Inc.",
"footer": {
"about": "about",
+ "blog": "blog",
"docs": "docs",
"source": "source",
"social": "social",
@@ -75,6 +76,18 @@
"links": "Links",
"tap_to_search": "Tap to search"
},
+ "blog": {
+ "title": "Blog",
+ "heading": "blog",
+ "meta_description": "Insights and updates from the npmx community",
+ "author": {
+ "view_profile": "View {name}'s profile on Bluesky"
+ },
+ "atproto": {
+ "loading_bluesky_post": "Loading Bluesky post...",
+ "view_on_bluesky": "View this post on Bluesky"
+ }
+ },
"settings": {
"title": "settings",
"tagline": "customise your npmx experience",
diff --git a/lunaria/files/en-US.json b/lunaria/files/en-US.json
index 899428e68..f34019e17 100644
--- a/lunaria/files/en-US.json
+++ b/lunaria/files/en-US.json
@@ -12,6 +12,7 @@
"trademark_disclaimer": "npm is a registered trademark of npm, Inc. This site is not affiliated with npm, Inc.",
"footer": {
"about": "about",
+ "blog": "blog",
"docs": "docs",
"source": "source",
"social": "social",
@@ -75,6 +76,18 @@
"links": "Links",
"tap_to_search": "Tap to search"
},
+ "blog": {
+ "title": "Blog",
+ "heading": "blog",
+ "meta_description": "Insights and updates from the npmx community",
+ "author": {
+ "view_profile": "View {name}'s profile on Bluesky"
+ },
+ "atproto": {
+ "loading_bluesky_post": "Loading Bluesky post...",
+ "view_on_bluesky": "View this post on Bluesky"
+ }
+ },
"settings": {
"title": "settings",
"tagline": "customize your npmx experience",
diff --git a/lunaria/files/fr-FR.json b/lunaria/files/fr-FR.json
index d8e3a7cf7..367b62f54 100644
--- a/lunaria/files/fr-FR.json
+++ b/lunaria/files/fr-FR.json
@@ -75,6 +75,10 @@
"links": "Liens",
"tap_to_search": "Toucher pour rechercher"
},
+ "blog": {
+ "title": "Blog",
+ "author": {}
+ },
"settings": {
"title": "paramètres",
"tagline": "personnalisez votre expérience npmx",
diff --git a/nuxt.config.ts b/nuxt.config.ts
index 1b2f963d0..11ba3cd21 100644
--- a/nuxt.config.ts
+++ b/nuxt.config.ts
@@ -1,10 +1,12 @@
import process from 'node:process'
import { currentLocales } from './config/i18n'
+import Markdown from 'unplugin-vue-markdown/vite'
import { isCI, isTest, provider } from 'std-env'
const isStorybook = process.env.STORYBOOK === 'true' || process.env.VITEST_STORYBOOK === 'true'
export default defineNuxtConfig({
+ extensions: ['.md'],
modules: [
'@unocss/nuxt',
'@nuxtjs/html-validator',
@@ -155,7 +157,10 @@ export default defineNuxtConfig({
'/settings': { prerender: true },
'/recharging': { prerender: true },
// proxy for insights
- '/_v/script.js': { proxy: 'https://npmx.dev/_vercel/insights/script.js' },
+ '/blog/**': { isr: true, prerender: true },
+ '/_v/script.js': {
+ proxy: 'https://npmx.dev/_vercel/insights/script.js',
+ },
'/_v/view': { proxy: 'https://npmx.dev/_vercel/insights/view' },
'/_v/event': { proxy: 'https://npmx.dev/_vercel/insights/event' },
'/_v/session': { proxy: 'https://npmx.dev/_vercel/insights/session' },
@@ -338,6 +343,28 @@ export default defineNuxtConfig({
},
vite: {
+ vue: {
+ include: [/\.vue($|\?)/, /\.(md|markdown)($|\?)/],
+ },
+ plugins: [
+ Markdown({
+ include: [/\.(md|markdown)($|\?)/],
+ wrapperComponent: 'BlogPostWrapper',
+ wrapperClasses: 'text-fg-muted leading-relaxed',
+ async markdownItSetup(md) {
+ const shiki = await import('@shikijs/markdown-it')
+ md.use(
+ await shiki.default({
+ themes: {
+ dark: 'github-dark',
+ light: 'github-light',
+ },
+ }),
+ )
+ },
+ }),
+ ],
+
optimizeDeps: {
include: [
'@vueuse/core',
diff --git a/package.json b/package.json
index 133ba75a5..0cab550f2 100644
--- a/package.json
+++ b/package.json
@@ -53,6 +53,8 @@
"chromatic": "chromatic"
},
"dependencies": {
+ "@atcute/bluesky-richtext-segmenter": "3.0.0",
+ "@atproto/api": "^0.18.17",
"@atproto/common": "0.5.13",
"@atproto/lex": "0.0.19",
"@atproto/oauth-client-node": "^0.3.15",
@@ -122,6 +124,7 @@
"@intlify/core-base": "11.2.8",
"@npm/types": "2.1.0",
"@playwright/test": "1.58.2",
+ "@shikijs/markdown-it": "^3.21.0",
"@storybook-vue/nuxt": "9.0.1",
"@storybook/addon-a11y": "^10.2.7",
"@storybook/addon-docs": "^10.2.7",
@@ -129,6 +132,8 @@
"@types/sanitize-html": "2.16.0",
"@types/semver": "7.7.1",
"@types/validate-npm-package-name": "4.0.2",
+ "@valibot/to-json-schema": "^1.5.0",
+ "@vitest/browser-playwright": "4.0.18",
"@vitest/coverage-v8": "4.0.18",
"@vue/test-utils": "2.4.6",
"axe-core": "4.11.1",
@@ -147,6 +152,7 @@
"simple-git-hooks": "2.13.1",
"storybook": "^10.2.7",
"typescript": "5.9.3",
+ "unplugin-vue-markdown": "^29.2.0",
"vitest": "npm:@voidzero-dev/vite-plus-test@0.0.0-g52709db6.20260226-1136",
"vitest-environment-nuxt": "1.0.1",
"vue-i18n-extract": "2.0.7",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 74c23b6e0..f235a39f0 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -21,6 +21,12 @@ importers:
.:
dependencies:
+ '@atcute/bluesky-richtext-segmenter':
+ specifier: 3.0.0
+ version: 3.0.0
+ '@atproto/api':
+ specifier: ^0.18.17
+ version: 0.18.21
'@atproto/common':
specifier: 0.5.13
version: 0.5.13
@@ -223,6 +229,9 @@ importers:
'@playwright/test':
specifier: 1.58.2
version: 1.58.2
+ '@shikijs/markdown-it':
+ specifier: ^3.21.0
+ version: 3.23.0(markdown-it-async@2.2.0)
'@storybook-vue/nuxt':
specifier: 9.0.1
version: 9.0.1(943de74da843ff392a0ce19e8d4f1cd5)
@@ -244,6 +253,12 @@ importers:
'@types/validate-npm-package-name':
specifier: 4.0.2
version: 4.0.2
+ '@valibot/to-json-schema':
+ specifier: ^1.5.0
+ version: 1.5.0(valibot@1.2.0(typescript@5.9.3))
+ '@vitest/browser-playwright':
+ specifier: 4.0.18
+ version: 4.0.18(@voidzero-dev/vite-plus-test@0.0.0-g52709db6.20260226-1136(@opentelemetry/api@1.9.0)(@types/node@24.10.13)(esbuild@0.27.3)(happy-dom@20.3.5)(jiti@2.6.1)(jsdom@27.4.0(@noble/hashes@1.8.0))(terser@5.46.0)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2))(playwright@1.58.2)(vite@8.0.0-beta.10(@types/node@24.10.13)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))
'@vitest/coverage-v8':
specifier: 4.0.18
version: 4.0.18(@vitest/browser@4.0.18(@voidzero-dev/vite-plus-test@0.0.0-g52709db6.20260226-1136(@opentelemetry/api@1.9.0)(@types/node@24.10.13)(esbuild@0.27.3)(happy-dom@20.3.5)(jiti@2.6.1)(jsdom@27.4.0(@noble/hashes@1.8.0))(terser@5.46.0)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2))(vite@8.0.0-beta.10(@types/node@24.10.13)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)))(@voidzero-dev/vite-plus-test@0.0.0-g52709db6.20260226-1136(@opentelemetry/api@1.9.0)(@types/node@24.10.13)(esbuild@0.27.3)(happy-dom@20.3.5)(jiti@2.6.1)(jsdom@27.4.0(@noble/hashes@1.8.0))(terser@5.46.0)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2))
@@ -295,6 +310,9 @@ importers:
typescript:
specifier: 5.9.3
version: 5.9.3
+ unplugin-vue-markdown:
+ specifier: ^29.2.0
+ version: 29.2.0(vite@8.0.0-beta.10(@types/node@24.10.13)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))
vitest:
specifier: npm:@voidzero-dev/vite-plus-test@0.0.0-g52709db6.20260226-1136
version: '@voidzero-dev/vite-plus-test@0.0.0-g52709db6.20260226-1136(@opentelemetry/api@1.9.0)(@types/node@24.10.13)(esbuild@0.27.3)(happy-dom@20.3.5)(jiti@2.6.1)(jsdom@27.4.0(@noble/hashes@1.8.0))(terser@5.46.0)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2)'
@@ -352,7 +370,7 @@ importers:
dependencies:
'@nuxt/ui':
specifier: 4.5.0
- version: 4.5.0(@nuxt/content@3.11.2(better-sqlite3@12.6.2)(magicast@0.5.1)(valibot@1.2.0(typescript@5.9.3)))(@tiptap/extensions@3.20.0(@tiptap/core@3.20.0(@tiptap/pm@3.20.0))(@tiptap/pm@3.20.0))(@tiptap/y-tiptap@3.0.2(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.6)(y-protocols@1.0.7(yjs@13.6.29))(yjs@13.6.29))(@upstash/redis@1.36.2)(db0@0.3.4(better-sqlite3@12.6.2))(embla-carousel@8.6.0)(focus-trap@8.0.0)(ioredis@5.9.2)(magicast@0.5.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(tailwindcss@4.2.1)(typescript@5.9.3)(valibot@1.2.0(typescript@5.9.3))(vite@8.0.0-beta.10(@types/node@25.0.10)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(vue-router@4.6.4(vue@3.5.29(typescript@5.9.3)))(vue@3.5.29(typescript@5.9.3))(yjs@13.6.29)(zod@4.3.6)
+ version: 4.5.0(@nuxt/content@3.11.2(@valibot/to-json-schema@1.5.0(valibot@1.2.0(typescript@5.9.3)))(better-sqlite3@12.6.2)(magicast@0.5.1)(valibot@1.2.0(typescript@5.9.3)))(@tiptap/extensions@3.20.0(@tiptap/core@3.20.0(@tiptap/pm@3.20.0))(@tiptap/pm@3.20.0))(@tiptap/y-tiptap@3.0.2(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.6)(y-protocols@1.0.7(yjs@13.6.29))(yjs@13.6.29))(@upstash/redis@1.36.2)(db0@0.3.4(better-sqlite3@12.6.2))(embla-carousel@8.6.0)(focus-trap@8.0.0)(ioredis@5.9.2)(magicast@0.5.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(tailwindcss@4.2.1)(typescript@5.9.3)(valibot@1.2.0(typescript@5.9.3))(vite@8.0.0-beta.10(@types/node@25.0.10)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(vue-router@4.6.4(vue@3.5.29(typescript@5.9.3)))(vue@3.5.29(typescript@5.9.3))(yjs@13.6.29)(zod@4.3.6)
'@nuxtjs/mdc':
specifier: 0.20.1
version: 0.20.1(magicast@0.5.1)
@@ -361,7 +379,7 @@ importers:
version: 12.6.2
docus:
specifier: 5.6.1
- version: 5.6.1(1b65878319a3c9b5a58513a82f531338)
+ version: 5.6.1(1bb863532954efd1f2acaa1e1d613817)
nuxt:
specifier: 4.3.1
version: 4.3.1(@parcel/watcher@2.5.4)(@types/node@25.0.10)(@upstash/redis@1.36.2)(@vue/compiler-sfc@3.5.29)(better-sqlite3@12.6.2)(cac@6.7.14)(db0@0.3.4(better-sqlite3@12.6.2))(encoding@0.1.13)(eslint@9.39.2(jiti@2.6.1))(ioredis@5.9.2)(magicast@0.5.1)(optionator@0.9.4)(oxlint@1.50.0(oxlint-tsgolint@0.15.0))(rolldown@1.0.0-rc.5)(rollup@4.56.0)(terser@5.46.0)(tsx@4.21.0)(typescript@5.9.3)(vite@8.0.0-beta.10(@types/node@25.0.10)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(vue-tsc@3.2.5(typescript@5.9.3))(yaml@2.8.2)
@@ -495,6 +513,9 @@ packages:
'@asamuzakjp/nwsapi@2.3.9':
resolution: {integrity: sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==}
+ '@atcute/bluesky-richtext-segmenter@3.0.0':
+ resolution: {integrity: sha512-NhZTUKtFpeBBbILwAcxj5u4RobIoHOmGw3CAaaEFNebKYSvmTecrXJ7XufHw5DFOUdr8SiKXQVRQxGAxulMNWg==}
+
'@atproto-labs/did-resolver@0.2.6':
resolution: {integrity: sha512-2K1bC04nI2fmgNcvof+yA28IhGlpWn2JKYlPa7To9JTKI45FINCGkQSGiL2nyXlyzDJJ34fZ1aq6/IRFIOIiqg==}
@@ -524,6 +545,9 @@ packages:
'@atproto-labs/simple-store@0.3.0':
resolution: {integrity: sha512-nOb6ONKBRJHRlukW1sVawUkBqReLlLx6hT35VS3imaNPwiXDxLnTK7lxw3Lrl9k5yugSBDQAkZAq3MPTEFSUBQ==}
+ '@atproto/api@0.18.21':
+ resolution: {integrity: sha512-s35MIJerGT/pKe2xJtKKswqlIr/ola2r2iURBKBL0Mk1OKe6jP4YvTMh1N2d2PEANFzNNTbKoDaLfJPo2Uvc/w==}
+
'@atproto/common-web@0.4.17':
resolution: {integrity: sha512-sfxD8NGxyoxhxmM9EUshEFbWcJ3+JHEOZF4Quk6HsCh1UxpHBmLabT/vEsAkDWl+C/8U0ine0+c/gHyE/OZiQQ==}
@@ -2011,6 +2035,18 @@ packages:
engines: {node: '>=18'}
hasBin: true
+ '@mdit-vue/plugin-component@3.0.2':
+ resolution: {integrity: sha512-Fu53MajrZMOAjOIPGMTdTXgHLgGU9KwTqKtYc6WNYtFZNKw04euSfJ/zFg8eBY/2MlciVngkF7Gyc2IL7e8Bsw==}
+ engines: {node: '>=20.0.0'}
+
+ '@mdit-vue/plugin-frontmatter@3.0.2':
+ resolution: {integrity: sha512-QKKgIva31YtqHgSAz7S7hRcL7cHXiqdog4wxTfxeQCHo+9IP4Oi5/r1Y5E93nTPccpadDWzAwr3A0F+kAEnsVQ==}
+ engines: {node: '>=20.0.0'}
+
+ '@mdit-vue/types@3.0.2':
+ resolution: {integrity: sha512-00aAZ0F0NLik6I6Yba2emGbHLxv+QYrPH00qQ5dFKXlAo1Ll2RHDXwY7nN2WAfrx2pP+WrvSRFTGFCNGdzBDHw==}
+ engines: {node: '>=20.0.0'}
+
'@mdx-js/react@3.1.1':
resolution: {integrity: sha512-f++rKLQgUVYDAtECQ6fn/is15GkEH9+nZPM3MS0RcxVqoTfawHvDlSCH7JbMhAM6uJ32v3eXLvLmLvjGu7PTQw==}
peerDependencies:
@@ -4101,12 +4137,29 @@ packages:
'@shikijs/engine-oniguruma@3.22.0':
resolution: {integrity: sha512-DyXsOG0vGtNtl7ygvabHd7Mt5EY8gCNqR9Y7Lpbbd/PbJvgWrqaKzH1JW6H6qFkuUa8aCxoiYVv8/YfFljiQxA==}
+ '@shikijs/engine-oniguruma@3.23.0':
+ resolution: {integrity: sha512-1nWINwKXxKKLqPibT5f4pAFLej9oZzQTsby8942OTlsJzOBZ0MWKiwzMsd+jhzu8YPCHAswGnnN1YtQfirL35g==}
+
'@shikijs/langs@3.22.0':
resolution: {integrity: sha512-x/42TfhWmp6H00T6uwVrdTJGKgNdFbrEdhaDwSR5fd5zhQ1Q46bHq9EO61SCEWJR0HY7z2HNDMaBZp8JRmKiIA==}
+ '@shikijs/langs@3.23.0':
+ resolution: {integrity: sha512-2Ep4W3Re5aB1/62RSYQInK9mM3HsLeB91cHqznAJMuylqjzNVAVCMnNWRHFtcNHXsoNRayP9z1qj4Sq3nMqYXg==}
+
+ '@shikijs/markdown-it@3.23.0':
+ resolution: {integrity: sha512-0tgFk+UUxBDXmdS/3xznAj0hhZWAF88UgpGGfgQppAEohtYKw+5MAxpuPQwa+baK/NbRrGlyfGdcpeXZqqEQSw==}
+ peerDependencies:
+ markdown-it-async: ^2.2.0
+ peerDependenciesMeta:
+ markdown-it-async:
+ optional: true
+
'@shikijs/themes@3.22.0':
resolution: {integrity: sha512-o+tlOKqsr6FE4+mYJG08tfCFDS+3CG20HbldXeVoyP+cYSUxDhrFf3GPjE60U55iOkkjbpY2uC3It/eeja35/g==}
+ '@shikijs/themes@3.23.0':
+ resolution: {integrity: sha512-5qySYa1ZgAT18HR/ypENL9cUSGOeI2x+4IvYJu4JgVJdizn6kG4ia5Q1jDEOi7gTbN4RbuYtmHh0W3eccOrjMA==}
+
'@shikijs/transformers@3.23.0':
resolution: {integrity: sha512-F9msZVxdF+krQNSdQ4V+Ja5QemeAoTQ2jxt7nJCwhDsdF1JWS3KxIQXA3lQbyKwS3J61oHRUSv4jYWv3CkaKTQ==}
@@ -4823,6 +4876,11 @@ packages:
'@upstash/redis@1.36.2':
resolution: {integrity: sha512-C0Yt8hc12vLaQYRG1fMci8iPrLtnTdbJG0HR5T8vKnvEP/1RdMMblsOJs5/jp0JXZJ1oSzMnQz4J9EVezNpI6A==}
+ '@valibot/to-json-schema@1.5.0':
+ resolution: {integrity: sha512-GE7DmSr1C2UCWPiV0upRH6mv0cCPsqYGs819fb6srCS1tWhyXrkGGe+zxUiwzn/L1BOfADH4sNjY/YHCuP8phQ==}
+ peerDependencies:
+ valibot: ^1.2.0
+
'@vercel/nft@1.3.0':
resolution: {integrity: sha512-i4EYGkCsIjzu4vorDUbqglZc5eFtQI2syHb++9ZUDm6TU4edVywGpVnYDein35x9sevONOn9/UabfQXuNXtuzQ==}
engines: {node: '>=20'}
@@ -4859,6 +4917,12 @@ packages:
vite: ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0
vue: ^3.2.25
+ '@vitest/browser-playwright@4.0.18':
+ resolution: {integrity: sha512-gfajTHVCiwpxRj1qh0Sh/5bbGLG4F/ZH/V9xvFVoFddpITfMta9YGow0W6ZpTTORv2vdJuz9TnrNSmjKvpOf4g==}
+ peerDependencies:
+ playwright: '*'
+ vitest: 4.0.18
+
'@vitest/browser@4.0.18':
resolution: {integrity: sha512-gVQqh7paBz3gC+ZdcCmNSWJMk70IUjDeVqi+5m5vYpEHsIwRgw3Y545jljtajhkekIpIp5Gg8oK7bctgY0E2Ng==}
peerDependencies:
@@ -5466,6 +5530,9 @@ packages:
resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==}
engines: {node: '>= 0.4'}
+ await-lock@2.2.2:
+ resolution: {integrity: sha512-aDczADvlvTGajTDjcjpJMqRkOF6Qdz3YbPZm/PyW6tKPkx2hlYBzxMhEywM/tU72HrVZjgl5VCdRuMlA7pZ8Gw==}
+
axe-core@4.11.1:
resolution: {integrity: sha512-BASOg+YwO2C+346x3LZOeoovTIoTrRqEsqMa6fmfAV0P+U9mFr9NsyOEpiYvFjbc64NMrSswhV50WdXzdb/Z5A==}
engines: {node: '>=4'}
@@ -7737,6 +7804,9 @@ packages:
resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==}
engines: {node: '>=10'}
+ markdown-it-async@2.2.0:
+ resolution: {integrity: sha512-sITME+kf799vMeO/ww/CjH6q+c05f6TLpn6VOmmWCGNqPJzSh+uFgZoMB9s0plNtW6afy63qglNAC3MhrhP/gg==}
+
markdown-it@14.1.1:
resolution: {integrity: sha512-BuU2qnTti9YKgK5N+IeMubp14ZUKUUw7yeJbkjtosvHiP0AZ5c8IAgEMk79D0eC8F23r4Ac/q8cAIFdm2FtyoA==}
hasBin: true
@@ -9260,6 +9330,9 @@ packages:
shiki@3.22.0:
resolution: {integrity: sha512-LBnhsoYEe0Eou4e1VgJACes+O6S6QC0w71fCSp5Oya79inkwkm15gQ1UF6VtQ8j/taMDh79hAB49WUk8ALQW3g==}
+ shiki@3.23.0:
+ resolution: {integrity: sha512-55Dj73uq9ZXL5zyeRPzHQsK7Nbyt6Y10k5s7OjuFZGMhpp4r/rsLBH0o/0fstIzX1Lep9VxefWljK/SKCzygIA==}
+
side-channel-list@1.0.0:
resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==}
engines: {node: '>= 0.4'}
@@ -9668,6 +9741,10 @@ packages:
resolution: {integrity: sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==}
engines: {node: '>=14.0.0'}
+ tlds@1.261.0:
+ resolution: {integrity: sha512-QXqwfEl9ddlGBaRFXIvNKK6OhipSiLXuRuLJX5DErz0o0Q0rYxulWLdFryTkV5PkdZct5iMInwYEGe/eR++1AA==}
+ hasBin: true
+
tldts-core@7.0.19:
resolution: {integrity: sha512-lJX2dEWx0SGH4O6p+7FPwYmJ/bu1JbcGJ8RLaG9b7liIgZ85itUVEPbMtWRVrde/0fnDPEPHW10ZsKW3kVsE9A==}
@@ -9987,6 +10064,12 @@ packages:
'@nuxt/kit':
optional: true
+ unplugin-vue-markdown@29.2.0:
+ resolution: {integrity: sha512-/x2hFgQ6cWN1Kls+yK5mAI9YDmeTofftynVGgOy1llBlDX1ifaXsQBls/bpORaiwn7cxA7HkOo0wn/xKcrXBHA==}
+ engines: {node: '>=20'}
+ peerDependencies:
+ vite: ^2.0.0 || ^3.0.0-0 || ^4.0.0-0 || ^5.0.0-0 || ^6.0.0 || ^7.0.0
+
unplugin-vue-router@0.16.2:
resolution: {integrity: sha512-lE6ZjnHaXfS2vFI/PSEwdKcdOo5RwAbCKUnPBIN9YwLgSWas3x+qivzQvJa/uxhKzJldE6WK43aDKjGj9Rij9w==}
deprecated: 'Merged into vuejs/router. Migrate: https://router.vuejs.org/guide/migration/v4-to-v5.html'
@@ -10866,6 +10949,8 @@ snapshots:
'@asamuzakjp/nwsapi@2.3.9':
optional: true
+ '@atcute/bluesky-richtext-segmenter@3.0.0': {}
+
'@atproto-labs/did-resolver@0.2.6':
dependencies:
'@atproto-labs/fetch': 0.2.3
@@ -10913,6 +10998,17 @@ snapshots:
'@atproto-labs/simple-store@0.3.0': {}
+ '@atproto/api@0.18.21':
+ dependencies:
+ '@atproto/common-web': 0.4.17
+ '@atproto/lexicon': 0.6.1
+ '@atproto/syntax': 0.4.3
+ '@atproto/xrpc': 0.7.7
+ await-lock: 2.2.2
+ multiformats: 9.9.0
+ tlds: 1.261.0
+ zod: 3.25.76
+
'@atproto/common-web@0.4.17':
dependencies:
'@atproto/lex-data': 0.0.12
@@ -12512,6 +12608,20 @@ snapshots:
- encoding
- supports-color
+ '@mdit-vue/plugin-component@3.0.2':
+ dependencies:
+ '@types/markdown-it': 14.1.2
+ markdown-it: 14.1.1
+
+ '@mdit-vue/plugin-frontmatter@3.0.2':
+ dependencies:
+ '@mdit-vue/types': 3.0.2
+ '@types/markdown-it': 14.1.2
+ gray-matter: 4.0.3
+ markdown-it: 14.1.1
+
+ '@mdit-vue/types@3.0.2': {}
+
'@mdx-js/react@3.1.1(@types/react@19.2.14)(react@19.2.4)':
dependencies:
'@types/mdx': 2.0.13
@@ -12668,7 +12778,7 @@ snapshots:
- magicast
- supports-color
- '@nuxt/content@3.11.2(better-sqlite3@12.6.2)(magicast@0.5.1)(valibot@1.2.0(typescript@5.9.3))':
+ '@nuxt/content@3.11.2(@valibot/to-json-schema@1.5.0(valibot@1.2.0(typescript@5.9.3)))(better-sqlite3@12.6.2)(magicast@0.5.1)(valibot@1.2.0(typescript@5.9.3))':
dependencies:
'@nuxt/kit': 4.3.1(magicast@0.5.1)
'@nuxtjs/mdc': 0.20.1(magicast@0.5.1)
@@ -12719,6 +12829,7 @@ snapshots:
zod: 3.25.76
zod-to-json-schema: 3.25.1(zod@3.25.76)
optionalDependencies:
+ '@valibot/to-json-schema': 1.5.0(valibot@1.2.0(typescript@5.9.3))
better-sqlite3: 12.6.2
valibot: 1.2.0(typescript@5.9.3)
transitivePeerDependencies:
@@ -13295,7 +13406,7 @@ snapshots:
- typescript
- vite
- '@nuxt/ui@4.5.0(@nuxt/content@3.11.2(better-sqlite3@12.6.2)(magicast@0.5.1)(valibot@1.2.0(typescript@5.9.3)))(@tiptap/extensions@3.20.0(@tiptap/core@3.20.0(@tiptap/pm@3.20.0))(@tiptap/pm@3.20.0))(@tiptap/y-tiptap@3.0.2(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.6)(y-protocols@1.0.7(yjs@13.6.29))(yjs@13.6.29))(@upstash/redis@1.36.2)(db0@0.3.4(better-sqlite3@12.6.2))(embla-carousel@8.6.0)(focus-trap@8.0.0)(ioredis@5.9.2)(magicast@0.5.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(tailwindcss@4.2.1)(typescript@5.9.3)(valibot@1.2.0(typescript@5.9.3))(vite@8.0.0-beta.10(@types/node@25.0.10)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(vue-router@4.6.4(vue@3.5.29(typescript@5.9.3)))(vue@3.5.29(typescript@5.9.3))(yjs@13.6.29)(zod@4.3.6)':
+ '@nuxt/ui@4.5.0(@nuxt/content@3.11.2(@valibot/to-json-schema@1.5.0(valibot@1.2.0(typescript@5.9.3)))(better-sqlite3@12.6.2)(magicast@0.5.1)(valibot@1.2.0(typescript@5.9.3)))(@tiptap/extensions@3.20.0(@tiptap/core@3.20.0(@tiptap/pm@3.20.0))(@tiptap/pm@3.20.0))(@tiptap/y-tiptap@3.0.2(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.6)(y-protocols@1.0.7(yjs@13.6.29))(yjs@13.6.29))(@upstash/redis@1.36.2)(db0@0.3.4(better-sqlite3@12.6.2))(embla-carousel@8.6.0)(focus-trap@8.0.0)(ioredis@5.9.2)(magicast@0.5.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(tailwindcss@4.2.1)(typescript@5.9.3)(valibot@1.2.0(typescript@5.9.3))(vite@8.0.0-beta.10(@types/node@25.0.10)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(vue-router@4.6.4(vue@3.5.29(typescript@5.9.3)))(vue@3.5.29(typescript@5.9.3))(yjs@13.6.29)(zod@4.3.6)':
dependencies:
'@floating-ui/dom': 1.7.5
'@iconify/vue': 5.0.0(vue@3.5.29(typescript@5.9.3))
@@ -13364,7 +13475,7 @@ snapshots:
vaul-vue: 0.4.1(reka-ui@2.8.2(vue@3.5.29(typescript@5.9.3)))(vue@3.5.29(typescript@5.9.3))
vue-component-type-helpers: 3.2.5
optionalDependencies:
- '@nuxt/content': 3.11.2(better-sqlite3@12.6.2)(magicast@0.5.1)(valibot@1.2.0(typescript@5.9.3))
+ '@nuxt/content': 3.11.2(@valibot/to-json-schema@1.5.0(valibot@1.2.0(typescript@5.9.3)))(better-sqlite3@12.6.2)(magicast@0.5.1)(valibot@1.2.0(typescript@5.9.3))
valibot: 1.2.0(typescript@5.9.3)
vue-router: 4.6.4(vue@3.5.29(typescript@5.9.3))
zod: 4.3.6
@@ -14779,14 +14890,34 @@ snapshots:
'@shikijs/types': 3.22.0
'@shikijs/vscode-textmate': 10.0.2
+ '@shikijs/engine-oniguruma@3.23.0':
+ dependencies:
+ '@shikijs/types': 3.23.0
+ '@shikijs/vscode-textmate': 10.0.2
+
'@shikijs/langs@3.22.0':
dependencies:
'@shikijs/types': 3.22.0
+ '@shikijs/langs@3.23.0':
+ dependencies:
+ '@shikijs/types': 3.23.0
+
+ '@shikijs/markdown-it@3.23.0(markdown-it-async@2.2.0)':
+ dependencies:
+ markdown-it: 14.1.1
+ shiki: 3.23.0
+ optionalDependencies:
+ markdown-it-async: 2.2.0
+
'@shikijs/themes@3.22.0':
dependencies:
'@shikijs/types': 3.22.0
+ '@shikijs/themes@3.23.0':
+ dependencies:
+ '@shikijs/types': 3.23.0
+
'@shikijs/transformers@3.23.0':
dependencies:
'@shikijs/core': 3.23.0
@@ -15658,6 +15789,10 @@ snapshots:
dependencies:
uncrypto: 0.1.3
+ '@valibot/to-json-schema@1.5.0(valibot@1.2.0(typescript@5.9.3))':
+ dependencies:
+ valibot: 1.2.0(typescript@5.9.3)
+
'@vercel/nft@1.3.0(encoding@0.1.13)(rollup@4.56.0)':
dependencies:
'@mapbox/node-pre-gyp': 2.0.3(encoding@0.1.13)
@@ -15739,6 +15874,19 @@ snapshots:
vite: '@voidzero-dev/vite-plus-core@0.0.0-g52709db6.20260226-1136(@types/node@25.0.10)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.46.0)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2)'
vue: 3.5.29(typescript@5.9.3)
+ '@vitest/browser-playwright@4.0.18(@voidzero-dev/vite-plus-test@0.0.0-g52709db6.20260226-1136(@opentelemetry/api@1.9.0)(@types/node@24.10.13)(esbuild@0.27.3)(happy-dom@20.3.5)(jiti@2.6.1)(jsdom@27.4.0(@noble/hashes@1.8.0))(terser@5.46.0)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2))(playwright@1.58.2)(vite@8.0.0-beta.10(@types/node@24.10.13)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))':
+ dependencies:
+ '@vitest/browser': 4.0.18(@voidzero-dev/vite-plus-test@0.0.0-g52709db6.20260226-1136(@opentelemetry/api@1.9.0)(@types/node@24.10.13)(esbuild@0.27.3)(happy-dom@20.3.5)(jiti@2.6.1)(jsdom@27.4.0(@noble/hashes@1.8.0))(terser@5.46.0)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2))(vite@8.0.0-beta.10(@types/node@24.10.13)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))
+ '@vitest/mocker': 4.0.18(vite@8.0.0-beta.10(@types/node@24.10.13)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))
+ playwright: 1.58.2
+ tinyrainbow: 3.0.3
+ vitest: '@voidzero-dev/vite-plus-test@0.0.0-g52709db6.20260226-1136(@opentelemetry/api@1.9.0)(@types/node@24.10.13)(esbuild@0.27.3)(happy-dom@20.3.5)(jiti@2.6.1)(jsdom@27.4.0(@noble/hashes@1.8.0))(terser@5.46.0)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2)'
+ transitivePeerDependencies:
+ - bufferutil
+ - msw
+ - utf-8-validate
+ - vite
+
'@vitest/browser@4.0.18(@voidzero-dev/vite-plus-test@0.0.0-g52709db6.20260226-1136(@opentelemetry/api@1.9.0)(@types/node@24.10.13)(esbuild@0.27.3)(happy-dom@20.3.5)(jiti@2.6.1)(jsdom@27.4.0(@noble/hashes@1.8.0))(terser@5.46.0)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2))(vite@8.0.0-beta.10(@types/node@24.10.13)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))':
dependencies:
'@vitest/mocker': 4.0.18(vite@8.0.0-beta.10(@types/node@24.10.13)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))
@@ -15755,7 +15903,6 @@ snapshots:
- msw
- utf-8-validate
- vite
- optional: true
'@vitest/coverage-v8@4.0.18(@vitest/browser@4.0.18(@voidzero-dev/vite-plus-test@0.0.0-g52709db6.20260226-1136(@opentelemetry/api@1.9.0)(@types/node@24.10.13)(esbuild@0.27.3)(happy-dom@20.3.5)(jiti@2.6.1)(jsdom@27.4.0(@noble/hashes@1.8.0))(terser@5.46.0)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2))(vite@8.0.0-beta.10(@types/node@24.10.13)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)))(@voidzero-dev/vite-plus-test@0.0.0-g52709db6.20260226-1136(@opentelemetry/api@1.9.0)(@types/node@24.10.13)(esbuild@0.27.3)(happy-dom@20.3.5)(jiti@2.6.1)(jsdom@27.4.0(@noble/hashes@1.8.0))(terser@5.46.0)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2))':
dependencies:
@@ -15788,7 +15935,6 @@ snapshots:
magic-string: 0.30.21
optionalDependencies:
vite: 8.0.0-beta.10(@types/node@24.10.13)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)
- optional: true
'@vitest/pretty-format@3.2.4':
dependencies:
@@ -15802,8 +15948,7 @@ snapshots:
dependencies:
tinyspy: 4.0.4
- '@vitest/spy@4.0.18':
- optional: true
+ '@vitest/spy@4.0.18': {}
'@vitest/utils@3.2.4':
dependencies:
@@ -16454,6 +16599,8 @@ snapshots:
dependencies:
possible-typed-array-names: 1.1.0
+ await-lock@2.2.2: {}
+
axe-core@4.11.1: {}
b4a@1.7.3: {}
@@ -17057,7 +17204,7 @@ snapshots:
doctypes@1.1.0: {}
- docus@5.6.1(1b65878319a3c9b5a58513a82f531338):
+ docus@5.6.1(1bb863532954efd1f2acaa1e1d613817):
dependencies:
'@ai-sdk/gateway': 3.0.55(zod@4.3.6)
'@ai-sdk/mcp': 1.0.21(zod@4.3.6)
@@ -17065,10 +17212,10 @@ snapshots:
'@iconify-json/lucide': 1.2.93
'@iconify-json/simple-icons': 1.2.71
'@iconify-json/vscode-icons': 1.2.43
- '@nuxt/content': 3.11.2(better-sqlite3@12.6.2)(magicast@0.5.1)(valibot@1.2.0(typescript@5.9.3))
+ '@nuxt/content': 3.11.2(@valibot/to-json-schema@1.5.0(valibot@1.2.0(typescript@5.9.3)))(better-sqlite3@12.6.2)(magicast@0.5.1)(valibot@1.2.0(typescript@5.9.3))
'@nuxt/image': 2.0.0(@upstash/redis@1.36.2)(db0@0.3.4(better-sqlite3@12.6.2))(ioredis@5.9.2)(magicast@0.5.1)
'@nuxt/kit': 4.3.1(magicast@0.5.1)
- '@nuxt/ui': 4.5.0(@nuxt/content@3.11.2(better-sqlite3@12.6.2)(magicast@0.5.1)(valibot@1.2.0(typescript@5.9.3)))(@tiptap/extensions@3.20.0(@tiptap/core@3.20.0(@tiptap/pm@3.20.0))(@tiptap/pm@3.20.0))(@tiptap/y-tiptap@3.0.2(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.6)(y-protocols@1.0.7(yjs@13.6.29))(yjs@13.6.29))(@upstash/redis@1.36.2)(db0@0.3.4(better-sqlite3@12.6.2))(embla-carousel@8.6.0)(focus-trap@8.0.0)(ioredis@5.9.2)(magicast@0.5.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(tailwindcss@4.2.1)(typescript@5.9.3)(valibot@1.2.0(typescript@5.9.3))(vite@8.0.0-beta.10(@types/node@25.0.10)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(vue-router@4.6.4(vue@3.5.29(typescript@5.9.3)))(vue@3.5.29(typescript@5.9.3))(yjs@13.6.29)(zod@4.3.6)
+ '@nuxt/ui': 4.5.0(@nuxt/content@3.11.2(@valibot/to-json-schema@1.5.0(valibot@1.2.0(typescript@5.9.3)))(better-sqlite3@12.6.2)(magicast@0.5.1)(valibot@1.2.0(typescript@5.9.3)))(@tiptap/extensions@3.20.0(@tiptap/core@3.20.0(@tiptap/pm@3.20.0))(@tiptap/pm@3.20.0))(@tiptap/y-tiptap@3.0.2(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.6)(y-protocols@1.0.7(yjs@13.6.29))(yjs@13.6.29))(@upstash/redis@1.36.2)(db0@0.3.4(better-sqlite3@12.6.2))(embla-carousel@8.6.0)(focus-trap@8.0.0)(ioredis@5.9.2)(magicast@0.5.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(tailwindcss@4.2.1)(typescript@5.9.3)(valibot@1.2.0(typescript@5.9.3))(vite@8.0.0-beta.10(@types/node@25.0.10)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(vue-router@4.6.4(vue@3.5.29(typescript@5.9.3)))(vue@3.5.29(typescript@5.9.3))(yjs@13.6.29)(zod@4.3.6)
'@nuxtjs/i18n': 10.2.3(@upstash/redis@1.36.2)(@vue/compiler-dom@3.5.29)(db0@0.3.4(better-sqlite3@12.6.2))(eslint@9.39.2(jiti@2.6.1))(ioredis@5.9.2)(magicast@0.5.1)(rollup@4.56.0)(vue@3.5.29(typescript@5.9.3))
'@nuxtjs/mcp-toolkit': 0.7.0(magicast@0.5.1)(zod@4.3.6)
'@nuxtjs/mdc': 0.20.1(magicast@0.5.1)
@@ -19117,6 +19264,11 @@ snapshots:
dependencies:
semver: 7.7.4
+ markdown-it-async@2.2.0:
+ dependencies:
+ '@types/markdown-it': 14.1.2
+ markdown-it: 14.1.1
+
markdown-it@14.1.1:
dependencies:
argparse: 2.0.1
@@ -21654,6 +21806,17 @@ snapshots:
'@shikijs/vscode-textmate': 10.0.2
'@types/hast': 3.0.4
+ shiki@3.23.0:
+ dependencies:
+ '@shikijs/core': 3.23.0
+ '@shikijs/engine-javascript': 3.23.0
+ '@shikijs/engine-oniguruma': 3.23.0
+ '@shikijs/langs': 3.23.0
+ '@shikijs/themes': 3.23.0
+ '@shikijs/types': 3.23.0
+ '@shikijs/vscode-textmate': 10.0.2
+ '@types/hast': 3.0.4
+
side-channel-list@1.0.0:
dependencies:
es-errors: 1.3.0
@@ -22092,6 +22255,8 @@ snapshots:
tinyspy@4.0.4: {}
+ tlds@1.261.0: {}
+
tldts-core@7.0.19:
optional: true
@@ -22468,6 +22633,18 @@ snapshots:
optionalDependencies:
'@nuxt/kit': 4.3.1(magicast@0.5.1)
+ unplugin-vue-markdown@29.2.0(vite@8.0.0-beta.10(@types/node@24.10.13)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)):
+ dependencies:
+ '@mdit-vue/plugin-component': 3.0.2
+ '@mdit-vue/plugin-frontmatter': 3.0.2
+ '@mdit-vue/types': 3.0.2
+ '@types/markdown-it': 14.1.2
+ markdown-it: 14.1.1
+ markdown-it-async: 2.2.0
+ unplugin: 2.3.11
+ unplugin-utils: 0.3.1
+ vite: 8.0.0-beta.10(@types/node@24.10.13)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)
+
unplugin-vue-router@0.16.2(@vue/compiler-sfc@3.5.29)(vue-router@4.6.4(vue@3.5.29(typescript@5.9.3)))(vue@3.5.29(typescript@5.9.3)):
dependencies:
'@babel/generator': 7.29.1
diff --git a/server/api/atproto/author-profiles.get.ts b/server/api/atproto/bluesky-author-profiles.get.ts
similarity index 99%
rename from server/api/atproto/author-profiles.get.ts
rename to server/api/atproto/bluesky-author-profiles.get.ts
index e0e405196..1122376d8 100644
--- a/server/api/atproto/author-profiles.get.ts
+++ b/server/api/atproto/bluesky-author-profiles.get.ts
@@ -52,6 +52,7 @@ export default defineCachedEventHandler(
.catch(() => ({ profiles: [] }))
const avatarMap = new Map()
+
for (const profile of response.profiles) {
if (profile.avatar) {
avatarMap.set(profile.handle, profile.avatar)
diff --git a/shared/schemas/atproto.ts b/shared/schemas/atproto.ts
new file mode 100644
index 000000000..588517023
--- /dev/null
+++ b/shared/schemas/atproto.ts
@@ -0,0 +1,18 @@
+import { object, string, startsWith, minLength, regex, pipe } from 'valibot'
+import type { InferOutput } from 'valibot'
+import { AT_URI_REGEX } from '#shared/utils/constants'
+
+/**
+ * INFO: Validates AT Protocol URI format (at://did:plc:.../app.bsky.feed.post/...)
+ * Used for referencing Bluesky posts in our database and API routes.
+ */
+export const BlueSkyUriSchema = object({
+ uri: pipe(
+ string(),
+ startsWith('at://'),
+ minLength(10),
+ regex(AT_URI_REGEX, 'Must be a valid at:// URI'),
+ ),
+})
+
+export type BlueSkyUri = InferOutput
diff --git a/shared/utils/constants.ts b/shared/utils/constants.ts
index eb18e8cef..5101fc4de 100644
--- a/shared/utils/constants.ts
+++ b/shared/utils/constants.ts
@@ -18,6 +18,8 @@ export const ERROR_PACKAGE_ANALYSIS_FAILED = 'Failed to analyze package.'
export const ERROR_PACKAGE_VERSION_AND_FILE_FAILED = 'Version and file path are required.'
export const ERROR_PACKAGE_REQUIREMENTS_FAILED =
'Package name, version, and file path are required.'
+export const ERROR_BLUESKY_URL_FAILED =
+ 'Invalid Bluesky URL format. Expected: https://bsky.app/profile/HANDLE/post/POST_ID'
export const ERROR_FILE_LIST_FETCH_FAILED = 'Failed to fetch file list.'
export const ERROR_CALC_INSTALL_SIZE_FAILED = 'Failed to calculate install size.'
export const NPM_MISSING_README_SENTINEL = 'ERROR: No README data found!'
@@ -77,5 +79,9 @@ export const BACKGROUND_THEMES = {
} as const
// Regex
+export const AT_URI_REGEX = /^at:\/\/(did:plc:[a-z0-9]+)\/app\.bsky\.feed\.post\/([a-z0-9]+)$/
+export const BLUESKY_URL_REGEX = /^https:\/\/bsky\.app\/profile\/[^/]+\/post\/[^/]+$/
+// INFO: For capture groups
+export const BLUESKY_URL_EXTRACT_REGEX = /profile\/([^/]+)\/post\/([^/]+)/
export const BSKY_POST_AT_URI_REGEX =
/^at:\/\/(did:plc:[a-z0-9]+)\/app\.bsky\.feed\.post\/([a-z0-9]+)$/
diff --git a/uno.config.ts b/uno.config.ts
index 506c8977d..e865abbd0 100644
--- a/uno.config.ts
+++ b/uno.config.ts
@@ -1,6 +1,7 @@
import {
defineConfig,
presetIcons,
+ presetTypography,
presetWind4,
transformerDirectives,
transformerVariantGroup,
@@ -34,6 +35,7 @@ export default defineConfig({
custom: customIcons,
},
}),
+ presetTypography(),
// keep this preset last
...(process.env.CI ? [] : [presetRtl(), presetA11y()]),
].filter(Boolean),