davidwindham.com move. Favicon links are root-relative so every
* property (WP, /work, /rtc, /photo) can point at the same shared /favicon.* at the root.
*/
if ( ! defined( 'DW_SITE_NAME' ) ) { define( 'DW_SITE_NAME', 'David A. Windham' ); }
if ( ! defined( 'DW_TWITTER' ) ) { define( 'DW_TWITTER', '@windhamdavid' ); }
// Canonical brand domain (the consolidation target). The JSON-LD *identity* nodes point
// here. Per-page og:url / stay on the serving domain (home_url) so
// they don't point at not-yet-migrated pages; they flip automatically when siteurl moves.
if ( ! defined( 'DW_CANONICAL' ) ) { define( 'DW_CANONICAL', 'https://davidwindham.com' ); }
/** Shared favicon links (root-hosted; same files served for every property). */
function dw_seo_favicon() {
echo "\n";
echo "\n";
echo "\n";
echo "\n";
}
/** Plain-text description from a post's content WITHOUT rendering dynamic blocks.
*
* The old path (dw_good_excerpt) ran the full the_content filter here, which re-renders any
* embedded Gravity Forms block during wp_head — wasteful, and it trips a GF Post-Category
* warning on the guestbook/contact pages. excerpt_remove_blocks() drops the form (and other
* dynamic blocks) first, so only static text blocks get rendered. Returns a trimmed string. */
function dw_seo_excerpt_from_content( $id ) {
$post = get_post( $id );
if ( ! $post ) {
return '';
}
$text = trim( (string) $post->post_excerpt );
if ( $text === '' ) {
$content = (string) $post->post_content;
if ( function_exists( 'excerpt_remove_blocks' ) ) {
$content = excerpt_remove_blocks( $content ); // strip forms + other dynamic blocks
}
$text = do_blocks( $content );
}
$text = wp_strip_all_tags( strip_shortcodes( $text ) );
$text = trim( preg_replace( '/\s+/', ' ', $text ) );
if ( mb_strlen( $text ) > 300 ) {
$text = rtrim( mb_substr( $text, 0, 300 ) ) . '…';
}
return $text;
}
/** Page description: post meta_desc field -> excerpt -> site tagline. Returns a string. */
function dw_seo_description() {
if ( is_singular() ) {
$id = get_queried_object_id();
$d = trim( (string) get_post_meta( $id, 'meta_desc', true ) );
if ( $d === '' ) {
$d = dw_seo_excerpt_from_content( $id );
}
if ( $d !== '' ) {
return $d;
}
}
$tagline = trim( (string) get_bloginfo( 'description' ) );
return $tagline !== '' ? $tagline : DW_SITE_NAME;
}
/** OG image: the post's featured image on singulars, else the shared default. Absolute. */
function dw_seo_image() {
if ( is_singular() && has_post_thumbnail() ) {
$src = wp_get_attachment_image_src( get_post_thumbnail_id(), 'large' );
if ( $src ) {
return $src[0];
}
}
return home_url( '/og-image.jpg' ); // shared image at the web root, same-origin
}
/** Absolute URL for the current view (og:url). */
function dw_seo_url() {
if ( is_front_page() ) {
return home_url( '/' );
}
if ( is_singular() ) {
return get_permalink();
}
if ( is_category() || is_tag() || is_tax() ) {
$link = get_term_link( get_queried_object() );
if ( ! is_wp_error( $link ) ) {
return $link;
}
}
global $wp;
return home_url( '/' . ltrim( $wp->request, '/' ) );
}
/** OpenGraph + Twitter card tags for pages / archives / front page.
*
* Single POSTS are intentionally skipped — dw_opengraph() in inc/tweaks.php owns their
* OG, including the featured-video setup (og:video + twitter:player -> /container/ ->
* single-container.php). Doubling up here would emit conflicting tags and break that. */
function dw_seo_opengraph() {
if ( is_single() ) {
return;
}
$title = wp_get_document_title();
$desc = dw_seo_description();
$url = dw_seo_url();
$img = dw_seo_image();
$type = is_single() ? 'article' : 'website';
printf( "\n", esc_attr( $type ) );
printf( "\n", esc_attr( DW_SITE_NAME ) );
printf( "\n", esc_attr( $title ) );
printf( "\n", esc_attr( $desc ) );
printf( "\n", esc_url( $url ) );
printf( "\n", esc_url( $img ) );
printf( "\n" );
printf( "\n", esc_attr( DW_TWITTER ) );
printf( "\n", esc_attr( $title ) );
printf( "\n", esc_attr( $desc ) );
printf( "\n", esc_url( $img ) );
}
/** Person + WebSite JSON-LD, site-wide. */
function dw_seo_jsonld() {
$person = array(
'@context' => 'https://schema.org',
'@type' => 'Person',
'@id' => DW_CANONICAL . '/#person', // stable identity per-page nodes reference
'name' => DW_SITE_NAME,
'url' => DW_CANONICAL,
'image' => DW_CANONICAL . '/og-image.jpg',
'jobTitle' => 'Web Developer',
'sameAs' => array(
'https://davidwindham.com',
'https://github.com/windhamdavid',
'https://en.wikipedia.org/wiki/User:Windhamdavid',
'https://mastodon.social/@windhamdavid',
'https://www.linkedin.com/in/windhamdavid',
'https://www.facebook.com/windhamdavid',
'https://www.reddit.com/user/windhamdavid',
'https://news.ycombinator.com/user?id=windhamdavid',
),
);
$website = array(
'@context' => 'https://schema.org',
'@type' => 'WebSite',
'url' => DW_CANONICAL,
'name' => DW_SITE_NAME,
'potentialAction' => array(
'@type' => 'SearchAction',
'target' => DW_CANONICAL . '/?s={search_term_string}',
'query-input' => 'required name=search_term_string',
),
);
echo '\n";
echo '\n";
}
/** BlogPosting JSON-LD on single posts. author/publisher reference the site Person
* (@id) emitted by dw_seo_jsonld() on the same page, so it all resolves to one entity. */
function dw_seo_jsonld_article() {
if ( ! is_single() ) {
return;
}
$id = get_queried_object_id();
$url = get_permalink( $id );
$author = array( '@type' => 'Person', '@id' => DW_CANONICAL . '/#person', 'name' => DW_SITE_NAME );
$node = array(
'@context' => 'https://schema.org',
'@type' => 'BlogPosting',
'@id' => $url . '#article',
'mainEntityOfPage' => $url,
'url' => $url,
'headline' => get_the_title( $id ),
'datePublished' => get_the_date( 'c', $id ),
'dateModified' => get_the_modified_date( 'c', $id ),
'author' => $author,
'publisher' => $author,
'image' => dw_seo_image(),
'description' => dw_seo_description(),
);
echo '\n";
}
/** BreadcrumbList JSON-LD — hierarchical pages (Home > ancestors > page), posts
* (Home > first category > post), and term archives (Home > term). */
function dw_seo_jsonld_breadcrumbs() {
if ( is_front_page() ) {
return;
}
$crumbs = array( array( 'name' => 'Home', 'url' => home_url( '/' ) ) );
if ( is_singular() ) {
$id = get_queried_object_id();
if ( is_page() ) {
foreach ( array_reverse( get_post_ancestors( $id ) ) as $ancestor ) {
$crumbs[] = array( 'name' => get_the_title( $ancestor ), 'url' => get_permalink( $ancestor ) );
}
} elseif ( is_single() ) {
$cats = get_the_category( $id );
if ( ! empty( $cats ) ) {
$crumbs[] = array( 'name' => $cats[0]->name, 'url' => get_category_link( $cats[0]->term_id ) );
}
}
$crumbs[] = array( 'name' => get_the_title( $id ), 'url' => get_permalink( $id ) );
} elseif ( is_category() || is_tag() || is_tax() ) {
$term = get_queried_object();
$link = get_term_link( $term );
$crumbs[] = array( 'name' => $term->name, 'url' => is_wp_error( $link ) ? home_url( '/' ) : $link );
} else {
return; // home / search / date views get no meaningful trail
}
if ( count( $crumbs ) < 2 ) {
return;
}
$items = array();
foreach ( $crumbs as $i => $c ) {
$items[] = array(
'@type' => 'ListItem',
'position' => $i + 1,
'name' => $c['name'],
'item' => $c['url'],
);
}
$node = array(
'@context' => 'https://schema.org',
'@type' => 'BreadcrumbList',
'itemListElement' => $items,
);
echo '\n";
}
/** Emit the whole head block. Hooked to wp_head; front-page.php calls it directly. */
function dw_seo_head() {
dw_seo_favicon();
dw_seo_opengraph();
dw_seo_jsonld();
dw_seo_jsonld_article();
dw_seo_jsonld_breadcrumbs();
}
add_action( 'wp_head', 'dw_seo_head', 5 );