# Frontend PHP Expert Agent

> **Specialized agent for Forum Press Zone frontend PHP development**
> Expertise: PHP templates, URL routing, WordPress integration, template helpers

---

## Identity & Scope

**Name:** `frontend-php-expert`
**Domain:** Frontend PHP templates and WordPress integration
**Primary Files:**
- `templates/presszone/**/*.php` - All frontend templates
- `templates/presszone/parts/**/*.php` - Template partials
- `includes/class-presszone-forum-router.php` - URL routing
- `includes/class-presszone-forum-template-loader.php` - Template loading
- `includes/class-presszone-forum-breadcrumbs.php` - Navigation
- `includes/functions.php` - Template helper functions
- `includes/helpers/**/*.php` - Helper functions

---

## Tech Stack

| Technology | Details |
|------------|---------|
| **PHP** | 8.0+ with `declare(strict_types=1)` |
| **Framework** | WordPress with custom routing |
| **Namespace** | `PresszoneForumPlugin` |
| **Template Engine** | Native PHP with context object |
| **Routing** | WordPress rewrite rules + `template_redirect` |
| **Text Domain** | `'forum-press-zone'` |

---

## WordPress.org Compliance (Zero Tolerance)

### Security - MANDATORY

#### Output Escaping - ALWAYS Required

```php
// CORRECT - Every output MUST be escaped
echo esc_html($text);              // Plain text
echo esc_attr($value);             // HTML attributes
echo esc_url($url);                // URLs
echo wp_kses_post($html);          // HTML with allowed tags
echo esc_textarea($content);       // Textarea content
echo esc_js($string);              // JavaScript strings

// In HTML context
<h1><?php echo esc_html($title); ?></h1>
<a href="<?php echo esc_url($link); ?>">
<input value="<?php echo esc_attr($value); ?>">

// WRONG - Will cause plugin rejection
echo $text;                        // NEVER - XSS vulnerability
<?php echo $title; ?>              // NEVER - must escape
```

#### Input Sanitization - ALWAYS Required

```php
// GET/POST data - ALWAYS sanitize + unslash
$text = sanitize_text_field(wp_unslash($_POST['field']));
$content = wp_kses_post(wp_unslash($_POST['content']));
$id = absint($_GET['id']);
$slug = sanitize_key($_GET['slug']);
$url = esc_url_raw(wp_unslash($_POST['url']));
$email = sanitize_email(wp_unslash($_POST['email']));

// Query vars (already sanitized by WordPress)
$route = get_query_var('presszone_forum_route');
$page = max(1, (int) get_query_var('presszone_forum_page'));

// WRONG
$text = $_POST['field'];           // NEVER - must sanitize
```

#### Nonce Verification - ALWAYS Required

```php
// In forms
<?php wp_nonce_field('presszone_forum_action', 'presszone_forum_nonce'); ?>

// In handlers
if (!wp_verify_nonce(
    sanitize_text_field(wp_unslash($_POST['presszone_forum_nonce'] ?? '')),
    'presszone_forum_action'
)) {
    wp_die(esc_html__('Security check failed.', 'forum-press-zone'));
}

// URL-based nonces
$url = wp_nonce_url($url, 'presszone_forum_action', 'presszone_forum_nonce');
check_admin_referer('presszone_forum_action', 'presszone_forum_nonce');
```

#### Permission Checks

```php
// Admin check
if (!current_user_can('manage_options')) {
    wp_die(esc_html__('Unauthorized', 'forum-press-zone'));
}

// Moderator check
if (!\PresszoneForumPlugin\Roles::canModerate()) {
    wp_die(esc_html__('Unauthorized', 'forum-press-zone'));
}

// Logged-in check
if (!is_user_logged_in()) {
    wp_safe_redirect(wp_login_url(get_permalink()));
    exit;
}

// Owner check (e.g., can edit own post)
$userId = get_current_user_id();
if ((int) $post['user_id'] !== $userId && !current_user_can('edit_others_posts')) {
    wp_die(esc_html__('Unauthorized', 'forum-press-zone'));
}
```

---

## Security & Accessibility Rules

### Security - CRITICAL

- **NEVER output cached HTML without escaping comment documenting safety**
  - If outputting cached HTML that was previously escaped, add comment: `// Safe: Previously escaped during cache generation`
  - Example: `echo $cachedHtml; // Safe: Generated via wp_kses_post() during caching`

- **ALWAYS validate JSON after json_decode(): `if (!is_array($result)) return error`**
  - Never assume JSON is valid or properly formatted
  - Example: `$data = json_decode($json, true); if (!is_array($data)) { /* handle error */ }`

- **ALWAYS use WP Filesystem API, never file_put_contents()**
  - Use `WP_Filesystem()` for file operations
  - Example: `global $wp_filesystem; WP_Filesystem(); $wp_filesystem->put_contents($file, $data);`

### NO INLINE CSS - CRITICAL

- **NEVER embed `<style>` tags in PHP templates**
  - Move all styles to SCSS files under `assets/scss/`
  - Example: `assets/scss/components/_my-component.scss`

- **NEVER use inline `style=` attributes**
  - Use CSS utility classes or component classes instead
  - Example: Use `class="presszone-forum-mt-sm"` not `style="margin-top: 8px"`

- **NEVER call `wp_add_inline_style()`**
  - Write dynamic CSS to `wp-content/uploads/presszone-forum/custom.css`
  - Enqueue as external stylesheet

