Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
2cf9c07
feat: relase/0.8 blog post
patak-cat Apr 8, 2026
805bc5d
feat: support external embed in bskypost
alexdln Apr 8, 2026
931c6e9
chore: remove console log
alexdln Apr 8, 2026
0ec3f5a
chore: update
patak-cat Apr 8, 2026
b90c0dc
chore: typo
patak-cat Apr 8, 2026
70ab86c
chore: update link
patak-cat Apr 8, 2026
e91b2dd
fix: everything
patak-cat Apr 8, 2026
5c531a8
chore: update
patak-cat Apr 8, 2026
552f4e2
chore: add youtube embed to blog
alexdln Apr 8, 2026
fc61793
chore: images
patak-cat Apr 8, 2026
bd250e7
chore: add og-image to release post
alexdln Apr 8, 2026
4f03334
test: fix tests
alexdln Apr 8, 2026
6a6a879
[autofix.ci] apply automated fixes
autofix-ci[bot] Apr 8, 2026
809f73a
fix: add cmd palette demo video and screenshots
serhalp Apr 8, 2026
1e11445
fix: actually add assets
serhalp Apr 8, 2026
8d1eb64
fix: add partial quadrant graph section
serhalp Apr 8, 2026
a581cac
[autofix.ci] apply automated fixes
autofix-ci[bot] Apr 8, 2026
2dcee85
fix: try to fix video tag
serhalp Apr 8, 2026
3c9bf1b
fix: add quadrant graph copy
serhalp Apr 8, 2026
3433918
fix: actually add assets, yet again
serhalp Apr 8, 2026
6e362cd
fix: adjust caption rendering
serhalp Apr 8, 2026
2ad1fa2
[autofix.ci] apply automated fixes
autofix-ci[bot] Apr 8, 2026
1a9e52e
fix: add media-src=self to CSP
serhalp Apr 8, 2026
56c2904
chore: update
patak-cat Apr 8, 2026
ce74479
fix: add some example links
serhalp Apr 8, 2026
0b24eac
chore: update title in new blog post
alexdln Apr 8, 2026
09e9c7e
fix: fix html validation errors
serhalp Apr 8, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 8 additions & 5 deletions app/components/global/BlogPostWrapper.vue
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ useSeoMeta({
ogTitle: props.frontmatter.title,
ogDescription: props.frontmatter.description || props.frontmatter.excerpt,
ogType: 'article',
ogImage: props.frontmatter.image,
...(props.frontmatter.draft ? { robots: 'noindex, nofollow' } : {}),
})

Expand All @@ -27,11 +28,13 @@ useHead({
],
})

defineOgImageComponent('BlogPost', {
title: props.frontmatter.title,
authors: post.value?.authors ?? [],
date: props.frontmatter.date,
})
if (!props.frontmatter.image) {
defineOgImageComponent('BlogPost', {
title: props.frontmatter.title,
authors: post.value?.authors ?? [],
date: props.frontmatter.date,
})
}

const slug = computed(() => props.frontmatter.slug)

Expand Down
32 changes: 30 additions & 2 deletions app/components/global/BlueskyPostEmbed.client.vue
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,18 @@ interface EmbedImage {
aspectRatio?: { width: number; height: number }
}

interface EmbedExternal {
description?: string
thumb?: string
title?: string
uri: string
}

