Building a Custom ULID Widget for Decap CMS: A Complete Implementation Guide

Building a Custom ULID Widget for Decap CMS: A Complete Implementation Guide

Learn how to create a custom ULID widget for Decap CMS that automatically generates unique identifiers for content entries. Complete implementation with error handling and best practices.

When building content management systems, having unique, sortable identifiers is crucial for data integrity and performance. While UUIDs are popular, ULIDs (Universally Unique Lexicographically Sortable Identifiers) offer better performance and human-readable timestamps. In this comprehensive guide, I'll show you how to build a custom ULID widget for Decap CMS that automatically generates these identifiers.

Why ULIDs Matter

ULIDs combine the best of both worlds: they're unique like UUIDs but also lexicographically sortable, making them perfect for database indexing and chronological ordering. Unlike UUIDs, ULIDs are:

  • Sortable: Naturally ordered by creation time
  • URL-safe: No special characters
  • Human-readable: Include timestamp information
  • Compact: 26 characters vs 36 for UUIDs

The Challenge with Decap CMS

Decap CMS (formerly Netlify CMS) is a powerful headless CMS, but it doesn't include a built-in ULID widget. The default approach often involves using timestamps or manual input, which can lead to:

  • Duplicate IDs
  • Non-sortable identifiers
  • Manual maintenance overhead
  • Inconsistent data patterns

Complete Implementation

Let's build a robust ULID widget that integrates seamlessly with Decap CMS 3.8.3.

1. ULID Generation Algorithm

First, we need a reliable ULID implementation:

// ULID implementation
class ULID {
static encodeTime(time, len = 10) {
const alphabet = '0123456789ABCDEFGHJKMNPQRSTVWXYZ';
let str = '';
while (len > 0) {
str = alphabet[time % 32] + str;
time = Math.floor(time / 32);
len--;
}
return str;
}
static encodeRandom(len = 16) {
const alphabet = '0123456789ABCDEFGHJKMNPQRSTVWXYZ';
let str = '';
for (let i = 0; i < len; i++) {
str += alphabet[Math.floor(Math.random() * 32)];
}
return str;
}
static generate() {
const timestamp = Date.now();
const timePart = this.encodeTime(timestamp);
const randomPart = this.encodeRandom();
return timePart + randomPart;
}
}

2. Decap CMS Widget Components

The key to success is using the correct syntax for Decap CMS 3.8.3:

// Control component for the admin interface
const UlidControl = createClass({
componentDidMount: function() {
// Generate ULID if no value exists
if (!this.props.value) {
const newValue = ULID.generate();
this.props.onChange(newValue);
}
},
handleGenerateNew: function() {
const newValue = ULID.generate();
this.props.onChange(newValue);
},
render: function() {
const value = this.props.value || ULID.generate();
return h('div', {
className: 'ulid-widget',
style: { marginBottom: '16px' }
}, [
h('input', {
type: 'text',
value: value,
readOnly: true,
className: 'ulid-input',
style: {
width: '100%',
padding: '8px 12px',
border: '1px solid #d1d5db',
borderRadius: '4px',
backgroundColor: '#f9fafb',
fontFamily: 'Monaco, Menlo, Ubuntu Mono, monospace',
fontSize: '14px',
color: '#374151',
cursor: 'not-allowed',
boxSizing: 'border-box'
}
}),
h('button', {
type: 'button',
onClick: this.handleGenerateNew,
className: 'ulid-generate-btn',
style: {
marginTop: '8px',
padding: '6px 12px',
backgroundColor: '#3b82f6',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: 'pointer',
fontSize: '12px',
transition: 'background-color 0.2s'
}
}, 'Generate New ULID'),
h('div', {
style: {
marginTop: '4px',
fontSize: '12px',
color: '#6b7280',
fontStyle: 'italic'
}
}, 'This ULID is automatically generated and cannot be edited manually.')
]);
}
});
// Preview component for the admin interface
const UlidPreview = createClass({
render: function() {
const value = this.props.value;
return h('div', {
className: 'ulid-preview',
style: {
fontFamily: 'Monaco, Menlo, Ubuntu Mono, monospace',
fontSize: '12px',
color: '#6b7280',
backgroundColor: '#f3f4f6',
padding: '4px 8px',
borderRadius: '3px',
border: '1px solid #e5e7eb',
display: 'inline-block'
}
}, value || 'No ULID generated');
}
});
// Register the widget
CMS.registerWidget('ulid', UlidControl, UlidPreview);

