<rss xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:wfw="http://wellformedweb.org/CommentAPI/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:sy="http://purl.org/rss/1.0/modules/syndication/" xmlns:slash="http://purl.org/rss/1.0/modules/slash/" xmlns:georss="http://www.georss.org/georss" xmlns:geo="http://www.w3.org/2003/01/geo/wgs84_pos#" version="2.0">

<channel data-v-component-posts="feed" data-v-limit="50" data-v-type="post" data-v-direction="desc" data-v-order_by="updated_at">	<title>News Vvveb</title>
	<link>https://vvveb.com/feed/posts</link>
	<description>The latest news about Vvveb</description>
	<language>en-US</language>
	<sy:updatePeriod>hourly</sy:updatePeriod>
	<sy:updateFrequency>1</sy:updateFrequency>
	<generator>https://vvveb.com</generator>

	<image>
		<url>/favicon.ico</url>
		<title>Vvveb News</title>
		<link>https://www.vvveb.com/feed/posts</link>
		<width>32</width>
		<height>32</height>
	</image> 

		<item data-v-post="post" data-v-id="430">
			<title data-v-post-name="name">Component product variants</title>
			<link data-v-post-full-url="name">https://dev.vvveb.com/component-product-variants</link>
			<dc:creator data-v-post-display_name="name"><![CDATA[Admin]]></dc:creator>
			<pubDate data-v-post-pubDate="date">Mon, 28 Sep 2026 17:24:02 +0000</pubDate>
			<category data-v-post-category-name="category" data-filter-cdata=""><![CDATA[Category]]></category>
			<description data-v-post-excerpt="excerpt" data-filter-cdata=""><![CDATA[The product-variants component (Vvveb\Component\Product\Variants) lists the variants of a product (combinations of option values, e.g. size × color) with variant-specific prices, images and URLs. 
Activate the component by adding:]]></description>
			<content:encoded data-v-post-content="content" data-filter-cdata=""><![CDATA[
<p>The <strong>product-variants</strong> component (<code>Vvveb\Component\Product\Variants</code>) lists the variants of a product (combinations of option values, e.g. size × color) with variant-specific prices, images and URLs.</p>
<p>Activate the component by adding:</p>
<pre><code>data-v-component-product-variants
</code></pre>
<h2>Component Options</h2>
<p>Add options as HTML attributes using the <code>data-v-*</code> syntax.</p>
<table class="table table-bordered">
<thead class="table-light">
<tr>
<th>Option</th>
<th>Attribute</th>
<th>Description</th>
<th>Default</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>start</strong></td>
<td><code>data-v-start</code></td>
<td>Starting offset</td>
<td><code>0</code></td>
</tr>
<tr>
<td><strong>limit</strong></td>
<td><code>data-v-limit</code></td>
<td>Number of variants (<code>null</code> = all)</td>
<td><code>null</code></td>
</tr>
<tr>
<td><strong>site_id</strong></td>
<td><code>data-v-site_id</code></td>
<td>Site ID</td>
<td>Current site</td>
</tr>
<tr>
<td><strong>language_id</strong></td>
<td><code>data-v-language_id</code></td>
<td>Language ID</td>
<td>Current language</td>
</tr>
<tr>
<td><strong>product_id</strong></td>
<td><code>data-v-product_id</code></td>
<td>Product ID, from URL by default</td>
<td><code>url</code></td>
</tr>
<tr>
<td><strong>parent_id</strong></td>
<td><code>data-v-parent_id</code></td>
<td>Filter by parent</td>
<td><code>null</code></td>
</tr>
<tr>
<td><strong>search</strong></td>
<td><code>data-v-search</code></td>
<td>Filter by text</td>
<td><code>null</code></td>
</tr>
<tr>
<td><strong>name</strong></td>
<td><code>data-v-name</code></td>
<td>Filter by variant name</td>
<td><code>null</code></td>
</tr>
<tr>
<td><strong>url</strong></td>
<td><code>data-v-url</code></td>
<td>Filter by variant URL</td>
<td><code>null</code></td>
</tr>
</tbody>
</table>
<h2>Component Properties</h2>
<p>Variants render inside <code>data-v-variant</code> repeatables with nested <code>data-v-value</code> entries for the option-value combination. Fields include variant <code>name</code>, <code>sku</code>, <code>price</code> / <code>price_formatted</code> (tax-aware), <code>image</code>, <code>url</code> / <code>full-url</code> and stock data. The component also exposes <code>count</code> and the <code>product_variant</code> list.</p>
<h2>HTML Example</h2>
<pre><code class="language-html">&lt;div data-v-component-product-variants&gt;
	&lt;div data-v-variant&gt;
		&lt;span data-v-variant-name&gt;&lt;/span&gt;
		&lt;span data-v-variant-price_formatted&gt;&lt;/span&gt;
	&lt;/div&gt;
&lt;/div&gt;
</code></pre>
]]></content:encoded>		
		</item><item data-v-post="post" data-v-id="431">
			<title data-v-post-name="name">Component fields</title>
			<link data-v-post-full-url="name">https://dev.vvveb.com/component-fields</link>
			<dc:creator data-v-post-display_name="name"><![CDATA[Admin]]></dc:creator>
			<pubDate data-v-post-pubDate="date">Mon, 28 Sep 2026 17:24:02 +0000</pubDate>
			<category data-v-post-category-name="category" data-filter-cdata=""><![CDATA[Category]]></category>
			<description data-v-post-excerpt="excerpt" data-filter-cdata=""><![CDATA[The fields component (Vvveb\Component\Fields) renders custom field values for posts, products, users, orders and taxonomy items. The type option selects the entity; without a valid type the component returns an empty array. Caching is disabled]]></description>
			<content:encoded data-v-post-content="content" data-filter-cdata=""><![CDATA[
<p>The <strong>fields</strong> component (<code>Vvveb\Component\Fields</code>) renders custom field values for posts, products, users, orders and taxonomy items. The <code>type</code> option selects the entity; without a valid <code>type</code> the component returns an empty array. Caching is disabled (<code>$cacheExpire = 0</code>).</p>
<p>Activate the component by adding:</p>
<pre><code>data-v-component-fields
</code></pre>
<h2>Component Options</h2>
<p>Add options as HTML attributes using the <code>data-v-*</code> syntax.</p>
<table class="table table-bordered">
<thead class="table-light">
<tr>
<th>Option</th>
<th>Attribute</th>
<th>Description</th>
<th>Default</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>start</strong></td>
<td><code>data-v-start</code></td>
<td>Starting offset</td>
<td><code>0</code></td>
</tr>
<tr>
<td><strong>limit</strong></td>
<td><code>data-v-limit</code></td>
<td>Number of fields to display</td>
<td><code>100</code></td>
</tr>
<tr>
<td><strong>site_id</strong></td>
<td><code>data-v-site_id</code></td>
<td>Site ID</td>
<td>Current site</td>
</tr>
<tr>
<td><strong>language_id</strong></td>
<td><code>data-v-language_id</code></td>
<td>Language ID</td>
<td>Current language</td>
</tr>
<tr>
<td><strong>type</strong></td>
<td><code>data-v-type</code></td>
<td>Entity type: <code>post</code>, <code>product</code>, <code>user</code>, <code>order</code>, <code>taxonomy_item</code> (required)</td>
<td><code>null</code></td>
</tr>
<tr>
<td><strong>subtype</strong></td>
<td><code>data-v-subtype</code></td>
<td>Field subtype filter</td>
<td><code>null</code></td>
</tr>
<tr>
<td><strong>post_type</strong></td>
<td><code>data-v-post_type</code></td>
<td>Post type context</td>
<td><code>null</code></td>
</tr>
<tr>
<td><strong>product_id</strong></td>
<td><code>data-v-product_id</code></td>
<td>Filter by product, from URL by default</td>
<td><code>url</code></td>
</tr>
<tr>
<td><strong>post_id</strong></td>
<td><code>data-v-post_id</code></td>
<td>Filter by post, from URL by default</td>
<td><code>url</code></td>
</tr>
<tr>
<td><strong>user_id</strong></td>
<td><code>data-v-user_id</code></td>
<td>Filter by user, from URL by default</td>
<td><code>url</code></td>
</tr>
<tr>
<td><strong>order_id</strong></td>
<td><code>data-v-order_id</code></td>
<td>Filter by order, from URL by default</td>
<td><code>url</code></td>
</tr>
<tr>
<td><strong>taxonomy_item_id</strong></td>
<td><code>data-v-taxonomy_item_id</code></td>
<td>Filter by category/tag, from URL by default</td>
<td><code>url</code></td>
</tr>
<tr>
<td><strong>field_id</strong></td>
<td><code>data-v-field_id</code></td>
<td>Filter to specific field IDs (array)</td>
<td><code>null</code></td>
</tr>
<tr>
<td><strong>field_group_id</strong></td>
<td><code>data-v-field_group_id</code></td>
<td>Filter by field group</td>
<td><code>null</code></td>
</tr>
</tbody>
</table>
<h2>Component Properties</h2>
<p>Each field is wrapped in an element marked with <code>data-v-field</code>. The component decodes the <code>settings</code> JSON into <code>name</code>/<code>type</code> and exposes the stored <code>value</code> (JSON-decoded when it holds an object). Nested sub-fields render inside <code>data-v-subfields</code> / <code>data-v-subfields-field</code>.</p>
<h2>HTML Example</h2>
<pre><code class="language-html">&lt;div data-v-component-fields data-v-type=&quot;product&quot; data-v-product_id=&quot;url&quot;&gt;
	&lt;div data-v-field&gt;
		&lt;span data-v-field-name&gt;Field name&lt;/span&gt;
		&lt;span data-v-field-value&gt;Field value&lt;/span&gt;
	&lt;/div&gt;
&lt;/div&gt;
</code></pre>
<h2>Notes</h2>
<ul>
<li><code>type</code> must be one of <code>post</code>, <code>product</code>, <code>user</code>, <code>order</code>, <code>taxonomy_item</code>; any other value returns nothing.</li>
<li>Entity ID options (<code>product_id</code>, <code>post_id</code>, …) default to <code>url</code>, i.e. they are read from the page GET parameters on detail pages.</li>
</ul>
]]></content:encoded>		
		</item><item data-v-post="post" data-v-id="432">
			<title data-v-post-name="name">Component product subscriptions</title>
			<link data-v-post-full-url="name">https://dev.vvveb.com/component-product-subscriptions</link>
			<dc:creator data-v-post-display_name="name"><![CDATA[Admin]]></dc:creator>
			<pubDate data-v-post-pubDate="date">Mon, 28 Sep 2026 17:24:02 +0000</pubDate>
			<category data-v-post-category-name="category" data-filter-cdata=""><![CDATA[Category]]></category>
			<description data-v-post-excerpt="excerpt" data-filter-cdata=""><![CDATA[The product-subscriptions component (Vvveb\Component\Product\Subscriptions) lists the subscription plans attached to a product (recurring purchase options shown next to the one-time price). 
Activate the component by adding:]]></description>
			<content:encoded data-v-post-content="content" data-filter-cdata=""><![CDATA[
<p>The <strong>product-subscriptions</strong> component (<code>Vvveb\Component\Product\Subscriptions</code>) lists the subscription plans attached to a product (recurring purchase options shown next to the one-time price).</p>
<p>Activate the component by adding:</p>
<pre><code>data-v-component-product-subscriptions
</code></pre>
<h2>Component Options</h2>
<p>Add options as HTML attributes using the <code>data-v-*</code> syntax.</p>
<table class="table table-bordered">
<thead class="table-light">
<tr>
<th>Option</th>
<th>Attribute</th>
<th>Description</th>
<th>Default</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>start</strong></td>
<td><code>data-v-start</code></td>
<td>Starting offset</td>
<td><code>0</code></td>
</tr>
<tr>
<td><strong>limit</strong></td>
<td><code>data-v-limit</code></td>
<td>Number of subscriptions</td>
<td><code>7</code></td>
</tr>
<tr>
<td><strong>site_id</strong></td>
<td><code>data-v-site_id</code></td>
<td>Site ID</td>
<td>Current site</td>
</tr>
<tr>
<td><strong>language_id</strong></td>
<td><code>data-v-language_id</code></td>
<td>Language ID</td>
<td>Current language</td>
</tr>
<tr>
<td><strong>product_id</strong></td>
<td><code>data-v-product_id</code></td>
<td>Product ID, from URL by default</td>
<td><code>url</code></td>
</tr>
<tr>
<td><strong>parent_id</strong></td>
<td><code>data-v-parent_id</code></td>
<td>Filter by parent</td>
<td><code>null</code></td>
</tr>
<tr>
<td><strong>search</strong></td>
<td><code>data-v-search</code></td>
<td>Filter by text</td>
<td><code>null</code></td>
</tr>
</tbody>
</table>
<h2>Component Properties</h2>
<p>Each subscription renders inside a <code>data-v-subscription</code> repeatable with plan <code>name</code>, billing period, <code>price</code> / <code>price_formatted</code> and trial fields. The component also exposes <code>count</code>.</p>
<h2>HTML Example</h2>
<pre><code class="language-html">&lt;div data-v-component-product-subscriptions&gt;
	&lt;div data-v-subscription&gt;
		&lt;span data-v-subscription-name&gt;&lt;/span&gt;
		&lt;span data-v-subscription-price_formatted&gt;&lt;/span&gt;
	&lt;/div&gt;
&lt;/div&gt;
</code></pre>
<h2>Notes</h2>
<ul>
<li>For the standalone subscription-plan catalogue see the <strong>product-subscription-plans</strong> component (<code>data-v-component-product-subscription-plans</code>).</li>
</ul>
]]></content:encoded>		
		</item><item data-v-post="post" data-v-id="433">
			<title data-v-post-name="name">Component product filters</title>
			<link data-v-post-full-url="name">https://dev.vvveb.com/component-product-filters</link>
			<dc:creator data-v-post-display_name="name"><![CDATA[Admin]]></dc:creator>
			<pubDate data-v-post-pubDate="date">Mon, 28 Sep 2026 17:24:02 +0000</pubDate>
			<category data-v-post-category-name="category" data-filter-cdata=""><![CDATA[Category]]></category>
			<description data-v-post-excerpt="excerpt" data-filter-cdata=""><![CDATA[The product-filters component (Vvveb\Component\Product\Filters) lists filter definitions for a product (faceted-search filter values). Note: there is no dedicated product/filters.tpl template file, so rendering uses the generic data-v-* attribute binding]]></description>
			<content:encoded data-v-post-content="content" data-filter-cdata=""><![CDATA[
<p>The <strong>product-filters</strong> component (<code>Vvveb\Component\Product\Filters</code>) lists filter definitions for a product (faceted-search filter values). Note: there is no dedicated <code>product/filters.tpl</code> template file, so rendering uses the generic <code>data-v-*</code> attribute binding for the returned <code>filter</code> records.</p>
<p>Activate the component by adding:</p>
<pre><code>data-v-component-product-filters
</code></pre>
<h2>Component Options</h2>
<p>Add options as HTML attributes using the <code>data-v-*</code> syntax.</p>
<table class="table table-bordered">
<thead class="table-light">
<tr>
<th>Option</th>
<th>Attribute</th>
<th>Description</th>
<th>Default</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>start</strong></td>
<td><code>data-v-start</code></td>
<td>Starting offset</td>
<td><code>0</code></td>
</tr>
<tr>
<td><strong>limit</strong></td>
<td><code>data-v-limit</code></td>
<td>Number of filters</td>
<td><code>100</code></td>
</tr>
<tr>
<td><strong>site_id</strong></td>
<td><code>data-v-site_id</code></td>
<td>Site ID</td>
<td>Current site</td>
</tr>
<tr>
<td><strong>language_id</strong></td>
<td><code>data-v-language_id</code></td>
<td>Language ID</td>
<td>Current language</td>
</tr>
<tr>
<td><strong>product_id</strong></td>
<td><code>data-v-product_id</code></td>
<td>Product ID, from URL by default</td>
<td><code>url</code></td>
</tr>
<tr>
<td><strong>search</strong></td>
<td><code>data-v-search</code></td>
<td>Filter by text</td>
<td><code>null</code></td>
</tr>
<tr>
<td><strong>image_size</strong></td>
<td><code>data-v-image_size</code></td>
<td>Filter image size</td>
<td><code>thumb</code></td>
</tr>
</tbody>
</table>
<h2>Component Properties</h2>
<p>Filter records with <code>name</code>, <code>value</code> and image fields, bound with generic <code>data-v-filter-*</code> attributes.</p>
<h2>HTML Example</h2>
<pre><code class="language-html">&lt;div data-v-component-product-filters&gt;
	&lt;div data-v-filter&gt;
		&lt;span data-v-filter-name&gt;&lt;/span&gt;
	&lt;/div&gt;
&lt;/div&gt;
</code></pre>
]]></content:encoded>		
		</item><item data-v-post="post" data-v-id="434">
			<title data-v-post-name="name">Component product category</title>
			<link data-v-post-full-url="name">https://dev.vvveb.com/component-product-category</link>
			<dc:creator data-v-post-display_name="name"><![CDATA[Admin]]></dc:creator>
			<pubDate data-v-post-pubDate="date">Mon, 28 Sep 2026 17:24:02 +0000</pubDate>
			<category data-v-post-category-name="category" data-filter-cdata=""><![CDATA[Category]]></category>
			<description data-v-post-excerpt="excerpt" data-filter-cdata=""><![CDATA[The product-category component (Vvveb\Component\Product\Category) displays a single product category. It extends the base category component (Vvveb\Component\Category) without changing options, so it auto-detects the category from the page URL on product]]></description>
			<content:encoded data-v-post-content="content" data-filter-cdata=""><![CDATA[
<p>The <strong>product-category</strong> component (<code>Vvveb\Component\Product\Category</code>) displays a single product category. It extends the base <strong>category</strong> component (<code>Vvveb\Component\Category</code>) without changing options, so it auto-detects the category from the page URL on product category pages.</p>
<p>Activate the component by adding:</p>
<pre><code>data-v-component-product-category
</code></pre>
<h2>Component Options</h2>
<p>Same as the <strong>category</strong> component:</p>
<table class="table table-bordered">
<thead class="table-light">
<tr>
<th>Option</th>
<th>Attribute</th>
<th>Description</th>
<th>Default</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>taxonomy_item_id</strong></td>
<td><code>data-v-taxonomy_item_id</code></td>
<td>Category ID, from URL by default</td>
<td><code>url</code></td>
</tr>
<tr>
<td><strong>slug</strong></td>
<td><code>data-v-slug</code></td>
<td>Category slug, from URL by default</td>
<td><code>url</code></td>
</tr>
<tr>
<td><strong>type</strong></td>
<td><code>data-v-type</code></td>
<td>Taxonomy type</td>
<td><code>null</code></td>
</tr>
<tr>
<td><strong>post_type</strong></td>
<td><code>data-v-post_type</code></td>
<td>Post type</td>
<td><code>null</code></td>
</tr>
<tr>
<td><strong>language_id</strong></td>
<td><code>data-v-language_id</code></td>
<td>Language ID</td>
<td>Current language</td>
</tr>
<tr>
<td><strong>site_id</strong></td>
<td><code>data-v-site_id</code></td>
<td>Site ID</td>
<td>Current site</td>
</tr>
</tbody>
</table>
<h2>Component Properties</h2>
<p>Single-category fields through <code>data-v-category-*</code>: <code>name</code>, <code>slug</code>, <code>content</code>, <code>image</code>, <code>image_url</code>, <code>url</code>, <code>full-url</code>, <code>count</code>.</p>
<h2>HTML Example</h2>
<pre><code class="language-html">&lt;div data-v-component-product-category&gt;
	&lt;h1 data-v-category-name&gt;&lt;/h1&gt;
	&lt;div data-v-category-content&gt;&lt;/div&gt;
&lt;/div&gt;
</code></pre>
]]></content:encoded>		
		</item><item data-v-post="post" data-v-id="435">
			<title data-v-post-name="name">Component product subscription plans</title>
			<link data-v-post-full-url="name">https://dev.vvveb.com/component-product-subscription-plans</link>
			<dc:creator data-v-post-display_name="name"><![CDATA[Admin]]></dc:creator>
			<pubDate data-v-post-pubDate="date">Mon, 28 Sep 2026 17:24:02 +0000</pubDate>
			<category data-v-post-category-name="category" data-filter-cdata=""><![CDATA[Category]]></category>
			<description data-v-post-excerpt="excerpt" data-filter-cdata=""><![CDATA[The product-subscription-plans component (Vvveb\Component\Product\Subscription\Plans) lists subscription plans for a product from the subscription catalogue (billing cycles available for recurring orders). 
Activate the component by adding:]]></description>
			<content:encoded data-v-post-content="content" data-filter-cdata=""><![CDATA[
<p>The <strong>product-subscription-plans</strong> component (<code>Vvveb\Component\Product\Subscription\Plans</code>) lists subscription plans for a product from the subscription catalogue (billing cycles available for recurring orders).</p>
<p>Activate the component by adding:</p>
<pre><code>data-v-component-product-subscription-plans
</code></pre>
<h2>Component Options</h2>
<p>Add options as HTML attributes using the <code>data-v-*</code> syntax.</p>
<table class="table table-bordered">
<thead class="table-light">
<tr>
<th>Option</th>
<th>Attribute</th>
<th>Description</th>
<th>Default</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>start</strong></td>
<td><code>data-v-start</code></td>
<td>Starting offset</td>
<td><code>0</code></td>
</tr>
<tr>
<td><strong>limit</strong></td>
<td><code>data-v-limit</code></td>
<td>Number of plans</td>
<td><code>7</code></td>
</tr>
<tr>
<td><strong>site_id</strong></td>
<td><code>data-v-site_id</code></td>
<td>Site ID</td>
<td>Current site</td>
</tr>
<tr>
<td><strong>language_id</strong></td>
<td><code>data-v-language_id</code></td>
<td>Language ID</td>
<td>Current language</td>
</tr>
<tr>
<td><strong>product_id</strong></td>
<td><code>data-v-product_id</code></td>
<td>Product ID, from URL by default</td>
<td><code>url</code></td>
</tr>
<tr>
<td><strong>parent_id</strong></td>
<td><code>data-v-parent_id</code></td>
<td>Filter by parent</td>
<td><code>null</code></td>
</tr>
<tr>
<td><strong>search</strong></td>
<td><code>data-v-search</code></td>
<td>Filter by text</td>
<td><code>null</code></td>
</tr>
</tbody>
</table>
<h2>Component Properties</h2>
<p>Each plan exposes the subscription-plan record fields (<code>name</code>, billing frequency, <code>price</code> / <code>price_formatted</code>, trial data).</p>
<h2>HTML Example</h2>
<pre><code class="language-html">&lt;div data-v-component-product-subscription-plans&gt;
	&lt;div data-v-plan&gt;
		&lt;span data-v-plan-name&gt;&lt;/span&gt;
		&lt;span data-v-plan-price_formatted&gt;&lt;/span&gt;
	&lt;/div&gt;
&lt;/div&gt;
</code></pre>
]]></content:encoded>		
		</item><item data-v-post="post" data-v-id="436">
			<title data-v-post-name="name">Component content categories</title>
			<link data-v-post-full-url="name">https://dev.vvveb.com/component-content-categories</link>
			<dc:creator data-v-post-display_name="name"><![CDATA[Admin]]></dc:creator>
			<pubDate data-v-post-pubDate="date">Mon, 28 Sep 2026 17:24:02 +0000</pubDate>
			<category data-v-post-category-name="category" data-filter-cdata=""><![CDATA[Category]]></category>
			<description data-v-post-excerpt="excerpt" data-filter-cdata=""><![CDATA[The content-categories component (Vvveb\Component\Content\Categories) is the post-scoped twin of the categories component: it lists categories-type taxonomy items for post post types (blog categories). For product taxonomies use product-categories]]></description>
			<content:encoded data-v-post-content="content" data-filter-cdata=""><![CDATA[
<p>The <strong>content-categories</strong> component (<code>Vvveb\Component\Content\Categories</code>) is the post-scoped twin of the <strong>categories</strong> component: it lists <code>categories</code>-type taxonomy items for <code>post</code> post types (blog categories). For product taxonomies use <strong>product-categories</strong> instead.</p>
<p>Activate the component by adding:</p>
<pre><code>data-v-component-content-categories
</code></pre>
<h2>Component Options</h2>
<p>Add options as HTML attributes using the <code>data-v-*</code> syntax.</p>
<table class="table table-bordered">
<thead class="table-light">
<tr>
<th>Option</th>
<th>Attribute</th>
<th>Description</th>
<th>Default</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>start</strong></td>
<td><code>data-v-start</code></td>
<td>Starting offset</td>
<td><code>0</code></td>
</tr>
<tr>
<td><strong>limit</strong></td>
<td><code>data-v-limit</code></td>
<td>Number of categories</td>
<td><code>7</code></td>
</tr>
<tr>
<td><strong>site_id</strong></td>
<td><code>data-v-site_id</code></td>
<td>Site ID</td>
<td>Current site</td>
</tr>
<tr>
<td><strong>language_id</strong></td>
<td><code>data-v-language_id</code></td>
<td>Language ID</td>
<td>Current language</td>
</tr>
<tr>
<td><strong>order</strong></td>
<td><code>data-v-order</code></td>
<td>Sorting rules, overridable from URL</td>
<td><code>url</code> fallback <code>price asc</code></td>
</tr>
<tr>
<td><strong>taxonomy_id</strong></td>
<td><code>data-v-taxonomy_id</code></td>
<td>Filter by taxonomy ID</td>
<td><code>null</code></td>
</tr>
<tr>
<td><strong>post_id</strong></td>
<td><code>data-v-post_id</code></td>
<td>Show only categories assigned to a post</td>
<td><code>null</code></td>
</tr>
<tr>
<td><strong>parent_id</strong></td>
<td><code>data-v-parent_id</code></td>
<td>Filter by parent (<code>0</code> = top-level)</td>
<td><code>null</code></td>
</tr>
<tr>
<td><strong>search</strong></td>
<td><code>data-v-search</code></td>
<td>Filter by text</td>
<td><code>null</code></td>
</tr>
<tr>
<td><strong>type</strong></td>
<td><code>data-v-type</code></td>
<td>Taxonomy type</td>
<td><code>categories</code></td>
</tr>
<tr>
<td><strong>post_type</strong></td>
<td><code>data-v-post_type</code></td>
<td>Post type</td>
<td><code>post</code></td>
</tr>
<tr>
<td><strong>parents_only</strong></td>
<td><code>data-v-parents_only</code></td>
<td>Only parent categories</td>
<td><code>false</code></td>
</tr>
<tr>
<td><strong>parents_children_only</strong></td>
<td><code>data-v-parents_children_only</code></td>
<td>Only parents that have children</td>
<td><code>false</code></td>
</tr>
<tr>
<td><strong>parents_without_children</strong></td>
<td><code>data-v-parents_without_children</code></td>
<td>Only parents without children</td>
<td><code>false</code></td>
</tr>
</tbody>
</table>
<h2>Component Properties</h2>
<p>Same as the <strong>categories</strong> component: <code>data-v-cats</code> container with <code>data-v-cat</code> repeatables exposing <code>name</code>, <code>slug</code>, <code>url</code>, <code>image</code>, <code>count</code>, and nested children.</p>
<h2>HTML Example</h2>
<pre><code class="language-html">&lt;div data-v-component-content-categories data-v-limit=&quot;7&quot;&gt;
	&lt;div data-v-cats&gt;
		&lt;div data-v-cat&gt;
			&lt;a href=&quot;&quot; data-v-cat-url&gt;&lt;span data-v-cat-name&gt;&lt;/span&gt;&lt;/a&gt;
		&lt;/div&gt;
	&lt;/div&gt;
&lt;/div&gt;
</code></pre>
]]></content:encoded>		
		</item><item data-v-post="post" data-v-id="437">
			<title data-v-post-name="name">Component product options</title>
			<link data-v-post-full-url="name">https://dev.vvveb.com/component-product-options</link>
			<dc:creator data-v-post-display_name="name"><![CDATA[Admin]]></dc:creator>
			<pubDate data-v-post-pubDate="date">Mon, 28 Sep 2026 17:24:02 +0000</pubDate>
			<category data-v-post-category-name="category" data-filter-cdata=""><![CDATA[Category]]></category>
			<description data-v-post-excerpt="excerpt" data-filter-cdata=""><![CDATA[The product-options component (Vvveb\Component\Product\Options) lists the selectable options of a product (dropdowns, radios, checkboxes, file uploads) with their values and price modifiers. It is used on product pages to render the option selectors.]]></description>
			<content:encoded data-v-post-content="content" data-filter-cdata=""><![CDATA[
<p>The <strong>product-options</strong> component (<code>Vvveb\Component\Product\Options</code>) lists the selectable options of a product (dropdowns, radios, checkboxes, file uploads) with their values and price modifiers. It is used on product pages to render the option selectors.</p>
<p>Activate the component by adding:</p>
<pre><code>data-v-component-product-options
</code></pre>
<h2>Component Options</h2>
<p>Add options as HTML attributes using the <code>data-v-*</code> syntax.</p>
<table class="table table-bordered">
<thead class="table-light">
<tr>
<th>Option</th>
<th>Attribute</th>
<th>Description</th>
<th>Default</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>start</strong></td>
<td><code>data-v-start</code></td>
<td>Starting offset</td>
<td><code>0</code></td>
</tr>
<tr>
<td><strong>limit</strong></td>
<td><code>data-v-limit</code></td>
<td>Number of options (<code>null</code> = all)</td>
<td><code>null</code></td>
</tr>
<tr>
<td><strong>site_id</strong></td>
<td><code>data-v-site_id</code></td>
<td>Site ID</td>
<td>Current site</td>
</tr>
<tr>
<td><strong>language_id</strong></td>
<td><code>data-v-language_id</code></td>
<td>Language ID</td>
<td>Current language</td>
</tr>
<tr>
<td><strong>product_id</strong></td>
<td><code>data-v-product_id</code></td>
<td>Product ID, from URL by default</td>
<td><code>url</code></td>
</tr>
<tr>
<td><strong>product_variant_id</strong></td>
<td><code>data-v-product_variant_id</code></td>
<td>Pre-select values for a variant</td>
<td><code>null</code></td>
</tr>
<tr>
<td><strong>parent_id</strong></td>
<td><code>data-v-parent_id</code></td>
<td>Filter by parent option</td>
<td><code>null</code></td>
</tr>
<tr>
<td><strong>search</strong></td>
<td><code>data-v-search</code></td>
<td>Filter by text</td>
<td><code>null</code></td>
</tr>
<tr>
<td><strong>image_size</strong></td>
<td><code>data-v-image_size</code></td>
<td>Option-value image size</td>
<td><code>thumb</code></td>
</tr>
</tbody>
</table>
<h2>Component Properties</h2>
<p>Options render inside <code>data-v-option</code> repeatables with nested <code>data-v-value</code> repeats for the selectable values. Fields include option <code>name</code>, <code>type</code> (<code>select</code>, <code>radio</code>, <code>checkbox</code>, …), <code>required</code> flag, and per-value <code>name</code>, <code>price</code> / <code>price_formatted</code> modifiers and images. The component also exposes <code>count</code>.</p>
<h2>HTML Example</h2>
<pre><code class="language-html">&lt;div data-v-component-product-options&gt;
	&lt;div data-v-option&gt;
		&lt;label data-v-option-name&gt;&lt;/label&gt;
		&lt;div data-v-value&gt;
			&lt;span data-v-value-name&gt;&lt;/span&gt;
			&lt;span data-v-value-price_formatted&gt;&lt;/span&gt;
		&lt;/div&gt;
	&lt;/div&gt;
&lt;/div&gt;
</code></pre>
]]></content:encoded>		
		</item><item data-v-post="post" data-v-id="438">
			<title data-v-post-name="name">Component content tags</title>
			<link data-v-post-full-url="name">https://dev.vvveb.com/component-content-tags</link>
			<dc:creator data-v-post-display_name="name"><![CDATA[Admin]]></dc:creator>
			<pubDate data-v-post-pubDate="date">Mon, 28 Sep 2026 17:24:02 +0000</pubDate>
			<category data-v-post-category-name="category" data-filter-cdata=""><![CDATA[Category]]></category>
			<description data-v-post-excerpt="excerpt" data-filter-cdata=""><![CDATA[The content-tags component (Vvveb\Component\Content\Tags) lists tags-type taxonomy items for posts (blog tags cloud / tag index). 
Activate the component by adding: 
data-v-component-content-tags  
Component Options 
Add options as HTML attributes using]]></description>
			<content:encoded data-v-post-content="content" data-filter-cdata=""><![CDATA[
<p>The <strong>content-tags</strong> component (<code>Vvveb\Component\Content\Tags</code>) lists <code>tags</code>-type taxonomy items for posts (blog tags cloud / tag index).</p>
<p>Activate the component by adding:</p>
<pre><code>data-v-component-content-tags
</code></pre>
<h2>Component Options</h2>
<p>Add options as HTML attributes using the <code>data-v-*</code> syntax.</p>
<table class="table table-bordered">
<thead class="table-light">
<tr>
<th>Option</th>
<th>Attribute</th>
<th>Description</th>
<th>Default</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>start</strong></td>
<td><code>data-v-start</code></td>
<td>Starting offset</td>
<td><code>0</code></td>
</tr>
<tr>
<td><strong>limit</strong></td>
<td><code>data-v-limit</code></td>
<td>Number of tags</td>
<td><code>7</code></td>
</tr>
<tr>
<td><strong>page</strong></td>
<td><code>data-v-page</code></td>
<td>Page number</td>
<td><code>1</code></td>
</tr>
<tr>
<td><strong>language_id</strong></td>
<td><code>data-v-language_id</code></td>
<td>Language ID</td>
<td>Current language</td>
</tr>
<tr>
<td><strong>count</strong></td>
<td><code>data-v-count</code></td>
<td>Include post counts</td>
<td><code>null</code></td>
</tr>
<tr>
<td><strong>order</strong></td>
<td><code>data-v-order</code></td>
<td>Sorting rules, overridable from URL</td>
<td><code>url</code> fallback <code>price asc</code></td>
</tr>
<tr>
<td><strong>type</strong></td>
<td><code>data-v-type</code></td>
<td>Taxonomy type</td>
<td><code>tags</code></td>
</tr>
<tr>
<td><strong>post_type</strong></td>
<td><code>data-v-post_type</code></td>
<td>Post type</td>
<td><code>post</code></td>
</tr>
<tr>
<td><strong>id_manufacturer</strong></td>
<td><code>data-v-id_manufacturer</code></td>
<td>Legacy filter</td>
<td><code>null</code></td>
</tr>
<tr>
<td><strong>id_category</strong></td>
<td><code>data-v-id_category</code></td>
<td>Legacy filter</td>
<td><code>null</code></td>
</tr>
<tr>
<td><strong>parents_only</strong></td>
<td><code>data-v-parents_only</code></td>
<td>Only parent tags</td>
<td><code>false</code></td>
</tr>
<tr>
<td><strong>parents_children_only</strong></td>
<td><code>data-v-parents_children_only</code></td>
<td>Only parents with children</td>
<td><code>false</code></td>
</tr>
<tr>
<td><strong>parents_without_children</strong></td>
<td><code>data-v-parents_without_children</code></td>
<td>Only parents without children</td>
<td><code>false</code></td>
</tr>
</tbody>
</table>
<h2>Component Properties</h2>
<p>Tags render inside <code>data-v-tags</code> / <code>data-v-archive</code> repeatables with <code>name</code>, <code>slug</code>, <code>url</code> and <code>count</code> fields.</p>
<h2>HTML Example</h2>
<pre><code class="language-html">&lt;div data-v-component-content-tags data-v-limit=&quot;20&quot;&gt;
	&lt;div data-v-tags&gt;
		&lt;div data-v-archive&gt;
			&lt;a href=&quot;&quot; data-v-archive-url&gt;&lt;span data-v-archive-name&gt;&lt;/span&gt;&lt;/a&gt;
		&lt;/div&gt;
	&lt;/div&gt;
&lt;/div&gt;
</code></pre>
]]></content:encoded>		
		</item><item data-v-post="post" data-v-id="439">
			<title data-v-post-name="name">Component product attributes</title>
			<link data-v-post-full-url="name">https://dev.vvveb.com/component-product-attributes</link>
			<dc:creator data-v-post-display_name="name"><![CDATA[Admin]]></dc:creator>
			<pubDate data-v-post-pubDate="date">Mon, 28 Sep 2026 17:24:02 +0000</pubDate>
			<category data-v-post-category-name="category" data-filter-cdata=""><![CDATA[Category]]></category>
			<description data-v-post-excerpt="excerpt" data-filter-cdata=""><![CDATA[The product-attributes component (Vvveb\Component\Product\Attributes) lists attribute values for a product (specification table: e.g. color, size, material). 
Activate the component by adding: 
data-v-component-product-attributes  
Component Options 
Add]]></description>
			<content:encoded data-v-post-content="content" data-filter-cdata=""><![CDATA[
<p>The <strong>product-attributes</strong> component (<code>Vvveb\Component\Product\Attributes</code>) lists attribute values for a product (specification table: e.g. color, size, material).</p>
<p>Activate the component by adding:</p>
<pre><code>data-v-component-product-attributes
</code></pre>
<h2>Component Options</h2>
<p>Add options as HTML attributes using the <code>data-v-*</code> syntax.</p>
<table class="table table-bordered">
<thead class="table-light">
<tr>
<th>Option</th>
<th>Attribute</th>
<th>Description</th>
<th>Default</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>start</strong></td>
<td><code>data-v-start</code></td>
<td>Starting offset</td>
<td><code>0</code></td>
</tr>
<tr>
<td><strong>limit</strong></td>
<td><code>data-v-limit</code></td>
<td>Number of attributes</td>
<td><code>100</code></td>
</tr>
<tr>
<td><strong>site_id</strong></td>
<td><code>data-v-site_id</code></td>
<td>Site ID</td>
<td>Current site</td>
</tr>
<tr>
<td><strong>language_id</strong></td>
<td><code>data-v-language_id</code></td>
<td>Language ID</td>
<td>Current language</td>
</tr>
<tr>
<td><strong>product_id</strong></td>
<td><code>data-v-product_id</code></td>
<td>Product ID, from URL by default</td>
<td><code>url</code></td>
</tr>
<tr>
<td><strong>search</strong></td>
<td><code>data-v-search</code></td>
<td>Filter by text</td>
<td><code>null</code></td>
</tr>
<tr>
<td><strong>image_size</strong></td>
<td><code>data-v-image_size</code></td>
<td>Attribute image size</td>
<td><code>thumb</code></td>
</tr>
</tbody>
</table>
<h2>Component Properties</h2>
<p>Attributes render inside the <code>data-v-attributes</code> repeatable with attribute <code>name</code>, <code>value</code>, and image fields.</p>
<h2>HTML Example</h2>
<pre><code class="language-html">&lt;div data-v-component-product-attributes&gt;
	&lt;div data-v-attributes&gt;
		&lt;span data-v-attributes-name&gt;&lt;/span&gt;
		&lt;span data-v-attributes-value&gt;&lt;/span&gt;
	&lt;/div&gt;
&lt;/div&gt;
</code></pre>
]]></content:encoded>		
		</item><item data-v-post="post" data-v-id="440">
			<title data-v-post-name="name">Component product manufacturers</title>
			<link data-v-post-full-url="name">https://dev.vvveb.com/component-product-manufacturers</link>
			<dc:creator data-v-post-display_name="name"><![CDATA[Admin]]></dc:creator>
			<pubDate data-v-post-pubDate="date">Mon, 28 Sep 2026 17:24:02 +0000</pubDate>
			<category data-v-post-category-name="category" data-filter-cdata=""><![CDATA[Category]]></category>
			<description data-v-post-excerpt="excerpt" data-filter-cdata=""><![CDATA[The product-manufacturers component (Vvveb\Component\Product\Manufacturers) lists product manufacturers (brands) with pagination and category filtering. 
Activate the component by adding: 
data-v-component-product-manufacturers  
Component Options 
Add]]></description>
			<content:encoded data-v-post-content="content" data-filter-cdata=""><![CDATA[
<p>The <strong>product-manufacturers</strong> component (<code>Vvveb\Component\Product\Manufacturers</code>) lists product manufacturers (brands) with pagination and category filtering.</p>
<p>Activate the component by adding:</p>
<pre><code>data-v-component-product-manufacturers
</code></pre>
<h2>Component Options</h2>
<p>Add options as HTML attributes using the <code>data-v-*</code> syntax.</p>
<table class="table table-bordered">
<thead class="table-light">
<tr>
<th>Option</th>
<th>Attribute</th>
<th>Description</th>
<th>Default</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>start</strong></td>
<td><code>data-v-start</code></td>
<td>Starting offset</td>
<td><code>0</code></td>
</tr>
<tr>
<td><strong>limit</strong></td>
<td><code>data-v-limit</code></td>
<td>Number of manufacturers</td>
<td><code>7</code></td>
</tr>
<tr>
<td><strong>page</strong></td>
<td><code>data-v-page</code></td>
<td>Page number</td>
<td><code>1</code></td>
</tr>
<tr>
<td><strong>count</strong></td>
<td><code>data-v-count</code></td>
<td>Include product counts, overridable from URL</td>
<td><code>4</code></td>
</tr>
<tr>
<td><strong>manufacturer_id</strong></td>
<td><code>data-v-manufacturer_id</code></td>
<td>Filter by manufacturer ID</td>
<td><code>null</code></td>
</tr>
<tr>
<td><strong>category_id</strong></td>
<td><code>data-v-category_id</code></td>
<td>Filter by category ID</td>
<td><code>null</code></td>
</tr>
<tr>
<td><strong>order</strong></td>
<td><code>data-v-order</code></td>
<td>Sorting rules, overridable from URL</td>
<td><code>url</code> fallback <code>price asc</code></td>
</tr>
<tr>
<td><strong>type</strong></td>
<td><code>data-v-type</code></td>
<td>Record type filter</td>
<td><code>tags</code></td>
</tr>
<tr>
<td><strong>filter</strong></td>
<td><code>data-v-filter</code></td>
<td>Extra filter value</td>
<td><code>null</code></td>
</tr>
<tr>
<td><strong>parents_only</strong></td>
<td><code>data-v-parents_only</code></td>
<td>Only parents</td>
<td><code>false</code></td>
</tr>
<tr>
<td><strong>parents_children_only</strong></td>
<td><code>data-v-parents_children_only</code></td>
<td>Only parents with children</td>
<td><code>false</code></td>
</tr>
<tr>
<td><strong>parents_without_children</strong></td>
<td><code>data-v-parents_without_children</code></td>
<td>Only parents without children</td>
<td><code>false</code></td>
</tr>
</tbody>
</table>
<h2>Component Properties</h2>
<p>Each manufacturer renders inside a <code>data-v-manufacturer</code> repeatable with <code>name</code>, <code>slug</code>, <code>image</code> and <code>url</code> fields.</p>
<h2>HTML Example</h2>
<pre><code class="language-html">&lt;div data-v-component-product-manufacturers data-v-limit=&quot;12&quot;&gt;
	&lt;div data-v-manufacturer&gt;
		&lt;a href=&quot;&quot; data-v-manufacturer-url&gt;&lt;span data-v-manufacturer-name&gt;&lt;/span&gt;&lt;/a&gt;
	&lt;/div&gt;
&lt;/div&gt;
</code></pre>
]]></content:encoded>		
		</item><item data-v-post="post" data-v-id="441">
			<title data-v-post-name="name">Architecture</title>
			<link data-v-post-full-url="name">https://dev.vvveb.com/architecture</link>
			<dc:creator data-v-post-display_name="name"><![CDATA[Admin]]></dc:creator>
			<pubDate data-v-post-pubDate="date">Mon, 28 Sep 2026 17:24:02 +0000</pubDate>
			<category data-v-post-category-name="category" data-filter-cdata=""><![CDATA[Category]]></category>
			<description data-v-post-excerpt="excerpt" data-filter-cdata=""><![CDATA[Vvveb CMS uses an MVCC (Model-View-Controller-Component) architecture. It extends traditional MVC with a Component layer that declaratively embeds data-fetching logic directly in HTML templates. 
Request Lifecycle 
HTTP Request │ ▼ index.php (entry]]></description>
			<content:encoded data-v-post-content="content" data-filter-cdata=""><![CDATA[
<p>Vvveb CMS uses an <strong>MVCC (Model-View-Controller-Component)</strong> architecture. It extends traditional MVC with a <strong>Component</strong> layer that declaratively embeds data-fetching logic directly in HTML templates.</p>
<h2>Request Lifecycle</h2>
<pre><code>HTTP Request
     │
     ▼
index.php (entry point)
     │
     ├── env.php (environment constants)
     ├── system/core/startup.php (bootstrap: autoloader, error handlers, DB config)
     │
     ▼
System\Core\start()
     │
     ├── Sites::getSiteData()  -  resolve current site
     ├── Plugins::loadPlugins()  -  load active plugins
     │
     ▼
FrontController::dispatch($site)
     │
     ├── Routes::init() + Routes::match($uri)  -  URL routing
     │      (cached per site/app, extensible via Event::trigger)
     │
     ▼
FrontController::call($class, $action, $file)
     │
     ├── include base.php (admin or app base controller)
     ├── include controller file
     ├── new $controllerClass()
     ├── FrontController::di($controller)  -  inject Request, Response, View, Session
     ├── $controller-&gt;init()  -  language, currency, permissions, menu, site settings
     ├── View::template('module/action.html')
     ├── $controller-&gt;$action()
     │       │
     │       ├── Sets $this-&gt;view-&gt;property = data (available in template)
     │       └── Returns: false (no template), array (JSON), string (override template)
     │
     ▼
View::render()
     │
     ├── Component::getInstance()  -  discover and execute data-v-component-* attributes
     │       ├── generateRequiredComponents()  -  parse HTML template DOM
     │       ├── loadComponents()  -  execute each component, fetch data, cache results
     │       └── Store in $view-&gt;_component[name][index]
     │
     ├── checkNeedRecompile()  -  recompile HTML template to PHP if changed
     ├── Vtpl engine compiles HTML → PHP template
     └── include compiled template (accesses $view properties + $view-&gt;_component)
     │
     ▼
Response::output() → HTTP Response
</code></pre>
<h2>Directory Structure</h2>
<pre><code>├── app/                    # Front-end application
│   ├── controller/         # Front-end controllers
│   ├── component/          # Front-end components (data-v-component-*)
│   ├── template/           # Front-end HTML templates
│   ├── sql/                # Front-end SQL model definitions
│   └── validate/           # Front-end validation rules
├── admin/                  # Admin back-end
│   ├── controller/         # Admin controllers
│   ├── component/          # Admin components
│   ├── template/           # Admin HTML templates
│   ├── sql/                # Admin SQL model definitions
│   └── validate/           # Admin validation rules
├── install/                # Web installer
├── system/                 # Core framework
│   ├── core/               # Bootstrap, FrontController, Request, Response, View
│   ├── cache/              # Cache drivers (file, apcu, memcached, redis)
│   ├── db/                 # Database drivers (mysqli, pgsql, sqlite)
│   ├── session/            # Session drivers (php, request)
│   ├── extensions/         # Plugin and theme management
│   ├── sqlp/               # SQL parser and code generator
│   ├── vtpl/               # Template engine
│   ├── user/               # Authentication (Admin, User, Role, Auth)
│   ├── meta/               # SEO/meta handling
│   ├── media/              # Image processing
│   ├── mail/               # Email sending
│   └── import/             # Data import utilities
├── config/                 # Configuration files
├── plugins/                # plugins
├── public/                 # Web-accessible assets
│   ├── themes/             # front-end themes
│   ├── admin/              # Admin theme assets
│   ├── js/                 # JavaScript (builder.js, etc.)
│   ├── css/                # Stylesheets
│   └── media/              # User-uploaded files
├── rest/                   # REST API entry point
├── graphql/                # GraphQL API entry point
├── mcp/                    # MCP (AI) integration
├── storage/                # Runtime storage (cache, compiled templates, backups)
└── locale/                 # Translation files
</code></pre>
<h2>Entry Points</h2>
<table class="table table-bordered">
<thead class="table-light">
<tr>
<th>Entry Point</th>
<th>URL</th>
<th>APP Constant</th>
<th>Purpose</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>index.php</code></td>
<td><code>/</code></td>
<td><code>app</code></td>
<td>Front-end website (also dispatches <code>/rest/*</code>, <code>/graphql/*</code>, <code>/mcpa/*</code> by URI prefix)</td>
</tr>
<tr>
<td><code>public/index.php</code></td>
<td><code>/</code> (web root)</td>
<td><code>app</code></td>
<td>Web-accessible front controller, includes <code>../index.php</code></td>
</tr>
<tr>
<td><code>admin/index.php</code></td>
<td><code>/admin/</code></td>
<td><code>admin</code></td>
<td>Admin panel</td>
</tr>
<tr>
<td><code>install/</code></td>
<td><code>/install/</code></td>
<td><code>install</code></td>
<td>Web installer (used when the app is not installed yet)</td>
</tr>
<tr>
<td><code>rest/index.php</code></td>
<td><code>/rest/</code></td>
<td><code>rest</code></td>
<td>REST API</td>
</tr>
<tr>
<td><code>graphql/index.php</code></td>
<td><code>/graphql/</code></td>
<td><code>graphql</code></td>
<td>GraphQL API</td>
</tr>
<tr>
<td><code>mcp/index.php</code></td>
<td><code>/mcpa/*</code></td>
<td><code>admin</code></td>
<td>MCP (AI) integration (dispatched from the root <code>index.php</code>, runs with <code>DISPATCH</code> disabled)</td>
</tr>
<tr>
<td><code>cli.php</code></td>
<td>CLI</td>
<td>varies</td>
<td>Command-line interface</td>
</tr>
</tbody>
</table>
<p>The <code>APP</code> constant determines which application directory (<code>app/</code>, <code>admin/</code>, …) is active. The root <code>index.php</code> sets it from the request URI (or forces <code>install</code> when the app is not installed yet), then <code>system/core/startup.php</code> bootstraps the framework.</p>
<h2>Autoloading</h2>
<p>Vvveb uses a custom autoloader (<code>Vvveb\System\Core\autoload()</code>, registered via <code>spl_autoload_register()</code> in <code>system/core/startup.php</code>). It maps <code>Vvveb\*</code> namespaces to file paths with <code>camelToUnderscore()</code> conversion (each uppercase letter becomes <code>_lowercase</code>) and falls back to a lowercased path:</p>
<ul>
<li><code>Vvveb\System\Core\FrontController</code> → <code>system/core/front_controller.php</code></li>
<li><code>Vvveb\Controller\Product\Products</code> → <code>{APP}/controller/product/products.php</code> (<code>APP</code> selects <code>app/</code> vs <code>admin/</code>, plugin controllers are remapped per <code>APP</code>)</li>
<li><code>Vvveb\Component\Posts</code> → <code>app/component/posts.php</code></li>
<li><code>Vvveb\Sql\ProductSQL</code> → auto-generated from <code>admin/sql/{mysqli,pgsql,sqlite}/product.sql</code> (engine-specific subfolder, compiled to <code>storage/model/</code>)  -  <code>Vvveb\Sql\Plugins\*</code> models resolve to <code>plugins/{slug}/sql/{engine}/*.sql</code></li>
</ul>
<p>Special roots apply: <code>Vvveb\System\*</code>, <code>Vvveb\Plugins\*</code> and <code>Vvveb\Sql\Plugins\*</code> load from the project root, <code>Vvveb\App\*</code> from <code>app/</code>, <code>Vvveb\Admin\*</code> from <code>admin/</code>, everything else from the active <code>APP</code> directory.</p>
<h2>Database Drivers</h2>
<p>Vvveb supports three database engines via a driver pattern:</p>
<table class="table table-bordered">
<thead class="table-light">
<tr>
<th>Driver</th>
<th>Class</th>
<th>Engine</th>
</tr>
</thead>
<tbody>
<tr>
<td>MySQLi</td>
<td><code>Vvveb\System\Db\Mysqli</code></td>
<td>MySQL/MariaDB</td>
</tr>
<tr>
<td>PostgreSQL</td>
<td><code>Vvveb\System\Db\Pgsql</code></td>
<td>PostgreSQL 11+</td>
</tr>
<tr>
<td>SQLite</td>
<td><code>Vvveb\System\Db\Sqlite</code></td>
<td>SQLite 3</td>
</tr>
</tbody>
</table>
<p>The <code>Vvveb\System\Db</code> facade creates driver instances via <code>Db::getInstance()</code> using the <code>DB_ENGINE</code> constant set in <code>startup.php</code> from <code>config/db.php</code>.</p>
<h2>Configuration</h2>
<p>Configuration uses a flat PHP file system with dot-notation access:</p>
<pre><code class="language-php">// Read
$value = \Vvveb\config('db.default.host');
$value = \Vvveb\getSetting('site', 'name');

// Write (persists to file)
\Vvveb\setConfig('app.cache.driver', 'apcu');
\Vvveb\setSetting('site', 'name', 'My Site');
</code></pre>
<p>Config files live in <code>config/</code> and return PHP arrays. They are loaded on-demand and cached in memory. The <code>Config</code> class handles loading, saving, and dot-notation navigation.</p>
<p>Key configuration files:</p>
<table class="table table-bordered">
<thead class="table-light">
<tr>
<th>File</th>
<th>Purpose</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>config/db.php</code></td>
<td>Database connection settings</td>
</tr>
<tr>
<td><code>config/app.php</code></td>
<td>Application settings (cache driver, etc.)</td>
</tr>
<tr>
<td><code>config/app-routes.php</code></td>
<td>Front-end URL routes</td>
</tr>
<tr>
<td><code>config/admin-menu.php</code></td>
<td>Admin sidebar navigation</td>
</tr>
<tr>
<td><code>config/rest-routes.php</code></td>
<td>REST API routes</td>
</tr>
<tr>
<td><code>config/graphql-routes.php</code></td>
<td>GraphQL routes</td>
</tr>
<tr>
<td><code>config/sites.php</code></td>
<td>Multi-site definitions</td>
</tr>
<tr>
<td><code>config/plugins.php</code></td>
<td>Per-site plugin status</td>
</tr>
<tr>
<td><code>config/mail.php</code></td>
<td>Email/SMTP settings</td>
</tr>
</tbody>
</table>
<h2>Event System</h2>
<p>The event system provides publish-subscribe extensibility at every major lifecycle point:</p>
<pre><code class="language-php">// Register a listener
\Vvveb\System\Event::on('Vvveb\Component\Products', 'results', 'my-plugin', function($results) {
    $results['product'] = array_filter($results['product'], fn($p) =&gt; $p['status']);
    return [$results]; // listeners must return their parameters wrapped in an array
}, 1000);

// Trigger (called by framework)
\Vvveb\System\Event::trigger('Vvveb\Component\Products', 'results', $results);

// Remove listener
\Vvveb\System\Event::off('Vvveb\Component\Products', false, 'my-plugin');
</code></pre>
<blockquote>
<p><strong>Important</strong>
Listeners must return all received parameters wrapped in an array (e.g. <code>return [$results];</code>). If a listener returns <code>null</code> the original parameters pass through unchanged, but any modifications are lost. Callbacks with a lower priority run first; the default priority is <code>1000</code>.</p>
</blockquote>
<p>Events are stored per namespace and name, ordered by priority with <code>ksort()</code> (lower values run first) and keyed by listener id: <code>$events[$namespace][$name][$priority][$id]</code>. Use <code>Event::getEvents()</code> to inspect registered listeners.</p>
<p>Key lifecycle events:</p>
<table class="table table-bordered">
<thead class="table-light">
<tr>
<th>Event</th>
<th>When Fired</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>FrontController::call</code> (after)</td>
<td>After controller action execution</td>
</tr>
<tr>
<td><code>View::compile</code> / <code>compile:after</code></td>
<td>Before/after template compilation</td>
</tr>
<tr>
<td><code>View::render</code></td>
<td>Before template rendering</td>
</tr>
<tr>
<td><code>Routes::init</code></td>
<td>When routes are compiled, before caching</td>
</tr>
<tr>
<td><code>ComponentBase::__construct</code></td>
<td>After component options are resolved</td>
</tr>
<tr>
<td>Component class <code>results</code></td>
<td>After component fetches data</td>
</tr>
<tr>
<td><code>Base::init-menu</code></td>
<td>After admin menu is built</td>
</tr>
</tbody>
</table>
<h2>Page Cache</h2>
<p>When enabled (<code>PAGE_CACHE</code> in <code>env.php</code>), full-page caching serves static HTML for anonymous visitors:</p>
<ol>
<li>On first request, <code>PageCache::startCapture()</code> begins output buffering (wired in root <code>index.php</code>)</li>
<li>The full page is rendered normally</li>
<li><code>PageCache::saveCache()</code> writes the output to <code>public/page-cache/{host}/{path}.html</code>  -  only for HTTP 200 responses that pass <code>canSaveCache()</code> (anonymous: no logged-in user/admin)</li>
<li>Subsequent requests serve the cached file directly via <code>getCache()</code></li>
<li>A lock/stale file pair guards concurrent regeneration; non-200 responses clean up locks and stale files</li>
</ol>
<p>A request is cacheable (<code>canCache()</code>) only when: <code>APP</code> is <code>app</code>, no POST data, none of the <code>user</code>/<code>cart</code>/<code>admin</code>/<code>nocache</code> cookies are set, and the URL passes <code>validUrl()</code>  -  no <code>?</code>, <code>&amp;</code>, <code>\</code> or <code>..</code>, max 4 path levels, and not under <code>/user</code>, <code>/cart</code>, <code>/checkout</code> or <code>/feed</code>. Logged-in sessions therefore bypass (not invalidate) the cache; invalidation happens explicitly via <code>CacheManager::clearPageCache()</code> (called when settings, plugins, themes or content are saved).</p>
<h2>Template Engine (Vtpl)</h2>
<p>Vtpl compiles theme HTML plus <code>.tpl</code> selector rules into PHP. Theme files declare data needs with <code>data-v-component-*</code> attributes and bind values with <code>data-v-*</code> attributes; the matching rules in <code>app/template/components/*.tpl</code> wire each attribute to the component's result array:</p>
<pre><code class="language-html">&lt;!-- Theme HTML: declare the component and bind its fields --&gt;
&lt;div data-v-component-products data-v-limit=&quot;4&quot; data-v-type=&quot;product&quot;&gt;
	&lt;div data-v-product&gt;
		&lt;h2 data-v-product-name&gt;Product name&lt;/h2&gt;
		&lt;span data-v-product-price_formatted&gt;$100&lt;/span&gt;
		&lt;a href=&quot;&quot; data-v-product-url&gt;View&lt;/a&gt;
	&lt;/div&gt;
&lt;/div&gt;

&lt;!-- Conditional rendering, `category` is the current menu item --&gt;
&lt;span data-v-menu-item-name data-v-if=&quot;category.img&quot;&gt;Has image&lt;/span&gt;
&lt;span data-v-menu-item-name data-v-if-not=&quot;category.type = 'text'&quot;&gt;Documentation&lt;/span&gt;
</code></pre>
<pre><code># app/template/components/products.tpl (excerpt): repeatable + catch-all binding
@product = [data-v-component-products] [data-v-product]
@product|deleteAllButFirstChild
@product [data-v-product-*]|innerText = $product['@@__data-v-product-(*)__@@']
</code></pre>
<p>Compiled templates are stored in <code>storage/compiled-templates/</code> and recompiled when source files change (<code>View::checkNeedRecompile()</code>).</p>
<h2>Security Model</h2>
<ol>
<li><strong>Single public entry point</strong>: Only <code>public/index.php</code> is exposed. All controller logic is outside the web root.</li>
<li><strong>CSRF protection</strong>: per-session tokens (<code>admin/controller/base.php</code> generates <code>session csrf</code>, <code>checkCsrf()</code> validates it on admin form posts, 403 on mismatch); theme forms carry <code>&lt;input type=&quot;hidden&quot; name=&quot;csrf&quot; data-v-csrf&gt;</code>.</li>
<li><strong>Input filtering</strong>: All <code>$_GET</code>, <code>$_POST</code>, <code>$_COOKIE</code>, <code>$_FILES</code> are recursively filtered through <code>Request::filter()</code>.</li>
<li><strong>SQL injection prevention</strong>: SqlP uses parameterized queries with bound parameters.</li>
<li><strong>Password hashing</strong>: <code>password_hash()</code> with <code>PASSWORD_DEFAULT</code> (auto-rehash on cost/algorithm changes, see <code>Vvveb\System\User\Auth</code>).</li>
<li><strong>Permission system</strong>: allow/deny rules evaluated by <code>Vvveb\System\User\Role::has()</code> and checked on controller actions.</li>
<li><strong>Brute force protection</strong>: the admin login (<code>admin/controller/user/login.php</code>) caps failed attempts per account and responds with &quot;Too many login attempts, try again in one hour!&quot;.</li>
</ol>
<h2>Multi-Site Support</h2>
<p>Vvveb supports multiple sites from a single installation. Sites are defined in <code>config/sites.php</code> keyed by host pattern (e.g. <code>'* * *'</code>, <code>'blog *'</code>), each entry carrying <code>host</code>, <code>theme</code>, <code>state</code>, <code>site_id</code> (plus optional <code>template</code>/<code>path</code>):</p>
<pre><code class="language-php">// config/sites.php (excerpt)  -  keys are host patterns
return [
	'* * *' =&gt; [
		'name'    =&gt; 'admin',
		'host'    =&gt; '*.*.*',
		'theme'   =&gt; 'landing',
		'state'   =&gt; 'live',
		'site_id' =&gt; 1,
	],
];
</code></pre>
<p>Each site has its own:</p>
<ul>
<li>Theme configuration</li>
<li>Plugin activation status</li>
<li>Settings (stored with <code>site_id</code> foreign key)</li>
<li>Language and currency</li>
</ul>
<p>The <code>Sites</code> class (<code>Sites::getSiteData($site_id, $path)</code>) resolves the current site from the request hostname. Note plugins load after site resolution only for non-admin apps (<code>Plugins::loadPlugins()</code> is skipped when <code>APP === 'admin'</code>).</p>
<h2>Localization</h2>
<p>Vvveb uses PHP's gettext extension (or a PHP fallback) for translations. Translation files are stored in <code>locale/</code>. Content can be translated via the admin's multi-language support, with each post/product having content in multiple languages.</p>
]]></content:encoded>		
		</item><item data-v-post="post" data-v-id="442">
			<title data-v-post-name="name">Controller Development Guide</title>
			<link data-v-post-full-url="name">https://dev.vvveb.com/controller-development-guide</link>
			<dc:creator data-v-post-display_name="name"><![CDATA[Admin]]></dc:creator>
			<pubDate data-v-post-pubDate="date">Mon, 28 Sep 2026 17:24:02 +0000</pubDate>
			<category data-v-post-category-name="category" data-filter-cdata=""><![CDATA[Category]]></category>
			<description data-v-post-excerpt="excerpt" data-filter-cdata=""><![CDATA[Controllers handle HTTP requests and prepare data for template rendering. Vvveb has separate controller hierarchies for the admin back-end and front-end application. 
Base Classes 
Front-End Controller 
namespace Vvveb\Controller; class Base { public]]></description>
			<content:encoded data-v-post-content="content" data-filter-cdata=""><![CDATA[
<p>Controllers handle HTTP requests and prepare data for template rendering. Vvveb has separate controller hierarchies for the admin back-end and front-end application.</p>
<h2>Base Classes</h2>
<h3>Front-End Controller</h3>
<pre><code class="language-php">namespace Vvveb\Controller;

class Base {
    public Request $request;    // Vvveb\System\Core\Request
    public Response $response;  // Vvveb\System\Core\Response
    public View $view;          // Vvveb\System\Core\View
    public Session $session;    // Vvveb\System\Session
    protected array $global = []; // Global template data (language, locale, ...)
}
</code></pre>
<p>All four properties are injected by <code>FrontController::di()</code> before <code>init()</code> is called.</p>
<h3>Admin Controller</h3>
<p>The admin base controller (<code>admin/controller/base.php</code>) extends the same pattern but adds:</p>
<ul>
<li>CSRF token generation and validation (<code>checkCsrf()</code>, 403 on mismatch)</li>
<li>Admin authentication via <code>Admin::current()</code></li>
<li>Permission checking via <code>$this-&gt;permission()</code> (denies with &quot;Your role does not have permission to access this action!&quot;)</li>
<li>Admin menu building from <code>config/admin-menu.php</code> (cached in object cache under <code>admin-menu</code>)</li>
<li>Site switching support</li>
<li>Language and currency resolution</li>
<li>Plugin loading</li>
</ul>
<h2>Creating a Controller</h2>
<h3>Front-End Controller</h3>
<p>Create a file in <code>app/controller/</code> following the module path convention:</p>
<pre><code class="language-php">// app/controller/product/product.php
namespace Vvveb\Controller\Product;

class Product extends \Vvveb\Controller\Base {
    public function init(): void {
        // Optional: runs before the action method
        // Load site settings, check maintenance mode, etc.
    }

    public function index(): ?string {
        // The action method. Return:
        //   null/false  -  use default template (product/product.html)
        //   string  -  use a different template
        //   array  -  output as JSON

        $this-&gt;view-&gt;title = 'Product Page';
        $this-&gt;view-&gt;product_id = $this-&gt;request-&gt;get['product_id'] ?? null;

        return null; // Uses product/product.html template
    }
}
</code></pre>
<p>This controller is accessible at URLs matching the route pattern for <code>product/product/index</code>.</p>
<h3>Admin Controller</h3>
<pre><code class="language-php">// admin/controller/product/products.php
namespace Vvveb\Controller\Product;

class Products extends \Vvveb\Controller\Base {
    public string $type = 'product';
    public string $list = 'product';
    public string $module = 'product';

    public function index(): void {
        $model = model('product');
        $results = $model-&gt;getAll($this-&gt;request-&gt;get);

        $this-&gt;view-&gt;set($results);
        $this-&gt;view-&gt;status = [
            0 =&gt; __('Inactive'),
            1 =&gt; __('Active'),
        ];
    }
}
</code></pre>
<p>Admin controllers are accessed via query parameter: <code>?module=product/products</code>.</p>
<h2>CRUD Pattern</h2>
<p>For standard CRUD operations, use the built-in <code>Crud</code> controller or <code>CrudTrait</code>:</p>
<pre><code class="language-php">namespace Vvveb\Controller\Product;

class Products extends \Vvveb\Controller\Crud {
    public string $type = 'product';
    public string $list = 'product';
    public string $module = 'product';
    // Provides: index(), save(), delete() automatically
}
</code></pre>
<p>The <code>Crud</code> trait reads POST data, instantiates the SQL model, and calls <code>add()</code>, <code>edit()</code>, or <code>delete()</code> automatically.</p>
<h2>Listing Pattern</h2>
<p>For paginated list views, extend <code>Listing</code>:</p>
<pre><code class="language-php">namespace Vvveb\Controller\Order;

class Orders extends \Vvveb\Controller\Listing {
    public string $type = 'order';
    public string $list = 'order';
    public string $module = 'order';

    public function index(): void {
        parent::index(); // Handles pagination, row URLs
        // Additional data
        $this-&gt;view-&gt;status = getSetting('order', 'status');
    }
}
</code></pre>
<h2>Template Assignment</h2>
<p>Controllers set data on <code>$this-&gt;view</code> which becomes available in the template:</p>
<pre><code class="language-php">// Set individual properties
$this-&gt;view-&gt;title = 'Page Title';
$this-&gt;view-&gt;posts = $results['post'];

// Set multiple properties at once
$this-&gt;view-&gt;set($results);

// Set a JSON response instead of rendering a template
return $this-&gt;results;

// Override the template file
return 'custom/template.html';

// Disable template rendering entirely (for API responses)
return false;
</code></pre>
<h2>Request Data</h2>
<p>Access request data through <code>$this-&gt;request</code>:</p>
<pre><code class="language-php">// GET parameters
$page = $this-&gt;request-&gt;get['page'] ?? 1;

// POST data (already filtered)
$name = $this-&gt;request-&gt;post['name'] ?? '';

// Check request method
if ($this-&gt;request-&gt;getMethod() === 'post') { ... }

// Check if AJAX
if ($this-&gt;request-&gt;isAjax()) { ... }

// File uploads
$file = $this-&gt;request-&gt;files['upload'];
</code></pre>
<h2>Response Control</h2>
<pre><code class="language-php">// Set response type
$this-&gt;response-&gt;setType('json');

// Set HTTP status
$this-&gt;response-&gt;setStatus(404);

// Add headers
$this-&gt;response-&gt;addHeader('X-Custom', 'value');

// Redirect
$this-&gt;response-&gt;redirect('/new-url');

// Output data as JSON
$this-&gt;response-&gt;output($data);
</code></pre>
<h2>Session Usage</h2>
<pre><code class="language-php">// Read
$value = $this-&gt;session-&gt;get('key');

// Write
$this-&gt;session-&gt;set('key', $value);

// Delete
$this-&gt;session-&gt;delete('key');
</code></pre>
<h2>Error Handling</h2>
<pre><code class="language-php">// 404 Not Found
\Vvveb\System\Core\FrontController::notFound();

// Add validation errors (displayed in admin)
$this-&gt;view-&gt;errors[] = __('Name is required');

// Add success messages
$this-&gt;view-&gt;success[] = __('Saved successfully');
</code></pre>
<h2>Routing</h2>
<h3>Front-End Routes</h3>
<p>Routes are defined in <code>config/app-routes.php</code>:</p>
<pre><code class="language-php">/routes['/product/{slug}'] = ['module' =&gt; 'product/product/index'];
/routes['/blog/{slug}'] = ['module' =&gt; 'content/post/index'];
/routes['/cart/add/#product_id#'] = ['module' =&gt; 'cart/cart/add'];
</code></pre>
<p>Pattern types (see <code>Vvveb\System\Routes</code> regex constants):</p>
<ul>
<li><code>{name}</code>  -  matches a string segment, captured as <code>$this-&gt;request-&gt;get['name']</code></li>
<li><code>#name#</code>  -  matches a numeric segment (<code>\d+</code>), captured the same way</li>
<li><code>{name{min,max}}</code> / <code>#name{min,max}#</code>  -  same with length limits</li>
<li><code>*</code>  -  wildcard</li>
</ul>
<h3>Admin Routes</h3>
<p>Admin uses query-string routing: <code>?module=controller/action&amp;param=value</code></p>
<h3>URL Generation</h3>
<p>URLs are generated with <code>\Vvveb\url()</code> by passing the <strong>module name</strong> (<code>controller/action</code>); the router picks a matching route pattern for that module and fills its placeholders with the given parameters:</p>
<pre><code class="language-php">// Generate a front-end URL from a module name
$url = \Vvveb\url('product/product/index', ['product_id' =&gt; 123, 'slug' =&gt; 'my-product']);
// e.g. results in /product/my-product when a matching pattern exists

// Admin URLs use query-string modules
$url = \Vvveb\url('product/products/index');
</code></pre>
<p>If no route pattern matches (e.g. missing parameters), a fallback query-string URL is returned.</p>
<blockquote>
<p><strong>Note</strong>
Pass the module name, not the URL pattern: <code>\Vvveb\url('/product/{slug}', ...)</code> does not resolve because patterns are not lookup keys.</p>
</blockquote>
<h2>Plugin Controllers</h2>
<p>Plugins add controllers under <code>plugins/{slug}/admin/controller/</code> and <code>plugins/{slug}/app/controller/</code> (the folder selects the context), both using the <code>Vvveb\Plugins\&lt;Name&gt;\Controller</code> namespace:</p>
<pre><code class="language-php">// plugins/contact-form/admin/controller/message.php
namespace Vvveb\Plugins\ContactForm\Controller;

class Message extends \Vvveb\Controller\Base {
    public function index(): void {
        $this-&gt;view-&gt;data = 'Hello from plugin';
    }
}
</code></pre>
<p>Plugin routes are registered via events in the plugin's <code>plugin.php</code>.</p>
]]></content:encoded>		
		</item><item data-v-post="post" data-v-id="443">
			<title data-v-post-name="name">Payment sandbox runbook</title>
			<link data-v-post-full-url="name">https://dev.vvveb.com/payment-sandbox-runbook</link>
			<dc:creator data-v-post-display_name="name"><![CDATA[Admin]]></dc:creator>
			<pubDate data-v-post-pubDate="date">Mon, 28 Sep 2026 17:24:02 +0000</pubDate>
			<category data-v-post-category-name="category" data-filter-cdata=""><![CDATA[Category]]></category>
			<description data-v-post-excerpt="excerpt" data-filter-cdata=""><![CDATA[Verify each gateway end to end before going live, in this order - PayPal recurring first (newest code, no live call yet), then Mollie, then Stripe. 
Prerequisites: worker/site in test mode, storage/ writable (Payum tokens), cron endpoint reachable]]></description>
			<content:encoded data-v-post-content="content" data-filter-cdata=""><![CDATA[
<p>Verify each gateway end to end before going live, in this order  -  PayPal
recurring first (newest code, no live call yet), then Mollie, then Stripe.</p>
<p>Prerequisites: worker/site in test mode, <code>storage/</code> writable (Payum tokens),
cron endpoint reachable (renewals, trial counters, order expiry).</p>
<h2>PayPal recurring (Payum, PayPal Express)</h2>
<ol>
<li>Configure <code>PayPal_Express</code> with sandbox API username/password/signature
and <code>testMode</code> checked.</li>
<li>Buy a subscription product as a logged-in test user with a personal
sandbox buyer account.</li>
<li>Expect: redirect to sandbox PayPal → approve → return creates the
recurring profile; order moves to the success status; the core
subscription row claims <code>PayPal_Express-&lt;index&gt;:I-...</code> and goes active.</li>
<li>In admin, pause then resume the subscription: profile suspends/reactivates
in the sandbox merchant account, local status follows.</li>
<li>Cancel: profile cancels immediately; local row goes cancelled.</li>
<li>Expire: profile bills until term end (<code>cancel_at_period_end</code>); the
<code>customer.subscription.deleted</code>-equivalent IPN flips the row to cancelled.</li>
<li>Swap: change <code>AMT</code> via <code>swapSubscription</code> (used by plan propagation).</li>
</ol>
<h2>Mollie (test mode)</h2>
<ol>
<li>Use a <code>test_...</code> api key; buy with any method (e.g. iDEAL + test issuer).</li>
<li>Expect: hosted checkout → return marks the order paid, cart empties,
confirmation mails.</li>
<li>Cancel on Mollie's page: lands on the failed page with &quot;Payment was
cancelled!&quot;, order keeps the error status, cart intact for retry.</li>
<li>Refund from Mollie's dashboard: webhook path stores the refund and updates
the order; local refund is inbound-only (no outbound refund button).</li>
</ol>
<h2>Stripe (test mode)</h2>
<ol>
<li>Use <code>sk_test_...</code>/<code>pk_test_...</code>, test card <code>4242 4242 4242 4242</code>.</li>
<li>One-time Elements: approve → <code>/stripe-return</code> verifies the intent,
creates the order and confirms. Replay the return URL: must land closed
on the failed page (snapshot consumed, no duplicate order).</li>
<li>Subscriptions: approve → subscription row links <code>stripe:sub_...</code>.
Advance a <a href="https://docs.stripe.com/billing/testing">test clock</a> past the
trial/period: webhook creates exactly one renewal order per invoice
(re-fire the event to prove idempotency), trial counters refresh.</li>
<li><code>invoice.payment_failed</code>: row syncs to failed plus a dunning log note.</li>
<li>Admin cancel/pause/resume/swap hit the live subscription; failures keep
local status untouched with the API message shown.</li>
<li>Zero-decimal check: repeat a one-time purchase in JPY  -  charged amount
must equal the cart total, not ×100.</li>
</ol>
<h2>Double-submit and failure paths (all gateways)</h2>
<ol>
<li>Double-click Place Order: exactly one order (single-use <code>submit_token</code>;
the duplicate lands on confirmation).</li>
<li>Decline the payment (e.g. Stripe <code>4000 0000 0000 0002</code>): stay on checkout
with the gateway message, cart intact, no confirmation mail.</li>
<li>Kill the session mid-off-site-flow: return lands on the failed page, no
orphan paid-without-order state (verify against the gateway dashboard).</li>
</ol>
<h2>Cron</h2>
<p>Hit the cron endpoint and confirm <code>order-expire</code> cancels only stale
not_paid/awaiting online-method orders (bank-transfer/COD untouched) and
<code>subscription-subscription</code> refreshes <code>trial_left</code> without touching billing.</p>
]]></content:encoded>		
		</item><item data-v-post="post" data-v-id="444">
			<title data-v-post-name="name">Rest Api</title>
			<link data-v-post-full-url="name">https://dev.vvveb.com/rest-api-setup</link>
			<dc:creator data-v-post-display_name="name"><![CDATA[Admin]]></dc:creator>
			<pubDate data-v-post-pubDate="date">Mon, 28 Sep 2026 17:24:02 +0000</pubDate>
			<category data-v-post-category-name="category" data-filter-cdata=""><![CDATA[Category]]></category>
			<description data-v-post-excerpt="excerpt" data-filter-cdata=""><![CDATA[Rest Api is enabled by default (defined('REST') || define('REST', true) in env.php). To disable it, define REST as false: 
defined('REST') || define('REST', true);  
Authentication 
Authentication is done with HTTP Authorization or Bearer tokens using]]></description>
			<content:encoded data-v-post-content="content" data-filter-cdata=""><![CDATA[
<p>Rest Api is enabled by default (<code>defined('REST') || define('REST', true)</code> in <code>env.php</code>). To disable it, define <code>REST</code> as false:</p>
<pre><code class="language-php">defined('REST') || define('REST', true);
</code></pre>
<h2>Authentication</h2>
<p>Authentication is done with HTTP Authorization or Bearer tokens using any admin user credentials that has REST role permissions.</p>
<pre><code class="language-bash">curl -H 'Authorization: Bearer mytoken' 'https://demo.vvveb.com/rest/posts/hello-world-1'
</code></pre>
<pre><code class="language-bash">curl --user demo:demo 'https://demo.vvveb.com/rest/posts/hello-world-1'
</code></pre>
<h2>Permissions</h2>
<p>Permission can be controlled from admin user roles and can be set for both resource and action.</p>
<h2>Open API</h2>
<p>Open Api documentation is provided by the Open Api plugin <a href="https://plugins.vvveb.com/product/openapi">https://plugins.vvveb.com/product/openapi</a></p>
<h2>Next.js starter</h2>
<p>Simple sample blog built with Next.js with REST Api <a href="https://github.com/givanz/nextjs-headless-rest-vvveb">https://github.com/givanz/nextjs-headless-rest-vvveb</a></p>
<p>Demo <a href="https://nextjs-headless-rest-vvveb.vercel.app/">nextjs-rest-vvveb.vercel.app</a></p>
]]></content:encoded>		
		</item><item data-v-post="post" data-v-id="445">
			<title data-v-post-name="name">Vvveb CMS GraphQL API Documentation</title>
			<link data-v-post-full-url="name">https://dev.vvveb.com/vvveb-cms-graphql-api-documentation</link>
			<dc:creator data-v-post-display_name="name"><![CDATA[Admin]]></dc:creator>
			<pubDate data-v-post-pubDate="date">Mon, 28 Sep 2026 17:24:02 +0000</pubDate>
			<category data-v-post-category-name="category" data-filter-cdata=""><![CDATA[Category]]></category>
			<description data-v-post-excerpt="excerpt" data-filter-cdata=""><![CDATA[Based on the provided schema, here is the technical documentation for the core entities and types available in the Vvveb CMS GraphQL API. 
 
This documentation outlines the primary data structures and types available for querying and mutations within the]]></description>
			<content:encoded data-v-post-content="content" data-filter-cdata=""><![CDATA[<p>Based on the provided schema, here is the technical documentation for the core entities and types available in the Vvveb CMS GraphQL API.</p>
<hr />

<p>This documentation outlines the primary data structures and types available for querying and mutations within the Vvveb CMS environment.</p>
<h2>Core Commerce Types</h2>
<h3>Product (<code>product</code>)</h3>
<p>The central entity for the storefront. It contains physical attributes, pricing, and associations with content and taxonomies.</p>
<table class="table table-bordered">
<thead class="table-light">
<tr>
<th>Field</th>
<th>Type</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>product_id</code></td>
<td><code>ID</code></td>
<td>Unique identifier for the product.</td>
</tr>
</tbody>
</table>
<p>|
| <code>model</code> | <code>String</code> | Product model identifier.</p>
<p>|
| <code>sku</code> | <code>String</code> | Stock Keeping Unit.</p>
<p>|
| <code>price</code> | <code>Float</code> | The current price of the product.</p>
<p>|
| <code>old_price</code> | <code>Float</code> | The original price (useful for showing discounts).</p>
<p>|
| <code>stock_quantity</code> | <code>Int</code> | Number of items currently in stock.</p>
<p>|
| <code>image</code> | <code>String</code> | Path to the main product image.</p>
<p>|
| <code>weight</code> | <code>Float</code> | Physical weight for shipping calculations.</p>
<p>|
| <code>variants</code> | <code>[ProductVariantType]</code> | Available variations (size, color, etc.).</p>
<p>|
| <code>contents</code> | <code>[ProductContentType]</code> | Localized names and descriptions.</p>
<p>|</p>
<h3>Cart (<code>cart</code>)</h3>
<p>Represents a user's active shopping session.</p>
<table class="table table-bordered">
<thead class="table-light">
<tr>
<th>Field</th>
<th>Type</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>cart_id</code></td>
<td><code>Id</code></td>
<td>Unique identifier for the cart.</td>
</tr>
</tbody>
</table>
<p>|
| <code>products</code> | <code>[CartProductType]</code> | List of items currently in the cart.</p>
<p>|
| <code>totals</code> | <code>[TotalType]</code> | Breakdown of subtotal, tax, and shipping.</p>
<p>|
| <code>total_price</code> | <code>Float</code> | The grand total amount.</p>
<p>|
| <code>total_formatted</code> | <code>String</code> | Currency-formatted total string.</p>
<p>|
| <code>checkout_url</code> | <code>String</code> | Direct link to the checkout page.</p>
<p>|</p>
<hr />
<h2>Content &amp; Blogging</h2>
<h3>Post (<code>post</code>)</h3>
<p>Used for blog articles, news, or generic CMS pages.</p>
<table class="table table-bordered">
<thead class="table-light">
<tr>
<th>Field</th>
<th>Type</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>post_id</code></td>
<td><code>ID</code></td>
<td>Unique identifier for the post.</td>
</tr>
</tbody>
</table>
<p>|
| <code>status</code> | <code>String</code> | The post status (e.g., &quot;published&quot;, &quot;draft&quot;).</p>
<p>|
| <code>comment_count</code> | <code>Int</code> | Total number of approved comments.</p>
<p>|
| <code>created_at</code> | <code>String</code> | Timestamp of post creation.</p>
<p>|
| <code>contents</code> | <code>[PostContentType]</code> | Multi-language content (title, body, slug).</p>
<p>|
| <code>seo</code> | <code>[SeoType]</code> | Metadata for search engine optimization.</p>
<p>|</p>
<h3>Media (<code>media</code>)</h3>
<p>Represents uploaded assets like images and videos.</p>
<table class="table table-bordered">
<thead class="table-light">
<tr>
<th>Field</th>
<th>Type</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>media_id</code></td>
<td><code>ID</code></td>
<td>Unique identifier.</td>
</tr>
</tbody>
</table>
<p>|
| <code>file</code> | <code>String</code> | The file path or URL.</p>
<p>|
| <code>type</code> | <code>String</code> | MIME type or category (image, video, etc.).</p>
<p>|</p>
<hr />
<h2>Orders &amp; Customers</h2>
<h3>Order (<code>order</code>)</h3>
<p>A finalized transaction containing snapshot data of the purchase.</p>
<ul>
<li>
<p><strong>Identifiers</strong>: Includes <code>order_id</code>, <code>invoice_no</code>, and <code>customer_order_id</code>.</p>
</li>
<li>
<p><strong>Customer Info</strong>: Stores <code>first_name</code>, <code>last_name</code>, <code>email</code>, and <code>phone_number</code>.</p>
</li>
<li>
<p><strong>Shipping/Billing</strong>: Comprehensive address fields including <code>address_1</code>, <code>city</code>, <code>post_code</code>, and <code>country_id</code>.</p>
</li>
<li>
<p><strong>Financials</strong>: Tracks <code>payment_method</code>, <code>shipping_method</code>, <code>total</code>, and <code>currency_id</code>.</p>
</li>
<li>
<p><strong>Associations</strong>: Links to <code>products</code>, <code>shipments</code>, <code>subscriptions</code>, and <code>vouchers</code>.</p>
</li>
</ul>
<hr />
<h2>️ System &amp; Configuration</h2>
<ul>
<li>
<p><strong>Localization</strong>: Supported via <code>language</code> (codes, locales, RTL support) and <code>currency</code> (symbols, decimal places, exchange values).</p>
</li>
<li>
<p><strong>Geography</strong>: <code>country</code> data including ISO codes and status.</p>
</li>
<li>
<p><strong>Administration</strong>: <code>admin</code> type manages backend users, including <code>role_id</code>, <code>auth_tokens</code>, and <code>failed_logins</code>.</p>
</li>
<li>
<p><strong>Marketing</strong>: <code>coupon</code> management (discount types, usage limits, date ranges) and <code>newsletter_list</code> management.</p>
</li>
<li>
<p><strong>Customization</strong>: <code>field</code> and <code>field_group</code> allow for extensible data models across posts and products.</p>
</li>
</ul>
<hr />
<h3>Pro Tip: Localized Content</h3>
<p>Most primary entities (Product, Post, Category) do not store names directly. Instead, they use a <code>contents</code> array (e.g., <code>ProductContentType</code>) which maps content to a specific <code>language_id</code>. Always query the <code>contents</code> field to retrieve the correct translation for your frontend.</p>
]]></content:encoded>		
		</item><item data-v-post="post" data-v-id="446">
			<title data-v-post-name="name">Sidebar</title>
			<link data-v-post-full-url="name">https://dev.vvveb.com/sidebar</link>
			<dc:creator data-v-post-display_name="name"><![CDATA[Admin]]></dc:creator>
			<pubDate data-v-post-pubDate="date">Mon, 28 Sep 2026 17:24:02 +0000</pubDate>
			<category data-v-post-category-name="category" data-filter-cdata=""><![CDATA[Category]]></category>
			<description data-v-post-excerpt="excerpt" data-filter-cdata=""><![CDATA[Overview 
Components 
Inputs 
Sections 
Events]]></description>
			<content:encoded data-v-post-content="content" data-filter-cdata=""><![CDATA[<br/>
<br/>
<p align="center"><img src="https://www.vvveb.com/admin/default/img/biglogo.png" alt="VvvebJs" align="center"/></p>
<br/>
<br/>
<ul>
<li><a href="./Home">Overview</a></li>
<li><a href="./Components">Components</a></li>
<li><a href="./Inputs">Inputs</a></li>
<li><a href="./Sections">Sections</a></li>
<li><a href="./Events">Events</a></li>
</ul>
]]></content:encoded>		
		</item><item data-v-post="post" data-v-id="447">
			<title data-v-post-name="name">Payments and subscriptions</title>
			<link data-v-post-full-url="name">https://dev.vvveb.com/payments-and-subscriptions</link>
			<dc:creator data-v-post-display_name="name"><![CDATA[Admin]]></dc:creator>
			<pubDate data-v-post-pubDate="date">Mon, 28 Sep 2026 17:24:02 +0000</pubDate>
			<category data-v-post-category-name="category" data-filter-cdata=""><![CDATA[Category]]></category>
			<description data-v-post-excerpt="excerpt" data-filter-cdata=""><![CDATA[How payment methods, off-site redirects, webhooks and recurring subscriptions fit together. 
Payment methods (Vvveb\System\PaymentMethod) 
system/payment-method.php is the contract every gateway plugin implements (plugins/*/payment.php). Only three]]></description>
			<content:encoded data-v-post-content="content" data-filter-cdata=""><![CDATA[
<p>How payment methods, off-site redirects, webhooks and recurring subscriptions fit together.</p>
<h2>Payment methods (<code>Vvveb\System\PaymentMethod</code>)</h2>
<p><code>system/payment-method.php</code> is the contract every gateway plugin implements
(<code>plugins/*/payment.php</code>). Only three methods are abstract:</p>
<ul>
<li><code>getMethod(&amp;$checkoutInfo, $options)</code>  -  availability + display data, or <code>false</code></li>
<li><code>init()</code> / <code>setMethod()</code>  -  cart total/tax bookkeeping</li>
</ul>
<p>Everything else has a <code>return false</code> default stub, so simple offline methods
(bank transfer, COD) stay tiny and nothing fatals on a missing operation.</p>
<h3>Operation results</h3>
<p>Transaction methods return <code>bool</code>, except <code>authorize()</code>/<code>purchase()</code> which may
also return a redirect result:</p>
<pre><code class="language-php">return $this-&gt;redirect($url); // ['status' =&gt; 'redirect', 'redirect' =&gt; $url]
</code></pre>
<ul>
<li><code>true</code>  -  success, core continues (mail, cart empty, confirmation).</li>
<li><code>false</code>  -  failure. Set the reason first with <code>$this-&gt;fail('...')</code>; checkout
stays on the page with the message and keeps the cart.</li>
<li>redirect array  -  off-site gateway (PayPal, Mollie, 3DS). <strong>Never redirect or
<code>exit</code> inside the method</strong>  -  core persists payment data and redirects, so
order creation, events and mail stay in one place.</li>
</ul>
<p>Inspect details via <code>Payment::getError()</code> / <code>Payment::getRedirectUrl()</code>.</p>
<h3>Capabilities</h3>
<p><code>$method-&gt;supports('refund')</code> reports whether the concrete class overrides an
operation  -  no per-method declarations needed. Admin buttons (capture, refund,
void) and <code>Payment::createCard()</code>/<code>webhook()</code> dispatch honor it.</p>
<h3>Shared helpers (protected)</h3>
<ul>
<li><code>toCents($total)</code>, <code>idempotencyKey($checkoutInfo)</code>  -  pass the key as the
gateway transaction reference so retried submits don't double-charge.</li>
<li><code>storedPaymentData()</code> / <code>storePaymentData($info, $data, $statusId = null)</code>  -
array-or-JSON tolerant, persists to the order when <code>order_id</code> is present.</li>
<li><code>verifyHmac($payload, $signature, $secret)</code>  -  webhook signature checks.</li>
</ul>
<h2>Webhooks</h2>
<p>Each plugin keeps its own route but implements the shared shape:
verify signature → apply update idempotently → plain <code>OK</code>/JSON ack so the
gateway stops retrying. Failures must never break the HTTP 200 response.</p>
<h2>Shipments and multi-package quotes</h2>
<p><code>ship()</code> records one <code>order_shipment</code> row per package (idempotent per
method+tracking) instead of returning a blind <code>true</code>; checkout aborts with
the method error when it fails, before any charge. The admin order page
renders the rows; carrier booking fills tracking numbers later.</p>
<p>Carriers split cart lines into shipping groups by shipping class (one
default group preserves single-package behavior), quote each package
through the cached rate layer, and sum the totals  -  a group that fails
falls the whole method back to its flat rate so pricing stays coherent.
<code>AbstractCarrier::groupItems()</code> is the pure, unit-tested seam; flat,
weight and pickup methods stay single-package. Every package carries an
explicit origin resolved per group (explicit <code>origins</code> map, then the
<code>AbstractCarrier::origin</code> event, then the fulfilment warehouse, then the
site address). Warehouses (<code>order/warehouses</code> admin) hold name + address
and product assignments; cart lines are tagged with their warehouse
(<code>tagWarehouses()</code>, default = first active) and grouped per warehouse, so
multi-origin quoting and shipment origins work out of the box.</p>
<p>The method contract mirrors payments (<code>supports('ship')</code>, <code>getError()</code>),
and the dispatcher gained <code>getMethodInstance()</code> plus an optional cart.</p>
<p>Live carriers (UPS OAuth2, FedEx OAuth2, DHL key, USPS key) implement
rating/tracking/address/pickup behind per-carrier settings with flat-rate
fallback; each honors a <code>test_mode</code> sandbox endpoint. Rate parsers are
fixture-tested with stubbed HTTP (<code>CarrierRateTest</code>), so only live
credentials stand between here and verified live quotes.</p>
<h2>Partial capture and refund</h2>
<p><code>capture()</code>/<code>refund()</code> honor an <code>amount</code> checkout key (partial
capture/refund); absent means full. The admin order page exposes amount
inputs prefilled with the ledger-derived remaining total and rejects
over-amounts server-side. Stripe and Omnipay thread the override;
Payum capture and subscription-invoice refunds are full-only and fail
closed when a partial is requested. Every partial writes its own ledger
row, so the Transactions timeline is the audit trail.</p>
<h2>Subscriptions</h2>
<p>Core <code>subscription</code> rows are gateway-agnostic. Unlike one-shot Omnipay
drivers, Payum supports recurring billing natively: PayPal Express
recurring profiles (<code>CreateRecurringPaymentProfile</code>,
<code>ManageRecurringPaymentsProfileStatus</code>, <code>UpdateRecurringPaymentProfile</code>) and
Stripe subscriptions (<code>CreateSubscription</code>, plus SDK cancel/pause/resume/swap).
The payum plugin implements the full subscription contract for both
(<code>SUPPORTS_SUBSCRIPTIONS</code>), including a billing-agreement checkout branch
and a <code>payum-recurring</code> return action that claims the pending core row. Rows provisioned at a gateway
store a <code>gateway:id</code> pair in <code>payment_method</code> (e.g. <code>stripe:sub_1</code>).</p>
<h3>Lifecycle operations</h3>
<p><code>cancelSubscription</code>, <code>pauseSubscription</code>, <code>resumeSubscription</code>,
<code>swapSubscription</code> (plus <code>createSubscription</code>) default to <code>false</code> on the base;
Stripe implements cancel/pause/resume/swap. Context carries
<code>gateway_subscription_id</code> (see <code>gatewaySubscriptionId()</code>).</p>
<p><code>Vvveb\System\SubscriptionManager::gatewayOperation($op, $row, $order)</code> runs
the gateway call first and returns an error string on failure  -  callers
(admin <code>order/subscription</code>, customer <code>user/subscription</code>) only change local
status when it returns null. Rows without a gateway reference keep the legacy
local-only behavior. Lifecycle actions require POST (+CSRF customer-side).</p>
<p>The customer portal self-serves pause/resume/cancel plus plan swap (price
ensured from the plan mapping, no new trial) and skip-a-payment (next date
pushed one interval at the gateway and locally via
<code>SubscriptionManager::addInterval()</code>).</p>
<h3>Checkout provisioning</h3>
<p>Checkout creates <strong>pending</strong> core rows for every subscription line, for any
method. Stripe claims them (via <code>local_subscription_id</code> metadata) instead of
inserting duplicates; manual methods stay pending until confirmed.</p>
<h3>Elements return flow</h3>
<p>The Elements form leaves the store before any order exists, so
<code>plugins/stripe/index/client</code> snapshots the checkout form, and
<code>/stripe-return</code> verifies the Setup/PaymentIntent server-side, creates the
order, links subscription rows race-safely both ways, and confirms. The
displayed amount composes trial prices, recurring amounts and one-time
lines. Guest checkouts that opt into registration get an account with a
random password plus a reset link; account problems never fail the paid
order. Snapshots are consumed, so replays land closed. Both the Elements return
and renewal orders fire the checkout <code>add:after</code> event, so inventory
deduction, gift-card debit, accounting, tax, fraud and marketing listeners
run uniformly on every order path (admin manual orders stay out  -  they
start lineless and merchant-managed). Core warehouses are fulfilment
origins for shipping; the inventory plugin's locations remain the stock
ledger  -  separate systems, bridged by the shared order event.</p>
<h3>Order statuses</h3>
<p>Default / Pending / Success / Error payment statuses mirror the Omnipay
plugin (defaults awaiting 2, paid 4, canceled 5). Success drives order
creation, captures and renewals; errors are now recorded on failed
capture/refund/void where previously nothing was written. Note: capture
success previously wrote status 2 and now writes the success status.</p>
<h3>Link autofill</h3>
<p>Link by Stripe (one-click autofill in the payment form) is on by default
and can be switched off with the plugin's Link setting, which passes
<code>wallets: {link: 'never'}</code> to Elements. Stripe still requires Link to be
turned off in the dashboard (Settings &gt; Payments) for full account-wide
removal.</p>
<h3>Express wallets</h3>
<p>One-time checkouts offer Apple Pay / Google Pay through Stripe's Payment
Request Button (hidden for subscription carts and when the wallet is
unavailable). The wallet sheet collects contact and shipping details, the
existing <code>/stripe-client</code> endpoint mints the PaymentIntent plus snapshot,
and the same return URL verifies and orders. The sheet total is the cart
total at button time; wallet-address shipping differences are recorded on
the order rather than re-charged.</p>
<h3>Renewals and dunning</h3>
<p>Stripe's <code>invoice.payment_succeeded</code> webhook creates an idempotent renewal
order (per-invoice log marker, totals from the invoice, lines cloned from the
original order). <code>invoice.payment_failed</code> climbs a 4-attempt dunning ladder (day 0/1/3/7)
shared between webhook and the daily <code>subscription-dunning</code> cron pass via
structured log notes  -  retries pay open invoices (already-paid ones report
success without double-charging), recoveries flip back to active, and
exhausted ladders cancel at the gateway. Manual-method rows stay out of
automatic dunning. Mixed carts bill one-time lines onto the first subscription invoice
(<code>syncOneTimeInvoiceItems()</code>, reconciled per order/cart reference so retries
can't double-bill).</p>
<h3>Stripe Elements return flow</h3>
<p>The Elements form leaves the store before any order exists, so
<code>plugins/stripe/index/client</code> snapshots the checkout form into
<code>stripe_pending</code>, and <code>/stripe-return</code> verifies the Setup/PaymentIntent
server-side, creates the order, links subscription rows (race-safe both ways
via <code>linkSubscriptionOrder()</code> + metadata stamping) and confirms. The snapshot
is consumed  -  replays land closed on the failed page.</p>
<h3>Deliberate non-goals</h3>
<ul>
<li>Plan-<em>definition</em> edits propagate intervals on explicit save (new price +
swap of active gateway subs), but nothing auto-fires  -  gateway prices are
immutable, so this stays an intentional admin action.</li>
<li><code>expire()</code> stops billing at term end for provisioned rows, locally expiring
immediately; the gateway webhook flips the row to cancelled at term end.</li>
<li>Guest Elements one-time orders skip account auto-registration (no credential
survives the off-site round-trip).</li>
</ul>
]]></content:encoded>		
		</item><item data-v-post="post" data-v-id="448">
			<title data-v-post-name="name">Component Development Guide</title>
			<link data-v-post-full-url="name">https://dev.vvveb.com/component-development-guide</link>
			<dc:creator data-v-post-display_name="name"><![CDATA[Admin]]></dc:creator>
			<pubDate data-v-post-pubDate="date">Mon, 28 Sep 2026 17:24:02 +0000</pubDate>
			<category data-v-post-category-name="category" data-filter-cdata=""><![CDATA[Category]]></category>
			<description data-v-post-excerpt="excerpt" data-filter-cdata=""><![CDATA[Components are the heart of Vvveb's MVCC pattern. They are self-contained data-fetching units declared in HTML templates via data-v-component-* attributes. The template declares what data it needs, and the component system automatically discovers,]]></description>
			<content:encoded data-v-post-content="content" data-filter-cdata=""><![CDATA[
<p>Components are the heart of Vvveb's MVCC pattern. They are self-contained data-fetching units declared in HTML templates via <code>data-v-component-*</code> attributes. The template declares what data it needs, and the component system automatically discovers, executes, and caches the results.</p>
<h2>How Components Work</h2>
<ol>
<li><strong>Discovery</strong>: During template compilation, the system parses the HTML for <code>data-v-component-*</code> attributes</li>
<li><strong>Execution</strong>: Each component class is instantiated with its options, and <code>results()</code> is called</li>
<li><strong>Caching</strong>: Results are cached per component + options hash</li>
<li><strong>Injection</strong>: Results are stored in <code>$view-&gt;_component[name][index]</code> and available in the template</li>
</ol>
<h2>Creating a Component</h2>
<h3>Define the Component Class</h3>
<pre><code class="language-php">// app/component/products.php
namespace Vvveb\Component;

use Vvveb\System\Component\ComponentBase;
use Vvveb\System\Event;
use Vvveb\System\Images;
use Vvveb\Sql\ProductSQL;

class Products extends ComponentBase {
    public int $cacheExpire = 3600; // Cache for 1 hour, 0 = no cache

    public static array $defaultOptions = [
        'limit'            =&gt; 4,
        'page'             =&gt; 1,
        'type'             =&gt; 'product',
        'status'           =&gt; 1,
        'manufacturer_id'  =&gt; null,
        'taxonomy_item_id' =&gt; null,
        'order_by'         =&gt; null,
        'direction'        =&gt; null,
    ];

    public function results(): array {
        $products = new ProductSQL();

        $results = $products-&gt;getAll($this-&gt;options);

        // Format prices and images (see app/component/product.php for a full example)
        $currency = \Vvveb\System\Cart\Currency::getInstance($this-&gt;options);

        foreach ($results['product'] as &amp;$product) {
            $product['price'] = $currency-&gt;format($product['price']);
            $product['image'] = Images::image($product['image'], 'product');
            $product['url']   = \Vvveb\url('product/product/index', ['product_id' =&gt; $product['product_id'], 'slug' =&gt; $product['slug']]);
        }

        // Allow plugins to modify results
        list($results) = Event::trigger(__CLASS__, __FUNCTION__, $results);

        return $results;
    }

    public function request(array &amp;$results, int $index = 0): array {
        // Called per-request (after cache). Use for dynamic per-request tweaks.
        // Mark active category, strip password-protected content, etc.
        return $results;
    }
}
</code></pre>
<h3>Declare in Template</h3>
<pre><code class="language-html">&lt;!-- Simple usage with defaults --&gt;
&lt;div data-v-component-products&gt;&lt;/div&gt;

&lt;!-- With options (only names listed in the class's $defaultOptions are accepted) --&gt;
&lt;div data-v-component-products
     data-v-limit=&quot;8&quot;
     data-v-taxonomy_item_id=&quot;5&quot;
     data-v-order_by=&quot;price&quot;
     data-v-direction=&quot;asc&quot;&gt;
&lt;/div&gt;

&lt;!-- Dynamic options from URL --&gt;
&lt;div data-v-component-products
     data-v-manufacturer_id=&quot;url&quot;&gt;
&lt;/div&gt;

&lt;!-- Dynamic with fallback (falls back to 12 when the URL has no limit) --&gt;
&lt;div data-v-component-products
     data-v-taxonomy_item_id=&quot;url&quot;
     data-v-limit='[&quot;url&quot;, 12]'&gt;
&lt;/div&gt;
</code></pre>
<h3>Use in Template</h3>
<pre><code class="language-html">&lt;div data-v-component-products data-v-limit=&quot;8&quot;&gt;
    &lt;div data-v-product&gt;
        &lt;h3 data-v-product-name&gt;Product name&lt;/h3&gt;
        &lt;span data-v-product-price_formatted&gt;$100&lt;/span&gt;
        &lt;a href=&quot;&quot; data-v-product-url&gt;View&lt;/a&gt;
    &lt;/div&gt;
&lt;/div&gt;
</code></pre>
<p>Each <code>data-v-product</code> block repeats per product; the rules in <code>app/template/components/products.tpl</code> bind every <code>data-v-product-*</code> attribute to the matching result field.</p>
<h2>Component Options</h2>
<h3>Default Options</h3>
<p>Define <code>$defaultOptions</code> in your component class. These are merged with any <code>data-v-*</code> attributes from the template:</p>
<pre><code class="language-php">public static array $defaultOptions = [
    'limit' =&gt; 12,
    'status' =&gt; 1,
    'order_by' =&gt; null,
    'direction' =&gt; null,
    'type' =&gt; null,
];
</code></pre>
<p>Only attributes whose names appear in <code>$defaultOptions</code> are accepted  -  unknown <code>data-v-*</code> attributes are ignored (<code>Component::parseComponentElement()</code>). Global context (<code>site_id</code>, <code>language_id</code>, <code>currency</code>, <code>user_id</code>, …) is merged into the defaults automatically, and empty <code>site_id</code>/<code>language_id</code>/<code>currency_id</code>/<code>user_id</code>/<code>user_group_id</code> fall back to the current request values.</p>
<h3>Option Types</h3>
<table class="table table-bordered">
<thead class="table-light">
<tr>
<th>Template Attribute</th>
<th>Value Type</th>
<th>Example</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>data-v-limit=&quot;8&quot;</code></td>
<td>Integer</td>
<td><code>data-v-limit=&quot;8&quot;</code></td>
</tr>
<tr>
<td><code>data-v-status=&quot;1&quot;</code></td>
<td>Mixed</td>
<td><code>data-v-status=&quot;1&quot;</code></td>
</tr>
<tr>
<td><code>data-v-type=&quot;post&quot;</code></td>
<td>String</td>
<td><code>data-v-type=&quot;post&quot;</code></td>
</tr>
<tr>
<td><code>data-v-manufacturer_id=&quot;url&quot;</code></td>
<td>From URL</td>
<td>Reads <code>$_GET</code>/<code>$_REQUEST['manufacturer_id']</code> (supports <code>url.field</code> dotted form)</td>
</tr>
<tr>
<td><code>data-v-limit='[&quot;url&quot;, 5]'</code></td>
<td>With fallback</td>
<td><code>[url, 5]</code>  -  URL param or default <code>5</code> (PHP-side form is <code>'limit' =&gt; ['url', 5]</code>)</td>
</tr>
<tr>
<td><code>data-v-options='{&quot;key&quot;:&quot;val&quot;}'</code></td>
<td>JSON</td>
<td>Decoded to array</td>
</tr>
</tbody>
</table>
<h3>URL Parameter Binding</h3>
<p>When an option value is <code>'url'</code>, it's automatically resolved from the current request:</p>
<pre><code class="language-php">// Template: data-v-category_id=&quot;url&quot;
// URL: /category/electronics?category_id=5
// Resolved: $this-&gt;options['category_id'] = 5

// With fallback: data-v-category_id='[&quot;url&quot;, 1]'
// If not in URL: $this-&gt;options['category_id'] = 1
</code></pre>
<h2>Caching</h2>
<p>Components support automatic caching via the <code>Cache</code> system:</p>
<pre><code class="language-php">class Products extends ComponentBase {
    public int $cacheExpire = 3600; // 1 hour (0 = no cache)

    public function cacheKey(): ?string {
        return null; // Disable caching entirely
        // Or return a custom key
    }
}
</code></pre>
<h3>Cache Behavior</h3>
<ul>
<li><strong>Cache hit</strong>: Returns cached results directly</li>
<li><strong>Cache expired</strong>: If within the stale window, serves stale data while regenerating in background</li>
<li><strong>Concurrent requests</strong>: Uses lock files with retry/wait to prevent thundering herd</li>
<li><strong>Cache key</strong>: Generated from class name + serialized options hash</li>
</ul>
<h3>When to Disable Caching</h3>
<pre><code class="language-php">public int $cacheExpire = 0; // No caching
// Or
public function cacheKey(): ?string {
    return null; // Disable caching
}
</code></pre>
<p>Use <code>cacheExpire = 0</code> when:</p>
<ul>
<li>Component shows user-specific data (e.g., cart, wishlist)</li>
<li>Component shows real-time data (e.g., stock levels)</li>
<li>Component data changes frequently and must be fresh</li>
</ul>
<h2>Component Lifecycle</h2>
<pre><code>1. ComponentBase::__construct($options)
   ├── Merge $options with $defaultOptions
   ├── Resolve URL-bound options
   ├── Fire Event::trigger(__CLASS__, '__construct', $this-&gt;options)
   └── $this-&gt;di($this)  -  inject request, view, session

2. Component::loadComponents() (called by View::render)
   ├── Check cache (Cache::getMulti)
   ├── If cache hit → use cached results
   ├── If cache miss → call $obj-&gt;results()
   │   ├── Execute SQL model query
   │   ├── Format data (images, prices, URLs)
   │   └── Fire Event::trigger(__CLASS__, 'results', $results)
   ├── Store in $view-&gt;_component[name][index]
   └── Call $obj-&gt;request() on each component

3. Template rendering
   └── Access via $view-&gt;_component[name][index]
</code></pre>
<h2>Available Components</h2>
<p>See the <a href="code-reference/components/components-overview.md">code-reference</a> for the full documented list (45 components: shop, products, content, users, navigation). Common front-end examples (<code>app/component/</code>): <code>posts</code>, <code>products</code>, <code>product</code>, <code>categories</code>, <code>category</code>, <code>cart</code>, <code>checkout/payment</code>, <code>checkout/shipping</code>, <code>product/options</code>, <code>product/variants</code>, <code>content/tags</code>, <code>user/wishlist</code>, <code>menu</code>, <code>currency</code>, <code>breadcrumb</code>, <code>fields</code>. Admin components (<code>admin/component/</code>) include <code>posts</code>, <code>products</code>, <code>users</code>, <code>orders</code>, <code>comments</code>, <code>languages</code>, <code>sites</code>, <code>stats</code>, <code>notifications</code>, among others  -  they serve the admin templates rather than themes.</p>
<h3>Plugin Components</h3>
<p>Plugins can register components with the prefix <code>data-v-component-plugin-{plugin-name}-{namespace}</code>:</p>
<pre><code class="language-html">&lt;div data-v-component-plugin-contact-form-contact-form&gt;&lt;/div&gt;
</code></pre>
<h2>The Component Engine</h2>
<p>The <code>Component</code> class (<code>system/component/component.php</code>) is the singleton orchestrator:</p>
<pre><code class="language-php">// Manually trigger component loading (rarely needed)
$component = \Vvveb\System\Component\Component::getInstance($view);
</code></pre>
<h3>Component Discovery</h3>
<p><code>generateRequiredComponents()</code> parses the HTML template DOM:</p>
<ol>
<li>Processes <code>data-v-copy-from</code> attributes (template inheritance)</li>
<li>Queries XPath for all <code>data-v-component-*</code> attributes</li>
<li>Extracts component name and options from <code>data-v-*</code> attributes</li>
<li>Validates options against <code>$defaultOptions</code></li>
<li>Caches definitions to <code>.component</code> files</li>
</ol>
<h3>Component Results Storage</h3>
<p>Results are stored in the View object:</p>
<pre><code class="language-php">// In template:
$products = $view-&gt;_component['products'][0];
$posts = $view-&gt;_component['posts'][0];
$categories = $view-&gt;_component['categories'][0];
</code></pre>
<p>Each component instance gets its own index, allowing multiple instances of the same component on one page.</p>
<h2>Events</h2>
<p>Components fire events that plugins can hook into:</p>
<pre><code class="language-php">// After results are fetched
\Vvveb\System\Event::on('Vvveb\Component\Products', 'results', 'my-plugin', function($results) {
    // Modify products before they reach the template
    $results['product'] = array_filter($results['product'], fn($p) =&gt; $p['featured']);
    return [$results]; // listeners must return their parameters wrapped in an array
}, 1000);

// After component is constructed
\Vvveb\System\Event::on('Vvveb\Component\Products', '__construct', 'my-plugin', function($options) {
    // Modify options before query execution
    $options['limit'] = min($options['limit'], 20);
    return [$options];
}, 1000);
</code></pre>
]]></content:encoded>		
		</item><item data-v-post="post" data-v-id="449">
			<title data-v-post-name="name">VvvebJs components (legacy)</title>
			<link data-v-post-full-url="name">https://dev.vvveb.com/vvvebjs-components</link>
			<dc:creator data-v-post-display_name="name"><![CDATA[Admin]]></dc:creator>
			<pubDate data-v-post-pubDate="date">Mon, 28 Sep 2026 17:24:02 +0000</pubDate>
			<category data-v-post-category-name="category" data-filter-cdata=""><![CDATA[Category]]></category>
			<description data-v-post-excerpt="excerpt" data-filter-cdata=""><![CDATA[Overview 
Component object is used to define the html blocks that can be added and edited on the page. 
To add a component you need to use Vvveb.Components.add method or extend an existing component, like a base component with Vvveb.Components.extend]]></description>
			<content:encoded data-v-post-content="content" data-filter-cdata=""><![CDATA[
<h2>Overview</h2>
<p>Component object is used to define the html blocks that can be added and edited on the page.</p>
<p>To add a component you need to use <code>Vvveb.Components.add</code> method or extend an existing component, like a base component with <code>Vvveb.Components.extend</code></p>
<p>For example image component extends _base component and is defined as</p>
<pre><code class="language-js">Vvveb.Components.extend(&quot;_base&quot;, &quot;html/image&quot;, {
    nodes: [&quot;img&quot;],
    name: &quot;Image&quot;,
    html: '&lt;img src=&quot;../libs/builder/icons/image.svg&quot; height=&quot;128&quot; width=&quot;128&quot;&gt;',
    image: &quot;icons/image.svg&quot;,
    properties: [{
        name: &quot;Image&quot;,
        key: &quot;src&quot;,
        htmlAttr: &quot;src&quot;,
        inputtype: FileUploadInput
    }, {
        name: &quot;Width&quot;,
        key: &quot;width&quot;,
        htmlAttr: &quot;width&quot;,
        inputtype: TextInput
    }, {
        name: &quot;Height&quot;,
        key: &quot;height&quot;,
        htmlAttr: &quot;height&quot;,
        inputtype: TextInput
    }, {
        name: &quot;Alt&quot;,
        key: &quot;alt&quot;,
        htmlAttr: &quot;alt&quot;,
        inputtype: TextInput
    }]
});
</code></pre>
<p>Base component does not extend any existing component and is using  <code>Vvveb.Components.add</code> being defined as</p>
<pre><code class="language-js">Vvveb.Components.add(&quot;_base&quot;, {
    name: &quot;Element&quot;,
	properties: [{
        name: &quot;Id&quot;,
        key: &quot;id&quot;,
        htmlAttr: &quot;id&quot;,
        inputtype: TextInput
    }, {
        name: &quot;Class&quot;,
        key: &quot;class&quot;,
        htmlAttr: &quot;class&quot;,
        inputtype: TextInput
    }]
});
</code></pre>
<p>In this way any component that extends <em>_base</em> component inherits two properties id and class used to edit the corresponding html attributes.</p>
<p><em>Note</em>: _base component is loaded for any html node that has no specific component assigned, for example when clicking on a regular <em>span</em> node.</p>
<h2>Component definition</h2>
<p>Component object can have the following keys</p>
<ul>
<li><strong>name</strong> - Used to display the name on the components list in the left panel.</li>
<li><strong>image</strong> - Image url to display in the components list.</li>
<li><strong>html</strong> - When dropping the component on the page this html is used to insert it in the page.</li>
<li><strong>nodes</strong> - An array with dom node names to allow the editor to detect the Component when clicking on the corresponding node, example <code>nodes: [&quot;h1&quot;, &quot;h2&quot;,&quot;h3&quot;, &quot;h4&quot;,&quot;h5&quot;,&quot;h6&quot;],</code> or <code>nodes: [&quot;img&quot;],</code></li>
<li><strong>classes</strong> - An array with class names to allow the editor to detect the Component when clicking on the corresponding node, example <code>classes: [&quot;btn&quot;, &quot;btn-link&quot;]</code></li>
<li><strong>classesRegex</strong> - An array with regexs used for class names to allow the editor to detect the Component when clicking on the corresponding node, example <code>classesRegex: [&quot;col-&quot;],</code> this regex is used by Grid Component to detect bootstrap columns such as col-md-3 or col-sm-6.</li>
<li><strong>attributes</strong> - An array with attributes  names to allow the editor to detect the Component when clicking on the corresponding node, example <code>attributes: [&quot;data-component-maps&quot;],</code> this attribute is used by maps component.</li>
<li><strong>afterDrop</strong> -  <em>function (node) {}</em>  A function that is called after the element is droped, the node of the new element is passed as parameter, for example map component uses this function to change the html of the node fom a image of a map with the actual map iframe for performance reasons when dragging.</li>
<li><strong>init</strong> - <em>function (node) {}</em> A function that is called after the Component properties are loaded and displayed in the right panel , the node of the new element is passed as parameter, for example video component that supports youtube, vimeo and html5 uses this event to hide vimeo and html5 inputs when a youtube video is detected.</li>
<li><strong>beforeInit</strong> - <em>function (node) {}</em> A function that is called before Component properties are displayed , the node of the new element is passed as parameter, this can be useful to dinamically change component properties for example Select Input or Grid component that needs to load a variable number of inputs based on child nodes uses this event to alter component properties and add the necessary properties for each child node.</li>
<li><strong>onChange</strong> - <em>function (node, property, value)</em> This event is called when a component input in the right panel is changed, the first parameter is the dom <em>node</em> that is edited, second parameter <em>property</em> is the name of the property that has changed (the <em>key</em> attribute of the property), <em>value</em> is the new value of the property.
Usually each input has it's own onChange event but because the component onChange event is triggered for all inputs can be usefull when using a common function to process all input changes.
This event is usefull when changing complex features, for example the map component uses it to compose a new google maps url by adding all component properties such as latitude, longitude and zoom etc.</li>
<li><strong>properties</strong> - A collection of inputs and corespponding configuration for each, to allow the Component to be edited.</li>
</ul>
<p>A complete Component definition with all possible keys</p>
<pre><code class="language-js">Vvveb.Components.extend(&quot;_base&quot;, &quot;html/image&quot;, {
	name: &quot;Image&quot;,
	image: &quot;icons/image.svg&quot;,
	html: '&lt;img src=&quot;../libs/builder/icons/image.svg&quot; height=&quot;128&quot; width=&quot;128&quot;&gt;',
	nodes: [&quot;img&quot;],
	classes: [&quot;btn&quot;, &quot;btn-link&quot;],
	attributes: [&quot;data-component-maps&quot;],
	afterDrop: function (node) {},
	init: function (node) {},
	beforeInit: function (node) {},
	onChange: function (node, property, value) {},
	properties: [{
		name: &quot;Id&quot;,
		key: &quot;id&quot;,
		htmlAttr: &quot;id&quot;,
		inputtype: TextInput
    }
});   
</code></pre>
<h2>Component properties</h2>
<p>Component properties are used to define the inputs that are used to edit the component.</p>
<pre><code class="language-js">properties: [{
        name: &quot;Id&quot;,
        key: &quot;id&quot;,
        htmlAttr: &quot;id&quot;,
        inputtype: TextInput
    }, {
        name: &quot;Class&quot;,
        key: &quot;class&quot;,
        htmlAttr: &quot;class&quot;,
        inputtype: TextInput
    }]
</code></pre>
<p>A property can have the following keys</p>
<ul>
<li><strong>name</strong> - Used as label for the input.</li>
<li><strong>key</strong> - Unique identifier for the input, this is pased on component <em>onChange</em> event second parameter <em>property</em>.</li>
<li><strong>inputtype</strong> - The input type used for the property, any Input object such as TextInput, SelectInput, ToggleInput, ColorInput etc from <em>inputs.js</em> can be used.</li>
<li><strong>htmlAttr</strong> - If htmlAttr is specified then the value is used to edit the coresponding html attribute, the input is filled automatically with the initial value of the attribute and updated automatically when the input is changed, no need to use onChange event, for example <em>htmlAttr: &quot;id&quot;,</em>
<ul>
<li><strong>style</strong> - If htmlAttr is set to <em>style</em> then the property key is used as the css property to be edited, in the example below <em>background-color</em> css property is edited directly using ColorInput that will display a color picker.</li>
<li><strong>class</strong> - If htmlAttr is set to <em>class</em> and the property has <em>validValues</em> key then all classed that are contained in validValues array are removed before adding the new value, this can be usefull when having a select input to choose a unique class from a set.
For example in the example below bootstrap container component uses validValues to allow either <em>container</em> or <em>container-fluid</em> class to be added from a select.</li>
</ul>
</li>
</ul>
<pre><code class="language-js">properties: [
{
	name: &quot;Background Color&quot;,
	key: &quot;background-color&quot;,
	htmlAttr: &quot;style&quot;,
	inputtype: ColorInput,
},
{
	name: &quot;Type&quot;,
	key: &quot;type&quot;,
	htmlAttr: &quot;class&quot;,
	inputtype: SelectInput,
	validValues: [&quot;container&quot;, &quot;container-fluid&quot;],
	data: {
		options: [{
			value: &quot;container&quot;,
			text: &quot;Default&quot;
		}, {
			value: &quot;container-fluid&quot;,
			text: &quot;Fluid&quot;
		}]
	}        name: &quot;Class&quot;,
	key: &quot;class&quot;,
	htmlAttr: &quot;class&quot;,
	inputtype: TextInput
}]
</code></pre>
<ul>
<li><strong>onChange</strong> - <em>function(node, value)</em> This event is triggered when the input is changed (this can depend on Input definition, check inputs.js) you can use this when htmlAttr or style is not enough.</li>
<li><strong>init</strong> - <em>function(node)</em> This function is called when the input is initialized, this can be used for example to load the input with a different value than default.
For example when editing width css property and using a NumberInput you need to use init to remove the px from &quot;100px&quot; to set the value for NumberInput correctly.</li>
</ul>
<pre><code class="language-js">{
	name: &quot;Width&quot;,
	key: &quot;width&quot;,
	htmlAttr: &quot;style&quot;,
	parent:&quot;&quot;,
	inputtype: NumberInput,//can also be replaced with RangeInput
	data: {
		value: &quot;320&quot;,//default
		min: &quot;50&quot;,
		max: &quot;1024&quot;,
		step: &quot;10&quot;
	},
	init: function (node)//use init because number input does not ignore &quot;px&quot;
	{
		return parseInt($(node).css(&quot;width&quot;));//remove px
	}
}
</code></pre>
<ul>
<li><strong>data</strong> - Some Inputs need aditional data, for example ToggleInput needs values for <em>on</em> and <em>off</em> states, or SelectInput needs <em>data:{options: [{}]}</em> for values
For example using toggle to set disabled to true or false for a bootstrap button</li>
</ul>
<pre><code class="language-js">//ToggleInput
{
	name: &quot;Disabled&quot;,
	key: &quot;disabled&quot;,
	htmlAttr: &quot;disabled&quot;,
	inputtype: ToggleInput,
	data: {
		on: &quot;disabled-true&quot;,
		off: &quot;disabled-false&quot;
	}
}
//SelectInput
{
        name: &quot;Size&quot;,
        key: &quot;size&quot;,
        htmlAttr: &quot;class&quot;,
        inputtype: SelectInput,
        validValues: [&quot;btn-lg&quot;, &quot;btn-sm&quot;],
        data: {
            options: [{
                value: &quot;&quot;,
                text: &quot;Default&quot;
            }, {
                value: &quot;btn-lg&quot;,
                text: &quot;Large&quot;
            }, {
                value: &quot;btn-sm&quot;,
                text: &quot;Small&quot;
            }]
        }
    }
</code></pre>
<ul>
<li><strong>child</strong> - Sometimes you need to edit a node that is inside the parent node, for this you can use <em>child</em> property the value must be the css selector for the child element.
For example bootstrap progress bar component has two divs, one for background and one for the progress to use a input to change the progress div instead of the background div, child is set as <em>child:&quot;.progress-bar&quot;</em>.</li>
</ul>
<pre><code class="language-js">{
	name: &quot;Animated&quot;,
	key: &quot;animated&quot;,
	child:&quot;.progress-bar&quot;,
	htmlAttr: &quot;class&quot;,
	validValues: [&quot;&quot;, &quot;progress-bar-animated&quot;],
	inputtype: ToggleInput,
	data: {
		on: &quot;progress-bar-animated&quot;,
		off: &quot;&quot;,
	}
}	
</code></pre>
<ul>
<li><strong>parent</strong> - Same as child but used to edit parent elements, when only parent is set with no selector then the parentNode is used, if a css selector is used then the selector is used to find parent node (this uses jQuery.parents())</li>
</ul>
<h2>Component group</h2>
<p>For a component to be visible in the left panel and to be used for drag and drop it needs to be added to a component group, you can do this by including it in the <code>Vvveb.ComponentsGroup</code> array using the group name as a key.</p>
<p>For example Widgets component group has the following definition.</p>
<pre><code class="language-js">Vvveb.ComponentsGroup['Widgets'] = [&quot;widgets/googlemaps&quot;, &quot;widgets/video&quot;];
</code></pre>
]]></content:encoded>		
		</item><item data-v-post="post" data-v-id="450">
			<title data-v-post-name="name">VvvebJs inputs (legacy)</title>
			<link data-v-post-full-url="name">https://dev.vvveb.com/vvvebjs-inputs</link>
			<dc:creator data-v-post-display_name="name"><![CDATA[Admin]]></dc:creator>
			<pubDate data-v-post-pubDate="date">Mon, 28 Sep 2026 17:24:02 +0000</pubDate>
			<category data-v-post-category-name="category" data-filter-cdata=""><![CDATA[Category]]></category>
			<description data-v-post-excerpt="excerpt" data-filter-cdata=""><![CDATA[Overview 
Inputs objects are used to edit component properties 
For example TextInput extends Input object and is defined as 
var TextInput = $.extend({}, Input, { events: { &quot;keyup&quot;: ['onChange', 'input'], }, setValue: function(value)]]></description>
			<content:encoded data-v-post-content="content" data-filter-cdata=""><![CDATA[
<h2>Overview</h2>
<p>Inputs objects are used to edit component properties</p>
<p>For example TextInput extends Input object and is defined as</p>
<pre><code class="language-js">var TextInput = $.extend({}, Input, {

    events: {
        &quot;keyup&quot;: ['onChange', 'input'],
	 },

	setValue: function(value) {
		$('input', this.element).val(value);
	},
	
	init: function(data) {
		return this.render(&quot;textinput&quot;, data);
	},
  }
);
</code></pre>
<p>Inputs also require a template that is defined as a <script> tag in the editor html (inside editor.html) with the id <code>vvveb-input-inputname</code> for example for text input is <code>vvveb-input-textinput</code> and is defined as</p>
<pre><code class="language-js">&lt;script id=&quot;vvveb-input-textinput&quot; type=&quot;text/html&quot;&gt;
	
	&lt;div&gt;
		&lt;input name=&quot;{%=key%}&quot; type=&quot;text&quot; class=&quot;form-control&quot;/&gt;
	&lt;/div&gt;
	
&lt;/script&gt;
</code></pre>
<p>{%=key%} is used to set the unique input name defined as &quot;key&quot; in component properties, if additional data is provided then these must be set in the template, for example ToggleInput needs <em>on</em> and <em>off</em></p>
<pre><code class="language-js">&lt;script id=&quot;vvveb-input-toggle&quot; type=&quot;text/html&quot;&gt;
	
    &lt;div class=&quot;toggle&quot;&gt;
        &lt;input type=&quot;checkbox&quot; name=&quot;{%=key%}&quot; value=&quot;{%=on%}&quot; data-value-off=&quot;{%=off%}&quot; data-value-on=&quot;{%=on%}&quot; class=&quot;toggle-checkbox&quot; id=&quot;{%=key%}&quot;&gt;
...
</code></pre>
<h2>Definition</h2>
<p>Usually Input objects define the following keys</p>
<ul>
<li><em>events</em> - This provides a collection of definitions for functions to be called on certain events, the format is <em>event_name :['InputObject_function_to_be_called', 'css_selector_for_element_in_template']</em>
For example text input monitors keyup event for input element in the template, while select input monitors onchange event for select element in template, by default they both call onChange function on parent Input object.</li>
</ul>
<pre><code class="language-js">var TextInput = $.extend({}, Input, {

    events: {
        &quot;keyup&quot;: ['onChange', 'input'],
	 },
...


var SelectInput = $.extend({}, Input, {
	

    events: {
        &quot;change&quot;: ['onChange', 'select'],
	 },
...
</code></pre>
<p>Sometimes you need to read another attribute than value from the input such as checked for checkbox in this case you need to override onChange method to use <em>checked</em> attribute.</p>
<pre><code class="language-js">var CheckboxInput = $.extend({}, Input, {

	onChange: function(event, node) {
		
		if (event.data &amp;&amp; event.data.element)
		{
			event.data.element.trigger('propertyChange', [this.checked, this]);
		}
	},
</code></pre>
<ul>
<li><em>setValue</em> - This method is called automatically by Vvveb.Builder when setting the value for the input.</li>
<li><em>init</em> - This method is called when the input is initialized, the return value is used as html to render the input, by default the parent Input.render() is called with the template name and additional data as parameters.</li>
</ul>
<h2>Parent Input</h2>
<p>All input object extends Input object that handles template loading and processing and also provides a default onChange function that automatically processes events using value attribute.</p>
<pre><code class="language-js">var Input = {
	
	init: function(name) {
	},


	onChange: function(event, node) {
		
		if (event.data &amp;&amp; event.data.element)
		{
			event.data.element.trigger('propertyChange', [this.value, this]);
		}
	},

	renderTemplate: function(name, data) {
		return tmpl(&quot;vvveb-input-&quot; + name, data);
	},

	render: function(name, data) {
		this.element = $(this.renderTemplate(name, data));
		
		//bind events
		if (this.events)
		for (var event in this.events)
		{
			fun = this[ this.events[event][0] ];
			el = this.events[event][1];
		
			this.element.on(event, el, {element: this.element}, fun);
		}
		
		return this.element;
	}
};
</code></pre>
<p>You can check editor.html for input templates and inputs.js for inputs code.</p>
]]></content:encoded>		
		</item><item data-v-post="post" data-v-id="451">
			<title data-v-post-name="name">VvvebJs overview (legacy)</title>
			<link data-v-post-full-url="name">https://dev.vvveb.com/vvvebjs-home</link>
			<dc:creator data-v-post-display_name="name"><![CDATA[Admin]]></dc:creator>
			<pubDate data-v-post-pubDate="date">Mon, 28 Sep 2026 17:24:02 +0000</pubDate>
			<category data-v-post-category-name="category" data-filter-cdata=""><![CDATA[Category]]></category>
			<description data-v-post-excerpt="excerpt" data-filter-cdata=""><![CDATA[Overview 
The editor is build using Bootstrap 4 and jQuery. 
The editor html is located in editor.html and to load the demo editor the following scripts are included. 
&lt;!-- jquery--&gt; &lt;script src=&quot;js/jquery.min.js&quot;&gt;&lt;/script&gt;]]></description>
			<content:encoded data-v-post-content="content" data-filter-cdata=""><![CDATA[<h2>Overview</h2>
<p>The editor is build using Bootstrap 4 and jQuery.</p>
<p>The editor html is located in editor.html and to load the demo editor the following scripts are included.</p>
<pre><code class="language-html">&lt;!-- jquery--&gt;
&lt;script src=&quot;js/jquery.min.js&quot;&gt;&lt;/script&gt;
&lt;!-- Enable shortcut support such as ctrl+z for undo and ctrl+e for export etc--&gt;
&lt;script src=&quot;js/jquery.hotkeys.js&quot;&gt;&lt;/script&gt;


&lt;!-- bootstrap--&gt;
&lt;script src=&quot;js/popper.min.js&quot;&gt;&lt;/script&gt;
&lt;script src=&quot;js/bootstrap.min.js&quot;&gt;&lt;/script&gt;

&lt;!-- builder code--&gt;
&lt;!-- This is the main editor code --&gt;
&lt;script src=&quot;libs/builder/builder.js&quot;&gt;&lt;/script&gt;

&lt;!-- undo manager--&gt;
&lt;script src=&quot;libs/builder/undo.js&quot;&gt;&lt;/script&gt;

&lt;!-- inputs--&gt;
&lt;!-- The inputs library, here is the code for inputs such as text, select etc used for component properties --&gt;
&lt;script src=&quot;libs/builder/inputs.js&quot;&gt;&lt;/script&gt;

&lt;!-- components--&gt;
&lt;!-- Components for Bootstrap 4 group --&gt;
&lt;script src=&quot;libs/builder/components-bootstrap4.js&quot;&gt;&lt;/script&gt;
&lt;!-- Components for Widgets group --&gt;
&lt;script src=&quot;libs/builder/components-widgets.js&quot;&gt;&lt;/script&gt;


&lt;!-- plugins --&gt;

&lt;!-- code mirror libraries - code editor syntax highlighting for html editor --&gt;
&lt;link href=&quot;libs/codemirror/lib/codemirror.css&quot; rel=&quot;stylesheet&quot;/&gt;
&lt;link href=&quot;libs/codemirror/theme/material.css&quot; rel=&quot;stylesheet&quot;/&gt;
&lt;script src=&quot;libs/codemirror/lib/codemirror.js&quot;&gt;&lt;/script&gt;
&lt;script src=&quot;libs/codemirror/lib/xml.js&quot;&gt;&lt;/script&gt;
&lt;script src=&quot;libs/codemirror/lib/formatting.js&quot;&gt;&lt;/script&gt;

&lt;!-- code mirror vvveb plugin --&gt;
&lt;!-- replaces default textarea as html code editor with codemirror--&gt;
&lt;script src=&quot;libs/builder/plugin-codemirror.js&quot;&gt;&lt;/script&gt;	

</code></pre>
<p>To initialize the editor Vvveb.Builder.init is called.</p>
<p>The first parameter is the url to load for editing, this must be on the same subdomain to allow editing.</p>
<p>The second parameter is a function to call when the page is finished loading, by default the editor Gui.init() is called.</p>
<pre><code class="language-javascript">$(document).ready(function() 
{
	Vvveb.Builder.init('demo/index.html', function() {
		//load code after page is loaded here
		Vvveb.Gui.init();
	});
});
&lt;/script&gt;
</code></pre>
<h2>Structure</h2>
<p>[[img/structure.png]]</p>
<p><code>Component Group</code> is a collection of <a href="Components">Components</a>, for example Bootstrap 4 group is composed of Components such as <code>Button</code> and <code>Grid</code>, this object is used only for grouping components in the editor left panel.</p>
<p>For example Widgets component group has only two components video and maps and is defined as</p>
<pre><code class="language-javascript">Vvveb.ComponentsGroup['Widgets'] = [&quot;widgets/googlemaps&quot;, &quot;widgets/video&quot;];
</code></pre>
<p>A <a href="Components">Component</a> is an object that provides html that can be dropped on the canvas and also properties that can be edited when the component is selected, for example Video Component.</p>
<p>The html link <a href="Components">Component</a> that has Url and Target properties is defined as</p>
<pre><code class="language-javascript">Vvveb.Components.extend(&quot;_base&quot;, &quot;html/link&quot;, {
    nodes: [&quot;a&quot;],
    name: &quot;Link&quot;,
    properties: [{
        name: &quot;Url&quot;,
        key: &quot;href&quot;,
        htmlAttr: &quot;href&quot;,
        inputtype: LinkInput
    }, {
        name: &quot;Target&quot;,
        key: &quot;target&quot;,
        htmlAttr: &quot;target&quot;,
        inputtype: TextInput
    }]
});
</code></pre>
<p>An <a href="Inputs">Input</a> object is used in <a href="Components">Component</a> properties collection for editing the property, for example text input, select, color, grid row etc
For example TextInput extends Input object and is defined as</p>
<pre><code class="language-js">var TextInput = $.extend({}, Input, {

    events: {
        &quot;keyup&quot;: ['onChange', 'input'],
	 },

	setValue: function(value) {
		$('input', this.element).val(value);
	},
	
	init: function(data) {
		return this.render(&quot;textinput&quot;, data);
	},
  }
);
</code></pre>
<p>Inputs also require a template that is defined as a <script> tag in the editor html (inside editor.html) with the id <code>vvveb-input-inputname</code> for example for text input is <code>vvveb-input-textinput</code> and is defined as</p>
<pre><code class="language-js">&lt;script id=&quot;vvveb-input-textinput&quot; type=&quot;text/html&quot;&gt;
	
	&lt;div&gt;
		&lt;input name=&quot;{%=key%}&quot; type=&quot;text&quot; class=&quot;form-control&quot;/&gt;
	&lt;/div&gt;
	
&lt;/script&gt;
</code></pre>
]]></content:encoded>		
		</item><item data-v-post="post" data-v-id="452">
			<title data-v-post-name="name">VTpl A CSS‑Selector‑Driven PHP Template Engine</title>
			<link data-v-post-full-url="name">https://dev.vvveb.com/vtpl-a-css-selector-driven-php-template-engine</link>
			<dc:creator data-v-post-display_name="name"><![CDATA[Admin]]></dc:creator>
			<pubDate data-v-post-pubDate="date">Mon, 28 Sep 2026 17:24:02 +0000</pubDate>
			<category data-v-post-category-name="category" data-filter-cdata=""><![CDATA[Category]]></category>
			<description data-v-post-excerpt="excerpt" data-filter-cdata=""><![CDATA[Vtpl is a PHP template engine designed to keep HTML clean, readable, and free of embedded logic. Instead of mixing PHP code directly into HTML, Vtpl uses CSS selectors to define where dynamic content should be inserted. This separation of concerns makes]]></description>
			<content:encoded data-v-post-content="content" data-filter-cdata=""><![CDATA[
<p>Vtpl is a PHP template engine designed to keep HTML clean, readable, and free of embedded logic.
Instead of mixing PHP code directly into HTML, Vtpl uses <strong>CSS selectors</strong> to define where dynamic content should be inserted.
This separation of concerns makes templates easier to maintain, theme, and redesign, especially in CMS environments where HTML changes frequently.</p>
<p>With Vtpl, templates are simply lists of rules that map:</p>
<pre><code>CSS selector  →  PHP variable or PHP code
</code></pre>
<p>Because the HTML remains untouched, designers can modify layouts freely while developers maintain logic independently. This approach enables systems like <strong>Vvveb CMS</strong>, where any HTML file can be replaced or redesigned without breaking dynamic content rendering.</p>
<hr />
<h2>How Vtpl Works</h2>
<p>A Vtpl file contains a series of <code>selector = value</code> rules.<br />
The <strong>selector</strong> identifies elements in the HTML, and the <strong>value</strong> is the content or PHP code to insert.</p>
<h3>Supported value types</h3>
<ul>
<li>
<p><strong>Static strings</strong></p>
<pre><code class="language-css">div#id &gt; span.title = &quot;Lorem ipsum&quot;
</code></pre>
</li>
<li>
<p><strong>PHP variables</strong></p>
<pre><code class="language-css">div#id &gt; span.title = $title
</code></pre>
</li>
<li>
<p><strong>Inline PHP logic</strong></p>
<pre><code class="language-css">div#id &gt; span.title = &lt;?php if ($title) echo $title; ?&gt;
</code></pre>
</li>
</ul>
<h3>Recommended selector style</h3>
<p>Use stable selectors such as <code>data-*</code> attributes:</p>
<pre><code class="language-css">[data-product-name] = $product['name']
</code></pre>
<p>This avoids conflicts with classes or IDs used for styling.</p>
<hr />
<h1>Including HTML from Other Templates</h1>
<p>Vtpl supports reusing HTML fragments using the <code>from</code> directive. This is useful for shared elements like headers, footers, or repeated UI blocks.</p>
<pre><code class="language-css">div#top &gt; div.header = from(homepage.html|div#top &gt; div.header)
</code></pre>
<p>If no selector is provided, Vtpl assumes the same selector (eg div#top &gt; div.header):</p>
<pre><code class="language-css">div#top &gt; div.header = from(homepage.html)
</code></pre>
<p>This ensures consistent markup across templates without duplicating HTML.</p>
<hr />
<h1>Modifiers</h1>
<p>Modifiers change how Vtpl inserts content into the selected elements.</p>
<h3>innerHTML (default)</h3>
<p>Replaces the inside of the element.</p>
<pre><code class="language-css">div#id &gt; span.title = &quot;Lorem ipsum&quot;
div#id &gt; span.title|innerHTML = &quot;Lorem ipsum&quot;
</code></pre>
<h3>outerHTML</h3>
<p>To replace the entire elements with the specified code use outerHTML modifier</p>
<pre><code class="language-css">div#id &gt; span.title|outerHTML = &quot;Lorem ipsum&quot;
</code></pre>
<h3>innerText</h3>
<p>To replace only the first text inside element but leave other elements intact</p>
<pre><code class="language-css">div#id &gt; span.title|innerText = &quot;Lorem ipsum&quot;
</code></pre>
<h3>before</h3>
<p>Inserts content before the element.</p>
<pre><code class="language-css">div#id &gt; span.class a|before = &quot;lorem ipsum&quot;
</code></pre>
<h3>prepend</h3>
<p>Inserts content at the beginning of the element.</p>
<pre><code class="language-css">div#id &gt; span.class a|prepend = &quot;lorem ipsum&quot;
</code></pre>
<h3>after</h3>
<p>Inserts content after the element.</p>
<pre><code class="language-css">div#id &gt; span.class a|after = &quot;lorem ipsum&quot;
</code></pre>
<h3>append</h3>
<p>Inserts content at the end of the element.</p>
<pre><code class="language-css">div#id &gt; span.class a|append = &quot;lorem ipsum&quot;
</code></pre>
<h3>deleteAllButFirst</h3>
<p>Keeps only the first matched element, useful when designers include multiple mock elements but we only need one to iterate.</p>
<pre><code class="language-css">div.items a|deleteAllButFirst
</code></pre>
<p>Example:</p>
<pre><code class="language-html">&lt;!-- Original --&gt;
&lt;div class=&quot;items&quot;&gt;
  &lt;a href=&quot;#&quot;&gt;1&lt;/a&gt;
  &lt;a href=&quot;#&quot;&gt;2&lt;/a&gt;
  &lt;a href=&quot;#&quot;&gt;3&lt;/a&gt;
&lt;/div&gt;

&lt;!-- After deleteAllButFirst --&gt;
&lt;div class=&quot;items&quot;&gt;
  &lt;a href=&quot;#&quot;&gt;1&lt;/a&gt;
&lt;/div&lt;
</code></pre>
<h3>hide (conditional rendering)</h3>
<p>Removes elements if a variable is empty or false.</p>
<pre><code class="language-css">div.articles|if_exists = $articles
</code></pre>
<h3>delete</h3>
<p>Removes the selected elements entirely.</p>
<pre><code class="language-css">div.ad-banner|delete
</code></pre>
<h3>Attribute insert</h3>
<p>To modify an attribute, specify it as a modifier:</p>
<pre><code class="language-css">a.button|href = $url
a.button|title = &quot;Go to page&quot;
</code></pre>
<hr />
<h1>Additional Commands</h1>
<h3>import</h3>
<p>Includes another Vtpl file. Useful for splitting large templates into smaller, maintainable parts.</p>
<pre><code class="language-css">import(profile/activity_tab.tpl)
</code></pre>
<hr />
<h1>Comments</h1>
<p>Vtpl supports both single‑line and multi‑line comments:</p>
<pre><code class="language-css">// Single line comment

/* 
Multi-line
comment
*/
</code></pre>
<hr />
<h1>Debugging</h1>
<p>Enable debugging inside a Vtpl file:</p>
<pre><code class="language-css">debug = true
</code></pre>
<p>Disable it:</p>
<pre><code class="language-css">debug = false
</code></pre>
<p>When debugging is enabled, Vtpl outputs a list of all template inserts, helping you understand exactly how the HTML was transformed.</p>
<hr />
<p>Vtpl’s CSS‑selector‑based approach keeps HTML clean, separates logic from presentation, and makes large CMS systems easier to theme and maintain.</p>
]]></content:encoded>		
		</item><item data-v-post="post" data-v-id="453">
			<title data-v-post-name="name">Vtpl template language</title>
			<link data-v-post-full-url="name">https://dev.vvveb.com/template-vtpl</link>
			<dc:creator data-v-post-display_name="name"><![CDATA[Admin]]></dc:creator>
			<pubDate data-v-post-pubDate="date">Mon, 28 Sep 2026 17:24:02 +0000</pubDate>
			<category data-v-post-category-name="category" data-filter-cdata=""><![CDATA[Category]]></category>
			<description data-v-post-excerpt="excerpt" data-filter-cdata=""><![CDATA[Unlike most template engines that work by editing html code and inserting pieces of code or variables Vtpl uses css selectors to specify where to insert php code or variables into the html code with the goal to keep the html clean and untouched so you]]></description>
			<content:encoded data-v-post-content="content" data-filter-cdata=""><![CDATA[
<p>Unlike most template engines that work by editing html code and inserting pieces of code or variables Vtpl uses css selectors to specify where to insert php code or variables into the html code with the goal to keep the html clean and untouched so you don't end up with html interwinded with php or other template language logic, keeping things clean and separate increases maintainability.</p>
<p>Vtpl templates are just lists of php code and variable names to insert into html, to specify the elements where to insert the code css selectors are used.</p>
<p>With Vtpl when the frontend template design of your app is changed you don’t have to change anything, the logic for the html file will be automatically applied for these new html files.</p>
<p>Vtpl ensures proper separations of concerns, the frontend logic is separated from presentation.</p>
<p>Vtpl makes it possible to build CMS's like <a href="https://www.vvveb.com">Vvveb</a> where any html page of the CMS can be changed to the last element without affecting the rendering of dynamic content from the database.</p>
<h2>Documentation</h2>
<p>Vtpl is just a list of <code>key = value</code> pairs.</p>
<p>On the left you need to specify the CSS selector and on the right the code that must be inserted for the selector.</p>
<p>The code to be inserted can be one of the following</p>
<h5>Simple strings</h5>
<pre><code>div#id &gt; span.class a = &quot;lorem ipsum&quot;
</code></pre>
<h5>Php variables</h5>
<pre><code class="language-css">div#id &gt; span.class a = $variable

</code></pre>
<h5>Php code for complex logic</h5>
<pre><code class="language-css">div#id &gt; span.class a = &lt;?php if (isset($var)) echo htmlentities($var);?&gt;
</code></pre>
<blockquote>
<p><strong>Note</strong>
It's a good practice to adopt a standard for the css selectors from the beginning, for example use data attributes like <code>[data-product-name]</code> instead of generic id's or css classes to keep things clean.</p>
</blockquote>
<h5>Include external html sections for better reuse by using <code>from</code></h5>
<pre><code class="language-css">/*
from, a special command that copies html from other templates,
useful to include up to date html code into all templates from the currently maintained template for the specified section 
a common use is to apply the header from homepage to all other other html templates
*/

div#top &gt; div.header = from(homepage.html|div#top &gt; div.header)

/*
Or you can skip the selector part, in this case the same selector (eg div#top &gt; div.header) is assumed.
*/
div#top &gt; div.header = from(homepage.html)

</code></pre>
<h2>List of modifiers</h2>
<h5>innerHTML</h5>
<pre><code class="language-css">/*
by default code is inserted into the specified elements without replacing the current elements (innerHTML) 
to replace the entire elements with the specified code use outerHTML modifier
*/

div#id &gt; span.class a|innerHTML = &quot;lorem ipsum&quot;
</code></pre>
<h5>Before</h5>
<pre><code class="language-css">/*
Inserts the code before the element(s)
*/
div#id &gt; span.class a|before = &quot;lorem ipsum&quot;

</code></pre>
<h5>Prepend</h5>
<pre><code class="language-css">/*
Inserts the code at the beginning inside the element(s)
*/
div#id &gt; span.class a|prepend = &quot;lorem ipsum&quot;

</code></pre>
<h5>After</h5>
<pre><code class="language-css">/*
Inserts the code after the element(s)
*/
div#id &gt; span.class a|after = &quot;lorem ipsum&quot;

</code></pre>
<h5>Append</h5>
<pre><code class="language-css">/*
Inserts the code at the end in the element(s)
*/
div#id &gt; span.class a|append = &quot;lorem ipsum&quot;
</code></pre>
<h5>deleteAllButFirst</h5>
<pre><code class="language-css">/* Deletes all elements for the specified selector except for the first elements,  
usually in mockups front end developers add multiple elements to better show the final page look,  
the programmer just needs one element to iterate and fill data*/
 
div#id &gt; span.class a|deleteAllButFirst

/*

&lt;!-- this --&gt;
&lt;div id=&quot;id&quot;&gt;
&lt;span class=&quot;class&quot;&gt;
&lt;a&gt;link 1&lt;/a&gt;
&lt;a&gt;link 2&lt;/a&gt;
&lt;a&gt;link 3&lt;/a&gt;
&lt;/span&gt;
&lt;/div

**will result into this**

&lt;div id=&quot;id&quot;&gt;
&lt;span class=&quot;class&quot;&gt;
&lt;a&gt;link 1&lt;/a&gt;
&lt;/span&gt;
&lt;/div&gt;
*/
</code></pre>
<h5>deleteAllButFirstChild</h5>
<p>The same as <code>deleteAllButFirst</code> but will keep the first element (child element) for each set, all parent elements will keep only one child element in contrast with <code>deleteAllButFirst</code> that works globally.</p>
<p>This is used for loops when you only need one item to iterate</p>
<h4>hide</h4>
<pre><code class="language-css">/*removes the specified elements if the variable ($articles) is false*/
div.articles|if_exists = $articles
</code></pre>
<h4>delete</h4>
<pre><code class="language-css">/* removes the specified elements*/
div.articles|delete
</code></pre>
<h4>attributes</h4>
<pre><code class="language-css">/*
to inject code into a tag's attribute you must specify the attribute as modifier
*/
div#id &gt; span.class a|href = &quot;www.thewebsite.com&quot;

div#id &gt; span.class a|title = &quot;The website&quot;
</code></pre>
<h4>Class and attribute helpers</h4>
<pre><code class="language-css">/* add / remove css classes */
div.alert|addClass = &quot;show&quot;
div.alert|removeClass = &quot;hidden&quot;

/* add a new attribute (fails if it already exists) / remove one */
input|addNewAttribute = &quot;required&quot;
a|removeAttribute = &quot;title&quot;

/* set element text (alias: innerText) */
span.name|text = $user['name']
</code></pre>
<p>You can also use variables directly in the attributes</p>
<pre><code class="language-html"> 
&lt;img alt=&quot;$image.alt&quot; src=&quot;&quot;&gt;
 
&lt;a href=&quot;$this.product.url&quot;&gt;link&lt;/a&gt;
 
&lt;span title=&quot;$date_modified&quot;&gt;&lt;/span&gt;
 
</code></pre>
<h2>Additional commands</h2>
<h2>Import</h2>
<p>Includes additional files, usefull to separate logic when things get bigger and harder to maintain in one file</p>
<pre><code class="language-css">import(profile/activity_tab.tpl)
</code></pre>
<h3>Optional parameters</h3>
<pre><code class="language-css">import(crud.tpl, {&quot;type&quot;:&quot;return_status&quot;})
</code></pre>
<p>then in crud.tpl you can use the parameters as follows</p>
<pre><code class="language-css">[data-v-{{type}}] [data-v-name] = $this-&gt;{{type}}['name'] //{{type}} will be replaced with return_status
[data-v-{{type}}] [data-v-*] = $this-&gt;{{type}}[''@@__data-v-(*)__@@'']//catch all
</code></pre>
<h2>Comments</h2>
<p>Vtpl can have comments</p>
<pre><code> //single line  
 /* Or multiple line  
 comments  
 */  
</code></pre>
<h2>Debugging</h2>
<pre><code class="language-css">/*
just on some portions with the following directive directly into the Vtpl files
/*
you can turn on debug with
*/
debug = true
/*
or turn it off with
*/
debug = false
</code></pre>
<p>When in debug mode, you will have a list of all template injections made on the html to visualize.</p>
]]></content:encoded>		
		</item><item data-v-post="post" data-v-id="454">
			<title data-v-post-name="name">Security</title>
			<link data-v-post-full-url="name">https://dev.vvveb.com/security</link>
			<dc:creator data-v-post-display_name="name"><![CDATA[Admin]]></dc:creator>
			<pubDate data-v-post-pubDate="date">Mon, 28 Sep 2026 17:24:02 +0000</pubDate>
			<category data-v-post-category-name="category" data-filter-cdata=""><![CDATA[Category]]></category>
			<description data-v-post-excerpt="excerpt" data-filter-cdata=""><![CDATA[Vvveb implements multiple layers of security protection. 
CSRF Protection 
Admin POST requests require a valid CSRF token (Admin\Base::init() calls checkCsrf() automatically; a mismatch returns 403 &quot;Invalid csrf!&quot;). 
How it works:  
A random]]></description>
			<content:encoded data-v-post-content="content" data-filter-cdata=""><![CDATA[
<p>Vvveb implements multiple layers of security protection.</p>
<h2>CSRF Protection</h2>
<p>Admin POST requests require a valid CSRF token (<code>Admin\Base::init()</code> calls <code>checkCsrf()</code> automatically; a mismatch returns 403 &quot;Invalid csrf!&quot;).</p>
<p><strong>How it works</strong>:</p>
<ol>
<li>A random token is generated per session and stored in the session (<code>csrf</code> key)</li>
<li>The token is embedded in forms as a hidden <code>csrf</code> field (admin templates bind it automatically via <code>data-v-csrf</code>)</li>
<li>On POST, the token is validated against the session</li>
</ol>
<p><strong>In templates</strong>:</p>
<pre><code class="language-html">&lt;!-- admin templates: value is filled automatically from the session --&gt;
&lt;input type=&quot;hidden&quot; name=&quot;csrf&quot; data-v-csrf&gt;
</code></pre>
<p><strong>In controllers</strong> (admin):</p>
<pre><code class="language-php">// CSRF is automatically validated in Admin\Base::init() through checkCsrf()
// for POST requests; a mismatch returns 403 (&quot;Invalid csrf!&quot;)
// Manual validation when needed (same semantics):
protected function save() {
    if (! $this-&gt;checkCsrf()) {
        return null;
    }
    // ...
}
</code></pre>
<p>Note: <code>checkCsrf()</code> only enforces POST requests; front-end (<code>app/</code>) controllers generate the session token but validate it per-action  -  include the hidden <code>csrf</code> field in theme forms that post to custom handlers.</p>
<h2>Authentication</h2>
<h3>Admin Authentication</h3>
<pre><code class="language-php">// Check if admin is logged in
$admin = \Vvveb\System\User\Admin::current();

// Login
$admin = \Vvveb\System\User\Admin::login($data, $additionalInfo, $feedback);

// Logout
\Vvveb\System\User\Admin::logout();

// Require authentication (in controller)
if (!\Vvveb\System\User\Admin::current()) {
    $this-&gt;response-&gt;redirect('/admin/?module=/admin/auth/login');
}
</code></pre>
<h3>Front-End Authentication</h3>
<pre><code class="language-php">// Check if user is logged in
$user = \Vvveb\System\User\User::current();

// Login
$user = \Vvveb\System\User\User::login($data, $additionalInfo, $feedback);

// Logout
\Vvveb\System\User\User::logout();
</code></pre>
<h3>Token-Based Authentication</h3>
<p>For API authentication:</p>
<pre><code class="language-php">// Generate token
$token = \Vvveb\System\User\Admin::auth($admin_auth_token);

// Validate token
$admin = \Vvveb\System\User\Admin::get(['token' =&gt; $token]);
</code></pre>
<h2>Password Security</h2>
<ul>
<li>Passwords are hashed using <code>password_hash()</code> with bcrypt (cost 11)</li>
<li>Auto-rehashing when PHP version changes the default cost</li>
<li>Password complexity requirements enforced via validation rules</li>
</ul>
<pre><code class="language-php">// Hash a password
$hash = \Vvveb\System\User\Auth::password('my-password');

// Verify
$valid = \Vvveb\System\User\Auth::checkPassword('my-password', $hash);
</code></pre>
<h2>Permission System</h2>
<p>Regex-based allow/deny rules control access to controllers and actions.</p>
<h3>Checking Permissions</h3>
<p>Admin controllers are checked automatically: <code>Base::init()</code> calls <code>$this-&gt;permission()</code>, which builds the permission string from the current module/action (e.g. <code>product/products/save</code>) and returns a 403 error page when the role denies access.</p>
<p>You can call it manually with an explicit permission string; the method does not return  -  it terminates with a 403 response on failure:</p>
<pre><code class="language-php">// In an admin controller action or init()
$this-&gt;permission('product/products');

// Check a capability without triggering the 403 response
if (\Vvveb\System\User\Admin::hasPermission('product/products')) {
    // Allow
}

// Check a named capability for an app (&quot;admin&quot;, &quot;app&quot;, ...)
if (\Vvveb\System\User\Admin::hasCapability('edit_products', 'admin')) {
    // Allow
}
</code></pre>
<h3>Role Management</h3>
<pre><code class="language-php">// Check if role has permission
$hasPermission = \Vvveb\System\User\Role::has(
    'product/products',
    $allowRules,
    $denyRules,
    $roleId
);
</code></pre>
<h3>Permission Rules</h3>
<p>Rules use wildcard patterns:</p>
<pre><code>product/*           # Allow all product controllers
product/products    # Allow specific controller
*/save              # Allow save action on all controllers
</code></pre>
<h2>Input Filtering</h2>
<p>All superglobals are recursively filtered:</p>
<pre><code class="language-php">// Request class filters automatically:
// - $_GET keys limited to 255 chars
// - $_POST values filtered through filterText()
// - $_REQUEST merged from filtered GET + POST
// - $_COOKIE filtered
// - $_FILES filtered

// Manual filtering
$clean = \Vvveb\filter('/^[a-z0-9-]+$/', $input);// first argument is a regex, returns the match or false
$cleanText = \Vvveb\filterText($input);// strips tags and encodes special chars
</code></pre>
<h2>SQL Injection Prevention</h2>
<p>SqlP uses parameterized queries:</p>
<pre><code class="language-sql">-- In .sql model files
CREATE PROCEDURE getProduct(IN product_id INT)
BEGIN
    SELECT * FROM product WHERE product_id = :product_id;
END
</code></pre>
<p>The generated PHP code binds parameters, preventing injection.</p>
<h2>XSS Prevention</h2>
<ul>
<li>Theme output is rendered through Vtpl <code>.tpl</code> rules (e.g. <code>[data-v-product-*]|innerText = ...</code>); escape user content server-side before it reaches the view.</li>
<li><code>\Vvveb\filterText($input)</code> strips tags and encodes special chars (<code>FILTER_SANITIZE_FULL_SPECIAL_CHARS</code>); request GET/cookie data already passes through it.</li>
<li>Admin templates use <code>htmlspecialchars()</code> for user content.</li>
</ul>
<h2>File Upload Security</h2>
<pre><code class="language-php">// Validate uploaded files against a rule file (constructor takes rule-file names)
$validator = new \Vvveb\System\Validator(['upload']);
$errors = $validator-&gt;validate($this-&gt;request-&gt;files);

// Check file type
$allowedTypes = ['image/jpeg', 'image/png', 'image/gif'];
if (!in_array($file['type'], $allowedTypes)) {
    throw new \Exception('Invalid file type');
}
</code></pre>
<h2>Brute Force Protection</h2>
<p>Login throttling is built into <code>Vvveb\Controller\User\LoginTrait</code> (front-end and admin logins share it): failed attempts are logged per account/IP in hourly buckets (<code>$failedTimeInterval = 'Y-m-d H:00:00'</code>), and more than <code>$failedCount = 10</code> failures blocks further attempts with &quot;Too many login attempts, try again in one hour!&quot;. Override the two properties in a subclass to tune the window.</p>
<h2>Security Headers</h2>
<p>Admin controllers set security headers:</p>
<pre><code class="language-php">$this-&gt;response-&gt;addHeader('X-Frame-Options: SAMEORIGIN');
$this-&gt;response-&gt;addHeader('X-Content-Type-Options: nosniff');
</code></pre>
<h2>Production Checklist</h2>
<ol>
<li>Set <code>DEBUG = false</code> in <code>env.php</code></li>
<li>Set <code>SQL_CHECK = false</code> in <code>env.php</code></li>
<li>Enable HTTPS (redirect HTTP to HTTPS)</li>
<li>Set secure cookie flags</li>
<li>Enable OPcache with <code>validate_timestamps=0</code></li>
<li>Restrict file permissions (<code>chmod 644</code> for files, <code>755</code> for directories)</li>
<li>Remove installer files after installation</li>
<li>Use a non-default admin URL</li>
<li>Keep PHP and dependencies updated</li>
<li>Regular backups via Tools &gt; Backup</li>
</ol>
]]></content:encoded>		
		</item><item data-v-post="post" data-v-id="455">
			<title data-v-post-name="name">Plugin Development</title>
			<link data-v-post-full-url="name">https://dev.vvveb.com/plugin-development</link>
			<dc:creator data-v-post-display_name="name"><![CDATA[Admin]]></dc:creator>
			<pubDate data-v-post-pubDate="date">Mon, 28 Sep 2026 17:24:02 +0000</pubDate>
			<category data-v-post-category-name="category" data-filter-cdata=""><![CDATA[Category]]></category>
			<description data-v-post-excerpt="excerpt" data-filter-cdata=""><![CDATA[Plugin Structure 
A plugin lives in plugins/{plugin-name}/ and has this structure: 
plugins/my-plugin/ ├── plugin.php # Entry point (required) ├── install.php # Optional: setup code run on first activation ├── admin/ # Admin back-end]]></description>
			<content:encoded data-v-post-content="content" data-filter-cdata=""><![CDATA[
<h2>Plugin Structure</h2>
<p>A plugin lives in <code>plugins/{plugin-name}/</code> and has this structure:</p>
<pre><code>plugins/my-plugin/
├── plugin.php              # Entry point (required)
├── install.php             # Optional: setup code run on first activation
├── admin/                  # Admin back-end context
│   ├── controller/         # Admin controllers
│   ├── template/           # Admin .tpl templates
│   └── validate/           # Validation rules
├── app/                    # Front-end context
│   ├── controller/         # Front-end controllers
│   ├── template/           # Front-end .tpl templates
│   └── validate/           # Validation rules
├── component/
│   └── my-widget.php       # Custom data component
├── sql/                    # SQL query models, one folder per engine
│   ├── mysqli/
│   ├── pgsql/
│   └── sqlite/
├── install/sql/            # Schema run on activation, one folder per engine
│   ├── mysqli/schema/
│   ├── pgsql/schema/
│   └── sqlite/schema/
├── public/                 # Assets copied/symlinked to public/plugins/{slug}/ on activation
└── vendor/                 # Plugin-specific dependencies (optional)
</code></pre>
<p>The folder (<code>admin/</code> vs <code>app/</code>) selects the context in which a file is used; both use the same PHP namespace.</p>
<p>Reference plugins: <a href="https://github.com/givanz/Vvveb/tree/master/plugins/insert-scripts"><code>plugins/insert-scripts</code></a> (minimal), <a href="https://github.com/givanz/Vvveb/tree/master/plugins/contact-form"><code>plugins/contact-form</code></a> (component + database tables).</p>
<h2>plugin.php Header</h2>
<p>The entry point declares metadata in a block comment:</p>
<pre><code class="language-php">&lt;?php

/*
Name: My Plugin
Slug: my-plugin
Category: utility
Url: https://example.com/my-plugin
Description: A brief description of what the plugin does.
Thumb: screenshot.svg
Author: Your Name
Version: 1.0
Author url: https://example.com
Settings: /admin/index.php?module=plugins/my-plugin/settings
*/

use Vvveb\System\Event;

if (! defined('V_VERSION')) {
	die('Invalid request!');
}

class MyPluginPlugin {
	public function admin() {
		// admin-only initialization (runs only in the admin app)
	}

	public function app() {
		// front-end initialization (runs only in the public app)
	}

	public function __construct() {
		if (APP == 'app') {
			$this-&gt;app();
		} else if (APP == 'admin') {
			$this-&gt;admin();
		}
	}
}

new MyPluginPlugin();
</code></pre>
<p>Register events inside <code>admin()</code> or <code>app()</code> so they only run in the relevant context; register context-independent events directly in the constructor.</p>
<h2>Adding Routes</h2>
<p>Plugins load before routes are matched, so you can register routes directly from the plugin's <code>app()</code> method:</p>
<pre><code class="language-php">use Vvveb\System\Routes;

public function app(): void {
	Routes::addRoute('/my-page', [
		'module' =&gt; 'plugins/my-plugin/index/index',
	]);

	// routes with an &quot;edit&quot; key can be opened in the page builder
	Routes::addRoute('/my-page/{slug}', [
		'module' =&gt; 'plugins/my-plugin/index/index',
		'edit'   =&gt; '/admin/?module=plugins/my-plugin/index',
	]);
}
</code></pre>
<p>To add or remove routes defined elsewhere, hook into the <code>Routes::init</code> event instead:</p>
<pre><code class="language-php">public function app(): void {
	Event::on('Vvveb\System\Routes', 'init', __CLASS__, function ($routes) {
		unset($routes['/cart']); // remove an existing route

		//listeners must return their parameters wrapped in an array
		return [$routes];
	});
}
</code></pre>
<h2>Adding Admin Menu Items</h2>
<p>Register admin menu items via the <code>Base::init-menu</code> event:</p>
<pre><code class="language-php">public function admin(): void {
	$admin_path = \Vvveb\adminPath();

	Event::on('Vvveb\Controller\Base', 'init-menu', __CLASS__, function ($menu) use ($admin_path) {
		//entries are keyed by slug; child pages go into &quot;items&quot;
		$menu['plugins']['items']['my-plugin'] = [
			'name'     =&gt; __('My Plugin'),
			'url'      =&gt; $admin_path . 'index.php?module=plugins/my-plugin/settings',
			'icon'     =&gt; 'iconoir-settings',
			//'icon-img' =&gt; PUBLIC_PATH . 'plugins/my-plugin/my-plugin.svg',
			'module'   =&gt; 'plugins/my-plugin/settings',
			'action'   =&gt; 'index',
		];

		//listeners must return their parameters wrapped in an array
		return [$menu];
	});
}
</code></pre>
<h2>Creating Controllers</h2>
<p>Plugin controllers use the namespace <code>Vvveb\Plugins\{Name}\Controller</code> for <strong>both</strong> admin and front-end  -  the folder (<code>admin/controller/</code> vs <code>app/controller/</code>) selects the context.</p>
<h3>Admin Controller</h3>
<pre><code class="language-php">// plugins/my-plugin/admin/controller/settings.php
namespace Vvveb\Plugins\MyPlugin\Controller;

use Vvveb\Controller\Base;

class Settings extends Base {
	public function index() {
		$this-&gt;view-&gt;settings = \Vvveb\getSetting('my-plugin');
	}

	public function save() {
		if ($this-&gt;checkCsrf()) {
			\Vvveb\setSetting('my-plugin', 'api_key', $this-&gt;request-&gt;post['api_key'] ?? '');
			$this-&gt;view-&gt;success[] = __('Settings saved');
		}
	}
}
</code></pre>
<p>Access it at <code>/admin/?module=plugins/my-plugin/settings</code> (module path is relative to the plugin's context folders).</p>
<h3>Front-End Controller</h3>
<pre><code class="language-php">// plugins/my-plugin/app/controller/index.php
namespace Vvveb\Plugins\MyPlugin\Controller;

use Vvveb\Controller\Base;

class Index extends Base {
	public function index() {
		$this-&gt;view-&gt;data = 'Hello from my plugin';
	}
}
</code></pre>
<p>Front-end controllers are reachable through routes added as shown above.</p>
<h2>Creating Components</h2>
<pre><code class="language-php">// plugins/my-plugin/component/my-widget.php
namespace Vvveb\Plugins\MyPlugin\Component;

use Vvveb\System\Component\ComponentBase;

class MyWidget extends ComponentBase {
	public int $cacheExpire = 3600;

	public static array $defaultOptions = [
		'limit' =&gt; 5,
	];

	public function results(): array {
		// Fetch data and return it
		return ['items' =&gt; []];
	}
}
</code></pre>
<p>Use in themes with the <code>data-v-component-plugin-{plugin}-{component}</code> naming convention (same single-dash style as the bundled example <code>data-v-component-plugin-contact-form-form</code> for plugin <code>contact-form</code>, component <code>form</code>):</p>
<pre><code class="language-html">&lt;div data-v-component-plugin-my-plugin-widget data-v-limit=&quot;3&quot;&gt;&lt;/div&gt;
</code></pre>
<p>The name is parsed by splitting at the <strong>last</strong> dash (<code>Vvveb\System\Component\Component::pluginComponentDetails()</code>): everything between <code>plugin-</code> and the last dash is the plugin slug, the rest is the component path (<code>-</code> becomes <code>/</code>, so <code>sub-widget</code> maps to <code>component/sub/widget.php</code>). Keep names dash-light and never include the word <code>plugin</code> in either part  -  e.g. <code>plugin-my-plugin-my-widget</code> misparses because of the extra dashes.</p>
<p>The results are available in <code>.tpl</code> templates via <code>$this-&gt;_component['plugin_my_plugin_widget'][$index]</code>.</p>
<h2>Hooking into Events</h2>
<h3>Modify Component Results</h3>
<pre><code class="language-php">Event::on('Vvveb\Component\Products', 'results', __CLASS__, function($results) {
	foreach ($results['product'] ?? [] as &amp;$product) {
		$product['custom_field'] = 'added by plugin';
	}

	return [$results];
}, 1000);
</code></pre>
<blockquote>
<p>Listeners must return all received parameters wrapped in an array (<code>return [$results];</code>), otherwise the modifications are lost.</p>
</blockquote>
<h3>Before/After Controller Execution</h3>
<pre><code class="language-php">// After any controller action, before rendering
Event::on('Vvveb\System\Core\FrontController', 'call', __CLASS__, function($template, $controller, $actionName) {
	// Modify template or response
	return [$template, $controller, $actionName];
}, 1000);
</code></pre>
<h3>Inject Template Commands</h3>
<p>Use the <code>View::compile:after</code> event to append <code>.tpl</code> commands when a template is compiled, for example to render your component inside existing themes:</p>
<pre><code class="language-php">Event::on('Vvveb\System\Core\View', 'compile:after', __CLASS__,
	function ($template, $htmlFile, $tplFile, $vTpl, $view) {
		$vTpl-&gt;loadTemplateFile(__DIR__ . '/template/widget.tpl');
		return [$template, $htmlFile, $tplFile, $vTpl, $view];
	});
</code></pre>
<p>See <a href="../events/events-introduction.md">Events introduction</a> and the <a href="../events/events-list.md">events list</a> for all available hooks.</p>
<h2>Plugin Lifecycle</h2>
<table class="table table-bordered">
<thead class="table-light">
<tr>
<th>Phase</th>
<th>Method</th>
<th>When</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>Activation</strong></td>
<td><code>Plugins::activate()</code></td>
<td>Sets <code>config/plugins.php</code> status to <code>active</code>, copies/symlinks <code>public/</code> dir, fires <code>activate</code> event</td>
</tr>
<tr>
<td><strong>First activation</strong></td>
<td><code>setup</code> event</td>
<td>Fired on first activation only (<code>Vvveb\System\Extensions\Plugins</code>, <code>setup</code>, parameters <code>$pluginName</code>, <code>$site_id</code>)  -  use it to create tables</td>
</tr>
<tr>
<td><strong>Loading</strong></td>
<td><code>Plugins::loadPlugins()</code></td>
<td>Includes <code>plugin.php</code> for each active plugin on every request</td>
</tr>
<tr>
<td><strong>Deactivation</strong></td>
<td><code>Plugins::deactivate()</code></td>
<td>Sets status to <code>inactive</code></td>
</tr>
<tr>
<td><strong>Uninstall</strong></td>
<td><code>Plugins::uninstall()</code></td>
<td>Removes files, unsets config, fires <code>uninstall</code> event</td>
</tr>
</tbody>
</table>
<p>To create your tables on first activation, either ship an <code>install.php</code> that runs your installer class, or listen to the <code>setup</code> event:</p>
<pre><code class="language-php">Event::on('Vvveb\System\Extensions\Plugins', 'setup', __CLASS__, function ($pluginName, $siteId) {
	if ($pluginName == 'my-plugin') {
		(new \Vvveb\Plugins\MyPlugin\Install())-&gt;run();
	}

	return [$pluginName, $siteId];
});
</code></pre>
<p>Schema files placed in <code>install/sql/{engine}/schema/*.sql</code> are executed against the database on activation.</p>
<h2>Validation Rules</h2>
<p>Add custom validation rules in <code>validate/</code> (under <code>admin/validate/</code> or <code>app/validate/</code> depending on context):</p>
<pre><code class="language-php">// plugins/my-plugin/app/validate/my-form.php
return [
	'name' =&gt; [
		'NotEmpty' =&gt; ['message' =&gt; __('Name is required')],
		'MaxLength' =&gt; ['max' =&gt; 100, 'message' =&gt; __('Name too long')],
	],
	'email' =&gt; [
		'NotEmpty' =&gt; ['message' =&gt; __('Email is required')],
		'Email' =&gt; ['message' =&gt; __('Invalid email')],
	],
];
</code></pre>
<p>Use in controllers:</p>
<pre><code class="language-php">$validator = new \Vvveb\System\Validator(['plugins/my-plugin/my-form']);
$errors = $validator-&gt;validate($this-&gt;request-&gt;post);
if ($errors !== true) {
	$this-&gt;view-&gt;errors = $errors;
	return;
}
</code></pre>
<h2>Public Assets</h2>
<p>Files in <code>plugin/public/</code> are copied/symlinked to <code>public/plugins/{plugin-name}/</code> on activation:</p>
<pre><code class="language-html">&lt;!-- In templates --&gt;
&lt;link rel=&quot;stylesheet&quot; href=&quot;/plugins/my-plugin/css/style.css&quot;&gt;
&lt;script src=&quot;/plugins/my-plugin/js/script.js&quot;&gt;&lt;/script&gt;
</code></pre>
<h2>SQL Models</h2>
<p>Define queries in <code>sql/{engine}/</code> using the SqlP stored-procedure DSL (see the <a href="../sqlp/sqlp.md">SqlP guide</a>):</p>
<pre><code class="language-sql">-- plugins/my-plugin/sql/mysqli/message.sql
-- table names are plain: the SqlP parser applies the DB_PREFIX automatically
CREATE PROCEDURE getAll(
	IN site_id INT,
	IN start INT,
	IN limit INT,
	OUT fetch_all,
	OUT fetch_one
)
BEGIN
	SELECT * FROM my_plugin_message
	WHERE site_id = :site_id
	LIMIT :start, :limit;

	SELECT count(*) FROM my_plugin_message
	WHERE site_id = :site_id;
END
</code></pre>
<p>Write one file per engine (<code>mysqli</code>, <code>pgsql</code>, <code>sqlite</code>); backtick identifiers for mysqli/sqlite and double quotes for pgsql. The parser generates a class usable through the model helper:</p>
<pre><code class="language-php">$message = model('Plugins\MyPlugin\Message'); // Vvveb\Sql\Plugins\MyPlugin\MessageSQL
$rows    = $message-&gt;getAll(['site_id' =&gt; SITE_ID, 'start' =&gt; 0, 'limit' =&gt; 20]);
</code></pre>
<h2>Plugin Settings</h2>
<p>Store plugin settings using the Settings system:</p>
<pre><code class="language-php">// Save
\Vvveb\setSetting('my-plugin', 'api_key', 'abc123');

// Read all settings of the plugin
$settings = \Vvveb\getSetting('my-plugin');

// Read one setting with default
$key = \Vvveb\getSetting('my-plugin', 'api_key', '');
</code></pre>
<h2>Best Practices</h2>
<ol>
<li><strong>Namespace everything</strong>: Use <code>Vvveb\Plugins\{PluginName}\</code> namespace</li>
<li><strong>Use events, not overrides</strong>: Hook into events rather than modifying core files</li>
<li><strong>Cache aggressively</strong>: Set appropriate <code>$cacheExpire</code> on components</li>
<li><strong>Validate input</strong>: Always validate user input in controllers and check CSRF on POST</li>
<li><strong>Use the model layer</strong>: Define SQL procedures for database operations, one file per engine</li>
<li><strong>Keep public assets minimal</strong>: Only include what's needed</li>
<li><strong>Test with multiple sites</strong>: Ensure your plugin works with multi-site setups (settings are stored per <code>site_id</code>)</li>
</ol>
]]></content:encoded>		
		</item><item data-v-post="post" data-v-id="411">
			<title data-v-post-name="name">Queue System</title>
			<link data-v-post-full-url="name">https://dev.vvveb.com/queue-system</link>
			<dc:creator data-v-post-display_name="name"><![CDATA[Admin]]></dc:creator>
			<pubDate data-v-post-pubDate="date">Mon, 28 Sep 2026 17:24:01 +0000</pubDate>
			<category data-v-post-category-name="category" data-filter-cdata=""><![CDATA[Category]]></category>
			<description data-v-post-excerpt="excerpt" data-filter-cdata=""><![CDATA[Vvveb\Queue (system/queue.php) provides a background job queue: push jobs onto named queues, process them with a worker (CLI or cron single-pass), retry failures with exponential backoff, and schedule delayed jobs. 
Backends are providers behind a common]]></description>
			<content:encoded data-v-post-content="content" data-filter-cdata=""><![CDATA[
<p><code>Vvveb\Queue</code> (<code>system/queue.php</code>) provides a background job queue: push jobs onto named queues, process them with a worker (CLI or cron single-pass), retry failures with exponential backoff, and schedule delayed jobs.</p>
<p>Backends are <strong>providers</strong> behind a common interface. Two ship built in:</p>
<ul>
<li><strong>database</strong> (default)  -  stores jobs in <code>queue_jobs</code> / <code>failed_jobs</code> tables using Vvveb's existing DB connection; works with PostgreSQL, MySQL/MariaDB and SQLite.</li>
<li><strong>redis</strong>  -  reliable-queue pattern with lists + sorted sets (requires the phpredis extension).</li>
</ul>
<p>Plugins can register more backends (Beanstalkd, SQS ...) through the provider registration event  -  same pattern as payment/notification providers.</p>
<h2>Usage</h2>
<pre><code class="language-php">use Vvveb\Queue;

// register a handler for a job name (e.g. during plugin bootstrap)
Queue::registerHandler('SendOrderConfirmation', function (array $payload, array $job) {
    // do work; throw to fail the job
});

// enqueue  -  runs as soon as a worker picks it up
Queue::push('default', 'SendOrderConfirmation', ['order_id' =&gt; 10]);

// enqueue with options
Queue::push('emails', 'ResizeImage', ['media_id' =&gt; 5], ['max_attempts' =&gt; 5]);

// delayed job
Queue::later('default', 3600, 'SendReminder', ['user_id' =&gt; 5]);   // in 1 hour

// monitoring
Queue::size('default');        // pending count
Queue::stats('default');       // ['ready' =&gt; .., 'delayed' =&gt; .., 'reserved' =&gt; ..]
</code></pre>
<p>Payload must be serializable  -  arrays/scalars only (JSON encoded). Objects must be encoded as arrays and rehydrated inside the handler.</p>
<h2>Worker</h2>
<p>Handlers are registered per process, so the worker needs the same bootstrap your site runs (plugins that call <code>Queue::registerHandler()</code> are loaded automatically):</p>
<pre><code class="language-bash"># persistent worker (recommended)
php cli.php queue:work --queue=default --sleep=1 --max-jobs=0

# drain everything and exit
php cli.php queue:work --drain=1

# register handlers from an extra file
php cli.php queue:work --bootstrap=/path/to/handlers.php
</code></pre>
<p>For shared hosting without a persistent worker, the cron entry point (<code>?module=cron</code> / <code>run-cron/&lt;key&gt;</code>) also drains a short batch of jobs after running registered cron jobs (see <code>cron_pass</code> in <code>config/queue.php</code>).</p>
<p>The worker catches all errors per-job so one bad job never kills the loop. Handler execution: pop → run handler → <code>complete()</code>, or on throw → <code>fail()</code>.</p>
<h2>Retries &amp; failed jobs</h2>
<ul>
<li>Each claim increments <code>attempts</code>.</li>
<li>On failure, if <code>attempts &lt; max_attempts</code>, the job is requeued with exponential backoff: <code>base_delay_seconds * 2^attempts</code>, capped at <code>max_delay_seconds</code> (defaults 30s/3600s).</li>
<li>Once retries are exhausted the job moves to the <strong>failed store</strong> (<code>failed_jobs</code> table / Redis hash) with the error message, and a <code>queue.job.failed</code> notification event is triggered (routed via the notifications system  -  email/Slack/webhooks).</li>
</ul>
<p>Failed jobs can be retried or deleted from <strong>Admin → Tools → Queue</strong> (per-job buttons plus <em>Retry all</em> / <em>Clear all</em> bulk actions). Retrying resets attempts to 0.</p>
<h2>Stalled jobs</h2>
<p>If a worker crashes after claiming a job, the reservation would hold it forever. A reaper (<code>reserved_timeout_seconds</code>, default 300s) clears stale reservations  -  it runs periodically inside <code>Queue::work()</code> / <code>Queue::cronPass()</code>.</p>
<h2>Providers</h2>
<p>Interface (<code>system/queue/providers/provider.php</code>):</p>
<pre><code class="language-php">interface Provider {
    public function push(string $queue, array $job): string;              // returns job id
    public function pop(string $queue): ?array;                           // atomically claim next available job
    public function complete(string $jobId): void;                        // remove/ack
    public function release(string $jobId, int $delaySeconds = 0): void;  // return to queue after delay
    public function fail(string $jobId, string $error): bool;             // true = moved to failed store permanently
    public function size(string $queue): int;                             // pending count
}
</code></pre>
<p>Full-featured backends should also implement <code>FailedJobsAware</code> (<code>getFailed</code>, <code>retryFailed</code>, <code>deleteFailed</code>, <code>clearFailed</code>, <code>requeueStalled</code>, <code>stats</code>) so admin monitoring works.</p>
<p>Plugins register additional backends like this:</p>
<pre><code class="language-php">use Vvveb\System\Event;

Event::on('Vvveb\Queue\Providers', 'register', __CLASS__,
    function ($providers) {
        $providers['beanstalkd'] = \Vvveb\Plugins\MyBeanstalkd\QueueProvider::class;
        return [$providers];
    });
</code></pre>
<h2>Database provider internals</h2>
<p>One schema for all engines; timestamps are unix integers, payload is JSON:</p>
<ul>
<li><strong>PostgreSQL</strong>  -  atomic single-statement claim with <code>FOR UPDATE SKIP LOCKED</code> + <code>RETURNING</code>; concurrent workers poll without blocking or double-claiming.</li>
<li><strong>MySQL / MariaDB</strong>  -  on MySQL 8.0+ / MariaDB 10.6+ a transactional <code>SELECT ... FOR UPDATE SKIP LOCKED</code> + <code>UPDATE</code> skips rows locked by other workers. On older servers it falls back to an inherently atomic single-statement claim using the <code>LAST_INSERT_ID(id)</code> trick to read back which row was claimed (no version sniffing failures  -  both paths share the numeric auto-increment PK + UUID <code>job_id</code> columns).</li>
<li><strong>SQLite</strong>  -  no row-level locking (single writer by design); claims run inside <code>BEGIN IMMEDIATE</code> with WAL + busy timeout. This backend is realistically only suitable for low-throughput / single-worker setups  -  pair it with the cron single-pass mode rather than a multi-process supervisor.</li>
</ul>
<p>Tables are created by the installer (<code>install/sql/&lt;engine&gt;/schema/queue/</code>) and lazily ensured on first use for upgraded installations.</p>
<h2>Redis provider internals</h2>
<p>Keys per queue (prefixed, default <code>vvveb.queue:</code>):</p>
<table class="table table-bordered">
<thead class="table-light">
<tr>
<th>Key</th>
<th>Type</th>
<th>Purpose</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>{prefix}{name}</code></td>
<td>list</td>
<td>ready jobs</td>
</tr>
<tr>
<td><code>{prefix}{name}:delayed</code></td>
<td>zset</td>
<td>scheduled jobs scored by <code>available_at</code></td>
</tr>
<tr>
<td><code>{prefix}{name}:reserved</code></td>
<td>list</td>
<td>claimed/in-flight jobs</td>
</tr>
<tr>
<td><code>{prefix}{name}:failed</code></td>
<td>hash</td>
<td>dead jobs keyed by job id</td>
</tr>
</tbody>
</table>
<p><code>pop()</code> migrates due delayed jobs into the ready list (atomic Lua script) then moves a job to reserved with <code>RPOPLPUSH</code>, so crashed workers never lose claimed jobs  -  stalled reservations are requeued by the reaper. Connection settings come from <code>config/queue.php</code> under <code>providers.redis</code>; when no host is set it shares the cache driver's redis settings (<code>config/app.php</code> → <code>cache</code> section).</p>
<h2>Config</h2>
<p><code>config/queue.php</code> follows the plain-array convention of <code>config/notifications.php</code>:</p>
<pre><code class="language-php">return [
	'default'      =&gt; 'database',          //or 'redis'
	'max_attempts' =&gt; 3,
	'providers'    =&gt; [
		'redis'    =&gt; [/* host, port, password, database, prefix */],
		'database' =&gt; [],                  //reuses config/db.php connection
	],
	'retry'                    =&gt; ['base_delay_seconds' =&gt; 30, 'max_delay_seconds' =&gt; 3600],
	'reserved_timeout_seconds' =&gt; 300,
	'cron_pass'                =&gt; ['enabled' =&gt; true, 'max_jobs' =&gt; 25, 'max_seconds' =&gt; 20],
];
</code></pre>
<p>Provider selection and these values are editable in <strong>Admin → Tools → Queue</strong>.</p>
]]></content:encoded>		
		</item><item data-v-post="post" data-v-id="412">
			<title data-v-post-name="name">Caching System</title>
			<link data-v-post-full-url="name">https://dev.vvveb.com/caching-system</link>
			<dc:creator data-v-post-display_name="name"><![CDATA[Admin]]></dc:creator>
			<pubDate data-v-post-pubDate="date">Mon, 28 Sep 2026 17:24:01 +0000</pubDate>
			<category data-v-post-category-name="category" data-filter-cdata=""><![CDATA[Category]]></category>
			<description data-v-post-excerpt="excerpt" data-filter-cdata=""><![CDATA[Vvveb implements a multi-tier caching architecture for maximum performance. 
Cache Tiers 
Page Cache 
Full-page HTML caching for anonymous visitors. When enabled, complete page output is stored as static HTML files. 
Enable: Set PAGE_CACHE = true]]></description>
			<content:encoded data-v-post-content="content" data-filter-cdata=""><![CDATA[
<p>Vvveb implements a multi-tier caching architecture for maximum performance.</p>
<h2>Cache Tiers</h2>
<h3>Page Cache</h3>
<p>Full-page HTML caching for anonymous visitors. When enabled, complete page output is stored as static HTML files.</p>
<p><strong>Enable</strong>: Set <code>PAGE_CACHE = true</code> in <code>env.php</code> and configure your web server.</p>
<p><strong>Storage</strong>: <code>public/page-cache/{host}/{path}.html</code></p>
<p><strong>Behavior</strong>:</p>
<ul>
<li>Serves cached HTML for anonymous users (no <code>user</code>, <code>cart</code>, <code>admin</code>, <code>nocache</code> cookies)</li>
<li>Skips URLs with query parameters, <code>/user/</code>, <code>/cart/</code>, <code>/checkout/</code>, <code>/feed/</code> paths</li>
<li>Uses stale-while-revalidate: serves old cache while generating new version in background</li>
<li>Automatically disabled when users log in or add items to cart</li>
<li>Maximum wait: root <code>index.php</code> waits up to 10 seconds (<code>$waitSeconds</code>) for an in-progress generation before serving stale content (prevents thundering herd)</li>
</ul>
<p><strong>Management</strong>:</p>
<pre><code class="language-php">// Clear page cache (optionally scoped by host/path)
\Vvveb\System\CacheManager::clearPageCache();

// Toggle caching at runtime; $type is one of user/cart/admin/nocache
// (controls which cookie gates the cache)
\Vvveb\System\PageCache::disable('user'); // set the `user` cookie, stop caching
\Vvveb\System\PageCache::enable('user');  // clear the `user` cookie, resume caching
</code></pre>
<h3>Object Cache</h3>
<p>In-memory key-value caching with multiple driver backends.</p>
<p><strong>Drivers</strong>: File, APCu, Memcached, Redis</p>
<p><strong>Configuration</strong> in <code>config/app.php</code>:</p>
<pre><code class="language-php">return [
    'cache' =&gt; [
        'driver'  =&gt; 'file', // file, apcu, memcached, redis
        'file' =&gt; [
            'cacheDir' =&gt; DIR_STORAGE . '/cache',
        ],
        'memcached' =&gt; [
            'host' =&gt; '127.0.0.1',
            'port' =&gt; 11211,
        ],
        'redis' =&gt; [
            'host' =&gt; '127.0.0.1',
            'port' =&gt; 6379,
        ],
    ],
];
</code></pre>
<p><strong>Usage</strong>:</p>
<pre><code class="language-php">$cache = \Vvveb\System\Cache::getInstance();

// Basic get/set
$value = $cache-&gt;get('namespace', 'key');
$cache-&gt;set('namespace', 'key', $value, 3600);

// Get-or-compute pattern
$data = $cache-&gt;cache('namespace', 'key', function() {
    return expensiveOperation();
}, 3600);

// Multi operations
$values = $cache-&gt;getMulti('namespace', ['key1', 'key2']);
$cache-&gt;setMulti('namespace', ['key1' =&gt; $val1, 'key2' =&gt; $val2], 3600);

// Delete
$cache-&gt;delete('namespace', 'key');

// Purge entire namespace
$cache-&gt;purge('namespace');
</code></pre>
<h3>Component Cache</h3>
<p>Each component has its own cache layer with per-component expiration:</p>
<pre><code class="language-php">class Products extends ComponentBase {
    public int $cacheExpire = 3600; // Cache for 1 hour
}
</code></pre>
<p>The component system uses batch get/set (<code>getMulti</code>/<code>setMulti</code>) for efficiency and implements stale-while-revalidate with lock files.</p>
<h3>Template Cache</h3>
<p>Compiled PHP templates are cached in <code>storage/compiled-templates/</code>. Templates are recompiled when the source HTML file changes (based on file modification time).</p>
<h3>Route Cache</h3>
<p>Compiled route regex patterns are cached in the object cache under the <code>routes</code> namespace. Routes are recompiled when route config files change.</p>
<h2>Cache Management</h2>
<pre><code class="language-php">// Clear everything
\Vvveb\System\CacheManager::delete('namespace');

// Clear specific tiers
\Vvveb\System\CacheManager::clearFrontend();      // Front-end assets
\Vvveb\System\CacheManager::clearModelCache();     // SQL model cache
\Vvveb\System\CacheManager::clearImageCache();     // Image cache
\Vvveb\System\CacheManager::clearObjectCache('namespace'); // Object cache
\Vvveb\System\CacheManager::clearPageCache();      // Full page cache
\Vvveb\System\CacheManager::clearCompiledFiles();  // Compiled templates
</code></pre>
<h3>Via Admin Panel</h3>
<p>Navigate to <strong>Tools &gt; Cache</strong> in the admin dashboard to clear all cache layers with one click.</p>
<h3>Via CLI</h3>
<pre><code class="language-bash">php cli.php admin module=tools/cache action=delete
</code></pre>
<h2>OPcache</h2>
<p>For production, enable PHP OPcache for maximum performance:</p>
<pre><code class="language-ini">; php.ini
opcache.enable=1
opcache.memory_consumption=128
opcache.max_accelerated_files=10000
opcache.validate_timestamps=0  ; Set to 0 in production
</code></pre>
<p>When <code>opcache.validate_timestamps=0</code>, PHP won't check for file changes. Clear OPcache after deployments:</p>
<pre><code class="language-php">opcache_reset();
</code></pre>
<h2>Best Practices</h2>
<ol>
<li><strong>Use appropriate cache expiration</strong>: Short TTL for dynamic data, long TTL for static content</li>
<li><strong>Use the get-or-compute pattern</strong>: <code>Cache::cache($namespace, $key, $callback, $expire)</code> avoids cache stampede</li>
<li><strong>Disable caching for user-specific data</strong>: Set <code>$cacheExpire = 0</code> for cart, wishlist, etc.</li>
<li><strong>Clear cache after deployments</strong>: Model changes and template edits require cache invalidation</li>
<li><strong>Use APCu or Redis in production</strong>: File cache is fine for development but slow under high load</li>
</ol>
]]></content:encoded>		
		</item><item data-v-post="post" data-v-id="413">
			<title data-v-post-name="name">GraphQL API</title>
			<link data-v-post-full-url="name">https://dev.vvveb.com/graphql-api</link>
			<dc:creator data-v-post-display_name="name"><![CDATA[Admin]]></dc:creator>
			<pubDate data-v-post-pubDate="date">Mon, 28 Sep 2026 17:24:01 +0000</pubDate>
			<category data-v-post-category-name="category" data-filter-cdata=""><![CDATA[Category]]></category>
			<description data-v-post-excerpt="excerpt" data-filter-cdata=""><![CDATA[The GraphQL API provides a flexible query language for accessing Vvveb data. It uses the webonyx/graphql-php library. 
Enabling 
The API is enabled by default (defined('GRAPHQL') || define('GRAPHQL', true) in env.php; GRAPHQL_CAMELCASE controls camelCase]]></description>
			<content:encoded data-v-post-content="content" data-filter-cdata=""><![CDATA[
<p>The GraphQL API provides a flexible query language for accessing Vvveb data. It uses the <a href="https://github.com/webonyx/graphql-php">webonyx/graphql-php</a> library.</p>
<h2>Enabling</h2>
<p>The API is enabled by default (<code>defined('GRAPHQL') || define('GRAPHQL', true)</code> in <code>env.php</code>; <code>GRAPHQL_CAMELCASE</code> controls camelCase field names). The <code>webonyx/graphql-php</code> library is already required in <code>composer.json</code>, so a plain <code>composer install</code> suffices.</p>
<p>The API is accessible at <code>/graphql</code>. To disable it, define <code>GRAPHQL</code> as false.</p>
<h2>Authentication</h2>
<p>GraphQL uses the same authentication as the admin panel:</p>
<ul>
<li><strong>HTTP Basic Auth</strong>: Include <code>Authorization: Basic base64(user:pass)</code> header</li>
<li><strong>Bearer Token</strong>: Include <code>Authorization: Bearer {token}</code> header</li>
<li><strong>Session</strong>: Include admin session cookies</li>
</ul>
<h2>Schema</h2>
<p>The schema is auto-generated from the database structure and SQL model files. It's cached in <code>storage/model/cached_schema.php</code>.</p>
<h3>Types</h3>
<p>All database tables are automatically mapped to GraphQL types:</p>
<pre><code class="language-graphql">type Product {
    product_id: Int
    name: String
    slug: String
    price: Float
    description: String
    status: Int
    date_added: String
}

type Post {
    post_id: Int
    title: String
    slug: String
    content: String
    status: Int
    date_added: String
}
</code></pre>
<h3>Queries</h3>
<p>Each entity with a <code>getAll</code> SQL procedure gets a query:</p>
<pre><code class="language-graphql">type RootQuery {
    products(start: Int, limit: Int, sort: String, order: String): ProductConnection
    product(product_id: Int, slug: String): Product
    posts(start: Int, limit: Int, type: String): PostConnection
    post(post_id: Int, slug: String): Post
    users(start: Int, limit: Int): UserConnection
    orders(start: Int, limit: Int): OrderConnection
    # ... all other entities
}
</code></pre>
<h3>Mutations</h3>
<p>Entities with <code>add</code>, <code>edit</code>, <code>delete</code> procedures get mutations:</p>
<pre><code class="language-graphql">type MutationType {
    addProduct(input: ProductInput): Product
    editProduct(product_id: Int!, input: ProductInput): Product
    deleteProduct(product_id: Int!): Boolean
    addPost(input: PostInput): Post
    editPost(post_id: Int!, input: PostInput): Post
    deletePost(post_id: Int!): Boolean
    # ... all other entities
}
</code></pre>
<h3>Connections (Pagination)</h3>
<p>List queries return Connection types with pagination:</p>
<pre><code class="language-graphql">type ProductConnection {
    nodes: [Product]
    PageInfo: PageInfo
}

type PageInfo {
    totalPages: Int
    totalItems: Int
    currentPage: Int
    hasNextPage: Boolean
    hasPreviousPage: Boolean
}
</code></pre>
<h2>Example Queries</h2>
<h3>Get Products</h3>
<pre><code class="language-graphql">query {
    products(limit: 10, sort: &quot;date_added&quot;, order: &quot;DESC&quot;) {
        nodes {
            product_id
            name
            slug
            price
            image
        }
        PageInfo {
            totalPages
            totalItems
        }
    }
}
</code></pre>
<h3>Get Single Product</h3>
<pre><code class="language-graphql">query {
    product(slug: &quot;my-product&quot;) {
        product_id
        name
        description
        price
        categories {
            name
            slug
        }
    }
}
</code></pre>
<h3>Get Posts</h3>
<pre><code class="language-graphql">query {
    posts(limit: 5, type: &quot;post&quot;) {
        nodes {
            post_id
            title
            slug
            excerpt
            date_added
        }
    }
}
</code></pre>
<h3>Create Product (Admin)</h3>
<pre><code class="language-graphql">mutation {
    addProduct(input: {
        name: &quot;New Product&quot;
        slug: &quot;new-product&quot;
        price: 29.99
        description: &quot;Product description&quot;
        status: 1
    }) {
        product_id
        name
    }
}
</code></pre>
<h3>Update Product (Admin)</h3>
<pre><code class="language-graphql">mutation {
    editProduct(product_id: 42, input: {
        price: 39.99
    }) {
        product_id
        name
        price
    }
}
</code></pre>
<h2>Interactive Schema</h2>
<p>Access the full schema documentation at <code>/graphql/schema</code> or use a GraphQL IDE plugin with introspection enabled.</p>
<h2>Transforms</h2>
<p>Entity data can be transformed before returning. Default transforms are defined in <code>graphql/transform/</code>:</p>
<ul>
<li><code>Product</code>  -  adds formatted prices, images, URLs</li>
<li><code>Post</code>  -  adds content, URLs, author info</li>
</ul>
<p>Plugins can register custom transforms via the <code>graphql.transforms</code> event:</p>
<pre><code class="language-php">\Event::on('Vvveb\Controller\Index', 'transforms', 'my-plugin', function($transforms) {
    $transforms['my_entity'] = 'Vvveb\Plugins\MyPlugin\Transform\MyEntity';
    return $transforms;
});
</code></pre>
<h2>Custom Queries/Mutations</h2>
<p>Plugins can register custom GraphQL resolvers:</p>
<pre><code class="language-php">// In plugin.php
public function admin(): void {
    \Vvveb\System\Event::on('Vvveb\Controller\Index', 'queries', 'my-plugin', function($queries) {
        $queries['myCustomQuery'] = function($root, $args, $context, $info) {
            return ['data' =&gt; 'value'];
        };
        return $queries;
    }, 1000);
}
</code></pre>
<h2>Configuration</h2>
<p>GraphQL configuration in <code>config/graphql.php</code>:</p>
<pre><code class="language-php">return [
    'schema_file'     =&gt; DIR_GRAPHQL . '/schema.gql',
    'controller_path' =&gt; DIR_GRAPHQL . '/controller',
    'camelcase'       =&gt; true,  // Use camelCase for field names
];
</code></pre>
<h2>Schema Regeneration</h2>
<p>The schema is cached. To regenerate:</p>
<pre><code class="language-bash"># Via API
curl http://localhost/graphql/schema &gt; graphql/schema.gql

# Or clear cache
php cli.php cache module=tools/action/cache
</code></pre>
<h2>Rate Limiting</h2>
<p>The GraphQL API does not have built-in rate limiting. Use web server-level rate limiting (nginx, Apache) or the <code>captcha</code> plugin for public-facing queries.</p>
]]></content:encoded>		
		</item><item data-v-post="post" data-v-id="414">
			<title data-v-post-name="name">Notifications System</title>
			<link data-v-post-full-url="name">https://dev.vvveb.com/notifications-system</link>
			<dc:creator data-v-post-display_name="name"><![CDATA[Admin]]></dc:creator>
			<pubDate data-v-post-pubDate="date">Mon, 28 Sep 2026 17:24:01 +0000</pubDate>
			<category data-v-post-category-name="category" data-filter-cdata=""><![CDATA[Category]]></category>
			<description data-v-post-excerpt="excerpt" data-filter-cdata=""><![CDATA[Vvveb\System\Notifications (system/notifications.php) lets core code and plugins register notification events and trigger them when something happens. Each triggered event is routed to one or more configurable providers (email, SMS, WhatsApp, Slack,]]></description>
			<content:encoded data-v-post-content="content" data-filter-cdata=""><![CDATA[
<p><code>Vvveb\System\Notifications</code> (<code>system/notifications.php</code>) lets core code and plugins <strong>register</strong> notification events and <strong>trigger</strong> them when something happens. Each triggered event is routed to one or more configurable <strong>providers</strong> (email, SMS, WhatsApp, Slack, webhooks) based on the settings stored in <code>config/notifications.php</code>.</p>
<p>The API mirrors <code>Vvveb\System\Event</code>, so it feels familiar:</p>
<pre><code class="language-php">use Vvveb\System\Notifications;

// Register an event (idempotent  -  safe to call from plugin bootstrap)
Notifications::register('myplugin.export.done', [
    'description'        =&gt; 'A data export finishes',
    'category'           =&gt; 'other',              // content|users|commerce|engagement|media|system|other
    'default_recipients' =&gt; ['site_admin'],       // site_admin|user|admin_user
]);

// Trigger it with a payload
Notifications::trigger('myplugin.export.done', [
    'subject' =&gt; 'Export finished',
    'message' =&gt; 'Your export is ready.',
    'export'  =&gt; $exportRecord,
]);
</code></pre>
<p>All built-in events are registered automatically via <code>Notifications::registerDefaults()</code> (called lazily by <code>trigger()</code> and by the admin settings page). Plugins can register additional events on the <code>Vvveb\System\Notifications::register</code> event hook.</p>
<h2>Envelope</h2>
<p>Every trigger passes a consistent envelope to providers:</p>
<pre><code class="language-php">[
    'event'     =&gt; 'order.new',
    'timestamp' =&gt; time(),
    'recipient' =&gt; ['id' =&gt; ..., 'name' =&gt; ..., 'email' =&gt; ..., 'phone' =&gt; ...],
    'subject'   =&gt; '...',   // short human-readable title
    'message'   =&gt; '...',   // human-readable body
    'data'      =&gt; [ /* raw event-specific payload */ ],
]
</code></pre>
<p>Recipient resolution per event is configurable in the admin UI:</p>
<ul>
<li><strong>site_admin</strong>  -  site contact email</li>
<li><strong>user</strong>  -  subject of the event; resolved from <code>$data['user']</code>, <code>$data['customer']</code>, <code>$data['order']</code>, etc., or top-level keys (<code>email</code>, <code>first_name</code>, <code>phone</code>...)</li>
<li><strong>admin_user</strong>  -  admin tied to the record (<code>$data['admin_email']</code> / nested <code>admin</code> record)</li>
<li><strong>additional emails / phones</strong>  -  free-form lists configured per event</li>
<li><strong>explicit override</strong>  -  passing <code>'to'</code> in the payload replaces auto-resolved targets (used e.g. by the contact-form plugin's <em>send to</em> option); <code>additional_*</code> entries still append</li>
</ul>
<p>If no recipient resolves (e.g. webhook-only events), dispatch still happens once with an empty recipient.</p>
<h2>Providers</h2>
<p>Providers implement <code>Vvveb\System\Notifications\Providers\Provider</code>:</p>
<pre><code class="language-php">interface Provider {
    public static function send(string $event, array $data, array $settings): bool;
    public static function settingsFields(): array;
}
</code></pre>
<p>Built-ins live in <code>system/notifications/providers/</code>: <code>Email</code> (reuses the site mail settings), <code>Sms</code> (Twilio-style), <code>Whatsapp</code> (Cloud API), <code>Slack</code> (incoming webhook), <code>Telegram</code> (bot API  -  needs bot token from @BotFather plus target chat ID; per-recipient <code>chat_id</code> override supported via <code>$data['recipient']['chat_id']</code>), <code>Webhooks</code> (any number of named endpoints with URL/method/headers).</p>
<h3>Adding provider types from a plugin</h3>
<pre><code class="language-php">\Vvveb\System\Event::on('Vvveb\Notifications\Providers', 'register', 'myplugin', function ($providers) {
    $providers['telegram'] = \MyPlugin\TelegramProvider::class;

    return [$providers]; // listeners must return their parameters
});
</code></pre>
<p>New providers appear as columns/cards in the admin settings page after save. A provider missing from config defaults to disabled until configured. Third-party examples: Discord, push notifications.</p>
<h2>Storage</h2>
<p>Settings persist in <code>config/notifications.php</code>:</p>
<pre><code class="language-php">return [
    'providers' =&gt; [
        'email' =&gt; ['enabled' =&gt; true, 'cooldown' =&gt; 0],
        /* sms | slack | whatsapp | webhooks */
        'global' =&gt; ['cooldown' =&gt; null],
    ],
    'events' =&gt; [
        'order.new' =&gt; [
            'providers'  =&gt; ['email'],
            'recipients' =&gt; ['user', 'site_admin'],
            'additional_emails' =&gt; [], 'additional_phones' =&gt; [], 'additional_webhooks' =&gt; [],
        ],
    ],
    'low_stock_threshold' =&gt; 5,
];
</code></pre>
<p>Admin page: <strong>Settings → Notifications</strong> (<code>admin/controller/settings/notifications.php</code> + <code>admin/template/settings/notifications.tpl</code>).</p>
<h2>Rate limiting / dedup</h2>
<p>Dispatch is throttled per <code>(event, recipient, provider)</code> tuple using the cache store:</p>
<ul>
<li>cooldown resolution order: <strong>per-event</strong> (<code>events.&lt;event&gt;.cooldown</code>) → <strong>per-provider</strong> (<code>providers.&lt;slug&gt;.cooldown</code>) → global → built-in defaults (email/webhooks: none, SMS/Slack/WhatsApp: 1 hour)</li>
<li><code>_dedup_key</code> in the payload adds an extra discriminator so identical events for different subjects (e.g. low stock per product) don't suppress each other</li>
<li>this mainly targets <code>cronjob.fail</code> / <code>php.fatal_error</code>, which are included in throttling by design</li>
</ul>
<h2>Low stock</h2>
<p><code>product.lowstock</code> uses the single global threshold at <code>low_stock_threshold</code>. Call <code>Notifications::checkLowStock($product)</code> wherever stock can drop (wired into checkout completion and admin product save). Leave the threshold empty to disable.</p>
<h2>Migrated legacy mail flows</h2>
<p>These former direct <code>email()</code> call sites now fire notifications instead:</p>
<table class="table table-bordered">
<thead class="table-light">
<tr>
<th>Call site</th>
<th>Event</th>
</tr>
</thead>
<tbody>
<tr>
<td>storefront checkout confirmation</td>
<td><code>order.new</code></td>
</tr>
<tr>
<td>storefront signup welcome</td>
<td><code>user.signup</code></td>
</tr>
<tr>
<td>storefront password reset request</td>
<td><code>user.password_reset</code></td>
</tr>
<tr>
<td>admin password reset request</td>
<td><code>admin.password_reset</code></td>
</tr>
<tr>
<td>admin order update email</td>
<td><code>order.status_change</code></td>
</tr>
<tr>
<td>contact-form plugin delivery</td>
<td><code>contact-form.message</code></td>
</tr>
</tbody>
</table>
]]></content:encoded>		
		</item><item data-v-post="post" data-v-id="415">
			<title data-v-post-name="name">Testing</title>
			<link data-v-post-full-url="name">https://dev.vvveb.com/testing</link>
			<dc:creator data-v-post-display_name="name"><![CDATA[Admin]]></dc:creator>
			<pubDate data-v-post-pubDate="date">Mon, 28 Sep 2026 17:24:01 +0000</pubDate>
			<category data-v-post-category-name="category" data-filter-cdata=""><![CDATA[Category]]></category>
			<description data-v-post-excerpt="excerpt" data-filter-cdata=""><![CDATA[Suites    Suite Directory Database required     system tests/system No - pure unit tests (also runnable DB-free via tests/system-runner.php)   model tests/model Yes - runs real SQL models against MySQL or SQLite    
Run everything from the project root:]]></description>
			<content:encoded data-v-post-content="content" data-filter-cdata=""><![CDATA[
<h2>Suites</h2>
<table class="table table-bordered">
<thead class="table-light">
<tr>
<th>Suite</th>
<th>Directory</th>
<th>Database required</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>system</code></td>
<td><code>tests/system</code></td>
<td>No  -  pure unit tests (also runnable DB-free via <code>tests/system-runner.php</code>)</td>
</tr>
<tr>
<td><code>model</code></td>
<td><code>tests/model</code></td>
<td>Yes  -  runs real SQL models against MySQL <strong>or</strong> SQLite</td>
</tr>
</tbody>
</table>
<p>Run everything from the project root:</p>
<pre><code class="language-sh">php phpunit                      # all suites
php phpunit --testsuite model    # model tests only
php tests/system-runner.php      # system tests without PHPUnit/DB
</code></pre>
<h2>Continuous integration</h2>
<p><code>.github/workflows/ci.yml</code> runs on every push and pull request:</p>
<table class="table table-bordered">
<thead class="table-light">
<tr>
<th>Job</th>
<th>What it does</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>lint</code></td>
<td><code>php -l</code> over every tracked PHP file (catches syntax errors in plugins too)</td>
</tr>
<tr>
<td><code>tests</code></td>
<td>Full suite on PHP 8.3/8.5 × MySQL (empty <code>vvveb_test</code> service database, auto-provisioned) and SQLite (throwaway file database)</td>
</tr>
<tr>
<td><code>phpstan</code></td>
<td>Static analysis of <code>system</code>, <code>app</code>, <code>admin</code>  -  <strong>non-blocking</strong> until the ~600 known findings are baselined, then flip <code>continue-on-error</code> off</td>
</tr>
</tbody>
</table>
<p>The MySQL job exercises the same auto-provisioning path used locally with
<code>DB_NAME=&lt;empty database&gt;</code>; the SQLite job exercises the fallback engine.</p>
<h2>Database selection for model tests</h2>
<p>Model tests resolve their database through <code>tests/support/TestDb.php</code>, which
runs before framework boot (<code>tests-bootstrap.php</code>). Selection is controlled by
the <code>DB_ENGINE</code> environment variable:</p>
<table class="table table-bordered">
<thead class="table-light">
<tr>
<th><code>DB_ENGINE</code></th>
<th>Behaviour</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>auto</code> <em>(default)</em></td>
<td>Use MySQL when reachable, otherwise fall back to SQLite</td>
</tr>
<tr>
<td><code>mysqli</code></td>
<td>Always use MySQL</td>
</tr>
<tr>
<td><code>sqlite</code></td>
<td>Always use SQLite (requires the <code>sqlite3</code> PHP extension, e.g. <code>apt install php8.5-sqlite3</code>)</td>
</tr>
</tbody>
</table>
<p>If neither backend is available the bootstrap stops with an actionable error
instead of running hundreds of broken tests.</p>
<p>Credentials are read from <code>config/db.php</code> and can be overridden per run:</p>
<pre><code class="language-sh">DB_ENGINE=mysqli DB_HOST=127.0.0.1 DB_NAME=vvveb_test DB_USER=root DB_PASS=secret \
  php phpunit --testsuite model
</code></pre>
<table class="table table-bordered">
<thead class="table-light">
<tr>
<th>Variable</th>
<th>Purpose</th>
<th>Default</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>DB_ENGINE</code></td>
<td><code>auto</code> | <code>mysqli</code> | <code>sqlite</code></td>
<td><code>auto</code></td>
</tr>
<tr>
<td><code>DB_HOST</code></td>
<td>MySQL host</td>
<td>from <code>config/db.php</code></td>
</tr>
<tr>
<td><code>DB_NAME</code></td>
<td>MySQL database</td>
<td>from <code>config/db.php</code></td>
</tr>
<tr>
<td><code>DB_USER</code> / <code>DB_PASS</code></td>
<td>MySQL credentials</td>
<td>from <code>config/db.php</code></td>
</tr>
<tr>
<td><code>DB_PORT</code></td>
<td>MySQL port</td>
<td>from <code>config/db.php</code></td>
</tr>
<tr>
<td><code>DB_PREFIX</code></td>
<td>Table prefix</td>
<td>empty</td>
</tr>
<tr>
<td><code>SQLITE_DB</code></td>
<td>Path of the throwaway SQLite database</td>
<td><code>storage/sqlite/test-unit.db</code></td>
</tr>
<tr>
<td><code>V_TEST_DB_REBUILD</code></td>
<td><code>1</code> forces a rebuild of the SQLite test database</td>
<td>unset</td>
</tr>
</tbody>
</table>
<p>Pointing <code>DB_NAME</code> at a dedicated database (e.g. <code>vvveb_test</code>) keeps test rows
out of your development data; the schema can be created the same way the
installer does it (<code>install/sql/mysqli/schema</code> + <code>install/sql/insert</code>).</p>
<h2>How the SQLite fallback works</h2>
<p>When SQLite is selected, a throwaway database file is built at bootstrap time:</p>
<ol>
<li>schema loaded from <code>install/sql/sqlite/schema/**/*.sql</code></li>
<li>seed data loaded from the shared <code>install/sql/insert/**/*.sql</code></li>
<li>SQL models are generated on demand into <code>storage/model/&lt;app&gt;/*.sqlite.php</code>
from <code>admin/sql/sqlite/*.sql</code> by the normal autoloader</li>
</ol>
<p>The build is fingerprinted (md5 over path/mtime/size of every schema and seed
file, stored next to the database as <code>&lt;db&gt;.meta.json</code>). The database is rebuilt
only when the fingerprint changes or when <code>V_TEST_DB_REBUILD=1</code>.</p>
<p>The generated models and queries are identical across engines because every
model has a per-engine <code>.sql</code> definition (<code>admin/sql/{mysqli,pgsql,sqlite}/</code>),
so a passing model suite exercises the same application logic regardless of
backend.</p>
]]></content:encoded>		
		</item><item data-v-post="post" data-v-id="416">
			<title data-v-post-name="name">Component questions</title>
			<link data-v-post-full-url="name">https://dev.vvveb.com/component-questions</link>
			<dc:creator data-v-post-display_name="name"><![CDATA[Admin]]></dc:creator>
			<pubDate data-v-post-pubDate="date">Mon, 28 Sep 2026 17:24:01 +0000</pubDate>
			<category data-v-post-category-name="category" data-filter-cdata=""><![CDATA[Category]]></category>
			<description data-v-post-excerpt="excerpt" data-filter-cdata=""><![CDATA[The Product Questions Component renders a list of questions for a product. It is typically used on product detail pages, but it can also power recent‑questions widgets, sidebar lists, or threaded question sections. 
Activate the component by adding:]]></description>
			<content:encoded data-v-post-content="content" data-filter-cdata=""><![CDATA[
<p>The <strong>Product Questions Component</strong> renders a list of questions for a product. It is typically used on product detail pages, but it can also power recent‑questions widgets, sidebar lists, or threaded question sections.</p>
<p>Activate the component by adding:</p>
<pre><code>data-v-component-questions
</code></pre>
<p>The component supports filtering, sorting, pagination, and nested replies.</p>
<hr />
<h2><strong>Component Options</strong></h2>
<p>Options are added as HTML attributes using the <code>data-v-*</code> syntax.</p>
<h3><strong>Pagination &amp; Sorting</strong></h3>
<table class="table table-bordered">
<thead class="table-light">
<tr>
<th>Option</th>
<th>Attribute</th>
<th>Description</th>
<th>Default</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>start</strong></td>
<td><code>data-v-start</code></td>
<td>Starting offset for pagination</td>
<td><code>0</code></td>
</tr>
<tr>
<td><strong>limit</strong></td>
<td><code>data-v-limit</code></td>
<td>Number of questions to display</td>
<td><code>10</code></td>
</tr>
<tr>
<td><strong>order</strong></td>
<td><code>data-v-order</code></td>
<td>Sort direction for threaded questions (<code>asc</code>, <code>desc</code>)</td>
<td><code>asc</code></td>
</tr>
</tbody>
</table>
<h3><strong>Filtering</strong></h3>
<table class="table table-bordered">
<thead class="table-light">
<tr>
<th>Option</th>
<th>Attribute</th>
<th>Description</th>
<th>Default</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>status</strong></td>
<td><code>data-v-status</code></td>
<td>Question status: <code>1</code> = published, <code>0</code> = pending, <code>2</code> = spam, <code>3</code> = trash</td>
<td><code>1</code></td>
</tr>
<tr>
<td><strong>product_id</strong></td>
<td><code>data-v-product_id</code></td>
<td>Filter by product ID</td>
<td><code>null</code></td>
</tr>
<tr>
<td><strong>slug</strong></td>
<td><code>data-v-slug</code></td>
<td>Filter by product slug</td>
<td><code>null</code></td>
</tr>
</tbody>
</table>
<h3><strong>Additional Options</strong></h3>
<table class="table table-bordered">
<thead class="table-light">
<tr>
<th>Option</th>
<th>Attribute</th>
<th>Description</th>
<th>Default</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>product_title</strong></td>
<td><code>data-v-product_title</code></td>
<td>Include product title (useful for recent‑questions widgets)</td>
<td><code>false</code></td>
</tr>
</tbody>
</table>
<hr />
<h2><strong>Component Properties</strong></h2>
<p>Each question is wrapped in:</p>
<pre><code>data-v-question
</code></pre>
<p>The following fields are available inside each question element.</p>
<h3><strong>Author Information</strong></h3>
<table class="table table-bordered">
<thead class="table-light">
<tr>
<th>Property</th>
<th>Attribute</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>username</strong></td>
<td><code>data-v-question-username</code></td>
<td>Username of the questioner</td>
</tr>
<tr>
<td><strong>email</strong></td>
<td><code>data-v-question-email</code></td>
<td>Email address</td>
</tr>
<tr>
<td><strong>first_name</strong></td>
<td><code>data-v-question-first_name</code></td>
<td>First name</td>
</tr>
<tr>
<td><strong>last_name</strong></td>
<td><code>data-v-question-last_name</code></td>
<td>Last name</td>
</tr>
<tr>
<td><strong>display_name</strong></td>
<td><code>data-v-question-display_name</code></td>
<td>Display name</td>
</tr>
<tr>
<td><strong>author</strong></td>
<td><code>data-v-question-author</code></td>
<td>Full author name</td>
</tr>
<tr>
<td><strong>avatar</strong></td>
<td><code>data-v-question-avatar</code></td>
<td>Avatar filename</td>
</tr>
<tr>
<td><strong>avatar_url</strong></td>
<td><code>data-v-question-avatar_url</code></td>
<td>Full avatar URL</td>
</tr>
<tr>
<td><strong>bio</strong></td>
<td><code>data-v-question-bio</code></td>
<td>Author biography</td>
</tr>
<tr>
<td><strong>subscribe</strong></td>
<td><code>data-v-question-subscribe</code></td>
<td>Whether the user subscribed to replies</td>
</tr>
</tbody>
</table>
<hr />
<h3><strong>Question Metadata</strong></h3>
<table class="table table-bordered">
<thead class="table-light">
<tr>
<th>Property</th>
<th>Attribute</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>question_id</strong></td>
<td><code>data-v-question-question_id</code></td>
<td>Unique question ID</td>
</tr>
<tr>
<td><strong>product_id</strong></td>
<td><code>data-v-question-product_id</code></td>
<td>ID of the product the question belongs to</td>
</tr>
<tr>
<td><strong>user_id</strong></td>
<td><code>data-v-question-user_id</code></td>
<td>ID of the user who producted the question</td>
</tr>
<tr>
<td><strong>url</strong></td>
<td><code>data-v-question-url</code></td>
<td>Author’s website URL</td>
</tr>
<tr>
<td><strong>ip</strong></td>
<td><code>data-v-question-ip</code></td>
<td>IP address (if stored)</td>
</tr>
<tr>
<td><strong>status</strong></td>
<td><code>data-v-question-status</code></td>
<td>Question status</td>
</tr>
<tr>
<td><strong>votes</strong></td>
<td><code>data-v-question-votes</code></td>
<td>Upvotes/downvotes</td>
</tr>
<tr>
<td><strong>type</strong></td>
<td><code>data-v-question-type</code></td>
<td>Question type (empty for standard questions)</td>
</tr>
<tr>
<td><strong>parent_id</strong></td>
<td><code>data-v-question-parent_id</code></td>
<td>Parent question ID (for threaded replies)</td>
</tr>
<tr>
<td><strong>level</strong></td>
<td><code>data-v-question-level</code></td>
<td>Nesting level for threaded questions</td>
</tr>
<tr>
<td><strong>created_at</strong></td>
<td><code>data-v-question-created_at</code></td>
<td>Creation timestamp</td>
</tr>
<tr>
<td><strong>updated_at</strong></td>
<td><code>data-v-question-updated_at</code></td>
<td>Last update timestamp</td>
</tr>
</tbody>
</table>
<hr />
<h3><strong>Question Content</strong></h3>
<table class="table table-bordered">
<thead class="table-light">
<tr>
<th>Property</th>
<th>Attribute</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>content</strong></td>
<td><code>data-v-question-content</code></td>
<td>Question text</td>
</tr>
</tbody>
</table>
<hr />
<h3><strong>Product Information (Optional)</strong></h3>
<p>Displayed only when <code>data-v-product_title=&quot;true&quot;</code> is used.</p>
<table class="table table-bordered">
<thead class="table-light">
<tr>
<th>Property</th>
<th>Attribute</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>name</strong></td>
<td><code>data-v-question-name</code></td>
<td>Title of the product the question belongs to</td>
</tr>
<tr>
<td><strong>slug</strong></td>
<td><code>data-v-question-slug</code></td>
<td>Slug of the product the question belongs to</td>
</tr>
</tbody>
</table>
<hr />
<h2><strong>HTML Example</strong></h2>
<pre><code class="language-html">&lt;div data-v-component-questions data-v-product_id=&quot;url&quot;&gt;

  &lt;ol data-v-if=&quot;count &gt; 0&quot;&gt;

    &lt;li data-v-question&gt;

      &lt;div class=&quot;question-wrap&quot;&gt;

        &lt;figure class=&quot;author-avatar me-2&quot;&gt;
          &lt;img data-v-question-avatar_url
               alt=&quot;user&quot;
               width=&quot;60&quot;
               loading=&quot;lazy&quot;
               src=&quot;/media/vvveb.svg&quot; 
               data-v-if=&quot;question.avatar_url&quot;&gt;
        &lt;/figure&gt;

        &lt;div class=&quot;question-author&quot;&gt;

          &lt;a rel=&quot;external nofollow ugc&quot; data-v-if=&quot;question.url&quot;&gt;
            &lt;span data-v-question-author&gt;John Doe&lt;&lt;/span&gt;
          &lt;/a&gt;


	&lt;div class=&quot;question-meta&quot;&gt;
	  &lt;span data-v-question-created_at data-filter-friendly_date&gt;3 year ago&lt;/span&gt;
	&lt;/div&gt;


        &lt;/div&gt;

      &lt;/div&gt;

		&lt;div data-v-question-content&gt;This is an approved question.&lt;/div&gt;

      &lt;div class=&quot;reply&quot;&gt;
        &lt;a href=&quot;#question-form&quot;
           class=&quot;reply-btn&quot;
           data-v-vvveb-action=&quot;replyTo&quot;
           data-question_id
           data-question_author&gt;
          Reply &lt;i class=&quot;la la-reply&quot;&gt;&lt;/i&gt;
        &lt;/a&gt;
      &lt;/div&gt;

    &lt;/li&gt;

  &lt;/ol&gt;

  &lt;div class=&quot;alert&quot; role=&quot;alert&quot; data-v-if-not=&quot;this.default_question_status&quot;&gt;
    &lt;div data-v-notification-text&gt;
      &lt;span&gt;Questions are closed&lt;/span&gt;
    &lt;/div&gt;
  &lt;/div&gt;

</code></pre>
<p>This example displays a threaded question list with avatars, author names, timestamps, content, and reply actions.</p>
<h1><strong>Product Question Form Component</strong></h1>
<p>The <strong>Question Form</strong> handles question submission on product pages.
It works together with the <strong>Product Questions Component</strong>, allowing users to submit new questions or reply to existing ones. The form supports logged‑in users, anonymous questioners (if enabled), CSRF protection, and threaded replies.</p>
<p>To use javascript to product the question without refreshing the page add the following attributes:</p>
<pre><code>data-v-vvveb-action=&quot;addQuestion&quot;
data-v-vvveb-on=&quot;submit&quot;
</code></pre>
<p>These attributes instruct Vvveb CMS to process the form submission through javascript fetch.</p>
<hr />
<h2><strong>How the Question Form Works</strong></h2>
<p>The form products data to the current page and is intercepted by the Vvveb action handler. The handler:</p>
<ul>
<li>Validates required fields</li>
<li>Checks CSRF token</li>
<li>Determines whether the user is logged in or anonymous</li>
<li>Creates a new question in the database</li>
<li>Assigns the question to the correct product</li>
<li>Handles threaded replies via <code>parent_id</code></li>
<li>Returns success or error notifications</li>
</ul>
<p>If questions require moderation, the submitted question may be stored with <code>status = 0</code> (pending).</p>
<hr />
<h2><strong>Form Fields</strong></h2>
<p>The form includes a mix of visible and hidden fields. These fields are required for proper question submission.</p>
<h3><strong>Hidden System Fields</strong></h3>
<table class="table table-bordered">
<thead class="table-light">
<tr>
<th>Field</th>
<th>Purpose</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>product_id</strong></td>
<td>Identifies the product the question belongs to</td>
</tr>
<tr>
<td><strong>slug</strong></td>
<td>Product slug (used for redirects and validation)</td>
</tr>
<tr>
<td><strong>parent_id</strong></td>
<td>ID of the parent question (0 for top‑level questions)</td>
</tr>
<tr>
<td><strong>csrf</strong></td>
<td>CSRF token for security</td>
</tr>
<tr>
<td><strong>firstname-empty</strong></td>
<td>Honeypot anti‑spam field (must remain empty)</td>
</tr>
<tr>
<td><strong>subject-empty</strong></td>
<td>Honeypot anti‑spam field</td>
</tr>
<tr>
<td><strong>lastname-empty</strong></td>
<td>Honeypot anti‑spam field</td>
</tr>
</tbody>
</table>
<p>Honeypot fields help block bots without affecting real users.</p>
<hr />
<h3><strong>User Identity Fields</strong></h3>
<p>These fields are shown only when the user is <strong>not logged in</strong>:</p>
<table class="table table-bordered">
<thead class="table-light">
<tr>
<th>Field</th>
<th>Required</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>author</strong></td>
<td>Yes</td>
<td>Name of the questioner</td>
</tr>
<tr>
<td><strong>email</strong></td>
<td>Yes</td>
<td>Email address of the questioner</td>
</tr>
</tbody>
</table>
<p>When the user is logged in, these fields are omitted because the system already knows the user’s identity.</p>
<hr />
<h3><strong>Question Content</strong></h3>
<table class="table table-bordered">
<thead class="table-light">
<tr>
<th>Field</th>
<th>Required</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>content</strong></td>
<td>Yes</td>
<td>The actual question text</td>
</tr>
</tbody>
</table>
<p>This is the main body of the question.</p>
<hr />
<h2><strong>Replying to Questions</strong></h2>
<p>The reply links in the question list use:</p>
<pre><code>data-v-vvveb-action=&quot;replyTo&quot;
</code></pre>
<p>This action:</p>
<ul>
<li>Scrolls to the question form</li>
<li>Sets the hidden <code>parent_id</code> field to the ID of the question being replied to</li>
<li>Optionally displays the name of the user being replied to</li>
</ul>
<p>This enables threaded/nested questions.</p>
<hr />
<h2><strong>Anonymous Questions</strong></h2>
<p>The form includes:</p>
<pre><code>data-v-if=&quot;this.global.user_id || this.anonymous_questions&quot;
</code></pre>
<p>This means:</p>
<ul>
<li>If the user is logged in → show the form</li>
<li>If the user is not logged in but anonymous questions are allowed → show the form</li>
<li>Otherwise → hide the form</li>
</ul>
<p>Anonymous question settings are controlled in the site configuration.</p>
<hr />
<h2><strong>Validation &amp; Security</strong></h2>
<p>The question form includes several built‑in protections:</p>
<ul>
<li><strong>CSRF token</strong> (<code>data-v-csrf</code>)</li>
<li><strong>Honeypot fields</strong> (<code>firstname-empty</code>, <code>subject-empty</code>, <code>lastname-empty</code>)</li>
<li><strong>Server‑side validation</strong> of required fields</li>
<li><strong>Status filtering</strong> (pending, spam, trash)</li>
</ul>
<p>If moderation is enabled, questions may not appear immediately.</p>
<hr />
<h2><strong>HTML Example</strong></h2>
<pre><code class="language-html">&lt;form id=&quot;question-form&quot;
      method=&quot;post&quot;
      action=&quot;&quot;
      data-v-vvveb-action=&quot;addQuestion&quot;
      data-v-vvveb-on=&quot;submit&quot;
      data-v-if=&quot;this.global.user_id || this.anonymous_questions&quot;&gt;

  &lt;input type=&quot;hidden&quot; name=&quot;product_id&quot; data-v-product-product_id&gt;
  &lt;input type=&quot;hidden&quot; name=&quot;slug&quot; data-v-product-slug&gt;
  &lt;input type=&quot;hidden&quot; name=&quot;parent_id&quot; value=&quot;0&quot;&gt;
  &lt;input type=&quot;hidden&quot; name=&quot;csrf&quot; data-v-csrf&gt;

  &lt;!-- Honeypot anti-spam fields --&gt;
  &lt;input type=&quot;hidden&quot; name=&quot;firstname-empty&quot;&gt;
  &lt;input type=&quot;text&quot; class=&quot;visually-hidden&quot; name=&quot;lastname-empty&quot; tabindex=&quot;-1&quot;&gt;
  &lt;input type=&quot;text&quot; class=&quot;d-none&quot; name=&quot;subject-empty&quot;&gt;

  &lt;!-- Anonymous user fields --&gt;
  &lt;div data-v-if-not=&quot;this.global.user_id&quot;&gt;
    &lt;div class=&quot;mb-3&quot;&gt;
      &lt;label class=&quot;form-label&quot;&gt;Name&lt;/label&gt;
      &lt;input type=&quot;text&quot; name=&quot;author&quot; class=&quot;form-control&quot; required&gt;
    &lt;/div&gt;

    &lt;div class=&quot;mb-3&quot;&gt;
      &lt;label class=&quot;form-label&quot;&gt;Email address&lt;/label&gt;
      &lt;input type=&quot;email&quot; name=&quot;email&quot; class=&quot;form-control&quot; required&gt;
    &lt;/div&gt;
  &lt;/div&gt;

  &lt;!-- Question text --&gt;
  &lt;div class=&quot;mb-3&quot;&gt;
    &lt;textarea name=&quot;content&quot; rows=&quot;5&quot; class=&quot;form-control&quot; placeholder=&quot;Question&quot; required&gt;&lt;/textarea&gt;
  &lt;/div&gt;

  &lt;!-- Submit --&gt;
  &lt;button type=&quot;submit&quot; class=&quot;btn btn-primary btn-submit&quot;&gt;
    &lt;span class=&quot;loading d-none&quot;&gt;
      &lt;span class=&quot;spinner-border spinner-border-sm&quot;&gt;&lt;/span&gt;
      &lt;span&gt;Producting...&lt;/span&gt;
    &lt;/span&gt;

    &lt;span class=&quot;button-text&quot;&gt;
      &lt;span&gt;Product question&lt;/span&gt;
      &lt;i class=&quot;la la-long-arrow-alt-right ms-1&quot;&gt;&lt;/i&gt;
    &lt;/span&gt;
  &lt;/button&gt;

&lt;/form&gt;
</code></pre>
<p>This example includes all required fields, anonymous user handling, CSRF protection, and reply support.</p>
]]></content:encoded>		
		</item><item data-v-post="post" data-v-id="417">
			<title data-v-post-name="name">Component product categories</title>
			<link data-v-post-full-url="name">https://dev.vvveb.com/component-product-categories</link>
			<dc:creator data-v-post-display_name="name"><![CDATA[Admin]]></dc:creator>
			<pubDate data-v-post-pubDate="date">Mon, 28 Sep 2026 17:24:01 +0000</pubDate>
			<category data-v-post-category-name="category" data-filter-cdata=""><![CDATA[Category]]></category>
			<description data-v-post-excerpt="excerpt" data-filter-cdata=""><![CDATA[The product-categories component (Vvveb\Component\Product\Categories) lists categories-type taxonomy items for the product post type (shop category trees, filters, sidebars). 
Activate the component by adding: 
data-v-component-product-categories  
]]></description>
			<content:encoded data-v-post-content="content" data-filter-cdata=""><![CDATA[
<p>The <strong>product-categories</strong> component (<code>Vvveb\Component\Product\Categories</code>) lists <code>categories</code>-type taxonomy items for the <code>product</code> post type (shop category trees, filters, sidebars).</p>
<p>Activate the component by adding:</p>
<pre><code>data-v-component-product-categories
</code></pre>
<h2>Component Options</h2>
<p>Add options as HTML attributes using the <code>data-v-*</code> syntax.</p>
<table class="table table-bordered">
<thead class="table-light">
<tr>
<th>Option</th>
<th>Attribute</th>
<th>Description</th>
<th>Default</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>start</strong></td>
<td><code>data-v-start</code></td>
<td>Starting offset</td>
<td><code>0</code></td>
</tr>
<tr>
<td><strong>limit</strong></td>
<td><code>data-v-limit</code></td>
<td>Number of categories</td>
<td><code>7</code></td>
</tr>
<tr>
<td><strong>site_id</strong></td>
<td><code>data-v-site_id</code></td>
<td>Site ID</td>
<td>Current site</td>
</tr>
<tr>
<td><strong>language_id</strong></td>
<td><code>data-v-language_id</code></td>
<td>Language ID</td>
<td>Current language</td>
</tr>
<tr>
<td><strong>order</strong></td>
<td><code>data-v-order</code></td>
<td>Sorting rules, overridable from URL</td>
<td><code>url</code> fallback <code>price asc</code></td>
</tr>
<tr>
<td><strong>taxonomy_id</strong></td>
<td><code>data-v-taxonomy_id</code></td>
<td>Filter by taxonomy ID</td>
<td><code>null</code></td>
</tr>
<tr>
<td><strong>product_id</strong></td>
<td><code>data-v-product_id</code></td>
<td>Show only categories assigned to a product</td>
<td><code>null</code></td>
</tr>
<tr>
<td><strong>parent_id</strong></td>
<td><code>data-v-parent_id</code></td>
<td>Filter by parent (<code>0</code> = top-level)</td>
<td><code>null</code></td>
</tr>
<tr>
<td><strong>search</strong></td>
<td><code>data-v-search</code></td>
<td>Filter by text</td>
<td><code>null</code></td>
</tr>
<tr>
<td><strong>type</strong></td>
<td><code>data-v-type</code></td>
<td>Taxonomy type</td>
<td><code>categories</code></td>
</tr>
<tr>
<td><strong>post_type</strong></td>
<td><code>data-v-post_type</code></td>
<td>Post type</td>
<td><code>product</code></td>
</tr>
<tr>
<td><strong>parents_only</strong></td>
<td><code>data-v-parents_only</code></td>
<td>Only parent categories</td>
<td><code>false</code></td>
</tr>
<tr>
<td><strong>parents_children_only</strong></td>
<td><code>data-v-parents_children_only</code></td>
<td>Only parents that have children</td>
<td><code>false</code></td>
</tr>
<tr>
<td><strong>parents_without_children</strong></td>
<td><code>data-v-parents_without_children</code></td>
<td>Only parents without children</td>
<td><code>false</code></td>
</tr>
</tbody>
</table>
<h2>Component Properties</h2>
<p>Same shape as the <strong>categories</strong> component: <code>data-v-cats</code> container with <code>data-v-cat</code> repeatables (<code>name</code>, <code>slug</code>, <code>url</code>, <code>image</code>, <code>count</code>, children).</p>
<h2>HTML Example</h2>
<pre><code class="language-html">&lt;div data-v-component-product-categories data-v-parent_id=&quot;0&quot;&gt;
	&lt;div data-v-cats&gt;
		&lt;div data-v-cat&gt;
			&lt;a href=&quot;&quot; data-v-cat-url&gt;&lt;span data-v-cat-name&gt;&lt;/span&gt;&lt;/a&gt;
		&lt;/div&gt;
	&lt;/div&gt;
&lt;/div&gt;
</code></pre>
]]></content:encoded>		
		</item><item data-v-post="post" data-v-id="418">
			<title data-v-post-name="name">Component pagination</title>
			<link data-v-post-full-url="name">https://dev.vvveb.com/component-pagination</link>
			<dc:creator data-v-post-display_name="name"><![CDATA[Admin]]></dc:creator>
			<pubDate data-v-post-pubDate="date">Mon, 28 Sep 2026 17:24:01 +0000</pubDate>
			<category data-v-post-category-name="category" data-filter-cdata=""><![CDATA[Category]]></category>
			<description data-v-post-excerpt="excerpt" data-filter-cdata=""><![CDATA[Pagination is used by components that return multiple pages of results - such as posts, products, comments, orders, or any list‑based component. It automatically generates page links, previous/next navigation, and first/last page controls. 
To activate]]></description>
			<content:encoded data-v-post-content="content" data-filter-cdata=""><![CDATA[
<p>Pagination is used by components that return multiple pages of results - such as <strong>posts</strong>, <strong>products</strong>, <strong>comments</strong>, <strong>orders</strong>, or any list‑based component. It automatically generates page links, previous/next navigation, and first/last page controls.</p>
<p>To activate pagination, wrap your pagination markup in an element containing:</p>
<pre><code>data-pagination
</code></pre>
<p>This container will automatically receive pagination data from the parent component.</p>
<hr />
<h2><strong>Connecting Pagination to a Component</strong></h2>
<p>Pagination must know <em>which component</em> it belongs to. This is done using the following attributes:</p>
<table class="table table-bordered">
<thead class="table-light">
<tr>
<th>Attribute</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>data-v-parent-component</strong></td>
<td>Component name (<code>products</code>, <code>posts</code>, <code>comments</code>, etc.)</td>
</tr>
<tr>
<td><strong>data-v-parent-index</strong></td>
<td>Index of the component on the page (useful when multiple components of the same type exist)</td>
</tr>
<tr>
<td><strong>data-v-query-string</strong></td>
<td>Whether to preserve existing URL parameters (<code>true</code>/<code>false</code>)</td>
</tr>
<tr>
<td><strong>data-v-parameters</strong></td>
<td>Extra parameters to include in pagination URLs (JSON object) `</td>
</tr>
</tbody>
</table>
<h3>Example</h3>
<pre><code class="language-html">&lt;div data-pagination
     data-v-parent-component=&quot;products&quot;
     data-v-parent-index=&quot;0&quot;
     data-v-query-string=&quot;true&quot;
     data-v-parameters='{&quot;type&quot;:&quot;post&quot;,&quot;myparam&quot;:&quot;value&quot;}'&gt;
</code></pre>
<p>This ensures the pagination controls update the correct component and preserve or extend URL parameters.</p>
<hr />
<h2><strong>Pagination Data Attributes</strong></h2>
<p>The pagination container receives several dynamic values:</p>
<table class="table table-bordered">
<thead class="table-light">
<tr>
<th>Attribute</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>data-count</strong></td>
<td>Total number of pages</td>
</tr>
<tr>
<td><strong>data-current-page</strong></td>
<td>Current page number</td>
</tr>
<tr>
<td><strong>data-current-url</strong></td>
<td>Current page URL</td>
</tr>
</tbody>
</table>
<p>These values can be used to show page indicators, disable buttons, or highlight the active page.</p>
<hr />
<h2><strong>Page Elements</strong></h2>
<p>Pagination uses several repeatable or conditional elements to build navigation.</p>
<h3><strong>Page List Items</strong></h3>
<p>Each page number is wrapped in:</p>
<pre><code>data-page
</code></pre>
<p>Inside it, you can use:</p>
<ul>
<li><strong>data-page-url</strong>  -  URL for that page</li>
<li><strong>data-page-no</strong>  -  Page number</li>
</ul>
<p>Example:</p>
<pre><code class="language-html">&lt;li data-page&gt;
  &lt;a data-page-url data-page-no href=&quot;/shop/2&quot;&gt;2&lt;/a&gt;
&lt;/li&gt;
</code></pre>
<hr />
<h3><strong>First Page</strong></h3>
<p>Rendered only when the current page is not the first.</p>
<pre><code class="language-html">&lt;li data-first&gt;
  &lt;a data-page-url&gt;First&lt;/a&gt;
&lt;/li&gt;
</code></pre>
<hr />
<h3><strong>Previous Page</strong></h3>
<p>Rendered only when <code>current_page &gt; 1</code>.</p>
<pre><code class="language-html">&lt;li data-prev&gt;
  &lt;a data-page-url title=&quot;Previous page&quot;&gt;
    &lt;span class=&quot;la la-arrow-left&quot;&gt;&lt;/span&gt;
  &lt;/a&gt;
&lt;/li&gt;
</code></pre>
<hr />
<h3><strong>Next Page</strong></h3>
<p>Rendered only when <code>current_page &lt; pagecount</code>.</p>
<pre><code class="language-html">&lt;li data-next&gt;
  &lt;a data-page-url title=&quot;Next page&quot;&gt;
    &lt;span class=&quot;la la-arrow-right&quot;&gt;&lt;/span&gt;
  &lt;/a&gt;
&lt;/li&gt;
</code></pre>
<hr />
<h3><strong>Last Page</strong></h3>
<p>Rendered only when the current page is not the last.</p>
<pre><code class="language-html">&lt;li data-last&gt;
  &lt;a data-page-url&gt;Last&lt;/a&gt;
&lt;/li&gt;
</code></pre>
<hr />
<h2><strong>Full Pagination Example</strong></h2>
<pre><code class="language-html">&lt;div data-pagination
     data-v-parent-component=&quot;products&quot;
     data-v-parent-index=&quot;0&quot;
     data-v-query-string=&quot;true&quot;
     data-v-parameters=&quot;{}&quot;&gt;

  &lt;nav data-v-if=&quot;pagecount &gt; 1&quot;&gt;
    &lt;ul&gt;

      &lt;!-- Previous --&gt;
      &lt;li class=&quot;page-item pager-prev&quot; data-prev&gt;
        &lt;a data-page-url data-v-if=&quot;current_page &gt; 1&quot; title=&quot;Previous page&quot;&gt;
          &lt;span class=&quot;la la-arrow-left la-lg&quot;&gt;&lt;/span&gt;
        &lt;/a&gt;
      &lt;/li&gt;

      &lt;!-- Page Numbers --&gt;
      &lt;li class=&quot;active&quot; data-page&gt;
        &lt;a data-page-url data-page-no href=&quot;/shop&quot;&gt;1&lt;/a&gt;
      &lt;/li&gt;

      &lt;li data-page&gt;
        &lt;a data-page-url data-page-no href=&quot;/shop/2&quot;&gt;2&lt;/a&gt;
      &lt;/li&gt;

      &lt;li data-page&gt;
        &lt;a data-page-url data-page-no href=&quot;/shop/3&quot;&gt;3&lt;/a&gt;
      &lt;/li&gt;

      &lt;li data-page&gt;
        &lt;a data-page-url data-page-no href=&quot;/shop/4&quot;&gt;4&lt;/a&gt;
      &lt;/li&gt;

      &lt;!-- Next --&gt;
      &lt;li data-next&gt;
        &lt;a data-page-url data-v-if=&quot;current_page &lt; pagecount&quot; title=&quot;Next page&quot;&gt;
          &lt;span class=&quot;la la-arrow-right la-lg&quot;&gt;&lt;/span&gt;
        &lt;/a&gt;
      &lt;/li&gt;

    &lt;/ul&gt;
  &lt;/nav&gt;

&lt;/div&gt;
</code></pre>
<p>This example shows a typical pagination block with previous/next arrows and numbered pages.</p>
<hr />
<h2><strong>How Pagination Works Internally</strong></h2>
<ul>
<li>The parent component (posts, products, comments, etc.) calculates:
<ul>
<li>total items</li>
<li>items per page</li>
<li>total pages</li>
<li>current page</li>
</ul>
</li>
<li>Pagination receives these values and generates:
<ul>
<li>page links</li>
<li>first/prev/next/last controls</li>
<li>URLs with preserved or extended query parameters</li>
</ul>
</li>
<li>When a user clicks a page link, the component reloads with the new page number.</li>
</ul>
]]></content:encoded>		
		</item><item data-v-post="post" data-v-id="419">
			<title data-v-post-name="name">Component product vendors</title>
			<link data-v-post-full-url="name">https://dev.vvveb.com/component-product-vendors</link>
			<dc:creator data-v-post-display_name="name"><![CDATA[Admin]]></dc:creator>
			<pubDate data-v-post-pubDate="date">Mon, 28 Sep 2026 17:24:01 +0000</pubDate>
			<category data-v-post-category-name="category" data-filter-cdata=""><![CDATA[Category]]></category>
			<description data-v-post-excerpt="excerpt" data-filter-cdata=""><![CDATA[The product-vendors component (Vvveb\Component\Product\Vendors) lists product vendors (sellers in a marketplace setup) with pagination and category filtering. 
Activate the component by adding: 
data-v-component-product-vendors  
Component Options 
Add]]></description>
			<content:encoded data-v-post-content="content" data-filter-cdata=""><![CDATA[
<p>The <strong>product-vendors</strong> component (<code>Vvveb\Component\Product\Vendors</code>) lists product vendors (sellers in a marketplace setup) with pagination and category filtering.</p>
<p>Activate the component by adding:</p>
<pre><code>data-v-component-product-vendors
</code></pre>
<h2>Component Options</h2>
<p>Add options as HTML attributes using the <code>data-v-*</code> syntax.</p>
<table class="table table-bordered">
<thead class="table-light">
<tr>
<th>Option</th>
<th>Attribute</th>
<th>Description</th>
<th>Default</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>start</strong></td>
<td><code>data-v-start</code></td>
<td>Starting offset</td>
<td><code>0</code></td>
</tr>
<tr>
<td><strong>limit</strong></td>
<td><code>data-v-limit</code></td>
<td>Number of vendors</td>
<td><code>7</code></td>
</tr>
<tr>
<td><strong>page</strong></td>
<td><code>data-v-page</code></td>
<td>Page number</td>
<td><code>1</code></td>
</tr>
<tr>
<td><strong>count</strong></td>
<td><code>data-v-count</code></td>
<td>Include product counts, overridable from URL</td>
<td><code>4</code></td>
</tr>
<tr>
<td><strong>vendor_id</strong></td>
<td><code>data-v-vendor_id</code></td>
<td>Filter by vendor ID</td>
<td><code>null</code></td>
</tr>
<tr>
<td><strong>category_id</strong></td>
<td><code>data-v-category_id</code></td>
<td>Filter by category ID</td>
<td><code>null</code></td>
</tr>
<tr>
<td><strong>order</strong></td>
<td><code>data-v-order</code></td>
<td>Sorting rules, overridable from URL</td>
<td><code>url</code> fallback <code>price asc</code></td>
</tr>
<tr>
<td><strong>type</strong></td>
<td><code>data-v-type</code></td>
<td>Record type filter</td>
<td><code>tags</code></td>
</tr>
<tr>
<td><strong>filter</strong></td>
<td><code>data-v-filter</code></td>
<td>Extra filter value</td>
<td><code>false</code></td>
</tr>
<tr>
<td><strong>parents_only</strong></td>
<td><code>data-v-parents_only</code></td>
<td>Only parents</td>
<td><code>false</code></td>
</tr>
<tr>
<td><strong>parents_children_only</strong></td>
<td><code>data-v-parents_children_only</code></td>
<td>Only parents with children</td>
<td><code>false</code></td>
</tr>
<tr>
<td><strong>parents_without_children</strong></td>
<td><code>data-v-parents_without_children</code></td>
<td>Only parents without children</td>
<td><code>false</code></td>
</tr>
</tbody>
</table>
<h2>Component Properties</h2>
<p>Each vendor renders inside a <code>data-v-vendor</code> repeatable with <code>name</code>, <code>slug</code>, <code>image</code> and <code>url</code> fields.</p>
<h2>HTML Example</h2>
<pre><code class="language-html">&lt;div data-v-component-product-vendors data-v-limit=&quot;12&quot;&gt;
	&lt;div data-v-vendor&gt;
		&lt;a href=&quot;&quot; data-v-vendor-url&gt;&lt;span data-v-vendor-name&gt;&lt;/span&gt;&lt;/a&gt;
	&lt;/div&gt;
&lt;/div&gt;
</code></pre>
]]></content:encoded>		
		</item><item data-v-post="post" data-v-id="420">
			<title data-v-post-name="name">Component Checkout Payment</title>
			<link data-v-post-full-url="name">https://dev.vvveb.com/component-checkout-payment</link>
			<dc:creator data-v-post-display_name="name"><![CDATA[Admin]]></dc:creator>
			<pubDate data-v-post-pubDate="date">Mon, 28 Sep 2026 17:24:01 +0000</pubDate>
			<category data-v-post-category-name="category" data-filter-cdata=""><![CDATA[Category]]></category>
			<description data-v-post-excerpt="excerpt" data-filter-cdata=""><![CDATA[Retrieves the available payment methods for the checkout process. When the checkout option is enabled, it reads checkout session data to provide region-aware payment methods. Returns each payment method with its details and a total count of available]]></description>
			<content:encoded data-v-post-content="content" data-filter-cdata=""><![CDATA[
<p>Retrieves the available payment methods for the checkout process. When the <code>checkout</code> option is enabled, it reads checkout session data to provide region-aware payment methods. Returns each payment method with its details and a total count of available methods.</p>
<h2>Component Options</h2>
<table class="table table-bordered">
<thead class="table-light">
<tr>
<th>Option</th>
<th>Type</th>
<th>Default</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>checkout</code></td>
<td><code>bool|null</code></td>
<td><code>null</code></td>
<td>When truthy, loads checkout session data to provide context-aware payment methods (e.g. region-specific pricing).</td>
</tr>
</tbody>
</table>
<h2>Component Properties</h2>
<table class="table table-bordered">
<thead class="table-light">
<tr>
<th>Property</th>
<th>Type</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>payment</code></td>
<td><code>array</code></td>
<td>Array of available payment methods, keyed by method name.</td>
</tr>
<tr>
<td><code>payment.{name}.name</code></td>
<td><code>string</code></td>
<td>Internal payment method identifier.</td>
</tr>
<tr>
<td><code>payment.{name}.title</code></td>
<td><code>string</code></td>
<td>Display title for the payment method.</td>
</tr>
<tr>
<td><code>payment.{name}.description</code></td>
<td><code>string</code></td>
<td>Description of the payment method.</td>
</tr>
<tr>
<td><code>payment.{name}.cost</code></td>
<td><code>float</code></td>
<td>Additional cost for using this payment method.</td>
</tr>
<tr>
<td><code>payment.{name}.tax</code></td>
<td><code>int</code></td>
<td>Tax flag for this payment method.</td>
</tr>
<tr>
<td><code>payment.{name}.region_id</code></td>
<td><code>int</code></td>
<td>Region ID this payment method applies to.</td>
</tr>
<tr>
<td><code>payment.{name}.render</code></td>
<td><code>string</code></td>
<td>HTML snippet to render the payment form (e.g. Stripe element markup).</td>
</tr>
<tr>
<td><code>count</code></td>
<td><code>int</code></td>
<td>Total number of available payment methods.</td>
</tr>
</tbody>
</table>
<h2>HTML Example</h2>
<pre><code class="language-html">&lt;div data-v-component-checkout-payment&gt;
  &lt;div data-v-payment&gt;
    &lt;input type=&quot;radio&quot; name=&quot;payment&quot; data-v-payment-name value=&quot;&quot; /&gt;
    &lt;label data-v-payment-title&gt;&lt;/label&gt;
    &lt;p data-v-payment-description&gt;&lt;/p&gt;
    &lt;span data-v-payment-cost&gt;&lt;/span&gt;
  &lt;/div&gt;
&lt;/div&gt;
</code></pre>
<h2>Template Example</h2>
<pre><code class="language-html">&lt;div data-v-component-checkout-payment&gt;
  &lt;template data-v-template=&quot;payment-method&quot;&gt;
    &lt;div class=&quot;payment-option&quot;&gt;
      &lt;input type=&quot;radio&quot; name=&quot;payment_method&quot; data-v-payment-name /&gt;
      &lt;label data-v-payment-title&gt;&lt;/label&gt;
      &lt;p data-v-payment-description&gt;&lt;/p&gt;
      &lt;span data-v-if=&quot;payment.cost&quot;&gt;Fee: &lt;span data-v-payment-cost&gt;&lt;/span&gt;&lt;/span&gt;
      &lt;div data-v-payment-render&gt;&lt;/div&gt;
    &lt;/div&gt;
  &lt;/template&gt;
&lt;/div&gt;
</code></pre>
]]></content:encoded>		
		</item><item data-v-post="post" data-v-id="421">
			<title data-v-post-name="name">Component categories pages</title>
			<link data-v-post-full-url="name">https://dev.vvveb.com/component-categories-pages</link>
			<dc:creator data-v-post-display_name="name"><![CDATA[Admin]]></dc:creator>
			<pubDate data-v-post-pubDate="date">Mon, 28 Sep 2026 17:24:01 +0000</pubDate>
			<category data-v-post-category-name="category" data-filter-cdata=""><![CDATA[Category]]></category>
			<description data-v-post-excerpt="excerpt" data-filter-cdata=""><![CDATA[The categories-pages component (Vvveb\Component\Categories\Pages) lists taxonomy categories together with their posts (pages). It behaves like the categories component but additionally loads a page of posts per category, which makes it suitable for]]></description>
			<content:encoded data-v-post-content="content" data-filter-cdata=""><![CDATA[
<p>The <strong>categories-pages</strong> component (<code>Vvveb\Component\Categories\Pages</code>) lists taxonomy categories together with their posts (pages). It behaves like the <strong>categories</strong> component but additionally loads a page of posts per category, which makes it suitable for sitemap-style or magazine index pages.</p>
<p>Activate the component by adding:</p>
<pre><code>data-v-component-categories-pages
</code></pre>
<h2>Component Options</h2>
<p>Add options as HTML attributes using the <code>data-v-*</code> syntax.</p>
<table class="table table-bordered">
<thead class="table-light">
<tr>
<th>Option</th>
<th>Attribute</th>
<th>Description</th>
<th>Default</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>start</strong></td>
<td><code>data-v-start</code></td>
<td>Starting category offset</td>
<td><code>0</code></td>
</tr>
<tr>
<td><strong>limit</strong></td>
<td><code>data-v-limit</code></td>
<td>Number of categories to display</td>
<td><code>7</code></td>
</tr>
<tr>
<td><strong>posts_start</strong></td>
<td><code>data-v-posts_start</code></td>
<td>Starting post offset inside each category</td>
<td><code>0</code></td>
</tr>
<tr>
<td><strong>posts_limit</strong></td>
<td><code>data-v-posts_limit</code></td>
<td>Number of posts per category</td>
<td><code>null</code></td>
</tr>
<tr>
<td><strong>type</strong></td>
<td><code>data-v-type</code></td>
<td>Taxonomy type</td>
<td><code>categories</code></td>
</tr>
<tr>
<td><strong>post_type</strong></td>
<td><code>data-v-post_type</code></td>
<td>Post type loaded per category</td>
<td><code>post</code></td>
</tr>
<tr>
<td><strong>site_id</strong></td>
<td><code>data-v-site_id</code></td>
<td>Site ID</td>
<td>Current site</td>
</tr>
<tr>
<td><strong>order</strong></td>
<td><code>data-v-order</code></td>
<td>Sorting rules, overridable from URL</td>
<td><code>url</code> fallback <code>price asc</code></td>
</tr>
<tr>
<td><strong>taxonomy_item_id</strong></td>
<td><code>data-v-taxonomy_item_id</code></td>
<td>Load a single category branch</td>
<td><code>null</code></td>
</tr>
<tr>
<td><strong>page</strong></td>
<td><code>data-v-page</code></td>
<td>Page number for pagination</td>
<td><code>1</code></td>
</tr>
<tr>
<td><strong>parent_id</strong></td>
<td><code>data-v-parent_id</code></td>
<td>Filter by parent category (<code>0</code> = top-level)</td>
<td><code>null</code></td>
</tr>
<tr>
<td><strong>levels</strong></td>
<td><code>data-v-levels</code></td>
<td>Depth of category tree to load</td>
<td><code>null</code></td>
</tr>
</tbody>
</table>
<h2>Component Properties</h2>
<p>Categories render inside <code>data-v-cats</code> / <code>data-v-cat</code> repeatables (same fields as the <strong>categories</strong> component: <code>name</code>, <code>slug</code>, <code>url</code>, <code>image</code>, <code>count</code>, …). Each category additionally carries its posts list for nested <code>data-v-post</code> rendering.</p>
<h2>HTML Example</h2>
<pre><code class="language-html">&lt;div data-v-component-categories-pages data-v-limit=&quot;7&quot; data-v-posts_limit=&quot;5&quot;&gt;
	&lt;div data-v-cats&gt;
		&lt;div data-v-cat&gt;
			&lt;a href=&quot;&quot; data-v-cat-url&gt;&lt;span data-v-cat-name&gt;&lt;/span&gt;&lt;/a&gt;
		&lt;/div&gt;
	&lt;/div&gt;
&lt;/div&gt;
</code></pre>
]]></content:encoded>		
		</item><item data-v-post="post" data-v-id="422">
			<title data-v-post-name="name">Component user wishlist</title>
			<link data-v-post-full-url="name">https://dev.vvveb.com/component-user-wishlist</link>
			<dc:creator data-v-post-display_name="name"><![CDATA[Admin]]></dc:creator>
			<pubDate data-v-post-pubDate="date">Mon, 28 Sep 2026 17:24:01 +0000</pubDate>
			<category data-v-post-category-name="category" data-filter-cdata=""><![CDATA[Category]]></category>
			<description data-v-post-excerpt="excerpt" data-filter-cdata=""><![CDATA[The user-wishlist component (Vvveb\Component\User\Wishlist) lists raw wishlist records (user_wishlist table) for a user, each enriched with its product data and detail-page URL. 
Activate the component by adding: 
data-v-component-user-wishlist  
]]></description>
			<content:encoded data-v-post-content="content" data-filter-cdata=""><![CDATA[
<p>The <strong>user-wishlist</strong> component (<code>Vvveb\Component\User\Wishlist</code>) lists raw wishlist records (<code>user_wishlist</code> table) for a user, each enriched with its product data and detail-page URL.</p>
<p>Activate the component by adding:</p>
<pre><code>data-v-component-user-wishlist
</code></pre>
<h2>Component Options</h2>
<p>Add options as HTML attributes using the <code>data-v-*</code> syntax.</p>
<table class="table table-bordered">
<thead class="table-light">
<tr>
<th>Option</th>
<th>Attribute</th>
<th>Description</th>
<th>Default</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>user_id</strong></td>
<td><code>data-v-user_id</code></td>
<td>Wishlist owner; defaults to the logged-in user</td>
<td><code>null</code></td>
</tr>
<tr>
<td><strong>start</strong></td>
<td><code>data-v-start</code></td>
<td>Starting offset; recomputed from <code>page</code> when set</td>
<td><code>0</code></td>
</tr>
<tr>
<td><strong>page</strong></td>
<td><code>data-v-page</code></td>
<td>Page number for pagination</td>
<td><code>1</code></td>
</tr>
<tr>
<td><strong>limit</strong></td>
<td><code>data-v-limit</code></td>
<td>Number of records per page</td>
<td><code>10</code></td>
</tr>
<tr>
<td><strong>image_size</strong></td>
<td><code>data-v-image_size</code></td>
<td>Product image size (<code>xlarge</code>, <code>large</code>, <code>medium</code>, <code>thumb</code>)</td>
<td><code>thumb</code></td>
</tr>
</tbody>
</table>
<h2>Component Properties</h2>
<p>Each record is wrapped in an element marked with <code>data-v-product</code> and contains the wishlist fields plus the joined product fields (<code>name</code>, <code>slug</code>, <code>price</code>, <code>image</code>, <code>url</code>, …).</p>
<h2>HTML Example</h2>
<pre><code class="language-html">&lt;div data-v-component-user-wishlist data-v-limit=&quot;10&quot;&gt;
	&lt;div data-v-product&gt;
		&lt;img src=&quot;&quot; data-v-product-image alt=&quot;Product image&quot;&gt;
		&lt;h3 data-v-product-name&gt;&lt;/h3&gt;
		&lt;a href=&quot;&quot; data-v-product-url&gt;View&lt;/a&gt;
	&lt;/div&gt;
&lt;/div&gt;
</code></pre>
]]></content:encoded>		
		</item><item data-v-post="post" data-v-id="423">
			<title data-v-post-name="name">Component content category</title>
			<link data-v-post-full-url="name">https://dev.vvveb.com/component-content-category</link>
			<dc:creator data-v-post-display_name="name"><![CDATA[Admin]]></dc:creator>
			<pubDate data-v-post-pubDate="date">Mon, 28 Sep 2026 17:24:01 +0000</pubDate>
			<category data-v-post-category-name="category" data-filter-cdata=""><![CDATA[Category]]></category>
			<description data-v-post-excerpt="excerpt" data-filter-cdata=""><![CDATA[The content-category component (Vvveb\Component\Content\Category) displays a single blog category. It extends the base category component (Vvveb\Component\Category) without changing options, so it auto-detects the category from the page URL on category]]></description>
			<content:encoded data-v-post-content="content" data-filter-cdata=""><![CDATA[
<p>The <strong>content-category</strong> component (<code>Vvveb\Component\Content\Category</code>) displays a single blog category. It extends the base <strong>category</strong> component (<code>Vvveb\Component\Category</code>) without changing options, so it auto-detects the category from the page URL on category pages.</p>
<p>Activate the component by adding:</p>
<pre><code>data-v-component-content-category
</code></pre>
<h2>Component Options</h2>
<p>Same as the <strong>category</strong> component:</p>
<table class="table table-bordered">
<thead class="table-light">
<tr>
<th>Option</th>
<th>Attribute</th>
<th>Description</th>
<th>Default</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>taxonomy_item_id</strong></td>
<td><code>data-v-taxonomy_item_id</code></td>
<td>Category ID, from URL by default</td>
<td><code>url</code></td>
</tr>
<tr>
<td><strong>slug</strong></td>
<td><code>data-v-slug</code></td>
<td>Category slug, from URL by default</td>
<td><code>url</code></td>
</tr>
<tr>
<td><strong>type</strong></td>
<td><code>data-v-type</code></td>
<td>Taxonomy type</td>
<td><code>null</code></td>
</tr>
<tr>
<td><strong>post_type</strong></td>
<td><code>data-v-post_type</code></td>
<td>Post type</td>
<td><code>null</code></td>
</tr>
<tr>
<td><strong>language_id</strong></td>
<td><code>data-v-language_id</code></td>
<td>Language ID</td>
<td>Current language</td>
</tr>
<tr>
<td><strong>site_id</strong></td>
<td><code>data-v-site_id</code></td>
<td>Site ID</td>
<td>Current site</td>
</tr>
</tbody>
</table>
<h2>Component Properties</h2>
<p>Single-category fields exposed through <code>data-v-category-*</code> attributes: <code>name</code>, <code>slug</code>, <code>content</code>, <code>image</code>, <code>image_url</code>, <code>url</code>, <code>full-url</code>, <code>count</code>, and related taxonomy data.</p>
<h2>HTML Example</h2>
<pre><code class="language-html">&lt;div data-v-component-content-category&gt;
	&lt;h1 data-v-category-name&gt;&lt;/h1&gt;
	&lt;div data-v-category-content&gt;&lt;/div&gt;
&lt;/div&gt;
</code></pre>
]]></content:encoded>		
		</item><item data-v-post="post" data-v-id="424">
			<title data-v-post-name="name">Component Checkout Shipping</title>
			<link data-v-post-full-url="name">https://dev.vvveb.com/component-checkout-shipping</link>
			<dc:creator data-v-post-display_name="name"><![CDATA[Admin]]></dc:creator>
			<pubDate data-v-post-pubDate="date">Mon, 28 Sep 2026 17:24:01 +0000</pubDate>
			<category data-v-post-category-name="category" data-filter-cdata=""><![CDATA[Category]]></category>
			<description data-v-post-excerpt="excerpt" data-filter-cdata=""><![CDATA[Retrieves the available shipping methods for the checkout process. When the checkout option is enabled, it reads checkout session data to provide region-aware shipping methods. Returns each shipping method with its details and a total count of available]]></description>
			<content:encoded data-v-post-content="content" data-filter-cdata=""><![CDATA[
<p>Retrieves the available shipping methods for the checkout process. When the <code>checkout</code> option is enabled, it reads checkout session data to provide region-aware shipping methods. Returns each shipping method with its details and a total count of available methods.</p>
<h2>Component Options</h2>
<table class="table table-bordered">
<thead class="table-light">
<tr>
<th>Option</th>
<th>Type</th>
<th>Default</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>checkout</code></td>
<td><code>bool|null</code></td>
<td><code>null</code></td>
<td>When truthy, loads checkout session data to provide context-aware shipping methods (e.g. region-specific rates, weight-based pricing).</td>
</tr>
</tbody>
</table>
<h2>Component Properties</h2>
<table class="table table-bordered">
<thead class="table-light">
<tr>
<th>Property</th>
<th>Type</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>shipping</code></td>
<td><code>array</code></td>
<td>Array of available shipping methods, keyed by method name.</td>
</tr>
<tr>
<td><code>shipping.{name}.name</code></td>
<td><code>string</code></td>
<td>Internal shipping method identifier.</td>
</tr>
<tr>
<td><code>shipping.{name}.title</code></td>
<td><code>string</code></td>
<td>Display title for the shipping method.</td>
</tr>
<tr>
<td><code>shipping.{name}.description</code></td>
<td><code>string</code></td>
<td>Description of the shipping method.</td>
</tr>
<tr>
<td><code>shipping.{name}.cost</code></td>
<td><code>float</code></td>
<td>Base cost for this shipping method.</td>
</tr>
<tr>
<td><code>shipping.{name}.free_shipping</code></td>
<td><code>float</code></td>
<td>Minimum order amount for free shipping.</td>
</tr>
<tr>
<td><code>shipping.{name}.text</code></td>
<td><code>string</code></td>
<td>Display text (e.g. &quot;Free shipping&quot; when conditions are met).</td>
</tr>
<tr>
<td><code>shipping.{name}.region_id</code></td>
<td><code>int</code></td>
<td>Region ID this shipping method applies to.</td>
</tr>
<tr>
<td><code>shipping.{name}.tax_type_id</code></td>
<td><code>int</code></td>
<td>Tax type associated with this shipping method.</td>
</tr>
<tr>
<td><code>shipping.{name}.weight</code></td>
<td><code>array</code></td>
<td>Weight-based pricing tiers with <code>above_weight</code> and <code>price</code> values.</td>
</tr>
<tr>
<td><code>count</code></td>
<td><code>int</code></td>
<td>Total number of available shipping methods.</td>
</tr>
</tbody>
</table>
<h2>HTML Example</h2>
<pre><code class="language-html">&lt;div data-v-component-checkout-shipping&gt;
  &lt;div data-v-shipping&gt;
    &lt;input type=&quot;radio&quot; name=&quot;shipping&quot; data-v-shipping-name value=&quot;&quot; /&gt;
    &lt;label data-v-shipping-title&gt;&lt;/label&gt;
    &lt;p data-v-shipping-description&gt;&lt;/p&gt;
    &lt;span data-v-shipping-cost&gt;&lt;/span&gt;
    &lt;span data-v-shipping-text&gt;&lt;/span&gt;
  &lt;/div&gt;
&lt;/div&gt;
</code></pre>
<h2>Template Example</h2>
<pre><code class="language-html">&lt;div data-v-component-checkout-shipping&gt;
  &lt;template data-v-template=&quot;shipping-method&quot;&gt;
    &lt;div class=&quot;shipping-option&quot;&gt;
      &lt;input type=&quot;radio&quot; name=&quot;shipping_method&quot; data-v-shipping-name /&gt;
      &lt;label data-v-shipping-title&gt;&lt;/label&gt;
      &lt;p data-v-shipping-description&gt;&lt;/p&gt;
      &lt;span data-v-if-not=&quot;shipping.cost&quot;&gt;Free&lt;/span&gt;
      &lt;span data-v-if=&quot;shipping.cost&quot; data-v-shipping-cost&gt;&lt;/span&gt;
      &lt;span data-v-shipping-text&gt;&lt;/span&gt;
    &lt;/div&gt;
  &lt;/template&gt;
&lt;/div&gt;
</code></pre>
]]></content:encoded>		
		</item><item data-v-post="post" data-v-id="425">
			<title data-v-post-name="name">Component compare</title>
			<link data-v-post-full-url="name">https://dev.vvveb.com/component-compare</link>
			<dc:creator data-v-post-display_name="name"><![CDATA[Admin]]></dc:creator>
			<pubDate data-v-post-pubDate="date">Mon, 28 Sep 2026 17:24:01 +0000</pubDate>
			<category data-v-post-category-name="category" data-filter-cdata=""><![CDATA[Category]]></category>
			<description data-v-post-excerpt="excerpt" data-filter-cdata=""><![CDATA[The compare component (Vvveb\Component\Compare) is a placeholder for the product-comparison feature. It declares no options ($defaultOptions is empty) and its results() method only triggers the Vvveb\Component\Compare::results event, returning whatever]]></description>
			<content:encoded data-v-post-content="content" data-filter-cdata=""><![CDATA[
<p>The <strong>compare</strong> component (<code>Vvveb\Component\Compare</code>) is a placeholder for the product-comparison feature. It declares no options (<code>$defaultOptions</code> is empty) and its <code>results()</code> method only triggers the <code>Vvveb\Component\Compare::results</code> event, returning whatever listeners provide.</p>
<p>Activate the component by adding:</p>
<pre><code>data-v-component-compare
</code></pre>
<h2>Component Options</h2>
<p>None. The component accepts no <code>data-v-*</code> options. Comparison data is supplied by event listeners or plugin code.</p>
<h2>Component Properties</h2>
<p>No built-in properties. Any data assigned by event listeners is exposed to the template under the <code>compare</code> component key.</p>
<h2>HTML Example</h2>
<pre><code class="language-html">&lt;div data-v-component-compare&gt;
	&lt;!-- comparison table rendered by event-provided data --&gt;
&lt;/div&gt;
</code></pre>
<h2>Notes</h2>
<ul>
<li>To implement comparison, listen for <code>Vvveb\Component\Compare::results</code> with <code>Event::on('Vvveb\Component\Compare', 'results', $id, $callback)</code> and return <code>[$results]</code>.</li>
<li>Product add-to-compare URLs are available on product data as <code>data-v-product-add_compare_url</code>.</li>
</ul>
]]></content:encoded>		
		</item><item data-v-post="post" data-v-id="426">
			<title data-v-post-name="name">Component admin</title>
			<link data-v-post-full-url="name">https://dev.vvveb.com/component-admins</link>
			<dc:creator data-v-post-display_name="name"><![CDATA[Admin]]></dc:creator>
			<pubDate data-v-post-pubDate="date">Mon, 28 Sep 2026 17:24:01 +0000</pubDate>
			<category data-v-post-category-name="category" data-filter-cdata=""><![CDATA[Category]]></category>
			<description data-v-post-excerpt="excerpt" data-filter-cdata=""><![CDATA[The admins component displays a list of administrator users. It supports pagination, filtering, and searching, making it useful for author listings, team pages, or admin management sections within a theme. 
 
Component Options 
Add any option as an HTML]]></description>
			<content:encoded data-v-post-content="content" data-filter-cdata=""><![CDATA[
<p>The <strong>admins</strong> component displays a list of administrator users.
It supports pagination, filtering, and searching, making it useful for author listings, team pages, or admin management sections within a theme.</p>
<hr />
<h2>Component Options</h2>
<p>Add any option as an HTML attribute using the <code>data-v-*</code> syntax.</p>
<ul>
<li><strong>start</strong> - Starting offset for pagination. Useful when manually controlling the list position. Default: <code>0</code>.</li>
<li><strong>limit</strong> - Number of admin users to display per page, overridable from URL. Default: <code>4</code>.</li>
<li><strong>status</strong> - Filter by account status (<code>1</code> for enabled, <code>0</code> for disabled). Default: <code>1</code>.</li>
<li><strong>search</strong> - Search by username, first name, or last name.</li>
<li><strong>admin_id</strong> - Filter by one or more admin IDs. Accepts a single value or a comma‑separated list.</li>
</ul>
<p>These options allow you to build flexible admin listings, from simple team pages to filtered author directories.</p>
<hr />
<h2>Component Properties</h2>
<p>Each admin user is wrapped in an element marked with <code>data-v-admin</code>.<br />
Inside each element, the following properties are available:</p>
<ul>
<li><strong>first_name</strong> - <code>data-v-admin-first_name</code> - Admin’s first name.</li>
<li><strong>last_name</strong> - <code>data-v-admin-last_name</code> - Admin’s last name.</li>
<li><strong>avatar_url</strong> - <code>data-v-admin-avatar_url</code> - Profile avatar image URL.</li>
<li><strong>cover_url</strong> - <code>data-v-admin-cover_url</code> - Cover or background image URL.</li>
<li><strong>url</strong> - <code>data-v-admin-url</code> - Public profile page URL.</li>
<li><strong>username</strong> - <code>data-v-admin-username</code> - Username used for login.</li>
<li><strong>display_name</strong> - <code>data-v-admin-display_name</code> - Public display name.</li>
<li><strong>website</strong> - <code>data-v-admin-website</code> - Personal or professional website.</li>
<li><strong>email</strong> - <code>data-v-admin-email</code> - Internal email for login and notifications.</li>
<li><strong>public_email</strong> - <code>data-v-admin-public_email</code> - Publicly visible email address.</li>
<li><strong>bio</strong> - <code>data-v-admin-bio</code> - Short biography or description.</li>
<li><strong>created_at</strong> - <code>data-v-admin-created_at</code> - Account creation timestamp.</li>
<li><strong>updated_at</strong> - <code>data-v-admin-updated_at</code> - Last update timestamp.</li>
<li><strong>role_id</strong> - <code>data-v-admin-role_id</code> - Role ID assigned to the admin.</li>
<li><strong>role</strong> - <code>data-v-admin-role</code> - Role name (e.g., Administrator, Editor).</li>
<li><strong>last_ip</strong> - <code>data-v-admin-last_ip</code> - Last login IP address.</li>
<li><strong>phone_number</strong> - <code>data-v-admin-phone_number</code> - Public phone number (if provided).</li>
</ul>
<p>These properties repeat for each admin returned by the component.</p>
<hr />
<h2>HTML Example</h2>
<pre><code class="language-html">&lt;div data-v-component-admins data-v-limit=&quot;4&quot;&gt;

	&lt;div data-v-admin&gt;

		&lt;div class=&quot;profile-header&quot;&gt;
			&lt;div class=&quot;cover&quot;&gt;
				&lt;img src=&quot;&quot; data-v-admin-cover_url&gt;
			&lt;/div&gt;

			&lt;div class=&quot;avatar&quot;&gt;
				&lt;img data-v-admin-avatar_url class=&quot;rounded-circle avatar&quot; alt=&quot;Profile Picture&quot;&gt;
			&lt;/div&gt;
		&lt;/div&gt;

		&lt;div class=&quot;px-3 m-3 me-5 d-flex justify-content-between&quot;&gt;
			&lt;div&gt;
				&lt;h1 class=&quot;h3 m-0&quot; data-v-admin-display_name&gt;User&lt;/h1&gt;
			&lt;/div&gt;

			&lt;div class=&quot;social-links d-inline-block&quot;&gt;
				&lt;!-- Optional social links --&gt;
			&lt;/div&gt;
		&lt;/div&gt;

		&lt;div class=&quot;px-3 m-3 me-5 d-flex&quot;&gt;

			&lt;div&gt;
				&lt;div class=&quot;text-muted&quot;&gt;@&lt;span data-v-admin-username&gt;username&lt;/span&gt;&lt;/div&gt;

				&lt;div&gt;
					&lt;a class=&quot;d-block&quot; data-v-admin-website href=&quot;#&quot;&gt;
						&lt;span data-v-admin-website&gt;example.com&lt;/span&gt;
					&lt;/a&gt;

					&lt;a class=&quot;d-block text-body small&quot; href=&quot;#&quot;&gt;
						&lt;span data-v-admin-public_email&gt;admin@vvveb.com&lt;/span&gt;
					&lt;/a&gt;

					&lt;a class=&quot;d-block text-body small&quot; href=&quot;#&quot;&gt;
						&lt;span data-v-admin-phone_number&gt;123456&lt;/span&gt;
					&lt;/a&gt;
				&lt;/div&gt;
			&lt;/div&gt;

			&lt;div class=&quot;ms-5&quot;&gt;
				&lt;div data-v-admin-bio&gt;My bio&lt;/div&gt;
			&lt;/div&gt;

		&lt;/div&gt;

	&lt;/div&gt;

&lt;/div&gt;
</code></pre>
<p>This example displays a list of up to four admin users, each rendered using the available admin properties.</p>
]]></content:encoded>		
		</item><item data-v-post="post" data-v-id="427">
			<title data-v-post-name="name">Component wishlist</title>
			<link data-v-post-full-url="name">https://dev.vvveb.com/component-wishlist</link>
			<dc:creator data-v-post-display_name="name"><![CDATA[Admin]]></dc:creator>
			<pubDate data-v-post-pubDate="date">Mon, 28 Sep 2026 17:24:01 +0000</pubDate>
			<category data-v-post-category-name="category" data-filter-cdata=""><![CDATA[Category]]></category>
			<description data-v-post-excerpt="excerpt" data-filter-cdata=""><![CDATA[The wishlist component (Vvveb\Component\Wishlist) lists the products in the current user's wishlist with pagination. It enriches each product with the standard product data (prices, taxes, images, URLs). Caching is disabled ($cacheExpire = 0). 
Activate]]></description>
			<content:encoded data-v-post-content="content" data-filter-cdata=""><![CDATA[
<p>The <strong>wishlist</strong> component (<code>Vvveb\Component\Wishlist</code>) lists the products in the current user's wishlist with pagination. It enriches each product with the standard product data (prices, taxes, images, URLs). Caching is disabled (<code>$cacheExpire = 0</code>).</p>
<p>Activate the component by adding:</p>
<pre><code>data-v-component-wishlist
</code></pre>
<h2>Component Options</h2>
<p>Add options as HTML attributes using the <code>data-v-*</code> syntax.</p>
<table class="table table-bordered">
<thead class="table-light">
<tr>
<th>Option</th>
<th>Attribute</th>
<th>Description</th>
<th>Default</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>start</strong></td>
<td><code>data-v-start</code></td>
<td>Starting offset; recomputed from <code>page</code> when set</td>
<td><code>0</code></td>
</tr>
<tr>
<td><strong>page</strong></td>
<td><code>data-v-page</code></td>
<td>Page number for pagination</td>
<td><code>1</code></td>
</tr>
<tr>
<td><strong>limit</strong></td>
<td><code>data-v-limit</code></td>
<td>Number of products per page</td>
<td><code>4</code></td>
</tr>
<tr>
<td><strong>language_id</strong></td>
<td><code>data-v-language_id</code></td>
<td>Language for product names</td>
<td>Current language</td>
</tr>
<tr>
<td><strong>site_id</strong></td>
<td><code>data-v-site_id</code></td>
<td>Site ID</td>
<td>Current site</td>
</tr>
<tr>
<td><strong>user_id</strong></td>
<td><code>data-v-user_id</code></td>
<td>Wishlist owner; defaults to the logged-in user</td>
<td><code>null</code></td>
</tr>
<tr>
<td><strong>image_size</strong></td>
<td><code>data-v-image_size</code></td>
<td><code>xlarge</code>, <code>large</code>, <code>medium</code>, <code>thumb</code></td>
<td><code>medium</code></td>
</tr>
</tbody>
</table>
<h2>Component Properties</h2>
<p>Each product is wrapped in an element marked with <code>data-v-product</code> and exposes the same fields as the <strong>products</strong> component (<code>name</code>, <code>url</code>, <code>image</code>, <code>price</code>, <code>price_formatted</code>, <code>add_cart_url</code>, …). The component also exposes <code>limit</code>, <code>start</code> and <code>search</code> for pagination templates.</p>
<h2>HTML Example</h2>
<pre><code class="language-html">&lt;div data-v-component-wishlist data-v-limit=&quot;8&quot;&gt;
	&lt;div data-v-product&gt;
		&lt;img src=&quot;&quot; data-v-product-image alt=&quot;Product image&quot;&gt;
		&lt;h3 data-v-product-name&gt;&lt;/h3&gt;
		&lt;span data-v-product-price_formatted&gt;&lt;/span&gt;
		&lt;a href=&quot;&quot; data-v-product-url&gt;View&lt;/a&gt;
	&lt;/div&gt;
&lt;/div&gt;
</code></pre>
<h2>Notes</h2>
<ul>
<li>For the per-user wishlist table variant see the <strong>user-wishlist</strong> component (<code>data-v-component-user-wishlist</code>), which reads the <code>user_wishlist</code> records directly.</li>
</ul>
]]></content:encoded>		
		</item><item data-v-post="post" data-v-id="428">
			<title data-v-post-name="name">Component digital assets</title>
			<link data-v-post-full-url="name">https://dev.vvveb.com/component-digital_assets</link>
			<dc:creator data-v-post-display_name="name"><![CDATA[Admin]]></dc:creator>
			<pubDate data-v-post-pubDate="date">Mon, 28 Sep 2026 17:24:01 +0000</pubDate>
			<category data-v-post-category-name="category" data-filter-cdata=""><![CDATA[Category]]></category>
			<description data-v-post-excerpt="excerpt" data-filter-cdata=""><![CDATA[The digital_assets component (Vvveb\Component\Digital_assets) lists downloadable digital assets (files attached to products/orders) and builds a secure download URL for each one. Caching is disabled ($cacheExpire = 0) because download links contain]]></description>
			<content:encoded data-v-post-content="content" data-filter-cdata=""><![CDATA[
<p>The <strong>digital_assets</strong> component (<code>Vvveb\Component\Digital_assets</code>) lists downloadable digital assets (files attached to products/orders) and builds a secure download URL for each one. Caching is disabled (<code>$cacheExpire = 0</code>) because download links contain per-order keys.</p>
<p>Activate the component by adding:</p>
<pre><code>data-v-component-digital_assets
</code></pre>
<h2>Component Options</h2>
<p>Add options as HTML attributes using the <code>data-v-*</code> syntax.</p>
<table class="table table-bordered">
<thead class="table-light">
<tr>
<th>Option</th>
<th>Attribute</th>
<th>Description</th>
<th>Default</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>start</strong></td>
<td><code>data-v-start</code></td>
<td>Starting offset for pagination</td>
<td><code>0</code></td>
</tr>
<tr>
<td><strong>limit</strong></td>
<td><code>data-v-limit</code></td>
<td>Number of assets to display, overridable from URL</td>
<td><code>4</code></td>
</tr>
<tr>
<td><strong>user_id</strong></td>
<td><code>data-v-user_id</code></td>
<td>Filter by customer (user) ID</td>
<td><code>null</code></td>
</tr>
<tr>
<td><strong>product_id</strong></td>
<td><code>data-v-product_id</code></td>
<td>Filter by product ID</td>
<td><code>null</code></td>
</tr>
<tr>
<td><strong>order_status_id</strong></td>
<td><code>data-v-order_status_id</code></td>
<td>Filter by order status ID</td>
<td><code>null</code></td>
</tr>
<tr>
<td><strong>digital_asset</strong></td>
<td><code>data-v-digital_asset</code></td>
<td>Sorting rules, overridable from URL</td>
<td><code>price asc</code></td>
</tr>
</tbody>
</table>
<h2>Component Properties</h2>
<p>Each asset is wrapped in an element marked with <code>data-v-digital_asset</code>. Available fields include the asset record fields plus:</p>
<table class="table table-bordered">
<thead class="table-light">
<tr>
<th>Property</th>
<th>Attribute</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>url</strong></td>
<td><code>data-v-digital_asset-url</code></td>
<td>Secure download URL (<code>user/downloads/download</code> with asset id, public key and order key)</td>
</tr>
<tr>
<td><strong>digital_asset_id</strong></td>
<td><code>data-v-digital_asset-digital_asset_id</code></td>
<td>Asset ID</td>
</tr>
<tr>
<td><strong>customer_order_id</strong></td>
<td><code>data-v-digital_asset-customer_order_id</code></td>
<td>Customer-facing order ID the asset belongs to</td>
</tr>
</tbody>
</table>
<h2>HTML Example</h2>
<pre><code class="language-html">&lt;div data-v-component-digital_assets data-v-limit=&quot;10&quot;&gt;
	&lt;div data-v-digital_asset&gt;
		&lt;span data-v-digital_asset-name&gt;File name&lt;/span&gt;
		&lt;a href=&quot;#&quot; data-v-digital_asset-url&gt;Download&lt;/a&gt;
	&lt;/div&gt;
&lt;/div&gt;
</code></pre>
]]></content:encoded>		
		</item><item data-v-post="post" data-v-id="429">
			<title data-v-post-name="name">Component content archives</title>
			<link data-v-post-full-url="name">https://dev.vvveb.com/component-content-archives</link>
			<dc:creator data-v-post-display_name="name"><![CDATA[Admin]]></dc:creator>
			<pubDate data-v-post-pubDate="date">Mon, 28 Sep 2026 17:24:01 +0000</pubDate>
			<category data-v-post-category-name="category" data-filter-cdata=""><![CDATA[Category]]></category>
			<description data-v-post-excerpt="excerpt" data-filter-cdata=""><![CDATA[The content-archives component (Vvveb\Component\Content\Archives) builds a date-based archive index (month/year) for posts, with post counts per period. It powers blog archive widgets and archive pages. 
Activate the component by adding:]]></description>
			<content:encoded data-v-post-content="content" data-filter-cdata=""><![CDATA[
<p>The <strong>content-archives</strong> component (<code>Vvveb\Component\Content\Archives</code>) builds a date-based archive index (month/year) for posts, with post counts per period. It powers blog archive widgets and archive pages.</p>
<p>Activate the component by adding:</p>
<pre><code>data-v-component-content-archives
</code></pre>
<h2>Component Options</h2>
<p>Add options as HTML attributes using the <code>data-v-*</code> syntax.</p>
<table class="table table-bordered">
<thead class="table-light">
<tr>
<th>Option</th>
<th>Attribute</th>
<th>Description</th>
<th>Default</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>start</strong></td>
<td><code>data-v-start</code></td>
<td>Starting offset</td>
<td><code>0</code></td>
</tr>
<tr>
<td><strong>limit</strong></td>
<td><code>data-v-limit</code></td>
<td>Number of archive entries</td>
<td><code>0</code> (all)</td>
</tr>
<tr>
<td><strong>language_id</strong></td>
<td><code>data-v-language_id</code></td>
<td>Language ID</td>
<td><code>1</code></td>
</tr>
<tr>
<td><strong>site_id</strong></td>
<td><code>data-v-site_id</code></td>
<td>Site ID</td>
<td><code>1</code></td>
</tr>
<tr>
<td><strong>count</strong></td>
<td><code>data-v-count</code></td>
<td>Include post counts, overridable from URL</td>
<td><code>4</code></td>
</tr>
<tr>
<td><strong>order</strong></td>
<td><code>data-v-order</code></td>
<td>Sorting rules, overridable from URL</td>
<td><code>url</code> fallback <code>price asc</code></td>
</tr>
<tr>
<td><strong>type</strong></td>
<td><code>data-v-type</code></td>
<td>Post type to archive</td>
<td><code>post</code></td>
</tr>
<tr>
<td><strong>interval</strong></td>
<td><code>data-v-interval</code></td>
<td>Grouping interval (<code>month</code>, <code>year</code>, …)</td>
<td><code>month</code></td>
</tr>
<tr>
<td><strong>id_manufacturer</strong></td>
<td><code>data-v-id_manufacturer</code></td>
<td>Legacy filter, unused for posts</td>
<td><code>null</code></td>
</tr>
<tr>
<td><strong>id_category</strong></td>
<td><code>data-v-id_category</code></td>
<td>Legacy filter, unused for posts</td>
<td><code>null</code></td>
</tr>
</tbody>
</table>
<h2>Component Properties</h2>
<p>Each entry renders inside <code>data-v-archives</code> / <code>data-v-archive</code> repeatables with the archive period (<code>year</code>, <code>month</code>), post <code>count</code> and archive URL fields.</p>
<h2>HTML Example</h2>
<pre><code class="language-html">&lt;div data-v-component-content-archives data-v-interval=&quot;month&quot;&gt;
	&lt;div data-v-archives&gt;
		&lt;div data-v-archive&gt;
			&lt;a href=&quot;&quot; data-v-archive-url&gt;&lt;span data-v-archive-name&gt;&lt;/span&gt;&lt;/a&gt;
			&lt;span data-v-archive-count&gt;&lt;/span&gt;
		&lt;/div&gt;
	&lt;/div&gt;
&lt;/div&gt;
</code></pre>
]]></content:encoded>		
		</item><item data-v-post="post" data-v-id="275">
			<title data-v-post-name="name">Sidebar</title>
			<link data-v-post-full-url="name">https://dev.vvveb.com/_Sidebar</link>
			<dc:creator data-v-post-display_name="name"><![CDATA[Admin]]></dc:creator>
			<pubDate data-v-post-pubDate="date">Fri, 27 Mar 2026 21:03:40 +0000</pubDate>
			<category data-v-post-category-name="category" data-filter-cdata=""><![CDATA[Category]]></category>
			<description data-v-post-excerpt="excerpt" data-filter-cdata=""><![CDATA[Overview 
Components 
Inputs 
Sections 
Events]]></description>
			<content:encoded data-v-post-content="content" data-filter-cdata=""><![CDATA[<br/>
<br/>
<p align="center"><img src="https://www.vvveb.com/admin/default/img/biglogo.png" alt="VvvebJs" align="center"/></p>
<br/>
<br/>
<ul>
<li><a href="./Home">Overview</a></li>
<li><a href="./Components">Components</a></li>
<li><a href="./Inputs">Inputs</a></li>
<li><a href="./Sections">Sections</a></li>
<li><a href="./Events">Events</a></li>
</ul>
]]></content:encoded>		
		</item><item data-v-post="post" data-v-id="276">
			<title data-v-post-name="name">Sections</title>
			<link data-v-post-full-url="name">https://dev.vvveb.com/sections</link>
			<dc:creator data-v-post-display_name="name"><![CDATA[Admin]]></dc:creator>
			<pubDate data-v-post-pubDate="date">Fri, 27 Mar 2026 21:03:40 +0000</pubDate>
			<category data-v-post-category-name="category" data-filter-cdata=""><![CDATA[Category]]></category>
			<description data-v-post-excerpt="excerpt" data-filter-cdata=""><![CDATA[Overview 
Sections are predefined horizontal page &quot;slices&quot; that can be inserted in the page. 
To add a section you need to use Vvveb.Sections.add method, the first parameter is the unique section name. 
The second parameter is an object with]]></description>
			<content:encoded data-v-post-content="content" data-filter-cdata=""><![CDATA[
<h2>Overview</h2>
<p>Sections are predefined horizontal page &quot;slices&quot; that can be inserted in the page.</p>
<p>To add a section you need to use <code>Vvveb.Sections.add</code> method, the first parameter is the unique section name.</p>
<p>The second parameter is an object with the following keys</p>
<ul>
<li>name</li>
<li>image</li>
<li>html</li>
</ul>
<p>Example:</p>
<pre><code class="language-js">Vvveb.Sections.add(&quot;bootstrap5/my-section&quot;, {
    name: &quot;My custom section&quot;,
    image: &quot;https://assets.startbootstrap.com/img/screenshots/snippets/sign-in-split.jpg&quot;, //screenshot
    html: `
&lt;section data-name=&quot;sigin-split&quot;&gt;    
&lt;div class=&quot;container-fluid&quot;&gt;
  &lt;div class=&quot;row no-gutter&quot;&gt;
	&lt;h1&gt;My section&lt;/h1&gt;
  &lt;/div&gt;  
&lt;/div&gt;  
`,
});    
</code></pre>
<p>A good practice is to have the section html wrapped in a <code>&lt;section&gt;</code> tag, you can also use <code>&lt;header&gt;</code> or <code>&lt;footer&gt;</code>.</p>
<p>Based on this tag the editor will insert the section either at the end of the page for <code>&lt;section&gt;</code> or at the top for <code>&lt;header&gt;</code> and for <code>&lt;footer&gt;</code> after the existing <code>&lt;footer&gt;</code> if there is one already added in the page.</p>
<h2>Section group</h2>
<p>For a section to be visible in the left panel it needs to be added to a section group, you can do this by including it in the <code>Vvveb.SectionsGroup</code> array using the group name as a key.</p>
<p>For example Bootstrap section group has the following definition.</p>
<pre><code class="language-js">Vvveb.SectionsGroup['Bootstrap'] = [
  &quot;bootstrap5/signin-split&quot;,  
  &quot;bootstrap5/image-gallery&quot;, 
  &quot;bootstrap5/video-header&quot;, 
  &quot;bootstrap5/slider-header&quot;
];
</code></pre>
<h2>Automatic generation from html files</h2>
<p>Because sections are mostly html code it's easier to maintain them as html files grouped in folders and generate the <code>sections.js</code> file to be included in VvvebJs when changing or adding new sections.</p>
<p>This approach is used in the bundled <a href="https://github.com/givanz/landing">Landing theme</a> where sections are grouped in folders <a href="https://github.com/givanz/landing/tree/main/src/sections">https://github.com/givanz/landing/tree/main/src/sections</a></p>
<p>In the theme folder <code>/demo/landing</code> run the following commands to (re)generate sections.</p>
<h3>Generate html files</h3>
<pre><code class="language-bash">npm run gulp
</code></pre>
<h3>Watch for changes for development</h3>
<pre><code class="language-bash">npm run gulp watch
</code></pre>
<h3>Generate sections list for VvvebJs page builder</h3>
<pre><code class="language-bash">npm run gulp sections
</code></pre>
<p>This will generate <code>demo/landing/sections/sections.js</code> file that is included in VvvebJs</p>
<h3>Generate blocks list for VvvebJs editor</h3>
<pre><code class="language-bash">npm run gulp blocks
</code></pre>
<h3>Generate screenshots for sections</h3>
<pre><code class="language-bash">npm run gulp screenshots
</code></pre>
]]></content:encoded>		
		</item><item data-v-post="post" data-v-id="277">
			<title data-v-post-name="name">Events</title>
			<link data-v-post-full-url="name">https://dev.vvveb.com/events</link>
			<dc:creator data-v-post-display_name="name"><![CDATA[Admin]]></dc:creator>
			<pubDate data-v-post-pubDate="date">Fri, 27 Mar 2026 21:03:40 +0000</pubDate>
			<category data-v-post-category-name="category" data-filter-cdata=""><![CDATA[Category]]></category>
			<description data-v-post-excerpt="excerpt" data-filter-cdata=""><![CDATA[Events 
Editor 
propertyChange 
Called by a input object when the property changes, the editor will listen to this event to update the element when a property changes. 
Inputs trigger this event using 
new CustomEvent('propertyChange', { detail: {value,]]></description>
			<content:encoded data-v-post-content="content" data-filter-cdata=""><![CDATA[<h2>Events</h2>
<h2>Editor</h2>
<h3>propertyChange</h3>
<p>Called by a input object when the property changes, the editor will listen to this event to update the element when a property changes.</p>
<p>Inputs trigger this event using</p>
<pre><code class="language-javascript">new CustomEvent('propertyChange', { detail: {value, input, origEvent} });
</code></pre>
<h3>vvveb.iframe.loaded</h3>
<p>The editor triggers this event after the page/iframe is loaded and the the editor finished setting up the page.</p>
<p>The detail parameter is set to iframe document object.</p>
<pre><code class="language-javascript">new CustomEvent(&quot;vvveb.iframe.loaded&quot;, {detail: self.frameDoc}));
</code></pre>
<h3>vvveb.getHtml.before</h3>
<p>This event is triggered by the Vvveb.Builder.getHtml method before removing helpers and other processing.</p>
<pre><code class="language-javascript">new CustomEvent(&quot;vvveb.getHtml.before&quot;, {detail: document});
</code></pre>
<p>Example use by aos plugin to remove classes before page save</p>
<pre><code class="language-javascript">//clean aos classes on save
window.addEventListener(&quot;vvveb.getHtml.before&quot;, function(event) {
	let doc = event.detail;
	doc.querySelectorAll(&quot;[data-aos]&quot;).forEach(e =&gt; e.classList.remove(&quot;aos-animate&quot;, &quot;aos-init&quot;));
});
</code></pre>
<h3>vvveb.getHtml.after</h3>
<p>This event is triggered by the Vvveb.Builder.getHtml method finished processing the html.</p>
<pre><code class="language-javascript">new CustomEvent(&quot;vvveb.getHtml.after&quot;, {detail: doc}));
</code></pre>
<p>Example use by aos plugin to add back classes after page save</p>
<pre><code class="language-javascript">window.addEventListener(&quot;vvveb.getHtml.after&quot;, function(event) {
	let doc = event.detail;
	doc.querySelectorAll(&quot;[data-aos]&quot;).forEach(e =&gt; e.classList.add(&quot;aos-animate&quot;, &quot;aos-init&quot;));
});		 
</code></pre>
<h3>vvveb.getHtml.filter</h3>
<p>Similar with vvveb.getHtml.after but passes html code instead of document object, used for processing html code instead of manipulating the document.</p>
<pre><code class="language-javascript">new CustomEvent(&quot;vvveb.getHtml.filter&quot;, {detail: html}));
</code></pre>
<h3>vvveb.ModalCodeEditor.save</h3>
<p>Called by the code editor modal with <code>detail</code> set as the value of the editor content.</p>
<pre><code class="language-javascript">new CustomEvent(&quot;vvveb.ModalCodeEditor.save&quot;, {detail: value}));
</code></pre>
<h3>vvveb.FileManager.deletePage</h3>
<p>Called when a page is deleted from file manager, <code>detail</code> parameter is set to page object</p>
<p>Page object structure <code>{name, title,  url, file}</code></p>
<pre><code class="language-javascript">new CustomEvent(&quot;vvveb.FileManager.deletePage&quot;, {detail: page})
</code></pre>
<h3>vvveb.FileManager.renamePage</h3>
<p>Called when a page is renamed from file manager, <code>detail</code> parameter is set to page object and new file name.</p>
<p>Page object structure <code>{name, title,  url, file}</code></p>
<pre><code class="language-javascript">new CustomEvent(&quot;vvveb.FileManager.renamePage&quot;, {detail: {page, newfile}});
</code></pre>
<h3>vvveb.FileManager.addPage</h3>
<p>Called when a new page is added to file manager.</p>
<p>Detail parameter contains an array with page name and page object.</p>
<pre><code class="language-javascript">new CustomEvent(&quot;vvveb.FileManager.addPage&quot;, {detail: [name, data]})
</code></pre>
<h3>vvveb.FileManager.loadPage</h3>
<p>Called when a page is loaded.</p>
<p>Detail parameter contains the page object.</p>
<pre><code class="language-javascript">new CustomEvent(&quot;vvveb.FileManager.loadPage&quot;, {detail: page})
</code></pre>
<h2>Undo Manager</h2>
<h3>vvveb.undo.add</h3>
<p>Called when a undo action is registered, this happens on a change made to the page</p>
<p>Mutation object has the following structure</p>
<pre><code class="language-javascript">let mutation = {
	type: 'style', //style = css change, attributes = attribute change, characterData = text content change, move = element is moved on the page, childList = new node is added to page
	target: element, 
	attributeName: &quot;color&quot;, 
	oldValue: &quot;red&quot;, 
	newValue: &quot;blue&quot;
}
</code></pre>
<pre><code class="language-javascript">const event = new CustomEvent(&quot;vvveb.undo.add&quot;, {detail: mutation});
</code></pre>
<p>Example used by code editor to update the page html inside the code editor when a change is made to the page</p>
<pre><code class="language-javascript">Vvveb.Builder.frameBody.addEventListener(&quot;vvveb.undo.add&quot;, () =&gt; Vvveb.CodeEditor.setValue());
</code></pre>
<h3>vvveb.undo.restore</h3>
<p>Called when a undo action is restored/reverted</p>
<pre><code class="language-javascript">const event = new CustomEvent(&quot;vvveb.undo.restore&quot;, {detail: mutation});
</code></pre>
<p>Example used by code editor to update the page html inside the code editor when a change is made to the page</p>
<pre><code class="language-javascript">Vvveb.Builder.frameBody.addEventListener(&quot;vvveb.undo.restore&quot;, () =&gt; Vvveb.CodeEditor.setValue());
</code></pre>
]]></content:encoded>		
		</item><item data-v-post="post" data-v-id="265">
			<title data-v-post-name="name">Component pagination</title>
			<link data-v-post-full-url="name">https://dev.vvveb.com/pagination</link>
			<dc:creator data-v-post-display_name="name"><![CDATA[Admin]]></dc:creator>
			<pubDate data-v-post-pubDate="date">Fri, 27 Mar 2026 00:03:23 +0000</pubDate>
			<category data-v-post-category-name="category" data-filter-cdata=""><![CDATA[Category]]></category>
			<description data-v-post-excerpt="excerpt" data-filter-cdata=""><![CDATA[Pagination is used by components that return multiple pages of results—such as posts, products, comments, orders, or any list‑based component. It automatically generates page links, previous/next ]]></description>
			<content:encoded data-v-post-content="content" data-filter-cdata=""><![CDATA[
<p>Pagination is used by components that return multiple pages of results—such as <strong>posts</strong>, <strong>products</strong>, <strong>comments</strong>, <strong>orders</strong>, or any list‑based component. It automatically generates page links, previous/next navigation, and first/last page controls.</p>
<p>To activate pagination, wrap your pagination markup in an element containing:</p>
<pre><code>data-pagination
</code></pre>
<p>This container will automatically receive pagination data from the parent component.</p>
<hr />
<h2><strong>Connecting Pagination to a Component</strong></h2>
<p>Pagination must know <em>which component</em> it belongs to. This is done using the following attributes:</p>
<table class="table">
<thead>
<tr>
<th>Attribute</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>data-v-parent-component</strong></td>
<td>Component name (<code>products</code>, <code>posts</code>, <code>comments</code>, etc.)</td>
</tr>
<tr>
<td><strong>data-v-parent-index</strong></td>
<td>Index of the component on the page (useful when multiple components of the same type exist)</td>
</tr>
<tr>
<td><strong>data-v-query-string</strong></td>
<td>Whether to preserve existing URL parameters (<code>true</code>/<code>false</code>)</td>
</tr>
<tr>
<td><strong>data-v-parameters</strong></td>
<td>Extra parameters to include in pagination URLs (JSON object) `</td>
</tr>
</tbody>
</table>
<h3>Example</h3>
<pre><code class="language-html">&lt;div data-pagination
     data-v-parent-component=&quot;products&quot;
     data-v-parent-index=&quot;0&quot;
     data-v-query-string=&quot;true&quot;
     data-v-parameters='{&quot;type&quot;:&quot;post&quot;,&quot;myparam&quot;:&quot;value&quot;}'&gt;
</code></pre>
<p>This ensures the pagination controls update the correct component and preserve or extend URL parameters.</p>
<hr />
<h2><strong>Pagination Data Attributes</strong></h2>
<p>The pagination container receives several dynamic values:</p>
<table class="table">
<thead>
<tr>
<th>Attribute</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>data-count</strong></td>
<td>Total number of pages</td>
</tr>
<tr>
<td><strong>data-current-page</strong></td>
<td>Current page number</td>
</tr>
<tr>
<td><strong>data-current-url</strong></td>
<td>Current page URL</td>
</tr>
</tbody>
</table>
<p>These values can be used to show page indicators, disable buttons, or highlight the active page.</p>
<hr />
<h2><strong>Page Elements</strong></h2>
<p>Pagination uses several repeatable or conditional elements to build navigation.</p>
<h3><strong>Page List Items</strong></h3>
<p>Each page number is wrapped in:</p>
<pre><code>data-page
</code></pre>
<p>Inside it, you can use:</p>
<ul>
<li><strong>data-page-url</strong> — URL for that page</li>
<li><strong>data-page-no</strong> — Page number</li>
</ul>
<p>Example:</p>
<pre><code class="language-html">&lt;li data-page&gt;
  &lt;a data-page-url data-page-no href=&quot;/shop/2&quot;&gt;2&lt;/a&gt;
&lt;/li&gt;
</code></pre>
<hr />
<h3><strong>First Page</strong></h3>
<p>Rendered only when the current page is not the first.</p>
<pre><code class="language-html">&lt;li data-first&gt;
  &lt;a data-page-url&gt;First&lt;/a&gt;
&lt;/li&gt;
</code></pre>
<hr />
<h3><strong>Previous Page</strong></h3>
<p>Rendered only when <code>current_page &gt; 1</code>.</p>
<pre><code class="language-html">&lt;li data-prev&gt;
  &lt;a data-page-url title=&quot;Previous page&quot;&gt;
    &lt;span class=&quot;la la-arrow-left&quot;&gt;&lt;/span&gt;
  &lt;/a&gt;
&lt;/li&gt;
</code></pre>
<hr />
<h3><strong>Next Page</strong></h3>
<p>Rendered only when <code>current_page &lt; pagecount</code>.</p>
<pre><code class="language-html">&lt;li data-next&gt;
  &lt;a data-page-url title=&quot;Next page&quot;&gt;
    &lt;span class=&quot;la la-arrow-right&quot;&gt;&lt;/span&gt;
  &lt;/a&gt;
&lt;/li&gt;
</code></pre>
<hr />
<h3><strong>Last Page</strong></h3>
<p>Rendered only when the current page is not the last.</p>
<pre><code class="language-html">&lt;li data-last&gt;
  &lt;a data-page-url&gt;Last&lt;/a&gt;
&lt;/li&gt;
</code></pre>
<hr />
<h2><strong>Full Pagination Example</strong></h2>
<pre><code class="language-html">&lt;div data-pagination
     data-v-parent-component=&quot;products&quot;
     data-v-parent-index=&quot;0&quot;
     data-v-query-string=&quot;true&quot;
     data-v-parameters=&quot;{}&quot;&gt;

  &lt;nav data-v-if=&quot;pagecount &gt; 1&quot;&gt;
    &lt;ul&gt;

      &lt;!-- Previous --&gt;
      &lt;li class=&quot;page-item pager-prev&quot; data-prev&gt;
        &lt;a data-page-url data-v-if=&quot;current_page &gt; 1&quot; title=&quot;Previous page&quot;&gt;
          &lt;span class=&quot;la la-arrow-left la-lg&quot;&gt;&lt;/span&gt;
        &lt;/a&gt;
      &lt;/li&gt;

      &lt;!-- Page Numbers --&gt;
      &lt;li class=&quot;active&quot; data-page&gt;
        &lt;a data-page-url data-page-no href=&quot;/shop&quot;&gt;1&lt;/a&gt;
      &lt;/li&gt;

      &lt;li data-page&gt;
        &lt;a data-page-url data-page-no href=&quot;/shop/2&quot;&gt;2&lt;/a&gt;
      &lt;/li&gt;

      &lt;li data-page&gt;
        &lt;a data-page-url data-page-no href=&quot;/shop/3&quot;&gt;3&lt;/a&gt;
      &lt;/li&gt;

      &lt;li data-page&gt;
        &lt;a data-page-url data-page-no href=&quot;/shop/4&quot;&gt;4&lt;/a&gt;
      &lt;/li&gt;

      &lt;!-- Next --&gt;
      &lt;li data-next&gt;
        &lt;a data-page-url data-v-if=&quot;current_page &lt; pagecount&quot; title=&quot;Next page&quot;&gt;
          &lt;span class=&quot;la la-arrow-right la-lg&quot;&gt;&lt;/span&gt;
        &lt;/a&gt;
      &lt;/li&gt;

    &lt;/ul&gt;
  &lt;/nav&gt;

&lt;/div&gt;
</code></pre>
<p>This example shows a typical pagination block with previous/next arrows and numbered pages.</p>
<hr />
<h2><strong>How Pagination Works Internally</strong></h2>
<ul>
<li>The parent component (posts, products, comments, etc.) calculates:
<ul>
<li>total items</li>
<li>items per page</li>
<li>total pages</li>
<li>current page</li>
</ul>
</li>
<li>Pagination receives these values and generates:
<ul>
<li>page links</li>
<li>first/prev/next/last controls</li>
<li>URLs with preserved or extended query parameters</li>
</ul>
</li>
<li>When a user clicks a page link, the component reloads with the new page number.</li>
</ul>
]]></content:encoded>		
		</item><item data-v-post="post" data-v-id="264">
			<title data-v-post-name="name">Component users</title>
			<link data-v-post-full-url="name">https://dev.vvveb.com/component-users</link>
			<dc:creator data-v-post-display_name="name"><![CDATA[Admin]]></dc:creator>
			<pubDate data-v-post-pubDate="date">Fri, 27 Mar 2026 00:01:00 +0000</pubDate>
			<category data-v-post-category-name="category" data-filter-cdata=""><![CDATA[Category]]></category>
			<description data-v-post-excerpt="excerpt" data-filter-cdata=""><![CDATA[The users component displays a list of users. It supports pagination, filtering, and searching, making it useful for author listings, team pages, or user management sections within a theme. 
 
Component Options 
Add any option as an HTML attribute using]]></description>
			<content:encoded data-v-post-content="content" data-filter-cdata=""><![CDATA[
<p>The <strong>users</strong> component displays a list of users.
It supports pagination, filtering, and searching, making it useful for author listings, team pages, or user management sections within a theme.</p>
<hr />
<h2>Component Options</h2>
<p>Add any option as an HTML attribute using the <code>data-v-*</code> syntax.</p>
<ul>
<li><strong>start</strong> - Starting offset for pagination. Useful when manually controlling the list position. Default: <code>0</code>.</li>
<li><strong>limit</strong> - Number of users to display per page, overridable from URL. Default: <code>4</code>.</li>
<li><strong>status</strong> - Filter by account status (<code>1</code> for enabled, <code>0</code> for disabled). Default: <code>1</code>.</li>
<li><strong>search</strong> - Search by username, first name, or last name.</li>
<li><strong>user_id</strong> - Filter by one or more user IDs. Accepts a single value or a comma‑separated list.</li>
</ul>
<p>These options allow you to build flexible user listings, from simple team pages to filtered author directories.</p>
<hr />
<h2>Component Properties</h2>
<p>Each user is wrapped in an element marked with <code>data-v-user</code>.<br />
Inside each element, the following properties are available:</p>
<ul>
<li><strong>first_name</strong> - <code>data-v-user-first_name</code> - User’s first name.</li>
<li><strong>last_name</strong> - <code>data-v-user-last_name</code> - User’s last name.</li>
<li><strong>avatar_url</strong> - <code>data-v-user-avatar_url</code> - Profile avatar image URL.</li>
<li><strong>cover_url</strong> - <code>data-v-user-cover_url</code> - Cover or background image URL.</li>
<li><strong>url</strong> - <code>data-v-user-url</code> - Public profile page URL.</li>
<li><strong>username</strong> - <code>data-v-user-username</code> - Username used for login.</li>
<li><strong>display_name</strong> - <code>data-v-user-display_name</code> - Public display name.</li>
<li><strong>website</strong> - <code>data-v-user-website</code> - Personal or professional website.</li>
<li><strong>email</strong> - <code>data-v-user-email</code> - Internal email for login and notifications.</li>
<li><strong>public_email</strong> - <code>data-v-user-public_email</code> - Publicly visible email address.</li>
<li><strong>bio</strong> - <code>data-v-user-bio</code> - Short biography or description.</li>
<li><strong>created_at</strong> - <code>data-v-user-created_at</code> - Account creation timestamp.</li>
<li><strong>updated_at</strong> - <code>data-v-user-updated_at</code> - Last update timestamp.</li>
<li><strong>last_ip</strong> - <code>data-v-user-last_ip</code> - Last login IP address.</li>
<li><strong>phone_number</strong> - <code>data-v-user-phone_number</code> - Public phone number (if provided).</li>
</ul>
<p>These properties repeat for each user returned by the component.</p>
<hr />
<h2>HTML Example</h2>
<pre><code class="language-html">&lt;div data-v-component-users data-v-limit=&quot;4&quot;&gt;

	&lt;div data-v-user&gt;

		&lt;div class=&quot;profile-header&quot;&gt;
			&lt;div class=&quot;cover&quot;&gt;
				&lt;img src=&quot;&quot; data-v-user-cover_url&gt;
			&lt;/div&gt;

			&lt;div class=&quot;avatar&quot;&gt;
				&lt;img data-v-user-avatar_url class=&quot;rounded-circle avatar&quot; alt=&quot;Profile Picture&quot;&gt;
			&lt;/div&gt;
		&lt;/div&gt;

		&lt;div class=&quot;px-3 m-3 me-5 d-flex justify-content-between&quot;&gt;
			&lt;div&gt;
				&lt;h1 class=&quot;h3 m-0&quot; data-v-user-display_name&gt;User&lt;/h1&gt;
			&lt;/div&gt;

			&lt;div class=&quot;social-links d-inline-block&quot;&gt;
				&lt;!-- Optional social links --&gt;
			&lt;/div&gt;
		&lt;/div&gt;

		&lt;div class=&quot;px-3 m-3 me-5 d-flex&quot;&gt;

			&lt;div&gt;
				&lt;div class=&quot;text-muted&quot;&gt;@&lt;span data-v-user-username&gt;username&lt;/span&gt;&lt;/div&gt;

				&lt;div&gt;
					&lt;a class=&quot;d-block&quot; data-v-user-website href=&quot;#&quot;&gt;
						&lt;span data-v-user-website&gt;example.com&lt;/span&gt;
					&lt;/a&gt;

					&lt;a class=&quot;d-block text-body small&quot; href=&quot;#&quot;&gt;
						&lt;span data-v-user-public_email&gt;user@vvveb.com&lt;/span&gt;
					&lt;/a&gt;

					&lt;a class=&quot;d-block text-body small&quot; href=&quot;#&quot;&gt;
						&lt;span data-v-user-phone_number&gt;123456&lt;/span&gt;
					&lt;/a&gt;
				&lt;/div&gt;
			&lt;/div&gt;

			&lt;div class=&quot;ms-5&quot;&gt;
				&lt;div data-v-user-bio&gt;My bio&lt;/div&gt;
			&lt;/div&gt;

		&lt;/div&gt;

	&lt;/div&gt;

&lt;/div&gt;
</code></pre>
<p>This example displays a list of up to four users, each rendered using the available user properties.</p>
]]></content:encoded>		
		</item>		
		
		
		
	</channel>
</rss>
