seo.php 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146
  1. <?php
  2. /**
  3. * SEO / social head metadata — favicon, OpenGraph, Twitter cards, JSON-LD.
  4. *
  5. * There's no SEO plugin, so the theme emits this itself. dw_seo_head() is hooked to
  6. * wp_head (covers every block/classic page, post, and archive). The bespoke
  7. * front-page.php deliberately skips wp_head(), so it calls dw_seo_head() directly.
  8. *
  9. * URLs are built from home_url()/get_permalink() so they stay correct through the
  10. * davidawindham.com -> davidwindham.com move. Favicon links are root-relative so every
  11. * property (WP, /work, /rtc, /photo) can point at the same shared /favicon.* at the root.
  12. */
  13. if ( ! defined( 'DW_SITE_NAME' ) ) { define( 'DW_SITE_NAME', 'David A. Windham' ); }
  14. if ( ! defined( 'DW_TWITTER' ) ) { define( 'DW_TWITTER', '@windhamdavid' ); }
  15. // Canonical brand domain (the consolidation target). The JSON-LD *identity* nodes point
  16. // here. Per-page og:url / <link rel=canonical> stay on the serving domain (home_url) so
  17. // they don't point at not-yet-migrated pages; they flip automatically when siteurl moves.
  18. if ( ! defined( 'DW_CANONICAL' ) ) { define( 'DW_CANONICAL', 'https://davidwindham.com' ); }
  19. /** Shared favicon links (root-hosted; same files served for every property). */
  20. function dw_seo_favicon() {
  21. echo "<link rel=\"icon\" href=\"/favicon.ico\" sizes=\"any\">\n";
  22. echo "<link rel=\"icon\" type=\"image/png\" sizes=\"32x32\" href=\"/favicon-32x32.png\">\n";
  23. echo "<link rel=\"icon\" type=\"image/png\" sizes=\"16x16\" href=\"/favicon-16x16.png\">\n";
  24. echo "<link rel=\"apple-touch-icon\" href=\"/apple-touch-icon.png\">\n";
  25. }
  26. /** Page description: post meta_desc field -> excerpt -> site tagline. Returns a string. */
  27. function dw_seo_description() {
  28. if ( is_singular() ) {
  29. $id = get_queried_object_id();
  30. $d = trim( (string) get_post_meta( $id, 'meta_desc', true ) );
  31. if ( $d === '' && function_exists( 'dw_good_excerpt' ) ) {
  32. ob_start();
  33. dw_good_excerpt( 300 );
  34. $d = trim( (string) ob_get_clean() );
  35. }
  36. if ( $d !== '' ) {
  37. return $d;
  38. }
  39. }
  40. $tagline = trim( (string) get_bloginfo( 'description' ) );
  41. return $tagline !== '' ? $tagline : DW_SITE_NAME;
  42. }
  43. /** OG image: the post's featured image on singulars, else the shared default. Absolute. */
  44. function dw_seo_image() {
  45. if ( is_singular() && has_post_thumbnail() ) {
  46. $src = wp_get_attachment_image_src( get_post_thumbnail_id(), 'large' );
  47. if ( $src ) {
  48. return $src[0];
  49. }
  50. }
  51. return home_url( '/og-image.jpg' ); // shared image at the web root, same-origin
  52. }
  53. /** Absolute URL for the current view (og:url). */
  54. function dw_seo_url() {
  55. if ( is_front_page() ) {
  56. return home_url( '/' );
  57. }
  58. if ( is_singular() ) {
  59. return get_permalink();
  60. }
  61. if ( is_category() || is_tag() || is_tax() ) {
  62. $link = get_term_link( get_queried_object() );
  63. if ( ! is_wp_error( $link ) ) {
  64. return $link;
  65. }
  66. }
  67. global $wp;
  68. return home_url( '/' . ltrim( $wp->request, '/' ) );
  69. }
  70. /** OpenGraph + Twitter card tags for pages / archives / front page.
  71. *
  72. * Single POSTS are intentionally skipped — dw_opengraph() in inc/tweaks.php owns their
  73. * OG, including the featured-video setup (og:video + twitter:player -> /container/ ->
  74. * single-container.php). Doubling up here would emit conflicting tags and break that. */
  75. function dw_seo_opengraph() {
  76. if ( is_single() ) {
  77. return;
  78. }
  79. $title = wp_get_document_title();
  80. $desc = dw_seo_description();
  81. $url = dw_seo_url();
  82. $img = dw_seo_image();
  83. $type = is_single() ? 'article' : 'website';
  84. printf( "<meta property=\"og:type\" content=\"%s\">\n", esc_attr( $type ) );
  85. printf( "<meta property=\"og:site_name\" content=\"%s\">\n", esc_attr( DW_SITE_NAME ) );
  86. printf( "<meta property=\"og:title\" content=\"%s\">\n", esc_attr( $title ) );
  87. printf( "<meta property=\"og:description\" content=\"%s\">\n", esc_attr( $desc ) );
  88. printf( "<meta property=\"og:url\" content=\"%s\">\n", esc_url( $url ) );
  89. printf( "<meta property=\"og:image\" content=\"%s\">\n", esc_url( $img ) );
  90. printf( "<meta name=\"twitter:card\" content=\"summary_large_image\">\n" );
  91. printf( "<meta name=\"twitter:creator\" content=\"%s\">\n", esc_attr( DW_TWITTER ) );
  92. printf( "<meta name=\"twitter:title\" content=\"%s\">\n", esc_attr( $title ) );
  93. printf( "<meta name=\"twitter:description\" content=\"%s\">\n", esc_attr( $desc ) );
  94. printf( "<meta name=\"twitter:image\" content=\"%s\">\n", esc_url( $img ) );
  95. }
  96. /** Person + WebSite JSON-LD, site-wide. */
  97. function dw_seo_jsonld() {
  98. $person = array(
  99. '@context' => 'https://schema.org',
  100. '@type' => 'Person',
  101. 'name' => DW_SITE_NAME,
  102. 'url' => DW_CANONICAL,
  103. 'image' => DW_CANONICAL . '/og-image.jpg',
  104. 'jobTitle' => 'Web Developer',
  105. 'sameAs' => array(
  106. 'https://davidwindham.com',
  107. 'https://github.com/windhamdavid',
  108. 'https://en.wikipedia.org/wiki/User:Windhamdavid',
  109. 'https://mastodon.social/@windhamdavid',
  110. 'https://www.linkedin.com/in/windhamdavid',
  111. 'https://www.facebook.com/windhamdavid',
  112. 'https://www.reddit.com/user/windhamdavid',
  113. 'https://news.ycombinator.com/user?id=windhamdavid',
  114. ),
  115. );
  116. $website = array(
  117. '@context' => 'https://schema.org',
  118. '@type' => 'WebSite',
  119. 'url' => DW_CANONICAL,
  120. 'name' => DW_SITE_NAME,
  121. 'potentialAction' => array(
  122. '@type' => 'SearchAction',
  123. 'target' => DW_CANONICAL . '/?s={search_term_string}',
  124. 'query-input' => 'required name=search_term_string',
  125. ),
  126. );
  127. echo '<script type="application/ld+json">' . wp_json_encode( $person, JSON_UNESCAPED_SLASHES ) . "</script>\n";
  128. echo '<script type="application/ld+json">' . wp_json_encode( $website, JSON_UNESCAPED_SLASHES ) . "</script>\n";
  129. }
  130. /** Emit the whole head block. Hooked to wp_head; front-page.php calls it directly. */
  131. function dw_seo_head() {
  132. dw_seo_favicon();
  133. dw_seo_opengraph();
  134. dw_seo_jsonld();
  135. }
  136. add_action( 'wp_head', 'dw_seo_head', 5 );