npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@fydemy/cms

v1.0.7

Published

Simple file-based CMS for Next.js without database - with GitHub integration for production

Readme

@fydemy/cms

npm version License: MIT TypeScript Node.js

A minimal, secure, file-based CMS for Next.js without database requirements. Store content as markdown files with GitHub integration for production deployments.

Features

  • 📝 File-based Storage - Markdown files with frontmatter in /public/content
  • 🔐 Secure Authentication - Timing-safe password comparison, rate limiting, input validation
  • 🚀 Vercel Compatible - Deploy without any database setup
  • 🐙 GitHub Integration - Automatic file commits in production
  • 📦 Zero Config - Minimal setup required
  • 🎯 TypeScript First - Full type safety with comprehensive type definitions
  • Lightweight - Small bundle size (~30KB), minimal dependencies
  • 🛡️ Security Hardened - Built with security best practices

Installation

npm install @fydemy/cms
# or
pnpm add @fydemy/cms
# or
yarn add @fydemy/cms

Quick Start

1. Initialize the CMS

Run the initialization command in your Next.js App Router project:

npx fydemy-cms init

This command will automatically:

  • Create the content directory
  • Scaffold Admin UI pages (/app/admin)
  • Create API routes (/app/api/cms)
  • Create a .env.local.example file
  • Provide instructions for updating middleware.ts

2. Configure Environment

Copy .env.local.example to .env.local and set your credentials:

cp .env.local.example .env.local

Update variables in .env.local:

# Required for authentication
CMS_ADMIN_USERNAME=admin
CMS_ADMIN_PASSWORD=your_secure_password
CMS_SESSION_SECRET=your-secret-key-must-be-at-least-32-characters-long

# Optional: For production (GitHub integration)
GITHUB_TOKEN=ghp_your_github_token
GITHUB_REPO=username/repository
GITHUB_BRANCH=main

Security Note: Use strong passwords and keep CMS_SESSION_SECRET at least 32 characters long.

3. Read Content in Your App

import { getMarkdownContent } from "@fydemy/cms";

export default async function BlogPost({
  params,
}: {
  params: { slug: string };
}) {
  const post = await getMarkdownContent(`${params.slug}.md`);

  return (
    <article>
      <h1>{post.data.title}</h1>
      <p>{post.data.description}</p>
      <div>{post.content}</div>
    </article>
  );
}

Security Features

Built-in Security

  • Timing-Safe Authentication: Uses crypto.timingSafeEqual to prevent timing attacks
  • Rate Limiting: 5 login attempts per 15 minutes per IP address
  • Input Validation: All inputs validated and sanitized
  • Path Validation: Prevents directory traversal attacks
  • File Size Limits: Default 10MB maximum file size
  • Secure Sessions: httpOnly, sameSite, and secure cookies in production
  • No Username Enumeration: Generic error messages

Security Best Practices

  1. Strong Credentials: Use strong, unique passwords for CMS_ADMIN_PASSWORD
  2. Secret Management: Keep CMS_SESSION_SECRET at least 32 characters
  3. GitHub Token Security: Use minimal permissions (only repo scope)
  4. HTTPS Only: Always use HTTPS in production
  5. Regular Updates: Keep dependencies up to date
  6. Environment Variables: Never commit .env files

For more security information, see SECURITY.md.

API Reference

Content Management

// Read markdown file
const content = await getMarkdownContent("blog/post.md");
// Returns: { data: {...}, content: "..." }

// Write markdown file
await saveMarkdownContent(
  "blog/post.md",
  { title: "My Post", date: "2024-01-01" },
  "# Hello World",
);

// Delete file
await deleteMarkdownContent("blog/post.md");

// List files
const files = await listMarkdownFiles("blog");
// Returns: ['blog/post1.md', 'blog/post2.md']

// Check if file exists
const exists = await markdownFileExists("blog/post.md");

Parsing Utilities

import { parseMarkdown, stringifyMarkdown } from "@fydemy/cms";

// Parse markdown string
const { data, content } = parseMarkdown(rawMarkdown);

// Convert to markdown
const markdown = stringifyMarkdown({ title: "Post" }, "Content here");

Authentication

import { validateCredentials, createSession } from "@fydemy/cms";

// Validate credentials
const isValid = validateCredentials("admin", "password");

// Create session (returns JWT)
const token = await createSession("admin");

Validation Utilities

