When defining a custom content URL with the constant WP_CONTENT_URL, the Yoast SEO panel while editing a page is broken.
The following errors are generated in the JS console:
Uncaught null
Script error.
This happening due to web workers limitations as spotted in the following issues: #13157 and #10875
Yoast plugin metabox loads fine without any errors.
* Which browser is affected (or browsers):
- [x] Chrome
- [x] Firefox
- [x] Safari
- [ ] Other
Hi @mickaelperrin and thank you for your report.
Unfortunately webworkers indeed don't work cross-domain. As we're not looking to work around this "limitation" this issue is a won't fix on our part (closing this issue as such). However, I am curious to the reason as to why you're loading these assets from another domain. Is this CDN related? If so, this can probably be done without this limitation getting in the way.
@Djennez Thanks for the honest answer ;) I was sure you won't fix that issue. I have implemented a "simple" fix to symlink and move the needed js to the main domain.
We have made this architecture choice essentially for performance and security reasons as we disabled PHP execution on that "media" subdomain which handles themes / plugins / uploads...
Cool, thanks for the explanation!
HI @Djennez ,
I'm working on a similar issue and I'm wondering if your plugin works fine on the multisite architecture. This because we have a portal with several subsites mapped into different subdomains and we enables the Yoast Plugin for each subsite.
and it works only on domain.com. I understood that the problem are related to webworkers, we are using this solution: https://roots.io/bedrock/
What would be a workaround? If exists.
@CeccoCQ I have ended up with the following solution, that copies the webworker related files to the main domain. Maybe you could tuned it to resolve your issue.
<?php
/**
* Fix issue with Yoast SEO and WP_CONTENT_URL
* by creating symlinks of javascript web workers
* in the original domain to prevent cross domain issues
* becuase scripts are loaded on the media subdomain.
*/
defined( 'ABSPATH' ) || exit; // Exit if accessed directly.
// Currently the fix is only needed for websites with WP_CONTENT_URL defined
if (!defined('WP_CONTENT_URL')) {
return;
}
add_action('admin_enqueue_scripts', function($hook_suffix) {
// Only trigger the fix if Yoast SEO is loaded
if (!is_plugin_active('wordpress-seo/wp-seo.php')) {
return;
}
$fix = new WP_Yoast_SEO_Fix();
$fix->run();
}, PHP_INT_MAX);
class WP_Yoast_SEO_Fix {
private $symlinksFolderPath;
private $symlinksFolderName = 'scripts-moved-from-media';
/**
* @var array key => value array
* key: the handle of the script to process
* value: the name of method that does the processing
*/
protected $scriptsProcessors = [
'yoast-seo-post-scraper' => 'yoastseopostscraper',
'yoast-seo-commons' => 'simplyCreateLink',
'yoast-seo-analysis' => 'simplyCreateLink',
];
public function __construct() {
$this->symlinksFolderPath = ABSPATH . $this->symlinksFolderName;
$this->ensureFolderExists($this->symlinksFolderPath);
}
private function ensureFolderExists($folder) {
if (!file_exists($folder)) {
mkdir($folder);
}
}
public function run() {
global $wp_scripts;
foreach( $wp_scripts->registered as $handle => $config ) {
if (!in_array($handle, array_keys($this->scriptsProcessors))) {
continue;
}
// Rune the assocaited method with the handler
$this->{$this->scriptsProcessors[$handle]}($config, $handle);
}
}
private function yoastseopostscraper($config, $handle) {
global $wp_scripts;
$data = $config->extra['data'];
$data = preg_replace_callback(
'/var wpseoAnalysisWorkerL10n = (.*);/i', function ($match) use ($handle) {
if (!count($match) == 2 || !$config = json_decode($match[1])) {
return $match[0];
}
$filePath = WP_CONTENT_DIR . str_replace(WP_CONTENT_URL, '', $config->url);
$this->moveScriptToBaseHost($filePath, $handle);
$config->url = site_url() . '/' . $this->symlinksFolderName . '/' . basename($filePath);
return 'var wpseoAnalysisWorkerL10n = ' . json_encode($config);
}
, $data);
$wp_scripts->registered[$handle]->extra['data'] = $data;
}
private function moveScriptToBaseHost($originalFilePath, $handle) {
if (file_exists($originalFilePath) && !file_exists( $this->symlinksFolderPath . '/' . basename($originalFilePath))) {
symlink($originalFilePath, $this->symlinksFolderPath . '/' . basename($originalFilePath));
}
}
private function simplyCreateLink($config, $handle) {
global $wp_scripts;
$filePath = WP_CONTENT_DIR . str_replace(WP_CONTENT_URL, '', $config->src);
$this->moveScriptToBaseHost($filePath, $handle);
$wp_scripts->registered[$handle]->src = site_url() . '/' . $this->symlinksFolderName . '/' . basename($filePath);
}
}
I found this to be a much more simple fix.
In the wp-config.php where you define the WP_CONTENT_URL wrap it in a if statement to see if the path begins with /wp-admin, if yes it will not set the WP_CONTENT_URL, also it checks if it is a call to the admin-ajax.php so that it can use the WP_CONTENT_URL there so that front end ajax loaded content can continue to use the CDN domain for delivering images
$path = filter_input(INPUT_SERVER, 'REQUEST_URI', FILTER_SANITIZE_URL);
if(strpos($path, '/wp-admin') === false || strpos($path, '/wp-admin/admin-ajax.php') === true ) {
define("WP_CONTENT_URL", "https://the-cdn-domain.com/");
define("COOKIE_DOMAIN", ".the-main-domain.com");
}
@andykillen It depends on how your directory structure is. Mine is derived from themosis/bedrock, and plugins/theme ressources are simply not available under the main domain.
@mickaelperrin That will be a total nightmare not having them on the main domain. you are right, it's never going to work.
However, I have to say that your solution can't work for me either, the CI/CD will delete it every time that there is a new release and provide a read-only structure.... so problems all round. :(
I'll update mine in a moment to deal with AJAX as it also has the problem of loading the main url for images that are loaded from AJAX based queries.
makes me wonder how much of this in future is going to stop basic good infrastructure configuration for the sake of browser based enhancements.
@CeccoCQ I have ended up with the following solution, that copies the webworker related files to the main domain. Maybe you could tuned it to resolve your issue.
<?php /** * Fix issue with Yoast SEO and WP_CONTENT_URL * by creating symlinks of javascript web workers * in the original domain to prevent cross domain issues * becuase scripts are loaded on the media subdomain. */ defined( 'ABSPATH' ) || exit; // Exit if accessed directly. // Currently the fix is only needed for websites with WP_CONTENT_URL defined if (!defined('WP_CONTENT_URL')) { return; } add_action('admin_enqueue_scripts', function($hook_suffix) { // Only trigger the fix if Yoast SEO is loaded if (!is_plugin_active('wordpress-seo/wp-seo.php')) { return; } $fix = new WP_Yoast_SEO_Fix(); $fix->run(); }, PHP_INT_MAX); class WP_Yoast_SEO_Fix { private $symlinksFolderPath; private $symlinksFolderName = 'scripts-moved-from-media'; /** * @var array key => value array * key: the handle of the script to process * value: the name of method that does the processing */ protected $scriptsProcessors = [ 'yoast-seo-post-scraper' => 'yoastseopostscraper', 'yoast-seo-commons' => 'simplyCreateLink', 'yoast-seo-analysis' => 'simplyCreateLink', ]; public function __construct() { $this->symlinksFolderPath = ABSPATH . $this->symlinksFolderName; $this->ensureFolderExists($this->symlinksFolderPath); } private function ensureFolderExists($folder) { if (!file_exists($folder)) { mkdir($folder); } } public function run() { global $wp_scripts; foreach( $wp_scripts->registered as $handle => $config ) { if (!in_array($handle, array_keys($this->scriptsProcessors))) { continue; } // Rune the assocaited method with the handler $this->{$this->scriptsProcessors[$handle]}($config, $handle); } } private function yoastseopostscraper($config, $handle) { global $wp_scripts; $data = $config->extra['data']; $data = preg_replace_callback( '/var wpseoAnalysisWorkerL10n = (.*);/i', function ($match) use ($handle) { if (!count($match) == 2 || !$config = json_decode($match[1])) { return $match[0]; } $filePath = WP_CONTENT_DIR . str_replace(WP_CONTENT_URL, '', $config->url); $this->moveScriptToBaseHost($filePath, $handle); $config->url = site_url() . '/' . $this->symlinksFolderName . '/' . basename($filePath); return 'var wpseoAnalysisWorkerL10n = ' . json_encode($config); } , $data); $wp_scripts->registered[$handle]->extra['data'] = $data; } private function moveScriptToBaseHost($originalFilePath, $handle) { if (file_exists($originalFilePath) && !file_exists( $this->symlinksFolderPath . '/' . basename($originalFilePath))) { symlink($originalFilePath, $this->symlinksFolderPath . '/' . basename($originalFilePath)); } } private function simplyCreateLink($config, $handle) { global $wp_scripts; $filePath = WP_CONTENT_DIR . str_replace(WP_CONTENT_URL, '', $config->src); $this->moveScriptToBaseHost($filePath, $handle); $wp_scripts->registered[$handle]->src = site_url() . '/' . $this->symlinksFolderName . '/' . basename($filePath); } }
Hey @mickaelperrin, could you let us know where should we save your php code under what name? We do have exact same issue and wondering maybe you can give us a hand.
Appreciate
Copy-paste it in the functions.phpfile of your theme. I guess this is the simplest and it should work.
Thank you for your prompt reply and attention @mickaelperrin. We really appreciate for the time you put on the table for us.
We are using the premium version and couldn't get it to work even when we change 'wordpress-seo/wp-seo.php' to 'wordpress-seo-premium/wp-seo-premium.php'. We are hosting 'media' and 'blog' on docker and using Nginx if they make any difference. I also have to add, we tweaked the nginx a little bit to some degrees that it shows the blog sub-directory like this: website.com/Blog not website.com/blog all the time.
The issue still remains even when we use the free version over the premium. Could you please advise us what has to be done in this situation? You are the only hope we have to resolve this issue.
Thanks again
I want to give fresh ideas about possible workarounds. I've tried to reproduce previous issues.
It's possible that there is problem with CORS:
_Cross-Origin Request Blocked_: The Same Origin Policy disallows reading the remote resource at https://cdn......com/wp-content/plugins/... (Reason: CORS header ‘_Access-Control-Allow-Origin_’ missing)
CDN (or sub-domains) should sent proper header (eg. for Apache):
<IfModule mod_headers.c>
<FilesMatch "\.(ttf|ttc|otf|eot|woff|woff2|font.css|css|js)$">
Header set Access-Control-Allow-Origin "https://mainsite.com/"
</FilesMatch>
</IfModule>
define( 'CONCATENATE_SCRIPTS', false );
if ( is_admin() ) {
add_filter( 'plugins_url', function( $url, $path, $plugin ) {
if ( strrpos( $plugin, 'wordpress-seo/wp-seo.php' ) === false ) {
return $url;
}
if ( ! preg_match( '/^js\/dist\/(.+)\-[0-9A-Z\-]+(\.min)?\.js/', $path, $matches ) ) {
return $url;
}
switch ( $matches[1] ) {
case 'analysis' :
case 'commons' :
case 'wp-seo-post-scraper' :
case 'wp-seo-term-scraper' :
case 'wp-seo-analysis-worker' :
case 'wp-seo-used-keywords-assessment' :
// change $url
break;
}
return $url;
}, 10, 3 );
}
@saman-masoomi Did you find errors in browser console? I didn't try Nginx, I've tried basic configuration in Apache (and it works if I add CORS header without PHP code). I've tested with latest WP/Yoast SEO.
Thanks @stodorovic for your ideas; however, we still can't use premium version. But in some how the free version is working out of box! We did absolutely nothing for situation we have with the free version, in other words, if we have used the free version from day one, we would have never seen any issue!
We gave the free version a chance, once we found no luck by trying ALL above suggestions.
This is how we designed our websites:
https://website.com/Blog1
https://website.com/Blog2
https://website.com/Blog3
https://website.com has its own docker for nginx, php and mysql
Blog1, Blog2, Blog3 are using website.com's dockers and their own wp-content has been renamed and mounted to media.website.com (another docker)
There is no CORS error, on Blog1, but there are some on Blog2 and Blog3. I have to mention there is absolutely no issue to use Yoast free version on Blog2 and Blog3.
Functions.php for each website is default, we haven't modified.
website.com, Blog1, Blog2 and Blog3 have their own WordPress and MySQL on docker, but nginx and php are shared.
We have set rules on Nginx to force SSL and showing blog like this Blog not blog
We tried the php codes and added "-premium" to where we thought it would help, but no luck there.
Do you think is there anything wrong with premium version?
Why don't we have any issue like what @mickaelperrin had? (Asking to see maybe by reproducing the issue on the free version, we can actually solve the issue we get on premium version)
Thanks for your time
Most helpful comment
@CeccoCQ I have ended up with the following solution, that copies the webworker related files to the main domain. Maybe you could tuned it to resolve your issue.