Skip to content

Traffic Boost: Fix "Invalid page template" error#3294

Merged
alecgeatches merged 1 commit intoadd/traffic-boostfrom
fix/invalid-page-template-error
May 7, 2025
Merged

Traffic Boost: Fix "Invalid page template" error#3294
alecgeatches merged 1 commit intoadd/traffic-boostfrom
fix/invalid-page-template-error

Conversation

@alecgeatches
Copy link
Contributor

@alecgeatches alecgeatches commented Apr 25, 2025

Description

In testing, we encountered some "Invalid page template." errors followed by "Failed to accept suggestion. The current link is already linked to this smart link." when attempting to insert links in Traffic Boost:

Screenshot 2025-04-25 at 10 35 21 AM

This was caused by our posts and pages using an out-of-date page template. The error works like this:

  1. Link insertion works all the way up until we want to update the post with content.
  2. Deep down in WordPress code, wp_insert_page() checks the page_template value saved with the post.
  3. In our test data, the page_template for the page we're inserting to is wp-custom-template-single-post-page.
  4. Because that template is no longer registered with the theme, and we've passed $wp_error = true, the page template mismatch is returned as an error.
  5. However, the database update has already happened in wp_insert_page() to change the content with the proper link.

In short, I think the root cause is data that uses posts with templates that no longer exist. When we attempt to update these pages, we get an error, and due to the way wp_insert_page() works, we also successfully insert the link at the same time.

To fix this, the PR specifically ignores the invalid_page_template error. The content is already updated, and there's nothing we can do about it at the point we receive the error anyhow.

How has this been tested?

I've tested locally with the original data and confirmed that updates to a post with an invalid page template now work correctly.

If you'd like to try testing manually with other data:

  1. In the add/traffic-boost branch (without this fix), generate Traffic Boost links for a post. Do not insert yet.
  2. Find the post ID for one of the suggested posts to insert. Update the post's _wp_page_template post meta value to something invalid, e.g. not-a-valid-page-template.
  3. Go back to the Traffic Boost UI and attempt to insert a link into the post.
  4. See the "Invalid page template" error. Check the post's content, and see that it was correctly updated.
  5. Switch to this branch fix/invalid-page-template-error.
  6. Find a new post ID to insert, and update the _wp_page_template to an invalid value.
  7. Try the insertion again. See it work.

@alecgeatches alecgeatches requested a review from a team as a code owner April 25, 2025 18:12
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Apr 25, 2025

📝 Walkthrough

Walkthrough

This update introduces a comprehensive Traffic Boost feature to the Parse.ly WordPress plugin, affecting both backend and frontend. Major changes include new PHP models and REST API endpoints for managing inbound smart links and traffic boost suggestions, enhanced smart link data structures, and canonical URL handling. The frontend gains a React-based dashboard with Traffic Boost pages, a sidebar, preview components, and extensive UI for managing and previewing link suggestions. Supporting infrastructure includes new settings, permissions, validation classes, and enriched caching and status management for smart links. Additionally, the update modifies existing smart linking logic to handle ITM parameters and context, and extends settings and menu integration across the plugin.

Changes

