Skill: imjohnathanblog-spec/build-amazon-affiliate-plugin (version 1.0.0)

--- name: build-amazon-affiliate-plugin description: Create a production-ready WordPress plugin that displays Amazon product ads when users insert Amazon affiliate links. Use when asked to build an Amazon Associates/affiliate WordPress plugin with features like: (1) Detecting Amazon links in post content, (2) Extracting ASIN from URLs, (3) Generating ad units with product info, (4) Admin settings page for affiliate tag and display options, (5) Caching for performance, (6) Gutenberg block support. Follows WordPress best practices and Amazon Associates Program Policies. --- # Build Amazon Affiliate Ad Plugin ## Quick Start 1. Read [PLUGIN_SPEC.md](references/PLUGIN_SPEC.md) for detailed requirements 2. Read [AMAZON_API.md](references/AMAZON_API.md) for PA-API integration 3. Generate the plugin code following the specifications ## Plugin Requirements Summary - **Name**: Ad Symbiont - **Features**: - Detect Amazon links (amazon.com, amazon.ca, amazon.co.uk, amzn.to) - Extract ASIN from URLs - Admin settings: Affiliate tag, display mode (replace/append), styling toggle - Fallback to simple HTML ad if no PA-API credentials - Caching for ad units (transients) - Gutenberg block support - WordPress 6.x compatible - Secure: sanitize/escape all inputs/outputs ## Output Generate: 1. Complete plugin PHP file 2. CSS/JS assets if needed 3. Installation instructions 4. Expansion options documentation Save all files to: `/home/imjohnathan/.openclaw/workspace/skills/build-amazon-affiliate-plugin/`

path: assets/plugin/assets/blocks/build/index.js
content: (()=>{"use strict";const e=window.React,a=window.wp.blocks,t=window.wp.components,n=window.wp.blockEditor,l=window.wp.i18n;(0,a.registerBlockType)("ad-symbiont/amazon-ad",{title:(0,l.__)("Amazon Ad","ad-symbiont"),description:(0,l.__)("Display an Amazon product ad using ASIN","ad-symbiont"),category:"widgets",icon:"shopping-cart",keywords:[(0,l.__)("amazon","ad-symbiont"),(0,l.__)("affiliate","ad-symbiont"),(0,l.__)("ad","ad-symbiont")],attributes:{asin:{type:"string",default:""},displayMode:{type:"string",default:"append"},showImage:{type:"boolean",default:!0},showPrice:{type:"boolean",default:!0}},edit:({attributes:a,setAttributes:o})=>{const i=(0,n.useBlockProps)();return(0,e.createElement)("div",{...i},(0,e.createElement)(n.InspectorControls,null,(0,e.createElement)(t.PanelBody,{title:(0,l.__)("Amazon Ad Settings","ad-symbiont"),initialOpen:!0},(0,e.createElement)(t.PanelRow,null,(0,e.createElement)(t.TextControl,{label:(0,l.__)("ASIN","ad-symbiont"),value:a.asin,onChange:e=>o({asin:e.toUpperCase()}),placeholder:(0,l.__)("e.g., B08N5WRWNW","ad-symbiont"),help:(0,l.__)("Enter the 10-character Amazon ASIN","ad-symbiont")})),(0,e.createElement)(t.PanelRow,null,(0,e.createElement)(t.SelectControl,{label:(0,l.__)("Display Mode","ad-symbiont"),value:a.displayMode,options:[{label:(0,l.__)("Append below link","ad-symbiont"),value:"append"},{label:(0,l.__)("Replace with ad","ad-symbiont"),value:"replace"}],onChange:e=>o({displayMode:e})})),(0,e.createElement)(t.PanelRow,null,(0,e.createElement)(t.ToggleControl,{label:(0,l.__)("Show Product Image","ad-symbiont"),checked:a.showImage,onChange:e=>o({showImage:e})})),(0,e.createElement)(t.PanelRow,null,(0,e.createElement)(t.ToggleControl,{label:(0,l.__)("Show Price","ad-symbiont"),checked:a.showPrice,onChange:e=>o({showPrice:e})})))),(0,e.createElement)("div",{className:"ad-symbiont-block-placeholder"},a.asin?(0,e.createElement)("div",{className:"ad-symbiont-preview"},(0,e.createElement)("div",{className:"ad-symbiont-preview-icon"},"🛒"),(0,e.createElement)("p",null,(0,e.createElement)("strong",null,"Amazon Product Ad")),(0,e.createElement)("p",{className:"ad-symbiont-asin"},"ASIN: ",a.asin)):(0,e.createElement)("div",{className:"ad-symbiont-placeholder-text"},(0,e.createElement)("p",null,(0,e.createElement)("strong",null,(0,l.__)("Amazon Ad","ad-symbiont"))),(0,e.createElement)("p",null,(0,l.__)("Enter an ASIN to display an Amazon product ad","ad-symbiont")))))},save:()=>null})})();

