Instantly share code, notes, and snippets.
Created
December 5, 2022 17:31
-
Star
(1)
1
You must be signed in to star a gist -
Fork
(1)
1
You must be signed in to fork a gist
-
Save spaceninja/d6dcb0e891d384186b93a2cbffb17fda to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace CloudFour\Helpers; | |
use Timber\Image; | |
use Timber\ImageHelper; | |
use Timber\Post; | |
use Timber\Term; | |
use Timber\TextHelper; | |
use Timber\User; | |
use function CloudFour\Helpers\attribute_safe_string; | |
use function CloudFour\Helpers\get_archive_title; | |
use function CloudFour\Helpers\get_default_feature_image; | |
use function CloudFour\TwigFilters\markdown; | |
/** | |
* Trim and Make Descriptions Safe for use as Attributes | |
* | |
* @param string $string | |
* @return string | |
*/ | |
function format_description($string) | |
{ | |
// Use site description as a fallback value | |
if (!$string) { | |
$string = get_bloginfo('description'); | |
} | |
return attribute_safe_string(TextHelper::trim_words($string)); | |
} | |
/** | |
* Add Author Open Graph Tags | |
* | |
* Outputs the relevant open graph tags for a post's author. | |
* | |
* @param \Timber\User $author | |
* @return mixed[] | |
*/ | |
function add_article_author_open_graph_tags($author) | |
{ | |
$author_tags = [ | |
[ | |
'property' => 'article:author', | |
'content' => $author->link(), | |
], | |
[ | |
'property' => 'profile:first_name', | |
'content' => $author->first_name, | |
], | |
[ | |
'property' => 'profile:last_name', | |
'content' => $author->last_name, | |
], | |
]; | |
// Get the user links, and if they have a twitter link, | |
// add the `twitter:creator` tag | |
$user_links = $author->get_meta_field('user_links'); | |
if ($user_links) { | |
foreach ($user_links as $link) { | |
if (strtolower($link['title']) == 'twitter') { | |
$path = parse_url($link['url'], PHP_URL_PATH); | |
if ($path) { | |
$username = trim($path, '/'); | |
if ($username) { | |
$author_tags[] = [ | |
'property' => 'twitter:creator', | |
'content' => '@' . $username, | |
]; | |
} | |
} | |
} | |
} | |
} | |
return $author_tags; | |
} | |
/** | |
* Add Open Graph Tags | |
* | |
* Extremely helpful reference links: | |
* | |
* @link https://ogp.me/ | |
* @link https://developers.facebook.com/docs/sharing/webmasters/images/ | |
* @link https://developer.twitter.com/en/docs/twitter-for-websites/cards/overview/markup | |
* @link https://developer.yoast.com/features/opengraph/functional-specification/ | |
* @link https://developer.yoast.com/features/twitter/functional-specification/ | |
* @link https://indieweb.org/The-Open-Graph-protocol | |
* | |
* Note: If you read those docs, you may find that you don't need to | |
* set some `og` tags if a corresponding HTML tag exists. | |
* | |
* e.g., in theory, you can skip `og:title` and it'll use the `title` element, | |
* or `og:description` can be dropped in favor of `<meta name="description">`. | |
* | |
* In practice, they're saying _Facebook_ doesn't care, but any number of | |
* other tools that use the `og` tags might, such as Slack or Apple News. | |
* | |
* As a result, we've opted for a more thorough approach than may be strictly | |
* necessary to ensure we don't unintentionally break other integrations. | |
*/ | |
function add_open_graph_tags() | |
{ | |
echo "\n<!-- Theme Open Graph Tags -->\n"; | |
global $site; | |
// Save the site name since we reference it repeatedly | |
$site_name = get_bloginfo('name'); | |
$site_description = get_bloginfo('description'); | |
// Define fallback image, for use in Image OG Tags below | |
$image = new Image( | |
$site->patterns->assets_directory_uri . '/favicons/icon-512.png' | |
); | |
// Common OG tags for all pages | |
$open_graph_tags = [ | |
['property' => 'og:site_name', 'content' => $site_name], | |
['property' => 'og:locale', 'content' => get_locale()], | |
['property' => 'twitter:site', 'content' => '@cloudfour'], | |
]; | |
// Homepage OG tags | |
// https://cloudfour.com/ | |
if (is_front_page()) { | |
echo "<!-- This is the homepage -->\n"; | |
$open_graph_tags = array_merge($open_graph_tags, [ | |
['property' => 'og:type', 'content' => 'website'], | |
['property' => 'og:url', 'content' => get_bloginfo('url')], | |
['property' => 'og:title', 'content' => $site_name], | |
[ | |
'property' => 'og:description', | |
'content' => format_description($site_description), | |
], | |
[ | |
'name' => 'description', | |
'content' => format_description($site_description), | |
], | |
]); | |
} | |
// Page OG tags | |
// page: https://cloudfour.com/is/ | |
elseif (is_page()) { | |
echo "<!-- This is a page -->\n"; | |
$timber_post = new Post(); | |
$page_excerpt = has_excerpt() | |
? // @phpstan-ignore-next-line read_more accepts a boolean | |
$timber_post->preview()->read_more(false) | |
: ''; | |
$page_subhead = $timber_post->meta('subhead'); | |
$page_description = $page_excerpt ?: $page_subhead; | |
$open_graph_tags = array_merge($open_graph_tags, [ | |
// The correct `og:type` for pages is debatable. Yoast uses `article`, | |
// but we don't treat pages like articles, we treat them as | |
// relatively static content, so I think `website` is a better fit. | |
['property' => 'og:type', 'content' => 'website'], | |
['property' => 'og:url', 'content' => $timber_post->link()], | |
['property' => 'og:title', 'content' => $timber_post->title()], | |
// Description tries to use excerpt, followed by subhead, if either is set. | |
[ | |
'property' => 'og:description', | |
'content' => format_description($page_description), | |
], | |
[ | |
'name' => 'description', | |
'content' => format_description($page_description), | |
], | |
]); | |
// Set the image if it exists, for use in Image OG Tags below | |
if ($timber_post->thumbnail()) { | |
$image = $timber_post->thumbnail(); | |
$open_graph_tags[] = [ | |
'property' => 'twitter:card', | |
'content' => 'summary_large_image', | |
]; | |
} | |
} | |
// Talk OG tags | |
// https://cloudfour.com/presents/planning-your-pwa/ | |
elseif (get_post_type() == 'c4_talk') { | |
echo "<!-- This is a talk -->\n"; | |
$timber_post = new Post(); | |
$talk_description = property_exists($timber_post, 'description') | |
? $timber_post->description | |
: ''; | |
$open_graph_tags = array_merge($open_graph_tags, [ | |
['property' => 'og:type', 'content' => 'article'], | |
['property' => 'og:url', 'content' => $timber_post->link()], | |
['property' => 'og:title', 'content' => $timber_post->title()], | |
[ | |
'property' => 'og:description', | |
'content' => format_description($talk_description), | |
], | |
[ | |
'name' => 'description', | |
'content' => format_description($talk_description), | |
], | |
[ | |
'property' => 'article:published_time', | |
'content' => get_post_time('c', true), | |
], | |
[ | |
'property' => 'article:modified_time', | |
'content' => get_post_modified_time('c', true), | |
], | |
]); | |
// Add the presenters | |
foreach (get_field('presenters') as $presenter) { | |
$timber_presenter = new User($presenter['ID']); | |
$open_graph_tags = array_merge( | |
$open_graph_tags, | |
add_article_author_open_graph_tags($timber_presenter) | |
); | |
} | |
// Set the image if it exists, for use in Image OG Tags below | |
if (property_exists($timber_post, 'featured_image')) { | |
$image = new Image($timber_post->featured_image); | |
$open_graph_tags[] = [ | |
'property' => 'twitter:card', | |
'content' => 'summary_large_image', | |
]; | |
} | |
} | |
// Single Post OG tags | |
// image: https://cloudfour.com/thinks/faster-integration-with-web-components/ | |
// no image: https://cloudfour.com/thinks/component-specific-design-tokens/ | |
elseif (is_single()) { | |
echo "<!-- This is a single post -->\n"; | |
$timber_post = new Post(); | |
// @phpstan-ignore-next-line read_more accepts a boolean | |
$post_excerpt = $timber_post->preview()->read_more(false); | |
$open_graph_tags = array_merge($open_graph_tags, [ | |
['property' => 'og:type', 'content' => 'article'], | |
['property' => 'og:url', 'content' => $timber_post->link()], | |
['property' => 'og:title', 'content' => $timber_post->title()], | |
[ | |
'property' => 'og:description', | |
'content' => format_description($post_excerpt), | |
], | |
[ | |
'name' => 'description', | |
'content' => format_description($post_excerpt), | |
], | |
[ | |
'property' => 'article:published_time', | |
'content' => get_post_time('c', true), | |
], | |
[ | |
'property' => 'article:modified_time', | |
'content' => get_post_modified_time('c', true), | |
], | |
[ | |
// WP allows multiple categories, but OG only accepts one section. | |
// We're outputting the rest below as tags. | |
'property' => 'article:section', | |
'content' => $timber_post->category(), | |
], | |
// We'll always have a thumbnail, so we can set this confidently | |
['property' => 'twitter:card', 'content' => 'summary_large_image'], | |
]); | |
// Add the authors | |
foreach ($timber_post->authors() as $author) { | |
$open_graph_tags = array_merge( | |
$open_graph_tags, | |
add_article_author_open_graph_tags($author) | |
); | |
} | |
// Add the categories as tags | |
foreach ($timber_post->categories() as $category) { | |
$open_graph_tags[] = [ | |
'property' => 'article:tag', | |
'content' => $category, | |
]; | |
} | |
// Add the tags | |
foreach ($timber_post->tags() as $tag) { | |
$open_graph_tags[] = [ | |
'property' => 'article:tag', | |
'content' => $tag, | |
]; | |
} | |
// Set the image, for use in Image OG Tags below | |
if ($timber_post->thumbnail()) { | |
$image = $timber_post->thumbnail(); | |
} else { | |
$image = get_default_feature_image($timber_post, 'png'); | |
} | |
} | |
// Author OG tags | |
// https://cloudfour.com/is/jason-grigsby/ | |
// https://cloudfour.com/is/jason-grigsby/page/2/ | |
elseif (is_author()) { | |
echo "<!-- This is an author bio page -->\n"; | |
global $wp_query; | |
// for some reason `new User()` gets the logged-in user rather than | |
// the user from the current context, so we need to get the ID. | |
$author_id = $wp_query->query_vars['author']; | |
if (isset($author_id)) { | |
$timber_user = new User($author_id); | |
$job_title = property_exists($timber_user, 'job_title') | |
? ", $timber_user->job_title" | |
: ''; | |
// "Aileen Jeffries, Co-founder at Cloud Four" | |
$title = $timber_user->name() . "$job_title at $site_name"; | |
$bio = property_exists($timber_user, 'short_biography') | |
? // We have to manually add the user's name here to match `single.twig` | |
$timber_user->name() . ' ' . markdown($timber_user->short_biography) | |
: ''; | |
if (is_paged()) { | |
// "Articles by Aileen, Page 2" | |
$title = | |
"Articles by $timber_user->first_name, Page " . | |
get_query_var('paged'); | |
} | |
$open_graph_tags = array_merge($open_graph_tags, [ | |
['property' => 'og:type', 'content' => 'profile'], | |
['property' => 'og:url', 'content' => $timber_user->link()], | |
['property' => 'og:title', 'content' => $title], | |
// Description is the user's short biography. | |
[ | |
'property' => 'og:description', | |
'content' => format_description($bio), | |
], | |
[ | |
'name' => 'description', | |
'content' => format_description($bio), | |
], | |
[ | |
'property' => 'profile:first_name', | |
'content' => $timber_user->first_name, | |
], | |
[ | |
'property' => 'profile:last_name', | |
'content' => $timber_user->last_name, | |
], | |
]); | |
// Set the image to the user's avatar, for use in Image OG Tags below | |
$image = $timber_user->avatar(); | |
} | |
} | |
// Articles Page OG tags | |
// Our Articles page uses the `index.php` template, | |
// which is why this matches the `is_home` condition. | |
// https://cloudfour.com/thinks/ | |
// https://cloudfour.com/thinks/page/2/ | |
elseif (is_home()) { | |
echo "<!-- This is the Articles page -->\n"; | |
$timber_post = new Post(); | |
$title = $timber_post->title(); // "Articles" | |
if (is_paged()) { | |
$title .= ', Page ' . get_query_var('paged'); // "Articles, Page 2" | |
} | |
$articles_subhead = $timber_post->meta('subhead'); | |
$open_graph_tags = array_merge($open_graph_tags, [ | |
['property' => 'og:type', 'content' => 'website'], | |
['property' => 'og:url', 'content' => $timber_post->link()], | |
['property' => 'og:title', 'content' => $title], | |
// Description is the subhead if one is set | |
[ | |
'property' => 'og:description', | |
'content' => format_description($articles_subhead), | |
], | |
[ | |
'name' => 'description', | |
'content' => format_description($articles_subhead), | |
], | |
]); | |
} | |
// Taxonomy OG tags (category, tag, etc) | |
// https://cloudfour.com/topics/css/ | |
// https://cloudfour.com/topics/css/page/2/ | |
elseif (is_category() || is_tag() || is_tax()) { | |
echo "<!-- This is a taxonomy page -->\n"; | |
$timber_term = new Term(); | |
/** @var string */ | |
$title = get_archive_title(); // "CSS" | |
if (is_paged()) { | |
$title .= ', Page ' . get_query_var('paged'); // "CSS, Page 2" | |
} | |
$term_description = $timber_term->description(); | |
$open_graph_tags = array_merge($open_graph_tags, [ | |
['property' => 'og:type', 'content' => 'website'], | |
['property' => 'og:url', 'content' => $timber_term->link()], | |
['property' => 'og:title', 'content' => $title], | |
// Description is the term's description, if one is set | |
[ | |
'property' => 'og:description', | |
'content' => format_description($term_description), | |
], | |
[ | |
'name' => 'description', | |
'content' => format_description($term_description), | |
], | |
]); | |
} | |
// Archives OG tags (dates, etc) | |
// https://cloudfour.com/thinks/2021/ | |
// https://cloudfour.com/thinks/2021/page/2/ | |
elseif (is_archive()) { | |
echo "<!-- This is an archive page -->\n"; | |
global $wp; | |
/** @var string */ | |
$title = get_archive_title(); // "Archive: 2021" | |
if (is_paged()) { | |
$title .= ', Page ' . get_query_var('paged'); // "Archive: 2021, Page 2" | |
} | |
$open_graph_tags = array_merge($open_graph_tags, [ | |
['property' => 'og:type', 'content' => 'website'], | |
[ | |
'property' => 'og:url', | |
'content' => home_url(add_query_arg([], $wp->request)), | |
], | |
['property' => 'og:title', 'content' => $title], | |
[ | |
'property' => 'og:description', | |
'content' => format_description($site_description), | |
], | |
[ | |
'name' => 'description', | |
'content' => format_description($site_description), | |
], | |
]); | |
} | |
// Image OG tags | |
// `og:image` is required. Rather than repeat these in every content type, | |
// we set `$image` to a default image, and allow content types to override it | |
// when they have a better image to use. | |
// Facebook wants 1200x630. Twitter wants a 2:1 aspect ratio. To avoid the | |
// cost of generating two nearly identical images, we're letting Twitter clip | |
// the Facebook image by 30px and calling it good. | |
if ($image instanceof Image && is_string($image->src())) { | |
$img_src = $image->src(); | |
$img_width = $image->width(); | |
$img_height = $image->height(); | |
$img_alt = $image->alt(); | |
// only scale images down | |
if ($img_width > 1200) { | |
$aspect_ratio = $img_height / $img_width; | |
$img_width = 1200; | |
$img_height = round($img_width * $aspect_ratio); | |
$img_src = ImageHelper::resize($img_src, $img_width); | |
} | |
$open_graph_tags = array_merge($open_graph_tags, [ | |
['property' => 'og:image', 'content' => $img_src], | |
['property' => 'og:image:secure_url', 'content' => $img_src], | |
['property' => 'twitter:image', 'content' => $img_src], | |
['property' => 'og:image:alt', 'content' => $img_alt], | |
['property' => 'twitter:image:alt', 'content' => $img_alt], | |
['property' => 'og:image:width', 'content' => $img_width], | |
['property' => 'og:image:height', 'content' => $img_height], | |
[ | |
'property' => 'og:image:type', | |
'content' => get_post_mime_type($image->id), | |
], | |
]); | |
} | |
// Echo the OG tags to the page | |
foreach ($open_graph_tags as $tag) { | |
// don't print empty values | |
if (!$tag['content']) { | |
continue; | |
} | |
// handle non-OG tags like meta description | |
if (array_key_exists('name', $tag)) { | |
echo sprintf( | |
"<meta name='%s' content='%s' />\n", | |
$tag['name'], | |
$tag['content'] | |
); | |
continue; | |
} | |
echo sprintf( | |
"<meta property='%s' content='%s' />\n", | |
$tag['property'], | |
$tag['content'] | |
); | |
} | |
echo "<!-- End Theme Open Graph Tags -->\n\n"; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment