@foliokit/cms-core 0.4.2 → 1.0.1

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 (83) hide show
  1. package/README.md +62 -2
  2. package/eslint.config.mjs +48 -0
  3. package/ng-package.json +7 -0
  4. package/package.json +5 -18
  5. package/project.json +32 -0
  6. package/{esm2022/index.js → src/index.ts} +9 -2
  7. package/src/lib/cms-core/cms-core.html +1 -0
  8. package/src/lib/cms-core/cms-core.scss +0 -0
  9. package/src/lib/cms-core/cms-core.spec.ts +44 -0
  10. package/src/lib/cms-core/cms-core.ts +9 -0
  11. package/src/lib/firebase/firebase-admin.ts +32 -0
  12. package/src/lib/firebase/firebase.config.ts +26 -0
  13. package/src/lib/firebase/firebase.providers.ts +89 -0
  14. package/src/lib/firebase/foliokit.providers.ts +178 -0
  15. package/src/lib/models/author.model.ts +16 -0
  16. package/src/lib/models/page.model.ts +11 -0
  17. package/src/lib/models/post.model.ts +41 -0
  18. package/src/lib/models/site-config.model.ts +103 -0
  19. package/src/lib/models/tag.model.ts +5 -0
  20. package/src/lib/pipes/tag-label.pipe.ts +16 -0
  21. package/src/lib/resolvers/about-page.resolver.ts +76 -0
  22. package/src/lib/resolvers/links-page.resolver.ts +77 -0
  23. package/src/lib/resolvers/posts.resolver.ts +51 -0
  24. package/src/lib/services/auth.service.ts +49 -0
  25. package/src/lib/services/author.service.ts +88 -0
  26. package/src/lib/services/post.service.spec.ts +255 -0
  27. package/src/lib/services/post.service.ts +148 -0
  28. package/src/lib/services/site-config.service.ts +86 -0
  29. package/src/lib/services/tag.service.ts +24 -0
  30. package/src/lib/tokens/post-service.token.ts +14 -0
  31. package/src/lib/tokens/site-config-service.token.ts +12 -0
  32. package/src/lib/utils/normalize-author.ts +50 -0
  33. package/src/lib/utils/normalize-post.ts +66 -0
  34. package/src/lib/utils/normalize-site-config.ts +145 -0
  35. package/testing/firestore.stub.ts +65 -0
  36. package/tsconfig.json +31 -0
  37. package/tsconfig.lib.json +12 -0
  38. package/tsconfig.lib.prod.json +9 -0
  39. package/tsconfig.spec.json +8 -0
  40. package/esm2022/foliokit-cms-core.js +0 -5
  41. package/esm2022/foliokit-cms-core.js.map +0 -1
  42. package/esm2022/index.js.map +0 -1
  43. package/esm2022/lib/firebase/firebase.config.js +0 -8
  44. package/esm2022/lib/firebase/firebase.config.js.map +0 -1
  45. package/esm2022/lib/firebase/firebase.providers.js +0 -54
  46. package/esm2022/lib/firebase/firebase.providers.js.map +0 -1
  47. package/esm2022/lib/models/author.model.js +0 -1
  48. package/esm2022/lib/models/author.model.js.map +0 -1
  49. package/esm2022/lib/models/page.model.js +0 -1
  50. package/esm2022/lib/models/page.model.js.map +0 -1
  51. package/esm2022/lib/models/post.model.js +0 -1
  52. package/esm2022/lib/models/post.model.js.map +0 -1
  53. package/esm2022/lib/models/site-config.model.js +0 -1
  54. package/esm2022/lib/models/site-config.model.js.map +0 -1
  55. package/esm2022/lib/models/tag.model.js +0 -1
  56. package/esm2022/lib/models/tag.model.js.map +0 -1
  57. package/esm2022/lib/services/auth.service.js +0 -42
  58. package/esm2022/lib/services/auth.service.js.map +0 -1
  59. package/esm2022/lib/services/post.service.js +0 -83
  60. package/esm2022/lib/services/post.service.js.map +0 -1
  61. package/esm2022/lib/services/site-config.service.js +0 -30
  62. package/esm2022/lib/services/site-config.service.js.map +0 -1
  63. package/esm2022/lib/services/tag.service.js +0 -22
  64. package/esm2022/lib/services/tag.service.js.map +0 -1
  65. package/esm2022/lib/tokens/post-service.token.js +0 -5
  66. package/esm2022/lib/tokens/post-service.token.js.map +0 -1
  67. package/esm2022/lib/utils/normalize-post.js +0 -66
  68. package/esm2022/lib/utils/normalize-post.js.map +0 -1
  69. package/foliokit-cms-core.d.ts +0 -5
  70. package/index.d.ts +0 -13
  71. package/lib/firebase/firebase.config.d.ts +0 -11
  72. package/lib/firebase/firebase.providers.d.ts +0 -3
  73. package/lib/models/author.model.d.ts +0 -13
  74. package/lib/models/page.model.d.ts +0 -37
  75. package/lib/models/post.model.d.ts +0 -39
  76. package/lib/models/site-config.model.d.ts +0 -26
  77. package/lib/models/tag.model.d.ts +0 -5
  78. package/lib/services/auth.service.d.ts +0 -13
  79. package/lib/services/post.service.d.ts +0 -16
  80. package/lib/services/site-config.service.d.ts +0 -10
  81. package/lib/services/tag.service.d.ts +0 -9
  82. package/lib/tokens/post-service.token.d.ts +0 -10
  83. package/lib/utils/normalize-post.d.ts +0 -6
