Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
f021138
update tests
nalchevanidze Oct 28, 2025
82f5676
update
nalchevanidze Oct 28, 2025
dbdfafc
update
nalchevanidze Oct 28, 2025
eec4d03
localStorage
nalchevanidze Oct 28, 2025
20c614f
CLIENT_ID
nalchevanidze Oct 30, 2025
67d28b0
update
nalchevanidze Oct 30, 2025
cc3035e
🤖 chore(web): bla
nalchevanidze Oct 30, 2025
2d99a0f
update
nalchevanidze Oct 30, 2025
7cb90ef
URI.ssr
nalchevanidze Oct 30, 2025
9793a9c
update
nalchevanidze Oct 30, 2025
35a3d51
unit
nalchevanidze Oct 30, 2025
b50e89d
node-ssr
nalchevanidze Oct 30, 2025
4b27b67
clean up
nalchevanidze Oct 30, 2025
95669b9
remove css url
nalchevanidze Oct 30, 2025
cbf0581
CLIENT_ID
nalchevanidze Oct 30, 2025
502d40c
update
nalchevanidze Oct 30, 2025
f9341d4
update descriptions
nalchevanidze Oct 30, 2025
a993b21
html
nalchevanidze Oct 30, 2025
dce3b43
playwrit code
nalchevanidze Oct 30, 2025
2f825a4
sdk
nalchevanidze Oct 30, 2025
4c80764
Merge branch 'main' into NT-1768
nalchevanidze Oct 30, 2025
fae915d
backend uses profile id from client
nalchevanidze Oct 30, 2025
c9b69af
getAnonymousId
nalchevanidze Oct 30, 2025
e1f7710
update
nalchevanidze Oct 30, 2025
cf442a5
update
nalchevanidze Oct 31, 2025
b4954f4
Merge remote-tracking branch 'origin/main' into NT-1768
nalchevanidze Oct 31, 2025
2065751
update
nalchevanidze Oct 31, 2025
c6ad497
clean up script
nalchevanidze Oct 31, 2025
d8b5592
ssr-mocks
nalchevanidze Oct 31, 2025
258fa2f
🤖 chore(): merge
nalchevanidze Oct 31, 2025
e897206
serve
nalchevanidze Oct 31, 2025
c86205f
update tests
nalchevanidze Oct 31, 2025
66e548a
update tests
nalchevanidze Oct 31, 2025
7a09ae3
mock unit tests
nalchevanidze Oct 31, 2025
7881277
setId
nalchevanidze Oct 31, 2025
2991eb8
update
nalchevanidze Oct 31, 2025
de2693f
dynamic id
nalchevanidze Oct 31, 2025
bc98d96
customUnidentifiedId
nalchevanidze Oct 31, 2025
b389859
id
nalchevanidze Oct 31, 2025
0a7e87b
🤖 chore(): :wq
nalchevanidze Nov 3, 2025
719dc1d
update
nalchevanidze Nov 3, 2025
d4f9e04
remove unused log
nalchevanidze Nov 3, 2025
2111d39
fix client
nalchevanidze Nov 3, 2025
df7e76e
update ssr example
nalchevanidze Nov 3, 2025
8567c0f
anonymousId
nalchevanidze Nov 4, 2025
f0338d6
maximus
nalchevanidze Nov 5, 2025
c7f786d
setAnonymousId
nalchevanidze Nov 5, 2025
862600c
add utils for cookie
nalchevanidze Nov 5, 2025
c08a2e0
smoke-test
nalchevanidze Nov 5, 2025
877d368
Apply suggestion from @phobetron
nalchevanidze Nov 5, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions implementations/node-ssr/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Contentful Optimization Node.JS Node SSR SDK Reference Implementation

This is a reference implementation for the [Optimization Node.JS Node SSR SDK](../../platforms/javascript/node/README.md) and is part of the [Contentful Optimization SDK Suite](../../README.md).
55 changes: 55 additions & 0 deletions implementations/node-ssr/e2e/example.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { ANONYMOUS_ID_COOKIE } from '@contentful/optimization-core'
import { expect, test } from '@playwright/test'

const CLIENT_ID = process.env.VITE_NINETAILED_CLIENT_ID ?? 'error'
const ANONYMOUS_ID = '__ctfl_opt_anonymous_id__'

const UID_LENGTH = 36

