Back

Orchard Blog with Angular SSR

To adapt your Orchard Core CMS-based blog site with Angular SSR to use **Cloudflare Workers** for Server-Side Rendering (SSR), we’ll leverage Cloudflare Workers’ serverless environment to render your Angular Universal app at the edge.

To adapt your Orchard Core CMS-based blog site with Angular SSR to use Cloudflare Workers for Server-Side Rendering (SSR), we’ll leverage Cloudflare Workers’ serverless environment to render your Angular Universal app at the edge. This replaces the Node.js-based SSR setup (e.g., Express) from the previous approach with a Cloudflare Worker, maintaining Orchard Core as the headless CMS backend. Cloudflare Workers don’t natively run full Node.js environments, but with tools like @cfworker/universal, you can execute Angular SSR efficiently at the edge.


Architecture Overview

  • Orchard Core Backend: Headless CMS serving blog data via the Content Delivery API.
  • Angular Frontend with SSR: Angular Universal app rendered by a Cloudflare Worker.
  • Cloudflare Worker: Handles SSR at the edge, fetching data from Orchard Core and serving pre-rendered HTML.

Steps to Use Cloudflare Workers with Angular SSR

1. Set Up Orchard Core (Unchanged)

  • Use the existing Orchard Core setup:
    • Project: OrchardBlogApi with Blog recipe.
    • API: /api/content (e.g., http://localhost:5000/api/content?contentType=BlogPost).
    • Content: "My Blog" with "First Post" and "Second Post".
  • Deploy or run locally:
    dotnet run
    
  • Deployed URL (e.g., https://your-orchard-api.com/api/content) for production.

2. Set Up Angular with SSR

  • Create Angular Project:

    npm install -g @angular/cli
    ng new angular-orchard-ssr-worker --style=scss --routing=true
    cd angular-orchard-ssr-worker
    ng add @nguniversal/express-engine
    
  • Install Bootstrap (optional):

    npm install bootstrap
    

    Update angular.json:

    "styles": ["node_modules/bootstrap/dist/css/bootstrap.min.css", "src/styles.scss"]
    

3. Define Angular Model

  • src/app/models/blog-post.ts (unchanged):
    export interface BlogPost {
      contentItemId: string;
      displayText: string;
      content: string;
      publishedUtc: string;
    }
    

4. Create Angular Service

  • Generate and update src/app/services/blog.service.ts:
    import { Injectable, Inject, PLATFORM_ID } from '@angular/core';
    import { HttpClient } from '@angular/common/http';
    import { Observable } from 'rxjs';
    import { map } from 'rxjs/operators';
    import { BlogPost } from '../models/blog-post';
    import { isPlatformServer } from '@angular/common';
    
    @Injectable({
      providedIn: 'root'
    })
    export class BlogService {
      private apiUrl = 'http://localhost:5000/api/content'; // Update with Orchard API URL
    
      constructor(
        private http: HttpClient,
        @Inject(PLATFORM_ID) private platformId: Object
      ) {}
    
      getBlogPosts(): Observable<BlogPost[]> {
        return this.http.get<any>(`${this.apiUrl}?contentType=BlogPost`).pipe(
          map(response => response.data.map(item => ({
            contentItemId: item.contentItemId,
            displayText: item.displayText,
            content: item.blogPost.content.html,
            publishedUtc: item.publishedUtc
          })))
        );
      }
    
      getBlogPost(id: string): Observable<BlogPost> {
        return this.http.get<any>(`${this.apiUrl}/${id}`).pipe(
          map(item => ({
            contentItemId: item.contentItemId,
            displayText: item.displayText,
            content: item.blogPost.content.html,
            publishedUtc: item.publishedUtc
          }))
        );
      }
    }
    

5. Set Up Routing

  • app-routing.module.ts:
    import { NgModule } from '@angular/core';
    import { RouterModule, Routes } from '@angular/router';
    import { BlogListComponent } from './components/blog-list/blog-list.component';
    import { BlogPostComponent } from './components/blog-post/blog-post.component';
    
    const routes: Routes = [
      { path: '', component: BlogListComponent },
      { path: 'post/:id', component: BlogPostComponent },
      { path: '**', redirectTo: '' }
    ];
    
    @NgModule({
      imports: [RouterModule.forRoot(routes, { initialNavigation: 'enabledBlocking' })],
      exports: [RouterModule]
    })
    export class AppRoutingModule {}
    

6. Create Components

  • Blog List:

    ng generate component components/blog-list
    
    • blog-list.component.ts:
      import { Component, OnInit } from '@angular/core';
      import { BlogService } from '../../services/blog.service';
      import { BlogPost } from '../../models/blog-post';
      
      @Component({
        selector: 'app-blog-list',
        templateUrl: './blog-list.component.html'
      })
      export class BlogListComponent implements OnInit {
        posts: BlogPost[] = [];
      
        constructor(private blogService: BlogService) {}
      
        ngOnInit() {
          this.blogService.getBlogPosts().subscribe(posts => {
            this.posts = posts;
          });
        }
      }
      
    • blog-list.component.html:
      <div class="container">
        <h1>My Blog</h1>
        <ul class="list-group">
          <li *ngFor="let post of posts" class="list-group-item">
            <a [routerLink]="['/post', post.contentItemId]">{{ post.displayText }}</a> - 
            {{ post.publishedUtc | date:'MMMM dd, yyyy' }}
          </li>
        </ul>
      </div>
      
  • Blog Post:

    ng generate component components/blog-post
    
    • blog-post.component.ts:
      import { Component, OnInit } from '@angular/core';
      import { ActivatedRoute } from '@angular/router';
      import { BlogService } from '../../services/blog.service';
      import { BlogPost } from '../../models/blog-post';
      
      @Component({
        selector: 'app-blog-post',
        templateUrl: './blog-post.component.html'
      })
      export class BlogPostComponent implements OnInit {
        post: BlogPost | null = null;
      
        constructor(private route: ActivatedRoute, private blogService: BlogService) {}
      
        ngOnInit() {
          const id = this.route.snapshot.paramMap.get('id');
          if (id) {
            this.blogService.getBlogPost(id).subscribe(post => {
              this.post = post;
            });
          }
        }
      }
      
    • blog-post.component.html:
      <div class="container" *ngIf="post">
        <h1>{{ post.displayText }}</h1>
        <p><em>Published on: {{ post.publishedUtc | date:'MMMM dd, yyyy' }}</em></p>
        <div [innerHTML]="post.content"></div>
        <a routerLink="/">Back to Blog</a>
      </div>
      

7. Update Angular Modules

  • app.module.ts:

    import { NgModule } from '@angular/core';
    import { BrowserModule } from '@angular/platform-browser';
    import { HttpClientModule } from '@angular/common/http';
    import { AppRoutingModule } from './app-routing.module';
    import { AppComponent } from './app.component';
    import { BlogListComponent } from './components/blog-list/blog-list.component';
    import { BlogPostComponent } from './components/blog-post/blog-post.component';
    
    @NgModule({
      declarations: [AppComponent, BlogListComponent, BlogPostComponent],
      imports: [
        BrowserModule.withServerTransition({ appId: 'angular-orchard-ssr' }),
        HttpClientModule,
        AppRoutingModule
      ],
      bootstrap: [AppComponent]
    })
    export class AppModule {}
    
  • app.server.module.ts:

    import { NgModule } from '@angular/core';
    import { ServerModule } from '@angular/platform-server';
    import { AppModule } from './app.module';
    import { AppComponent } from './app.component';
    
    @NgModule({
      imports: [AppModule, ServerModule],
      bootstrap: [AppComponent]
    })
    export class AppServerModule {}
    

8. Build Angular for SSR

  • Build the app:
    npm run build:ssr
    
  • This generates dist/angular-orchard-ssr-worker/browser and dist/angular-orchard-ssr-worker/server.

9. Set Up Cloudflare Worker for SSR

  • Install Wrangler:

    npm install -g @cloudflare/wrangler
    
  • Initialize Worker Project:

    wrangler init ssr-worker
    cd ssr-worker
    
  • Install @cfworker/universal:

    npm install @cfworker/universal
    
  • Create Worker Script (index.js):

    import { renderModuleFactory } from '@cfworker/universal';
    import { AppServerModuleNgFactory } from '../dist/angular-orchard-ssr-worker/server/main';
    
    addEventListener('fetch', event => {
      event.respondWith(handleRequest(event.request));
    });
    
    async function handleRequest(request) {
      const url = new URL(request.url);
      const apiUrl = 'https://your-orchard-api.com/api/content'; // Update with deployed Orchard URL
    
      try {
        const html = await renderModuleFactory(AppServerModuleNgFactory, {
          document: '<app-root></app-root>',
          url: url.pathname + url.search,
          extraProviders: [
            { provide: 'REQUEST_URL', useValue: url.toString() },
            { provide: 'API_URL', useValue: apiUrl }
          ]
        });
    
        return new Response(html, {
          headers: { 'Content-Type': 'text/html' }
        });
      } catch (error) {
        return new Response(`Error: ${error.message}`, { status: 500 });
      }
    }
    
  • Copy Angular Build Output:

    • Move dist/angular-orchard-ssr-worker/server/main.js into the ssr-worker folder (or adjust the import path).
  • Update wrangler.toml:

    name = "ssr-worker"
    main = "index.js"
    compatibility_date = "2023-10-01"
    [build]
    command = "npm install"
    

10. Deploy to Cloudflare

  • Login:
    wrangler login
    
  • Deploy:
    wrangler deploy
    
  • Access your Worker at https://ssr-worker.your-subdomain.workers.dev.

11. Test

  • Visit the Worker URL to see the SSR-rendered blog list.
  • Click a post to navigate to its SSR-rendered detail page.

Why Cloudflare Workers with SSR?

  • Edge Rendering: SSR at Cloudflare’s edge reduces latency and improves SEO (web:0, web:11).
  • Scalability: Workers scale automatically, handling traffic spikes effortlessly.
  • Orchard Core: Its API-first design integrates seamlessly (web:13).

Notes

  • Static Assets: Serve Angular’s dist/browser files (CSS, JS) via Cloudflare Pages or a Worker route:
    if (url.pathname.startsWith('/assets') || url.pathname.endsWith('.js') || url.pathname.endsWith('.css')) {
      return fetch(request); // Proxy to a static host
    }
    
  • Security: Add an API key or JWT to the Orchard API and validate it in the Worker.
  • Limitations: Workers have a 128MB memory limit and no full Node.js runtime; @cfworker/universal bridges this for SSR.

This setup combines Orchard Core’s blogging with Angular SSR at the Cloudflare edge, aligning with web results on headless CMS and SSR (web:0, web:11). Let me know if you need help with assets or security!