(
+ f: ( ...args: Args ) => Return
+) => {
+ let token: number|null = null;
+ let lastArgs: Args|null = null;
+
+ const invoke = () => {
+ if ( lastArgs !== null ) {
+ f( ...lastArgs );
+ }
+
+ token = null;
+ };
+
+ const result = ( ...args: Args ) => {
+ lastArgs = args;
+ if ( ! token ) {
+ token = requestAnimationFrame( invoke );
+ }
+ };
+
+ result.cancel = () => token && cancelAnimationFrame( token );
+ return result;
+};
diff --git a/src/content-helper/dashboard-page/pages/traffic-boost/preview/hooks/use-iframe-highlight.ts b/src/content-helper/dashboard-page/pages/traffic-boost/preview/hooks/use-iframe-highlight.ts
index 37bd7754bb..e70814532a 100644
--- a/src/content-helper/dashboard-page/pages/traffic-boost/preview/hooks/use-iframe-highlight.ts
+++ b/src/content-helper/dashboard-page/pages/traffic-boost/preview/hooks/use-iframe-highlight.ts
@@ -1,16 +1,25 @@
/**
- * WordPress imports
+ * WordPress dependencies
*/
-import { useCallback } from '@wordpress/element';
+import { createRoot, useCallback } from '@wordpress/element';
import { __ } from '@wordpress/i18n';
/**
- * Internal imports
+ * Internal dependencies
*/
+import { throttle } from '@wordpress/compose';
import { escapeRegExp } from '../../../../../common/utils/functions';
import { TrafficBoostLink } from '../../provider';
import { LinkType } from '../components/link-counter';
import { TextSelection } from '../preview';
+import { DRAG_MARGIN_PX } from './use-draggable';
+import { useWordpressComponentStyles } from './use-wordpress-component-styles';
+
+declare global {
+ interface Window {
+ wpParselyTrafficBoostCleanupActionsBar?: () => void;
+ }
+}
/**
* Props for the useIframeHighlight hook.
@@ -24,6 +33,7 @@ interface UseIframeHighlightProps {
selectedText?: TextSelection | null;
isInboundLink: boolean;
onRestoreOriginal: () => void;
+ actionsBar: React.ReactNode;
}
/**
@@ -42,7 +52,10 @@ export const useIframeHighlight = ( {
selectedText,
isInboundLink,
onRestoreOriginal,
+ actionsBar,
}: UseIframeHighlightProps ) => {
+ const { injectWordpressComponentStyles } = useWordpressComponentStyles();
+
/**
* Injects highlight styles into the iframe.
*
@@ -56,6 +69,8 @@ export const useIframeHighlight = ( {
return;
}
+ injectWordpressComponentStyles( iframeDocument );
+
const style = iframeDocument.createElement( 'style' );
style.textContent = `
/** Smart link highlight styles. */
@@ -152,9 +167,89 @@ export const useIframeHighlight = ( {
color: inherit;
}
}
+
+ /* Actions bar styles. */
+ .parsely-traffic-boost-actions-container {
+ position: absolute;
+ z-index: 1000;
+ top: ${ DRAG_MARGIN_PX }px;
+ left: ${ DRAG_MARGIN_PX }px;
+ user-select: none;
+ opacity: 0;
+ transition: opacity 0.1s ease-in-out;
+ }
+
+ .parsely-traffic-boost-actions-container.fade-in {
+ opacity: 1;
+ }
+
+ .parsely-traffic-boost-actions-container.align-left {
+ left: ${ DRAG_MARGIN_PX }px;
+ }
+
+ .parsely-traffic-boost-actions-container.align-right {
+ left: auto;
+ right: ${ DRAG_MARGIN_PX }px;
+ }
+
+ .parsely-traffic-boost-actions-container .traffic-boost-preview-actions {
+ /* Reset font family to editor defaults to avoid inheriting frontend font in actions toolbar. */
+ font-family: -apple-system, BlinkMacSystemFont,"Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell,"Helvetica Neue", sans-serif;
+ height: 48px;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ background: rgba(255, 255, 255, 1);
+ border: 1px solid #1e1e1e;
+ border-radius: 2px;
+ gap: 8px;
+ }
+
+ .parsely-traffic-boost-actions-container .traffic-boost-preview-actions-drag-handle {
+ flex-shrink: 0;
+ cursor: grab;
+ border-right: 1px solid #1e1e1e;
+ padding: 0 8px;
+ height: 100%;
+ display: flex;
+ align-items: center;
+ }
+
+ .parsely-traffic-boost-actions-container .traffic-boost-preview-actions-drag-handle.dragging {
+ cursor: grabbing;
+ }
+
+ .parsely-traffic-boost-actions-container .traffic-boost-preview-actions-buttons {
+ display: flex;
+ gap: 4px;
+ align-items: center;
+ flex-wrap: nowrap;
+ justify-content: center;
+ padding-right: 8px;
+ }
+
+ .parsely-traffic-boost-actions-container .traffic-boost-preview-actions-buttons .components-button {
+ height: 36px;
+ white-space: nowrap;
+ }
+
+ .parsely-traffic-boost-actions-container .traffic-boost-preview-actions-hint {
+ display: flex;
+ cursor: help;
+ user-select: none;
+ align-items: center;
+ }
+
+ .parsely-traffic-boost-actions-container .traffic-boost-preview-actions-hint-text {
+ font-size: 13px;
+ font-family: inherit;
+ white-space: nowrap;
+ margin-left: 4px;
+ color: #2F2F2F;
+ }
`;
iframeDocument.head.appendChild( style );
- }, [] );
+ }, [ injectWordpressComponentStyles ] );
/**
* Finds all ranges containing the text.
@@ -263,6 +358,11 @@ export const useIframeHighlight = ( {
return;
}
+ const existingActions = iframeDocument.querySelector( '.parsely-traffic-boost-actions-container' );
+ if ( existingActions && window.wpParselyTrafficBoostCleanupActionsBar ) {
+ window.wpParselyTrafficBoostCleanupActionsBar();
+ }
+
const fragment = range.cloneContents();
const highlightSpan = iframeDocument.createElement( 'span' );
highlightSpan.className = isPrevious
@@ -288,7 +388,6 @@ export const useIframeHighlight = ( {
if ( isFullLinkSelected && linkNode ) {
// Create a new span and insert it before the link.
linkNode.parentNode?.insertBefore( highlightSpan, linkNode );
-
// Move the link into the span.
highlightSpan.appendChild( linkNode );
} else {
@@ -298,12 +397,109 @@ export const useIframeHighlight = ( {
range.insertNode( highlightSpan );
}
+ const actionsContainer = iframeDocument.createElement( 'div' );
+ actionsContainer.className = 'parsely-traffic-boost-actions-container';
+ iframeDocument.body.appendChild( actionsContainer );
+
+ // Create popover content.
+ const root = createRoot( actionsContainer );
+ root.render( actionsBar );
+
+ /**
+ * Sets up the actions bar cleanup function.
+ *
+ * @since 3.20.0
+ */
+ window.wpParselyTrafficBoostCleanupActionsBar = () => {
+ window.wpParselyTrafficBoostCleanupActionsBar = undefined;
+
+ // resizeHandler is throttled, so cancel any pending calls.
+ if ( typeof resizeHandler.cancel === 'function' ) {
+ resizeHandler.cancel();
+ }
+
+ iframeDocument.defaultView?.removeEventListener( 'resize', resizeHandler );
+
+ root.unmount();
+
+ if ( actionsContainer.parentNode ) {
+ actionsContainer.parentNode.removeChild( actionsContainer );
+ }
+ };
+
+ /**
+ * Positions the actions bar, ensuring it remains visible and
+ * aligned within boundaries.
+ *
+ * @since 3.20.0
+ */
+ const positionActionsBar = () => {
+ const renderedActionsBar = iframeDocument.querySelector( '.traffic-boost-preview-actions' ) as HTMLElement;
+ if ( ! renderedActionsBar ) {
+ return;
+ }
+
+ // Reset any transform that's already applied to the
+ // actionsBar from a manual drag.
+ renderedActionsBar.style.transform = '';
+
+ const highlightRect = highlightSpan.getBoundingClientRect();
+ const iframeRect = iframeDocument.documentElement.getBoundingClientRect();
+ const actionsRect = renderedActionsBar.getBoundingClientRect();
+
+ // Reset any existing alignment classes.
+ actionsContainer.classList.remove( 'align-left', 'align-right' );
+
+ // Calculate base position above highlight, accounting for scroll position.
+ const PIXELS_ABOVE_HIGHLIGHT = 35;
+ const scrollTop = iframeDocument.documentElement.scrollTop;
+ const top = highlightRect.top + scrollTop - PIXELS_ABOVE_HIGHLIGHT - actionsRect.height;
+ const left = highlightRect.left + ( highlightRect.width / 2 ) - ( actionsRect.width / 2 );
+
+ // Set initial position
+ actionsContainer.style.top = `${ Math.max( top, 0 ) }px`;
+
+ // Check if the actions bar would be cut off on either side.
+ const actionsWidth = actionsRect.width;
+ const iframeWidth = iframeRect.width;
+ const actionsLeft = left;
+ const actionsRight = left + actionsWidth;
+
+ if ( actionsRight > iframeWidth ) {
+ // Would be cut off on right, align to right.
+ actionsContainer.classList.add( 'align-right' );
+ actionsContainer.style.left = ''; // Clear inline left style.
+ } else if ( actionsLeft < 0 ) {
+ // Would be cut off on left, align to left.
+ actionsContainer.classList.add( 'align-left' );
+ actionsContainer.style.left = ''; // Clear inline left style.
+ } else {
+ // Center position is fine, set left directly.
+ actionsContainer.style.left = `${ left }px`;
+ }
+
+ // Add fade-in animation after positioning.
+ actionsContainer.classList.add( 'fade-in' );
+ };
+
+ // Setup initial position. Wait 400ms for auto-scroll to complete
+ // so that position calculations from scrollTop are correct.
+ if ( iframeDocument.documentElement.scrollTop === 0 ) {
+ setTimeout( positionActionsBar, 400 );
+ } else {
+ setTimeout( positionActionsBar, 0 );
+ }
+
+ // Reposition on resize.
+ const resizeHandler = throttle( () => positionActionsBar(), 100 );
+ iframeDocument.defaultView?.addEventListener( 'resize', resizeHandler );
+
return highlightSpan;
} catch ( e ) {
// eslint-disable-next-line no-console
console.error( 'WP Parsely: Error highlighting range', e );
}
- }, [ iframeRef ] );
+ }, [ iframeRef, actionsBar ] );
/**
* Removes highlight spans from the iframe content.
@@ -327,9 +523,14 @@ export const useIframeHighlight = ( {
* @since 3.19.0
*
* @param {Element} highlight The highlight element to remove.
- * @param {ParentNode} parent The parent node of the highlight.
+ * @param {ParentNode} parent The parent node of the container, e.g. a tag.
*/
const removeAndClean = ( highlight: Element, parent: ParentNode ) => {
+ // Clean up actions bar if it exists
+ if ( window.wpParselyTrafficBoostCleanupActionsBar ) {
+ window.wpParselyTrafficBoostCleanupActionsBar();
+ }
+
// Create a document fragment to temporarily hold the children.
const fragment = iframeDocument.createDocumentFragment();
@@ -405,7 +606,7 @@ export const useIframeHighlight = ( {
}
} );
} catch ( error ) {
- // Silently fail if there's an error removing highlights.
+ console.error( 'WP Parsely: Error removing highlights:', error ); // eslint-disable-line no-console
}
}, [] );
diff --git a/src/content-helper/dashboard-page/pages/traffic-boost/preview/hooks/use-wordpress-component-styles.ts b/src/content-helper/dashboard-page/pages/traffic-boost/preview/hooks/use-wordpress-component-styles.ts
new file mode 100644
index 0000000000..4a2b61bc6d
--- /dev/null
+++ b/src/content-helper/dashboard-page/pages/traffic-boost/preview/hooks/use-wordpress-component-styles.ts
@@ -0,0 +1,37 @@
+/**
+ * WordPress dependencies
+ */
+import { useCallback } from '@wordpress/element';
+
+/**
+ * Injects WordPress component styles into a document.
+ *
+ * @since 3.20.0
+ *
+ * @return {Object} An object containing the injectWordpressComponentStyles function.
+ */
+export const useWordpressComponentStyles = () => {
+ const injectWordpressComponentStyles = useCallback( ( iframeDocument: Document ) => {
+ const urlWpComponents = window?.wpParselyDependencies?.urlWpComponents;
+
+ if ( ! urlWpComponents ) {
+ console.error( 'WordPress component styles URL not found' ); // eslint-disable-line no-console
+ return;
+ }
+
+ let wordpressComponentStyling: HTMLLinkElement | null = iframeDocument.querySelector( 'link[data-wp-parsely-component-styles]' );
+
+ if ( wordpressComponentStyling === null ) {
+ // Inject WordPress components styles.
+ wordpressComponentStyling = iframeDocument.createElement( 'link' );
+ wordpressComponentStyling.rel = 'stylesheet';
+ wordpressComponentStyling.href = urlWpComponents;
+ wordpressComponentStyling.setAttribute( 'data-wp-parsely-component-styles', 'true' );
+ iframeDocument.head.appendChild( wordpressComponentStyling );
+ }
+ }, [] );
+
+ return {
+ injectWordpressComponentStyles,
+ };
+};
diff --git a/src/content-helper/dashboard-page/pages/traffic-boost/preview/preview.scss b/src/content-helper/dashboard-page/pages/traffic-boost/preview/preview.scss
index e879a327e2..c6d7a37a08 100644
--- a/src/content-helper/dashboard-page/pages/traffic-boost/preview/preview.scss
+++ b/src/content-helper/dashboard-page/pages/traffic-boost/preview/preview.scss
@@ -108,7 +108,7 @@
background: var(--white);
z-index: 5;
- .traffic-boost-preview-actions {
+ .traffic-boost-preview-header-actions {
display: flex;
gap: var(--grid-unit-10);
diff --git a/src/content-helper/dashboard-page/pages/traffic-boost/preview/preview.tsx b/src/content-helper/dashboard-page/pages/traffic-boost/preview/preview.tsx
index 76c822b50d..ad3f79a240 100644
--- a/src/content-helper/dashboard-page/pages/traffic-boost/preview/preview.tsx
+++ b/src/content-helper/dashboard-page/pages/traffic-boost/preview/preview.tsx
@@ -3,7 +3,7 @@
*/
import { Icon } from '@wordpress/components';
import { useDispatch, useSelect } from '@wordpress/data';
-import { useEffect, useState } from '@wordpress/element';
+import { useEffect, useState, useCallback } from '@wordpress/element';
import { __ } from '@wordpress/i18n';
import { error, link as linkIcon, linkOff } from '@wordpress/icons';
import { addQueryArgs } from '@wordpress/url';
@@ -162,6 +162,11 @@ export const TrafficBoostPreview = ( {
}
}, [ activePost, isFrontendPreview, previewUrl ] ); // eslint-disable-line react-hooks/exhaustive-deps
+ // Use useCallback for onRestoreOriginal to maintain reference stability
+ const handleRestoreOriginal = useCallback( () => {
+ setSelectedText( null );
+ }, [] );
+
/**
* Opens the post in a new tab.
*
@@ -476,6 +481,7 @@ export const TrafficBoostPreview = ( {
isFrontendPreview={ isFrontendPreview }
setIsFrontendPreview={ setIsFrontendPreview }
/>
+
{
setSelectedText( { text, offset } );
} }
- onRestoreOriginal={ () => {
- setSelectedText( null );
- } }
+ onRestoreOriginal={ handleRestoreOriginal }
isFrontendPreview={ isFrontendPreview }
onLoadingChange={ setIsLoading }
+ onAccept={ handleAccept }
+ onDiscard={ handleDiscard }
+ onUpdateLink={ handleUpdateLink }
+ onRemove={ handleRemove }
/>
+
{
- setSelectedText( null );
- } }
- selectedText={ selectedText }
/>
);
diff --git a/src/content-helper/editor-sidebar/smart-linking/utils.ts b/src/content-helper/editor-sidebar/smart-linking/utils.ts
index 1f215bccef..f7c8152e21 100644
--- a/src/content-helper/editor-sidebar/smart-linking/utils.ts
+++ b/src/content-helper/editor-sidebar/smart-linking/utils.ts
@@ -769,4 +769,3 @@ export function addSmartLinkITMParamsToURL( url: string, smartLinkUid: string ):
term: smartLinkUid,
} );
}
-
diff --git a/src/services/suggestions-api/class-suggestions-api-service.php b/src/services/suggestions-api/class-suggestions-api-service.php
index 132cc6ead9..fb8cb5437f 100644
--- a/src/services/suggestions-api/class-suggestions-api-service.php
+++ b/src/services/suggestions-api/class-suggestions-api-service.php
@@ -19,8 +19,6 @@
*
* @since 3.17.0
*
- * @link https://content-suggestions-api.parsely.net/prod/docs
- *
* @phpstan-import-type Endpoint_Suggest_Brief_Options from Endpoints\Endpoint_Suggest_Brief
* @phpstan-import-type Endpoint_Suggest_Headline_Options from Endpoints\Endpoint_Suggest_Headline
* @phpstan-import-type Endpoint_Suggest_Linked_Reference_Options from Endpoints\Endpoint_Suggest_Linked_Reference
@@ -36,7 +34,7 @@ class Suggestions_API_Service extends Base_API_Service {
* @return string
*/
public static function get_base_url(): string {
- return 'https://content-suggestions-api.parsely.net/prod';
+ return 'https://suggestions-api.parsely.com';
}
/**
diff --git a/src/services/suggestions-api/endpoints/class-endpoint-suggest-brief.php b/src/services/suggestions-api/endpoints/class-endpoint-suggest-brief.php
index 8e0d3a20cc..8837d00e73 100644
--- a/src/services/suggestions-api/endpoints/class-endpoint-suggest-brief.php
+++ b/src/services/suggestions-api/endpoints/class-endpoint-suggest-brief.php
@@ -17,8 +17,6 @@
*
* @since 3.17.0
*
- * @link https://content-suggestions-api.parsely.net/prod/docs#/default/suggest_brief_suggest_brief_post
- *
* @phpstan-type Endpoint_Suggest_Brief_Options = array{
* persona?: string,
* style?: string,
diff --git a/src/services/suggestions-api/endpoints/class-endpoint-suggest-headline.php b/src/services/suggestions-api/endpoints/class-endpoint-suggest-headline.php
index 05e6694df8..9a50c92200 100644
--- a/src/services/suggestions-api/endpoints/class-endpoint-suggest-headline.php
+++ b/src/services/suggestions-api/endpoints/class-endpoint-suggest-headline.php
@@ -17,8 +17,6 @@
*
* @since 3.17.0
*
- * @link https://content-suggestions-api.parsely.net/prod/docs#/default/suggest_headline_suggest_headline_post
- *
* @phpstan-type Traffic_Source = array{
* source: string,
* weight: float
diff --git a/src/services/suggestions-api/endpoints/class-endpoint-suggest-inbound-link-positions.php b/src/services/suggestions-api/endpoints/class-endpoint-suggest-inbound-link-positions.php
index 0eca244941..2f94853320 100644
--- a/src/services/suggestions-api/endpoints/class-endpoint-suggest-inbound-link-positions.php
+++ b/src/services/suggestions-api/endpoints/class-endpoint-suggest-inbound-link-positions.php
@@ -19,8 +19,6 @@
*
* @since 3.19.0
*
- * @link https://content-suggestions-api.parsely.net/prod/docs#/prototype/suggest_inbound_link_positions_suggest_inbound_link_positions_post
- *
* @phpstan-type LinkPositionResponse = array{
* anchor_texts: array,
* title: string,
diff --git a/src/services/suggestions-api/endpoints/class-endpoint-suggest-inbound-links.php b/src/services/suggestions-api/endpoints/class-endpoint-suggest-inbound-links.php
index 04b2c9705e..ef595e8720 100644
--- a/src/services/suggestions-api/endpoints/class-endpoint-suggest-inbound-links.php
+++ b/src/services/suggestions-api/endpoints/class-endpoint-suggest-inbound-links.php
@@ -18,8 +18,6 @@
*
* @since 3.19.0
*
- * @link https://content-suggestions-api.parsely.net/prod/docs#/default/suggest_inbound_links_suggest_inbound_links_post
- *
* @phpstan-type Endpoint_Suggest_Inbound_Links_Options = array{
* max_items?: int,
* url_exclusion_list?: array,
diff --git a/src/services/suggestions-api/endpoints/class-endpoint-suggest-linked-reference.php b/src/services/suggestions-api/endpoints/class-endpoint-suggest-linked-reference.php
index ae962ba1b4..f47802e6cb 100644
--- a/src/services/suggestions-api/endpoints/class-endpoint-suggest-linked-reference.php
+++ b/src/services/suggestions-api/endpoints/class-endpoint-suggest-linked-reference.php
@@ -18,8 +18,6 @@
*
* @since 3.17.0
*
- * @link https://content-suggestions-api.parsely.net/prod/docs#/default/suggest_linked_reference_suggest_linked_reference_post
- *
* @phpstan-type Traffic_Source = array{
* source: string,
* weight: float
diff --git a/src/services/suggestions-api/endpoints/class-suggestions-api-base-endpoint.php b/src/services/suggestions-api/endpoints/class-suggestions-api-base-endpoint.php
index c352d13e07..8c6eda584a 100644
--- a/src/services/suggestions-api/endpoints/class-suggestions-api-base-endpoint.php
+++ b/src/services/suggestions-api/endpoints/class-suggestions-api-base-endpoint.php
@@ -18,8 +18,6 @@
*
* @since 3.17.0
*
- * @link https://content-suggestions-api.parsely.net/prod/docs
- *
* @phpstan-import-type WP_HTTP_Response from Base_Service_Endpoint
* @phpstan-import-type WP_HTTP_Request_Args from Base_Service_Endpoint
*/
@@ -46,7 +44,10 @@ abstract class Suggestions_API_Base_Endpoint extends Base_Service_Endpoint {
protected function get_request_options( string $method ): array {
$options = array(
'method' => $method,
- 'headers' => array( 'Content-Type' => 'application/json; charset=utf-8' ),
+ 'headers' => array(
+ 'Content-Type' => 'application/json; charset=utf-8',
+ 'X-INTERNAL-SERVICE' => 'content-helper',
+ ),
'data_format' => 'body',
'timeout' => 90, //phpcs:ignore WordPressVIPMinimum.Performance.RemoteRequestTimeout.timeout_timeout
'body' => '{}',
diff --git a/tests/Unit/Utils/UtilsTest.php b/tests/Unit/Utils/UtilsTest.php
index 732c6af846..fbf67132d6 100644
--- a/tests/Unit/Utils/UtilsTest.php
+++ b/tests/Unit/Utils/UtilsTest.php
@@ -41,7 +41,7 @@
*
* @phpstan-type Test_Convert_To_Associative_Data array{
* args: array{
- * obj: stdClass,
+ * obj: \stdClass,
* },
* expected_output: array,
* msg: string,
diff --git a/tests/e2e/utils.ts b/tests/e2e/utils.ts
index 9d8496e892..543beee31b 100644
--- a/tests/e2e/utils.ts
+++ b/tests/e2e/utils.ts
@@ -8,7 +8,7 @@ import { type Page } from '@playwright/test';
*/
import { Admin } from '@wordpress/e2e-test-utils-playwright';
-export const PLUGIN_VERSION = '3.19.3';
+export const PLUGIN_VERSION = '3.20.0';
export const VALID_SITE_ID = 'demoaccount.parsely.com';
export const INVALID_SITE_ID = 'invalid.parsely.com';
export const VALID_API_SECRET = 'valid_api_secret';
diff --git a/wp-parsely.php b/wp-parsely.php
index 3c2426c5c1..cc25df2024 100644
--- a/wp-parsely.php
+++ b/wp-parsely.php
@@ -11,14 +11,14 @@
* Plugin Name: Parse.ly
* Plugin URI: https://docs.parse.ly/wordpress
* Description: This plugin makes it a snap to add Parse.ly tracking code and metadata to your WordPress blog.
- * Version: 3.19.3
+ * Version: 3.20.0
* Author: Parse.ly
* Author URI: https://www.parse.ly
* Text Domain: wp-parsely
* License: GPL-2.0-or-later
* License URI: http://www.gnu.org/licenses/gpl-2.0.txt
* GitHub Plugin URI: https://github.com/Parsely/wp-parsely
- * Requires PHP: 7.2
+ * Requires PHP: 7.4
* Requires WP: 6.0.0
*/
@@ -49,7 +49,7 @@
return;
}
-const PARSELY_VERSION = '3.19.3';
+const PARSELY_VERSION = '3.20.0';
const PARSELY_FILE = __FILE__;
const PARSELY_DATA_SCHEMA_VERSION = '1';
const PARSELY_CACHE_GROUP = 'wp-parsely';