@next-friday/create-app 2.0.0 → 2.1.1

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 (86) hide show
  1. package/README.md +16 -15
  2. package/dist/main.mjs +18 -7
  3. package/package.json +2 -2
  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/scripts/affected-apps.sh +31 -0
  23. package/templates/turborepo/.github/workflows/ci.yml +300 -0
  24. package/templates/turborepo/.github/workflows/deploy.yml +200 -0
  25. package/templates/turborepo/.github/workflows/sast.yml +46 -0
  26. package/templates/turborepo/.lintstagedrc.json +11 -0
  27. package/templates/turborepo/.prettierignore +5 -0
  28. package/templates/turborepo/AGENTS.md +20 -0
  29. package/templates/turborepo/CONTRIBUTING.md +58 -0
  30. package/templates/turborepo/README.md +114 -0
  31. package/templates/turborepo/_gitignore +53 -0
  32. package/templates/turborepo/apps/app/.dependency-cruiser.json +34 -0
  33. package/templates/turborepo/apps/app/Dockerfile +25 -0
  34. package/templates/turborepo/apps/app/app/favicon.ico +0 -0
  35. package/templates/turborepo/apps/app/app/globals.css +26 -0
  36. package/templates/turborepo/apps/app/app/health/route.test.ts +20 -0
  37. package/templates/turborepo/apps/app/app/health/route.ts +23 -0
  38. package/templates/turborepo/apps/app/app/layout.tsx +29 -0
  39. package/templates/turborepo/apps/app/app/page.test.ts +11 -0
  40. package/templates/turborepo/apps/app/app/page.tsx +8 -0
  41. package/templates/turborepo/apps/app/next.config.ts +40 -0
  42. package/templates/turborepo/apps/app/package.json +41 -0
  43. package/templates/turborepo/apps/app/playwright.config.ts +27 -0
  44. package/templates/turborepo/apps/app/postcss.config.mjs +7 -0
  45. package/templates/turborepo/apps/app/test/home.e2e.test.ts +11 -0
  46. package/templates/turborepo/apps/app/tsconfig.json +33 -0
  47. package/templates/turborepo/apps/app/vitest.config.ts +12 -0
  48. package/templates/turborepo/apps/backoffice/.dependency-cruiser.json +34 -0
  49. package/templates/turborepo/apps/backoffice/Dockerfile +28 -0
  50. package/templates/turborepo/apps/backoffice/next.config.ts +48 -0
  51. package/templates/turborepo/apps/backoffice/package.json +49 -0
  52. package/templates/turborepo/apps/backoffice/playwright.config.ts +27 -0
  53. package/templates/turborepo/apps/backoffice/src/app/(frontend)/layout.tsx +18 -0
  54. package/templates/turborepo/apps/backoffice/src/app/(frontend)/page.test.ts +14 -0
  55. package/templates/turborepo/apps/backoffice/src/app/(frontend)/page.tsx +14 -0
  56. package/templates/turborepo/apps/backoffice/src/app/(frontend)/styles.css +15 -0
  57. package/templates/turborepo/apps/backoffice/src/app/(payload)/admin/[[...segments]]/not-found.tsx +24 -0
  58. package/templates/turborepo/apps/backoffice/src/app/(payload)/admin/[[...segments]]/page.tsx +24 -0
  59. package/templates/turborepo/apps/backoffice/src/app/(payload)/admin/importMap.ts +6 -0
  60. package/templates/turborepo/apps/backoffice/src/app/(payload)/api/[...slug]/route.ts +19 -0
  61. package/templates/turborepo/apps/backoffice/src/app/(payload)/custom.scss +0 -0
  62. package/templates/turborepo/apps/backoffice/src/app/(payload)/layout.tsx +31 -0
  63. package/templates/turborepo/apps/backoffice/src/app/health/route.test.ts +20 -0
  64. package/templates/turborepo/apps/backoffice/src/app/health/route.ts +23 -0
  65. package/templates/turborepo/apps/backoffice/src/collections/media.test.ts +27 -0
  66. package/templates/turborepo/apps/backoffice/src/collections/media.ts +18 -0
  67. package/templates/turborepo/apps/backoffice/src/collections/users.test.ts +10 -0
  68. package/templates/turborepo/apps/backoffice/src/collections/users.ts +10 -0
  69. package/templates/turborepo/apps/backoffice/src/environment.test.ts +32 -0
  70. package/templates/turborepo/apps/backoffice/src/environment.ts +27 -0
  71. package/templates/turborepo/apps/backoffice/src/migrations/20261030_005055_initial.json +969 -0
  72. package/templates/turborepo/apps/backoffice/src/migrations/20261030_005055_initial.ts +134 -0
  73. package/templates/turborepo/apps/backoffice/src/migrations/index.ts +9 -0
  74. package/templates/turborepo/apps/backoffice/src/payload-types.ts +339 -0
  75. package/templates/turborepo/apps/backoffice/src/payload.config.test.ts +31 -0
  76. package/templates/turborepo/apps/backoffice/src/payload.config.ts +50 -0
  77. package/templates/turborepo/apps/backoffice/test/home.e2e.test.ts +16 -0
  78. package/templates/turborepo/apps/backoffice/tsconfig.json +34 -0
  79. package/templates/turborepo/apps/backoffice/vitest.config.ts +19 -0
  80. package/templates/turborepo/eslint.config.ts +40 -0
  81. package/templates/turborepo/knip.json +9 -0
  82. package/templates/turborepo/package.json +70 -0
  83. package/templates/turborepo/pnpm-workspace.yaml +46 -0
  84. package/templates/turborepo/prettier.config.ts +11 -0
  85. package/templates/turborepo/tsconfig.json +16 -0
  86. package/templates/turborepo/turbo.json +45 -0