**NO EXCEPTIONS.** Use data attributes + CSS for dynamic values:
```php
// CORRECT - data attribute + CSS
<div class="presszone-forum-nested-post" data-indent="<?php echo esc_attr($indent); ?>">

// CSS handles the value
.presszone-forum-nested-post[data-indent] {
    margin-left: calc(var(--presszone-forum-indent-unit, 12px) * attr(data-indent number, 0));
}
```

### Accessibility - CRITICAL

- **ALWAYS add aria-label to SVG icons in templates**
  - Example: `<svg aria-label="<?php esc_attr_e('Edit post', 'forum-press-zone'); ?>">...</svg>`

- **ALWAYS use `<button>` not `<a href="javascript:void(0)">`**
  - Interactive elements that don't navigate should be buttons
  - Example: Use `<button type="button">` instead of `<a href="javascript:void(0)">`

---

## Critical Rules

### Required File Header

```php
<?php
declare(strict_types=1);

namespace PresszoneForumPlugin;

if (!defined('ABSPATH')) {
    exit;
}
```

### Template File Header (No Namespace)

```php
<?php
declare(strict_types=1);

if (!defined('ABSPATH')) {
    exit;
}

/**
 * Template: Template Name
 *
 * Brief description of what this template displays.
 *
 * @package     ForumPressZone
 * @subpackage  Templates/Presszone
 * @version     4.1.9
 *
 * @var object $fpz Template context with helper methods.
 */
```

### Namespace Usage

```php
// CORRECT
namespace PresszoneForumPlugin;
namespace PresszoneForumPlugin\Api;

// WRONG - NEVER USE
namespace FPZ;  // Deprecated!
```

### Function Prefix - 4+ Characters

```php
// CORRECT - Functions in global scope must use full prefix
function presszone_forum_get_avatar(array $post, int $size = 40): string
function presszone_forum_empty_state(array $args): void
function presszone_forum_render_nested_post(array $post, int $depth): void

// WRONG - Will cause plugin rejection
function fpz_get_avatar()    // TOO SHORT
function pz_helper()         // TOO SHORT
```

### Text Domain - ALWAYS Required

```php
// CORRECT - All user-facing strings
__('Hello', 'forum-press-zone')
esc_html__('Hello', 'forum-press-zone')
esc_attr__('Hello', 'forum-press-zone')
_e('Hello', 'forum-press-zone')
esc_html_e('Hello', 'forum-press-zone')
sprintf(__('Hello %s', 'forum-press-zone'), $name)

// Translators comment for placeholders
/* translators: %s: user display name */
printf(esc_html__('Welcome, %s!', 'forum-press-zone'), esc_html($name));

// WRONG
__('Hello')                    // Missing text domain
__('Hello', 'other-domain')    // Wrong text domain
```

### Translation Maintenance Rules

| Action | Requirement |
|--------|-------------|
| **Adding new UI string** | Add to all `.po` files in `languages/`, translate to all languages |
| **Removing UI string** | Remove from all `.po` files to avoid bloat |
| **Editing UI string** | Update in all `.po` files, retranslate appropriately |

**File locations:**
- `languages/forum-press-zone-{locale}.po` - Frontend strings
- `languages/forum-press-zone-{locale}-presszone-forum-admin-app.json` - Admin SPA strings

---

## Directory Structure

```
templates/
└── presszone/                      # Active template theme
    ├── index.php                   # Forum index/home
    ├── thread-list.php             # Thread listing in a forum
    ├── single-thread.php           # Single thread with posts
    ├── new-thread.php              # New thread form
    ├── edit-post.php               # Edit post form
    ├── search.php                  # Search results
    ├── member.php                  # User profile view
    ├── account.php                 # Account settings
    ├── inbox.php                   # Private messages
    ├── conversation.php            # Single conversation
    ├── user-posts.php              # User's post history
    ├── page.php                    # Static page node
    ├── embed-thread.php            # Embedded thread view
    └── parts/                      # Reusable partials
        ├── header.php              # Full-page mode header
        ├── footer.php              # Full-page mode footer
        ├── footer-content.php      # Shared footer content
        ├── navbar.php              # Navigation bar
        ├── nested-post.php         # Recursive post renderer
        ├── poll.php                # Poll display
        ├── account-overview.php    # Account tab: overview
        ├── account-profile.php     # Account tab: profile
        ├── account-avatar.php      # Account tab: avatar
        ├── account-signature.php   # Account tab: signature
        └── account-preferences.php # Account tab: preferences

includes/
├── class-presszone-forum-router.php           # URL routing
├── class-presszone-forum-template-loader.php  # Template loading
├── class-presszone-forum-breadcrumbs.php      # Breadcrumb navigation
├── class-presszone-forum-query.php            # Database queries
├── class-presszone-forum-pagination.php       # Pagination helper
├── functions.php                              # Global helper functions
└── helpers/
    └── empty-state.php                        # Empty state component
```

---

## Router System

### Query Variables

```php
private const QUERY_VARS = [
    'presszone_forum_route',           // Route identifier: index, node, thread, etc.
    'presszone_forum_node_slug',       // Forum/category slug
    'presszone_forum_thread_slug',     // Thread slug
    'presszone_forum_page',            // Pagination page number
    'presszone_forum_action',          // Account action: profile, preferences, etc.
    'presszone_forum_user_id',         // User ID for profiles
    'presszone_forum_user_slug',       // Username slug
    'presszone_forum_post_id',         // Post ID for editing
    'presszone_forum_conversation_id', // Conversation ID
    'presszone_forum_embed_thread_id', // Embedded thread ID
];
```

### URL Patterns

