@next-friday/create-app 2.0.0 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (85) hide show
  1. package/README.md +16 -15
  2. package/dist/main.mjs +18 -7
  3. package/package.json +1 -1
  4. package/templates/base/pnpm-workspace.yaml +24 -2
  5. package/templates/payload/README.md +1 -1
  6. package/templates/payload/package.json +0 -1
  7. package/templates/payload/src/payload.config.test.ts +31 -0
  8. package/templates/turborepo/.agents/rules/typescript-first.md +7 -0
  9. package/templates/turborepo/.agents/rules.json +41 -0
  10. package/templates/turborepo/.claude/rules/deterministic-testing.md +21 -0
  11. package/templates/turborepo/.claude/rules/typescript-first.md +15 -0
  12. package/templates/turborepo/.cursor/rules/deterministic-testing.mdc +21 -0
  13. package/templates/turborepo/.cursor/rules/typescript-first.mdc +13 -0
  14. package/templates/turborepo/.dockerignore +12 -0
  15. package/templates/turborepo/.editorconfig-checker.json +3 -0
  16. package/templates/turborepo/.env.example +4 -0
  17. package/templates/turborepo/.github/ISSUE_TEMPLATE/bug.yml +66 -0
  18. package/templates/turborepo/.github/ISSUE_TEMPLATE/feature.yml +51 -0
  19. package/templates/turborepo/.github/dependabot.yml +79 -0
  20. package/templates/turborepo/.github/labeler.yml +25 -0
  21. package/templates/turborepo/.github/pull_request_template.md +25 -0
  22. package/templates/turborepo/.github/workflows/ci.yml +212 -0
  23. package/templates/turborepo/.github/workflows/deploy.yml +135 -0
  24. package/templates/turborepo/.github/workflows/sast.yml +46 -0
  25. package/templates/turborepo/.lintstagedrc.json +10 -0
  26. package/templates/turborepo/.prettierignore +5 -0
  27. package/templates/turborepo/AGENTS.md +20 -0
  28. package/templates/turborepo/CONTRIBUTING.md +58 -0
  29. package/templates/turborepo/README.md +102 -0
  30. package/templates/turborepo/_gitignore +53 -0
  31. package/templates/turborepo/apps/app/.dependency-cruiser.json +34 -0
  32. package/templates/turborepo/apps/app/Dockerfile +25 -0
  33. package/templates/turborepo/apps/app/app/favicon.ico +0 -0
  34. package/templates/turborepo/apps/app/app/globals.css +26 -0
  35. package/templates/turborepo/apps/app/app/health/route.test.ts +20 -0
  36. package/templates/turborepo/apps/app/app/health/route.ts +23 -0
  37. package/templates/turborepo/apps/app/app/layout.tsx +29 -0
  38. package/templates/turborepo/apps/app/app/page.test.ts +11 -0
  39. package/templates/turborepo/apps/app/app/page.tsx +8 -0
  40. package/templates/turborepo/apps/app/next.config.ts +40 -0
  41. package/templates/turborepo/apps/app/package.json +41 -0
  42. package/templates/turborepo/apps/app/playwright.config.ts +27 -0
  43. package/templates/turborepo/apps/app/postcss.config.mjs +7 -0
  44. package/templates/turborepo/apps/app/test/home.e2e.test.ts +11 -0
  45. package/templates/turborepo/apps/app/tsconfig.json +33 -0
  46. package/templates/turborepo/apps/app/vitest.config.ts +12 -0
  47. package/templates/turborepo/apps/backoffice/.dependency-cruiser.json +34 -0
  48. package/templates/turborepo/apps/backoffice/Dockerfile +28 -0
  49. package/templates/turborepo/apps/backoffice/next.config.ts +48 -0
  50. package/templates/turborepo/apps/backoffice/package.json +49 -0
  51. package/templates/turborepo/apps/backoffice/playwright.config.ts +27 -0
  52. package/templates/turborepo/apps/backoffice/src/app/(frontend)/layout.tsx +18 -0
  53. package/templates/turborepo/apps/backoffice/src/app/(frontend)/page.test.ts +14 -0
  54. package/templates/turborepo/apps/backoffice/src/app/(frontend)/page.tsx +14 -0
  55. package/templates/turborepo/apps/backoffice/src/app/(frontend)/styles.css +15 -0
  56. package/templates/turborepo/apps/backoffice/src/app/(payload)/admin/[[...segments]]/not-found.tsx +24 -0
  57. package/templates/turborepo/apps/backoffice/src/app/(payload)/admin/[[...segments]]/page.tsx +24 -0
  58. package/templates/turborepo/apps/backoffice/src/app/(payload)/admin/importMap.ts +6 -0
  59. package/templates/turborepo/apps/backoffice/src/app/(payload)/api/[...slug]/route.ts +19 -0
  60. package/templates/turborepo/apps/backoffice/src/app/(payload)/custom.scss +0 -0
  61. package/templates/turborepo/apps/backoffice/src/app/(payload)/layout.tsx +31 -0
  62. package/templates/turborepo/apps/backoffice/src/app/health/route.test.ts +20 -0
  63. package/templates/turborepo/apps/backoffice/src/app/health/route.ts +23 -0
  64. package/templates/turborepo/apps/backoffice/src/collections/media.test.ts +27 -0
  65. package/templates/turborepo/apps/backoffice/src/collections/media.ts +18 -0
  66. package/templates/turborepo/apps/backoffice/src/collections/users.test.ts +10 -0
  67. package/templates/turborepo/apps/backoffice/src/collections/users.ts +10 -0
  68. package/templates/turborepo/apps/backoffice/src/environment.test.ts +32 -0
  69. package/templates/turborepo/apps/backoffice/src/environment.ts +27 -0
  70. package/templates/turborepo/apps/backoffice/src/migrations/20261030_005055_initial.json +969 -0
  71. package/templates/turborepo/apps/backoffice/src/migrations/20261030_005055_initial.ts +134 -0
  72. package/templates/turborepo/apps/backoffice/src/migrations/index.ts +9 -0
  73. package/templates/turborepo/apps/backoffice/src/payload-types.ts +339 -0
  74. package/templates/turborepo/apps/backoffice/src/payload.config.test.ts +31 -0
  75. package/templates/turborepo/apps/backoffice/src/payload.config.ts +50 -0
  76. package/templates/turborepo/apps/backoffice/test/home.e2e.test.ts +16 -0
  77. package/templates/turborepo/apps/backoffice/tsconfig.json +34 -0
  78. package/templates/turborepo/apps/backoffice/vitest.config.ts +19 -0
  79. package/templates/turborepo/eslint.config.ts +40 -0
  80. package/templates/turborepo/knip.json +9 -0
  81. package/templates/turborepo/package.json +70 -0
  82. package/templates/turborepo/pnpm-workspace.yaml +45 -0
  83. package/templates/turborepo/prettier.config.ts +11 -0
  84. package/templates/turborepo/tsconfig.json +16 -0
  85. package/templates/turborepo/turbo.json +50 -0
