# Pressific Bio for LLMs.txt — External Integration Contract

This document describes how external systems (agency WordPress management
platforms, automation tools, deployment pipelines, migration scripts) can read
and write this plugin's state without going through the WP admin UI.

**Plugin version:** 1.5.0
**Integration contract version:** 1
**Machine-readable version:** see `integration.json` in the plugin root.

---

## Design principles

1. **No custom API.** Everything is a standard WordPress option. Any code that
   can call `get_option()` and `update_option()` can manage this plugin — full
   stop.
2. **Writes are observable.** Any write to a managed option fires
   `update_option_*` core hooks, which this plugin listens to and uses to
   purge CDN/page caches and refresh internal state.
3. **Reads are cheap.** All read operations are plain option reads. No
   database joins, no runtime computation.
4. **Idempotent.** Writing the same value twice is safe. Writing incomplete
   state is safe (the plugin reads each option independently).
5. **Stable contract.** The option names and types documented here are part
   of a versioned contract. Breaking changes bump the integration contract
   version in `integration.json`.

---

## Options

All options live in the standard `wp_options` table. Use WordPress's
`get_option()` / `update_option()` functions — not direct SQL — so the
`update_option_*` action hooks fire and caches are purged automatically.

### Read-write options

| Option name | Type | Default | Served at | Description |
|-------------|------|---------|-----------|-------------|
| `pressific_content_short` | string | `""` | `{home}/llms.txt` | Short-form llms.txt content. Markdown recommended. |
| `pressific_content_full` | string | `""` | `{home}/llms-full.txt` | Full-text llms-full.txt content. Markdown recommended. |
| `pressific_allow_ai_crawlers` | int (0 or 1) | `0` | — | When `1`, the plugin appends Allow directives for major AI user-agents to the virtual robots.txt. |

### Read-only options (set by the plugin)

| Option name | Type | Description |
|-------------|------|-------------|
| `pressific_health_summary` | array | `['has_issues' => bool, 'issue_codes' => string[], 'computed_at' => int]`. Updated after every status check. |
| `pressific_version` | string | Last-seen plugin version. Used internally for upgrade detection. |

### Transients (cached state)

| Transient name | TTL | Description |
|----------------|-----|-------------|
| `pressific_status_cache` | 300s | Full dashboard snapshot — site visibility, llms.txt sources, crawler access, conflicting plugins. Safe to read; safe to delete (it will regenerate on next admin page load or on the hourly cron). |

---

## Hooks

### Actions you can fire (external → plugin)

#### `do_action( 'pressific_sync' )`

Forces a cache purge and status refresh. Useful if your external system wrote
to options via raw SQL (bypassing `update_option()` and therefore bypassing
the auto-purge hooks), or if you want to force a refresh without modifying
anything.

**When to use:**
- After a raw-SQL write to any `pressific_*` option
- After a site restore from backup that bypassed WordPress
- Before polling `pressific_status_cache` when you need guaranteed-fresh data

**When NOT to use:**
- After `update_option()` — the plugin already handles that automatically

### Actions this plugin fires (plugin → external)

#### `do_action( 'pressific_purge_url', $url )`

Fires once per relevant URL (`/llms.txt` and `/llms-full.txt`) whenever any
managed content option changes. External cache layers (Cloudflare, Bunny,
custom edge caches) can listen to this to invalidate their stored copies.

```php
add_action( 'pressific_purge_url', function ( $url ) {
    my_custom_cdn_purge( $url );
} );
```

### Core WP hooks you can listen to

Because all writes go through WordPress options, the standard core hooks fire
and can be listened to without anything plugin-specific:

```php
// Observe content changes from any source (admin UI, your worker, WP-CLI, REST)
add_action( 'update_option_pressific_content_short', function ( $old, $new ) {
    my_dashboard_push( 'short', $old, $new );
}, 10, 2 );

add_action( 'update_option_pressific_content_full', function ( $old, $new ) {
    my_dashboard_push( 'full', $old, $new );
}, 10, 2 );
```

---

## Response headers on served URLs

Requests to `/llms.txt` and `/llms-full.txt` include a distinctive header so
external systems can confirm the plugin is the responder (vs. a static file
or another plugin):

```
X-Pressific: 1.5.0
```

If you request one of these URLs and do not see `X-Pressific` in the response
headers, something else is serving the URL — likely a static file in the
webroot or a conflicting plugin.

---

## Worked examples

### Reading state