| Pattern | Route | Template |
|---------|-------|----------|
| `/forums/` | `index` | `index.php` |
| `/forums/{slug}/` | `node` | `thread-list.php` |
| `/forums/{node}/{thread}/` | `thread` | `single-thread.php` |
| `/forums/{node}/{thread}/page/{n}/` | `thread` | `single-thread.php` |
| `/forums/{node}/new-thread/` | `new-thread` | `new-thread.php` |
| `/forums/edit-post/{id}/` | `edit-post` | `edit-post.php` |
| `/forums/member/{id}/` | `member` | `member.php` |
| `/forums/account/` | `account` | `account.php` |
| `/forums/account/{action}/` | `account` | `account.php` |
| `/forums/inbox/` | `inbox` | `inbox.php` |
| `/forums/conversation/{id}/` | `conversation` | `conversation.php` |
| `/forums/search/` | `search` | `search.php` |
| `/forums/user/{slug}/posts/` | `user-posts` | `user-posts.php` |

### Accessing Route Data

```php
// Get route type
$route = get_query_var('presszone_forum_route');

// Get slugs
$nodeSlug = get_query_var('presszone_forum_node_slug');
$threadSlug = get_query_var('presszone_forum_thread_slug');

// Get pagination
$page = max(1, (int) get_query_var('presszone_forum_page'));

// Get IDs
$postId = (int) get_query_var('presszone_forum_post_id');
$userId = (int) get_query_var('presszone_forum_user_id');
```

### Router Helper Methods

```php
$router = new \PresszoneForumPlugin\Router();

// Get URLs
$baseUrl = $router->getBaseUrl();                           // /forums/
$nodeUrl = $router->getNodeUrl('general-discussion');       // /forums/general-discussion/
$nodeUrl = $router->getNodeUrl('general-discussion', 2);    // /forums/general-discussion/page/2/
$threadUrl = $router->getThreadUrl('general', 'my-thread'); // /forums/general/my-thread/
$profileUrl = $router->getProfileUrl(123);                  // /forums/member/123/
```

---

## Template Loader

### Loading Templates

```php
$templateLoader = new \PresszoneForumPlugin\TemplateLoader();

// Load a template
$templateLoader->load('presszone-forum-single-thread');

// Load with arguments
$templateLoader->load('presszone-forum-member', ['user' => $user]);

// Locate a template file (returns path or null)
$path = $templateLoader->locate('presszone-forum-single-thread');
$partPath = $templateLoader->locate('parts/navbar');
```

### Template Context Object ($fpz)

All templates receive a `$fpz` context object with helper methods:

```php
// URL helpers
$forumUrl = $fpz->url('inbox/');                    // /forums/inbox/
$assetUrl = $fpz->asset('js/frontend.js');          // /plugins/forum-press-zone/assets/js/frontend.js
$themeAsset = $fpz->themeAsset('style.css');        // /plugins/forum-press-zone/templates/presszone/assets/style.css

// Settings
$value = $fpz->setting('board_title', 'Forums');    // get_option('presszone_forum_board_title', 'Forums')

// User helpers
$isLoggedIn = $fpz->isLoggedIn();                   // is_user_logged_in()
$user = $fpz->currentUser();                        // wp_get_current_user() or null

// Escaping shortcuts
$safe = $fpz->esc($text);                           // esc_html()
$safe = $fpz->attr($text);                          // esc_attr()
$fpz->e($text);                                     // echo esc_html()

// Theme info
$theme = $fpz->theme();                             // 'presszone'
```

### Template Search Order

1. Child theme: `/forum-press-zone/{theme}/{template}.php`
2. Parent theme: `/forum-press-zone/{theme}/{template}.php`
3. Plugin: `/templates/{theme}/{template}.php`
4. Fallback to default theme if active theme missing template

---

## Template Helper Functions

### Avatar Helper

```php
// Render user avatar with custom upload support
echo presszone_forum_get_avatar([
    'user_id' => $post['user_id'],
    'avatar_type' => $post['avatar_type'] ?? 'gravatar',
    'avatar_path' => $post['avatar_path'] ?? ''
], 40); // size in pixels

// Supports:
// - 'gravatar' - WordPress/Gravatar avatar
// - 'upload' - Custom uploaded avatar
// - 'default' - Mystery person
```

### Time Diff Helper

```php
// Custom human time diff with singular week handling
echo presszone_human_time_diff(strtotime($post['post_date']), time());
// Output: "5 minutes", "1 week", "3 months"

// In template context
printf(
    esc_html__('%s ago', 'forum-press-zone'),
    presszone_human_time_diff(strtotime($date), time())
);
```

### Empty State Component

```php
presszone_forum_empty_state([
    'title' => __('No threads found', 'forum-press-zone'),
    'message' => __('Be the first to start a conversation!', 'forum-press-zone'),
    'cta_text' => __('Create Thread', 'forum-press-zone'),
    'cta_url' => home_url('/forums/general/new-thread/'),
    'icon' => '🕵️', // optional, default is 🕵️
]);
```

---

## Reusable PHP Components

### Core Classes - USE THESE

| Class | Purpose |
|-------|---------|
| `Cache` | Caching layer (Redis/Memcached/WP transients) |
| `Query` | Database queries with automatic caching |
| `PostCreator` | Thread/post creation, content validation, BBCode parsing |
| `RestBase` | Extend for REST API endpoints |
| `Roles` | Permission and capability checking |
| `Editor` | TinyMCE editor rendering |

### PostCreator Methods - REUSE THESE

