Skip to content

Instantly share code, notes, and snippets.

@spaceninja
Created December 5, 2022 17:31
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save spaceninja/d6dcb0e891d384186b93a2cbffb17fda to your computer and use it in GitHub Desktop.
Save spaceninja/d6dcb0e891d384186b93a2cbffb17fda to your computer and use it in GitHub Desktop.
<?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