To override Yoast SEO's title and meta description in PHP, hook the wpseo_title and wpseo_metadesc filters and return your replacement string. Drop this into a small must-use plugin (or functions.php):
add_filter( 'wpseo_title', function ( $title ) {
if ( is_page( 'pricing' ) ) {
return 'Pricing and Plans | Acme';
}
return $title;
} );
add_filter( 'wpseo_metadesc', function ( $description ) {
if ( is_page( 'pricing' ) ) {
return 'Compare Acme plans and pricing. Monthly and annual billing, no setup fees.';
}
return $description;
} );Both filters pass you the value Yoast was about to print. Return a different string to replace it, or return the value untouched to leave Yoast alone. That is the whole mechanism: every Yoast frontend tag has a filter, and each one expects a string back. The Yoast Metadata API docs list the full set.
The reason to reach for these filters is the case Yoast's UI cannot handle: a page whose content is generated, not stored as a post. A rewrite-driven endpoint, a search-results view, a custom archive built from a query var, a virtual page assembled in a template. Yoast has no meta box to fill in for those, because there is no post to attach it to. The filters are how you set the title and description anyway.
The full set of Yoast frontend filters
Yoast runs every meta tag it prints through a named filter. The frontend ones you will actually use:
| Filter | Controls | Returns |
|---|---|---|
wpseo_title | <title> text | The page title string |
wpseo_metadesc | <meta name="description"> | The meta description string |
wpseo_canonical | <link rel="canonical"> | The canonical URL |
wpseo_robots | <meta name="robots"> | The robots directive string |
wpseo_opengraph_title | og:title | The Open Graph title |
wpseo_opengraph_desc | og:description | The Open Graph description |
wpseo_opengraph_image | og:image | The Open Graph image URL |
wpseo_twitter_title | twitter:title | The X (Twitter) card title |
wpseo_twitter_description | twitter:description | The X (Twitter) card description |
Two things to know before you wire all of these up.
First, the social filters do not inherit from wpseo_title and wpseo_metadesc. If you override the page title with wpseo_title but leave wpseo_opengraph_title alone, Yoast will still build the Open Graph title from its own template, and your share previews on Facebook and X will not match the <title> you set. When you change a page's title or description programmatically, set the matching OG and Twitter filters too, or accept that the social cards drift from the page.
Second, current Yoast passes a second argument to most of these callbacks: a presentation object carrying the page's context (the indexable, the queried object, and so on). You can ask for it with the standard add_filter arity and use it to decide what to return. The add_filter reference is add_filter( $hook, $callback, $priority, $accepted_args ); set $accepted_args to 2 to receive it:
add_filter( 'wpseo_opengraph_title', function ( $og_title, $presentation ) {
// $presentation->model is the Yoast indexable for this page.
if ( is_singular( 'product' ) ) {
return get_the_title() . ' | Acme Store';
}
return $og_title;
}, 10, 2 );If you do not need the context, omit the arity and just take the string. Both styles are fine; the second argument is there when the first is not enough to decide.
Setting canonical, Open Graph, and Twitter together
When a programmatic page needs its full social and canonical metadata set, override the cluster of filters in one plugin so they stay consistent. Here a rewrite-driven page is keyed off a custom query var (the rewrite itself is out of scope here; see the cross-links at the end):
add_action( 'init', function () {
add_filter( 'wpseo_title', 'te_yoast_report_title' );
add_filter( 'wpseo_metadesc', 'te_yoast_report_metadesc' );
add_filter( 'wpseo_canonical', 'te_yoast_report_canonical' );
add_filter( 'wpseo_opengraph_title', 'te_yoast_report_title' );
add_filter( 'wpseo_opengraph_desc', 'te_yoast_report_metadesc' );
add_filter( 'wpseo_twitter_title', 'te_yoast_report_title' );
add_filter( 'wpseo_twitter_description', 'te_yoast_report_metadesc' );
add_filter( 'wpseo_opengraph_image', 'te_yoast_report_image' );
} );
function te_yoast_report_slug() {
$slug = get_query_var( 'report_slug' );
return $slug ? sanitize_title( $slug ) : '';
}
function te_yoast_report_title( $title ) {
$slug = te_yoast_report_slug();
if ( ! $slug ) {
return $title;
}
$name = ucwords( str_replace( '-', ' ', $slug ) );
return sprintf( '%s Report | Acme', $name );
}
function te_yoast_report_metadesc( $description ) {
$slug = te_yoast_report_slug();
if ( ! $slug ) {
return $description;
}
$name = ucwords( str_replace( '-', ' ', $slug ) );
return sprintf( 'Latest %s data, updated daily. Charts, trends, and the raw numbers.', $name );
}
function te_yoast_report_canonical( $canonical ) {
$slug = te_yoast_report_slug();
if ( ! $slug ) {
return $canonical;
}
return home_url( '/reports/' . $slug . '/' );
}
function te_yoast_report_image( $image ) {
$slug = te_yoast_report_slug();
if ( ! $slug ) {
return $image;
}
return home_url( '/reports/' . $slug . '/share.png' );
}Reusing one title callback for wpseo_title, wpseo_opengraph_title, and wpseo_twitter_title is the simplest way to keep the three in lockstep. The wpseo_canonical override matters on rewrite-driven pages especially: without it, Yoast often canonicalizes a virtual URL to something wrong (the front page, or a guessed permalink), and the wrong canonical is worse than none.
Conditional overrides per context
The whole game is deciding when to override and returning the original string otherwise. Use WordPress's conditional tags, and remember they only work after the query is parsed, so attach the filters on a hook that runs late enough (wp, template_redirect, or just registering the add_filter calls normally so they fire at output time). A few patterns I reach for:
add_filter( 'wpseo_title', function ( $title ) {
// A specific page by slug.
if ( is_page( 'contact' ) ) {
return 'Contact Acme Support';
}
// A whole custom post type archive.
if ( is_post_type_archive( 'event' ) ) {
return 'Upcoming Events | Acme';
}
// A specific taxonomy term.
if ( is_tax( 'region', 'emea' ) ) {
return 'EMEA Coverage | Acme';
}
// Any single product.
if ( is_singular( 'product' ) ) {
return get_the_title() . ' | Acme Store';
}
// A rewrite-driven page identified by a custom query var.
if ( get_query_var( 'report_slug' ) ) {
return te_yoast_report_title( $title );
}
return $title;
} );is_page() takes a slug, ID, or title. is_tax() takes a taxonomy and an optional term. is_singular() and is_post_type_archive() take a post type. get_query_var() is what makes rewrite-driven pages addressable, which is exactly the case Yoast's UI leaves you stranded on. Keep the bare return $title; at the end so every page you did not name keeps Yoast's normal output. A filter that forgets the fall-through rewrites the title of your entire site.
Extending Yoast's value instead of replacing it
Sometimes you do not want to throw away what an editor typed into the Yoast meta box, you want to build on it. Yoast stores its per-post overrides in standard post meta, so you can read the editor's title or description and append to it. The stored keys:
| Meta key | Holds |
|---|---|
_yoast_wpseo_title | The SEO title the editor set in the meta box |
_yoast_wpseo_metadesc | The meta description the editor set |
_yoast_wpseo_canonical | A manual canonical override, if set |
Read them with get_post_meta, passing true as the third argument to get a single value rather than an array:
add_filter( 'wpseo_metadesc', function ( $description ) {
if ( ! is_singular( 'product' ) ) {
return $description;
}
$stored = get_post_meta( get_the_ID(), '_yoast_wpseo_metadesc', true );
if ( $stored ) {
// Editor wrote one. Append a stock-status note rather than replace it.
$in_stock = get_post_meta( get_the_ID(), '_stock_status', true ) === 'instock';
return $stored . ' ' . ( $in_stock ? 'In stock now.' : 'Currently on backorder.' );
}
return $description;
} );One sharp edge here: the value Yoast prints is not always the raw value in _yoast_wpseo_title. Yoast templates support %% variables, so an editor may have stored %%title%% %%sep%% %%sitename%%. The string Yoast hands your wpseo_title filter is the expanded result, but the string you read straight out of get_post_meta is the unexpanded template with the %% tokens still in it. If you want the rendered text, prefer the value the filter already gives you; only drop to get_post_meta when you specifically need the editor's raw input. Mixing the two (reading raw meta and printing it where the expanded title belonged) is how %%sep%% ends up visible in someone's <title>.
Put it in a must-use plugin
I do not keep this in functions.php. Meta overrides that vanish when a theme changes are a nasty surprise to debug later, and a site-wide title rule belongs to the site, not the theme. Drop it into wp-content/mu-plugins/te-yoast-meta.php, where it loads automatically and survives theme switches:
<?php
/**
* Plugin Name: TE Yoast Meta
* Plugin URI: https://techearl.com/wordpress-override-yoast-title-meta
* Description: Programmatic overrides for Yoast SEO title, meta description, canonical, and social tags on dynamic and rewrite-driven pages.
* Version: 1.0.0
* Author: Ishan Karunaratne
* Author URI: https://techearl.com
* License: GPL-2.0-or-later
* Text Domain: te-yoast-meta
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
add_action( 'wp', function () {
if ( is_admin() ) {
return;
}
add_filter( 'wpseo_title', 'te_yoast_filter_title' );
add_filter( 'wpseo_metadesc', 'te_yoast_filter_metadesc' );
add_filter( 'wpseo_opengraph_title', 'te_yoast_filter_title' );
add_filter( 'wpseo_opengraph_desc', 'te_yoast_filter_metadesc' );
add_filter( 'wpseo_twitter_title', 'te_yoast_filter_title' );
add_filter( 'wpseo_twitter_description', 'te_yoast_filter_metadesc' );
} );
function te_yoast_filter_title( $title ) {
if ( get_query_var( 'report_slug' ) ) {
$name = ucwords( str_replace( '-', ' ', sanitize_title( get_query_var( 'report_slug' ) ) ) );
return sprintf( '%s Report | Acme', $name );
}
return $title;
}
function te_yoast_filter_metadesc( $description ) {
if ( get_query_var( 'report_slug' ) ) {
$name = ucwords( str_replace( '-', ' ', sanitize_title( get_query_var( 'report_slug' ) ) ) );
return sprintf( 'Latest %s data, refreshed daily.', $name );
}
return $description;
}The is_admin() guard keeps the filters off the admin screens, where you want Yoast's own values so the editor sees what they configured. The wp hook fires after the main query is parsed, so the conditional tags and get_query_var() work inside the callbacks.
Verify it from the command line
Do not trust the browser cache or your memory of what you set. Fetch the page and grep the tags out of the raw HTML:
curl -s https://example.com/reports/uptime/ | grep -iE '<title>|og:title|twitter:title|name="description"|rel="canonical"'You want the <title>, the og:title, the twitter:title, the description, and the canonical all reflecting your override and all agreeing with each other. If the <title> changed but og:title did not, you forgot the Open Graph filter. If nothing changed at all, the usual causes are a page cache serving stale HTML (purge it), the filter attached too early to see the query var, or a conditional that never matches. Add a quick error_log() inside the callback to confirm it is even running.
See also
- Override SEO Titles and Meta Descriptions Without a Plugin: the core-WordPress approach with
document_title_partsandwp_head, for sites not running Yoast - Override Rank Math's Title and Meta Description in PHP: the same job for the other big SEO plugin, with its own
rank_math/frontend/*filter set - Clean Up wp_head in WordPress: trimming the default head output around the meta tags you are now controlling, so the
<head>stays lean
Sources
Authoritative references this article was fact-checked against.
- Metadata API: Yoast SEO developer documentationdeveloper.yoast.com
- add_filter(): WordPress Developer Referencedeveloper.wordpress.org
- get_post_meta(): WordPress Developer Referencedeveloper.wordpress.org