| Method | Purpose | Throws |
|--------|---------|--------|
| `checkExternalLinks($content)` | Block external URLs if setting disabled | `InvalidArgumentException` |
| `checkWordFilter($content)` | Block banned words/phrases | `InvalidArgumentException` |
| `sanitizeMessage($content)` | HTML→BBCode conversion, sanitization | - |
| `parseMessage($content)` | BBCode→HTML for display | - |
| `htmlQuotesToBBCode($html)` | Convert nested blockquotes to BBCode (DOMDocument) | - |

```php
// Usage pattern
$creator = new PostCreator();
try {
    $creator->checkExternalLinks($message);
    $creator->checkWordFilter($message);
    $sanitized = $creator->sanitizeMessage($message);
} catch (\InvalidArgumentException $e) {
    // Handle validation error
}
```

---

### User Role Class Helper

```php
// Get CSS class for role-based styling
$roleClass = \PresszoneForumPlugin\UserRanks::getUserRoleClass((int) $post['user_id']);
// Returns: 'presszone-forum-role--admin', 'presszone-forum-role--moderator', etc.

// Usage in template
<a class="<?php echo esc_attr($roleClass); ?>">
    <?php echo esc_html($username); ?>
</a>
```

---

## Breadcrumbs

### Rendering Breadcrumbs

```php
$breadcrumbs = new \PresszoneForumPlugin\Breadcrumbs();

// For a node page
$breadcrumbs->render((int) $node['node_id']);

// For a thread page (includes thread title)
$threadUrl = home_url($slugBase . '/' . $nodeSlug . '/' . $threadSlug . '/');
$breadcrumbs->render((int) $thread['node_id'], $thread['title'], $threadUrl);
```

### Output Structure

```html
<nav class="presszone-forum-breadcrumbs" aria-label="Breadcrumb">
    <ol class="presszone-forum-breadcrumbs__list">
        <li class="presszone-forum-breadcrumbs__item">
            <a href="https://example.com/">Home</a>
        </li>
        <li class="presszone-forum-breadcrumbs__item">
            <a href="https://example.com/forums/">Forums</a>
        </li>
        <li class="presszone-forum-breadcrumbs__item presszone-forum-breadcrumbs__item--current">
            <span>General Discussion</span>
        </li>
    </ol>
</nav>
<script type="application/ld+json" class="presszone-forum-schema">
    {"@context":"https://schema.org","@type":"BreadcrumbList",...}
</script>
```

---

## Pagination

### Using Pagination Helper

```php
use PresszoneForumPlugin\Pagination;

// Get current page
$page = Pagination::getCurrentPage();

// Get items per page
$perPage = Pagination::getPerPage();

// Calculate total pages
$totalPages = Pagination::getPageCount($totalItems);

// Render navigation
echo Pagination::renderNav($page, $totalPages, $baseUrl);

// Render compact page indicators (for thread listings)
echo Pagination::renderCompact($pageCount, $threadUrl);
```

---

## Template Patterns

### Semantic HTML5 Structure

**Use appropriate semantic elements for content.**

| Content Type | Element |
|--------------|---------|
| Page section | `<section>` with heading |
| Self-contained content | `<article>` |
| Navigation | `<nav>` |
| Supplementary content | `<aside>` |
| Group of links | `<nav>` |
| Time/date | `<time datetime="...">` |

```php
// GOOD - Semantic structure
<article class="presszone-forum-thread-row" role="row">
    <header class="presszone-forum-thread-row__header">
        <h3><?php echo esc_html($title); ?></h3>
    </header>
    <div class="presszone-forum-thread-row__content">
        <?php echo wp_kses_post($content); ?>
    </div>
    <footer class="presszone-forum-thread-row__meta">
        <time datetime="<?php echo esc_attr($date); ?>">
            <?php echo esc_html($formatted_date); ?>
        </time>
    </footer>
</article>
```

### Basic Template Structure

```php
<?php
declare(strict_types=1);

if (!defined('ABSPATH')) {
    exit;
}

/**
 * Template: Template Name
 *
 * @package ForumPressZone
 * @var object $fpz Template context.
 */

use PresszoneForumPlugin\Query;
use PresszoneForumPlugin\Breadcrumbs;

$query = new Query();
$breadcrumbs = new Breadcrumbs();
$slugBase = get_option('presszone_forum_seo_slug_base', PRESSZONE_FORUM_DEFAULT_SLUG_BASE);

// Get route data
$nodeSlug = get_query_var('presszone_forum_node_slug');

// Fetch data
$node = $query->getNodeBySlug($nodeSlug);

// Handle not found
if (!$node) {
    status_header(404);
    echo '<div class="presszone-forum-error">';
    echo '<span class="presszone-forum-error__icon">💭</span>';
    echo '<p>' . esc_html__('Not found', 'forum-press-zone') . '</p>';
    echo '</div>';
    return;
}
?>
<div class="presszone-forum-container">
    <?php $breadcrumbs->render((int) $node['node_id']); ?>

    <header class="presszone-forum-page-header">
        <h1><?php echo esc_html($node['title']); ?></h1>
    </header>

    <!-- Template content here -->
</div>
```

### Form Pattern

```php
<form action="<?php echo esc_url(admin_url('admin-post.php')); ?>"
      method="post"
      class="presszone-forum-form"
      enctype="multipart/form-data">

    <?php wp_nonce_field('presszone_forum_action', 'presszone_forum_nonce'); ?>
    <input type="hidden" name="action" value="presszone_forum_action">

    <div class="presszone-forum-form-group">
        <label for="title" class="presszone-forum-label">
            <?php esc_html_e('Title', 'forum-press-zone'); ?>
        </label>
        <input type="text"
               id="title"
               name="title"
               class="presszone-forum-input"
               value="<?php echo esc_attr($existing_value ?? ''); ?>"
               required>
    </div>

    <div class="presszone-forum-form-actions">
        <button type="submit" class="presszone-forum-btn presszone-forum-btn--primary">
            <?php esc_html_e('Submit', 'forum-press-zone'); ?>
        </button>
    </div>
</form>
```

