create-momentum-app 0.5.0 → 0.5.2

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 (33) hide show
  1. package/README.md +1 -0
  2. package/index.cjs +12 -3
  3. package/package.json +22 -21
  4. package/templates/angular/src/server.ts.tmpl +3 -1
  5. package/templates/nestjs/angular.json.tmpl +73 -0
  6. package/templates/nestjs/eslint.config.mjs +31 -0
  7. package/templates/nestjs/package.json.tmpl +69 -0
  8. package/templates/nestjs/postcss.config.js +6 -0
  9. package/templates/nestjs/src/app/app.config.server.ts +10 -0
  10. package/templates/nestjs/src/app/app.config.ts +18 -0
  11. package/templates/nestjs/src/app/app.routes.server.ts +8 -0
  12. package/templates/nestjs/src/app/app.routes.ts +22 -0
  13. package/templates/nestjs/src/app/app.ts +9 -0
  14. package/templates/nestjs/src/app/pages/blocks/hero-block.component.ts +41 -0
  15. package/templates/nestjs/src/app/pages/blocks/image-text-block.component.ts +56 -0
  16. package/templates/nestjs/src/app/pages/blocks/text-block.component.ts +28 -0
  17. package/templates/nestjs/src/app/pages/post-block-providers.ts +17 -0
  18. package/templates/nestjs/src/app/pages/post-detail.resolver.ts +31 -0
  19. package/templates/nestjs/src/app/pages/post-detail.ts +139 -0
  20. package/templates/nestjs/src/app/pages/posts.ts +130 -0
  21. package/templates/nestjs/src/app/pages/welcome.ts +151 -0
  22. package/templates/nestjs/src/collections/posts.collection.ts +89 -0
  23. package/templates/nestjs/src/generated/.gitkeep +0 -0
  24. package/templates/nestjs/src/index.html.tmpl +20 -0
  25. package/templates/nestjs/src/main.server.ts +8 -0
  26. package/templates/nestjs/src/main.ts +5 -0
  27. package/templates/nestjs/src/momentum.config.ts.tmpl +73 -0
  28. package/templates/nestjs/src/server.ts.tmpl +84 -0
  29. package/templates/nestjs/src/styles.css +140 -0
  30. package/templates/nestjs/src/types/.gitkeep +0 -0
  31. package/templates/nestjs/tailwind.config.js +11 -0
  32. package/templates/nestjs/tsconfig.app.json +9 -0
  33. package/templates/nestjs/tsconfig.json +26 -0
package/README.md CHANGED
@@ -30,4 +30,5 @@ npx create-momentum-app my-app --flavor angular --database postgres
30
30
  - REST API at `/api`
31
31
  - Authentication via Better Auth
32
32
  - Drizzle ORM with PostgreSQL or SQLite
33
+ - Docker Compose setup for PostgreSQL (when using postgres)
33
34
  - Tailwind CSS with the Momentum admin theme
package/index.cjs CHANGED
@@ -184,7 +184,15 @@ const pool = (dbAdapter as PostgresAdapterWithRaw).getPool();` : "",
184
184
  dbDevPackage: database === "postgres" ? "" : '"@types/better-sqlite3": "^7.6.13",',
185
185
  envDbVar: database === "postgres" ? "DATABASE_URL=postgresql://postgres:postgres@localhost:5432/momentum" : "DATABASE_PATH=./data/momentum.db",
186
186
  defaultPort: "4200",
187
- externalDependencies: database === "postgres" ? '"pg", "pg-native"' : '"better-sqlite3"',
187
+ externalDependencies: [
188
+ database === "postgres" ? '"pg", "pg-native"' : '"better-sqlite3"',
189
+ ...flavor === "nestjs" ? [
190
+ '"@nestjs/microservices"',
191
+ '"@nestjs/websockets"',
192
+ '"class-validator"',
193
+ '"class-transformer"'
194
+ ] : []
195
+ ].join(", "),
188
196
  prerequisitesDocker: database === "postgres" ? `- **Docker** (for PostgreSQL database)