import {
  validateFilePath,
  validateUsername,
  validatePassword,
  sanitizeFrontmatter,
} from "@fydemy/cms";

// Validate file path (prevents directory traversal)
const safePath = validateFilePath("blog/post.md");

// Validate username
validateUsername("admin"); // throws if invalid

// Sanitize frontmatter data
const safe = sanitizeFrontmatter({ title: "Test", script: "<script>" });

Storage

Development

Files are stored locally in /public/content directory.

Production

You have two options for production storage:

Option 1: GitHub Storage (Default)

When NODE_ENV=production and GITHUB_TOKEN is set, all file operations are performed via GitHub API, creating commits directly to your repository.

Option 2: Cloudflare R2 Storage

When Cloudflare R2 credentials are set, files are stored in your R2 bucket. This is ideal for high-traffic sites and provides better performance.

See CLOUDFLARE_R2_SETUP.md for detailed setup instructions.

Environment Variables

Authentication (Required)

| Variable | Required | Description | | -------------------- | -------- | ------------------------- | | CMS_ADMIN_USERNAME | Yes | Admin username | | CMS_ADMIN_PASSWORD | Yes | Admin password | | CMS_SESSION_SECRET | Yes | JWT secret (min 32 chars) |

Storage: GitHub (Optional)

| Variable | Required | Description | | --------------- | ---------- | ------------------------------- | | GITHUB_TOKEN | Production | GitHub personal access token | | GITHUB_REPO | Production | Repository (format: owner/repo) | | GITHUB_BRANCH | Production | Branch name (default: main) |

Storage: Cloudflare R2 (Optional)

| Variable | Required | Description | | ------------------------------ | -------- | -------------------------- | | CLOUDFLARE_ACCOUNT_ID | Yes | Your Cloudflare Account ID | | CLOUDFLARE_ACCESS_KEY_ID | Yes | R2 API Access Key ID | | CLOUDFLARE_SECRET_ACCESS_KEY | Yes | R2 API Secret Access Key | | NEXT_PUBLIC_R2_PUBLIC_URL | Yes | Public URL for R2 bucket | | R2_BUCKET_NAME | Yes | Name of your R2 bucket |

GitHub Setup

  1. Create a GitHub Personal Access Token with repo permissions
  2. Add the token to your environment variables
  3. Deploy to Vercel and configure the environment variables

Cloudflare R2 Setup

  1. Create an R2 bucket in your Cloudflare dashboard
  2. Generate API tokens with read/write permissions
  3. Configure your bucket's public access settings
  4. Add the credentials to your environment variables

For detailed instructions, see CLOUDFLARE_R2_SETUP.md.

FAQ

Is this suitable for production?

Yes! The package includes security hardening, rate limiting, and has been tested for production use. Make sure to follow security best practices.

Can I use this with other frameworks?

This package is designed for Next.js App Router (13+). For other frameworks, you can use the core utilities but will need to implement your own API routes.

How do I customize the file size limit?

import { MAX_FILE_SIZE } from "@fydemy/cms";
// Default is 10MB, you can check this constant

To change it, you'll need to implement your own validation layer.

Does it support images?

Yes! The package includes file upload functionality. Images can be uploaded and stored in /public/uploads (local) or via GitHub API (production).

How do I backup my content?

Since content is stored in your GitHub repository (in production), it's automatically backed up with full version history. In development, the /public/content directory can be committed to git.

What about rate limiting in production?

The built-in rate limiter is memory-based and resets on server restart. For production with multiple instances, consider implementing Redis-based rate limiting.

Can I add more admin users?

Currently, the package supports a single admin user via environment variables. For multi-user support, you'd need to implement a custom authentication layer.

Example Admin UI

Check the /apps/dev directory in this repository for a complete example with:

  • Login page
  • Admin dashboard
  • File editor
  • File management

Troubleshooting

"CMS_SESSION_SECRET must be at least 32 characters"

Make sure your session secret is long enough. Generate a secure random string:

node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"

Rate limiting not working across restarts

The rate limiter is in-memory. For persistent rate limiting, implement Redis storage.

GitHub API rate limits

GitHub API has rate limits. For high-traffic sites, consider caching content or using a CDN.

License

MIT

Contributing

Contributions welcome! This is a minimal CMS focused on simplicity and maintainability.

Please report security vulnerabilities privately to [email protected] or via GitHub security advisories.

Links