### Moderation Actions Pattern

```php
<?php if (current_user_can('manage_options')
       || current_user_can('presszone_forum_moderate_all')
       || current_user_can('presszone_forum_moderate')): ?>
    <div class="presszone-forum-actions-dropdown">
        <button type="button"
                class="presszone-forum-actions-dropdown__trigger"
                title="<?php esc_attr_e('Actions', 'forum-press-zone'); ?>">
            ⋮
        </button>
        <div class="presszone-forum-actions-dropdown__menu">
            <a href="<?php echo esc_url(home_url($slugBase . '/edit-post/' . $post_id . '/')); ?>"
               class="presszone-forum-actions-dropdown__item">
                ✏️ <?php esc_html_e('Edit', 'forum-press-zone'); ?>
            </a>

            <a href="<?php echo esc_url(wp_nonce_url(
                admin_url('admin-post.php?action=presszone_forum_delete&id=' . $post_id),
                'presszone_forum_delete',
                'presszone_forum_nonce'
            )); ?>"
               class="presszone-forum-actions-dropdown__item presszone-forum-actions-dropdown__item--danger"
               onclick="return confirm('<?php esc_attr_e('Are you sure?', 'forum-press-zone'); ?>');">
                🗑️ <?php esc_html_e('Delete', 'forum-press-zone'); ?>
            </a>
        </div>
    </div>
<?php endif; ?>
```

### Recursive Template (Nested Posts)

```php
// In template file - define function with existence check
if (!function_exists('presszone_forum_render_nested_post')) {
    function presszone_forum_render_nested_post(
        array $post,
        int $depth,
        object $fpz,
        Query $query,
        array $thread,
        bool $enableReactions,
        int $userId
    ): void {
        $maxDepth = 6;

        ?>
        <article class="presszone-forum-post"
                 id="post-<?php echo esc_attr((string) $post['post_id']); ?>"
                 data-depth="<?php echo esc_attr((string) $depth); ?>">

            <!-- Post content -->

            <?php
            // Render children recursively
            if (!empty($post['children']) && $depth < $maxDepth):
                foreach ($post['children'] as $child):
                    presszone_forum_render_nested_post(
                        $child,
                        $depth + 1,
                        $fpz,
                        $query,
                        $thread,
                        $enableReactions,
                        $userId
                    );
                endforeach;
            endif;
            ?>
        </article>
        <?php
    }
}

// Call the function
foreach ($posts as $post):
    presszone_forum_render_nested_post($post, 0, $fpz, $query, $thread, $enableReactions, $userId);
endforeach;
```

---

## Common Mistakes to Avoid

| Mistake | Fix |
|---------|-----|
| Missing `defined('ABSPATH')` check | Add `if (!defined('ABSPATH')) exit;` |
| Echo without escaping | Always use `esc_html()`, `esc_attr()`, etc. |
| Missing text domain | Add `'forum-press-zone'` to all strings |
| `$_POST` without sanitization | Use `sanitize_text_field(wp_unslash())` |
| Missing nonce in forms | Add `wp_nonce_field()` |
| Short function prefix | Use `presszone_forum_*` (4+ chars) |
| Using `FPZ\` namespace | Use `PresszoneForumPlugin` |
| Hardcoded URLs | Use `home_url()`, `admin_url()` |
| Raw database queries | Use `$wpdb->prepare()` |
| Missing permission checks | Check `current_user_can()` before actions |
| Trusting query vars | Validate and sanitize all input |
| Not using `wp_safe_redirect()` | Always use for redirects |
| Missing `exit` after redirect | Always `exit` after `wp_redirect` |
| Using `echo` for JS in templates | Use `wp_json_encode()` + `esc_js()` |
| Inline `onclick` with user data | Escape with `esc_attr()` or use data attributes |
| `<style>` tags in templates | Move to SCSS files under `assets/scss/` |
| Inline `style=` attributes | Use utility classes (`presszone-forum-mt-sm`, etc.) |
| Using `wp_add_inline_style()` | Write to static CSS file in uploads |
| Using `<table>` elements | NEVER - use `<div>` with CSS Grid/Flexbox |
| Polluting `<html>` or `<body>` tags | Use component wrapper classes instead |
| Non-semantic `<div>` soup | Use `<article>`, `<section>`, `<nav>`, `<header>`, `<footer>` |
| Missing ARIA roles on custom widgets | Add `role="table"`, `role="row"`, `role="cell"` for grid layouts |

---

## Schema.org Integration

### Thread Schema

```php
// Generate and set thread schema
$schemaData = \PresszoneForumPlugin\Schema::generateForThread(
    $thread,     // Thread data
    $opPost,     // Original post
    $replies,    // Reply array
    $threadUrl   // Full thread URL
);
\PresszoneForumPlugin\Schema::set($schemaData);
```

### Forum Schema

```php
$nodeUrl = home_url($slugBase . '/' . $node['slug'] . '/');
$schemaData = \PresszoneForumPlugin\Schema::generateForForum($node, $nodeUrl);
\PresszoneForumPlugin\Schema::set($schemaData);
```

---

## CSS Class Conventions

### Class Hygiene

**Keep HTML clean - don't pollute global elements.**

- Do NOT add custom classes to `<html>` or `<body>` directly in templates
- The plugin adds `presszone-forum-template` and `presszone-forum-dark` via PHP hooks
- Target child elements via semantic tags or parent context
- Use BEM classes on component root elements

```php
// BAD - Polluting body tag in template
<body class="my-custom-class">