package/README.md CHANGED
@@ -1,3 +1,63 @@
1
1
  # @foliokit/cms-core
2
- Part of the [Folio](https://github.com/doug-williamson/foliokit) ecosystem.
3
- > This package is in early development. API is unstable.
2
+
3
+ Core Firebase services, data models, and injection tokens for the FolioKit CMS.
4
+ Provides `provideFolioKit()` — the single-call bootstrapper that sets up Firebase
5
+ (Firestore, Storage, Auth) and default service bindings for Angular applications.
6
+
7
+ Part of the [FolioKit](https://github.com/doug-williamson/foliokit) ecosystem.
8
+
9
+ ## Install
10
+
11
+ ```bash
12
+ npm install @foliokit/cms-core
13
+ ```
14
+
15
+ ## Peer Dependencies
16
+
17
+ | Package | Version |
18
+ |---------|---------|
19
+ | `@angular/common` | `^21.2.4` |
20
+ | `@angular/core` | `^21.2.4` |
21
+ | `firebase` | `^11.10.0` |
22
+ | `rxjs` | `~7.8.0` |
23
+
24
+ ## Quick Start
25
+
26
+ ```typescript
27
+ // app.config.ts
28
+ import { provideFolioKit } from '@foliokit/cms-core';
29
+
30
+ export const appConfig: ApplicationConfig = {
31
+ providers: [
32
+ provideFolioKit({
33
+ firebaseConfig: {
34
+ apiKey: '...',
35
+ authDomain: '...',
36
+ projectId: '...',
37
+ storageBucket: '...',
38
+ messagingSenderId: '...',
39
+ appId: '...',
40
+ },
41
+ }),
42
+ ],
43
+ };
44
+ ```
45
+
46
+ `provideFolioKit()` registers:
47
+ - Firebase App, Firestore, Storage, and Auth (SSR-safe — returns `null` on the server)
48
+ - `PostService` bound to `BLOG_POST_SERVICE`
49
+ - `SiteConfigService` bound to `SITE_CONFIG_SERVICE`
50
+ - Optional `SITE_ID` token (when `siteId` is provided)
51
+
52
+ ## What's Included
53
+
54
+ - **`provideFolioKit(config)`** — single-call bootstrapper
55
+ - **`provideFirebase(options, useEmulator)`** — lower-level Firebase-only setup
56
+ - **Data models** — `BlogPost`, `SiteConfig`, `NavItem`, `Author`, `Tag`, `SeoMeta`, `AboutPageConfig`, `LinksPageConfig`, `HomePageConfig`
57
+ - **Services** — `PostService`, `SiteConfigService`, `AuthorService`, `AuthService`, `TagService`
58
+ - **DI tokens** — `BLOG_POST_SERVICE`, `SITE_CONFIG_SERVICE`, `SITE_ID`, `FIREBASE_OPTIONS`, `FIRESTORE`, `FIREBASE_STORAGE`, `FIREBASE_AUTH`
59
+ - **Pipes** — `TagLabelPipe`
60
+
61
+ ## Full Documentation
62
+
63
+ [foliokitcms.com/docs/getting-started](https://foliokitcms.com/docs/getting-started)
@@ -0,0 +1,48 @@
1
+ import nx from '@nx/eslint-plugin';
2
+ import baseConfig from '../../eslint.config.mjs';
3
+
4
+ export default [
5
+ ...baseConfig,
6
+ {
7
+ files: ['**/*.json'],
8
+ rules: {
9
+ '@nx/dependency-checks': [
10
+ 'error',
11
+ {
12
+ ignoredFiles: ['{projectRoot}/eslint.config.{js,cjs,mjs,ts,cts,mts}'],
13
+ },
14
+ ],
15
+ },
16
+ languageOptions: {
17
+ parser: await import('jsonc-eslint-parser'),
18
+ },
19
+ },
20
+ ...nx.configs['flat/angular'],
21
+ ...nx.configs['flat/angular-template'],
22
+ {
23
+ files: ['**/*.ts'],
24
+ rules: {
25
+ '@angular-eslint/directive-selector': [
26
+ 'error',
27
+ {
28
+ type: 'attribute',
29
+ prefix: 'lib',
30
+ style: 'camelCase',
31
+ },
32
+ ],
33
+ '@angular-eslint/component-selector': [
34
+ 'error',
35
+ {
36
+ type: 'element',
37
+ prefix: 'lib',
38
+ style: 'kebab-case',
39
+ },
40
+ ],
41
+ },
42
+ },
43
+ {
44
+ files: ['**/*.html'],
45
+ // Override or add rules here
46
+ rules: {},
47
+ },
48
+ ];
@@ -0,0 +1,7 @@
1
+ {
2
+ "$schema": "../../node_modules/ng-packagr/ng-package.schema.json",
3
+ "dest": "../../dist/libs/cms-core",
4
+ "lib": {
5
+ "entryFile": "src/index.ts"
6
+ }
7
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@foliokit/cms-core",
3
- "version": "0.4.2",
3
+ "version": "1.0.1",
4
4
  "description": "Core Firebase services, models, and tokens for FolioKit CMS",
5
5
  "keywords": [
6
6
  "angular",
@@ -12,7 +12,7 @@
12
12
  "license": "MIT",
13
13
  "repository": {
14
14
  "type": "git",
15
- "url": "https://github.com/dougwilliamson/foliokit"
15
+ "url": "git+https://github.com/dougwilliamson/foliokit.git"
16
16
  },
17
17
  "publishConfig": {
18
18
  "access": "public"
@@ -20,22 +20,9 @@
20
20
  "peerDependencies": {
21
21
  "@angular/common": "^21.2.4",
22
22
  "@angular/core": "^21.2.4",
23
+ "@angular/router": "^21.2.4",
23
24
  "firebase": "^11.10.0",
24
25
  "rxjs": "~7.8.0"
25
26
  },
26
- "sideEffects": false,
27
- "module": "esm2022/foliokit-cms-core.js",
28
- "typings": "foliokit-cms-core.d.ts",
29
- "exports": {
30
- "./package.json": {
31
- "default": "./package.json"
32
- },
33
- ".": {
34
- "types": "./foliokit-cms-core.d.ts",
35
- "default": "./esm2022/foliokit-cms-core.js"
36
- }
37
- },
38
- "dependencies": {
39
- "tslib": "^2.3.0"
40
- }
41
- }
27
+ "sideEffects": false
28
+ }
package/project.json ADDED
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "cms-core",
3
+ "$schema": "../../node_modules/nx/schemas/project-schema.json",
4
+ "sourceRoot": "libs/cms-core/src",
5
+ "prefix": "lib",
6
+ "projectType": "library",
7
+ "tags": [],
8
+ "targets": {
9
+ "build": {
10
+ "executor": "@nx/angular:ng-packagr-lite",
11
+ "outputs": ["{workspaceRoot}/dist/{projectRoot}"],
12
+ "options": {
13
+ "project": "libs/cms-core/ng-package.json",
14
+ "tsConfig": "libs/cms-core/tsconfig.lib.json"
15
+ },
16
+ "configurations": {
17
+ "production": {
18
+ "tsConfig": "libs/cms-core/tsconfig.lib.prod.json"
19
+ },
20
+ "development": {}
21
+ },
22
+ "defaultConfiguration": "production"
23
+ },
24
+ "test": {
25
+ "executor": "@nx/angular:unit-test",
26
+ "options": {}
27
+ },
28
+ "lint": {
29
+ "executor": "@nx/eslint:lint"
30
+ }
31
+ }
32
+ }
@@ -1,5 +1,6 @@
1
1
  export * from './lib/firebase/firebase.config';
2
2
  export * from './lib/firebase/firebase.providers';
3
+ export * from './lib/firebase/foliokit.providers';
3
4
  // firebase-admin.ts is intentionally excluded — server-only, import directly in SSR server files
4
5
  export * from './lib/models/post.model';
5
6
  export * from './lib/models/site-config.model';
@@ -7,9 +8,15 @@ export * from './lib/models/page.model';
7
8
  export * from './lib/models/tag.model';
8
9
  export * from './lib/models/author.model';
9
10
  export * from './lib/services/auth.service';
11
+ export * from './lib/services/author.service';
10
12
  export * from './lib/services/post.service';
11
13
  export * from './lib/services/site-config.service';
12
14
  export * from './lib/services/tag.service';
13
15
  export * from './lib/tokens/post-service.token';
14
- export * from './lib/utils/normalize-post';
15
- //# sourceMappingURL=index.js.map
16
+ export * from './lib/tokens/site-config-service.token';
17
+ export * from './lib/pipes/tag-label.pipe';
18
+
19
+ // ── Resolvers ─────────────────────────────────────────────────────────────────
20
+ export * from './lib/resolvers/about-page.resolver';
21
+ export * from './lib/resolvers/links-page.resolver';
22
+ export * from './lib/resolvers/posts.resolver';
@@ -0,0 +1 @@
1
+ <p>CmsCore works!</p>
File without changes
@@ -0,0 +1,44 @@
1
+ import { TestBed } from '@angular/core/testing';
2
+ import { ADMIN_EMAIL, SITE_ID, provideFolioKit } from '@foliokit/cms-core';
3
+
4
+ // Minimal Firebase config for testing — no real project needed; these tests
5
+ // never establish a network connection.
6
+ const testFirebaseConfig = {
7
+ apiKey: 'test-key',
8
+ authDomain: 'test.firebaseapp.com',
9
+ projectId: 'test-project',
10
+ storageBucket: 'test-project.appspot.com',
11
+ messagingSenderId: '000000000000',
12
+ appId: '1:000000000000:web:000000000000',
13
+ };
14
+
15
+ describe('cms-core public API smoke tests', () => {
16
+ afterEach(() => TestBed.resetTestingModule());
17
+
18
+ it('provideFolioKit() returns a truthy EnvironmentProviders object', () => {
19
+ const result = provideFolioKit({ firebaseConfig: testFirebaseConfig });
20
+ expect(result).toBeTruthy();
21
+ });
22
+
23
+ it('ADMIN_EMAIL token is injectable when provided', () => {
24
+ TestBed.configureTestingModule({
25
+ providers: [{ provide: ADMIN_EMAIL, useValue: 'admin@example.com' }],
26
+ });
27
+ const email = TestBed.inject(ADMIN_EMAIL);
28
+ expect(email).toBe('admin@example.com');
29
+ });
30
+
31
+ it('SITE_ID token is optionally injectable — returns null when absent', () => {
32
+ TestBed.configureTestingModule({ providers: [] });
33
+ const siteId = TestBed.inject(SITE_ID, null);
34
+ expect(siteId).toBeNull();
35
+ });
36
+
37
+ it('SITE_ID token is injectable when provided', () => {
38
+ TestBed.configureTestingModule({
39
+ providers: [{ provide: SITE_ID, useValue: 'my-site' }],
40
+ });
41
+ const siteId = TestBed.inject(SITE_ID);
42
+ expect(siteId).toBe('my-site');
43
+ });
44
+ });
@@ -0,0 +1,9 @@
1
+ import { Component } from '@angular/core';
2
+
3
+ @Component({
4
+ selector: 'lib-cms-core',
5
+ imports: [],
6
+ templateUrl: './cms-core.html',
7
+ styleUrl: './cms-core.scss',
8
+ })
9
+ export class CmsCore {}
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Firebase Admin SDK initializer — Node.js only.
3
+ *
4
+ * Credential resolution order:
5
+ * 1. If GOOGLE_APPLICATION_CREDENTIALS contains a JSON object (Firebase App
6
+ * Hosting injects secret values as raw strings, not file paths), parse it
7
+ * directly with admin.credential.cert().
8
+ * 2. Otherwise fall back to Application Default Credentials (ADC), which
9
+ * covers local dev (file-path env var) and Cloud Run managed SA.
10
+ *
11
+ * Never import this module in browser bundles. Import directly in SSR server
12
+ * entry files (e.g. apps/blog/src/server.ts) — it is intentionally excluded
13
+ * from the @foliokit/cms-core barrel export.
14
+ */
15
+ import admin from 'firebase-admin';
16
+
17
+ export function initAdminApp(): admin.app.App {
18
+ if (!admin.apps.length) {
19
+ const credEnv = process.env['GOOGLE_APPLICATION_CREDENTIALS'];
20
+ const credential = credEnv?.trimStart().startsWith('{')
21
+ ? admin.credential.cert(
22
+ JSON.parse(credEnv) as admin.ServiceAccount,
23
+ )
24
+ : admin.credential.applicationDefault();
25
+
26
+ admin.initializeApp({
27
+ credential,
28
+ projectId: process.env['FIREBASE_PROJECT_ID'],
29
+ });
30
+ }
31
+ return admin.app();
32
+ }
@@ -0,0 +1,26 @@
1
+ import { InjectionToken } from '@angular/core';
2
+ import type { FirebaseApp, FirebaseOptions } from 'firebase/app';
3
+ import type { Firestore } from 'firebase/firestore';
4
+ import type { FirebaseStorage } from 'firebase/storage';
5
+ import type { Auth } from 'firebase/auth';
6
+
7
+ export const FIREBASE_OPTIONS = new InjectionToken<FirebaseOptions>('FIREBASE_OPTIONS');
8
+ export const FIREBASE_APP = new InjectionToken<FirebaseApp>('FIREBASE_APP');
9
+ export const FIRESTORE = new InjectionToken<Firestore | null>('FIRESTORE');
10
+ export const FIREBASE_STORAGE = new InjectionToken<FirebaseStorage | null>('FIREBASE_STORAGE');
11
+ export const FIREBASE_AUTH = new InjectionToken<Auth | null>('FIREBASE_AUTH');
12
+ /**
13
+ * The email address that identifies the admin user.
14
+ *
15
+ * Injected by `provideAdminKit({ adminEmail })` and read by
16
+ * `AuthService.isAdmin()` to gate all write operations.
17
+ *
18
+ * **Must match all four authorization surfaces:**
19
+ * 1. The Firebase Authentication account (Google sign-in email)
20
+ * 2. The `adminEmail` passed to `provideAdminKit()` in `app.config.ts`
21
+ * 3. The `isAdmin()` function in `firestore.rules`
22
+ * 4. The author document created by the seed script
23
+ *
24
+ * @see {@link https://github.com/dougwilliamson/foliokit/blob/main/docs/security/admin-authorization.md}
25
+ */
26
+ export const ADMIN_EMAIL = new InjectionToken<string>('ADMIN_EMAIL');
@@ -0,0 +1,89 @@
1
+ import {
2
+ EnvironmentProviders,
3
+ PLATFORM_ID,
4
+ inject,
5
+ makeEnvironmentProviders,
6
+ } from '@angular/core';
7
+ import { isPlatformBrowser } from '@angular/common';
8
+ import { FirebaseOptions, getApp, getApps, initializeApp } from 'firebase/app';
9
+ import {
10
+ connectFirestoreEmulator,
11
+ getFirestore,
12
+ initializeFirestore,
13
+ memoryLocalCache,
14
+ } from 'firebase/firestore';
15
+ import { connectStorageEmulator, getStorage } from 'firebase/storage';
16
+ import { connectAuthEmulator, getAuth } from 'firebase/auth';
17
+ import {
18
+ FIREBASE_APP,
19
+ FIREBASE_AUTH,
20
+ FIREBASE_OPTIONS,
21
+ FIREBASE_STORAGE,
22
+ FIRESTORE,
23
+ } from './firebase.config';
24
+
25
+ export function provideFirebase(
26
+ options: FirebaseOptions,
27
+ useEmulator = false
28
+ ): EnvironmentProviders {
29
+ return makeEnvironmentProviders([
30
+ {
31
+ provide: FIREBASE_OPTIONS,
32
+ useValue: options,
33
+ },
34
+ {
35
+ // Firebase app is a Node.js-process-level singleton. In SSR, each request
36
+ // creates a new DI context but shares the same global Firebase registry, so
37
+ // we must reuse the existing app rather than calling initializeApp again.
38
+ provide: FIREBASE_APP,
39
+ useFactory: () =>
40
+ getApps().length ? getApp() : initializeApp(inject(FIREBASE_OPTIONS)),
41
+ },
42
+ {
43
+ // Same singleton constraint applies to the Firestore instance.
44
+ // initializeFirestore throws if called a second time for the same app, so
45
+ // fall back to getFirestore() which returns the already-configured instance.
46
+ // Client SDK must not run on the server — Admin SDK handles SSR reads.
47
+ provide: FIRESTORE,
48
+ useFactory: () => {
49
+ const platformId = inject(PLATFORM_ID);
50
+ if (!isPlatformBrowser(platformId)) return null;
51
+ const app = inject(FIREBASE_APP);
52
+ try {
53
+ const db = initializeFirestore(app, { localCache: memoryLocalCache(), ignoreUndefinedProperties: true });
54
+ if (useEmulator) {
55
+ connectFirestoreEmulator(db, '127.0.0.1', 8080);
56
+ }
57
+ return db;
58
+ } catch {
59
+ return getFirestore(app);
60
+ }
61
+ },
62
+ },
63
+ {
64
+ provide: FIREBASE_STORAGE,
65
+ useFactory: () => {
66
+ const platformId = inject(PLATFORM_ID);
67
+ if (!isPlatformBrowser(platformId)) return null;
68
+ const storage = getStorage(inject(FIREBASE_APP));
69
+ if (useEmulator) {
70
+ connectStorageEmulator(storage, '127.0.0.1', 9199);
71
+ }
72
+ return storage;
73
+ },
74
+ },
75
+ {
76
+ // Auth relies on browser APIs (IndexedDB, localStorage) — null on server
77
+ provide: FIREBASE_AUTH,
78
+ useFactory: () => {
79
+ const platformId = inject(PLATFORM_ID);
80
+ if (!isPlatformBrowser(platformId)) return null;
81
+ const auth = getAuth(inject(FIREBASE_APP));
82
+ if (useEmulator) {
83
+ connectAuthEmulator(auth, 'http://127.0.0.1:9099', { disableWarnings: true });
84
+ }
85
+ return auth;
86
+ },
87
+ },
88
+ ]);
89
+ }
@@ -0,0 +1,178 @@
1
+ import {
2
+ APP_INITIALIZER,
3
+ EnvironmentProviders,
4
+ InjectionToken,
5
+ PLATFORM_ID,
6
+ inject,
7
+ makeEnvironmentProviders,
8
+ } from '@angular/core';
9
+ import { isPlatformBrowser } from '@angular/common';
10
+ import type { FirebaseOptions } from 'firebase/app';
11
+ import { provideFirebase } from './firebase.providers';
12
+ import { FIREBASE_AUTH } from './firebase.config';
13
+ import { PostService } from '../services/post.service';
14
+ import { SiteConfigService } from '../services/site-config.service';
15
+ import { BLOG_POST_SERVICE } from '../tokens/post-service.token';
16
+ import { SITE_CONFIG_SERVICE } from '../tokens/site-config-service.token';
17
+
18
+ /**
19
+ * Injection token for the current site identifier.
20
+ *
21
+ * **Provided by {@link provideFolioKit}** when `siteId` is set in the config.
22
+ * Consumers should inject this token to read the active site ID — do not
23
+ * provide it yourself unless you are bypassing `provideFolioKit()`.
24
+ *
25
+ * Useful for multi-site deployments where a single Firebase project
26
+ * serves several distinct sites. Services like `SiteConfigService` can
27
+ * use this to scope Firestore reads to a specific site document.
28
+ *
29
+ * @example
30
+ * ```ts
31
+ * // In any component or service — reads the value set by provideFolioKit():
32
+ * readonly siteId = inject(SITE_ID, { optional: true });
33
+ *
34
+ * loadConfig() {
35
+ * const id = this.siteId ?? 'default';
36
+ * return this.siteConfigService.getSiteConfig(id);
37
+ * }
38
+ * ```
39
+ */
40
+ export const SITE_ID = new InjectionToken<string>('SITE_ID');
41
+
42
+ /**
43
+ * Configuration object accepted by {@link provideFolioKit}.
44
+ */
45
+ export interface FolioKitConfig {
46
+ /**
47
+ * Firebase project credentials. Obtain from the Firebase Console under
48
+ * Project Settings → Your apps → SDK setup and configuration.
49
+ *
50
+ * @example
51
+ * ```ts
52
+ * firebaseConfig: {
53
+ * apiKey: 'AIza...',
54
+ * authDomain: 'my-project.firebaseapp.com',
55
+ * projectId: 'my-project',
56
+ * storageBucket: 'my-project.appspot.com',
57
+ * messagingSenderId: '1234567890',
58
+ * appId: '1:1234567890:web:abc123',
59
+ * }
60
+ * ```
61
+ */
62
+ firebaseConfig: FirebaseOptions;
63
+
64
+ /**
65
+ * Optional site identifier for multi-site deployments.
66
+ *
67
+ * When provided, the value is registered as the {@link SITE_ID} injection
68
+ * token so that services and components can vary behaviour per site without
69
+ * requiring separate Firebase projects.
70
+ */
71
+ siteId?: string;
72
+
73
+ /**
74
+ * Optional Firebase Authentication tenant ID.
75
+ *
76
+ * When provided, `auth.tenantId` is set on the Auth instance during app
77
+ * initialisation via `APP_INITIALIZER`. Required for Google Cloud Identity
78
+ * Platform multi-tenant projects. Has no effect in SSR contexts where Auth
79
+ * is null.
80
+ *
81
+ * @see https://firebase.google.com/docs/auth/web/multi-tenancy
82
+ */
83
+ tenantId?: string;
84
+
85
+ /**
86
+ * When `true`, all Firebase services connect to the local emulator suite
87
+ * instead of production endpoints:
88
+ * - Firestore → `127.0.0.1:8080`
89
+ * - Storage → `127.0.0.1:9199`
90
+ * - Auth → `http://127.0.0.1:9099`
91
+ *
92
+ * @default false
93
+ */
94
+ useEmulator?: boolean;
95
+ }
96
+
97
+ /**
98
+ * Bootstrap FolioKit in a single call.
99
+ *
100
+ * Registers all Firebase services (app, Firestore, Storage, Auth), binds the
101
+ * default {@link PostService} and {@link SiteConfigService} implementations to
102
+ * their public tokens, and optionally stores `siteId` / `tenantId` config.
103
+ *
104
+ * Use this instead of `provideFirebase()` unless you need custom service
105
+ * implementations — in which case call `provideFirebase()` directly and
106
+ * provide your own service aliases.
107
+ *
108
+ * **Overriding the default service bindings**
109
+ *
110
+ * The last provider wins in Angular's DI system. Add your own binding *after*
111
+ * `provideFolioKit()` in the providers array to override the defaults:
112
+ *
113
+ * ```ts
114
+ * providers: [
115
+ * provideFolioKit({ firebaseConfig: environment.firebase }),
116
+ * { provide: BLOG_POST_SERVICE, useExisting: MyCustomPostService },
117
+ * ]
118
+ * ```
119
+ *
120
+ * @param config - FolioKit configuration including Firebase credentials and
121
+ * optional site/tenant identifiers.
122
+ * @returns An `EnvironmentProviders` token suitable for use in
123
+ * `ApplicationConfig.providers` or `bootstrapApplication`.
124
+ *
125
+ * @example
126
+ * ```ts
127
+ * // app.config.ts
128
+ * export const appConfig: ApplicationConfig = {
129
+ * providers: [
130
+ * provideRouter(routes),
131
+ * provideAnimationsAsync(),
132
+ * provideHttpClient(withFetch()),
133
+ * provideMarkdown(),
134
+ * provideFolioKit({
135
+ * firebaseConfig: environment.firebase,
136
+ * useEmulator: environment.useEmulator,
137
+ * }),
138
+ * ],
139
+ * };
140
+ * ```
141
+ */
142
+ export function provideFolioKit(config: FolioKitConfig): EnvironmentProviders {
143
+ const providers: Parameters<typeof makeEnvironmentProviders>[0] = [
144
+ // Firebase services: app, Firestore, Storage, Auth (SSR-safe).
145
+ provideFirebase(config.firebaseConfig, config.useEmulator ?? false),
146
+
147
+ // Default concrete service implementations bound to their public tokens.
148
+ // Override by re-providing the token after this call in the same array.
149
+ { provide: BLOG_POST_SERVICE, useExisting: PostService },
150
+ { provide: SITE_CONFIG_SERVICE, useExisting: SiteConfigService },
151
+ ];
152
+
153
+ // Optionally expose siteId as an injectable constant.
154
+ if (config.siteId !== undefined) {
155
+ providers.push({ provide: SITE_ID, useValue: config.siteId });
156
+ }
157
+
158
+ // Optionally apply a Firebase Auth tenant ID for multi-tenant projects.
159
+ // Uses APP_INITIALIZER to run after the Auth instance is created.
160
+ if (config.tenantId !== undefined) {
161
+ const tenantId = config.tenantId;
162
+ providers.push({
163
+ provide: APP_INITIALIZER,
164
+ useFactory: () => {
165
+ const platformId = inject(PLATFORM_ID);
166
+ const auth = inject(FIREBASE_AUTH);
167
+ return () => {
168
+ if (isPlatformBrowser(platformId) && auth) {
169
+ auth.tenantId = tenantId;
170
+ }
171
+ };
172
+ },
173
+ multi: true,
174
+ });
175
+ }
176
+
177
+ return makeEnvironmentProviders(providers);
178
+ }
@@ -0,0 +1,16 @@
1
+ import type { SocialLink } from './site-config.model';
2
+
3
+ export interface Author {
4
+ id: string;
5
+ displayName: string;
6
+ bio?: string;
7
+ photoUrl?: string;
8
+ /** Firebase Storage URL — shown in dark mode when set */
9
+ photoUrlDark?: string;
10
+ socialLinks?: SocialLink[];
11
+ email?: string;
12
+ /** Unix milliseconds. */
13
+ createdAt: number;
14
+ /** Unix milliseconds. */
15
+ updatedAt: number;
16
+ }
@@ -0,0 +1,11 @@
1
+ import type { SocialPlatform } from './site-config.model';
2
+
3
+ export interface LinksLink {
4
+ id: string;
5
+ label: string;
6
+ url: string;
7
+ icon?: string;
8
+ platform?: SocialPlatform;
9
+ highlighted?: boolean;
10
+ order: number;
11
+ }
@@ -0,0 +1,41 @@
1
+ export interface SeoMeta {
2
+ title?: string;
3
+ description?: string;
4
+ keywords?: string[];
5
+ ogImage?: string;
6
+ canonicalUrl?: string;
7
+ noIndex?: boolean;
8
+ }
9
+
10
+ export interface EmbeddedMediaEntry {
11
+ token: string;
12
+ storagePath: string;
13
+ downloadUrl: string;
14
+ alt: string;
15
+ mimeType: string;
16
+ }
17
+
18
+ export interface BlogPost {
19
+ id: string;
20
+ slug: string;
21
+ title: string;
22
+ subtitle?: string;
23
+ status: 'published' | 'draft' | 'scheduled' | 'archived';
24
+ content: string;
25
+ excerpt?: string;
26
+ thumbnailUrl?: string;
27
+ thumbnailAlt?: string;
28
+ tags: string[];
29
+ authorId?: string;
30
+ readingTimeMinutes?: number;
31
+ embeddedMedia: Record<string, EmbeddedMediaEntry>;
32
+ seo: SeoMeta;
33
+ /** Unix milliseconds. Stored as Firestore Timestamp but always normalized on read. */
34
+ publishedAt: number;
35
+ /** Unix milliseconds, optional. */
36
+ scheduledPublishAt?: number;
37
+ /** Unix milliseconds. */
38
+ updatedAt: number;
39
+ /** Unix milliseconds. */
40
+ createdAt: number;
41
+ }