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:
OrchardBlogApiwith Blog recipe. - API:
/api/content(e.g.,http://localhost:5000/api/content?contentType=BlogPost). - Content: "My Blog" with "First Post" and "Second Post".
- Project:
- 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-engineInstall Bootstrap (optional):
npm install bootstrapUpdate
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-listblog-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-postblog-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/browseranddist/angular-orchard-ssr-worker/server.
9. Set Up Cloudflare Worker for SSR
Install Wrangler:
npm install -g @cloudflare/wranglerInitialize Worker Project:
wrangler init ssr-worker cd ssr-workerInstall
@cfworker/universal:npm install @cfworker/universalCreate 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.jsinto thessr-workerfolder (or adjust the import path).
- Move
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/browserfiles (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/universalbridges 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!