Skip to content

Placeholder System

The placeholder system lets readers customize IP addresses, ASNs, and other deployment-specific values throughout the documentation. Authors write tokens in their Markdown; the browser replaces them with user-supplied values at runtime.

Tokens follow the regex pattern:

x([A-Z][A-Z0-9_]+)x

A token starts and ends with a lowercase x and contains an uppercase identifier. For example, xCUSTOMER_ASNx references the CUSTOMER_ASN placeholder.

The regex is defined in src/scripts/placeholder-dom.ts:

const PH_REGEX = /x([A-Z][A-Z0-9_]+)x/g;

All placeholders are declared in src/data/placeholders.json. Each entry has this shape:

{
"CUSTOMER_ASN": {
"type": "text",
"default": "64496",
"description": "Your public ASN (registered with ARIN/RIR)"
}
}
FieldRequiredDescription
typeyes"text" for free-form input, "dropdown" for select menus
defaultyesInitial value shown before the reader changes anything
descriptionyesLabel displayed in the form
optionsonly for dropdownArray of allowed values

src/lib/placeholder-store.ts handles all placeholder state.

Values are persisted in localStorage under the key f5xc-placeholders. The store exposes four functions:

FunctionPurpose
getDefaults()Returns a map of every placeholder key to its default value from JSON
loadValues()Reads from localStorage, falls back to getDefaults()
saveValues(values)Writes the current map to localStorage
clearValues()Removes the localStorage entry

FIELD_GROUPS organizes placeholder keys into labeled sections for the form UI:

export const FIELD_GROUPS: FieldGroup[] = [
{ label: 'Data Center & Scrubbing Centers', keys: ['DC_NAME', 'CENTER_1', 'CENTER_2'] },
{ label: 'Protected Prefixes', keys: ['PROTECTED_CIDR_V4', 'PROTECTED_NET_V4', ...] },
{ label: 'BGP', keys: ['CUSTOMER_ASN', 'F5_XC_ASN', 'BGP_PASSWORD'] },
// ... more groups
];

Some values are derived from user input rather than entered directly. getComputedValues() calculates these from lookup tables:

const cidrToMask: Record<string, string> = {
'/24 (256 IPs)': '255.255.255.0',
'/23 (512 IPs)': '255.255.254.0',
// ...
};

Two computed placeholders are produced:

Computed KeyDerived FromExample
PROTECTED_MASK_V4PROTECTED_CIDR_V4 via cidrToMask lookup255.255.255.0
PROTECTED_PREFIX_V4PROTECTED_NET_V4 + PROTECTED_CIDR_V4 via cidrToShort192.0.2.0/24

getAllValues() merges user-entered values with computed values, giving a complete map for substitution.

emitChange() dispatches a placeholder-change CustomEvent on document with the full value map as detail:

export function emitChange(values: Record<string, string>) {
document.dispatchEvent(
new CustomEvent('placeholder-change', { detail: getAllValues(values) }),
);
}

This event drives both the DOM span updates and Mermaid re-rendering.

src/components/PlaceholderForm.tsx provides the editing UI.

  • State: useState initialized from loadValues()
  • On mount: useEffect calls emitChange() to trigger the initial DOM substitution
  • handleChange: Updates React state, calls saveValues() and emitChange()
  • handleReset: Calls clearValues(), resets state to getDefaults(), emits change
  • Rendering: Iterates FIELD_GROUPS, rendering a <fieldset> per group. Each key gets either an <input> (text type) or <select> (dropdown type)
  • Layout: The form is wrapped in a <details> element, collapsed by default

src/components/PlaceholderFormWrapper.astro connects the React component to the Astro page:

<PlaceholderForm client:only="react" />
<script>
import '../scripts/placeholder-dom.ts';
</script>

client:only="react" tells Astro to hydrate the component purely on the client (no SSR). The <script> tag imports the DOM walker so it runs on every page that includes this wrapper.

The wrapper also injects global CSS for form styling (.ph-form-wrapper, .ph-grid, .ph-value, etc.).

src/scripts/placeholder-dom.ts handles the client-side token replacement.

On page load, init() runs:

  1. Selects .sl-markdown-content as the root (falls back to document.body)
  2. Calls walkTextNodes(root, values) which uses document.createTreeWalker with NodeFilter.SHOW_TEXT
  3. For each text node matching the token regex, splits it into a document fragment of plain text nodes and <span data-ph="KEY" class="ph-value"> elements
  4. Replaces the original text node with the fragment

After the walk, the DOM contains spans with data-ph attributes instead of raw tokens.

When the form emits a placeholder-change event, updateSpans() runs:

document.querySelectorAll<HTMLSpanElement>('span[data-ph]').forEach((span) => {
const name = span.getAttribute('data-ph')!;
if (values[name] !== undefined) {
span.textContent = values[name];
}
});

This avoids re-walking the tree — it directly updates the span text content.

The script registers two listeners:

EventHandlerPurpose
placeholder-changehandleChangeUpdates spans and re-renders Mermaid diagrams
astro:page-loadinitRe-walks the DOM after Astro client-side navigation
  1. Add the JSON entry in src/data/placeholders.json:

    "MY_NEW_VALUE": {
    "type": "text",
    "default": "example",
    "description": "Description shown in the form"
    }
  2. Add the key to a field group in src/lib/placeholder-store.ts. Either add it to an existing group’s keys array or create a new group in FIELD_GROUPS.

  3. Use the token in content: Write xMY_NEW_VALUEx in any .mdx file. The DOM walker will replace it at runtime.

Computed values are derived from other placeholders. To add one:

  1. Add a lookup table (if needed) in src/lib/placeholder-store.ts, following the pattern of cidrToMask.

  2. Extend getComputedValues() to include the new derived key:

    export function getComputedValues(values: Record<string, string>): Record<string, string> {
    // ... existing logic
    return {
    PROTECTED_MASK_V4: mask,
    PROTECTED_PREFIX_V4: `${net}${short}`,
    MY_COMPUTED: derivedValue, // add here
    };
    }
  3. Use xMY_COMPUTEDx in content like any other token. Computed values do not need a placeholders.json entry or a field group — they are invisible to the form.