Skip to content

Commit

Permalink
Feat: Add support for server side rendering (#13)
Browse files Browse the repository at this point in the history
  • Loading branch information
RikudouSage committed Sep 12, 2023
1 parent 1290aa4 commit 7f89ce9
Show file tree
Hide file tree
Showing 14 changed files with 778 additions and 54 deletions.
18 changes: 16 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,19 +1,32 @@
# Frontend for Fediseer

Building:
## Running

- edit [environment.ts](src/environments/environment.ts) to your liking
- especially change the maintainer, please
- `yarn install`
- to install dependencies
- `yarn build`
- to build the app
- copy contents of `dist/fediseer-gui` to your favorite webserver
- copy contents of `dist/FediseerGUI/browser` to your favorite webserver
- configure your webserver to route all pages to `index.html`

> You can build the app using a single docker command:
> `docker run --rm -v $(pwd):/app -w /app -u $(id -u):$(id -g) node:18 bash -c 'yarn install && yarn build'`
## Running with server side rendering

- edit [environment.ts](src/environments/environment.ts) to your liking
- especially change the maintainer, please
- `yarn install`
- to install dependencies
- `yarn build:ssr`
- to build the app, both client and server
- copy contents of `dist/FediseerGUI` to your server
- `node dist/FediseerGUI/server/main.js`
- runs the node express server (replace with real path to your dir)
- configure your reverse proxy to route to localhost:4000

## Running using Docker

A ready-to-use docker image is available as [`ghcr.io/rikudousage/fediseer-gui`](https://ghcr.io/rikudousage/fediseer-gui).
Expand All @@ -29,5 +42,6 @@ Configuration is done using environment variables.
| FEDISEER_DEFAULT_CENSURE_LIST_FILTER_INSTANCES | The default instances to use in the censure list filters. List them as a comma separated values, for example `lemmings.world,lemmy.dbzer0.com`. The special value `__all__` can be used to mean all guaranteed instances | \_\_all__ |
| FEDISEER_SOURCE_CODE_LINK | The URL to the source code repository. You may want to set it to your fork URL if you're not using my version. | https://github.com/RikudouSage/FediseerGUI |
| FEDISEER_APP_VERSION | The current version of the Fediseer GUI. | gets the default from [environment.ts](src/environments/environment.ts) |
| FEDISEER_ENABLE_SSR | Set to any value to enable server-side rendering | `none` |

The app is running on port 80 inside the container.
63 changes: 62 additions & 1 deletion angular.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
"build": {
"builder": "@angular-devkit/build-angular:browser",
"options": {
"outputPath": "dist/fediseer-gui",
"outputPath": "dist/FediseerGUI/browser",
"index": "src/index.html",
"main": "src/main.ts",
"polyfills": [
Expand Down Expand Up @@ -103,6 +103,67 @@
],
"scripts": []
}
},
"server": {
"builder": "@angular-devkit/build-angular:server",
"options": {
"outputPath": "dist/FediseerGUI/server",
"main": "server.ts",
"tsConfig": "tsconfig.server.json",
"inlineStyleLanguage": "scss"
},
"configurations": {
"production": {
"outputHashing": "media"
},
"development": {
"buildOptimizer": false,
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.development.ts"
}
],
"optimization": false,
"sourceMap": true,
"extractLicenses": false,
"vendorChunk": true
}
},
"defaultConfiguration": "production"
},
"serve-ssr": {
"builder": "@nguniversal/builders:ssr-dev-server",
"configurations": {
"development": {
"browserTarget": "FediseerGUI:build:development",
"serverTarget": "FediseerGUI:server:development"
},
"production": {
"browserTarget": "FediseerGUI:build:production",
"serverTarget": "FediseerGUI:server:production"
}
},
"defaultConfiguration": "development"
},
"prerender": {
"builder": "@nguniversal/builders:prerender",
"options": {
"routes": [
"/"
]
},
"configurations": {
"production": {
"browserTarget": "FediseerGUI:build:production",
"serverTarget": "FediseerGUI:server:production"
},
"development": {
"browserTarget": "FediseerGUI:build:development",
"serverTarget": "FediseerGUI:server:development"
}
},
"defaultConfiguration": "production"
}
}
}
Expand Down
10 changes: 9 additions & 1 deletion docker-build.sh
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,12 @@ JSON="{apiUrl: '$FEDISEER_API_URL', apiVersion: '$FEDISEER_API_VERSION', appName