function genAnonymousIdCookie(id: string): {
name: string
value: string
path: string
domain: string
} {
return { name: ANONYMOUS_ID_COOKIE, value: id, path: '/', domain: 'localhost' }
}

test('BACKEND: check client ID rendered from Optimization API on server-side render', async ({
request,
}) => {
const response = await request.get('/')

expect(await response.text()).toContain(`"clientId":"${CLIENT_ID}"`)
})

test('BACKEND: generates new Profile id', async ({ context, page }) => {
await page.goto('/')

const state = await context.storageState()

const storage = state.origins[0]?.localStorage ?? []
const storedId = storage.find((item) => item.name === ANONYMOUS_ID)?.value

expect(storedId).toBeDefined()
expect(storedId).toHaveLength(UID_LENGTH)
})

test('BACKEND: identifies profile id and associates it with user id', async ({ context, page }) => {
const customIdentifiedId = 'custom-profile-id'
await context.addCookies([genAnonymousIdCookie(customIdentifiedId)])
await page.goto(`/user/maximus`)
const {
origins: [origin],
} = await context.storageState()

expect(origin?.localStorage).toEqual([{ name: ANONYMOUS_ID, value: customIdentifiedId }])
})

test('FRONTEND: check client ID rendered from Optimization API on client-side render', async ({
page,
}) => {
await page.goto('/')

await expect(page.getByTestId('clientId')).toHaveText(CLIENT_ID)
})
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"private": true,
"name": "@implementation/node-web",
"name": "@implementation/node-ssr",
"description": "Reference implementation for NodeJS Web projects",
"license": "MIT",
"main": "dist/app.js",
Expand All @@ -9,21 +9,21 @@
"build": "pnpm clean; pnpm build:sdk",
"build:sdk": "pnpm --filter '../../platforms/javascript/(api-schemas|api-client|core|web)' build && cp -r ../../platforms/javascript/web/dist ./public/dist",
"clean": "rimraf ./dist ./public/dist ./coverage ./playwright-report ./test-results tsconfig.tsbuildinfo",
"serve": "pnpm serve:mocks && pnpm serve:app",
"serve:app": "pnpm build && docker compose up -d && pm2 start --name node-app \"tsx --env-file=.env ./src/app.ts\"",
"serve:mocks": "pm2 start --name node-mocks \"pnpm --filter mocks serve\" && pm2 start --name web-mocks \"pnpm --filter mocks serve\"",
"serve:stop": "docker compose down && pm2 stop web-mocks node-app node-mocks && pm2 delete web-mocks node-app node-mocks",
"serve": "pnpm build && pm2 start --name ssr-mocks \"pnpm --filter mocks serve\" && pm2 start --name node-app \"tsx --env-file=.env ./src/app.ts\"",
"serve:stop": "pm2 stop ssr-mocks node-app && pm2 delete ssr-mocks node-app",
"test:e2e": "pnpm serve && playwright test; E2E_RESULT=$?; pnpm serve:stop; exit $E2E_RESULT",
"test:e2e:codegen": "playwright codegen",
"test:e2e:report": "playwright show-report",
"test:e2e:ui": "pnpm build && playwright test --ui",
"test:e2e:ui": "pnpm build && pnpm serve && playwright test --ui",
"test:unit": "vitest run --coverage",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@contentful/optimization-node": "workspace:*",
"@contentful/optimization-web": "workspace:*",
"express": "catalog:",
"express-rate-limit": "catalog:",
"cookie-parser": "1.4.7",
"tslib": "catalog:"
},
"devDependencies": {
Expand All @@ -32,6 +32,7 @@
"@types/node": "catalog:",
"@types/supertest": "catalog:",
"@vitest/coverage-v8": "catalog:",
"@types/cookie-parser": "1.4.7",
"dotenv": "catalog:",
"pm2": "catalog:",
"rimraf": "catalog:",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ const CLIENT_ID = process.env.VITE_NINETAILED_CLIENT_ID ?? 'error'

describe('GET /', () => {
it('returns the client ID', async () => {
const response: Response = await request(app).get('/')
const response: Response = await request(app).get('/smoke-test')

expect(response.text).toEqual(CLIENT_ID)
expect(response.text).toContain(`"clientId":"${CLIENT_ID}"`)
})
})
124 changes: 124 additions & 0 deletions implementations/node-ssr/src/app.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import Optimization, { ANONYMOUS_ID_COOKIE } from '@contentful/optimization-node'
import cookieParser from 'cookie-parser'
import express, { type Express, type Response } from 'express'
import rateLimit from 'express-rate-limit'

const limiter = rateLimit({
windowMs: 900_000,
max: 100,
})

const app: Express = express()

app.use(cookieParser())

app.use(limiter)

const CLIENT_ID = process.env.VITE_NINETAILED_CLIENT_ID ?? ''
const ENVIRONMENT = process.env.VITE_NINETAILED_ENVIRONMENT ?? ''
const VITE_INSIGHTS_API_BASE_URL = process.env.VITE_INSIGHTS_API_BASE_URL ?? ''
const VITE_EXPERIENCE_API_BASE_URL = process.env.VITE_EXPERIENCE_API_BASE_URL ?? ''

const render = (sdk: Optimization): string => `<!doctype html>
<html>
<head>
<title>Test SDK page</title>
<script src="/dist/index.umd.cjs"></script>
<script> window.response = ${JSON.stringify({ clientId: sdk.config.clientId })} </script>
</head>
<body>
<script>
const optimization = new Optimization({
clientId: '${CLIENT_ID}',
environment: '${ENVIRONMENT}',
logLevel: 'debug',
api: {
analytics: { baseUrl: '${VITE_INSIGHTS_API_BASE_URL}' },
personalization: { baseUrl: '${VITE_EXPERIENCE_API_BASE_URL}' },
},
})

function renderElement(id, text) {
const p = document.createElement('p')
p.dataset.testid = id
p.innerText = text
document.body.appendChild(p)
}

renderElement('clientId', optimization.config.clientId)
</script>
</body>
</html>
`

function initSDK(anonymousId: string | undefined): Optimization {
const url = new URL('http://localhost:3000/')

return new Optimization({
clientId: CLIENT_ID,
environment: ENVIRONMENT,
logLevel: 'debug',
eventBuilder: {
getAnonymousId: () => anonymousId,
getLocale: () => 'en-US',
getUserAgent: () => 'node-js-server',
getPageProperties: () => ({
path: url.pathname,
query: {},
referrer: 'http://localhost:3000/',
search: url.search,
url: url.toString(),
}),
},
api: {
analytics: { baseUrl: VITE_INSIGHTS_API_BASE_URL },
personalization: { baseUrl: VITE_EXPERIENCE_API_BASE_URL },
},
})
}

function setAnonymousId(res: Response, id: string): void {
res.cookie(ANONYMOUS_ID_COOKIE, id, {
path: '/',
domain: 'localhost',
})
}

function getAnonymousIdFromCookies(cookies: unknown): string | undefined {
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- req.cookies is of type any
return (cookies as Record<string, string>)[ANONYMOUS_ID_COOKIE] ?? undefined
}

app.get('/', limiter, async (req, res) => {
const sdk = initSDK(getAnonymousIdFromCookies(req.cookies))
const { profile } = await sdk.personalization.page({})

setAnonymousId(res, profile.id)
res.send(render(sdk))
})

app.get('/user/:userId', limiter, async (req, res) => {
const { userId } = req.params as Record<string, string>
const sdk = initSDK(getAnonymousIdFromCookies(req.cookies))
if (userId) await sdk.personalization.identify({ userId })
const { profile } = await sdk.personalization.page({})

setAnonymousId(res, profile.id)
res.send(render(sdk))
})

app.get('/smoke-test', limiter, (_, res) => {
const sdk = initSDK(undefined)
res.send(render(sdk))
})

app.use('/dist', express.static('./public/dist'))

const port = 3000

app.listen(port, () => {
// eslint-disable-next-line no-console -- debug
console.log(`Express is listening at http://localhost:${port}`)
})

export default app
3 changes: 0 additions & 3 deletions implementations/node-web/README.md

This file was deleted.

11 changes: 0 additions & 11 deletions implementations/node-web/docker-compose.yaml

This file was deleted.

15 changes: 0 additions & 15 deletions implementations/node-web/e2e/example.spec.ts

This file was deleted.

36 changes: 0 additions & 36 deletions implementations/node-web/nginx/nginx.conf

This file was deleted.

30 changes: 0 additions & 30 deletions implementations/node-web/nginx/templates/default.conf.template

This file was deleted.

25 changes: 0 additions & 25 deletions implementations/node-web/public/index.html

This file was deleted.

34 changes: 0 additions & 34 deletions implementations/node-web/src/app.ts

This file was deleted.

Loading