LearnDash Login Redirect Plugin

LearnDash After Login Plugin

Redirect LearnDash Users After Login Free Plugin

By Wellington Duarte | learndashdev.com

Hello, this is Wellington Duarte.

Many of my clients constantly need student redirects inside LearnDash after the login page. There are some plugins available for this, but they are either limited or paid.

The problem seems simple, but the solution requires understanding how WordPress, LearnDash, and very often WooCommerce, which is commonly used as the student dashboard, interact during the authentication process. In this article, I’ll explain why I created this plugin, how I architected each part of it, the technical challenges I faced, including a real battle with WooCommerce, and how you can use it today in your projects completely free of charge.

Why Does the Post Login Experience Matter So Much?

When you create a course in LearnDash, it is essential to control the student journey inside the course so users do not get lost, and this is still a gap that LearnDash has not solved natively. Because of that, developers often need to rely on plugins or custom code to redirect students after they log into the site.

Think about the real use cases that appear in client projects:

  • New student enrolled in a course → should go directly to a custom student page and not always to the WooCommerce “My Account” page if you are using it as the login method.
  • VIP member of a premium group → should access a private community or exclusive dashboard.
  • Student enrolled in multiple courses → may need a centralized learning hub.
  • Customer who purchased through WooCommerce → should not land on “My Account,” but instead on the content they just purchased.

By default, LearnDash does not provide granular control over this. WordPress has the login_redirect filter, but handling it directly, especially when other plugins such as WooCommerce also manipulate it, is a task that requires deep knowledge of the WordPress core.

I developed LearnDash After Login to solve this problem in an elegant, free, and secure way.

The Plugin Architecture

Before writing a single line of code, I defined three design principles:

  • Clear rule hierarchy. The administrator must know exactly which rule will prevail when multiple configurations exist.
  • Admin safe. The plugin should never interfere with administrator access.
  • Zero interference when not configured. If no URL is filled in, the plugin should not touch WordPress default behavior.

The plugin is structured into three main classes, each with a single responsibility:

LDAL_Settings — Data Management

This class handles all settings persistence. The data is stored in the wp_options table under the key ldal_settings as a serialized array with three keys:

[
    'global_redirect'  => 'https://site.com/dashboard',
    'group_redirects'  => [
        [ 'ids' => [42, 87], 'url' => 'https://site.com/vip-group' ],
        [ 'all' => true,     'url' => 'https://site.com/students' ],
    ],
    'course_redirects' => [
        [ 'ids' => [101], 'url' => 'https://site.com/advanced-course' ],
    ],
]

Saving happens via AJAX with double security verification: nonce validation using wp_create_nonce and check_ajax_referer, and capability validation using current_user_can('manage_options'). Every URL is sanitized with esc_url_raw() before being persisted.

LDAL_Redirect — The Core Logic

This class contains the redirect logic itself and is where most of the technical complexity lives. It registers itself in multiple WordPress hooks, and the reason for that will become clear shortly.

LDAL_Admin — The Administration Interface

Responsible for registering the submenu under LearnDash → After Login and rendering the admin panel. The interface was built using pure HTML, CSS, and JavaScript with no framework dependencies and communicates with the backend exclusively via AJAX. Existing groups and courses are passed from PHP to JavaScript using wp_localize_script(), avoiding unnecessary requests.

The Priority System

The central decision logic follows a clear cascade. When a student logs in, the plugin evaluates rules in this order:

1. Course Rules  →  is there a specific rule for this course?
       ↓ no
2. Group Rules   →  does the student belong to a group with a rule?
       ↓ no
3. Global Rule   →  is there a global URL configured?
       ↓ no
4. Default WordPress behavior (plugin does not interfere)

The intentional design here is that the most specific rule always wins. A student enrolled in the “Advanced Python Course” who also belongs to the “Premium Students” group will follow the course rule, not the group rule. This gives administrators precise control without ambiguity.

The PHP implementation looks like this:

private function resolve_redirect( $user, $settings ) {

    // 1. Course rules have highest priority
    if ( ! empty( $settings['course_redirects'] ) ) {
        $url = $this->match_course_redirect( $user, $settings['course_redirects'] );
        if ( ! empty( $url ) ) return $url;
    }

    // 2. Group rules
    if ( ! empty( $settings['group_redirects'] ) ) {
        $url = $this->match_group_redirect( $user, $settings['group_redirects'] );
        if ( ! empty( $url ) ) return $url;
    }

    // 3. Global redirect
    if ( ! empty( $settings['global_redirect'] ) ) {
        return $settings['global_redirect'];
    }

    // 4. No rules matched: plugin does not interfere
    return null;
}

