> ## Documentation Index
> Fetch the complete documentation index at: https://docs.trybluesky.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Install the Bluesky WordPress plugin

> Serve verified AI answer-engine crawlers your AEO content on WordPress or plain PHP — a drop-in plugin, no code.

If your site runs on **WordPress** (or plain PHP), you install the Agent Edge Network
as a small plugin. It runs on every front-end request, passes humans and search bots
straight through, and serves verified AI answer-engine crawlers your pre-generated
[optimized artifact](/guides/optimized-pages).

<Note>
  This is the drop-in plugin — self-contained, zero dependencies, verified by published
  IP ranges. It is the WordPress counterpart of the
  [Next.js middleware](/guides/nextjs-middleware) and
  [Cloudflare Worker](/guides/cloudflare-worker). It ships in **shadow** mode (log-only),
  so installing it can never change what your visitors see until you switch to Live.
</Note>

## What the plugin does

On every front-end request, at `template_redirect` (before your theme renders), it:

* **Passes through** anything that isn't a known AI crawler — your site behaves exactly as before.
* **Never touches search bots** — Googlebot, Bingbot and Applebot always get your real page (the [cloaking firewall](/guides/cloaking-firewall)).
* **Verifies** answer-engine crawlers by their published IP ranges, then — in Live mode — serves them your optimized artifact instead of your HTML page.
* **Reports every hit** to [Agent Analytics](/guides/agent-analytics), so you see who crawled what and whether it was served.

There is no LLM call and no extra round-trip for your visitors — artifacts are generated
offline and cached, and telemetry is fire-and-forget.

## Before you start

* A WordPress site you can add a plugin to (WordPress 5.6+, PHP 7.4+).
* An **edge token** from **Agent Analytics → Setup** in the Bluesky app (format `aen_...`).

## Install it

<Tabs>
  <Tab title="Upload the plugin (recommended)">
    <Steps>
      <Step title="Download the plugin">
        Get **[bluesky-aen.zip](https://raw.githubusercontent.com/trybluesky/blue-sky-docs/main/aen/bluesky-aen.zip)** —
        or use the **Download plugin** button under **Agent Analytics → Setup → WordPress** in the app.
      </Step>

      <Step title="Upload and activate">
        In WordPress admin, go to **Plugins → Add New → Upload Plugin**, choose the `.zip`,
        click **Install Now**, then **Activate**.
      </Step>

      <Step title="Paste your edge token">
        Go to **Settings → Bluesky AEN**, paste the key from **Agent Analytics → Setup**, and save.
      </Step>
    </Steps>
  </Tab>

  <Tab title="Single file (no zip)">
    WordPress recognises any PHP file with a plugin header placed in the plugins folder.

    <Steps>
      <Step title="Create the file">
        Create `wp-content/plugins/bluesky-aen.php` and paste the
        [full plugin source](#full-plugin-source) below.
      </Step>

      <Step title="Activate">
        In **Plugins**, find **Bluesky Agent Edge Network** and click **Activate**.
      </Step>

      <Step title="Paste your edge token">
        Go to **Settings → Bluesky AEN** and paste your key — or, if you prefer, define it in
        `wp-config.php`:

        ```php theme={null}
        define('AEN_EDGE_TOKEN', 'aen_your_token_here');
        ```
      </Step>
    </Steps>
  </Tab>
</Tabs>

## Test it

The plugin ships in **shadow** mode — it serves your normal page to everyone and only
**logs** what it would have served, so there is zero risk while you validate.

<Steps>
  <Step title="Trigger a real AI crawler">
    Open **ChatGPT** (or Perplexity) with browsing and ask a question that makes it look up your
    site. Its crawler fetches your page from a published OpenAI IP.
  </Step>

  <Step title="Watch the hit land">
    The plugin detects the crawler as `IP_VERIFIED` and the visit appears in
    **Agent Analytics**, broken down by agent and verification level.
  </Step>

  <Step title="Go live">
    Once you see verified hits, switch the serve mode to **Live** in **Agent Analytics → Setup**.
    No redeploy — the plugin reads the mode from the dashboard. Verified answer-engine agents now
    receive the optimized artifact. See [Serve modes](/guides/serve-modes).
  </Step>
</Steps>

## Caching

If you run a full-page cache (WP Super Cache, W3TC, LiteSpeed, or a CDN), it may serve a
cached HTML page to crawlers before the plugin runs. **Exclude AI crawler user-agents from
the cache** (e.g. `ChatGPT-User`, `PerplexityBot`, `ClaudeBot`, `Google-Extended`) so the
plugin can run for them. Human traffic stays fully cached.

## Troubleshooting

<AccordionGroup>
  <Accordion title="No hits appear in Agent Analytics">
    Confirm the plugin is **active**, the edge token is saved under **Settings → Bluesky AEN**,
    and you actually triggered a browsing AI. Only verified crawlers are counted.
  </Accordion>

  <Accordion title="My site looks unchanged">
    That's expected in shadow mode, and for every human visitor in any mode — the plugin only
    changes the response for verified AI crawlers.
  </Accordion>

  <Accordion title="The variant is never served (Live mode)">
    The request must be `IP_VERIFIED`, the crawler's purpose must be enabled, and Bluesky must have
    generated an artifact for that URL. Check the page's status in Agent Analytics → Pages.
  </Accordion>

  <Accordion title="Will this slow my site down?">
    No. Human and search-bot traffic passes straight through, served artifacts are pre-generated
    and cached, and telemetry is sent non-blocking. If Bluesky is ever unreachable, the request
    fails open to your normal page.
  </Accordion>
</AccordionGroup>

## Full plugin source

The plugin is a single self-contained file — no Composer, no build step.

<Accordion title="bluesky-aen.php">
  ```php bluesky-aen.php theme={null} theme={null}
  <?php
  /**
   * Plugin Name:       Bluesky Agent Edge Network
   * Plugin URI:        https://docs.trybluesky.com/guides/wordpress
   * Description:       Detect verified AI answer-engine crawlers (ChatGPT, Perplexity, Claude…) and serve them your AEO-optimized content. Humans and search bots always get your normal page.
   * Version:           0.1.0
   * Requires at least: 5.6
   * Requires PHP:      7.4
   * Author:            Bluesky
   * Author URI:        https://trybluesky.com
   * License:           GPLv2 or later
   * Text Domain:       bluesky-aen
   */

  if (!defined('ABSPATH')) {
      exit; // no direct access
  }

  if (!defined('AEN_BASE')) {
      define('AEN_BASE', 'https://app.trybluesky.com');
  }

  /** The edge token: a wp-config constant wins, else the saved option. */
  function aen_token() {
      if (defined('AEN_EDGE_TOKEN') && AEN_EDGE_TOKEN) {
          return AEN_EDGE_TOKEN;
      }
      return trim((string) get_option('aen_edge_token', ''));
  }

  function aen_auth_headers($token) {
      return array('Authorization' => 'Bearer ' . $token);
  }

  /** Bot registry, cached in a transient (~1h). */
  function aen_get_botset($token) {
      $cached = get_transient('aen_botset');
      if (is_array($cached)) {
          return $cached;
      }
      $res = wp_remote_get(AEN_BASE . '/api/v1/edge/botset', array('headers' => aen_auth_headers($token), 'timeout' => 5));
      if (is_wp_error($res) || wp_remote_retrieve_response_code($res) !== 200) {
          return array();
      }
      $json = json_decode(wp_remote_retrieve_body($res), true);
      $bots = isset($json['botset']['bots']) ? $json['botset']['bots'] : array();
      set_transient('aen_botset', $bots, HOUR_IN_SECONDS);
      return $bots;
  }

  /** Dashboard-controlled config (serve mode, purposes), cached ~5 min. */
  function aen_get_config($token) {
      $cached = get_transient('aen_config');
      if (is_array($cached)) {
          return $cached;
      }
      $cfg = array('serveMode' => 'shadow', 'enabledPurposes' => array('USER_TRIGGERED', 'RETRIEVAL'));
      $res = wp_remote_get(AEN_BASE . '/api/v1/edge/config', array('headers' => aen_auth_headers($token), 'timeout' => 5));
      if (!is_wp_error($res) && wp_remote_retrieve_response_code($res) === 200) {
          $body = json_decode(wp_remote_retrieve_body($res), true);
          if (is_array($body)) {
              $cfg = array_merge($cfg, $body);
          }
      }
      set_transient('aen_config', $cfg, 300);
      return $cfg;
  }

  /** Longest user-agent token match (word-boundary), like the Next/Cloudflare drop-ins. */
  function aen_match_bot($ua, $bots) {
      $low = strtolower($ua);
      $best = null;
      foreach ($bots as $b) {
          if (empty($b['uaToken'])) {
              continue;
          }
          $tok = preg_quote(strtolower($b['uaToken']), '/');
          if (preg_match('/(?:^|[^a-z0-9])' . $tok . '(?:[^a-z0-9]|$)/i', $low)) {
              if ($best === null || strlen($b['uaToken']) > strlen($best['uaToken'])) {
                  $best = $b;
              }
          }
      }
      return $best;
  }

  /** IPv4 CIDR membership (IPv6 is skipped in this drop-in). */
  function aen_ip_in_cidrs($ip, $cidrs) {
      if (strpos($ip, ':') !== false) {
          return false;
      }
      $ipLong = ip2long($ip);
      if ($ipLong === false) {
          return false;
      }
      foreach ((array) $cidrs as $cidr) {
          if (strpos($cidr, ':') !== false) {
              continue;
          }
          $parts = explode('/', $cidr);
          $net = ip2long($parts[0]);
          $bits = isset($parts[1]) ? (int) $parts[1] : 32;
          if ($net === false) {
              continue;
          }
          $mask = $bits === 0 ? 0 : (-1 << (32 - $bits)) & 0xFFFFFFFF;
          if ((($ipLong & $mask) & 0xFFFFFFFF) === (($net & $mask) & 0xFFFFFFFF)) {
              return true;
          }
      }
      return false;
  }

  function aen_client_ip() {
      if (!empty($_SERVER['HTTP_CF_CONNECTING_IP'])) {
          return trim($_SERVER['HTTP_CF_CONNECTING_IP']);
      }
      if (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
          $parts = explode(',', $_SERVER['HTTP_X_FORWARDED_FOR']);
          return trim($parts[0]);
      }
      return isset($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : '';
  }

  function aen_current_url() {
      $scheme = is_ssl() ? 'https' : 'http';
      $host = isset($_SERVER['HTTP_HOST']) ? $_SERVER['HTTP_HOST'] : '';
      $path = isset($_SERVER['REQUEST_URI']) ? strtok($_SERVER['REQUEST_URI'], '?') : '/';
      return $scheme . '://' . $host . $path;
  }

  function aen_telemetry($token, $bot, $url, $ua, $ip, $verified, $served) {
      $path = parse_url($url, PHP_URL_PATH);
      $hit = array(
          'botName'            => $bot['uaToken'],
          'path'               => $path ? $path : '/',
          'statusCode'         => 200,
          'userAgent'          => $ua,
          'botIp'              => $ip,
          'trustLevel'         => $verified ? 'IP_VERIFIED' : 'UA_ONLY',
          'purposeClass'       => isset($bot['purpose']) ? $bot['purpose'] : 'UNKNOWN',
          'verificationMethod' => $verified ? 'cidr' : null,
          'served'             => $served,
      );
      wp_remote_post(AEN_BASE . '/api/v1/edge/telemetry', array(
          'headers'  => array_merge(aen_auth_headers($token), array('Content-Type' => 'application/json')),
          'body'     => wp_json_encode(array('hits' => array($hit))),
          'timeout'  => 2,
          'blocking' => false, // fire-and-forget, never delays the response
      ));
  }

  /** The interception. Runs before the theme renders; serves verified AI crawlers the artifact. */
  function aen_intercept() {
      if (is_admin()) {
          return;
      }
      $token = aen_token();
      if (!$token) {
          return;
      }
      $cfg = aen_get_config($token);
      $mode = isset($cfg['serveMode']) ? $cfg['serveMode'] : 'shadow';
      if ($mode === 'off') {
          return;
      }

      $ua = isset($_SERVER['HTTP_USER_AGENT']) ? $_SERVER['HTTP_USER_AGENT'] : '';
      $bots = aen_get_botset($token);
      $bot = aen_match_bot($ua, $bots);
      if (!$bot) {
          return;
      }
      if (!empty($bot['isSearchIndex'])) {
          return; // cloaking firewall: Googlebot/Bingbot/Applebot always get the real page
      }

      $ip = aen_client_ip();
      $verified = aen_ip_in_cidrs($ip, isset($bot['cidrs']) ? $bot['cidrs'] : array());
      $enabled = isset($cfg['enabledPurposes']) ? $cfg['enabledPurposes'] : array('USER_TRIGGERED', 'RETRIEVAL');
      $eligible = $verified && in_array($bot['purpose'], $enabled, true);
      $url = aen_current_url();
      $served = 'PASSTHROUGH';

      $base = explode(':', $mode)[0];
      if ($eligible && ($base === 'default' || $base === 'canary')) {
          $uh = hash('sha256', $url);
          $doServe = $base === 'default';
          if ($base === 'canary') {
              $mparts = explode(':', $mode);
              $pct = isset($mparts[1]) ? (int) $mparts[1] : 0;
              $doServe = (hexdec(substr($uh, 0, 8)) % 100) < $pct;
          }
          if ($doServe) {
              $res = wp_remote_get(AEN_BASE . '/api/v1/edge/artifact?u=' . $uh, array('headers' => aen_auth_headers($token), 'timeout' => 5));
              if (!is_wp_error($res) && wp_remote_retrieve_response_code($res) === 200) {
                  $json = json_decode(wp_remote_retrieve_body($res), true);
                  $md = isset($json['artifact']['markdown']) ? $json['artifact']['markdown'] : null;
                  if ($md) {
                      $served = 'VARIANT';
                      aen_telemetry($token, $bot, $url, $ua, $ip, $verified, $served);
                      header('Content-Type: text/markdown; charset=utf-8');
                      header('X-AEN-Served: variant');
                      header('Cache-Control: no-store');
                      echo $md; // phpcs:ignore WordPress.Security.EscapeOutput — markdown body, served as-is to the crawler
                      exit;
                  }
              }
          }
      }

      aen_telemetry($token, $bot, $url, $ua, $ip, $verified, $served);
  }
  add_action('template_redirect', 'aen_intercept', 0);

  /* ------------------------------------------------------------------ */
  /* Settings page                                                       */
  /* ------------------------------------------------------------------ */

  add_action('admin_menu', function () {
      add_options_page('Bluesky AEN', 'Bluesky AEN', 'manage_options', 'bluesky-aen', 'aen_settings_page');
  });

  add_action('admin_init', function () {
      register_setting('aen_settings', 'aen_edge_token', array('sanitize_callback' => 'sanitize_text_field'));
  });

  function aen_settings_page() {
      if (!current_user_can('manage_options')) {
          return;
      }
      ?>
      <div class="wrap">
          <h1>Bluesky Agent Edge Network</h1>
          <p>Serve verified AI crawlers your AEO-optimized content. Get your edge token from
             <a href="https://app.trybluesky.com/en/agent-analytics" target="_blank" rel="noreferrer">Agent Analytics &rarr; Settings</a>.</p>
          <form method="post" action="options.php">
              <?php settings_fields('aen_settings'); ?>
              <table class="form-table">
                  <tr>
                      <th scope="row"><label for="aen_edge_token">Edge token</label></th>
                      <td>
                          <input name="aen_edge_token" id="aen_edge_token" type="text" class="regular-text"
                                 value="<?php echo esc_attr(get_option('aen_edge_token', '')); ?>" placeholder="aen_…" />
                          <p class="description">It ships in <strong>shadow</strong> mode (safe). Switch to Live from the Agent Analytics dashboard.</p>
                      </td>
                  </tr>
              </table>
              <?php submit_button(); ?>
          </form>
      </div>
      <?php
  }
  ```
</Accordion>

## How it differs from the other drop-ins

* It runs **inside WordPress** at `template_redirect` priority `0`, before your theme renders.
* The client IP comes from `CF-Connecting-IP` → `X-Forwarded-For` → `REMOTE_ADDR` (in that order),
  so it works behind Cloudflare or a load balancer.
* The serve mode is read live from your dashboard (cached \~5 min) — no redeploy to go Live.

Everything else — the [cloaking firewall](/guides/cloaking-firewall),
[verification levels](/guides/detection-verification), [serve modes](/guides/serve-modes),
and offline artifact generation — behaves exactly as it does for the middleware and Worker.
