Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve Story Discoverability on Frontend #2191

Merged
merged 5 commits into from
Jun 4, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 () {
swissspidy marked this conversation as resolved.
Show resolved Hide resolved
// 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() {
swissspidy marked this conversation as resolved.
Show resolved Hide resolved
$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