File(s) / Path(s) Change Summary
.husky/pre-commit, phpstan.neon Increased PHPStan memory limit and slightly lowered type coverage thresholds.
composer.json, package.json Added new PHP and JS dependencies for HTML5 parsing, routing, cookies, and debouncing.
src/Models/class-inbound-smart-link.php, src/Models/class-smart-link.php, src/Models/class-smart-link-status.php Major enhancements to smart link models: new methods for validation, placement, status, context, ITM parameter handling, and caching; introduced status constants class.
src/Utils/class-utils.php Added ITM parameter appending utility and new PHPStan type alias.
src/class-parsely.php Added canonical URL management methods, updated menu slug and settings URL, integrated Traffic Boost feature.
src/class-permissions.php Added Traffic Boost support to permissions and settings arrays.
src/UI/class-dashboard-page.php, src/UI/class-recommended-widget.php, src/UI/class-row-actions.php, src/UI/class-settings-page.php, src/UI/class-site-health.php, src/UI/settings-page.php Added dashboard page with Traffic Boost integration, updated settings/menu URLs, injected settings and permissions, added Traffic Boost row actions and settings field.
src/content-helper/common/* Extended types, error codes, and permissions for Traffic Boost; added new loading and snackbar components and styles; updated smart link ITM logic.
src/content-helper/dashboard-page/* Introduced full dashboard React app: routing, containers, posts table, Traffic Boost pages, preview, sidebar, and supporting components and styles.
src/content-helper/editor-sidebar/smart-linking/* Updated smart link types and provider to support ITM parameters, context, status, and canonical URLs; improved link validation and UI rendering.
src/css/admin-settings.scss, src/js/admin-settings.ts Updated settings page CSS selector and added Traffic Boost checkbox event handling.
src/rest-api/class-base-validation.php, src/rest-api/content-helper/validations/* Introduced base validation class and specific validation/sanitization for blending weight and URL exclusion lists.
src/rest-api/content-helper/class-content-helper-controller.php, src/rest-api/content-helper/class-endpoint-smart-linking.php, src/rest-api/content-helper/class-endpoint-traffic-boost.php Registered and implemented new Traffic Boost REST API endpoints; updated smart link endpoint to handle new status, context, and ITM structure.
src/rest-api/settings/class-endpoint-traffic-boost-settings.php, src/rest-api/settings/class-settings-controller.php Added REST API endpoint for Traffic Boost settings and registered it in the settings controller.
src/rest-api/stats/class-endpoint-posts.php Added URL validation callback for stats endpoint.
src/services/class-base-service-endpoint.php, src/services/suggestions-api/class-suggestions-api-service.php, src/services/suggestions-api/endpoints/* Added endpoints and methods for inbound link suggestions and positions, updated request options and timeouts, consolidated URL exclusion logic.
src/@types/assets/window.d.ts Added optional property for Traffic Boost preview nonce to Window interface.
tests/Integration/PermissionsTest.php, tests/Integration/RestAPI/ContentHelper/ContentHelperControllerTest.php, tests/Integration/RestAPI/ContentHelper/EndpointSmartLinkingTest.php Updated and extended tests for Traffic Boost features, endpoints, and smart link ITM structure.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant WP_Admin
    participant DashboardPage (PHP)
    participant ReactApp
    participant REST_API
    participant SuggestionsAPI
    participant SmartLinkModel

    User->>WP_Admin: Navigates to Parse.ly Dashboard/Traffic Boost
    WP_Admin->>DashboardPage: Loads dashboard page
    DashboardPage->>ReactApp: Enqueues scripts, injects settings/permissions
    ReactApp->>REST_API: Fetches posts, settings, permissions
    ReactApp->>REST_API: Requests Traffic Boost suggestions/inbound links
    REST_API->>SuggestionsAPI: Calls suggest-inbound-links or suggest-inbound-link-positions
    SuggestionsAPI->>REST_API: Returns suggested links
    REST_API->>SmartLinkModel: Creates/updates Inbound_Smart_Link(s)
    SmartLinkModel->>REST_API: Applies/updates/removes smart links as needed
    REST_API->>ReactApp: Returns updated smart link data
    ReactApp->>User: Renders suggestions, inbound links, preview, and actions
    User->>ReactApp: Accepts, discards, or updates a suggestion
    ReactApp->>REST_API: Applies change (accept/discard/update)
    REST_API->>SmartLinkModel: Updates smart link status/content
    SmartLinkModel->>REST_API: Confirms operation
    REST_API->>ReactApp: Returns result
    ReactApp->>User: Updates UI and feedback
Loading

Possibly related PRs

  • Parsely/wp-parsely#2940: Implements React Router-based dashboard navigation and submenu highlighting, directly related to new dashboard and menu logic.
  • Parsely/wp-parsely#3216: Adds context and ITM parameter handling to the Smart_Link class, closely related to this PR's smart link enhancements.
  • Parsely/wp-parsely#3149: Adds backend support for Traffic Boost, including the masterminds/html5 dependency and smart link changes, foundational to this PR's Traffic Boost features.

Suggested labels

Changelog: Fixed

✨ Finishing Touches
  • 📝 Generate Docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@alecgeatches alecgeatches changed the base branch from develop to add/traffic-boost April 25, 2025 18:13
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Inline review comments failed to post. This is likely due to GitHub's limits when posting large numbers of comments. If you are seeing this consistently it is likely a permissions issue. Please check "Moderation" -> "Code review limits" under your organization settings.

🛑 Comments failed to post (51)
package.json (1)

81-83: 💡 Verification agent

🧩 Analysis chain

Duplicate dependency and version consistency issues

There are two issues here:

  1. lodash.debounce is already defined in dependencies (line 42)
  2. react-router v7.1.3 is used, but @types/react-router v5.1.20 is used - these versions might be incompatible
-		"lodash.debounce": "^4.0.8",

🌐 Web query:

Are @types/react-router v5.1.20 and react-router v7.1.3 compatible?

💡 Result:

Compatibility of @types/react-router v5.1.20 with react-router v7.1.3

Summary:
@types/react-router v5.1.20 is not compatible with react-router v7.1.3.


Detailed Explanation

Type Packages and Major Versions

  • The @types/react-router package provides TypeScript type definitions for React Router versions prior to v6, specifically for v4 and v5 APIs[1][3].
  • As of React Router v6, and continuing in v7, the library is written in TypeScript and ships its own type definitions as part of the package. Therefore, no separate @types/react-router package is needed or maintained for v6 or later[1][2][3].

Breaking API Changes

  • React Router v7 introduces significant breaking changes compared to v5, including removal and redesign of many APIs[2][10]. As a result, the types in @types/react-router v5.1.20 do not match the new APIs and internal structure of v7.
  • Attempting to use @types/react-router v5.1.20 with react-router v7.1.3 will lead to mismatches, type errors, and potentially missing or incorrect types for new/changed APIs.

Type Management in React Router v7

  • For v7+, type definitions are included natively in the react-router package. No separate type installation is needed[2][10].
  • Installing or referencing @types/react-router with React Router v7 can cause conflicts or type errors due to overlaps or outdated expectations of the API surface.

Correct Approach for React Router v7.1.3 and TypeScript

  • Only install react-router v7.1.3; do not install @types/react-router.
  • Use the built-in type support that comes with react-router v7. This ensures you have accurate, up-to-date type coverage for all APIs.

Conclusion

@types/react-router v5.1.20 is only for React Router v4/v5, and is not compatible with react-router v7.1.3. For v7, rely on the built-in types and remove any separate @types/react-router dependency from your project[1][2][10].

Citations:


Remove duplicate lodash.debounce and outdated @types/react-router

To keep your dependencies clean and avoid type conflicts with React Router v7, please:

  • Delete the duplicate lodash.debounce entry from devDependencies (it’s already declared under dependencies at line 42).
  • Remove the @types/react-router v5.1.20 dependency—React Router v7.1.3 includes its own TypeScript definitions.

Suggested diff:

--- package.json
@@ devDependencies
-   "lodash.debounce": "^4.0.8",
-   "@types/react-router": "5.1.20",

Committable suggestion skipped: line range outside the PR's diff.

src/css/admin-settings.scss (1)

5-5: 🛠️ Refactor suggestion

Convert pixel values to rem units.

Per project guidelines, replace --padding-default: 15px; with --padding-default: to_rem(15px); for scalable spacing.

src/js/admin-settings.ts (1)

72-73: 💡 Verification agent

🧩 Analysis chain

Successfully added Traffic Boost to the feature checkboxes.

The Traffic Boost checkbox has been correctly integrated into the existing pattern of feature checkbox selectors, which will ensure it benefits from the same UI toggling behavior as other features.


🏁 Script executed:

#!/bin/bash
# Verify that the Traffic Boost checkbox HTML element exists in the settings page

# Look for the Traffic Boost checkbox ID in PHP files
echo "Checking for Traffic Boost checkbox definition in PHP files:"
rg -A 2 "content_helper_traffic_boost_enabled" --type php

Length of output: 188


🏁 Script executed:

#!/bin/bash
# Verify presence of Traffic Boost checkbox ID across all files in the repo

echo "Global search for 'content_helper_traffic_boost_enabled':"
rg -n content_helper_traffic_boost_enabled || echo "No occurrences found."

echo
echo "Global search for 'traffic_boost':"
rg -n traffic_boost || echo "No occurrences found."

Length of output: 7774


Add the Traffic Boost checkbox to the settings page markup

The JS now targets input#content_helper_traffic_boost_enabled, but there’s no matching HTML input in the settings form. Please add the checkbox for Traffic Boost in the PHP settings class.

• File: src/UI/class-settings-page.php
– Around where other content_helper feature fields are rendered (e.g. lines ~120–130 and ~510–520), insert:

<tr>
  <th scope="row">
    <label for="content_helper_traffic_boost_enabled">
      <?php esc_html_e( 'Traffic Boost', 'wp-parsely' ); ?>
    </label>
  </th>
  <td>
    <input
      type="checkbox"
      id="content_helper_traffic_boost_enabled"
      name="parsely_settings[content_helper][traffic_boost][enabled]"
      value="1"
      <?php checked( $options['content_helper']['traffic_boost']['enabled'], true ); ?>
    />
  </td>
</tr>

• Verify that the new input’s ID matches your JS selector and that its name/option keys align with the existing settings schema.
• Rerun the settings page tests to ensure the element is detected by your UI toggling code.

src/Telemetry/telemetry-init.php (1)

55-55: 💡 Verification agent

🧩 Analysis chain

Updated action hook name to match new admin page structure.

The telemetry hook has been correctly updated to use the new admin page slug pattern ('parse-ly_page_parsely-settings'). This ensures telemetry events will continue to work with the updated admin menu structure.


🏁 Script executed:

#!/bin/bash
# Verify that the new hook string is consistent with admin page registration

# Look for the admin page registration to confirm the hook should match
echo "Checking for admin page registration:"
rg -A 2 "add_submenu_page|register_setting" --type php | grep -i "parsely-settings"

# Check if there are any remaining references to the old hook
echo "Checking for references to old hook 'load-settings_page_parsely':"
rg "load-settings_page_parsely" --type php

Length of output: 358


🏁 Script executed:

#!/bin/bash
# Search for menu page registration related to Parse.ly settings
echo "=== add_submenu_page calls ==="
rg -n "add_submenu_page" -C3 --type php

echo "=== add_options_page calls ==="
rg -n "add_options_page" -C3 --type php

echo "=== register_setting for Parsely ==="
rg -n "register_setting" -C3 --type php

echo "=== All 'parsely' mentions ==="
rg -i -n "parsely" -C3 --type php

Length of output: 68827


🏁 Script executed:

#!/bin/bash
echo "=== Telemetry-init: action_hook assignment ==="
rg -n "action_hook" -C2 src/Telemetry/telemetry-init.php

echo "=== Telemetry-init: add_action calls ==="
rg -n "add_action" -C2 src/Telemetry/telemetry-init.php

echo "=== Settings Page registration in class-settings-page.php ==="
sed -n '190,210p' src/UI/class-settings-page.php

Length of output: 1870


Fix telemetry action hook for settings page

The hard-coded hook in Telemetry initialization doesn’t match the actual submenu hook suffix (parsely-dashboard-page_page_parsely-settings). Update it so telemetry fires when the settings page loads.

• File: src/Telemetry/telemetry-init.php (around line 55)
Change from:

'action_hook' => 'load-parse-ly_page_parsely-settings',

To:

'action_hook' => 'load-parsely-dashboard-page_page_parsely-settings',
src/content-helper/dashboard-page/components/typography-components.tsx (2)

12-15: 🛠️ Refactor suggestion

Props type definition doesn't match implementation.

The type definition has a props?: HeadingProps property, but in the implementation (line 27), you're spreading all remaining props with ...props. This mismatch could cause TypeScript errors or unexpected behavior.

-export type DashboardHeadingProps = {
-	children: React.ReactNode;
-	props?: HeadingProps
-};
+export type DashboardHeadingProps = HeadingProps & {
+	children: React.ReactNode;
+};

27-35: ⚠️ Potential issue

Component implementation needs to match the type definition.

The component implementation doesn't align with the type definition. Either update the type definition as suggested above or modify the implementation to use the nested props property.

If keeping the current type definition, update the implementation:

-export const DashboardHeading = ( { children, ...props }: DashboardHeadingProps ) => {
+export const DashboardHeading = ( { children, props = {} }: DashboardHeadingProps ) => {
 	return (
 		<Heading
 			className="parsely-dashboard-header"
-			{ ...props }>
+			{ ...props }>
 			{ children }
 		</Heading>
 	);
 };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

export const DashboardHeading = ( { children, props = {} }: DashboardHeadingProps ) => {
	return (
		<Heading
			className="parsely-dashboard-header"
			{ ...props }>
			{ children }
		</Heading>
	);
};
src/content-helper/dashboard-page/pages/traffic-boost/sidebar/components/post-details.tsx (1)

44-44: ⚠️ Potential issue

Security concern: Using dangerouslySetInnerHTML without sanitization.

The use of dangerouslySetInnerHTML to render HTML content could expose users to cross-site scripting (XSS) attacks if the post title contains malicious code.

Consider sanitizing the HTML content before rendering it or using a safer alternative. If WordPress already sanitizes post?.title.rendered, add a code comment explaining this:

-<div className="post-title" dangerouslySetInnerHTML={ { __html: post?.title.rendered ?? __( '(no title)', 'wp-parsely' ) } } />
+{/* WordPress Core sanitizes post.title.rendered, making it safe to use with dangerouslySetInnerHTML */}
+<div className="post-title" dangerouslySetInnerHTML={ { __html: post?.title.rendered ?? __( '(no title)', 'wp-parsely' ) } } />
🧰 Tools
🪛 Biome (1.9.4)

