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/utils/slug.ts ADDED
@@ -0,0 +1,106 @@
1
+ // ─────────────────────────────────────────────────────────────
2
+ // astro-blog-kit · utils/slug.ts
3
+ // ─────────────────────────────────────────────────────────────
4
+
5
+ import type { BlogPost, BlogKitConfig, PageStaticPath, PostStaticPath } from "../types";
6
+
7
+ // ── getStaticPaths para posts individuales ────────────────────
8
+
9
+ /**
10
+ * Genera el array para getStaticPaths en [...slug].astro
11
+ *
12
+ * @example
13
+ * export const getStaticPaths = () => getStaticPathsForPosts(MOCK_POSTS);
14
+ */
15
+ export function getStaticPathsForPosts(
16
+ posts: BlogPost[]
17
+ ): PostStaticPath[] {
18
+ return posts.map((post) => ({
19
+ params: { slug: post.slug },
20
+ props: { post },
21
+ }));
22
+ }
23
+
24
+ // ── getStaticPaths para páginas de listado ────────────────────
25
+
26
+ /**
27
+ * Genera el array para getStaticPaths en [page].astro
28
+ *
29
+ * @example
30
+ * export const getStaticPaths = () =>
31
+ * getStaticPathsForPages(MOCK_POSTS, { postsPerPage: 5 });
32
+ */
33
+ export function getStaticPathsForPages(
34
+ allPosts: BlogPost[],
35
+ options: { postsPerPage?: number; config?: BlogKitConfig } = {}
36
+ ): PageStaticPath[] {
37
+ const postsPerPage =
38
+ options.postsPerPage ?? options.config?.postsPerPage ?? 5;
39
+
40
+ const totalPages = Math.ceil(allPosts.length / postsPerPage);
41
+
42
+ return Array.from({ length: totalPages }, (_, i) => {
43
+ const pageNumber = i + 1;
44
+ const start = i * postsPerPage;
45
+ const end = start + postsPerPage;
46
+
47
+ return {
48
+ params: { page: String(pageNumber) },
49
+ props: {
50
+ posts: allPosts.slice(start, end),
51
+ currentPage: pageNumber,
52
+ totalPages,
53
+ },
54
+ };
55
+ });
56
+ }
57
+
58
+ // ── Utilidades ────────────────────────────────────────────────
59
+
60
+ /**
61
+ * Sanitiza un string para usarlo como slug URL-safe.
62
+ *
63
+ * @example
64
+ * toSlug("¡Hola Mundo!") // → "hola-mundo"
65
+ */
66
+ export function toSlug(text: string): string {
67
+ return text
68
+ .toLowerCase()
69
+ .normalize("NFD")
70
+ .replace(/[\u0300-\u036f]/g, "")
71
+ .replace(/[^a-z0-9\s-]/g, "")
72
+ .trim()
73
+ .replace(/[\s_-]+/g, "-")
74
+ .replace(/^-+|-+$/g, "");
75
+ }
76
+
77
+ /**
78
+ * Calcula el tiempo de lectura estimado de un contenido HTML o texto plano.
79
+ *
80
+ * @param content - HTML o texto del post
81
+ * @param wordsPerMinute - @default 200
82
+ */
83
+ export function estimateReadingTime(
84
+ content: string,
85
+ wordsPerMinute = 200
86
+ ): number {
87
+ const plainText = content.replace(/<[^>]*>/g, " ");
88
+ const wordCount = plainText
89
+ .trim()
90
+ .split(/\s+/)
91
+ .filter((w) => w.length > 0).length;
92
+
93
+ return Math.max(1, Math.ceil(wordCount / wordsPerMinute));
94
+ }
95
+
96
+ /**
97
+ * Extrae la URL de la imagen destacada de un post.
98
+ * Compatible con WordPress (_embedded) y Content Collections (heroImage).
99
+ */
100
+ export function getFeaturedImageUrl(post: BlogPost): string | undefined {
101
+ return (
102
+ post._embedded?.["wp:featuredmedia"]?.[0]?.source_url ??
103
+ post.heroImage ??
104
+ undefined
105
+ );
106
+ }
@@ -0,0 +1,162 @@
1
+ // ─────────────────────────────────────────────────────────────
2
+ // astro-blog-kit · utils/wordpress.ts
3
+ // ─────────────────────────────────────────────────────────────
4
+
5
+
6
+ import type { BlogPost } from "../types";
7
+ import { normalizeWPPost } from "./collections";
8
+
9
+ export interface WPComment {
10
+ id: number;
11
+ parent: number;
12
+ author_name: string;
13
+ author_url?: string;
14
+ date: string;
15
+ content: { rendered: string };
16
+ status: "approved" | "hold" | "spam" | "trash";
17
+ author_avatar_urls?: Record<string, string>;
18
+ }
19
+
20
+ export interface WPCategory {
21
+ id: number;
22
+ name: string;
23
+ slug: string;
24
+ count: number;
25
+ description?: string;
26
+ }
27
+
28
+ export interface WPTag {
29
+ id: number;
30
+ name: string;
31
+ slug: string;
32
+ count: number;
33
+ }
34
+
35
+ export interface FetchPostsOptions {
36
+ perPage?: number;
37
+ page?: number;
38
+ lang?: string;
39
+ categorySlug?: string;
40
+ tag?: string;
41
+ search?: string;
42
+ }
43
+
44
+ export interface SubmitCommentPayload {
45
+ post: number;
46
+ author_name: string;
47
+ author_email: string;
48
+ content: string;
49
+ parent?: number;
50
+ }
51
+
52
+ export function createWPClient(baseUrl: string) {
53
+ const api = baseUrl.replace(/\/$/, "") + "/wp-json/wp/v2";
54
+
55
+ async function getPosts(options: FetchPostsOptions = {}): Promise<{
56
+ posts: BlogPost[];
57
+ total: number;
58
+ totalPages: number;
59
+ }> {
60
+ const { perPage = 10, page = 1, lang, categorySlug, search } = options;
61
+
62
+ const params = new URLSearchParams({
63
+ _embed: "true",
64
+ per_page: String(perPage),
65
+ page: String(page),
66
+ status: "publish",
67
+ });
68
+
69
+ if (lang) params.set("lang", lang);
70
+ if (search) params.set("search", search);
71
+
72
+ if (categorySlug) {
73
+ const catId = await getCategoryIdBySlug(categorySlug);
74
+ if (catId) params.set("categories", String(catId));
75
+ }
76
+
77
+ const response = await fetch(`${api}/posts?${params}`);
78
+ if (!response.ok) {
79
+ throw new Error(`WP API error: ${response.status} ${response.statusText}`);
80
+ }
81
+
82
+ const total = Number(response.headers.get("X-WP-Total") ?? 0);
83
+ const totalPages = Number(response.headers.get("X-WP-TotalPages") ?? 1);
84
+ const data = await response.json();
85
+
86
+ return { posts: data.map(normalizeWPPost), total, totalPages };
87
+ }
88
+
89
+ async function getPost(slug: string): Promise<BlogPost | undefined> {
90
+ const params = new URLSearchParams({
91
+ _embed: "true",
92
+ slug,
93
+ status: "publish",
94
+ });
95
+
96
+ const response = await fetch(`${api}/posts?${params}`);
97
+ if (!response.ok) {
98
+ throw new Error(`WP API error: ${response.status} ${response.statusText}`);
99
+ }
100
+
101
+ const data = await response.json();
102
+ if (!Array.isArray(data) || data.length === 0) return undefined;
103
+ return normalizeWPPost(data[0]);
104
+ }
105
+
106
+ async function getAllPosts(lang?: string): Promise<BlogPost[]> {
107
+ const firstPage = await getPosts({ perPage: 100, page: 1, lang });
108
+ const allPosts = [...firstPage.posts];
109
+
110
+ if (firstPage.totalPages > 1) {
111
+ const rest = await Promise.all(
112
+ Array.from({ length: firstPage.totalPages - 1 }, (_, i) =>
113
+ getPosts({ perPage: 100, page: i + 2, lang })
114
+ )
115
+ );
116
+ rest.forEach((r) => allPosts.push(...r.posts));
117
+ }
118
+
119
+ return allPosts;
120
+ }
121
+
122
+ async function getComments(postId: number): Promise<WPComment[]> {
123
+ const params = new URLSearchParams({
124
+ post: String(postId),
125
+ per_page: "100",
126
+ orderby: "date",
127
+ order: "asc",
128
+ });
129
+
130
+ const response = await fetch(`${api}/comments?${params}`);
131
+ if (!response.ok) {
132
+ throw new Error(`WP API error: ${response.status} ${response.statusText}`);
133
+ }
134
+
135
+ return response.json();
136
+ }
137
+
138
+ async function getCategories(): Promise<WPCategory[]> {
139
+ const response = await fetch(`${api}/categories?per_page=100&hide_empty=true`);
140
+ if (!response.ok) {
141
+ throw new Error(`WP API error: ${response.status} ${response.statusText}`);
142
+ }
143
+ return response.json();
144
+ }
145
+
146
+ async function getCategoryIdBySlug(slug: string): Promise<number | undefined> {
147
+ const response = await fetch(`${api}/categories?slug=${slug}`);
148
+ if (!response.ok) return undefined;
149
+ const data = await response.json();
150
+ return data[0]?.id;
151
+ }
152
+
153
+ async function getTags(): Promise<WPTag[]> {
154
+ const response = await fetch(`${api}/tags?per_page=100&hide_empty=true`);
155
+ if (!response.ok) {
156
+ throw new Error(`WP API error: ${response.status} ${response.statusText}`);
157
+ }
158
+ return response.json();
159
+ }
160
+
161
+ return { getPosts, getPost, getAllPosts, getComments, getCategories, getTags };
162
+ }
package/virtual.d.ts ADDED
@@ -0,0 +1,19 @@
1
+ // ─────────────────────────────────────────────────────────────
2
+ // astro-blog-kit · virtual.d.ts
3
+ // Tipos para módulos virtuales y globals inyectados.
4
+ // ─────────────────────────────────────────────────────────────
5
+
6
+ import type { BlogKitConfig } from "./types";
7
+
8
+ // ── Global inyectado por la integration ──────────────────────
9
+
10
+ declare global {
11
+ var __BLOG_KIT_CONFIG__: Required<BlogKitConfig>;
12
+ }
13
+
14
+ // ── Módulo virtual (para uso futuro) ─────────────────────────
15
+
16
+ declare module "virtual:astro-blog-kit/config" {
17
+ const config: Required<BlogKitConfig>;
18
+ export default config;
19
+ }