A Contentful App Framework app that replaces freeform text entry for icon
names with a searchable, visual picker over the Lucide
icon set. It's a single entry-field app that adapts to the field it's
assigned to:
- Short Text field → picks a single icon; the field stores its exact
PascalCase name (e.g.
ArrowRight,ChartNoAxesCombined). - Object (JSON) field → an icon + text repeater: an array of
{ "icon": "ArrowRight", "text": "Fast" }rows, reorderable by drag and drop, each row using the same icon picker.
Which one renders is decided at runtime from sdk.field.type — there's
nothing to configure per installation.
Built for a FontAwesome → Lucide migration across several Astro sites that
render icons via @lucide/astro's icons namespace: icons[name]. This app
guarantees the stored name matches that lookup exactly.
Every Lucide framework package (lucide-react, @lucide/astro,
lucide-vue-next, ...) is generated from the same icon set and published in
lockstep — at a given Lucide release, all framework packages ship the same
version number and the same icon names. This app depends on lucide-react
(pinned to an exact version in package.json, not a range) purely to get:
Object.keys(icons)— the canonical list of valid icon names, used for search and validation.- The actual
icons[name]React components — used to render live SVG previews, guaranteed pixel-identical to what@lucide/astrorenders since both packages share the same path data.
When the Astro sites upgrade their @lucide/astro version, bump this app's
lucide-react dependency to the identical version (npm install lucide-react@<version>) so the two icon sets never drift apart. @lucide/astro
itself is never installed or imported here — this app is plain React and never
renders Astro components.
src/App.tsx is the only implemented location (entry-field); it checks
sdk.field.type and renders one of two editors:
- Shows the currently selected icon's live preview + humanized name.
- "Choose icon" / "Change" expands an inline search + virtualized grid of all
~1738 icons (
src/components/IconGridPicker.tsx), filtered as you type. Selecting a tile writes that icon's exact name to the field and collapses the picker. - If the stored value doesn't match any known Lucide icon name (e.g. a typo, or a leftover value from before this app was installed), a warning banner is shown so the editor notices and can fix it — the picker itself can only ever write valid names.
- "Clear icon" empties the field.
- Stores an array of
{ icon: string; text: string }rows in one field. - Each row has a compact icon button (
src/components/IconPickerButton.tsx), a text input, and a delete button, all the same height so the row reads as one aligned control instead of mismatched button/input sizes. Clicking the icon button expands the grid picker in normal document flow below the row (not a floating popover) — Contentful's field-iframe auto-resizer only measures in-flow layout height, so an absolutely-positioned overlay gets clipped and needs scrolling to reach. - At most one row's picker is open at a time (opening one closes any other), and a row with no icon yet opens its picker automatically — on first mount and right after "Add item" — so picking an icon for a new row doesn't need an extra click-to-open step that would otherwise cause a layout shift.
- Rows reorder via real drag and drop (
@dnd-kit, with Forma36'sDragHandle), including keyboard support (focus the handle, arrow keys to move, Enter/Space to lift and drop) — not just up/down buttons. - "Add item" appends a blank row; the field is cleared entirely (not stored
as
[]) once the last row is removed.
No installation parameters and no per-instance config either way — the same app definition can be assigned to any Short Text or Object field in any content type, across any space.
npm install
npm startnpm start creates/updates the app definition (prompts for Contentful login
on first run) and starts the Vite dev server at http://localhost:3000.
npm run create-app-definition(ornpm start) to create the app definition in your organization. When prompted for locations, select only App configuration screen: no / Entry field.- In the app definition's frontend settings, set the URL to
http://localhost:3000for local development. - Install the app into a space (Apps → the app → Install).
- On the content type that has (or will have) a Lucide icon field, assign
this app as the field's appearance/editor:
- Single icon → a Short Text field.
- Icon + text repeater → an Object (JSON object) field.
- Open an entry with that field to use the picker.
For a deployed version, npm run build then npm run upload bundles and
activates the app on Contentful (no separate hosting needed), after which you
can point the app definition's frontend at the hosted bundle instead of
localhost.
npm start/npm run dev— local dev servernpm run build— production build tobuild/npm test/npm run test:ci— unit tests (Vitest + Testing Library)npm run create-app-definition— create/select the app definitionnpm run add-locations— add locations to an existing app definitionnpm run upload— bundle and upload to Contentful