nca-ai-cms-astro-plugin 1.0.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.
Files changed (73) hide show
  1. package/.claude/settings.local.json +9 -0
  2. package/README.md +87 -0
  3. package/package.json +53 -0
  4. package/src/api/_utils.ts +20 -0
  5. package/src/api/articles/[id]/apply.ts +89 -0
  6. package/src/api/articles/[id]/regenerate-image.ts +49 -0
  7. package/src/api/articles/[id]/regenerate-text.ts +57 -0
  8. package/src/api/articles/[id].ts +53 -0
  9. package/src/api/auth/check.ts +6 -0
  10. package/src/api/auth/login.ts +43 -0
  11. package/src/api/auth/logout.ts +6 -0
  12. package/src/api/generate-content.ts +43 -0
  13. package/src/api/generate-image.ts +33 -0
  14. package/src/api/prompts.ts +45 -0
  15. package/src/api/save-image.ts +38 -0
  16. package/src/api/save.ts +49 -0
  17. package/src/api/scheduler/[id].ts +31 -0
  18. package/src/api/scheduler/generate.ts +94 -0
  19. package/src/api/scheduler/publish.ts +96 -0
  20. package/src/api/scheduler.ts +51 -0
  21. package/src/components/Editor.tsx +115 -0
  22. package/src/components/editor/GenerateTab.tsx +384 -0
  23. package/src/components/editor/PlannerTab.tsx +345 -0
  24. package/src/components/editor/SettingsTab.tsx +185 -0
  25. package/src/components/editor/styles.ts +597 -0
  26. package/src/components/editor/types.ts +49 -0
  27. package/src/components/editor/useTabNavigation.ts +69 -0
  28. package/src/config.d.ts +4 -0
  29. package/src/db/tables.ts +39 -0
  30. package/src/domain/entities/Article.test.ts +138 -0
  31. package/src/domain/entities/Article.ts +90 -0
  32. package/src/domain/entities/ScheduledPost.test.ts +228 -0
  33. package/src/domain/entities/ScheduledPost.ts +152 -0
  34. package/src/domain/entities/Source.test.ts +57 -0
  35. package/src/domain/entities/Source.ts +43 -0
  36. package/src/domain/entities/index.ts +9 -0
  37. package/src/domain/index.ts +16 -0
  38. package/src/domain/value-objects/ArticleFinder.test.ts +104 -0
  39. package/src/domain/value-objects/ArticleFinder.ts +61 -0
  40. package/src/domain/value-objects/SEOMetadata.test.ts +48 -0
  41. package/src/domain/value-objects/SEOMetadata.ts +19 -0
  42. package/src/domain/value-objects/Slug.test.ts +51 -0
  43. package/src/domain/value-objects/Slug.ts +33 -0
  44. package/src/domain/value-objects/index.ts +4 -0
  45. package/src/index.ts +146 -0
  46. package/src/middleware.ts +30 -0
  47. package/src/pages/editor.astro +22 -0
  48. package/src/pages/login.astro +117 -0
  49. package/src/services/ArticleService.test.ts +148 -0
  50. package/src/services/ArticleService.ts +150 -0
  51. package/src/services/AutoPublisher.ts +122 -0
  52. package/src/services/ContentFetcher.ts +89 -0
  53. package/src/services/ContentGenerator.ts +320 -0
  54. package/src/services/FileWriter.test.ts +80 -0
  55. package/src/services/FileWriter.ts +59 -0
  56. package/src/services/ImageConverter.ts +15 -0
  57. package/src/services/ImageGenerator.ts +108 -0
  58. package/src/services/PromptService.ts +84 -0
  59. package/src/services/SchedulerDBAdapter.ts +75 -0
  60. package/src/services/SchedulerService.test.ts +286 -0
  61. package/src/services/SchedulerService.ts +149 -0
  62. package/src/services/index.ts +27 -0
  63. package/src/utils/authUtils.test.ts +60 -0
  64. package/src/utils/authUtils.ts +25 -0
  65. package/src/utils/envUtils.test.ts +40 -0
  66. package/src/utils/envUtils.ts +26 -0
  67. package/src/utils/index.ts +7 -0
  68. package/src/utils/markdown.test.ts +65 -0
  69. package/src/utils/markdown.ts +13 -0
  70. package/src/utils/sanitize.test.ts +180 -0
  71. package/src/utils/sanitize.ts +98 -0
  72. package/tsconfig.json +22 -0
  73. package/vitest.config.ts +14 -0
