@natesena/blog-lib 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +293 -0
- package/dist/chunk-EG563RZL.js +475 -0
- package/dist/chunk-EG563RZL.js.map +1 -0
- package/dist/chunk-OPJV2ECE.js +122 -0
- package/dist/chunk-OPJV2ECE.js.map +1 -0
- package/dist/components/index.js +447 -0
- package/dist/components/index.js.map +1 -0
- package/dist/index-TnUz7zF0.d.ts +404 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.js +21 -0
- package/dist/index.js.map +1 -0
- package/dist/sdk/index.d.ts +2 -0
- package/dist/sdk/index.js +19 -0
- package/dist/sdk/index.js.map +1 -0
- package/dist/server/index.d.ts +131 -0
- package/dist/server/index.js +132 -0
- package/dist/server/index.js.map +1 -0
- package/package.json +93 -0
package/README.md
ADDED
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
# @natesena/blog-lib
|
|
2
|
+
|
|
3
|
+
Drop-in blog infrastructure for Next.js. Bring your own database, auth, and storage — use only what you need.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
npm install @natesena/blog-lib
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## What's included
|
|
12
|
+
|
|
13
|
+
| Import path | What you get | Required extras |
|
|
14
|
+
|---|---|---|
|
|
15
|
+
| `@natesena/blog-lib` | BlogSDK, PostgresAdapter, types | `@prisma/client` |
|
|
16
|
+
| `@natesena/blog-lib/components` | BlogPost, BlogPostList, BlogLayout, SEO, ImageUploader | Tailwind CSS |
|
|
17
|
+
| `@natesena/blog-lib/server` | Auth helpers, upload server actions | — |
|
|
18
|
+
|
|
19
|
+
Everything is **opt-in**. Install the core package, then add extras only when you need them.
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## 1. Minimal setup (SDK only)
|
|
24
|
+
|
|
25
|
+
This gives you full CRUD for posts, authors, and tags — no components, no auth, no storage.
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
npm install @natesena/blog-lib @prisma/client
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Copy the Prisma schema from `node_modules/@natesena/blog-lib/dist` or the repo's `src/db/schema.prisma` into your project, then:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
npx prisma migrate dev
|
|
35
|
+
npx prisma generate
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
```typescript
|
|
39
|
+
import { BlogSDK, PostgresAdapter } from '@natesena/blog-lib';
|
|
40
|
+
import { PrismaClient } from '@prisma/client';
|
|
41
|
+
|
|
42
|
+
const prisma = new PrismaClient();
|
|
43
|
+
const blog = new BlogSDK({ adapter: new PostgresAdapter(prisma) });
|
|
44
|
+
|
|
45
|
+
// Create an author
|
|
46
|
+
const author = await blog.authors.create({
|
|
47
|
+
name: 'Nate',
|
|
48
|
+
email: 'nate@example.com',
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
// Create a post
|
|
52
|
+
const post = await blog.posts.create({
|
|
53
|
+
title: 'Hello World',
|
|
54
|
+
content: '<p>My first post</p>',
|
|
55
|
+
authorId: author.id,
|
|
56
|
+
status: 'PUBLISHED',
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
// List published posts
|
|
60
|
+
const { posts, total } = await blog.posts.list({
|
|
61
|
+
status: 'PUBLISHED',
|
|
62
|
+
limit: 10,
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
// Get by slug
|
|
66
|
+
const found = await blog.posts.getBySlug('hello-world');
|
|
67
|
+
|
|
68
|
+
// Tags
|
|
69
|
+
const tag = await blog.tags.create({ name: 'TypeScript', slug: 'typescript' });
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
That's it. You have a working blog SDK. Everything below is optional.
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
## 2. Add React components
|
|
77
|
+
|
|
78
|
+
Pre-built server-rendered components styled with Tailwind CSS.
|
|
79
|
+
|
|
80
|
+
**Requires:** Tailwind CSS configured in your project.
|
|
81
|
+
|
|
82
|
+
```tsx
|
|
83
|
+
import { BlogPost, BlogPostList, BlogLayout } from '@natesena/blog-lib/components';
|
|
84
|
+
|
|
85
|
+
// Single post page
|
|
86
|
+
export default async function PostPage({ params }: { params: { slug: string } }) {
|
|
87
|
+
const post = await blog.posts.getBySlug(params.slug);
|
|
88
|
+
return <BlogPost post={post} />;
|
|
89
|
+
}
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
```tsx
|
|
93
|
+
// Post listing
|
|
94
|
+
export default async function BlogPage() {
|
|
95
|
+
const { posts, total } = await blog.posts.list({ status: 'PUBLISHED' });
|
|
96
|
+
return (
|
|
97
|
+
<BlogPostList
|
|
98
|
+
posts={posts}
|
|
99
|
+
totalPostCount={total}
|
|
100
|
+
onPostClick={(post) => router.push(`/blog/${post.slug}`)}
|
|
101
|
+
/>
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### SEO metadata
|
|
107
|
+
|
|
108
|
+
```typescript
|
|
109
|
+
import { generateBlogPostMetadata } from '@natesena/blog-lib/components';
|
|
110
|
+
|
|
111
|
+
export async function generateMetadata({ params }) {
|
|
112
|
+
const post = await blog.posts.getBySlug(params.slug);
|
|
113
|
+
return generateBlogPostMetadata({
|
|
114
|
+
postTitle: post.title,
|
|
115
|
+
postExcerpt: post.excerpt,
|
|
116
|
+
postCoverImageUrl: post.coverImage,
|
|
117
|
+
postSlug: post.slug,
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
---
|
|
123
|
+
|
|
124
|
+
## 3. Add image uploads (GCS)
|
|
125
|
+
|
|
126
|
+
Upload images directly to Google Cloud Storage via presigned URLs.
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
npm install @google-cloud/storage
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
```typescript
|
|
133
|
+
const blog = new BlogSDK({
|
|
134
|
+
adapter: new PostgresAdapter(prisma),
|
|
135
|
+
gcs: {
|
|
136
|
+
bucket: process.env.GCS_BUCKET!,
|
|
137
|
+
projectId: process.env.GCP_PROJECT_ID!,
|
|
138
|
+
keyFile: process.env.GCP_KEYFILE, // optional if using ADC
|
|
139
|
+
accessMode: 'public', // or 'private' for signed read URLs
|
|
140
|
+
},
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
// Get a presigned upload URL (use in your API route)
|
|
144
|
+
const { uploadUrl, publicUrl } = await blog.getImageUploadUrl(
|
|
145
|
+
'my-image.jpg',
|
|
146
|
+
'image/jpeg',
|
|
147
|
+
{ folder: 'blog/posts' },
|
|
148
|
+
);
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
### ImageUploader component (Next.js)
|
|
152
|
+
|
|
153
|
+
```tsx
|
|
154
|
+
import { ImageUploader } from '@natesena/blog-lib/components';
|
|
155
|
+
|
|
156
|
+
function PostForm() {
|
|
157
|
+
const [coverImage, setCoverImage] = useState('');
|
|
158
|
+
return <ImageUploader onUploadComplete={setCoverImage} uploadFolder="blog/posts" />;
|
|
159
|
+
}
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
### Private buckets
|
|
163
|
+
|
|
164
|
+
If your GCS bucket isn't publicly readable, set `accessMode: 'private'`. All read URLs will be v4 signed URLs instead of direct links:
|
|
165
|
+
|
|
166
|
+
```typescript
|
|
167
|
+
gcs: {
|
|
168
|
+
bucket: 'my-private-bucket',
|
|
169
|
+
projectId: 'my-project',
|
|
170
|
+
accessMode: 'private',
|
|
171
|
+
signedReadUrlExpiresInSeconds: 3600, // default: 1 hour
|
|
172
|
+
}
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
> **No GCS?** Skip this section entirely. The SDK works fine without it — you just won't have the `getImageUploadUrl` method or `ImageUploader` component.
|
|
176
|
+
|
|
177
|
+
---
|
|
178
|
+
|
|
179
|
+
## 4. Add auth
|
|
180
|
+
|
|
181
|
+
blog-lib has **no opinion on auth**. Use whatever you already have.
|
|
182
|
+
|
|
183
|
+
### Option A: Simple API key (for headless CMS APIs)
|
|
184
|
+
|
|
185
|
+
```env
|
|
186
|
+
CMS_API_KEY=your-secret-key
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
```typescript
|
|
190
|
+
import { checkApiKeyAuth } from '@natesena/blog-lib/server';
|
|
191
|
+
|
|
192
|
+
// In your API route:
|
|
193
|
+
export async function GET() {
|
|
194
|
+
const isAuthorized = await checkApiKeyAuth(); // reads x-api-key header
|
|
195
|
+
if (!isAuthorized) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
|
196
|
+
// ...
|
|
197
|
+
}
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
### Option B: Email-based permissions (any auth provider)
|
|
201
|
+
|
|
202
|
+
```env
|
|
203
|
+
CMS_ADMIN_EMAILS=admin@example.com,editor@example.com
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
```typescript
|
|
207
|
+
import { canAccessCMS } from '@natesena/blog-lib/server';
|
|
208
|
+
|
|
209
|
+
// After getting your user from your auth provider:
|
|
210
|
+
const hasAccess = canAccessCMS({ id: user.id, email: user.email }); // true/false
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
### Option C: Pluggable auth for upload actions
|
|
214
|
+
|
|
215
|
+
If you use the built-in upload server actions, wire in your auth:
|
|
216
|
+
|
|
217
|
+
```typescript
|
|
218
|
+
import { configureBlogAuth } from '@natesena/blog-lib/server';
|
|
219
|
+
|
|
220
|
+
configureBlogAuth({
|
|
221
|
+
getCurrentUser: async () => {
|
|
222
|
+
const session = await auth(); // your auth provider
|
|
223
|
+
if (!session?.user) return null;
|
|
224
|
+
return { id: session.user.id, email: session.user.email };
|
|
225
|
+
},
|
|
226
|
+
});
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
See `examples/auth/` for complete examples with **Clerk**, **Auth0**, **NextAuth.js v5**, and **API keys**.
|
|
230
|
+
|
|
231
|
+
> **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.
|
|
232
|
+
|
|
233
|
+
---
|
|
234
|
+
|
|
235
|
+
## Environment variables
|
|
236
|
+
|
|
237
|
+
Only set what you use:
|
|
238
|
+
|
|
239
|
+
```env
|
|
240
|
+
# Required: Database
|
|
241
|
+
DATABASE_URL=postgresql://user:password@localhost:5432/blog
|
|
242
|
+
|
|
243
|
+
# Optional: GCS image uploads
|
|
244
|
+
GCP_PROJECT_ID=your-project-id
|
|
245
|
+
GCS_BUCKET=your-bucket-name
|
|
246
|
+
GCP_KEYFILE=/path/to/service-account.json
|
|
247
|
+
|
|
248
|
+
# Optional: Auth
|
|
249
|
+
CMS_ADMIN_EMAILS=admin@example.com
|
|
250
|
+
CMS_API_KEY=your-secret-key
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
---
|
|
254
|
+
|
|
255
|
+
## Error handling
|
|
256
|
+
|
|
257
|
+
All SDK errors throw `BlogLibError` with a typed `code` field:
|
|
258
|
+
|
|
259
|
+
```typescript
|
|
260
|
+
import { BlogLibError } from '@natesena/blog-lib';
|
|
261
|
+
|
|
262
|
+
try {
|
|
263
|
+
await blog.posts.create({ title: '', content: '...', authorId: '...' });
|
|
264
|
+
} catch (error) {
|
|
265
|
+
if (error instanceof BlogLibError) {
|
|
266
|
+
switch (error.code) {
|
|
267
|
+
case 'VALIDATION_ERROR': // invalid input
|
|
268
|
+
case 'NOT_FOUND': // entity doesn't exist
|
|
269
|
+
case 'DUPLICATE': // slug/email already taken
|
|
270
|
+
case 'STORAGE_ERROR': // GCS operation failed
|
|
271
|
+
case 'AUTH_ERROR': // authentication/authorization failed
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
---
|
|
278
|
+
|
|
279
|
+
## Architecture
|
|
280
|
+
|
|
281
|
+
```
|
|
282
|
+
@natesena/blog-lib
|
|
283
|
+
├── index.ts → BlogSDK, PostgresAdapter, types
|
|
284
|
+
├── sdk/index.ts → SDK internals (posts, authors, tags, storage)
|
|
285
|
+
├── components/index.ts → BlogPost, BlogPostList, BlogLayout, SEO, ImageUploader
|
|
286
|
+
└── server/index.ts → Auth helpers, upload server actions
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
---
|
|
290
|
+
|
|
291
|
+
## License
|
|
292
|
+
|
|
293
|
+
MIT
|