To change the "Add to cart" text or link on a LearnDash Course product in WooCommerce, hook the learndash_woocommerce_product_add_to_cart_text filter for the label and learndash_woocommerce_product_add_to_cart_url for the link. Both run inside the add-on's Course product class and receive the current value plus the product object, so you can change the button per product, per stock state or per learner.
Two things trip people up: these filters only cover the shop and archive button, not the single product page, and a purchasable Course product's archive button is usually an AJAX button that ignores its URL. Both are covered below, with two snippets you can adapt.
Checked against: the LearnDash LMS - WooCommerce Integration 2.0.0.1 source (with LearnDash 4.18.0.1) and the current WooCommerce source on GitHub, October 2026. LearnDash 5.x is current, and LearnDash 5.2.0 moved the WooCommerce add-on's functionality into core, as our LearnDash 5 upgrade notes explain. Before you ship, confirm the filter names still exist in the copy that runs on your site.
Where the two filters run
The filters live in src/App/Models/Products/Course.php, in the class LearnDash\WooCommerce\Models\Products\Course. That class extends WC_Product and backs the "Course" product type the add-on registers (type key course). If you have not used that product type before, start with the WooCommerce Course product type explained.
| Filter | Arguments | Default value |
|---|---|---|
learndash_woocommerce_product_add_to_cart_text | $text, $product | "Add to cart" when purchasable, "Read More" when not (for example, no price set) |
learndash_woocommerce_product_add_to_cart_url | $url, $product | An ?add-to-cart=<ID> link when purchasable and in stock, otherwise the product permalink |
WooCommerce calls add_to_cart_text() and add_to_cart_url() when it renders the classic loop template (loop/add-to-cart.php) and the Product Button block, so the filters reach shop pages, category archives, related products and block-based product grids.
What these filters do not change
- The single product page. The Course class does not override
single_add_to_cart_text(). The add-on hookswoocommerce_course_add_to_cartto WooCommerce'swoocommerce_simple_add_to_cart, so the product page uses the standard simple form and WooCommerce's ownwoocommerce_product_single_add_to_cart_textfilter. - Simple or variable products linked to courses. You can link a course to a simple product or a variation too. Those use WooCommerce's classes, so the LearnDash filters never run for them.
- Cart and checkout. Course products are sold individually and are virtual. Button text has no effect on quantity or enrolment.
Snippet 1: consistent labels on archive and product pages
This sets one label for buyable courses on both the archive and the product page, and a clearer label than "Read More" when a course cannot be bought. Put it in a small site plugin, not the theme. Not run on a live site; test on staging first.
<?php
/**
* Plugin Name: AIOLMS Course Button Labels
*/
add_filter( 'learndash_woocommerce_product_add_to_cart_text', 'aiolms_course_archive_button_text', 10, 2 );
function aiolms_course_archive_button_text( $text, $product ) {
if ( ! $product instanceof WC_Product ) {
return $text;
}
return $product->is_purchasable()
? __( 'Enrol now', 'aiolms' )
: __( 'View course details', 'aiolms' );
}
add_filter( 'woocommerce_product_single_add_to_cart_text', 'aiolms_course_single_button_text', 10, 2 );
function aiolms_course_single_button_text( $text, $product ) {
if ( $product instanceof WC_Product && 'course' === $product->get_type() ) {
return __( 'Enrol now', 'aiolms' );
}
return $text;
}
Return plain text. WooCommerce escapes the label with esc_html() when it prints it, so HTML in the string shows up as literal tags.
Snippet 2: send existing learners to the course
A learner who already has access should not be offered the same course again. This version checks the course linked in the product's _related_course meta with sfwd_lms_has_access() and, when the learner has access, changes the button to "Go to course" pointing at the course permalink. It only acts when the product links exactly one course. Not run on a live site; test on staging first.
<?php
function aiolms_owned_course_id( $product ) {
static $cache = array();
if ( ! is_user_logged_in() || ! $product instanceof WC_Product || 'course' !== $product->get_type() ) {
return 0;
}
$product_id = $product->get_id();
if ( isset( $cache[ $product_id ] ) ) {
return $cache[ $product_id ];
}
$course_ids = array_filter( array_map( 'absint', (array) get_post_meta( $product_id, '_related_course', true ) ) );
$course_id = ( 1 === count( $course_ids ) ) ? (int) reset( $course_ids ) : 0;
if ( $course_id && ! sfwd_lms_has_access( $course_id, get_current_user_id() ) ) {
$course_id = 0;
}
$cache[ $product_id ] = $course_id;
return $course_id;
}
add_filter( 'learndash_woocommerce_product_add_to_cart_text', 'aiolms_owned_course_text', 20, 2 );
function aiolms_owned_course_text( $text, $product ) {
return aiolms_owned_course_id( $product ) ? __( 'Go to course', 'aiolms' ) : $text;
}
add_filter( 'learndash_woocommerce_product_add_to_cart_url', 'aiolms_owned_course_url', 20, 2 );
function aiolms_owned_course_url( $url, $product ) {
$course_id = aiolms_owned_course_id( $product );
return $course_id ? get_permalink( $course_id ) : $url;
}
add_filter( 'woocommerce_loop_add_to_cart_args', 'aiolms_owned_course_loop_args', 20, 2 );
function aiolms_owned_course_loop_args( $args, $product ) {
if ( isset( $args['class'] ) && aiolms_owned_course_id( $product ) ) {
$args['class'] = trim( str_replace( 'ajax_add_to_cart', '', $args['class'] ) );
}
return $args;
}
Two side effects to know about. Administrators, and any role allowed to auto-enrol, pass sfwd_lms_has_access() for every course, so they see "Go to course" everywhere. And if a full-page cache serves the shop to logged-in users, one learner's button can be cached for everyone; most page caches skip logged-in sessions, but check yours.
Why a custom URL can be ignored
The Course class declares support for ajax_add_to_cart. With the WooCommerce option "Enable AJAX add to cart buttons on archives" switched on (WooCommerce > Settings > Products, under "Add to cart behaviour", on by default), a purchasable, in-stock Course product's loop button gets the ajax_add_to_cart class. WooCommerce's front-end script catches the click, cancels the link and posts the product ID to the cart, so your URL is never followed.
That is why snippet 2 removes the class through woocommerce_loop_add_to_cart_args. It only works for the classic loop template. The Product Button block works out its AJAX state in its own render code and renders a <button> with no link at all, so on a block theme a custom URL only applies when AJAX archive buttons are off or "Redirect to the cart page after successful addition" is on.
Moving off the old WooCommerce filters
Before version 2.0.0, code for course products hooked WooCommerce's generic woocommerce_product_add_to_cart_text and woocommerce_product_add_to_cart_url. The Course class still runs both through apply_filters_deprecated(), so old callbacks keep working for now, but WordPress logs a deprecation notice when WP_DEBUG is on.
- Search your theme and plugins for the two generic filter names.
- If a callback only targets course products, move it to the LearnDash filter and drop any product-type check you no longer need.
- If it targets every product type, leave it on the WooCommerce filter and skip Course products, so the label is not changed twice.
Test before release
- A logged-out visitor, a learner without access and a learner with access, on the shop page, a category archive and the product page.
- A Course product with no price, which should show your "not purchasable" label.
- Both AJAX archive settings, and a block-based shop template if your theme uses one.
- The debug log, for deprecation notices from older callbacks.
For broader context on how LearnDash sells through WooCommerce, see our LearnDash guide.
How we can help
If your store mixes Course products, subscriptions and group seats and the buttons, cart and enrolment logic need to agree, our LMS plugin development team can build and test the customisation as a maintainable site plugin.
Frequently asked questions
How do I change the Add to cart text for LearnDash courses in WooCommerce?
For products of the Course type, hook learndash_woocommerce_product_add_to_cart_text, which receives the text and the product object. For the button on the single product page, also hook WooCommerce's woocommerce_product_single_add_to_cart_text and check that the product type is course.
Why does my changed add to cart URL not work on the shop page?
When AJAX add to cart buttons on archives are enabled, a purchasable Course product's button carries the ajax_add_to_cart class and WooCommerce's script adds the item by AJAX instead of following the link. Remove that class with woocommerce_loop_add_to_cart_args, or turn AJAX archive buttons off.
Why does a Course product button say Read More?
The Course class returns "Read More" when the product is not purchasable, which usually means it has no price. The button then links to the product page instead of adding to cart.
Do these filters work on simple products linked to a LearnDash course?
No. They run only in the add-on's Course product class. Simple and variable products linked to courses use WooCommerce's own classes and filters.
Free 30-minute consultation
Not sure where to start? Talk to an LMS engineer.
Tell us what platform you run and what you need. You’ll get honest, practical advice - even if the answer is that you don’t need us.
Sources & references
- LearnDash developer documentation - LearnDash
- LearnDash LMS changelog - Nexcess / LearnDash
- WooCommerce source repository - WooCommerce
Learn more about this topic with AI
Gemini opens with the prompt copied - paste it into the chat to start.