@@ -0,0 +1,114 @@
1
+ # next-friday-template-turborepo
2
+
3
+ Next Friday starter for a Turborepo workspace. `apps/app` is a Next.js site on port 3000. `apps/backoffice` is a Payload CMS on port 3001. The backoffice API is REST at `/api`.
4
+
5
+ ## Getting started
6
+
7
+ ```sh
8
+ npm install --global pnpm
9
+ pnpm install
10
+ pnpm dev
11
+ ```
12
+
13
+ The CLI created `.env` from `.env.example` and set `PAYLOAD_SECRET` to a random secret. Before `pnpm dev`, set in `.env`:
14
+
15
+ - `DATABASE_URL`: a PostgreSQL database.
16
+ - `APP_URL`: the URL you open the backoffice at, such as `http://localhost:3001`.
17
+ - `PAYLOAD_SECRET`, only if you recreate `.env` from `.env.example`: generate one with `openssl rand -hex 32`.
18
+
19
+ Then open `http://localhost:3000` for the site and `http://localhost:3001/admin` to create the first user.
20
+
21
+ **Warning:** until the first user exists, anyone who can reach the backoffice `/admin` can create it. Create the first user in each deployed environment right after its first deploy, before you share the URL.
22
+
23
+ In development, Payload applies collection changes to the database directly. In production, the server applies the migrations in `apps/backoffice/src/migrations/` when it starts.
24
+
25
+ ## After creating a project from this template
26
+
27
+ 1. Install gitleaks and jq (see [CONTRIBUTING.md](CONTRIBUTING.md)). Make the first commit on `main` and push it. The Git hooks allow only this first commit and push on `main`; work on branches after that.
28
+
29
+ ```sh
30
+ git add --all
31
+ git commit -m "chore(repo): start from template"
32
+ git push --set-upstream origin main
33
+ ```
34
+
35
+ 2. Add your team as the owner of every file in `.github/CODEOWNERS`, and replace the owner placeholder in `.github/zizmor.yml`.
36
+ 3. Replace the template's description and owner: `package.json#description`, `repository.url`, and `author`, and the `Next Friday Next.js template` text in `apps/app/app/layout.tsx`, `apps/app/app/page.tsx`, and their tests, and the `Next Friday Payload template` text in `apps/backoffice/src/app/(frontend)/layout.tsx`, `apps/backoffice/src/app/(frontend)/page.tsx`, and their tests. Change `license` from `UNLICENSED` if the project is open source.
37
+ 4. In the repository settings:
38
+ - **Actions → General → Workflow permissions:** select **Read repository contents and packages permissions**. Every workflow requests the extra permissions it needs.
39
+ - **Advanced Security:** enable private vulnerability reporting, Dependabot alerts, Dependabot security updates, secret scanning, and push protection. `SECURITY.md` sends reporters to private vulnerability reporting.
40
+ - **Cursor Bugbot:** enable the repository at [Bugbot](https://cursor.com/automations/from-cursor/bugbot). Bugbot reviews pull requests against the rules linked from `.cursor/BUGBOT.md`.
41
+ - **Rules → Rulesets:** protect `main` with squash merges only, a linear history, no force pushes or deletion, and the required checks `All Checks` and `Validate pull request title`. Rulesets in a private repository need GitHub Pro, Team, or Enterprise; on GitHub Free, make the repository public or upgrade.
42
+ - Create the `needs triage` label, which the issue forms apply: `gh label create "needs triage"`.
43
+
44
+ ## Scripts
45
+
46
+ | Script | Purpose |
47
+ | :------------------------ | :----------------------------------------------------------------------------------------------- |
48
+ | `pnpm dev` | Start the Next.js app on port 3000 and the Payload backoffice on port 3001 |
49
+ | `pnpm build` | Build both apps for production |
50
+ | `pnpm start` | Serve both standalone production builds after `pnpm build` |
51
+ | `pnpm test` | Run unit tests in both apps |
52
+ | `pnpm test:e2e` | Run Playwright end-to-end tests in both apps. The backoffice uses the database in `DATABASE_URL` |
53
+ | `pnpm generate:types` | Regenerate `apps/backoffice/src/payload-types.ts` after a collection change |
54
+ | `pnpm generate:importmap` | Regenerate the admin import map after adding a custom component |
55
+ | `pnpm migrate:create` | Create a migration from collection changes, for example `pnpm migrate:create add-posts` |
56
+ | `pnpm migrate` | Apply pending migrations to the database in `DATABASE_URL` |
57
+ | `pnpm check` | Run the static checks and the unit tests for changed code (the `pre-push` hook) |
58
+ | `pnpm verify` | Run the static checks and all unit tests with coverage |
59
+
60
+ CI also runs the build, the end-to-end tests, the container image scans, and the security scans. Pull requests run Turborepo with `--affected` against the base branch, so only packages that changed are typed, tested, built, and scanned. Pushes to `main` and the weekly schedule still run every app. Patch coverage runs only for apps that produced a coverage file.
61
+
62
+ To reuse task output across CI runs, create a Vercel team, add the repository variable `TURBO_TEAM`, and follow [Turborepo on GitHub Actions](https://turborepo.dev/docs/guides/ci-vendors/github-actions) to allow OIDC. The workflow exchanges GitHub OIDC for a short-lived Remote Cache token. If OIDC is unavailable, set the `TURBO_TOKEN` secret instead. Leave both unset to run without a Remote Cache.
63
+
64
+ ## Container
65
+
66
+ Each app has a Dockerfile. Build both from the workspace root. The images run as the `node` user. The app listens on `PORT`, or 3000 when it is unset. The backoffice listens on `PORT`, or 3000 when it is unset, and needs `DATABASE_URL`, `PAYLOAD_SECRET`, and `APP_URL`.
67
+
68
+ ```sh
69
+ docker build --file apps/app/Dockerfile --tag app .
70
+ docker build --file apps/backoffice/Dockerfile --tag backoffice .
71
+ docker run --publish 3000:3000 app
72
+ docker run --publish 3001:3001 --env PORT=3001 --env DATABASE_URL --env PAYLOAD_SECRET --env APP_URL backoffice
73
+ ```
74
+
75
+ Uploaded images, up to 10 MB each, are written to the backoffice container filesystem, which a redeploy discards. Add a Payload storage adapter, for example `@payloadcms/storage-s3`, before storing media in production.
76
+
77
+ On Vercel, deploy an app without its image. The Next.js preset takes precedence over the Dockerfile.
78
+
79
+ ## Deploy
80
+
81
+ `.github/workflows/deploy.yml` deploys both apps and promotes one commit through `dev`, `uat`, and `prod`. `railway.json` sets the Railway health check to `/health`. Create one Railway service per app, with that service's Dockerfile path set to `apps/app/Dockerfile` or `apps/backoffice/Dockerfile`. The workflow stays off until you configure it:
82
+
83
+ 1. In Railway, create a project with one service for each app, one PostgreSQL database for the backoffice, and the environments `dev`, `uat`, and `prod`. The workflow passes these names to `railway up --environment`. In each backoffice environment, set the service variables:
84
+ - `DATABASE_URL` to `${{Postgres.DATABASE_URL}}`;
85
+ - `PAYLOAD_SECRET` to a separate long random string;
86
+ - `APP_URL` to that environment's public backoffice `https://` URL.
87
+
88
+ Create one project token per environment under **Project Settings → Tokens**. CI tests against PostgreSQL 17; change the image in `ci.yml` if the database runs another major version.
89
+
90
+ 2. In GitHub, create the `dev`, `uat`, and `prod` environments under **Settings → Environments**, and add to each:
91
+ - the `RAILWAY_TOKEN` secret: that environment's project token;
92
+ - the `RAILWAY_SERVICE_APP` variable: the Next.js service name;
93
+ - the `RAILWAY_SERVICE_BACKOFFICE` variable: the Payload service name;
94
+ - the `APP_URL` variable: the public Next.js `https://` URL;
95
+ - the `BACKOFFICE_URL` variable: the public backoffice `https://` URL.
96
+
97
+ Limit `prod` to the `main` branch and add required reviewers. Environment secrets and variables in a private repository need GitHub Pro, Team, or Enterprise; required reviewers in a private repository need GitHub Enterprise.
98
+
99
+ 3. Set the repository variable `DEPLOY_ENABLED` to `true`. Each push to `main` that passes CI then deploys to `dev`. The workflow compares the commit with the last GitHub deployment to that environment and runs `railway up` only for services whose packages changed. A promotion through `uat` or `prod` uses the same comparison against the commit last deployed there, so several commits still ship every changed service. The first deploy to an environment ships both services.
100
+ 4. Promote with **Actions → Deploy → Run workflow**, giving the environment and the full commit SHA. The workflow requires a successful CI run on `main` for that commit and a successful deployment to the previous environment, then waits for `/health` on each service it uploaded.
101
+
102
+ Remove the workflow, `railway.json`, and the `deploy-dev` job in `ci.yml` if the project deploys elsewhere.
103
+
104
+ ## Mobile app
105
+
106
+ The template does not ship an Expo app. Add one under `apps/mobile` when you need it:
107
+
108
+ 1. Create the app from the workspace root with `pnpm create expo-app@latest apps/mobile`, then add `packages/*` workspace packages as you share code. Keep `eas.json` and `credentials.json` inside `apps/mobile` and run the EAS CLI from that directory ([Expo: Build with monorepos](https://docs.expo.dev/build-reference/build-with-monorepos/)).
109
+ 2. Use `@expo/fingerprint` with `expo-github-action/fingerprint` so a pull request requests a native build only when the native layer changed ([Expo fingerprint](https://docs.expo.dev/versions/latest/sdk/fingerprint/)).
110
+ 3. Keep a single `react-native` version in the workspace. Duplicate React Native versions in one monorepo are not supported ([Expo: Monorepos](https://docs.expo.dev/guides/monorepos/)).
111
+
112
+ EAS Build and Submit replace Railway for that app. CI `--affected` already includes `apps/*` and `packages/*`, so an Expo app is skipped when it did not change.
113
+
114
+ See [CONTRIBUTING.md](CONTRIBUTING.md) for the contribution workflow.
@@ -0,0 +1,53 @@
1
+ node_modules
2
+ .pnpm-store
3
+
4
+ coverage
5
+ playwright-report
6
+ test-results
7
+ *.tsbuildinfo
8
+
9
+ .cache
10
+
11
+ *.pem
12
+
13
+ npm-debug.log*
14
+ pnpm-debug.log*
15
+ *.log
16
+
17
+ **/.env*
18
+ !.env.example
19
+
20
+ .eslintcache
21
+
22
+ .vscode/*
23
+ !.vscode/settings.json
24
+ !.vscode/extensions.json
25
+ /.idea
26
+
27
+ logs
28
+ .temp
29
+ .tmp
30
+
31
+ .DS_Store
32
+ .DS_Store?
33
+ ._*
34
+ .Spotlight-V100
35
+ .Trashes
36
+ ehthumbs.db
37
+ Thumbs.db
38
+ **/.DS_Store
39
+
40
+ *.key
41
+ *.p12
42
+ *.pfx
43
+ id_rsa*
44
+ id_ed25519*
45
+
46
+ /.claude/settings.local.json
47
+ /.claude/worktrees/
48
+
49
+ .next
50
+ .turbo
51
+ apps/backoffice/media
52
+ /out
53
+ next-env.d.ts
@@ -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
+ });