Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
13 changes: 13 additions & 0 deletions .env
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,19 @@ NEXT_PUBLIC_GTM_ID=
NEXT_PUBLIC_API_URL=$API_URL
API_BASIC_AUTH_SYSTEM_USER_PASSWORD=$BASIC_AUTH_SYSTEM_USER_PASSWORD

# mailpit (catches all mails sent in development, web interface: http://localhost:${MAILPIT_UI_PORT})
MAILPIT_SMTP_PORT=1025
MAILPIT_UI_PORT=8025

# mail (sent by the site, see the contact form route)
MAIL_HOST=localhost
MAIL_PORT=$MAILPIT_SMTP_PORT
# mailpit doesn't require authentication
MAIL_USER=
MAIL_PASSWORD=
MAIL_FROM=
CONTACT_FORM_TO_EMAIL=

# jaegertracing
JAEGER_UI_PORT=16686
JAEGER_HOST=localhost
Expand Down
5 changes: 5 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ The `site-configs/` directory manages site configurations, compiled into environ
- PostgreSQL (port 5432)
- imgproxy (port 6080) - image optimization
- Jaeger (port 16686) - distributed tracing
- Mailpit (SMTP port 1025, web interface port 8025) - catches all mails sent in development

### Local Ports

Expand Down Expand Up @@ -165,6 +166,10 @@ The `site-configs/` directory manages site configurations, compiled into environ

After making code changes, always run `npm --prefix <package> run lint:fix` for each affected package. This auto-fixes import ordering, removes unused imports, and applies Prettier formatting. Run this before committing or presenting changes as complete.

### BFF Routes

Server-side logic that doesn't belong into the CMS API is implemented as Next.js route handlers in `site/src/app/[visibility]/[domain]/`. See `api/contact-form/route.ts` for an example: it validates the submitted values with zod and sends them as a mail with nodemailer, directly from the site instead of the API. In development, mails are caught by Mailpit.

### API Module Structure

Feature-based organization: `auth/`, `documents/`, `dam/`, `menus/`, `footers/`, `redirects/`, `healthcheck/`
Expand Down
7 changes: 7 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,13 @@ services:
COLLECTOR_OTLP_ENABLED: "true"
COLLECTOR_OTLP_HTTP_HOST_PORT: 0.0.0.0:4318

mailpit:
image: mirror.gcr.io/axllent/mailpit:v1.31
pull_policy: weekly
ports:
- "127.0.0.1:${MAILPIT_SMTP_PORT}:1025"
- "127.0.0.1:${MAILPIT_UI_PORT}:8025"

# valkey:
# image: mirror.gcr.io/valkey/valkey:9
# pull_policy: weekly
Expand Down
25 changes: 23 additions & 2 deletions site/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 4 additions & 1 deletion site/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,13 +43,15 @@
"keyv": "^5.6.0",
"lru-cache": "^11.5.2",
"next": "^16.3.3",
"nodemailer": "^9.0.6",
"react": "^19.2.8",
"react-dom": "^19.2.8",
"react-focus-lock": "^2.13.7",
"react-intl": "^7.1.14",
"redraft": "^0.10.2",
"swiper": "^14.2.0",
"usehooks-ts": "^3.1.1"
"usehooks-ts": "^3.1.1",
"zod": "^4.4.3"
},
"devDependencies": {
"@dextinity/cli": "10.2.0",
Expand All @@ -63,6 +65,7 @@
"@graphql-codegen/typescript-operations": "^6.1.6",
"@parcel/watcher": "^2.6.0",
"@types/node": "^24.13.3",
"@types/nodemailer": "^8.0.1",
"@types/react": "^19.2.18",
"@types/react-dom": "^19.2.5",
"check-node-version": "^4.2.1",
Expand Down
58 changes: 58 additions & 0 deletions site/src/app/[visibility]/[domain]/api/contact-form/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { type NextRequest, NextResponse } from "next/server";
import { createTransport } from "nodemailer";
import { z } from "zod";

const queryValidationSchema = z.object({
email: z.email(),
message: z.string(),
});

// The [visibility] and [domain] segments are added by the domain rewrite middleware, so a form submits to /api/contact-form.
export async function POST(request: NextRequest) {
const body = await request.json();
const validationResult = queryValidationSchema.safeParse(body);

if (!validationResult.success) {
return NextResponse.json(
{
cause: validationResult.error,
message: "Validation failed",
},
{
status: 400,
},
);
}

const { email, message } = validationResult.data;

try {
const port = parseInt(process.env.MAIL_PORT || "587", 10);
const user = process.env.MAIL_USER;

const transport = createTransport({
host: process.env.MAIL_HOST,
port,
secure: port === 465, // all other ports use STARTTLS
auth: user ? { user, pass: process.env.MAIL_PASSWORD } : undefined,
});

await transport.sendMail({
from: process.env.MAIL_FROM,
to: process.env.CONTACT_FORM_TO_EMAIL,
replyTo: email,
subject: "Contact form",
text: message,
});

return NextResponse.json(
{ success: true },
{
status: 200,
},
);
} catch (e) {
console.error(e);
return NextResponse.json({ error: "Something went wrong processing the contact form" }, { status: 500 });
}
}