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.
@@ -0,0 +1,245 @@
1
+ ---
2
+ import type { WPComment } from "../utils/wordpress";
3
+
4
+ interface Props {
5
+ comments: WPComment[];
6
+ postId: number;
7
+ }
8
+
9
+ const { comments, postId } = Astro.props;
10
+
11
+ // Organiza comentarios en árbol (replies anidadas)
12
+ const topLevel = comments.filter((c: WPComment) => c.parent === 0);
13
+ const getReplies = (parentId: number) =>
14
+ comments.filter((c: WPComment) => c.parent === parentId);
15
+ ---
16
+
17
+ <section class="comments" id="comments">
18
+
19
+ <h2 class="comments__title">
20
+ {comments.length === 0
21
+ ? "No comments yet"
22
+ : `${comments.length} Comment${comments.length === 1 ? "" : "s"}`
23
+ }
24
+ </h2>
25
+
26
+ {comments.length === 0 && (
27
+ <p class="comments__empty">Be the first to leave a comment.</p>
28
+ )}
29
+
30
+ {topLevel.length > 0 && (
31
+ <ol class="comments__list">
32
+ {topLevel.map((comment: WPComment) => {
33
+ const replies = getReplies(comment.id);
34
+ const avatar = comment.author_avatar_urls?.["48"];
35
+
36
+ return (
37
+ <li class="comment" id={`comment-${comment.id}`}>
38
+ <div class="comment__header">
39
+ {avatar && (
40
+ <img
41
+ src={avatar}
42
+ alt={comment.author_name}
43
+ class="comment__avatar"
44
+ width="48"
45
+ height="48"
46
+ loading="lazy"
47
+ />
48
+ )}
49
+ <div class="comment__meta">
50
+ <span class="comment__author">{comment.author_name}</span>
51
+ <time class="comment__date">
52
+ {new Date(comment.date).toLocaleDateString("en-US", {
53
+ year: "numeric",
54
+ month: "long",
55
+ day: "numeric",
56
+ })}
57
+ </time>
58
+ </div>
59
+ </div>
60
+
61
+ <div
62
+ class="comment__body"
63
+ set:html={comment.content.rendered}
64
+ />
65
+
66
+ <button
67
+ class="comment__reply-btn"
68
+ data-reply-to={comment.id}
69
+ data-reply-name={comment.author_name}
70
+ >
71
+ ↩ Reply
72
+ </button>
73
+
74
+ {replies.length > 0 && (
75
+ <ol class="comment__replies">
76
+ {replies.map((reply: WPComment) => {
77
+ const replyAvatar = reply.author_avatar_urls?.["48"];
78
+ return (
79
+ <li class="comment comment--reply" id={`comment-${reply.id}`}>
80
+ <div class="comment__header">
81
+ {replyAvatar && (
82
+ <img
83
+ src={replyAvatar}
84
+ alt={reply.author_name}
85
+ class="comment__avatar comment__avatar--small"
86
+ width="36"
87
+ height="36"
88
+ loading="lazy"
89
+ />
90
+ )}
91
+ <div class="comment__meta">
92
+ <span class="comment__author">{reply.author_name}</span>
93
+ <time class="comment__date">
94
+ {new Date(reply.date).toLocaleDateString("en-US", {
95
+ year: "numeric",
96
+ month: "long",
97
+ day: "numeric",
98
+ })}
99
+ </time>
100
+ </div>
101
+ </div>
102
+ <div
103
+ class="comment__body"
104
+ set:html={reply.content.rendered}
105
+ />
106
+ </li>
107
+ );
108
+ })}
109
+ </ol>
110
+ )}
111
+ </li>
112
+ );
113
+ })}
114
+ </ol>
115
+ )}
116
+
117
+ </section>
118
+
119
+ <style>
120
+ .comments {
121
+ max-width: 720px;
122
+ margin: 4rem auto 0;
123
+ padding: 0 2rem;
124
+ font-family: var(--font-body);
125
+ }
126
+
127
+ .comments__title {
128
+ font-family: var(--font-display);
129
+ font-size: 1.5rem;
130
+ font-weight: 700;
131
+ color: var(--color-text);
132
+ margin: 0 0 2rem;
133
+ padding-bottom: 1rem;
134
+ border-bottom: 1px solid var(--color-border);
135
+ }
136
+
137
+ .comments__empty {
138
+ color: var(--color-muted);
139
+ font-size: 0.95rem;
140
+ }
141
+
142
+ .comments__list {
143
+ list-style: none;
144
+ padding: 0;
145
+ margin: 0;
146
+ display: flex;
147
+ flex-direction: column;
148
+ gap: 2rem;
149
+ }
150
+
151
+ /* Comment */
152
+ .comment {
153
+ display: flex;
154
+ flex-direction: column;
155
+ gap: 0.75rem;
156
+ }
157
+
158
+ .comment--reply {
159
+ padding-left: 1.5rem;
160
+ border-left: 2px solid var(--color-border);
161
+ }
162
+
163
+ .comment__header {
164
+ display: flex;
165
+ align-items: center;
166
+ gap: 0.75rem;
167
+ }
168
+
169
+ .comment__avatar {
170
+ width: 48px;
171
+ height: 48px;
172
+ border-radius: 50%;
173
+ object-fit: cover;
174
+ border: 2px solid var(--color-border);
175
+ flex-shrink: 0;
176
+ }
177
+
178
+ .comment__avatar--small {
179
+ width: 36px;
180
+ height: 36px;
181
+ }
182
+
183
+ .comment__meta {
184
+ display: flex;
185
+ flex-direction: column;
186
+ gap: 0.15rem;
187
+ }
188
+
189
+ .comment__author {
190
+ font-size: 0.9rem;
191
+ font-weight: 700;
192
+ color: var(--color-text);
193
+ }
194
+
195
+ .comment__date {
196
+ font-size: 0.75rem;
197
+ color: var(--color-muted);
198
+ font-family: var(--font-mono);
199
+ }
200
+
201
+ .comment__body {
202
+ font-size: 0.95rem;
203
+ color: var(--color-muted-light);
204
+ line-height: 1.75;
205
+ }
206
+
207
+ .comment__body :global(p) {
208
+ margin: 0 0 0.75rem;
209
+ }
210
+
211
+ .comment__body :global(p:last-child) {
212
+ margin: 0;
213
+ }
214
+
215
+ .comment__reply-btn {
216
+ align-self: flex-start;
217
+ background: none;
218
+ border: none;
219
+ cursor: pointer;
220
+ font-size: 0.8rem;
221
+ font-family: var(--font-mono);
222
+ color: var(--color-muted);
223
+ padding: 0;
224
+ transition: color 0.15s;
225
+ }
226
+
227
+ .comment__reply-btn:hover {
228
+ color: var(--color-accent);
229
+ }
230
+
231
+ .comment__replies {
232
+ list-style: none;
233
+ padding: 0;
234
+ margin: 0.5rem 0 0;
235
+ display: flex;
236
+ flex-direction: column;
237
+ gap: 1.5rem;
238
+ }
239
+
240
+ @media (max-width: 768px) {
241
+ .comments {
242
+ padding: 0 1.25rem;
243
+ }
244
+ }
245
+ </style>
@@ -0,0 +1,101 @@
1
+ ---
2
+ import type { PaginationProps } from "../types";
3
+
4
+ interface Props extends PaginationProps {}
5
+
6
+ const { currentPage, totalPages, basePath, blogBase, t } = Astro.props;
7
+
8
+ const pages = Array.from({ length: totalPages }, (_, i) => i + 1);
9
+ ---
10
+
11
+ {totalPages > 1 && (
12
+ <nav class="pagination" aria-label="Blog pagination">
13
+
14
+ {currentPage > 1 && (
15
+
16
+ <a href={currentPage === 2 ? blogBase : `${basePath}${currentPage - 1}/`}
17
+ class="pagination__btn"
18
+ >
19
+ ← {t.blog.btn_prev}
20
+ </a>
21
+ )}
22
+
23
+ <div class="pagination__pages">
24
+ {pages.map((pageNum) => (
25
+
26
+ <a href={pageNum === 1 ? blogBase : `${basePath}${pageNum}/`}
27
+ class={`pagination__page ${pageNum === currentPage ? "pagination__page--active" : ""}`}
28
+ aria-current={pageNum === currentPage ? "page" : undefined}
29
+ >
30
+ {pageNum}
31
+ </a>
32
+ ))}
33
+ </div>
34
+
35
+ {currentPage < totalPages && (
36
+ <a href={`${basePath}${currentPage + 1}/`} class="pagination__btn">
37
+ {t.blog.btn_next} →
38
+ </a>
39
+ )}
40
+
41
+ </nav>
42
+ )}
43
+
44
+ <style>
45
+ .pagination {
46
+ display: flex;
47
+ align-items: center;
48
+ justify-content: center;
49
+ gap: 0.75rem;
50
+ flex-wrap: wrap;
51
+ padding-top: 3rem;
52
+ border-top: 2px solid var(--color-gray-200);
53
+ }
54
+
55
+ .pagination__btn {
56
+ padding: 0.6rem 1.25rem;
57
+ background-color: var(--color-black);
58
+ color: var(--color-white);
59
+ font-family: var(--font-heading);
60
+ font-size: 0.9rem;
61
+ font-weight: 700;
62
+ text-transform: uppercase;
63
+ letter-spacing: 0.08em;
64
+ text-decoration: none;
65
+ transition: background-color 0.2s ease, color 0.2s ease;
66
+ }
67
+
68
+ .pagination__btn:hover {
69
+ background-color: var(--color-yellow);
70
+ color: var(--color-black);
71
+ }
72
+
73
+ .pagination__pages {
74
+ display: flex;
75
+ gap: 0.4rem;
76
+ }
77
+
78
+ .pagination__page {
79
+ width: 2.5rem;
80
+ height: 2.5rem;
81
+ display: grid;
82
+ place-items: center;
83
+ font-weight: 700;
84
+ font-size: 0.9rem;
85
+ color: var(--color-gray-600);
86
+ border: 2px solid var(--color-gray-200);
87
+ text-decoration: none;
88
+ transition: border-color 0.2s ease, color 0.2s ease;
89
+ }
90
+
91
+ .pagination__page:hover {
92
+ border-color: var(--color-black);
93
+ color: var(--color-black);
94
+ }
95
+
96
+ .pagination__page--active {
97
+ background-color: var(--color-yellow);
98
+ border-color: var(--color-yellow);
99
+ color: var(--color-black);
100
+ }
101
+ </style>
@@ -0,0 +1,14 @@
1
+ // ─────────────────────────────────────────────────────────────
2
+ // astro-blog-kit · components/index.ts
3
+ // Punto de entrada de todos los componentes.
4
+ // Importa desde: 'astro-blog-kit/components'
5
+ // ─────────────────────────────────────────────────────────────
6
+
7
+ export { default as BlogCard } from "./BlogCard.astro";
8
+ export { default as BlogList } from "./BlogList.astro";
9
+ export { default as BlogPost } from "./BlogPost.astro";
10
+ export { default as Pagination } from "./Pagination.astro";
11
+ export { default as Comments } from "./Comments.astro";
12
+ export { default as CommentForm } from "./CommentForm.astro";
13
+
14
+ export { MagazineLayout, GridLayout, ListLayout } from "./BlogList/index.ts";
@@ -0,0 +1,65 @@
1
+ // ─────────────────────────────────────────────────────────────
2
+ // astro-blog-kit · define-config.ts
3
+ // Función helper para tipar la config del blog.
4
+ // ─────────────────────────────────────────────────────────────
5
+
6
+ import type { BlogKitConfig, BlogTheme } from "./types";
7
+
8
+ export interface BlogConfig {
9
+ /** URL de tu WordPress. Ej: https://cms.tudominio.com */
10
+ wpUrl: string;
11
+ /** Posts por página. @default 5 */
12
+ postsPerPage?: number;
13
+ /** Layout por defecto. @default "magazine" */
14
+ defaultLayout?: "grid" | "list" | "magazine";
15
+ /** Locale por defecto. @default "en" */
16
+ locale?: string;
17
+ /** Tema visual */
18
+ theme?: BlogTheme;
19
+ /** Configuración de i18n */
20
+ i18n?: {
21
+ locales: string[];
22
+ defaultLocale: string;
23
+ };
24
+ }
25
+
26
+ /**
27
+ * Define la configuración del blog con tipado completo.
28
+ * Genera blog.config.ts en la raíz del proyecto.
29
+ *
30
+ * @example
31
+ * ```ts
32
+ * // blog.config.ts
33
+ * import { defineBlogConfig } from 'astro-blog-kit';
34
+ *
35
+ * export default defineBlogConfig({
36
+ * wpUrl: 'https://cms.tudominio.com',
37
+ * postsPerPage: 5,
38
+ * defaultLayout: 'magazine',
39
+ * locale: 'en',
40
+ * theme: {
41
+ * accent: '#facc15',
42
+ * },
43
+ * });
44
+ * ```
45
+ */
46
+ export function defineBlogConfig(config: BlogConfig): BlogConfig {
47
+ return {
48
+ postsPerPage: 5,
49
+ defaultLayout: "magazine",
50
+ locale: "en",
51
+ ...config,
52
+ };
53
+ }
54
+
55
+ /**
56
+ * Convierte BlogConfig a BlogKitConfig para usar en astro.config.mjs
57
+ */
58
+ export function toBlogKitConfig(config: BlogConfig): BlogKitConfig {
59
+ return {
60
+ postsPerPage: config.postsPerPage,
61
+ defaultLayout: config.defaultLayout,
62
+ theme: config.theme,
63
+ i18n: config.i18n,
64
+ };
65
+ }
package/index.ts ADDED
@@ -0,0 +1,41 @@
1
+ // ─────────────────────────────────────────────────────────────
2
+ // astro-blog-kit · index.ts
3
+ // Punto de entrada principal del paquete.
4
+ // Importa desde: 'astro-blog-kit'
5
+ // ─────────────────────────────────────────────────────────────
6
+
7
+ // Tipos
8
+ export type {
9
+ BlogPost,
10
+ BlogListLayout,
11
+ BlogListProps,
12
+ BlogPostProps,
13
+ BlogTranslations,
14
+ BlogKitConfig,
15
+ I18nConfig,
16
+ PaginationProps,
17
+ PageStaticPath,
18
+ PostStaticPath,
19
+ } from "./types";
20
+
21
+ // Utils
22
+ export {
23
+ // i18n
24
+ isI18nEnabled,
25
+ getLang,
26
+ useTranslations,
27
+ getBlogBase,
28
+ getPageBase,
29
+ getDateLocale,
30
+ // slug
31
+ getStaticPathsForPosts,
32
+ getStaticPathsForPages,
33
+ toSlug,
34
+ estimateReadingTime,
35
+ getFeaturedImageUrl,
36
+ // collections
37
+ normalizeWPPost,
38
+ sortPostsByDate,
39
+ filterPostsByLang,
40
+ preparePosts,
41
+ } from "./utils/index.ts";
package/integration.ts ADDED
@@ -0,0 +1,137 @@
1
+ // ─────────────────────────────────────────────────────────────
2
+ // astro-blog-kit · integration.ts
3
+ // ─────────────────────────────────────────────────────────────
4
+
5
+ import type { AstroIntegration } from "astro";
6
+ import type { BlogKitConfig, BlogTheme } from "./types";
7
+
8
+ /**
9
+ * Genera el bloque de CSS variables a partir del tema.
10
+ */
11
+ function generateThemeCSS(theme: BlogTheme = {}): string {
12
+ const t = {
13
+ accent: theme.accent ?? "#facc15",
14
+ background: theme.background ?? "#ffffff",
15
+ surface: theme.surface ?? "#f8f8f8",
16
+ text: theme.text ?? "#0a0a0a",
17
+ muted: theme.muted ?? "#6b7280",
18
+ mutedLight: theme.mutedLight ?? "#9ca3af",
19
+ border: theme.border ?? "#e5e7eb",
20
+ black: theme.black ?? "#0a0a0a",
21
+ white: theme.white ?? "#ffffff",
22
+ fontHeading: theme.fontHeading ?? "Georgia, serif",
23
+ fontBody: theme.fontBody ?? "system-ui, sans-serif",
24
+ fontMono: theme.fontMono ?? "monospace",
25
+ fontDisplay: theme.fontDisplay ?? "Georgia, serif",
26
+ containerMax: theme.containerMax ?? "1200px",
27
+ };
28
+
29
+ return `
30
+ :root {
31
+ --color-accent: ${t.accent};
32
+ --color-bg: ${t.background};
33
+ --color-surface: ${t.surface};
34
+ --color-text: ${t.text};
35
+ --color-muted: ${t.muted};
36
+ --color-muted-light: ${t.mutedLight};
37
+ --color-border: ${t.border};
38
+ --color-black: ${t.black};
39
+ --color-white: ${t.white};
40
+ --color-yellow: ${t.accent};
41
+ --color-gray-100: #f3f4f6;
42
+ --color-gray-200: #e5e7eb;
43
+ --color-gray-300: #d1d5db;
44
+ --color-gray-400: #9ca3af;
45
+ --color-gray-600: #4b5563;
46
+ --font-heading: ${t.fontHeading};
47
+ --font-body: ${t.fontBody};
48
+ --font-mono: ${t.fontMono};
49
+ --font-display: ${t.fontDisplay};
50
+ --container-max: ${t.containerMax};
51
+ --transition: all 0.2s ease;
52
+ }
53
+
54
+ *, *::before, *::after {
55
+ box-sizing: border-box;
56
+ margin: 0;
57
+ padding: 0;
58
+ }
59
+
60
+ body {
61
+ font-family: var(--font-body);
62
+ background-color: var(--color-bg);
63
+ color: var(--color-text);
64
+ line-height: 1.6;
65
+ }
66
+ `;
67
+ }
68
+
69
+ /**
70
+ * Integración principal de astro-blog-kit.
71
+ *
72
+ * @example
73
+ * ```js
74
+ * // astro.config.mjs
75
+ * import { defineConfig } from 'astro/config';
76
+ * import { blogKit } from 'astro-blog-kit/integration';
77
+ *
78
+ * export default defineConfig({
79
+ * integrations: [
80
+ * blogKit({
81
+ * postsPerPage: 6,
82
+ * defaultLayout: 'magazine',
83
+ * theme: {
84
+ * accent: '#facc15',
85
+ * fontHeading: 'Inter, sans-serif',
86
+ * },
87
+ * }),
88
+ * ],
89
+ * });
90
+ * ```
91
+ */
92
+ export function blogKit(config: BlogKitConfig = {}): AstroIntegration {
93
+ const resolvedConfig: Required<BlogKitConfig> = {
94
+ postsPerPage: config.postsPerPage ?? 5,
95
+ defaultLayout: config.defaultLayout ?? "magazine",
96
+ collectionName: config.collectionName ?? "blog",
97
+ i18n: config.i18n ?? { locales: [], defaultLocale: "en" },
98
+ theme: config.theme ?? {},
99
+ };
100
+
101
+ return {
102
+ name: "astro-blog-kit",
103
+
104
+ hooks: {
105
+ "astro:config:setup": ({ injectScript, logger }) => {
106
+ logger.info(
107
+ `astro-blog-kit initialized — layout: ${resolvedConfig.defaultLayout}, postsPerPage: ${resolvedConfig.postsPerPage}`
108
+ );
109
+
110
+ // Inyecta CSS variables del tema globalmente
111
+ const themeCSS = generateThemeCSS(resolvedConfig.theme);
112
+ injectScript("head-inline", `
113
+ (() => {
114
+ const style = document.createElement('style');
115
+ style.id = 'astro-blog-kit-theme';
116
+ style.textContent = \`${themeCSS.replace(/`/g, "\\`")}\`;
117
+ document.head.appendChild(style);
118
+ })();
119
+ `);
120
+
121
+ // Inyecta config global
122
+ injectScript(
123
+ "page-ssr",
124
+ `globalThis.__BLOG_KIT_CONFIG__ = ${JSON.stringify(resolvedConfig)};`
125
+ );
126
+ },
127
+
128
+ "astro:config:done": ({ logger }) => {
129
+ if (resolvedConfig.i18n.locales.length > 0) {
130
+ logger.info(
131
+ `i18n enabled — locales: [${resolvedConfig.i18n.locales.join(", ")}], default: ${resolvedConfig.i18n.defaultLocale}`
132
+ );
133
+ }
134
+ },
135
+ },
136
+ };
137
+ }
package/package.json ADDED
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "astro-blog-kit",
3
+ "version": "0.1.0",
4
+ "description": "A ready-to-use blog system for Astro with WordPress headless support, optional i18n, multiple layouts, and a comment system.",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "exports": {
8
+ ".": "./index.ts",
9
+ "./integration": "./integration.ts",
10
+ "./components": "./components/index.ts",
11
+ "./utils": "./utils/index.ts"
12
+ },
13
+ "files": [
14
+ "index.ts",
15
+ "integration.ts",
16
+ "types.ts",
17
+ "virtual.d.ts",
18
+ "cli.ts",
19
+ "define-config.ts",
20
+ "templates",
21
+ "components",
22
+ "utils"
23
+ ],
24
+ "keywords": [
25
+ "astro",
26
+ "blog",
27
+ "wordpress",
28
+ "i18n",
29
+ "astro-integration",
30
+ "astro-component",
31
+ "headless-cms"
32
+ ],
33
+ "peerDependencies": {
34
+ "astro": "^4.0.0 || ^5.0.0"
35
+ },
36
+ "devDependencies": {
37
+ "@clack/prompts": "latest",
38
+ "@types/node": "latest",
39
+ "astro": "^6.3.1"
40
+ }
41
+ }