feat(cms): Home/About/Services/Contact/Common Globals + JSON migration (MITHOME-87)

Payload Global configs mirroring proto/src/content/types.ts:
- src/globals/{Home,About,Services,Contact,Common}.ts
- src/globals/fields/stringArray.ts — shared helper: Payload has no
  native string[] field, so every plain string array from the old
  content types (trustBullets, paragraphs, features, spec items, …)
  becomes an array of one-field { value } objects.
- home.partners is intentionally NOT included — that becomes its own
  Partners collection (logo -> Media upload) in MITHOME-89, to avoid
  two disagreeing sources for the same data.
- contact.form.fields.gdpr.label stays a plain textarea (not lexical
  richText): the JSON source is a hand-written HTML string with an
  <a> tag; richText's node-tree serialization would need its own
  migration/render logic, out of scope for this bootstrap pass.

scripts/migrate-content-to-payload.ts: one-shot, idempotent Local API
migration reading the existing JSON files and calling updateGlobal —
does not touch or delete the JSON files. Run via the new
`npm run migrate:content` script.

Registered the five Globals in payload.config.ts.

Also included here (belongs with the previous "resolve double-root-
layout conflict" commit but didn't actually get staged there —
verified only now by diffing HEAD against the working tree):
(frontend)/layout.tsx's relative imports corrected to ../../ instead
of ../ (one directory deeper than the original src/app/layout.tsx).

Verified:
- npm run build / lint, tsc --noEmit, npm test (58 passed) all clean
- npm run migrate:content against the real dev MongoDB container,
  then read back via payload.findGlobal() — hero.title, trustBullets,
  services.items[0].features, footer.address, faq.items.length all
  match the JSON source
- Real browser: logged into /admin, opened the Home global editor —
  Hero/Trust bullets group renders and shows the migrated Hungarian
  content correctly (see screenshot shared in chat)

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Do Siki
2026-09-10 03:41:03 +02:00
co-authored by Claude Sonnet 5
parent 3183098926
commit 3132fcb8eb
10 changed files with 624 additions and 14 deletions
+73
View File
@@ -0,0 +1,73 @@
import type { GlobalConfig } from 'payload'
import { stringArrayField } from './fields/stringArray'
/** Mirrors AboutPageContent (proto/src/content/types.ts) — MITHOME-87. */
export const About: GlobalConfig = {
slug: 'about',
admin: {
group: 'Oldalak',
},
fields: [
{
name: 'meta',
type: 'group',
fields: [
{ name: 'title', type: 'text', required: true },
{ name: 'description', type: 'textarea', required: true },
{ name: 'ogDescription', type: 'textarea', required: true },
],
},
{
name: 'hero',
type: 'group',
fields: [
{ name: 'title', type: 'text', required: true },
{ name: 'subtitle', type: 'text', required: true },
],
},
{
name: 'story',
type: 'group',
fields: [
{ name: 'title', type: 'text', required: true },
stringArrayField('paragraphs', 'Paragraphs'),
],
},
{
name: 'mission',
type: 'group',
fields: [
{ name: 'title', type: 'text', required: true },
{ name: 'subtitle', type: 'text', required: true },
{
name: 'values',
type: 'array',
fields: [
{ name: 'title', type: 'text', required: true },
{ name: 'description', type: 'textarea', required: true },
{ name: 'icon', type: 'text', required: true },
],
},
],
},
{
name: 'team',
type: 'group',
fields: [
{ name: 'title', type: 'text', required: true },
{ name: 'subtitle', type: 'text', required: true },
stringArrayField('paragraphs', 'Paragraphs'),
],
},
{
name: 'cta',
type: 'group',
fields: [
{ name: 'title', type: 'text', required: true },
{ name: 'subtitle', type: 'textarea', required: true },
{ name: 'button', type: 'text', required: true },
],
},
],
}
+73
View File
@@ -0,0 +1,73 @@
import type { GlobalConfig } from 'payload'
/** Mirrors CommonContent (proto/src/content/types.ts) — MITHOME-87. */
export const Common: GlobalConfig = {
slug: 'common',
admin: {
group: 'Oldalak',
},
fields: [
{
name: 'buttons',
type: 'group',
fields: [
{ name: 'contact', type: 'text', required: true },
{ name: 'learnMore', type: 'text', required: true },
{ name: 'webmail', type: 'text', required: true },
{ name: 'sendMessage', type: 'text', required: true },
],
},
{
name: 'labels',
type: 'group',
fields: [
{ name: 'required', type: 'text', required: true },
{ name: 'features', type: 'text', required: true },
],
},
{
name: 'validation',
type: 'group',
fields: [
{ name: 'required', type: 'text', required: true },
{ name: 'invalidEmail', type: 'text', required: true },
{
name: 'minLength',
type: 'text',
required: true,
admin: {
description: 'A {min} placeholder futásidőben cserélődik ki.',
},
},
],
},
{
name: 'staging',
type: 'group',
fields: [{ name: 'banner', type: 'text', required: true }],
},
{
name: 'footer',
type: 'group',
fields: [
{
name: 'copyright',
type: 'text',
required: true,
admin: {
description: 'A {year} placeholder futásidőben cserélődik ki.',
},
},
{ name: 'address', type: 'text', required: true },
],
},
{
name: 'a11y',
type: 'group',
fields: [
{ name: 'openMenu', type: 'text', required: true },
{ name: 'closeMenu', type: 'text', required: true },
],
},
],
}
+142
View File
@@ -0,0 +1,142 @@
import type { GlobalConfig } from 'payload'
/**
* Mirrors ContactPageContent (proto/src/content/types.ts) — MITHOME-87.
*
* WHY `gdpr.label` textarea és nem richText: a JSON forrás egy kézzel
* beírt <a> taget tartalmazó HTML string (lásd contact.json). A lexical
* richText mező más (JSON node-fa) szerializációt használna, ami extra
* migrációs/render logikát igényelne — ezt itt, bootstrap szinten nem
* bontjuk ki, marad egyszerű (HTML-t tartalmazó) szöveg mező.
*/
export const Contact: GlobalConfig = {
slug: 'contact',
admin: {
group: 'Oldalak',
},
fields: [
{
name: 'meta',
type: 'group',
fields: [
{ name: 'title', type: 'text', required: true },
{ name: 'description', type: 'textarea', required: true },
],
},
{
name: 'hero',
type: 'group',
fields: [
{ name: 'title', type: 'text', required: true },
{ name: 'subtitle', type: 'text', required: true },
],
},
{
name: 'form',
type: 'group',
fields: [
{ name: 'title', type: 'text', required: true },
{ name: 'successMessage', type: 'text', required: true },
{ name: 'errorMessage', type: 'text', required: true },
{
name: 'fields',
type: 'group',
fields: [
{
name: 'name',
type: 'group',
fields: [
{ name: 'label', type: 'text', required: true },
{ name: 'placeholder', type: 'text', required: true },
{ name: 'error', type: 'text', required: true },
],
},
{
name: 'email',
type: 'group',
fields: [
{ name: 'label', type: 'text', required: true },
{ name: 'placeholder', type: 'text', required: true },
{ name: 'errorRequired', type: 'text', required: true },
{ name: 'errorInvalid', type: 'text', required: true },
],
},
{
name: 'subject',
type: 'group',
fields: [
{ name: 'label', type: 'text', required: true },
{ name: 'placeholder', type: 'text', required: true },
{ name: 'error', type: 'text', required: true },
],
},
{
name: 'message',
type: 'group',
fields: [
{ name: 'label', type: 'text', required: true },
{ name: 'placeholder', type: 'text', required: true },
{ name: 'errorRequired', type: 'text', required: true },
{ name: 'errorMinLength', type: 'text', required: true },
],
},
{
name: 'gdpr',
type: 'group',
fields: [
{ name: 'label', type: 'textarea', required: true },
{ name: 'error', type: 'text', required: true },
],
},
],
},
{ name: 'submitButton', type: 'text', required: true },
{ name: 'submittingButton', type: 'text', required: true },
],
},
{
name: 'info',
type: 'group',
fields: [
{ name: 'title', type: 'text', required: true },
{
name: 'email',
type: 'group',
fields: [
{ name: 'title', type: 'text', required: true },
{ name: 'responseTime', type: 'text', required: true },
],
},
{
name: 'company',
type: 'group',
fields: [{ name: 'title', type: 'text', required: true }],
},
{
name: 'webmail',
type: 'group',
fields: [
{ name: 'title', type: 'text', required: true },
{ name: 'linkText', type: 'text', required: true },
{ name: 'subtitle', type: 'text', required: true },
],
},
],
},
{
name: 'faq',
type: 'group',
fields: [
{ name: 'title', type: 'text', required: true },
{
name: 'items',
type: 'array',
fields: [
{ name: 'question', type: 'text', required: true },
{ name: 'answer', type: 'textarea', required: true },
],
},
],
},
],
}
+105
View File
@@ -0,0 +1,105 @@
import type { GlobalConfig } from 'payload'
import { stringArrayField } from './fields/stringArray'
/**
* Mirrors HomePageContent (proto/src/content/types.ts) — MITHOME-87.
*
* WHY nincs `partners` mező: a home.partners.items a JSON-ban ma egy
* beágyazott lista, de a Payload oldalon önálló `Partners` collection lesz
* (logó = Media upload reference) — lásd MITHOME-89. Itt szándékosan
* kihagyjuk, nehogy két, egymásnak ellentmondó forrás legyen.
*/
export const Home: GlobalConfig = {
slug: 'home',
admin: {
group: 'Oldalak',
},
fields: [
{
name: 'hero',
type: 'group',
fields: [
{ name: 'title', type: 'text', required: true },
{ name: 'subtitle', type: 'text', required: true },
{ name: 'description', type: 'textarea', required: true },
stringArrayField('trustBullets', 'Trust bullets'),
{
name: 'cta',
type: 'group',
fields: [
{
name: 'primary',
type: 'group',
fields: [
{ name: 'text', type: 'text', required: true },
{ name: 'href', type: 'text', required: true },
{ name: 'external', type: 'checkbox', defaultValue: false },
],
},
{
name: 'secondary',
type: 'group',
fields: [
{ name: 'text', type: 'text', required: true },
{ name: 'href', type: 'text', required: true },
{ name: 'external', type: 'checkbox', defaultValue: false },
],
},
],
},
],
},
{
name: 'about',
type: 'group',
fields: [
{ name: 'title', type: 'text', required: true },
{
name: 'usps',
type: 'array',
fields: [
{ name: 'id', type: 'text', required: true },
{ name: 'title', type: 'text', required: true },
{ name: 'description', type: 'textarea', required: true },
{ name: 'icon', type: 'text', required: true },
],
},
],
},
{
name: 'services',
type: 'group',
fields: [
{ name: 'title', type: 'text', required: true },
{ name: 'subtitle', type: 'text', required: true },
{
name: 'items',
type: 'array',
fields: [
{ name: 'id', type: 'text', required: true },
{ name: 'title', type: 'text', required: true },
{ name: 'description', type: 'textarea', required: true },
{ name: 'icon', type: 'text', required: true },
stringArrayField('features', 'Features'),
{ name: 'ctaText', type: 'text', required: true },
],
},
],
},
{
name: 'cta',
type: 'group',
fields: [
{ name: 'title', type: 'text', required: true },
{ name: 'subtitle', type: 'textarea', required: true },
{ name: 'button', type: 'text', required: true },
],
},
{
name: 'serviceFeatures',
type: 'group',
fields: [{ name: 'title', type: 'text', required: true }],
},
],
}
+82
View File
@@ -0,0 +1,82 @@
import type { GlobalConfig } from 'payload'
import { stringArrayField } from './fields/stringArray'
/** Mirrors ServicesPageContent (proto/src/content/types.ts) — MITHOME-87. */
export const Services: GlobalConfig = {
slug: 'services',
admin: {
group: 'Oldalak',
},
fields: [
{
name: 'meta',
type: 'group',
fields: [
{ name: 'title', type: 'text', required: true },
{ name: 'description', type: 'textarea', required: true },
{ name: 'ogDescription', type: 'textarea', required: true },
],
},
{
name: 'hero',
type: 'group',
fields: [
{ name: 'title', type: 'text', required: true },
{ name: 'subtitle', type: 'text', required: true },
],
},
{
name: 'details',
type: 'group',
fields: [
{ name: 'title', type: 'text', required: true },
{ name: 'subtitle', type: 'text', required: true },
{
name: 'services',
type: 'array',
fields: [
{ name: 'icon', type: 'text', required: true },
{ name: 'title', type: 'text', required: true },
{ name: 'description', type: 'textarea', required: true },
{
name: 'specs',
type: 'group',
fields: [
{ name: 'title', type: 'text', required: true },
stringArrayField('items', 'Items'),
],
},
],
},
],
},
{
name: 'support',
type: 'group',
fields: [
{ name: 'title', type: 'text', required: true },
{ name: 'subtitle', type: 'text', required: true },
{
name: 'channels',
type: 'array',
fields: [
{ name: 'icon', type: 'text', required: true },
{ name: 'title', type: 'text', required: true },
{ name: 'description', type: 'text', required: true },
],
},
],
},
{
name: 'cta',
type: 'group',
fields: [
{ name: 'title', type: 'text', required: true },
{ name: 'subtitle', type: 'textarea', required: true },
{ name: 'primaryButton', type: 'text', required: true },
{ name: 'secondaryButton', type: 'text', required: true },
],
},
],
}
+24
View File
@@ -0,0 +1,24 @@
import type { ArrayField } from 'payload'
/**
* Payload has no native "array of plain strings" field — the closest
* built-in shape is an array of one-field objects. Used for every
* string[] in the old content/types.ts (trustBullets, paragraphs,
* features, spec items, …) so the JSON -> Payload migration script has a
* single, consistent shape to transform into.
*/
export function stringArrayField(name: string, label?: string, required = false): ArrayField {
return {
name,
type: 'array',
label,
fields: [
{
name: 'value',
type: 'text',
required: true,
},
],
required,
}
}