This is a Next.js v16 project.
- Next.js 16 App router
- React 19
- TypeScript
- shadcn/ui
- TailwindCSS 4
- Lucide icons
- next-intl
- next-themes (for dark mode)
- next-recaptcha-v3
- react-hook-form
- zod
- @tanstack/react-query
- @tanstack/react-table
- react-use
- @sentry/nextjs
- dayjs
- lodash
- tailwindcss, cva, tw-animate-css
Copy & rename .env.local.example to .env.local and fill or update in the values. Following environment variables are required to enable build-time pre-rendering or runtime ISR:
STRAPI_URL– The URL of the Strapi instance. Required at build time when pre-rendering ISR pages withgenerateStaticParams(). Optional if ISR pages are generated entirely at runtime.STRAPI_REST_READONLY_API_KEY– API key for read-only access to Strapi content. Required at build time if content is fetched during pre-rendering.APP_PUBLIC_URL– Used to generate canonical URLs, absolute links and metadata. Required when pre-rendering pages.
If ISR pages are generated only at runtime, STRAPI_URL and other environment variables must be available at runtime instead of build time. See Production Docker and Environment variables - usage sections for more details.
To fetch public content from Strapi, you need to set STRAPI_REST_READONLY_API_KEY env variable. Based on Strapi docs you have to go to Settings > API Tokens and "Create new API token". Token configuration:
Name: any name
Description: optional
Token duration: Unlimited
Token type: Read-only
Token will only be displayed once.
For other requests methods (POST, PUT, DELETE), we use custom API token, that has manually allowed permissions for specific operations and content types. Create it when you need to modify data in Strapi from the UI app. This token is also set as an environmental variable STRAPI_REST_CUSTOM_API_KEY and has following configuration:
Name: any name
Description: optional
Token duration: Unlimited
Token type: Custom
Permissions: <set permissions as needed>
To run the app locally use:
(nvm use) # switch node version
(pnpm install) # deps should be installed running `pnpm install` in the root
pnpm run devApp runs on http://localhost:3000 by default.
To build and run Next.js in Docker container use Dockerfile prepared for production environment. It follows recommended way of running app in Turborepo monorepo structure.
Warning
Note, that Turborepo requires access to root package.json, pnpm-lock.yaml and turbo.json files so you have to build it within whole monorepo context - run docker build from monorepo root.
More info here.
Next.js requires standalone output mode to run in Docker container. In this mode, only the necessary files and dependencies are included in the final image, so the image size is smaller and more efficient. This is hardcoded in Dockerfile using NEXT_OUTPUT=standalone env variable that is then read by next.config.mjs. Based on your needs, you can build Docker image in two ways:
- build once, deploy many times - build Docker image without env variables required at build time. This will opt-out pre-rendering of pages that require
STRAPI_URLat build time (i.e. pages usinggenerateStaticParams()function). These pages will be generated entirely at runtime (with runtime configuration) using ISR (or SSR). This approach is more flexible, as the same image can be deployed to different environments (e.g. staging, production) without rebuilding. However, the first (or every) request to these pages may be slower due to on-demand generation.
# from monorepo root
# Build image without providing Strapi information.
# Those pages won't be pre-rendered at build time.
docker build -t starter-ui:latest -f apps/ui/Dockerfile .- build per environment - build Docker image with
STRAPI_URLand other required env variables passed as build-time arguments. This will pre-render pages that depend onSTRAPI_URLduring the build process, resulting in faster initial load times for these pages. However, the image will be tied to a specific Strapi instance and need to be rebuilt for different environments. This will bake the Strapi READONLY API key into the image, so make sure you are okay with that.
# from monorepo root
# Build image with Strapi information passed as a build-time arguments.
# Pages depending on it will be pre-rendered during build.
# These vars will be baked into the image.
# You can set STRAPI_URL to your local Strapi instance or remote one - "https://strapi-next-starter-api-dev-c6c718c7e60e.herokuapp.com"
docker build -t starter-ui:latest -f apps/ui/Dockerfile \
--build-arg STRAPI_URL="http://host.docker.internal:1337" \
--build-arg STRAPI_REST_READONLY_API_KEY="your-readonly-api-key" \
--build-arg APP_PUBLIC_URL="http://localhost:3000" \
--progress=plain \
.# run container using image with runtime config (.env.local)
docker run -it --rm --name starter-ui -p 3000:3000 --env-file apps/ui/.env.local starter-ui:latestPort is 3000 and mapping can be changed in docker run command using -p flag (host:container).
Next.js has three output modes:
export– Static HTML/CSS/JS files are generated at build time and can be served by any static hosting or CDN. No Node.js server is required. Dynamic features are not supported. This mode is not supported in this starter by default due to its dynamic features (e.g. the POST endpoint). With some modifications, it can be turned into a fully static app.standalone– Optimized output for self-hosting in a Docker container (see above). It includes only the necessary files and dependencies.undefined– Default build output in the.nextdirectory. This mode is used withnext startin production or by hosting providers like Vercel. It requires a Node.js server.
Important
There is an additional script included in this repository:
pnpm run build:ui:static which triggers the output: "export" build, however, this one is not working out of the box, as it's necessary to remove usage of dynamic functions (see docs for more info). This includes BetterAuth, etc. You would also need to adjust const revalidate and const dynamic attributes for your dynamic segments and layouts.
If you're using this variant, you should also enable the CI check for static export builds in the GitHub Actions workflow by uncommenting the relevant step. Building the app with output: "export" will help identify any unsupported features that need to be addressed.
This approach allows static content to be updated without rebuilding the entire site. Data revalidation does not work in plain static export output mode, as the app is fully static and lacks a server to handle revalidation. Incremental Static Regeneration (ISR) improves performance and reduces server load.
In this starter, ISR with time-based revalidation is used by default. Revalidation is applied globally to all fetch requests, but it can also be controlled individually via parameters in the fetch functions (see BaseStrapiClient). The requests revalidation interval is set to 0 (no caching) during development and 60 seconds in production by default.
For dynamic routes where some slugs are not known at build time, Next.js can statically generate pages on the first request and cache them using ISR.
export const dynamic = "force-static"
export const dynamicParams = true
export const revalidate = 300- Unknown slugs are generated once on first request, then cached as static HTML.
- Pages are revalidated every 300 seconds.
- Rendering remains static (not per-request SSR); request-time APIs like
cookies()are not allowed. - Allows access to environment variables available only at runtime without triggering
DYNAMIC_SERVER_USAGE.
Tip
Don’t use this setup for pages that render user-specific data (e.g. a logged-in user session). Without cacheComponents enabled, accessing request-time APIs (cookies(), headers(), auth) will force the entire route to render dynamically and disable static/ISR behavior.
There is a lot of code prepared in this template:
src/app/[locale]- page-builder pages fetched from Strapi and frontend pages for authorizationsrc/components- folder with components
For more details see project structure section below.
Not all predefined components or routes are needed in the final app, so unnecessary parts should be removed. A function called removeThisWhenYouNeedMe is placed at the top of each route or component and logs a warning message to the console. It helps identify unused or placeholder code. If the function, component, or page is required, simply remove the call to removeThisWhenYouNeedMe. Any code that still includes this call should be removed as development progresses.
/src/app– Next.js App Router main application. Components related to a specific page (i.e. used only on that page or its nested routes) should be placed in/src/pageName/_components. For example, do not place theSignInFormcomponent in a shared folder like/src/components/formsif it is only used on one page./src/components– Shared components used across multiple pages or globally (e.g. providers). Organize them into subfolders based on purpose:/src/components/elementary– Basic or standalone components that can be reused anywhere/src/components/forms– Components related to forms, such as wrappers and field types/src/components/page-builder– Components for the Strapi page builder, more info here/src/components/providers– Global wrapper components (e.g. context providers)/src/components/typography– Component handling h1, h2, h3, p, etc./src/components/ui– Tailwind wrappers around Radix UI components from theshadcn/uilibrary. This directory is controlled by shadcn. You can edit individual files (e.g. to adjust design or fix issues), but do not rename the folder or component files. More info here
/src/hooks– Custom React hooks/src/lib– Shared utilities, helpers, and functions for auth, theme, i18n, dates, navigation, reCAPTCHA, styles, etc./src/lib/metadata– Helpers for building page metadata from Strapi content/src/lib/strapi-api– Public and private API clients for fetching data from Strapi (see Data fetching)
/src/locales– Localization files/src/styles– Global styles/src/types– Type definitions (global types, API types, helper types, etc.)
Shadcn/ui is a lightweight UI library that combines Radix UI components with Tailwind CSS for styling. A list of available components can be found in the docs. Many components are pre-installed in this project by default. If you need additional components, you can either manually copy them from the docs or, preferably, use the CLI.
For example, to add an accordion component, run pnpm dlx shadcn@latest add select in this directory. The component will be added to the /src/components/ui folder, as defined in the Shadcn config file located at components.json. Once added, the component can be imported normally or its source code modified as needed.
It is possible to build your own theme at https://ui.shadcn.com/themes, where you can configure global component styles, colors, border radius, export the theme, and integrate it into your project. More details are available in the docs.
/src/styles/globals.css– Contains the project theme, Tailwind CSS configuration, and imports shared styles from the @repo/design-system package.
To merge multiple Tailwind classes and handle dynamic class names more effectively, strictly use the cn function defined in /src/lib/styles.ts.
import { cn } from "@/lib/styles"
export function MyComponent() {
return (
<div className={cn("flex items-center justify-center", className)}>...</div>
)
}There are 2 ways to fetch data from Strapi. Both of them use BaseStrapiClient class as a base class. This class isn't used directly, but extended by other classes.
To fetch data from the public Strapi API, it is assumed that you have generated the necessary API tokens. The PublicStrapiClient class is used for making public API requests — no user JWT token is included in these requests.
To fetch data from server context (SSR components, server actions, etc.) use this client instance without setting useProxy option in CustomFetchOptions parameter. This is default behavior. In this case the client calls Strapi directly.
To fetch data from client context (client components, client hooks, etc.), you must set useProxy: true in the CustomFetchOptions. In this case the client uses route handler as a public proxy. See PagesCatalogue component for an example of how to use it. This proxy serves two main purposes:
- To hide the authenticated API token from the client request.
- To obscure the Strapi backend URL, preventing users from accessing it directly.
The API client (called directly or through proxy) automatically injects either the STRAPI_REST_READONLY_API_KEY or STRAPI_REST_CUSTOM_API_KEY into the Authorization header. These keys are stored securely on the server. The choice of token depends on the HTTP method: GET requests use the read-only token, while other methods use the custom token.
With the read-only token, it's possible to fetch data from any Strapi content type (assuming the endpoint name can be guessed). To improve security and prevent unrestricted data access, an additional layer — ALLOWED_STRAPI_ENDPOINTS — is used. This list defines the specific Strapi endpoints that are allowed to be accessed. Keep it up to date with the content types you want to expose.
Applications with authentication pages (e.g. /auth/signin, /auth/register) require the Strapi Users & Permissions plugin to be enabled. This is enabled by default. The PrivateStrapiClient class is used for making private API requests — user JWT tokens are automatically injected on both the server and client sides, and it returns data related to the logged-in user.
It works similarly to the public API client - for requests coming from the server context, you should use the client instance without setting useProxy option in CustomFetchOptions (by default). In this case the Strapi is called directly. For requests coming from the client context, you must set useProxy: true in the CustomFetchOptions. In this case the client uses route handler as a private proxy. This proxy hides the Strapi backend URL, preventing users from accessing it directly.
In the middleware.ts file, the authMiddleware is used to check whether the user is authenticated. A list called authPages contains the routes that require authentication. If a user is not authenticated and tries to access a private route, they are redirected to the login page.
To retrieve the session (logged-in user) in server components, use getSessionSSR() function that returns typed session data:
import { headers } from "next/headers"
import { getSessionSSR } from "@/lib/auth"
const session = await getSessionSSR(await headers())To get session in client components use useSession() (reactive) or getSession():
import { authClient } from "@/lib/client"
// in client component/hook
const { data: session } = authClient.useSession()
// or imperatively
const { data: session } = await authClient.getSession()To omit the Authorization header and skip token detection, you can pass omitUserAuthorization: true in the options object of the fetchAPI function. Token detection is a dynamic operation, which prevents static rendering of the page.
The BaseStrapiClient class contains functions that wrap the native fetch() method, with pre-configured base path, token management, headers, and query parameter handling. It provides the following functions: fetchAPI, fetchOne, fetchMany, fetchAll, fetchOneBySlug, and fetchOneByFullPath.
fetchAPI– the most general-purpose function for making API requests (GET,POST,PUT,DELETE). It can be used in any scenario, but the return type must be manually specified. This function is especially useful when:- Fetching data from or sending data to a custom Strapi endpoint (e.g.
GET /users/my-logic-endpoint) - The data is not associated with any Strapi content type
- The endpoint is already used by another handler (e.g. the content type
"plugin::users-permissions.user"is reserved forGET /users, soGET /users/memust usefetchAPIinstead — see below):
- Fetching data from or sending data to a custom Strapi endpoint (e.g.
import { Result } from "@repo/strapi-types"
const fetchedUser: Result = await Strapi.PrivateStrapiClient(
"/users/me",
undefined,
undefined,
{
userJWT: token.strapiJWT,
}
)- other fetch functions – these are directly tied to Strapi content types. When calling them, you must specify the UUID (e.g.
"api::","admin::") of theContentTypeyou want to fetch. Based on this UUID, the response type is automatically inferred. Read the @repo/strapi-types documentation for more details on how type inference works. To make this work, you need to maintain a mapping between theContentTypeUUID and the corresponding endpoint URL path—refer to theAPI_ENDPOINTSobject in the BaseStrapiClient file. Also, Strapi must have types generation enabled (true by default).
In client React components/hooks use useQuery (or useMutation) hook from @tanstack/react-query to query/mutate data in reactive way (see example in usePages). In server components call endpoint directly and fetch data (/GET endpoints) on server side. Fetch functions are stored in strapi-api/content/server file. You can also use Next.js' server actions.
// src/lib/strapi-api/content/server.ts
export async function fetchHeader(locale: Locale) {
try {
return await PublicStrapiClient.fetchOne("api::header.header", undefined, {
locale,
populateDynamicZone: { content: true },
})
} catch (e: unknown) {
...
}
}
// src/components/page-builder/single-types/navbar/StrapiNavbar.tsx
import { fetchNavbar } from "@/lib/strapi-api/content/server"
export async function StrapiNavbar({ locale }: { readonly locale: Locale }) {
const response = await fetchNavbar(locale)
}Client side example:
// Client component
"use client"
import { useAllPages } from "@/hooks/usePages"
export default function PagesCatalog() {
const query = useAllPages()
return (...)
}
// useQuery hook
// src/hooks/usePages
"use client"
import { useQuery } from "@tanstack/react-query"
import { useLocale } from "next-intl"
import { PublicStrapiClient } from "@/lib/strapi-api"
export function useAllPages() {
const locale = useLocale()
return useQuery({
queryKey: ["pages", locale],
queryFn: async () =>
PublicStrapiClient.fetchAll(
"api::page.page",
{
locale,
status: "published",
},
undefined,
// This is a client-side query, so we use the proxy to transform the request
// to the Strapi API URL
{ useProxy: true }
),
})
}Page builder landing page is rendered inside the main dynamic route. It is an optional catch-all segment that captures every segment — in our case, the fullPath attribute of pages (e.g. /page-1-full-path) from Strapi. All published pages are rendered as nested URLs.
Special case is the Index/Root page. By default, its fullPath is set to shared value / in the @repo/shared-data package.
Another important aspect is the mapping between Strapi components and frontend components. This is handled in the src/components/page-builder/index.ts file. ContentComponents contains a mapping between Strapi component UIDs and their corresponding React components. This single registry serves all three dynamic zones (page, header, footer) via a shared DynamicZoneRenderer.
Warning
The mapping is not automatically generated, and it is your responsibility to keep it up to date. If you add a new dynamic-zone-level component in Strapi, you need to add it here as well. Currently, there is a performance issue with dynamic lazy-loading and all components are preloaded in the page builder. This is a known issue and will be fixed in the future. See #65
Tip
Not all Strapi components should be rendered at the dynamic zone level. Some components are intended to be used as subcomponents within other components (e.g. elements, utilities, navbar sub-components).
The frontend includes an internal page builder overview designed to improve orientation and understanding of how components are used across the project.
Displays a list of all frontend page builder components and which pages use each component. → Helps quickly evaluate the impact of component changes.
Displays a list of all pages and which components they contain. → Makes it easy to understand a page structure without manually navigating the Strapi.
This is a developer-only tool and is not intended for production use.
To generate metadata for each Page Builder page, the generateMetadata() function is used. It is called in the main page builder page and generates metadata based on the Strapi page's seo attribute. It creates standard page metadata, as well as Open Graph and Twitter tags, with fallbacks from locale files. See getMetadataFromStrapi function for more details. To add structured data (LD-JSON), use the StrapiStructuredData component, which is included by default.
To generate sitemap.xml, we use the built-in Next.js sitemap.ts file. It generates a sitemap based on Strapi data. You can specify which collections are pageable and should appear in the XML (defaults to "api::page.page"). The sitemap is created at runtime and revalidated similarly to default fetch revalidation. This behavior can be easily customized in the fetchAll function. The sitemap is not generated in environments other than production (env.APP_ENV === "production"). The sitemap is available at localhost:3000/sitemap.xml.
To generate robots.txt, we also use the built-in Next.js robots.ts file. It generates a robots.txt file with a basic configuration. Like sitemap.xml, this file is created only in the production environment. The robots.txt file is available at localhost:3000/robots.txt.
This starter supports Strapi's new feature Previews. It works by embedding an iframe of the frontend application directly inside the editor. In order to enable the feature, you need to configure the following STRAPI_PREVIEW_SECRET env variable. It must be same for frontend and backend.
We use Next.js's built-in draft mode to enable draft mode in the app. Configuration is done in the route handler and the StrapiPreviewListener component.
App is ready for localization. It uses next-intl package with basic configuration. For more in-depth configuration, see the docs. Relevant files:
- Next-intl plugin is defined in src/lib/i18n.ts and used by src/middleware.ts and registered in next.config.mjs
- locales (messages) in src/locales directory
- augmented types are configured in
src/types/global.d.ts- so messages keys in
useTranslation()orgetTranslations()are auto-completeable during development - so
Localefromnext-intlis typed to the locales used in the app (so no need to defineAppLocale, andLocaleis used directly by other utilities fromnext-intl) - additionally also
Formatcan be configured souseFormatteris type-safe and auto-completeable
- so messages keys in
- Navigation utils are wrapped using
createNavigation()in src/lib/navigation.ts to provideusePathname,Link,redirectanduseRouterwith correct locale prefix
Usage:
// Client or Server component without "async" - `useTranslations()`
import { useTranslations } from "next-intl"
export default function Page() {
const t = useTranslations("general")
return <div>{t("loading")}...</div> // "Loading..."
}// Server component with "async" - `getTranslations()`
import { getTranslations } from "next-intl/server"
export default async function ProfilePage() {
const user = await fetchUser()
const t = await getTranslations("ProfilePage")
return (
<PageLayout title={t("title", { username: user.name })}>
<UserDetails user={user} />
</PageLayout>
)
}// Component using Locale
import { Locale } from "next-intl"
export default async function Layout({
children,
params,
}: LayoutProps<"/[locale]">) {
const { locale } = (await params) as { locale: Locale }
...
}Next-intl might not be yet fully compatible with upcoming Next features, please refer to this section.
For full navigation functionality in cooperation with next-intl, some functions/components from next/navigation must be wrapped (see above). This applies to: Link, redirect, usePathname, useRouter. You have to use them instead of the original ones.
// ❌ NOT OK
import {
Link,
notFound,
redirect,
useRouter,
useSearchParams,
} from "next/navigation"
// ✅ OK
import { Link, redirect, useRouter } from "@/lib/navigation"Define them in .env.local.example, .env.local and src/env.mjs file where @t3-oss/env-nextjs validation package is used. This package is used to validate and type-check environment variables.
Always prefer getEnvVar() helper to read env variables! It works both in CSR and SSR context. See more info in env-vars.ts file.
import { env } from "@/env.mjs"
import { getEnvVar } from "@/lib/env-vars"
// ✅ OK
console.log(getEnvVar("RECAPTCHA_SECRET_KEY"))
// ✅ good, but prefer above
console.log(env.RECAPTCHA_SECRET_KEY)
// ❌ NOT OK
console.log(process.env.RECAPTCHA_SECRET_KEY)All default server-side variables are optional. This lets you build the application once and run it in different environments with different configurations without the need to rebuild. Baking them into the Docker image or build artifacts may not be desirable in many cases. In that case, their correctness must be checked at runtime (handled in getEnvVar by throwing an error). Requiring them at build time is also possible by updating the schema in env.mjs (marking them required) and passing them from the environment:
- as build-time arguments in Docker (see Production Docker section),
- in
env.localfile when building locally, - config/env vars in hosting providers (e.g. Vercel, Heroku).
Environment variables starting with NEXT_PUBLIC_ are automatically available in the client-side code. Don't store any sensitive information in these variables, as they are exposed. They must be present at build time.
Tip
SPA applications are notorious for embedding environmental variables into the build output of the application. If you want to avoid this, you can use custom injector. This allows you to omit the NEXT_PUBLIC_ prefix and follow "build once, run anywhere" approach (without baking these vars into the client-side code especially when different enviroments require different values).
Root layout can read specific values within the server context (in layout) and injects then into the window using a script tag. This way, the values don't have to be available during build, but they will be injected into the app during runtime. This is configurable through CSR_ENVs variable in the root layout file.
The getEnvVar helper function also reads these dynamically injected variables from the window object (CSR_CONFIG variable) when running in the client context so you can use it seamlessly in both server and client components.
Environment variables that need to be available in the build-time context of Turborepo tasks must be defined in the turbo.json file under the globalEnv section. The build step (turbo run build) runs in a sandboxed environment where only explicitly specified environment variables are accessible.
General unexpected rendering and lifecycle errors (not event handlers, not async code) are automatically caught by boundary defined in root error.tsx. This file can be defined at different levels/segments in route hierarchy.
For even more granular error handling use custom ErrorBoundary component. ErrorBoundary is easily configurable client-side component that utilizes react-error-boundary package and catches errors in smaller parts of the UI or individual components. By default it wraps Strapi components as their content is fetched from CMS and don't guarantee correctness.
import { ErrorBoundary } from "@/components/elementary/ErrorBoundary"
export default function Page() {
return (
<ErrorBoundary
customErrorTitle="Uh-oh, we broke something! Again..."
showErrorMessage
>
<StrapiNavbar />
</ErrorBoundary>
)
}The Next.js middleware is composed of standalone proxy functions, each handling a specific concern. They run in sequence — the first one to return a response short-circuits the chain.
| Proxy | File | Description |
|---|---|---|
| Basic Auth | basicAuth.ts | HTTP Basic Authentication, enabled via BASIC_AUTH_ENABLED env var |
| HTTPS Redirect | httpsRedirect.ts | Redirects HTTP to HTTPS in production (e.g. Heroku) |
| Auth Guard | authGuard.ts | Protects private pages by requiring an active session |
| Dynamic Rewrite | dynamicRewrite.ts | Rewrites requests with search params to the /dynamic/ route for SSR rendering |
Errors passed through <ErrorBoundary /> or error.tsx are automatically logged to Sentry. To turn Sentry on, set NEXT_PUBLIC_SENTRY_DSN to environment variables. SENTRY_AUTH_TOKEN, SENTRY_ORG and SENTRY_PROJECT are optional and serve for uploading source maps to Sentry during deployment. Uncaught errors are logged automatically.
Configuration is done in sentry.client.config.ts, sentry.server.config.ts, sentry.edge.config.ts, instrumentation.ts and next.config.mjs files. More information can be found in Sentry documentation.
The Next.js Image component is performance-optimized but also demanding, so it must be used carefully, especially with SEO considerations. For a full understanding of how it works, refer to the official documentation. Review the current configuration in the next.config.mjs file.
The following image components are provided as wrappers around the native Image component:
/src/components/elementary/ImageWithBlur.tsx– Displays images with a synchronous blur effect. Ideal for improving UX and performance. Has no side effects./src/components/elementary/ImageWithFallback.tsx– A client-only enhancement ofImageWithBlur. It checks if the image is loaded and displays a fallback image if not. If both primary and secondary sources fail, it falls back to a local placeholder.
reCAPTCHA v3 is preconfigured in the app. To enable it, set the NEXT_PUBLIC_RECAPTCHA_SITE_KEY and RECAPTCHA_SECRET_KEY environment variables. Wrap your form with the ReCaptchaProvider component and use the useReCaptcha hook to execute reCAPTCHA. The resulting reCAPTCHA token should then be validated on the server side using the validateRecaptcha function.
// server action
import { validateRecaptcha } from "@/lib/recaptcha"
export const submitContactUsForm = (payload: FormData) => {
const recaptchaToken = payload.get("recaptchaToken")
const isValid = await validateRecaptcha(recaptchaToken)
if (!isValid) {
throw new Error("Invalid reCAPTCHA token")
}
}import { ReCaptchaProvider } from "next-recaptcha-v3"
import { getEnvVar } from "@/lib/env-vars"
import { ContactUsForm } from "@/components/forms/ContactUsForm"
// Wrap form with reCAPTCHA provider
export default function Page() {
return (
<ReCaptchaProvider
reCaptchaKey={getEnvVar("NEXT_PUBLIC_RECAPTCHA_SITE_KEY")}
>
<ContactUsForm />
</ReCaptchaProvider>
)
}// Form component
import { useReCaptcha } from "next-recaptcha-v3"
import { submitContactUsForm } from "@/lib/actions"
export function ContactUsForm() {
const { executeRecaptcha } = useRecaptcha()
const onSubmit = async (data: FormData) => {
const recaptchaToken = await executeRecaptcha("submit_form")
data.append("recaptchaToken", recaptchaToken)
submitContactUsForm(data)
}
return <form onSubmit={onSubmit}>{/* ... */}</form>
}A simple health check endpoint is available at /api/health, e.g. http://localhost:3000/api/health. This can be used for monitoring and uptime checks.
There are a few configuration flags which can help you reduce chatter in the console. Enabling them is done by setting the respective environment variable to true in your environment configuration (e.g. .env.local file). By default, all of them are disabled.
Important
These debug options will log errors caught in fetch, so some events such as "No Page Found" will not be logged if you're using findMany endpoint with filters (such request will have 200 status, but data will be an empty array). This is used in the page-builder for example.
As a rule of thumb, if you're passing filters attribute to a Strapi GET request, you may need to handle no results cases manually.
Tip
We recommend developing with these logs, as they may help you identify issues early. Before deploying to production, consider disabling them to reduce log verbosity.
This flag will enable debugStaticParams function which logs the output of generateStaticParams function used for page generation, making it easier to see if you're missing or duplicating any pages.
- This flag will show non-blocking errors in the console, which are otherwise hidden to avoid cluttering the output. Non-blocking errors are those that do not prevent the application from functioning but may indicate potential issues.
Log example:
[BaseStrapiClient] Strapi API request error: {
name: 'NotFoundError',
message: 'Not Found',
details: {},
status: 404
}
This flag will log all API calls made by the Strapi client. This is useful for debugging data fetching issues and ensuring that the correct endpoints are being called.
Log example:
{
message: "Error fetching navbar for locale 'cs'",
error: {
error: '{"name":"NotFoundError","message":"Not Found","details":{},"status":404}',
stack: 'Error: {"name":"NotFoundError","message":"Not Found","details":{},"status":404}\n' +
' at PublicClient.fetchAPI (./strapi-next-monorepo-starter/apps/ui/.next/dev/server/chunks/ssr/[root-of-the-server]__0a38093c._.js:737:19)\n' +
' at process.processTicksAndRejections (node:internal/process/task_queues:95:5)\n' +
' at async PublicClient.fetchOne (./strapi-next-monorepo-starter/apps/ui/.next/dev/server/chunks/ssr/[root-of-the-server]__0a38093c._.js:746:16)\n' +
' at async fetchNavbar (./strapi-next-monorepo-starter/apps/ui/.next/dev/server/chunks/ssr/[root-of-the-server]__0a38093c._.js:1396:16)\n' +
' at async StrapiNavbar (./strapi-next-monorepo-starter/apps/ui/.next/dev/server/chunks/ssr/[root-of-the-server]__0a38093c._.js:2376:22)'
}
}