TechEarl

ACF Options Pages: Site Settings, Permissions and Troubleshooting

Register ACF Options Pages, organize child screens, read and update values, choose permissions, and diagnose missing pages without confusing storage and menus.

Ishan Karunaratne⏱️ 6 min readUpdated
Share thisCopied
ACF Options Pages for site-wide settings: header CTAs, footer links, social profiles. Registration, reading patterns, parent/child structure, cache strategy.

Use an ACF Options Page for values that belong to the whole site: contact details, footer text, social links or a default announcement. Read the default options storage with get_field('field_name', 'option'). Use a post field when each post needs its own value.

Options Pages are an ACF PRO feature. ACF PRO 6.2 and later can register them through the admin UI; PHP registration remains useful when configuration lives with the site's code. Secure Custom Fields is a separate plugin with its own Options Pages feature, so identify which product is installed before diagnosing availability.

Basic registration

Save this registration in a site plugin or a must-use plugin. I default site-wide settings to administrator access:

php
add_action( 'acf/init', function () {
    if ( ! function_exists( 'acf_add_options_page' ) ) {
        return;
    }
    acf_add_options_page( array(
        'page_title' => 'Site Settings',
        'menu_title' => 'Site Settings',
        'menu_slug'  => 'te-site-settings',
        'capability' => 'manage_options',
        'redirect'  => false,
        'autoload'  => false,
    ) );
} );

In the field-group editor, add a location rule targeting this Options Page. In the UI workflow, create the page under ACF → Options Pages and attach the field group there instead. Do not register the same slug twice through competing UI and code definitions.

acf/init is the appropriate time to use ACF's registration API. The function check prevents a fatal error if the product is disabled or lacks that feature; it does not install the feature for you.

Parent + sub-pages structure for many settings

Use a parent when separate groups deserve separate editing screens. Set the capability on the children too:

php
add_action( 'acf/init', function () {
    if ( ! function_exists( 'acf_add_options_page' ) ) {
        return;
    }
    acf_add_options_page( array(
        'page_title' => 'Site Options',
        'menu_slug'  => 'te-site-options',
        'capability' => 'manage_options',
        'redirect'  => true,
    ) );
    foreach ( array( 'Header', 'Footer', 'Contact' ) as $section ) {
        acf_add_options_sub_page( array(
            'page_title'  => $section,
            'menu_slug'   => 'te-options-' . strtolower( $section ),
            'parent_slug' => 'te-site-options',
            'capability'  => 'manage_options',
        ) );
    }
} );

Attach each field group to the correct child page. Menu organization does not isolate stored values: default options pages share the same storage namespace. Use distinct names such as header_cta_url and footer_cta_url. If two screens intentionally use the same field, they can share its value. A custom storage prefix/context is a separate configuration choice, and reads must use the corresponding context.

Reading values from an Options Page

php
$text = (string) get_field( 'header_cta_text', 'option' );
$url  = (string) get_field( 'header_cta_url', 'option' );
if ( '' !== $text && '' !== $url ) {
    printf( '<a href="%s">%s</a>', esc_url( $url ), esc_html( $text ) );
}

Escape at the point of output. A URL belongs in esc_url(), plain text in esc_html(), and an attribute value in esc_attr(). If a field intentionally permits limited HTML, define that contract and use an appropriate allowed-HTML policy instead of treating every field as raw markup.

Repeater values use the same options context:

php
if ( have_rows( 'social_profiles', 'option' ) ) {
    while ( have_rows( 'social_profiles', 'option' ) ) {
        the_row();
        $url = (string) get_sub_field( 'url' );
        $label = (string) get_sub_field( 'label' );
        if ( $url && $label ) {
            printf( '<a href="%s">%s</a>', esc_url( $url ), esc_html( $label ) );
        }
    }
}

Updating Options Page values programmatically

Use the actual field key from your field-group export when initially creating a value, so ACF can establish its field reference:

php
// Replace these keys with the keys in your registered field group.
update_field( 'field_te_header_cta_text', 'Contact me', 'option' );
update_field( 'field_te_header_cta_url', 'https://example.com/contact/', 'option' );