path: assets/plugin/assets/blocks/package.json
content: {
  "name": "ad-symbiont-blocks",
  "version": "1.0.0",
  "description": "Gutenberg blocks for Ad Symbiont plugin",
  "main": "src/index.js",
  "scripts": {
    "build": "wp-scripts build",
    "start": "wp-scripts start"
  },
  "dependencies": {
    "@wordpress/block-editor": "^12.0.0",
    "@wordpress/blocks": "^12.0.0",
    "@wordpress/components": "^25.0.0",
    "@wordpress/element": "^3.0.0",
    "@wordpress/i18n": "^4.0.0"
  },
  "devDependencies": {
    "@wordpress/scripts": "^26.0.0"
  }
}


path: assets/plugin/assets/blocks/src/index.js
content: /**
 * Ad Symbiont Amazon Ad Block
 *
 * @package Ad_Symbiont
 */

import { registerBlockType } from '@wordpress/blocks';
import { 
    TextControl, 
    SelectControl, 
    ToggleControl,
    PanelBody,
    PanelRow
} from '@wordpress/components';
import { 
    InspectorControls,
    useBlockProps
} from '@wordpress/block-editor';
import { __ } from '@wordpress/i18n';

registerBlockType( 'ad-symbiont/amazon-ad', {
    title: __( 'Amazon Ad', 'ad-symbiont' ),
    description: __( 'Display an Amazon product ad using ASIN', 'ad-symbiont' ),
    category: 'widgets',
    icon: 'shopping-cart',
    keywords: [ __( 'amazon', 'ad-symbiont' ), __( 'affiliate', 'ad-symbiont' ), __( 'ad', 'ad-symbiont' ) ],
    
    attributes: {
        asin: {
            type: 'string',
            default: '',
        },
        displayMode: {
            type: 'string',
            default: 'append',
        },
        showImage: {
            type: 'boolean',
            default: true,
        },
        showPrice: {
            type: 'boolean',
            default: true,
        },
    },
    
    edit: ( { attributes, setAttributes } ) => {
        const blockProps = useBlockProps();
        
        return (
            <div { ...blockProps }>
                <InspectorControls>
                    <PanelBody title={ __( 'Amazon Ad Settings', 'ad-symbiont' ) } initialOpen={ true }>
                        <PanelRow>
                            <TextControl
                                label={ __( 'ASIN', 'ad-symbiont' ) }
                                value={ attributes.asin }
                                onChange={ ( value ) => setAttributes( { asin: value.toUpperCase() } ) }
                                placeholder={ __( 'e.g., B08N5WRWNW', 'ad-symbiont' ) }
                                help={ __( 'Enter the 10-character Amazon ASIN', 'ad-symbiont' ) }
                            />
                        </PanelRow>
                        <PanelRow>
                            <SelectControl
                                label={ __( 'Display Mode', 'ad-symbiont' ) }
                                value={ attributes.displayMode }
                                options={ [
                                    { label: __( 'Append below link', 'ad-symbiont' ), value: 'append' },
                                    { label: __( 'Replace with ad', 'ad-symbiont' ), value: 'replace' },
                                ] }
                                onChange={ ( value ) => setAttributes( { displayMode: value } ) }
                            />
                        </PanelRow>
                        <PanelRow>
                            <ToggleControl
                                label={ __( 'Show Product Image', 'ad-symbiont' ) }
                                checked={ attributes.showImage }
                                onChange={ ( value ) => setAttributes( { showImage: value } ) }
                            />
                        </PanelRow>
                        <PanelRow>
                            <ToggleControl
                                label={ __( 'Show Price', 'ad-symbiont' ) }
                                checked={ attributes.showPrice }
                                onChange={ ( value ) => setAttributes( { showPrice: value } ) }
                            />
                        </PanelRow>
                    </PanelBody>
                </InspectorControls>
                
                <div className="ad-symbiont-block-placeholder">
                    { attributes.asin ? (
                        <div className="ad-symbiont-preview">
                            <div className="ad-symbiont-preview-icon">🛒</div>
                            <p>
                                <strong>Amazon Product Ad</strong>
                            </p>
                            <p className="ad-symbiont-asin">ASIN: { attributes.asin }</p>
                        </div>
                    ) : (
                        <div className="ad-symbiont-placeholder-text">
                            <p><strong>{ __( 'Amazon Ad', 'ad-symbiont' ) }</strong></p>
                            <p>{ __( 'Enter an ASIN to display an Amazon product ad', 'ad-symbiont' ) }</p>
                        </div>
                    ) }
                </div>
            </div>
        );
    },
    
    save: () => {
        // Dynamic block - render callback handles output.
        return null;
    },
} );


