There was much I wanted to say about SSR support but Angular 22 deprecated their CommonEngine and replaced it with AngularNodeAppEngine, I need to hit the drawing board again to replace my single-build-multilingual solution, that relies on CommonEngine and its ability to let in providers from the server world. Until then, I'm pissed off.
Following are quick notes about each framework, diving deeper is a different Tuesday.
anchorIntent
Like everything in Angular, building for SSR is explicit. Need to include the right bootstrapping functions, and the correct server routes. In Svelte, I always wondered if I needed the server handle hook in all client-side app. I do. It's part of a custom Node server implementation, not necessarily SSR enabled.
// bootstrapping in Angular
const _app = (context: BootstrapContext) => bootstrapApplication(AppComponent, {
providers: [
// client providers
...clientProviders,
// then the Angular providers for SSR
provideServerRendering(withRoutes(serverRoutes)),
]}, context
);
export default _app;Enabling in Svelte:
// root layout.ts
export const ssr = true;
// then you need to add a file hook.server.ts in src folderanchorServer side rendering and hydration
In both frameworks you can choose which routes to include in server side rendering, and which are not, in Angular it is a configuration file, and in Svelte it's a boolean you set at the root layout file. Which makes it quite all over the place.
// Angular: app.routes.server.ts
export const serverRoutes: ServerRoute[] = [
{
path: '**',
renderMode: RenderMode.Server
}
];
// SvelteKit any roote +layout.ts
export const ssr = false | true;Svelte hydration is very particular, and if you use http requests, it must be through their load version of fetch. Angular is less particular but just as stubborn. Angular supports incremental hydration with a lot of ways to fine tune it out of the box. Svelte may disable ssr on a whole branch of a route tree, and that's just about it.
One downside of hydration expectations is when you need to manipulate the DOM. Angular however has its own DOCUMENT implementation using out of the box Node implementation (which, let's be frank, is probably a forth party under the hood) which allows directives to target the document anywhere it makes sense.
anchorRequest and Response
In addition to DOCUMENT token, Angular also provides REQUEST, REQUEST_CONTEXT, and RESPONSE_INIT. Svelte provides the shady getRequestEvent. I have personal beef with this function. To make the request event available, it must belong to the load tree. Axios for example, does not play nice with it.
To make the request available when you need it, you must burry it inside a .server file or inside lib/server file. If you want to implement a both server/browser behavior with access to that event, here is a good pattern:
// in hook.server
export const init: ServerInit = async () => {
// pass server locals to all unsafe regions
setLocals(() => getRequestEvent().locals);
}
// somewhere in utility file,
// expected function in client side is null
let _localsResolver: () => any = null;
export const setLocals = (fn: () => any) => {
_localsResolver = fn;
};
// safe outside a request scope (browser, init, background work)
export const SiteLocals = (): any => {
try {
// that's where calling an ssr function, or a null client function
return _localsResolver?.() || {};
} catch {
return {};
}
};
// you use it in, say, an authorization header where server locals have the user
if (!browser){
_token = SiteLocals().user?.accessToken;
}
I will keep Angular SSR deep dive for another Tuesday, because it's worth it.
anchorServing and building
Both frameworks serve SSR version during development time, ever since Angular adopted Vite. After building, running on a real server requires Node server setup, the server listener in Svelte is buried inside handler.js, and uses Polka, but exporting the handler, you can listen through a custom Node server . The Angular listener is part of the source code, and can be dropped, in favor of an external listener with the correct functions exported. A whole new Tuesday.
Injecting a configuration.json from the server in Svelte is not a straight forward operation. Here is a pattern to use:
// example custom Node server in Svelte
// this is how a path of global config is injected from /config/config.json
global.configPath = join(import.meta.dirname, 'config', 'config.json');
// hanlder is hoisted, better keep it late
const { handler } = await import('./build/handler.js');
const app = express();
// ... setup express as you like, with routes and special API handling
// then use handler
app.use(handler);
// then listen
app.listen(process.env.port, async function (err) {
console.log('started to listen to port: ' + process.env.port);
if (err) {
console.log(err);
return;
}
});Then in the source code of Svelte:
// server.hook.ts
export const init: ServerInit = async () => {
// to read config file during dev time from /static, and keep it external on run time, the configPath is now dynamically read:
const configFile = join((<any>global).staticRoot || join(process.cwd(), 'static'), 'config.json');
// then the configFile can be ready normally as a Node file. I found this quite messy.
const raw = await readFile(configPath, 'utf-8');
// ...
// this is the configuration now ready to be setup locally and used
ServerConfig.Config = JSON.parse(raw);In Angular however, because we have the App initializer that waits for any fetch operation to end, it is safe to simply fetch it. We can also provide it in the runner of the custom Node server. But here is an example of the configuration factory
// in main.ts
bootstrapApplication(AppComponent, {providers: [
// ...
provideAppInitializer(configFactory),
]});
// the factory looks like this
const configFactory = () => {
const config = inject(ConfigService);
return config.loadAppConfig(); // this fetches the file
};
In the older (deprecated) version of ngExpressEngine, the ConfigService can optionally define a token that is provided on the server
// config.service
// string token provided by express routes (CommonEngine), inject() typing does not accept strings
const LOCAL_CONFIG = 'localConfig' as ProviderToken<IConfig>;
@Injectable({
providedIn: 'root'
})
export class ConfigService {
private http = inject(HttpClient);
protected localConfig = inject(LOCAL_CONFIG, { optional: true });
loadAppConfig(): Observable<boolean> {
// this will be awaited until it resolves, so you can read the localConfig token on server, or you can simply fetch once, and cache forever
return this.http.get(this._getUrl).pipe(
// ... map, catch ... return observable whenever ready
);
}
}
This has already been covered in details previously in Utilizing Angular Tokens. But it has all been deprecated and replaced. The http fetch thought is still solid.
Now here is another trick to force Svelte to serve a certain file, from other than the static folder, during development time. Utilizing Vite plugins:
// in vite.config.ts
export default defineConfig((env) => {
return {
plugins: [
{
// quick middleware for dev environment during client fetch, to serve host/config/config.dev.json for /config.json
name: 'config-json-proxy',
configureServer(server) {
server.middlewares.use('/config.json', (_req, res) => {
// file location on disk
const json = readFileSync('./host/config/config.dev.json', 'utf8');
res.setHeader('Content-Type', 'application/json');
res.end(json);
});
},
},
sveltekit(),
// ...
};
}) satisfies UserConfigFn;Here is the same trick in Angular, this means any call in client side on /localdata/config.json, will fetch the physical file sitting in configs folder. This also means on build time, Angular will copy the file and carry it forward to host/localdata
// angular.json
{
// projects.projectname.architect.build.options.assets
assets: [
{
"glob": "*",
"input": "configs",
"output": "/localdata"
},
]
}anchorAPI server support
Both frameworks have excellent support for API points, which is something I never use, because the music you face in real life is: API is a foreign server, written by a strictly-server technology assumed better than Node. Angular uses explicit routing, and Svelte uses file-system base routing. Which is again, a downside.
anchorVite
Both use vite under the hood, Svelte provides one configuration file to serve and build, and it exposes vite config to developer.
// vite.config.ts
export default defineConfig((env) => {
return {
plugins: [sveltekit()],
preprocess: [vitePreprocess()],
kit: {
// ...
}
// ...
}
}Angular CLI wraps the vite under the hood, and has a multipoint configuration file that allows one to serve and build in multiple formats. There is also support for schematics that allows developer to do more post processing of Angular output. Also proxying files, replacing files on build time, and changing which index.html are just to name a few of the delights of working with angular.json. It's just Vite, wrapped with Sullivan paper.
This has an upside, when Vite updates, you don't bother, because Angular framework will do the work out of the box.
anchorMultilingual support
Multilingual in Svelte is available during development, most translation solutions are runtime solutions, where the translations are loaded separately on runtime. Angular provides out of the box solution for localization and internationalization, with great support for things like date format, currencies, and number formats. Angular serves one version at a time. On build-time, it bakes in the language content, so it generates multiple builds, one for each language, and your job is to serve the right build for the language requested by URL. Attractive but heavy.
anchorNext
That was a heavy one, but it left a lot to be said. I decided to give it one more episode to rant one last time about Svelte, vs Angular. 😴