Skip to content

Commit 8d07833

Browse files
authored
chore(themebuilder): custom server adapter (#4326)
1 parent 160f3c2 commit 8d07833

9 files changed

Lines changed: 207 additions & 37 deletions

File tree

apps/themebuilder/package.json

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@
44
"type": "module",
55
"scripts": {
66
"build": "react-router build",
7-
"dev": "react-router dev",
8-
"start": "react-router-serve ./dist/server/index.js",
7+
"dev": "cross-env NODE_ENV=development node server.js",
8+
"start": "node server.js",
99
"types": "react-router typegen && tsc"
1010
},
1111
"dependencies": {
@@ -16,11 +16,15 @@
1616
"@internal/components": "workspace:^",
1717
"@navikt/aksel-icons": "^7.35.1",
1818
"@radix-ui/react-slot": "^1.2.4",
19+
"@react-router/express": "^7.10.1",
1920
"@react-router/node": "^7.10.1",
20-
"@react-router/serve": "^7.10.1",
2121
"clsx": "^2.1.1",
22+
"compression": "^1.8.1",
23+
"cross-env": "^10.1.0",
24+
"express": "^5.2.1",
2225
"i18next": "^25.7.1",
2326
"isbot": "^5.1.32",
27+
"morgan": "^1.10.1",
2428
"react": "^19.2.1",
2529
"react-color-palette": "^7.3.1",
2630
"react-dom": "^19.2.1",
@@ -30,11 +34,14 @@
3034
},
3135
"devDependencies": {
3236
"@react-router/dev": "^7.10.1",
37+
"@types/express": "^5.0.6",
38+
"@types/express-serve-static-core": "^5.1.0",
39+
"@types/morgan": "^1.9.10",
3340
"@types/node": "^22.19.1",
3441
"@types/ramda": "^0.31.1",
35-
"ramda": "^0.32.0",
3642
"@types/react": "^19.2.7",
3743
"@types/react-dom": "^19.2.3",
44+
"ramda": "^0.32.0",
3845
"typescript": "^5.9.3",
3946
"vite": "^7.2.7",
4047
"vite-tsconfig-paths": "^5.1.4"
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
11
User-agent: *
2-
Allow: /
2+
Disallow: /

apps/themebuilder/react-router.config.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,6 @@ import type { Config } from '@react-router/dev/config';
77
const dirname = cwd();
88

99
const config: Config = {
10-
// Config options...
11-
// Server-side render by default, to enable SPA mode set this to `false`
1210
ssr: true,
1311
buildDirectory: 'dist',
1412
presets: [],
@@ -22,6 +20,10 @@ const config: Config = {
2220
console.log(`Writing robots.txt to ${robotsPath}`);
2321
try {
2422
writeFileSync(robotsPath, robotsContent);
23+
writeFileSync(
24+
join(dirname, 'dist', 'client', 'robots.txt'),
25+
robotsContent,
26+
);
2527
} catch (error) {
2628
console.error(`Error writing robots.txt file: ${error}`);
2729
throw new Error(`Failed to write robots.txt file: ${error}`);

apps/themebuilder/server.js

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import compression from 'compression';
2+
import express from 'express';
3+
import morgan from 'morgan';
4+
5+
// Short-circuit the type-checking of the built output.
6+
const BUILD_PATH = './dist/server/index.js';
7+
const DEVELOPMENT = process.env.NODE_ENV === 'development';
8+
const PORT = Number.parseInt(process.env.PORT || '3000', 10);
9+
10+
const app = express();
11+
12+
app.use(compression());
13+
app.disable('x-powered-by');
14+
15+
if (DEVELOPMENT) {
16+
console.log('Starting development server');
17+
const viteDevServer = await import('vite').then((vite) =>
18+
vite.createServer({
19+
server: { middlewareMode: true },
20+
}),
21+
);
22+
app.use(viteDevServer.middlewares);
23+
app.use(async (req, res, next) => {
24+
try {
25+
const source = await viteDevServer.ssrLoadModule('./server/app.ts');
26+
return await source.app(req, res, next);
27+
} catch (error) {
28+
if (typeof error === 'object' && error instanceof Error) {
29+
viteDevServer.ssrFixStacktrace(error);
30+
}
31+
next(error);
32+
}
33+
});
34+
} else {
35+
console.log('Starting production server');
36+
app.use(
37+
'/assets',
38+
express.static('dist/client/assets', { immutable: true, maxAge: '1y' }),
39+
);
40+
app.use(morgan('tiny'));
41+
app.use(
42+
'/.well-known',
43+
express.static('dist/client/.well-known', { maxAge: '1y' }),
44+
);
45+
app.use(express.static('dist/client', { maxAge: '30d' }));
46+
app.use(await import(BUILD_PATH).then((mod) => mod.app));
47+
}
48+
49+
app.listen(PORT, () => {
50+
console.log(`Server is running on http://localhost:${PORT}`);
51+
});

apps/themebuilder/server/app.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import 'react-router';
2+
import { createRequestHandler } from '@react-router/express';
3+
import express from 'express';
4+
5+
const DEVELOPMENT = process.env.NODE_ENV === 'development';
6+
7+
const connectSrc = [
8+
"'self'",
9+
'https://altinncdn.no',
10+
DEVELOPMENT && 'ws://localhost:*',
11+
]
12+
.filter(Boolean)
13+
.join(' ');
14+
15+
export const app = express();
16+
17+
app.use((req, res, next) => {
18+
res.setHeader('X-Content-Type-Options', 'nosniff');
19+
res.setHeader(
20+
'Strict-Transport-Security',
21+
'max-age=31536000; includeSubDomains',
22+
);
23+
res.setHeader('X-Frame-Options', 'SAMEORIGIN');
24+
res.setHeader(
25+
'Content-Security-Policy-Report-Only',
26+
`default-src 'none';base-uri 'self';script-src 'self' 'unsafe-inline' 'unsafe-eval';style-src 'self' https://altinncdn.no https://siteimproveanalytics.com 'unsafe-inline';font-src 'self' https://altinncdn.no;img-src 'self' data:;connect-src ${connectSrc};frame-ancestors 'self';form-action 'self';manifest-src 'self';`,
27+
);
28+
res.setHeader(
29+
'Strict-Transport-Security',
30+
'max-age=31536000; includeSubDomains',
31+
);
32+
res.setHeader('X-Content-Type-Options', 'nosniff');
33+
res.setHeader('Cache-Control', 'max-age');
34+
/* Stop TRACE request */
35+
if (req.method === 'TRACE') {
36+
res
37+
.status(405)
38+
.type('text/plain; charset=utf-8')
39+
.send('method not allowed');
40+
return;
41+
}
42+
next();
43+
});
44+
45+
app.use(
46+
createRequestHandler({
47+
build: () => import('virtual:react-router/server-build'),
48+
}),
49+
);

apps/themebuilder/vite.config.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,12 @@ import { reactRouter } from '@react-router/dev/vite';
22
import { defineConfig } from 'vite';
33
import tsconfigPaths from 'vite-tsconfig-paths';
44

5-
export default defineConfig({
5+
export default defineConfig(({ isSsrBuild }) => ({
66
plugins: [reactRouter(), tsconfigPaths()],
77
ssr: {
8-
noExternal: ['ramda'],
8+
noExternal: ['@navikt/aksel-icons', 'ramda'],
99
},
10-
});
10+
build: {
11+
rollupOptions: isSsrBuild ? { input: './server/app.ts' } : undefined,
12+
},
13+
}));

apps/www/app/_utils/redirects.server.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,11 +80,11 @@ const redirects = [
8080
to: '/en/fundamentals/design-elements/shadows',
8181
},
8282
{
83-
from: '/no/fundamentals/resources/icons/',
83+
from: '/no/fundamentals/resources/icons',
8484
to: '/no/fundamentals/design-elements/icons',
8585
},
8686
{
87-
from: '/en/fundamentals/resources/icons/',
87+
from: '/en/fundamentals/resources/icons',
8888
to: '/en/fundamentals/design-elements/icons',
8989
},
9090
];

apps/www/server/app.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,11 @@ app.use((req, res, next) => {
2525
'Content-Security-Policy-Report-Only',
2626
`default-src 'none';base-uri 'self';script-src 'self' 'unsafe-inline' 'unsafe-eval';style-src 'self' https://altinncdn.no https://siteimproveanalytics.com 'unsafe-inline';font-src 'self' https://altinncdn.no;img-src 'self' data:;connect-src ${connectSrc};frame-ancestors 'self';form-action 'self';manifest-src 'self';`,
2727
);
28+
res.setHeader(
29+
'Strict-Transport-Security',
30+
'max-age=31536000; includeSubDomains',
31+
);
32+
res.setHeader('X-Content-Type-Options', 'nosniff');
2833
res.setHeader('Cache-Control', 'max-age');
2934
/* Stop TRACE request */
3035
if (req.method === 'TRACE') {

0 commit comments

Comments
 (0)