path: assets/plugin/assets/css/admin.css
content: /**
 * Admin CSS for Ad Symbiont
 */

.ad-symbiont-admin-wrap {
    max-width: 800px;
}

.ad-symbiont-settings-table {
    width: 100%;
    margin-bottom: 20px;
}

.ad-symbiont-settings-table th {
    width: 200px;
    vertical-align: top;
    padding: 10px 0;
}

.ad-symbiont-settings-table td {
    padding: 10px 0;
}

.ad-symbiont-clear-cache-msg {
    margin-left: 10px;
    color: #28a745;
}

.ad-symbiont-success {
    color: #28a745;
}

.ad-symbiont-error {
    color: #dc3545;
}

/* Settings form styling */
#ad-symbiont_settings-affiliate_tag,
#ad_symbiont_settings_paapi_access_key,
#ad_symbiont_settings_paapi_secret_key,
#ad_symbiont_settings_paapi_tag {
    max-width: 400px;
}

#ad_symbiont_settings_cache_duration {
    width: 80px;
}


path: assets/plugin/assets/css/block-editor.css
content: /**
 * Block Editor CSS for Ad Symbiont
 */

.ad-symbiont-block-placeholder {
    padding: 20px;
    background: #f5f5f5;
    border: 2px dashed #ccc;
    border-radius: 4px;
    text-align: center;
}

.ad-symbiont-preview {
    padding: 15px;
    background: #fff;
    border-radius: 4px;
}

.ad-symbiont-preview-icon {
    font-size: 32px;
    margin-bottom: 10px;
}

.ad-symbiont-asin {
    font-family: monospace;
    background: #eee;
    padding: 4px 8px;
    border-radius: 4px;
    display: inline-block;
}

.ad-symbiont-placeholder-text p {
    margin: 5px 0;
    color: #666;
}

/* Block inspector panel */
.ad-symbiont-inspector-panel {
    padding: 10px;
}

.components-panel__body.ad-symbiont-panel .components-panel__body-title {
    background: #f0f0f0;
}


path: assets/plugin/assets/css/block.css
content: /**
 * Frontend Block CSS for Ad Symbiont
 */

/* Block-specific styles (additional to main ad styles) */
.ad-symbiont-ad.block {
    /* Block-specific overrides if needed */
}

.ad-symbiont-ad.block .ad-symbiont-image {
    width: 150px;
}

.ad-symbiont-ad.block .ad-symbiont-placeholder {
    width: 150px;
    height: 200px;
}


path: assets/plugin/assets/js/admin.js
content: /**
 * Admin JavaScript for Ad Symbiont
 */

jQuery(document).ready(function($) {
    // Clear cache button
    $('#ad-symbiont-clear-cache').on('click', function() {
        var $button = $(this);
        var $msg = $('#ad-symbiont-clear-cache-msg');
        
        $button.prop('disabled', true).text('Clearing...');
        
        $.ajax({
            url: adSymbiont.ajaxUrl,
            type: 'POST',
            data: {
                action: 'ad_symbiont_clear_cache',
                nonce: adSymbiont.nonce
            },
            success: function(response) {
                if (response.success) {
                    $msg.text('Cache cleared! (' + response.data.deleted + ' items)').addClass('ad-symbiont-success');
                    setTimeout(function() {
                        $msg.text('').removeClass('ad-symbiont-success');
                    }, 3000);
                } else {
                    $msg.text('Error clearing cache').addClass('ad-symbiont-error');
                }
            },
            error: function() {
                $msg.text('Error clearing cache').addClass('ad-symbiont-error');
            },
            complete: function() {
                $button.prop('disabled', false).text('Clear Cache Now');
            }
        });
    });
});


