Skip to content

Commit

Permalink
Prerender
Browse files Browse the repository at this point in the history
  • Loading branch information
max-lt committed Feb 19, 2024
1 parent f912602 commit 3a389e0
Show file tree
Hide file tree
Showing 10 changed files with 211 additions and 12 deletions.
16 changes: 16 additions & 0 deletions angular.json
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,22 @@
"options": {
"buildTarget": "app:build"
}
},
"ssr": {
"builder": "@angular-builders/custom-webpack:server",
"options": {
"customWebpackConfig": {
"path": "./webpack/webpack.extra.config.js"
},
"sourceMap": true,
"outputPath": "dist/server",
"tsConfig": "tsconfig.ssr.json",
"main": "prerender.ts",
"optimization": {
"styles": false,
"scripts": true
}
}
}
}
}
Expand Down
6 changes: 5 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,11 @@
"dev": "ng serve --host 127.0.0.1 --port 4300",
"dev:prod": "ng serve --host 127.0.0.1 --port 4300 --configuration production",
"start": "ng serve",
"build": "ng build --configuration production",
"build": "ng build && ng run app:ssr && npm run prerender",
"build:ssr": "ng run app:ssr",
"build:app": "ng build --configuration production",
"build:app:dev": "ng build --configuration development",
"prerender": "node dist/server/main.js",
"watch": "ng build --watch --configuration development",
"postinstall": "cd webpack && npm install",
"clean": "rm -rf dist .angular node_modules webpack/node_modules"
Expand Down
154 changes: 154 additions & 0 deletions prerender.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
import { existsSync, mkdir, readFileSync, writeFile, copyFileSync } from 'node:fs';
import { join } from 'node:path';
import { promisify } from 'node:util';

import 'zone.js/node';

import { MainComponent } from './src/app/main.component';
import { routes } from './src/app/app.routes';

import { ApplicationConfig, Provider } from '@angular/core';
import { bootstrapApplication } from '@angular/platform-browser';
import { BEFORE_APP_SERIALIZED, provideServerRendering, renderApplication } from '@angular/platform-server';

import { DOCUMENT } from '@angular/common';
import { provideRouter } from '@angular/router';

const distFolder = join(process.cwd(), 'dist/browser');

// Copy index.html to index.original.html if not exists
let indexHtml = existsSync(join(distFolder, 'index.original.html')) ? 'index.original.html' : 'index.html';
if (indexHtml === 'index.html') {
copyFileSync(join(distFolder, 'index.html'), join(distFolder, 'index.original.html'));
indexHtml = 'index.original.html';
}

const documentPath = join(distFolder, indexHtml);
const document = readFileSync(documentPath).toString();

const writeFileAsync = promisify(writeFile);
const mkdirAsync = promisify(mkdir);

const cache = new Map<string, string>();

const appConfig: ApplicationConfig = {
providers: [
provideRouter(routes),
provideServerRendering(),
{
provide: BEFORE_APP_SERIALIZED,
useFactory: (document: Document) => () => {
const styles = Array.from(document.head.querySelectorAll('style'));

// Remove inlined "critical" styles
for (const style of styles) {
style.remove();
}

// Remove style imports and replace them with inlined styles
const links = Array.from(document.head.querySelectorAll('link[rel="stylesheet"]'));
for (const link of links) {
const name = link.getAttribute('href');
link.remove();

if (!name) {
console.warn('Missing href attribute on link', link);
return;
}

let css = cache.get(name);

if (!css) {
// Reading file and writing it to the final html file
css = readFileSync(join(distFolder, name))
.toString()
// Remove comments
.replace(/\/\*[\s\S]*?\*\/|([^:]|^)\/\/.*$/gm, '')
// Remove newlines
.replace(/\n/g, ' ')
// Remove multiple spaces
.replace(/\s{2,}/g, ' ');

cache.set(name, css);
}

document.head.appendChild(document.createElement('style')).textContent = css;
}
},
deps: [DOCUMENT],
multi: true
}
]
};

function render(url: string, platformProviders?: Provider[]) {
return renderApplication(() => bootstrapApplication(MainComponent, appConfig), {
url,
document,
platformProviders
});
}

enum Rules {
DEFAULT,
GITHUB
}

