Wix-Data List Field Mapping
Design Log #09 - Wix Data List Field Mapping
Written for AI agents. See Log Methodology Note below for details.
Background
The wix-data plugin generates contracts and components for Wix Data collections. The collection-list.ts component renders list pages (index and category pages) with items from a collection.
Currently:
- The list contract generator includes ALL non-system, non-reference, non-richContent fields in the contract
- The component's
transformDataFieldsfunction spreads all data fields into the view state without specific mapping - There's no way to configure which fields appear in list views vs item detail pages
Problem
The materialized contract list-recipes-list.jay-contract includes too many fields:
# Current: ALL fields included
- { tag: title, type: data, dataType: string }
- { tag: servings, type: data, dataType: string }
- { tag: description, type: data, dataType: string }
- { tag: shortDescription, type: data, dataType: string }
- { tag: preparationTime, type: data, dataType: string }
- { tag: order, type: data, dataType: number }
- { tag: link-recipes-title, type: data, dataType: string }
- { tag: link-recipes-all, type: data, dataType: string }
- tag: image ...
- tag: ImageBanner ...
For a list page, we typically only need:
- Title (for display)
- URL/slug (for linking)
- One image (for thumbnail)
- Maybe 1-2 preview fields (e.g., short description)
Questions and Answers
Q1: Should we require explicit configuration or provide smart defaults?
A: Both. Provide sensible defaults that work out-of-the-box, but allow override via wix-data.yaml.
Q2: What should the default field mapping be?
A: Priority-based detection:
title: Field named "title", "name", or first TEXT field marked as display fieldurl: Auto-generated from pathPrefix + slugField (already done)image: First IMAGE field in the schemadescription: Field named "shortDescription", "description", or "summary" (first match)
Q3: Should the mapping be for contract generation only or also used at runtime?
A: Both. The contract generator uses it to determine which tags to include. The component uses the same mapping to extract data from Wix Data API responses and map to the view state.
Q4: How do we handle the case where a collection doesn't have expected fields?
A: Skip missing fields - the contract won't include tags for fields that don't exist.
Design
1. Configuration Schema Extension
Add listFields to CollectionConfig in types.ts:
export interface ListFieldsConfig {
/** Field to use as title. Default: auto-detect "title" or "name" */
title?: string;
/** Field to use as image thumbnail. Default: first IMAGE field */
image?: string;
/** Field to use as description/excerpt. Default: "shortDescription" or "description" */
description?: string;
/** Additional fields to include in list view */
additional?: string[];
}
export interface CollectionConfig {
// ... existing fields ...
/**
* Field mapping for list/card views.
* If not specified, smart defaults are used.
*/
listFields?: ListFieldsConfig;
}
2. YAML Configuration Example
collections:
- collectionId: Recipes
visible: true
pathPrefix: /recipes
slugField: title
components:
itemPage: true
indexPage: true
listFields:
title: title
image: image
description: shortDescription
# No 'additional' - keep it minimal
3. Default Field Detection
Add to processed-schema.ts:
export interface ListFieldMapping {
titleField?: string;
imageField?: string;
descriptionField?: string;
additionalFields: string[];
}
export function resolveListFieldMapping(
schema: ProcessedSchema,
config?: ListFieldsConfig,
): ListFieldMapping {
const fields = schema.fields;
// Title: explicit > "title" > "name" > first simple text field
const titleField =
config?.title ||
findField(fields, 'title') ||
findField(fields, 'name') ||
fields.find((f) => f.category === 'simple' && f.jayType === 'string')?.key;
// Image: explicit > first image field
const imageField = config?.image || fields.find((f) => f.category === 'image')?.key;
// Description: explicit > "shortDescription" > "description" > "summary"
const descriptionField =
config?.description ||
findField(fields, 'shortDescription') ||
findField(fields, 'description') ||
findField(fields, 'summary');
return {
titleField,
imageField,
descriptionField,
additionalFields: config?.additional || [],
};
}
4. Contract Generator Update
Update list-contract-generator.ts to use the mapping:
function buildCardTags(schema: ProcessedSchema, indent = 6): string[] {
const mapping = resolveListFieldMapping(schema, schema.config.listFields);
const cardTags: string[] = [
dataTag('_id', 'string', undefined, indent),
dataTag('url', 'string', 'Full URL to item page', indent),
interactiveTag('itemLink', 'HTMLAnchorElement', undefined, indent),
];
// Add mapped fields
if (mapping.titleField) {
const field = schema.fields.find((f) => f.key === mapping.titleField);
if (field) cardTags.push(dataTag('title', field.jayType, 'Title', indent));
}
if (mapping.imageField) {
cardTags.push(imageSubContract('image', 'Thumbnail', indent));
}
if (mapping.descriptionField) {
cardTags.push(dataTag('description', 'string', 'Description', indent));
}
// Add any additional fields
mapping.additionalFields.forEach((fieldKey) => {
const field = schema.fields.find((f) => f.key === fieldKey);
if (field) {
const tag = fieldToTag(field, indent);
if (tag) cardTags.push(tag);
}
});
return cardTags;
}
5. Component Mapping Update
Update collection-list.ts to map using the same config.
Important: Wix image URLs come in a special protocol format:
wix:image://v1/0e4eed_92220e22e7604162a3f16d11a765c00e~mv2.jpg/Devanco-June-2021-001.jpg#originWidth=6484&originHeight=4115
These must be transformed to public URLs using formatWixMediaUrl from @jay-framework/wix-utils:
https://static.wixstatic.com/media/0e4eed_92220e22e7604162a3f16d11a765c00e~mv2.jpg
import { formatWixMediaUrl, parseWixMediaUrl } from '@jay-framework/wix-utils';
interface MappedListItem {
_id: string;
url: string;
title?: string;
image?: { url: string; altText: string; width?: number; height?: number };
description?: string;
[key: string]: unknown; // additional fields
}
/**
* Transform a Wix image field value to view state format.
* Handles wix:image:// protocol URLs and extracts dimensions.
*/
function mapImageField(imgValue: unknown): MappedListItem['image'] | undefined {
if (!imgValue) return undefined;
// Handle string URL (wix:image:// or http(s)://)
if (typeof imgValue === 'string') {
const parsed = parseWixMediaUrl(imgValue);
return {
url: formatWixMediaUrl('', imgValue),
altText: '',
width: parsed?.originWidth,
height: parsed?.originHeight,
};
}
// Handle object with src/url property
if (typeof imgValue === 'object' && imgValue !== null) {
const img = imgValue as Record<string, unknown>;
const srcUrl = (img.src || img.url || '') as string;
const parsed = parseWixMediaUrl(srcUrl);
return {
url: formatWixMediaUrl('', srcUrl),
altText: (img.alt as string) || '',
width: parsed?.originWidth ?? (img.width as number),
height: parsed?.originHeight ?? (img.height as number),
};
}
return undefined;
}
function mapItemToViewState(
item: any,
pathPrefix: string,
slugField: string,
mapping: ListFieldMapping,
): MappedListItem {
const data = item.data || {};
const mapped: MappedListItem = {
_id: item._id!,
url: `${pathPrefix}/${data[slugField] || item._id}`,
};
// Map title first (needed for image alt fallback)
if (mapping.titleField && data[mapping.titleField] != null) {
mapped.title = String(data[mapping.titleField]);
}
// Map image (with wix:// URL transformation, use title as alt fallback)
if (mapping.imageField) {
mapped.image = mapImageField(data[mapping.imageField]);
if (mapped.image && !mapped.image.altText && mapped.title) {
mapped.image.altText = mapped.title;
}
}
// Map description
if (mapping.descriptionField && data[mapping.descriptionField] != null) {
mapped.description = String(data[mapping.descriptionField]);
}
// Map additional fields
mapping.additionalFields.forEach((key) => {
if (data[key] != null) {
mapped[key] = data[key];
}
});
return mapped;
}
6. Carry Forward the Mapping
The mapping needs to be available in both slow and fast phases. Options:
- Option A: Include mapping in carryForward (serialize it)
- Option B: Re-resolve mapping from config in each phase
Going with Option A - include in carryForward for consistency and efficiency.
Implementation Plan
Phase 1: Types and Config Parsing
- Add
ListFieldsConfigtotypes.ts - Update YAML parsing in
config-loader.tsto handlelistFields
Phase 2: Field Resolution
- Add
ListFieldMappinginterface toprocessed-schema.ts - Implement
resolveListFieldMapping()function - Add mapping to
ProcessedSchemaoutput
Phase 3: Contract Generator
- Update
buildCardTags()inlist-contract-generator.tsto use mapping - Remove
isCardFieldfilter (no longer needed - we use explicit mapping)
Phase 4: Component Mapping
- Add
ListFieldMappingtoListSlowCarryForwardandListFastCarryForward - Import
formatWixMediaUrl,parseWixMediaUrlfrom@jay-framework/wix-utils - Create
mapImageField()helper for wix:// URL transformation - Create
mapItemToViewState()helper - Update
renderSlowlyChanging()to use mapping - Update
ListInteractiveto use mapping for loaded items
Phase 5: Test and Update Examples
- Update
wix-data.yamlwithlistFieldsfor Recipes collection - Regenerate contracts
- Verify reduced contract size
- Test list page rendering
Expected Result
Contract changes from:
# Before: 12+ fields per item
- { tag: title, ... }
- { tag: servings, ... }
- { tag: description, ... }
- { tag: shortDescription, ... }
- { tag: preparationTime, ... }
- { tag: order, ... }
- { tag: link-recipes-title, ... }
- { tag: link-recipes-all, ... }
- tag: image (sub-contract)
- tag: ImageBanner (sub-contract)
To:
# After: 4 core fields
- { tag: _id, ... }
- { tag: url, ... }
- { tag: itemLink, ... }
- { tag: title, ... }
- tag: image (sub-contract)
- { tag: description, ... }
Trade-offs
| Decision | Trade-off |
|---|---|
| Smart defaults | Magic behavior - may not always pick the right field |
Normalized output (always title, image, description) |
Original field names lost - but consistent template authoring |
Config in wix-data.yaml |
Requires regeneration if changed |
| Mapping in carryForward | Larger serialized state, but consistent behavior |
Verification Criteria
- ✅ Recipes list contract only has 4-5 fields per item (was 12+)
- ✅ Default detection works for collections without explicit
listFields - ✅ Explicit
listFieldsconfig overrides defaults - ✅
additionalfields appear in contract and view state - ✅ Image URLs transformed from
wix:image://tohttps://static.wixstatic.com/media/ - ✅ Image dimensions extracted from URL hash params (
originWidth,originHeight) - ✅ List page renders correctly with mapped data
Design Revision: Generic Field Whitelist per Component
After initial implementation, revised to use a more generic approach: field whitelist per component type instead of predefined semantic mapping (title, image, description).
Rationale
- More generic - applies to any component type (list, item, table, card)
- No field renaming - keeps original field names, no "title → title" mapping confusion
- Cleaner config - component configuration in one place
- Extensible - easy to add new component types
Revised Configuration Schema
Before:
components:
itemPage: true
indexPage: true
categoryPage: true
listFields:
title: title
image: image
description: shortDescription
After:
components:
itemPage: true # all fields (default)
indexPage:
fields: [title, image, shortDescription]
categoryPage:
fields: [title, image, shortDescription]
Type Changes
// Before
export interface ComponentsConfig {
itemPage?: boolean;
indexPage?: boolean;
categoryPage?: boolean;
tableWidget?: boolean;
cardWidget?: boolean;
}
// After
export type ComponentConfig =
| boolean
| {
/** Whitelist of field keys to include. If omitted, all fields included. */
fields?: string[];
};
export interface ComponentsConfig {
itemPage?: ComponentConfig;
indexPage?: ComponentConfig;
categoryPage?: ComponentConfig;
tableWidget?: ComponentConfig;
cardWidget?: ComponentConfig;
}
Helper Function
/**
* Get field whitelist for a component type.
* Returns undefined if all fields should be included.
*/
export function getComponentFields(config: ComponentConfig | undefined): string[] | undefined {
if (config === undefined || config === false) return undefined;
if (config === true) return undefined; // all fields
return config.fields;
}
Contract Generator Changes
function buildCardTags(schema: ProcessedSchema, indent = 6): string[] {
const whitelist = getComponentFields(schema.config.components.indexPage);
const cardTags: string[] = [
dataTag('_id', 'string', undefined, indent),
dataTag('url', 'string', 'Full URL to item page', indent),
interactiveTag('itemLink', 'HTMLAnchorElement', undefined, indent),
];
// Add whitelisted fields (or all if no whitelist)
const fieldsToInclude = whitelist
? schema.fields.filter((f) => whitelist.includes(f.key))
: schema.fields.filter(isCardField);
fieldsToInclude.forEach((field) => {
const tag = fieldToTag(field, indent);
if (tag) cardTags.push(tag);
});
return cardTags;
}
Component Changes
function mapItemToViewState(
item: { _id?: string; data?: Record<string, unknown> },
pathPrefix: string,
slugField: string,
whitelist: string[] | undefined,
titleField?: string, // For image alt fallback
): Record<string, unknown> {
const data = item.data || {};
const mapped: Record<string, unknown> = {
_id: item._id!,
url: `${pathPrefix}/${data[slugField] || item._id}`,
};
// Get title for alt text fallback
const title = titleField ? String(data[titleField] || '') : '';
// Include whitelisted fields (or all non-system fields if no whitelist)
const keysToInclude = whitelist || Object.keys(data).filter((k) => !k.startsWith('_'));
keysToInclude.forEach((key) => {
const value = data[key];
if (value == null) return;
// Transform image fields
if (isImageValue(value)) {
mapped[key] = mapImageField(value, title);
} else {
mapped[key] = value;
}
});
return mapped;
}
Benefits
| Aspect | Semantic Mapping | Field Whitelist |
|---|---|---|
| Field names | Normalized (title, image) | Original (title, shortDescription) |
| Config complexity | More options | Simpler (just list fields) |
| Template authoring | Consistent names | Must know actual field names |
| Flexibility | Fixed semantic slots | Any fields |
| Defaults | Smart detection | All fields or explicit list |
Implementation Changes Required
- types.ts - Change
ComponentsConfigto useComponentConfigunion type - processed-schema.ts - Remove
ListFieldMapping, addgetComponentFields() - list-contract-generator.ts - Use whitelist instead of mapping
- collection-list.ts - Use whitelist for item mapping
- wix-data.yaml - Update config format
Log Methodology Note
Note: These design logs are written primarily for AI agents as part of the Design Log methodology and made accessible here for human readers. The language and structure are optimized for machine consumption — expect precise, specification-style prose rather than narrative documentation.