npm package: @natesena/blog-lib
Description: Universal blog library for Next.js — SDK, Components, and CMS UI with pluggable auth
Author: Nate
Latest version: 0.4.0

# @natesena/blog-lib

Drop-in blog infrastructure for Next.js. Bring your own database, auth, and storage — use only what you need.

```bash
npm install @natesena/blog-lib
```

---

## What's included

| Import path | What you get | Required extras |
|---|---|---|
| `@natesena/blog-lib` | BlogSDK, PostgresAdapter, types | `@prisma/client` |
| `@natesena/blog-lib/components` | BlogPost, BlogPostList, BlogLayout, SEO, ImageUploader | Tailwind CSS |
| `@natesena/blog-lib/server` | Auth helpers, upload server actions | — |

Everything is **opt-in**. Install the core package, then add extras only when you need them.

---

## 1. Minimal setup (SDK only)

This gives you full CRUD for posts, authors, and tags — no components, no auth, no storage.

```bash
npm install @natesena/blog-lib @prisma/client
```

Copy the Prisma schema from `node_modules/@natesena/blog-lib/dist` or the repo's `src/db/schema.prisma` into your project, then:

```bash
npx prisma migrate dev
npx prisma generate
```

```typescript
import { BlogSDK, PostgresAdapter } from '@natesena/blog-lib';
import { PrismaClient } from '@prisma/client';

const prisma = new PrismaClient();
const blog = new BlogSDK({ adapter: new PostgresAdapter(prisma) });

// Create an author
const author = await blog.authors.create({
  name: 'Nate',
  email: 'nate@example.com',
});

// Create a post
const post = await blog.posts.create({
  title: 'Hello World',
  content: '<p>My first post</p>',
  authorId: author.id,
  status: 'PUBLISHED',
});

// List published posts
const { posts, total } = await blog.posts.list({
  status: 'PUBLISHED',
  limit: 10,
});

// Get by slug
const found = await blog.posts.getBySlug('hello-world');

// Tags
const tag = await blog.tags.create({ name: 'TypeScript', slug: 'typescript' });
```

That's it. You have a working blog SDK. Everything below is optional.

---

## 2. Add React components

Pre-built server-rendered components styled with Tailwind CSS.

**Requires:** Tailwind CSS configured in your project.

```tsx
import { BlogPost, BlogPostList, BlogLayout } from '@natesena/blog-lib/components';

// Single post page
export default async function PostPage({ params }: { params: { slug: string } }) {
  const post = await blog.posts.getBySlug(params.slug);
  return <BlogPost post={post} />;
}
```

```tsx
// Post listing
export default async function BlogPage() {
  const { posts, total } = await blog.posts.list({ status: 'PUBLISHED' });
  return (
    <BlogPostList
      posts={posts}
      totalPostCount={total}
      onPostClick={(post) => router.push(`/blog/${post.slug}`)}
    />
  );
}
```

### SEO metadata

```typescript
import { generateBlogPostMetadata } from '@natesena/blog-lib/components';

export async function generateMetadata({ params }) {
  const post = await blog.posts.getBySlug(params.slug);
  return generateBlogPostMetadata({
    postTitle: post.title,
    postExcerpt: post.excerpt,
    postCoverImageUrl: post.coverImage,
    postSlug: post.slug,
  });
}
```

---

## 3. Add image uploads (GCS)

Upload images directly to Google Cloud Storage via presigned URLs.

```bash
npm install @google-cloud/storage
```

```typescript
const blog = new BlogSDK({
  adapter: new PostgresAdapter(prisma),
  gcs: {
    bucket: process.env.GCS_BUCKET!,
    projectId: process.env.GCP_PROJECT_ID!,
    keyFile: process.env.GCP_KEYFILE, // optional if using ADC
    accessMode: 'public', // or 'private' for signed read URLs
  },
});

// Get a presigned upload URL (use in your API route)
const { uploadUrl, publicUrl } = await blog.getImageUploadUrl(
  'my-image.jpg',
  'image/jpeg',
  { folder: 'blog/posts' },
);
```

### ImageUploader component (Next.js)