[error] 44-44: Avoid passing content using the dangerouslySetInnerHTML prop.

Setting content using code can expose users to cross-site scripting (XSS) attacks

(lint/security/noDangerouslySetInnerHtml)

src/content-helper/dashboard-page/pages/traffic-boost/preview/preview.scss (2)

6-6: 🛠️ Refactor suggestion

Convert pixel values to rem units using variables.

According to the coding guidelines, dimensions greater than or equal to 3px should be converted to rem units using the to_rem function, and variables for sizes should be used from the variables.scss file instead of hardcoding values.

While you're already using the to_rem function correctly, consider using size variables defined in variables.scss instead of hardcoded pixel values:

-	border-radius: to_rem(8px);
+	border-radius: var(--border-radius);

If --border-radius doesn't exist in the variables.scss file, you should add it there first.

Committable suggestion skipped: line range outside the PR's diff.


189-196: 🛠️ Refactor suggestion

Avoid repeated width values for the popover.

The same width value is used in both the popover and its content, which could lead to maintenance issues if one value changes but not the other.

Define the width once in a variable and reuse it:

+/* Import or add to variables.scss */
+/* --popover-width: to_rem(275px); */

 .wp-parsely-traffic-boost-preview-settings-popover {
-	width: to_rem(275px);
+	width: var(--popover-width);
 	margin-top: var(--grid-unit-20);
 
 	&-content {
-		width: to_rem(275px);
+		width: var(--popover-width);
 		padding: var(--grid-unit-20);
-		min-width: to_rem(220px);
+		min-width: var(--popover-min-width);
 	}
 }

Committable suggestion skipped: line range outside the PR's diff.

src/rest-api/settings/class-endpoint-traffic-boost-settings.php (1)

52-55: ⚠️ Potential issue

Replace placeholder settings with actual implementation.

The current implementation includes a placeholder setting 'Setting1' with a default value of 'Hello World!', which appears to be development code rather than a production-ready implementation.