path: assets/plugin/readme.txt
content: === Ad Symbiont ===
Contributors: yourname
Donate link: https://example.com/donate
Tags: amazon, affiliate, ads, ecommerce, widget
Requires at least: 5.8
Tested up to: 6.4
Stable tag: 1.0.0
License: GPL v2 or later
License URI: https://www.gnu.org/licenses/gpl-2.0.html

Automatically displays Amazon product ads when Amazon affiliate links are detected in post content.

== Description ==

Ad Symbiont automatically detects Amazon affiliate links in your WordPress posts and pages, and displays attractive product ad units. The plugin extracts the ASIN from Amazon URLs and generates ads with product images, titles, and prices.

### Features

*   **Automatic Detection**: Detects Amazon links from amazon.com, amazon.co.uk, amazon.de, and more
*   **Flexible Display**: Choose to replace links with ads or append ads below links
*   **PA-API Integration**: Optional Product Advertising API for real product data
*   **Caching**: Built-in caching improves performance and reduces API calls
*   **Gutenberg Block**: Easily insert Amazon ads with the block editor
*   **Shortcode**: Manual ad placement with `[ad_symbiont asin="B08N5WRWNW"]`
*   **Styling Options**: Enable/disable default styling or use your own CSS
*   **Secure**: Proper sanitization and escaping throughout

### Supported Amazon Domains

*   amazon.com, amazon.ca, amazon.co.uk
*   amazon.de, amazon.fr, amazon.es, amazon.it
*   amazon.co.jp, amazon.com.au
*   amzn.to (short URLs)

== Installation ==

1. Upload the `ad-symbiont` folder to the `/wp-content/plugins/` directory
2. Activate the plugin through the 'Plugins' menu in WordPress
3. Go to Settings > Ad Symbiont to configure
4. Enter your Amazon Affiliate Tag
5. Optionally configure PA-API credentials for enhanced ads

== Frequently Asked Questions ==

= Do I need an Amazon Affiliate account? =

Yes, you need an Amazon Associates account to earn commissions.

= Does this work with Amazon short URLs (amzn.to)? =

Yes, the plugin detects both full Amazon URLs and short amzn.to links.

= What if PA-API credentials are not provided? =

The plugin will display a fallback ad with your affiliate link. Users will still see a "Buy on Amazon" button.

= Can I manually insert ads? =

Yes, use the Gutenberg block or shortcode: `[ad_symbiont asin="B08N5WRWNW"]`

= How do I find a product ASIN? =

ASIN is the 10-character ID in Amazon product URLs. For example, in `amazon.com/dp/B08N5WRWNW`, the ASIN is `B08N5WRWNW`.

= Does this support multiple Amazon domains? =

Yes, supports amazon.com, .co.uk, .de, .fr, .es, .it, .co.jp, .ca, .com.au, and amzn.to short URLs.

== Screenshots ==

1. Admin settings page
2. Amazon ad display example
3. Gutenberg block

== Changelog ==

= 1.0.0 =
* Initial release
* Automatic Amazon link detection
* Admin settings page
* PA-API integration
* Gutenberg block support
* Shortcode support
* Caching system

== Upgrade Notice ==

= 1.0.0 =
Initial release of Ad Symbiont.


path: references/AMAZON_API.md
content: # Amazon PA-API Integration Guide

## Overview

Product Advertising API (PA-API) 5.0 provides programmatic access to Amazon product catalog data.

## Requirements

- Amazon Associate account in good standing
- PA-API access (must have made 3 qualifying sales in 180 days)
- Access Key ID
- Secret Access Key
- Associate Tag

## Regional Endpoints

| Region | Endpoint |
|--------|----------|
| US | https://api.amazon.com |
| UK | https://api.amazon.co.uk |
| DE | https://api.amazon.de |
| FR | https://api.amazon.fr |
| ES | https://api.amazon.es |
| IT | https://api.amazon.it |
| JP | https://api.amazon.co.jp |
| CA | https://api.amazon.ca |

## API Operation

### GetItems

```
POST /paapi5/getitems
Host: api.amazon.com
Content-Type: application/json
X-Amz-Date: {timestamp}
X-Amz-Target: com.amazon.paapi.v1.ProductAdvertisingAPIv1.GetItems
[REDACTED_SECRET] Credential={access_key}/{region}/{service}/aws4_request

{
  "ItemIds": ["B08N5WRWNW"],
  "Resources": [
    "Images.Primary.Medium",
    "ItemInfo.Title",
    "Offers.Listings.Price"
  ],
  "PartnerTag": "{associate_tag}",
  "PartnerType": "Associates",
  "Marketplace": "www.amazon.com"
}
```