```php
$short  = (string) get_option( 'pressific_content_short', '' );
$full   = (string) get_option( 'pressific_content_full', '' );
$robots = (int)    get_option( 'pressific_allow_ai_crawlers', 0 );
$health = (array)  get_option( 'pressific_health_summary', [] );

// Full status snapshot (may be stale by up to 5 minutes)
$status = get_transient( 'pressific_status_cache' );
if ( false === $status ) {
    // Not cached — force a fresh snapshot
    do_action( 'pressific_sync' );
    $status = get_transient( 'pressific_status_cache' );
}
```

### Writing content

```php
// Your worker plugin, called from a central dashboard push:
update_option( 'pressific_content_short', $content_from_dashboard );
update_option( 'pressific_content_full',  $full_from_dashboard );
update_option( 'pressific_allow_ai_crawlers', 1 );

// No further action needed. The plugin has already:
// - Purged LiteSpeed Cache, WP Rocket, W3TC, WP Super Cache
// - Fired pressific_purge_url for each affected URL
// - Cleared its own status cache
// - Refreshed the health summary
```

### Bulk apply a manifest

```php
function pressific_apply_manifest( array $manifest ) : array {
    $results = [];
    foreach ( $manifest as $option_name => $value ) {
        if ( 0 !== strpos( $option_name, 'pressific_' ) ) {
            continue; // Ignore non-managed keys
        }
        $results[ $option_name ] = update_option( $option_name, $value );
    }
    return $results;
}

pressific_apply_manifest( [
    'pressific_content_short'     => $short_markdown,
    'pressific_content_full'      => $full_markdown,
    'pressific_allow_ai_crawlers' => 1,
] );
```

### Verifying the plugin is serving

```php
$response = wp_remote_head( home_url( '/llms.txt' ), [ 'timeout' => 5 ] );
$header   = wp_remote_retrieve_header( $response, 'x-pressific' );

if ( $header ) {
    // Plugin version $header is serving this URL
} else {
    // Something else is serving — investigate
}
```

### Remote HTTP (from outside the WP site, no worker plugin)

If your external system does not have a worker plugin installed on the WP
site, you can still read/write via the standard WordPress REST API using
[Application Passwords](https://make.wordpress.org/core/2020/11/05/application-passwords-integration-guide/):

```
GET  /wp-json/wp/v2/settings           # includes any options registered via register_setting()
POST /wp-json/wp/v2/settings           # {"pressific_content_short": "..."}
```

All three managed options (`pressific_content_short`, `pressific_content_full`,
`pressific_allow_ai_crawlers`) are registered with `show_in_rest` disabled by
default. If you want to expose them over REST, add this filter from your
worker plugin:

```php
add_filter( 'register_setting_args', function ( $args, $defaults, $option_name ) {
    if ( 0 === strpos( $option_name, 'pressific_' ) ) {
        $args['show_in_rest'] = true;
    }
    return $args;
}, 10, 3 );
```

---

## Versioning policy

The **integration contract version** is declared in `integration.json` as
`integration_version`. This plugin follows these rules:

- **Contract version bumps only on breaking changes** to option names, types,
  or observable behaviour.
- **Adding new options or hooks is not a breaking change** — contract version
  stays the same.
- **Renaming or removing an option is a breaking change** — contract version
  increments.

Your worker plugin should read `integration_version` at startup and warn /
fail if it is higher than what the worker was written against.

```php
$manifest = json_decode( file_get_contents(
    WP_PLUGIN_DIR . '/pressific-bio-for-llms-txt/integration.json'
), true );

if ( (int) $manifest['integration_version'] > MY_WORKER_SUPPORTED_CONTRACT ) {
    trigger_error( 'Pressific Bio for LLMs.txt contract version newer than expected', E_USER_WARNING );
}
```

---

## What this contract deliberately does NOT include

To keep this simple, the following are **out of scope** for the plugin:

- **Content generation** — build it in your platform, push rendered content.
- **Templating / variable substitution** — resolve templates in your platform
  before writing the resolved markdown to the option.
- **Version history / rollback** — your platform keeps the canonical version
  ledger, not the plugin.
- **Audit trail** — your worker plugin should observe the `update_option_*`
  hooks and push change events to your central dashboard.
- **Managed-mode UI lock** — not currently implemented; tell us if you hit
  the "client edits locally and overwrites our push" problem and we will
  add a lock filter.

These are intentionally pushed to the platform layer because the plugin is
deployed on thousands of WordPress sites and should remain small and focused.

---

## Support / feedback

Integration issues, contract clarifications, or feature requests: contact
Pressific at https://pressific.com.
