autoblogger 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,164 @@
1
+ # Autoblogger
2
+
3
+ A headless CMS with AI writing tools, WYSIWYG editor, and RSS auto-draft for Next.js.
4
+
5
+ ## Features
6
+
7
+ - **AI Writing** - Generate and edit essays with Claude or GPT
8
+ - **WYSIWYG Editor** - Tiptap-based editor with markdown support
9
+ - **RSS Auto-Draft** - Subscribe to feeds and auto-generate drafts
10
+ - **Custom Fields** - Extend posts with site-specific fields
11
+ - **WYSIWYG Parity** - Editor matches your public page styling
12
+
13
+ ## Installation
14
+
15
+ ```bash
16
+ npm install autoblogger
17
+ ```
18
+
19
+ ## Quick Start
20
+
21
+ ### 1. Copy and merge schema
22
+
23
+ ```bash
24
+ cp node_modules/autoblogger/prisma/schema.prisma ./prisma/
25
+ npx prisma migrate dev --name add-autoblogger
26
+ ```
27
+
28
+ ### 2. Configure CMS
29
+
30
+ ```typescript
31
+ // lib/cms.ts
32
+ import { createAutoblogger } from 'autoblogger'
33
+ import { auth } from '@/lib/auth'
34
+ import { prisma } from '@/lib/db'
35
+
36
+ export const cms = createAutoblogger({
37
+ prisma,
38
+
39
+ auth: {
40
+ getSession: auth,
41
+ isAdmin: (session) => session?.user?.role === 'admin',
42
+ canPublish: (session) => ['admin', 'writer'].includes(session?.user?.role ?? ''),
43
+ },
44
+
45
+ ai: {
46
+ anthropicKey: process.env.ANTHROPIC_API_KEY,
47
+ openaiKey: process.env.OPENAI_API_KEY,
48
+ },
49
+
50
+ storage: {
51
+ upload: async (file) => {
52
+ // Your upload implementation
53
+ return { url: 'https://...' }
54
+ }
55
+ },
56
+
57
+ styles: {
58
+ container: 'max-w-2xl mx-auto px-6',
59
+ title: 'text-2xl font-bold',
60
+ prose: 'prose dark:prose-invert max-w-none',
61
+ },
62
+ })
63
+
64
+ export const cmsStyles = cms.config.styles
65
+ ```
66
+
67
+ ### 3. Mount API
68
+
69
+ ```typescript
70
+ // app/api/cms/[...path]/route.ts
71
+ import { cms } from '@/lib/cms'
72
+ import { createAPIHandler } from 'autoblogger'
73
+
74
+ const handler = createAPIHandler(cms, { basePath: '/api/cms' })
75
+
76
+ export const GET = handler
77
+ export const POST = handler
78
+ export const PATCH = handler
79
+ export const DELETE = handler
80
+ ```
81
+
82
+ ### 4. Mount Dashboard
83
+
84
+ ```typescript
85
+ // app/writer/[[...path]]/page.tsx
86
+ 'use client'
87
+
88
+ import { AutobloggerDashboard } from 'autoblogger/ui'
89
+ import { cmsStyles } from '@/lib/cms'
90
+
91
+ export default function WriterPage() {
92
+ return (
93
+ <AutobloggerDashboard
94
+ basePath="/writer"
95
+ apiBasePath="/api/cms"
96
+ styles={cmsStyles}
97
+ />
98
+ )
99
+ }
100
+ ```
101
+
102
+ ### 5. Add Tailwind preset
103
+
104
+ ```javascript
105
+ // tailwind.config.js
106
+ module.exports = {
107
+ presets: [require('autoblogger/styles/preset')],
108
+ content: [
109
+ './app/**/*.{js,ts,jsx,tsx}',
110
+ './node_modules/autoblogger/dist/**/*.{js,jsx}',
111
+ ],
112
+ }
113
+ ```
114
+
115
+ ### 6. Use in public pages
116
+
117
+ ```typescript
118
+ // app/blog/[slug]/page.tsx
119
+ import { cms } from '@/lib/cms'
120
+ import { renderMarkdown, getSeoValues } from 'autoblogger'
121
+
122
+ export default async function PostPage({ params }) {
123
+ const post = await cms.posts.findBySlug(params.slug)
124
+
125
+ return (
126
+ <article>
127
+ <h1>{post.title}</h1>
128
+ <div dangerouslySetInnerHTML={{ __html: renderMarkdown(post.markdown) }} />
129
+ </article>
130
+ )
131
+ }
132
+
133
+ export async function generateMetadata({ params }) {
134
+ const post = await cms.posts.findBySlug(params.slug)
135
+ const seo = getSeoValues(post)
136
+ return { title: seo.title, description: seo.description }
137
+ }
138
+ ```
139
+
140
+ ## Custom Fields
141
+
142
+ Add site-specific fields to posts:
143
+
144
+ ```typescript
145
+ // app/writer/[[...path]]/page.tsx
146
+ import { AutobloggerDashboard } from 'autoblogger/ui'
147
+ import { MyCustomField } from '@/components/MyCustomField'
148
+
149
+ export default function WriterPage() {
150
+ return (
151
+ <AutobloggerDashboard
152
+ basePath="/writer"
153
+ apiBasePath="/api/cms"
154
+ fields={[
155
+ { name: 'customData', label: 'Custom', component: MyCustomField, position: 'footer' }
156
+ ]}
157
+ />
158
+ )
159
+ }
160
+ ```
161
+
162
+ ## License
163
+
164
+ MIT
@@ -0,0 +1,431 @@
1
+ import { P as Post } from './seo-DUb5WwP3.mjs';
2
+ export { A as AISettings, C as Comment, N as NewsItem, a as PostTag, R as Revision, T as Tag, b as TopicSubscription, g as getSeoValues } from './seo-DUb5WwP3.mjs';
3
+ export { htmlToMarkdown, parseMarkdown, renderMarkdown } from './lib/markdown.mjs';
4
+ import { Mark, Editor } from '@tiptap/core';
5
+ import { ComponentType } from 'react';
6
+ import 'marked';
7
+
8
+ interface PostHooks {
9
+ beforePublish?: (post: Post) => Promise<void>;
10
+ afterSave?: (post: Post) => Promise<void>;
11
+ }
12
+ interface CreatePostInput {
13
+ title: string;
14
+ subtitle?: string;
15
+ slug?: string;
16
+ markdown?: string;
17
+ status?: string;
18
+ [key: string]: unknown;
19
+ }
20
+ interface UpdatePostInput {
21
+ title?: string;
22
+ subtitle?: string;
23
+ slug?: string;
24
+ markdown?: string;
25
+ status?: string;
26
+ publishedAt?: Date;
27
+ [key: string]: unknown;
28
+ }
29
+ declare function createPostsData(prisma: any, hooks?: PostHooks): {
30
+ count(where?: {
31
+ status?: string;
32
+ }): Promise<any>;
33
+ findPublished(): Promise<any>;
34
+ findBySlug(slug: string): Promise<any>;
35
+ findById(id: string): Promise<any>;
36
+ findDrafts(): Promise<any>;
37
+ findAll(options?: {
38
+ status?: string;
39
+ orderBy?: any;
40
+ skip?: number;
41
+ take?: number;
42
+ includeRevisionCount?: boolean;
43
+ }): Promise<any>;
44
+ create(data: CreatePostInput): Promise<any>;
45
+ update(id: string, data: UpdatePostInput): Promise<any>;
46
+ delete(id: string): Promise<any>;
47
+ getPreviewUrl(id: string, basePath?: string): Promise<string>;
48
+ findByPreviewToken(token: string): Promise<any>;
49
+ };
50
+
51
+ /**
52
+ * Comments data layer for autoblogger.
53
+ * Supports both public blog comments (simple) and editor comments (with quotedText, replies, resolve).
54
+ */
55
+ interface CommentsConfig {
56
+ mode?: 'authenticated' | 'public' | 'disabled';
57
+ }
58
+ interface EditorComment {
59
+ id: string;
60
+ postId: string;
61
+ userId: string;
62
+ quotedText: string;
63
+ content: string;
64
+ parentId: string | null;
65
+ resolved: boolean;
66
+ createdAt: Date;
67
+ updatedAt: Date;
68
+ user: {
69
+ id: string;
70
+ name: string | null;
71
+ email: string;
72
+ };
73
+ replies?: EditorComment[];
74
+ }
75
+ interface CreateEditorCommentInput {
76
+ postId: string;
77
+ quotedText: string;
78
+ content: string;
79
+ parentId?: string;
80
+ }
81
+ interface CreatePublicCommentInput {
82
+ postId: string;
83
+ content: string;
84
+ authorId?: string;
85
+ authorName?: string;
86
+ authorEmail?: string;
87
+ }
88
+ declare function createCommentsData(prisma: any, config?: CommentsConfig): {
89
+ count(): Promise<any>;
90
+ findByPost(postId: string): Promise<any>;
91
+ findAll(options?: {
92
+ postId?: string;
93
+ approved?: boolean;
94
+ page?: number;
95
+ limit?: number;
96
+ }): Promise<{
97
+ data: any;
98
+ total: any;
99
+ page: number;
100
+ totalPages: number;
101
+ }>;
102
+ create(data: CreatePublicCommentInput): Promise<any>;
103
+ approve(id: string): Promise<any>;
104
+ delete(id: string): Promise<any>;
105
+ getMode(): "authenticated" | "public" | "disabled";
106
+ /**
107
+ * Find all editor comments for a post with nested replies.
108
+ */
109
+ findEditorComments(postId: string, userId?: string): Promise<EditorComment[]>;
110
+ /**
111
+ * Create an editor comment (with quotedText and optional parentId for replies).
112
+ */
113
+ createEditorComment(postId: string, userId: string, data: CreateEditorCommentInput): Promise<EditorComment>;
114
+ /**
115
+ * Update a comment's content.
116
+ */
117
+ updateEditorComment(commentId: string, content: string, userId?: string): Promise<EditorComment>;
118
+ /**
119
+ * Soft delete a comment.
120
+ */
121
+ deleteEditorComment(commentId: string): Promise<void>;
122
+ /**
123
+ * Toggle resolved status.
124
+ */
125
+ toggleResolve(commentId: string): Promise<EditorComment>;
126
+ /**
127
+ * Resolve all open comments for a post.
128
+ */
129
+ resolveAll(postId: string): Promise<{
130
+ resolved: number;
131
+ }>;
132
+ };
133
+
134
+ declare function createTagsData(prisma: any): {
135
+ findAll(): Promise<any>;
136
+ findAllWithCounts(): Promise<any>;
137
+ count(): Promise<any>;
138
+ findById(id: string): Promise<any>;
139
+ findByName(name: string): Promise<any>;
140
+ create(name: string): Promise<any>;
141
+ update(id: string, name: string): Promise<any>;
142
+ delete(id: string): Promise<any>;
143
+ addToPost(postId: string, tagId: string): Promise<any>;
144
+ removeFromPost(postId: string, tagId: string): Promise<any>;
145
+ getPostTags(postId: string): Promise<any>;
146
+ };
147
+
148
+ declare function createRevisionsData(prisma: any): {
149
+ findAll(options?: {
150
+ postId?: string;
151
+ skip?: number;
152
+ take?: number;
153
+ }): Promise<any>;
154
+ count(where?: {
155
+ postId?: string;
156
+ }): Promise<any>;
157
+ findByPost(postId: string): Promise<any>;
158
+ findById(id: string): Promise<any>;
159
+ create(postId: string, data: {
160
+ title?: string;
161
+ subtitle?: string;
162
+ markdown: string;
163
+ }): Promise<any>;
164
+ restore(revisionId: string): Promise<any>;
165
+ compare(revisionId1: string, revisionId2: string): Promise<{
166
+ older: any;
167
+ newer: any;
168
+ }>;
169
+ pruneOldest(postId: string, keepCount: number): Promise<any>;
170
+ delete(id: string): Promise<any>;
171
+ };
172
+
173
+ declare function createAISettingsData(prisma: any): {
174
+ get(): Promise<any>;
175
+ update(data: {
176
+ rules?: string;
177
+ chatRules?: string;
178
+ rewriteRules?: string;
179
+ autoDraftRules?: string;
180
+ planRules?: string;
181
+ defaultModel?: string;
182
+ autoDraftWordCount?: number;
183
+ generateTemplate?: string | null;
184
+ chatTemplate?: string | null;
185
+ rewriteTemplate?: string | null;
186
+ autoDraftTemplate?: string | null;
187
+ planTemplate?: string | null;
188
+ expandPlanTemplate?: string | null;
189
+ }): Promise<any>;
190
+ };
191
+
192
+ interface CreateTopicInput {
193
+ name: string;
194
+ keywords?: string[];
195
+ rssFeeds?: string[];
196
+ isActive?: boolean;
197
+ useKeywordFilter?: boolean;
198
+ frequency?: string;
199
+ maxPerPeriod?: number;
200
+ essayFocus?: string;
201
+ }
202
+ interface UpdateTopicInput {
203
+ name?: string;
204
+ keywords?: string[];
205
+ rssFeeds?: string[];
206
+ isActive?: boolean;
207
+ useKeywordFilter?: boolean;
208
+ frequency?: string;
209
+ maxPerPeriod?: number;
210
+ essayFocus?: string;
211
+ lastRunAt?: Date;
212
+ }
213
+ declare function createTopicsData(prisma: any): {
214
+ findAll(): Promise<any>;
215
+ count(): Promise<any>;
216
+ findActive(): Promise<any>;
217
+ findById(id: string): Promise<any>;
218
+ create(data: CreateTopicInput): Promise<any>;
219
+ update(id: string, data: UpdateTopicInput): Promise<any>;
220
+ delete(id: string): Promise<any>;
221
+ markRun(id: string): Promise<any>;
222
+ };
223
+
224
+ interface CreateNewsItemInput {
225
+ topicId: string;
226
+ url: string;
227
+ title: string;
228
+ summary?: string;
229
+ publishedAt?: Date;
230
+ }
231
+ declare function createNewsItemsData(prisma: any): {
232
+ findPending(): Promise<any>;
233
+ findByTopic(topicId: string): Promise<any>;
234
+ findById(id: string): Promise<any>;
235
+ create(data: CreateNewsItemInput): Promise<any>;
236
+ skip(id: string): Promise<any>;
237
+ markGenerated(id: string, postId: string): Promise<any>;
238
+ delete(id: string): Promise<any>;
239
+ generateDraft(id: string, createPost: (data: any) => Promise<any>): Promise<any>;
240
+ };
241
+
242
+ interface CreateUserInput {
243
+ email: string;
244
+ name?: string;
245
+ role?: string;
246
+ }
247
+ interface UpdateUserInput {
248
+ name?: string;
249
+ role?: string;
250
+ }
251
+ declare function createUsersData(prisma: any): {
252
+ count(): Promise<any>;
253
+ findAll(): Promise<any>;
254
+ findById(id: string): Promise<any>;
255
+ findByEmail(email: string): Promise<any>;
256
+ create(data: CreateUserInput): Promise<any>;
257
+ update(id: string, data: UpdateUserInput): Promise<any>;
258
+ delete(id: string): Promise<any>;
259
+ };
260
+
261
+ interface Session {
262
+ user?: {
263
+ id?: string;
264
+ email?: string;
265
+ name?: string;
266
+ role?: string;
267
+ [key: string]: unknown;
268
+ };
269
+ [key: string]: unknown;
270
+ }
271
+ interface StylesConfig {
272
+ container?: string;
273
+ title?: string;
274
+ subtitle?: string;
275
+ byline?: string;
276
+ prose?: string;
277
+ }
278
+ interface AutobloggerServerConfig {
279
+ prisma: unknown;
280
+ auth: {
281
+ getSession: () => Promise<Session | null>;
282
+ isAdmin: (session: Session | null) => boolean;
283
+ canPublish: (session: Session | null) => boolean;
284
+ };
285
+ ai?: {
286
+ anthropicKey?: string;
287
+ openaiKey?: string;
288
+ };
289
+ storage?: {
290
+ upload: (file: File) => Promise<{
291
+ url: string;
292
+ }>;
293
+ };
294
+ comments?: {
295
+ mode: 'authenticated' | 'public' | 'disabled';
296
+ };
297
+ styles?: StylesConfig;
298
+ hooks?: {
299
+ beforePublish?: (post: Post) => Promise<void>;
300
+ afterSave?: (post: Post) => Promise<void>;
301
+ };
302
+ }
303
+ interface AutobloggerServer {
304
+ config: AutobloggerServerConfig & {
305
+ styles: Required<StylesConfig>;
306
+ };
307
+ posts: ReturnType<typeof createPostsData>;
308
+ comments: ReturnType<typeof createCommentsData>;
309
+ tags: ReturnType<typeof createTagsData>;
310
+ revisions: ReturnType<typeof createRevisionsData>;
311
+ aiSettings: ReturnType<typeof createAISettingsData>;
312
+ topics: ReturnType<typeof createTopicsData>;
313
+ newsItems: ReturnType<typeof createNewsItemsData>;
314
+ users: ReturnType<typeof createUsersData>;
315
+ }
316
+ declare function createAutoblogger(config: AutobloggerServerConfig): AutobloggerServer;
317
+
318
+ interface APIHandlerOptions {
319
+ basePath?: string;
320
+ onMutate?: (type: string, data: unknown) => Promise<void>;
321
+ }
322
+ type NextRequest = Request & {
323
+ nextUrl: URL;
324
+ };
325
+ declare function createAPIHandler(cms: AutobloggerServer, options?: APIHandlerOptions): (req: NextRequest) => Promise<Response>;
326
+
327
+ interface SchemaValidationResult {
328
+ valid: boolean;
329
+ missingTables: string[];
330
+ }
331
+ declare function validateSchema(prisma: unknown): Promise<SchemaValidationResult>;
332
+
333
+ /**
334
+ * Format a date for display
335
+ */
336
+ declare function formatDate(date: Date | string, options?: Intl.DateTimeFormatOptions): string;
337
+ /**
338
+ * Truncate text to a maximum length
339
+ */
340
+ declare function truncate(text: string, maxLength: number): string;
341
+
342
+ /**
343
+ * Comment types and client-side API helpers for the editor commenting system.
344
+ * Used for collaborative inline comments on posts.
345
+ */
346
+ interface CommentUser {
347
+ id: string;
348
+ name: string | null;
349
+ email: string;
350
+ }
351
+ interface CommentWithUser {
352
+ id: string;
353
+ postId: string;
354
+ userId: string;
355
+ quotedText: string;
356
+ content: string;
357
+ parentId: string | null;
358
+ resolved: boolean;
359
+ createdAt: string;
360
+ updatedAt: string;
361
+ user: CommentUser;
362
+ replies?: CommentWithUser[];
363
+ }
364
+ interface CreateCommentData {
365
+ quotedText: string;
366
+ content: string;
367
+ parentId?: string;
368
+ }
369
+ interface SelectionState {
370
+ text: string;
371
+ from: number;
372
+ to: number;
373
+ hasExistingComment?: boolean;
374
+ }
375
+ declare function canDeleteComment(comment: CommentWithUser, currentUserEmail: string, isAdmin: boolean): boolean;
376
+ declare function canEditComment(comment: CommentWithUser, currentUserEmail: string): boolean;
377
+ declare function createCommentsClient(apiBasePath?: string): {
378
+ fetchComments(postId: string): Promise<CommentWithUser[]>;
379
+ createComment(postId: string, data: CreateCommentData): Promise<CommentWithUser>;
380
+ updateComment(postId: string, commentId: string, content: string): Promise<CommentWithUser>;
381
+ deleteComment(postId: string, commentId: string): Promise<void>;
382
+ toggleResolve(postId: string, commentId: string): Promise<CommentWithUser>;
383
+ resolveAllComments(postId: string): Promise<{
384
+ resolved: number;
385
+ }>;
386
+ };
387
+
388
+ interface CommentMarkOptions {
389
+ onCommentClick?: (commentId: string) => void;
390
+ }
391
+ declare module '@tiptap/core' {
392
+ interface Commands<ReturnType> {
393
+ comment: {
394
+ setComment: (commentId: string) => ReturnType;
395
+ unsetComment: (commentId: string) => ReturnType;
396
+ };
397
+ }
398
+ }
399
+ declare const CommentMark: Mark<CommentMarkOptions, any>;
400
+ /**
401
+ * Apply comment mark to the current selection
402
+ */
403
+ declare function addCommentMark(editor: Editor, commentId: string, from: number, to: number): void;
404
+ /**
405
+ * Remove comment mark from the document
406
+ */
407
+ declare function removeCommentMark(editor: Editor, commentId: string): void;
408
+ /**
409
+ * Re-apply comment marks based on quoted text matching.
410
+ * Called when loading a post with existing comments.
411
+ */
412
+ declare function applyCommentMarks(editor: Editor, comments: CommentWithUser[]): void;
413
+ /**
414
+ * Scroll to a comment mark in the editor
415
+ */
416
+ declare function scrollToComment(editor: Editor, commentId: string): void;
417
+
418
+ interface CustomFieldProps<T = unknown> {
419
+ value: T;
420
+ onChange: (value: T) => void;
421
+ post: Post;
422
+ disabled?: boolean;
423
+ }
424
+ interface CustomFieldConfig {
425
+ name: string;
426
+ label?: string;
427
+ component: ComponentType<CustomFieldProps<unknown>>;
428
+ position?: 'footer' | 'sidebar';
429
+ }
430
+
431
+ export { type AutobloggerServer as Autoblogger, type AutobloggerServerConfig as AutobloggerConfig, CommentMark, type CommentWithUser, type CreateCommentData, type CustomFieldConfig, type CustomFieldProps, Post, type SelectionState, type Session, type StylesConfig, addCommentMark, applyCommentMarks, canDeleteComment, canEditComment, createAPIHandler, createAutoblogger, createCommentsClient, formatDate, removeCommentMark, scrollToComment, truncate, validateSchema };