
Research
/Security News
PolinRider Spreads Through Compromised GitHub Accounts and Packagist
Operators behind PolinRider used a compromised GitHub account to plant malware in four development versions of a Packagist package with 700,000+ downloads.
Type-safe content collections for Next.js.
Like Astro content collections, but for Next.js -- define schemas, drop in Markdown/MDX/JSON/YAML files, and query them with full TypeScript inference.
npm install pressroom
// lib/pressroom.ts
import { createPressroom } from "pressroom";
export const pressroom = createPressroom({
posts: {
schema: {
title: "string",
date: "date",
draft: { type: "boolean", required: false, default: false },
tags: "array",
},
directory: "./content/posts",
format: "md",
},
authors: {
schema: {
name: { type: "string", required: true },
avatar: "image",
bio: "string",
},
directory: "./content/authors",
format: "json",
},
});
<!-- content/posts/hello-world.md -->
---
title: Hello World
date: 2026-01-15
tags:
- intro
- blog
---
Welcome to my blog. This is the first post.
// app/blog/page.tsx
import { pressroom } from "@/lib/pressroom";
export default async function BlogIndex() {
const posts = await pressroom
.collection("posts")
.query()
.where("draft", "eq", false)
.sort("date", "desc")
.exec();
return (
<ul>
{posts.map((post) => (
<li key={post.slug}>
<a href={`/blog/${post.slug}`}>{post.data.title}</a>
</li>
))}
</ul>
);
}
Each field in a schema can be a shorthand type string or a full definition:
const schema = {
// Shorthand
title: "string",
count: "number",
published: "boolean",
createdAt: "date",
tags: "array",
metadata: "object",
cover: "image",
urlSlug: "slug",
// Full definition with validation
email: {
type: "string",
required: true,
validate: (v) => String(v).includes("@") || "Must be a valid email",
},
// With defaults
draft: {
type: "boolean",
required: false,
default: false,
},
};
| Type | TypeScript | Description |
|---|---|---|
string | string | Text value |
number | number | Numeric value (auto-coerced) |
boolean | boolean | True/false (auto-coerced) |
date | Date | Date object (auto-coerced) |
array | unknown[] | Array of values |
object | Record | Nested object |
image | string | Image path/URL |
slug | string | URL-safe identifier |
Set format in your collection config:
"md" (default) -- Markdown with YAML frontmatter"mdx" -- MDX with YAML frontmatter"json" -- JSON files (entire file is data, no body content)"yaml" -- YAML files (entire file is data, no body content)const collection = pressroom.collection("posts");
// Get all entries
const all = await collection.getAll();
// Get by slug (filename without extension)
const post = await collection.getBySlug("hello-world");
// With options
const recent = await collection.getAll({
sort: { field: "date", order: "desc" },
limit: 10,
});
const results = await pressroom
.collection("posts")
.query()
.where("draft", "eq", false)
.where("tags", "contains", "tutorial")
.sort("date", "desc")
.limit(5)
.offset(10)
.exec();
| Operator | Description |
|---|---|
eq | Equal |
neq | Not equal |
gt | Greater than |
gte | Greater than or equal |
lt | Less than |
lte | Less than or equal |
in | Value is in array |
contains | String/array contains |
import { Markdown, ContentList, PressroomProvider, usePressroom } from "pressroom/react";
Renders a markdown string to React elements. Supports headings, paragraphs, bold, italic, inline code, links, images, code blocks, and lists.
<Markdown content={post.content} className="prose" />
<ContentList
entries={posts}
renderItem={(post) => (
<article key={post.slug}>
<h2>{post.data.title}</h2>
</article>
)}
/>
// In a client layout
<PressroomProvider pressroom={pressroom}>
<App />
</PressroomProvider>
// In a child component
function PostList() {
const pr = usePressroom();
// ...
}
import { generateStaticParams, getStaticContent, createContentHandler } from "pressroom/next";
[slug] routes)// app/blog/[slug]/page.tsx
import { pressroom } from "@/lib/pressroom";
import { generateStaticParams as genParams } from "pressroom/next";
export async function generateStaticParams() {
return genParams(pressroom, "posts");
}
import { getStaticContent } from "pressroom/next";
export default async function PostPage({ params }: { params: { slug: string } }) {
const post = await getStaticContent(pressroom, "posts", params.slug);
if (!post) return <div>Not found</div>;
return <article>{post.data.title}</article>;
}
// app/api/content/[collection]/route.ts
import { createContentHandler } from "pressroom/next";
import { pressroom } from "@/lib/pressroom";
export const GET = createContentHandler(pressroom);
Query via URL params:
GET /api/content/posts?sort=date&order=desc&limit=10
GET /api/content/posts?slug=hello-world
GET /api/content/posts?where.draft.eq=false
your-project/
├── content/
│ ├── posts/
│ │ ├── hello-world.md
│ │ └── second-post.md
│ └── authors/
│ ├── alice.json
│ └── bob.json
├── lib/
│ └── pressroom.ts # createPressroom config
├── app/
│ ├── blog/
│ │ ├── page.tsx # List page
│ │ └── [slug]/
│ │ └── page.tsx # Detail page
│ └── api/
│ └── content/
│ └── [collection]/
│ └── route.ts # Optional API
└── package.json
MIT
FAQs
Type-safe content collections for Next.js.
The npm package pressroom receives a total of 1 weekly downloads. As such, pressroom popularity was classified as not popular.
We found that pressroom demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.

Research
/Security News
Operators behind PolinRider used a compromised GitHub account to plant malware in four development versions of a Packagist package with 700,000+ downloads.

Security News
GitHub Actions now supports cache-mode, a least-privilege control on the Actions cache aimed at the cache poisoning technique behind recent compromises.

Company News
Allow myself to introduce... myself.