Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
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
4 changes: 3 additions & 1 deletion frontend/.env.example
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
NEXT_PUBLIC_API_URL=http://localhost:8000/api/v1/
NEXT_PUBLIC_CSRF_URL=http://localhost:8000/csrf/
NEXT_PUBLIC_ENVIRONMENT=local
NEXT_PUBLIC_CSRF_URL_DOCKER=http://nest-backend:8000/csrf/
NEXT_PUBLIC_ENVIRONMENT=docker
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Docker-specific URLs and environment change

The addition of Docker-specific URLs and changing the default environment to 'docker' supports the server-side rendering infrastructure, but this could affect non-Docker development setups.

Also applies to: 6-6


🌐 Web query:

What is the standard practice for managing multiple environment configurations in Next.js applications?

💡 Result:

Standard Practice for Managing Multiple Environment Configurations in Next.js

Managing multiple environment configurations is critical in Next.js applications to ensure security, proper functionality, and ease of deployment across development, testing, staging, and production. The Next.js ecosystem provides a clear, standardized approach for handling this.

Environment-Specific .env Files

  • Utilize separate environment variable files for each environment:
    • .env or .env.local for local development
    • .env.development for development
    • .env.test for testing
    • .env.production for production
  • Next.js automatically loads the appropriate file based on the current NODE_ENV (e.g., setting NODE_ENV=production loads .env.production) [4][5][10].
  • Sensitive variables should be stored in files that are excluded from version control (such as .env.local), while placeholders or defaults can be provided in .env.example [8].

Public vs Private Environment Variables

  • Variables that need to be accessed in client-side code must be prefixed with NEXT_PUBLIC_ (e.g., NEXT_PUBLIC_API_URL). These are exposed to both server and client [7][8][10].
  • Variables without this prefix are only available on the server side and should be used for sensitive data such as API secrets [7][8].

Accessing Variables in Code

  • Access your environment variables using process.env, for example:
    const apiUrl = process.env.NEXT_PUBLIC_API_URL;
  • Use fallback/default values to handle missing variables and prevent runtime errors:
    const siteTitle = process.env.NEXT_PUBLIC_SITE_TITLE || 'Default Site Title';

Dynamic Configurations with next.config.js

  • For more advanced scenarios, use next.config.js for programmatic configuration:
    • Modify or inject variables based on logic, such as detecting the current phase/environment [3][4].
    • Use the configuration function pattern, leveraging phase from next/constants to target specific environments:
      const { PHASE_DEVELOPMENT_SERVER } = require('next/constants');
      module.exports = (phase) => {
        if (phase === PHASE_DEVELOPMENT_SERVER) {
          return {
            // Dev-specific config
          };
        }
        return {
          // Prod or other configs
        };
      };
  • Environment variables configured in next.config.js can also be exposed to the browser if set in the env field or by using Webpack's DefinePlugin [2][3][4].

Scripts for Clear Separation

  • Define scripts in package.json to help visually and operationally separate environment commands:
    "scripts": {
      "dev": "next dev",
      "build": "next build",
      "start": "next start",
      "test": "NODE_ENV=test next build"
    }
    This avoids mistakes like running a build with the wrong environment settings [4].

Best Practices Summary

  • Never commit sensitive environment files (like .env.local) to version control.
  • Use NEXT_PUBLIC_ only for variables safe to expose to the browser.
  • Keep variable naming consistent and descriptive, avoiding special characters [6].
  • Use environment files for static variables, and consider runtime configuration solutions for variables that need to change without rebuilding the app [6].

By following these practices, teams can confidently manage environment-specific configurations in Next.js, enabling safer, more predictable, and easier-to-maintain deployments across multiple environments [4][5][10].

Citations:


Align environment configurations with Next.js recommended .env conventions

To avoid impacting non‑Docker development and follow Next.js best practices:

  • Remove Docker‑specific defaults from frontend/.env.example so it remains a generic template of all required keys.
  • Create dedicated files for each environment:
    • .env.local (ignored in VCS) for your local overrides
    • .env.development for standard development (non‑Docker)
    • .env.docker (or extend your Docker Compose) to hold NEXT_PUBLIC_CSRF_URL_DOCKER and NEXT_PUBLIC_ENVIRONMENT=docker
    • .env.production for production defaults
  • Use NODE_ENV or a custom script to load the correct .env file. For example in package.json:
    "scripts": {
      "dev": "next dev",
      "dev:docker": "cp .env.docker .env.local && next dev",
      "build": "next build",
      "start": "next start"
    }
  • Document in your README how to start the app in each environment and which .env files to use.

