Docs

This page isn't translated yet

v1.4.0

CMS pages default to static generation with on-demand revalidation, and Payload gains a form builder with an email system and automatic upload optimization.

On this page

⚠️ Breaking & Behavioral ChangesLink to this section

Minimum Dependency VersionsLink to this section

@systhemaui/payload now requires Next.js 15.4.11 or higher on version 15 (or 16.2.0+ once it exits canary). The minimum PayloadCMS version is bumped to 3.79.0 across all @payloadcms/* peer dependencies.

Static Generation & Revalidation OverhaulLink to this section

CMS pages now default to static site generation (SSG) instead of server-side rendering. This is a significant performance improvement but requires changes to your project's route files.

The sitemap route switches from force-dynamic to hourly ISR. A new /sys/revalidate POST route with Bearer auth enables on-demand revalidation with wildcard support. The home page now also generates proper metadata via generatePageMetadata.

The revalidation endpoint requires the SYSTHEMA_API_SECRET environment variable to be set — it's used as the Bearer token for authentication.

Required project changesLink to this section

src/app/(site)/(systhema)/[...path]/page.tsx — Remove export const dynamic = 'auto' and add a long revalidate value:

- export const dynamic = 'auto'
  export const revalidate = 2629746 // 1 month

src/app/(site)/(systhema)/page.tsx — Add revalidate, import generatePageMetadata, and export a generateMetadata function so the home page gets proper SEO metadata:

+ import type { Metadata } from 'next'
- import { RootPage } from '@systhemaui/payload/next'
+ import { RootPage, generatePageMetadata } from '@systhemaui/payload/next'

+ export const revalidate = 2629746 // 1 month

  export default async function Page({ params }: { params: Promise<{ path: string }> }) {
    const payload = await getPayload({ config: configPromise })
    return <RootPage params={params} payloadInstance={Promise.resolve(payload)} />
  }

+ export async function generateMetadata(): Promise<Metadata | null> {
+   return generatePageMetadata({
+     params: Promise.resolve({ path: undefined }),
+     payloadInstance: getPayload({ config: configPromise }),
+   })
+ }

src/app/(site)/(systhema)/(sitemaps)/pages-sitemap.xml/route.ts — Replace force-dynamic with hourly ISR:

- export const dynamic = 'force-dynamic'
  export const revalidate = 3600 // Revalidate every hour

src/app/(site)/(systhema)/sys/[route]/route.ts — Add a POST handler for the new revalidation API:

+ export const POST = (
+   request: NextRequest,
+   { params }: { params: Promise<{ route: string }> },
+ ): Promise<Response> =>
+   systhemaApiRoutes(request, { params }, getPayload({ config: configPromise }))

src/app/robots.ts — Disallow the /api/ route:

- disallow: ['/sys/', '/admin/'],
+ disallow: ['/api/', '/sys/', '/admin/'],

✨ New Feature HighlightsLink to this section

Form Builder & Email SystemLink to this section

A full-featured form builder powered by @payloadcms/plugin-form-builder is now integrated into Systhema. Create forms directly in the admin panel with a drawer-based UX, unique field IDs, customizable labels, and multi-column layouts via <FormRow>. New field types include file upload with split-button design, date/datetime with validation presets, and hidden fields with IP/geo template variables. Forms can also be rendered standalone via the new <RenderForm> component.

Hidden fields support template variables like {{ip}}, {{geo}}, {{url}}, {{timestamp}}, URL query parameters via {{param:utm_source}}, and browser metadata. These are resolved automatically on form load and included in submissions. Existing form handler integrations may need to account for the new hidden field values.

All outgoing emails are centralized through a single processing pipeline with support for file attachments, Lexical rich text editing, inline CSS styles resolved from design tokens, and humanized wildcard field names. A built-in branded email template handles layout and dark mode out of the box, but can be fully replaced with a custom react-email template.

ConfigurationLink to this section

Forms are enabled by default in withSysthema(). To send form notification emails, a PayloadCMS email adapter must be configured in the base Payload config. This example uses @payloadcms/email-resend, but any PayloadCMS-compatible email adapter works (e.g. @payloadcms/email-nodemailer). If no email adapter is configured, forms still work but email features are automatically disabled.

// payload.config.ts
import { buildConfig } from 'payload'
import { withSysthema, type SysthemaPayloadPluginOptions } from '@systhemaui/payload'
import { resendAdapter } from '@payloadcms/email-resend'

const userSysthemaConfig: SysthemaPayloadPluginOptions = {
  forms: {
    enabled: true,
    redirectRelationships: ['pages'],
    fields: {
      text: true,      // text input with placeholder and max length
      email: true,     // email input with validation
      number: true,    // number input with min/max/step
      textarea: true,  // multi-line text with placeholder and max length
      select: true,    // dropdown select
      radio: true,     // radio button group
      checkbox: true,  // multi-option checkbox group
      date: true,      // date or datetime-local input with validation presets
      file: true,      // file upload with type/size restrictions
      message: true,   // instructional rich text content (not a form input)
      country: false,  // country selector (disabled by default)
      state: false,    // state/province selector (disabled by default)
    },
    formOverrides: {
      // standard PayloadCMS collection overrides
    },
    formSubmissionOverrides: {
      // standard PayloadCMS collection overrides
    },
  },
}

export default buildConfig(
  withSysthema(
    {
      secret: process.env.PAYLOAD_SECRET || '',
      db: yourDatabaseAdapter({ /* ... */ }),
      email: resendAdapter({
        defaultFromAddress: process.env.SYSTHEMA_EMAIL_FROM ?? 'Company <noreply@mail.example.com>',
        defaultFromName: 'Company',
        apiKey: process.env.RESEND_API_KEY || '',
      }),
    },
    userSysthemaConfig,
  ),
)