interface BlueskyPost {
uri: string
author: PostAuthor
record: { text: string; createdAt: string }
embed?: { $type: string; images?: EmbedImage[] }
embed?: { $type: string; images?: EmbedImage[]; external?: EmbedExternal }
likeCount?: number
replyCount?: number
repostCount?: number
Expand Down Expand Up @@ -107,7 +114,7 @@ const postUrl = computed(() => {
:href="postUrl ?? '#'"
target="_blank"
rel="noopener noreferrer"
class="not-prose block rounded-lg border border-border bg-bg-subtle p-4 sm:p-5 no-underline hover:border-border-hover transition-colors duration-200 relative group"
class="not-prose block my-4 rounded-lg border border-border bg-bg-subtle p-4 sm:p-5 no-underline hover:border-border-hover transition-colors duration-200 relative group"
>
<!-- Bluesky icon -->
<span
Expand Down Expand Up @@ -154,6 +161,27 @@ const postUrl = computed(() => {
/>
</template>

<!-- Embedded external embed -->
<template v-if="post.embed?.external && post.embed.external.uri">
<div class="block mb-3 p-0.5 bg-bg-muted rounded-lg">
<img
v-if="post.embed.external.thumb"
:src="post.embed.external.thumb"
alt=""
class="w-full rounded-lg object-cover"
loading="lazy"
/>
<div class="text-fg-muted text-sm p-2">
<p class="font-medium truncate">
{{ post.embed.external.title || post.embed.external.uri }}
</p>
<p v-if="post.embed.external.description" class="text-sm line-clamp-2 mt-1">
{{ post.embed.external.description }}
</p>
</div>
</div>
</template>

<!-- Timestamp + engagement -->
<div class="flex items-center gap-4 text-sm text-fg-subtle">
<DateTime :datetime="post.record.createdAt" date-style="medium" />
Expand Down
121 changes: 121 additions & 0 deletions app/pages/blog/release/0.8.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
---
authors:
- name: Alex Savelyev
blueskyHandle: alexdln.com
- name: Philippe Serhal
blueskyHandle: philippeserhal.com
- name: Matias Capeletto
blueskyHandle: patak.cat
title: 'npmx 0.8: npm at your fingertips'
tags: ['OpenSource', 'Release']
excerpt: "Today we're releasing npmx 0.8 – including a new command palette and a lot of other features."
date: '2026-04-08'
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify schema expectation and compare with existing blog frontmatter date patterns.

set -euo pipefail

echo "== RawBlogPostSchema date validator =="
rg -n -C2 "date:\s*pipe\(string\(\),\s*isoTimestamp\(\)\)" shared/schemas/blog.ts

echo
echo "== Sample frontmatter dates in blog markdown files =="
rg -n --iglob "*.md" "^date:\s*['\"]?.+['\"]?$" app/pages/blog | head -n 80

echo
echo "== Check if this file uses date-only format =="
rg -n "^date:\s*['\"]?2026-04-08['\"]?$" app/pages/blog/release/0.8.md

Repository: npmx-dev/npmx.dev

Length of output: 639


🌐 Web query:

valibot isoTimestamp() validator accepts date format ISO 8601

💡 Result:

In Valibot, isoTimestamp() validates an ISO 8601 “timestamp” string in these shapes (timezone required):

  • yyyy-mm-ddThh:mm:ss.sssZ (UTC “Z”)
  • yyyy-mm-ddThh:mm:ss.sss±hh:mm (offset with colon)
  • yyyy-mm-ddThh:mm:ss.sss±hhmm (offset without colon) (valibot.dev)

Notes about what it accepts beyond those examples:

  • The fractional seconds part (.sss) is optional, and if present can be 1 to 9 digits. (valibot.dev)
  • It also allows a space instead of T between date and time. (valibot.dev)
  • It’s regex-based and won’t catch invalid calendar dates like June 31. (valibot.dev)

If you want an ISO 8601 date-time without a timezone, Valibot has isoDateTime() (format yyyy-mm-ddThh:mm). (valibot.dev)

Citations:


🏁 Script executed:

# Check valibot version and search for any validation errors or documentation in the repo
fd -e json -e ts -e md -e txt . | xargs grep -l "isoTimestamp" 2>/dev/null | head -20

Repository: npmx-dev/npmx.dev

Length of output: 83


🏁 Script executed:

# Look for package.json to find valibot version
cat package.json 2>/dev/null | grep -A2 -B2 "valibot" || echo "package.json not found in root"

Repository: npmx-dev/npmx.dev

Length of output: 211


🏁 Script executed:

# Search for any comments or documentation about the date format in the schema file
cat -n shared/schemas/blog.ts | head -60

Repository: npmx-dev/npmx.dev

Length of output: 1964


Convert date to ISO 8601 timestamp format.

Line 12 uses date-only format (2026-04-08), but the schema requires isoTimestamp(), which only accepts full timestamps with time and timezone (e.g. 2026-04-08T00:00:00.000Z). Update to a valid ISO timestamp.

slug: 'release/0.8'
image: 'https://npmx.dev/blog/og/release-0_8.png'
description: "Today we're releasing npmx 0.8 – including a new command palette and a lot of other features."
draft: false
---
Comment on lines +1 to +17
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Add required path to frontmatter to avoid post being dropped.

RawBlogPostSchema requires a path field; this frontmatter omits it. In modules/blog.ts, failed validation is skipped (continue), so the post may never render/list. Please add path (for example, /blog/release/0.8).

🛠️ Proposed fix
 title: 'npmx 0.8: around the moon'
 tags: ['OpenSource', 'Release']
 excerpt: "Today we're releasing npmx 0.8 – including a new command palette and a lot of other features."
 date: '2026-04-08'
+path: '/blog/release/0.8'
 slug: '0.8'
 description: "Today we're releasing npmx 0.8 – including a new command palette and a lot of other features."
 draft: false


# npmx 0.8: npm at your fingertips

Today we continue to improve [npmx.dev](https://npmx.dev). npmx 0.8 includes new features to help our users navigate the npm registry and chose great dependencies to build their projects. Our [repository](https://repo.npmx.dev) has more than 3K stars and we're 240+ contributors working together to craft a modern browser for our packages.

## Unleash your mechanical keyboard with ⌘ K

npmx now has a rich command palette for efficient, keyboard-driven access to every page, toggle, and action across the website. Open it by hitting ⌘ K on macOS or Ctrl+K on Windows/Linux, or by clicking the new “jump to…” item in the header.

<figure>
<video
poster="/blog/release/0.8/cmd-palette-demo-cover.png"
controls
loop
muted
playsinline
preload="metadata"
aria-label="Screen recording of npmx's command palette in action: opening it with Cmd+K, toggling dark/light theme, switching languages, navigating to a package page, browsing source files, switching package versions, and searching for a new package, all without leaving the keyboard."
>
<source src="/blog/release/0.8/cmd-palette-demo.mp4" type="video/mp4" />
</video>
<figcaption class="sr-only">
A screen recording demonstrating npmx's command palette. The video shows: the npmx home page in dark mode; opening the command palette with Cmd+K to reveal navigation, connections, and settings sections; using the palette to toggle to light mode, then back to dark mode; using the palette to switch the interface language to Vietnamese and back to English; navigating to the Vue package page and using the palette to access package actions like "download tarball" and "copy package name"; browsing into the package's source files in the built-in code viewer; using the palette to switch between Vue versions from a list filtered by a semantic versioning specifier; and finally typing "tinyexec" into the palette to search for a different package. Throughout, individual key presses are shown in an on-screen overlay.
</figcaption>
</figure>

There are already 68 total commands across the website, but you’ll always see just what’s relevant to your current view:

- On [a package page](https://npmx.dev/package/@clack/prompts), quickly download its tarball, copy its install command to your clipboard, open its repo, jump to its `@types/` package, and more
- On the [compare page](https://npmx.dev/compare/), toggle between the table and charts views, copy the table to your clipboard, and more
- When viewing [a package’s code](https://npmx.dev/package-code/unenv/v/2.0.0-rc.24/dist%2Findex.d.mts#L58), copy a link or the raw file contents to your clipboard, toggle between raw and preview mode, and more
- From anywhere, jump to another page, toggle dark/light mode, change your language, get help by showing all keyboard shortcuts, jumping to the docs or to the npmx Discord server.

You’ll always see a fallback option to submit your input as a search:

![Light-themed command palette interface with a title ‘command palette’ and subtitle about
navigating npm packages. A focused input field contains ‘tinylibs’. Below it, a single option
appears with a magnifying glass icon and the text ‘search for “tinylibs”](/blog/release/0.8/cmd-palette-search-fallback.png)

Oh, and one more thing.

On a package page, type or paste any version or [SemVer range specifier](https://docs.npmx.dev/guide/semver-ranges) and you’ll immediately see matching versions of that package.

![Dark-themed command palette modal titled ‘command palette’ with a subtitle about navigating npm packages. A search input contains ‘~1.0.1’. Below, a list labeled ‘Versions of tinyexec’ shows selectable versions: 1.0.4, 1.0.3, 1.0.2, and 1.0.1.](/blog/release/0.8/cmd-palette-semver.png)

The command palette is intended to be accessible to everyone, with a full, rich experience on mobile and desktop, keyboard, mouse, touch screens, and screen readers.

## New package comparison view: quadrant graphs

The [compare page](https://npmx.dev/compare/) graph mode now shows an additional view of the same
data: a quadrant graph. This is a scatter plot of all selected packages (up to 10 now, up from
four!), with an aggregate measure of traction (downloads, freshness, likes) on the x-axis and developer ergonomics (install size, dependencies, vulnerabilities, type support) on the y-axis.

<figure>
<img src="/blog/release/0.8/quadrant-graph-example.png" alt="Scatter plot of package traction vs. ergonomics, with delineated quadrants, showing seven UI component packages" />
<figcaption class="sr-only">
Dark-themed scatter plot titled ‘Package traction vs ergonomics.’ The vertical axis is labeled ‘Ergonomics’ (increasing upward), and the horizontal axis is labeled ‘Traction’ (increasing to the right). The chart is divided into four labeled regions: top left ‘Hidden gems,’ top right ‘Solid picks,’ bottom right ‘Popular with tradeoffs,’ and bottom left ‘Avoid.’ Several UI component libraries are plotted as colored dots:

- ‘emotion’ appears near the top center, indicating high ergonomics with moderate traction.
- ‘bootstrap’ is in the upper right quadrant, showing strong traction and relatively high ergonomics.
- ‘reka-ui’ and ‘radix-ui’ are on the right side near the horizontal midline, indicating high traction with moderate ergonomics.
- ‘@chakra-ui/react’ is in the lower right quadrant, suggesting high traction but lower ergonomics.
- ‘@material/web’ is near the bottom center, indicating low ergonomics and moderate traction.
- ‘solid-ui’ is on the far left near the horizontal midline, indicating low traction with moderate ergonomics.

Faint concentric rounded rectangles in the background suggest scoring tiers, and axis lines intersect at the center of the chart.

</figcaption>
</figure>

See the above example [live in action
here](https://npmx.dev/compare?view=charts&packages=bootstrap,@chakra-ui/react,emotion,@material/web,solid-ui,reka-ui,radix-ui).

## We were @ ATmosphereConf 26!

The atproto community gathered in Vancouver for their annual conference. Five members of the npmx community were there, thanks to the support of the atproto community. This wasn't a regular tech conference. We were very impressed by the quality of the talks and conversations, especially about non-technical topics. There was so much hope in the atmosphere.

<BlueskyPostEmbed url="https://bsky.app/profile/npmx.dev/post/3miamtd537s2m" />

There is a lot to unpack, a lot of potential for collaborating with other atproto projects on shared lexicons and cross-linking between our websites. We'd like to thank the organisers and the community at large once more for the warm welcome, and for inviting us to present npmx in one of the keynotes:

<iframe width="560" height="315" src="https://www.youtube-nocookie.com/embed/xPJkoJ6dlqE?si=28jAlZlogB7DuMpq" title="YouTube video player" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" class="w-full aspect-ratio-video rounded-lg" allowfullscreen></iframe>

## Noodles!

Thinking about who we are and what we stand for, we decided to launch noodles. These little changes on the main page focused on what we care about.

<BlueskyPostEmbed url="https://bsky.app/profile/npmx.dev/post/3miet6zoc5s2i" />

Today we're launching our second noodle to celebrate how far humanity can go when we work together. Visit [our landing](/) to enjoy a trip around the moon while searching for packages. If you'd like to help us brainstorm ideas, please join the conversation in the #noodles channel of our builders community and let's cook the best noodles together.

## News

A lot has happened since our [alpha launch](/blog/alpha-release). You can read Alex’s post for an overview of this past month, and to learn about some of the ideas the community is working on.

<BlueskyPostEmbed url="https://bsky.app/profile/alexdln.com/post/3mijbaws34c2q" />

We were also featured in the latest Igalia Chats episode discussing the history of npmx, the community, our ideas regarding funding, and more.

<BlueskyPostEmbed url="https://bsky.app/profile/igalia.com/post/3miwbkgbgyk2m" />

## What’s next

We have several features in the making, npmx 0.9 is already looking like a very interesting milestone for us. If you’re interested in getting involved, join us at [build.npmx.dev](https://build.npmx.dev). We’re just getting started!
2 changes: 1 addition & 1 deletion modules/blog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ function resolveAuthors(authors: Author[], avatarMap: Map<string, string>): Reso
* Resolves Bluesky avatars at build time.
*/
async function loadBlogPosts(blogDir: string, imagesDir: string): Promise<BlogPostFrontmatter[]> {
const files = await Array.fromAsync(glob(join(blogDir, '*.md').replace(/\\/g, '/')))
const files = await Array.fromAsync(glob(join(blogDir, '**/*.md').replace(/\\/g, '/')))

// First pass: extract raw frontmatter and collect all Bluesky handles
const rawPosts: Array<{ frontmatter: Record<string, unknown> }> = []
Expand Down
2 changes: 2 additions & 0 deletions modules/security-headers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ export default defineNuxtModule({
const frameSrc = [
'https://bsky.app',
'https://pdsmoover.com',
'https://www.youtube-nocookie.com/',
...(isDevtoolsRuntime ? ["'self'"] : []),
].join(' ')

Expand All @@ -69,6 +70,7 @@ export default defineNuxtModule({
`script-src 'self' 'unsafe-inline'`,
`style-src 'self' 'unsafe-inline'`,
`img-src ${imgSrc}`,
`media-src 'self'`,
`font-src 'self'`,
`connect-src ${connectSrc}`,
`frame-src ${frameSrc}`,
Expand Down
Binary file added public/blog/command-palette-fallback.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/blog/command-palette-versions.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/blog/og/release-0_8.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/blog/release/0.8/cmd-palette-demo.mp4
Binary file not shown.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/blog/release/0.8/cmd-palette-semver.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 2 additions & 0 deletions shared/schemas/blog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ export const RawBlogPostSchema = object({
excerpt: optional(string()),
tags: optional(array(string())),
draft: optional(boolean()),
image: optional(string()),
})

/** Schema for blog post frontmatter with resolved author data (avatars, profile URLs) */
Expand All @@ -58,6 +59,7 @@ export const BlogPostSchema = object({
excerpt: optional(string()),
tags: optional(array(string())),
draft: optional(boolean()),
image: optional(string()),
})

export type Author = InferOutput<typeof AuthorSchema>
Expand Down
8 changes: 6 additions & 2 deletions test/unit/modules/security-headers.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,9 @@ describe('security headers module', () => {
const csp = getCsp(nuxt)

expect(csp).toContain('ws://localhost:*')
expect(csp).toContain("frame-src https://bsky.app https://pdsmoover.com 'self'")
expect(csp).toContain(
"frame-src https://bsky.app https://pdsmoover.com https://www.youtube-nocookie.com/ 'self'",
)
expect(nuxt.options.routeRules['/**']?.headers).toEqual(
expect.objectContaining({
'Permissions-Policy': 'camera=()',
Expand Down Expand Up @@ -110,7 +112,9 @@ describe('security headers module', () => {
const csp = getCsp(nuxt)

expect(csp).not.toContain('ws://localhost:*')
expect(csp).not.toContain("frame-src https://bsky.app https://pdsmoover.com 'self'")
expect(csp).not.toContain(
"frame-src https://bsky.app https://pdsmoover.com https://www.youtube-nocookie.com/ 'self'",
)
expect(nuxt.options.routeRules['/__nuxt_devtools__/**']).toBeUndefined()
})
})
Loading