@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 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