For a repeater, write a complete row array with the correct subfield keys:

php
$rows = array(
    array(
        'field_te_social_label' => 'GitHub',
        'field_te_social_url'   => 'https://github.com/example',
    ),
);
update_field( 'field_te_social_profiles', $rows, 'option' );

This replaces the repeater value; it is not an append operation. Back up existing values before a bulk replacement. update_field() can return false when the value is unchanged, so distinguish a no-op from a failure using the intended before/after state. For a complete migration, use the programmatic field-update guide and safe batch-update procedure.

Cache strategy for high-traffic sites

Default options-page values live in wp_options, with reference metadata alongside them. Autoload is configurable; do not assume every ACF option is loaded on every request. Enable autoload only for a measured, bounded set of small values used throughout the site. A large repeater used on one route is a poor candidate for universal loading.

ACF and WordPress already cache values during normal reads. If profiling shows repeated work in your own adapter, a request-local wrapper can make that ownership explicit:

php
function te_site_setting( string $name, $default = null ) {
    static $values = array();
    if ( ! array_key_exists( $name, $values ) ) {
        $value = get_field( $name, 'option' );
        $values[ $name ] = ( false === $value || null === $value ) ? $default : $value;
    }
    return $values[ $name ];
}

This wrapper treats false/null as missing; do not use that policy for a boolean setting where false is meaningful. It also assumes the value does not change later in the same request. Use normal ACF/WordPress update APIs for writes so their caches are invalidated. A persistent cache or a rendered-page cache you add needs its own invalidation policy; manually deleting arbitrary option-cache entries is not the normal update procedure.

Options page missing: ordered diagnostic checklist

  1. Confirm the installed plugin/product and feature availability. For ACF, verify ACF PRO is active; also check whether its UI features are disabled by configuration.
  2. Look for the page in the Options Pages UI and in PHP registration. Missing acf_add_options_page() text alone does not mean the page is unregistered.
  3. Verify registration runs on acf/init, the slug is unique, and PHP logs contain no error that prevents registration.
  4. Test with a user who has the configured capability. edit_posts is also held by default Authors and Contributors, not just Editors. Use manage_options or an intentionally provisioned custom capability for privileged site settings.
  5. If the menu exists but the screen has no fields, inspect the group's location rules, active status and the selected child-page slug.
  6. For a missing child, confirm parent_slug matches the registered parent and that both capability settings allow the user to see it.
  7. If fields appear but the frontend is blank, check the field name, options storage context, and whether a value has actually been saved. Do not use the menu slug as the default 'option' context.

An editor-specific capability should be provisioned during a controlled setup/migration, not added to a role on every frontend request. Test with that role rather than inferring behavior from an administrator session.

Multilingual considerations

Options fields do not become language-specific simply because the site has multiple languages. With WPML, use the ACFML integration and its documented Options Page translation workflow, including field translation preferences and saving values in the intended language. Other multilingual plugins have different integrations; verify their current support before designing storage around it.

Test each language's saved value and rendered output separately, including fallback behavior. Make language/context part of any custom cache key. Keep global values that really should be shared, such as a single corporate identifier, distinct from translated text.

For layout-specific values, use the Flexible Content component pattern. For slow options reads, measure the actual cache and query behavior before duplicating the settings into another store.

Sources

Authoritative references this article was fact-checked against.

TagsWordPressACFOptions PagesArchitecture

Found this useful? Pass it on.

Copied

Ishan Karunaratne

Systems and Network Architect · Chief Technology Officer

Systems and network architect and Chief Technology Officer with more than two decades designing, building, and running production software, cloud and network architecture, Linux systems, and the bare metal underneath them, and lately working AI into the stack. A US Army veteran who served in Operation Iraqi Freedom. What I write here is drawn from the full arc of that work, across architecture, engineering, and operations, not any single job.

Keep reading

Related posts

Why SSH rejects your key with Permission denied (publickey) and the ordered checklist that fixes it: agent, identity file, server authorized_keys, and permissions.

Fix SSH "Permission denied (publickey)"

The ordered checklist for SSH Permission denied (publickey): is the key loaded, is it the right key, is the public half on the server, and are the server-side permissions sane.