async function writeHtml(rule: Rules, path: string, html: string) {
switch (rule) {
case Rules.DEFAULT: {
// Create folder if not exists
const folder = join(distFolder, path);

if (!existsSync(folder)) {
await mkdirAsync(folder, { recursive: true });
}

await writeFileAsync(join(distFolder, path, 'index.html'), html);

break;
}
// https://stackoverflow.com/questions/33270605/github-pages-trailing-slashes
case Rules.GITHUB: {
const parts = path.split('/').filter((part) => part !== '');

// Parts is empty if path is '/'
const file = parts.pop() ?? 'index';

const base = join(...parts);

const folder = join(distFolder, base);

if (!existsSync(folder)) {
await mkdirAsync(folder, { recursive: true });
}

await writeFileAsync(join(distFolder, base, `${file}.html`), html);

break;
}
}
}

(async () => {
for (const route of routes) {
console.info('Prerendering', route.path ?? route);

if (typeof route.path !== 'string') {
console.warn('Skipping route without path', route);
continue;
}

const path = route.path === '**' ? '404' : route.path;

try {
const html = await render(path, []);

await writeHtml(Rules.GITHUB, path, html);
} catch (err) {
console.error(`Error rendering ${route}`, err);
}
}

console.info('Done');
})();
2 changes: 1 addition & 1 deletion src/app/app.routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,6 @@ export const routes: Routes = [
...createRoutes('docs', docsUrl, docsConfig, 'https://github.com/openworkers/openworkers-website/tree/master'),
{
path: '**',
redirectTo: '/'
loadComponent: () => import('./pages/not-found/not-found.page')
}
];
4 changes: 2 additions & 2 deletions src/app/pages/main/main.page.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { CommonModule } from '@angular/common';
import { HttpClient } from '@angular/common/http';
import { Component } from '@angular/core';
import { Component, Optional } from '@angular/core';
import { FormControl, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
import { RouterModule } from '@angular/router';
import { firstValueFrom } from 'rxjs';
Expand All @@ -24,7 +24,7 @@ export default class MainPage {

public readonly content = atob(parsed);

constructor(private http: HttpClient) {}
constructor(@Optional() private http: HttpClient) {}

public async subscribe() {
const form = this.newsletterForm;
Expand Down
11 changes: 11 additions & 0 deletions src/app/pages/not-found/not-found.page.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@

<div class="container max-w-7xl flex-col items-center justify-center min-h-[calc(100vh-2.5rem)]">
<div class="sm:flex items-center">
<h1 class="text-4xl font-bold text-center sm:mr-6 sm:pr-6 sm:border-r text-red-theme">404</h1>
<h2 class="text-lg text-center mt-4 sm:mt-0">This page could not be found</h2>
</div>

<a class="text-lg my-8 text-slate-700 hover:text-red-theme rounded-md px-2 py-1" routerLink="/">
Go back to the homepage
</a>
</div>
9 changes: 9 additions & 0 deletions src/app/pages/not-found/not-found.page.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { Component } from '@angular/core';
import { RouterModule } from '@angular/router';

@Component({
standalone: true,
imports: [RouterModule],
templateUrl: './not-found.page.html'
})
export default class NotFoundPage {}
12 changes: 12 additions & 0 deletions tsconfig.ssr.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"incremental": true,
"outDir": "./out-tsc/app",
"types": ["node"]
},
"files": [
"prerender.ts"
],
"include": ["src/**/*.d.ts"]
}
2 changes: 1 addition & 1 deletion webpack/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 0 additions & 7 deletions webpack/webpack.extra.config.js
Original file line number Diff line number Diff line change
@@ -1,21 +1,14 @@
import webpack from 'webpack';

console.log('Using custom webpack config (./webpack.extra.config.js)\n');

export default {
target: 'es2020',
experiments: {
outputModule: true,
buildHttp: {
// https://webpack.js.org/configuration/experiments/#experimentsbuildhttpalloweduris
allowedUris: ['https://raw.githubusercontent.com/'],
cacheLocation: '/tmp/webpack-cache',
frozen: false
}
},
output: {
module: true
},
module: {
rules: [
{
Expand Down

0 comments on commit 3a389e0

Please sign in to comment.