echo "export const environment = $JSON;" > src/environments/environment.ts

cd /app && yarn build && mv dist/fediseer-gui/* /usr/share/nginx/html && cd "$ORIGINAL_DIR"
cd /app
if [ -z ${FEDISEER_ENABLE_SSR+x} ]; then
yarn build && mv dist/FediseerGUI/browser/* /usr/share/nginx/html
else
cp nginx-proxy.conf /etc/nginx/conf.d/default.conf
yarn build:ssr && node dist/FediseerGUI/server/main.js &
fi

cd "$ORIGINAL_DIR"
12 changes: 12 additions & 0 deletions nginx-proxy.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
server {
listen 80;
server_name localhost;

location / {
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Host $http_host;
proxy_redirect off;
proxy_pass http://127.0.0.1:4000;
}
}
14 changes: 12 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,11 @@
"start": "ng serve",
"build": "ng build",
"watch": "ng build --watch --configuration development",
"test": "ng test"
"test": "ng test",
"dev:ssr": "ng run FediseerGUI:serve-ssr",
"serve:ssr": "node dist/FediseerGUI/server/main.js",
"build:ssr": "ng build && ng run FediseerGUI:server",
"prerender": "ng run FediseerGUI:prerender"
},
"private": true,
"dependencies": {
Expand All @@ -17,10 +21,13 @@
"@angular/forms": "^16.2.0",
"@angular/platform-browser": "^16.2.0",
"@angular/platform-browser-dynamic": "^16.2.0",
"@angular/platform-server": "^16.2.0",
"@angular/router": "^16.2.0",
"@ng-bootstrap/ng-bootstrap": "^15.1.1",
"@nguniversal/express-engine": "^16.2.0",
"@popperjs/core": "^2.11.8",
"@types/node": "^20.5.9",
"express": "^4.15.2",
"rxjs": "~7.8.0",
"tom-select": "^2.2.2",
"tslib": "^2.3.0",
Expand All @@ -30,7 +37,10 @@
"@angular-devkit/build-angular": "^16.2.1",
"@angular/cli": "~16.2.1",
"@angular/compiler-cli": "^16.2.0",
"@nguniversal/builders": "^16.2.0",
"@types/express": "^4.17.0",
"@types/jasmine": "~4.3.0",
"@types/node": "^16.11.7",
"jasmine-core": "~4.6.0",
"karma": "~6.4.0",
"karma-chrome-launcher": "~3.2.0",
Expand All @@ -40,4 +50,4 @@
"replace-in-file": "^7.0.1",
"typescript": "~5.1.3"
}
}
}
59 changes: 59 additions & 0 deletions server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import 'zone.js/node';

import { APP_BASE_HREF } from '@angular/common';
import { ngExpressEngine } from '@nguniversal/express-engine';
import * as express from 'express';
import { existsSync } from 'node:fs';
import { join } from 'node:path';
import { AppServerModule } from './src/main.server';

// The Express app is exported so that it can be used by serverless Functions.
export function app(): express.Express {
const server = express();
const distFolder = join(process.cwd(), 'dist/FediseerGUI/browser');
const indexHtml = existsSync(join(distFolder, 'index.original.html')) ? 'index.original.html' : 'index';

// Our Universal express-engine (found @ https://github.com/angular/universal/tree/main/modules/express-engine)
server.engine('html', ngExpressEngine({
bootstrap: AppServerModule
}));

server.set('view engine', 'html');
server.set('views', distFolder);

// Example Express Rest API endpoints
// server.get('/api/**', (req, res) => { });
// Serve static files from /browser
server.get('*.*', express.static(distFolder, {
maxAge: '1y'
}));

// All regular routes use the Universal engine
server.get('*', (req, res) => {
res.render(indexHtml, { req, providers: [{ provide: APP_BASE_HREF, useValue: req.baseUrl }] });
});

return server;
}

function run(): void {
const port = process.env['PORT'] || 4000;

// Start up the Node server
const server = app();
server.listen(port, () => {
console.log(`Node Express server listening on http://localhost:${port}`);
});
}

// Webpack will replace 'require' with '__webpack_require__'
// '__non_webpack_require__' is a proxy to Node 'require'
// The below code is to ensure that the server is run only when not requiring the bundle.
declare const __non_webpack_require__: NodeRequire;
const mainModule = __non_webpack_require__.main;
const moduleFilename = mainModule && mainModule.filename || '';
if (moduleFilename === __filename || moduleFilename.includes('iisnode')) {
run();
}

export * from './src/main.server';
4 changes: 3 additions & 1 deletion src/app/app-routing.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,9 @@ const routes: Routes = [
];

@NgModule({
imports: [RouterModule.forRoot(routes)],
imports: [RouterModule.forRoot(routes, {
initialNavigation: 'enabledBlocking'
})],
exports: [RouterModule]
})
export class AppRoutingModule { }
4 changes: 2 additions & 2 deletions src/app/app.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ export class AppComponent implements OnInit {

this.createMaintainerLink();

if (window.outerWidth <= this.autoCollapse) {
if (typeof window !== 'undefined' && window.outerWidth <= this.autoCollapse) {
await this.toggleSideMenu();
}

Expand All @@ -78,7 +78,7 @@ export class AppComponent implements OnInit {
if (event instanceof NavigationEnd) {
this.errorNotifications = [];
this.successNotifications = [];
if (window.outerWidth <= this.autoCollapse) {
if (typeof window !== 'undefined' && window.outerWidth <= this.autoCollapse) {
this.hideMenu();
}
}
Expand Down
4 changes: 2 additions & 2 deletions src/app/app.module.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import {BrowserModule, provideClientHydration} from '@angular/platform-browser';

import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
Expand All @@ -22,7 +22,7 @@ import {ReactiveFormsModule} from "@angular/forms";
SharedModule,
ReactiveFormsModule,
],
providers: [],
providers: [provideClientHydration()],
bootstrap: [AppComponent]
})
export class AppModule { }
14 changes: 14 additions & 0 deletions src/app/app.server.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
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 {}
37 changes: 31 additions & 6 deletions src/app/services/database.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ export class DatabaseService {
private readonly censureListFiltersKey = 'censure_list_filters';

public getStoredInstance(): Instance | null {
if (typeof localStorage === 'undefined') {
return null;
}
const stored = localStorage.getItem(this.storedInstanceKey);
if (stored !== null) {
return JSON.parse(stored);
Expand All @@ -22,25 +25,39 @@ export class DatabaseService {
}

public get lemmyPassword(): string | null {
if (typeof sessionStorage === 'undefined') {
return null;
}
return sessionStorage.getItem(this.lemmyPasswordKey);
}

public set lemmyPassword(password: string) {
if (typeof sessionStorage === 'undefined') {
return;
}
sessionStorage.setItem(this.lemmyPasswordKey, password);
}

public setStoredInstance(instance: Instance): void {
if (typeof localStorage === 'undefined') {
return;
}
localStorage.setItem(this.storedInstanceKey, JSON.stringify(instance));
}

public removeStoredInstance(): void {
if (typeof localStorage === 'undefined') {
return;
}
localStorage.removeItem(this.storedInstanceKey);
}

public getLemmySynchronizationSettings(): SynchronizeSettings {
const stored = localStorage.getItem(this.lemmySynchronizationSettingsKey);
if (stored !== null) {
return JSON.parse(stored);
if (typeof localStorage !== 'undefined') {
const stored = localStorage.getItem(this.lemmySynchronizationSettingsKey);
if (stored !== null) {
return JSON.parse(stored);
}
}

return {
Expand All @@ -54,13 +71,18 @@ export class DatabaseService {
}

public setLemmySynchronizationSettings(settings: SynchronizeSettings): void {
if (typeof localStorage === 'undefined') {
return;
}
localStorage.setItem(this.lemmySynchronizationSettingsKey, JSON.stringify(settings));
}

public get censureListFilters(): CensureListFilters {
const stored = localStorage.getItem(this.censureListFiltersKey);
if (stored !== null) {
return JSON.parse(stored);
if (typeof localStorage !== 'undefined') {
const stored = localStorage.getItem(this.censureListFiltersKey);
if (stored !== null) {
return JSON.parse(stored);
}
}

return {
Expand All @@ -74,6 +96,9 @@ export class DatabaseService {
}

public set censureListFilters(filters: CensureListFilters) {
if (typeof localStorage === 'undefined') {
return;
}
localStorage.setItem(this.censureListFiltersKey, JSON.stringify(filters));
}
}
2 changes: 2 additions & 0 deletions src/main.server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@

export { AppServerModule } from './app/app.server.module';
Loading

0 comments on commit 7f89ce9

Please sign in to comment.