Skip to content

Commit

Permalink
Improve Story Discoverability on Frontend (#2191)
Browse files Browse the repository at this point in the history
  • Loading branch information
swissspidy authored Jun 4, 2020
1 parent 5d193b8 commit a4caabd
Show file tree
Hide file tree
Showing 6 changed files with 393 additions and 280 deletions.
310 changes: 310 additions & 0 deletions includes/Discovery.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,310 @@
<?php
/**
* Class Discovery.
*
* Responsible for improved discovery of stories on the web.
*
* @package Google\Web_Stories
* @copyright 2020 Google LLC
* @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0
* @link https://github.com/google/web-stories-wp
*/

/**
* Copyright 2020 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

namespace Google\Web_Stories;

use Google\Web_Stories\REST_API\Stories_Controller;
use WP_Post;

/**
* Discovery class.
*/
class Discovery {
/**
* Initialize discovery functionality.
*
* @return void
*/
public function init() {
add_action(
'web_stories_story_head',
static function () {
// Theme support for title-tag is implied for stories. See _wp_render_title_tag().
echo '<title>' . esc_html( wp_get_document_title() ) . '</title>' . "\n";
},
1
);

add_action( 'web_stories_story_head', [ $this, 'print_schemaorg_metadata' ] );
add_action( 'web_stories_story_head', [ $this, 'print_open_graph_metadata' ] );
add_action( 'web_stories_story_head', [ $this, 'print_twitter_metadata' ] );

// @todo Check if there's something to skip in the new version.
add_action( 'web_stories_story_head', 'rest_output_link_wp_head', 10, 0 );
add_action( 'web_stories_story_head', 'wp_resource_hints', 2 );
add_action( 'web_stories_story_head', 'feed_links', 2 );
add_action( 'web_stories_story_head', 'feed_links_extra', 3 );
add_action( 'web_stories_story_head', 'rsd_link' );
add_action( 'web_stories_story_head', 'wlwmanifest_link' );
add_action( 'web_stories_story_head', 'adjacent_posts_rel_link_wp_head', 10, 0 );
add_action( 'web_stories_story_head', 'noindex', 1 );
add_action( 'web_stories_story_head', 'wp_generator' );
add_action( 'web_stories_story_head', 'rel_canonical' );
add_action( 'web_stories_story_head', 'wp_shortlink_wp_head', 10, 0 );
add_action( 'web_stories_story_head', 'wp_site_icon', 99 );
add_action( 'web_stories_story_head', 'wp_oembed_add_discovery_links' );
}

/**
* Prints the schema.org metadata on the single story template.
*
* @return void
*/
public function print_schemaorg_metadata() {
$metadata = $this->get_schemaorg_metadata();

?>
<script type="application/ld+json"><?php echo wp_json_encode( $metadata, JSON_UNESCAPED_UNICODE ); ?></script>
<?php
}

/**
* Get schema.org metadata for the current query.
*
* @see https://developers.google.com/search/docs/guides/enable-web-stories
*
* @return array $metadata All schema.org metadata for the post.
*/
protected function get_schemaorg_metadata() {
$publisher = self::get_publisher_data();

$metadata = [
'@context' => 'http://schema.org',
'publisher' => [
'@type' => 'Organization',
'name' => $publisher['name'],
'logo' => $publisher['logo'],
],
];

/**
* We're expecting a post object.
*
* @var WP_Post $post
*/
$post = get_queried_object();

if ( $post instanceof WP_Post ) {
$metadata = array_merge(
$metadata,
[
'@type' => 'Article',
'mainEntityOfPage' => get_permalink( $post ),
'headline' => get_the_title( $post ),
'datePublished' => mysql2date( 'c', $post->post_date_gmt, false ),
'dateModified' => mysql2date( 'c', $post->post_modified_gmt, false ),
]
);

$post_author = get_userdata( (int) $post->post_author );

if ( $post_author ) {
$metadata['author'] = [
'@type' => 'Person',
'name' => html_entity_decode( $post_author->display_name, ENT_QUOTES, get_bloginfo( 'charset' ) ),
];
}

if ( has_post_thumbnail( $post->ID ) ) {
$metadata['image'] = wp_get_attachment_image_url( (int) get_post_thumbnail_id( $post->ID ), 'full' );
}
}

/**
* Filters the schema.org metadata for a given story.
*
* @param array $metadata The structured data.
* @param WP_Post $post The current post object.
*/
return apply_filters( 'web_stories_story_schema_metadata', $metadata, $post );
}

/**
* Prints Open Graph metadata.
*
* @return void
*/
public function print_open_graph_metadata() {
?>
<meta property="og:locale" content="<?php echo esc_attr( get_bloginfo( 'language' ) ); ?>" />
<meta property="og:type" content="article" />
<meta property="og:title" content="<?php the_title_attribute(); ?>" />
<meta property="og:url" content="<?php the_permalink(); ?>">
<meta property="og:site_name" content="<?php echo esc_attr( get_bloginfo( 'name' ) ); ?>">
<?php
if ( ! get_post() ) {
return;
}
?>
<meta property="article:published_time" content="<?php echo esc_attr( (string) get_the_date( 'c' ) ); ?>">
<meta property="article:modified_time" content="<?php echo esc_attr( (string) get_the_modified_date( 'c' ) ); ?>">
<?php

if ( ! has_post_thumbnail() ) {
return;
}

$poster = wp_get_attachment_image_src( (int) get_post_thumbnail_id(), 'full' );

if ( ! $poster ) {
return;
}
?>
<meta property="og:image" content="<?php echo esc_url( $poster[0] ); ?>">
<meta property="og:image:width" content="<?php echo esc_attr( $poster[1] ); ?>">
<meta property="og:image:height" content="<?php echo esc_attr( $poster[2] ); ?>">
<?php
}

/**
* Prints Twitter card metadata.
*
* @return void
*/
public function print_twitter_metadata() {
?>
<meta name="twitter:card" content="summary_large_image" />
<?php

if ( ! has_post_thumbnail() ) {
return;
}

$poster = wp_get_attachment_image_url( (int) get_post_thumbnail_id(), Media::STORY_POSTER_IMAGE_SIZE );

if ( ! $poster ) {
return;
}
?>
<meta property="twtter:image" content="<?php echo esc_url( $poster ); ?>">
<?php
}

/**
* Gets a valid publisher logo URL. Loops through sizes and looks for a square image.
*
* @param integer $image_id Attachment ID.
*
* @return string|false Either the URL or false if error.
*/
private static function get_valid_publisher_image( $image_id ) {
$logo_image_url = false;

// Get metadata for finding a square image.
$metadata = wp_get_attachment_metadata( $image_id );
if ( empty( $metadata ) ) {
return $logo_image_url;
}
// First lets check if the image is square by default.
$fullsize_img = wp_get_attachment_image_src( $image_id, 'full', false );
if ( $metadata['width'] === $metadata['height'] && is_array( $fullsize_img ) ) {
return array_shift( $fullsize_img );
}

if ( empty( $metadata['sizes'] ) ) {
return $logo_image_url;
}

// Loop through other size to find a square image.
foreach ( $metadata['sizes'] as $size ) {
if ( $size['width'] === $size['height'] && $size['width'] >= 96 ) {
$logo_img = wp_get_attachment_image_src( $image_id, [ $size['width'], $size['height'] ], false );
if ( is_array( $logo_img ) ) {
return array_shift( $logo_img );
}
}
}

// If a square image was not found, return the full size nevertheless,
// the editor should take care of warning about incorrect size.
return is_array( $fullsize_img ) ? array_shift( $fullsize_img ) : false;
}

/**
* Get the publisher logo.
*
* @link https://developers.google.com/search/docs/data-types/article#logo-guidelines
* @link https://amp.dev/documentation/components/amp-story/#publisher-logo-src-guidelines
*
* @return string Publisher logo image URL. WordPress logo if no site icon or custom logo defined, and no logo provided via 'amp_site_icon_url' filter.
*/
public static function get_publisher_logo() {
$logo_image_url = null;

$publisher_logo_settings = get_option( Stories_Controller::PUBLISHER_LOGOS_OPTION, [] );
$has_publisher_logo = ! empty( $publisher_logo_settings['active'] );
if ( $has_publisher_logo ) {
$publisher_logo_id = absint( $publisher_logo_settings['active'] );
$logo_image_url = self::get_valid_publisher_image( $publisher_logo_id );
}

// @todo Once we are enforcing setting publisher logo in the editor, we shouldn't need the fallback options.
// Currently, it's marked as required but that's not actually enforced.

// Finding fallback image.
$custom_logo_id = get_theme_mod( 'custom_logo' );
if ( empty( $logo_image_url ) && has_custom_logo() && $custom_logo_id ) {
$logo_image_url = self::get_valid_publisher_image( $custom_logo_id );
}

// Try Site Icon, though it is not ideal for non-Story because it should be square.
$site_icon_id = get_option( 'site_icon' );
if ( empty( $logo_image_url ) && $site_icon_id ) {
$logo_image_url = self::get_valid_publisher_image( $site_icon_id );
}

// Fallback to serving the WordPress logo.
if ( empty( $logo_image_url ) ) {
$logo_image_url = WEBSTORIES_PLUGIN_DIR_URL . 'assets/images/fallback-wordpress-publisher-logo.png';
}

/**
* Filters the publisher's logo.
*
* This should point to a square image.
*
* @param string $logo_image_url URL to the publisher's logo.
*/
return apply_filters( 'web_stories_publisher_logo', $logo_image_url );
}

/**
* Returns the publisher data.
*
* @return array Publisher name and logo.
*/
public static function get_publisher_data() {
$publisher = get_bloginfo( 'name' );
$publisher_logo = self::get_publisher_logo();

return [
'name' => $publisher,
'logo' => $publisher_logo,
];
}
}
73 changes: 73 additions & 0 deletions includes/Media.php
Original file line number Diff line number Diff line change
Expand Up @@ -319,4 +319,77 @@ public static function delete_video_poster( $attachment_id ) {
wp_delete_attachment( $post_id, true );
}
}

/**
* Returns a list of allowed file types.
*
* @return array List of allowed file types.
*/
public static function get_allowed_file_types() {
$allowed_mime_types = self::get_allowed_mime_types();
$mime_types = [];

foreach ( $allowed_mime_types as $mimes ) {
// Otherwise this throws a warning on PHP < 7.3.
if ( ! empty( $mimes ) ) {
array_push( $mime_types, ...$mimes );
}
}

$allowed_file_types = [];
$all_mime_types = wp_get_mime_types();

foreach ( $all_mime_types as $ext => $mime ) {
if ( in_array( $mime, $mime_types, true ) ) {
array_push( $allowed_file_types, ...explode( '|', $ext ) );
}
}
sort( $allowed_file_types );

return $allowed_file_types;
}

/**
* Returns a list of allowed mime types per media type (image, audio, video).
*
* @return array List of allowed mime types.
*/
public static function get_allowed_mime_types() {
$default_allowed_mime_types = [
'image' => [
'image/png',
'image/jpeg',
'image/jpg',
'image/gif',
],
'audio' => [], // todo: support audio uploads.
'video' => [
'video/mp4',
'video/webm',
],
];

/**
* Filter list of allowed mime types.
*
* This can be used to add additionally supported formats, for example by plugins
* that do video transcoding.
*
* @since 1.3
*
* @param array $default_allowed_mime_types Associative array of allowed mime types per media type (image, audio, video).
*/
$allowed_mime_types = apply_filters( 'web_stories_allowed_mime_types', $default_allowed_mime_types );

foreach ( array_keys( $default_allowed_mime_types ) as $media_type ) {
if ( ! is_array( $allowed_mime_types[ $media_type ] ) || empty( $allowed_mime_types[ $media_type ] ) ) {
$allowed_mime_types[ $media_type ] = $default_allowed_mime_types[ $media_type ];
}

// Only add currently supported mime types.
$allowed_mime_types[ $media_type ] = array_values( array_intersect( $allowed_mime_types[ $media_type ], wp_get_mime_types() ) );
}

return $allowed_mime_types;
}
}
Loading

0 comments on commit a4caabd

Please sign in to comment.