diff --git a/docs/developer-docs/6.x/headless-cms/advanced-filtering.ai.txt b/docs/developer-docs/6.x/headless-cms/advanced-filtering.ai.txt new file mode 100644 index 000000000..fc535a0da --- /dev/null +++ b/docs/developer-docs/6.x/headless-cms/advanced-filtering.ai.txt @@ -0,0 +1,62 @@ +AI Context: Advanced Filtering (advanced-filtering.mdx) + +## Source of Information + +1. `docs/developer-docs/5.x/headless-cms/basics/using-graphql-api-advanced-filtering.mdx` — original v5 article; ported and updated for v6 +2. `docs/developer-docs/6.x/headless-cms/using-webiny-sdk.mdx` — SDK usage patterns (listEntries, where syntax) + +## Key Changes from v5 Article + +1. Raw GraphQL queries replaced with `sdk.cms.listEntries({ where: ... })` — v6 docs use SDK, not raw GraphQL +2. User field filters now use `values.` prefix (e.g. `values.title_contains`, `values.category`) — in v5 fields were at the root +3. System meta fields (`createdOn`, `savedOn`, etc.) do NOT use `values.` prefix — they stay at the root +4. Removed "In the 5.x version of Webiny we introduced..." — no version references +5. Removed link to `date-time-and-identity-meta-fields` reference (no v6 equivalent) — replaced with inline note +6. Removed `meta` block from v5 raw GraphQL examples (not relevant in SDK context) +7. Reference field filter values are strings (entry IDs), not integers — updated examples accordingly + +## Understanding + +### Filter key convention +- User-defined fields: `values.{fieldId}_{operator}` (e.g. `values.title_contains`, `values.price_gte`) +- System fields: `{fieldId}_{operator}` (e.g. `createdOn_between`, `savedOn_gte`) +- Reference fields: filter by ID string, e.g. `"values.category": "some-entry-id"` + +### AND semantics +- All conditions in the array must match +- Multiple root-level filters behave as implicit AND +- Use AND array when you need to repeat the same operator key (not possible at root level) + +### OR semantics +- At least one condition must match +- Multiple filters within a single OR branch are implicitly ANDed together + +### Nesting +- AND and OR can be nested indefinitely +- Deep nesting has performance implications, especially on DynamoDB-only deployments + +### SDK usage +```typescript +sdk.cms.listEntries
({ + modelId: "article", + fields: [...], + where: { + OR: [...], + AND: [...], + "values.someField_operator": value + } +}) +``` + +## Related Documents + +- `using-webiny-sdk.mdx` — full SDK reference for listEntries, filters, pagination +- `graphql-api-overview.mdx` — overview of the three APIs +- `content-models-via-code.mdx` — field IDs used in filter keys come from content model definitions + +## Tone Guidelines + +- Lead each example with a plain-English description of what is being searched for +- Show the "equivalent condition" as a pseudo-SQL string after each query — helps readers verify understanding +- Keep examples practical (article/category/author model) +- No analogies, no marketing language diff --git a/docs/developer-docs/6.x/headless-cms/advanced-filtering.mdx b/docs/developer-docs/6.x/headless-cms/advanced-filtering.mdx new file mode 100644 index 000000000..ff17541e2 --- /dev/null +++ b/docs/developer-docs/6.x/headless-cms/advanced-filtering.mdx @@ -0,0 +1,273 @@ +--- +id: hcms6af01 +title: Advanced Filtering +description: Learn how to use AND and OR conditionals to build complex filters when querying Headless CMS entries. +--- + +import { Alert } from "@/components/Alert"; + + + +- How to use `AND` and `OR` conditionals when filtering entries +- How nested `AND` / `OR` queries work +- How to combine both conditionals in a single query + + + +## Overview + +Both `AND` and `OR` conditionals are arrays of filter conditions. The available filter keys depend on the fields defined on the model you are querying. All user-defined field filters use the `values.` prefix (e.g. `values.title_contains`). + +The examples below use the Webiny SDK. The `where` object is passed directly to `sdk.cms.listEntries()`. + +## The `AND` Conditional + +`AND` requires **all** conditions in the array to match. It behaves the same as placing filters at the root of `where`, with the added ability to repeat the same operator multiple times (which is not possible at the root level). + +### Simple `AND` Examples + +#### Search for entries where the title contains both "headless" and "cms" + +At the root level you can only use `values.title_contains` once. Wrapping conditions in `AND` lets you apply the same filter key multiple times: + +```typescript +const result = await sdk.cms.listEntries
({ + modelId: "article", + fields: ["id", "entryId", "values.title"], + where: { + AND: [{ "values.title_contains": "headless" }, { "values.title_contains": "cms" }] + } +}); +``` + +Equivalent condition: `(values.title contains "headless" AND values.title contains "cms")` + +#### Combine a root-level filter with `AND` + +Root-level filters and `AND` are applied together — all must match: + +```typescript +const result = await sdk.cms.listEntries
({ + modelId: "article", + fields: ["id", "entryId", "values.title"], + where: { + "values.category": "cat-id-1", + AND: [{ "values.title_contains": "headless" }, { "values.title_contains": "cms" }] + } +}); +``` + +Equivalent condition: `(values.category = "cat-id-1" AND values.title contains "headless" AND values.title contains "cms")` + +### Complex `AND` Example + +Search for articles that: + +- are in category `cat-id-1` +- have both "headless" and "cms" in the title +- are authored by one of three authors +- were created in 2022 + +```typescript +const result = await sdk.cms.listEntries
({ + modelId: "article", + fields: ["id", "entryId", "values.title"], + where: { + "values.category": "cat-id-1", + AND: [ + { "values.title_contains": "headless" }, + { "values.title_contains": "cms" }, + { + AND: [ + { "values.author_in": ["author-5", "author-6", "author-7"] }, + { createdOn_between: ["2022-01-01", "2022-12-31"] } + ] + } + ] + } +}); +``` + +Equivalent condition: `(values.category = "cat-id-1" AND values.title contains "headless" AND values.title contains "cms" AND (values.author in [...] AND createdOn between 2022))` + + + +`createdOn` is a system meta field available on all entries. It does not use the `values.` prefix. + + + +The same query can be flattened when there is no need for repeated keys: + +```typescript +const result = await sdk.cms.listEntries
({ + modelId: "article", + fields: ["id", "entryId", "values.title"], + where: { + "values.category": "cat-id-1", + AND: [{ "values.title_contains": "headless" }, { "values.title_contains": "cms" }], + "values.author_in": ["author-5", "author-6", "author-7"], + createdOn_between: ["2022-01-01", "2022-12-31"] + } +}); +``` + +This produces the same result as the nested version above. + +## The `OR` Conditional + +`OR` requires **at least one** condition in the array to match. + +### Simple `OR` Examples + +#### Search for entries where the title contains "headless" or "cms" + +```typescript +const result = await sdk.cms.listEntries
({ + modelId: "article", + fields: ["id", "entryId", "values.title"], + where: { + OR: [{ "values.title_contains": "headless" }, { "values.title_contains": "cms" }] + } +}); +``` + +Equivalent condition: `(values.title contains "headless" OR values.title contains "cms")` + +#### Multiple filters inside one `OR` branch + +When an `OR` branch contains more than one filter, all filters in that branch must match (implicit AND within the branch): + +```typescript +const result = await sdk.cms.listEntries
({ + modelId: "article", + fields: ["id", "entryId", "values.title"], + where: { + OR: [ + { "values.title_contains": "headless", "values.category": "cat-id-1" }, + { "values.title_contains": "cms" } + ] + } +}); +``` + +Equivalent condition: `((values.title contains "headless" AND values.category = "cat-id-1") OR values.title contains "cms")` + +### Complex `OR` Example + +Search for articles that match any of: + +- title contains "headless" +- title contains "cms" +- category is `cat-id-1` or `cat-id-2` + +```typescript +const result = await sdk.cms.listEntries
({ + modelId: "article", + fields: ["id", "entryId", "values.title"], + where: { + OR: [ + { "values.title_contains": "headless" }, + { "values.title_contains": "cms" }, + { + OR: [{ "values.category": "cat-id-1" }, { "values.category": "cat-id-2" }] + } + ] + } +}); +``` + +Equivalent condition: `(values.title contains "headless" OR values.title contains "cms" OR (values.category = "cat-id-1" OR values.category = "cat-id-2"))` + +## Mixing `AND` and `OR` + +### `OR` at the root with nested `AND` and `OR` + +Search for articles that match any of: + +- title contains "headless" +- title contains "cms" +- title contains both "webiny" and "serverless", and was created in January 2021 or January 2022 + +```typescript +const result = await sdk.cms.listEntries
({ + modelId: "article", + fields: ["id", "entryId", "values.title"], + where: { + OR: [ + { "values.title_contains": "headless" }, + { "values.title_contains": "cms" }, + { + AND: [ + { "values.title_contains": "webiny" }, + { "values.title_contains": "serverless" }, + { + OR: [ + { createdOn_between: ["2021-01-01", "2021-01-31"] }, + { createdOn_between: ["2022-01-01", "2022-01-31"] } + ] + } + ] + } + ] + } +}); +``` + +### `AND` at the root with nested `OR` and `AND` + +Search for articles that match all of: + +- title contains "headless" +- title contains "cms" +- title contains "webiny" or "serverless", or was created in January 2021 or January 2022 + +```typescript +const result = await sdk.cms.listEntries
({ + modelId: "article", + fields: ["id", "entryId", "values.title"], + where: { + AND: [ + { "values.title_contains": "headless" }, + { "values.title_contains": "cms" }, + { + OR: [ + { "values.title_contains": "webiny" }, + { "values.title_contains": "serverless" }, + { + AND: [ + { createdOn_between: ["2021-01-01", "2021-01-31"] }, + { createdOn_between: ["2022-01-01", "2022-01-31"] } + ] + } + ] + } + ] + } +}); +``` + +### `OR` and `AND` both at the root level + +Search for articles that: + +- are written by author `author-1` OR are in category `cat-id-2` +- AND have both "headless" and "cms" in the title + +```typescript +const result = await sdk.cms.listEntries
({ + modelId: "article", + fields: ["id", "entryId", "values.title"], + where: { + OR: [{ "values.author": "author-1" }, { "values.category": "cat-id-2" }], + AND: [{ "values.title_contains": "headless" }, { "values.title_contains": "cms" }] + } +}); +``` + +Equivalent condition: `((values.author = "author-1" OR values.category = "cat-id-2") AND (values.title contains "headless" AND values.title contains "cms"))` + + + +`AND` and `OR` conditionals can be nested indefinitely, but deep nesting may result in performance issues — particularly on DynamoDB-only deployments. Keep nesting shallow where possible. + + diff --git a/docs/developer-docs/6.x/headless-cms/date-time-and-identity-meta-fields.ai.txt b/docs/developer-docs/6.x/headless-cms/date-time-and-identity-meta-fields.ai.txt new file mode 100644 index 000000000..15cc17353 --- /dev/null +++ b/docs/developer-docs/6.x/headless-cms/date-time-and-identity-meta-fields.ai.txt @@ -0,0 +1,52 @@ +AI Context: Date/Time and Identity Meta Fields (date-time-and-identity-meta-fields.mdx) + +## Source of Information + +1. `docs/developer-docs/5.x/headless-cms/references/date-time-and-identity-meta-fields.mdx` — original v5 article; ported for v6 +2. `docs/developer-docs/6.x/headless-cms/using-webiny-sdk.mdx` — SDK query patterns (listEntries, fields, where) + +## Key Changes from v5 Article + +1. Removed "Can I use this? Available since v5.39.0" alert — no version references in v6 docs +2. Removed v5 GraphQL query example — replaced with SDK-based example using `sdk.cms.listEntries()` +3. Removed v5 lifecycle events code example (`ContextPlugin`) — v6 uses class-based event handlers; no replacement example added (would bloat this reference article) +4. "Introduction" heading → "Overview" to match v6 article conventions +5. Meta fields do NOT use the `values.` prefix — they are top-level system fields + +## Understanding + +### Two levels of meta fields + +- **Revision-level**: prefixed with `revision` — describe a specific revision; change with each revision operation +- **Entry-level**: no prefix — describe the entry as a whole; `createdOn` is set once and never changes + +### Nullability rules + +- `createdOn`, `savedOn`, `revisionCreatedOn`, `revisionSavedOn`, `createdBy`, `savedBy`, `revisionCreatedBy`, `revisionSavedBy` — never null +- All `modified*`, `*PublishedOn`, `*PublishedBy`, `deleted*`, `restored*` — can be null + +### modified vs saved + +- `savedOn` is updated on every write (including create) +- `modifiedOn` is only set after the first update — null if entry was never updated +- After first update: `modifiedOn === savedOn` always + +### SDK usage + +Meta fields are top-level fields — no `values.` prefix: +```typescript +fields: ["id", "entryId", "createdOn", "lastPublishedOn", "createdBy", "values.title"] +where: { createdOn_between: ["2024-01-01", "2024-12-31"] } +sort: ["createdOn_DESC"] +``` + +## Related Documents + +- `using-webiny-sdk.mdx` — full SDK query/mutation reference +- `migrating-to-webiny.mdx` — links here for meta field migration guidance +- `advanced-filtering.mdx` — filtering by meta fields (createdOn_between, etc.) + +## Tone Guidelines + +- Reference article — concise, table-driven, minimal prose +- FAQ section kept from v5 — it answers the most common confusion (modified vs saved) diff --git a/docs/developer-docs/6.x/headless-cms/date-time-and-identity-meta-fields.mdx b/docs/developer-docs/6.x/headless-cms/date-time-and-identity-meta-fields.mdx new file mode 100644 index 000000000..f2633d8c8 --- /dev/null +++ b/docs/developer-docs/6.x/headless-cms/date-time-and-identity-meta-fields.mdx @@ -0,0 +1,106 @@ +--- +id: hcms6meta +title: Date/Time and Identity Meta Fields +description: Learn about the date/time and identity-related meta fields that are automatically available on all content entries. +--- + +import { Alert } from "@/components/Alert"; + + + +- What are meta fields and where do they come from? +- What is the difference between revision-level and entry-level meta fields? +- What is the difference between `modified` and `saved` fields? + + + +## Overview + +Apart from the fields defined in a content model, all content entries automatically have a set of date/time and identity-related meta fields. For example: + +- `createdOn` — the date/time when an entry was first created +- `lastPublishedOn` — the date/time when an entry was last published +- `revisionCreatedOn` — the date/time when a specific revision was created +- `revisionFirstPublishedOn` — the date/time when a specific revision was first published + +These fields are populated automatically by the system. You can use them when querying entries via the SDK or when implementing event handlers. + +## Revision-level vs. Entry-level Fields + +Meta fields exist at two levels. + +**Revision-level** fields carry the `revision` prefix and describe a specific revision of an entry. They change each time a new revision is created, modified, or published. + +**Entry-level** fields have no prefix and describe the entry as a whole. They reflect the state of the entry across all revisions — for example, `createdOn` is set when the entry is first created and never changes, regardless of how many revisions are added later. + +## Meta Fields Reference + +### Revision-level Meta Fields + +| Field | Description | Can be `null` | +| -------------------------- | --------------------------------------------------- | ------------- | +| `revisionCreatedOn` | When this revision was created. | No | +| `revisionModifiedOn` | When this revision was last modified. | Yes | +| `revisionSavedOn` | When this revision was last saved. | No | +| `revisionFirstPublishedOn` | When this revision was first published. | Yes | +| `revisionLastPublishedOn` | When this revision was last published. | Yes | +| `revisionDeletedOn` | When this revision was moved to the trash. | Yes | +| `revisionRestoredOn` | When this revision was restored from the trash. | Yes | +| `revisionCreatedBy` | The user who created this revision. | No | +| `revisionModifiedBy` | The user who last modified this revision. | Yes | +| `revisionSavedBy` | The user who last saved this revision. | No | +| `revisionFirstPublishedBy` | The user who first published this revision. | Yes | +| `revisionLastPublishedBy` | The user who last published this revision. | Yes | +| `revisionDeletedBy` | The user who moved this revision to the trash. | Yes | +| `revisionRestoredBy` | The user who restored this revision from the trash. | Yes | + +### Entry-level Meta Fields + +| Field | Description | Can be `null` | +| ------------------ | ----------------------------------------------- | ------------- | +| `createdOn` | When the entry was first created. | No | +| `modifiedOn` | When the entry was last modified. | Yes | +| `savedOn` | When the entry was last saved. | No | +| `firstPublishedOn` | When the entry was first published. | Yes | +| `lastPublishedOn` | When the entry was last published. | Yes | +| `deletedOn` | When the entry was moved to the trash. | Yes | +| `restoredOn` | When the entry was restored from the trash. | Yes | +| `createdBy` | The user who created the entry. | No | +| `modifiedBy` | The user who last modified the entry. | Yes | +| `savedBy` | The user who last saved the entry. | No | +| `firstPublishedBy` | The user who first published the entry. | Yes | +| `lastPublishedBy` | The user who last published the entry. | Yes | +| `deletedBy` | The user who moved the entry to the trash. | Yes | +| `restoredBy` | The user who restored the entry from the trash. | Yes | + +## Using Meta Fields + +Meta fields are available as top-level fields (no `values.` prefix) when querying via the SDK: + +```typescript +const result = await sdk.cms.listEntries
({ + modelId: "article", + fields: ["id", "entryId", "createdOn", "lastPublishedOn", "createdBy", "values.title"], + sort: ["createdOn_DESC"] +}); +``` + +You can also filter by them: + +```typescript +const result = await sdk.cms.listEntries
({ + modelId: "article", + fields: ["id", "entryId", "values.title", "createdOn"], + where: { + createdOn_between: ["2024-01-01", "2024-12-31"] + } +}); +``` + +## FAQ + +### What is the difference between `modified` and `saved`? + +`savedOn` / `savedBy` are set on every write — including the initial create. They are never `null`. + +`modifiedOn` / `modifiedBy` are only set after the first update. If an entry has never been updated, `modifiedOn` is `null`. After the first update, `modifiedOn` and `savedOn` will always have the same value. diff --git a/docs/developer-docs/6.x/headless-cms/migrating-to-webiny.ai.txt b/docs/developer-docs/6.x/headless-cms/migrating-to-webiny.ai.txt new file mode 100644 index 000000000..610cb314f --- /dev/null +++ b/docs/developer-docs/6.x/headless-cms/migrating-to-webiny.ai.txt @@ -0,0 +1,61 @@ +AI Context: Migrating to Webiny Headless CMS (migrating-to-webiny.mdx) + +## Source of Information + +1. `https://github.com/webiny/docs.webiny.com/blob/9c1e464e9ff8abdfeae2ab02ad1e223fc7e41614/docs/developer-docs/5.39.x/headless-cms/basics/migrating-to-webiny.mdx` — original v5.39.x article; ported and updated for v6 +2. `docs/developer-docs/6.x/headless-cms/graphql-api-overview.mdx` — v6 API overview (linked) +3. `docs/developer-docs/6.x/headless-cms/content-models-via-code.mdx` — ModelFactory API (linked) +4. `docs/developer-docs/6.x/headless-cms/content-modeling-best-practices.mdx` — modeling guidance (linked) + +## Key Changes from v5 Article + +1. No locale prefix on endpoints — v6 Manage API is `/cms/manage` (not `/cms/manage/{locale}`) +2. Links updated: `content-models-via-code` replaces old `extending/content-models-via-code`; `graphql-api-overview` replaces old `basics/graphql-api` +3. Removed link to `date-time-and-identity-meta-fields` reference article (no v6 equivalent yet) — described inline instead +4. Removed link to `programmatic-file-upload` (no v6 equivalent yet) — described inline +5. Removed link to `lexical-tools` (no v6 equivalent yet) — described inline with the HTML → Lexical approach +6. Removed link to `file-aliases` release note (v5.35.0) — described inline as a feature +7. Removed mention of "SDK" for programmatic access — v6 uses API keys + GraphQL directly +8. Summary section added (not in v5) for quick reference + +## Understanding + +### Migration phases + +1. Plan — define goals, avoid importing old messiness +2. Model — UI editor or ModelFactory code API +3. Migrate — script reads old CMS, writes to Webiny Manage API in batches + +### Entry ID preservation + +Setting `entryId` in the mutation to the original system's ID allows cross-references to resolve automatically without ID mapping or strict ordering. + +### Meta fields + +`createdOn`, `modifiedOn`, `createdBy`, `modifiedBy`, `firstPublishedOn`, `lastPublishedOn` — must be supplied explicitly in the migration script to preserve original timestamps/authorship. Otherwise Webiny sets them to the current time. + +### Rich text + +Export as HTML from old CMS → run through Webiny's HTML-to-Lexical converter → store Lexical JSON via API. Cannot import HTML directly. + +### Assets + +Upload via File Manager API → record new URLs → update references. Use file aliases to preserve original URLs and avoid broken references in rich text or other URL-based fields. + +### DAM replacement + +Replacing the built-in File Manager with a 3rd-party DAM is possible but requires a conversation with the Webiny team. + +## Related Documents + +- `graphql-api-overview.mdx` — API endpoints and structure +- `content-models-via-code.mdx` — ModelFactory API +- `content-modeling-best-practices.mdx` — how to structure models well +- `using-webiny-sdk.mdx` — SDK usage (for querying; migration writes via raw GraphQL) + +## Tone Guidelines + +- Practical and actionable — this is a guide for developers executing a migration +- Acknowledge complexity without dramatizing it +- The "clean up your content structure" framing from the original is worth keeping — it's good advice +- No version comparisons (v5 vs v6); this is a standalone v6 article diff --git a/docs/developer-docs/6.x/headless-cms/migrating-to-webiny.mdx b/docs/developer-docs/6.x/headless-cms/migrating-to-webiny.mdx new file mode 100644 index 000000000..b13b4f605 --- /dev/null +++ b/docs/developer-docs/6.x/headless-cms/migrating-to-webiny.mdx @@ -0,0 +1,132 @@ +--- +id: hcms6mig1 +title: Migrating to Webiny Headless CMS +description: Learn how to approach migrating your existing content to Webiny Headless CMS. +--- + +import { Alert } from "@/components/Alert"; + + + +- How to plan a content migration +- How to model your content in Webiny +- How to migrate content via the GraphQL API +- What to consider for reference fields, meta fields, rich text, and assets + + + +## Overview + +Migrating content from one CMS to another is a complex task that requires planning and preparation. This article walks through the key steps and things to consider when moving your existing content into Webiny Headless CMS. + +The process follows three broad phases: + +1. **Plan the migration** — define your goals and scope before touching anything +2. **Model your content** — create content models and fields in Webiny +3. **Migrate your content** — write and run a migration script that pushes content via the GraphQL API + +## Planning the Migration + +Before starting, be clear about what you want to achieve. Are you migrating to gain better scalability through serverless infrastructure? To improve developer experience? To give editors a better authoring UI? Having a concrete goal lets you measure success after the migration. + +A common mistake is carrying the messiness of the old system into the new one — recreating disorganized models, unused fields, and legacy naming conventions. Treat the migration as an opportunity to clean up your content structure, not just transplant it. + +## Modeling Your Content + +Once you have a clear goal, create the content models and fields in Webiny. There are two ways to do this: + +- **UI editor** — create models interactively in the Admin app +- **Code** — [define models programmatically](/{version}/headless-cms/content-models-via-code); recommended for teams that want models in version control + +If you're migrating from a traditional CMS (Drupal, WordPress, Sitecore, AEM) and are also rewriting your frontend, take the opportunity to improve on the existing data structure. If you're keeping the frontend as-is, staying close to the original structure minimizes the mapping work in your migration script. + +See [Content Modeling Best Practices](/{version}/headless-cms/content-modeling-best-practices) for guidance on structuring models well from the start. + +## Migrating Your Content + +With the models in place, the migration itself is a Node.js script that reads content from your old CMS and writes it into Webiny using the [Webiny SDK](/{version}/headless-cms/using-webiny-sdk). + +To authenticate, create an API key with write permissions in the Admin app under **API Keys** and configure the SDK with it. Each entry is created as a draft — call `publishEntry()` afterward if the entry should be publicly visible. + +A minimal migration loop looks like: + +```typescript +import { sdk } from "./webiny"; // your initialized SDK instance + +for (const article of articlesFromOldCms) { + const result = await sdk.cms.createEntry({ + modelId: "article", + data: { + values: { + title: article.title, + slug: article.slug, + body: article.body + } + }, + fields: ["id", "entryId"] + }); + + if (result.isFail()) { + console.error(`Failed to migrate "${article.title}":`, result.error.message); + continue; + } + + await sdk.cms.publishEntryRevision({ + modelId: "article", + revisionId: result.value.id, + fields: ["id"] + }); +} +``` + +For large datasets, run the migration in batches rather than all at once. This keeps individual runs recoverable and avoids hitting API throughput limits. + +## Additional Considerations + +### Reference Fields + +Most CMS platforms have reference fields that link one content item to another. The typical challenge: you need to create all referenced items first, then map their old IDs to new Webiny IDs before creating the items that reference them. + +Webiny lets you migrate content along with its original IDs from the source system. If you set the `entryId` of each entry to match the ID from your old system, all cross-references resolve automatically — no manual ID mapping required and no strict ordering of migration batches. + +### Date, Time, and Identity Meta Fields + +Webiny automatically sets meta fields (`createdOn`, `modifiedOn`, `createdBy`, `modifiedBy`, `firstPublishedOn`, `lastPublishedOn`) whenever an entry is written. If you don't supply these in your migration script, Webiny will set them to the time of migration — which means your content may appear in a different order on the frontend than it did before. + +To preserve the original timestamps and authorship, include these meta fields explicitly in your mutation input. This ensures ordering, pagination, and "last modified" displays remain consistent with your previous system. + +See [Date/Time and Identity Meta Fields](/{version}/headless-cms/date-time-and-identity-meta-fields) for the full list of available fields. + +### Rich Text Content + +Rich text migration is complex and depends heavily on the source format. Reach out on the [Webiny Slack community](https://webiny.com/slack) to discuss the best approach for your specific situation before starting. + +### Assets (Images, Videos, Files) + +Migrating assets is a separate step from migrating content entries. The general process: + +1. Upload each file programmatically via the File Manager API +2. Record the new Webiny URL returned for each asset +3. Update any content entries that reference those assets with the new URLs + +By default, uploaded assets get a new URL. If your existing content references assets by URL (particularly inside rich text), this will break those references. To avoid this, Webiny supports **file aliases** — you can assign an alias to an uploaded file that preserves the original URL, so existing references continue to work without modification. + + + +If you need to replace the built-in File Manager with a third-party DAM solution, reach out to the Webiny team to discuss that path before starting your migration. + + + +## Summary + +The migration process in brief: + +1. Define a clear goal for the migration +2. Model your content in Webiny — use code-defined models for production projects +3. Write a migration script that reads from your old CMS and writes to the Webiny Manage API +4. Handle reference fields by preserving original entry IDs +5. Include meta fields in your script to preserve timestamps and authorship +6. Upload assets separately and use file aliases to preserve URLs +7. Run in batches; verify each batch before proceeding + +For questions or help with your migration, reach out on the [Webiny Slack community](https://webiny.com/slack). diff --git a/docs/developer-docs/6.x/headless-cms/using-webiny-sdk.mdx b/docs/developer-docs/6.x/headless-cms/using-webiny-sdk.mdx index cd988d96c..ee97426aa 100644 --- a/docs/developer-docs/6.x/headless-cms/using-webiny-sdk.mdx +++ b/docs/developer-docs/6.x/headless-cms/using-webiny-sdk.mdx @@ -201,13 +201,15 @@ if (result.isOk()) { ```typescript const result = await sdk.cms.createEntry({ modelId: "product", - values: { - name: "Laptop", - description: "High-performance laptop", - price: 1299, - sku: "LAP-001" + data: { + values: { + name: "Laptop", + description: "High-performance laptop", + price: 1299, + sku: "LAP-001" + } }, - fields: ["id", "entryId", "values.name", "values.price"] + fields: ["id", "entryId", "createdOn", "values.name", "values.price"] }); if (result.isOk()) { @@ -220,16 +222,18 @@ if (result.isOk()) { - `createEntry()` uses the Manage API - Entry is created in draft status -- Call `publishEntry()` to make it publicly visible +- Call `publishEntryRevision()` to make it publicly visible -### Updating Entries +### Updating Entry Revisions ```typescript -const result = await sdk.cms.updateEntry({ +const result = await sdk.cms.updateEntryRevision({ modelId: "product", - entryId: "abc123", - values: { - price: 1199 // updated price + revisionId: "69b2b114d26d020002c001d2#0001", + data: { + values: { + price: 1199 // updated price + } }, fields: ["id", "entryId", "values.name", "values.price"] }); @@ -239,14 +243,14 @@ if (result.isOk()) { } ``` -**Note:** `updateEntry()` only modifies the specified fields. Other fields remain unchanged. +**Note:** `updateEntryRevision()` only modifies the specified fields. Other fields remain unchanged. -### Publishing Entries +### Publishing Entry Revisions ```typescript -const result = await sdk.cms.publishEntry({ +const result = await sdk.cms.publishEntryRevision({ modelId: "product", - entryId: "abc123", + revisionId: "69b2b114d26d020002c001d2#0001", fields: ["id", "entryId", "values.name", "values.price"] }); @@ -257,12 +261,12 @@ if (result.isOk()) { Publishing makes an entry available via the Read API. Unpublished (draft) entries are only accessible via the Manage API. -### Deleting Entries +### Deleting Entry Revisions ```typescript -const result = await sdk.cms.deleteEntry({ +const result = await sdk.cms.deleteEntryRevision({ modelId: "product", - entryId: "abc123" + revisionId: "69b2b114d26d020002c001d2#0001" }); if (result.isOk()) { diff --git a/docs/developer-docs/6.x/navigation.tsx b/docs/developer-docs/6.x/navigation.tsx index e2ea84de4..c86f0662e 100644 --- a/docs/developer-docs/6.x/navigation.tsx +++ b/docs/developer-docs/6.x/navigation.tsx @@ -51,6 +51,7 @@ export const Navigation = ({ children }: { children: React.ReactNode }) => { + { title={"Content Modeling Best Practices"} /> + + {/**/} {/* */} {/* */} diff --git a/docs/developer-docs/6.x/website-builder/event-handlers.mdx b/docs/developer-docs/6.x/website-builder/event-handlers.mdx index 979014871..2174654a1 100644 --- a/docs/developer-docs/6.x/website-builder/event-handlers.mdx +++ b/docs/developer-docs/6.x/website-builder/event-handlers.mdx @@ -41,20 +41,20 @@ Use `Before` handlers for validation and data transformation. Use `After` handle Every event handler follows the same structure: ```typescript extensions/website-builder/page/eventHandler/update/beforeUpdate.ts -import { PageBeforeUpdateHandler } from "webiny/api/website-builder/page"; +import { PageBeforeUpdateEventHandler } from "webiny/api/website-builder/page"; import { Logger } from "webiny/api/logger"; -class PageBeforeUpdateHandlerImpl implements PageBeforeUpdateHandler.Interface { +class PageBeforeUpdateEventHandlerImpl implements PageBeforeUpdateEventHandler.Interface { public constructor(private logger: Logger.Interface) {} - public async handle(event: PageBeforeUpdateHandler.Event): Promise { + public async handle(event: PageBeforeUpdateEventHandler.Event): Promise { const { original, input } = event.payload; // your logic here } } -export default PageBeforeUpdateHandler.createImplementation({ - implementation: PageBeforeUpdateHandlerImpl, +export default PageBeforeUpdateEventHandler.createImplementation({ + implementation: PageBeforeUpdateEventHandlerImpl, dependencies: [Logger] }); ``` @@ -72,13 +72,13 @@ Key points: This `Before` handler runs before a page is created and throws if the slug does not match a required pattern. ```typescript extensions/website-builder/page/eventHandler/create/beforeCreate.ts -import { PageBeforeCreateHandler } from "webiny/api/website-builder/page"; +import { PageBeforeCreateEventHandler } from "webiny/api/website-builder/page"; import { Logger } from "webiny/api/logger"; -class PageBeforeCreateHandlerImpl implements PageBeforeCreateHandler.Interface { +class PageBeforeCreateEventHandlerImpl implements PageBeforeCreateEventHandler.Interface { public constructor(private logger: Logger.Interface) {} - public async handle(event: PageBeforeCreateHandler.Event): Promise { + public async handle(event: PageBeforeCreateEventHandler.Event): Promise { const { input } = event.payload; const slug = input.path; @@ -95,8 +95,8 @@ class PageBeforeCreateHandlerImpl implements PageBeforeCreateHandler.Interface { } } -export default PageBeforeCreateHandler.createImplementation({ - implementation: PageBeforeCreateHandlerImpl, +export default PageBeforeCreateEventHandler.createImplementation({ + implementation: PageBeforeCreateEventHandlerImpl, dependencies: [Logger] }); ``` @@ -106,13 +106,13 @@ export default PageBeforeCreateHandler.createImplementation({ This `After` handler fires after a page is published and sends the page data to an external webhook. ```typescript extensions/website-builder/page/eventHandler/publish/afterPublish.ts -import { PageAfterPublishHandler } from "webiny/api/website-builder/page"; +import { PageAfterPublishEventHandler } from "webiny/api/website-builder/page"; import { Logger } from "webiny/api/logger"; -class PageAfterPublishHandlerImpl implements PageAfterPublishHandler.Interface { +class PageAfterPublishEventHandlerImpl implements PageAfterPublishEventHandler.Interface { public constructor(private logger: Logger.Interface) {} - public async handle(event: PageAfterPublishHandler.Event): Promise { + public async handle(event: PageAfterPublishEventHandler.Event): Promise { const { page } = event.payload; this.logger.info(`Page published: ${page.id} (${page.path})`); @@ -129,8 +129,8 @@ class PageAfterPublishHandlerImpl implements PageAfterPublishHandler.Interface { } } -export default PageAfterPublishHandler.createImplementation({ - implementation: PageAfterPublishHandlerImpl, +export default PageAfterPublishEventHandler.createImplementation({ + implementation: PageAfterPublishEventHandlerImpl, dependencies: [Logger] }); ``` @@ -168,5 +168,5 @@ If a `Before` handler throws an error, the operation is aborted and subsequent h See the reference pages for the full list of available handlers: -- [Page Event Handlers](/{version}/reference/api/website-builder/page#page-after-create-handler) — create, update, delete, publish, unpublish, duplicate, move, and revision creation +- [Page Event Handlers](/{version}/reference/api/website-builder/page#page-after-create-event-handler) — create, update, delete, publish, unpublish, duplicate, move, and revision creation - [Redirect Event Handlers](/{version}/reference/api/website-builder/redirect#redirect-after-create-handler) — create, update, delete, and move