Integrate #angular/cli with server-side EJS templates - node.js

Goal: Modify Open Graph meta tags when web crawlers visit different routes.
I know Angular2 4.0.0 has a MetaService, and there is always jQuery, but web crawlers except Googlebot don't execute Javascript, so it is kind of useless to my purpose. While Angular Universal sounds like an overkill for changing a couple meta tags.
So far my solution is to copy and paste the compiled index.html in /dist to index.ejs, and modify the necessary fields. Is it possible to integrate the workflow with the angular-cli compilation process, by having the entry point changed from index.html to index.ejs? If not, what are the alternatives that I should explore?
In my index.ejs :
<meta property="og:url" content="<%= url %>" />
In my Express route index.js :
res.render('index', {
url: site_url,
});
In my server.js:
app.set('views', path.join(__dirname, '/dist'));
app.set('view engine', 'ejs');
Please refrain your answer to the current #angular/cli version (v1.0.1 compatible).
Some related discussions:
Add customization options for HtmlWebpackPlugin #3338
Conditional template logic in index.html for beta.11-webpack #1544
Provide config to rename index.html in dist folder #2241

I was able to solve this by using Nunjucks to render templates served via Angular Universal. I'm sure it's possible to use other engines such as EJS as well. Here are the relevant portions of my server.ts:
import * as dotenv from 'dotenv';
dotenv.config();
import 'zone.js/dist/zone-node';
import 'reflect-metadata';
import * as nunjucks from 'nunjucks';
import { renderModuleFactory } from '#angular/platform-server';
import { enableProdMode } from '#angular/core';
import * as express from 'express';
import { join } from 'path';
import { readFileSync } from 'fs';
// Faster server renders w/ Prod mode (dev mode never needed)
enableProdMode();
// Express server
const app = express();
const PORT = process.env.PORT || 4201;
const DIST_FOLDER = join(process.cwd(), 'dist');
// Our index.html we'll use as our template
const template = readFileSync(join(DIST_FOLDER, 'index.html')).toString();
// * NOTE :: leave this as require() since this file is built Dynamically from webpack
const { AppServerModuleNgFactory, LAZY_MODULE_MAP } = require('./dist-server/main.bundle');
const { provideModuleMap } = require('#nguniversal/module-map-ngfactory-loader');
nunjucks.configure(DIST_FOLDER, {
autoescape: true,
express: app,
});
app.engine('html', (_, options, callback) => {
renderModuleFactory(AppServerModuleNgFactory, {
// Our index.html
document: template,
url: options.req.url,
// DI so that we can get lazy-loading to work differently (since we need it to just instantly render it)
extraProviders: [
provideModuleMap(LAZY_MODULE_MAP)
]
}).then(html => {
callback(null, html);
});
});
app.set('view engine', 'html');
// Server static files from dist folder
app.get('*.*', express.static(DIST_FOLDER));
// All regular routes use the Universal engine
// You can pass in server-side values here to have Nunjucks render them
app.get('*', (req, res) => {
res.render('index', { req, yourVar: 'some-value' });
});
// Start up the Node server
app.listen(PORT, () => {
console.log(`Node server listening on http://localhost:${PORT}`);
});

Related

Angular Universal server express deployment to production errors

Hi I am getting always error in production when I try to deploy to webserver.
I am deploying everything to /var/myapp folder so basically in my server I have this paths:
/var/myapp/client/dist/client/browser folder where there is index.html and
/var/myapp/client/dist/client/server where there is the main.js file
I am running on server node /var/myapp/client/dist/client/server/main.js
Please help with the right configuration on server.ts file
import { ngExpressEngine } from '#nguniversal/express-engine';
import * as express from 'express';
import { join } from 'path';
import { AppServerModule } from './src/main.server';
import { APP_BASE_HREF } from '#angular/common';
import { existsSync } from 'fs';
import 'localstorage-polyfill';
global.localStorage;
// The Express app is exported so that it can be used by serverless Functions.
export function app() {
const server = express();
const distFolder = join(process.cwd(), '/var/myapp/client/dist/client/browser');
const indexHtml = existsSync(join(distFolder, 'index.html')) ? 'index.html' : 'index';
// Our Universal express-engine (found # https://github.com/angular/universal/tree/master/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() {
const port = 4000;
// Start up the Node server
const server = app();
server.listen(port, '127.0.0.1', function() {
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';
Angular universal deployment issue content is displayed but no css.
I am getting those errors:
runtime-es2015.3f64214655012374b6e0.js:1 Failed to load module script: The server responded with a non-JavaScript MIME type of "text/html". Strict MIME type checking is enforced for module scripts per HTML spec.
polyfills-es2015.629687f397387c194377.js:1 Failed to load module script: The server responded with a non-JavaScript MIME type of "text/html". Strict MIME type checking is enforced for module scripts per HTML spec.
main-es2015.a772fe1bf7606c343730.js:1 Failed to load module script: The server responded with a non-JavaScript MIME type of "text/html". Strict MIME type checking is enforced for module scripts per HTML spec.

Angular Universal with api internal express calls

I have checked several tutorials and walkthroughs on how to add SSR (server-side rendering) to existing angular CLI & Express projects.
I was able to run node with SSR but I'm unable to make API calls.
I wish to run one node process that can serve static file (Angular with SSR) and dynamic content with API calls which access SQLITE DB using KNEX.
This is my server.js file
// These are important and needed before anything else
import 'zone.js/dist/zone-node';
import 'reflect-metadata';
import { enableProdMode } from '#angular/core';
import * as express from 'express';
import { join } from 'path';
// Faster server renders w/ Prod mode (dev mode never needed)
enableProdMode();
// Express server
const app = express();
const PORT = process.env.PORT || 3000;
const DIST_FOLDER = join(process.cwd(), 'bin/dist');
// * NOTE :: leave this as require() since this file is built Dynamically from webpack
const { AppServerModuleNgFactory, LAZY_MODULE_MAP } = require('./bin/dist/server/main');
// Express Engine
import { ngExpressEngine } from '#nguniversal/express-engine';
// Import module map for lazy loading
import { provideModuleMap } from '#nguniversal/module-map-ngfactory-loader';
app.engine('html', ngExpressEngine({
bootstrap: AppServerModuleNgFactory,
providers: [
provideModuleMap(LAZY_MODULE_MAP)
]
}));
app.set('view engine', 'html');
app.set('views', join(DIST_FOLDER, ''));
// import * as apiS from './server/index';
// apiS.load(app);
// TODO: implement data requests securely
// app.get('/api/*', (req, res) => {
// res.status(404).send('data requests are not supported');
// });
import * as LoadRoutes from './server/boot/routes';
LoadRoutes.load(app);
// Server static files from /browser
app.get('*.*', express.static(join(DIST_FOLDER, '')));
// All regular routes use the Universal engine
app.get('*', (req, res) => {
res.render('index', { req });
});
// Start up the Node server
app.listen(PORT, () => {
console.log(`Node server listening on http://localhost:${PORT}`);
});
Is it even possible to accomplish SSR + API calls from the same node process and if so can someone post here answer?
It's already there. Just uncomment these lines and extend the API routes with Express router:
app.get('/api/*', (req, res) => {
res.status(404).send('data requests are not supported');
});
Use the router file instead:
app.use('/api/*', appRoutes);
Every URL starting with api/ will be served by backend APIs.

Why my logo isn't displaying nodejs app

Here is a picture of my project directory.
I have my logo in the template/assets/logo.png.
In my server.ts file, I use middleware express as app.use( express.static( "template" ) );
unfortunately, When I try to display the logo in my html file, it doesn't display.
<img src="assets/logo.png" alt="Logo">
Here is my server.ts file:
// Import everything from express and assign it to the express variable
import express from 'express';
import fs from 'fs';
// Import WelcomeController from controllers entry point
import {WelcomeController} from './app/controllers';
// Create a new express application instance
const app: express.Application = express();
// The port the express app will listen on
const port: any = process.env.PORT || 3000;
// Template folder
app.use( express.static( "template" ) );
// Mount the WelcomeController at the /welcome route
app.use('/', WelcomeController);
// Serve the application at the given port
app.listen(port, () => {
const source = fs.createReadStream('/src/app');
const dest = fs.createWriteStream('/dist');
source.pipe(dest);
source.on('end', function() { /* copied */ });
source.on('error', function(err) { /* error */ });
// Success callback
console.log(`Listening at http://localhost:${port}/`);
});
What would be the way to display my templates with the files showing?
I was doing this wrong app.use( express.static( "template" ) );
I should be doing app.use(express.static(__dirname + '/app/template'));
i think you need to define what the root directory is first in express before importing assets (at least, it helps!)
so if you import express and then assign express() to a variable ,e.g.
var express = require express();
var app = express;
then you could do something like:
var root = require('path').join(__dirname,'/template');
app.use(express.static(root));
Once the root is defined, all further files will be called from that place when a url is referenced.
hope this helps

Express homepage not rendered on server

I am developing a react app that uses server-side rendering.
My routes work, except the homepage. Not sure why. I'm assuming I'm not setting up my express server correctly to handle the index route...?
The following is the module which handles express' setup and whatnot. I'm assuming I have the app.get('*) or app.use(express.static) incorrect.
app.js (server)
require('ignore-styles')
const compression = require('compression')
const express = require('express')
const path = require('path')
require('babel-register')({
ignore: /\/(build|node_modules)\//,
presets: ['env', 'react-app']
})
const universalLoader = require('./universal')
const app = express()
// Support Gzip
app.use(compression())
// Serve static assets
app.use(express.static(path.resolve(__dirname, '..', 'build')))
// Always return the main index.html, so react-router render the route in the client
app.get('*', universalLoader)
module.exports = app
universalLoader.js (server)
import path from 'path'
import fs from 'fs'
import React from 'react'
import { Provider } from 'react-redux'
import { renderToString } from 'react-dom/server'
import { StaticRouter, matchPath } from 'react-router-dom'
import configureStore from '../src/store'
import App from '../src/components/App'
import routes from '../src/shared/routes'
import { getSiteInfo } from '../src/store/actions/siteInfo'
import { REACT_APP_SITE_KEY } from '../src/shared/vars'
import Helmet from 'react-helmet'
module.exports = function universalLoader(req, res, next) {
// console.log('Loading....')
const store = configureStore()
const fullUrl = req.protocol + '://' + req.get('host') + req.originalUrl
let routeFound = false
// Try to find a matched route
const promises = routes.reduce((promise, route) => {
var props = matchPath(req.url, route)
// If route was matched, component exists, and has an initialAction
// then call it's initialAction.
// This action will most-likely load some data asyncronously
if (props && route.component && route.component.initialAction) {
promise.push(Promise.resolve(store.dispatch(route.component.initialAction(store, props))))
}
return promise
}, [])
// Load initial site data
promises.push(Promise.resolve(store.dispatch(getSiteInfo(REACT_APP_SITE_KEY))))
// Wait until all async data has been loaded
Promise.all(promises)
.then(() => {
// Load index file path
const filePath = path.resolve(__dirname, '..', 'build', 'index.html')
// Read index file
fs.readFile(filePath, 'utf8', (err, htmlData) => {
if(err){
console.error('read err', err)
return res.status(404).end()
}
const preloadedState = store.getState()
// console.log("PreloadedState:", preloadedState)
const context = preloadedState
// console.log(context)
// Note: Pass in serverRequest prop so the App knows the domain it's on for meta tags
const markup = renderToString(
<Provider store={store}>
<StaticRouter location={req.url} context={context}>
<App serverRequest={req} serverResponse={res} />
</StaticRouter>
</Provider>
)
const helmet = Helmet.renderStatic()
// Somewhere a `<Redirect>` was rendered
if(context.url){
console.log('Redirected:', context.url)
redirect(301, context.url)
// we're good, send the response
}else{
// Page meta data
const meta = helmet.title.toString() + helmet.meta.toString() + helmet.link.toString()
// Prep state to be injected into DOM for client
const pageState = `<script>window.__PRELOADED_STATE__ = ${JSON.stringify(preloadedState).replace(/</g, '\\u003c')}</script>`
// Inject state and markup
const RenderedApp = htmlData
.replace('<script></script>', pageState) // Store state to pass to client
.replace('<meta name="helmet">', meta) // Meta data
.replace('{{SSR}}', markup) // Actual markup/component html
console.log("SSR Rendered: ", req.path)
res.send(RenderedApp)
}
})
})
.catch(err => {
console.log("Error:", err)
})
}
I am console.log()-ing when a route is being handled within universalLoader(). All routes show in the console that stuff is happening. Except my homepage. It does not even show the "Loading..." message.
express.static will be serving up any files in your build directory. If it finds the requested file it will serve it up and end the request/response. No middleware registered after express.static will get the chance to run if a suitable file is found.
Based on this line:
const filePath = path.resolve(__dirname, '..', 'build', 'index.html')
it would appear that you have a file called index.html in your build directory. This will get served up by express.static when you hit the URL for index.html but it will also get served up if you just hit the / URL because express.static defaults to serving up index.html. See the index property here:
https://expressjs.com/en/4x/api.html#express.static
The directory you point express.static at needs to contain files that are static, i.e. that require no processing whatsoever. If any of the files need processing they need to live elsewhere. Note the parallels with how an Express app typically has a separate folder for views, which in many ways is similar to what you're trying to do.
I would also suggest commenting out express.static to see what effect that has. It should be a quick way to confirm that express.static is responsible for stopping your index route being reached.
Update
Based on your comments it would seem that you do have a static directory at build/static that contains your JS and CSS files. You can serve this up directly using:
app.use(express.static(path.resolve(__dirname, '..', 'build', 'static')))
However, this will cause all your URLs to change too, so http://localhost/static/js/example.js will now be http://localhost/js/example.js. To retain the original URLs you would need to put the static back in via the route path:
app.use('/static', express.static(path.resolve(__dirname, '..', 'build', 'static')))

Node/Express with Angular 2

I am trying to build the ToDo app with the MEAN stack and I can't get the Express server to connect to Angular 2. I believe it has something to do with where I have my index.html view relative to the Angular installation, but I can't figure it out.
The error I am getting is the HTML on index.html is rendering but not picking up the logic behind the selector so my assumption is my tags are wrong or something. I have tried every which way to adjust the tags, but I can't get it to work when running server.js. I know it is something silly but been working on this for a while.
Server.js
var express = require('express');
var path = require('path');
var bodyParser = require('body-parser');
var index = require('./routes/index');
var todos = require('./routes/todos');
var app = express();
// View Engine
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'ejs');
app.engine('html', require('ejs').renderFile);
app.use(express.static(path.join(__dirname,'client'))); //folder where angular will be
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({extended: false}));
app.use('/', index);
app.use('/api/v1/', todos);
app.listen(3000, function(){
console.log('Server started on port 3000...');
});
Index.html (in Views folder)
<!DOCTYPE html>
<html>
<head>
<title>Angular QuickStart</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="src/styles.css">
<!-- Polyfill(s) for older browsers -->
<script src="node_modules/core-js/client/shim.min.js"></script>
<script src="node_modules/zone.js/dist/zone.js"></script>
<script src="node_modules/systemjs/dist/system.src.js"></script>
<script src="src/systemjs.config.js"></script>
<script>
System.import('src/main.js').catch(function(err){ console.error(err); });
</script>
</head>
<body>
<my-app>Loading AppComponent FROM SERVER SIDE content here ...</my-app>
</body>
</html>
app.module.ts
import { NgModule } from '#angular/core';
import { BrowserModule } from '#angular/platform-browser';
import { AppComponent } from './app.component';
#NgModule({
imports: [ BrowserModule ],
declarations: [ AppComponent ],
bootstrap: [ AppComponent ]
})
export class AppModule { }
app.component.ts
import { Component } from '#angular/core';
#Component({
selector: 'my-app',
template: `<h1>Hello {{name}}</h1>`,
})
export class AppComponent { name = 'Angular'; }
Below are the two errors I am getting in the console:
GET http://localhost:3000/src/app/app.module 404 (Not Found)
scheduleTask # zone.js:1960 ZoneDelegate.scheduleTask # zone.js:349
(404 Not Found) loading http:…pp.module" from
http://localhost:3000/src/main.js", originalErr:
ZoneAwareError}
Any help would be much appreciated, I can't get past this.
It is not liking something about the reference in this line and getting lost somewhere in zone.js, but I can't get it right. I am using the starter kit from angular.io and using their file layout.
System.import('src/main.js').catch(function(err){ console.error(err); });
I was able to fix by adding two more static routes to the express server so it looked in every folder.
app.use(express.static(path.join(__dirname, 'client'))); // folder where angular will be installed
app.use(express.static(path.join(__dirname, 'client', 'src')));
app.use(express.static(path.join(__dirname, 'client', 'src', 'app')));
This fixed the issue.
I have encountered the same problem with the new version of Quickstart. The fact that it has a different structure (src folder added) affects how express will behave. In my scenario I have NOT altered this portion.
System.import('src/main.js').catch(function(err){ console.error(err); });
Instead I left it as default (I believe angular handles where to look for it).
System.import('main.js').catch(function(err){ console.error(err); });
I have added one more static route. Make sure you have both, one of them will not suffice.
app.use(express.static(path.join(__dirname, 'client')));
app.use(express.static(path.join(__dirname, 'client/src')));
if you are following the TRAVERSY MEDIA: (original Source is EDUONIX)
https://www.youtube.com/watch?v=PFP0oXNNveg&t=2304s
after creating the 'client' folder. Skip the whole JSON Part.
Open Terminal
git clone https://www.github.com/angular/quickstart client
npm install
npm start (this will give you the complete start of the angular front-end)
ctrl + C (close the webserver)
npm run build
server.js
var express = require ('express');
var path = require ('path');
var bodyParser = require ('body-parser');
var index = require ('./routes/index');
var todos = require ('./routes/todos');
var app = express();
//View Engine
app.use(express.static( path.join(__dirname, 'client') ) );
app.use(express.static( path.join(__dirname, 'client/src')));
app.use(express.static( path.join(__dirname, 'client/bower_components')));
app.set('views', path.join(__dirname, 'client/src') );
app.set('view engine', 'ejs');
app.engine ('html', require('ejs').renderFile );
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({extended: false} ));
app.use('/', index);
app.use('/api/v1', todos);
app.listen(3000, function() {
console.log ('server started on port 3000');
/routes/index.js
var express = require('express');
var router = express.Router();
router.get('/', function(req, res, next) {
res.render('index.html');
});
module.exports = router;
Last but not the least:
make sure this route would execute:
http://localhost:3000/api/v1/todos
yes? (you did it)

Resources