---
name: phpue
description: "Build, edit, debug and review PHPue apps: .pvue single-file components compiled to PHP by conversion.php and served by index.php. Covers PHPue 0.0.3 (and 0.0.2 projects) Vanilla (script/header/template/cscript blocks, mustache output, p-if, p-for, @require, #require, @AJAX, the View placeholder, backend/ autoloading, PHPUE_SESSION lazy sessions, phpue_set_lang, httpReqs 404 pages, .dist builds and deployment) and PHPue + AlpineJS + the x-ssr extension (client-side updates that keep server-rendered lists). Use whenever a project has App.pvue, views/*.pvue, components/*.pvue, conversion.php or alpine.ext.*.ssr.js, or the user mentions PHPue, .pvue files, p-directives, @AJAX, cscript or x-ssr."
---

# PHPue

PHPue (current release **0.0.3**, by Edward Patch, https://phpue.co.uk) is a **server-side-rendering PHP framework** that uses Vue-*like* single-file components. `conversion.php` turns `.pvue` files into plain PHP (0.0.3: a top-level block parser plus tag-aware directive conversion; 0.0.2: plain regular expressions). `index.php` routes, serves and builds. It is **not Vue**: there is no virtual DOM, no reactivity, no slots or emits, and no hydration of its own. There is also no prop API, by design: a component is inlined into its parent, so it already sees every variable the parent has (plain PHP scope, the same as `include`). Everything inside `{{ }}`, `p-if` and `p-for` runs **once on the server**.

Core ideas:
- **PHP owns the server. The browser layer is optional.** Client behaviour comes from plain JS in `<cscript>`, `@AJAX` calls, or (optionally) AlpineJS.
- **Raw PHP is always allowed and idiomatic.** When a directive can't express something, use `<?php if … ?>`, `<?php foreach … ?>` or `<?= … ?>` in the template. The author does this all the time, so it is not a hack.
- **Privacy-first forms.** There is no input binding or keystroke tracking. Users submit forms on purpose (explicit `fetch` to `@AJAX`, or a normal POST). `p-model` still exists in `conversion.php` only as a leftover placeholder that the official release deliberately abandoned, so **never use it**. Write `name="email" value="{{ $email }}"` instead.
- **No mandatory toolchain.** No bundler and no npm (except optional Tailwind CLI). A PHP server is enough: **0.0.3 needs PHP 8.0+** (`conversion.php` calls `str_contains()`), even though the PHPue README badge still says 7.4+.

## Step 1: Work out the version and the flavour

**Version first.** The rules differ a lot between releases:
```bash
grep -m1 PHPUE_VERSION path/to/www/index.php      # define('PHPUE_VERSION', '0.0.3');
```
0.0.3 is a bug-fix release that removed most of 0.0.2's markup traps. The hard rules below are written for 0.0.3; lines marked **0.0.2:** are the stricter rules that still apply to 0.0.2 projects. Never "fix" working 0.0.3 markup into the 0.0.2 shape, and never write 0.0.3-only markup into a 0.0.2 project. Upgrading a project = replacing **both** `index.php` and `conversion.php` (they are one release).

| Signal in the project | Flavour | Read |
|---|---|---|
| Only `.pvue` + `conversion.php` + `index.php`, interactivity via `onclick="fn()"` and `fetch('')` in `<cscript>` | **PHPue Vanilla** | `references/vanilla.md` |
| `alpine.min.js` **and** `phpue-ext-csr/alpine.ext.*.ssr.js` in an App or view `<header>`, `x-data`, `x-ssr` | **PHPue + AlpineJS + x-ssr** | `references/vanilla.md` **then** `references/alpine-xssr.md` |
| Any build, deploy, routing, 404, hot reload, dev-tool or `.dist` question | both | `references/runtime-build-deploy.md` |
| Something renders wrong, a PHP parse error appears in compiled output, you're writing tricky markup, or you're upgrading 0.0.2 → 0.0.3 | both | `references/engine-gotchas.md` |
| SPA-like navigation, HTMX, partial page updates, page caching (NGINX), sessions vs cacheability | both | `references/htmx-spa.md` |
| SEO/meta tags, a blog or CMS, page content from files, **multilingual sites** (language packs, switcher, hreflang, per-language routes), pretty URLs | both | `references/extensions.md`: use the **official/author extensions** (MetaControl, Matrix CMS, x-ssr, LanguageSelector) instead of hand-rolling them |

The Alpine flavour is **PHPue Vanilla plus Alpine**. All Vanilla rules still apply, because the `.pvue` file is still compiled by the same engine.

Starter files live in `templates/` as a small app that passes the linter (0.0.2 and 0.0.3 rules) and runs: `templates/www/` (App.pvue, `views/index.pvue` for Vanilla AJAX + session, `views/contact.pvue` for an explicit form submit, `views/catalogue.pvue` for Alpine + x-ssr, `components/ui/` with Navbar and a ProductCard that reads the parent's loop variable, a `backend/` singleton, `httpReqs/` 404 files, `.htaccess`, Tailwind config, Alpine + x-ssr JS) plus `templates/docker-compose.yaml` and `templates/nginx.default.conf`. Copy them and adapt; don't invent new structure. `index.php` and `conversion.php` come from the project or https://github.com/PHPue/PHPue.

## Step 2: Hard rules (break these and output silently breaks)

**File anatomy.** Each block appears at most once: `<script>` (PHP, not JS), `<header>` (head tags), `<template>` (HTML), `<cscript>` (client JS). Keep them in that order. Anything outside the blocks is ignored. `<script setup>` is also accepted for the PHP block. 0.0.3 only recognises blocks at the **top level** of the file, so tag names inside comments, strings or the template don't hijack blocks.

1. **`{{ }}` converts expressions that start with `$` or with a function/static call:** `{{ $user['name'] }}`, `{{ $a ? 'x' : 'y' }}`, `{{ count($items) }}`, `{{ \App\Fmt::money($p) }}`. Anything else (`{{ !$x }}`, `{{ (int)$x }}`, `{{ item.name }}`) is left as literal text, which is handy for Alpine/docs but means a PHP expression must start with `$` or a call. Never `{{ $user.name }}`: `.` is PHP concatenation. Output is always `htmlspecialchars`'d (arrays throw); for raw HTML use `<?= $html ?>`. Beware: Alpine-looking `{{ fn() }}` is compiled as a **PHP** call. **0.0.2:** only `$…` converts; `{{ count($x) }}` stays literal, so use `<?= count($x) ?>`.
2. **`p-if="cond"` works on any element in any layout:** single-line (`<p p-if="$x">Hi</p>`), multi-line opening tags, same-tag children, `>`/`=>`/`{{ }}` in the element's other attributes, single or double quotes, and void elements (`<input p-if="$x">`). The element **must be closed** (or be void/self-closing); otherwise only its opening tag is wrapped and the content always renders. Directives inside `<?php ?>`, HTML comments, `{{ }}` and `<script>`/`<style>` bodies are not converted. There is no `p-else`; use a negated second `p-if` or raw PHP. **0.0.2:** opening tag on **one line**, **nothing after `>` on that line**, children on the following lines, closing tag on its own line, no `>` and no `{{ }}`/`<?= ?>` in the p-if element's attributes, and no one-line same-tag child (`<div>x</div>` inside `<div p-if>`). Breaking these deletes markup or leaves the `if` open, sometimes while still passing `php -l`.
3. **`p-for="$item in <expr>"`** (or `"$k => $v in <expr>"`): `<expr>` can be any PHP expression, including function calls (`range(1, 3)`), arrays, generators and Traversables (`PDOStatement`, `ArrayIterator`). Non-iterables render nothing (`phpue_iterable()`). Nested same-tag children are fine. **`p-for` + `p-if` on one element** loops first and then filters each item: `<li p-for="$n in $nums" p-if="$n % 2 === 0">`. **0.0.2:** the source must be a variable/index (it's wrapped in `isset()`: a call is fatal, and objects are skipped), the loop ends at the **first** closing tag of the same name (never nest that tag), and `p-for` + `p-if` on one element breaks.
4. **Nested `<template>` elements are fine in 0.0.3** (`<template x-for>`, `<template x-if>`, `<template id>` for cloning) as long as each one is closed. An unclosed one makes the PHPue template swallow the rest of the file. **0.0.2:** the template ends at the **first** `</template>`, so never use any nested `<template>`; use `p-for`, `x-show` or `x-ssr`.
5. **Components** are declared with `@require Name 'path/from/www/root.pvue';` (0.0.3 also accepts double quotes) and used as `<Name/>`, `<Name />` or `<Name></Name>`. No other attributes and no children. The one 0.0.3 exception is `p-if`/`p-for` on the tag: `<Card p-for="$item in $items" />` renders the component once per item. Components need no props: they are inlined at compile time and **inherit every variable of the parent** at that point (a PHP feature, like `include`). Set or reuse a variable before the tag (`<?php $title = 'X'; ?><Card/>`, or a `p-for` loop variable) and default it in the component (`$title = $title ?? '';`). Don't invent a props mechanism (attributes, arrays of props, `extract()`) for inline components. **0.0.2:** single quotes only, and exactly `<Name/>` or `<Name></Name>` (with a space it isn't replaced).
6. **Views** are `views/*.pvue` (top level only). The file name is the route: `index` is `/` and `ue-insights` is `/ue-insights`. There are no nested paths, so use query strings. Register every view in `App.pvue` with `#require 'views/name.pvue';` (unnamed). The 0.0.2 build only generates `@AJAX` files for `#require`d views; 0.0.3 builds every view's handlers, but keep the convention. `<View/>` goes only in `App.pvue`.
7. **The view `<header>` is static HTML.** `{{ }}` and PHP there are **not evaluated** (`{{ $x }}` prints a literal `$x`). Only `App.pvue`'s header is dynamic. It's a PHP heredoc, so `{{ $var }}`, `{{ $arr['k'] }}` and `{{ $obj->prop }}` (0.0.3) plus native `{$arr[0]}` / `{$obj->prop}` are injected **raw** (escape when building the strings). Function calls are not converted, and `<?php ?>` there is printed literally. Dynamic per-page meta goes through the official **MetaControl** extension plus a backend meta router (`references/extensions.md`). **0.0.2:** only `{{ $simpleVar }}` converts in the App header; use `{$meta['title']}` for arrays.
8. **Scope.** App, views and components run inside framework methods. **App variables are not visible in views** or in `@AJAX` handlers. Share data with `$_SESSION`, `define()`, `$GLOBALS`, or `backend/` singletons (`Class::getInstance()`).
9. **`@AJAX('POST')` / `@AJAX('GET')`** goes directly before a `function name(...) {`. In 0.0.3, double quotes, typed parameters, defaults with parentheses and return types are all fine (`@AJAX("POST") function save(array $input = array()): void {`). The **declared method is enforced** (the other method gets a 405 JSON response), and **only `@AJAX` functions** can be called through `action` (anything else falls through to the normal page). Handlers must **`echo`** their response (usually `json_encode`), because return values are discarded. They are plain functions that run **before** the App script, so they see no App/view variables. Function names are global, so keep them unique app-wide. The client side is `fetch('', {method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({action:'name', ...})})` for POST, and `fetch('?action=name&x=1')` for GET. 0.0.3 builds component handlers too (`.dist/ajax/ajax-component-<Name>.php`). **0.0.2:** single quotes only, no return type, no `)` inside parameter defaults (otherwise `@AJAX(...)` is left in the PHP as a parse error), the method isn't enforced, **any** existing function (built-ins included) can be called by name, and handlers in **components** are dropped by the build, so put them in views or App.pvue.
10. **`<cscript>`** becomes a `<script>` tag. Inside it, `{{ $expr }}` becomes `json_encode($expr)` (0.0.3: any `$` expression such as `{{ $data['items'] }}`; **0.0.2:** simple variable names only). Never wrap it in quotes (`"{{ $name }}"` → `""Ada""`). Function calls aren't converted there: use `<?= json_encode(count($x)) ?>`. A bare `<script>` inside a 0.0.3 template is fine. **0.0.2:** a bare `<script>` in a template becomes the PHP block (the JS is executed **as PHP**) unless the PHP `<script>` block comes first.
11. **A block's own closing tag can't appear inside it.** A literal closing script tag inside the PHP block (even in a string or comment) ends the block, and the rest of your PHP is silently dropped or becomes a parse error. Split it: `'</scr' . 'ipt>'`. The same goes for `</cscript>` in cscript and `</header>` in the header. In the template, PHP blocks and comments are skipped when nesting is counted, but a `<template>`/`</template>` inside a string in an inline `<script>` still counts. **0.0.2:** the compiler doesn't understand comments at all. A literal `<header>`, `<template>`, `<cscript>`, `</script>` or `<title>` in any PHP comment, string or HTML comment is matched as the real thing (write "header block" / "title tag"), and a bare `<header>` element inside `<template>` is stripped (add an attribute: `<header class="…">`).
12. **Never declare named PHP functions or classes in a component** (they redeclare if the component is used twice or inside `p-for`). Put them in `backend/`. Top-level `let`/`const` in a component's `<cscript>` also collides when the component renders more than once.
13. **`__DIR__` in .pvue code changes with mode:** it's `www/` in source mode, but `www/.dist` (App) or `www/.dist/pages` (views) when compiled. Use `getcwd()` (always the web root) for file paths. **0.0.2:** `php index.php build` crashes if `components/` doesn't exist, so keep that folder even when it's empty.
14. **Sessions.** `PHPUE_SESSION` in `index.php`: `'auto'` (default, 0.0.2 behaviour) starts a session on every request, so every response gets `Set-Cookie` + `Cache-Control: no-store`. `'lazy'` only resumes an existing session, so anonymous pages stay cacheable. With `'lazy'`, call `phpue_session(true)` before **writing** `$_SESSION` during page rendering. POST `@AJAX` handlers get a session automatically; GET handlers don't. If the App script text contains `session_start()` (even in a comment), PHPue's session handling is skipped entirely. **0.0.2:** always auto-start; do lazy sessions by hand (`references/htmx-spa.md`). In both versions, `$_GET['page']` is reserved for the route (paginate with `?p=`).
15. **If `.dist/` exists, the server serves it and ignores your `.pvue` edits.** During development, delete `.dist/` or rebuild (`php index.php build` from `www/`).
16. **0.0.3 runtime APIs** (defined in `conversion.php`, available everywhere): `phpue_session(bool $write = false)`; `phpue_set_lang('cy')` sets `<html lang>` for this request (call it from the App script or `backend/`, because a view renders after `<html>` is printed; priority: `phpue_set_lang()` > view `<meta name="lang">` > `PHPUE_LANG`); `phpue_html_lang()`; `phpue_iterable($x)`. `PHPUE_DEV_TOOLS`: `?compile=`, `?hot-reload` and web `?build` only work in source mode (no `.dist/App.php` or `App.php`) unless you define it. Don't force it on in production.

## Step 3: Workflow when writing PHPue code

1. Check `PHPUE_VERSION`, then read `App.pvue`, a sibling view and a sibling component, and mirror their conventions: Tailwind classes, `/assets/...` absolute paths, `b`-prefixed booleans (`$bShowX`), singletons from `backend/`, and the 4-space indented blocks.
2. Put server data and logic in `<script>`, markup in `<template>`, head tags in `<header>` (static for views) and browser code in `<cscript>`.
3. Check compiled output whenever you use directives in non-trivial markup. `<skill-dir>` is this skill's folder: `~/.claude/skills/phpue` (Claude Code) or `~/.codex/skills/phpue` (Codex). It uses the project's own `conversion.php` and `PHPUE_VERSION`.
   ```bash
   cd path/to/www && php <skill-dir>/scripts/phpue-compile.php views/page.pvue          # view/component
   cd path/to/www && php <skill-dir>/scripts/phpue-compile.php App.pvue --root
   ```
4. Lint the project. It reads `PHPUE_VERSION` from `index.php` and applies the 0.0.2 or 0.0.3 rules (the first output line says which; override with `--version=0.0.3`). It flags the pitfalls above, reports the 0.0.3 compiler's own warnings (unclosed p-if/p-for elements, bad p-for syntax), and runs `php -l` on every compiled file:
   ```bash
   php <skill-dir>/scripts/phpue-lint.php path/to/www
   ```
   If a lint error contradicts what the page actually renders, trust the compiled output and the browser, and check the version line.
5. Run it: `cd www && php -S localhost:3000` and browse `http://localhost:3000` (use `localhost`, not 127.0.0.1; add `?live` for hot reload), or the project's `docker compose up`. Make sure there's no stale `.dist/`. Test `@AJAX` with `curl -X POST -H 'Content-Type: application/json' -d '{"action":"fn"}' http://localhost:3000/page` (0.0.3: a 405 means the handler is declared for the other method).

## Don't bring in

Vue (`v-if`, `:prop`, `@event` on components, `<style scoped>`, JavaScript inside `<script>`/`<script setup>`; that block is PHP), Blade (`@if`, `@foreach`), React/JSX, client routers, SPA hydration, npm bundlers, Composer at project root (vendor code goes in `backend/`), `p-else`/`p-show` (they don't exist), `p-model` (abandoned placeholder, see above), or (0.0.2 only) Alpine `<template>` tags. If you think a framework change is needed, say so instead of editing `conversion.php` or `index.php`. They're the framework core, and the same file is shared by every project.

## Reference map

- `references/vanilla.md`: full PHPue Vanilla reference (blocks, directives, components, views, App, AJAX, backend, routing helpers, styling, patterns from TechDemo).
- `references/alpine-xssr.md`: AlpineJS integration and the x-ssr directive in depth (setup, seeding state, SSR-to-CSR handoff, templates, `process()`, patterns from FreeCAD and Analytics).
- `references/runtime-build-deploy.md`: serving modes, dev tools, hot reload, `.dist` build output, deployment layouts, `.htaccess`/nginx, 404 pages, security notes.
- `references/extensions.md`: official extensions (MetaControl, Matrix CMS, x-ssr, plus LanguageSelector/PrettyUrlRedirect) with APIs, install paths and caveats. The repo is https://github.com/PHPue/PHPue-Extensions (one branch per category; clone it and read with `git show <branch>:<path>`).
- `references/htmx-spa.md`: PHPue + HTMX (hx-boost + hx-select, json-enc for `@AJAX`, `HX-Trigger`, `PHPueFragment` for standalone component partials, Alpine components across swaps) and the **NGINX cache / session** wall (`PHPUE_SESSION = 'lazy'` in 0.0.3, the manual lazy-session fix in 0.0.2).
- `references/engine-gotchas.md`: verified input-to-output behaviour of the compiler, split into what still bites in 0.0.3 and what only bites in 0.0.2 (the upgrade checklist).
- `templates/`: copy-ready starters.
- `scripts/phpue-compile.php`, `scripts/phpue-lint.php`: verification tools (need only PHP 8 CLI).