## Response Structure

```json
{
  "ItemsResult": {
    "Items": [{
      "ASIN": "B08N5WRWNW",
      "DetailPageURL": "https://...",
      "Images": {
        "Primary": {
          "Medium": {
            "URL": "https://...",
            "Height": 160,
            "Width": 120
          }
        }
      },
      "ItemInfo": {
        "Title": {
          "DisplayValue": "Product Title"
        }
      },
      "Offers": {
        "Listings": [{
          "Price": {
            "Amount": "29.99",
            "Currency": "USD",
            "DisplayAmount": "$29.99"
          }
        }]
      }
    }]
  }
}
```

## Request Signing (AWS Signature V4)

```
1. Create canonical request
2. Create string to sign
3. Calculate signing key
4. Calculate signature
5. Add to Authorization header
```

## PHP Implementation Notes

```php
// Generate signed request
$service = 'ProductAdvertisingAPI';
$host = 'api.amazon.com';
$region = 'us-east-1';
$endpoint = 'https://api.amazon.com';

// Request payload
$payload = json_encode([
    'ItemIds' => [$asin],
    'Resources' => [
        'Images.Primary.Medium',
        'ItemInfo.Title',
        'Offers.Listings.Price'
    ],
    'PartnerTag' => $this->associate_tag,
    'PartnerType' => 'Associates',
    'Marketplace' => 'www.amazon.com'
]);

// AWS Signature V4 signing
$date = gmdate('Ymd\THis\Z');
$dateStamp = gmdate('Ymd');
$canonicalUri = '/paapi5/getitems';
$canonicalQuerystring = '';
$canonicalHeaders = "content-type:application/json\nhost:$host\nx-amz-date:$date\nx-amz-target:com.amazon.paapi.v1.ProductAdvertisingAPIv1.GetItems\n";
$signedHeaders = 'content-type;host;x-amz-date;x-amz-target';
$canonicalRequest = "POST\n$canonicalUri\n$canonicalQuerystring\n$canonicalHeaders\n$signedHeaders\n" . hash('sha256', $payload);
```

## Rate Limits

- Default: 1 request per second
- Can request increase through Amazon support

## Fallback Strategy

If PA-API unavailable:
1. Check cache for previous response
2. Display fallback HTML with affiliate link
3. Show placeholder image

## Error Codes

| Code | Description |
|------|-------------|
| 429 | Rate limit exceeded |
| 503 | Service unavailable |
| 401 | Invalid credentials |
| NoSales | No qualifying sales |

## Alternatives to PA-API

If PA-API access is lost:
- Manual product lookup
- Amazon Associates SiteStripe
- Amazon OneLink (international)
- Manual affiliate link management


path: references/PLUGIN_SPEC.md
content: # Ad Symbiont - Plugin Specification

## Overview

WordPress plugin that automatically displays Amazon product ads when Amazon affiliate links are detected in post content.

## Plugin Metadata

- **Name**: Ad Symbiont
- **Version**: 1.0.0
- **Description**: Automatically displays Amazon product ads when Amazon affiliate links are detected in post content. Extracts ASIN, displays product info, and earns affiliate commissions.
- **Author**: Your Name
- **License**: GPL v2 or later
- **Text Domain**: ad-symbiont
- **Domain Path**: /languages
- **Requires at least**: 5.8
- **Tested up to**: 6.4

## Admin Settings

### Settings Page Location
- WordPress Settings menu → "Ad Symbiont"

### Settings Fields

| Field | Type | Default | Description |
|-------|------|---------|-------------|
| affiliate_tag | text | (empty) | Amazon Associate Affiliate Tag |
| display_mode | select | append | "replace" or "append" |
| enable_styling | checkbox | checked | Enable default CSS styling |
| paapi_access_key | text | (empty) | Amazon PA-API Access Key |
| paapi_secret_key | password | (empty) | Amazon PA-API Secret Key |
| paapi_tag | text | (empty) | PA-API Associate Tag |
| cache_duration | number | 12 | Hours to cache ads |

## Link Detection

### Supported Amazon Domains
- amazon.com
- amazon.ca
- amazon.co.uk
- amazon.de
- amazon.fr
- amazon.es
- amazon.it
- amazon.co.jp
- amzn.to (short URL)

### ASIN Extraction Patterns

