328 lines
9.8 KiB
PHP
328 lines
9.8 KiB
PHP
<?php
|
|
/**
|
|
* Plugin Name: Ruady OG Head
|
|
* Description: Generates Open Graph tags for Social Media sharing (Facebook, Twitter/X, etc.)
|
|
* Version: 0.1.0
|
|
* Author: David Madl
|
|
*/
|
|
|
|
// note: install by copying into wp-content/plugins/ruady-og-head/ruady-og-head.php
|
|
|
|
declare(strict_types=1);
|
|
|
|
if ( ! defined( 'ABSPATH' ) ) {
|
|
exit;
|
|
}
|
|
|
|
final class Ruady_OG_Head {
|
|
public static function init(): void {
|
|
add_action( 'init', [ __CLASS__, 'register_endpoint' ] );
|
|
add_action( 'template_redirect', [ __CLASS__, 'maybe_render_image' ] );
|
|
add_action( 'wp_head', [ __CLASS__, 'render_head_markup' ], 20 );
|
|
|
|
register_activation_hook( __FILE__, [ __CLASS__, 'activate' ] );
|
|
register_deactivation_hook( __FILE__, [ __CLASS__, 'deactivate' ] );
|
|
}
|
|
|
|
public static function register_endpoint(): void {
|
|
// Adds /og-image/ to singular permalinks.
|
|
add_rewrite_endpoint( 'og-image', EP_PERMALINK | EP_PAGES );
|
|
}
|
|
|
|
public static function activate(): void {
|
|
self::register_endpoint();
|
|
flush_rewrite_rules();
|
|
}
|
|
|
|
public static function deactivate(): void {
|
|
flush_rewrite_rules();
|
|
}
|
|
|
|
/**
|
|
* Echo markup into <head> for the currently displayed singular object.
|
|
*/
|
|
public static function render_head_markup(): void {
|
|
// Adjust this if you only want standard posts:
|
|
// if ( ! is_singular( 'post' ) ) { return; }
|
|
|
|
if ( is_front_page() ) {
|
|
$html = self::build_markup_for_front_page();
|
|
if ( $html === '' ) {
|
|
return;
|
|
}
|
|
echo "\n" . $html . "\n";
|
|
return;
|
|
}
|
|
|
|
if ( ! is_singular() ) {
|
|
return;
|
|
}
|
|
|
|
|
|
if ( ! is_singular() ) {
|
|
return;
|
|
}
|
|
|
|
$post_id = get_queried_object_id();
|
|
if ( ! $post_id ) {
|
|
return;
|
|
}
|
|
|
|
$post = get_post( $post_id );
|
|
if ( ! $post instanceof WP_Post ) {
|
|
return;
|
|
}
|
|
|
|
if ( ! has_category( 'Kurzgeschichten', $post_id ) ) {
|
|
return;
|
|
}
|
|
|
|
$html = self::build_markup_for_post( $post );
|
|
|
|
if ( $html === '' ) {
|
|
return;
|
|
}
|
|
|
|
echo "\n" . $html . "\n";
|
|
}
|
|
|
|
private static function make_description( string $text ): string {
|
|
$target_length = 150;
|
|
$parts = explode(' ', $text);
|
|
$lengths = array_map('strlen', $parts);
|
|
$total_length = 0;
|
|
$i = 0;
|
|
for ( ; $i < count($lengths); $i++ ) {
|
|
if ( $total_length + $lengths[$i] > $target_length ) break;
|
|
$total_length += $lengths[$i];
|
|
}
|
|
$desc = implode(' ', array_slice($parts, 0, $i));
|
|
$ellipsis = substr($desc, -1, 1) == '.' ? '' : ' ...'; // full sentences with a '.' don't need ellipsis '...'
|
|
return $desc . $ellipsis;
|
|
}
|
|
|
|
private static string $page_headline = "Da Ruady. Hansdampf in allen Gassen.";
|
|
|
|
private static function build_markup_for_front_page(): string {
|
|
|
|
$site_title = get_bloginfo( 'name' );
|
|
$site_url = site_url();
|
|
$site_url_p = parse_url($site_url);
|
|
$site_host = $site_url_p['host'];
|
|
$post_url = $site_url;
|
|
|
|
$og_image_url = get_the_post_thumbnail_url(33, 'full');
|
|
$description = self::$page_headline;
|
|
$og_description = self::$page_headline;
|
|
|
|
ob_start();
|
|
?>
|
|
<meta name="description" content="<?php echo esc_attr( $description ); ?>">
|
|
|
|
<!-- Open Graph Meta Tags -->
|
|
<meta property="og:url" content="<?php echo esc_url( $post_url ); ?>">
|
|
<meta property="og:type" content="website">
|
|
<meta property="og:title" content="<?php echo esc_attr( $site_title ); ?>">
|
|
<meta property="og:description" content="<?php echo esc_attr( $og_description ); ?>">
|
|
<meta property="og:image" content="<?php echo esc_url( $og_image_url ) ?>">
|
|
<meta property="og:image:width" content="1200">
|
|
<meta property="og:image:height" content="630">
|
|
|
|
<!-- Twitter Meta Tags -->
|
|
<meta name="twitter:card" content="summary_large_image">
|
|
<meta property="twitter:domain" content="<?php echo esc_attr( $site_host ); ?>">
|
|
<meta property="twitter:url" content="<?php echo esc_url( $site_url ); ?>">
|
|
<meta name="twitter:title" content="<?php echo esc_attr( $site_title ); ?>">
|
|
<meta name="twitter:description" content="<?php echo esc_attr( $og_description ); ?>">
|
|
<meta property="twitter:image" content="<?php echo esc_url( $og_image_url ) ?>">
|
|
<?php
|
|
|
|
return trim( (string) ob_get_clean() );
|
|
}
|
|
|
|
/**
|
|
* Build the head markup for one post.
|
|
*/
|
|
private static function build_markup_for_post( WP_Post $post ): string {
|
|
// Example 1: conditionally emit markup only for a specific post type.
|
|
if ( $post->post_type !== 'post' ) {
|
|
return '';
|
|
}
|
|
|
|
$post_url = get_permalink( $post );
|
|
$post_title = get_the_title( $post );
|
|
$excerpt = get_the_excerpt( $post );
|
|
$site_title = get_bloginfo( 'name' );
|
|
$site_url = site_url();
|
|
$site_url_p = parse_url($site_url);
|
|
$site_host = $site_url_p['host'];
|
|
|
|
$og_image_url = user_trailingslashit( trailingslashit( $post_url ) . 'og-image' );
|
|
$description = self::make_description(wp_strip_all_tags( $excerpt ));
|
|
$og_description = self::$page_headline;
|
|
|
|
// Example business logic:
|
|
// - add a meta tag for every post
|
|
// - add JSON-LD only if the post is in category "news"
|
|
$is_news = has_category( 'news', $post );
|
|
|
|
ob_start();
|
|
?>
|
|
<meta name="description" content="<?php echo esc_attr( $description ); ?>">
|
|
|
|
<!-- Open Graph Meta Tags -->
|
|
<meta property="og:url" content="<?php echo esc_url( $post_url ); ?>">
|
|
<meta property="og:type" content="website">
|
|
<meta property="og:title" content="<?php echo esc_attr( $post_title ); ?> – <?php echo esc_attr( $site_title ); ?>">
|
|
<meta property="og:description" content="<?php echo esc_attr( $og_description ); ?>">
|
|
<meta property="og:image" content="<?php echo esc_url( $og_image_url ) ?>">
|
|
<meta property="og:image:width" content="1200">
|
|
<meta property="og:image:height" content="630">
|
|
<meta property="og:image:alt" content="<?php echo esc_attr( $description ); ?>">
|
|
|
|
<!-- Twitter Meta Tags -->
|
|
<meta name="twitter:card" content="summary_large_image">
|
|
<meta property="twitter:domain" content="<?php echo esc_attr( $site_host ); ?>">
|
|
<meta property="twitter:url" content="<?php echo esc_url( $site_url ); ?>">
|
|
<meta name="twitter:title" content="<?php echo esc_attr( $post_title ); ?> – <?php echo esc_attr( $site_title ); ?>">
|
|
<meta name="twitter:description" content="<?php echo esc_attr( $og_description ); ?>">
|
|
<meta property="twitter:image" content="<?php echo esc_url( $og_image_url ) ?>">
|
|
|
|
<link rel="canonical" href="<?php echo esc_url( $post_url ); ?>">
|
|
<?php
|
|
|
|
return trim( (string) ob_get_clean() );
|
|
}
|
|
|
|
public static function maybe_render_image(): void {
|
|
// Endpoint absent.
|
|
$endpoint_value = get_query_var( 'og-image', null );
|
|
if ( null === $endpoint_value ) {
|
|
return;
|
|
}
|
|
|
|
if ( ! is_singular() ) {
|
|
self::render_text_error( 404, 'Not found' );
|
|
}
|
|
|
|
$post = get_queried_object();
|
|
if ( ! $post instanceof WP_Post ) {
|
|
self::render_text_error( 404, 'Not found' );
|
|
}
|
|
|
|
self::render_png_for_post( $post );
|
|
}
|
|
|
|
private static function wrap_text_ttf(string $text, int $maxWidth, string $fontFile, float $size): array {
|
|
$words = preg_split('/\s+/', trim($text));
|
|
$lines = [];
|
|
$current = '';
|
|
|
|
foreach ($words as $word) {
|
|
$test = $current === '' ? $word : $current . ' ' . $word;
|
|
$box = imagettfbbox($size, 0, $fontFile, $test);
|
|
$width = $box[2] - $box[0];
|
|
|
|
if ($width <= $maxWidth) {
|
|
$current = $test;
|
|
} else {
|
|
if ($current !== '') {
|
|
$lines[] = $current;
|
|
}
|
|
$current = $word;
|
|
}
|
|
}
|
|
|
|
if ($current !== '') {
|
|
$lines[] = $current;
|
|
}
|
|
|
|
return $lines;
|
|
}
|
|
|
|
private static function render_png_for_post( WP_Post $post ): void {
|
|
if ( ! function_exists( 'imagecreatetruecolor' ) ) {
|
|
self::render_text_error( 500, 'GD extension not available' );
|
|
}
|
|
|
|
$width = 1200;
|
|
$height = 630;
|
|
|
|
$image = imagecreatetruecolor( $width, $height );
|
|
if ( ! $image ) {
|
|
self::render_text_error( 500, 'Could not create image' );
|
|
}
|
|
|
|
$bg = imagecolorallocate( $image, 224, 241, 212 ); // theme background (light green)
|
|
$fg = imagecolorallocate( $image, 0, 0, 0 ); // theme foreground (black)
|
|
$tt = imagecolorallocate( $image, 72, 119, 40 ); // theme primary (dark green)
|
|
$accent = imagecolorallocate( $image, 204, 235, 235 ); // theme tertiary (light blue)
|
|
|
|
imagefilledrectangle( $image, 0, 0, $width, $height, $bg );
|
|
imagefilledrectangle( $image, 0, $height-10, $width, $height, $tt );
|
|
|
|
$title = wp_strip_all_tags( get_the_title( $post ) );
|
|
$site = wp_strip_all_tags( get_bloginfo( 'name' ) );
|
|
|
|
// Minimal bitmap-font rendering. Fine for a skeleton.
|
|
// For better typography, switch to imagettftext() with a bundled font.
|
|
$title = self::truncate( $title, 90 );
|
|
$site = self::truncate( $site, 90 );
|
|
|
|
/*
|
|
imagestring( $image, 5, 40, 60, $title, $tt );
|
|
imagestring( $image, 3, 40, 110, $site, $fg );
|
|
imagestring( $image, 2, 40, 580, 'Post ID: ' . (string) $post->ID, $accent );
|
|
*/
|
|
|
|
//////////////////////////////////
|
|
|
|
$excerpt = get_the_excerpt( $post );
|
|
$description = html_entity_decode(wp_strip_all_tags( $excerpt ));
|
|
|
|
$fontFile = __DIR__ . '/fonts/EBGaramond-VariableFont_wght.ttf';
|
|
//$size = 28;
|
|
$size = 36;
|
|
$x = 60;
|
|
$y = 120;
|
|
$lineHeight = (int) ($size * 1.9);
|
|
$maxWidth = 1080;
|
|
$maxHeight = $height - 60;
|
|
|
|
// wrap_text_ttf(string $text, int $maxWidth, string $fontFile, float $size)
|
|
$lines = self::wrap_text_ttf($description, $maxWidth, $fontFile, $size);
|
|
|
|
foreach ($lines as $line) {
|
|
imagettftext($image, $size, 0, $x, $y, $fg, $fontFile, $line);
|
|
$y += $lineHeight;
|
|
if ($y > $maxHeight) break;
|
|
}
|
|
//////////////////////////////////
|
|
|
|
status_header( 200 );
|
|
header( 'Content-Type: image/png' );
|
|
header( 'Cache-Control: public, max-age=3600' );
|
|
|
|
imagepng( $image );
|
|
imagedestroy( $image );
|
|
exit;
|
|
}
|
|
|
|
private static function render_text_error( int $status, string $message ): void {
|
|
status_header( $status );
|
|
header( 'Content-Type: text/plain; charset=utf-8' );
|
|
echo $message;
|
|
exit;
|
|
}
|
|
|
|
private static function truncate( string $text, int $max_len ): string {
|
|
if ( mb_strlen( $text ) <= $max_len ) {
|
|
return $text;
|
|
}
|
|
|
|
return mb_substr( $text, 0, $max_len - 1 ) . '…';
|
|
}
|
|
}
|
|
|
|
Ruady_OG_Head::init();
|