datocms-plugin-project-exporter
v1.3.0
Published
A plugin that allows you to export records and assets from your project directly from your browser
Readme
DatoCMS Project Exporter Plugin
A powerful DatoCMS plugin that allows you to export your project's records and assets directly from the dashboard. Whether you need a complete backup, a specific set of data for analysis, or just a single record, Project Exporter handles it with support for multiple popular formats.

Features
- Multiple Export Formats: Export your data in JSON, CSV, XML, or XLSX.
- Bulk Record Export: Download all records in your project.
- Filtered Exports:
- By Model: Select specific models to export records from.
- By Text Search: Export records matching a specific search query.
- Import-Friendly JSON Envelope: JSON exports include
manifest, schema ID/API-key maps,projectConfiguration(site/settings resources), and areferenceIndexfor records/uploads/blocks/structured-text links. - Chunked Asset Export: Assets are split into conservative multi-ZIP chunks to reduce browser memory pressure.
- Asset Mapping Manifests: Every ZIP includes
manifest.jsonand deterministic filename conventions for easier re-import. - Single Record Export: JSON single-record exports use the same envelope shape as bulk JSON exports.
Installation
- Go to your DatoCMS project dashboard.
- Navigate to Settings > Plugins.
- Click the Plus icon to add a new plugin.
- Search for Project Exporter or install it manually using the package name
datocms-plugin-project-exporter.
Configuration
Once installed, you can configure the default export format in the plugin settings:
- Navigate to Settings > Plugins.
- Click on Project Exporter.
- In the configuration area (or via the plugin's main page), you can select your preferred default format:
JSONCSVXMLXLSX
Note: You can also change the format on-the-fly when performing an export.
Usage
Exporting Records (Bulk)
To perform bulk exports, navigate to the plugin's configuration screen (typically found under Settings > Plugins > Project Exporter > Config Screen or the dedicated plugin page if applicable).
- Select Format: Choose between JSON, CSV, XML, or XLSX from the dropdown menu.
- Filter by Model: Use the dropdown to select one or multiple models. Click "Download records from selected models" to export only those records.
- Filter by Text: Enter a search term in the text field. Click "Download records from text query" to export matches.
- Export All: Click "Download all records" to export everything.
Exporting Assets
- On the main plugin screen, click the Download all assets button.
- The plugin scans assets and creates one or more ZIP files using conservative limits.
- Every ZIP includes:
- Asset binaries
manifest.jsonwith source upload IDs and metadata
- ZIP entry filenames follow:
u_<sourceUploadId>__<sanitizedOriginalFilename>
Exporting a Single Record
When editing a specific record:
- Look for the Record Downloader panel in the right sidebar.
- Click Download this record.
- The record will be downloaded in the format currently selected in the plugin's global configuration.
Development
This plugin is built with React and the DatoCMS Plugin SDK. To contribute or modify the plugin locally:
- Clone the repository:
git clone https://github.com/marcelofinamorvieira/datocms-plugin-project-exporter.git - Install dependencies:
npm install # or pnpm install - Start the development server:
npm start - Follow the DatoCMS Plugin SDK documentation to link your local server to a DatoCMS project for testing.
Tech Stack
- Framework: React, TypeScript
- DatoCMS:
datocms-plugin-sdk,datocms-react-ui - Utilities:
json-2-csv(CSV generation)jsontoxml(XML generation)exceljs(Excel generation)jszip(Asset zipping)
Export Contracts (Import-Oriented)
This section documents the concrete output contract of this plugin so you can build an importer with predictable behavior.
Record Export File Names
All record exports download with this filename pattern:
allDatocmsRecords<ISO_TIMESTAMP>.<extension>Examples:
allDatocmsRecords2026-02-10T18:12:33.271Z.jsonallDatocmsRecords2026-02-10T18:12:33.271Z.csv
Notes:
JSONexports contain the full envelope described below (manifest,schema,projectConfiguration,referenceIndex, etc.).CSV,XML, andXLSXexports contain only the exported record data (no manifest/schema/reference index).
Asset ZIP File Names and Entry Names
Assets are split into one or more ZIP files using this naming template:
allAssets.part-<PPP>-of-<TTT>.<timestamp>.zipPPPandTTTare zero-padded to 3 digits (001,012, etc.).timestampis generated fromnew Date().toISOString().replace(/:/g, '-').- Example:
allAssets.part-003-of-012.2026-02-09T12-00-00.000Z.zip.
Each binary asset inside a ZIP uses:
u_<sourceUploadId>__<sanitizedOriginalFilename>Sanitization rules:
sourceUploadId:- trim
- replace spaces with
_ - replace characters not matching
[A-Za-z0-9_-]with- - collapse repeated
- - fallback to
unknownif empty
originalFilename:- trim
- replace spaces with
_ - replace characters not matching
[A-Za-z0-9._-]with- - collapse repeated
- - remove leading dots
- fallback to
fileif empty
Example:
- Source:
upload:123+Hero Image (Final).png - ZIP entry:
u_upload-123__Hero_Image_-Final-.png
manifest.json Inside Each Asset ZIP
Every ZIP contains a manifest.json at the root with this structure:
type AssetZipManifest = {
manifestVersion: "2.0.0";
generatedAt: string; // ISO timestamp
chunk: {
index: number; // 1-based chunk index
totalChunks: number;
filename: string; // actual ZIP filename for this chunk
assetCount: number;
estimatedBytes: number; // conservative estimate used for chunking
};
conventions: {
zipEntryName: "u_<sourceUploadId>__<sanitizedOriginalFilename>";
zipFilename: "allAssets.part-{part}-of-{total}.{timestamp}.zip";
};
limits: {
maxZipBytes: 157286400; // 150 * 1024 * 1024
maxFilesPerZip: 100;
sizeSafetyFactor: 1.2;
};
assets: AssetManifestEntry[];
};
type AssetManifestEntry = {
sourceUploadId: string;
zipEntryName: string;
originalFilename: string;
size: number | null;
mimeType: string | null;
width: number | null;
height: number | null;
checksum: string | null; // upload md5
url: string | null;
path: string | null;
metadata: {
// only included if present on the source upload:
default_field_metadata?: unknown;
field_metadata?: unknown;
custom_data?: unknown;
tags?: unknown;
notes?: unknown;
author?: unknown;
copyright?: unknown;
focal_point?: unknown;
is_image?: unknown;
blurhash?: unknown;
};
};JSON Record Envelope (.json record exports)
JSON record exports (bulk and single-record) use this envelope:
type RecordExportEnvelope = {
manifest: {
exportVersion: "2.1.0";
pluginVersion: string;
exportedAt: string; // ISO timestamp
sourceProjectId: string | null;
sourceEnvironment: string | null;
defaultLocale: string | null;
locales: string[];
scope: "bulk" | "single-record";
filtersUsed: {
modelIDs?: string[];
textQuery?: string;
};
configurationExport: {
includedResources: (
| "site"
| "scheduledPublications"
| "scheduledUnpublishings"
| "fieldsets"
| "menuItems"
| "schemaMenuItems"
| "modelFilters"
| "plugins"
| "workflows"
| "roles"
| "webhooks"
| "buildTriggers"
)[];
warningCount: number;
};
};
schema: {
itemTypes: Record<string, unknown>[]; // raw itemTypes from CMA
fields: Record<string, unknown>[]; // raw fields from CMA
itemTypeIdToApiKey: Record<string, string>; // model id -> api_key
fieldIdToApiKey: Record<string, string>; // field id -> api_key
fieldsByItemType: Record<
string,
{
fieldId: string;
apiKey: string;
fieldType: string;
localized: boolean;
}[]
>;
};
projectConfiguration: {
site: Record<string, unknown> | null; // full Site payload from CMA
scheduledPublications: {
itemId: string;
itemTypeId: string | null;
scheduledAt: string;
currentVersion: string | null;
}[];
scheduledUnpublishings: {
itemId: string;
itemTypeId: string | null;
scheduledAt: string;
currentVersion: string | null;
}[];
fieldsets: Record<string, unknown>[];
menuItems: Record<string, unknown>[];
schemaMenuItems: Record<string, unknown>[];
modelFilters: Record<string, unknown>[];
plugins: Record<string, unknown>[];
workflows: Record<string, unknown>[];
roles: Record<string, unknown>[];
webhooks: Record<string, unknown>[];
buildTriggers: Record<string, unknown>[];
warnings: {
resource: string;
message: string;
}[];
};
records: Record<string, unknown>[]; // raw records from CMA iterator
referenceIndex: {
recordRefs: RecordReference[];
uploadRefs: UploadReference[];
structuredTextRefs: StructuredTextReference[];
blockRefs: BlockReference[];
};
assetPackageInfo: {
packageVersion: "2.0.0";
zipNamingConvention: "allAssets.part-{part}-of-{total}.{timestamp}.zip";
zipEntryNamingConvention:
"u_<sourceUploadId>__<sanitizedOriginalFilename>";
manifestFilename: "manifest.json";
chunkingDefaults: {
maxZipBytes: 157286400;
maxFilesPerZip: 100;
sizeSafetyFactor: 1.2;
};
lastAssetExportSnapshot: LastAssetExportSnapshot | null;
};
};
type LastAssetExportSnapshot = {
packageVersion: string;
generatedAt: string;
chunkFilenames: string[];
totalChunks: number;
totalAssets: number;
maxZipBytes: number;
maxFilesPerZip: number;
sizeSafetyFactor: number;
};Where references are:
type BaseRef = {
recordSourceId: string;
sourceBlockId: string | null;
fieldApiKey: string;
locale: string | null;
jsonPath: string;
};
type RecordReference = BaseRef & {
targetSourceId: string;
kind: string;
};
type UploadReference = BaseRef & {
targetSourceId: string;
kind: string;
};
type StructuredTextReference = BaseRef & {
targetSourceId: string;
targetType: "record" | "block";
kind: "link" | "block";
};
type BlockReference = BaseRef & {
blockSourceId: string;
blockModelId: string | null;
parentBlockSourceId: string | null;
kind: string;
synthetic: boolean;
};manifest Behavior Details
manifest.exportVersionis currently fixed at2.1.0.manifest.pluginVersioncomes from:REACT_APP_PLUGIN_VERSION, elsenpm_package_version, else- fallback
"1.0.0".
manifest.scope:bulkfor config-screen bulk exportssingle-recordfor sidebar single-record export
manifest.filtersUsed:- bulk export can include
modelIDsand/ortextQuery - single-record JSON export uses
{}.
- bulk export can include
- Site fields (
sourceProjectId,sourceEnvironment,defaultLocale,locales) are derived fromprojectConfiguration.site; if site fetch fails they fall back tonull/[]. manifest.configurationExport.includedResourceslists all configuration collections shipped inprojectConfiguration.manifest.configurationExport.warningCountis the number of non-fatal resource fetch failures collected inprojectConfiguration.warnings.
Project Configuration Semantics
projectConfiguration.siteis the full rawsiteresource payload.scheduledPublications/scheduledUnpublishingsare derived from exported records (meta.publication_scheduled_at/meta.unpublishing_scheduled_at) because CMA does not expose a global list endpoint for those resources.fieldsetsare collected across all exported models/blocks.modelFilterscomes from CMAitem_type_filterresources.- All arrays in
projectConfigurationare present even when empty. projectConfiguration.warningsstores per-resource fetch failures without aborting the export.
Reference Index Semantics
jsonPathalways points to the location inrecordswhere the relationship was found.- Path root starts at
$.records[index]. - Localized fields include locale in both:
jsonPath(for example$.records[0].content.en.document...)localeproperty ("en","pt", etc.).
sourceBlockIdisnullfor top-level record fields and set for relationships found inside blocks.- Deduplication is applied; each unique
(context + target + kind)is emitted once.
kind values currently emitted by this version:
recordRefs.kind:link,links,structured_text_itemLink,structured_text_inlineItem,structured_text_links_array,unknown_itemuploadRefs.kind:file,gallery,unknown_uploadblockRefs.kind:modular_content,single_block,nested_block,structured_text_block,structured_text_blocks_array
When a block-like object has no id, the exporter creates a synthetic block ID:
synthetic::<recordSourceId>::<jsonPath>This appears in blockRefs.blockSourceId with synthetic: true.
Import Guidance (Recommended Order)
If you are writing an importer, this order is reliable for the current contract:
- Read
schemamaps (itemTypeIdToApiKey,fieldIdToApiKey,fieldsByItemType) andprojectConfiguration. - Recreate project-level configuration resources as needed (
site, menu/schema menu items, workflows, roles, webhooks, build triggers, etc.). - Import assets from ZIPs and build
sourceUploadId -> targetUploadIdmapping from each ZIPmanifest.json. - Create records first (without resolving all links yet), preserving a
sourceRecordId -> targetRecordIdmap. - Reconcile links/uploads/blocks using
referenceIndex. - Apply structured text links and block references in a final pass using
structuredTextRefsandblockRefs.
License
This project is licensed under the MIT License.