All fields support a width percentage for multi-column layouts via <FormRow>. The values shown above are the defaults — you only need to specify fields you want to change.

Environment variablesLink to this section

VariablePurposeExample
SYSTHEMA_EMAIL_FROMDefault "from" address for all emails"Company <noreply@mail.example.com>"
SYSTHEMA_EMAIL_REPLY_TODefault reply-to addresssupport@example.com
SYSTHEMA_EMAIL_ADMIN_ADDRESSFallback recipient when form has no addresseeadmin@example.com
RESEND_API_KEYResend API key (or your provider's equivalent)re_xxxxxxxxxxxx
NEXT_PUBLIC_SERVER_URLServer URL for logo/attachment resolutionhttps://example.com

Email settings can also be managed from the admin panel via the Emails global (from name, from address, reply-to, logo). The precedence is: form-level settings > Emails global > environment variables.

Custom email template with react-emailLink to this section

The built-in template works out of the box, but you can replace it entirely with a react-email template. The emailTemplate option receives the pre-processed HTML body (with inline styles already applied) and an options object with logoUrl and serverUrl.

Create your template components:

// src/emails/EmailLayout.tsx
import { Html, Head, Body, Container, Img } from '@react-email/components'
import { Tailwind } from '@react-email/tailwind'
import { getEmailTailwindConfig } from '@systhemaui/core'

export function EmailLayout({
  children,
  logoUrl,
}: {
  children: React.ReactNode
  logoUrl?: string
}) {
  return (
    <Html>
      <Head />
      <Tailwind config={getEmailTailwindConfig()}>
        <Body>
          <Container style={{ maxWidth: '512px' }}>
            {logoUrl && <Img src={logoUrl} alt="Logo" height={48} />}
            {children}
          </Container>
        </Body>
      </Tailwind>
    </Html>
  )
}
// src/emails/FormNotificationEmail.tsx
import { EmailLayout } from './EmailLayout'

export function FormNotificationEmail({
  content,
  logoUrl,
}: {
  content: string
  logoUrl?: string
  serverUrl?: string
}) {
  return (
    <EmailLayout logoUrl={logoUrl}>
      <div dangerouslySetInnerHTML={{ __html: content }} />
    </EmailLayout>
  )
}
// src/emails/renderer.ts
import { render } from '@react-email/components'
import { FormNotificationEmail } from './FormNotificationEmail'

export async function renderEmailTemplate(
  html: string,
  options: { logoUrl?: string; serverUrl?: string },
): Promise<string> {
  return render(
    FormNotificationEmail({
      content: html,
      logoUrl: options.logoUrl,
      serverUrl: options.serverUrl,
    }),
  )
}

Then wire it up in your payload config:

// payload.config.ts
import { renderEmailTemplate } from './emails/renderer'

const userSysthemaConfig: SysthemaPayloadPluginOptions = {
  emailTemplate: renderEmailTemplate,
}

Upload OptimizationLink to this section

Uploaded images are now automatically optimized before storage. By default, images wider than 2560px are resized, JPEGs are compressed to 60% quality, and opaque PNGs (without actual transparency) are converted to JPEG. A 100MB file size limit is enforced both client-side (with UI feedback) and server-side.

All defaults can be customized via the uploads option:

const userSysthemaConfig: SysthemaPayloadPluginOptions = {
  uploads: {
    maxFileSize: 52428800, // 50MB
    imageOptimization: {
      maxWidth: 1920,          // resize to max 1920px wide (default: 2560)
      jpegQuality: 80,         // JPEG quality 1-100 (default: 60)
      convertPngToJpeg: false, // keep PNGs as-is (default: true)
      pngQuality: 80,          // PNG quality 1-100 (default: 60)
    },
  },
}

Set imageOptimization: false to disable all processing entirely.

List of all changesLink to this section

🚀 FeaturesLink to this section

coreLink to this section

  • feat(core): add email Tailwind config with typography and color primitives (44bbc16)

core,reactLink to this section

  • feat(core,react): added width="" attribute to all form field components, so we can correctly render them inside a next to each other, and also added placeholder styles to the select input (3f7ff94)

core,react,payloadLink to this section

  • feat(core,react,payload): add FileField component with split-button design (1206612)

react,nextLink to this section

  • feat(react,next): add DateField component and set NumberField inputMode to decimal (baf2457)

payloadLink to this section

  • !feat(payload): add hidden fields, keyField, and IP/geo template variables to forms (breaking) (ab0f60d)
  • feat(payload): add form builder integration with @payloadcms/plugin-form-builder (adc3f35)
  • feat(payload): add RenderForm component for standalone form rendering (2ab9017)
  • feat(payload): add file upload field to form builder (ff19d81)
  • feat(payload): add configurable max file size to form file upload field (998d420)
  • feat(payload): add configurable upload optimization and file size validation (7324fad)
  • feat(payload): improve form builder admin UX with drawers, icons, and global email settings (689b3f6)
  • feat(payload): add date/datetime form field with validation and step presets (07f51e2)
  • feat(payload): add branded email template, file attachments, and case-insensitive field resolution (3932159)
  • feat(payload): improve form builder UX with clearer labels, unique IDs, and simplified options (367fa68)
  • feat(payload): render message field with Lexical richtext, simplify options display, remove date min/max/step (d81f040)
  • feat(payload): add programmatic payload type generation script (1c7a459)
  • feat(payload): integrate type generation into build and dev scripts (4fd4266)
  • feat(payload): add /sys/revalidate route with Bearer auth and wildcard support (d69c0ce)
  • feat(payload): hide collections from admin nav based on user roles (7c8a4e1)
  • feat(payload): hide globals from admin nav for non-editor users and export hidden helpers (cdb2c13)
  • feat(payload): add email lexical editor and inline email styles (998cbc1)
  • feat(payload): resolve all typography properties in email inline styles (023e492)
  • feat(payload): humanize field names in email wildcard output (b027a93)

templates/payloadLink to this section

  • feat(templates/payload): add react-email template example with emailTemplate plugin option (9aa4e83)
  • feat(payload-template): wire up Systhema email Tailwind config and auto-width logo (885f199)

♻️ RefactorsLink to this section

payloadLink to this section

  • refactor(payload): remove date/datetime min, max, step types until custom implementation (c95089e)

generalLink to this section

  • refactor: gate email features behind adapter check and refine Emails global (2a4ef81)

🐛 Bug fixesLink to this section

coreLink to this section

  • fix(core): flatten media-overlay CSS selectors and use pseudo-element for icon mask (a1cf89d)

core,payloadLink to this section

  • fix(core,payload): append px unit to bare numeric token values in email styles (39af1ea)

reactLink to this section

  • fix(react): disable Swiper loop when not enough slides for loop mode (c03c716)
  • fix(react): stop passing required to individual checkboxes in CheckboxField (a4b7cf4)

react,nextLink to this section

  • fix(react,next): suppress hydration warnings for config-dependent class names (15fcab1)
  • fix(react,next,payload): added missing preload="auto" to <video> tags (2772883)

payloadLink to this section

  • fix(payload): removed unnecessary margins and paddings from inline blocks that have a systhema style renderer in them (2c73d64)
  • fix(payload): correct FormBlock width alignment detection and hide empty drawer (24f372a)
  • fix(payload): remove file size limit constraint on form file upload field (91d5e0b)
  • fix(payload): improve video block controls and parallax logic (316641a)
  • fix(payload): add playsInline to video elements with poster images (3a79728)
  • fix(payload): make sure UploadFeature's jsx converter is also using the Image component from @systhemaui/next (c40a5a6)
  • fix(payload): coerce non-string defaultValues in buildInitialFormState (0ae021c)
  • fix(payload): correct private IP range check for 172.16-31.x.x in geo route (9eb90dd)
  • fix(payload): prevent redundant API calls in async hidden field resolution (05cd6a2)
  • fix(payload): harden form-upload endpoint with server-side validation (f3ae591)
  • fix(payload): replace lite-youtube embed with standard YouTube iframe (d028cd3)
  • fix(payload): enforce 16/9 aspect ratio on YouTube iframe embeds (d4d0344)
  • fix(payload): disallow api route in robots.txt (89424a9)
  • fix(payload): revert admin.hidden access control changes (c89ee81)
  • fix(payload): restrict global update access to editor role (b39532b)
  • fix(payload): enable SSG for CMS pages in catch-all route (94982eb)
  • fix(payload): replace force-dynamic with hourly ISR on sitemap route (08b6a4e)
  • fix(payload): fix queryPageByPath cache key and add home page metadata (059a138)
  • fix(payload): use empty string check for homepage path in revalidateDelete (3265bdb)
  • fix(payload): Next.js version constraints in correct place of the package.json file (f68bccb)
  • fix(payload): default video and YouTube block aspect ratio to 16/9 (81f96a4)
  • fix(payload): use object form for typescript.declare config option (8cc6ae2)
  • fix(payload,next): add viewport meta, robots noindex for admin, and Safari iOS zoom prevention (7331470)
  • fix(payload): fix type mismatches exposed by auto-generated payload types (33650c1)
  • fix(payload): centralize email processing for all outgoing emails (8a0188b)
  • fix(payload): parse defaultFromAddress format and change email logo to upload field (e533bba)
  • fix(payload): fix form field ID generation, button borders, and form builder UX (e1d9151)
  • fix(payload): update addClassNamesToElement import for richtext-lexical 3.79.0 (2f522f0)
  • fix(payload): preserve custom ID field values on page reload (b4a2567)
  • fix(payload): resolve key field lock checkbox path for nested fields (3fa20f7)
  • fix(payload): use custom select placeholder and default to first option (166f8d8)
  • fix(payload): simplify default email template to plain layout (1265fcf)
  • fix(payload): remove explicit colors from email templates for dark mode (b58c9ab)
  • fix(payload): fetch file content as buffer for email attachments (d5e6a1f)
  • fix(payload): use auto width for email logo and restrict upload to jpg/png (aa7daaa)

templates/payloadLink to this section

  • fix(templates/payload): use valid EditorName value in custom block config (5947098)
  • fix(templates/payload): include optimized version of the @tailwindcss/postcss package instead of the default messed up one (1bdb2d2)

generalLink to this section

  • fix: apply aspect ratio to video blocks with poster images (1da30a3)
  • fix: remove useField from drawer component to fix form field creation crash (26717be)

⚡ PerformanceLink to this section

payloadLink to this section

  • perf(payload): optimize revalidation hooks and richText field processing (bfc861e)

💄 StyleLink to this section

payloadLink to this section

  • style(payload): format auto-generated payload types (3b6a756)
  • style(payload): center email template container and remove side padding (6204f5f)

📚 DocsLink to this section

payloadLink to this section

  • docs(payload): update form types comment to reference auto-generation (7aab781)

🧹 ChoresLink to this section

depsLink to this section

  • chore(deps): update dependencies and bump peer dependency ranges (51a5695)
  • chore(deps): update dependencies across all packages and templates (f7dbe27)
  • chore(deps): updated dependencies on all packages and templates to latest versions (cc6b55e)
  • chore(deps): updated next packages to safest 15.4.11 version, still waiting for 16.2.0 (034616d)

payloadLink to this section

  • chore(payload): sync auto-generated payload types (f5314c3)
  • chore(payload): update Next.js version constraint in package.json for compatibility and security (1cb0520)

generalLink to this section

  • chore: publish internal packages under internal dist-tag (86ce792)
  • chore: renamed GITHUBTOKEN to have a SYSTHEMA prefix, so i won't make conflicts with other applications, like the gh cli (5db8f5a)