Fix Fourthwall parsing breakage, add JSON-LD/sitemap sources, rework caching
Create Release / build (push) Successful in 5s

Fourthwall moved the product title from <h2> to <h1>, which silently emptied
[fourthwall_single] and [fourthwall_random]: both gated rendering on a title
match that could no longer succeed. Verified against a live store.

Parsing
- Read schema.org JSON-LD first, falling back to scraped markup field by field,
  then to og: meta tags. Each field degrades independently, so a future markup
  change can only affect what it actually touched.
- Match CSS classes with a padded contains() predicate instead of @class="...",
  and select on data-testid where available. Exact class matching is what broke.
- Surface sku, availability and currency, which were previously discarded.

Product discovery
- Enumerate products from sitemap.xml rather than scraping the store page, which
  only sees the collection it features. [fourthwall_random count="10"] could not
  return more than the 3 items a featured homepage renders.
- New source="auto|sitemap|page" attribute. Off-host <loc> entries are rejected
  so a hostile sitemap cannot redirect fetches.

Caching
- Stale-while-revalidate: stale entries are served immediately and refreshed by
  WP-Cron, so cache expiry never costs a visitor a network round trip.
- Refresh scheduling dedupes via wp_next_scheduled and takes a lock, collapsing
  a stampede to a single job.
- Fetch uncached product pages concurrently with curl_multi instead of serially.
- Cache lifetime is now configurable (default 60 minutes).
- Entries written by earlier versions are read as stale and upgraded in place,
  so caches turn over without blanking a store.

Fixes
- Build absolute product links properly. "/collections/all" + "/products/x"
  produced 404s; a trailing slash produced "//products/x".
- Escape the price on output; it was interpolated raw.
- strpos() misuse meant an error string at offset 0 was treated as success.
- Guard a null description node that would fatal when show_description="true".
- Report non-200 responses instead of caching an empty body silently.
- Drop the shared /tmp/cookies.txt jar. It was cross-site mutable state and made
  concurrent fetching unsafe; all endpoints return 200 without it.
- Remove the ssl_verify setting. It was a development affordance and its
  checkbox never worked - unchecked meant "key absent", which read as true.
  Certificate verification is now pinned on.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-06 09:33:06 -07:00
co-authored by Claude Opus 5
parent f22615c6d5
commit ecef4d3f2e
4 changed files with 1094 additions and 165 deletions
+37 -9
View File
@@ -27,7 +27,8 @@ class fourthwall_settings {
register_setting(
'fourthwall_settings_group',
'fourthwall_settings_name'
'fourthwall_settings_name',
array( 'sanitize_callback' => array( $this, 'sanitize_settings' ) )
);
add_settings_section(
@@ -46,15 +47,37 @@ class fourthwall_settings {
);
add_settings_field(
'ssl_verify',
__( 'SSL Verification', 'fourthwall_text_domain' ),
array( $this, 'render_ssl_verify_field' ),
'cache_ttl',
__( 'Cache Lifetime', 'fourthwall_text_domain' ),
array( $this, 'render_cache_ttl_field' ),
'fourthwall_settings_name',
'fourthwall_settings_name_section'
);
}
/**
* Sanitize settings before they are stored.
*
* @param array $input Raw submitted values
* @return array Cleaned values
*/
public function sanitize_settings( $input ) {
$input = is_array( $input ) ? $input : array();
$output = array();
if ( isset( $input['fourth_url'] ) ) {
$output['fourth_url'] = esc_url_raw( trim( $input['fourth_url'] ) );
}
$ttl = isset( $input['cache_ttl'] ) ? intval( $input['cache_ttl'] ) : 60;
$output['cache_ttl'] = $ttl < 1 ? 60 : $ttl;
return $output;
}
public function fourthwall_page_layout() {
// Check required user capability
@@ -94,7 +117,9 @@ class fourthwall_settings {
echo ' <p><strong>' . __( 'Display random products:', 'fourthwall_text_domain' ) . '</strong> <code>[fourthwall_random count="5"]</code></p>' . "\n";
echo ' <p><strong>' . __( 'Random from specific URLs:', 'fourthwall_text_domain' ) . '</strong> <code>[fourthwall_random count="3" urls="https://store.com/product1,https://store.com/product2,https://store.com/product3"]</code></p>' . "\n";
echo ' <p><strong>' . __( 'Random from different store:', 'fourthwall_text_domain' ) . '</strong> <code>[fourthwall_random count="2" store_url="https://different-store.fourthwall.com"]</code></p>' . "\n";
echo ' <p><em>' . __( 'Note: Disable SSL verification only for local development. Keep enabled for production sites.', 'fourthwall_text_domain' ) . '</em></p>' . "\n";
echo ' <p><strong>' . __( 'Random from the store page instead of the sitemap:', 'fourthwall_text_domain' ) . '</strong> <code>[fourthwall_random count="3" source="page"]</code></p>' . "\n";
echo ' <p><em>' . __( 'Random products are drawn from your store sitemap, which covers your whole catalog. Set source="page" to only use products shown on the store page itself.', 'fourthwall_text_domain' ) . '</em></p>' . "\n";
echo ' <p><em>' . __( 'Tip: point the Store URL at your /collections/all page to list every product with [fourthwall].', 'fourthwall_text_domain' ) . '</em></p>' . "\n";
echo ' </div>' . "\n";
echo '</div>' . "\n";
@@ -114,17 +139,20 @@ class fourthwall_settings {
}
function render_ssl_verify_field() {
function render_cache_ttl_field() {
// Retrieve data from the database.
$options = get_option( 'fourthwall_settings_name' );
// Set default value.
$value = isset( $options['ssl_verify'] ) ? $options['ssl_verify'] : '';
$value = isset( $options['cache_ttl'] ) ? intval( $options['cache_ttl'] ) : 60;
if ( $value < 1 ) {
$value = 60;
}
// Field output.
echo '<input type="checkbox" name="fourthwall_settings_name[ssl_verify]" value="1" ' . checked( $value, 1, false ) . '>';
echo '<p class="description">' . __( 'Enable SSL verification', 'fourthwall_text_domain' ) . '</p>';
echo '<input type="number" min="1" step="1" name="fourthwall_settings_name[cache_ttl]" class="small-text" value="' . esc_attr( $value ) . '"> ' . esc_html__( 'minutes', 'fourthwall_text_domain' );
echo '<p class="description">' . __( 'How long store content stays fresh before it is refreshed (default: 60). Expired content is still shown instantly while it refreshes in the background, so raising this does not slow your pages down.', 'fourthwall_text_domain' ) . '</p>';
}