---
title: "Add a “Go to Settings” Link After Installing Any WordPress Plugin (Full Code)"
url: https://adityaarsharma.com/go-to-plugin-settings-link-after-install/
date: 2026-07-10
modified: 2026-07-10
author: "Aditya R Sharma"
description: "After you install or update a plugin, WordPress makes you hunt for its settings page. This snippet adds a correct “Go to settings” link (with a Plugins-page fallback) for any plugin, in any language — no hardcoded list."
categories:
  - "Automation"
  - "WordPress"
word_count: 2723
---

# Add a “Go to Settings” Link After Installing Any WordPress Plugin (Full Code)

**The problem:** you install or update a plugin, WordPress hands you “Activate Plugin” and “Return to Plugin Installer” — and then leaves you to *hunt* for where that plugin’s settings actually live. This started as an answer to a question Rodolfo Melogli raised: after installing a plugin from a ZIP, why isn’t there a “Go to plugin settings” link, with a fallback to the Plugins page? Here is the **complete snippet** that does it — and does it for *any* plugin, in any language, without a hardcoded list. Grab the full code here: [the snippet on GitHub Gist](https://gist.github.com/adityaarsharma/46cec46c9dac76afa395866bb43fc805). *Last updated: July 2026.*

## Why the obvious fixes don’t hold up

Every “just add a settings link” approach I tried first broke somewhere. These are the four dead ends, because the fix only makes sense once you’ve seen them fail:

- **Hardcode a table of plugin → settings URL.** Fine for the top 20, useless for the long tail, and it drifts the moment a plugin renames its page.
- **Match the word “Settings” in the admin menu.** Dies on any non-English admin, and false-matches WordPress’ own *Settings* menu.
- **Guess `admin.php?page={slug}`.** Wrong for a large share of plugins. Akismet lives at `options-general.php?page=akismet-key-config`, ACF at `edit.php?post_type=acf-field-group`, Redirection under *Tools*. A guessed URL that 404s is worse than no link at all.
- **Use `install_plugin_overwrite_actions`.** Tempting name, wrong hook — it fires on the overwrite *confirmation* screen, not the install/update *result*. The right hook is `install_plugin_complete_actions`, which fires after a normal install, an update, *and* a ZIP upload.

## The one reliable signal: the plugin’s own settings link

Every plugin that has a settings page already adds its own link to its row on the Plugins list, through the `plugin_action_links_{$plugin_file}` filter. That link is authored by the plugin, points at the correct URL, and is completely independent of the admin language. So instead of guessing, the snippet **reads that**.

It fires the filter, parses the returned HTML with `DOMDocument` (not regex), keeps only internal `wp-admin` URLs, throws away the core row-actions (activate / deactivate / delete / editor), and prefers the candidate whose URL contains the plugin’s own slug. Whatever survives is the plugin’s real settings URL — ground truth, not a guess.

## Learn it once, cache it, let it self-heal

The catch: that filter only returns anything while the plugin is **loaded** (active). So the snippet learns the link at the two moments it can, and remembers it:

- on `activated_plugin` — the instant a plugin activates;
- on `load-plugins.php` — where every active plugin’s action links exist, so it back-fills the whole active set.

Both write to a single option keyed by plugin file. Resolution priority is **learned cache → seed map → live best-effort**. The seed map (~22 popular plugins) is only a bootstrap so the very first click is right before the cache warms; the learned cache overrides it, so a stale seed URL self-corrects the first time you open the Plugins screen. Only the reliable action-link signal is ever cached — the fuzzy menu scan is used live but never stored, so it can’t poison the cache.

## Never show a dead link: activate first, then resolve

After a ZIP install the plugin isn’t active yet, so its settings page doesn’t exist and its own link can’t be read. A “Go to settings” link there would be dead. So the result screen branches:

- **Already active** (you just updated it) → *Go to settings*, straight to the resolved URL.
- **Freshly installed, not active** → *Activate & open settings*: one nonce-protected click that activates the plugin, reads its now-available real link, and redirects you there.
- **Always** → *Go to Plugins page*.

The privileged step sits behind `admin_post` + `check_admin_referer` + a capability check, so a logged-out or low-role user can’t trigger anything. Here is the whole flow:

`install / update result screen
|
Is the plugin active?
|-- yes --> resolve URL (cache > seed > live) --> "Go to settings"
|-- no --> "Activate & open settings"
| --> activate_plugin() --> read the plugin's OWN link --> redirect there
|
always --> "Go to Plugins page"

get_settings_url( plugin_file ):
1. learned cache (the plugin's own link, saved on activate + on the Plugins screen)
2. seed map (22 popular plugins - bootstrap only, cache overrides it)
3. live best-effort (read the action link now; else a strict full-slug menu scan)`

## The complete snippet

```
<?php
/**
* Plugin Name: Smart "Plugin Installed" Links
* Description: After installing/updating a plugin (incl. ZIP uploads), adds a correct "Go to settings"
* link + a reliable "Go to Plugins page" link. LEARNS each plugin's real settings link (from
* the plugin's OWN declared Settings action link) on activation and on the Plugins screen, so
* it is self-correcting, version-proof and language-independent. No English keyword matching.
* Version: 2.1.0
* Author: Aditya Sharma
* License: GPL-2.0-or-later
*
* Resolution priority: learned cache > seed map > live best-effort. Only high-confidence signals are cached.
*/

if ( ! defined( 'ABSPATH' ) ) {
exit;
}

// Guard so the same code is safe whether dropped in as a plugin, mu-plugin, or theme functions.php snippet.
if ( ! class_exists( 'Aditya_Plugin_Install_Links' ) ) :

final class Aditya_Plugin_Install_Links {

const OPTION = 'aditya_plugin_settings_urls'; // [ plugin_file => admin-relative path ]

public static function init() {
add_filter( 'install_plugin_complete_actions', array( __CLASS__, 'install_actions' ), 10, 3 );
add_action( 'activated_plugin', array( __CLASS__, 'learn_one' ), 999, 1 );
add_action( 'load-plugins.php', array( __CLASS__, 'learn_active' ) );
add_action( 'admin_post_aditya_activate_goto_settings', array( __CLASS__, 'handle_activate_goto_settings' ) );
}

/**
* Seed map — bootstraps top plugins BEFORE their first activation. The learned cache (each plugin's
* OWN link) overrides these, so any drift self-heals. Admin-relative paths. Extend freely.
*/
public static function seed_map() {
return array(
'woocommerce/woocommerce.php' => 'admin.php?page=wc-settings',
'wordpress-seo/wp-seo.php' => 'admin.php?page=wpseo_dashboard',
'elementor/elementor.php' => 'admin.php?page=elementor',
'contact-form-7/wp-contact-form-7.php' => 'admin.php?page=wpcf7',
'akismet/akismet.php' => 'options-general.php?page=akismet-key-config',
'classic-editor/classic-editor.php' => 'options-writing.php',
'jetpack/jetpack.php' => 'admin.php?page=jetpack',
'wpforms-lite/wpforms.php' => 'admin.php?page=wpforms-settings',
'all-in-one-seo-pack/all_in_one_seo_pack.php' => 'admin.php?page=aioseo-settings',
'wordfence/wordfence.php' => 'admin.php?page=Wordfence',
'seo-by-rank-math/rank-math.php' => 'admin.php?page=rank-math',
'litespeed-cache/litespeed-cache.php' => 'admin.php?page=litespeed-cache',
'wp-super-cache/wp-cache.php' => 'options-general.php?page=wpsupercache',
'w3-total-cache/w3-total-cache.php' => 'admin.php?page=w3tc_general',
'wp-rocket/wp-rocket.php' => 'options-general.php?page=wprocket',
'updraftplus/updraftplus.php' => 'options-general.php?page=updraftplus',
'wp-mail-smtp/wp_mail_smtp.php' => 'admin.php?page=wp-mail-smtp',
'google-site-kit/google-site-kit.php' => 'admin.php?page=googlesitekit-settings',
'google-analytics-for-wordpress/googleanalytics.php' => 'admin.php?page=monsterinsights_settings',
'wp-smushit/wp-smush.php' => 'admin.php?page=smush',
'redirection/redirection.php' => 'tools.php?page=redirection.php',
'advanced-custom-fields/acf.php' => 'edit.php?post_type=acf-field-group',
);
}

/** Absolute settings URL or '' if unknown. Priority: learned cache > seed > live best-effort. */
public static function get_settings_url( $plugin_file ) {
if ( empty( $plugin_file ) ) {
return '';
}
$cache = get_option( self::OPTION, array() );
if ( is_array( $cache ) && ! empty( $cache[ $plugin_file ] ) ) {
return admin_url( $cache[ $plugin_file ] );
}
$seed = self::seed_map();
if ( isset( $seed[ $plugin_file ] ) ) {
return admin_url( $seed[ $plugin_file ] );
}
$rel = self::live_detect( $plugin_file );
return $rel ? admin_url( $rel ) : '';
}

/** Learn just-activated plugin (cache ONLY the reliable action-link signal). */
public static function learn_one( $plugin_file ) {
$rel = self::settings_from_action_links( $plugin_file );
if ( $rel ) {
self::cache_set( $plugin_file, $rel );
}
}

/** On the Plugins screen, every active plugin's action links exist → cache them (reliable signal only). */
public static function learn_active() {
if ( ! current_user_can( 'activate_plugins' ) ) {
return;
}
$cache = get_option( self::OPTION, array() );
$cache = is_array( $cache ) ? $cache : array();
$changed = false;
foreach ( (array) get_option( 'active_plugins', array() ) as $file ) {
if ( ! empty( $cache[ $file ] ) ) {
continue;
}
$rel = self::settings_from_action_links( $file );
if ( $rel ) {
$cache[ $file ] = $rel;
$changed = true;
}
}
if ( $changed ) {
update_option( self::OPTION, $cache, false );
}
}

private static function cache_set( $file, $rel ) {
$cache = get_option( self::OPTION, array() );
$cache = is_array( $cache ) ? $cache : array();
$cache[ $file ] = $rel;
update_option( self::OPTION, $cache, false );
}

/* ------------------------------------------------------------------ *
* Detection
* ------------------------------------------------------------------ */

/** RELIABLE signal (cacheable): the plugin's OWN declared "Settings" action link. Language-independent. */
private static function settings_from_action_links( $plugin_file ) {
$links = array();
$links = (array) apply_filters( "plugin_action_links_{$plugin_file}", $links, $plugin_file, array(), 'all' );
$links = (array) apply_filters( "network_admin_plugin_action_links_{$plugin_file}", $links, $plugin_file, array(), 'all' );

$admin = admin_url();
$net = network_admin_url();
$needles = self::slug_needles( $plugin_file );
$candidates = array();

foreach ( $links as $html ) {
if ( ! is_string( $html ) || false === stripos( $html, 'href' ) ) {
continue;
}
$dom = new DOMDocument();
libxml_use_internal_errors( true );
$dom->loadHTML( '<?xml encoding="utf-8"?><div>' . $html . '</div>' );
libxml_clear_errors();

foreach ( $dom->getElementsByTagName( 'a' ) as $a ) {
$href = html_entity_decode( trim( $a->getAttribute( 'href' ) ), ENT_QUOTES );
if ( '' === $href || '#' === $href ) {
continue;
}
if ( 0 !== strpos( $href, 'http' ) && 0 !== strpos( $href, '/' ) ) {
$href = $admin . ltrim( $href, '/' );
}
if ( 0 !== strpos( $href, $admin ) && 0 !== strpos( $href, $net ) ) {
continue; // internal admin URLs only
}
if ( preg_match( '#(plugins\.php\?|plugin-editor\.php|action=(activate|deactivate|delete)|update\.php|tgmpa)#i', $href ) ) {
continue; // skip core plugin-row actions
}
$candidates[] = $href;
}
}

if ( empty( $candidates ) ) {
return '';
}
// Prefer a candidate whose URL mentions the plugin's full slug; else the first custom link (conventionally Settings).
foreach ( $candidates as $c ) {
foreach ( $needles as $n ) {
if ( false !== stripos( $c, $n ) ) {
return self::to_relative( $c );
}
}
}
return self::to_relative( $candidates[0] );
}

/** BEST-EFFORT (live only, never cached): strict full-slug menu scan. */
private static function settings_from_menu( $plugin_file ) {
$needles = self::slug_needles( $plugin_file );
global $submenu, $menu;
foreach ( array( (array) $submenu, (array) $menu ) as $group ) {
foreach ( $group as $items ) {
foreach ( self::rows( $items ) as $row ) {
$page = isset( $row[2] ) ? (string) $row[2] : '';
if ( '' === $page ) {
continue;
}
foreach ( $needles as $n ) {
if ( false !== stripos( $page, $n ) ) {
return ( false !== strpos( $page, '.php' ) ) ? $page : 'admin.php?page=' . $page;
}
}
}
}
}
return '';
}

private static function live_detect( $plugin_file ) {
$rel = self::settings_from_action_links( $plugin_file );
return $rel ? $rel : self::settings_from_menu( $plugin_file );
}

/** Full-slug needles only (NOT generic tokens) — avoids "editor" matching site-editor.php etc. */
private static function slug_needles( $plugin_file ) {
$slug = ( false !== strpos( $plugin_file, '/' ) ) ? dirname( $plugin_file ) : basename( $plugin_file, '.php' );
$slug = sanitize_title( $slug );
if ( strlen( $slug ) < 4 ) {
return array();
}
return array_values( array_unique( array( $slug, str_replace( '-', '_', $slug ), str_replace( '-', '', $slug ) ) ) );
}

private static function rows( $items ) {
return ( isset( $items[2] ) && is_string( $items[2] ) ) ? array( $items ) : (array) $items;
}

private static function to_relative( $url ) {
$admin = admin_url();
if ( 0 === strpos( $url, $admin ) ) {
return substr( $url, strlen( $admin ) );
}
$net = network_admin_url();
if ( 0 === strpos( $url, $net ) ) {
return substr( $url, strlen( $net ) );
}
return $url;
}

/* ------------------------------------------------------------------ *
* The install / update result screen
* ------------------------------------------------------------------ */

public static function install_actions( $actions, $api, $plugin_file ) {
if ( ! current_user_can( 'activate_plugins' ) ) {
return $actions;
}
if ( ! function_exists( 'is_plugin_active' ) ) {
require_once ABSPATH . 'wp-admin/includes/plugin.php';
}

$sep = '<span aria-hidden="true">|</span>';
$is_active = $plugin_file && is_plugin_active( $plugin_file );
$can_set = $plugin_file && current_user_can( 'manage_options' );

if ( $can_set && $is_active ) {
// Plugin is already active (e.g. updating an active plugin) → its settings page exists → link straight to it.
$settings = self::get_settings_url( $plugin_file );
if ( $settings ) {
$actions['aditya_settings'] = $sep . sprintf(
'<a href="%s" target="_parent"><strong>%s</strong></a>',
esc_url( $settings ),
esc_html__( 'Go to settings', 'aditya-plugin-links' )
);
}
} elseif ( $can_set && ! $is_active ) {
// Not active yet → don't offer a dead settings link. One click that activates THEN opens settings.
$url = wp_nonce_url(
admin_url( 'admin-post.php?action=aditya_activate_goto_settings&plugin=' . rawurlencode( $plugin_file ) ),
'aditya_activate_' . $plugin_file
);
$actions['aditya_activate_settings'] = $sep . sprintf(
'<a href="%s" target="_parent"><strong>%s</strong></a>',
esc_url( $url ),
esc_html__( 'Activate & open settings', 'aditya-plugin-links' )
);
}

$actions['aditya_plugins_page'] = $sep . sprintf(
'<a href="%s" target="_parent">%s</a>',
esc_url( self_admin_url( 'plugins.php' ) ),
esc_html__( 'Go to Plugins page', 'aditya-plugin-links' )
);
return $actions;
}

/** Activate the plugin, then redirect to its settings (resolving it live now that the plugin is loaded). */
public static function handle_activate_goto_settings() {
$file = isset( $_GET['plugin'] ) ? sanitize_text_field( wp_unslash( $_GET['plugin'] ) ) : '';
if ( '' === $file || ! current_user_can( 'activate_plugins' ) ) {
wp_die( esc_html__( 'You are not allowed to do that.', 'aditya-plugin-links' ) );
}
check_admin_referer( 'aditya_activate_' . $file );

if ( ! function_exists( 'activate_plugin' ) ) {
require_once ABSPATH . 'wp-admin/includes/plugin.php';
}
$res = activate_plugin( $file );
if ( is_wp_error( $res ) ) {
wp_safe_redirect( self_admin_url( 'plugins.php?plugin_activation_error=1' ) );
exit;
}

// Plugin is loaded now → learn its real settings link, then send the user there.
$rel = self::settings_from_action_links( $file );
if ( $rel ) {
self::cache_set( $file, $rel );
}
$url = self::get_settings_url( $file );
wp_safe_redirect( $url ? $url : self_admin_url( 'plugins.php' ) );
exit;
}
}

Aditya_Plugin_Install_Links::init();

endif;

```

## Install it

- Paste it into your (child) theme’s `functions.php`, save it as an mu-plugin (`wp-content/mu-plugins/aditya-plugin-links.php`), or drop it into a code-snippets plugin set to run everywhere.
- That’s the whole setup — no options, no config. The next time you install or update a plugin, the links appear on the result screen.
- Optional: rename the `Aditya_Plugin_Install_Links` class and the `aditya_` prefixes to your own.

It only hooks the `wp-admin` install and Plugins screens, so it does nothing on the front end and adds zero page-load overhead for visitors.

## How well does it really cover “any plugin”?

Honest accounting, because “works with everything” is usually a lie:

- **Plugins with a settings page — covered.** The destination is whatever the plugin itself declares as its settings link. Tested across 15+ plugins (WooCommerce, Yoast, Rank Math, AIOSEO, Elementor, Contact Form 7, WPForms, Jetpack, LiteSpeed Cache, WP Mail SMTP, ACF, Akismet, Redirection and more) with no mismatches.
- **Plugins with no settings page** (Hello Dolly, Classic Widgets) — the correct behaviour is *no* settings link. You just get “Go to Plugins page.” The snippet never invents a page that doesn’t exist.
- **Non-English admins — fine.** Matching is on URLs, never on the word “Settings,” so a German or Japanese dashboard resolves identically.
- **The known soft spot.** For the rare plugin that exposes several custom action links with *none* of them containing its slug in the URL, the snippet picks the first one — which by long-standing convention is Settings. A plugin that orders its links differently could send you to the wrong one of its *own* pages. That’s the one case the URL-slug heuristic can miss; extend the seed map for that plugin and the learned cache takes over.

## FAQ

### Does it work for ZIP uploads, not just the WordPress.org installer?

Yes. `install_plugin_complete_actions` fires for a WordPress.org install, an update, and a manual ZIP upload alike. ZIP uploads were part of the testing.

### Will it ever send me to a 404?

For an active plugin it links to the plugin’s own declared URL. For an inactive one it doesn’t offer a settings link at all until *after* activation, so there’s no dead settings link to click. Worst case, you land on the Plugins page.

### Is it safe to run?

The activate-and-redirect path is a nonce-checked `admin_post` action that requires `activate_plugins` / `manage_options`. Nothing privileged is exposed to a logged-out or low-role user.

### Will it slow my site down?

No. Every hook is on the `wp-admin` install and Plugins screens. The front end never loads it.

### Which plugins are in the seed map, and why?

About 22 popular ones — purely so the first click is correct before the learned cache warms up. Once you activate a plugin or visit the Plugins screen, the plugin’s own link is cached and overrides the seed.

Full code: [the snippet on GitHub Gist](https://gist.github.com/adityaarsharma/46cec46c9dac76afa395866bb43fc805). Drop it in, install your next plugin, and the “now where are the settings?” step is just gone. If you hit a plugin that resolves to the wrong page, that’s exactly the edge the learned cache is built to swallow — tell me and I’ll fold it into the seed.