Compare commits

..
Author SHA1 Message Date
shadowdaoandClaude Opus 5 ecef4d3f2e 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>
2026-08-06 09:33:06 -07:00
shadowdaoandClaude Opus 4.6 f22615c6d5 Fix single product parsing after Fourthwall HTML change
Create Release / build (push) Successful in 5s
Fourthwall changed product title tags from h1 to h2 on individual
product pages, breaking the fourthwall_random and fourthwall_single
shortcodes. Updated XPath query to match new h2 tag structure.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 12:25:23 -08:00
shadowdaoandClaude 9b4e28ece1 Improve product tile styling with uniform sizing and theme inheritance
Create Release / build (push) Successful in 3s
- Make all product images uniform size (300px height) with object-fit: cover
- Equalize product tile containers using flexbox layout
- Inherit site colors and fonts instead of hardcoding white background
- Add proper flexbox structure for consistent tile heights

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-05 15:33:06 -07:00
shadowdao b4f17c80b3 Add fourthwall_random shortcode examples to admin settings page
Create Release / build (push) Successful in 2s
- Add three usage examples for the new [fourthwall_random] shortcode
- Include examples for basic random products, specific URLs, and different stores
- Place examples alongside existing shortcode documentation in admin panel
- Improve user experience by providing clear usage guidance
2025-06-24 11:40:08 -07:00
shadowdao 779fb54995 Add random products shortcode feature
Create Release / build (push) Successful in 5s
- Add [fourthwall_random] shortcode for displaying random products
- Support for specifying count of products to display
- Option to use all store products or specific product URLs
- Add fwembed_extract_product_urls() function to extract product URLs
- Add fwembed_get_random_products() function for randomization logic
- Update README.md with comprehensive documentation and examples
- Maintain backward compatibility with existing shortcodes
2025-06-24 11:36:36 -07:00
shadowdao 64afcb71cb Refactor plugin with improved architecture and new features
Create Release / build (push) Successful in 5s
- Eliminated code duplication by creating common HTTP request function
- Added configurable SSL verification setting for development/production
- Implemented smart caching system with 1-hour cache duration
- Enhanced admin interface with cache management and better styling
- Improved error handling and user experience
- Added comprehensive documentation and troubleshooting guide
- Updated README with new features and configuration options
2025-06-24 11:21:04 -07:00
jknapp 94329dc91a Merge pull request 'Fix: Release not creating Zip file created' (#9) from feature-single-product into main
Create Release / build (push) Successful in 3s
Reviewed-on: #9
2025-05-20 21:41:37 +00:00
shadowdao ca3275fb4b Merge branch 'main' into feature-single-product 2025-05-20 14:38:37 -07:00
shadowdao 609883fc7f resolving issue with release not having rsync 2025-05-20 14:37:42 -07:00
jknapp 60d5060398 Merge pull request 'Update automation to fix folder mess with updates and notes' (#8) from feature-single-product into main
Create Release / build (push) Failing after 2s
Reviewed-on: #8
2025-05-20 21:34:21 +00:00
jknapp 49703fd66d Merge branch 'main' into feature-single-product 2025-05-20 21:33:45 +00:00
shadowdao 9e1d8b8684 Update automation to fix folder mess with updates and notes 2025-05-20 14:29:59 -07:00
jknapp 554e2ba93e Merge pull request 'Adding Feature Request for single product highlight' (#7) from feature-single-product into main
Create Release / build (push) Successful in 3s
Reviewed-on: #7
2025-05-18 17:27:06 +00:00
shadowdao 1955b14ac5 Adding Feature Request for single product highlight 2025-05-18 10:25:53 -07:00
jknapp 10634e8f7e Merge pull request 'Remove Debug Code for version' (#5) from fix-debug-notice into main
Create Release / build (push) Successful in 3s
Reviewed-on: #5
2025-04-22 20:47:06 +00:00
shadowdao e94ef7b84b Remove Debug Code for version 2025-04-22 13:46:42 -07:00
jknapp db1d5f8945 Merge pull request 'Major Update to add auto-update features' (#4) from auto-update into main
Create Release / build (push) Successful in 3s
Reviewed-on: #4
2025-04-22 20:38:32 +00:00
jknapp 10ec29c30a Merge branch 'main' into auto-update 2025-04-22 20:37:39 +00:00
shadowdao bf99f442ba Major Update to add auto-update features 2025-04-22 13:35:47 -07:00
jknapp 0d0cb65633 Add workflow to update version number on release
Create Release / build (push) Successful in 2s
2025-04-22 20:28:17 +00:00
jknapp c392af46e7 Merge pull request 'Update for FW switching to sending Binary instead of raw html' (#3) from Fix-fw-binary into main
Create Release / build (push) Successful in 3s
Reviewed-on: #3
2025-04-22 17:50:27 +00:00
shadowdao 1bdae9a815 Update for FW switching to sending Binary instead of raw html 2025-04-22 10:48:19 -07:00
shadowdao be7db52eec Update DOM crawler to resolver PHP warning
Create Release / build (push) Successful in 3s
2025-01-26 12:30:03 -08:00
jknapp 059cc94063 Merge pull request 'updating docs and settings for release' (#1) from update-documentation into main
Create Release / build (push) Successful in 2s
Reviewed-on: CyberCoveLLC/fourth-wall-embed-wp#1
2025-01-11 03:58:05 +00:00
9 changed files with 1950 additions and 65 deletions
+43 -2
View File
@@ -14,6 +14,7 @@ jobs:
with: with:
username: ${{ secrets.CI_USER }} username: ${{ secrets.CI_USER }}
password: ${{ secrets.CI_TOKEN }} password: ${{ secrets.CI_TOKEN }}
fetch-depth: 0 # Important: Fetch all history for commit messages
- name: Get version - name: Get version
id: get_version id: get_version
@@ -24,15 +25,55 @@ jobs:
echo "version=$(date +'%Y.%m.%d-%H%M')" >> $GITHUB_OUTPUT echo "version=$(date +'%Y.%m.%d-%H%M')" >> $GITHUB_OUTPUT
fi fi
- name: Generate release notes
id: release_notes
run: |
# Find the most recent tag
LATEST_TAG=$(git describe --tags --abbrev=0 --always 2>/dev/null || echo "none")
if [ "$LATEST_TAG" = "none" ]; then
# If no previous tag exists, get all commits
COMMITS=$(git log --pretty=format:"* %s (%h)" --no-merges)
else
# Get commits since the last tag
COMMITS=$(git log --pretty=format:"* %s (%h)" ${LATEST_TAG}..HEAD --no-merges)
fi
# Create release notes with header (without encoding newlines)
echo "notes<<EOF" >> $GITHUB_OUTPUT
echo "## What's New in ${{ steps.get_version.outputs.version }}" >> $GITHUB_OUTPUT
echo "" >> $GITHUB_OUTPUT
echo "$COMMITS" >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
- name: Update plugin version
run: |
# Replace version placeholder with actual version
sed -i "s/Version: {auto_update_value_on_deploy}/Version: ${{ steps.get_version.outputs.version }}/" fw-store-embed.php
# Verify the change was made
grep "Version:" fw-store-embed.php
- name: Create ZIP archive - name: Create ZIP archive
run: | run: |
zip -r fourthwall-store-embed.zip . -x ".git/*" ".gitea/*" # Create a temp directory with the correct plugin folder name
mkdir -p /tmp/fourthwall-store-embed
# Copy files to the temp directory (excluding git and other unnecessary files)
cp -r * /tmp/fourthwall-store-embed/ 2>/dev/null || true
# Exclude .git and .gitea directories
rm -rf /tmp/fourthwall-store-embed/.git /tmp/fourthwall-store-embed/.gitea 2>/dev/null || true
# Create the ZIP file with the proper structure
cd /tmp
zip -r $GITHUB_WORKSPACE/fourthwall-store-embed.zip fourthwall-store-embed
- name: Create Release - name: Create Release
uses: softprops/action-gh-release@v2 uses: softprops/action-gh-release@v2
with: with:
token: "${{ secrets.REPO_TOKEN }}" token: "${{ secrets.REPO_TOKEN }}"
title: Fourthwall-store-embed Release ${{ steps.get_version.outputs.version }} title: "Fourthwall-store-embed Release ${{ steps.get_version.outputs.version }}"
tag_name: ${{ steps.get_version.outputs.version }} tag_name: ${{ steps.get_version.outputs.version }}
body: "${{ steps.release_notes.outputs.notes }}"
files: | files: |
fourthwall-store-embed.zip fourthwall-store-embed.zip
+51
View File
@@ -0,0 +1,51 @@
name: Update Plugin Version
on:
release:
types: [created, edited]
jobs:
update-version:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Get release tag
id: get_tag
run: echo "TAG=${GITEA_REF#refs/tags/}" >> $GITEA_ENV
- name: Update version in plugin file
run: |
# Replace version in main plugin file
sed -i "s/Version: .*/Version: ${{ env.TAG }}/" fw-store-embed.php
# Verify change
grep "Version:" fw-store-embed.php
- name: Commit changes
run: |
git config --local user.email "action@gitea.com"
git config --local user.name "Gitea Action"
git add fw-store-embed.php
git commit -m "Update version to ${{ env.TAG }}"
git push
- name: Create plugin zip
run: |
mkdir -p /tmp/fourthwall-store-embed
rsync -av --exclude=".git" --exclude=".gitea" --exclude="build" . /tmp/fourthwall-store-embed/
cd /tmp
zip -r $GITEA_WORK_DIR/fourthwall-store-embed.zip fourthwall-store-embed
- name: Upload zip to release
uses: actions/upload-release-asset@v1
env:
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
with:
upload_url: ${{ gitea.event.release.upload_url }}
asset_path: build/fourthwall-store-embed.zip
asset_name: fourthwall-store-embed.zip
asset_content_type: application/zip
+244
View File
@@ -0,0 +1,244 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Overview
This is a WordPress plugin that embeds Fourthwall store products into WordPress sites via shortcodes. The plugin fetches product data from Fourthwall stores using cURL, parses HTML to extract product information, and displays them with custom CSS styling.
## Core Architecture
### File Structure
- **fw-store-embed.php** - Main plugin entry point that loads libraries and registers CSS
- **libs/settings.php** - Admin settings page, cache management, and WordPress options API integration
- **libs/shortcode.php** - Core functionality for fetching, parsing, and displaying products
- **libs/self-update.php** - Auto-update system that checks Gitea releases API
- **css/fw-store-embed.css** - Styling for product tiles and admin interface
### Key Components
**HTTP Request Layer** (`fwembed_make_request` / `fwembed_make_requests` in shortcode.php):
- Centralized cURL-based HTTP client with browser-like headers to avoid 403 blocks
- Stale-while-revalidate transient caching (cache key: `fwembed_{md5(url)}`), with
a configurable freshness window - see **Caching Strategy** below
- `fwembed_make_requests()` fetches uncached URLs concurrently via `curl_multi`
- TLS certificate verification is always on and not configurable
- No cookie jar (see **Caching Strategy**)
**HTML Parsing** (shortcode.php):
- Uses DOMDocument/DOMXPath to extract product data from Fourthwall HTML
- Looks for `[data-testid="product"]` elements, falling back to `.product-tile`
- Extracts product tiles, images, descriptions, titles, and prices via CSS class selectors
- `loadHTML5()` wrapper ensures proper HTML5 parsing
**Single-product extraction is layered** (`fwembed_parse_single_product`)
Each field is resolved independently from the most stable source available, so a
markup change can only degrade the field it actually touched:
1. **JSON-LD** (`fwembed_product_from_jsonld`) - product pages embed a
`schema.org/Product` block. This is a published contract rather than styling,
so it survives theme changes. Source of truth for title, image, description
text, `sku`, `priceCurrency` and `availability`.
2. **Scraped markup** (`fwembed_product_from_html`) - fills gaps, and *wins* for
two fields on purpose: the price (it carries the store's own formatting, e.g.
"from $20") and the description (JSON-LD flattens it to plain text).
3. **OpenGraph `<meta>` tags** - last-resort title/image.
`fwembed_find_jsonld_product` handles the shapes publishers emit: bare object,
top-level list, and `@graph` / `itemListElement` / `mainEntity` wrappers.
`fwembed_jsonld_offer` takes the cheapest offer, since variants each get their
own `Offer` and `AggregateOffer` states it as `lowPrice`.
**Product discovery** (`fwembed_collect_product_urls`)
`sitemap.xml` is the full catalog; the store page only renders whichever
collection it features. Sitemap first, page scrape as fallback - `[fourthwall_random
source="auto|sitemap|page"]` overrides. `fwembed_read_sitemap` follows one level
of sitemap index, caps at 10 nested sitemaps, and rejects any `<loc>` whose host
differs from the configured store so a hostile sitemap cannot redirect fetches.
`robots.txt` allows `/products/*` and `/collections/all` for `*`; `/cart.js`,
`/checkout/*` and `/admin` are disallowed - do not fetch those.
**Surviving Fourthwall markup changes**
Fourthwall changes their theme markup without notice, and every past breakage has
been an over-specific selector. Three helpers exist to keep that from recurring —
use them for any new extraction:
- `fwembed_class_predicate($class)` - builds a padded `contains()` predicate.
Never write `@class="foo"`; class order and extra classes change between releases.
(The `[fourthwall_single]` breakage was `//h2[@class="product-info__title"]` after
the title tag became `<h1>` — match the class or `data-testid`, not the tag name.)
- `fwembed_query_first($xpath, [$q1, $q2, ...], $context)` - returns the first
matching query. Order selectors most-stable first: `data-testid` attributes,
then class names, then OpenGraph `<meta>` tags as a last resort.
- `fwembed_resolve_url($href, $page_url)` - resolves relative hrefs. Product links
are root-relative (`/products/foo`), so never concatenate onto the store URL:
that breaks on a trailing slash and on store URLs with a path (`/collections/all`).
Known-stable hooks as of the last verification: the JSON-LD `Product` block, the
OpenGraph meta tags, and `data-testid` values `product`, `product.name`,
`product.price`, `product.image`.
### Markup emitted for single products
Tiles carry `data-availability` (`InStock` / `OutOfStock`) and `data-sku` from
JSON-LD, and gain a `product-tile--sold-out` class when out of stock. The class
is deliberately unstyled - it is a hook for themes, not a built-in badge.
### Verifying against a live store
There is no test suite in-repo. To check parsing after a Fourthwall change, load
`libs/shortcode.php` under PHP CLI with stubs for `get_option`, `get_transient`,
`set_transient`, `delete_transient`, `add_shortcode`, `add_action`,
`shortcode_atts`, `wp_next_scheduled`, `wp_schedule_single_event`, `esc_url`,
`esc_attr`, `esc_html`, `wpautop`, plus the `MINUTE_IN_SECONDS` / `DAY_IN_SECONDS`
constants. Then assert each shortcode returns a non-empty title, price, image and
a well-formed absolute link. Worth covering:
- Store URL with and without a trailing slash, and as `/collections/all`.
- Degradation: strip the JSON-LD, then rename the CSS classes, then remove the
`og:` tags - the first two must still render, the last must render nothing.
- Freshness: entries are aged by rewriting the stored `expires_at`, **not** by
faking the clock - the plugin calls real `time()`, so a fake clock silently
tests nothing.
**Shortcodes**:
- `[fourthwall]` - Displays all store products
- `[fourthwall_single url="..." show_description="true"]` - Single product display
- `[fourthwall_random count="5" urls="..." store_url="..."]` - Random product selection
**Auto-Update System** (self-update.php):
- Hooks into WordPress plugin update transients (`site_transient_update_plugins`)
- Fetches latest release from `https://repo.anhonesthost.net/api/v1/repos/wp-plugins/fourth-wall-embed-wp/releases/latest`
- Provides changelog via `plugins_api` filter
- Version placeholder `{auto_update_value_on_deploy}` is replaced during CI/CD build
### Settings Storage
WordPress options API key: `fourthwall_settings_name`
- `fourth_url` - Default Fourthwall store URL
- `cache_ttl` - Freshness window in **minutes** (default 60); `fwembed_cache_ttl()`
converts to seconds and falls back to 60 minutes for values below 1
Sanitised on save by `fourthwall_settings::sanitize_settings()`.
The old `ssl_verify` option was removed - it was a development affordance, and
its checkbox never worked anyway (unchecked meant "key absent", which the read
treated as `true`). `fwembed_curl_handle()` now pins `CURLOPT_SSL_VERIFYPEER`
and `CURLOPT_SSL_VERIFYHOST`. Do not make certificate verification configurable
again; the plugin only ever talks to public HTTPS storefronts.
## Development Commands
### Testing the Plugin Locally
1. Symlink or copy to WordPress plugins directory:
```bash
ln -s $(pwd) /path/to/wordpress/wp-content/plugins/fourth-wall-embed-wp
```
2. Activate in WordPress admin at: **Plugins > Installed Plugins**
3. Configure at: **Settings > Fourthwall Store Embed**
### Cache Management
Clear transient cache from admin UI or manually:
```sql
DELETE FROM wp_options WHERE option_name LIKE '_transient_fwembed_%';
DELETE FROM wp_options WHERE option_name LIKE '_transient_timeout_fwembed_%';
```
## CI/CD Pipeline
### Gitea Actions Workflows
**`.gitea/workflows/release.yml`** - Runs on push to `main`:
1. Generates version tag from date/time: `YYYY.MM.DD-HHMM`
2. Creates release notes from commits since last tag
3. Updates version placeholder in `fw-store-embed.php`
4. Creates ZIP archive with plugin folder structure: `fourthwall-store-embed/`
5. Creates GitHub-style release with ZIP attachment
**`.gitea/workflows/update-version.yml`** - Version update automation
### Release Process
Releases are automatic on merge/push to `main`. The ZIP file structure must match WordPress conventions:
```
fourthwall-store-embed.zip
└── fourthwall-store-embed/
├── fw-store-embed.php
├── libs/
├── css/
└── README.md
```
## Important Implementation Notes
### DOMDocument HTML Parsing
- Always use `libxml_use_internal_errors(true)` to suppress HTML5 parsing warnings
- Clear errors with `libxml_clear_errors()` after parsing
- Set `$dom->documentURI` for proper relative URL resolution
### Caching Strategy
Stale-while-revalidate. A cache entry carries its own `expires_at`, and the
transient's own expiry is the much longer *retention* window:
- **Fresh** (`now < expires_at`) - served directly.
- **Stale** (past `expires_at`, still stored) - served **immediately**, and a
WP-Cron job is queued to refresh it. Expiry therefore never lands the cost of
a network round trip on a visitor.
- **Absent** (past retention) - fetched synchronously. This is the only path
that blocks a page render.
Consequences to keep in mind when changing this:
- Never shorten transient expiry to the TTL. `fwembed_cache_retention()`
(>= 24h) is what keeps stale content available to serve; TTL only controls
when a refresh is triggered.
- `fwembed_schedule_refresh()` dedupes via `wp_next_scheduled()`, so a thousand
visitors hitting one stale entry queue a single job. `fwembed_do_refresh()`
additionally takes a `_lock` transient so two overlapping cron runs cannot
both fetch.
- Entries cached by older versions have no `expires_at`. They are read as stale,
served once, then rewritten in the new format - do not "clean up" that branch.
- Error responses (non-200) are still never cached.
- If `DISABLE_WP_CRON` is set with no real cron running, refreshes never fire and
content is served stale until retention lapses, then refetched synchronously.
`fwembed_make_requests()` fetches everything uncached in one `curl_multi` batch
(8 concurrent, chunked). `[fourthwall_random]` uses it so a cold cache costs one
round trip instead of N sequential ones. Prefer it over looping
`fwembed_make_request()` whenever the URL set is known up front.
**No cookie jar.** The old code shared `/tmp/cookies.txt` across every request
and every site on the host. It broke `curl_multi` (concurrent writes to one jar)
and was unnecessary - the storefront, product pages and sitemap all return 200
without cookies. Do not reintroduce a shared jar; give each handle its own file
if session state ever becomes genuinely necessary.
Cache keys use `md5($url)`; locks are `<key>_lock`, so the admin "Clear Cache"
`LIKE '_transient_fwembed_%'` sweep clears both.
### Security Considerations
- All user input sanitized via `esc_attr()`, `esc_html()`, `htmlspecialchars()`
- Nonce verification for cache clearing: `wp_verify_nonce()`
- Capability checks: `current_user_can('manage_options')`
- TLS certificate verification is pinned on and cannot be disabled
### Version Management
- Main plugin file contains placeholder: `Version: {auto_update_value_on_deploy}`
- CI/CD replaces this during build with actual version
- Update checker compares version strings exactly (not semantic versioning)
## WordPress Compatibility
- **Requires**: WordPress 6.0+, PHP 7.4+
- **Tested up to**: WordPress 6.8
- **Required PHP extensions**: cURL, libxml, DOM
+143 -5
View File
@@ -1,12 +1,150 @@
# fourth-wall-embed-wp # Fourthwall Store Embed WordPress Plugin
A WordPress Plugin to embed a fourth wall embed. A WordPress plugin to embed Fourthwall Store products in your WordPress site.
## Shortcodes
### Display All Store Products
```
[fourthwall]
```
Displays all products from your Fourthwall store.
### Display Single Product
```
[fourthwall_single url="https://your-store.com/product-url" show_description="true"]
```
Displays a single product with optional description.
**Parameters:**
- `url` (required): The full URL of the product
- `show_description` (optional): Set to "true" to show product description
### Display Random Products
```
[fourthwall_random count="5"]
```
Displays a random selection of products from your store.
**Parameters:**
- `count` (optional): Number of products to display (default: 3)
- `urls` (optional): Comma-separated list of specific product URLs to randomize from
- `store_url` (optional): Custom store URL (uses default from settings if not provided)
- `source` (optional): Where to discover products - `auto` (default), `sitemap`, or `page`
**About `source`:** products are discovered from your store's `sitemap.xml`, which
lists your entire catalog. The older behaviour scraped the store page, which only
sees the collection that page happens to feature - so if your homepage shows 3
featured items, `count="10"` could never return more than 3. `auto` uses the
sitemap and falls back to scraping the page if the sitemap is unavailable; use
`page` to force the old behaviour.
#### Examples:
**Random 5 products from all store products:**
```
[fourthwall_random count="5"]
```
**Random 3 products from specific URLs:**
```
[fourthwall_random count="3" urls="https://store.com/product1,https://store.com/product2,https://store.com/product3"]
```
**Random 2 products from a different store:**
```
[fourthwall_random count="2" store_url="https://different-store.com"]
```
## Installation
1. Upload the plugin files to `/wp-content/plugins/fourth-wall-embed-wp/`
2. Activate the plugin through the 'Plugins' menu in WordPress
3. Go to Settings > Fourthwall Store Embed to configure your store URL
## Configuration
In the WordPress admin, go to Settings > Fourthwall Store Embed to set your Fourthwall store URL.
**Cache Lifetime** (default: 60 minutes) controls how often store content is
refreshed. Content is cached and served instantly; once it passes the lifetime it
is *still* served immediately while a background refresh runs, so raising or
lowering this never slows a page down. Use **Clear Cache** to force an immediate
refresh after changing products in Fourthwall.
## Features
- Caches requests for better performance
- Responsive design
- Error handling for failed requests
- Random product selection
- Support for multiple store URLs
### How to use the plugin ### How to use the plugin
* Download the latest release from [releases](https://repo.anhonesthost.net/CyberCoveLLC/fourth-wall-embed-wp/releases) * Download the latest release from [releases](https://repo.anhonesthost.net/CyberCoveLLC/fourth-wall-embed-wp/releases)
* Upload the Plugin to WordPress * Upload the Plugin to WordPress
* In the WordPress Dashboard, Navigate to the Fourthwall settings page, and paste your store url. * In the WordPress Dashboard, Navigate to the Fourthwall settings page, and paste your store URL.
* On the page you want have your store displayed, add the shortcode ```[fourthwall]```
This is an early release of the plugin, and if you can think of things that could be improved or find any bugs, please open an issue on the repository. #### Configuration Options
**Store URL**: Enter your Fourthwall store URL (e.g., `https://your-store.fourthwall.com`)
**Cache Lifetime**: How long store content stays fresh, in minutes (default: 60)
**Cache Management**:
- Content is cached and served instantly, then refreshed in the background
- Use the "Clear Cache" button if products are not updating
#### Display your entire store
To display your entire Fourthwall store on a page, use the shortcode:
```[fourthwall]```
#### Display a single product
To display an individual product from your Fourthwall store, use the shortcode with the product URL:
```[fourthwall_single url="https://your-store.fourthwall.com/products/product-name"]```
You can also display the product description by setting the `show_description` attribute to "true":
```[fourthwall_single url="https://your-store.fourthwall.com/products/product-name" show_description="true"]```
### Features
- **Smart Caching**: Stale content is served instantly while it refreshes in the background, so cache expiry never slows a page down
- **Parallel Fetching**: Uncached products are fetched concurrently rather than one at a time
- **Error Handling**: Graceful fallbacks and clear error messages
- **Admin Interface**: User-friendly settings page with clear instructions
- **Cache Management**: Manual cache clearing for troubleshooting
- **Responsive Design**: CSS styling for both frontend and admin areas
### Requirements
* WordPress 6.0 or higher
* PHP 7.4 or higher
* Active Fourthwall store
* cURL extension enabled
### Performance Notes
- Content is cached to reduce requests to Fourthwall (default: 60 minutes)
- Once the cache lifetime passes, content is still served instantly while a
background refresh runs, so visitors never wait on the network
- Manual cache clearing available in admin settings
### Troubleshooting
**Products not updating?**
- Clear the cache using the "Clear Cache" button in admin settings
- Check your store URL is correct
- Lower the Cache Lifetime if you need changes to appear sooner
**Background refreshes not happening?**
- Refreshes run via WP-Cron. If `DISABLE_WP_CRON` is set with no system cron
configured, content is served stale until it is refetched on demand.
**403 Forbidden errors?**
- Fourthwall may be blocking automated requests
- Check if your store is publicly accessible
- Contact Fourthwall support if issues persist
This plugin allows you to integrate your Fourthwall store directly into your WordPress site, either showing your complete product collection or featuring specific products individually.
If you can think of things that could be improved or find any bugs, please open an issue on the repository.
+69 -5
View File
@@ -8,17 +8,29 @@
} }
.product-tile { .product-tile {
align-content: center; align-content: flex-start;
vertical-align: bottom; vertical-align: top;
background: white; display:inline-flex;
display:inline-block; flex-direction: column;
width:225px; width:225px;
margin:15px 15px 15px 15px; margin:15px 15px 15px 15px;
background: inherit;
}
.product-tile .product-link {
display: flex;
flex-direction: column;
height: 100%;
text-decoration: none;
color: inherit;
} }
.product-tile img { .product-tile img {
max-height: 350px; width: 100%;
height: 300px;
object-fit: cover;
object-position: center;
display: block;
} }
.image__badges { .image__badges {
@@ -39,4 +51,56 @@
.tile__heading { .tile__heading {
font-size: 0.88em; /* Reduced font size for product titles */ font-size: 0.88em; /* Reduced font size for product titles */
font-weight: bold; font-weight: bold;
margin: 0.5em 0;
color: inherit;
font-family: inherit;
}
.tile__description {
flex: 1;
display: flex;
flex-direction: column;
padding: 10px 5px;
color: inherit;
font-family: inherit;
}
.tile__prices {
margin-top: auto;
padding-top: 0.5em;
}
.tile__price {
color: inherit;
font-family: inherit;
}
/* Admin page styles */
.shortcode-info {
background: #fff;
border: 1px solid #ccd0d4;
border-radius: 4px;
padding: 20px;
margin-top: 20px;
}
.shortcode-info h3 {
margin-top: 0;
color: #23282d;
}
.shortcode-info code {
background: #f1f1f1;
padding: 2px 6px;
border-radius: 3px;
font-family: 'Courier New', monospace;
}
.shortcode-info p {
margin: 10px 0;
}
.shortcode-info em {
color: #666;
font-style: italic;
} }
+4 -2
View File
@@ -1,14 +1,15 @@
<?php <?php
/** /**
* Plugin Name: Fourthwall Store Embed * Plugin Name: Fourthwall Store Embed
* Plugin URL: https://cybercove.io/ * Plugin URL: https://darksideofperfection.com/
* Description: Embed Fourthwall Store in WordPress * Description: Embed Fourthwall Store in WordPress
* Version: 1.0.0. * Version: {auto_update_value_on_deploy}
* Author: Joshua Knapp * Author: Joshua Knapp
* Text Domain: fourthwall_text_domain * Text Domain: fourthwall_text_domain
* *
*/ */
include("libs/self-update.php");
include("libs/settings.php"); include("libs/settings.php");
include("libs/shortcode.php"); include("libs/shortcode.php");
function set_fw_store_embed_css() { function set_fw_store_embed_css() {
@@ -16,3 +17,4 @@ function set_fw_store_embed_css() {
} }
add_action( 'wp_enqueue_scripts', 'set_fw_store_embed_css' ); add_action( 'wp_enqueue_scripts', 'set_fw_store_embed_css' );
add_action( 'admin_enqueue_scripts', 'set_fw_store_embed_css' );
+177
View File
@@ -0,0 +1,177 @@
<?php
/**
* Plugin update functionality
*/
// Don't execute directly
if (!defined('ABSPATH')) {
exit;
}
function fwembed_check_for_updates() {
// IMPORTANT: Get the main plugin file path correctly
$main_plugin_file = dirname(dirname(__FILE__)) . '/fw-store-embed.php';
$plugin_slug = plugin_basename($main_plugin_file);
// Get plugin data from main file
if (!function_exists('get_plugin_data')) {
require_once(ABSPATH . 'wp-admin/includes/plugin.php');
}
// Get plugin data from main plugin file (not this file)
$plugin_data = get_plugin_data($main_plugin_file);
$current_version = !empty($plugin_data['Version']) ? $plugin_data['Version'] : '0.0.1';
// Debug data
$debug_data = [
'current_version' => $current_version,
'main_plugin_file' => $main_plugin_file,
'plugin_slug' => $plugin_slug,
'time' => current_time('mysql')
];
// URL to your Gitea releases API
$gitea_api_url = 'https://repo.anhonesthost.net/api/v1/repos/wp-plugins/fourth-wall-embed-wp/releases/latest';
// Use cURL to fetch release data
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $gitea_api_url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Accept: application/json']);
curl_setopt($ch, CURLOPT_USERAGENT, 'WordPress/Fourth-Wall-Plugin-Updater');
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
curl_setopt($ch, CURLOPT_TIMEOUT, 10);
$response = curl_exec($ch);
curl_close($ch);
if ($response) {
$release_info = json_decode($response);
// Get latest version from tag name
$latest_version = $release_info->tag_name;
// Simple version comparison - we want any update to trigger if version is different
if ($current_version != $latest_version) {
// Get the download URL for the ZIP
$download_url = null;
if (isset($release_info->assets) && is_array($release_info->assets)) {
foreach ($release_info->assets as $asset) {
if (isset($asset->name) && strpos($asset->name, '.zip') !== false) {
$download_url = $asset->browser_download_url;
break;
}
}
}
// Hook into WordPress update system
add_filter('site_transient_update_plugins', function($transient) use ($plugin_slug, $current_version, $latest_version, $download_url) {
// Initialize if needed
if (!is_object($transient)) {
$transient = new stdClass();
}
if (empty($transient->checked)) {
$transient->checked = [];
}
// Add our plugin to the checked list
$transient->checked[$plugin_slug] = $current_version;
if (empty($transient->response)) {
$transient->response = [];
}
// Add to the update list
$transient->response[$plugin_slug] = (object) [
'id' => 'fourthwall-store-embed',
'slug' => 'fourthwall-store-embed',
'plugin' => $plugin_slug,
'new_version' => $latest_version,
'url' => 'https://repo.anhonesthost.net/wp-plugins/fourth-wall-embed-wp',
'package' => $download_url,
'icons' => [],
'banners' => [],
'tested' => '6.8',
'requires' => '6.2',
'compatibility' => new stdClass(),
];
return $transient;
});
}
}
}
add_action('admin_init', 'fwembed_check_for_updates');
// Add plugin information for the details popup/changelog
add_filter('plugins_api', function($result, $action, $args) {
// Only handle requests for our plugin
$plugin_slug = 'fourthwall-store-embed'; // The slug is directory name
if ($action !== 'plugin_information' || !isset($args->slug) || $args->slug !== $plugin_slug) {
return $result;
}
// Get latest release info from Gitea API
$gitea_api_url = 'https://repo.anhonesthost.net/api/v1/repos/wp-plugins/fourth-wall-embed-wp/releases/latest';
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $gitea_api_url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Accept: application/json']);
curl_setopt($ch, CURLOPT_USERAGENT, 'WordPress/Fourth-Wall-Plugin-Updater');
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
curl_setopt($ch, CURLOPT_TIMEOUT, 10);
$response = curl_exec($ch);
curl_close($ch);
if (!$response) {
return $result; // Keep default result if API call fails
}
$release_info = json_decode($response);
// Build plugin info object
$plugin_info = new stdClass();
$plugin_info->name = 'Fourthwall Store Embed';
$plugin_info->slug = $plugin_slug;
$plugin_info->version = $release_info->tag_name;
$plugin_info->author = '<a href="https://cybercove.io/">Joshua Knapp</a>';
$plugin_info->homepage = 'https://repo.anhonesthost.net/wp-plugins/fourth-wall-embed-wp';
$plugin_info->requires = '5.0';
$plugin_info->tested = '6.8';
$plugin_info->downloaded = 10;
$plugin_info->last_updated = $release_info->published_at;
// Format the release notes
$changelog = !empty($release_info->body) ? $release_info->body : 'No changelog provided for this release.';
// Handle empty changelog
if (empty(trim($changelog))) {
$changelog = "Version " . $release_info->tag_name . "\n\n" .
"Released on " . date('F j, Y', strtotime($release_info->published_at)) . "\n\n" .
"* Updated plugin files";
}
// Format sections for display
$plugin_info->sections = [
'description' => '<p>Embed Fourthwall Store in WordPress.</p>',
'changelog' => '<pre>' . esc_html($changelog) . '</pre>',
'installation' => '<p>Upload the plugin to your WordPress site and activate it.</p>'
];
// Download link
$download_url = null;
if (isset($release_info->assets) && is_array($release_info->assets)) {
foreach ($release_info->assets as $asset) {
if (isset($asset->name) && strpos($asset->name, '.zip') !== false) {
$download_url = $asset->browser_download_url;
break;
}
}
}
$plugin_info->download_link = $download_url;
return $plugin_info;
}, 10, 3);
+98 -2
View File
@@ -27,7 +27,8 @@ class fourthwall_settings {
register_setting( register_setting(
'fourthwall_settings_group', 'fourthwall_settings_group',
'fourthwall_settings_name' 'fourthwall_settings_name',
array( 'sanitize_callback' => array( $this, 'sanitize_settings' ) )
); );
add_settings_section( add_settings_section(
@@ -45,6 +46,36 @@ class fourthwall_settings {
'fourthwall_settings_name_section' 'fourthwall_settings_name_section'
); );
add_settings_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() { public function fourthwall_page_layout() {
@@ -54,6 +85,12 @@ class fourthwall_settings {
wp_die( esc_html__( 'You do not have sufficient permissions to access this page.', 'fourthwall_text_domain' ) ); wp_die( esc_html__( 'You do not have sufficient permissions to access this page.', 'fourthwall_text_domain' ) );
} }
// Handle cache clearing
if (isset($_POST['clear_cache']) && wp_verify_nonce($_POST['_wpnonce'], 'clear_fwembed_cache')) {
$this->clear_fwembed_cache();
echo '<div class="notice notice-success"><p>' . __('Cache cleared successfully!', 'fourthwall_text_domain') . '</p></div>';
}
// Admin Page Layout // Admin Page Layout
echo '<div class="wrap">' . "\n"; echo '<div class="wrap">' . "\n";
echo ' <h1>' . get_admin_page_title() . '</h1>' . "\n"; echo ' <h1>' . get_admin_page_title() . '</h1>' . "\n";
@@ -64,7 +101,26 @@ class fourthwall_settings {
submit_button(); submit_button();
echo ' </form>' . "\n"; echo ' </form>' . "\n";
echo ' <div><p>To use the fourthwall Embed, use the shortcode [fourthwall]</p></div>';
// Cache clearing form
echo ' <form method="post" style="margin-top: 20px;">' . "\n";
echo ' ' . wp_nonce_field('clear_fwembed_cache', '_wpnonce', true, false) . "\n";
echo ' <input type="submit" name="clear_cache" class="button button-secondary" value="' . __('Clear Cache', 'fourthwall_text_domain') . '">' . "\n";
echo ' <p class="description">' . __('Clear the cached Fourthwall content. Use this if products are not updating.', 'fourthwall_text_domain') . '</p>' . "\n";
echo ' </form>' . "\n";
echo ' <div class="shortcode-info">' . "\n";
echo ' <h3>' . __( 'Shortcode Usage', 'fourthwall_text_domain' ) . '</h3>' . "\n";
echo ' <p><strong>' . __( 'Display entire store:', 'fourthwall_text_domain' ) . '</strong> <code>[fourthwall]</code></p>' . "\n";
echo ' <p><strong>' . __( 'Display single product:', 'fourthwall_text_domain' ) . '</strong> <code>[fourthwall_single url="https://your-store.fourthwall.com/products/product-name"]</code></p>' . "\n";
echo ' <p><strong>' . __( 'With description:', 'fourthwall_text_domain' ) . '</strong> <code>[fourthwall_single url="https://your-store.fourthwall.com/products/product-name" show_description="true"]</code></p>' . "\n";
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><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"; echo '</div>' . "\n";
} }
@@ -83,6 +139,46 @@ class fourthwall_settings {
} }
function render_cache_ttl_field() {
// Retrieve data from the database.
$options = get_option( 'fourthwall_settings_name' );
// Set default value.
$value = isset( $options['cache_ttl'] ) ? intval( $options['cache_ttl'] ) : 60;
if ( $value < 1 ) {
$value = 60;
}
// Field output.
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>';
}
/**
* Clear all Fourthwall embed cache
*/
private function clear_fwembed_cache() {
global $wpdb;
// Delete all transients that start with 'fwembed_'
$wpdb->query(
$wpdb->prepare(
"DELETE FROM {$wpdb->options} WHERE option_name LIKE %s",
'_transient_fwembed_%'
)
);
// Also delete transient timeouts
$wpdb->query(
$wpdb->prepare(
"DELETE FROM {$wpdb->options} WHERE option_name LIKE %s",
'_transient_timeout_fwembed_%'
)
);
}
} }
new fourthwall_settings; new fourthwall_settings;
+1109 -37
View File
File diff suppressed because it is too large Load Diff