@centrolabs/ngx-blogdown 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.
package/README.md ADDED
@@ -0,0 +1,143 @@
1
+ # @centrolabs/ngx-blogdown
2
+
3
+ A lightweight Angular library for building markdown-powered blogs. You handle the layout, we handle the pipeline.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @centrolabs/ngx-blogdown marked
9
+ ```
10
+
11
+ ## Setup
12
+
13
+ ### 1. Provide the library
14
+
15
+ ```ts
16
+ import { provideHttpClient } from '@angular/common/http';
17
+ import { provideNgBlogdown } from '@centrolabs/ngx-blogdown';
18
+
19
+ bootstrapApplication(AppComponent, {
20
+ providers: [
21
+ provideHttpClient(),
22
+ provideNgBlogdown({
23
+ indexPath: '/blog/index.json',
24
+ postsDir: '/blog/posts',
25
+ }),
26
+ ],
27
+ });
28
+ ```
29
+
30
+ ### 2. Write your posts
31
+
32
+ Create markdown files with YAML frontmatter in your posts directory:
33
+
34
+ ```markdown
35
+ title: My First Post
36
+ date: 2026-01-15
37
+ cover: /images/first-post.png
38
+ tagline: A short description of this post
39
+ author: Jane Doe
40
+ ---
41
+ # My First Post
42
+
43
+ Write your content here using **markdown**.
44
+ ```
45
+
46
+ ### 3. Generate the index
47
+
48
+ Use the bundled CLI to generate a JSON index from your markdown files:
49
+
50
+ ```bash
51
+ npx ngx-blogdown-index --postsDir src/content/posts --out src/content/index.json
52
+ ```
53
+
54
+ This scans all `.md` files, extracts frontmatter, and outputs a sorted JSON index.
55
+
56
+ ## Usage
57
+
58
+ Inject `BlogService` anywhere you need blog data:
59
+
60
+ ```ts
61
+ import { BlogService, BlogPostMeta } from '@centrolabs/ngx-blogdown';
62
+
63
+ @Component({
64
+ template: `
65
+ @for (post of posts; track post.slug) {
66
+ <article>
67
+ <h2>{{ post.title }}</h2>
68
+ <p>{{ post.tagline }}</p>
69
+ </article>
70
+ }
71
+ `,
72
+ changeDetection: ChangeDetectionStrategy.OnPush,
73
+ })
74
+ export class BlogListComponent {
75
+ private blogService = inject(BlogService);
76
+ posts: BlogPostMeta[] = [];
77
+
78
+ async ngOnInit() {
79
+ this.posts = await this.blogService.getPosts();
80
+ }
81
+ }
82
+ ```
83
+
84
+ ### Rendering a single post
85
+
86
+ ```ts
87
+ const post = await this.blogService.getPost('my-first-post');
88
+ if (post) {
89
+ // post.htmlContent contains the rendered HTML
90
+ }
91
+ ```
92
+
93
+ ### SEO tags
94
+
95
+ ```ts
96
+ const meta = posts.find(p => p.slug === 'my-first-post')!;
97
+ const seo = this.blogService.getSeoTags(meta);
98
+ // { title, description, image, date, author }
99
+ ```
100
+
101
+ ## API
102
+
103
+ ### `provideNgBlogdown(config)`
104
+
105
+ Registers the library providers. Call this in your application bootstrap.
106
+
107
+ | Parameter | Type | Description |
108
+ | ------------------ | -------- | ---------------------------------------- |
109
+ | `config.indexPath` | `string` | Path to the JSON index file |
110
+ | `config.postsDir` | `string` | Directory where markdown files are served |
111
+
112
+ ### `BlogService`
113
+
114
+ | Method | Returns | Description |
115
+ | ------------------------ | -------------------------- | ------------------------------------------------ |
116
+ | `getPosts()` | `Promise<BlogPostMeta[]>` | Fetches all post metadata. Cached after first call. |
117
+ | `getPost(slug)` | `Promise<BlogPost \| null>` | Fetches and renders a single post by slug. |
118
+ | `getSeoTags(postMeta)` | `SeoTags` | Derives SEO meta tags from post metadata. |
119
+
120
+ ### CLI: `ngx-blogdown-index`
121
+
122
+ ```
123
+ ngx-blogdown-index --postsDir <path> --out <file>
124
+ ```
125
+
126
+ | Flag | Description |
127
+ | ------------ | ------------------------------------------ |
128
+ | `--postsDir` | Directory containing `.md` files |
129
+ | `--out` | Output path for the generated JSON index |
130
+
131
+ The generated index is sorted by date (newest first). Slugs are derived from filenames (lowercased, spaces replaced with hyphens).
132
+
133
+ ## Peer Dependencies
134
+
135
+ | Package | Version |
136
+ | ---------------- | --------- |
137
+ | `@angular/core` | `^20.3.0` |
138
+ | `@angular/common`| `^20.3.0` |
139
+ | `marked` | `^17.0.0` |
140
+
141
+ ## License
142
+
143
+ MIT
@@ -0,0 +1,104 @@
1
+ import * as i0 from '@angular/core';
2
+ import { inject, signal, Injectable, InjectionToken } from '@angular/core';
3
+ import { HttpClient } from '@angular/common/http';
4
+ import { firstValueFrom } from 'rxjs';
5
+ import { marked } from 'marked';
6
+
7
+ /**
8
+ * Core service for fetching and rendering markdown blog posts.
9
+ *
10
+ * Provided via {@link provideNgBlogdown}. Requires `HttpClient` to be available.
11
+ */
12
+ class BlogService {
13
+ http = inject(HttpClient);
14
+ config = inject(NG_BLOG_CONFIG);
15
+ indexCache = signal(null, ...(ngDevMode ? [{ debugName: "indexCache" }] : []));
16
+ /**
17
+ * Fetches the blog post index. Results are cached in memory after the first call.
18
+ *
19
+ * @returns All blog post metadata from the configured index path.
20
+ */
21
+ async getPosts() {
22
+ if (this.indexCache())
23
+ return this.indexCache();
24
+ const posts = await firstValueFrom(this.http.get(this.config.indexPath));
25
+ this.indexCache.set(posts);
26
+ return posts;
27
+ }
28
+ /**
29
+ * Fetches a single blog post by its slug, parses its markdown body into HTML,
30
+ * and strips the YAML frontmatter.
31
+ *
32
+ * @param slug - The URL-friendly post identifier to look up.
33
+ * @returns The full blog post with rendered HTML, or `null` if not found.
34
+ */
35
+ async getPost(slug) {
36
+ const posts = await this.getPosts();
37
+ const meta = posts.find((p) => p.slug === slug);
38
+ if (!meta)
39
+ return null;
40
+ const raw = await firstValueFrom(this.http.get(`${this.config.postsDir}/${encodeURIComponent(meta.filename)}`, {
41
+ responseType: 'text',
42
+ }));
43
+ const separatorMatch = raw.match(/^-{3,}$/m);
44
+ const headerEnd = separatorMatch ? separatorMatch.index : -1;
45
+ const body = headerEnd !== -1 ? raw.slice(headerEnd + separatorMatch[0].length).trim() : raw;
46
+ const htmlContent = await marked(body);
47
+ return { ...meta, htmlContent };
48
+ }
49
+ /**
50
+ * Derives SEO meta tags from a post's metadata.
51
+ *
52
+ * @param postMeta - The post metadata to extract tags from.
53
+ * @returns SEO-friendly tag values for use in `<meta>` elements.
54
+ */
55
+ getSeoTags(postMeta) {
56
+ return {
57
+ title: postMeta.title,
58
+ description: postMeta.tagline,
59
+ image: postMeta.cover,
60
+ date: postMeta.date,
61
+ author: postMeta.author ?? null,
62
+ };
63
+ }
64
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: BlogService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
65
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: BlogService });
66
+ }
67
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: BlogService, decorators: [{
68
+ type: Injectable
69
+ }] });
70
+
71
+ /** Injection token for supplying {@link NgBlogConfig} to the library. */
72
+ const NG_BLOG_CONFIG = new InjectionToken('NG_BLOG_CONFIG');
73
+ /**
74
+ * Provides the ngx-blogdown library with the given configuration.
75
+ *
76
+ * Register the returned providers in your application's bootstrap or route config.
77
+ *
78
+ * @param config - Paths to the blog index file and posts directory.
79
+ * @returns An array of providers including {@link BlogService} and the config token.
80
+ *
81
+ * @example
82
+ * ```ts
83
+ * bootstrapApplication(AppComponent, {
84
+ * providers: [
85
+ * provideHttpClient(),
86
+ * provideNgBlogdown({ indexPath: '/blog/index.json', postsDir: '/blog/posts' }),
87
+ * ],
88
+ * });
89
+ * ```
90
+ */
91
+ function provideNgBlogdown(config) {
92
+ return [{ provide: NG_BLOG_CONFIG, useValue: config }, BlogService];
93
+ }
94
+
95
+ /*
96
+ * Public API Surface of ng-blog
97
+ */
98
+
99
+ /**
100
+ * Generated bundle index. Do not edit.
101
+ */
102
+
103
+ export { BlogService, NG_BLOG_CONFIG, provideNgBlogdown };
104
+ //# sourceMappingURL=centrolabs-ngx-blogdown.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"centrolabs-ngx-blogdown.mjs","sources":["../../../projects/ngx-blogdown/src/lib/blog.service.ts","../../../projects/ngx-blogdown/src/lib/blog.config.ts","../../../projects/ngx-blogdown/src/public-api.ts","../../../projects/ngx-blogdown/src/centrolabs-ngx-blogdown.ts"],"sourcesContent":["import { inject, Injectable, signal } from '@angular/core';\nimport { HttpClient } from '@angular/common/http';\nimport { firstValueFrom } from 'rxjs';\nimport { marked } from 'marked';\nimport { BlogPost, BlogPostMeta, SeoTags } from './blog.models';\nimport { NG_BLOG_CONFIG } from './blog.config';\n\n/**\n * Core service for fetching and rendering markdown blog posts.\n *\n * Provided via {@link provideNgBlogdown}. Requires `HttpClient` to be available.\n */\n@Injectable()\nexport class BlogService {\n private http = inject(HttpClient);\n private config = inject(NG_BLOG_CONFIG);\n\n private indexCache = signal<BlogPostMeta[] | null>(null);\n\n /**\n * Fetches the blog post index. Results are cached in memory after the first call.\n *\n * @returns All blog post metadata from the configured index path.\n */\n async getPosts(): Promise<BlogPostMeta[]> {\n if (this.indexCache()) return this.indexCache()!;\n\n const posts = await firstValueFrom(this.http.get<BlogPostMeta[]>(this.config.indexPath));\n this.indexCache.set(posts);\n return posts;\n }\n\n /**\n * Fetches a single blog post by its slug, parses its markdown body into HTML,\n * and strips the YAML frontmatter.\n *\n * @param slug - The URL-friendly post identifier to look up.\n * @returns The full blog post with rendered HTML, or `null` if not found.\n */\n async getPost(slug: string): Promise<BlogPost | null> {\n const posts = await this.getPosts();\n const meta = posts.find((p) => p.slug === slug);\n if (!meta) return null;\n\n const raw = await firstValueFrom(\n this.http.get(`${this.config.postsDir}/${encodeURIComponent(meta.filename)}`, {\n responseType: 'text',\n }),\n );\n\n const separatorMatch = raw.match(/^-{3,}$/m);\n const headerEnd = separatorMatch ? separatorMatch.index! : -1;\n const body = headerEnd !== -1 ? raw.slice(headerEnd + separatorMatch![0].length).trim() : raw;\n\n const htmlContent = await marked(body);\n\n return { ...meta, htmlContent };\n }\n\n /**\n * Derives SEO meta tags from a post's metadata.\n *\n * @param postMeta - The post metadata to extract tags from.\n * @returns SEO-friendly tag values for use in `<meta>` elements.\n */\n getSeoTags(postMeta: BlogPostMeta): SeoTags {\n return {\n title: postMeta.title,\n description: postMeta.tagline,\n image: postMeta.cover,\n date: postMeta.date,\n author: postMeta.author ?? null,\n };\n }\n}\n","import { InjectionToken, Provider } from '@angular/core';\nimport { NgBlogConfig } from './blog.models';\nimport { BlogService } from './blog.service';\n\n/** Injection token for supplying {@link NgBlogConfig} to the library. */\nexport const NG_BLOG_CONFIG = new InjectionToken<NgBlogConfig>('NG_BLOG_CONFIG');\n\n/**\n * Provides the ngx-blogdown library with the given configuration.\n *\n * Register the returned providers in your application's bootstrap or route config.\n *\n * @param config - Paths to the blog index file and posts directory.\n * @returns An array of providers including {@link BlogService} and the config token.\n *\n * @example\n * ```ts\n * bootstrapApplication(AppComponent, {\n * providers: [\n * provideHttpClient(),\n * provideNgBlogdown({ indexPath: '/blog/index.json', postsDir: '/blog/posts' }),\n * ],\n * });\n * ```\n */\nexport function provideNgBlogdown(config: NgBlogConfig): Provider[] {\n return [{ provide: NG_BLOG_CONFIG, useValue: config }, BlogService];\n}\n","/*\n * Public API Surface of ng-blog\n */\n\nexport * from './lib/blog.models';\nexport * from './lib/blog.config';\nexport * from './lib/blog.service';\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './public-api';\n"],"names":[],"mappings":";;;;;;AAOA;;;;AAIG;MAEU,WAAW,CAAA;AACd,IAAA,IAAI,GAAG,MAAM,CAAC,UAAU,CAAC;AACzB,IAAA,MAAM,GAAG,MAAM,CAAC,cAAc,CAAC;AAE/B,IAAA,UAAU,GAAG,MAAM,CAAwB,IAAI,sDAAC;AAExD;;;;AAIG;AACH,IAAA,MAAM,QAAQ,GAAA;QACZ,IAAI,IAAI,CAAC,UAAU,EAAE;AAAE,YAAA,OAAO,IAAI,CAAC,UAAU,EAAG;AAEhD,QAAA,MAAM,KAAK,GAAG,MAAM,cAAc,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAiB,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC;AACxF,QAAA,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,KAAK,CAAC;AAC1B,QAAA,OAAO,KAAK;IACd;AAEA;;;;;;AAMG;IACH,MAAM,OAAO,CAAC,IAAY,EAAA;AACxB,QAAA,MAAM,KAAK,GAAG,MAAM,IAAI,CAAC,QAAQ,EAAE;AACnC,QAAA,MAAM,IAAI,GAAG,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,IAAI,KAAK,IAAI,CAAC;AAC/C,QAAA,IAAI,CAAC,IAAI;AAAE,YAAA,OAAO,IAAI;QAEtB,MAAM,GAAG,GAAG,MAAM,cAAc,CAC9B,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAA,EAAG,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAA,CAAA,EAAI,kBAAkB,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAA,CAAE,EAAE;AAC5E,YAAA,YAAY,EAAE,MAAM;AACrB,SAAA,CAAC,CACH;QAED,MAAM,cAAc,GAAG,GAAG,CAAC,KAAK,CAAC,UAAU,CAAC;AAC5C,QAAA,MAAM,SAAS,GAAG,cAAc,GAAG,cAAc,CAAC,KAAM,GAAG,CAAC,CAAC;AAC7D,QAAA,MAAM,IAAI,GAAG,SAAS,KAAK,CAAC,CAAC,GAAG,GAAG,CAAC,KAAK,CAAC,SAAS,GAAG,cAAe,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,GAAG,GAAG;AAE7F,QAAA,MAAM,WAAW,GAAG,MAAM,MAAM,CAAC,IAAI,CAAC;AAEtC,QAAA,OAAO,EAAE,GAAG,IAAI,EAAE,WAAW,EAAE;IACjC;AAEA;;;;;AAKG;AACH,IAAA,UAAU,CAAC,QAAsB,EAAA;QAC/B,OAAO;YACL,KAAK,EAAE,QAAQ,CAAC,KAAK;YACrB,WAAW,EAAE,QAAQ,CAAC,OAAO;YAC7B,KAAK,EAAE,QAAQ,CAAC,KAAK;YACrB,IAAI,EAAE,QAAQ,CAAC,IAAI;AACnB,YAAA,MAAM,EAAE,QAAQ,CAAC,MAAM,IAAI,IAAI;SAChC;IACH;wGA5DW,WAAW,EAAA,IAAA,EAAA,EAAA,EAAA,MAAA,EAAA,EAAA,CAAA,eAAA,CAAA,UAAA,EAAA,CAAA;4GAAX,WAAW,EAAA,CAAA;;4FAAX,WAAW,EAAA,UAAA,EAAA,CAAA;kBADvB;;;ACRD;MACa,cAAc,GAAG,IAAI,cAAc,CAAe,gBAAgB;AAE/E;;;;;;;;;;;;;;;;;AAiBG;AACG,SAAU,iBAAiB,CAAC,MAAoB,EAAA;AACpD,IAAA,OAAO,CAAC,EAAE,OAAO,EAAE,cAAc,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,WAAW,CAAC;AACrE;;AC3BA;;AAEG;;ACFH;;AAEG;;;;"}
package/index.d.ts ADDED
@@ -0,0 +1,104 @@
1
+ import * as i0 from '@angular/core';
2
+ import { InjectionToken, Provider } from '@angular/core';
3
+
4
+ /** Metadata for a blog post, as stored in the index JSON file. */
5
+ interface BlogPostMeta {
6
+ /** URL-friendly identifier derived from the filename. */
7
+ slug: string;
8
+ /** Original markdown filename including extension. */
9
+ filename: string;
10
+ /** Display title of the post. */
11
+ title: string;
12
+ /** Publication date as an ISO date string (e.g. `"2026-01-15"`). */
13
+ date: string;
14
+ /** URL or path to the post's cover image. */
15
+ cover: string;
16
+ /** Short summary or subtitle for the post. */
17
+ tagline: string;
18
+ /** Author name, or `null` if not specified. */
19
+ author: string | null;
20
+ }
21
+ /** A full blog post including its rendered HTML content. */
22
+ interface BlogPost extends BlogPostMeta {
23
+ /** The post body rendered from markdown to HTML. */
24
+ htmlContent: string;
25
+ }
26
+ /** Configuration for the ngx-blogdown library. */
27
+ interface NgBlogConfig {
28
+ /** Path to the JSON index file containing all post metadata. */
29
+ indexPath: string;
30
+ /** Directory path where markdown post files are served from. */
31
+ postsDir: string;
32
+ }
33
+ /** SEO meta tag values extracted from a blog post. */
34
+ interface SeoTags {
35
+ /** Page title. */
36
+ title: string | null;
37
+ /** Meta description, derived from the post's tagline. */
38
+ description: string | null;
39
+ /** Open Graph image URL, derived from the post's cover. */
40
+ image: string | null;
41
+ /** Publication date. */
42
+ date: string | null;
43
+ /** Author name. */
44
+ author: string | null;
45
+ }
46
+
47
+ /** Injection token for supplying {@link NgBlogConfig} to the library. */
48
+ declare const NG_BLOG_CONFIG: InjectionToken<NgBlogConfig>;
49
+ /**
50
+ * Provides the ngx-blogdown library with the given configuration.
51
+ *
52
+ * Register the returned providers in your application's bootstrap or route config.
53
+ *
54
+ * @param config - Paths to the blog index file and posts directory.
55
+ * @returns An array of providers including {@link BlogService} and the config token.
56
+ *
57
+ * @example
58
+ * ```ts
59
+ * bootstrapApplication(AppComponent, {
60
+ * providers: [
61
+ * provideHttpClient(),
62
+ * provideNgBlogdown({ indexPath: '/blog/index.json', postsDir: '/blog/posts' }),
63
+ * ],
64
+ * });
65
+ * ```
66
+ */
67
+ declare function provideNgBlogdown(config: NgBlogConfig): Provider[];
68
+
69
+ /**
70
+ * Core service for fetching and rendering markdown blog posts.
71
+ *
72
+ * Provided via {@link provideNgBlogdown}. Requires `HttpClient` to be available.
73
+ */
74
+ declare class BlogService {
75
+ private http;
76
+ private config;
77
+ private indexCache;
78
+ /**
79
+ * Fetches the blog post index. Results are cached in memory after the first call.
80
+ *
81
+ * @returns All blog post metadata from the configured index path.
82
+ */
83
+ getPosts(): Promise<BlogPostMeta[]>;
84
+ /**
85
+ * Fetches a single blog post by its slug, parses its markdown body into HTML,
86
+ * and strips the YAML frontmatter.
87
+ *
88
+ * @param slug - The URL-friendly post identifier to look up.
89
+ * @returns The full blog post with rendered HTML, or `null` if not found.
90
+ */
91
+ getPost(slug: string): Promise<BlogPost | null>;
92
+ /**
93
+ * Derives SEO meta tags from a post's metadata.
94
+ *
95
+ * @param postMeta - The post metadata to extract tags from.
96
+ * @returns SEO-friendly tag values for use in `<meta>` elements.
97
+ */
98
+ getSeoTags(postMeta: BlogPostMeta): SeoTags;
99
+ static ɵfac: i0.ɵɵFactoryDeclaration<BlogService, never>;
100
+ static ɵprov: i0.ɵɵInjectableDeclaration<BlogService>;
101
+ }
102
+
103
+ export { BlogService, NG_BLOG_CONFIG, provideNgBlogdown };
104
+ export type { BlogPost, BlogPostMeta, NgBlogConfig, SeoTags };
package/package.json ADDED
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "@centrolabs/ngx-blogdown",
3
+ "version": "1.0.0",
4
+ "description": "A lightweight Angular library for building markdown-powered blogs. You handle the layout, we handle the pipeline.",
5
+ "peerDependencies": {
6
+ "@angular/common": "^20.3.0",
7
+ "@angular/core": "^20.3.0",
8
+ "marked": "^17.0.0"
9
+ },
10
+ "dependencies": {
11
+ "tslib": "^2.3.0"
12
+ },
13
+ "sideEffects": false,
14
+ "bin": {
15
+ "ngx-blogdown-index": "./bin/ngx-blogdown-index.mjs"
16
+ },
17
+ "repository": {
18
+ "type": "git",
19
+ "url": "https://github.com/centrolabs/ngx-blogdown"
20
+ },
21
+ "module": "fesm2022/centrolabs-ngx-blogdown.mjs",
22
+ "typings": "index.d.ts",
23
+ "exports": {
24
+ "./package.json": {
25
+ "default": "./package.json"
26
+ },
27
+ ".": {
28
+ "types": "./index.d.ts",
29
+ "default": "./fesm2022/centrolabs-ngx-blogdown.mjs"
30
+ }
31
+ }
32
+ }