1. Dynamic Canonical URLs Without a Plugin
A canonical tag tells search engines which version of a page is the "real" one — critical when the same content is reachable through more than one URL (pagination, tracking parameters, or a taxonomy archive that duplicates a page). Get this wrong and Google can split ranking signals across duplicate URLs instead of consolidating them onto one.
Where to insert: functions.php
add_action( 'wp_head', 'dn_canonical_url', 1 );
function dn_canonical_url() {
global $wp;
$canonical = home_url( add_query_arg( array(), $wp->request ) );
if ( is_singular() ) {
$canonical = get_permalink();
}
if ( is_front_page() ) {
$canonical = home_url( '/' );
}
echo '<link rel="canonical" href="' . esc_url( $canonical ) . '" />' . "\n";
}
Step-by-step:
- Paste the snippet into
functions.php. - View source on a single post, a page, and your homepage — each should now have a
<link rel="canonical">tag pointing to its own clean URL. - If your theme or another plugin already outputs a canonical tag, check your page source for duplicates — having two canonical tags is worse than having none, since it re-introduces the ambiguity you're trying to fix.
- Test a URL with a tracking parameter added (like
?utm_source=test) — the canonical tag should still point to the clean version without that parameter.
2. Dynamic Open Graph and Twitter Card Meta Tags
Without Open Graph tags, sharing your post on Facebook or LinkedIn pulls in whatever image and text those platforms can scrape blindly — often the wrong image, a broken preview, or no preview at all. These tags explicitly tell social platforms what title, description, and image to display.
Where to insert: functions.php
add_action( 'wp_head', 'dn_open_graph_tags', 5 );
function dn_open_graph_tags() {
if ( ! is_singular() ) {
return;
}
global $post;
$title = get_the_title();
$description = has_excerpt() ? get_the_excerpt() : wp_trim_words( strip_tags( $post->post_content ), 30 );
$url = get_permalink();
$image = has_post_thumbnail()
? get_the_post_thumbnail_url( $post->ID, 'large' )
: get_theme_mod( 'dn_default_share_image', home_url( '/default-share-image.jpg' ) );
?>
<meta property="og:type" content="article" />
<meta property="og:title" content="<?php echo esc_attr( $title ); ?>" />
<meta property="og:description" content="<?php echo esc_attr( $description ); ?>" />
<meta property="og:url" content="<?php echo esc_url( $url ); ?>" />
<meta property="og:image" content="<?php echo esc_url( $image ); ?>" />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content="<?php echo esc_attr( $title ); ?>" />
<meta name="twitter:description" content="<?php echo esc_attr( $description ); ?>" />
<meta name="twitter:image" content="<?php echo esc_url( $image ); ?>" />
<?php
}
Step-by-step:
- Paste the snippet into
functions.php, and replace/default-share-image.jpgwith a real fallback image path for posts that have no featured image. - Publish or update a post, then test the URL in Facebook's Sharing Debugger or Twitter/X's Card Validator — both let you preview exactly what will show when the link is shared.
- If a stale preview shows up (old title/image from before you added this snippet), use the "Scrape Again" button in the Sharing Debugger — social platforms cache previews and won't refresh automatically.
3. Custom SEO Title and Meta Description (No Yoast Required)
The page <title> tag and meta description are still two of the most direct ranking and click-through-rate levers you control — and WordPress's defaults are rarely optimized for either. This snippet lets you override both per-post using a plain custom field, no plugin UI needed.
Where to insert: functions.php
// Override the <title> tag when a custom SEO title is set.
add_filter( 'pre_get_document_title', 'dn_custom_seo_title' );
function dn_custom_seo_title( $title ) {
if ( is_singular() ) {
$custom = get_post_meta( get_the_ID(), '_dn_seo_title', true );
if ( $custom ) {
return $custom . ' | ' . get_bloginfo( 'name' );
}
}
return $title;
}
// Output a meta description, using a custom field if set, otherwise
// falling back to the excerpt or the first 30 words of the content.
add_action( 'wp_head', 'dn_meta_description', 1 );
function dn_meta_description() {
if ( ! is_singular() ) {
return;
}
$custom = get_post_meta( get_the_ID(), '_dn_meta_description', true );
$description = $custom ? $custom : ( has_excerpt() ? get_the_excerpt() : wp_trim_words( strip_tags( get_the_content() ), 30 ) );
echo '<meta name="description" content="' . esc_attr( $description ) . '" />' . "\n";
}
Step-by-step:
- Paste the snippet into
functions.php. - Open any post in the block editor, scroll to the Custom Fields panel (enable it from the three-dot menu → Preferences → Panels if it's hidden), and add a field named
_dn_seo_titleand/or_dn_meta_descriptionwith your desired values. - View the page source — the
<title>and meta description should reflect your custom values instead of the defaults. - Leave the custom fields empty on posts where the default title/excerpt is already good — this snippet only overrides when a value is actually set.
4. Article Schema (JSON-LD) for Blog Posts
Structured data doesn't move rankings directly, but it's what makes Google eligible to show rich results — author info, publish dates, and enhanced snippets in search. Without it, your posts are just plain blue links competing against ones that get extra visual real estate.
Where to insert: functions.php
add_action( 'wp_head', 'dn_article_schema' );
function dn_article_schema() {
if ( ! is_singular( 'post' ) ) {
return;
}
global $post;
$schema = array(
'@context' => 'https://schema.org',
'@type' => 'BlogPosting',
'headline' => get_the_title(),
'description' => has_excerpt() ? get_the_excerpt() : wp_trim_words( strip_tags( $post->post_content ), 30 ),
'datePublished' => get_the_date( 'c' ),
'dateModified' => get_the_modified_date( 'c' ),
'author' => array( '@type' => 'Person', 'name' => get_the_author() ),
'mainEntityOfPage' => array( '@type' => 'WebPage', '@id' => get_permalink() ),
);
if ( has_post_thumbnail() ) {
$schema['image'] = get_the_post_thumbnail_url( $post->ID, 'large' );
}
echo '<script type="application/ld+json">' . wp_json_encode( $schema ) . '</script>' . "\n";
}
Step-by-step:
- Paste the snippet into
functions.php. - Open any published post and copy its URL into Google's Rich Results Test tool.
- Confirm it detects a "BlogPosting" item with no errors — warnings about optional fields (like a missing publisher logo) are safe to ignore for a basic setup.
- Rich results aren't guaranteed even with valid schema — Google decides when to show them — but invalid or missing schema guarantees you're never considered for them.
5. Breadcrumbs With BreadcrumbList Schema
Breadcrumbs help both users and search engines understand where a page sits in your site's structure — and when marked up with schema, Google can display that breadcrumb trail directly in the search result instead of the raw URL, which measurably improves click-through rate.
Where to insert: the function goes in functions.php; the call goes in your theme's single.php or page.php template, wherever you want the breadcrumb trail to appear (typically right above the page title).
// In functions.php
function dn_breadcrumbs() {
if ( is_front_page() ) {
return;
}
$items = array( array( 'name' => 'Home', 'url' => home_url( '/' ) ) );
if ( is_singular( 'post' ) ) {
$categories = get_the_category();
if ( ! empty( $categories ) ) {
$items[] = array( 'name' => $categories[0]->name, 'url' => get_category_link( $categories[0]->term_id ) );
}
$items[] = array( 'name' => get_the_title(), 'url' => get_permalink() );
} elseif ( is_page() ) {
$items[] = array( 'name' => get_the_title(), 'url' => get_permalink() );
}
echo '<nav class="dn-breadcrumbs" aria-label="Breadcrumb"><ol>';
foreach ( $items as $i => $item ) {
echo '<li>';
if ( $i < count( $items ) - 1 ) {
echo '<a href="' . esc_url( $item['url'] ) . '">' . esc_html( $item['name'] ) . '</a> » ';
} else {
echo esc_html( $item['name'] );
}
echo '</li>';
}
echo '</ol></nav>';
$list_items = array();
foreach ( $items as $i => $item ) {
$list_items[] = array(
'@type' => 'ListItem',
'position' => $i + 1,
'name' => $item['name'],
'item' => $item['url'],
);
}
$schema = array(
'@context' => 'https://schema.org',
'@type' => 'BreadcrumbList',
'itemListElement' => $list_items,
);
echo '<script type="application/ld+json">' . wp_json_encode( $schema ) . '</script>';
}
// In single.php or page.php, wherever the breadcrumb trail should render:
<?php dn_breadcrumbs(); ?>
Step-by-step:
- Paste the function into
functions.php. - Open your theme's
single.php(for blog posts) andpage.php(for pages), and add<?php dn_breadcrumbs(); ?>just above the main title. - Style the
.dn-breadcrumbsclass in your theme's CSS to match your site's look — the function only outputs semantic markup, no styling is applied by default. - Run the page through Google's Rich Results Test again to confirm the BreadcrumbList schema validates alongside your Article schema from step 4.
Why Skip the Full SEO Plugin?
Plugins like Yoast and Rank Math are genuinely useful for sites that need a content-editor-friendly interface, XML sitemaps, redirect management, and a dozen other features in one place. But if all you actually use is the title/description override, Open Graph tags, and schema output, you're loading an entire settings framework, several database tables, and admin-side JS/CSS for a handful of features that these five snippets cover directly — with zero admin UI overhead on every single page load.
Prefer these as ready-to-copy cards with a live preview? Check the WordPress PHP Functions & Hooks category in our WordPress Snippet Generator.
Comments & Feature Requests
0 Found a bug, or want a new tool? Let us know below.
comments_no_comments