Files
Bookshelf/umbra-bookshelf/includes/library/class-umbra-bookshelf-helper.php
bdm dec732931c Consolidate three plugins into Umbra Bookshelf
Merge BD Bookshelf (0.1.0), BD BookShelfHelper (2.0.0) and BD SimpleEmail
(2.0.0) into a single plugin released as Umbra Bookshelf 1.0.0, with the
BD Bookshelf theme left as a separate, unrenamed artifact.

Modules
  includes/books/    catalogue, ISBN/ASIN, series, retail, ratings, shortcodes,
                     SEO, GDPR, scrapers, admin, CSV/post import
  includes/library/  book post type + series taxonomy, pen names, data entry,
                     shortlinks, reviews, migrate
  includes/email/    subscribers, newsletters, composition, sending

Renamed: plugin headers, text domains (all three -> umbra-bookshelf), classes
(BD_*/BDM_* -> Umbra_*), constants (-> UMBRA_BOOKSHELF_*), asset handles, JS
globals and admin slugs.

Deliberately not renamed -- persisted contract with live installs and with
published post content: table names, option keys, post meta keys, transients,
nonce actions, cron hooks, AJAX actions, shortcodes ([bd-book], [bd-books],
[bdm_signup]), post type `book`, taxonomy `series`, cookies and CSS classes.
BD_BookShelfHelper survives only as a class_alias for the unmodified theme.

Collisions resolved
  * class-umbra-theme-compatibility.php declared BD_BookShelfHelper, the same
    name as the library module's helper. Latent while the plugins were
    separate (only fired when both were active), but an unconditional fatal
    once merged. Renamed to Umbra_Theme_Compatibility with a class_alias.
  * All three modules called add_menu_page. One top-level menu is now
    registered by the bootstrap at admin_menu priority 9.
  * BD Bookshelf and BookShelfHelper both claimed bd-bookshelf-settings, leaving
    one Settings screen unreachable -> umbra-bookshelf-settings and
    umbra-bookshelf-library-settings.
  * Catalogue and library both enqueued bd-bookshelf-admin; the second enqueue
    was silently discarded -> unique handles per module.
  * Umbra_Admin and Umbra_Migration both registered bd_bookshelf_import_csv, so
    one handler silently replaced the other. The import page now lives only in
    Umbra_Admin (the variant wired to the admin JS); Umbra_Migration keeps only
    the preview/run endpoints it uniquely provides.
  * Modules were constructed from an init callback, so their own init
    registrations raced the hook being dispatched. Now constructed on
    plugins_loaded behind a re-entrancy guard.

Also
  * Missing assets created: assets/images/default-cover.png (referenced but
    never committed, so every cover-less book 404'd) and email/assets/js/
    admin.js (enqueued on every email admin page but never committed).
  * Library module's strpos($hook, 'bd-bookshelf') asset check matched every
    catalogue and email screen once slugs shared the umbra-bookshelf- prefix;
    now matched against its own slug list.
  * Consolidated uninstall deduplicates the double drop of bd_books/bd_series.
  * Specs aligned to the new name; pre-merge specs retained under
    spec/archive/ with provenance notes.
  * Tests added: tests/verify_structure.py (static) and tests/smoke_runtime.php
    (WordPress-stub runtime boot, front-end and admin contexts).

Deferred and recorded in docs/KNOWN-ISSUES.md: the catalogue and library
modules still create bd_books and bd_series with two incompatible schemas
(pre-existing, now unconditional), and Goodreads/Amazon scraping is still
implemented twice on shared cron hooks.
2026-10-18 19:55:36 -07:00

844 lines
27 KiB
PHP

<?php
// Copyright 2026 Umbra Tools (umbratools.com)
// SPDX-License-Identifier: Apache-2.0
/**
* Main Plugin Class
*
* @package Umbra_Bookshelf_Helper
*/
if (!defined('ABSPATH')) {
exit;
}
class Umbra_Bookshelf_Helper {
private static $instance = null;
/**
* Get instance
*
* @return Umbra_Bookshelf_Helper
*/
public static function instance() {
if (null === self::$instance) {
self::$instance = new self();
}
return self::$instance;
}
/**
* Constructor
*/
private function __construct() {
$this->init_hooks();
}
/**
* Initialize hooks
*/
private function init_hooks() {
add_action('init', array($this, 'register_post_types'));
add_action('init', array($this, 'register_taxonomies'));
add_action('init', array($this, 'register_shortlinks'));
add_action('init', array($this, 'register_shortcodes'));
add_action('template_redirect', array($this, 'handle_shortlink_requests'));
// Hook for future plugin integration
add_filter('bdm_nav_menu_items', array($this, 'filter_series_menu_item'), 10, 2);
if (is_admin()) {
Umbra_Bookshelf_Helper_Admin::instance();
}
// Schedule cron jobs on init
add_action('init', array($this, 'schedule_cron_events'));
// Cron callbacks
add_action('bd_bookshelf_goodreads_refresh', array($this, 'refresh_goodreads_data'));
add_action('bd_bookshelf_amazon_scrape', array($this, 'scrape_amazon_ratings'));
}
/**
* Register custom post type
*/
public function register_post_types() {
register_post_type('book', array(
'labels' => array(
'name' => esc_html__('Books', 'umbra-bookshelf'),
'singular_name' => esc_html__('Book', 'umbra-bookshelf'),
'add_new' => esc_html__('Add New', 'umbra-bookshelf'),
'add_new_item' => esc_html__('Add New Book', 'umbra-bookshelf'),
'edit_item' => esc_html__('Edit Book', 'umbra-bookshelf'),
'new_item' => esc_html__('New Book', 'umbra-bookshelf'),
'view_item' => esc_html__('View Book', 'umbra-bookshelf'),
'search_items' => esc_html__('Search Books', 'umbra-bookshelf'),
'not_found' => esc_html__('No books found', 'umbra-bookshelf'),
'not_found_in_trash' => esc_html__('No books found in trash', 'umbra-bookshelf'),
),
'public' => true,
'has_archive' => true,
'show_in_menu' => 'umbra-bookshelf',
'supports' => array('title', 'editor', 'thumbnail', 'excerpt', 'custom-fields'),
'rewrite' => array('slug' => 'book'),
'menu_icon' => 'dashicons-book-alt',
'show_in_rest' => true,
'capability_type' => 'post',
'map_meta_cap' => true,
));
}
/**
* Register taxonomies
*/
public function register_taxonomies() {
register_taxonomy('series', 'book', array(
'labels' => array(
'name' => esc_html__('Series', 'umbra-bookshelf'),
'singular_name' => esc_html__('Series', 'umbra-bookshelf'),
'search_items' => esc_html__('Search Series', 'umbra-bookshelf'),
'all_items' => esc_html__('All Series', 'umbra-bookshelf'),
'edit_item' => esc_html__('Edit Series', 'umbra-bookshelf'),
'update_item' => esc_html__('Update Series', 'umbra-bookshelf'),
'add_new_item' => esc_html__('Add New Series', 'umbra-bookshelf'),
'new_item_name' => esc_html__('New Series Name', 'umbra-bookshelf'),
'not_found' => esc_html__('No series found', 'umbra-bookshelf'),
),
'hierarchical' => true,
'public' => true,
'show_admin_column' => true,
'rewrite' => array('slug' => 'series'),
'show_in_rest' => true,
'show_ui' => true,
));
}
/**
* Register rewrite rules for short links
*/
public function register_shortlinks() {
add_rewrite_rule('^([a-z0-9]{2,6})/?$', 'index.php?book_short_slug=$matches[1]', 'top');
add_rewrite_rule('^([a-z0-9]{2,6})r([agbckpl])/?$', 'index.php?book_short_slug=$matches[1]&review_site=$matches[2]', 'top');
}
/**
* Handle short link requests
*/
public function handle_shortlink_requests() {
$short_slug = get_query_var('book_short_slug');
if (empty($short_slug)) {
return;
}
global $wpdb;
$book_id = $wpdb->get_var($wpdb->prepare(
"SELECT post_id FROM {$wpdb->postmeta} WHERE meta_key = 'short_link_slug' AND meta_value = %s",
$short_slug
));
if ($book_id) {
wp_redirect(get_permalink($book_id), 301);
exit;
}
}
/**
* Filter series menu item visibility
*
* @param string $items Menu items HTML.
* @param stdClass $args Menu args.
* @return string
*/
public function filter_series_menu_item($items, $args) {
if ($args->theme_location !== 'primary') {
return $items;
}
$series_count = self::series_count();
if ($series_count > 0) {
$series_item = '<li class="menu-item menu-item-type-taxonomy series-menu-item"><a href="' . home_url('/series/') . '">' . esc_html__('Series', 'umbra-bookshelf') . '</a></li>';
if (preg_match('/(<li[^>]*>.*?Books.*?<\/li>)/s', $items, $matches)) {
$books_item = $matches[1];
$items = str_replace($books_item, $books_item . $series_item, $items);
}
}
return $items;
}
/**
* Schedule cron events
*/
public function schedule_cron_events() {
if (!wp_next_scheduled('bd_bookshelf_goodreads_refresh')) {
wp_schedule_event(time(), 'daily', 'bd_bookshelf_goodreads_refresh');
}
if (!wp_next_scheduled('bd_bookshelf_amazon_scrape')) {
wp_schedule_event(time(), 'six_hours', 'bd_bookshelf_amazon_scrape');
}
}
/**
* Refresh Goodreads data
*/
public function refresh_goodreads_data() {
// Implementation for Goodreads API refresh
}
/**
* Scrape Amazon ratings
*/
public function scrape_amazon_ratings() {
// Implementation for Amazon rating scrape
}
/**
* Register shortcodes
*/
public function register_shortcodes() {
add_shortcode('bd-book', array($this, 'shortcode_book'));
add_shortcode('bd-books', array($this, 'shortcode_books_list'));
}
/**
* Shortcode: [bd-book]
* Display single book by ID or title
*/
public function shortcode_book($atts) {
$atts = shortcode_atts(array(
'book' => '',
), $atts);
if (empty($atts['book'])) {
return '<p class="bd-book-error">Please specify a book ID or title in the shortcode.</p>';
}
$book_id = intval($atts['book']);
if ($book_id > 0) {
$book = get_post($book_id);
} else {
$books = get_posts(array(
'post_type' => 'book',
'title' => $atts['book'],
'posts_per_page' => 1,
));
$book = !empty($books) ? $books[0] : null;
}
if (!$book || 'book' !== $book->post_type) {
return '<p class="bd-book-error">Book not found.</p>';
}
return $this->render_book_display($book);
}
/**
* Shortcode: [bd-books]
* Display all books in a list
*/
public function shortcode_books_list($atts) {
$atts = shortcode_atts(array(
'limit' => -1,
'order' => 'title',
), $atts);
$books = get_posts(array(
'post_type' => 'book',
'posts_per_page' => intval($atts['limit']),
'post_status' => 'publish',
'orderby' => 'title',
'order' => 'ASC',
));
if (empty($books)) {
return '<p class="bd-book-error">No books available.</p>';
}
$output = '<div class="bd-books-list">';
foreach ($books as $book) {
$output .= $this->render_book_display($book);
}
$output .= '</div>';
return $output;
}
/**
* Render book display HTML
*
* @param WP_Post $book Book post object.
* @return string
*/
private function render_book_display($book) {
$cover_url = Umbra_Bookshelf_Helper::get_cover_url($book->ID, 'medium');
$pen_name = Umbra_Bookshelf_Helper::get_pen_name($book->ID);
$blurb = Umbra_Bookshelf_Helper::get_localized_blurb($book->ID);
$links = Umbra_Bookshelf_Helper::get_retailer_links($book->ID);
ob_start();
?>
<div class="bd-book-listing">
<div class="book-entry" style="display: flex; gap: 2rem; margin-bottom: 2rem; padding-bottom: 2rem; border-bottom: 1px solid #ccc;">
<?php if ($cover_url): ?>
<div class="book-cover" style="flex: 0 0 150px;">
<img src="<?php echo esc_url($cover_url); ?>" alt="<?php echo esc_attr($book->post_title); ?> Cover" style="width: 100%; max-width: 150px; height: auto; box-shadow: 0 4px 8px rgba(0,0,0,0.2);">
</div>
<?php endif; ?>
<div class="book-details" style="flex: 1;">
<h2 style="margin-top: 0; margin-bottom: 0.5rem;"><?php echo esc_html($book->post_title); ?></h2>
<p style="margin: 0 0 1rem 0; color: #666;">by <?php echo esc_html($pen_name); ?></p>
<?php if ($blurb): ?>
<div class="book-description" style="margin-bottom: 1rem; padding: 1rem; border: 1px solid #e0e0e0; background: #f9f9f9; border-radius: 4px;">
<?php echo wp_kses_post(wp_trim_words($blurb, 50)); ?>
</div>
<?php endif; ?>
<?php if (!empty($links)): ?>
<div class="book-buttons" style="display: flex; gap: 1rem; flex-wrap: wrap;">
<?php foreach ($links as $link): ?>
<a href="<?php echo esc_url($link['url']); ?>" target="_blank" rel="noopener" class="bd-book-btn" style="background-color: #FF9900; color: #000; padding: 0.5rem 1rem; text-decoration: none; border-radius: 4px; font-weight: bold;"><?php echo esc_html($link['name']); ?></a>
<?php endforeach; ?>
</div>
<?php endif; ?>
</div>
</div>
</div>
<?php
return ob_get_clean();
}
/**
* Migrate books from BD Books plugin
*
* @return array Migration result
*/
public static function migrate_from_bd_books() {
global $wpdb;
$old_table = $wpdb->prefix . 'bd_books';
// Check if old table exists
$table_exists = $wpdb->get_var("SHOW TABLES LIKE '$old_table'") === $old_table;
if (!$table_exists) {
return array('success' => false, 'message' => 'Old BD Books table not found.');
}
$old_books = $wpdb->get_results("SELECT * FROM {$old_table}");
if (empty($old_books)) {
return array('success' => true, 'message' => 'No books to migrate.', 'count' => 0);
}
$migrated = 0;
foreach ($old_books as $old_book) {
// Check if already migrated (by title)
$existing = get_posts(array(
'post_type' => 'book',
'title' => $old_book->title,
'posts_per_page' => 1,
));
if (!empty($existing)) {
continue;
}
// Create new book post
$post_id = wp_insert_post(array(
'post_type' => 'book',
'post_title' => sanitize_text_field($old_book->title),
'post_content' => '',
'post_status' => 'publish',
'post_author' => get_current_user_id(),
'post_date' => $old_book->created_at,
));
if (is_wp_error($post_id)) {
continue;
}
// Set author/pen name
if (!empty($old_book->author)) {
// Try to find or create pen name
global $wpdb;
$pen_names_table = $wpdb->prefix . 'bd_pen_names';
$pen_name_id = $wpdb->get_var($wpdb->prepare(
"SELECT id FROM {$pen_names_table} WHERE pen_name = %s",
$old_book->author
));
if ($pen_name_id) {
update_post_meta($post_id, 'pen_name_id', $pen_name_id);
} else {
// Insert new pen name
$wpdb->insert($pen_names_table, array(
'pen_name' => sanitize_text_field($old_book->author),
'active' => 1,
'created_at' => current_time('mysql'),
));
$new_pen_id = $wpdb->insert_id;
if ($new_pen_id) {
update_post_meta($post_id, 'pen_name_id', $new_pen_id);
}
}
}
// Set description/blurb
if (!empty($old_book->description)) {
update_post_meta($post_id, 'blurb_en', wp_kses_post($old_book->description));
}
// Set cover image
if (!empty($old_book->cover_image_id)) {
set_post_thumbnail($post_id, intval($old_book->cover_image_id));
} elseif (!empty($old_book->cover_image_url)) {
// Download and attach external image
$attachment_id = self::import_external_image($old_book->cover_image_url, $old_book->title);
if ($attachment_id) {
set_post_thumbnail($post_id, $attachment_id);
}
}
// Parse and set purchase links
$links = json_decode($old_book->purchase_links, true);
if (!empty($links) && is_array($links)) {
foreach ($links as $link) {
$label = isset($link['label']) ? strtolower($link['label']) : '';
$url = isset($link['url']) ? $link['url'] : '';
if (strpos($label, 'amazon') !== false) {
update_post_meta($post_id, 'ebook_url_amazon', esc_url_raw($url));
} elseif (strpos($label, 'apple') !== false) {
update_post_meta($post_id, 'ebook_url_apple', esc_url_raw($url));
} elseif (strpos($label, 'barnes') !== false || strpos($label, 'b&n') !== false) {
update_post_meta($post_id, 'ebook_url_barnes', esc_url_raw($url));
} elseif (strpos($label, 'kobo') !== false) {
update_post_meta($post_id, 'ebook_url_kobo', esc_url_raw($url));
} elseif (strpos($label, 'google') !== false) {
update_post_meta($post_id, 'ebook_url_google', esc_url_raw($url));
}
}
}
$migrated++;
}
return array(
'success' => true,
'message' => sprintf('Migrated %d books.', $migrated),
'count' => $migrated,
);
}
/**
* Import external image as attachment
*
* @param string $url Image URL.
* @param string $title Image title.
* @return int|false Attachment ID
*/
private static function import_external_image($url, $title) {
require_once(ABSPATH . 'wp-admin/includes/file.php');
require_once(ABSPATH . 'wp-admin/includes/media.php');
require_once(ABSPATH . 'wp-admin/includes/image.php');
$tmp = download_url($url);
if (is_wp_error($tmp)) {
return false;
}
$file_array = array(
'name' => sanitize_file_name($title) . '.jpg',
'tmp_name' => $tmp,
);
$id = media_handle_sideload($file_array, 0, $title);
@unlink($tmp);
if (is_wp_error($id)) {
return false;
}
return $id;
}
// =====================
// Static Helper Methods
// =====================
/**
* Get featured books
*
* @param int $limit Number of books to return.
* @return array
*/
public static function get_featured_books($limit = 5) {
$query = new WP_Query(array(
'post_type' => 'book',
'posts_per_page' => $limit,
'post_status' => 'publish',
'orderby' => 'meta_value',
'meta_key' => 'publish_date',
'order' => 'DESC',
));
return $query->posts;
}
/**
* Get cover URL for a book
*
* @param int $book_id Book post ID.
* @param string $size Image size.
* @param string $lang Language code.
* @return string
*/
public static function get_cover_url($book_id, $size = 'thumbnail', $lang = 'en') {
$meta_key = 'cover_' . $lang;
$cover_id = get_post_meta($book_id, $meta_key, true);
if (!$cover_id) {
$cover_id = get_post_thumbnail_id($book_id);
}
if ($cover_id) {
$image = wp_get_attachment_image_url($cover_id, $size);
if ($image) {
return $image;
}
}
return UMBRA_BOOKSHELF_URL . 'assets/images/default-cover.png';
}
/**
* Get book URL (short link)
*
* @param int $book_id Book post ID.
* @return string
*/
public static function get_book_url($book_id) {
$slug = get_post_meta($book_id, 'short_link_slug', true);
if ($slug) {
return home_url('/' . $slug);
}
return get_permalink($book_id);
}
/**
* Get ratings for a book
*
* @param int $book_id Book post ID.
* @return array
*/
public static function get_ratings($book_id) {
$ratings = array();
$amazon = get_post_meta($book_id, 'amazon_rating', true);
if ($amazon) {
$ratings['amazon'] = $amazon;
}
$goodreads = get_post_meta($book_id, 'goodreads_rating', true);
if ($goodreads) {
$ratings['goodreads'] = $goodreads;
}
return $ratings;
}
/**
* Get retailer links for a book
*
* @param int $book_id Book post ID.
* @param string $format Format (ebook, paperback, hardback).
* @return array
*/
public static function get_retailer_links($book_id, $format = 'ebook') {
$links = array();
$retailers = array(
'amazon' => array('name' => 'Amazon', 'base_url' => 'https://www.amazon.com/dp/'),
'apple' => array('name' => 'Apple Books', 'base_url' => 'https://books.apple.com/'),
'barnes' => array('name' => 'Barnes & Noble', 'base_url' => 'https://www.barnesandnoble.com/'),
'kobo' => array('name' => 'Kobo', 'base_url' => 'https://www.kobo.com/'),
'google' => array('name' => 'Google Play', 'base_url' => 'https://play.google.com/store/apps/details?id='),
);
$affiliates = get_option('bdm_affiliate_codes', array());
foreach ($retailers as $key => $retailer) {
$url_meta = $format . '_url_' . $key;
$url = get_post_meta($book_id, $url_meta, true);
if (!$url && $key === 'amazon') {
$asin = get_post_meta($book_id, 'asin', true);
if ($asin) {
$affiliate = isset($affiliates[$key]) ? $affiliates[$key] : '';
$url = $retailer['base_url'] . $asin . '?tag=' . $affiliate;
}
}
if ($url) {
$links[] = array(
'name' => $retailer['name'],
'url' => $url,
'retailer' => $key,
);
}
}
return $links;
}
/**
* Display retailer links
*
* @param int $book_id Book post ID.
* @param string $format Format.
*/
public static function display_retailer_links($book_id, $format = 'ebook') {
$links = self::get_retailer_links($book_id, $format);
foreach ($links as $link) {
printf(
'<a href="%s" class="retailer-link retailer-%s" target="_blank" rel="noopener noreferrer">%s</a>',
esc_url($link['url']),
esc_attr($link['retailer']),
esc_html($link['name'])
);
}
}
/**
* Get localized title
*
* @param int $book_id Book post ID.
* @param string $lang Language code.
* @return string
*/
public static function get_localized_title($book_id, $lang = 'en') {
$title = get_post_meta($book_id, 'title_' . $lang, true);
return $title ?: get_the_title($book_id);
}
/**
* Get localized blurb
*
* @param int $book_id Book post ID.
* @param string $lang Language code.
* @return string
*/
public static function get_localized_blurb($book_id, $lang = 'en') {
return get_post_meta($book_id, 'blurb_' . $lang, true) ?: '';
}
/**
* Get preview excerpt
*
* @param int $book_id Book post ID.
* @param string $lang Language code.
* @return string
*/
public static function get_preview_excerpt($book_id, $lang = 'en') {
return get_post_meta($book_id, 'preview_excerpt_' . $lang, true) ?: '';
}
/**
* Get pull quotes
*
* @param int $book_id Book post ID.
* @param string $lang Language code.
* @return array
*/
public static function get_pull_quotes($book_id, $lang = 'en') {
$quotes = array();
for ($i = 1; $i <= 5; $i++) {
$quote = get_post_meta($book_id, 'pull_quote_' . $lang . '_' . $i, true);
if ($quote) {
$quotes[] = $quote;
}
}
return $quotes;
}
/**
* Get reviews for a book
*
* @param int $book_id Book post ID.
* @return array
*/
public static function get_reviews($book_id) {
global $wpdb;
$table_name = $wpdb->prefix . 'bd_reviews';
$reviews = $wpdb->get_results($wpdb->prepare(
"SELECT * FROM {$table_name} WHERE book_id = %d ORDER BY display_order ASC",
$book_id
));
return $reviews ?: array();
}
/**
* Get pen name for a book
*
* @param int $book_id Book post ID.
* @return string
*/
public static function get_pen_name($book_id) {
$pen_name_id = get_post_meta($book_id, 'pen_name_id', true);
if ($pen_name_id) {
global $wpdb;
$table_name = $wpdb->prefix . 'bd_pen_names';
$pen_name = $wpdb->get_var($wpdb->prepare(
"SELECT pen_name FROM {$table_name} WHERE id = %d AND active = 1",
$pen_name_id
));
return $pen_name ?: 'B.D. Murphy';
}
return 'B.D. Murphy';
}
/**
* Get series info for a book
*
* @param int $book_id Book post ID.
* @return array|null
*/
public static function get_series_info($book_id) {
$terms = get_the_terms($book_id, 'series');
if ($terms && !is_wp_error($terms)) {
$series = $terms[0];
$volume = get_post_meta($book_id, 'volume_number', true);
return array(
'id' => $series->term_id,
'name' => $series->name,
'slug' => $series->slug,
'volume' => $volume ?: 1,
);
}
return null;
}
/**
* Count series
*
* @return int
*/
public static function series_count() {
$terms = get_terms(array(
'taxonomy' => 'series',
'hide_empty' => true,
'fields' => 'count',
));
return $terms ?: 0;
}
/**
* Get supported languages
*
* @return array
*/
public static function get_supported_languages() {
return array(
'en' => 'English',
'fr' => 'Français',
'de' => 'Deutsch',
'es' => 'Español',
'it' => 'Italiano',
'pt' => 'Português',
'ru' => 'Русский',
'ja' => '日本語',
'zh' => '中文',
);
}
/**
* Get awards list
*
* @return array
*/
public static function get_awards() {
return get_option('bdm_awards', array());
}
/**
* Generate SEO tags for a book
*
* @param int $book_id Book post ID.
*/
public static function output_seo_tags($book_id) {
$title = get_post_meta($book_id, 'seo_title', true) ?: get_the_title($book_id) . ' | ' . get_bloginfo('name');
$description = get_post_meta($book_id, 'seo_description', true) ?: get_post_meta($book_id, 'blurb', true);
if ($description) {
$description = substr($description, 0, 160);
}
printf('<meta name="description" content="%s">' . "\n", esc_attr($description));
printf('<meta property="og:title" content="%s">' . "\n", esc_attr($title));
printf('<meta property="og:description" content="%s">' . "\n", esc_attr($description));
printf('<meta property="og:type" content="book">' . "\n");
printf('<meta name="twitter:card" content="summary_large_image">' . "\n");
}
/**
* Generate schema.org data
*
* @param int $book_id Book post ID.
* @return array
*/
public static function generate_schema($book_id) {
$book = get_post($book_id);
$schema = array(
'@context' => 'https://schema.org',
'@type' => 'Book',
'name' => $book->post_title,
'author' => array(
'@type' => 'Person',
'name' => get_bloginfo('name'),
),
);
$asin = get_post_meta($book_id, 'asin', true);
if ($asin) {
$schema['isbn'] = $asin;
}
$cover_id = get_post_thumbnail_id($book_id);
if ($cover_id) {
$cover_url = wp_get_attachment_url($cover_id);
$schema['image'] = $cover_url;
}
$blurb = get_post_meta($book_id, 'blurb', true);
if ($blurb) {
$schema['description'] = $blurb;
}
$series_term = get_the_terms($book_id, 'series');
if ($series_term && !is_wp_error($series_term)) {
$schema['isPartOf'] = array(
'@type' => 'BookSeries',
'name' => $series_term[0]->name,
);
}
return $schema;
}
}