3. Configuration Updates

Update your config.yml to use the new widget:

collections:
- name: "posts"
label: "Posts"
folder: "src/content/posts"
create: true
slug: "{{id}}--{{slug}}"
path: "{{slug}}/index"
extension: "md"
fields:
- {label: "ID (ULID)", name: "id", widget: "ulid"}
- {label: "Title", name: "title", widget: "string"}
# ... other fields

4. Script Loading

Ensure the widget loads correctly in your admin interface:

<script>
(function() {
var script = document.createElement('script');
script.src = 'https://unpkg.com/decap-cms@3.8.3/dist/decap-cms.js';
script.async = true;
script.onload = function() {
// Load ULID widget after Decap CMS is loaded
var ulidScript = document.createElement('script');
ulidScript.src = './ulid-widget.js';
ulidScript.async = true;
ulidScript.onload = function() {
setTimeout(function() {
if (typeof DecapCMS !== 'undefined') {
try {
DecapCMS.init();
} catch (error) {
console.error('Decap CMS initialization error:', error);
}
}
}, 200);
};
document.head.appendChild(ulidScript);
};
document.head.appendChild(script);
})();
</script>

Key Implementation Details

Error Handling

The implementation includes comprehensive error handling to ensure the CMS works even if the widget fails to load.

User Experience

  • Read-only input: Prevents accidental modifications
  • Generate button: Allows regeneration when needed
  • Visual feedback: Clear styling and hints
  • Monospace font: Better readability for technical identifiers

Performance Considerations

  • Lazy loading: Widget loads after Decap CMS
  • Efficient generation: Minimal computational overhead
  • Memory management: Proper cleanup and event handling

Common Pitfalls and Solutions

1. React Compatibility Issues

Problem: Decap CMS 3.8.3 doesn't expose React globally. Solution: Use createClass and h functions provided by Decap CMS.

2. Widget Registration Errors

Problem: "Widget registered without controlComponent" error. Solution: Use the correct registration syntax: CMS.registerWidget('name', Control, Preview).

3. Timing Issues

Problem: Widget loads before Decap CMS is ready. Solution: Implement proper loading sequence with callbacks and timeouts.

Testing and Validation

To ensure your widget works correctly:

  1. Test ID uniqueness: Create multiple entries and verify no duplicates
  2. Test sorting: Verify entries are sorted chronologically
  3. Test regeneration: Ensure the generate button works
  4. Test persistence: Verify IDs are saved and loaded correctly

Advanced Features

Consider extending the widget with:

  • Bulk generation: Generate multiple ULIDs at once
  • Custom prefixes: Add project-specific prefixes
  • Validation: Ensure ULID format compliance
  • History: Track ULID generation history

Conclusion

Building custom widgets for Decap CMS requires understanding both the CMS architecture and modern web development practices. This ULID widget implementation provides:

  • Automatic generation: No manual intervention required
  • Data integrity: Guaranteed unique identifiers
  • Performance: Optimized for database operations
  • User experience: Intuitive interface for content creators

The implementation demonstrates how to extend Decap CMS functionality while maintaining compatibility and following best practices. This approach can be adapted for other custom widgets, making it a valuable pattern for headless CMS development.

Resources


This implementation is part of the Quesby project, a privacy-first static site boilerplate. The complete source code is available on GitHub.