Skip to content

Commit

Permalink
Merge pull request #143 from alleyinteractive/feature/issue-142/add-o…
Browse files Browse the repository at this point in the history
…ption-pull-trending-posts

Issue-142: Add option to pull Trending posts
  • Loading branch information
mogmarsh authored Mar 7, 2024
2 parents d9f5aa0 + c70d5be commit 4b9f2dd
Show file tree
Hide file tree
Showing 12 changed files with 403 additions and 41 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

All notable changes to `WP Curate` will be documented in this file.

## 1.7.0 - 2024-03-06

- Enhancement: Integration with [Parse.ly plugin](https://wordpress.org/plugins/wp-parsely/) to support querying trending posts.

## 1.6.3 - 2024-02-14

- Bug Fix: Selecting a post more than once in a Query block causes empty slots at the end.
Expand Down
8 changes: 8 additions & 0 deletions blocks/query/block.json
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,14 @@
"OR"
],
"type": "string"
},
"orderby": {
"default": "date",
"enum": [
"date",
"trending"
],
"type": "string"
}
}
}
22 changes: 16 additions & 6 deletions blocks/query/edit.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
RangeControl,
SelectControl,
TextControl,
ToggleControl,
} from '@wordpress/components';
import { useSelect } from '@wordpress/data';
import {
Expand All @@ -22,7 +23,6 @@ import {
import { __, sprintf } from '@wordpress/i18n';
import { addQueryArgs } from '@wordpress/url';

import type { WP_REST_API_Post, WP_REST_API_Posts } from 'wp-types';
import { Template } from '@wordpress/blocks';
import type {
EditProps,
Expand All @@ -44,6 +44,7 @@ interface Window {
wpCurateQueryBlock: {
allowedPostTypes: Array<string>;
allowedTaxonomies: Array<string>;
parselyAvailable: string,
};
}

Expand All @@ -69,13 +70,15 @@ export default function Edit({
terms = {},
termRelations = {},
taxRelation = 'AND',
orderby = 'date',
},
setAttributes,
}: EditProps) {
const {
wpCurateQueryBlock: {
allowedPostTypes = [],
allowedTaxonomies = [],
parselyAvailable = 'false',
} = {},
} = (window as any as Window);

Expand Down Expand Up @@ -154,24 +157,22 @@ export default function Edit({
}
const fetchPosts = async () => {
let path = addQueryArgs(
'/wp/v2/posts',
'/wp-curate/v1/posts',
{
search: debouncedSearchTerm,
offset,
type: postTypeString,
status: 'publish',
per_page: 20,
orderby,
},
);
path += `&${termQueryArgs}`;

apiFetch({
path,
}).then((response) => {
const postIds: number[] = (response as WP_REST_API_Posts).map(
(post: WP_REST_API_Post) => post.id,
);
setAttributes({ backfillPosts: postIds });
setAttributes({ backfillPosts: response as Array<number> });
});
};
fetchPosts();
Expand All @@ -181,6 +182,7 @@ export default function Edit({
offset,
postTypeString,
availableTaxonomies,
orderby,
setAttributes,
]);

Expand Down Expand Up @@ -384,6 +386,14 @@ export default function Edit({
onChange={(next) => setAttributes({ searchTerm: next })}
value={searchTerm}
/>
{ parselyAvailable === 'true' ? (
<ToggleControl
label={__('Show Trending Content from Parsely', 'wp-curate')}
help={__('If enabled, the block will show trending content from Parsely.', 'wp-curate')}
checked={orderby === 'trending'}
onChange={(next) => setAttributes({ orderby: next ? 'trending' : 'date' })}
/>
) : null }
</PanelBody>
</InspectorControls>

Expand Down
11 changes: 11 additions & 0 deletions blocks/query/index.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,20 +31,31 @@ function wp_curate_query_block_init(): void {

/**
* Filter the post types that can be used in the Query block.
*
* @param array<string> $allowed_post_types The allowed post types.
*/
$allowed_post_types = apply_filters( 'wp_curate_allowed_post_types', [ 'post' ] );

/**
* Filter the taxonomies that can be used in the Query block.
*
* @param array<string> $allowed_taxonomies The allowed taxonomies.
*/
$allowed_taxonomies = apply_filters( 'wp_curate_allowed_taxonomies', [ 'category', 'post_tag' ] );

/**
* Filter whether to use Parsely.
*
* @param bool $use_parsely Whether to use Parsely.
*/
$parsely_available = apply_filters( 'wp_curate_use_parsely', false );
wp_localize_script(
'wp-curate-query-editor-script',
'wpCurateQueryBlock',
[
'allowedPostTypes' => $allowed_post_types,
'allowedTaxonomies' => $allowed_taxonomies,
'parselyAvailable' => $parsely_available ? 'true' : 'false',
]
);
}
Expand Down
1 change: 1 addition & 0 deletions blocks/query/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ interface EditProps {
[key: string]: string;
};
taxRelation?: string;
orderby?: string;
};
setAttributes: (attributes: any) => void;
}
Expand Down
2 changes: 1 addition & 1 deletion src/class-plugin-curated-posts.php
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ public function with_query_context( array $context, array $attributes, WP_Block_
'no_found_rows' => true,
'offset' => $attributes['offset'] ?? $block_type->attributes['offset']['default'],
'order' => 'DESC',
'orderby' => 'date',
'orderby' => $attributes['orderby'] ?? 'date',
'posts_per_page' => $attributes['numberOfPosts'] ?? $block_type->attributes['numberOfPosts']['default'],
'post_status' => 'publish',
'post_type' => $attributes['postTypes'] ?? $block_type->attributes['postTypes']['default'],
Expand Down
47 changes: 47 additions & 0 deletions src/class-trending-post-queries.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<?php
/**
* Trending_Post_Queries class file
*
* @package wp-type-extensions
*/

namespace Alley\WP\WP_Curate;

use Alley\WP\Types\Post_Queries;
use Alley\WP\Types\Post_Query;
use Alley\WP\Post_Query\WP_Query_Envelope;
use Alley\WP\WP_Curate\Features\Parsely_Support;
use Alley\WP\Post_Query\Post_IDs_Query;

/**
* Pull trending posts from Parsely.
*/
final class Trending_Post_Queries implements Post_Queries {
/**
* Set up.
*
* @param Post_Queries $origin Post_Queries object.
* @param Parsely_Support $parsely Parsely_Support object.
*/
public function __construct(
private readonly Post_Queries $origin,
private readonly Parsely_Support $parsely
) {}

/**
* Query for posts using literal arguments.
*
* @param array<string, mixed> $args The arguments to be used in the query.
* @return Post_Query
*/
public function query( array $args ): Post_Query {
if ( isset( $args['orderby'] ) && 'trending' === $args['orderby'] ) {
$trending = $this->parsely->get_trending_posts( $args );
if ( ! empty( $trending ) ) {
return new Post_IDs_Query( $trending );
}
}

return $this->origin->query( $args );
}
}
178 changes: 178 additions & 0 deletions src/features/class-parsely-support.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
<?php
/**
* Parsely_Support class file
*
* @package wp-curate
*/

namespace Alley\WP\WP_Curate\Features;

use Alley\WP\Types\Feature;
use Parsely\RemoteAPI\Analytics_Posts_API;
use Parsely\Parsely;

/**
* Add support for Parsely, if the plugin is installed.
*/
final class Parsely_Support implements Feature {
/**
* Set up.
*/
public function __construct() {}

/**
* Boot the feature.
*/
public function boot(): void {
if ( ! class_exists( 'Parsely\Parsely' ) ) {
return;
}
// Elsewhere in the plugin, we'll use $GLOBALS['parsely'], but it is not available here.
$parsely = new \Parsely\Parsely();
// If we don't have the API secret, we can't use the Parsely API.
if ( ! $parsely->api_secret_is_set() ) {
return;
}
add_filter( 'wp_curate_use_parsely', '__return_true' );
add_filter( 'wp_curate_trending_posts_query', [ $this, 'add_parsely_trending_posts_query' ], 10, 2 );
}

/**
* Gets the trending posts from Parsely.
*
* @param array<number> $posts The posts, which should be an empty array.
* @param array<string, mixed> $args The WP_Query args.
* @return array<number> Array of post IDs.
*/
public function add_parsely_trending_posts_query( array $posts, array $args ): array {
$parsely = $GLOBALS['parsely'];
if ( ! $parsely->api_secret_is_set() ) {
return $posts;
}
$trending_posts = $this->get_trending_posts( $args );
return $trending_posts;
}

/**
* Gets the trending posts from Parsely.
*
* @param array<string, mixed> $args The WP_Query args.
* @return array<int> An array of post IDs.
*/
public function get_trending_posts( array $args ): array {
if ( ! class_exists( '\Parsely\Parsely' ) || ! isset( $GLOBALS['parsely'] ) || ! $GLOBALS['parsely'] instanceof Parsely ) {
return [];
}
if ( ! class_exists( '\Parsely\RemoteAPI\Analytics_Posts_API' ) ) {
return [];
}

$parsely_options = $GLOBALS['parsely']->get_options();
/**
* Filter the period start for the Parsely API.
*
* @param string $period_start The period start.
* @param array<string, mixed> $args The WP_Query args.
*/
$period_start = apply_filters( 'wp_curate_parsely_period_start', '1d', $args );
/**
* Filter the period end for the Parsely API.
*
* @param string $period_end The period end.
* @param array<string, mixed> $args The WP_Query args.
*/
$period_end = apply_filters( 'wp_curate_parsely_period_end', 'now', $args );
$parsely_args = [
'limit' => $args['posts_per_page'] ?? get_option( 'posts_per_page' ),
'sort' => 'views',
'period_start' => $period_start,
'period_end' => $period_end,
];
if ( isset( $args['tax_query'] ) && is_array( $args['tax_query'] ) ) {
foreach ( $args['tax_query'] as $tax_query ) {
if ( isset( $tax_query['taxonomy'] ) && $parsely_options['custom_taxonomy_section'] === $tax_query['taxonomy'] ) {
$parsely_args['section'] = implode( ', ', $this->get_slugs_from_term_ids( $tax_query['terms'], $tax_query['taxonomy'] ) );
}
if ( isset( $tax_query['taxonomy'] ) && 'post_tag' === $tax_query['taxonomy'] ) {
$parsely_args['tag'] = implode( ', ', $this->get_slugs_from_term_ids( $tax_query['terms'], $tax_query['taxonomy'] ) );
}
}
if ( $parsely_options['cats_as_tags'] ) {
$tags = explode( ', ', $parsely_args['tag'] ?? '' );
$sections = explode( ', ', $parsely_args['section'] ?? '' );
$parsely_args['tag'] = implode( ', ', array_merge( $tags, $sections ) );
}
}
$cache_key = 'parsely_trending_posts_' . md5( wp_json_encode( $parsely_args ) ); // @phpstan-ignore-line - wp_Json_encode not likely to return false.
$ids = wp_cache_get( $cache_key );
if ( false === $ids || ! is_array( $ids ) ) {
$api = new Analytics_Posts_API( $GLOBALS['parsely'] );
$posts = $api->get_posts_analytics( $parsely_args );
$ids = array_map(
function ( $post ) {
// Check if the metadata contains post_id, if not, use the URL to get the post ID.
$metadata = json_decode( $post['metadata'] ?? '', true );
if ( is_array( $metadata ) && isset( $metadata['post_id'] ) ) {
$post_id = (int) $metadata['post_id'];
} elseif ( function_exists( 'wpcom_vip_url_to_postid' ) ) {
$post_id = wpcom_vip_url_to_postid( $post['url'] );
} else {
$post_id = url_to_postid( $post['url'] ); // phpcs:ignore WordPressVIPMinimum.Functions.RestrictedFunctions.url_to_postid_url_to_postid
}
/**
* Filters the post ID derived from Parsely post object.
*
* @param int $post_id The post ID.
* @param array $post The Parsely post object.
*/
return apply_filters( 'wp_curate_parsely_post_to_post_id', $post_id, $post );
},
$posts
);
/**
* Filters the cache duration for the trending posts from Parsely.
*
* @param int $cache_duration The cache duration.
* @param array<string, mixed> $args The WP_Query args.
*/
$cache_duration = apply_filters( 'wp_curate_parsely_trending_posts_cache_duration', 10 * MINUTE_IN_SECONDS, $args );
if ( 300 > $cache_duration ) {
$cache_duration = 300;
}
wp_cache_set( $cache_key, $ids, '', $cache_duration ); // phpcs:ignore WordPressVIPMinimum.Performance.LowExpiryCacheTime.CacheTimeUndetermined
}

/**
* Filters the trending posts from Parsely.
*
* @param array<int> $ids The list of post IDs.
* @param array<string, mixed> $parsely_args The Parsely API args.
* @param array<string, mixed> $args The WP_Query args.
*/
$ids = apply_filters( 'wp_curate_parsely_trending_posts', $ids, $parsely_args, $args );

return $ids;
}

/**
* Get slugs from term IDs.
*
* @param array<int> $ids The list of term ids.
* @param string $taxonomy The taxonomy.
* @return array<string> The list of term slugs.
*/
private function get_slugs_from_term_ids( $ids, $taxonomy ) {
$terms = array_filter(
array_map(
function ( $id ) use ( $taxonomy ) {
$term = get_term( $id, $taxonomy );
if ( $term instanceof \WP_Term ) {
return $term->slug;
}
},
$ids
)
);
return $terms;
}
}
Loading

0 comments on commit 4b9f2dd

Please sign in to comment.