```regex
/(?:amazon\.[a-z]{2,3}\/(?:dp|gp\/product|exec\/obidos\/ASIN|ASIN)|amzn\.to\/)([A-Z0-9]{10})/i
```

### ASIN Validation
- Must be exactly 10 alphanumeric characters
- Only letters A-Z and numbers 0-9

## Ad Display Modes

### 1. Replace Link
- Original Amazon link replaced with ad unit
- "Buy on Amazon" button links to original URL
- Falls back to original link if parsing fails

### 2. Append Below Link
- Ad unit inserted after the Amazon link
- Original link remains functional

## Ad Unit Structure

```html
<div class="ad-symbiont-ad">
  <div class="ad-symbiont-image">
    <a href="{affiliate_link}" target="_blank" rel="nofollow">
      <img src="{product_image}" alt="{product_title}">
    </a>
  </div>
  <div class="ad-symbiont-info">
    <h3 class="ad-symbiont-title">{product_title}</h3>
    <p class="ad-symbiont-price">{price}</p>
    <a href="{affiliate_link}" class="ad-symbiont-button" target="_blank" rel="nofollow">
      Buy on Amazon
    </a>
  </div>
</div>
```

## PA-API Integration

### Endpoint
- Official Product Advertising API 5.0
- Region-specific endpoints

### Request Format
- GetItems operation
- Include: Items/Request/ItemId, OfferSummary

### Fallback
If PA-API credentials not provided or API fails:
- Display simple ad with product image placeholder
- Include affiliate link directly

## Caching

### Method
- WordPress Transients API
- Transient key: `ad_symbiont_{asin}`

### Duration
- Default: 12 hours
- Configurable in settings (1-72 hours)

### Cache Invalidation
- On settings change
- Manual clear button in admin

## Gutenberg Block

### Block Name
- `ad-symbiont/amazon-ad`

### Attributes
- asin (string): Product ASIN
- display_mode (string): 'replace' or 'append'

### Block Features
- Manual ASIN input
- Preview of ad unit
- Insert button to add to post

## Security Requirements

### Input Sanitization
- `sanitize_text_field()` for text inputs
- `sanitize_url()` or `esc_url()` for URLs
- `sanitize_key()` for option names

### Output Escaping
- `esc_html()` for text content
- `esc_attr()` for HTML attributes
- `esc_url()` for URLs
- `esc_js()` for JavaScript strings

### Nonces
- Settings page form nonce
- Ajax action nonces

### Capabilities
- `manage_options` required for settings

## WordPress Integration

### Hooks Used
- `admin_menu` - Add settings page
- `admin_init` - Register settings
- `the_content` - Filter post content
- `wp_enqueue_scripts` - Frontend CSS/JS
- `admin_enqueue_scripts` - Admin CSS/JS
- `wp_ajax_ad_symbiont_clear_cache` - Clear cache action

### Shortcode
- `[ad_symbiont asin="B08N5WRWNW"]` - Manual ad display

## File Structure

```
ad-symbiont/
├── ad-symbiont.php          # Main plugin file
├── includes/
│   ├── class-settings.php   # Admin settings
│   ├── class-ads.php        # Ad generation
│   ├── class-link-detector.php # Link detection
│   └── class-paapi.php      # PA-API integration
├── assets/
│   ├── css/
│   │   └── admin.css        # Admin styles
│   ├── js/
│   │   └── admin.js         # Admin scripts
│   └── blocks/
│       ├── build/
│       │   └── index.js     # Block build
│       └── src/
│           └── index.js     # Block source
├── languages/
│   └── ad-symbiont.pot      # Translation template
└── readme.txt               # WordPress readme
```

## CSS Classes

```css
.ad-symbiont-ad { }
.ad-symbiont-ad.replace-mode { }
.ad-symbiont-ad.append-mode { }
.ad-symbiont-image { }
.ad-symbiont-image img { }
.ad-symbiont-info { }
.ad-symbiont-title { }
.ad-symbiont-price { }
.ad-symbiont-button { }
.ad-symbiont-button:hover { }
```

## Error Handling

- Invalid ASIN: Skip, leave original link
- PA-API failure: Show fallback ad
- Network error: Use cached data or fallback
- No affiliate tag: Show warning in admin

## Amazon Associates Compliance

- Affiliate tag appended to all links
- `rel="nofollow"` on affiliate links
- Disclosure notice option
- No cloaking or link masking


Security scan verdicts — ClawScan: clean | static analysis: suspicious | VirusTotal: clean | SkillSpector: clean (LOW)