@@ -0,0 +1,34 @@
1
+ {
2
+ "extends": "dependency-cruiser/configs/recommended-strict",
3
+ "forbidden": [
4
+ {
5
+ "name": "no-orphans",
6
+ "to": {},
7
+ "from": {
8
+ "orphan": true,
9
+ "pathNot": [
10
+ "(^|/)tsconfig\\.json$",
11
+ "(^|/)eslint\\.config\\.ts$",
12
+ "(^|/)next\\.config\\.ts$",
13
+ "(^|/)postcss[.]config[.]mjs$",
14
+ "(^|/)vitest[.]config[.]ts$",
15
+ "(^|/)playwright[.]config[.]ts$",
16
+ "^test/.*[.]e2e[.]test[.]ts$",
17
+ "^app/.*(page|layout|template|loading|error|not-found|default|route)[.]tsx?$"
18
+ ]
19
+ }
20
+ }
21
+ ],
22
+ "options": {
23
+ "skipAnalysisNotInRules": true,
24
+ "tsPreCompilationDeps": true,
25
+ "enhancedResolveOptions": {
26
+ "conditionNames": ["import", "require", "node", "default", "types"],
27
+ "exportsFields": ["exports"]
28
+ },
29
+ "exclude": "(^|/)(?:\\.next|coverage|node_modules)/",
30
+ "tsConfig": {
31
+ "fileName": "tsconfig.json"
32
+ }
33
+ }
34
+ }
@@ -0,0 +1,25 @@
1
+ # Runs on any OCI platform (Railway, Fly.io, Cloud Run). On Vercel, deploy without it. The platform sets `PORT`.
2
+ FROM node:24.22.1-alpine@sha256:06d7adf3e0ef61a13f1d28682726366f97a15aa1888c934d3ecec86c33918b53 AS build
3
+ ENV NEXT_TELEMETRY_DISABLED=1
4
+ WORKDIR /app
5
+ COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
6
+ COPY apps/app/package.json apps/app/package.json
7
+ COPY apps/backoffice/package.json apps/backoffice/package.json
8
+ RUN npm install --global "pnpm@$(node --print 'require("./package.json").packageManager.split("+")[0].split("@")[1]')"
9
+ RUN pnpm install --frozen-lockfile --ignore-scripts --filter ./apps/app...
10
+ COPY apps/app/next.config.ts apps/app/postcss.config.mjs apps/app/tsconfig.json apps/app/
11
+ COPY apps/app/app apps/app/app
12
+ RUN pnpm --filter ./apps/app build
13
+
14
+ FROM node:24.22.1-alpine@sha256:06d7adf3e0ef61a13f1d28682726366f97a15aa1888c934d3ecec86c33918b53
15
+ ENV NODE_ENV=production NEXT_TELEMETRY_DISABLED=1 HOSTNAME=0.0.0.0
16
+ WORKDIR /app
17
+ # Apply Alpine security fixes released after the base image, and drop npm, corepack, and yarn: the app never runs them, and their bundled dependencies carry their own CVEs.
18
+ RUN apk upgrade --no-cache \
19
+ && rm -rf /usr/local/lib/node_modules /usr/local/bin/npm /usr/local/bin/npx /usr/local/bin/corepack /usr/local/bin/yarn /usr/local/bin/yarnpkg /opt/yarn-*
20
+ # Next.js writes .next/cache at runtime. Add `COPY --from=build --chown=node:node /app/apps/app/public ./apps/app/public` when the app has a public/ folder.
21
+ COPY --from=build --chown=node:node /app/apps/app/.next/standalone ./
22
+ COPY --from=build --chown=node:node /app/apps/app/.next/static ./apps/app/.next/static
23
+ USER 1000
24
+ HEALTHCHECK --interval=30s --timeout=5s CMD ["node", "-e", "fetch('http://127.0.0.1:' + (process.env.PORT || 3000) + '/health').then(response => process.exit(response.ok ? 0 : 1), () => process.exit(1))"]
25
+ CMD ["node", "apps/app/server.js"]
@@ -0,0 +1,26 @@
1
+ @import "tailwindcss";
2
+
3
+ :root {
4
+ --background: #ffffff;
5
+ --foreground: #171717;
6
+ }
7
+
8
+ @theme inline {
9
+ --color-background: var(--background);
10
+ --color-foreground: var(--foreground);
11
+ --font-sans: var(--font-geist-sans);
12
+ --font-mono: var(--font-geist-mono);
13
+ }
14
+
15
+ @media (prefers-color-scheme: dark) {
16
+ :root {
17
+ --background: #0a0a0a;
18
+ --foreground: #ededed;
19
+ }
20
+ }
21
+
22
+ body {
23
+ background: var(--background);
24
+ color: var(--foreground);
25
+ font-family: var(--font-sans), sans-serif;
26
+ }
@@ -0,0 +1,20 @@
1
+ import {describe, expect, it, vi} from "vitest";
2
+
3
+ import {GET} from "./route";
4
+
5
+ vi.mock("next/server", () => ({
6
+ connection: vi.fn(async () => {}),
7
+ }));
8
+
9
+ describe("GET /health", () => {
10
+ it("reports ok without caching", async () => {
11
+ const response = await GET();
12
+
13
+ expect(response.status).toBe(200);
14
+ expect(response.headers.get("Cache-Control")).toBe("no-store");
15
+
16
+ await expect(response.json()).resolves.toEqual({
17
+ status: "ok",
18
+ });
19
+ });
20
+ });
@@ -0,0 +1,23 @@
1
+ import {connection} from "next/server";
2
+
3
+ /**
4
+ * Report that the server is up, for platform health checks. Rendered per request and never cached, so a
5
+ * stopped server cannot pass from a cached response.
6
+ * @returns A JSON response with `status: "ok"`.
7
+ */
8
+ async function get(): Promise<Response> {
9
+ await connection();
10
+
11
+ return Response.json(
12
+ {
13
+ status: "ok",
14
+ },
15
+ {
16
+ headers: {
17
+ "Cache-Control": "no-store",
18
+ },
19
+ },
20
+ );
21
+ }
22
+
23
+ export {get as GET};
@@ -0,0 +1,29 @@
1
+ import {Geist, Geist_Mono} from "next/font/google";
2
+ import type {Metadata} from "next";
3
+
4
+ import "./globals.css";
5
+
6
+ const geistSans = Geist({
7
+ variable: "--font-geist-sans",
8
+ subsets: ["latin"],
9
+ });
10
+
11
+ const geistMono = Geist_Mono({
12
+ variable: "--font-geist-mono",
13
+ subsets: ["latin"],
14
+ });
15
+
16
+ export const metadata: Metadata = {
17
+ title: "Next Friday Next.js template",
18
+ description: "Starter application for Next Friday Next.js projects.",
19
+ };
20
+
21
+ export default function RootLayout(props: LayoutProps<"/">) {
22
+ const {children} = props;
23
+
24
+ return (
25
+ <html className={`${geistSans.variable} ${geistMono.variable} h-full antialiased`} lang="en">
26
+ <body className="flex min-h-full flex-col">{children}</body>
27
+ </html>
28
+ );
29
+ }
@@ -0,0 +1,11 @@
1
+ import {createElement} from "react";
2
+ import {describe, expect, it} from "vitest";
3
+ import {renderToStaticMarkup} from "react-dom/server";
4
+
5
+ import Page from "./page";
6
+
7
+ describe("Page", () => {
8
+ it("renders the template heading", () => {
9
+ expect(renderToStaticMarkup(createElement(Page))).toContain("Next Friday Next.js template");
10
+ });
11
+ });
@@ -0,0 +1,8 @@
1
+ export default function Page() {
2
+ return (
3
+ <main className="flex flex-1 flex-col items-center justify-center gap-4 p-16 text-center">
4
+ <h1 className="text-3xl font-semibold tracking-tight">Next Friday Next.js template</h1>
5
+ <p className="text-lg text-zinc-600 dark:text-zinc-400">Edit app/page.tsx to get started.</p>
6
+ </main>
7
+ );
8
+ }
@@ -0,0 +1,40 @@
1
+ import {fileURLToPath} from "node:url";
2
+ import path from "node:path";
3
+ import type {NextConfig} from "next";
4
+
5
+ const workspaceRoot = path.join(path.dirname(fileURLToPath(import.meta.url)), "../..");
6
+
7
+ const nextConfig: NextConfig = {
8
+ outputFileTracingRoot: workspaceRoot,
9
+ output: "standalone",
10
+ poweredByHeader: false,
11
+ cacheComponents: true,
12
+ partialPrefetching: true,
13
+ async headers() {
14
+ return [
15
+ {
16
+ source: "/:path*",
17
+ headers: [
18
+ {
19
+ key: "Strict-Transport-Security",
20
+ value: "max-age=63072000; includeSubDomains",
21
+ },
22
+ {
23
+ key: "X-Content-Type-Options",
24
+ value: "nosniff",
25
+ },
26
+ {
27
+ key: "X-Frame-Options",
28
+ value: "SAMEORIGIN",
29
+ },
30
+ {
31
+ key: "Referrer-Policy",
32
+ value: "strict-origin-when-cross-origin",
33
+ },
34
+ ],
35
+ },
36
+ ];
37
+ },
38
+ };
39
+
40
+ export default nextConfig;
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "@next-friday-template-turborepo/app",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "description": "Next Friday Next.js app.",
6
+ "license": "UNLICENSED",
7
+ "type": "module",
8
+ "scripts": {
9
+ "build": "next build",
10
+ "depcruise": "depcruise --config .dependency-cruiser.json app test next.config.ts playwright.config.ts postcss.config.mjs vitest.config.ts",
11
+ "dev": "next dev --port 3000",
12
+ "start": "cp -R .next/static .next/standalone/apps/app/.next/ && (test ! -d public || cp -R public .next/standalone/apps/app/) && node .next/standalone/apps/app/server.js",
13
+ "test": "vitest run --passWithNoTests",
14
+ "test:changed": "if git rev-parse --quiet --verify origin/main > /dev/null && git diff --quiet \"$(git merge-base origin/main HEAD)\" -- pnpm-lock.yaml pnpm-workspace.yaml \"*package.json\" \"*tsconfig.json\" .husky; then vitest run --changed origin/main --passWithNoTests; else vitest run --passWithNoTests; fi",
15
+ "test:coverage": "vitest run --coverage --passWithNoTests",
16
+ "test:e2e": "playwright test",
17
+ "test:watch": "vitest",
18
+ "typecheck": "next typegen && tsc --noEmit -p tsconfig.json"
19
+ },
20
+ "dependencies": {
21
+ "next": "16.4.3",
22
+ "react": "19.3.1",
23
+ "react-dom": "19.3.1"
24
+ },
25
+ "devDependencies": {
26
+ "@playwright/test": "1.64.1",
27
+ "@tailwindcss/postcss": "4.3.3",
28
+ "@types/node": "26.6.5",
29
+ "@types/react": "19.3.0",
30
+ "@types/react-dom": "19.3.0",
31
+ "@vitest/coverage-v8": "5.0.5",
32
+ "dependency-cruiser": "18.7.0",
33
+ "fast-check": "4.10.2",
34
+ "tailwindcss": "4.3.3",
35
+ "typescript": "7.0.2",
36
+ "vitest": "5.0.5"
37
+ },
38
+ "engines": {
39
+ "node": ">=24.22.1 <25"
40
+ }
41
+ }
@@ -0,0 +1,27 @@
1
+ import {defineConfig, devices} from "@playwright/test";
2
+
3
+ const isCi = Boolean(process.env.CI);
4
+
5
+ export default defineConfig({
6
+ forbidOnly: isCi,
7
+ testDir: "test",
8
+ projects: [
9
+ {
10
+ name: "chromium",
11
+ use: devices["Desktop Chrome"],
12
+ },
13
+ ],
14
+ use: {
15
+ baseURL: "http://localhost:3000",
16
+ trace: "on-first-retry",
17
+ },
18
+ webServer: {
19
+ url: "http://localhost:3000",
20
+ command: isCi
21
+ ? "cp -R .next/static .next/standalone/apps/app/.next/ && (test ! -d public || cp -R public .next/standalone/apps/app/) && node .next/standalone/apps/app/server.js"
22
+ : "next dev --port 3000",
23
+ reuseExistingServer: !isCi,
24
+ },
25
+ reporter: isCi ? "github" : "list",
26
+ retries: isCi ? 2 : 0,
27
+ });
@@ -0,0 +1,7 @@
1
+ const postcssConfig = {
2
+ plugins: {
3
+ "@tailwindcss/postcss": {},
4
+ },
5
+ };
6
+
7
+ export default postcssConfig;
@@ -0,0 +1,11 @@
1
+ import {expect, test} from "@playwright/test";
2
+
3
+ test("home page shows the template heading", async ({page}) => {
4
+ await page.goto("/");
5
+
6
+ await expect(
7
+ page.getByRole("heading", {
8
+ level: 1,
9
+ }),
10
+ ).toHaveText("Next Friday Next.js template");
11
+ });
@@ -0,0 +1,33 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2023",
4
+ "lib": ["dom", "dom.iterable", "esnext"],
5
+ "module": "esnext",
6
+ "moduleResolution": "bundler",
7
+ "jsx": "react-jsx",
8
+ "esModuleInterop": true,
9
+ "incremental": true,
10
+ "isolatedModules": true,
11
+ "noEmit": true,
12
+ "resolveJsonModule": true,
13
+ "skipLibCheck": true,
14
+ "strict": true,
15
+ "plugins": [
16
+ {
17
+ "name": "next"
18
+ }
19
+ ],
20
+ "paths": {
21
+ "@/*": ["./*"]
22
+ },
23
+ "allowJs": true
24
+ },
25
+ "include": [
26
+ "next-env.d.ts",
27
+ "**/*.ts",
28
+ "**/*.tsx",
29
+ ".next/types/**/*.ts",
30
+ ".next/dev/types/**/*.ts"
31
+ ],
32
+ "exclude": ["node_modules", ".next"]
33
+ }
@@ -0,0 +1,12 @@
1
+ import {defineConfig} from "vitest/config";
2
+
3
+ export default defineConfig({
4
+ test: {
5
+ coverage: {
6
+ reporter: ["text", "html", "cobertura"],
7
+ include: ["app/**/*.{ts,tsx}"],
8
+ exclude: ["app/**/*.test.{ts,tsx}", "app/**/layout.tsx"],
9
+ },
10
+ include: ["app/**/*.test.{ts,tsx}"],
11
+ },
12
+ });
@@ -0,0 +1,34 @@
1
+ {
2
+ "extends": "dependency-cruiser/configs/recommended-strict",
3
+ "forbidden": [
4
+ {
5
+ "name": "no-orphans",
6
+ "to": {},
7
+ "from": {
8
+ "orphan": true,
9
+ "pathNot": [
10
+ "(^|/)tsconfig\\.json$",
11
+ "(^|/)eslint\\.config\\.ts$",
12
+ "(^|/)next\\.config\\.ts$",
13
+ "(^|/)vitest[.]config[.]ts$",
14
+ "(^|/)playwright[.]config[.]ts$",
15
+ "^test/.*[.]e2e[.]test[.]ts$",
16
+ "^src/app/.*(page|layout|template|loading|error|not-found|default|route)[.]tsx?$",
17
+ "^src/payload-types[.]ts$"
18
+ ]
19
+ }
20
+ }
21
+ ],
22
+ "options": {
23
+ "skipAnalysisNotInRules": true,
24
+ "tsPreCompilationDeps": true,
25
+ "enhancedResolveOptions": {
26
+ "conditionNames": ["import", "require", "node", "default", "types"],
27
+ "exportsFields": ["exports"]
28
+ },
29
+ "exclude": "(^|/)(?:\\.next|coverage|node_modules)/|^src/app/[(]payload[)]/",
30
+ "tsConfig": {
31
+ "fileName": "tsconfig.json"
32
+ }
33
+ }
34
+ }
@@ -0,0 +1,28 @@
1
+ # Runs on any OCI platform (Railway, Fly.io, Cloud Run). On Vercel, deploy without it. The platform sets `PORT`.
2
+ FROM node:24.22.1-alpine@sha256:06d7adf3e0ef61a13f1d28682726366f97a15aa1888c934d3ecec86c33918b53 AS build
3
+ ENV NEXT_TELEMETRY_DISABLED=1
4
+ WORKDIR /app
5
+ COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
6
+ COPY apps/app/package.json apps/app/package.json
7
+ COPY apps/backoffice/package.json apps/backoffice/package.json
8
+ RUN npm install --global "pnpm@$(node --print 'require("./package.json").packageManager.split("+")[0].split("@")[1]')"
9
+ RUN pnpm install --frozen-lockfile --ignore-scripts --filter ./apps/backoffice...
10
+ COPY apps/backoffice/next.config.ts apps/backoffice/tsconfig.json apps/backoffice/
11
+ COPY apps/backoffice/src apps/backoffice/src
12
+ # `next build` loads the Payload config, which requires these variables. The values are placeholders for the build only.
13
+ # The platform sets the real ones at runtime, and the build never connects to the database.
14
+ RUN PAYLOAD_SECRET=build-only DATABASE_URL=postgres://build-only@localhost/build-only APP_URL=http://localhost:3001 pnpm --filter ./apps/backoffice build
15
+
16
+ FROM node:24.22.1-alpine@sha256:06d7adf3e0ef61a13f1d28682726366f97a15aa1888c934d3ecec86c33918b53
17
+ ENV NODE_ENV=production NEXT_TELEMETRY_DISABLED=1 HOSTNAME=0.0.0.0
18
+ WORKDIR /app
19
+ # Apply Alpine security fixes released after the base image, and drop npm, corepack, and yarn: the app never runs them, and their bundled dependencies carry their own CVEs.
20
+ RUN apk upgrade --no-cache \
21
+ && rm -rf /usr/local/lib/node_modules /usr/local/bin/npm /usr/local/bin/npx /usr/local/bin/corepack /usr/local/bin/yarn /usr/local/bin/yarnpkg /opt/yarn-* \
22
+ && install -d -o node -g node apps/backoffice/media
23
+ # Next.js writes .next/cache at runtime. Uploads go to the container filesystem, which is lost on redeploy; use a storage adapter or a mounted volume in production.
24
+ COPY --from=build --chown=node:node /app/apps/backoffice/.next/standalone ./
25
+ COPY --from=build --chown=node:node /app/apps/backoffice/.next/static ./apps/backoffice/.next/static
26
+ USER 1000
27
+ HEALTHCHECK --interval=30s --timeout=5s CMD ["node", "-e", "fetch('http://127.0.0.1:' + (process.env.PORT || 3000) + '/health').then(response => process.exit(response.ok ? 0 : 1), () => process.exit(1))"]
28
+ CMD ["node", "apps/backoffice/server.js"]
@@ -0,0 +1,48 @@
1
+ import {fileURLToPath} from "node:url";
2
+ import path from "node:path";
3
+ import {withPayload} from "@payloadcms/next/withPayload";
4
+ import type {NextConfig} from "next";
5
+
6
+ const workspaceRoot = path.join(path.dirname(fileURLToPath(import.meta.url)), "../..");
7
+
8
+ const nextConfig: NextConfig = {
9
+ outputFileTracingRoot: workspaceRoot,
10
+ output: "standalone",
11
+ poweredByHeader: false,
12
+ images: {
13
+ localPatterns: [
14
+ {
15
+ pathname: "/api/media/file/**",
16
+ },
17
+ ],
18
+ },
19
+ async headers() {
20
+ return [
21
+ {
22
+ source: "/:path*",
23
+ headers: [
24
+ {
25
+ key: "Strict-Transport-Security",
26
+ value: "max-age=63072000; includeSubDomains",
27
+ },
28
+ {
29
+ key: "X-Content-Type-Options",
30
+ value: "nosniff",
31
+ },
32
+ {
33
+ key: "X-Frame-Options",
34
+ value: "SAMEORIGIN",
35
+ },
36
+ {
37
+ key: "Referrer-Policy",
38
+ value: "strict-origin-when-cross-origin",
39
+ },
40
+ ],
41
+ },
42
+ ];
43
+ },
44
+ };
45
+
46
+ export default withPayload(nextConfig, {
47
+ devBundleServerPackages: false,
48
+ });
@@ -0,0 +1,49 @@
1
+ {
2
+ "name": "@next-friday-template-turborepo/backoffice",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "description": "Next Friday Payload backoffice.",
6
+ "license": "UNLICENSED",
7
+ "type": "module",
8
+ "scripts": {
9
+ "build": "next build",
10
+ "depcruise": "depcruise --config .dependency-cruiser.json src test next.config.ts playwright.config.ts vitest.config.ts",
11
+ "dev": "next dev --port 3001",
12
+ "generate:importmap": "payload generate:importmap",
13
+ "generate:types": "payload generate:types",
14
+ "migrate": "payload migrate",
15
+ "migrate:create": "payload migrate:create",
16
+ "payload": "payload",
17
+ "start": "cp -R .next/static .next/standalone/apps/backoffice/.next/ && (test ! -d public || cp -R public .next/standalone/apps/backoffice/) && PORT=3001 node .next/standalone/apps/backoffice/server.js",
18
+ "test": "vitest run --passWithNoTests",
19
+ "test:changed": "if git rev-parse --quiet --verify origin/main > /dev/null && git diff --quiet \"$(git merge-base origin/main HEAD)\" -- pnpm-lock.yaml pnpm-workspace.yaml \"*package.json\" \"*tsconfig.json\" .husky; then vitest run --changed origin/main --passWithNoTests; else vitest run --passWithNoTests; fi",
20
+ "test:coverage": "vitest run --coverage --passWithNoTests",
21
+ "test:e2e": "playwright test",
22
+ "test:watch": "vitest",
23
+ "typecheck": "next typegen && tsc --noEmit -p tsconfig.json"
24
+ },
25
+ "dependencies": {
26
+ "@payloadcms/db-postgres": "3.92.1",
27
+ "@payloadcms/next": "3.92.1",
28
+ "@payloadcms/richtext-lexical": "3.92.1",
29
+ "next": "16.4.3",
30
+ "payload": "3.92.1",
31
+ "react": "19.3.1",
32
+ "react-dom": "19.3.1",
33
+ "sharp": "0.35.5"
34
+ },
35
+ "devDependencies": {
36
+ "@playwright/test": "1.64.1",
37
+ "@types/node": "26.6.5",
38
+ "@types/react": "19.3.0",
39
+ "@types/react-dom": "19.3.0",
40
+ "@vitest/coverage-v8": "5.0.5",
41
+ "dependency-cruiser": "18.7.0",
42
+ "fast-check": "4.10.2",
43
+ "typescript": "7.0.2",
44
+ "vitest": "5.0.5"
45
+ },
46
+ "engines": {
47
+ "node": ">=24.22.1 <25"
48
+ }
49
+ }
@@ -0,0 +1,27 @@
1
+ import {defineConfig, devices} from "@playwright/test";
2
+
3
+ const isCi = Boolean(process.env.CI);
4
+
5
+ export default defineConfig({
6
+ forbidOnly: isCi,
7
+ testDir: "test",
8
+ projects: [
9
+ {
10
+ name: "chromium",
11
+ use: devices["Desktop Chrome"],
12
+ },
13
+ ],
14
+ use: {
15
+ baseURL: "http://localhost:3001",
16
+ trace: "on-first-retry",
17
+ },
18
+ webServer: {
19
+ url: "http://localhost:3001",
20
+ command: isCi
21
+ ? "cp -R .next/static .next/standalone/apps/backoffice/.next/ && (test ! -d public || cp -R public .next/standalone/apps/backoffice/) && PORT=3001 node .next/standalone/apps/backoffice/server.js"
22
+ : "next dev --port 3001",
23
+ reuseExistingServer: !isCi,
24
+ },
25
+ reporter: isCi ? "github" : "list",
26
+ retries: isCi ? 2 : 0,
27
+ });
@@ -0,0 +1,18 @@
1
+ import type {Metadata} from "next";
2
+
3
+ import "./styles.css";
4
+
5
+ export const metadata: Metadata = {
6
+ title: "Next Friday Payload template",
7
+ description: "Starter application for Next Friday Payload projects.",
8
+ };
9
+
10
+ export default function RootLayout(props: LayoutProps<"/">) {
11
+ const {children} = props;
12
+
13
+ return (
14
+ <html lang="en">
15
+ <body>{children}</body>
16
+ </html>
17
+ );
18
+ }
@@ -0,0 +1,14 @@
1
+ import {createElement} from "react";
2
+ import {describe, expect, it} from "vitest";
3
+ import {renderToStaticMarkup} from "react-dom/server";
4
+
5
+ import Page from "./page";
6
+
7
+ describe("Page", () => {
8
+ it("renders the template heading and the admin link", () => {
9
+ const html = renderToStaticMarkup(createElement(Page));
10
+
11
+ expect(html).toContain("Next Friday Payload template");
12
+ expect(html).toContain('href="/admin"');
13
+ });
14
+ });
@@ -0,0 +1,14 @@
1
+ import Link from "next/link";
2
+
3
+ export default function Page() {
4
+ return (
5
+ <main>
6
+ <h1>Next Friday Payload template</h1>
7
+
8
+ <p>
9
+ Edit <code>src/app/(frontend)/page.tsx</code> to get started, or open the{" "}
10
+ <Link href="/admin">admin panel</Link>.
11
+ </p>
12
+ </main>
13
+ );
14
+ }
@@ -0,0 +1,15 @@
1
+ :root {
2
+ font-family: system-ui, sans-serif;
3
+ color-scheme: light dark;
4
+ }
5
+
6
+ main {
7
+ display: flex;
8
+ flex-direction: column;
9
+ gap: 1rem;
10
+ align-items: center;
11
+ justify-content: center;
12
+ min-height: 100vh;
13
+ padding: 4rem;
14
+ text-align: center;
15
+ }
@@ -0,0 +1,24 @@
1
+ /* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
2
+ /* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
3
+ import type { Metadata } from 'next'
4
+
5
+ import config from '@payload-config'
6
+ import { NotFoundPage, generatePageMetadata } from '@payloadcms/next/views'
7
+ import { importMap } from '../importMap'
8
+
9
+ type Args = {
10
+ params: Promise<{
11
+ segments: string[]
12
+ }>
13
+ searchParams: Promise<{
14
+ [key: string]: string | string[]
15
+ }>
16
+ }
17
+
18
+ export const generateMetadata = ({ params, searchParams }: Args): Promise<Metadata> =>
19
+ generatePageMetadata({ config, params, searchParams })
20
+
21
+ const NotFound = ({ params, searchParams }: Args) =>
22
+ NotFoundPage({ config, params, searchParams, importMap })
23
+
24
+ export default NotFound