import React from 'react' import { tryParse } from '@blotoutio/shared/universal' interface Rule { source: string match: string key: string tagUrl: string } interface CustomTagRulesProps { rulesJson: string onChange: (rulesJson: string) => void } const sourceOptions = [ { label: 'Domain', value: 'domain' }, { label: 'Path', value: 'path' }, { label: 'Query', value: 'query' }, { label: 'Referrer', value: 'referrer' }, ] const matchOptions = [ { label: 'Equals', value: 'equals' }, { label: 'Starts with', value: 'startsWith' }, { label: 'Contains', value: 'contains' }, { label: 'Ends with', value: 'endsWith' }, ] const parseRules = (json: string): Rule[] => tryParse(json, []) const getKeyPlaceholder = (source: string): string => { switch (source) { case 'domain': return 'e.g. budclub.com' case 'path': return 'e.g. /eu' case 'query': return 'e.g. ?locale=eu' case 'referrer': return 'e.g. google.com' default: return 'e.g. value' } } const styles = { heading: { fontSize: '16px', fontWeight: 600, margin: '16px 0 4px', }, description: { color: '#666', fontSize: '13px', marginBottom: '12px', }, rule: { border: '1px solid #ddd', borderRadius: '6px', padding: '12px', marginBottom: '8px', backgroundColor: '#fafafa', }, row: { display: 'flex', gap: '8px', marginBottom: '8px', }, field: { flex: 1, }, fieldLabel: { fontSize: '12px', fontWeight: 500, marginBottom: '4px', color: '#444', }, fullWidth: { marginBottom: '8px', }, removeBtn: { background: 'none', border: '1px solid #cc0000', color: '#cc0000', borderRadius: '4px', padding: '4px 12px', cursor: 'pointer', fontSize: '12px', }, addBtn: { background: 'none', border: '1px solid #2271b1', color: '#2271b1', borderRadius: '4px', padding: '6px 16px', cursor: 'pointer', fontSize: '13px', }, warning: { backgroundColor: '#fff3cd', border: '1px solid #ffc107', borderRadius: '4px', padding: '8px 12px', fontSize: '12px', color: '#856404', marginBottom: '12px', }, } /** * Manages a list of conditional tag-routing rules for WordPress EdgeTag settings. * * Rules are evaluated top-to-bottom at runtime; the first match wins. * If no rule matches, the default EdgeTag URL is used. * * @param props - {@link CustomTagRulesProps} * @param props.rulesJson - JSON string of the current rules array (empty string = no rules). * @param props.onChange - Called with the updated JSON string whenever rules are added, * removed, or edited. Passes an empty string when the last rule * is removed. * @returns The rendered rules editor. */ export const CustomTagRules: React.FC = ({ rulesJson, onChange, }) => { const rules = parseRules(rulesJson) const updateRules = (newRules: Rule[]) => { onChange(newRules.length > 0 ? JSON.stringify(newRules) : '') } const handleFieldChange = ( index: number, field: keyof Rule, value: string ) => { const updated = [...rules] updated[index] = { ...updated[index], [field]: value } updateRules(updated) } const handleAdd = () => { updateRules([ ...rules, { source: 'domain', match: 'equals', key: '', tagUrl: '' }, ]) } const handleRemove = (index: number) => { const updated = rules.filter((_, i) => i !== index) updateRules(updated) } return (
Custom Tag Rules
Load a different tag URL based on conditions. Rules are evaluated top-to-bottom; the first match wins. If no rules match, the default EdgeTag URL above is used.
{rules.length > 0 && (
All tag URLs listed in rules must share the same KV/D1 storage bindings for data continuity.
)} {rules.map((rule, index) => (
Source
Match
Key
handleFieldChange(index, 'key', e.target.value) } />
Tag URL
handleFieldChange(index, 'tagUrl', e.target.value) } style={{ width: '100%', boxSizing: 'border-box' }} />
))}
) }