Docs
Systhema Design (opens in new tab)
Changelog

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

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 (opens in new tab))

core,react,payloadLink to this section

react,nextLink to this section

payloadLink to this section

templates/payloadLink to this section

♻️ RefactorsLink to this section

payloadLink to this section

generalLink to this section

🐛 Bug fixesLink to this section

coreLink to this section

core,payloadLink to this section

reactLink to this section

react,nextLink to this section

payloadLink to this section

templates/payloadLink to this section

generalLink to this section

⚡ PerformanceLink to this section

payloadLink to this section

💄 StyleLink to this section

payloadLink to this section

📚 DocsLink to this section

payloadLink to this section

🧹 ChoresLink to this section

depsLink to this section

payloadLink to this section

generalLink to this section