```tsx
import { ImageUploader } from '@natesena/blog-lib/components';

function PostForm() {
  const [coverImage, setCoverImage] = useState('');
  return <ImageUploader onUploadComplete={setCoverImage} uploadFolder="blog/posts" />;
}
```

### Private buckets

If your GCS bucket isn't publicly readable, set `accessMode: 'private'`. All read URLs will be v4 signed URLs instead of direct links:

```typescript
gcs: {
  bucket: 'my-private-bucket',
  projectId: 'my-project',
  accessMode: 'private',
  signedReadUrlExpiresInSeconds: 3600, // default: 1 hour
}
```

> **No GCS?** Skip this section entirely. The SDK works fine without it — you just won't have the `getImageUploadUrl` method or `ImageUploader` component.

---

## 4. Add auth

blog-lib ships pre-built adapters for the most popular Next.js auth providers. One line of setup, zero glue code.

### NextAuth.js v5 (Auth.js)

```typescript
import { configureBlogAuthWithNextAuth } from '@natesena/blog-lib/server';
import { auth } from '@/auth'; // your Auth.js config

configureBlogAuthWithNextAuth(auth);
```

### Clerk

```bash
npm install @clerk/nextjs  # if not already installed
```

```typescript
import { configureBlogAuthWithClerk } from '@natesena/blog-lib/server';

configureBlogAuthWithClerk();
```

### Supabase Auth

```typescript
import { configureBlogAuthWithSupabase } from '@natesena/blog-lib/server';
import { createSupabaseServerClient } from '@/lib/supabase-server';

configureBlogAuthWithSupabase(() => createSupabaseServerClient());
```

### Custom (any provider)

If your auth provider isn't listed above, use `configureBlogAuth` directly:

```typescript
import { configureBlogAuth } from '@natesena/blog-lib/server';

configureBlogAuth({
  getCurrentUser: async () => {
    const session = await yourAuthProvider.getSession();
    if (!session?.user) return null;
    return { id: session.user.id, email: session.user.email };
  },
});
```

### Email-based CMS permissions

Control who can access CMS routes with an allowlist:

```env
CMS_ADMIN_EMAILS=admin@example.com,editor@example.com
```

```typescript
import { canAccessCMS } from '@natesena/blog-lib/server';

const hasAccess = canAccessCMS({ id: user.id, email: user.email }); // true/false
```

### API key auth (headless CMS)

For API-only setups without user sessions:

```env
CMS_API_KEY=your-secret-key
```

```typescript
import { checkApiKeyAuth } from '@natesena/blog-lib/server';

export async function GET() {
  const isAuthorized = await checkApiKeyAuth(); // reads x-api-key header
  if (!isAuthorized) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
  // ...
}
```

> **No auth?** Skip this section. The SDK methods have no auth built in — they're just database operations. Add auth at whatever layer makes sense for your app.

---

## Environment variables

Only set what you use:

```env
# Required: Database
DATABASE_URL=postgresql://user:password@localhost:5432/blog

# Optional: GCS image uploads
GCP_PROJECT_ID=your-project-id
GCS_BUCKET=your-bucket-name
GCP_KEYFILE=/path/to/service-account.json

# Optional: Auth
CMS_ADMIN_EMAILS=admin@example.com
CMS_API_KEY=your-secret-key
```

---

## Error handling

All SDK errors throw `BlogLibError` with a typed `code` field:

```typescript
import { BlogLibError } from '@natesena/blog-lib';

try {
  await blog.posts.create({ title: '', content: '...', authorId: '...' });
} catch (error) {
  if (error instanceof BlogLibError) {
    switch (error.code) {
      case 'VALIDATION_ERROR': // invalid input
      case 'NOT_FOUND':        // entity doesn't exist
      case 'DUPLICATE':        // slug/email already taken
      case 'STORAGE_ERROR':    // GCS operation failed
      case 'AUTH_ERROR':       // authentication/authorization failed
    }
  }
}
```

---

## Architecture

```
@natesena/blog-lib
├── index.ts              → BlogSDK, PostgresAdapter, types
├── sdk/index.ts          → SDK internals (posts, authors, tags, storage)
├── components/index.ts   → BlogPost, BlogPostList, BlogLayout, SEO, ImageUploader
└── server/index.ts       → Auth helpers, upload server actions
```

---

## License

MIT