Browse Source

Add inc/seo.php: favicon, OpenGraph, Twitter, Person/WebSite JSON-LD

No SEO plugin, so the theme emits social/schema metadata. Hooked to wp_head for
pages/archives/front; skips single posts so dw_opengraph() in tweaks.php keeps ownership
of post OG incl. the featured-video setup (og:video / twitter:player -> single-container).

- Favicon links -> shared root-hosted /favicon.* (fixes WP pages inheriting Lychee's icon)
- OG/Twitter with absolute URLs via home_url()/get_permalink() (survive the domain move)
- Person + WebSite JSON-LD on the canonical https://davidwindham.com (https schema.org)
- front-page.php (skips wp_head) calls dw_seo_head(); dynamic canonical; http->https
  itemtypes; removed its stale duplicate JSON-LD
windhamdavid 1 week ago
parent
commit
4dc896daaa
3 changed files with 149 additions and 1 deletions
  1. 2 1
      front-page.php
  2. 1 0
      functions.php
  3. 146 0
      inc/seo.php

File diff suppressed because it is too large
+ 2 - 1
front-page.php


+ 1 - 0
functions.php

@@ -5,6 +5,7 @@ function dw_setup() {
 	require( get_template_directory() . '/inc/utils.php' );
 	require( get_template_directory() . '/inc/template.php' );
 	require( get_template_directory() . '/inc/tweaks.php' );
+	require( get_template_directory() . '/inc/seo.php' );
 	if ( ! isset( $content_width ) ) $content_width = 1310;
 	add_theme_support( 'title-tag' );
   add_theme_support( 'wp-block-styles' );

+ 146 - 0
inc/seo.php

@@ -0,0 +1,146 @@
+<?php
+/**
+ * SEO / social head metadata — favicon, OpenGraph, Twitter cards, JSON-LD.
+ *
+ * There's no SEO plugin, so the theme emits this itself. dw_seo_head() is hooked to
+ * wp_head (covers every block/classic page, post, and archive). The bespoke
+ * front-page.php deliberately skips wp_head(), so it calls dw_seo_head() directly.
+ *
+ * URLs are built from home_url()/get_permalink() so they stay correct through the
+ * davidawindham.com -> 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 / <link rel=canonical> 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 "<link rel=\"icon\" href=\"/favicon.ico\" sizes=\"any\">\n";
+	echo "<link rel=\"icon\" type=\"image/png\" sizes=\"32x32\" href=\"/favicon-32x32.png\">\n";
+	echo "<link rel=\"icon\" type=\"image/png\" sizes=\"16x16\" href=\"/favicon-16x16.png\">\n";
+	echo "<link rel=\"apple-touch-icon\" href=\"/apple-touch-icon.png\">\n";
+}
+
+/** 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 === '' && function_exists( 'dw_good_excerpt' ) ) {
+			ob_start();
+			dw_good_excerpt( 300 );
+			$d = trim( (string) ob_get_clean() );
+		}
+		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( "<meta property=\"og:type\" content=\"%s\">\n", esc_attr( $type ) );
+	printf( "<meta property=\"og:site_name\" content=\"%s\">\n", esc_attr( DW_SITE_NAME ) );
+	printf( "<meta property=\"og:title\" content=\"%s\">\n", esc_attr( $title ) );
+	printf( "<meta property=\"og:description\" content=\"%s\">\n", esc_attr( $desc ) );
+	printf( "<meta property=\"og:url\" content=\"%s\">\n", esc_url( $url ) );
+	printf( "<meta property=\"og:image\" content=\"%s\">\n", esc_url( $img ) );
+
+	printf( "<meta name=\"twitter:card\" content=\"summary_large_image\">\n" );
+	printf( "<meta name=\"twitter:creator\" content=\"%s\">\n", esc_attr( DW_TWITTER ) );
+	printf( "<meta name=\"twitter:title\" content=\"%s\">\n", esc_attr( $title ) );
+	printf( "<meta name=\"twitter:description\" content=\"%s\">\n", esc_attr( $desc ) );
+	printf( "<meta name=\"twitter:image\" content=\"%s\">\n", esc_url( $img ) );
+}
+
+/** Person + WebSite JSON-LD, site-wide. */
+function dw_seo_jsonld() {
+	$person = array(
+		'@context' => 'https://schema.org',
+		'@type'    => 'Person',
+		'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 '<script type="application/ld+json">' . wp_json_encode( $person, JSON_UNESCAPED_SLASHES ) . "</script>\n";
+	echo '<script type="application/ld+json">' . wp_json_encode( $website, JSON_UNESCAPED_SLASHES ) . "</script>\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();
+}
+add_action( 'wp_head', 'dw_seo_head', 5 );

Some files were not shown because too many files changed in this diff