astro-blog-kit 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,298 @@
1
+ # astro-blog-kit
2
+
3
+ A ready-to-use blog system for Astro with WordPress headless support, optional i18n, multiple layouts, and a comment system.
4
+
5
+ ## Features
6
+
7
+ - 🚀 **One command setup** — `npx astro-blog-kit init`
8
+ - 📝 **WordPress headless** — connects to WordPress REST API out of the box
9
+ - 🎨 **Multiple layouts** — magazine, grid, and list
10
+ - 💬 **Comment system** — with secure proxy to WordPress
11
+ - 🌍 **Optional i18n** — multi-language support
12
+ - 🎨 **Themeable** — customize colors and fonts via `blogKit()` config
13
+ - 📦 **Zero config** — works without any configuration
14
+
15
+ ---
16
+
17
+ ## Quick Start
18
+
19
+ ### 1. Install
20
+
21
+ ```bash
22
+ npm install astro-blog-kit
23
+ ```
24
+
25
+ ### 2. Run the setup wizard
26
+
27
+ ```bash
28
+ npx astro-blog-kit init
29
+ ```
30
+
31
+ The wizard will ask you:
32
+ - WordPress URL
33
+ - Posts per page
34
+ - Default layout
35
+ - Default locale
36
+ - i18n support
37
+
38
+ And will automatically create:
39
+ - `blog.config.ts` — your blog configuration
40
+ - `src/pages/blog/index.astro` — blog listing page
41
+ - `src/pages/blog/[...slug].astro` — individual post page
42
+ - `src/pages/blog/page/[page].astro` — pagination pages
43
+ - `src/pages/api/comments.ts` — secure comment proxy
44
+ - `.env.example` — environment variables template
45
+
46
+ ### 3. Add the integration
47
+
48
+ ```js
49
+ // astro.config.mjs
50
+ import { defineConfig } from "astro/config";
51
+ import { blogKit } from "astro-blog-kit/integration";
52
+
53
+ export default defineConfig({
54
+ integrations: [
55
+ blogKit({
56
+ postsPerPage: 5,
57
+ defaultLayout: "magazine",
58
+ theme: {
59
+ accent: "#facc15",
60
+ fontHeading: "Georgia, serif",
61
+ },
62
+ }),
63
+ ],
64
+ });
65
+ ```
66
+
67
+ ### 4. Add your WordPress credentials
68
+
69
+ Copy `.env.example` to `.env` and fill in your credentials:
70
+
71
+ ```env
72
+ WP_API_URL=https://cms.yourdomain.com
73
+ WP_APP_USER=your-wordpress-username
74
+ WP_APP_PASSWORD=xxxx xxxx xxxx xxxx xxxx xxxx
75
+ ```
76
+
77
+ To generate an Application Password in WordPress:
78
+ 1. Go to **Users → Profile**
79
+ 2. Scroll to **Application Passwords**
80
+ 3. Enter a name (e.g. `astro-blog-kit`) and click **Add New**
81
+ 4. Copy the generated password
82
+
83
+ ### 5. Run your site
84
+
85
+ ```bash
86
+ npm run dev
87
+ ```
88
+
89
+ ---
90
+
91
+ ## Configuration
92
+
93
+ ### `blog.config.ts`
94
+
95
+ ```ts
96
+ import { defineBlogConfig } from "astro-blog-kit";
97
+
98
+ export default defineBlogConfig({
99
+ wpUrl: "https://cms.yourdomain.com",
100
+ postsPerPage: 5,
101
+ defaultLayout: "magazine",
102
+ locale: "en",
103
+ theme: {
104
+ accent: "#facc15",
105
+ background: "#ffffff",
106
+ text: "#0a0a0a",
107
+ fontHeading: "Georgia, serif",
108
+ fontBody: "system-ui, sans-serif",
109
+ },
110
+ });
111
+ ```
112
+
113
+ ### Theme options
114
+
115
+ | Option | Type | Default | Description |
116
+ |--------|------|---------|-------------|
117
+ | `accent` | `string` | `#facc15` | Primary accent color |
118
+ | `background` | `string` | `#ffffff` | Background color |
119
+ | `surface` | `string` | `#f8f8f8` | Surface color (cards, sidebars) |
120
+ | `text` | `string` | `#0a0a0a` | Primary text color |
121
+ | `muted` | `string` | `#6b7280` | Secondary text color |
122
+ | `border` | `string` | `#e5e7eb` | Border color |
123
+ | `fontHeading` | `string` | `Georgia, serif` | Heading font |
124
+ | `fontBody` | `string` | `system-ui, sans-serif` | Body font |
125
+ | `fontMono` | `string` | `monospace` | Monospace font |
126
+ | `containerMax` | `string` | `1200px` | Max container width |
127
+
128
+ ---
129
+
130
+ ## Layouts
131
+
132
+ ### Magazine (default)
133
+ Featured post on the left + smaller posts on the right.
134
+
135
+ ### Grid
136
+ 3-column card grid (responsive: 2 cols on tablet, 1 on mobile).
137
+
138
+ ### List
139
+ Horizontal rows with image on the left.
140
+
141
+ ```astro
142
+ <BlogList layout="grid" ... />
143
+ <BlogList layout="list" ... />
144
+ <BlogList layout="magazine" ... />
145
+ ```
146
+
147
+ ---
148
+
149
+ ## Components
150
+
151
+ ### `<BlogList>`
152
+
153
+ ```astro
154
+ ---
155
+ import { BlogList } from "astro-blog-kit/components";
156
+ ---
157
+ <BlogList
158
+ posts={posts}
159
+ currentPage={1}
160
+ totalPages={totalPages}
161
+ basePath="/blog/page/"
162
+ blogBase="/blog/"
163
+ dateLocale="en-US"
164
+ t={t}
165
+ locale="en"
166
+ layout="magazine"
167
+ />
168
+ ```
169
+
170
+ ### `<BlogPost>`
171
+
172
+ ```astro
173
+ ---
174
+ import { BlogPost } from "astro-blog-kit/components";
175
+ ---
176
+ <BlogPost post={post} t={t} lang="en" />
177
+ ```
178
+
179
+ ### `<Comments>`
180
+
181
+ ```astro
182
+ ---
183
+ import { Comments } from "astro-blog-kit/components";
184
+ ---
185
+ <Comments comments={comments} postId={post.id ?? 0} />
186
+ ```
187
+
188
+ ### `<CommentForm>`
189
+
190
+ ```astro
191
+ ---
192
+ import { CommentForm } from "astro-blog-kit/components";
193
+ ---
194
+ <CommentForm postId={post.id ?? 0} apiRoute="/api/comments" />
195
+ ```
196
+
197
+ ---
198
+
199
+ ## Utils
200
+
201
+ ### WordPress client
202
+
203
+ ```ts
204
+ import { createWPClient } from "astro-blog-kit/utils";
205
+
206
+ const wp = createWPClient("https://cms.yourdomain.com");
207
+
208
+ // Get paginated posts
209
+ const { posts, total, totalPages } = await wp.getPosts({ perPage: 5, page: 1 });
210
+
211
+ // Get all posts (for getStaticPaths)
212
+ const posts = await wp.getAllPosts();
213
+
214
+ // Get single post by slug
215
+ const post = await wp.getPost("my-post-slug");
216
+
217
+ // Get comments for a post
218
+ const comments = await wp.getComments(postId);
219
+
220
+ // Get categories
221
+ const categories = await wp.getCategories();
222
+ ```
223
+
224
+ ### i18n helpers
225
+
226
+ ```ts
227
+ import { getLang, useTranslations, getBlogBase } from "astro-blog-kit/utils";
228
+
229
+ const lang = getLang(Astro.url, import.meta.env.BASE_URL, config);
230
+ const t = useTranslations(lang, { en, es });
231
+ const blogBase = getBlogBase(lang, import.meta.env.BASE_URL, config);
232
+ ```
233
+
234
+ ### Static paths helpers
235
+
236
+ ```ts
237
+ import { getStaticPathsForPosts, getStaticPathsForPages } from "astro-blog-kit/utils";
238
+
239
+ // For [...slug].astro
240
+ export const getStaticPaths = () => getStaticPathsForPosts(posts);
241
+
242
+ // For [page].astro
243
+ export const getStaticPaths = () => getStaticPathsForPages(posts, { postsPerPage: 5 });
244
+ ```
245
+
246
+ ---
247
+
248
+ ## Automatic deploys with GitHub Actions
249
+
250
+ Copy `node_modules/astro-blog-kit/examples/deploy-ftp.yml` to `.github/workflows/deploy.yml`.
251
+
252
+ Add these secrets to your GitHub repository (**Settings → Secrets**):
253
+
254
+ | Secret | Description |
255
+ |--------|-------------|
256
+ | `FTP_HOST` | Your server hostname |
257
+ | `FTP_USERNAME` | FTP username |
258
+ | `FTP_PASSWORD` | FTP password |
259
+ | `WP_API_URL` | Your WordPress URL |
260
+
261
+ The workflow triggers on:
262
+ - Push to `main`
263
+ - WordPress webhook (`wp_update`, `wp_comment`)
264
+ - Manual trigger
265
+
266
+ To trigger from WordPress, install the **WP Webhooks** plugin and configure it to send a `repository_dispatch` event to GitHub when a post is published or a comment is approved.
267
+
268
+ ---
269
+
270
+ ## i18n Support
271
+
272
+ ```js
273
+ // astro.config.mjs
274
+ blogKit({
275
+ i18n: {
276
+ locales: ["en", "es"],
277
+ defaultLocale: "en",
278
+ },
279
+ })
280
+ ```
281
+
282
+ With i18n enabled:
283
+ - Default locale: `/blog/my-post`
284
+ - Other locales: `/es/blog/my-post`
285
+
286
+ ---
287
+
288
+ ## Requirements
289
+
290
+ - Astro v4 or v5
291
+ - Node.js 18+
292
+ - WordPress with REST API enabled
293
+
294
+ ---
295
+
296
+ ## License
297
+
298
+ MIT
package/cli.ts ADDED
@@ -0,0 +1,218 @@
1
+ #!/usr/bin/env node
2
+ // ─────────────────────────────────────────────────────────────
3
+ // astro-blog-kit · cli.ts
4
+ // Comando: npx astro-blog-kit init
5
+ // ─────────────────────────────────────────────────────────────
6
+
7
+ import * as p from "@clack/prompts";
8
+ import fs from "fs";
9
+ import path from "path";
10
+ import { fileURLToPath } from "url";
11
+
12
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
13
+
14
+ // ── Helpers ───────────────────────────────────────────────────
15
+
16
+ function copyTemplate(
17
+ templateName: string,
18
+ destPath: string,
19
+ replacements: Record<string, string>
20
+ ) {
21
+ const templatePath = path.join(__dirname, "templates", templateName);
22
+ let content = fs.readFileSync(templatePath, "utf-8");
23
+
24
+ for (const [key, value] of Object.entries(replacements)) {
25
+ content = content.replaceAll(`__${key}__`, value);
26
+ }
27
+
28
+ const dir = path.dirname(destPath);
29
+ if (!fs.existsSync(dir)) {
30
+ fs.mkdirSync(dir, { recursive: true });
31
+ }
32
+
33
+ if (fs.existsSync(destPath)) {
34
+ return false; // ya existe, no sobreescribir
35
+ }
36
+
37
+ fs.writeFileSync(destPath, content, "utf-8");
38
+ return true;
39
+ }
40
+
41
+ function log(symbol: string, message: string) {
42
+ console.log(`${symbol} ${message}`);
43
+ }
44
+
45
+ // ── Main ──────────────────────────────────────────────────────
46
+
47
+ async function main() {
48
+ console.log("");
49
+ p.intro("astro-blog-kit — Blog setup wizard");
50
+
51
+ // ── Preguntas ──────────────────────────────────────────────
52
+
53
+ const answers = await p.group(
54
+ {
55
+ wpUrl: () =>
56
+ p.text({
57
+ message: "WordPress URL",
58
+ placeholder: "https://cms.tudominio.com",
59
+ validate: (v) => {
60
+ if (!v) return "WordPress URL is required";
61
+ try {
62
+ new URL(v);
63
+ } catch {
64
+ return "Please enter a valid URL";
65
+ }
66
+ },
67
+ }),
68
+
69
+ postsPerPage: () =>
70
+ p.text({
71
+ message: "Posts per page",
72
+ placeholder: "5",
73
+ initialValue: "5",
74
+ validate: (v) => {
75
+ const n = Number(v);
76
+ if (isNaN(n) || n < 1) return "Must be a number greater than 0";
77
+ },
78
+ }),
79
+
80
+ defaultLayout: () =>
81
+ p.select({
82
+ message: "Default layout",
83
+ options: [
84
+ { value: "magazine", label: "Magazine — featured post + grid" },
85
+ { value: "grid", label: "Grid — 3 column card grid" },
86
+ { value: "list", label: "List — horizontal rows" },
87
+ ],
88
+ }),
89
+
90
+ locale: () =>
91
+ p.text({
92
+ message: "Default locale",
93
+ placeholder: "en",
94
+ initialValue: "en",
95
+ }),
96
+
97
+ i18n: () =>
98
+ p.confirm({
99
+ message: "Enable i18n (multiple languages)?",
100
+ initialValue: false,
101
+ }),
102
+ },
103
+ {
104
+ onCancel: () => {
105
+ p.cancel("Setup cancelled.");
106
+ process.exit(0);
107
+ },
108
+ }
109
+ );
110
+
111
+ // ── Replacements ──────────────────────────────────────────
112
+
113
+ const replacements: Record<string, string> = {
114
+ WP_URL: answers.wpUrl as string,
115
+ POSTS_PER_PAGE: answers.postsPerPage as string,
116
+ DEFAULT_LAYOUT: answers.defaultLayout as string,
117
+ LOCALE: answers.locale as string,
118
+ };
119
+
120
+ // ── Copia archivos ────────────────────────────────────────
121
+
122
+ const cwd = process.cwd();
123
+ const spinner = p.spinner();
124
+ spinner.start("Creating blog files...");
125
+
126
+ const files = [
127
+ {
128
+ template: "blog-config.ts.template",
129
+ dest: path.join(cwd, "blog.config.ts"),
130
+ },
131
+ {
132
+ template: "blog-index.astro.template",
133
+ dest: path.join(cwd, "src", "pages", "blog", "index.astro"),
134
+ },
135
+ {
136
+ template: "blog-slug.astro.template",
137
+ dest: path.join(cwd, "src", "pages", "blog", "[...slug].astro"),
138
+ },
139
+ {
140
+ template: "blog-page.astro.template",
141
+ dest: path.join(cwd, "src", "pages", "blog", "page", "[page].astro"),
142
+ },
143
+ {
144
+ template: "api-comments.ts.template",
145
+ dest: path.join(cwd, "src", "pages", "api", "comments.ts"),
146
+ },
147
+ ];
148
+
149
+ const created: string[] = [];
150
+ const skipped: string[] = [];
151
+
152
+ for (const file of files) {
153
+ const ok = copyTemplate(file.template, file.dest, replacements);
154
+ const relativePath = path.relative(cwd, file.dest);
155
+ if (ok) {
156
+ created.push(relativePath);
157
+ } else {
158
+ skipped.push(relativePath);
159
+ }
160
+ }
161
+
162
+ spinner.stop("Blog files ready!");
163
+
164
+ // ── Crea .env.example ─────────────────────────────────────
165
+
166
+ const envExample = path.join(cwd, ".env.example");
167
+ if (!fs.existsSync(envExample)) {
168
+ fs.writeFileSync(
169
+ envExample,
170
+ [
171
+ `WP_API_URL=${answers.wpUrl}`,
172
+ `WP_APP_USER=your-wordpress-username`,
173
+ `WP_APP_PASSWORD=xxxx xxxx xxxx xxxx xxxx xxxx`,
174
+ ].join("\n"),
175
+ "utf-8"
176
+ );
177
+ created.push(".env.example");
178
+ }
179
+
180
+ // ── Resumen ───────────────────────────────────────────────
181
+
182
+ console.log("");
183
+
184
+ if (created.length > 0) {
185
+ log("✅", "Created:");
186
+ created.forEach((f) => log(" ", f));
187
+ }
188
+
189
+ if (skipped.length > 0) {
190
+ console.log("");
191
+ log("⚠️ ", "Skipped (already exist):");
192
+ skipped.forEach((f) => log(" ", f));
193
+ }
194
+
195
+ console.log("");
196
+
197
+ p.note(
198
+ [
199
+ "1. Add your WordPress credentials to .env:",
200
+ " WP_APP_USER=your-username",
201
+ " WP_APP_PASSWORD=xxxx xxxx xxxx xxxx",
202
+ "",
203
+ "2. Add blogKit() to your astro.config.mjs:",
204
+ " import { blogKit } from 'astro-blog-kit/integration';",
205
+ " integrations: [blogKit()]",
206
+ "",
207
+ "3. Run: npm run dev",
208
+ ].join("\n"),
209
+ "Next steps"
210
+ );
211
+
212
+ p.outro("Your blog is ready! 🚀");
213
+ }
214
+
215
+ main().catch((err) => {
216
+ console.error(err);
217
+ process.exit(1);
218
+ });