Replace the placeholder with actual Traffic Boost settings:

 		return array(
-			'Setting1' => array(
-				'values'  => array(),
-				'default' => 'Hello World!',
+			'enabled' => array(
+				'values'  => array( true, false ),
+				'default' => true,
 			),
+			// Add other required Traffic Boost settings here
 		);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

        return array(
            'enabled' => array(
                'values'  => array( true, false ),
                'default' => true,
            ),
            // Add other required Traffic Boost settings here
        );
src/content-helper/dashboard-page/pages/traffic-boost/preview/preview-post.php (1)

30-31: ⚠️ Potential issue

Fix incorrect CSS property.

The property min-height: none is invalid. Use auto or 0 instead.

-	min-height: none;
+	min-height: 0;
src/UI/class-row-actions.php (1)

125-154: ⚠️ Potential issue

Incorrect ARIA label for Traffic Boost link.

The method uses the same ARIA label generation function for both Parse.ly Stats and Traffic Boost links, which results in an incorrect ARIA label for the Traffic Boost feature. This could confuse screen reader users.

Modify the code to use a Traffic Boost specific ARIA label:

-			esc_attr( $this->generate_aria_label_for_post( $post ) ),
+			esc_attr( sprintf(
+				/* translators: Post title */
+				__( 'Go to Traffic Boost for "%s"', 'wp-parsely' ),
+				$post->post_title
+			) ),

Committable suggestion skipped: line range outside the PR's diff.

src/content-helper/dashboard-page/pages/dashboard/page-component.tsx (3)

30-33: 🛠️ Refactor suggestion

The default settings object appears to be a placeholder.

The defaultSettings object currently contains only a placeholder value Setting1: 'Hello World!'. Consider replacing this with actual production-ready default settings that align with the Traffic Boost feature's requirements.

Would you like me to help define more appropriate default settings for the Traffic Boost feature?


52-54: 🛠️ Refactor suggestion

Add validation for all settings properties.

Currently, only Setting1 is being validated. As more settings are added, ensure you implement proper validation for each setting property to maintain robustness.


64-90: 💡 Verification agent

🧩 Analysis chain

Consider adding error state handling in the VerifyCredentials component.

The dashboard page renders content within the VerifyCredentials component, but there's no visible handling for credential verification failures. Consider adding error state handling to provide a better user experience when credentials are invalid.


🏁 Script executed:

#!/bin/bash
# Check if VerifyCredentials component has error handling
ast-grep --pattern $'<VerifyCredentials>
  $$$
  {
    $$$
    isLoading
    $$$
  }
  $$$
</VerifyCredentials>'

# Or find the VerifyCredentials component implementation
fd --type f --extension tsx --extension ts "VerifyCredentials" src

Length of output: 180


🏁 Script executed:

#!/bin/bash
# Show the import for VerifyCredentials in the page component
sed -n '1,20p' src/content-helper/dashboard-page/pages/dashboard/page-component.tsx

# Find all references to VerifyCredentials to locate its implementation
rg -n "VerifyCredentials" src

# Case-insensitive search for a file name containing verifycredentials
fd -i --type f --extension tsx --extension ts "verifycredentials" src

Length of output: 3466


🏁 Script executed:

#!/bin/bash
# Display the implementation of VerifyCredentials to check for error handling
sed -n '1,200p' src/content-helper/common/verify-credentials.tsx

Length of output: 1085


Add loading and robust error states to VerifyCredentials

The current VerifyCredentials component only checks for a missing‐credentials flag on window and otherwise renders its children. It doesn’t account for asynchronous credential checks (e.g. network failures or invalid API keys) or provide a loading indicator. Please enhance it to:

• Show a loading spinner or placeholder while verifying credentials.
• Render a clear error message (distinct from “empty credentials”) when the verification request fails or credentials are invalid.
• Expose props or callbacks so parent components can customize loading and error UIs.

Files to update:

  • src/content-helper/common/verify-credentials.tsx (add loading/error state and UI)
  • Any consumers (e.g. dashboard/page-component.tsx) to handle or style the new states accordingly.
src/content-helper/dashboard-page/pages/dashboard/dashboard.scss (1)

10-12: 💡 Verification agent

🧩 Analysis chain

Use variables instead of hardcoded colors.

Replace hardcoded colors with variables for better maintainability and theming support.

.dashboard-header-background {
-   background: linear-gradient(303deg, #000 30.51%, #487006 133.77%);
+   background: linear-gradient(303deg, var(--sidebar-black) 30.51%, var(--parsely-green) 133.77%);
}

🏁 Script executed:

#!/bin/bash
# Check available color variables
cat src/content-helper/common/css/variables.scss | grep -E "color|--sidebar|--parsely"

Length of output: 466


Action Required: Introduce matching CSS variables for exact colors

The existing theme variables don’t match the original values you’re replacing:

  • --sidebar-black is #1e1e1e (vs. #000)
  • --parsely-green is hsl(107, 42%, 46%) (vs. #487006)

To maintain exact color fidelity and enable theming:

  1. Define two new variables in
 src/content-helper/common/css/variables.scss:
    :root {
      --dashboard-header-black: #000;
      --dashboard-header-olive: #487006;
    }
  2. Update your SCSS:
    .dashboard-header-background {
    -  background: linear-gradient(303deg, #000 30.51%, #487006 133.77%);
    +  background: linear-gradient(
    +    303deg,
    +    var(--dashboard-header-black) 30.51%,
    +    var(--dashboard-header-olive)    133.77%
    +  );
    }

If you’d rather use the existing variables (--sidebar-black, --parsely-green), please confirm with the design team that the slight hue/contrast shifts are acceptable.

src/content-helper/dashboard-page/pages/dashboard/header-component.tsx (2)

19-32: 🛠️ Refactor suggestion

HeaderSummary component contains hardcoded text.

The component contains hardcoded performance metrics that should be dynamic or localized using WordPress translation functions.

-				<div className="summary-title">Today is an exceptional day.</div>
-				<div className="summary-text">75% more traffic than last week</div>
-				<div className="summary-text">Yesterday was the 33rd best Tuesday, 214th overall.</div>
+				<div className="summary-title">{ __( 'Today is an exceptional day.', 'wp-parsely' ) }</div>
+				<div className="summary-text">{ sprintf( 
+					/* translators: %s: percentage increase */
+					__( '%s more traffic than last week', 'wp-parsely' ), 
+					'75%' 
+				) }</div>
+				<div className="summary-text">{ sprintf(
+					/* translators: %1$s: day rank, %2$s: overall rank */
+					__( 'Yesterday was the %1$s best Tuesday, %2$s overall.', 'wp-parsely' ),
+					'33rd',
+					'214th'
+				) }</div>

Don't forget to import the WordPress i18n functions:

+import { __, sprintf } from '@wordpress/i18n';
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

import { __, sprintf } from '@wordpress/i18n';

const HeaderSummary = (): React.JSX.Element => {
	return (
		<div className="dashboard-header-summary">
			<div className="summary-info">
				<div className="summary-title">{ __( 'Today is an exceptional day.', 'wp-parsely' ) }</div>
				<div className="summary-text">{ sprintf(
					/* translators: %s: percentage increase */
					__( '%s more traffic than last week', 'wp-parsely' ),
					'75%'
				) }</div>
				<div className="summary-text">{ sprintf(
					/* translators: %1$s: day rank, %2$s: overall rank */
					__( 'Yesterday was the %1$s best Tuesday, %2$s overall.', 'wp-parsely' ),
					'33rd',
					'214th'
				) }</div>
			</div>
			<div className="summary-button">
				<Button variant="secondary">View more in Parse.ly</Button>
			</div>
		</div>
	);
};

85-110: 🛠️ Refactor suggestion

DashboardHeader component contains hardcoded metrics.

The component contains hardcoded statistics that should ideally be fetched from an API or passed as props.

Consider refactoring to accept dynamic data:

-export const DashboardHeader = (): React.JSX.Element => {
+type DashboardHeaderProps = {
+	summary: {
+		title: string;
+		trafficIncrease: string;
+		rankSummary: string;
+	};
+	topMetrics: {
+		trafficBoost: string;
+		smartLinkClicks: string;
+	};
+	metrics: Array<HeaderCardProps>;
+};

+export const DashboardHeader = ({
+	summary,
+	topMetrics,
+	metrics
+}: Readonly<DashboardHeaderProps>): React.JSX.Element => {

Then update the component to use these props instead of hardcoded values.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

type DashboardHeaderProps = {
	summary: {
		title: string;
		trafficIncrease: string;
		rankSummary: string;
	};
	topMetrics: {
		trafficBoost: string;
		smartLinkClicks: string;
	};
	metrics: Array<HeaderCardProps>;
};

-export const DashboardHeader = (): React.JSX.Element => {
+export const DashboardHeader = ({
+	summary,
+	topMetrics,
+	metrics
+}: Readonly<DashboardHeaderProps>): React.JSX.Element => {
	return (
		<div className="dashboard-header-background">
			<PageHeader className="dashboard-header">
				<HeaderSummary />
				<div className="dashboard-header-stats">
					<div className="stats-top">
						<StatCard className="stat-intro" title="Parse.ly Working For You" />
						<StatCard title="Traffic Boost" value="14%" icon={ link } />
						<StatCard title="Smart Link Clicks" value="784" icon={ link } />
					</div>
					{[
						{ title: 'Page Views', value: '4.6K', change: '24%' },
						{ title: 'Visitors', value: '1.5K', change: '25%' },
						{ title: 'Minutes', value: '32', change: '40%' },
						{ title: 'Avg. Time', value: '32', change: '40%', down: true },
						{ title: 'Soc. Interactions', value: '32', change: '40%' },
						{ title: 'New Posts', value: '2', change: '40%' },
					].map((metric, index) => (
						<StatCard key={index} {...metric} />
					))}
				</div>
			</PageHeader>
		</div>
	);
};
src/content-helper/dashboard-page/pages/traffic-boost/preview/components/preview-footer.tsx (2)

83-88: ⚠️ Potential issue

Add accessible labels to icon-only navigation buttons.

<Button> components that render only an icon should provide either the label prop or aria-label to ensure screen-reader users understand the action. Without it, WP accessibility guidelines (and WPCS) flag the control as non-compliant.

-<Button
-  variant="tertiary"
-  onClick={ onPrevious }
-  icon={ arrowLeft }
-/>
+<Button
+  variant="tertiary"
+  onClick={ onPrevious }
+  icon={ arrowLeft }
+  label={ __( 'Previous suggestion', 'wp-parsely' ) }
+/>

Apply the same fix to the “next” button (lines 197-201).

Also applies to: 197-201


65-73: 🛠️ Refactor suggestion

Separate loading flags for “Accept” vs “Update” actions.

isAccepting is reused for the Update Link button’s isBusy/disabled state. If the store distinguishes between “accept” and “update”, a long-running accept request will disable the update button (and vice-versa), confusing users.

Consider introducing an isUpdating selector:

const {
   isAccepting,
   isRemoving,
+  isUpdating,
   isGenerating,
 } = useSelect( ( select ) => ( {
   isAccepting: activeLink ? select( TrafficBoostStore ).isAccepting( activeLink ) : false,
   isRemoving:  activeLink ? select( TrafficBoostStore ).isRemoving( activeLink )  : false,
+  isUpdating:  activeLink ? select( TrafficBoostStore ).isUpdating( activeLink )  : false,
   isGenerating: activeLink ? select( TrafficBoostStore ).isGenerating( activeLink ) : false,
 } ), [ activeLink ] );

and wire isUpdating to the Update Link button.

Also applies to: 125-128

src/content-helper/dashboard-page/pages/traffic-boost/preview/components/text-selection-tooltip.tsx (1)

25-28: ⚠️ Potential issue

Hard-coded core CSS path breaks on non-standard installations.

/wp-includes/css/dist/components/style.css assumes WordPress is installed at the web-root and wp-includes is publicly accessible.
• Sub-directory installs (/wordpress/),
• custom WP_CONTENT_DIR, or
• reverse proxies
will return 404, leaving the tooltip unstyled.

Use the asset URL provided by wp_default_styles, or dynamically read it from window.wp.componentsStyle if exposed by the admin, e.g.:

- wpComponentsLink.href = '/wp-includes/css/dist/components/style.css';
+ // Prefer the URL registered by WordPress core.
+ wpComponentsLink.href =
+   ( window?.wp?.componentsStyleUrl ) ||
+   `${ window.location.origin }${ window?.wp?.url?.get( 'wp-includes/css/dist/components/style.css' ) }`;

Committable suggestion skipped: line range outside the PR's diff.

src/content-helper/dashboard-page/pages/traffic-boost/sidebar/components/links-list/links-list.tsx (1)

366-375: ⚠️ Potential issue

Add accessible labels to pagination icon buttons.

Similar to the preview footer, the previous/next buttons here are icon-only. Supply label or aria-label for screen-reader compliance.

<Button
   icon={ previous }
   onClick={ handlePrevious }
   disabled={ currentPage <= 1 }
+  label={ __( 'Previous page', 'wp-parsely' ) }
 />

Apply to the “next” button as well.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

                            <Button
                                icon={ previous }
                                onClick={ handlePrevious }
                                disabled={ currentPage <= 1 }
                                label={ __( 'Previous page', 'wp-parsely' ) }
                            />
                            <Button
                                icon={ next }
                                onClick={ handleNext }
                                disabled={ currentPage >= totalPages }
                                label={ __( 'Next page', 'wp-parsely' ) }
                            />
src/content-helper/common/base-wordpress-provider.tsx (1)

224-230: 🛠️ Refactor suggestion

thumbnail may become undefined while typed as string.

When the thumbnail size is missing you fall back to featuredMedia?.source_url ?? undefined;, so thumbnail can be undefined, violating its declared string type and risking runtime surprises.

-const featuredMedia = post._embedded[ 'wp:featuredmedia' ]?.[ 0 ];
-thumbnail = featuredMedia?.media_details?.sizes?.thumbnail?.source_url;
-
-if ( ! thumbnail ) {
-    thumbnail = featuredMedia?.source_url ?? undefined;
-}
+const featuredMedia = post._embedded?.[ 'wp:featuredmedia' ]?.[ 0 ];
+thumbnail =
+    featuredMedia?.media_details?.sizes?.thumbnail?.source_url ??
+    featuredMedia?.source_url ??
+    '';

This always returns a string, keeping typings honest.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

				const featuredMedia = post._embedded?.['wp:featuredmedia']?.[0];
				thumbnail =
					featuredMedia?.media_details?.sizes?.thumbnail?.source_url ??
					featuredMedia?.source_url ??
					'';
src/content-helper/dashboard-page/pages/traffic-boost/sidebar/components/tabs/suggestions-tab.tsx (1)

162-176: 🛠️ Refactor suggestion

await-ing Redux dispatches is misleading.

addSuggestion() and setIsGenerating() are synchronous action creators; returning void yet you await them. This gives readers the false impression that they are async and delays the following code by a micro-tick. Remove the await keywords (or convert the actions to return promises).

- await addSuggestion( trafficBoostLink );
- await setIsGenerating( trafficBoostLink, true );
+ addSuggestion( trafficBoostLink );
+ setIsGenerating( trafficBoostLink, true );
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

        const trafficBoostLink = trafficBoostProvider.createSuggestion( post );
        addSuggestion( trafficBoostLink );
        setIsGenerating( trafficBoostLink, true );

        try {
            const updatedLink = await trafficBoostProvider.generateSuggestionForPost(
                currentPost,
                post,
                trafficBoostLink
            );

            await updateSuggestion( updatedLink, trafficBoostLink.uid );
            setIsGenerating( trafficBoostLink, false );
        } catch ( err: unknown ) {
            let errorMessage = __( 'Failed to find a link placement.', 'wp-parsely' );
src/content-helper/editor-sidebar/smart-linking/utils.ts (2)

601-603: 🛠️ Refactor suggestion

URL matching may yield false positives

Using a.href.includes( missingSmartLink.href.raw ) can accidentally match unrelated URLs that merely contain the raw URL as a substring (e.g., a proxy, redirect, or analytics wrapper). To reduce the risk of restoring the wrong anchor, use strict, normalised comparison:

-				a.href.includes( missingSmartLink.href.raw ) &&
+				removeITMParamsFromURL( a.href ) === missingSmartLink.href.raw &&

This keeps behaviour consistent with your earlier normalisation logic.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

				removeITMParamsFromURL( a.href ) === missingSmartLink.href.raw &&
				! a.hasAttribute( 'data-smartlink' );

436-439: ⚠️ Potential issue

Possible ITM-parameter duplication

addSmartLinkITMParamsToURL() is invoked with the original link.href, which may already contain ITM parameters. Because the preceding raw field is explicitly stripped of those parameters, calling the ITM helper on the un-sanitised URL can lead to duplicated query-string keys (itm_campaign, itm_source, …) whenever the DOM already holds a decorated link.

-		const href = {
-			raw: removeITMParamsFromURL( link.href ),
-			itm: addSmartLinkITMParamsToURL( link.href, uid ),
+		const rawHref = removeITMParamsFromURL( link.href );
+		const href = {
+			raw: rawHref,
+			itm: addSmartLinkITMParamsToURL( rawHref, uid ),
 		};

This one-liner prevents query-string bloat and avoids potentially breaking canonicalisation / matching logic elsewhere.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

		const rawHref = removeITMParamsFromURL( link.href );
		const href = {
			raw: rawHref,
			itm: addSmartLinkITMParamsToURL( rawHref, uid ),
		};
src/content-helper/dashboard-page/pages/traffic-boost/single-post-component.tsx (2)

385-466: ⚠️ Potential issue

Stale-closure risk in suggestions useEffect

This effect intentionally suppresses exhaustive-deps linting, yet it references inboundLinks, settings, setSelectedLink, and setSuggestions which are not stable across renders. If any of these change after the first run, the effect will still hold the old snapshot, causing:

• Suggestions generation to omit new inbound links.
settings.maxItems changes to be ignored until a full reload.

Consider either:

a) moving helper functions outside the component and passing current values as parameters, or
b) including the mutable variables in the dependency array and restructuring to avoid an infinite loop.

Failing to address this will produce hard-to-debug state inconsistencies.


84-138: 🛠️ Refactor suggestion

ResizeObserver cleanup & cross-browser safety

Excellent job detaching the observer, but a couple of hardening tweaks will improve robustness:

  1. ResizeObserver is undefined in some older browsers / test environments. Wrap the instantiation in a feature-detection guard to avoid runtime crashes.
  2. You call resizeObserver.disconnect() inside return, yet you don’t guard for the case where the constructor failed or was unavailable.
-		const resizeObserver = new ResizeObserver( () => {
+		const resizeObserver = window.ResizeObserver
+			? new ResizeObserver( () => {
 				updateSidebarWidth();
-		} );
+			} )
+			: null;
 ...
-		resizeObserver.observe( adminMenuBack );
+		if ( resizeObserver ) {
+			resizeObserver.observe( adminMenuBack );
+		}
 ...
-			resizeObserver.disconnect();
+			resizeObserver?.disconnect();
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

	useEffect( () => {
		// Set the background color of the page container to the background color of the admin menu.
		const adminMenuBack = document.getElementById( 'adminmenuback' );
		if ( ! adminMenuBack ) {
			return;
		}

		const computedStyle = window.getComputedStyle( adminMenuBack );
		setBackgroundColor( computedStyle.backgroundColor );

		// Set the background color of #wpwrap to the background color of the admin menu.
		const wpWrap = document.querySelector( '#wpwrap' );
		if ( wpWrap ) {
			( wpWrap as HTMLElement ).style.backgroundColor = computedStyle.backgroundColor;
		}

		/**
		 * Updates the sidebar width.
		 *
		 * @since 3.18.0
		 */
		const updateSidebarWidth = () => {
			const width = adminMenuBack.getBoundingClientRect().width;
			setSidebarWidth( width );
		};

		// Grab the sidebar width so it can be used to calculate the container width.
		updateSidebarWidth();

		// Use a ResizeObserver to watch for width changes.
		const resizeObserver = window.ResizeObserver
			? new ResizeObserver( () => {
				updateSidebarWidth();
			} )
			: null;

		if ( resizeObserver ) {
			resizeObserver.observe( adminMenuBack );
		}

		return () => {
			// When the component unmounts, make sure to cancel all the provider requests.
			TrafficBoostProvider.getInstance().cancelAll();
			// Disconnect the resize observer.
			resizeObserver?.disconnect();
			// Remove the background color of #wpwrap.
			( wpWrap as HTMLElement ).style.backgroundColor = '';

			// Clean up the store state.
			setIsGeneratingSuggestions( false );
			setLoading( false );
			setError( null );
			setInboundLinks( [] );
			setSuggestions( [] );
			setCurrentPost( null );
			setSelectedLink( null );
			setSelectedTab( TrafficBoostSidebarTabs.SUGGESTIONS );
		};
	}, [] ); // eslint-disable-line react-hooks/exhaustive-deps
src/services/suggestions-api/endpoints/class-endpoint-suggest-inbound-link-positions.php (1)

113-128: ⚠️ Potential issue

Anchor-text sanitation too permissive

wp_kses_post() will allow inline HTML (e.g., <em>), but anchor text should be plain text. Using sanitize_text_field() prevents accidental markup or script execution leaking into the editor.

-				wp_kses_post( $anchor_text_suggestion['text'] ),
+				sanitize_text_field( $anchor_text_suggestion['text'] ),
src/services/suggestions-api/endpoints/class-endpoint-suggest-inbound-links.php (1)

92-98: 🛠️ Refactor suggestion

Cast offset to int & use sanitize_text_field for the title.

esc_attr() is intended for HTML attributes, not plain strings, and the offset arrives as mixed data from the API – explicitly casting helps avoid type juggling bugs.

-    esc_attr( $link['title'] ),
+    sanitize_text_field( $link['title'] ),
...
-    $anchor_text_suggestion['offset']
+    (int) $anchor_text_suggestion['offset']
src/content-helper/dashboard-page/pages/traffic-boost/store.ts (1)

401-405: 🛠️ Refactor suggestion

Avoid mutating the action object inside the reducer.

Redux actions should be immutable; mutating action.loadingType breaks time-travel debugging and violates functional principles. Prefer a local variable:

- if ( ! action.loadingType ) {
-   action.loadingType = 'default';
- }
+ const loadingType = action.loadingType ?? 'default';

and use loadingType below.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

			case 'SET_LOADING':
				const loadingType = action.loadingType ?? 'default';
src/class-parsely.php (3)

1042-1045: 🛠️ Refactor suggestion

Sanitize the stored canonical URL.

update_post_meta() should receive a cleaned value to avoid storing invalid / unsafe data.

- return false !== update_post_meta( $post_id, self::PARSELY_CANONICAL_URL_META_KEY, $url );
+ return false !== update_post_meta(
+   $post_id,
+   self::PARSELY_CANONICAL_URL_META_KEY,
+   esc_url_raw( $url )
+);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

 public static function set_canonical_url( $post, string $url ): bool {
     $post_id = is_int( $post ) ? $post : $post->ID;
-    return false !== update_post_meta( $post_id, self::PARSELY_CANONICAL_URL_META_KEY, $url );
+    return false !== update_post_meta(
+        $post_id,
+        self::PARSELY_CANONICAL_URL_META_KEY,
+        esc_url_raw( $url )
+    );
 }

984-999: ⚠️ Potential issue

Return an empty string or WP_Error, not the literal 'no permalink'.

Returning a sentinel string leaks into downstream consumers and can surface in canonical URLs. Prefer an early '' or a WP_Error.

- if ( false === $permalink ) {
-   return 'no permalink';
- }
-
- return self::get_canonical_url( $permalink );
+ if ( false === $permalink ) {
+   return '';
+ }
+ return self::get_canonical_url( $permalink );
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

public static function get_canonical_url_from_post( $post ): string {
    $post_id       = is_int( $post ) ? $post : $post->ID;
    $canonical_url = get_post_meta( $post_id, self::PARSELY_CANONICAL_URL_META_KEY, true );

    if ( null !== $canonical_url && is_string( $canonical_url ) && '' !== $canonical_url ) {
        return $canonical_url;
    }

    $permalink = get_permalink( $post );

    if ( false === $permalink ) {
        return '';
    }

    return self::get_canonical_url( $permalink );
}

1013-1031: 🛠️ Refactor suggestion

Host replacement logic fails for sub-domains & mixed protocols.

str_replace() on the full URL may miss cases such as www. prefixes or alternate schemes. Consider using wp_parse_url() to swap only the host:

- // Replace the current domain with the Parse.ly site ID.
- return str_replace( $home_url, $site_id, $url );
+ $parts = wp_parse_url( $url );
+ if ( ! $parts || empty( $parts['host'] ) ) {
+   return $url;
+ }
+ $parts['host'] = $site_id;
+ return untrailingslashit( ( $parts['scheme'] ?? 'https' ) . '://' . $parts['host'] . ( $parts['path'] ?? '' ) . ( $parts['query'] ? '?' . $parts['query'] : '' ) );
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

		// Strip the protocol from the home URL.
		$home_url = preg_replace( '/^https?:\/\//', '', $home_url );

		if ( null === $home_url ) {
			return $url;
		}

		$parts = wp_parse_url( $url );
		if ( ! $parts || empty( $parts['host'] ) ) {
			return $url;
		}
		$parts['host'] = $site_id;
		return untrailingslashit(
			( $parts['scheme'] ?? 'https' ) . '://' .
			$parts['host'] .
			( $parts['path']  ?? '' ) .
			( ! empty( $parts['query'] ) ? '?' . $parts['query'] : '' )
		);
	}
src/content-helper/dashboard-page/components/posts-table/component.tsx (2)

210-230: 🛠️ Refactor suggestion

query object in the effect dependency causes unnecessary re-fetches.

Because objects are compared by reference, passing an inline literal from a parent will trigger fetchPosts every render. Either:

  1. Memoize the query prop in the parent (useMemo), or
  2. Depend on a stable serialisation:
-}, [ currentPage, itemsPerPage, query ] );
+}, [ currentPage, itemsPerPage, JSON.stringify( query ) ] );

Be aware that large queries will incur a serialisation cost; option #1 is preferable.


198-204: 🛠️ Refactor suggestion

itemsPerPage never updates when query.per_page changes.

Because itemsPerPage is initialised from query only once (useState), subsequent prop updates will be ignored. Either compute it inside the effect or derive it with useMemo / useEffect.

-const [ itemsPerPage ] = useState<number>( query.per_page ?? 10 );
+const itemsPerPage = useMemo(
+	() => query.per_page ?? 10,
+	[ query.per_page ]
+);

Remove itemsPerPage from the state array, and update the dependencies accordingly.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

	const [ currentPage, setCurrentPage ] = useState<number>( 1 );
	const [ totalPages, setTotalPages ] = useState<number>( 1 );
-	const [ itemsPerPage ] = useState<number>( query.per_page ?? 10 );
+	const itemsPerPage = useMemo(
+		() => query.per_page ?? 10,
+		[ query.per_page ]
+	);

	const [ isLoading, setIsLoading ] = useState<boolean>( true );
	const didFirstSearch = useRef( false );
src/content-helper/dashboard-page/pages/traffic-boost/preview/preview.tsx (2)

494-499: 🛠️ Refactor suggestion

activeLink.smartLink = undefined mutates state and may create race conditions.

Create a fresh object instead of mutating the existing one. This guarantees Redux notice of the change and avoids side effects if another component still holds a reference.

-const oldSmartLink = activeLink.smartLink;
-activeLink.smartLink = undefined;
-// Update the active link.
-updateSuggestion( activeLink );
+const oldSmartLink = activeLink.smartLink;
+const updated = { ...activeLink, smartLink: undefined };
+updateSuggestion( updated );
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

		// Remove the smart link from the active link.
		const oldSmartLink = activeLink.smartLink;
		const updated = { ...activeLink, smartLink: undefined };
		updateSuggestion( updated );

292-299: ⚠️ Potential issue

Avoid mutating Redux-managed objects in place.

handleAccept flips link.isSuggestion by direct mutation, breaking the expectation of immutability and potentially bypassing change detection in selectors.

-// Flag isSuggestion to false.
-link.isSuggestion = false;
-// Add the link to the inbound links list.
-addInboundLink( link );
+const inboundCopy = { ...link, isSuggestion: false };
+addInboundLink( inboundCopy );

Apply the same pattern wherever the code mutates activeLink or its nested properties (e.g. regeneration handlers).

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

		removeSuggestion( link );

		const inboundCopy = { ...link, isSuggestion: false };
		addInboundLink( inboundCopy );
src/UI/class-dashboard-page.php (2)

111-113: 🛠️ Refactor suggestion

Use plugin_dir_path() for includes to avoid path-resolution issues.

include_once __DIR__ . '/../content-helper/...'; can break when the plugin directory is symlinked.
Prefer:

- include_once __DIR__ . '/../content-helper/dashboard-page/pages/traffic-boost/preview/preview-post.php';
+ include_once plugin_dir_path( PARSELY_FILE ) . 'src/content-helper/dashboard-page/pages/traffic-boost/preview/preview-post.php';

This is WordPress-safe and follows coding standards.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

		// Output the preview template.
		include_once plugin_dir_path( PARSELY_FILE ) . 'src/content-helper/dashboard-page/pages/traffic-boost/preview/preview-post.php';
		exit;

144-157: ⚠️ Potential issue

Internationalise all UI strings passed to add_menu_page / add_submenu_page.

WordPress requires that page titles and menu labels are wrapped in translation functions so that they can be localised.
'Parse.ly Dashboard Page', 'Parse.ly', 'Dashboard', and 'Traffic Boost' are currently hard-coded.

- 'Parse.ly Dashboard Page',
- 'Parse.ly',
+ __( 'Parse.ly Dashboard Page', 'wp-parsely' ),
+ __( 'Parse.ly', 'wp-parsely' ),

…and similarly for the other labels.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

	public function add_dashboard_page_to_menu(): void {
		// Base64-encoded version of leaf-icon.tsx with size of 20 and fill of #1d2327.
		// <svg width="20" height="20" viewBox="0 0 60 65" xmlns="http://www.w3.org/2000/svg">…</svg>.
		$icon = 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjAiIGhlaWdodD0iMjAiIHZpZXdCb3g9IjAgMCA2MCA2NSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cGF0aCBmaWxsPSIjMWQyMzI3IiBkPSJNMjMuNzIsNTEuNTNjMC0uMTgsMC0uMzQtLjA2LS41MmExMy4xMSwxMy4xMSwwLDAsMC0yLjEtNS41M0ExNC43NCwxNC43NCwwLDAsMCwxOS4xMiw0M2MtLjI3LS4yMS0uNS0uMTEtLjUxLjIybC0uMjQsMy40MmMwLC4zMy0uMzguMzUtLjQ5LDBsLTEuNS00LjhhMS40LDEuNCwwLDAsMC0uNzctLjc4LDIzLjkxLDIzLjkxLDAsMCwwLTMuMS0uODRjLTEuMzgtLjI0LTMuMzktLjM5LTMuMzktLjM5LS4zNCwwLS40NS4yMS0uMjUuNDlsMi4wNiwzLjc2Yy4yLjI3LDAsLjU0LS4yOS4zM2wtNC41MS0zLjZhMy42OCwzLjY4LDAsMCwwLTIuODYtLjQ4Yy0xLC4xNi0yLjQ0LjQ2LTIuNDQuNDZhLjY4LjY4LDAsMCwwLS4zOS4yNS43My43MywwLDAsMC0uMTQuNDVTLjQxLDQzLC41NCw0NGEzLjYzLDMuNjMsMCwwLDAsMS4yNSwyLjYyTDYuNDgsNTBjLjI4LjIuMDkuNDktLjIzLjM3bC00LjE4LS45NGMtLjMyLS4xMi0uNSwwLS40LjM3LDAsMCwuNjksMS44OSwxLjMxLDMuMTZhMjQsMjQsMCwwLDAsMS42NiwyLjc0LDEuMzQsMS4zNCwwLDAsMCwxLC41Mmw1LC4xM2MuMzMsMCwuNDEuMzguMS40OEw3LjUxLDU4Yy0uMzEuMS0uMzQuMzUtLjA3LjU1YTE0LjI5LDE0LjI5LDAsMCwwLDMuMDUsMS42NiwxMy4wOSwxMy4wOSwwLDAsMCw1LjkuNSwyNS4xMywyNS4xMywwLDAsMCw0LjM0LTEsOS41NSw5LjU1LDAsMCwxLS4wOC0xLjIsOS4zMiw5LjMyLDAsMCwxLDMuMDctNi45MSI+PC9wYXRoPjxwYXRoIGZpbGw9IiMxZDIzMjciIGQ9Ik01OS43LDQxLjUzYS43My43MywwLDAsMC0uMTQtLjQ1LjY4LjY4LDAsMCwwLS4zOS0uMjVzLTEuNDMtLjMtMi40NC0uNDZhMy42NCwzLjY0LDAsMCwwLTIuODYuNDhsLTQuNTEsMy42Yy0uMjYuMjEtLjQ5LS4wNi0uMjktLjMzbDIuMDYtMy43NmMuMi0uMjguMDktLjQ5LS4yNS0uNDksMCwwLTIsLjE1LTMuMzkuMzlhMjMuOTEsMjMuOTEsMCwwLDAtMy4xLjg0LDEuNCwxLjQsMCwwLDAtLjc3Ljc4bC0xLjUsNC44Yy0uMTEuMzItLjQ4LjMtLjQ5LDBsLS4yNC0zLjQyYzAtLjMzLS4yNC0uNDMtLjUxLS4yMmExNC43NCwxNC43NCwwLDAsMC0yLjQ0LDIuNDdBMTMuMTEsMTMuMTEsMCwwLDAsMzYuMzQsNTFjMCwuMTgsMCwuMzQtLjA2LjUyYTkuMjYsOS4yNiwwLDAsMSwzLDguMSwyNC4xLDI0LjEsMCwwLDAsNC4zNCwxLDEzLjA5LDEzLjA5LDAsMCwwLDUuOS0uNSwxNC4yOSwxNC4yOSwwLDAsMCwzLjA1LTEuNjZjLjI3LS4yLjI0LS40NS0uMDctLjU1bC0zLjIyLTEuMTdjLS4zMS0uMS0uMjMtLjQ3LjEtLjQ4bDUtLjEzYTEuMzgsMS4zOCwwLDAsMCwxLS41MkEyNC42LDI0LjYsMCwwLDAsNTcsNTIuOTJjLjYxLTEuMjcsMS4zMS0zLjE2LDEuMzEtMy4xNi4xLS4zMy0uMDgtLjQ5LS40LS4zN2wtNC4xOC45NGMtLjMyLjEyLS41MS0uMTctLjIzLS4zN2w0LjY5LTMuMzRBMy42MywzLjYzLDAsMCwwLDU5LjQ2LDQ0Yy4xMy0xLC4yNC0yLjQ3LjI0LTIuNDciPjwvcGF0aD48cGF0aCBmaWxsPSIjMWQyMzI3IiBkPSJNNDYuNSwyNS42MWMwLS41My0uMzUtLjcyLS44LS40M2wtNC44NiwyLjY2Yy0uNDUuMjgtLjU2LS4yNy0uMjMtLjY5bDQuNjYtNi4yM2EyLDIsMCwwLDAsLjI4LTEuNjgsMzYuNTEsMzYuNTEsMCwwLDAtMi4xOS00Ljg5LDM0LDM0LDAsMCwwLTIuODEtMy45NGMtLjMzLS40MS0uNzQtLjM1LS45MS4xNmwtMi4yOCw1LjY4Yy0uMTYuNS0uNi40OC0uNTktLjA1bC4yOC04LjkzYTIuNTQsMi41NCwwLDAsMC0uNjYtMS42NFMzNSw0LjI3LDMzLjg4LDMuMjcsMzAuNzguNjksMzAuNzguNjlhMS4yOSwxLjI5LDAsMCwwLTEuNTQsMHMtMS44OCwxLjQ5LTMuMTIsMi41OS0yLjQ4LDIuMzUtMi40OCwyLjM1QTIuNSwyLjUsMCwwLDAsMjMsNy4yN2wuMjcsOC45M2MwLC41My0uNDEuNTUtLjU4LjA1bC0yLjI5LTUuNjljLS4xNy0uNS0uNTctLjU2LS45MS0uMTRhMzUuNzcsMzUuNzcsMCwwLDAtMyw0LjIsMzUuNTUsMzUuNTUsMCwwLDAtMiw0LjYyLDIsMiwwLDAsMCwuMjcsMS42N2w0LjY3LDYuMjRjLjMzLjQyLjIzLDEtLjIyLjY5bC00Ljg3LTIuNjZjLS40NS0uMjktLjgyLS4xLS44Mi40M2ExOC42LDE4LjYsMCwwLDAsLjgzLDUuMDcsMjAuMTYsMjAuMTYsMCwwLDAsNS4zNyw3Ljc3YzMuMTksMyw1LjkzLDcuOCw3LjQ1LDExLjA4QTkuNiw5LjYsMCwwLDEsMzAsNDkuMDlhOS4zMSw5LjMxLDAsMCwxLDIuODYuNDVjMS41Mi0zLjI4LDQuMjYtOC4xMSw3LjQ0LTExLjA5YTIwLjQ2LDIwLjQ2LDAsMCwwLDUuMDktNywxOSwxOSwwLDAsMCwxLjExLTUuODIiPjwvcGF0aD48cGF0aCBmaWxsPSIjMWQyMzI3IiBkPSJNMzYuMTIsNTguNDRBNi4xMiw2LjEyLDAsMSwxLDMwLDUyLjMyYTYuMTEsNi4xMSwwLDAsMSw2LjEyLDYuMTIiPjwvcGF0aD48L3N2Zz4=';

		add_menu_page(
			__( 'Parse.ly Dashboard Page', 'wp-parsely' ),
			__( 'Parse.ly', 'wp-parsely' ),
			Parsely::CAPABILITY, // phpcs:ignore WordPress.WP.Capabilities.Undetermined
			'parsely-dashboard-page',
			array( $this, 'add_dashboard_page_placeholder' ),
			$icon,
			30
		);
	}
src/content-helper/dashboard-page/pages/traffic-boost/preview/hooks/use-iframe-highlight.ts (2)

594-603: ⚠️ Potential issue

siteUrl detection can mis-classify links; compare hostnames instead of includes().

A URL may contain the site domain as a query string or path segment, leading to false “internal” positives/negatives.

- links = links.filter( ( link ) => ! link.href.includes( siteUrl ) );
+ links = links.filter( ( link ) => {
+     try {
+         return new URL( link.href ).hostname !== siteUrl;
+     } catch {
+         return true; // treat malformed URLs as external.
+ }
+ } );

Apply the inverse logic for the 'internal' branch.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

switch ( selectedLinkType ) {
    case 'external':
        links = links.filter( ( link ) => {
            try {
                return new URL( link.href ).hostname !== siteUrl;
            } catch {
                return true; // treat malformed URLs as external.
            }
        } );
        break;
    case 'internal':
        links = links.filter( ( link ) => link.href.includes( siteUrl ) );
        break;
    case 'smart':
        links = links.filter( ( link ) => link.hasAttribute( 'data-smartlink' ) );
        break;
}

52-67: 🛠️ Refactor suggestion

Avoid injecting duplicate <style> tags on repeated calls.

injectHighlightStyles adds a new <style> element every time it runs, which can bloat the DOM if the hook re-initialises.

+ if ( iframeDocument.getElementById( 'parsely-highlight-styles' ) ) {
+     return; // already injected.
+ }
  const style = iframeDocument.createElement( 'style' );
+ style.id = 'parsely-highlight-styles';
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

const injectHighlightStyles = useCallback( ( iframe: HTMLIFrameElement ) => {
	const iframeDocument = iframe.contentDocument ?? iframe.contentWindow?.document;
	if ( ! iframeDocument ) {
		return;
	}

	if ( iframeDocument.getElementById( 'parsely-highlight-styles' ) ) {
		return; // already injected.
	}

	const style = iframeDocument.createElement( 'style' );
	style.id = 'parsely-highlight-styles';
	style.textContent = `
		/** Smart link highlight styles. */
		.smart-link-highlight {
			outline: 2px solid #3858E9;
			border-radius: 2px;
			background-color: #3858E9;
			color: #ffffff;
		}
	`;
	// ...append the style to the iframe document
}, [] );
src/rest-api/content-helper/class-endpoint-smart-linking.php (1)

104-110: ⚠️ Potential issue

Validation callbacks rely on a namespaced class that is not imported.

Validations\Validate_Url_Exclusion_List is referenced but the use statement is missing, leading to fatal errors in PHP < 8.0 where automatic class loading is case-sensitive.

+ use Parsely\REST_API\Validations;

(or fully-qualified class strings).

src/content-helper/dashboard-page/pages/traffic-boost/provider.ts (2)

596-612: 🛠️ Refactor suggestion

getPosts call lacks error handling – bubble up or default-guard.

If the REST getPosts request fails (network error or non-2xx status) the
provider will throw, crashing the whole batch.
Consider wrapping every promise with a try/catch and marking failures so the UI
can still render partial results.


252-258: ⚠️ Potential issue

Potential undefined pollution in the URL exclusion list.

parsely_canonical_url may be undefined; concatenating it into excludedUrls
will inject empty-string entries, artificially inflating the list and causing
includes() look-ups to succeed incorrectly.

-				excludedUrls = [ ...excludedUrls, ...filteredGeneratedSuggestions.map( ( suggestion ) => suggestion.smartLink?.post_data?.parsely_canonical_url ?? '' ) ];
+				excludedUrls = [
+					...excludedUrls,
+					...filteredGeneratedSuggestions
+						.map( ( s ) => s.smartLink?.post_data?.parsely_canonical_url )
+						.filter( ( url ): url is string => typeof url === 'string' && url.length )
+				];
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

const filteredGeneratedSuggestions = newGeneratedSuggestions.filter(
    ( suggestion ) => ! excludedUrls.includes( suggestion.smartLink?.post_data?.parsely_canonical_url ?? '' )
);

// Update the excluded URLs.
excludedUrls = [
    ...excludedUrls,
    ...filteredGeneratedSuggestions
        .map( ( s ) => s.smartLink?.post_data?.parsely_canonical_url )
        .filter( ( url ): url is string => typeof url === 'string' && url.length )
];
src/rest-api/content-helper/class-endpoint-traffic-boost.php (1)

88-128: ⚠️ Potential issue

permission_callback is not declared for the generated-suggestion route.

Without an explicit permission_callback, the route falls back to the default
one defined in Base_Endpoint only if it overrides register_rest_route.
When registering routes manually, it is safer to state the capability
requirement up-front:

'permission_callback' => array( $this, 'check_read_access' ),

This prevents unauthenticated users from POST-ing to /generate.

src/Models/class-inbound-smart-link.php (2)

172-180: ⚠️ Potential issue

Duplicate-link check may false-positive on substring matches.

strpos( $post->post_content, $this->href ) triggers even when $this->href
appears as a query-string portion of a different URL.
Parse the DOM and search for exact <a href> equality instead to avoid
incorrectly blocking valid placements.


1213-1221: ⚠️ Potential issue

find_line_with_text() relies on newline-delimited HTML.

Minified or REST-stripped post content is often single-line, causing the search
to return '' and later trigger a fatal str_replace() failure.
Split on /> boundaries or use DOM + XPath instead for reliable matching.

src/Models/class-smart-link.php (3)

900-904: 🛠️ Refactor suggestion

Cache lookup unnecessarily bypassed when result is an empty array

The additional count( $smart_links ) > 0 condition causes a DB query every time the cached value is an empty array (no smart links). Return as soon as the cache hit is non-false to avoid redundant queries.

- if ( false !== $smart_links && count( $smart_links ) > 0 ) {
+ if ( false !== $smart_links ) {

1042-1044: ⚠️ Potential issue

Invalid orderby value passed to WP_Query

'date modified' is not a supported orderby value. WordPress accepts 'modified' (for modified date) or 'date' (for published date). The current value will be ignored and the query will default to post_date DESC, altering the intended sort order.

- 'orderby' => 'date modified',
+ 'orderby' => 'modified',
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

				'orderby' => 'modified',
				'order'   => 'ASC',
			),

1092-1095: ⚠️ Potential issue

Fix static call to non-static method in flush_cache()

get_cache_group() is an instance method, but it’s being called statically.
PHP 8 throws Error: Cannot make static call to non-static method, so this will fatally break cache flushes (and anything that triggers them).

- wp_cache_delete( 'smart-link-uid-map-' . $this->uid . '-' . $this->source_post_id, self::get_cache_group() );
+ wp_cache_delete(
+     'smart-link-uid-map-' . $this->uid . '-' . $this->source_post_id,
+     $this->get_cache_group()
+ );
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

	protected function flush_cache(): void {
		// Delete the cache for the smart link UID to post ID association.
		wp_cache_delete(
			'smart-link-uid-map-' . $this->uid . '-' . $this->source_post_id,
			$this->get_cache_group()
		);
	}

@alecgeatches alecgeatches requested a review from acicovic April 25, 2025 19:22
@alecgeatches alecgeatches self-assigned this Apr 25, 2025
@acicovic acicovic added this to the 3.19.0 milestone May 6, 2025
Copy link
Collaborator

@acicovic acicovic left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for debugging and working on this!

@alecgeatches alecgeatches merged commit 280612b into add/traffic-boost May 7, 2025
41 checks passed
@alecgeatches alecgeatches deleted the fix/invalid-page-template-error branch May 7, 2025 16:45
@acicovic acicovic added Maintenance & Fixes Ticket/PR related to codebase maintenance tasks Feature: Engagement Boost Ticket/PR related to Engagement Boost labels May 13, 2025
@acicovic acicovic changed the title Fix "Invalid page template" error Traffic Boost: Fix "Invalid page template" error May 20, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Feature: Engagement Boost Ticket/PR related to Engagement Boost Maintenance & Fixes Ticket/PR related to codebase maintenance tasks

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants