When I build my Angular 17 SSR project with ng build
, I get this:
├── dist
│ └── my-project
│ ├── .DS_Store
│ ├── 3rdpartylicenses.txt
│ ├── browser
│ │ ├── .DS_Store
│ │ ├── assets
│ │ │ ├── ...
│ │ ├── chunk-5XNHKK4I.js
│ │ ├── ...
│ │ ├── favicon.ico
│ │ ├── index.html
│ │ ├── main-OTXGCZUU.js
│ │ ├── media
│ │ │ └── ...
│ │ ├── polyfills-RT5I6R6G.js
│ │ ├── styles-6I36XN7I.css
│ ├── prerendered-routes.json
│ └── server
│ ├── chunk-2VXXU7NI.mjs
│ ├── ...
│ ├── index.server.html
│ ├── main.server.mjs
│ ├── polyfills.server.mjs
│ ├── render-utils.server.mjs
│ └── server.mjs
Now my question is: How do I publish this to my cPanel / Traditional hosting provider?
I imagine that Angular made it easy for deploying SSR on Firebase, and other mainstream cloud services, but what about local cloud services using cPanel?
Dragging and dropping the /dist into the /public_html folder in the cPanel. Not the solution 😅
I found a solution!
Small prerequisite: Check if your cPanel allows you to create NodeJS.
In the root of the Angular project, I've created a main.js
file, and here's what's inside of it:
async function run() {
try {
// Import the app from the ES module
const server = await import("./server/server.mjs");
const app = await server.app();
const port = process.env["PORT"] || 4000;
// Start up the Node server
app.listen(port, () => {
console.log(`Node Express server listening on http://localhost:${port}`);
});
} catch (error) {
console.error("Failed to import app:", error);
}
}
run();
And here's how my server.ts
file looks like:
import 'zone.js/node';
import { APP_BASE_HREF } from '@angular/common';
import { CommonEngine } from '@angular/ssr';
import express from 'express';
import { fileURLToPath } from 'url';
import { dirname, join, resolve } from 'path';
import bootstrap 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 serverDistFolder = dirname(fileURLToPath(import.meta.url));
const browserDistFolder = resolve(serverDistFolder, '../browser');
const indexHtml = join(serverDistFolder, 'index.server.html');
const commonEngine = new CommonEngine();
server.set('view engine', 'html');
server.set('views', browserDistFolder);
// Example Express Rest API endpoints
// server.get('/api/**', (req, res) => { });
// Serve static files from /browser
server.get(
'*.*',
express.static(browserDistFolder, {
maxAge: '1y',
})
);
// All regular routes use the Angular engine
server.get('*', (req, res, next) => {
const { protocol, originalUrl, baseUrl, headers } = req;
commonEngine
.render({
bootstrap,
documentFilePath: indexHtml,
url: `${protocol}://${headers.host}${originalUrl}`,
publicPath: browserDistFolder,
providers: [{ provide: APP_BASE_HREF, useValue: baseUrl }],
})
.then((html) => res.send(html))
.catch((err) => next(err));
});
return server;
}
export * from './src/main.server';
Then in my project I've ran ng build
which generated me the aforementioned file tree.
These are the steps I took:
main.js
as the "Application startup file", this is important.Here's how the instance looked at the end:
Then I navigated to the application's root folder in the server. This was the rough file structure:
├── main.js
├── public
├── ... (some other files)
└── tmp
Then I just dumped the /dist/my-project
folder's contents inside of it, so the end result looks something like this:
├── .htaccess
├── 3rdpartylicenses.txt
├── browser
├── main.js
├── prerendered-routes.json
├── public
├── server
└── tmp
Then go back to the Node.js instance in the cPanel and hit "Restart"!
Then the Node.js instance in cPanel just knows what to do, because of main.js
.
Hope this helps!