Who Is Considered a “Student”?

One of the most important design decisions was defining who the plugin applies to. My premise was that the criteria should not be the user role itself, but whether the user is enrolled in at least one LearnDash course.

Why? Because different LearnDash installations may use custom roles. Websites integrated with WooCommerce, MemberPress, or BuddyBoss often use their own student related roles. If I filtered only by subscriber or student, the plugin would break in these scenarios.

The logic looks like this:

private function is_learndash_student( $user ) {

    // Admins are never affected
    if ( user_can( $user, 'manage_options' ) ) {
        return false;
    }

    // Group leaders are not redirected as students
    if ( in_array( 'group_leader', (array) $user->roles, true ) ) {
        return false;
    }

    // Any other user enrolled in at least one course gets redirected
    return ! empty( $this->get_user_enrolled_courses( $user->ID ) );
}

The Enrollment Detection Problem During Login

Here is one of the most subtle bugs I encountered during development.

The default LearnDash function for retrieving a user’s courses is ld_get_mycourses(). The problem is that during the login_redirect hook, the full user context is not always 100% initialized by WordPress. In some environments, especially those using opcode caching and PHP FPM configurations, this function returned an empty array for legitimately enrolled users.

The solution was to create a three layer fallback chain:

private function get_user_enrolled_courses( $user_id ) {

    // Layer 1: Modern LearnDash API (3.0+)
    if ( function_exists( 'learndash_user_get_enrolled_courses' ) ) {
        $courses = learndash_user_get_enrolled_courses( $user_id, [], true );
        if ( ! empty( $courses ) ) return $courses;
    }

    // Layer 2: Legacy API
    if ( function_exists( 'ld_get_mycourses' ) ) {
        $courses = ld_get_mycourses( $user_id );
        if ( ! empty( $courses ) ) return $courses;
    }

    // Layer 3: Direct query to wp_usermeta
    global $wpdb;

    $results = $wpdb->get_col( $wpdb->prepare(
        "SELECT DISTINCT CAST(
             SUBSTRING_INDEX(SUBSTRING_INDEX(meta_key, '_', -2), '_', 1)
         AS UNSIGNED) as course_id
         FROM {$wpdb->usermeta}
         WHERE user_id = %d
           AND meta_key LIKE 'course_%_access_from'
           AND meta_value != ''",
        $user_id
    ) );

    return ! empty( $results ) ? array_map( 'intval', array_filter( $results ) ) : [];
}

This approach guarantees that the plugin works regardless of the LearnDash version installed or the execution environment conditions.

The Battle with WooCommerce

This was the most interesting technical challenge of the project and the one that revealed the most about how WordPress processes login internally.

When WooCommerce is installed and a user logs in, the following happens:

  • WordPress triggers the login_redirect filter so plugins can define where the user should go.
  • WooCommerce has its own woocommerce_login_redirect filter that sends users to “My Account.”
  • In certain login flows, especially through checkout or “My Account,” WooCommerce calls wp_redirect() directly without passing through login_redirect.

This means no hook priority alone can solve the problem.

A single hook with priority 999 is not enough. The solution required three interception layers:

Layer 1: login_redirect with priority 999

add_filter( 'login_redirect', [ $this, 'handle_login_redirect' ], 999, 3 );

This covers the standard login flow through /wp-login.php.

Layer 2: woocommerce_login_redirect with priority 999

add_filter( 'woocommerce_login_redirect', [ $this, 'handle_woo_login_redirect' ], 999, 2 );

WooCommerce exposes this specific filter so other plugins can override its redirect behavior. Using this filter is cleaner than trying to beat WooCommerce inside login_redirect.

Layer 3: wp_login + template_redirect (the safety net)

This is the most ingenious part. At the exact moment of login using the wp_login hook, the plugin stores the target URL inside a transient with a 90 second TTL:

public function store_redirect_on_login( $user_login, $user ) {

    $target = $this->get_target_url( $user );

    if ( ! empty( $target ) ) {
        set_transient( 'ldal_pending_' . $user->ID, $target, 90 );
    }
}

Then, during the first page load after login, regardless of which page it is, including “My Account,” the template_redirect hook with priority 1 fires before any other plugin:

public function maybe_force_redirect() {

    if ( ! is_user_logged_in() ) return;

    $user_id    = get_current_user_id();
    $target_url = get_transient( 'ldal_pending_' . $user_id );

    if ( empty( $target_url ) ) return;

    // Delete immediately. Execute only once.
    delete_transient( 'ldal_pending_' . $user_id );

    // Prevent redirect loops
    $current = ( is_ssl() ? 'https' : 'http' ) . '://' . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'];

    if ( rtrim( $current, '/' ) === rtrim( $target_url, '/' ) ) return;

    wp_redirect( $this->safe_url( $target_url, '' ), 302 );
    exit;
}