@@ -0,0 +1,9 @@
1
+ {
2
+ "permissions": {
3
+ "allow": [
4
+ "Bash(tail:*)",
5
+ "Bash(npx tsc:*)",
6
+ "Bash(head:*)"
7
+ ]
8
+ }
9
+ }
package/README.md ADDED
@@ -0,0 +1,87 @@
1
+ # NCA AI CMS Astro Plugin
2
+
3
+ An Astro plugin that adds an AI-powered content editor to your site. Generate articles and images with Google Gemini, schedule posts, and manage everything from a built-in editor UI.
4
+
5
+ **Never code alone** — this is an open source project by [Never Code Alone](https://nevercodealone.de).
6
+
7
+ ## What it does
8
+
9
+ - Adds an `/editor` page with a React-based UI for content management
10
+ - Generates articles and images using Google Gemini AI
11
+ - Schedules and auto-publishes posts as Markdown files
12
+ - Handles authentication so your host project stays simple
13
+
14
+ ## Setup
15
+
16
+ ### 1. Install
17
+
18
+ ```bash
19
+ npm install nca-ai-cms-astro-plugin
20
+ ```
21
+
22
+ ### 2. Add to your Astro config
23
+
24
+ ```ts
25
+ // astro.config.mjs
26
+ import ncaAiCms from 'nca-ai-cms-astro-plugin';
27
+
28
+ export default defineConfig({
29
+ integrations: [ncaAiCms()],
30
+ });
31
+ ```
32
+
33
+ ### 3. Environment variables
34
+
35
+ ```env
36
+ GOOGLE_GEMINI_API_KEY=your-gemini-api-key
37
+ EDITOR_ADMIN=your-username
38
+ EDITOR_PASSWORD=your-password
39
+ ```
40
+
41
+ | Variable | Required | Description |
42
+ |---|---|---|
43
+ | `GOOGLE_GEMINI_API_KEY` | Yes | Google Gemini API key for content and image generation |
44
+ | `EDITOR_ADMIN` | Yes | Username for editor login |
45
+ | `EDITOR_PASSWORD` | Yes | Password for editor login |
46
+
47
+ ### 4. Requirements
48
+
49
+ - Astro 5+
50
+ - `@astrojs/db` and `@astrojs/react` integrations
51
+ - Node.js 18+
52
+
53
+ ## Options
54
+
55
+ ```ts
56
+ ncaAiCms({
57
+ contentPath: 'src/content/blog', // where Markdown files are saved (default: 'nca-ai-cms-content')
58
+ autoPublish: true, // auto-publish scheduled posts (default: true in production)
59
+ });
60
+ ```
61
+
62
+ ## Routes added
63
+
64
+ | Route | Description |
65
+ |---|---|
66
+ | `/login` | Login page |
67
+ | `/editor` | Content editor UI |
68
+ | `/api/auth/*` | Authentication endpoints |
69
+ | `/api/generate-content` | Generate article text |
70
+ | `/api/generate-image` | Generate article image |
71
+ | `/api/save` | Save Markdown file |
72
+ | `/api/prompts` | Manage prompt templates |
73
+ | `/api/scheduler` | Manage scheduled posts |
74
+ | `/api/articles/*` | Article operations |
75
+
76
+ All `/api/*` and `/editor` routes are protected by cookie-based authentication.
77
+
78
+ ## Development
79
+
80
+ ```bash
81
+ npm test # run tests
82
+ npm run typecheck # check types
83
+ ```
84
+
85
+ ## License
86
+
87
+ MIT
package/package.json ADDED
@@ -0,0 +1,53 @@
1
+ {
2
+ "name": "nca-ai-cms-astro-plugin",
3
+ "version": "1.0.0",
4
+ "type": "module",
5
+ "exports": {
6
+ ".": "./src/index.ts",
7
+ "./services": "./src/services/index.ts",
8
+ "./domain": "./src/domain/index.ts",
9
+ "./domain/entities": "./src/domain/entities/index.ts",
10
+ "./domain/value-objects": "./src/domain/value-objects/index.ts",
11
+ "./utils": "./src/utils/index.ts",
12
+ "./api/*": "./src/api/*"
13
+ },
14
+ "scripts": {
15
+ "test": "vitest run",
16
+ "test:watch": "vitest",
17
+ "typecheck": "tsc --noEmit"
18
+ },
19
+ "peerDependencies": {
20
+ "astro": "^5.0.0",
21
+ "@astrojs/db": "^0.18.0",
22
+ "@astrojs/react": "^4.0.0",
23
+ "@google/genai": "^1.0.0",
24
+ "@google/generative-ai": "^0.24.0",
25
+ "gray-matter": "^4.0.0",
26
+ "marked": "^17.0.0",
27
+ "react": "^19.0.0",
28
+ "react-dom": "^19.0.0",
29
+ "sanitize-html": "^2.0.0",
30
+ "sharp": "^0.34.0",
31
+ "turndown": "^7.0.0",
32
+ "zod": "^3.0.0"
33
+ },
34
+ "devDependencies": {
35
+ "@types/sanitize-html": "^2.16.0",
36
+ "@types/turndown": "^5.0.6",
37
+ "astro": "^5.16.4",
38
+ "@astrojs/db": "^0.18.3",
39
+ "@astrojs/react": "^4.4.2",
40
+ "@google/genai": "^1.31.0",
41
+ "@google/generative-ai": "^0.24.1",
42
+ "gray-matter": "^4.0.3",
43
+ "marked": "^17.0.1",
44
+ "react": "^19.2.1",
45
+ "react-dom": "^19.2.1",
46
+ "sanitize-html": "^2.17.0",
47
+ "sharp": "^0.34.5",
48
+ "turndown": "^7.2.2",
49
+ "zod": "^3.25.76",
50
+ "typescript": "^5.9.3",
51
+ "vitest": "^4.0.15"
52
+ }
53
+ }
@@ -0,0 +1,20 @@
1
+ export function jsonResponse(data: unknown, status = 200): Response {
2
+ return new Response(JSON.stringify(data), {
3
+ status,
4
+ headers: { 'Content-Type': 'application/json' },
5
+ });
6
+ }
7
+
8
+ export function jsonError(error: unknown, status = 500): Response {
9
+ const message = error instanceof Error ? error.message : String(error);
10
+ return new Response(JSON.stringify({ error: message }), {
11
+ status,
12
+ headers: { 'Content-Type': 'application/json' },
13
+ });
14
+ }
15
+
16
+ export async function parseBody<T = Record<string, unknown>>(
17
+ request: Request
18
+ ): Promise<T> {
19
+ return request.json() as Promise<T>;
20
+ }
@@ -0,0 +1,89 @@
1
+ import type { APIRoute } from 'astro';
2
+ import * as fs from 'fs/promises';
3
+ import * as path from 'path';
4
+ import matter from 'gray-matter';
5
+ import {
6
+ ArticleService,
7
+ ArticleNotFoundError,
8
+ } from '../../../services/ArticleService';
9
+ import { convertToWebP } from '../../../services/ImageConverter';
10
+ import { contentPath } from 'virtual:nca-ai-cms/config';
11
+ import { jsonResponse, jsonError } from '../../_utils';
12
+
13
+ interface ApplyRequest {
14
+ // For text updates
15
+ title?: string;
16
+ description?: string;
17
+ content?: string;
18
+ tags?: string[];
19
+ // For image updates
20
+ imageUrl?: string;
21
+ imageAlt?: string;
22
+ }
23
+
24
+ // POST /api/articles/[id]/apply - Save regenerated content or image
25
+ export const POST: APIRoute = async ({ params, request }) => {
26
+ try {
27
+ const slug = params.id;
28
+
29
+ if (!slug) {
30
+ return jsonError('Article ID required', 400);
31
+ }
32
+
33
+ const data: ApplyRequest = await request.json();
34
+ const service = new ArticleService(contentPath);
35
+
36
+ // Read existing article
37
+ const existingArticle = await service.read(slug);
38
+ if (!existingArticle) {
39
+ throw new ArticleNotFoundError(slug);
40
+ }
41
+
42
+ // Handle image update if imageUrl is provided
43
+ if (data.imageUrl) {
44
+ const heroPath = path.join(existingArticle.folderPath, 'hero.webp');
45
+
46
+ console.log('Saving image to:', heroPath);
47
+ console.log('Image URL length:', data.imageUrl.length);
48
+
49
+ // Decode base64 image data and convert to WebP
50
+ const base64Data = data.imageUrl.replace(/^data:image\/\w+;base64,/, '');
51
+
52
+ await convertToWebP(base64Data, heroPath);
53
+ console.log('Image saved successfully');
54
+
55
+ // Update imageAlt if provided
56
+ if (data.imageAlt) {
57
+ const indexPath = path.join(existingArticle.folderPath, 'index.md');
58
+ const fileContent = await fs.readFile(indexPath, 'utf-8');
59
+ const { data: frontmatter, content } = matter(fileContent);
60
+
61
+ frontmatter.imageAlt = data.imageAlt;
62
+
63
+ const updatedContent = matter.stringify(content, frontmatter);
64
+ await fs.writeFile(indexPath, updatedContent);
65
+ }
66
+ }
67
+
68
+ // Handle text update if content is provided
69
+ if (data.content || data.title || data.description) {
70
+ await service.updateContent(slug, {
71
+ ...(data.title && { title: data.title }),
72
+ ...(data.description && { description: data.description }),
73
+ ...(data.content && { content: data.content }),
74
+ });
75
+ }
76
+
77
+ return jsonResponse({
78
+ success: true,
79
+ articleId: existingArticle.articleId,
80
+ });
81
+ } catch (error) {
82
+ if (error instanceof ArticleNotFoundError) {
83
+ return jsonError(error, 404);
84
+ }
85
+
86
+ console.error('Apply changes error:', error);
87
+ return jsonError(error);
88
+ }
89
+ };
@@ -0,0 +1,49 @@
1
+ import type { APIRoute } from 'astro';
2
+ import {
3
+ ArticleService,
4
+ ArticleNotFoundError,
5
+ } from '../../../services/ArticleService';
6
+ import { ImageGenerator } from '../../../services/ImageGenerator';
7
+ import { getEnvVariable } from '../../../utils/envUtils';
8
+ import { contentPath } from 'virtual:nca-ai-cms/config';
9
+ import { jsonResponse, jsonError } from '../../_utils';
10
+
11
+ // POST /api/articles/[id]/regenerate-image - Generate new image for article
12
+ // Returns preview of new image URL WITHOUT saving
13
+ export const POST: APIRoute = async ({ params }) => {
14
+ try {
15
+ const slug = params.id;
16
+
17
+ if (!slug) {
18
+ return jsonError('Article ID required', 400);
19
+ }
20
+
21
+ // Read existing article to get title for image generation
22
+ const service = new ArticleService(contentPath);
23
+ const existingArticle = await service.read(slug);
24
+
25
+ if (!existingArticle) {
26
+ throw new ArticleNotFoundError(slug);
27
+ }
28
+
29
+ // Generate new image using article title
30
+ const apiKey = getEnvVariable('GOOGLE_GEMINI_API_KEY');
31
+ const generator = new ImageGenerator({ apiKey });
32
+ const image = await generator.generate(existingArticle.title);
33
+
34
+ return jsonResponse({
35
+ url: image.url,
36
+ alt: image.alt,
37
+ // Include article info for reference
38
+ articleId: existingArticle.articleId,
39
+ articleTitle: existingArticle.title,
40
+ });
41
+ } catch (error) {
42
+ if (error instanceof ArticleNotFoundError) {
43
+ return jsonError(error, 404);
44
+ }
45
+
46
+ console.error('Regenerate image error:', error);
47
+ return jsonError(error);
48
+ }
49
+ };
@@ -0,0 +1,57 @@
1
+ import type { APIRoute } from 'astro';
2
+ import {
3
+ ArticleService,
4
+ ArticleNotFoundError,
5
+ } from '../../../services/ArticleService';
6
+ import { ContentGenerator } from '../../../services/ContentGenerator';
7
+ import { PromptService } from '../../../services/PromptService';
8
+ import { getEnvVariable } from '../../../utils/envUtils';
9
+ import { contentPath } from 'virtual:nca-ai-cms/config';
10
+ import { jsonResponse, jsonError } from '../../_utils';
11
+
12
+ // POST /api/articles/[id]/regenerate-text - Generate new content for article
13
+ // Returns preview of new content WITHOUT saving
14
+ export const POST: APIRoute = async ({ params }) => {
15
+ try {
16
+ const slug = params.id;
17
+
18
+ if (!slug) {
19
+ return jsonError('Article ID required', 400);
20
+ }
21
+
22
+ // Read existing article to get title for regeneration
23
+ const service = new ArticleService(contentPath);
24
+ const existingArticle = await service.read(slug);
25
+
26
+ if (!existingArticle) {
27
+ throw new ArticleNotFoundError(slug);
28
+ }
29
+
30
+ // Generate new content using existing title as keywords
31
+ const apiKey = getEnvVariable('GOOGLE_GEMINI_API_KEY');
32
+ const promptService = new PromptService();
33
+ const generator = new ContentGenerator({ apiKey, promptService });
34
+
35
+ // Use the existing title as keywords for regeneration
36
+ const newArticle = await generator.generateFromKeywords(
37
+ existingArticle.title
38
+ );
39
+
40
+ return jsonResponse({
41
+ title: newArticle.title,
42
+ description: newArticle.description,
43
+ content: newArticle.content,
44
+ tags: newArticle.tags,
45
+ // Include original article info for reference
46
+ originalTitle: existingArticle.title,
47
+ articleId: existingArticle.articleId,
48
+ });
49
+ } catch (error) {
50
+ if (error instanceof ArticleNotFoundError) {
51
+ return jsonError(error, 404);
52
+ }
53
+
54
+ console.error('Regenerate text error:', error);
55
+ return jsonError(error);
56
+ }
57
+ };
@@ -0,0 +1,53 @@
1
+ import type { APIRoute } from 'astro';
2
+ import {
3
+ ArticleService,
4
+ ArticleNotFoundError,
5
+ } from '../../services/ArticleService';
6
+ import { contentPath } from 'virtual:nca-ai-cms/config';
7
+ import { jsonResponse, jsonError } from '../_utils';
8
+
9
+ // GET /api/articles/[id] - Get article details
10
+ export const GET: APIRoute = async ({ params }) => {
11
+ try {
12
+ const slug = params.id;
13
+
14
+ if (!slug) {
15
+ return jsonError('Article ID required', 400);
16
+ }
17
+
18
+ const service = new ArticleService(contentPath);
19
+ const article = await service.read(slug);
20
+
21
+ if (!article) {
22
+ return jsonError('Article not found', 404);
23
+ }
24
+
25
+ return jsonResponse(article);
26
+ } catch (error) {
27
+ console.error('Read error:', error);
28
+ return jsonError(error);
29
+ }
30
+ };
31
+
32
+ // DELETE /api/articles/[id] - Delete an article by slug
33
+ export const DELETE: APIRoute = async ({ params }) => {
34
+ try {
35
+ const slug = params.id;
36
+
37
+ if (!slug) {
38
+ return jsonError('Article ID required', 400);
39
+ }
40
+
41
+ const service = new ArticleService(contentPath);
42
+ await service.delete(slug);
43
+
44
+ return jsonResponse({ success: true });
45
+ } catch (error) {
46
+ if (error instanceof ArticleNotFoundError) {
47
+ return jsonError(error, 404);
48
+ }
49
+
50
+ console.error('Delete error:', error);
51
+ return jsonError(error);
52
+ }
53
+ };
@@ -0,0 +1,6 @@
1
+ import type { APIRoute } from 'astro';
2
+ import { jsonResponse } from '../_utils.js';
3
+
4
+ export const GET: APIRoute = async () => {
5
+ return jsonResponse({ authenticated: true });
6
+ };
@@ -0,0 +1,43 @@
1
+ import type { APIRoute } from 'astro';
2
+ import { z } from 'zod';
3
+ import { jsonResponse, jsonError } from '../_utils.js';
4
+ import { getEnvVariable } from '../../utils/envUtils.js';
5
+
6
+ const loginSchema = z.object({
7
+ username: z.string().min(1),
8
+ password: z.string().min(1),
9
+ });
10
+
11
+ export const POST: APIRoute = async ({ request, cookies }) => {
12
+ let body: unknown;
13
+ try {
14
+ body = await request.json();
15
+ } catch {
16
+ return jsonError('Invalid JSON', 400);
17
+ }
18
+
19
+ const result = loginSchema.safeParse(body);
20
+ if (!result.success) {
21
+ return jsonError('Username and password are required', 400);
22
+ }
23
+
24
+ const { username, password } = result.data;
25
+ const expectedUsername = getEnvVariable('EDITOR_ADMIN');
26
+ const expectedPassword = getEnvVariable('EDITOR_PASSWORD');
27
+
28
+ if (username !== expectedUsername || password !== expectedPassword) {
29
+ return jsonError('Invalid credentials', 401);
30
+ }
31
+
32
+ const token = btoa(`${username}:${password}`);
33
+
34
+ cookies.set('editor-auth', token, {
35
+ httpOnly: true,
36
+ secure: import.meta.env.PROD,
37
+ sameSite: 'strict',
38
+ path: '/',
39
+ maxAge: 60 * 60 * 24, // 24 hours
40
+ });
41
+
42
+ return jsonResponse({ success: true });
43
+ };
@@ -0,0 +1,6 @@
1
+ import type { APIRoute } from 'astro';
2
+
3
+ export const POST: APIRoute = async ({ cookies, redirect }) => {
4
+ cookies.delete('editor-auth', { path: '/' });
5
+ return redirect('/login', 302);
6
+ };
@@ -0,0 +1,43 @@
1
+ import type { APIRoute } from 'astro';
2
+ import { z } from 'zod';
3
+ import { ContentGenerator } from '../services/ContentGenerator';
4
+ import { PromptService } from '../services/PromptService';
5
+ import { getEnvVariable } from '../utils/envUtils';
6
+ import { jsonResponse, jsonError } from './_utils';
7
+
8
+ const GenerateContentSchema = z.object({
9
+ url: z.string().url().optional(),
10
+ keywords: z.string().min(1).optional(),
11
+ }).refine((data) => data.url || data.keywords, {
12
+ message: 'URL or keywords required',
13
+ });
14
+
15
+ export const POST: APIRoute = async ({ request }) => {
16
+ try {
17
+ const body = await request.json();
18
+ const parsed = GenerateContentSchema.safeParse(body);
19
+ if (!parsed.success) {
20
+ return jsonError(parsed.error.errors[0]?.message ?? 'Invalid request', 400);
21
+ }
22
+ const { url, keywords } = parsed.data;
23
+
24
+ const apiKey = getEnvVariable('GOOGLE_GEMINI_API_KEY');
25
+ const promptService = new PromptService();
26
+ const generator = new ContentGenerator({ apiKey, promptService });
27
+ const article = url
28
+ ? await generator.generateFromUrl(url)
29
+ : await generator.generateFromKeywords(keywords!);
30
+
31
+ return jsonResponse({
32
+ title: article.title,
33
+ description: article.description,
34
+ content: article.content,
35
+ filepath: article.filepath,
36
+ tags: article.tags,
37
+ date: article.date.toISOString(),
38
+ });
39
+ } catch (error) {
40
+ console.error('Generation error:', error);
41
+ return jsonError(error);
42
+ }
43
+ };
@@ -0,0 +1,33 @@
1
+ import type { APIRoute } from 'astro';
2
+ import { z } from 'zod';
3
+ import { ImageGenerator } from '../services/ImageGenerator';
4
+ import { getEnvVariable } from '../utils/envUtils';
5
+ import { jsonResponse, jsonError } from './_utils';
6
+
7
+ const GenerateImageSchema = z.object({
8
+ title: z.string().min(1, 'Title is required'),
9
+ });
10
+
11
+ export const POST: APIRoute = async ({ request }) => {
12
+ try {
13
+ const body = await request.json();
14
+ const parsed = GenerateImageSchema.safeParse(body);
15
+ if (!parsed.success) {
16
+ return jsonError(parsed.error.errors[0]?.message ?? 'Invalid request', 400);
17
+ }
18
+ const { title } = parsed.data;
19
+
20
+ const apiKey = getEnvVariable('GOOGLE_GEMINI_API_KEY');
21
+ const generator = new ImageGenerator({ apiKey });
22
+ const image = await generator.generate(title);
23
+
24
+ return jsonResponse({
25
+ url: image.url,
26
+ alt: image.alt,
27
+ filepath: image.filepath,
28
+ });
29
+ } catch (error) {
30
+ console.error('Image generation error:', error);
31
+ return jsonError(error);
32
+ }
33
+ };
@@ -0,0 +1,45 @@
1
+ import type { APIRoute } from 'astro';
2
+ import { PromptService } from '../services/PromptService';
3
+ import { jsonResponse, jsonError } from './_utils';
4
+
5
+ const service = new PromptService();
6
+
7
+ // GET /api/prompts - Get all prompts and settings
8
+ export const GET: APIRoute = async () => {
9
+ try {
10
+ const [prompts, settings] = await Promise.all([
11
+ service.getAllPrompts(),
12
+ service.getAllSettings(),
13
+ ]);
14
+
15
+ return jsonResponse({
16
+ prompts,
17
+ settings,
18
+ });
19
+ } catch (error) {
20
+ console.error('Get prompts error:', error);
21
+ return jsonError(error);
22
+ }
23
+ };
24
+
25
+ // POST /api/prompts - Update a prompt or setting
26
+ export const POST: APIRoute = async ({ request }) => {
27
+ try {
28
+ const data = await request.json();
29
+
30
+ if (data.type === 'prompt' && data.id && data.promptText !== undefined) {
31
+ await service.updatePrompt(data.id, data.promptText);
32
+ return jsonResponse({ success: true, type: 'prompt', id: data.id });
33
+ }
34
+
35
+ if (data.type === 'setting' && data.key && data.value !== undefined) {
36
+ await service.updateSetting(data.key, data.value);
37
+ return jsonResponse({ success: true, type: 'setting', key: data.key });
38
+ }
39
+
40
+ return jsonError('Invalid request: missing type, id/key, or value', 400);
41
+ } catch (error) {
42
+ console.error('Update prompt error:', error);
43
+ return jsonError(error);
44
+ }
45
+ };
@@ -0,0 +1,38 @@
1
+ import type { APIRoute } from 'astro';
2
+ import path from 'node:path';
3
+ import { z } from 'zod';
4
+ import { convertToWebP } from '../services/ImageConverter';
5
+ import { jsonResponse, jsonError } from './_utils';
6
+
7
+ const SaveImageSchema = z.object({
8
+ url: z.string().regex(/^data:image\/\w+;base64,.+$/, 'Invalid image data URL'),
9
+ folderPath: z.string().min(1, 'folderPath is required'),
10
+ });
11
+
12
+ export const POST: APIRoute = async ({ request }) => {
13
+ try {
14
+ const body = await request.json();
15
+ const parsed = SaveImageSchema.safeParse(body);
16
+ if (!parsed.success) {
17
+ return jsonError(parsed.error.errors[0]?.message ?? 'Invalid request', 400);
18
+ }
19
+ const { url, folderPath } = parsed.data;
20
+
21
+ // Regex is guaranteed to match since Zod already validated the format
22
+ const base64Data = url.split(',')[1]!;
23
+
24
+ // Save hero.webp in the article folder
25
+ const filepath = path.join(folderPath, 'hero.webp');
26
+ const fullPath = path.resolve(process.cwd(), filepath);
27
+
28
+ await convertToWebP(base64Data, fullPath);
29
+
30
+ return jsonResponse({
31
+ success: true,
32
+ filepath: filepath,
33
+ });
34
+ } catch (error) {
35
+ console.error('Image save error:', error);
36
+ return jsonError(error);
37
+ }
38
+ };