189
197
  - [macOS](https://www.docker.com/products/docker-desktop/)
190
198
  - [Linux](https://docs.docker.com/engine/install/)
@@ -325,7 +333,7 @@ function parseArgs(argv) {
325
333
  const arg = args[i];
326
334
  if (arg === "--flavor" && args[i + 1]) {
327
335
  const val = args[++i];
328
- if (val === "angular" || val === "analog") {
336
+ if (val === "angular" || val === "analog" || val === "nestjs") {
329
337
  opts.flavor = val;
330
338
  }
331
339
  } else if (arg === "--database" && args[i + 1]) {
@@ -369,7 +377,8 @@ async function runCLI() {
369
377
  message: "Which framework?",
370
378
  choices: [
371
379
  { title: "Angular (Express SSR)", value: "angular" },
372
- { title: "Analog (Nitro)", value: "analog" }
380
+ { title: "Analog (Nitro)", value: "analog" },
381
+ { title: "NestJS (Express)", value: "nestjs" }
373
382
  ]
374
383
  },
375
384
  {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-momentum-app",
3
- "version": "0.5.0",
3
+ "version": "0.5.2",
4
4
  "description": "Create a new Momentum CMS application",
5
5
  "license": "MIT",
6
6
  "author": "Momentum CMS Contributors",
@@ -35,28 +35,29 @@
35
35
  "README.md"
36
36
  ],
37
37
  "dependencies": {
38
- "@angular/aria": "21.1.2",
39
- "@angular/cdk": "21.1.2",
40
- "@angular/common": "21.1.2",
41
- "@angular/compiler": "21.1.2",
42
- "@angular/core": "21.1.2",
43
- "@angular/forms": "21.1.2",
44
- "@angular/platform-browser": "21.1.2",
45
- "@angular/platform-server": "21.1.2",
46
- "@angular/router": "21.1.2",
47
- "@angular/ssr": "21.1.2",
48
- "@aws-sdk/client-s3": "3.983.0",
49
- "@aws-sdk/s3-request-presigner": "3.983.0",
50
- "@ng-icons/core": "33.0.0",
51
- "@ng-icons/heroicons": "33.0.0",
52
- "@tiptap/core": "3.19.0",
53
- "@tiptap/extension-link": "3.19.0",
54
- "@tiptap/extension-placeholder": "3.19.0",
55
- "@tiptap/extension-underline": "3.19.0",
56
- "@tiptap/starter-kit": "3.19.0",
38
+ "@angular/aria": "21.2.0",
39
+ "@angular/cdk": "21.2.0",
40
+ "@angular/common": "21.2.0",
41
+ "@angular/compiler": "21.2.0",
42
+ "@angular/core": "21.2.0",
43
+ "@angular/forms": "21.2.0",
44
+ "@angular/platform-browser": "21.2.0",
45
+ "@angular/platform-server": "21.2.0",
46
+ "@angular/router": "21.2.0",
47
+ "@angular/ssr": "21.2.0",
48
+ "@aws-sdk/client-s3": "3.999.0",
49
+ "@aws-sdk/s3-request-presigner": "3.999.0",
50
+ "@ng-icons/core": "33.1.0",
51
+ "@ng-icons/heroicons": "33.1.0",
52
+ "@tiptap/core": "3.20.0",
53
+ "@tiptap/extension-link": "3.20.0",
54
+ "@tiptap/extension-placeholder": "3.20.0",
55
+ "@tiptap/extension-underline": "3.20.0",
56
+ "@tiptap/starter-kit": "3.20.0",
57
57
  "fs-extra": "^11.2.0",
58
- "graphql": "16.12.0",
58
+ "graphql": "16.13.0",
59
59
  "h3": "1.15.5",
60
+ "juice": "11.1.1",
60
61
  "picocolors": "^1.1.0",
61
62
  "prompts": "^2.4.2",
62
63
  "rxjs": "7.8.2"
@@ -16,7 +16,9 @@ import momentumConfig, { authPlugin } from './momentum.config';
16
16
  const serverDistFolder = dirname(fileURLToPath(import.meta.url));
17
17
  const browserDistFolder = resolve(serverDistFolder, '../browser');
18
18
 
19
- const angularApp = new AngularNodeAppEngine();
19
+ const angularApp = new AngularNodeAppEngine({
20
+ allowedHosts: ['localhost'],
21
+ });
20
22
 
21
23
  /**
22
24
  * Create the Momentum CMS server.
@@ -0,0 +1,73 @@
1
+ {
2
+ "$schema": "./node_modules/@angular/cli/lib/config/schema.json",
3
+ "version": 1,
4
+ "newProjectRoot": "projects",
5
+ "projects": {
6
+ "{{projectName}}": {
7
+ "projectType": "application",
8
+ "root": "",
9
+ "sourceRoot": "src",
10
+ "prefix": "app",
11
+ "architect": {
12
+ "build": {
13
+ "builder": "@angular/build:application",
14
+ "options": {
15
+ "outputPath": "dist/{{projectName}}",
16
+ "index": "src/index.html",
17
+ "browser": "src/main.ts",
18
+ "server": "src/main.server.ts",
19
+ "tsConfig": "tsconfig.app.json",
20
+ "assets": [
21
+ {
22
+ "glob": "**/*",
23
+ "input": "public"
24
+ }
25
+ ],
26
+ "styles": ["src/styles.css"],
27
+ "scripts": [],
28
+ "ssr": {
29
+ "entry": "src/server.ts"
30
+ },
31
+ "outputMode": "server",
32
+ "externalDependencies": [{{externalDependencies}}]
33
+ },
34
+ "configurations": {
35
+ "production": {
36
+ "budgets": [
37
+ {
38
+ "type": "initial",
39
+ "maximumWarning": "1MB",
40
+ "maximumError": "2MB"
41
+ },
42
+ {
43
+ "type": "anyComponentStyle",
44
+ "maximumWarning": "4kB",
45
+ "maximumError": "8kB"
46
+ }
47
+ ],
48
+ "outputHashing": "all"
49
+ },
50
+ "development": {
51
+ "optimization": false,
52
+ "extractLicenses": false,
53
+ "sourceMap": true
54
+ }
55
+ },
56
+ "defaultConfiguration": "production"
57
+ },
58
+ "serve": {
59
+ "builder": "@angular/build:dev-server",
60
+ "configurations": {
61
+ "production": {
62
+ "buildTarget": "{{projectName}}:build:production"
63
+ },
64
+ "development": {
65
+ "buildTarget": "{{projectName}}:build:development"
66
+ }
67
+ },
68
+ "defaultConfiguration": "development"
69
+ }
70
+ }
71
+ }
72
+ }
73
+ }
@@ -0,0 +1,31 @@
1
+ import tseslint from 'typescript-eslint';
2
+ import angular from 'angular-eslint';
3
+
4
+ export default tseslint.config(
5
+ {
6
+ files: ['**/*.ts'],
7
+ extends: [...tseslint.configs.recommended, ...angular.configs.tsRecommended],
8
+ rules: {
9
+ '@angular-eslint/directive-selector': [
10
+ 'error',
11
+ {
12
+ type: 'attribute',
13
+ prefix: 'app',
14
+ style: 'camelCase',
15
+ },
16
+ ],
17
+ '@angular-eslint/component-selector': [
18
+ 'error',
19
+ {
20
+ type: 'element',
21
+ prefix: 'app',
22
+ style: 'kebab-case',
23
+ },
24
+ ],
25
+ },
26
+ },
27
+ {
28
+ files: ['**/*.html'],
29
+ extends: [...angular.configs.templateRecommended, ...angular.configs.templateAccessibility],
30
+ },
31
+ );
@@ -0,0 +1,69 @@
1
+ {
2
+ "name": "{{projectName}}",
3
+ "version": "0.0.1",
4
+ "private": true,
5
+ "scripts": {
6
+ "dev": "ng serve",
7
+ "build": "ng build",
8
+ "start": "node dist/{{projectName}}/server/server.mjs",
9
+ "lint": "ng lint",
10
+ "generate": "tsx node_modules/@momentumcms/core/src/generators/generator.cjs src/momentum.config.ts --types src/generated/momentum.types.ts --config src/generated/momentum.config.ts",
11
+ "migrate:generate": "ng generate @momentumcms/migrations:generate",
12
+ "migrate:run": "ng generate @momentumcms/migrations:run",
13
+ "migrate:status": "ng generate @momentumcms/migrations:status",
14
+ "migrate:rollback": "ng generate @momentumcms/migrations:rollback"
15
+ },
16
+ "dependencies": {
17
+ "@angular/animations": "^21.1.0",
18
+ "@angular/aria": "^21.1.0",
19
+ "@angular/cdk": "^21.1.0",
20
+ "@angular/common": "^21.1.0",
21
+ "@angular/compiler": "^21.1.0",
22
+ "@angular/core": "^21.1.0",
23
+ "@angular/forms": "^21.1.0",
24
+ "@angular/platform-browser": "^21.1.0",
25
+ "@angular/platform-server": "^21.1.0",
26
+ "@angular/router": "^21.1.0",
27
+ "@angular/ssr": "^21.1.0",
28
+ "@momentumcms/admin": "^{{packageVersion}}",
29
+ "@momentumcms/auth": "^{{packageVersion}}",
30
+ "@momentumcms/core": "^{{packageVersion}}",
31
+ "@momentumcms/db-drizzle": "^{{packageVersion}}",
32
+ "@momentumcms/server-core": "^{{packageVersion}}",
33
+ "@momentumcms/server-nestjs": "^{{packageVersion}}",
34
+ "@momentumcms/storage": "^{{packageVersion}}",
35
+ "@momentumcms/logger": "^{{packageVersion}}",
36
+ "@momentumcms/migrations": "^{{packageVersion}}",
37
+ "@momentumcms/plugins-core": "^{{packageVersion}}",
38
+ "@momentumcms/plugins-seo": "^{{packageVersion}}",
39
+ "@nestjs/common": "^11.1.0",
40
+ "@nestjs/core": "^11.1.0",
41
+ "@nestjs/platform-express": "^11.1.0",
42
+ "@ng-icons/core": "^33.0.0",
43
+ "@ng-icons/heroicons": "^33.0.0",
44
+ "@tiptap/core": "^3.0.0",
45
+ "@tiptap/extension-link": "^3.0.0",
46
+ "@tiptap/extension-placeholder": "^3.0.0",
47
+ "@tiptap/extension-underline": "^3.0.0",
48
+ "@tiptap/starter-kit": "^3.0.0",
49
+ "dotenv": "^16.5.0",
50
+ "express": "^4.21.0",
51
+ {{dbPackage}},
52
+ "reflect-metadata": "^0.2.0",
53
+ "rxjs": "^7.8.0",
54
+ "tslib": "^2.8.0"
55
+ },
56
+ "devDependencies": {
57
+ "@angular/build": "^21.1.0",
58
+ "@angular/cli": "^21.1.0",
59
+ "@angular/compiler-cli": "^21.1.0",
60
+ {{dbDevPackage}}
61
+ "@types/express": "^4.17.0",
62
+ "@types/node": "^22.15.0",
63
+ "autoprefixer": "^10.4.21",
64
+ "postcss": "^8.5.4",
65
+ "tailwindcss": "^3.4.17",
66
+ "tsx": "^4.0.0",
67
+ "typescript": "~5.9.2"
68
+ }
69
+ }
@@ -0,0 +1,6 @@
1
+ module.exports = {
2
+ plugins: {
3
+ tailwindcss: {},
4
+ autoprefixer: {},
5
+ },
6
+ };
@@ -0,0 +1,10 @@
1
+ import { mergeApplicationConfig, ApplicationConfig } from '@angular/core';
2
+ import { provideServerRendering, withRoutes } from '@angular/ssr';
3
+ import { appConfig } from './app.config';
4
+ import { serverRoutes } from './app.routes.server';
5
+
6
+ const serverConfig: ApplicationConfig = {
7
+ providers: [provideServerRendering(withRoutes(serverRoutes))],
8
+ };
9
+
10
+ export const config = mergeApplicationConfig(appConfig, serverConfig);
@@ -0,0 +1,18 @@
1
+ import { ApplicationConfig, provideBrowserGlobalErrorListeners } from '@angular/core';
2
+ import { provideRouter, withViewTransitions } from '@angular/router';
3
+ import { provideHttpClient, withFetch, withInterceptors } from '@angular/common/http';
4
+ import { provideClientHydration, withEventReplay } from '@angular/platform-browser';
5
+ import { crudToastInterceptor, provideMomentumFieldRenderers } from '@momentumcms/admin';
6
+ import { routes } from './app.routes';
7
+ import { providePostBlocks } from './pages/post-block-providers';
8
+
9
+ export const appConfig: ApplicationConfig = {
10
+ providers: [
11
+ provideHttpClient(withFetch(), withInterceptors([crudToastInterceptor])),
12
+ provideClientHydration(withEventReplay()),
13
+ provideBrowserGlobalErrorListeners(),
14
+ provideRouter(routes, withViewTransitions()),
15
+ provideMomentumFieldRenderers(),
16
+ ...providePostBlocks(),
17
+ ],
18
+ };
@@ -0,0 +1,8 @@
1
+ import { RenderMode, ServerRoute } from '@angular/ssr';
2
+
3
+ export const serverRoutes: ServerRoute[] = [
4
+ // Admin routes render client-side — guards need to run without SSR hydration conflict
5
+ { path: 'admin/**', renderMode: RenderMode.Client },
6
+ // All other routes use server-side rendering
7
+ { path: '**', renderMode: RenderMode.Server },
8
+ ];
@@ -0,0 +1,22 @@
1
+ import { Route } from '@angular/router';
2
+ import { momentumAdminRoutes } from '@momentumcms/admin';
3
+ import { adminConfig } from '../generated/momentum.config';
4
+
5
+ export const routes: Route[] = [
6
+ {
7
+ path: '',
8
+ loadComponent: () => import('./pages/welcome').then((m) => m.WelcomePage),
9
+ },
10
+ {
11
+ path: 'posts',
12
+ loadComponent: () => import('./pages/posts').then((m) => m.PostsPageComponent),
13
+ },
14
+ {
15
+ path: 'posts/:slug',
16
+ loadComponent: () => import('./pages/post-detail').then((m) => m.PostDetailComponent),
17
+ resolve: {
18
+ postData: () => import('./pages/post-detail.resolver').then((m) => m.postDetailResolver),
19
+ },
20
+ },
21
+ ...momentumAdminRoutes(adminConfig),
22
+ ];
@@ -0,0 +1,9 @@
1
+ import { Component } from '@angular/core';
2
+ import { RouterOutlet } from '@angular/router';
3
+
4
+ @Component({
5
+ selector: 'app-root',
6
+ imports: [RouterOutlet],
7
+ template: '<router-outlet />',
8
+ })
9
+ export class App {}
@@ -0,0 +1,41 @@
1
+ import { ChangeDetectionStrategy, Component, computed, input } from '@angular/core';
2
+
3
+ @Component({
4
+ selector: 'app-hero-block',
5
+ changeDetection: ChangeDetectionStrategy.OnPush,
6
+ host: {
7
+ class: 'block',
8
+ '[attr.data-testid]': '"block-hero"',
9
+ },
10
+ template: `
11
+ <section class="bg-primary text-primary-foreground py-12 px-4 md:py-20 md:px-8 text-center">
12
+ <div class="mx-auto max-w-4xl">
13
+ <h1 class="text-3xl md:text-4xl lg:text-5xl font-bold mb-4" data-testid="hero-heading">
14
+ {{ heading() }}
15
+ </h1>
16
+ @if (subheading()) {
17
+ <p class="text-lg md:text-xl text-primary-foreground mb-8" data-testid="hero-subheading">
18
+ {{ subheading() }}
19
+ </p>
20
+ }
21
+ @if (ctaText()) {
22
+ <a
23
+ class="inline-block bg-primary-foreground text-primary font-semibold px-6 py-3 rounded-lg hover:bg-primary-foreground/90 transition-colors"
24
+ [href]="ctaLink() || '#'"
25
+ data-testid="hero-cta"
26
+ >
27
+ {{ ctaText() }}
28
+ </a>
29
+ }
30
+ </div>
31
+ </section>
32
+ `,
33
+ })
34
+ export class HeroBlockComponent {
35
+ readonly data = input.required<Record<string, unknown>>();
36
+
37
+ readonly heading = computed((): string => String(this.data()['heading'] ?? ''));
38
+ readonly subheading = computed((): string => String(this.data()['subheading'] ?? ''));
39
+ readonly ctaText = computed((): string => String(this.data()['ctaText'] ?? ''));
40
+ readonly ctaLink = computed((): string => String(this.data()['ctaLink'] ?? ''));
41
+ }
@@ -0,0 +1,56 @@
1
+ import { ChangeDetectionStrategy, Component, computed, input } from '@angular/core';
2
+
3
+ @Component({
4
+ selector: 'app-image-text-block',
5
+ changeDetection: ChangeDetectionStrategy.OnPush,
6
+ host: {
7
+ class: 'block',
8
+ '[attr.data-testid]': '"block-imageText"',
9
+ },
10
+ template: `
11
+ <section class="py-8 px-4 md:py-16 md:px-8">
12
+ <div
13
+ class="mx-auto max-w-6xl flex flex-col gap-8 md:gap-12 items-center"
14
+ [class.md:flex-row]="!reversed()"
15
+ [class.md:flex-row-reverse]="reversed()"
16
+ >
17
+ <!-- Image -->
18
+ <div class="w-full md:w-1/2" data-testid="image-text-image">
19
+ @if (imageUrl()) {
20
+ <img
21
+ [src]="imageUrl()"
22
+ [alt]="imageAlt() || heading()"
23
+ class="w-full h-auto rounded-lg object-cover bg-muted"
24
+ />
25
+ } @else {
26
+ <div class="w-full aspect-video rounded-lg bg-muted flex items-center justify-center">
27
+ <span class="text-muted-foreground text-sm">No image</span>
28
+ </div>
29
+ }
30
+ </div>
31
+
32
+ <!-- Text -->
33
+ <div class="w-full md:w-1/2">
34
+ <h2
35
+ class="text-2xl md:text-3xl font-bold text-foreground mb-4"
36
+ data-testid="image-text-heading"
37
+ >
38
+ {{ heading() }}
39
+ </h2>
40
+ <p class="text-lg text-muted-foreground leading-relaxed" data-testid="image-text-body">
41
+ {{ body() }}
42
+ </p>
43
+ </div>
44
+ </div>
45
+ </section>
46
+ `,
47
+ })
48
+ export class ImageTextBlockComponent {
49
+ readonly data = input.required<Record<string, unknown>>();
50
+
51
+ readonly heading = computed((): string => String(this.data()['heading'] ?? ''));
52
+ readonly body = computed((): string => String(this.data()['body'] ?? ''));
53
+ readonly imageUrl = computed((): string => String(this.data()['imageUrl'] ?? ''));
54
+ readonly imageAlt = computed((): string => String(this.data()['imageAlt'] ?? ''));
55
+ readonly reversed = computed((): boolean => this.data()['imagePosition'] === 'right');
56
+ }
@@ -0,0 +1,28 @@
1
+ import { ChangeDetectionStrategy, Component, computed, input } from '@angular/core';
2
+
3
+ @Component({
4
+ selector: 'app-text-block',
5
+ changeDetection: ChangeDetectionStrategy.OnPush,
6
+ host: {
7
+ class: 'block',
8
+ '[attr.data-testid]': '"block-textBlock"',
9
+ },
10
+ template: `
11
+ <section class="py-8 px-4 md:py-12 md:px-8 max-w-3xl mx-auto">
12
+ @if (heading()) {
13
+ <h2 class="text-2xl font-bold text-foreground mb-4" data-testid="text-heading">
14
+ {{ heading() }}
15
+ </h2>
16
+ }
17
+ <p class="text-lg text-muted-foreground leading-relaxed" data-testid="text-body">
18
+ {{ body() }}
19
+ </p>
20
+ </section>
21
+ `,
22
+ })
23
+ export class TextBlockComponent {
24
+ readonly data = input.required<Record<string, unknown>>();
25
+
26
+ readonly heading = computed((): string => String(this.data()['heading'] ?? ''));
27
+ readonly body = computed((): string => String(this.data()['body'] ?? ''));
28
+ }
@@ -0,0 +1,17 @@
1
+ import type { Provider } from '@angular/core';
2
+ import { provideBlockComponents } from '@momentumcms/ui';
3
+ import { HeroBlockComponent } from './blocks/hero-block.component';
4
+ import { TextBlockComponent } from './blocks/text-block.component';
5
+ import { ImageTextBlockComponent } from './blocks/image-text-block.component';
6
+
7
+ /**
8
+ * Provide all post block components for the block renderer.
9
+ * Add to the app's root providers.
10
+ */
11
+ export function providePostBlocks(): Provider[] {
12
+ return provideBlockComponents({
13
+ hero: HeroBlockComponent,
14
+ textBlock: TextBlockComponent,
15
+ imageText: ImageTextBlockComponent,
16
+ });
17
+ }
@@ -0,0 +1,31 @@
1
+ import type { ResolveFn } from '@angular/router';
2
+ import { injectMomentumAPI, type FindResult } from '@momentumcms/admin';
3
+
4
+ /**
5
+ * Resolver for the post detail route.
6
+ *
7
+ * Fetches the post by slug before the component renders. This ensures:
8
+ * - SSR renders the full post content (not just "Loading...")
9
+ * - The admin preview iframe (with scripts disabled) shows the real post
10
+ */
11
+ export const postDetailResolver: ResolveFn<FindResult<Record<string, unknown>>> = (route) => {
12
+ const api = injectMomentumAPI();
13
+ const slug = typeof route.params['slug'] === 'string' ? route.params['slug'] : undefined;
14
+
15
+ if (!slug) {
16
+ return {
17
+ docs: [],
18
+ totalDocs: 0,
19
+ page: 1,
20
+ totalPages: 0,
21
+ limit: 1,
22
+ hasNextPage: false,
23
+ hasPrevPage: false,
24
+ };
25
+ }
26
+
27
+ return api.collection('posts').find({
28
+ where: { slug: { equals: slug } },
29
+ limit: 1,
30
+ });
31
+ };