The result is that even if WooCommerce manages to send the user to “My Account” through a direct wp_redirect() call, milliseconds later the plugin captures the request and sends the student to the correct destination. The transient guarantees this only happens once and automatically expires after 90 seconds if something goes wrong.

Security: Protection Against Open Redirects

A redirect plugin must be careful with security. An Open Redirect attack consists of manipulating a redirect URL to send users to malicious websites, something commonly used in phishing campaigns.

WordPress provides the wp_validate_redirect() function specifically for this. By default, it only allows redirects to the same domain.

The plugin uses it as the first protection layer:

private function safe_url( $url, $fallback ) {

    // First attempt: native WordPress validation
    $safe = wp_validate_redirect( $url, '' );

    if ( ! empty( $safe ) ) return $safe;

    // External absolute URLs are allowed if intentionally configured by the admin
    if ( filter_var( $url, FILTER_VALIDATE_URL ) ) return $url;

    return $fallback;
}

The deliberate logic here is that external URLs are allowed because administrators may need to redirect users to external portals, SaaS tools, or communities on other domains. The assumption is that administrators configuring the plugin know what they are doing. What is never allowed is a malformed or empty URL.

How to Use the Plugin

Installation

  • Download the ZIP from GitHub: github.com/wduarte-learndash/learndash-after-login
  • Inside the WordPress dashboard, go to Plugins → Add New → Upload Plugin.
  • Upload the ZIP file and activate it.
  • The LearnDash → After Login menu will appear automatically.

Requirements: WordPress 5.8+, PHP 7.4+, LearnDash any version.

Configuring the Global Redirect

The Global Student Redirect field is the simplest one. Any student enrolled in at least one course who is not matched by a more specific rule will be sent to this URL after login.

Leaving the field blank is completely valid. The plugin simply will not interfere.

Configuring Group Based Rules

Inside the Group Based Redirects section, you can add as many rules as needed:

  • Add Group Rule opens a selector with checkboxes for all existing groups on the site. Select one or more groups and define the destination URL.
  • Add Rule for All Groups creates a rule that applies to any user who belongs to any LearnDash group.

Each rule has its own URL. If a student belongs to two groups with different rules, the first matching rule in the list will be used.

Configuring Course Based Rules

The Course Based Redirects section works the same way, but based on course enrollments:

  • Add Course Rule opens a selector with checkboxes for all published courses on the site.
  • Add Rule for All Courses applies to any student enrolled in any course.

Remember: course rules have priority over group rules and global redirects.

Saving Settings

Click Save Settings. A green confirmation message should appear within two seconds. If an error appears, verify that the plugin is active and that the nonce has not expired, which may happen if the page remains open for more than 12 hours without refreshing.

Conflict Cases and Known Limitations

  • WooCommerce: fully supported thanks to the three layer architecture described above.
  • Other redirect plugins: if you use plugins such as Peter’s Login Redirect or LoginWP, conflicts may occur. The solution depends on which plugin registers its hooks last. In these cases, test in a staging environment and adjust hook priorities if necessary.
  • AJAX login: some themes and plugins implement AJAX login flows that do not trigger the login_redirect hook conventionally. In these situations, the transient mechanism through template_redirect usually solves the issue, although there may be a one request delay.
  • Users not enrolled in any course: the plugin intentionally does not redirect users who are not enrolled in any LearnDash course. This guarantees that visitors and unrelated members are not affected.

Internationalization

The plugin has been translation ready since the first commit. All text strings use native WordPress functions:

__( 'Save Settings', 'learndash-after-login' )
_e( 'Redirect URL', 'learndash-after-login' )

The base .pot file is available in:

/languages/learndash-after-login.pot

To create a translation, use Poedit or Loco Translate and generate the corresponding .po and .mo files.

Open Source and Contributions

LearnDash After Login is a free and open source project. You can access the full repository, open issues, and submit pull requests at:

github.com/wduarte-learndash/learndash-after-login

I created this plugin because I believe fundamental tools for a great e-learning experience should not be locked behind a paywall. The LearnDash ecosystem still has a lot of room to grow in terms of native UX, and my contribution is helping fill these gaps with quality code.

If you found a bug, have a feature suggestion, or simply want to talk about LearnDash development, feel free to reach out:

📧 hello@learndashdev.com
🌐 learndashdev.com
💻 github.com/wduarte-learndash

Best regards,

Wellington Duarte
LearnDash Developer | learndashdev.com

Scroll to Top