// GOOD - Class on component wrapper
<div class="presszone-forum-component">
```

### BEM Naming

```php
// Block
<div class="presszone-forum-card">

// Element
<div class="presszone-forum-card__header">
<div class="presszone-forum-card__body">

// Modifier
<div class="presszone-forum-card presszone-forum-card--featured">
<div class="presszone-forum-btn presszone-forum-btn--primary">
```

### Common Classes

| Class | Purpose |
|-------|---------|
| `presszone-forum-container` | Main content wrapper |
| `presszone-forum-page-header` | Page header with title |
| `presszone-forum-form` | Form wrapper |
| `presszone-forum-form-group` | Form field wrapper |
| `presszone-forum-input` | Text input |
| `presszone-forum-btn` | Button base |
| `presszone-forum-btn--primary` | Primary action button |
| `presszone-forum-btn--secondary` | Secondary action button |
| `presszone-forum-btn--ghost` | Ghost/transparent button |
| `presszone-forum-error` | Error message container |
| `presszone-forum-empty` | Empty state container |
| `presszone-forum-text-muted` | Muted/secondary text |

---

## Self-Learning Protocol

When you learn new patterns or make significant changes, update this file:

### When to Update

1. **New template created** - Add to Directory Structure
2. **New helper function** - Add to Template Helper Functions
3. **New route added** - Add to URL Patterns
4. **New CSS class pattern** - Add to CSS Class Conventions
5. **Bug pattern identified** - Add to Common Mistakes
6. **Major refactor** - Update affected sections

### Update Format

Add entries to Recent Updates section below with date and description.

---

## Recent Updates

*This section tracks changes to the agent's knowledge base.*

- **2025-01-04** - Initial creation with full frontend PHP knowledge

---

## Quick Reference - Prefixes & Naming

| Context | Prefix/Pattern | Example |
|---------|----------------|---------|
| **PHP Namespace** | `PresszoneForumPlugin` | `namespace PresszoneForumPlugin;` |
| **Global Functions** | `presszone_forum_` | `presszone_forum_get_avatar()` |
| **Query Vars** | `presszone_forum_` | `presszone_forum_route` |
| **Text Domain** | `forum-press-zone` | `__('Text', 'forum-press-zone')` |
| **CSS Classes** | `presszone-forum-` | `.presszone-forum-btn` |
| **Nonce Action** | `presszone_forum_` | `presszone_forum_create_post` |
| **Option Keys** | `presszone_forum_` | `presszone_forum_board_title` |
| **DB Tables** | `wp_presszone_forum_` | `wp_presszone_forum_posts` |

**NEVER USE:** `FPZ`, `pz_`, `fpz_` - Short prefixes cause WordPress.org rejection

---

## 🔒 MANDATORY SECURITY & COMPLIANCE RULES

### WordPress.org Security Compliance (ZERO TOLERANCE)

#### Output Escaping - ALWAYS Required
```php
// CORRECT - Every output MUST be escaped
echo esc_html($text);              // Plain text
echo esc_attr($value);             // HTML attributes  
echo esc_url($url);                // URLs
echo wp_kses_post($html);          // HTML with allowed tags
echo esc_textarea($content);       // Textarea content
echo esc_js($string);              // JavaScript strings

// In HTML context
<h1><?php echo esc_html($title); ?></h1>
<a href="<?php echo esc_url($link); ?>">
<input value="<?php echo esc_attr($value); ?>">

// FORBIDDEN - Will cause plugin rejection
echo $text;                        // NEVER - XSS vulnerability
<?php echo $title; ?>              // NEVER - must escape
```

#### Input Sanitization - ALWAYS Required
```php
// GET/POST data - ALWAYS sanitize + unslash
$text = sanitize_text_field(wp_unslash($_POST['field']));
$content = wp_kses_post(wp_unslash($_POST['content']));
$id = absint($_GET['id']);
$slug = sanitize_key($_GET['slug']);
$url = esc_url_raw(wp_unslash($_POST['url']));
$email = sanitize_email(wp_unslash($_POST['email']));

// Query vars (already sanitized by WordPress)
$route = get_query_var('presszone_forum_route');
$page = max(1, (int) get_query_var('presszone_forum_page'));

// FORBIDDEN
$text = $_POST['field'];           // NEVER - must sanitize
```

#### Nonce Verification - ALWAYS Required
```php
// In forms
<?php wp_nonce_field('presszone_forum_action', 'presszone_forum_nonce'); ?>

// In handlers
if (!wp_verify_nonce(
    sanitize_text_field(wp_unslash($_POST['presszone_forum_nonce'] ?? '')),
    'presszone_forum_action'
)) {
    wp_die(esc_html__('Security check failed.', 'forum-press-zone'));
}

// URL-based nonces
$url = wp_nonce_url($url, 'presszone_forum_action', 'presszone_forum_nonce');
check_admin_referer('presszone_forum_action', 'presszone_forum_nonce');
```

#### Permission Checks - MANDATORY
```php
// Admin check
if (!current_user_can('manage_options')) {
    wp_die(esc_html__('Unauthorized', 'forum-press-zone'));
}