By splitting configs this way, you keep .env.example environment‑agnostic and ensure developers only pick up Docker settings when explicitly intended.

NEXT_PUBLIC_GRAPHQL_URL=http://localhost:8000/graphql/
NEXT_PUBLIC_GRAPHQL_URL_DOCKER=http://nest-backend:8000/graphql/
NEXT_PUBLIC_GTM_AUTH=your-google-tag-manager-auth
NEXT_PUBLIC_GTM_ID=your-google-tag-manager-id
NEXT_PUBLIC_GTM_PREVIEW=
Expand Down
2 changes: 1 addition & 1 deletion frontend/jest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ const config: Config = {
'src/**/*.{js,jsx,ts,tsx}',
'!src/**/*.d.ts',
'!src/api/**',
'!src/app/layout.tsx',
'!src/app/**/layout.tsx',
'!src/components/**',
'!src/hooks/**',
'!src/instrumentation.ts',
Expand Down
9 changes: 9 additions & 0 deletions frontend/src/app/about/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { Metadata } from 'next'
import React from 'react'
import { getStaticMetadata } from 'utils/metaconfig'

export const metadata: Metadata = getStaticMetadata('about', '/about')

export default function AboutLayout({ children }: { children: React.ReactNode }) {
return children
}
39 changes: 39 additions & 0 deletions frontend/src/app/chapters/[chapterKey]/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { Metadata } from 'next'
import React from 'react'
import { GET_CHAPTER_DATA } from 'server/queries/chapterQueries'
import { apolloServerClient } from 'utils/helpers/apolloClientServer'
import { generateSeoMetadata } from 'utils/metaconfig'

type Params = Promise<{ chapterKey: string }>

export async function generateMetadata({ params }: { params: Params }): Promise<Metadata> {
try {
const { chapterKey } = await params
const { data } = await apolloServerClient.query({
query: GET_CHAPTER_DATA,
variables: {
key: chapterKey,
},
})
const chapter = data?.chapter
if (!chapter) {
return
}
return generateSeoMetadata({
title: chapter.name,
description: chapter.summary ?? 'Discover details about this OWASP chapter.',
canonicalPath: `/chapter/${chapterKey}`,
keywords: ['owasp', 'security', 'chapter', chapterKey, chapter.name],
})
} catch {
return
}
}

export default function ChapterDetailsLayout({
children,
}: Readonly<{
children: React.ReactNode
}>) {
return <div className="chapter-layout">{children}</div>
}
9 changes: 9 additions & 0 deletions frontend/src/app/chapters/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { Metadata } from 'next'
import React from 'react'
import { getStaticMetadata } from 'utils/metaconfig'

export const metadata: Metadata = getStaticMetadata('chapters', '/chapters')

export default function ChaptersLayout({ children }: { children: React.ReactNode }) {
return children
}
41 changes: 41 additions & 0 deletions frontend/src/app/committees/[committeeKey]/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { Metadata } from 'next'
import React from 'react'
import { GET_COMMITTEE_DATA } from 'server/queries/committeeQueries'
import { apolloServerClient } from 'utils/helpers/apolloClientServer'
import { generateSeoMetadata } from 'utils/metaconfig'

export async function generateMetadata({
params,
}: {
params: Promise<{ committeeKey: string }>
}): Promise<Metadata> {
try {
const { committeeKey } = await params
const { data } = await apolloServerClient.query({
query: GET_COMMITTEE_DATA,
variables: {
key: committeeKey,
},
})
const committee = data?.committee
if (!committee) {
return
}
return generateSeoMetadata({
title: committee.name,
description: committee.summary ?? 'Discover details about this OWASP committee.',
canonicalPath: `/committees/${committeeKey}`,
keywords: ['owasp', 'security', 'committee', committeeKey, committee.name],
})
} catch {
return
}
}

export default function CommitteeDetailsLayout({
children,
}: Readonly<{
children: React.ReactNode
}>) {
return <div className="committee-layout">{children}</div>
}
9 changes: 9 additions & 0 deletions frontend/src/app/committees/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { Metadata } from 'next'
import React from 'react'
import { getStaticMetadata } from 'utils/metaconfig'

export const metadata: Metadata = getStaticMetadata('committees', '/committees')

export default function CommitteesLayout({ children }: { children: React.ReactNode }) {
return children
}
8 changes: 8 additions & 0 deletions frontend/src/app/contribute/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import React from 'react'
import { getStaticMetadata } from 'utils/metaconfig'

export const metadata = getStaticMetadata('contribute', '/contribute')

export default function ContributeLayout({ children }: { children: React.ReactNode }) {
return children
}
31 changes: 29 additions & 2 deletions frontend/src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,35 @@ const geistMono = Geist_Mono({
})

export const metadata: Metadata = {
title: 'OWASP Nest',
description: 'OWASP Nest',
title: 'Home – OWASP Nest',
description:
'OWASP Nest is a comprehensive platform designed to enhance collaboration and contribution within the OWASP community.',
openGraph: {
title: 'Home – OWASP Nest',
description:
'OWASP Nest is a comprehensive platform designed to enhance collaboration and contribution within the OWASP community.',
url: 'https://nest.owasp.org/',
siteName: 'OWASP Nest',
images: [
{
url: 'https://nest.owasp.org/img/owasp_icon_white_background.png',
width: 1200,
height: 630,
alt: 'OWASP logo',
},
],
locale: 'en_US',
type: 'website',
},
twitter: {
card: 'summary_large_image',
title: 'Home – OWASP Nest',
description:
'OWASP Nest is a comprehensive platform designed to enhance collaboration and contribution within the OWASP community.',
images: ['https://nest.owasp.org/img/owasp_icon_white_background.png'],
creator: '@owasp',
site: '@owasp',
},
icons: {
icon: 'https://owasp.org/www--site-theme/favicon.ico',
shortcut: 'https://owasp.org/www--site-theme/favicon.ico',
Expand Down
37 changes: 37 additions & 0 deletions frontend/src/app/members/[memberKey]/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { Metadata } from 'next'
import React from 'react'
import { GET_USER_DATA } from 'server/queries/userQueries'
import { apolloServerClient } from 'utils/helpers/apolloClientServer'
import { generateSeoMetadata } from 'utils/metaconfig'

export async function generateMetadata({
params,
}: {
params: Promise<{ memberKey: string }>
}): Promise<Metadata> {
try {
const { memberKey } = await params
const { data } = await apolloServerClient.query({
query: GET_USER_DATA,
variables: {
key: memberKey,
},
})
const user = data?.user
if (!user) {
return
}
return generateSeoMetadata({
title: user.name ?? user.login,
description: user.bio ?? 'Discover details about this OWASP community member.',
canonicalPath: `/members/${memberKey}`,
keywords: [user.login, user.name, 'owasp', 'owasp community member'],
})
} catch {
return
}
}

export default function UserDetailsLayout({ children }: { children: React.ReactNode }) {
return children
}
9 changes: 9 additions & 0 deletions frontend/src/app/members/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { Metadata } from 'next'
import React from 'react'
import { getStaticMetadata } from 'utils/metaconfig'

export const metadata: Metadata = getStaticMetadata('members', '/members')

export default function UsersLayout({ children }: { children: React.ReactNode }) {
return children
}
36 changes: 36 additions & 0 deletions frontend/src/app/organizations/[organizationKey]/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { Metadata } from 'next'
import React from 'react'
import { GET_ORGANIZATION_DATA } from 'server/queries/organizationQueries'
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The existing queries are too heavy for layout purposes -- they return a lot of information that is not going to be used at all.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes i will work on that and make separate query for the metadata

import { apolloServerClient } from 'utils/helpers/apolloClientServer'
import { generateSeoMetadata } from 'utils/metaconfig'

export async function generateMetadata({
params,
}: {
params: Promise<{ organizationKey: string }>
}): Promise<Metadata> {
try {
const { organizationKey } = await params
const { data } = await apolloServerClient.query({
query: GET_ORGANIZATION_DATA,
variables: {
login: organizationKey,
},
})
const organization = data?.organization
if (!organization) {
return
}
return generateSeoMetadata({
title: organization?.name ?? organization?.login,
description: organization?.description ?? 'Discover details about this OWASP organization.',
canonicalPath: `/organizations/${organizationKey}`,
})
} catch {
return
}
}

export default function OrganizationDetailsLayout({ children }: { children: React.ReactNode }) {
return children
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { Metadata } from 'next'
import React from 'react'
import { GET_REPOSITORY_DATA } from 'server/queries/repositoryQueries'
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here -- you only need name/description but request all this data

import { apolloServerClient } from 'utils/helpers/apolloClientServer'
import { generateSeoMetadata } from 'utils/metaconfig'

export async function generateMetadata({
params,
}: {
params: Promise<{
repositoryKey: string
organizationKey: string
}>
}): Promise<Metadata> {
try {
const { repositoryKey, organizationKey } = await params
const { data } = await apolloServerClient.query({
query: GET_REPOSITORY_DATA,
variables: { repositoryKey: repositoryKey, organizationKey: organizationKey },
})
const repository = data?.repository
if (!repository) {
return
}
return generateSeoMetadata({
title: repository.name,
description: repository.description ?? 'Discover details about this OWASP repository.',
canonicalPath: `/organizations/${organizationKey}/repositories/${repositoryKey}`,
keywords: ['owasp', 'repository', repositoryKey, repository.name],
})
} catch {
return
}
}

export default function RepositoryDetailsLayout({ children }: { children: React.ReactNode }) {
return children
}
9 changes: 9 additions & 0 deletions frontend/src/app/organizations/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { Metadata } from 'next'
import React from 'react'
import { getStaticMetadata } from 'utils/metaconfig'

export const metadata: Metadata = getStaticMetadata('organizations', '/organizations')

export default function OrganizationsLayout({ children }: { children: React.ReactNode }) {
return children
}
39 changes: 39 additions & 0 deletions frontend/src/app/projects/[projectKey]/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { Metadata } from 'next'
import React from 'react'
import { GET_PROJECT_DATA } from 'server/queries/projectQueries'
import { apolloServerClient } from 'utils/helpers/apolloClientServer'
import { generateSeoMetadata } from 'utils/metaconfig'

export async function generateMetadata({
params,
}: {
params: Promise<{
projectKey: string
}>
}): Promise<Metadata> {
try {
const { projectKey } = await params
const { data } = await apolloServerClient.query({
query: GET_PROJECT_DATA,
variables: {
key: projectKey,
},
})
const project = data?.project
if (!project) {
return
}
return generateSeoMetadata({
title: project.name,
description: project.summary ?? 'Discover details about this OWASP project.',
canonicalPath: `/projects/${projectKey}`,
keywords: ['owasp', 'project', projectKey, project.name],
})
} catch {
return
}
}

export default function ProjectDetailsLayout({ children }: { children: React.ReactNode }) {
return children
}
9 changes: 9 additions & 0 deletions frontend/src/app/projects/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { Metadata } from 'next'
import React from 'react'
import { getStaticMetadata } from 'utils/metaconfig'

export const metadata: Metadata = getStaticMetadata('projects', '/projects')

export default function ProjectsLayout({ children }: { children: React.ReactNode }) {
return children
}
34 changes: 34 additions & 0 deletions frontend/src/app/snapshots/[id]/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { Metadata } from 'next'
import React from 'react'
import { GET_SNAPSHOT_DETAILS } from 'server/queries/snapshotQueries'
import { apolloServerClient } from 'utils/helpers/apolloClientServer'
import { generateSeoMetadata } from 'utils/metaconfig'

export async function generateMetadata({
params,
}: {
params: Promise<{ id: string }>
}): Promise<Metadata> {
try {
const { id: snapshotKey } = await params
const { data } = await apolloServerClient.query({
query: GET_SNAPSHOT_DETAILS,
variables: { key: snapshotKey },
})
const snapshot = data?.snapshot
if (!snapshot) {
return
}
return generateSeoMetadata({
title: snapshot?.title,
description: `Discover details about ${snapshot?.title} OWASP snapshot.`,
canonicalPath: `/snapshots/${snapshotKey}`,
keywords: ['owasp', 'snapshot', snapshotKey, snapshot?.title],
})
} catch {
return
}
}
export default function SnapshotDetailsLayout({ children }: { children: React.ReactNode }) {
return children
}
Loading