diff --git a/app/components/global/BlogPostWrapper.vue b/app/components/global/BlogPostWrapper.vue index 372be4ba67..d43de21b02 100644 --- a/app/components/global/BlogPostWrapper.vue +++ b/app/components/global/BlogPostWrapper.vue @@ -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' } : {}), }) @@ -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) diff --git a/app/components/global/BlueskyPostEmbed.client.vue b/app/components/global/BlueskyPostEmbed.client.vue index 8e767e7b8c..4787b502c8 100644 --- a/app/components/global/BlueskyPostEmbed.client.vue +++ b/app/components/global/BlueskyPostEmbed.client.vue @@ -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 @@ -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" > { /> + + +
diff --git a/app/pages/blog/release/0.8.md b/app/pages/blog/release/0.8.md new file mode 100644 index 0000000000..91def1d263 --- /dev/null +++ b/app/pages/blog/release/0.8.md @@ -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' +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 +--- + +# 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. + +
+ +
+ 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. +
+
+ +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. + +
+ Scatter plot of package traction vs. ergonomics, with delineated quadrants, showing seven UI component packages +
+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. + +
+
+ +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. + + + +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: + + + +## 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. + + + +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. + + + +We were also featured in the latest Igalia Chats episode discussing the history of npmx, the community, our ideas regarding funding, and more. + + + +## 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! diff --git a/modules/blog.ts b/modules/blog.ts index 26af619377..812deeb758 100644 --- a/modules/blog.ts +++ b/modules/blog.ts @@ -88,7 +88,7 @@ function resolveAuthors(authors: Author[], avatarMap: Map): Reso * Resolves Bluesky avatars at build time. */ async function loadBlogPosts(blogDir: string, imagesDir: string): Promise { - 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 }> = [] diff --git a/modules/security-headers.ts b/modules/security-headers.ts index e5b6b0dfaa..f32c838c48 100644 --- a/modules/security-headers.ts +++ b/modules/security-headers.ts @@ -55,6 +55,7 @@ export default defineNuxtModule({ const frameSrc = [ 'https://bsky.app', 'https://pdsmoover.com', + 'https://www.youtube-nocookie.com/', ...(isDevtoolsRuntime ? ["'self'"] : []), ].join(' ') @@ -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}`, diff --git a/public/blog/command-palette-fallback.png b/public/blog/command-palette-fallback.png new file mode 100644 index 0000000000..c28f5d5967 Binary files /dev/null and b/public/blog/command-palette-fallback.png differ diff --git a/public/blog/command-palette-versions.png b/public/blog/command-palette-versions.png new file mode 100644 index 0000000000..ba799316ab Binary files /dev/null and b/public/blog/command-palette-versions.png differ diff --git a/public/blog/og/release-0_8.png b/public/blog/og/release-0_8.png new file mode 100644 index 0000000000..7fcc754f6c Binary files /dev/null and b/public/blog/og/release-0_8.png differ diff --git a/public/blog/release/0.8/cmd-palette-demo-cover.png b/public/blog/release/0.8/cmd-palette-demo-cover.png new file mode 100644 index 0000000000..0f566f49da Binary files /dev/null and b/public/blog/release/0.8/cmd-palette-demo-cover.png differ diff --git a/public/blog/release/0.8/cmd-palette-demo.mp4 b/public/blog/release/0.8/cmd-palette-demo.mp4 new file mode 100644 index 0000000000..2ff882dcfe Binary files /dev/null and b/public/blog/release/0.8/cmd-palette-demo.mp4 differ diff --git a/public/blog/release/0.8/cmd-palette-search-fallback.png b/public/blog/release/0.8/cmd-palette-search-fallback.png new file mode 100644 index 0000000000..c28f5d5967 Binary files /dev/null and b/public/blog/release/0.8/cmd-palette-search-fallback.png differ diff --git a/public/blog/release/0.8/cmd-palette-semver.png b/public/blog/release/0.8/cmd-palette-semver.png new file mode 100644 index 0000000000..ba799316ab Binary files /dev/null and b/public/blog/release/0.8/cmd-palette-semver.png differ diff --git a/public/blog/release/0.8/quadrant-graph-example.png b/public/blog/release/0.8/quadrant-graph-example.png new file mode 100644 index 0000000000..d1db61b4cb Binary files /dev/null and b/public/blog/release/0.8/quadrant-graph-example.png differ diff --git a/shared/schemas/blog.ts b/shared/schemas/blog.ts index 3e6e5f3a96..4988e47746 100644 --- a/shared/schemas/blog.ts +++ b/shared/schemas/blog.ts @@ -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) */ @@ -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 diff --git a/test/unit/modules/security-headers.spec.ts b/test/unit/modules/security-headers.spec.ts index 2ba961c1fe..addf91a106 100644 --- a/test/unit/modules/security-headers.spec.ts +++ b/test/unit/modules/security-headers.spec.ts @@ -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=()', @@ -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() }) })