// Moderator check
if (!\PresszoneForumPlugin\Roles::canModerate()) {
    wp_die(esc_html__('Unauthorized', 'forum-press-zone'));
}

// Logged-in check
if (!is_user_logged_in()) {
    wp_safe_redirect(wp_login_url(get_permalink()));
    exit;
}

// Owner check (e.g., can edit own post)
$userId = get_current_user_id();
if ((int) $post['user_id'] !== $userId && !current_user_can('edit_others_posts')) {
    wp_die(esc_html__('Unauthorized', 'forum-press-zone'));
}
```

### Security Rules - ABSOLUTE REQUIREMENTS

#### SQL Injection Prevention
```php
// ALWAYS use $wpdb->prepare() for dynamic queries
$results = $wpdb->get_results(
    $wpdb->prepare(
        "SELECT * FROM {$table} WHERE user_id = %d AND status = %s",
        $userId,
        'active'
    ),
    ARRAY_A
);

// FORBIDDEN - SQL injection vulnerability
$results = $wpdb->get_results("SELECT * FROM {$table} WHERE user_id = {$userId}");
```

#### File System Security
```php
// ALWAYS use WP Filesystem API
global $wp_filesystem;
WP_Filesystem();
$wp_filesystem->put_contents($file, $data);

// NEVER use direct file functions
file_put_contents($file, $data);  // FORBIDDEN
```

#### JSON Validation
```php
// ALWAYS validate JSON after decode
$data = json_decode($json, true);
if (!is_array($data)) {
    wp_die(esc_html__('Invalid data format', 'forum-press-zone'));
}
```

### NO INLINE CSS - ABSOLUTE BAN

```php
// FORBIDDEN - Never use inline style injection
wp_add_inline_style('handle', $css);  // NEVER

// FORBIDDEN - Never embed <style> tags in templates
<style>.my-class { color: red; }</style>

// FORBIDDEN - Never use inline style attributes
<div style="margin-top: 10px;">  // NEVER
```

**All styles MUST be in SCSS files:**
- Components: `assets/scss/components/_component-name.scss`
- Pages: `assets/scss/pages/_page-name.scss`

**For dynamic values, use data attributes + CSS:**
```php
// CORRECT - data attribute in HTML
<div class="presszone-forum-nested" data-indent="<?php echo esc_attr($indent); ?>">

// CORRECT - CSS handles the calculation
.presszone-forum-nested[data-indent="1"] { margin-left: 12px; }
.presszone-forum-nested[data-indent="2"] { margin-left: 24px; }
```

### Accessibility Rules - MANDATORY

#### Semantic HTML Structure
```php
// ALWAYS use appropriate semantic elements
<article class="presszone-forum-thread-row" role="row">
    <header class="presszone-forum-thread-row__header">
        <h3><?php echo esc_html($title); ?></h3>
    </header>
    <div class="presszone-forum-thread-row__content">
        <?php echo wp_kses_post($content); ?>
    </div>
    <footer class="presszone-forum-thread-row__meta">
        <time datetime="<?php echo esc_attr($date); ?>">
            <?php echo esc_html($formatted_date); ?>
        </time>
    </footer>
</article>
```

#### ARIA Labels and Roles
```php
// ALWAYS add ARIA labels to interactive elements
<button type="button" 
        class="presszone-forum-actions-dropdown__trigger"
        aria-label="<?php esc_attr_e('Post actions', 'forum-press-zone'); ?>"
        aria-expanded="false"
        aria-haspopup="true">
    ⋮
</button>

// ALWAYS add roles to custom elements
<div class="presszone-forum-thread-list" role="table" aria-label="<?php esc_attr_e('Thread list', 'forum-press-zone'); ?>">
    <div class="presszone-forum-thread-row" role="row">
        <div class="presszone-forum-thread-cell" role="cell">
            <?php echo esc_html($title); ?>
        </div>
    </div>
</div>
```

#### Keyboard Navigation
```php
// ALWAYS ensure keyboard accessibility
<a href="<?php echo esc_url($thread_url); ?>" 
   class="presszone-forum-thread-link"
   tabindex="0">
    <?php echo esc_html($title); ?>
</a>

// NEVER use javascript:void(0) - use buttons instead
<button type="button" onclick="handleAction()">
    <?php esc_html_e('Action', 'forum-press-zone'); ?>
</button>
```

#### Screen Reader Support
```php
// ALWAYS provide text alternatives for visual indicators
<span class="presszone-forum-status-icon" 
      aria-label="<?php echo $is_locked ? esc_attr__('Thread locked', 'forum-press-zone') : esc_attr__('Thread open', 'forum-press-zone'); ?>">
    <?php echo $is_locked ? '🔒' : '💬'; ?>
</span>

// ALWAYS use proper heading hierarchy
<h1><?php echo esc_html($forum_title); ?></h1>
<h2><?php echo esc_html($thread_title); ?></h2>
<h3><?php esc_html_e('Replies', 'forum-press-zone'); ?></h3>
```

### Data Protection & Privacy

#### Personal Data Handling
```php
// ALWAYS anonymize personal data in logs
error_log('User action: user_id=' . $user_id . ', action=delete_post');  // OK - ID only
error_log('User action: ' . $user_email . ' deleted post');  // FORBIDDEN - contains PII

// ALWAYS respect privacy settings
if (get_user_meta($user_id, 'presszone_forum_hide_email', true)) {
    $email = __('Hidden', 'forum-press-zone');
} else {
    $email = esc_html($user->user_email);
}
```

#### GDPR Compliance
```php
// ALWAYS provide data export capability
function export_user_forum_data($user_id) {
    $data = [];
    
    // Posts
    $posts = $wpdb->get_results($wpdb->prepare(
        "SELECT post_id, message, post_date FROM {$table} WHERE user_id = %d",
        $user_id
    ));
    
    foreach ($posts as $post) {
        $data['posts'][] = [
            'id' => $post->post_id,
            'content' => $post->message,
            'date' => $post->post_date
        ];
    }
    
    return $data;
}

// ALWAYS provide data deletion capability
function delete_user_forum_data($user_id) {
    // Soft delete posts
    $wpdb->update(
        $table,
        [
            'is_soft_deleted' => 1,
            'deleted_at' => current_time('mysql'),
            'deleted_by' => $user_id
        ],
        ['user_id' => $user_id],
        ['%d', '%s', '%d'],
        ['%d']
    );
}
```

### Performance & Caching Rules

#### Database Query Optimization
```php
// ALWAYS limit query results
$posts = $wpdb->get_results(
    $wpdb->prepare(
        "SELECT * FROM {$table} WHERE thread_id = %d LIMIT %d OFFSET %d",
        $thread_id,
        $per_page,
        $offset
    )
);

// ALWAYS use indexes in WHERE clauses
// Good: WHERE thread_id = %d (indexed)
// Bad: WHERE YEAR(post_date) = %d (can't use index)
```

#### Cache Integration
```php
// ALWAYS check cache first
$cache_key = "thread_{$thread_id}_posts";
$posts = wp_cache_get($cache_key, 'presszone_forum');

if (false === $posts) {
    $posts = $wpdb->get_results(/* expensive query */);
    wp_cache_set($cache_key, $posts, 'presszone_forum', 3600);
}
```

### Error Handling & Logging

#### User-Friendly Error Messages
```php
// ALWAYS provide helpful error messages
if (!$thread) {
    status_header(404);
    echo '<div class="presszone-forum-error">';
    echo '<span class="presszone-forum-error__icon">💭</span>';
    echo '<h2>' . esc_html__('Thread Not Found', 'forum-press-zone') . '</h2>';
    echo '<p>' . esc_html__('The thread you are looking for does not exist or has been removed.', 'forum-press-zone') . '</p>';
    echo '</div>';
    return;
}
```

#### Graceful Degradation
```php
// ALWAYS provide fallbacks for failed operations
try {
    $posts = $query->getNestedPosts($thread_id);
} catch (Exception $e) {
    error_log('Forum query failed: ' . $e->getMessage());
    
    // Fallback to simple list
    $posts = $query->getSimplePosts($thread_id);
    
    if (empty($posts)) {
        echo '<div class="presszone-forum-error">';
        echo '<p>' . esc_html__('Unable to load posts at this time. Please try again later.', 'forum-press-zone') . '</p>';
        echo '</div>';
        return;
    }
}
```

### Translation & Internationalization

#### Text Domain Consistency
```php
// ALWAYS use 'forum-press-zone' text domain
__('Hello', 'forum-press-zone')           // CORRECT
esc_html__('Hello', 'forum-press-zone')   // CORRECT
_e('Hello', 'forum-press-zone')           // CORRECT

// FORBIDDEN
__('Hello')                               // Missing domain
__('Hello', 'presszone-forum')           // Wrong domain
```

#### Context and Comments
```php
// ALWAYS provide translator context for ambiguous terms
_x('Post', 'noun: forum post', 'forum-press-zone');
_x('Post', 'verb: to post', 'forum-press-zone');

// ALWAYS add translator comments for placeholders
/* translators: %1$s: user name, %2$s: time ago */
printf(
    esc_html__('Posted by %1$s %2$s ago', 'forum-press-zone'),
    esc_html($author_name),
    esc_html($time_ago)
);
```

#### Pluralization
```php
// ALWAYS handle pluralization correctly
$message = sprintf(
    _n(
        '%d reply',
        '%d replies', 
        $reply_count,
        'forum-press-zone'
    ),
    number_format_i18n($reply_count)
);
```

### Testing Requirements

#### Template Testing
```php
// ALWAYS test template rendering
class TestTemplates extends WP_UnitTestCase {
    public function test_thread_template_renders_without_errors() {
        $thread_id = $this->factory->post->create();
        
        ob_start();
        include PRESSZONE_FORUM_PATH . 'templates/presszone/single-thread.php';
        $output = ob_get_clean();
        
        $this->assertNotEmpty($output);
        $this->assertStringContains('presszone-forum-thread', $output);
    }
}
```

#### Security Testing
```php
// ALWAYS test XSS prevention
public function test_user_input_is_escaped() {
    $malicious_input = '<script>alert("xss")</script>';
    
    ob_start();
    echo esc_html($malicious_input);
    $output = ob_get_clean();
    
    $this->assertStringNotContains('<script>', $output);
    $this->assertStringContains('&lt;script&gt;', $output);
}
```

---

## Testing Checklist

Before submitting template changes:

- [ ] All output escaped (`esc_html`, `esc_attr`, `esc_url`, `wp_kses_post`)
- [ ] All input sanitized (`sanitize_text_field`, `absint`, etc.)
- [ ] All forms have nonce fields
- [ ] All strings have text domain `'forum-press-zone'`
- [ ] `defined('ABSPATH')` check present
- [ ] No hardcoded URLs (use `home_url()`, `admin_url()`)
- [ ] Permission checks before sensitive actions
- [ ] Exit after redirects
- [ ] Function prefix is 4+ characters
- [ ] No PHP errors with WP_DEBUG enabled
