<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"
	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/"
	>

<channel>
	<title>Faqe Interneti</title>
	<atom:link href="https://sajdoko.com/feed/" rel="self" type="application/rss+xml" />
	<link>https://sajdoko.com</link>
	<description></description>
	<lastBuildDate>Thu, 10 Sep 2026 20:40:51 +0000</lastBuildDate>
	<language>en-US</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	<generator>https://wordpress.org/?v=7.1</generator>

<image>
	<url>https://sajdoko.com/wp-content/uploads/2024/08/cropped-cropped-black-32x32.png</url>
	<title>Faqe Interneti</title>
	<link>https://sajdoko.com</link>
	<width>32</width>
	<height>32</height>
</image> 
	<item>
		<title>Elementor Widget Part 4: WooCommerce Product Integration</title>
		<link>https://sajdoko.com/blog/elementor-widget-part-4-woocommerce-product-integration/</link>
					<comments>https://sajdoko.com/blog/elementor-widget-part-4-woocommerce-product-integration/#respond</comments>
		
		<dc:creator><![CDATA[sajdoko]]></dc:creator>
		<pubDate>Thu, 13 Nov 2025 19:50:07 +0000</pubDate>
				<category><![CDATA[Code]]></category>
		<category><![CDATA[Development]]></category>
		<category><![CDATA[Elementor]]></category>
		<category><![CDATA[WooCommerce]]></category>
		<category><![CDATA[WordPress]]></category>
		<category><![CDATA[Elementor Widget]]></category>
		<guid isPermaLink="false">https://sajdoko.al/blog/elementor-widget-part-4-woocommerce-product-integration/</guid>

					<description><![CDATA[We’ll query products, display their images and prices, and build a basic grid layout. # Learning&#8230;]]></description>
										<content:encoded><![CDATA[<h2 id="introduction" tabindex="-1"><a class="header-anchor" href="#introduction">#</a> Introduction</h2>
<p>This part connects the widget to WooCommerce products. We’ll query products, display their images and prices, and build a basic grid layout.</p>
<h2 id="learning-objectives" tabindex="-1"><a class="header-anchor" href="#learning-objectives">#</a> Learning Objectives</h2>
<p>By the end of this tutorial, you will:</p>
<ul>
<li>Query WooCommerce products using WP_Query</li>
<li>Build query arguments based on widget settings</li>
<li>Access product data using WooCommerce API</li>
<li>Display product titles, prices, and images</li>
<li>Work with product taxonomies</li>
<li>Create a basic product grid layout</li>
<li>Handle products with no results</li>
</ul>
<h2 id="step-1%3A-understanding-wp_query-for-products" tabindex="-1"><a class="header-anchor" href="#step-1%3A-understanding-wp_query-for-products">#</a> Step 1: Understanding WP_Query for Products</h2>
<p>WooCommerce products are a custom post type called <code>product</code>. We use WordPress’s <code>WP_Query</code> to retrieve them.</p>
<h3 id="basic-product-query-structure" tabindex="-1"><a class="header-anchor" href="#basic-product-query-structure">#</a> Basic Product Query Structure</h3>
<pre tabindex="0" class="hljs"><code class="language-php">
$args = [
    'post_type' =&gt; 'product',
    'posts_per_page' =&gt; 9,
    'post_status' =&gt; 'publish',
];

$query = new WP_Query( $args );
</code></pre>
<h3 id="common-query-parameters" tabindex="-1"><a class="header-anchor" href="#common-query-parameters">#</a> Common Query Parameters</h3>
<ul>
<li><strong>post_type</strong>: Must be ‘product’ for WooCommerce products</li>
<li><strong>posts_per_page</strong>: Number of products to retrieve</li>
<li><strong>orderby</strong>: How to sort (date, title, price, etc.)</li>
<li><strong>order</strong>: ASC or DESC</li>
<li><strong>tax_query</strong>: Filter by categories, tags, or attributes</li>
<li><strong>post_status</strong>: Usually ‘publish’ for public products</li>
</ul>
<h2 id="step-2%3A-update-the-render()-method" tabindex="-1"><a class="header-anchor" href="#step-2%3A-update-the-render()-method">#</a> Step 2: Update the render() Method</h2>
<p>Replace the placeholder <code>render()</code> method in <code>collection-products-widget.php</code> with a proper product query:</p>
<pre tabindex="0" class="hljs"><code class="language-php">
&lt;?php
protected function render() {
    $settings = $this-&gt;get_settings_for_display();

    // Build query arguments
    $args = [
        'post_type' =&gt; 'product',
        'posts_per_page' =&gt; $settings['products_per_page'],
        'orderby' =&gt; $settings['orderby'],
        'order' =&gt; $settings['order'],
        'post_status' =&gt; 'publish',
    ];

    // Add category filter if specified
    if ( ! empty( $settings['category'] ) ) {
        $args['tax_query'] = [
            [
                'taxonomy' =&gt; 'product_cat',
                'field' =&gt; 'term_id',
                'terms' =&gt; $settings['category'],
            ],
        ];
    }

    // Execute query
    $query = new WP_Query( $args );

    // Check if products found
    if ( $query-&gt;have_posts() ) {
        ?&gt;
        &lt;div class="collection-products-wrapper"&gt;
            &lt;div class="collection-products-grid"&gt;
                &lt;?php
                while ( $query-&gt;have_posts() ) {
                    $query-&gt;the_post();
                    global $product;

                    // We'll add product display code here
                    ?&gt;
                    &lt;div class="collection-product-item"&gt;
                        &lt;h3&gt;&lt;?php the_title(); ?&gt;&lt;/h3&gt;
                    &lt;/div&gt;
                    &lt;?php
                }
                ?&gt;
            &lt;/div&gt;
        &lt;/div&gt;
        &lt;?php
        wp_reset_postdata();
    } else {
        echo '&lt;p&gt;' . __( 'No products found.', 'hello-biz-child' ) . '&lt;/p&gt;';
    }
}
</code></pre>
<p><strong>Code Explanation:</strong></p>
<ul>
<li><strong>Build Arguments</strong>: Create array based on widget settings</li>
<li><strong>Tax Query</strong>: Filter by categories if user selected any</li>
<li><strong>WP_Query</strong>: Execute the query</li>
<li><strong>The Loop</strong>: Iterate through results with <code>have_posts()</code> and <code>the_post()</code></li>
<li><strong>global $product</strong>: Access WooCommerce product object</li>
<li><strong>wp_reset_postdata()</strong>: Reset query data after loop (important!)</li>
</ul>
<h2 id="step-3%3A-accessing-product-data" tabindex="-1"><a class="header-anchor" href="#step-3%3A-accessing-product-data">#</a> Step 3: Accessing Product Data</h2>
<p>WooCommerce provides rich product data through the <code>$product</code> object. Let’s explore common methods:</p>
<h3 id="common-product-methods" tabindex="-1"><a class="header-anchor" href="#common-product-methods">#</a> Common Product Methods</h3>
<pre tabindex="0" class="hljs"><code class="language-php">
global $product;

// Basic info
$product-&gt;get_id();                    // Product ID
$product-&gt;get_name();                  // Product name
$product-&gt;get_title();                 // Product title
$product-&gt;get_permalink();             // Product URL

// Pricing
$product-&gt;get_price();                 // Current price
$product-&gt;get_regular_price();         // Regular price
$product-&gt;get_sale_price();            // Sale price
$product-&gt;get_price_html();            // Formatted price HTML

// Images
$product-&gt;get_image();                 // Featured image HTML
$product-&gt;get_image_id();              // Featured image ID
get_the_post_thumbnail_url( $id );     // Image URL

// Product types
$product-&gt;is_type( 'simple' );         // Is simple product
$product-&gt;is_type( 'variable' );       // Is variable product
$product-&gt;is_on_sale();                // Is on sale

// Variations
$product-&gt;get_variation_attributes();  // For variable products
</code></pre>
<h2 id="step-4%3A-display-product-information" tabindex="-1"><a class="header-anchor" href="#step-4%3A-display-product-information">#</a> Step 4: Display Product Information</h2>
<p>Update the product loop to display complete product information:</p>
<pre tabindex="0" class="hljs"><code class="language-php">
&lt;?php
while ( $query-&gt;have_posts() ) {
    $query-&gt;the_post();
    global $product;

    $product_id = get_the_ID();
    ?&gt;
    &lt;div class="collection-product-item"&gt;
        &lt;a href="&lt;?php echo esc_url( get_permalink() ); ?&gt;" class="collection-product-link"&gt;
            &lt;div class="collection-product-image"&gt;
                &lt;?php
                $image_url = get_the_post_thumbnail_url( $product_id, 'full' );
                if ( $image_url ) :
                ?&gt;
                    &lt;img src="&lt;?php echo esc_url( $image_url ); ?&gt;"
                         alt="&lt;?php echo esc_attr( get_the_title() ); ?&gt;"&gt;
                &lt;?php endif; ?&gt;
            &lt;/div&gt;
        &lt;/a&gt;
        
        &lt;div class="collection-product-info"&gt;
            &lt;div class="collection-product-info-left"&gt;
                &lt;h3 class="collection-product-title"&gt;
                    &lt;a href="&lt;?php echo esc_url( get_permalink() ); ?&gt;"&gt;
                        &lt;?php the_title(); ?&gt;
                    &lt;/a&gt;
                &lt;/h3&gt;
            &lt;/div&gt;
            
            &lt;div class="collection-product-price"&gt;
                &lt;?php echo $product-&gt;get_price_html(); ?&gt;
            &lt;/div&gt;
        &lt;/div&gt;
    &lt;/div&gt;
    &lt;?php
}
</code></pre>
<p><strong>Code Explanation:</strong></p>
<ul>
<li><strong>Product Link</strong>: Wraps image in link to product page</li>
<li><strong>Product Image</strong>: Displays featured image</li>
<li><strong>esc_url()</strong>: Sanitizes URLs</li>
<li><strong>esc_attr()</strong>: Sanitizes attributes</li>
<li><strong>get_price_html()</strong>: Displays formatted price with currency</li>
</ul>
<h2 id="step-5%3A-add-basic-grid-css" tabindex="-1"><a class="header-anchor" href="#step-5%3A-add-basic-grid-css">#</a> Step 5: Add Basic Grid CSS</h2>
<p>Create <code>wp-content/themes/hello-biz-child/css/collection.css</code>:</p>
<pre tabindex="0" class="hljs"><code class="language-css">
/* Collection Products Grid */
.collection-products-wrapper {
    width: 100%;
    max-width: 100%;
}

.collection-products-grid {
    display: grid;
    grid-template-columns: repeat(2, 1fr);
    gap: 10px;
    width: 100%;
}

.collection-product-item {
    position: relative;
}

.collection-product-link {
    display: block;
    text-decoration: none;
    color: inherit;
}

.collection-product-image {
    position: relative;
    width: 100%;
    padding-bottom: 100%; /* Square aspect ratio */
    overflow: hidden;
    background: #f5f5f5;
}

.collection-product-image img {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    object-fit: cover;
}

.collection-product-info {
    padding: 15px 0;
    display: flex;
    justify-content: space-between;
    align-items: flex-start;
    gap: 15px;
}

.collection-product-info-left {
    flex: 1;
}

.collection-product-title {
    margin: 0;
    font-size: 14px;
    font-weight: 400;
    line-height: 1.4;
}

.collection-product-title a {
    color: #333;
    text-decoration: none;
}

.collection-product-title a:hover {
    color: #000;
}

.collection-product-price {
    font-size: 14px;
    color: #333;
    white-space: nowrap;
}

/* Responsive */
@media (max-width: 768px) {
    .collection-products-grid {
        grid-template-columns: 1fr;
    }
}
</code></pre>
<h2 id="step-6%3A-enqueue-the-css-file" tabindex="-1"><a class="header-anchor" href="#step-6%3A-enqueue-the-css-file">#</a> Step 6: Enqueue the CSS File</h2>
<p>Add this function to <code>collection-products-register.php</code>:</p>
<pre tabindex="0" class="hljs"><code class="language-php">
/**
 * Load widget styles.
 *
 * @return void
 */
function collection_products_widget_styles() {
    wp_enqueue_style(
        'collection-products-widget-style',
        get_stylesheet_directory_uri() . '/css/collection.css',
        [],
        HELLO_BIZ_CHILD_VERSION
    );
}
add_action( 'wp_enqueue_scripts', 'collection_products_widget_styles', 20 );
</code></pre>
<h2 id="step-7%3A-test-product-display" tabindex="-1"><a class="header-anchor" href="#step-7%3A-test-product-display">#</a> Step 7: Test Product Display</h2>
<ol>
<li>Edit your test page in Elementor</li>
<li>The widget should now display products in a grid</li>
<li>Try different settings:
<ul>
<li>Change products per page</li>
<li>Change order by</li>
<li>Select specific categories</li>
</ul>
</li>
<li>View the page on the frontend to see the live result</li>
</ol>
<p><img fetchpriority="high" decoding="async" class="alignnone size-full wp-image-996" src="/wp-content/uploads/2025/11/part4-products-list.webp" alt="" width="1920" height="893" /><br />
<em>WooCommerce products displayed in the admin</em></p>
<h2 id="step-8%3A-handle-special-cases" tabindex="-1"><a class="header-anchor" href="#step-8%3A-handle-special-cases">#</a> Step 8: Handle Special Cases</h2>
<h3 id="handle-products-without-images" tabindex="-1"><a class="header-anchor" href="#handle-products-without-images">#</a> Handle Products Without Images</h3>
<p>Update the image display code:</p>
<pre tabindex="0" class="hljs"><code class="language-php">
&lt;div class="collection-product-image"&gt;
    &lt;?php
    $image_url = get_the_post_thumbnail_url( $product_id, 'full' );
    if ( $image_url ) :
    ?&gt;
        &lt;img src="&lt;?php echo esc_url( $image_url ); ?&gt;"
             alt="&lt;?php echo esc_attr( get_the_title() ); ?&gt;"&gt;
    &lt;?php else : ?&gt;
        &lt;div class="no-image-placeholder"&gt;
            &lt;span&gt;&lt;?php _e( 'No Image', 'hello-biz-child' ); ?&gt;&lt;/span&gt;
        &lt;/div&gt;
    &lt;?php endif; ?&gt;
&lt;/div&gt;
</code></pre>
<p>Add to CSS:</p>
<pre tabindex="0" class="hljs"><code class="language-css">
.no-image-placeholder {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    display: flex;
    align-items: center;
    justify-content: center;
    background: #f0f0f0;
    color: #999;
}
</code></pre>
<h3 id="handle-variable-products" tabindex="-1"><a class="header-anchor" href="#handle-variable-products">#</a> Handle Variable Products</h3>
<p>Check if product is variable and show starting price:</p>
<pre tabindex="0" class="hljs"><code class="language-php">
&lt;div class="collection-product-price"&gt;
    &lt;?php
    if ( $product-&gt;is_type( 'variable' ) ) {
        echo __( 'From ', 'hello-biz-child' );
    }
    echo $product-&gt;get_price_html();
    ?&gt;
&lt;/div&gt;
</code></pre>
<h2 id="complete-render()-method" tabindex="-1"><a class="header-anchor" href="#complete-render()-method">#</a> Complete render() Method</h2>
<p>Here’s the complete <code>render()</code> method with all improvements:</p>
<pre tabindex="0" class="hljs"><code class="language-php">
&lt;?php
protected function render() {
    $settings = $this-&gt;get_settings_for_display();

    $args = [
        'post_type' =&gt; 'product',
        'posts_per_page' =&gt; $settings['products_per_page'],
        'orderby' =&gt; $settings['orderby'],
        'order' =&gt; $settings['order'],
        'post_status' =&gt; 'publish',
    ];

    // Add category filter
    if ( ! empty( $settings['category'] ) ) {
        $args['tax_query'] = [
            [
                'taxonomy' =&gt; 'product_cat',
                'field' =&gt; 'term_id',
                'terms' =&gt; $settings['category'],
            ],
        ];
    }

    $query = new WP_Query( $args );

    if ( $query-&gt;have_posts() ) {
        ?&gt;
        &lt;div class="collection-products-wrapper"&gt;
            &lt;div class="collection-products-grid"&gt;
                &lt;?php
                while ( $query-&gt;have_posts() ) {
                    $query-&gt;the_post();
                    global $product;

                    $product_id = get_the_ID();
                    ?&gt;
                    &lt;div class="collection-product-item"&gt;
                        &lt;a href="&lt;?php echo esc_url( get_permalink() ); ?&gt;" class="collection-product-link"&gt;
                            &lt;div class="collection-product-image"&gt;
                                &lt;?php
                                $image_url = get_the_post_thumbnail_url( $product_id, 'full' );
                                if ( $image_url ) :
                                ?&gt;
                                    &lt;img src="&lt;?php echo esc_url( $image_url ); ?&gt;"
                                         alt="&lt;?php echo esc_attr( get_the_title() ); ?&gt;"&gt;
                                &lt;?php else : ?&gt;
                                    &lt;div class="no-image-placeholder"&gt;
                                        &lt;span&gt;&lt;?php _e( 'No Image', 'hello-biz-child' ); ?&gt;&lt;/span&gt;
                                    &lt;/div&gt;
                                &lt;?php endif; ?&gt;
                            &lt;/div&gt;
                        &lt;/a&gt;
                        
                        &lt;div class="collection-product-info"&gt;
                            &lt;div class="collection-product-info-left"&gt;
                                &lt;h3 class="collection-product-title"&gt;
                                    &lt;a href="&lt;?php echo esc_url( get_permalink() ); ?&gt;"&gt;
                                        &lt;?php the_title(); ?&gt;
                                    &lt;/a&gt;
                                &lt;/h3&gt;
                            &lt;/div&gt;
                            
                            &lt;div class="collection-product-price"&gt;
                                &lt;?php echo $product-&gt;get_price_html(); ?&gt;
                            &lt;/div&gt;
                        &lt;/div&gt;
                    &lt;/div&gt;
                    &lt;?php
                }
                ?&gt;
            &lt;/div&gt;
        &lt;/div&gt;
        &lt;?php
        wp_reset_postdata();
    } else {
        echo '&lt;p&gt;' . __( 'No products found.', 'hello-biz-child' ) . '&lt;/p&gt;';
    }
}
</code></pre>
<h2 id="common-issues-and-solutions" tabindex="-1"><a class="header-anchor" href="#common-issues-and-solutions">#</a> Common Issues and Solutions</h2>
<h3 id="issue-1%3A-products-not-showing" tabindex="-1"><a class="header-anchor" href="#issue-1%3A-products-not-showing">#</a> Issue 1: Products Not Showing</h3>
<p><strong>Solutions:</strong></p>
<ul>
<li>Verify you have published products in WooCommerce</li>
<li>Check that product visibility is set to “Catalog and search”</li>
<li>Ensure products are in the selected categories</li>
<li>Check <code>post_status</code> is ‘publish’</li>
</ul>
<h3 id="issue-2%3A-wrong-products-showing" tabindex="-1"><a class="header-anchor" href="#issue-2%3A-wrong-products-showing">#</a> Issue 2: Wrong Products Showing</h3>
<p><strong>Solutions:</strong></p>
<ul>
<li>Verify <code>orderby</code> and <code>order</code> settings</li>
<li>Check category filter is working correctly</li>
<li>Ensure no other plugins are filtering the query</li>
</ul>
<h3 id="issue-3%3A-images-not-displaying" tabindex="-1"><a class="header-anchor" href="#issue-3%3A-images-not-displaying">#</a> Issue 3: Images Not Displaying</h3>
<p><strong>Solutions:</strong></p>
<ul>
<li>Verify products have featured images set</li>
<li>Check image URLs in browser console</li>
<li>Ensure file permissions are correct</li>
<li>Try regenerating thumbnails</li>
</ul>
<h3 id="issue-4%3A-prices-not-showing-correctly" tabindex="-1"><a class="header-anchor" href="#issue-4%3A-prices-not-showing-correctly">#</a> Issue 4: Prices Not Showing Correctly</h3>
<p><strong>Solutions:</strong></p>
<ul>
<li>Ensure WooCommerce currency settings are configured</li>
<li>Verify products have prices set</li>
<li>Check for theme conflicts with price display</li>
</ul>
<h2 id="summary" tabindex="-1"><a class="header-anchor" href="#summary">#</a> Summary</h2>
<p>You’ve successfully integrated WooCommerce products into your widget:</p>
<ul>
<li>Query products with WP_Query</li>
<li>Access product data via WooCommerce API</li>
<li>Display product images, titles, and prices</li>
<li>Handle products without images</li>
<li>Create responsive grid layout</li>
<li>Added basic styling</li>
</ul>
<h2 id="what%E2%80%99s-next%3F" tabindex="-1"><a class="header-anchor" href="#what%E2%80%99s-next%3F">#</a> What’s Next?</h2>
<p>In Part 5: Implementing Custom Product Meta Fields, we’ll:</p>
<ul>
<li>Add custom collection image field to products</li>
<li>Integrate WordPress media library uploader</li>
<li>Add product orientation field</li>
<li>Save and retrieve custom meta data</li>
<li>Update frontend to display custom images</li>
</ul>
<hr />
<p><strong>Previous:</strong> <a href="/blog/elementor-widget-part-3-building-the-basic-widget-structure/">← Part 3: Building the Basic Widget Structure</a><br />
<strong>Next:</strong> Part 5: Implementing Custom Product Meta Fields →</p>
]]></content:encoded>
					
					<wfw:commentRss>https://sajdoko.com/blog/elementor-widget-part-4-woocommerce-product-integration/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Elementor Widget Part 3: Building the Basic Widget Structure</title>
		<link>https://sajdoko.com/blog/elementor-widget-part-3-building-the-basic-widget-structure/</link>
					<comments>https://sajdoko.com/blog/elementor-widget-part-3-building-the-basic-widget-structure/#respond</comments>
		
		<dc:creator><![CDATA[sajdoko]]></dc:creator>
		<pubDate>Tue, 11 Nov 2025 10:37:34 +0000</pubDate>
				<category><![CDATA[Code]]></category>
		<category><![CDATA[Development]]></category>
		<category><![CDATA[Elementor]]></category>
		<category><![CDATA[WooCommerce]]></category>
		<category><![CDATA[WordPress]]></category>
		<category><![CDATA[Elementor Widget]]></category>
		<guid isPermaLink="false">https://sajdoko.al/blog/elementor-widget-part-3-building-the-basic-widget-structure/</guid>

					<description><![CDATA[Introduction We’ll now create the Collection Products widget files, add its controls and register it with Elementor. # Learning Objectives By the end of this&#8230;]]></description>
										<content:encoded><![CDATA[<h2 id="introduction" tabindex="-1"><a class="header-anchor" href="#introduction">#</a> Introduction</h2>
<p>We’ll now create the Collection Products widget files, add its controls and register it with Elementor.</p>
<h2 id="learning-objectives" tabindex="-1"><a class="header-anchor" href="#learning-objectives">#</a> Learning Objectives</h2>
<p>By the end of this tutorial, you will:</p>
<ul>
<li>Create the widget PHP files in the correct structure</li>
<li>Implement a complete widget class with all required methods</li>
<li>Register the widget with Elementor</li>
<li>Add basic query controls (products per page, order, category)</li>
<li>Add display controls (show/hide filters and sorting)</li>
<li>Test the widget in the Elementor editor</li>
<li>Understand how to organize widget files</li>
</ul>
<h2 id="step-1%3A-create-widget-files" tabindex="-1"><a class="header-anchor" href="#step-1%3A-create-widget-files">#</a> Step 1: Create Widget Files</h2>
<p>We’ll create two PHP files to keep our code organized:</p>
<ol>
<li><strong>collection-products-widget.php</strong> &#8211; The main widget class</li>
<li><strong>collection-products-register.php</strong> &#8211; Registration and helper functions</li>
</ol>
<h3 id="1.1-create-the-widget-class-file" tabindex="-1"><a class="header-anchor" href="#1.1-create-the-widget-class-file">#</a> 1.1 Create the Widget Class File</h3>
<p>Create a new file at <code>wp-content/themes/hello-biz-child/widgets/collection-products/collection-products-widget.php</code>:</p>
<pre tabindex="0"><code class="language-php">
&lt;?php
/**
 * Collection Products Widget for Elementor
 */

if ( ! defined( 'ABSPATH' ) ) {
    exit; // Exit if accessed directly.
}

class Collection_Products_Widget extends \Elementor\Widget_Base {

    /**
     * Get widget name.
     *
     * @return string Widget name.
     */
    public function get_name() {
        return 'collection_products';
    }

    /**
     * Get widget title.
     *
     * @return string Widget title.
     */
    public function get_title() {
        return __( 'Collection Products', 'hello-biz-child' );
    }

    /**
     * Get widget icon.
     *
     * @return string Widget icon.
     */
    public function get_icon() {
        return 'eicon-products';
    }

    /**
     * Get widget categories.
     *
     * @return array Widget categories.
     */
    public function get_categories() {
        return [ 'woocommerce-elements' ];
    }

    /**
     * Get widget keywords.
     *
     * @return array Widget keywords.
     */
    public function get_keywords() {
        return [ 'woocommerce', 'shop', 'store', 'products', 'collection' ];
    }

    /**
     * Register widget controls.
     */
    protected function register_controls() {
        // We'll add controls in the next steps
    }

    /**
     * Render widget output on the frontend.
     */
    protected function render() {
        echo '&lt;div class="collection-products-wrapper"&gt;';
        echo '&lt;p&gt;' . __( 'Collection Products Widget - Coming Soon!', 'hello-biz-child' ) . '&lt;/p&gt;';
        echo '&lt;/div&gt;';
    }
}
</code></pre>
<p><strong>Code Explanation:</strong></p>
<ul>
<li><strong>Security Check</strong>: <code>if ( ! defined( 'ABSPATH' ) )</code> prevents direct file access</li>
<li><strong>Class Declaration</strong>: Extends <code>\Elementor\Widget_Base</code></li>
<li><strong>get_name()</strong>: Returns unique identifier <code>collection_products</code></li>
<li><strong>get_title()</strong>: Returns display name for the widget panel</li>
<li><strong>get_icon()</strong>: Uses Elementor’s products icon</li>
<li><strong>get_categories()</strong>: Places widget in WooCommerce elements category</li>
<li><strong>get_keywords()</strong>: Helps users find the widget via search</li>
<li><strong>register_controls()</strong>: Empty for now, we’ll add controls next</li>
<li><strong>render()</strong>: Simple placeholder output</li>
</ul>
<h2 id="step-2%3A-create-registration-file" tabindex="-1"><a class="header-anchor" href="#step-2%3A-create-registration-file">#</a> Step 2: Create Registration File</h2>
<p>Create <code>wp-content/themes/hello-biz-child/widgets/collection-products/collection-products-register.php</code>:</p>
<pre tabindex="0"><code class="language-php">
&lt;?php
/**
 * Register Elementor Collection Products Widget
 */

/**
 * Register the widget with Elementor.
 *
 * @param \Elementor\Widgets_Manager $widgets_manager Elementor widgets manager.
 * @return void
 */
function register_collection_product_widget( $widgets_manager ) {
    require_once( __DIR__ . '/collection-products-widget.php' );
    $widgets_manager-&gt;register( new \Collection_Products_Widget() );
}
add_action( 'elementor/widgets/register', 'register_collection_product_widget' );
</code></pre>
<p><strong>Code Explanation:</strong></p>
<ul>
<li><strong>Function</strong>: <code>register_collection_product_widget()</code> handles registration</li>
<li><strong>require_once</strong>: Loads the widget class file</li>
<li><strong>register()</strong>: Registers a new instance of our widget</li>
<li><strong>Hook</strong>: <code>elementor/widgets/register</code> is fired when Elementor loads widgets</li>
</ul>
<h2 id="step-3%3A-include-registration-file-in-functions.php" tabindex="-1"><a class="header-anchor" href="#step-3%3A-include-registration-file-in-functions.php">#</a> Step 3: Include Registration File in functions.php</h2>
<p>Open <code>wp-content/themes/hello-biz-child/functions.php</code> and add at the bottom:</p>
<pre tabindex="0"><code class="language-php">
// Include collection products widget registration
require_once get_stylesheet_directory() . '/widgets/collection-products/collection-products-register.php';
</code></pre>
<p><strong>Complete functions.php should now look like:</strong></p>
<pre tabindex="0"><code class="language-php">
&lt;?php
/**
 * Theme functions and definitions.
 *
 * @package HelloBizChild
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit; // Exit if accessed directly.
}

define( 'HELLO_BIZ_CHILD_VERSION', '1.0.0' );

/**
 * Load child theme scripts &amp; styles.
 *
 * @return void
 */
function hello_biz_child_scripts_styles() {

	wp_enqueue_style(
		'hello-biz-child-style',
		get_stylesheet_directory_uri() . '/style.css',
		[
			'theme',
		],
		HELLO_BIZ_CHILD_VERSION
	);
}

add_action( 'wp_enqueue_scripts', 'hello_biz_child_scripts_styles', 20 );

// Include collection products widget registration
require_once get_stylesheet_directory() . '/widgets/collection-products/collection-products-register.php';
</code></pre>
<h2 id="step-4%3A-test-basic-widget" tabindex="-1"><a class="header-anchor" href="#step-4%3A-test-basic-widget">#</a> Step 4: Test Basic Widget</h2>
<p>Before adding more features, let’s test that the widget loads correctly.</p>
<h3 id="4.1-clear-cache" tabindex="-1"><a class="header-anchor" href="#4.1-clear-cache">#</a> 4.1 Clear Cache</h3>
<ol>
<li>Clear any WordPress caching plugins</li>
<li>Clear browser cache</li>
<li>If using object caching, flush it</li>
</ol>
<h3 id="4.2-test-in-elementor" tabindex="-1"><a class="header-anchor" href="#4.2-test-in-elementor">#</a> 4.2 Test in Elementor</h3>
<ol>
<li>Go to <strong>Pages → Add New</strong></li>
<li>Give the page a title (e.g., “Widget Test”)</li>
<li>Click <strong>Edit with Elementor</strong></li>
<li>In the left panel, search for “Collection Products”</li>
<li>Drag the widget onto the page</li>
<li>You should see “Collection Products Widget &#8211; Coming Soon!”</li>
</ol>
<p><img decoding="async" class="alignnone size-full wp-image-981" src="/wp-content/uploads/2025/11/part3-elementor-editor.webp" alt="" width="1920" height="893" /><br />
<em>Collection Products widget in the Elementor editor</em></p>
<p><strong>Troubleshooting:</strong></p>
<p>If the widget doesn’t appear:</p>
<ul>
<li>Check for PHP errors (enable <code>WP_DEBUG</code> in wp-config.php)</li>
<li>Verify file paths are correct</li>
<li>Ensure WooCommerce is active</li>
<li>Check that all files are saved properly</li>
</ul>
<h2 id="step-5%3A-add-query-controls" tabindex="-1"><a class="header-anchor" href="#step-5%3A-add-query-controls">#</a> Step 5: Add Query Controls</h2>
<p>Now let’s add controls to configure product queries. Update the <code>register_controls()</code> method in <code>collection-products-widget.php</code>:</p>
<pre tabindex="0"><code class="language-php">
protected function register_controls() {

    // Query Section
    $this-&gt;start_controls_section(
        'section_query',
        [
            'label' =&gt; __( 'Query', 'hello-biz-child' ),
        ]
    );

    $this-&gt;add_control(
        'products_per_page',
        [
            'label' =&gt; __( 'Products Per Page', 'hello-biz-child' ),
            'type' =&gt; \Elementor\Controls_Manager::NUMBER,
            'default' =&gt; 9,
            'min' =&gt; 3,
            'step' =&gt; 3,
            'description' =&gt; __( 'Number of products to display', 'hello-biz-child' ),
        ]
    );

    $this-&gt;add_control(
        'orderby',
        [
            'label' =&gt; __( 'Order By', 'hello-biz-child' ),
            'type' =&gt; \Elementor\Controls_Manager::SELECT,
            'default' =&gt; 'date',
            'options' =&gt; [
                'date' =&gt; __( 'Date', 'hello-biz-child' ),
                'title' =&gt; __( 'Title', 'hello-biz-child' ),
                'price' =&gt; __( 'Price', 'hello-biz-child' ),
                'popularity' =&gt; __( 'Popularity', 'hello-biz-child' ),
                'rating' =&gt; __( 'Rating', 'hello-biz-child' ),
                'rand' =&gt; __( 'Random', 'hello-biz-child' ),
                'menu_order' =&gt; __( 'Menu Order', 'hello-biz-child' ),
            ],
        ]
    );

    $this-&gt;add_control(
        'order',
        [
            'label' =&gt; __( 'Order', 'hello-biz-child' ),
            'type' =&gt; \Elementor\Controls_Manager::SELECT,
            'default' =&gt; 'DESC',
            'options' =&gt; [
                'DESC' =&gt; __( 'Descending', 'hello-biz-child' ),
                'ASC' =&gt; __( 'Ascending', 'hello-biz-child' ),
            ],
        ]
    );

    $this-&gt;add_control(
        'category',
        [
            'label' =&gt; __( 'Category', 'hello-biz-child' ),
            'type' =&gt; \Elementor\Controls_Manager::SELECT2,
            'multiple' =&gt; true,
            'options' =&gt; $this-&gt;get_product_categories(),
            'label_block' =&gt; true,
            'description' =&gt; __( 'Select categories to filter products', 'hello-biz-child' ),
        ]
    );

    $this-&gt;add_control(
        'show_filters',
        [
            'label' =&gt; __( 'Show Filters', 'hello-biz-child' ),
            'type' =&gt; \Elementor\Controls_Manager::SWITCHER,
            'label_on' =&gt; __( 'Yes', 'hello-biz-child' ),
            'label_off' =&gt; __( 'No', 'hello-biz-child' ),
            'return_value' =&gt; 'yes',
            'default' =&gt; 'yes',
        ]
    );

    $this-&gt;add_control(
        'show_sorting',
        [
            'label' =&gt; __( 'Show Sorting', 'hello-biz-child' ),
            'type' =&gt; \Elementor\Controls_Manager::SWITCHER,
            'label_on' =&gt; __( 'Yes', 'hello-biz-child' ),
            'label_off' =&gt; __( 'No', 'hello-biz-child' ),
            'return_value' =&gt; 'yes',
            'default' =&gt; 'yes',
        ]
    );

    $this-&gt;end_controls_section();
}
</code></pre>
<p><strong>Code Explanation:</strong></p>
<ul>
<li><strong>Products Per Page</strong>: Number control with default of 9, minimum 3, step of 3</li>
<li><strong>Order By</strong>: Dropdown with common sorting options</li>
<li><strong>Order</strong>: ASC or DESC</li>
<li><strong>Category</strong>: Multi-select dropdown of product categories</li>
<li><strong>Show Filters</strong>: Toggle to show/hide filter panel</li>
<li><strong>Show Sorting</strong>: Toggle to show/hide sorting dropdown</li>
</ul>
<h2 id="step-6%3A-add-helper-method-for-categories" tabindex="-1"><a class="header-anchor" href="#step-6%3A-add-helper-method-for-categories">#</a> Step 6: Add Helper Method for Categories</h2>
<p>Add this helper method to the widget class (before or after <code>register_controls()</code>):</p>
<pre tabindex="0"><code class="language-php">
/**
 * Get product categories for control options.
 *
 * @return array Categories array.
 */
protected function get_product_categories() {
    $categories = get_terms( 'product_cat', [
        'hide_empty' =&gt; false,
    ] );

    $options = [];
    
    if ( ! empty( $categories ) &amp;&amp; ! is_wp_error( $categories ) ) {
        foreach ( $categories as $category ) {
            $options[ $category-&gt;term_id ] = $category-&gt;name;
        }
    }

    return $options;
}
</code></pre>
<p><strong>Code Explanation:</strong></p>
<ul>
<li><strong>get_terms()</strong>: Retrieves WooCommerce product categories</li>
<li><strong>hide_empty =&gt; false</strong>: Include categories with no products</li>
<li><strong>Returns</strong>: Array formatted for SELECT2 control</li>
</ul>
<h2 id="step-7%3A-add-style-controls" tabindex="-1"><a class="header-anchor" href="#step-7%3A-add-style-controls">#</a> Step 7: Add Style Controls</h2>
<p>After the query section, add a style section:</p>
<pre tabindex="0"><code class="language-php">
// Style Section
$this-&gt;start_controls_section(
    'section_style',
    [
        'label' =&gt; __( 'Style', 'hello-biz-child' ),
        'tab' =&gt; \Elementor\Controls_Manager::TAB_STYLE,
    ]
);

$this-&gt;add_control(
    'gap',
    [
        'label' =&gt; __( 'Gap', 'hello-biz-child' ),
        'type' =&gt; \Elementor\Controls_Manager::SLIDER,
        'size_units' =&gt; [ 'px' ],
        'range' =&gt; [
            'px' =&gt; [
                'min' =&gt; 0,
                'max' =&gt; 50,
                'step' =&gt; 1,
            ],
        ],
        'default' =&gt; [
            'unit' =&gt; 'px',
            'size' =&gt; 10,
        ],
        'selectors' =&gt; [
            '{{WRAPPER}} .collection-products-grid' =&gt; 'gap: {{SIZE}}{{UNIT}};',
        ],
    ]
);

$this-&gt;end_controls_section();
</code></pre>
<p><strong>Code Explanation:</strong></p>
<ul>
<li><strong>TAB_STYLE</strong>: Places this section in the Style tab</li>
<li><strong>SLIDER</strong>: Creates a range slider control</li>
<li><strong>selectors</strong>: Automatically applies CSS when slider changes</li>
<li><strong>{{WRAPPER}}</strong>: Replaced with unique widget selector</li>
<li><strong>{{SIZE}}{{UNIT}}</strong>: Replaced with selected value (e.g., “10px”)</li>
</ul>
<h2 id="step-8%3A-update-render-method" tabindex="-1"><a class="header-anchor" href="#step-8%3A-update-render-method">#</a> Step 8: Update Render Method</h2>
<p>Update the <code>render()</code> method to display settings:</p>
<pre tabindex="0"><code class="language-php">
protected function render() {
    $settings = $this-&gt;get_settings_for_display();
    ?&gt;
    &lt;div class="collection-products-wrapper"&gt;
        &lt;h3&gt;Collection Products Widget&lt;/h3&gt;
        &lt;p&gt;&lt;strong&gt;Settings:&lt;/strong&gt;&lt;/p&gt;
        &lt;ul&gt;
            &lt;li&gt;Products per page: &lt;?php echo esc_html( $settings['products_per_page'] ); ?&gt;&lt;/li&gt;
            &lt;li&gt;Order by: &lt;?php echo esc_html( $settings['orderby'] ); ?&gt;&lt;/li&gt;
            &lt;li&gt;Order: &lt;?php echo esc_html( $settings['order'] ); ?&gt;&lt;/li&gt;
            &lt;li&gt;Show filters: &lt;?php echo esc_html( $settings['show_filters'] ); ?&gt;&lt;/li&gt;
            &lt;li&gt;Show sorting: &lt;?php echo esc_html( $settings['show_sorting'] ); ?&gt;&lt;/li&gt;
        &lt;/ul&gt;
    &lt;/div&gt;
    &lt;?php
}
</code></pre>
<p>This temporary render method helps us verify that settings are working correctly.</p>
<h2 id="step-9%3A-test-the-controls" tabindex="-1"><a class="header-anchor" href="#step-9%3A-test-the-controls">#</a> Step 9: Test the Controls</h2>
<ol>
<li>Refresh your Elementor editor page (or edit a new page)</li>
<li>Add/re-add the Collection Products widget</li>
<li>In the left panel, you should see:
<ul>
<li><strong>Content tab</strong> with Query section</li>
<li><strong>Style tab</strong> with Style section</li>
</ul>
</li>
<li>Try changing values:
<ul>
<li>Set “Products Per Page” to 12</li>
<li>Change “Order By” to “Price”</li>
<li>Toggle “Show Filters” off and on</li>
</ul>
</li>
<li>Verify the values appear in the preview</li>
</ol>
<h2 id="complete-widget-code-so-far" tabindex="-1"><a class="header-anchor" href="#complete-widget-code-so-far">#</a> Complete Widget Code So Far</h2>
<p>Here’s what your <code>collection-products-widget.php</code> should look like at this point:</p>
<pre tabindex="0"><code class="language-php">
&lt;?php
/**
 * Collection Products Widget for Elementor
 */

if ( ! defined( 'ABSPATH' ) ) {
    exit; // Exit if accessed directly.
}

class Collection_Products_Widget extends \Elementor\Widget_Base {

    public function get_name() {
        return 'collection_products';
    }

    public function get_title() {
        return __( 'Collection Products', 'hello-biz-child' );
    }

    public function get_icon() {
        return 'eicon-products';
    }

    public function get_categories() {
        return [ 'woocommerce-elements' ];
    }

    public function get_keywords() {
        return [ 'woocommerce', 'shop', 'store', 'products', 'collection' ];
    }

    protected function register_controls() {

        $this-&gt;start_controls_section(
            'section_query',
            [
                'label' =&gt; __( 'Query', 'hello-biz-child' ),
            ]
        );

        $this-&gt;add_control(
            'products_per_page',
            [
                'label' =&gt; __( 'Products Per Page', 'hello-biz-child' ),
                'type' =&gt; \Elementor\Controls_Manager::NUMBER,
                'default' =&gt; 9,
                'min' =&gt; 3,
                'step' =&gt; 3,
            ]
        );

        $this-&gt;add_control(
            'orderby',
            [
                'label' =&gt; __( 'Order By', 'hello-biz-child' ),
                'type' =&gt; \Elementor\Controls_Manager::SELECT,
                'default' =&gt; 'date',
                'options' =&gt; [
                    'date' =&gt; __( 'Date', 'hello-biz-child' ),
                    'title' =&gt; __( 'Title', 'hello-biz-child' ),
                    'price' =&gt; __( 'Price', 'hello-biz-child' ),
                    'popularity' =&gt; __( 'Popularity', 'hello-biz-child' ),
                    'rating' =&gt; __( 'Rating', 'hello-biz-child' ),
                    'rand' =&gt; __( 'Random', 'hello-biz-child' ),
                    'menu_order' =&gt; __( 'Menu Order', 'hello-biz-child' ),
                ],
            ]
        );

        $this-&gt;add_control(
            'order',
            [
                'label' =&gt; __( 'Order', 'hello-biz-child' ),
                'type' =&gt; \Elementor\Controls_Manager::SELECT,
                'default' =&gt; 'DESC',
                'options' =&gt; [
                    'DESC' =&gt; __( 'Descending', 'hello-biz-child' ),
                    'ASC' =&gt; __( 'Ascending', 'hello-biz-child' ),
                ],
            ]
        );

        $this-&gt;add_control(
            'category',
            [
                'label' =&gt; __( 'Category', 'hello-biz-child' ),
                'type' =&gt; \Elementor\Controls_Manager::SELECT2,
                'multiple' =&gt; true,
                'options' =&gt; $this-&gt;get_product_categories(),
                'label_block' =&gt; true,
            ]
        );

        $this-&gt;add_control(
            'show_filters',
            [
                'label' =&gt; __( 'Show Filters', 'hello-biz-child' ),
                'type' =&gt; \Elementor\Controls_Manager::SWITCHER,
                'label_on' =&gt; __( 'Yes', 'hello-biz-child' ),
                'label_off' =&gt; __( 'No', 'hello-biz-child' ),
                'return_value' =&gt; 'yes',
                'default' =&gt; 'yes',
            ]
        );

        $this-&gt;add_control(
            'show_sorting',
            [
                'label' =&gt; __( 'Show Sorting', 'hello-biz-child' ),
                'type' =&gt; \Elementor\Controls_Manager::SWITCHER,
                'label_on' =&gt; __( 'Yes', 'hello-biz-child' ),
                'label_off' =&gt; __( 'No', 'hello-biz-child' ),
                'return_value' =&gt; 'yes',
                'default' =&gt; 'yes',
            ]
        );

        $this-&gt;end_controls_section();

        // Style Section
        $this-&gt;start_controls_section(
            'section_style',
            [
                'label' =&gt; __( 'Style', 'hello-biz-child' ),
                'tab' =&gt; \Elementor\Controls_Manager::TAB_STYLE,
            ]
        );

        $this-&gt;add_control(
            'gap',
            [
                'label' =&gt; __( 'Gap', 'hello-biz-child' ),
                'type' =&gt; \Elementor\Controls_Manager::SLIDER,
                'size_units' =&gt; [ 'px' ],
                'range' =&gt; [
                    'px' =&gt; [
                        'min' =&gt; 0,
                        'max' =&gt; 50,
                        'step' =&gt; 1,
                    ],
                ],
                'default' =&gt; [
                    'unit' =&gt; 'px',
                    'size' =&gt; 10,
                ],
                'selectors' =&gt; [
                    '{{WRAPPER}} .collection-products-grid' =&gt; 'gap: {{SIZE}}{{UNIT}};',
                ],
            ]
        );

        $this-&gt;end_controls_section();
    }

    protected function get_product_categories() {
        $categories = get_terms( 'product_cat', [
            'hide_empty' =&gt; false,
        ] );

        $options = [];
        foreach ( $categories as $category ) {
            $options[ $category-&gt;term_id ] = $category-&gt;name;
        }

        return $options;
    }

    protected function render() {
        $settings = $this-&gt;get_settings_for_display();
        ?&gt;
        &lt;div class="collection-products-wrapper"&gt;
            &lt;h3&gt;Collection Products Widget&lt;/h3&gt;
            &lt;p&gt;<strong>Settings:</strong>&lt;/p&gt;
            &lt;ul&gt;
                &lt;li&gt;Products per page: &lt;?php echo esc_html( $settings['products_per_page'] ); ?&gt;&lt;/li&gt;
                &lt;li&gt;Order by: &lt;?php echo esc_html( $settings['orderby'] ); ?&gt;&lt;/li&gt;
                &lt;li&gt;Order: &lt;?php echo esc_html( $settings['order'] ); ?&gt;&lt;/li&gt;
                &lt;li&gt;Show filters: &lt;?php echo esc_html( $settings['show_filters'] ); ?&gt;&lt;/li&gt;
                &lt;li&gt;Show sorting: &lt;?php echo esc_html( $settings['show_sorting'] ); ?&gt;&lt;/li&gt;
            &lt;/ul&gt;
        &lt;/div&gt;
        &lt;?php
    }
}
</code></pre>
<h2 id="common-issues-and-solutions" tabindex="-1"><a class="header-anchor" href="#common-issues-and-solutions">#</a> Common Issues and Solutions</h2>
<h3 id="issue-1%3A-widget-not-appearing-in-panel" tabindex="-1"><a class="header-anchor" href="#issue-1%3A-widget-not-appearing-in-panel">#</a> Issue 1: Widget Not Appearing in Panel</h3>
<p><strong>Solutions:</strong></p>
<ul>
<li>Verify <code>get_categories()</code> returns a valid category</li>
<li>Check that WooCommerce is active (required for ‘woocommerce-elements’ category)</li>
<li>Clear Elementor cache: Elementor → Tools → Regenerate CSS</li>
<li>Check PHP error logs</li>
</ul>
<h3 id="issue-2%3A-controls-not-showing" tabindex="-1"><a class="header-anchor" href="#issue-2%3A-controls-not-showing">#</a> Issue 2: Controls Not Showing</h3>
<p><strong>Solutions:</strong></p>
<ul>
<li>Verify <code>register_controls()</code> is called correctly</li>
<li>Check for PHP syntax errors</li>
<li>Ensure <code>start_controls_section()</code> is matched with <code>end_controls_section()</code></li>
<li>Verify control IDs are unique</li>
</ul>
<h3 id="issue-3%3A-category-dropdown-empty" tabindex="-1"><a class="header-anchor" href="#issue-3%3A-category-dropdown-empty">#</a> Issue 3: Category Dropdown Empty</h3>
<p><strong>Solutions:</strong></p>
<ul>
<li>Create some product categories in WooCommerce</li>
<li>Check <code>get_product_categories()</code> method</li>
<li>Verify WooCommerce is installed and active</li>
</ul>
<h3 id="issue-4%3A-settings-not-updating-in-preview" tabindex="-1"><a class="header-anchor" href="#issue-4%3A-settings-not-updating-in-preview">#</a> Issue 4: Settings Not Updating in Preview</h3>
<p><strong>Solutions:</strong></p>
<ul>
<li>Refresh the Elementor editor</li>
<li>Check browser console for JavaScript errors</li>
<li>Verify <code>get_settings_for_display()</code> is used in <code>render()</code></li>
</ul>
<h2 id="summary" tabindex="-1"><a class="header-anchor" href="#summary">#</a> Summary</h2>
<p>The Collection Products widget now includes:</p>
<ul>
<li>Created widget class file</li>
<li>Created registration file</li>
<li>Registered widget with Elementor</li>
<li>Added query controls</li>
<li>Added style controls</li>
<li>Tested widget in Elementor editor</li>
</ul>
<p>Check that the widget appears in Elementor and that its settings update the preview.</p>
<h2 id="what%E2%80%99s-next%3F" tabindex="-1"><a class="header-anchor" href="#what%E2%80%99s-next%3F">#</a> What’s Next?</h2>
<p>In Part 4: WooCommerce Product Integration, we’ll:</p>
<ul>
<li>Query WooCommerce products using WP_Query</li>
<li>Access and display product data</li>
<li>Work with product categories and taxonomies</li>
<li>Display product prices and images</li>
<li>Handle product variations</li>
<li>Create the initial product grid layout</li>
</ul>
<hr />
<p><strong>Previous:</strong> <a href="/blog/elementor-widget-part-2-understanding-elementor-widget-architecture/">← Part 2: Understanding Elementor Widget Architecture</a><br />
<strong>Next:</strong> <a href="/blog/elementor-widget-part-4-woocommerce-product-integration/">Part 4: WooCommerce Product Integration →</a></p>
]]></content:encoded>
					
					<wfw:commentRss>https://sajdoko.com/blog/elementor-widget-part-3-building-the-basic-widget-structure/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Elementor Widget Part 2: Understanding Elementor Widget Architecture</title>
		<link>https://sajdoko.com/blog/elementor-widget-part-2-understanding-elementor-widget-architecture/</link>
					<comments>https://sajdoko.com/blog/elementor-widget-part-2-understanding-elementor-widget-architecture/#respond</comments>
		
		<dc:creator><![CDATA[sajdoko]]></dc:creator>
		<pubDate>Mon, 10 Nov 2025 10:25:51 +0000</pubDate>
				<category><![CDATA[Code]]></category>
		<category><![CDATA[Development]]></category>
		<category><![CDATA[Elementor]]></category>
		<category><![CDATA[WooCommerce]]></category>
		<category><![CDATA[WordPress]]></category>
		<category><![CDATA[Elementor Widget]]></category>
		<guid isPermaLink="false">https://sajdoko.al/blog/elementor-widget-part-2-understanding-elementor-widget-architecture/</guid>

					<description><![CDATA[We’ll use these concepts to build the Collection Products widget in the&#8230;]]></description>
										<content:encoded><![CDATA[<h2 id="introduction" tabindex="-1"><a class="header-anchor" href="#introduction">#</a> Introduction</h2>
<p>This part explains the Elementor widget base class, required methods and lifecycle. We’ll use these concepts to build the Collection Products widget in the next part.</p>
<h2 id="learning-objectives" tabindex="-1"><a class="header-anchor" href="#learning-objectives">#</a> Learning Objectives</h2>
<p>By the end of this tutorial, you will understand:</p>
<ul>
<li style="list-style-type: none;">
<ul>
<li>The structure of an Elementor widget class</li>
<li>Required methods and their purposes</li>
<li>How widgets are registered with Elementor</li>
</ul>
</li>
</ul>
<ul>
<li>The widget lifecycle from registration to rendering</li>
<li>Widget controls and how they create the settings panel</li>
<li>How widgets render content on the frontend</li>
<li>Best practices for widget development</li>
</ul>
<h2 id="the-elementor-widget-base-class" tabindex="-1"><a class="header-anchor" href="#the-elementor-widget-base-class">#</a> The Elementor Widget Base Class</h2>
<p>All Elementor widgets extend the <code>\Elementor\Widget_Base</code> abstract class. This base class provides the foundation and structure that Elementor expects from every widget.</p>
<h3 id="key-concept%3A-object-oriented-programming" tabindex="-1"><a class="header-anchor" href="#key-concept%3A-object-oriented-programming">#</a> Key Concept: Object-Oriented Programming</h3>
<p>Elementor uses Object-Oriented Programming (OOP) principles. Your widget is a PHP class that extends (inherits from) the base widget class.</p>
<pre tabindex="0"><code class="language-php">
    class My_Custom_Widget extends \Elementor\Widget_Base {
      // Your widget code here
    }
</code></pre>
<h2 id="required-methods" tabindex="-1"><a class="header-anchor" href="#required-methods">#</a> Required Methods</h2>
<p>Every Elementor widget <strong>must</strong> implement certain methods. Let’s examine each one:</p>
<h3 id="1.-get_name()" tabindex="-1"><a class="header-anchor" href="#1.-get_name()">#</a> 1. get_name()</h3>
<p><strong>Purpose:</strong> Returns a unique identifier for your widget.</p>
<pre tabindex="0"><code class="language-php">
  public function get_name() {
    return 'collection_products';
  }
</code></pre>
<p><strong>Important Points:</strong></p>
<ul>
<li>Must be unique across all widgets</li>
<li>Use lowercase letters, numbers, and underscores only</li>
<li>No spaces or special characters</li>
<li>This name is used internally by Elementor</li>
</ul>
<h3 id="2.-get_title()" tabindex="-1"><a class="header-anchor" href="#2.-get_title()">#</a> 2. get_title()</h3>
<p><strong>Purpose:</strong> Returns the human-readable title shown in the widget panel.</p>
<pre tabindex="0"><code class="language-php">
  public function get_title() {
    return __( 'Collection Products', 'hello-biz-child' );
  }
</code></pre>
<p><strong>Important Points:</strong></p>
<ul>
<li>This is what users see in the Elementor editor</li>
<li>Use <code>__()</code> for translation support</li>
<li>Keep it clear and descriptive</li>
</ul>
<h3 id="3.-get_icon()" tabindex="-1"><a class="header-anchor" href="#3.-get_icon()">#</a> 3. get_icon()</h3>
<p><strong>Purpose:</strong> Returns the icon class that appears next to the widget name.</p>
<pre tabindex="0"><code class="language-php">
  public function get_icon() {
    return 'eicon-products';
  }
</code></pre>
<p><strong>Important Points:</strong></p>
<ul>
<li>Uses Elementor’s icon library</li>
<li>Format: <code>eicon-{icon-name}</code></li>
<li>Browse available icons in Elementor’s icon library</li>
<li>Common icons: <code>eicon-posts-grid</code>, <code>eicon-products</code>, <code>eicon-gallery-grid</code></li>
</ul>
<h3 id="4.-get_categories()" tabindex="-1"><a class="header-anchor" href="#4.-get_categories()">#</a> 4. get_categories()</h3>
<p><strong>Purpose:</strong> Defines which category panel the widget appears in.</p>
<pre tabindex="0"><code class="language-php">
  public function get_categories() {
    return [ 'woocommerce-elements' ];
  }
</code></pre>
<p><strong>Available Categories:</strong></p>
<ul>
<li><code>basic</code> &#8211; Basic widgets</li>
<li><code>general</code> &#8211; General elements</li>
<li><code>woocommerce-elements</code> &#8211; WooCommerce widgets (we’ll use this)</li>
<li><code>theme-elements</code> &#8211; Theme-specific widgets</li>
<li>You can also create custom categories</li>
</ul>
<h3 id="5.-register_controls()-(optional-but-essential)" tabindex="-1"><a class="header-anchor" href="#5.-register_controls()-(optional-but-essential)">#</a> 5. register_controls() (Optional but Essential)</h3>
<p><strong>Purpose:</strong> Defines the widget’s settings panel in the Elementor editor.</p>
<pre tabindex="0"><code class="language-php">
  protected function register_controls() {
    // Define control sections and controls here
  }
</code></pre>
<p><strong>Important Points:</strong></p>
<ul>
<li>This method is <code>protected</code> (not <code>public</code>)</li>
<li>Controls are settings users can modify in the Elementor panel</li>
<li>Controls are organized into sections</li>
<li>This is where you define all user-configurable options</li>
</ul>
<h3 id="6.-render()" tabindex="-1"><a class="header-anchor" href="#6.-render()">#</a> 6. render()</h3>
<p><strong>Purpose:</strong> Outputs the HTML for the frontend display.</p>
<pre tabindex="0"><code class="language-php">
  protected function render() {
    $settings = $this-&gt;get_settings_for_display();

    // Output your HTML here
    echo '</code></pre>
<div class="my-widget">&#8216;; echo &#8216;Hello from my widget!&#8217;; echo &#8216;</div>
<pre tabindex="0"><code class="language-php"></code></pre>
<p>&#8216;; }</p>
<pre tabindex="0"><code class="language-php"></code></pre>
<p><strong>Important Points:</strong></p>
<ul>
<li>This method is <code>protected</code></li>
<li>Use <code>$this-&gt;get_settings_for_display()</code> to access user settings</li>
<li>Output sanitized HTML</li>
<li>This code runs on the frontend when the page loads</li>
</ul>
<h2 id="widget-lifecycle" tabindex="-1"><a class="header-anchor" href="#widget-lifecycle">#</a> Widget Lifecycle</h2>
<p>Understanding when methods are called helps you structure your code effectively:</p>
<ol>
<li><strong>Widget Registration</strong>
<ul>
<li>WordPress loads</li>
<li>Elementor initializes</li>
<li>Your widget class is registered via <code>elementor/widgets/register</code> hook</li>
</ul>
</li>
<li><strong>Editor Load</strong>
<ul>
<li>User opens Elementor editor</li>
<li><code>get_name()</code>, <code>get_title()</code>, <code>get_icon()</code>, <code>get_categories()</code> are called</li>
<li>Widget appears in the panel</li>
</ul>
</li>
<li><strong>Widget Inserted</strong>
<ul>
<li>User drags widget onto page</li>
<li><code>register_controls()</code> is called</li>
<li>Control panel appears in the editor</li>
</ul>
</li>
<li><strong>Settings Changed</strong>
<ul>
<li>User modifies controls</li>
<li>Widget preview updates in editor</li>
<li><code>render()</code> is called for preview</li>
</ul>
</li>
<li><strong>Page Saved</strong>
<ul>
<li>Settings are saved to the database</li>
<li>Widget configuration is stored</li>
</ul>
</li>
<li><strong>Frontend Display</strong>
<ul>
<li>Visitor loads the page</li>
<li><code>render()</code> is called</li>
<li>HTML is output to the page</li>
</ul>
</li>
</ol>
<h2 id="understanding-controls" tabindex="-1"><a class="header-anchor" href="#understanding-controls">#</a> Understanding Controls</h2>
<p>Controls are the building blocks of your widget’s settings panel. Let’s break down how they work:</p>
<h3 id="control-sections" tabindex="-1"><a class="header-anchor" href="#control-sections">#</a> Control Sections</h3>
<p>Controls are organized into sections. A section is a collapsible panel in the editor.</p>
<pre tabindex="0"><code class="language-php">
  protected function register_controls() {
    // Start a new section
    $this-&gt;start_controls_section(
      'section_content', // Section ID (must be unique within widget)
      [
        'label' =&gt; __( 'Content', 'hello-biz-child' ), // Section title
      ]
    );

    // Add controls here

    // End the section
    $this-&gt;end_controls_section();
  }
</code></pre>
<h3 id="adding-controls" tabindex="-1"><a class="header-anchor" href="#adding-controls">#</a> Adding Controls</h3>
<p>Between <code>start_controls_section()</code> and <code>end_controls_section()</code>, you add individual controls:</p>
<pre tabindex="0"><code class="language-php">
  $this-&gt;add_control(
    'products_per_page', // Control ID (must be unique within widget)
    [
      'label' =&gt; __( 'Products Per Page', 'hello-biz-child' ),
      'type' =&gt; \Elementor\Controls_Manager::NUMBER,
      'default' =&gt; 9,
      'min' =&gt; 3,
    ]
  );
</code></pre>
<h3 id="common-control-types" tabindex="-1"><a class="header-anchor" href="#common-control-types">#</a> Common Control Types</h3>
<p><strong>NUMBER</strong> &#8211; Numeric input</p>
<pre tabindex="0" class="hljs"><code><span class="hljs-string">'type'</span> =&gt; <span class="hljs-title class_">\Elementor\Controls_Manager</span>::<span class="hljs-variable constant_">NUMBER</span>,
<span class="hljs-string">'default'</span> =&gt; <span class="hljs-number">9</span>,
<span class="hljs-string">'min'</span> =&gt; <span class="hljs-number">1</span>,
<span class="hljs-string">'max'</span> =&gt; <span class="hljs-number">100</span>,
<span class="hljs-string">'step'</span> =&gt; <span class="hljs-number">3</span>,
</code></pre>
<p><strong>SELECT</strong> &#8211; Dropdown menu</p>
<pre tabindex="0" class="hljs"><code><span class="hljs-string">'type'</span> =&gt; <span class="hljs-title class_">\Elementor\Controls_Manager</span>::<span class="hljs-variable constant_">SELECT</span>,
<span class="hljs-string">'default'</span> =&gt; <span class="hljs-string">'date'</span>,
<span class="hljs-string">'options'</span> =&gt; [
    <span class="hljs-string">'date'</span> =&gt; <span class="hljs-title function_ invoke__">__</span>( <span class="hljs-string">'Date'</span>, <span class="hljs-string">'hello-biz-child'</span> ),
    <span class="hljs-string">'title'</span> =&gt; <span class="hljs-title function_ invoke__">__</span>( <span class="hljs-string">'Title'</span>, <span class="hljs-string">'hello-biz-child'</span> ),
    <span class="hljs-string">'price'</span> =&gt; <span class="hljs-title function_ invoke__">__</span>( <span class="hljs-string">'Price'</span>, <span class="hljs-string">'hello-biz-child'</span> ),
],
</code></pre>
<p><strong>SELECT2</strong> &#8211; Multi-select dropdown</p>
<pre tabindex="0" class="hljs"><code><span class="hljs-string">'type'</span> =&gt; <span class="hljs-title class_">\Elementor\Controls_Manager</span>::<span class="hljs-variable constant_">SELECT2</span>,
<span class="hljs-string">'multiple'</span> =&gt; <span class="hljs-literal">true</span>,
<span class="hljs-string">'options'</span> =&gt; <span class="hljs-variable language_">$this</span>-&gt;<span class="hljs-title function_ invoke__">get_categories</span>(),
<span class="hljs-string">'label_block'</span> =&gt; <span class="hljs-literal">true</span>,
</code></pre>
<p><strong>SWITCHER</strong> &#8211; Toggle switch (yes/no)</p>
<pre tabindex="0" class="hljs"><code><span class="hljs-string">'type'</span> =&gt; <span class="hljs-title class_">\Elementor\Controls_Manager</span>::<span class="hljs-variable constant_">SWITCHER</span>,
<span class="hljs-string">'label_on'</span> =&gt; <span class="hljs-title function_ invoke__">__</span>( <span class="hljs-string">'Yes'</span>, <span class="hljs-string">'hello-biz-child'</span> ),
<span class="hljs-string">'label_off'</span> =&gt; <span class="hljs-title function_ invoke__">__</span>( <span class="hljs-string">'No'</span>, <span class="hljs-string">'hello-biz-child'</span> ),
<span class="hljs-string">'return_value'</span> =&gt; <span class="hljs-string">'yes'</span>,
<span class="hljs-string">'default'</span> =&gt; <span class="hljs-string">'yes'</span>,
</code></pre>
<p><strong>SLIDER</strong> &#8211; Range slider</p>
<pre tabindex="0" class="hljs"><code><span class="hljs-string">'type'</span> =&gt; <span class="hljs-title class_">\Elementor\Controls_Manager</span>::<span class="hljs-variable constant_">SLIDER</span>,
<span class="hljs-string">'size_units'</span> =&gt; [ <span class="hljs-string">'px'</span> ],
<span class="hljs-string">'range'</span> =&gt; [
    <span class="hljs-string">'px'</span> =&gt; [
        <span class="hljs-string">'min'</span> =&gt; <span class="hljs-number">0</span>,
        <span class="hljs-string">'max'</span> =&gt; <span class="hljs-number">50</span>,
        <span class="hljs-string">'step'</span> =&gt; <span class="hljs-number">1</span>,
    ],
],
<span class="hljs-string">'default'</span> =&gt; [
    <span class="hljs-string">'unit'</span> =&gt; <span class="hljs-string">'px'</span>,
    <span class="hljs-string">'size'</span> =&gt; <span class="hljs-number">10</span>,
],
</code></pre>
<p><strong>TEXT</strong> &#8211; Text input</p>
<pre tabindex="0" class="hljs"><code><span class="hljs-string">'type'</span> =&gt; <span class="hljs-title class_">\Elementor\Controls_Manager</span>::<span class="hljs-variable constant_">TEXT</span>,
<span class="hljs-string">'default'</span> =&gt; <span class="hljs-title function_ invoke__">__</span>( <span class="hljs-string">'Default text'</span>, <span class="hljs-string">'hello-biz-child'</span> ),
<span class="hljs-string">'placeholder'</span> =&gt; <span class="hljs-title function_ invoke__">__</span>( <span class="hljs-string">'Enter text'</span>, <span class="hljs-string">'hello-biz-child'</span> ),
</code></pre>
<h2 id="style-controls" tabindex="-1"><a class="header-anchor" href="#style-controls">#</a> Style Controls</h2>
<p>For styling options, use the <code>TAB_STYLE</code> tab:</p>
<pre tabindex="0"><code class="language-php">
  $this-&gt;start_controls_section(
    'section_style',
    [
      'label' =&gt; __( 'Style', 'hello-biz-child' ),
      'tab' =&gt; \Elementor\Controls_Manager::TAB_STYLE, // Style tab
    ]
  );
</code></pre>
<h3 id="using-selectors" tabindex="-1"><a class="header-anchor" href="#using-selectors">#</a> Using Selectors</h3>
<p>Selectors allow controls to directly affect CSS:</p>
<pre tabindex="0"><code class="language-php">
$this-&gt;add_control(
  'gap',
  [
    'label' =&gt; __( 'Gap', 'hello-biz-child' ),
    'type' =&gt; \Elementor\Controls_Manager::SLIDER,
    'selectors' =&gt; [
      '{{WRAPPER}} .my-grid' =&gt; 'gap: {{SIZE}}{{UNIT}};',
    ],
  ]
);
</code></pre>
<p><strong>Key Points:</strong></p>
<ul>
<li><code>{{WRAPPER}}</code> is replaced with the widget’s unique wrapper selector</li>
<li><code>{{SIZE}}</code> is replaced with the slider value</li>
<li><code>{{UNIT}}</code> is replaced with the selected unit (px, %, em, etc.)</li>
</ul>
<h2 id="accessing-settings-in-render()" tabindex="-1"><a class="header-anchor" href="#accessing-settings-in-render()">#</a> Accessing Settings in render()</h2>
<p>To use the settings in your widget:</p>
<pre tabindex="0"><code class="language-php">
  protected function render() {
      // Get settings
      $settings = $this-&gt;get_settings_for_display();
      
      // Access individual settings
      $products_per_page = $settings['products_per_page'];
      $orderby = $settings['orderby'];
      $show_filters = $settings['show_filters'];
      
      // Use in your code
      if ( $show_filters === 'yes' ) {
          // Display filters
      }
  }
</code></pre>
<h2 id="widget-registration" tabindex="-1"><a class="header-anchor" href="#widget-registration">#</a> Widget Registration</h2>
<p>To make your widget available in Elementor, you must register it:</p>
<pre tabindex="0"><code class="language-php">
function register_my_widget( $widgets_manager ) {
    require_once( __DIR__ . '/my-widget.php' );
    $widgets_manager-&gt;register( new \My_Custom_Widget() );
}
add_action( 'elementor/widgets/register', 'register_my_widget' );
</code></pre>
<p><strong>Important Points:</strong></p>
<ul>
<li>Hook into <code>elementor/widgets/register</code></li>
<li>Require/include your widget file</li>
<li>Instantiate and register your widget class</li>
</ul>
<h2 id="best-practices" tabindex="-1"><a class="header-anchor" href="#best-practices">#</a> Best Practices</h2>
<h3 id="1.-namespace-your-widget" tabindex="-1"><a class="header-anchor" href="#1.-namespace-your-widget">#</a> 1. Namespace Your Widget</h3>
<p>Avoid naming conflicts by using a unique class name:</p>
<pre tabindex="0" class="hljs"><code><span class="hljs-comment">// Good</span>
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Collection_Products_Widget</span> <span class="hljs-keyword">extends</span> \<span class="hljs-title">Elementor</span>\<span class="hljs-title">Widget_Base</span> </span>{

<span class="hljs-comment">// Better (with namespace)</span>
<span class="hljs-keyword">namespace</span> <span class="hljs-title class_">MyTheme</span>\<span class="hljs-title class_">Widgets</span>;
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Collection_Products</span> <span class="hljs-keyword">extends</span> \<span class="hljs-title">Elementor</span>\<span class="hljs-title">Widget_Base</span> </span>{
</code></pre>
<h3 id="2.-translation-ready" tabindex="-1"><a class="header-anchor" href="#2.-translation-ready">#</a> 2. Translation Ready</h3>
<p>Always use translation functions:</p>
<pre tabindex="0" class="hljs"><code><span class="hljs-comment">// Good</span>
<span class="hljs-string">'label'</span> =&gt; <span class="hljs-title function_ invoke__">__</span>( <span class="hljs-string">'Products Per Page'</span>, <span class="hljs-string">'hello-biz-child'</span> ),

<span class="hljs-comment">// Bad</span>
<span class="hljs-string">'label'</span> =&gt; <span class="hljs-string">'Products Per Page'</span>,
</code></pre>
<h3 id="3.-sanitize-output" tabindex="-1"><a class="header-anchor" href="#3.-sanitize-output">#</a> 3. Sanitize Output</h3>
<p>Always escape output in <code>render()</code>:</p>
<pre tabindex="0" class="hljs"><code><span class="hljs-comment">// Good</span>
<span class="hljs-keyword">echo</span> <span class="hljs-string">'&lt;div class="'</span> . <span class="hljs-title function_ invoke__">esc_attr</span>( <span class="hljs-variable">$class</span> ) . <span class="hljs-string">'"&gt;'</span>;
<span class="hljs-keyword">echo</span> <span class="hljs-title function_ invoke__">esc_html</span>( <span class="hljs-variable">$title</span> );

<span class="hljs-comment">// Bad</span>
<span class="hljs-keyword">echo</span> <span class="hljs-string">'&lt;div class="'</span> . <span class="hljs-variable">$class</span> . <span class="hljs-string">'"&gt;'</span>;
<span class="hljs-keyword">echo</span> <span class="hljs-variable">$title</span>;
</code></pre>
<h3 id="4.-organize-controls" tabindex="-1"><a class="header-anchor" href="#4.-organize-controls">#</a> 4. Organize Controls</h3>
<p>Group related controls into sections:</p>
<pre tabindex="0" class="hljs"><code><span class="hljs-comment">// Query settings section</span>
<span class="hljs-variable language_">$this</span>-&gt;<span class="hljs-title function_ invoke__">start_controls_section</span>( <span class="hljs-string">'section_query'</span>, [...] );

<span class="hljs-comment">// Display settings section</span>
<span class="hljs-variable language_">$this</span>-&gt;<span class="hljs-title function_ invoke__">start_controls_section</span>( <span class="hljs-string">'section_display'</span>, [...] );

<span class="hljs-comment">// Style settings section  </span>
<span class="hljs-variable language_">$this</span>-&gt;<span class="hljs-title function_ invoke__">start_controls_section</span>( <span class="hljs-string">'section_style'</span>, [...] );
</code></pre>
<h3 id="5.-provide-defaults" tabindex="-1"><a class="header-anchor" href="#5.-provide-defaults">#</a> 5. Provide Defaults</h3>
<p>Always provide sensible default values:</p>
<pre tabindex="0" class="hljs"><code><span class="hljs-string">'default'</span> =&gt; <span class="hljs-number">9</span>,  <span class="hljs-comment">// Good default for products per page</span>
<span class="hljs-string">'default'</span> =&gt; <span class="hljs-string">'date'</span>,  <span class="hljs-comment">// Reasonable sort option</span>
</code></pre>
<h2 id="example%3A-simple-widget-structure" tabindex="-1"><a class="header-anchor" href="#example%3A-simple-widget-structure">#</a> Example: Simple Widget Structure</h2>
<p>Here’s a complete example of a minimal widget:</p>
<pre tabindex="0"><code class="language-php">
&lt;?php
class Simple_Widget extends \Elementor\Widget_Base {

    // Required: Unique widget name
    public function get_name() {
        return 'simple_widget';
    }

    // Required: Widget title in panel
    public function get_title() {
        return __( 'Simple Widget', 'hello-biz-child' );
    }

    // Required: Widget icon
    public function get_icon() {
        return 'eicon-posts-grid';
    }

    // Required: Widget category
    public function get_categories() {
        return [ 'general' ];
    }

    // Optional: Widget keywords for search
    public function get_keywords() {
        return [ 'simple', 'example' ];
    }

    // Define settings controls
    protected function register_controls() {
        
        // Content Section
        $this-&gt;start_controls_section(
            'content_section',
            [
                'label' =&gt; __( 'Content', 'hello-biz-child' ),
            ]
        );

        $this-&gt;add_control(
            'title',
            [
                'label' =&gt; __( 'Title', 'hello-biz-child' ),
                'type' =&gt; \Elementor\Controls_Manager::TEXT,
                'default' =&gt; __( 'Hello World', 'hello-biz-child' ),
            ]
        );

        $this-&gt;end_controls_section();
    }

    // Render widget output
    // Render widget output
    protected function render() {
        $settings = $this-&gt;get_settings_for_display();
        ?&gt;
&lt;div class="simple-widget"&gt;

&lt;h2&gt;&lt;?php echo esc_html( $settings['title'] ); ?&gt;&lt;/h2&gt;

&lt;/div&gt;
&lt;?php }
}
</code></pre>
<h2 id="understanding-the-data-flow" tabindex="-1"><a class="header-anchor" href="#understanding-the-data-flow">#</a> Understanding the Data Flow</h2>
<p>Let’s trace how data flows through a widget:</p>
<ol>
<li><strong>User Interaction</strong>: User sets “Products Per Page” to 12 in editor</li>
<li><strong>Setting Saved</strong>: Value stored as <code>['products_per_page' =&gt; 12]</code></li>
<li><strong>Render Called</strong>: <code>render()</code> method executes</li>
<li><strong>Settings Retrieved</strong>: <code>$settings = $this-&gt;get_settings_for_display()</code></li>
<li><strong>Value Accessed</strong>: <code>$settings['products_per_page']</code> returns 12</li>
<li><strong>Value Used</strong>: Used in WP_Query to limit products</li>
</ol>
<h2 id="common-mistakes-to-avoid" tabindex="-1"><a class="header-anchor" href="#common-mistakes-to-avoid">#</a> Common Mistakes to Avoid</h2>
<h3 id="mistake-1%3A-forgetting-required-methods" tabindex="-1"><a class="header-anchor" href="#mistake-1%3A-forgetting-required-methods">#</a> Mistake 1: Forgetting Required Methods</h3>
<pre tabindex="0" class="hljs"><code><span class="hljs-comment">// Bad - Missing required methods</span>
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">My_Widget</span> <span class="hljs-keyword">extends</span> \<span class="hljs-title">Elementor</span>\<span class="hljs-title">Widget_Base</span> </span>{
    <span class="hljs-keyword">protected</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">render</span>() </span>{
        <span class="hljs-keyword">echo</span> <span class="hljs-string">'Hello'</span>;
    }
}

<span class="hljs-comment">// Good - All required methods present</span>
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">My_Widget</span> <span class="hljs-keyword">extends</span> \<span class="hljs-title">Elementor</span>\<span class="hljs-title">Widget_Base</span> </span>{
    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">get_name</span>() </span>{ <span class="hljs-keyword">return</span> <span class="hljs-string">'my_widget'</span>; }
    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">get_title</span>() </span>{ <span class="hljs-keyword">return</span> <span class="hljs-string">'My Widget'</span>; }
    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">get_icon</span>() </span>{ <span class="hljs-keyword">return</span> <span class="hljs-string">'eicon-posts-grid'</span>; }
    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">get_categories</span>() </span>{ <span class="hljs-keyword">return</span> [<span class="hljs-string">'general'</span>]; }
    <span class="hljs-keyword">protected</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">render</span>() </span>{ <span class="hljs-keyword">echo</span> <span class="hljs-string">'Hello'</span>; }
}
</code></pre>
<h3 id="mistake-2%3A-not-escaping-output" tabindex="-1"><a class="header-anchor" href="#mistake-2%3A-not-escaping-output">#</a> Mistake 2: Not Escaping Output</h3>
<pre tabindex="0" class="hljs"><code><span class="hljs-comment">// Bad - No escaping</span>
<span class="hljs-keyword">echo</span> <span class="hljs-string">'&lt;div class="'</span> . <span class="hljs-variable">$class</span> . <span class="hljs-string">'"&gt;'</span> . <span class="hljs-variable">$content</span> . <span class="hljs-string">'&lt;/div&gt;'</span>;

<span class="hljs-comment">// Good - Properly escaped</span>
<span class="hljs-keyword">echo</span> <span class="hljs-string">'&lt;div class="'</span> . <span class="hljs-title function_ invoke__">esc_attr</span>( <span class="hljs-variable">$class</span> ) . <span class="hljs-string">'"&gt;'</span> . <span class="hljs-title function_ invoke__">esc_html</span>( <span class="hljs-variable">$content</span> ) . <span class="hljs-string">'&lt;/div&gt;'</span>;
</code></pre>
<h3 id="mistake-3%3A-using-echo-in-register_controls()" tabindex="-1"><a class="header-anchor" href="#mistake-3%3A-using-echo-in-register_controls()">#</a> Mistake 3: Using echo in register_controls()</h3>
<pre tabindex="0" class="hljs"><code><span class="hljs-comment">// Bad - Don't output in register_controls()</span>
<span class="hljs-keyword">protected</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">register_controls</span>() </span>{
    <span class="hljs-keyword">echo</span> <span class="hljs-string">'Adding controls'</span>;  <span class="hljs-comment">// Wrong!</span>
    <span class="hljs-variable language_">$this</span>-&gt;<span class="hljs-title function_ invoke__">add_control</span>(...);
}

<span class="hljs-comment">// Good - Only define controls</span>
<span class="hljs-keyword">protected</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">register_controls</span>() </span>{
    <span class="hljs-variable language_">$this</span>-&gt;<span class="hljs-title function_ invoke__">add_control</span>(...);
}
</code></pre>
<h2 id="testing-your-understanding" tabindex="-1"><a class="header-anchor" href="#testing-your-understanding">#</a> Testing Your Understanding</h2>
<p>To verify you understand these concepts, try answering:</p>
<ol>
<li>What happens if you don’t implement <code>get_name()</code>?</li>
<li>Which method outputs HTML to the frontend?</li>
<li>Where do you define the widget’s settings panel?</li>
<li>How do you access user settings in the <code>render()</code> method?</li>
<li>What’s the difference between <code>protected</code> and <code>public</code> methods?</li>
</ol>
<p><strong>Answers:</strong></p>
<ol>
<li>PHP will throw a fatal error &#8211; it’s a required abstract method</li>
<li>The <code>render()</code> method</li>
<li>In the <code>register_controls()</code> method</li>
<li>Using <code>$this-&gt;get_settings_for_display()</code></li>
<li><code>public</code> methods can be called from outside the class, <code>protected</code> only from within the class or child classes</li>
</ol>
<h2 id="summary" tabindex="-1"><a class="header-anchor" href="#summary">#</a> Summary</h2>
<p>You now understand:</p>
<ul>
<li>The Elementor widget base class structure</li>
<li>Required methods and their purposes</li>
<li>How to define widget controls</li>
<li>The widget lifecycle</li>
<li>How settings flow from editor to frontend</li>
<li>Best practices for widget development</li>
</ul>
<h2 id="what%E2%80%99s-next%3F" tabindex="-1"><a class="header-anchor" href="#what%E2%80%99s-next%3F">#</a> What’s Next?</h2>
<p>In Part 3: Building the Basic Widget Structure, we’ll apply this knowledge to create the foundation of our Collection Products widget. We’ll:</p>
<ul>
<li>Create the widget PHP files</li>
<li>Implement the required methods</li>
<li>Register the widget with Elementor</li>
<li>Add basic controls</li>
<li>Create a simple render method</li>
<li>Test the widget in Elementor</li>
</ul>
<h2 id="additional-resources" tabindex="-1"><a class="header-anchor" href="#additional-resources">#</a> Additional Resources</h2>
<ul>
<li><a href="https://developers.elementor.com/docs/widgets/">Elementor Developers &#8211; Widget Structure</a></li>
<li><a href="https://developers.elementor.com/docs/controls/">Elementor Controls Reference</a></li>
<li><a href="https://www.php.net/manual/en/language.oop5.php">PHP Object-Oriented Programming</a></li>
</ul>
<hr />
<p><strong>Previous:</strong> <a href="/blog/elementor-widget-part-1-setting-up-the-development-environment/">← Part 1: Setting Up the Development Environment</a><br />
<strong>Next:</strong> <a href="/blog/elementor-widget-part-3-building-the-basic-widget-structure/">Part 3: Building the Basic Widget Structure →</a></p>
]]></content:encoded>
					
					<wfw:commentRss>https://sajdoko.com/blog/elementor-widget-part-2-understanding-elementor-widget-architecture/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Elementor Widget Part 1: Setting Up the Development Environment</title>
		<link>https://sajdoko.com/blog/elementor-widget-part-1-setting-up-the-development-environment/</link>
					<comments>https://sajdoko.com/blog/elementor-widget-part-1-setting-up-the-development-environment/#respond</comments>
		
		<dc:creator><![CDATA[sajdoko]]></dc:creator>
		<pubDate>Sun, 09 Nov 2025 20:43:52 +0000</pubDate>
				<category><![CDATA[Code]]></category>
		<category><![CDATA[Development]]></category>
		<category><![CDATA[Elementor]]></category>
		<category><![CDATA[WooCommerce]]></category>
		<category><![CDATA[WordPress]]></category>
		<category><![CDATA[Elementor Widget]]></category>
		<guid isPermaLink="false">https://sajdoko.al/blog/elementor-widget-part-1-setting-up-the-development-environment/</guid>

					<description><![CDATA[Introduction This first part sets up the local WordPress, WooCommerce and Elementor environment for the Collection Products widget. # Learning Objectives By the end of&#8230;]]></description>
										<content:encoded><![CDATA[<h2 id="introduction" tabindex="-1"><a class="header-anchor" href="#introduction">#</a> Introduction</h2>
<p>This first part sets up the local WordPress, WooCommerce and Elementor environment for the Collection Products widget.</p>
<h2 id="learning-objectives" tabindex="-1"><a class="header-anchor" href="#learning-objectives">#</a> Learning Objectives</h2>
<p>By the end of this tutorial, you will:</p>
<ul>
<li>Have a working WordPress installation</li>
<li>Install and configure WooCommerce</li>
<li>Install and activate Elementor</li>
<li>Set up Hello Biz theme with a child theme</li>
<li>Understand the child theme file structure</li>
<li>Create the basic directory structure for the widget</li>
</ul>
<h2 id="prerequisites" tabindex="-1"><a class="header-anchor" href="#prerequisites">#</a> Prerequisites</h2>
<ul>
<li>Local development environment (XAMPP, MAMP, Local by Flywheel, or similar)</li>
<li>Basic understanding of WordPress installation</li>
<li>Text editor or IDE (VS Code, PHPStorm, Sublime Text, etc.)</li>
<li>FTP/SFTP client (optional, for remote development)</li>
</ul>
<h2 id="step-1%3A-wordpress-installation" tabindex="-1"><a class="header-anchor" href="#step-1%3A-wordpress-installation">#</a> Step 1: WordPress Installation</h2>
<h3 id="1.1-install-wordpress" tabindex="-1"><a class="header-anchor" href="#1.1-install-wordpress">#</a> 1.1 Install WordPress</h3>
<p>If you don’t already have WordPress installed:</p>
<ol>
<li>Download the latest version of WordPress from <a href="https://wordpress.org/download/">wordpress.org</a></li>
<li>Create a new MySQL database for your WordPress installation</li>
<li>Extract WordPress files to your web server directory</li>
<li>Navigate to your site URL in a browser</li>
<li>Follow the WordPress installation wizard:
<ul>
<li>Enter database details</li>
<li>Create an admin account</li>
<li>Complete the installation</li>
</ul>
</li>
</ol>
<h3 id="1.2-verify-installation" tabindex="-1"><a class="header-anchor" href="#1.2-verify-installation">#</a> 1.2 Verify Installation</h3>
<p>After installation:</p>
<ol>
<li>Log in to your WordPress admin dashboard (usually <code>http://yoursite.com/wp-admin</code>)</li>
<li>Check that you can access the dashboard successfully</li>
<li>Navigate to Settings → Permalinks and set to “Post name” (recommended)</li>
</ol>
<p><img decoding="async" class="alignnone size-full wp-image-950" src="/wp-content/uploads/2025/11/part1-wordpress-dashboard.webp" alt="" width="929" height="865" /><br />
<em>The WordPress admin dashboard after successful installation</em></p>
<h2 id="step-2%3A-install-required-plugins" tabindex="-1"><a class="header-anchor" href="#step-2%3A-install-required-plugins">#</a> Step 2: Install Required Plugins</h2>
<h3 id="2.1-install-woocommerce" tabindex="-1"><a class="header-anchor" href="#2.1-install-woocommerce">#</a> 2.1 Install WooCommerce</h3>
<p>WooCommerce is the e-commerce foundation for our widget.</p>
<ol>
<li>Go to <strong>Plugins → Add New</strong></li>
<li>Search for “WooCommerce”</li>
<li>Click <strong>Install Now</strong> on the official WooCommerce plugin</li>
<li>After installation, click <strong>Activate</strong></li>
<li>Follow the WooCommerce setup wizard:
<ul>
<li>Choose your store location</li>
<li>Select industry and product types</li>
<li>Configure business details (you can skip this for development)</li>
<li>Select a theme (we’ll install Hello Biz later)</li>
<li>Complete the setup</li>
</ul>
</li>
</ol>
<h3 id="2.2-install-elementor" tabindex="-1"><a class="header-anchor" href="#2.2-install-elementor">#</a> 2.2 Install Elementor</h3>
<p>Elementor is the page builder we’ll extend with our custom widget.</p>
<ol>
<li>Go to <strong>Plugins → Add New</strong></li>
<li>Search for “Elementor”</li>
<li>Click <strong>Install Now</strong> on the official Elementor plugin</li>
<li>After installation, click <strong>Activate</strong></li>
<li>You may see a welcome screen &#8211; you can skip the introduction</li>
</ol>
<p><strong>Note:</strong> The free version of Elementor is sufficient for this tutorial. Elementor Pro is not required.</p>
<h3 id="2.3-verify-plugin-installation" tabindex="-1"><a class="header-anchor" href="#2.3-verify-plugin-installation">#</a> 2.3 Verify Plugin Installation</h3>
<p>Check that both plugins are active:</p>
<ol>
<li>Go to <strong>Plugins → Installed Plugins</strong></li>
<li>Verify both WooCommerce and Elementor show as “Active”</li>
<li>You should see new menu items for both plugins in the admin sidebar</li>
</ol>
<p><img loading="lazy" decoding="async" class="alignnone size-full wp-image-951" src="/wp-content/uploads/2025/11/part1-plugins-installed.webp" alt="" width="929" height="865" /><br />
<em>WooCommerce and Elementor plugins installed and active</em></p>
<h2 id="step-3%3A-install-hello-biz-theme" tabindex="-1"><a class="header-anchor" href="#step-3%3A-install-hello-biz-theme">#</a> Step 3: Install Hello Biz Theme</h2>
<h3 id="3.1-install-the-parent-theme" tabindex="-1"><a class="header-anchor" href="#3.1-install-the-parent-theme">#</a> 3.1 Install the Parent Theme</h3>
<p>Hello Biz is a lightweight theme optimized for Elementor.</p>
<ol>
<li>Go to <strong>Appearance → Themes</strong></li>
<li>Click <strong>Add New</strong></li>
<li>Search for “Hello Biz”</li>
<li>Click <strong>Install</strong> on the Hello Biz theme</li>
<li>After installation, click <strong>Activate</strong></li>
</ol>
<p><strong>Alternative:</strong> If Hello Biz is not available in the repository, you can use “Hello Elementor” as an alternative parent theme.</p>
<h2 id="step-4%3A-create-a-child-theme" tabindex="-1"><a class="header-anchor" href="#step-4%3A-create-a-child-theme">#</a> Step 4: Create a Child Theme</h2>
<p>Creating a child theme is essential &#8211; it allows you to modify and extend the parent theme without losing changes when the parent theme updates.<br />
We can download the Hello Biz theme from the Official Github repository: <a href="https://github.com/elementor/hello-theme-child" target="_blank" rel="noopener noreferrer">Hello Biz Child Theme</a>, or you can create it manually.</p>
<h3 id="4.1-create-child-theme-directory" tabindex="-1"><a class="header-anchor" href="#4.1-create-child-theme-directory">#</a> 4.1 Create Child Theme Directory</h3>
<ol>
<li>Navigate to <code>wp-content/themes/</code> on your WordPress installation</li>
<li>Create a new folder named <code>hello-biz-child</code></li>
</ol>
<h3 id="4.2-create-style.css" tabindex="-1"><a class="header-anchor" href="#4.2-create-style.css">#</a> 4.2 Create style.css</h3>
<p>Create a file named <code>style.css</code> in the <code>hello-biz-child</code> folder with the following content:</p>
<pre tabindex="0" class="hljs"><code class="language-php"><span class="hljs-comment">/*
Theme Name: Hello Biz Child
Description: Child theme for Hello Biz with Collection Products widget
Author: Your Name
Author URI: https://yourwebsite.com
Template: hello-biz
Version: 1.0.0
License: GNU General Public License v3 or later
License URI: https://www.gnu.org/licenses/gpl-3.0.html
Text Domain: hello-biz-child
*/</span>

<span class="hljs-comment">/* Add your custom styles here */</span>
</code></pre>
<p><strong>Important:</strong> The <code>Template:</code> line must match the directory name of your parent theme exactly.</p>
<h3 id="4.3-create-functions.php" tabindex="-1"><a class="header-anchor" href="#4.3-create-functions.php">#</a> 4.3 Create functions.php</h3>
<p>Create a file named <code>functions.php</code> in the <code>hello-biz-child</code> folder:</p>
<pre tabindex="0"><code class="language-php">
/**
 * Theme functions and definitions.
 *
 * For additional information on potential customization options,
 * read the developers' documentation:
 *
 * @package HelloBizChild
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit; // Exit if accessed directly.
}

define( 'HELLO_BIZ_CHILD_VERSION', '1.0.0' );

/**
 * Load child theme scripts &amp; styles.
 *
 * @return void
 */
function hello_biz_child_scripts_styles() {

	wp_enqueue_style(
		'hello-biz-child-style',
		get_stylesheet_directory_uri() . '/style.css',
		[
			'theme', // This ensures parent theme styles load first
		],
		HELLO_BIZ_CHILD_VERSION
	);
}

add_action( 'wp_enqueue_scripts', 'hello_biz_child_scripts_styles', 20 );

// We'll add more code here in later tutorials
</code></pre>
<h3 id="4.4-activate-child-theme" tabindex="-1"><a class="header-anchor" href="#4.4-activate-child-theme">#</a> 4.4 Activate Child Theme</h3>
<ol>
<li>Go to <strong>Appearance → Themes</strong> in WordPress admin</li>
<li>You should see “Hello Biz Child” theme</li>
<li>Click <strong>Activate</strong> on the child theme</li>
<li>Your site should now be using the child theme</li>
</ol>
<p><img loading="lazy" decoding="async" class="alignnone size-full wp-image-952" src="/wp-content/uploads/2025/11/part1-theme-active.webp" alt="" width="929" height="865" /><br />
<em>Hello Biz Child theme activated in WordPress</em></p>
<h2 id="step-5%3A-create-widget-directory-structure" tabindex="-1"><a class="header-anchor" href="#step-5%3A-create-widget-directory-structure">#</a> Step 5: Create Widget Directory Structure</h2>
<p>Now let’s set up the directory structure for our Collection Products widget.</p>
<h3 id="5.1-create-directories" tabindex="-1"><a class="header-anchor" href="#5.1-create-directories">#</a> 5.1 Create Directories</h3>
<p>In your <code>hello-biz-child</code> folder, create the following directories:</p>
<pre tabindex="0" class="hljs"><code class="language-bash">hello-biz-child/
├── widgets/
│   └── collection-products/
├── js/
└── css/
</code></pre>
<p>You can create these via FTP, File Manager, or command line:</p>
<p><strong>Via Command Line:</strong></p>
<pre tabindex="0"><code class="language-bash">
cd wp-content/themes/hello-biz-child
mkdir -p widgets/collection-products
mkdir js
mkdir css
</code></pre>
<p><strong>Via File Manager or FTP:</strong><br />
Simply create each folder manually.</p>
<h3 id="5.2-verify-directory-structure" tabindex="-1"><a class="header-anchor" href="#5.2-verify-directory-structure">#</a> 5.2 Verify Directory Structure</h3>
<p>Your child theme should now have this structure:</p>
<pre tabindex="0"><code class="language-bash">
hello-biz-child/
├── functions.php
├── style.css
├── widgets/
│   └── collection-products/
├── js/
└── css/
</code></pre>
<h2 id="step-6%3A-add-sample-woocommerce-products-(optional-but-recommended)" tabindex="-1"><a class="header-anchor" href="#step-6%3A-add-sample-woocommerce-products-(optional-but-recommended)">#</a> Step 6: Add Sample WooCommerce Products (Optional but Recommended)</h2>
<p>To test our widget properly, let’s add some sample products.</p>
<h3 id="6.1-install-woocommerce-sample-data" tabindex="-1"><a class="header-anchor" href="#6.1-install-woocommerce-sample-data">#</a> 6.1 Install WooCommerce Sample Data</h3>
<ol>
<li>Go to <strong>WooCommerce → Status → Tools</strong></li>
<li>Click on “Create default WooCommerce pages” (if not already created)</li>
<li>For sample products, you can:
<ul>
<li>Install the “WooCommerce Sample Data” plugin, OR</li>
<li>Manually create a few products</li>
</ul>
</li>
</ol>
<h3 id="6.2-create-sample-products-manually" tabindex="-1"><a class="header-anchor" href="#6.2-create-sample-products-manually">#</a> 6.2 Create Sample Products Manually</h3>
<p>If creating manually:</p>
<ol>
<li>Go to <strong>Products → Add New</strong></li>
<li>Create at least 9 products (to test the grid layout)</li>
<li>For each product:
<ul>
<li>Add a title and description</li>
<li>Set a price</li>
<li>Add a featured image</li>
<li>Assign to a category</li>
<li>Publish the product</li>
</ul>
</li>
</ol>
<h3 id="6.3-add-product-variations-(for-color-swatches)" tabindex="-1"><a class="header-anchor" href="#6.3-add-product-variations-(for-color-swatches)">#</a> 6.3 Add Product Variations (for Color Swatches)</h3>
<p>For testing color swatches:</p>
<ol>
<li>Create a Variable Product</li>
<li>Add an attribute named “Color” (make it global attribute)</li>
<li>Add color terms (e.g., “Black”, “White”, “Blue”)</li>
<li>Create variations for each color</li>
<li>Save the product</li>
</ol>
<h2 id="step-7%3A-test-your-setup" tabindex="-1"><a class="header-anchor" href="#step-7%3A-test-your-setup">#</a> Step 7: Test Your Setup</h2>
<h3 id="7.1-verify-wordpress" tabindex="-1"><a class="header-anchor" href="#7.1-verify-wordpress">#</a> 7.1 Verify WordPress</h3>
<ol>
<li>Visit your site frontend</li>
<li>Confirm the site loads without errors</li>
<li>Check that the Hello Biz Child theme is active</li>
</ol>
<h3 id="7.2-verify-woocommerce" tabindex="-1"><a class="header-anchor" href="#7.2-verify-woocommerce">#</a> 7.2 Verify WooCommerce</h3>
<ol>
<li>Go to <strong>Products → All Products</strong></li>
<li>Verify your sample products are listed</li>
<li>Visit the Shop page on the frontend</li>
<li>Confirm products display correctly</li>
</ol>
<h3 id="7.3-verify-elementor" tabindex="-1"><a class="header-anchor" href="#7.3-verify-elementor">#</a> 7.3 Verify Elementor</h3>
<ol>
<li>Create a new page or edit an existing one</li>
<li>Click “Edit with Elementor”</li>
<li>Verify Elementor editor loads</li>
<li>Check the widgets panel on the left</li>
<li>You should see various Elementor widgets</li>
</ol>
<h2 id="common-issues-and-solutions" tabindex="-1"><a class="header-anchor" href="#common-issues-and-solutions">#</a> Common Issues and Solutions</h2>
<h3 id="issue-1%3A-child-theme-not-showing" tabindex="-1"><a class="header-anchor" href="#issue-1%3A-child-theme-not-showing">#</a> Issue 1: Child Theme Not Showing</h3>
<p><strong>Solution:</strong></p>
<ul>
<li>Check that <code>style.css</code> has the correct <code>Template:</code> header matching the parent theme folder name</li>
<li>Verify both <code>style.css</code> and <code>functions.php</code> exist in the child theme folder</li>
<li>Clear browser cache and WordPress cache</li>
</ul>
<h3 id="issue-2%3A-parent-theme-styles-not-loading" tabindex="-1"><a class="header-anchor" href="#issue-2%3A-parent-theme-styles-not-loading">#</a> Issue 2: Parent Theme Styles Not Loading</h3>
<p><strong>Solution:</strong></p>
<ul>
<li>Ensure the parent theme handle in <code>wp_enqueue_style()</code> is correct</li>
<li>Some themes use ‘theme-style’ or ‘hello-elementor’ &#8211; check your parent theme’s functions.php</li>
<li>Adjust the dependency array accordingly</li>
</ul>
<h3 id="issue-3%3A-woocommerce-not-showing-products" tabindex="-1"><a class="header-anchor" href="#issue-3%3A-woocommerce-not-showing-products">#</a> Issue 3: WooCommerce Not Showing Products</h3>
<p><strong>Solution:</strong></p>
<ul>
<li>Ensure products are published (not drafts)</li>
<li>Check permalink settings (Settings → Permalinks, click Save)</li>
<li>Verify WooCommerce pages were created (WooCommerce → Status)</li>
</ul>
<h3 id="issue-4%3A-elementor-editor-not-loading" tabindex="-1"><a class="header-anchor" href="#issue-4%3A-elementor-editor-not-loading">#</a> Issue 4: Elementor Editor Not Loading</h3>
<p><strong>Solution:</strong></p>
<ul>
<li>Deactivate all plugins except Elementor and WooCommerce</li>
<li>Switch to a default WordPress theme temporarily to isolate the issue</li>
<li>Check browser console for JavaScript errors</li>
<li>Clear WordPress cache and browser cache</li>
</ul>
<h2 id="testing-your-environment" tabindex="-1"><a class="header-anchor" href="#testing-your-environment">#</a> Testing Your Environment</h2>
<p>To ensure everything is set up correctly, perform these checks:</p>
<h3 id="checklist" tabindex="-1"><a class="header-anchor" href="#checklist">#</a> Checklist</h3>
<ul>
<li>[ ] WordPress is installed and accessible</li>
<li>[ ] You can log in to WordPress admin</li>
<li>[ ] WooCommerce is active and configured</li>
<li>[ ] Elementor is active</li>
<li>[ ] Hello Biz parent theme is installed</li>
<li>[ ] Hello Biz Child theme is active and working</li>
<li>[ ] Directory structure is created (widgets/, js/, css/)</li>
<li>[ ] Sample products are created in WooCommerce</li>
<li>[ ] You can edit pages with Elementor</li>
<li>[ ] WooCommerce shop page displays products</li>
</ul>
<h2 id="summary" tabindex="-1"><a class="header-anchor" href="#summary">#</a> Summary</h2>
<p>The development environment now includes:</p>
<ul>
<li>WordPress installed and configured</li>
<li>WooCommerce plugin activated with sample products</li>
<li>Elementor plugin activated</li>
<li>Hello Biz child theme created and activated</li>
<li>Directory structure ready for widget development</li>
</ul>
<h2 id="what%E2%80%99s-next%3F" tabindex="-1"><a class="header-anchor" href="#what%E2%80%99s-next%3F">#</a> What’s Next?</h2>
<p>In <a href="/blog/elementor-widget-part-2-understanding-elementor-widget-architecture/">Part 2: Understanding Elementor Widget Architecture</a>, we’ll examine the widget base class, required methods and lifecycle before building our widget.</p>
<h2 id="additional-resources" tabindex="-1"><a class="header-anchor" href="#additional-resources">#</a> Additional Resources</h2>
<ul>
<li><a href="https://developer.wordpress.org/themes/advanced-topics/child-themes/">WordPress Child Themes Documentation</a></li>
<li><a href="https://woocommerce.com/documentation/">WooCommerce Documentation</a></li>
<li><a href="https://developers.elementor.com/">Elementor Developers Documentation</a></li>
<li><a href="/blog/how-to-install-wordpress-on-localhost-using-xampp-step-by-step-guide/">Local WordPress Development</a></li>
</ul>
<hr />
<p><strong>Next:</strong> <a href="/blog/elementor-widget-part-2-understanding-elementor-widget-architecture/">Part 2: Understanding Elementor Widget Architecture →</a></p>
]]></content:encoded>
					
					<wfw:commentRss>https://sajdoko.com/blog/elementor-widget-part-1-setting-up-the-development-environment/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Building a Custom Elementor WooCommerce Collection Widget &#8211; Tutorial Series</title>
		<link>https://sajdoko.com/blog/building-a-custom-elementor-woocommerce-collection-widget-tutorial-series/</link>
					<comments>https://sajdoko.com/blog/building-a-custom-elementor-woocommerce-collection-widget-tutorial-series/#respond</comments>
		
		<dc:creator><![CDATA[sajdoko]]></dc:creator>
		<pubDate>Sun, 09 Nov 2025 20:21:54 +0000</pubDate>
				<category><![CDATA[Code]]></category>
		<category><![CDATA[Development]]></category>
		<category><![CDATA[Elementor]]></category>
		<category><![CDATA[WooCommerce]]></category>
		<category><![CDATA[WordPress]]></category>
		<category><![CDATA[Elementor Widget]]></category>
		<guid isPermaLink="false">https://sajdoko.al/blog/building-a-custom-elementor-woocommerce-collection-widget-tutorial-series/</guid>

					<description><![CDATA[It displays products in a Pinterest-style grid with collection images, filters and sorting controls. #&#8230;]]></description>
										<content:encoded><![CDATA[<h2 id="introduction" tabindex="-1"><a class="header-anchor" href="#introduction">#</a> Introduction</h2>
<p>This series builds a Collection Products widget for WooCommerce. It displays products in a Pinterest-style grid with collection images, filters and sorting controls.</p>
<h2 id="what-you%E2%80%99ll-build" tabindex="-1"><a class="header-anchor" href="#what-you%E2%80%99ll-build">#</a> What You’ll Build</h2>
<p>By the end of this tutorial series, you’ll have created a fully functional Elementor widget that:</p>
<ul>
<li style="list-style-type: none;">
<ul>
<li>Displays WooCommerce products in a responsive 2-column grid layout</li>
</ul>
</li>
</ul>
<ul>
<li>Supports custom “collection” images separate from product featured images</li>
<li>Shows featured images on hover (for portrait products)</li>
<li>Includes client-side filtering by colors and sizes</li>
<li>Provides sorting options (price low to high, high to low)</li>
<li>Features a slide-in filter panel</li>
<li>Displays product color swatches</li>
<li>Adapts the layout to desktop, tablet and phone screens</li>
</ul>
<p><img loading="lazy" decoding="async" class="alignnone size-full wp-image-957" src="/wp-content/uploads/2025/11/elementor-widget.webp" alt="" width="1920" height="911" /><br />
<em>The Elementor WooCommerce Collection Widget</em></p>
<h2 id="prerequisites" tabindex="-1"><a class="header-anchor" href="#prerequisites">#</a> Prerequisites</h2>
<p>Before starting this tutorial series, you should have:</p>
<ul>
<li>Basic understanding of PHP (WordPress/WooCommerce development)</li>
<li>Familiarity with JavaScript and jQuery</li>
<li>Understanding of HTML and CSS</li>
<li>Basic knowledge of WordPress hooks and filters</li>
<li>WordPress installation with admin access</li>
<li>WooCommerce plugin installed and activated</li>
<li>Elementor plugin installed and activated</li>
<li>Hello Biz theme with Hello Biz Child theme set up</li>
</ul>
<h2 id="tutorial-series-structure" tabindex="-1"><a class="header-anchor" href="#tutorial-series-structure">#</a> Tutorial Series Structure</h2>
<h3 id="part-1%3A-setting-up-the-development-environment" tabindex="-1"><a href="/blog/elementor-widget-part-1-setting-up-the-development-environment/">Part 1: Setting Up the Development Environment</a></h3>
<p>Learn how to set up your WordPress development environment, install necessary plugins, and create the child theme structure for the widget.</p>
<h3 id="part-2%3A-understanding-elementor-widget-architecture" tabindex="-1"><a href="/blog/elementor-widget-part-2-understanding-elementor-widget-architecture/">Part 2: Understanding Elementor Widget Architecture</a></h3>
<p>Dive into the fundamentals of Elementor widget development, understanding the base class, required methods, and the widget lifecycle.</p>
<h3 id="part-3%3A-building-the-basic-widget-structure" tabindex="-1"><a href="/blog/elementor-widget-part-3-building-the-basic-widget-structure/">Part 3: Building the Basic Widget Structure</a></h3>
<p>Create the foundation of your widget by implementing the widget class, registering it with Elementor, and adding basic controls.</p>
<h3 id="part-4%3A-woocommerce-product-integration" tabindex="-1"><a href="/blog/elementor-widget-part-4-woocommerce-product-integration/">Part 4: WooCommerce Product Integration</a></h3>
<p>Learn how to query WooCommerce products, access product data, work with product taxonomies, and display product information.</p>
<h3 id="part-5%3A-implementing-custom-product-meta-fields" tabindex="-1">Part 5: Implementing Custom Product Meta Fields</h3>
<p>Add custom meta fields to WooCommerce products including a collection image uploader using the WordPress media library and product orientation settings.</p>
<h3 id="part-6%3A-creating-the-frontend-rendering" tabindex="-1">Part 6: Creating the Frontend Rendering</h3>
<p>Build the grid layout using CSS Grid, implement the product rendering logic, add color swatches, and create responsive designs.</p>
<h3 id="part-7%3A-adding-sorting-and-filtering-functionality" tabindex="-1">Part 7: Adding Sorting and Filtering Functionality</h3>
<p>Implement client-side JavaScript filtering and sorting, create the filter panel UI, and add touch-friendly interactions.</p>
<h2 id="key-concepts-covered" tabindex="-1"><a class="header-anchor" href="#key-concepts-covered">#</a> Key Concepts Covered</h2>
<p>Throughout this series, you’ll learn:</p>
<ol>
<li><strong>Elementor Widget Development</strong>
<ul>
<li>Extending <code>\Elementor\Widget_Base</code></li>
<li>Implementing required methods</li>
<li>Adding widget controls</li>
<li>Rendering frontend output</li>
</ul>
</li>
<li><strong>WooCommerce Integration</strong>
<ul>
<li>Querying products with <code>WP_Query</code></li>
<li>Working with product data</li>
<li>Handling product variations</li>
<li>Displaying product meta</li>
</ul>
</li>
<li><strong>WordPress Development Best Practices</strong>
<ul>
<li>Using WordPress hooks and filters</li>
<li>Sanitizing and escaping data</li>
<li>Enqueuing scripts and styles</li>
<li>Following WordPress coding standards</li>
</ul>
</li>
<li><strong>Modern Frontend Development</strong>
<ul>
<li>CSS Grid layouts</li>
<li>Responsive design with media queries</li>
<li>JavaScript DOM manipulation</li>
<li>Client-side filtering without AJAX</li>
</ul>
</li>
<li><strong>WordPress Media Library</strong>
<ul>
<li>Integrating <code>wp.media()</code> API</li>
<li>Creating custom media uploaders</li>
<li>Handling image selection</li>
</ul>
</li>
</ol>
<h2 id="file-structure" tabindex="-1"><a class="header-anchor" href="#file-structure">#</a> File Structure</h2>
<p>By the end of this series, your child theme will have the following structure:</p>
<pre tabindex="0" class="hljs"><code>wp-content/themes/hello-biz-child/
├── functions.php                                    # Main theme functions
├── style.css                                        # Child theme styles
├── widgets/
│   └── collection-products/
│       ├── collection-products-register.php         # Widget registration &amp; helpers
│       └── collection-products-widget.php           # Main widget class
├── js/
│   ├── collection-image-uploader.js                # Admin media uploader
│   └── collection-filters.js                       # Frontend filtering &amp; sorting
└── css/
    └── collection.css                              # Widget styles
</code></pre>
<h2 id="tutorial-format" tabindex="-1"><a class="header-anchor" href="#tutorial-format">#</a> Tutorial Format</h2>
<p>Each tutorial part includes:</p>
<ul>
<li><strong>Clear objectives</strong> &#8211; What you’ll accomplish in that part</li>
<li><strong>Step-by-step instructions</strong> &#8211; Detailed implementation steps</li>
<li><strong>Code snippets with comments</strong> &#8211; Well-documented code examples</li>
<li><strong>Explanations</strong> &#8211; Understanding of what the code does and why</li>
<li><strong>Common issues and solutions</strong> &#8211; Troubleshooting tips</li>
<li><strong>Testing instructions</strong> &#8211; How to verify your implementation</li>
<li><strong>Summary</strong> &#8211; Key takeaways and what’s next</li>
</ul>
<h2 id="additional-resources" tabindex="-1"><a class="header-anchor" href="#additional-resources">#</a> Additional Resources</h2>
<ul>
<li><a href="https://developers.elementor.com/">Elementor Developers Documentation</a></li>
<li><a href="https://woocommerce.com/documentation/plugins/woocommerce/woocommerce-codex/">WooCommerce Developer Documentation</a></li>
<li><a href="https://developer.wordpress.org/">WordPress Developer Resources</a></li>
<li><a href="https://developer.wordpress.org/coding-standards/">WordPress Coding Standards</a></li>
</ul>
<h2 id="getting-started" tabindex="-1"><a class="header-anchor" href="#getting-started">#</a> Getting Started</h2>
<p>Ready to begin? Start with <a href="/blog/elementor-widget-part-1-setting-up-the-development-environment/">Part 1: Setting Up the Development Environment</a>!</p>
<h2 id="support-and-feedback" tabindex="-1"><a class="header-anchor" href="#support-and-feedback">#</a> Support and Feedback</h2>
<p>If you encounter any issues or have questions while following this tutorial series, please:</p>
<ol>
<li>Review the troubleshooting sections in each part</li>
<li>Check the code examples in the repository</li>
<li>Consult the WordPress and Elementor documentation</li>
<li>Search for similar issues in WordPress/Elementor forums</li>
</ol>
<p>Happy coding! Let’s build an amazing WooCommerce widget together!</p>
]]></content:encoded>
					
					<wfw:commentRss>https://sajdoko.com/blog/building-a-custom-elementor-woocommerce-collection-widget-tutorial-series/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Building a WordPress Plugin: Security, Styling, and Distribution Best Practices</title>
		<link>https://sajdoko.com/blog/building-a-wordpress-plugin-security-styling-and-distribution-best-practices/</link>
					<comments>https://sajdoko.com/blog/building-a-wordpress-plugin-security-styling-and-distribution-best-practices/#respond</comments>
		
		<dc:creator><![CDATA[sajdoko]]></dc:creator>
		<pubDate>Fri, 04 Jul 2025 20:05:12 +0000</pubDate>
				<category><![CDATA[Code]]></category>
		<category><![CDATA[Development]]></category>
		<category><![CDATA[Plugin]]></category>
		<category><![CDATA[WordPress]]></category>
		<category><![CDATA[Quick Note Manager]]></category>
		<guid isPermaLink="false">https://sajdoko.al/blog/building-a-wordpress-plugin-security-styling-and-distribution-best-practices/</guid>

					<description><![CDATA[Throughout this series, a complete and functional Quick Note Manager now has admin note management, AJAX submissions and frontend display via shortcodesin place. This final part&#8230;]]></description>
										<content:encoded><![CDATA[<p>Throughout this series, a complete and functional <i>Quick Note Manager</i> now has admin note management, AJAX submissions and <em><a href="/blog/building-a-wordpress-plugin-frontend-display-with-shortcodes/">frontend display via shortcodes</a></em>in place. This final part covers styling, security checks, testing and distribution.</p>
<p>We’ll add admin and frontend styles, review the security checks, and test and package the plugin for release. We’ll also prepare its <code>readme.txt</code> file, will be detailed.</p>
<h2>A Final Polish: Styling the Plugin</h2>
<p>A well-styled plugin provides a better user experience and feels more professional. Styles should be added using the standard WordPress enqueueing system to ensure they are loaded correctly and without conflicts.</p>
<h3>Enqueuing Frontend and Backend Stylesheets</h3>
<p>Separate stylesheets should be used for the frontend and the admin area to keep concerns separate and avoid loading unnecessary code.</p>
<ul>
<li style="font-weight: 400;" aria-level="1"><b>Frontend Styles</b>: For the shortcode and widget, a stylesheet can be enqueued using the <a href="https://developer.wordpress.org/reference/hooks/wp_enqueue_scripts/" target="_blank" rel="noopener"><code>wp_enqueue_scripts</code> </a>hook.</li>
<li style="font-weight: 400;" aria-level="1"><b>Admin Styles</b>: For the admin management page, the <code><a href="https://developer.wordpress.org/reference/hooks/admin_enqueue_scripts/" target="_blank" rel="noopener">admin_enqueue_scripts</a></code> hook is used, along with the <code>$hook_suffix</code> check to ensure the styles only load on the plugin&#8217;s page.</li>
</ul>
<p>The <code>qnm_enqueue_admin_assets</code> function can be updated to include an admin stylesheet, and a new function can be created for the frontend.</p>
<pre tabindex="0"><code class="language-php">
// Update the admin enqueue function
function qnm_enqueue_admin_assets($hook) {
    if ('toplevel_page_qnm-quick-notes' !== $hook) {
        return;
    }
    //... existing wp_enqueue_script and wp_localize_script calls...
    wp_enqueue_style(
        'qnm-admin-style',
        plugins_url('assets/css/admin-style.css', __FILE__)
    );
}
add_action('admin_enqueue_scripts', 'qnm_enqueue_admin_assets');

// Add a new function for frontend styles
function qnm_enqueue_frontend_assets() {
    wp_enqueue_style(
        'qnm-frontend-style',
        plugins_url('assets/css/frontend-style.css', __FILE__)
    );
}
add_action('wp_enqueue_scripts', 'qnm_enqueue_frontend_assets');
</code></pre>
<p><img loading="lazy" decoding="async" class="alignnone size-full wp-image-879" src="/wp-content/uploads/2025/07/enqueue-assets.png" alt="" width="1346" height="701" /></p>
<p>Simple CSS rules can then be added to <code>assets/css/admin-style.css</code> and <code>assets/css/frontend-style.css</code> to improve the plugin&#8217;s appearance. For example, styling the unordered list generated by the shortcode.</p>
<h2>Hardening the Plugin: A Security Checklist</h2>
<p>Security is not an afterthought but a continuous process. This section serves as a final audit of the security practices that have been integrated into the plugin from the beginning.</p>
<ul>
<li style="font-weight: 400;" aria-level="1"><b>Data Validation and Sanitization</b>: The principle of &#8220;Validate Early, Sanitize Late&#8221; has been applied. All data coming from the user-whether from a form submission or a widget setting-is cleaned before being saved to the database. Functions like <code>sanitize_textarea_field()</code> for text and <code>absint()</code> for numbers are used to strip potentially malicious code and ensure data integrity.</li>
<li style="font-weight: 400;" aria-level="1"><b>Output Escaping</b>: All data retrieved from the database is escaped just before it is rendered on the screen. This is the most effective defense against XSS attacks. Functions like <code>esc_html()</code>, <code>esc_attr()</code>, and <code>esc_url()</code> have been used consistently in the <code>WP_List_Table</code>, shortcode, and widget to ensure that stored data is displayed safely.</li>
<li style="font-weight: 400;" aria-level="1"><b>Nonces</b>: Nonces (Numbers Used Once) have been used to protect all form submissions and AJAX requests from CSRF attacks. The combination of <code>wp_nonce_field()</code> in forms, <code>wp_create_nonce()</code> for AJAX, and server-side verification with <code>wp_verify_nonce()</code> or <code>check_ajax_referer()</code> ensures that actions are performed only by the intended user from an authorized session.</li>
<li style="font-weight: 400;" aria-level="1"><b>Permissions (Capability Checks)</b>: Access to the plugin&#8217;s admin page and its data-modifying actions is restricted using <code>current_user_can('manage_options')</code>. This ensures that only users with the appropriate permissions (Administrators) can manage the notes, preventing unauthorized access.</li>
</ul>
<h2>Testing and Debugging</h2>
<p>Thorough testing is essential before releasing a plugin. WordPress provides built-in tools to aid in this process.</p>
<h3>Enabling <code>WP_DEBUG</code></h3>
<p>During development, the <code>WP_DEBUG</code> constants in the <code>wp-config.php</code> file should be enabled.</p>
<ul>
<li style="font-weight: 400;" aria-level="1"><code>define( 'WP_DEBUG', true );</code>: This enables the main debug mode, which will show all PHP errors, notices, and warnings.</li>
<li style="font-weight: 400;" aria-level="1"><code>define( 'WP_DEBUG_LOG', true );</code>: This logs all errors to a <code>debug.log</code> file in the <code>wp-content</code> directory, which is useful for debugging AJAX requests or issues that don&#8217;t display on the screen.<br />
These constants should always be set to <code>false</code> on a live production site to avoid exposing sensitive information.</li>
</ul>
<h3>Using Debugging Plugins</h3>
<p>For more advanced debugging, plugins like <b>Query Monitor</b> and <b>Debug Bar</b> are invaluable. They provide detailed insights into database queries, hooks fired on a page, PHP errors, and script dependencies. These tools can help identify performance bottlenecks and complex bugs that are not immediately obvious.</p>
<h2>Packaging the Plugin for Distribution</h2>
<p>Once the plugin is stable and secure, it can be packaged for sharing or submission to the WordPress.org plugin directory.</p>
<h3>The Importance of <code>readme.txt</code></h3>
<p>A <code>readme.txt</code> file in the plugin&#8217;s root directory is critical. It is not just a help file; the WordPress.org repository parses it to generate the plugin&#8217;s public-facing page, including the description, installation instructions, FAQ, and changelog.</p>
<p>A standard <code>readme.txt</code> file follows a specific Markdown-like format and includes the following sections:</p>
<ul>
<li style="font-weight: 400;" aria-level="1"><b>Header</b>: Contains metadata like Plugin Name, Contributors, Tags, Required and Tested WordPress versions, and License information.</li>
<li style="font-weight: 400;" aria-level="1"><b>Description</b>: A detailed explanation of what the plugin does.</li>
<li style="font-weight: 400;" aria-level="1"><b>Installation</b>: Step-by-step instructions for installation.</li>
<li style="font-weight: 400;" aria-level="1"><b>Frequently Asked Questions (FAQ)</b>: Answers to common user questions.</li>
<li style="font-weight: 400;" aria-level="1"><b>Screenshots</b>: Descriptions for screenshots that will be displayed on the plugin page.</li>
<li style="font-weight: 400;" aria-level="1"><b>Changelog</b>: A version-by-version list of changes.</li>
</ul>
<h3>Final File Structure Review</h3>
<p>A well-organized file structure makes the plugin easier to maintain. The final structure for the <i>Quick Note Manager</i> would look like this:</p>
<p>/quick-note-manager<br />
|&#8211; assets/<br />
| |&#8211; css/<br />
| | |&#8211; admin-style.css<br />
| | |&#8211; frontend-style.css<br />
| |&#8211; js/<br />
| | |&#8211; admin-notes.js<br />
|&#8211; quick-note-manager.php<br />
|&#8211; readme.txt</p>
<h3>Creating the <code>.zip</code> File</h3>
<p>To distribute the plugin, the entire <code>quick-note-manager</code> folder should be compressed into a <code>.zip</code> file. This file can then be uploaded to a WordPress site via the &#8220;Plugins &gt; Add New &gt; Upload Plugin&#8221; screen or submitted to the WordPress plugin repository.</p>
<h2>Series Conclusion and Next Steps</h2>
<p>This guide has walked through the complete process of building a WordPress plugin, from initial setup to final packaging. Along the way, it has covered essential aspects of the WordPress API, including plugin architecture, database interaction with <code>$wpdb</code>, creating admin pages with <code>WP_List_Table</code>, implementing secure AJAX, and leveraging the Shortcode APIs for frontend display.</p>
<p>The <i>Quick Note Manager</i> plugin is now a functional and secure tool. However, development is an iterative process. Potential future enhancements for developers to explore include:</p>
<ul>
<li style="font-weight: 400;" aria-level="1">Adding an &#8220;Edit Note&#8221; functionality, likely via an AJAX-powered modal window.</li>
<li style="font-weight: 400;" aria-level="1">Implementing bulk actions in the <code>WP_List_Table</code> (e.g., &#8220;Delete Selected&#8221;).</li>
<li style="font-weight: 400;" aria-level="1">Adding categories or tags to notes for better organization, which would require extending the database schema and admin interface.</li>
</ul>
<p>These challenges provide excellent opportunities to build upon the foundational skills acquired throughout this series.</p>
<p>You can find the complete source code for the Quick Note Manager plugin in the <a href="https://github.com/sajdoko/quick-note-manager">GitHub repository</a>. Feel free to explore, fork, or contribute to the project to enhance your learning experience and help improve the plugin for the community!</p>
<h2>Core Functions and Hooks Reference</h2>
<p>The table below lists the WordPress functions, hooks and classes used in this series.</p>
<div class="table-scroll" tabindex="0" role="region" aria-label="Horizontally scrollable table"><table>
<tbody>
<tr>
<td>Function / Hook / Class</td>
<td>Type</td>
<td>Purpose</td>
<td>Covered In</td>
</tr>
<tr>
<td>register_activation_hook</td>
<td>Function</td>
<td>Runs code when the plugin is activated.</td>
<td>Article 1: Plugin&#8217;s Foundation</td>
</tr>
<tr>
<td>register_deactivation_hook</td>
<td>Function</td>
<td>Runs code when the plugin is deactivated.</td>
<td>Article 1: Plugin&#8217;s Foundation</td>
</tr>
<tr>
<td>$wpdb-&gt;prefix, $wpdb-&gt;insert</td>
<td>Property / Method</td>
<td>Interacts with the WordPress database safely.</td>
<td>Article 2: Admin UI</td>
</tr>
<tr>
<td>add_menu_page</td>
<td>Function</td>
<td>Adds a new top-level menu to the admin sidebar.</td>
<td>Article 2: Admin UI</td>
</tr>
<tr>
<td>admin_menu</td>
<td>Action Hook</td>
<td>The correct hook for adding admin menus.</td>
<td>Article 2: Admin UI</td>
</tr>
<tr>
<td>wp_nonce_field</td>
<td>Function</td>
<td>Adds a hidden nonce field to a form for security.</td>
<td>Article 2: Admin UI</td>
</tr>
<tr>
<td>check_admin_referer</td>
<td>Function</td>
<td>Verifies a nonce for a non-AJAX admin request.</td>
<td>Article 2: Admin UI</td>
</tr>
<tr>
<td>sanitize_textarea_field</td>
<td>Function</td>
<td>Cleans multi-line text input.</td>
<td>Article 2: Admin UI</td>
</tr>
<tr>
<td><code>WP_List_Table</code></td>
<td>Class</td>
<td>The base class for creating standard admin list tables.</td>
<td>Article 2: Admin UI</td>
</tr>
<tr>
<td>admin_enqueue_scripts</td>
<td>Action Hook</td>
<td>The correct hook for adding scripts/styles to admin pages.</td>
<td>Article 3: AJAX</td>
</tr>
<tr>
<td>wp_enqueue_script</td>
<td>Function</td>
<td>Adds a JavaScript file to a page.</td>
<td>Article 3: AJAX</td>
</tr>
<tr>
<td>wp_enqueue_style</td>
<td>Function</td>
<td>Adds a CSS stylesheet to a page.</td>
<td>Article 5: Styling</td>
</tr>
<tr>
<td>wp_localize_script</td>
<td>Function</td>
<td>Passes data from PHP to a JavaScript file.</td>
<td>Article 3: AJAX</td>
</tr>
<tr>
<td>wp_ajax_{action}</td>
<td>Action Hook</td>
<td>The hook for handling AJAX requests for logged-in users.</td>
<td>Article 3: AJAX</td>
</tr>
<tr>
<td>wp_send_json_success</td>
<td>Function</td>
<td>Sends a success response for an AJAX request and dies.</td>
<td>Article 3: AJAX</td>
</tr>
<tr>
<td>wp_send_json_error</td>
<td>Function</td>
<td>Sends an error response for an AJAX request and dies.</td>
<td>Article 3: AJAX</td>
</tr>
<tr>
<td>add_shortcode</td>
<td>Function</td>
<td>Registers a new shortcode and its handler function.</td>
<td>Article 4: Frontend</td>
</tr>
<tr>
<td>shortcode_atts</td>
<td>Function</td>
<td>Parses shortcode attributes against a set of defaults.</td>
<td>Article 4: Frontend</td>
</tr>
<tr>
<td>WP_Widget</td>
<td>Class</td>
<td>The base class for creating custom widgets.</td>
<td>Article 4: Frontend</td>
</tr>
<tr>
<td>register_widget</td>
<td>Function</td>
<td>Registers a new widget class.</td>
<td>Article 4: Frontend</td>
</tr>
<tr>
<td>widgets_init</td>
<td>Action Hook</td>
<td>The correct hook for registering widgets.</td>
<td>Article 4: Frontend</td>
</tr>
<tr>
<td>esc_html, esc_attr</td>
<td>Functions</td>
<td>Escapes data for safe output in HTML content and attributes.</td>
<td>Article 4: Frontend</td>
</tr>
<tr>
<td>current_user_can</td>
<td>Function</td>
<td>Checks if the current user has a specific capability.</td>
<td>Article 2: Admin UI</td>
</tr>
</tbody>
</table></div>
]]></content:encoded>
					
					<wfw:commentRss>https://sajdoko.com/blog/building-a-wordpress-plugin-security-styling-and-distribution-best-practices/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Building a WordPress Plugin: Frontend Display with Shortcodes</title>
		<link>https://sajdoko.com/blog/building-a-wordpress-plugin-frontend-display-with-shortcodes/</link>
					<comments>https://sajdoko.com/blog/building-a-wordpress-plugin-frontend-display-with-shortcodes/#respond</comments>
		
		<dc:creator><![CDATA[sajdoko]]></dc:creator>
		<pubDate>Fri, 04 Jul 2025 19:42:17 +0000</pubDate>
				<category><![CDATA[Code]]></category>
		<category><![CDATA[Development]]></category>
		<category><![CDATA[Plugin]]></category>
		<category><![CDATA[WordPress]]></category>
		<category><![CDATA[Quick Note Manager]]></category>
		<guid isPermaLink="false">https://sajdoko.al/blog/building-a-wordpress-plugin-frontend-display-with-shortcodes/</guid>

					<description><![CDATA[In our previous article, Implementing AJAX for Dynamic Content, we added AJAX note submissions with JavaScript, a PHP handler and server-side validation. The admin interface can&#8230;]]></description>
										<content:encoded><![CDATA[<p>In our previous article, <a href="/blog/building-a-wordpress-plugin-implementing-ajax-for-dynamic-content/"><em>Implementing AJAX for Dynamic Content</em></a>, we added AJAX note submissions with JavaScript, a PHP handler and server-side validation.</p>
<p>The admin interface can now manage notes. Next, we’ll display them on public pages using a shortcode.</p>
<p>This article will cover the implementation of shortcodes. A <code>[quick_notes]</code> shortcode will be created, allowing users to display a list of their notes anywhere in their content.</p>
<h2>Displaying Notes with a Shortcode</h2>
<p>The WordPress Shortcode API lets users insert dynamic content into a page. A shortcode like <code>[quick_notes]</code> is more intuitive for a non-technical user than embedding PHP or HTML.</p>
<p><img loading="lazy" decoding="async" class="alignnone size-full wp-image-871" src="/wp-content/uploads/2025/07/Quick-Notes-Shortcodes.png" alt="" width="1463" height="792" /></p>
<p>&nbsp;</p>
<h3>Creating a Basic Shortcode</h3>
<p>Registering a shortcode is done with the <a href="https://developer.wordpress.org/reference/functions/add_shortcode/" target="_blank" rel="noopener"><code>add_shortcode()</code></a> function, which should be hooked into the <code>init</code> action. This function maps a shortcode tag (e.g., &#8216;quick_notes&#8217;) to a callback function that generates the output.</p>
<pre tabindex="0"><code class="language-php">
&lt;?php
/**
 * Renders the [quick_notes] shortcode.
 *
 * @param array $atts Shortcode attributes.
 * @return string The shortcode output.
 */
function qnm_render_notes_shortcode($atts) {
    global $wpdb;
    $table_name = $wpdb-&gt;prefix . 'quick_notes';

    $notes = $wpdb-&gt;get_results("SELECT note, created_at FROM $table_name ORDER BY created_at DESC", ARRAY_A);

    if (empty($notes)) {
        return '';
    }

    // Use output buffering to capture the HTML
    ob_start();
   ?&gt;
    &lt;ul class="qnm-notes-list"&gt;
        &lt;?php foreach ($notes as $note) : ?&gt;
            &lt;li&gt;&lt;?php echo esc_html($note['note']); ?&gt;&lt;/li&gt;
        &lt;?php endforeach; ?&gt;
    &lt;/ul&gt;
    &lt;?php
    return ob_get_clean();
}

/**
 * Initializes shortcodes.
 */
function qnm_shortcodes_init() {
    add_shortcode('quick_notes', 'qnm_render_notes_shortcode');
}
add_action('init', 'qnm_shortcodes_init');
</code></pre>
<p><img loading="lazy" decoding="async" class="alignnone size-full wp-image-869" src="/wp-content/uploads/2025/07/render-notes-shortcode.png" alt="" width="1418" height="757" /></p>
<p>In this handler function, several key practices are followed:</p>
<ul>
<li style="font-weight: 400;" aria-level="1">The global <code>$wpdb</code> object is used to query the <code>wp_quick_notes</code> table for all notes.</li>
<li style="font-weight: 400;" aria-level="1">Output buffering (<code>ob_start()</code>, <code>ob_get_clean()</code>) is used to build the HTML output as a string. This is crucial because shortcode handlers must <code>return</code> their output, not <code>echo</code> it directly.</li>
<li style="font-weight: 400;" aria-level="1">The note content is escaped using <code>esc_html()</code> before being displayed. This is a critical security measure to prevent Cross-Site Scripting (XSS) attacks by ensuring any HTML within the note is displayed as text rather than being executed by the browser.</li>
</ul>
<h3>Adding Attributes to the Shortcode</h3>
<p>Shortcodes can be made more flexible by accepting attributes. For instance, a <code>limit</code> attribute could control how many notes are displayed: <code>[quick_notes limit="5"]</code>.</p>
<p>The <code><a href="https://developer.wordpress.org/reference/functions/shortcode_atts/" target="_blank" rel="noopener">shortcode_atts()</a></code> function is used to parse these attributes, merging them with a set of default values. This provides sensible defaults and ensures the shortcode works even if no attributes are provided.</p>
<p>The handler function is modified to accept and process attributes:</p>
<pre tabindex="0"><code class="language-php">
function qnm_render_notes_shortcode($atts) {
    global $wpdb;
    $table_name = $wpdb-&gt;prefix . 'quick_notes';

    // 1. Define and parse attributes
    $atts = shortcode_atts(
        array(
            'limit' =&gt; -1, // Default: show all notes
        ),
        $atts,
        'quick_notes'
    );

    $limit = intval($atts['limit']);

    // 2. Build the query with the limit
    $query = "SELECT note, created_at FROM $table_name ORDER BY created_at DESC";
    if ($limit &gt; 0) {
        $query .= $wpdb-&gt;prepare(" LIMIT %d", $limit);
    }

    $notes = $wpdb-&gt;get_results($query, ARRAY_A);
    
    //... (rest of the function remains the same)
    //...
}
</code></pre>
<p>This updated function now uses <code>shortcode_atts()</code> to process the <code>limit</code> attribute, validates it as an integer, and modifies the SQL query accordingly.</p>
<p>Next up is <a href="/blog/building-a-wordpress-plugin-security-styling-and-distribution-best-practices/"><em>Security, Styling, and Distribution Best Practices</em></a>. We’ll add styles, review security, test the plugin and prepare it for distribution.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://sajdoko.com/blog/building-a-wordpress-plugin-frontend-display-with-shortcodes/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Building a WordPress Plugin: Implementing AJAX for Dynamic Content</title>
		<link>https://sajdoko.com/blog/building-a-wordpress-plugin-implementing-ajax-for-dynamic-content/</link>
					<comments>https://sajdoko.com/blog/building-a-wordpress-plugin-implementing-ajax-for-dynamic-content/#respond</comments>
		
		<dc:creator><![CDATA[sajdoko]]></dc:creator>
		<pubDate>Sun, 22 Jun 2025 17:15:30 +0000</pubDate>
				<category><![CDATA[Code]]></category>
		<category><![CDATA[Development]]></category>
		<category><![CDATA[Plugin]]></category>
		<category><![CDATA[WordPress]]></category>
		<category><![CDATA[Quick Note Manager]]></category>
		<guid isPermaLink="false">https://sajdoko.al/blog/building-a-wordpress-plugin-implementing-ajax-for-dynamic-content/</guid>

					<description><![CDATA[In the previous article, Building a WordPress Plugin: Creating the Admin Dashboard Page, we built the admin interface. Adding a note currently reloads the whole page.&#8230;]]></description>
										<content:encoded><![CDATA[<p>In the previous article, <a href="/blog/building-a-wordpress-plugin-creating-the-admin-dashboard-page/">Building a WordPress Plugin: Creating the Admin Dashboard Page</a>, we built the admin interface. Adding a note currently reloads the whole page. AJAX lets us submit the form and update the note list without that reload.</p>
<p>We’ll update Add Note to use AJAX, with JavaScript for submission and a PHP handler for validation and storage.</p>
<h2>Setting Up the AJAX Environment</h2>
<p>A proper <a href="/blog/simple-wordpress-ajax-example-a-step-by-step-guide/">AJAX implementation in WordPress</a> requires securely loading a JavaScript file and passing necessary data from PHP to it. This is not done by simply adding a <code>&lt;script&gt;</code> tag to the HTML; instead, WordPress provides a structured system of hooks and functions to manage this process correctly.</p>
<h3>Enqueuing JavaScript in the Admin</h3>
<p>Scripts intended for the admin area should be loaded using the <code>admin_enqueue_scripts</code> action hook. A critical best practice is to load assets only on the pages where they are needed. The <code><a href="https://developer.wordpress.org/reference/hooks/admin_enqueue_scripts/" target="_blank" rel="noopener">admin_enqueue_scripts</a></code> hook provides a <code>$hook_suffix</code> parameter, which is the unique identifier for the current admin page. By checking this value, the script can be loaded conditionally, preventing unnecessary overhead on other admin screens.</p>
<p>The following function enqueues a new JavaScript file, <code>admin-notes.js</code>, but only on the &#8220;Quick Notes&#8221; admin page.</p>
<pre tabindex="0"><code class="language-php">
/**
 * Enqueues admin scripts and styles.
 *
 * @param string $hook The current admin page hook.
 */
function qnm_enqueue_admin_assets($hook) {
    // Only load our script on the plugin's admin page
    if ('toplevel_page_qnm-quick-notes' !== $hook) {
        return;
    }

    wp_enqueue_script(
        'qnm-admin-script',
        plugins_url('assets/js/admin-notes.js', __FILE__),
        array('jquery'),
        '1.0.0',
        true // Load in footer
    );
}
add_action('admin_enqueue_scripts', 'qnm_enqueue_admin_assets');
</code></pre>
<h3><img loading="lazy" decoding="async" class="alignnone size-full wp-image-812" src="/wp-content/uploads/2025/06/enqueue-admin-scripts.png" alt="" width="1426" height="644" /></h3>
<p>&nbsp;</p>
<h3>Passing Data from PHP to JavaScript with <code>wp_localize_script()</code></h3>
<p>Client-side JavaScript cannot directly access server-side PHP variables or WordPress functions. To bridge this gap, WordPress provides the <a href="https://developer.wordpress.org/reference/functions/wp_localize_script/" target="_blank" rel="noopener"><code>wp_localize_script()</code></a> function. It is the standard, secure method for passing data, such as URLs and security tokens, from PHP to an enqueued JavaScript file.</p>
<p>The script needs two key pieces of data</p>
<ol>
<li style="font-weight: 400;" aria-level="1"><b>AJAX URL</b>: The endpoint for all WordPress AJAX requests, which is <code>admin-ajax.php</code>.</li>
<li style="font-weight: 400;" aria-level="1"><b>Nonce</b>: A security token to verify the request&#8217;s authenticity.</li>
</ol>
<p>The <code>qnm_enqueue_admin_assets</code> function is updated to include <code>wp_localize_script()</code></p>
<pre tabindex="0"><code class="language-php">
function qnm_enqueue_admin_assets($hook) {
    if ('toplevel_page_qnm-quick-notes' !== $hook) {
        return;
    }

    wp_enqueue_script(
        'qnm-admin-script',
        plugins_url('assets/js/admin-notes.js', __FILE__),
        array('jquery'),
        '1.0.0',
        true
    );

    // Pass data to the script
    wp_localize_script(
        'qnm-admin-script',
        'qnm_ajax_object',
        array(
            'ajax_url' =&gt; admin_url('admin-ajax.php'),
            'nonce'    =&gt; wp_create_nonce('qnm-ajax-nonce')
        )
    );
}
add_action('admin_enqueue_scripts', 'qnm_enqueue_admin_assets');
</code></pre>
<p>This code creates a JavaScript object named <code>qnm_ajax_object</code> that will be available in <code>admin-notes.js</code>. This object contains the <code>ajax_url</code> and a freshly generated nonce.</p>
<h2>The Client-Side: Writing the AJAX Request</h2>
<p>With the environment set up, the next step is to write the JavaScript that will intercept the form submission and send an AJAX request instead.</p>
<h3>Modifying the Form</h3>
<p>The existing HTML form needs a slight modification. The <code>action</code> and <code>method</code> attributes are no longer necessary, as JavaScript will handle the submission. An <code>id</code> is added to the form for easy targeting.</p>
<pre tabindex="0"><code class="language-html">
&lt;form id="qnm-add-note-form"&gt;
    &lt;?php wp_nonce_field('qnm_add_note_nonce', 'qnm_nonce_field'); ?&gt;
    &lt;textarea name="qnm_note" id="qnm-note-content" rows="4" cols="50" required&gt;&lt;/textarea&gt;
    &lt;?php submit_button('Add Note'); ?&gt;
&lt;/form&gt;
</code></pre>
<p><img loading="lazy" decoding="async" class="alignnone size-full wp-image-817" src="/wp-content/uploads/2025/06/modifying-admin-form.png" alt="" width="1377" height="567" /></p>
<p>Note: The original PHP nonce from <code>wp_nonce_field()</code> can be left for non-JavaScript fallback, but the AJAX request will use the nonce passed via <code>wp_localize_script()</code>.</p>
<h3>The JavaScript (<code>assets/js/admin-notes.js</code>)</h3>
<p>This file contains the logic to send the AJAX request using the modern <code>fetch</code> API.</p>
<pre tabindex="0"><code class="language-js">
document.addEventListener('DOMContentLoaded', function () {
    const form = document.getElementById('qnm-add-note-form');
    if (!form) {
        return;
    }

    form.addEventListener('submit', function (event) {
        event.preventDefault(); // Stop the default form submission

        const noteContent = document.getElementById('qnm-note-content').value;
        const feedbackDiv = document.getElementById('qnm-ajax-feedback'); // Add a div with this ID to your admin page HTML for feedback

        // Prepare data for the AJAX request
        const formData = new FormData();
        formData.append('action', 'qnm_add_note_ajax');
        formData.append('_ajax_nonce', qnm_ajax_object.nonce);
        formData.append('note', noteContent);

        // Send the request
        fetch(qnm_ajax_object.ajax_url, {
            method: 'POST',
            body: formData
        })
        .then(response =&gt; response.json())
        .then(data =&gt; {
            if (data.success) {
                // On success, clear the textarea and dynamically add the row to the table
                document.getElementById('qnm-note-content').value = '';
                if (feedbackDiv) {
                    feedbackDiv.textContent = 'Note added successfully!';
                    feedbackDiv.style.color = 'green';
                }
                const notesTable = document.getElementById('qnm-notes-table');
                if (notesTable &amp;&amp; data.data.note) {
                    const note = data.data.note;
                    const newRow = notesTable.insertRow(1); // Insert after header

                    // Checkbox cell
                    const cbCell = newRow.insertCell(0);
                    cbCell.innerHTML = `&lt;input type="checkbox" name="note" value="${note.id}" /&gt;`;

                    // Note cell
                    const noteCell = newRow.insertCell(1);
                    noteCell.textContent = note.note.length &gt; 100 ? note.note.substring(0, 100) + '...' : note.note;

                    // Created at cell
                    const createdCell = newRow.insertCell(2);
                    createdCell.textContent = note.created_at;

                    // Actions cell
                    const actionsCell = newRow.insertCell(3);
                    actionsCell.innerHTML = `&lt;a href="?page=qnm-quick-notes&amp;action=delete&amp;id=${note.id}" onclick="return confirm('Are you sure?')"&gt;Delete&lt;/a&gt;`;
                }
            } else {
                // On failure, display an error message
                if (feedbackDiv) {
                    feedbackDiv.textContent = 'Error: ' + data.data.message;
                    feedbackDiv.style.color = 'red';
                }
            }
        })
        .catch(error =&gt; {
            console.error('AJAX request failed:', error);
            if (feedbackDiv) {
                feedbackDiv.textContent = 'An unexpected error occurred.';
                feedbackDiv.style.color = 'red';
            }
        });
    });
});
</code></pre>
<p>This script listens for the form submission, prevents the default reload, and sends a <code>POST</code> request to the <code>admin-ajax.php</code> URL. It includes the action name, the nonce, and the note content. Upon receiving a response, it reloads the page to display the updated list. A more advanced implementation could parse the returned data and inject a new row into the table dynamically.</p>
<h2>The Server-Side: Handling the AJAX Request</h2>
<p>The final piece is the PHP function that receives and processes the AJAX request.</p>
<h3>The <code>wp_ajax_{action}</code> Hook</h3>
<p>WordPress uses a specific hook format for handling AJAX requests from logged-in users: <a href="https://developer.wordpress.org/reference/hooks/wp_ajax_action/" target="_blank" rel="noopener"><code>wp_ajax_{action_name}</code></a>. The</p>
<p><code>{action_name}</code> corresponds to the <code>action</code> parameter sent in the AJAX request. For this implementation, the hook will be <code>wp_ajax_qnm_add_note_ajax</code>.</p>
<h3>The PHP Handler Function</h3>
<p>This function will perform security checks, sanitize data, interact with the database, and send a structured JSON response back to the JavaScript.</p>
<pre tabindex="0"><code class="language-php">
/**
 * Handles the AJAX request to add a new note.
 */
function qnm_ajax_add_note() {
    // 1. Verify the nonce
    check_ajax_referer('qnm-ajax-nonce', '_ajax_nonce');

    // 2. Check user capabilities
    if (!current_user_can('manage_options')) {
        wp_send_json_error(array('message' =&gt; 'Permission denied.'), 403);
    }

    // 3. Sanitize and validate the input
    $note_content = isset($_POST['note']) ? sanitize_textarea_field(wp_unslash($_POST['note'])) : '';

    if (empty($note_content)) {
        wp_send_json_error(array('message' =&gt; 'Note content cannot be empty.'), 400);
    }

    // 4. Insert into the database
    global $wpdb;
    $table_name = $wpdb-&gt;prefix . 'quick_notes';

    $result = $wpdb-&gt;insert(
        $table_name,
        array('note' =&gt; $note_content),
        array('%s')
    );

    // 5. Send the response
    if ($result) {
        $note_id = $wpdb-&gt;insert_id;
        $note_row = $wpdb-&gt;get_row($wpdb-&gt;prepare("SELECT * FROM $table_name WHERE id = %d", $note_id), ARRAY_A);
        wp_send_json_success(array(
            'message' =&gt; 'Note added successfully!',
            'note' =&gt; array(
                'id' =&gt; $note_row['id'],
                'note' =&gt; $note_row['note'],
                'created_at' =&gt; $note_row['created_at'],
            )
        ));
    } else {
        wp_send_json_error(array('message' =&gt; 'Failed to add note to the database.'), 500);
    }
}
add_action('wp_ajax_qnm_add_note_ajax', 'qnm_ajax_add_note');
</code></pre>
<p><img loading="lazy" decoding="async" class="alignnone wp-image-833 size-full" src="/wp-content/uploads/2025/06/wp-ajax-hook-handler.png" alt="" width="1797" height="939" /></p>
<p>This server-side handler is built for security and clarity</p>
<ol>
<li style="font-weight: 400;" aria-level="1"><b>Nonce Verification</b>: It uses <code><a href="https://developer.wordpress.org/reference/functions/check_ajax_referer/" target="_blank" rel="noopener">check_ajax_referer()</a></code>, the recommended function for AJAX nonce validation, which automatically handles the check and terminates execution on failure.</li>
<li style="font-weight: 400;" aria-level="1"><b>Capability Check</b>: It ensures the user has the correct permissions.</li>
<li style="font-weight: 400;" aria-level="1"><b>Sanitization</b>: It sanitizes the input data before use.</li>
<li style="font-weight: 400;" aria-level="1"><b>Database Interaction</b>: It performs the database insertion.</li>
<li style="font-weight: 400;" aria-level="1"><b>JSON Response</b>: It uses <code>wp_send_json_success()</code> or <code>wp_send_json_error()</code> to send a standardized JSON response back to the client. These functions also correctly terminate the script with<br />
<code>wp_die()</code>, which is a requirement for all WordPress AJAX handlers.</li>
</ol>
<p>The request now follows the WordPress AJAX flow: script loading, server-side validation and a JSON response. Keep the nonce and permission checks in place when extending it.</p>
<p>In the next article, <a href="/blog/building-a-wordpress-plugin-frontend-display-with-shortcodes/"><em>Frontend Display with Shortcodes</em></a>, we’ll display notes on public pages with a shortcode.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://sajdoko.com/blog/building-a-wordpress-plugin-implementing-ajax-for-dynamic-content/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Building a WordPress Plugin: Creating the Admin Dashboard Page</title>
		<link>https://sajdoko.com/blog/building-a-wordpress-plugin-creating-the-admin-dashboard-page/</link>
					<comments>https://sajdoko.com/blog/building-a-wordpress-plugin-creating-the-admin-dashboard-page/#respond</comments>
		
		<dc:creator><![CDATA[sajdoko]]></dc:creator>
		<pubDate>Sun, 22 Jun 2025 13:52:06 +0000</pubDate>
				<category><![CDATA[Code]]></category>
		<category><![CDATA[Development]]></category>
		<category><![CDATA[Plugin]]></category>
		<category><![CDATA[WordPress]]></category>
		<category><![CDATA[Quick Note Manager]]></category>
		<guid isPermaLink="false">https://sajdoko.al/blog/building-a-wordpress-plugin-creating-the-admin-dashboard-page/</guid>

					<description><![CDATA[This guide continues the development of the Quick Note Manager plugin. The first article, Building a WordPress Plugin: A Step-by-Step Guide,  established the plugin&#8217;s foundation, including&#8230;]]></description>
										<content:encoded><![CDATA[<p>This guide continues the development of the <a href="/blog/tag/quick-note-manager/"><i>Quick Note Manager</i></a> plugin. The first article, <a href="/blog/building-a-wordpress-plugin-a-step-by-step-guide/">Building a WordPress Plugin: A Step-by-Step Guide</a>,  established the plugin&#8217;s foundation, including the main file, a custom database table named <code>wp_quick_notes</code>, and hooks to manage it during activation and deactivation. Next, we’ll build the admin interface for adding and viewing notes.</p>
<p>We’ll add a menu item and a page with a note list and an Add Note form. The example uses WordPress menu hooks, form handling and the <code>WP_List_Table </code>class to generate tables that are consistent with the WordPress core UI.</p>
<h2>Creating the Admin Menu and Page</h2>
<p>To integrate the plugin into the WordPress dashboard, a dedicated menu page is required. This is achieved by using the <code>admin_menu</code> action hook, which is the designated entry point for adding, modifying, or removing menus in the admin area. It is critical to use this specific hook; attempting to add menus earlier in the WordPress loading sequence can result in permission errors, as the user&#8217;s capabilities have not yet been established.</p>
<p>The core function for this task is <a href="https://developer.wordpress.org/reference/functions/add_menu_page/" target="_blank" rel="noopener"><code>add_menu_page()</code></a>. This function adds a new top-level menu item to the admin sidebar. Its parameters allow for precise control over the menu&#8217;s appearance and behavior.</p>
<p>The following code should be added to the main plugin file, <code>quick-note-manager.php</code>:</p>
<pre tabindex="0"><code class="language-php">
/**
 * Adds the admin menu page for the Quick Note Manager.
 */
function qnm_add_admin_menu() {
    add_menu_page(
        'Quick Note Manager',      // Page Title
        'Quick Notes',             // Menu Title
        'manage_options',          // Capability
        'qnm-quick-notes',         // Menu Slug
        'qnm_render_admin_page',   // Callback Function
        'dashicons-edit-page',     // Icon
        25                         // Position
    );
}
add_action('admin_menu', 'qnm_add_admin_menu');
</code></pre>
<p><img loading="lazy" decoding="async" class="alignnone wp-image-785 size-full" src="/wp-content/uploads/2025/06/Admin-Menu-Code.png" alt="" width="1325" height="661" /></p>
<p>A breakdown of the <code>add_menu_page()</code> parameters demonstrates their roles:</p>
<ul>
<li style="font-weight: 400;" aria-level="1"><b>$page_title</b>: &#8216;Quick Note Manager&#8217;. This text appears in the browser&#8217;s title bar when the admin page is active.</li>
<li style="font-weight: 400;" aria-level="1"><b>$menu_title</b>: &#8216;Quick Notes&#8217;. This is the shorter text that appears in the admin sidebar menu itself.</li>
<li style="font-weight: 400;" aria-level="1"><b>$capability</b>: &#8216;manage_options&#8217;. This is a crucial security parameter. It dictates that only users with the <code>manage_options</code> capability (typically Administrators) can see and access this menu page.</li>
<li style="font-weight: 400;" aria-level="1"><b>$menu_slug</b>: &#8216;qnm-quick-notes&#8217;. This is a unique, URL-friendly string that identifies the page. It is used in the URL, for example: <code>wp-admin/admin.php?page=qnm-quick-notes.</code></li>
<li style="font-weight: 400;" aria-level="1"><b>$callback</b>: &#8216;qnm_render_admin_page&#8217;. This is the name of the PHP function that will be called to generate and output the HTML content for the page.</li>
<li style="font-weight: 400;" aria-level="1"><b>$icon_url</b>: &#8216;dashicons-edit-page&#8217;. This specifies a built-in WordPress Dashicon to be used for the menu item, ensuring a professional and consistent look within the dashboard.</li>
<li style="font-weight: 400;" aria-level="1"><b>$position</b>: An integer that determines the menu&#8217;s position in the sidebar navigation order.</li>
</ul>
<p>With the menu registered, the next step is to create the callback function that renders the page&#8217;s content. This function provides the basic HTML structure for the admin page. WordPress admin pages conventionally use a <code>div</code> with the <code>class</code> wrap to contain the content.</p>
<pre tabindex="0"><code class="language-php">
/**
 * Renders the admin page content for the Quick Note Manager.
 */
function qnm_render_admin_page() {
   ?&gt;
    &lt;div class="wrap"&gt;
        &lt;h1&gt;Quick Note Manager&lt;/h1&gt;
        &lt;p&gt;Manage your personal notes here.&lt;/p&gt;
    &lt;/div&gt;
    &lt;?php
}
</code></pre>
<p>After adding this code and activating the plugin, a new &#8220;Quick Notes&#8221; menu item will appear in the WordPress admin sidebar.</p>
<p><img loading="lazy" decoding="async" class="alignnone wp-image-781 size-full" src="/wp-content/uploads/2025/06/qnm-admin-menu.png" alt="" width="1117" height="532" /></p>
<h2>Handling Note Creation</h2>
<p>To allow users to add new notes, a form is needed. The standard WordPress methodology for handling form submissions from the admin area involves directing the form&#8217;s action to <code>admin-post.php</code>. This approach ensures that the entire WordPress environment is loaded, providing access to core functions for security, user management, and database interaction.</p>
<h3>Building the &#8220;Add Note&#8221; Form</h3>
<p>The form will contain a textarea for the note and a submit button. Two hidden fields are essential for the WordPress form handling process:</p>
<ol>
<li style="font-weight: 400;" aria-level="1">A hidden <code>action</code> field with a unique value. This tells <code>admin-post.php</code> which action hook to trigger.</li>
<li style="font-weight: 400;" aria-level="1">A nonce field, generated by <a href="https://developer.wordpress.org/reference/functions/wp_nonce_field/" target="_blank" rel="noopener"><code>wp_nonce_field()</code></a>, to protect against Cross-Site Request Forgery (CSRF) attacks.</li>
</ol>
<p>The <code>qnm_render_admin_page()</code> function is updated to include this form:</p>
<pre tabindex="0"><code class="language-php">
function qnm_render_admin_page() {
   ?&gt;
    &lt;div class="wrap"&gt;
        &lt;h1&gt;Quick Note Manager&lt;/h1&gt;
        
        &lt;h2&gt;Add a New Note&lt;/h2&gt;
        &lt;form method="post" action="&lt;?php echo esc_url(admin_url('admin-post.php')); ?&gt;"&gt;
            &lt;input type="hidden" name="action" value="qnm_add_note"&gt;
            &lt;?php wp_nonce_field('qnm_add_note_nonce', 'qnm_nonce_field'); ?&gt;
            &lt;textarea name="qnm_note" rows="4" cols="50" required&gt;&lt;/textarea&gt;
            &lt;?php submit_button('Add Note'); ?&gt;
        &lt;/form&gt;

        &lt;hr&gt;

        &lt;h2&gt;Existing Notes&lt;/h2&gt;
    &lt;/div&gt;
    &lt;?php
}
</code></pre>
<h3>Creating the Form Handler Function</h3>
<p>When the form is submitted, <code>admin-post.php</code> triggers a hook based on the hidden action field. For an <code>action</code> value of <code>qnm_add_note</code>, the hook is <code>admin_post_qnm_add_note</code>. A handler function must be created and attached to this hook to process the submission.</p>
<pre tabindex="0"><code class="language-php">
/**
 * Handles the submission of the 'Add Note' form.
 */
function qnm_handle_add_note_form() {
    // 1. Verify the nonce
    if (!isset($_POST['qnm_nonce_field']) || !wp_verify_nonce($_POST['qnm_nonce_field'], 'qnm_add_note_nonce')) {
        wp_die('Security check failed.');
    }

    // 2. Check user capabilities
    if (!current_user_can('manage_options')) {
        wp_die('You do not have sufficient permissions to perform this action.');
    }

    // 3. Sanitize the input
    $note_content = isset($_POST['qnm_note']) ? sanitize_textarea_field($_POST['qnm_note']) : '';

    if (empty($note_content)) {
        // Redirect back with an error if the note is empty
        wp_safe_redirect(admin_url('admin.php?page=qnm-quick-notes&amp;note-added=false'));
        exit;
    }

    // 4. Insert into the database
    global $wpdb;
    $table_name = $wpdb-&gt;prefix . 'quick_notes';

    $wpdb-&gt;insert(
        $table_name,
        array(
            'note' =&gt; $note_content,
        ),
        array(
            '%s', // format for the 'note' column
        )
    );

    // 5. Redirect back to the admin page with a success message
    wp_safe_redirect(admin_url('admin.php?page=qnm-quick-notes&amp;note-added=true'));
    exit;
}
add_action('admin_post_qnm_add_note', 'qnm_handle_add_note_form');
</code></pre>
<p>This handler function follows a secure and robust process:</p>
<ol>
<li style="font-weight: 400;" aria-level="1"><b>Nonce Verification</b>: It first checks for the presence and validity of the nonce using <code>wp_verify_nonce()</code>. If the check fails, execution is terminated with <code>wp_die()</code>.</li>
<li style="font-weight: 400;" aria-level="1"><b>Capability Check</b>: It verifies that the current user has the &#8216;manage_options&#8217; capability using <code>current_user_can()</code>.</li>
<li style="font-weight: 400;" aria-level="1"><b>Input Sanitization</b>: It retrieves the note from <code>$_POST</code> and cleans it using <code>sanitize_textarea_field()</code>. This function removes potentially harmful code and standardizes formatting, which is a critical step to prevent database vulnerabilities.</li>
<li style="font-weight: 400;" aria-level="1"><b>Database Insertion</b>: It uses the global <code>$wpdb</code> object and its <code>insert()</code> method to safely add the data to the <code>wp_quick_notes</code> table. The <code>insert()</code> method handles SQL escaping automatically.</li>
<li style="font-weight: 400;" aria-level="1"><b>Redirection</b>: Finally, it uses <code>wp_safe_redirect()</code> to send the user back to the notes management page, appending a query parameter to indicate the result of the operation.</li>
</ol>
<p><img loading="lazy" decoding="async" class="alignnone size-full wp-image-787" src="/wp-content/uploads/2025/06/Add-new-note-view.png" alt="" width="1176" height="498" /></p>
<p>&nbsp;</p>
<h2>Displaying Notes with <code>WP_List_Table</code></h2>
<p>While a simple HTML table could display the notes, the standard WordPress approach is to use the <a href="https://developer.wordpress.org/reference/classes/wp_list_table/" target="_blank" rel="noopener"><code>WP_List_Table class</code></a>. This class provides the foundation for all list tables in the WordPress admin, ensuring a consistent user experience with features like pagination, sorting, and bulk actions built-in. Using it makes a plugin feel native to the WordPress environment.</p>
<h3>Creating the Custom Table Class</h3>
<p>First, the file containing the <code>WP_List_Table</code> class must be included, as it is not loaded by default on all admin pages. Then, a new class is created that extends <code>WP_List_Table</code>. We will start by creating a new class file named <code>class-quick-notes-list-table.php</code> in the plugin directory <code>/includes</code>. This file will contain the custom table class that extends <code>WP_List_Table</code>.</p>
<pre tabindex="0"><code class="language-php">
if (!class_exists('WP_List_Table')) {
require_once(ABSPATH. 'wp-admin/includes/class-wp-list-table.php');
}

class Quick_Notes_List_Table extends WP_List_Table {
// Class methods will be defined here
}
</code></pre>
<h3><img loading="lazy" decoding="async" class="alignnone size-full wp-image-790" src="/wp-content/uploads/2025/06/wp-list-table-class.png" alt="" width="1299" height="603" /></h3>
<h3>Implementing Core Methods</h3>
<p>Several methods within this class must be overridden to define the table&#8217;s structure and data.</p>
<pre tabindex="0"><code class="language-php">
&lt;?php

if (! class_exists('WP_List_Table')) {
  require_once ABSPATH . 'wp-admin/includes/class-wp-list-table.php';
}

class Quick_Notes_List_Table extends WP_List_Table {

/**
 * Constructor.
 */
  public function __construct() {
    parent::__construct([
      'singular' =&gt; 'Note',
      'plural'   =&gt; 'Notes',
      'ajax'     =&gt; true,
    ]);
  }

/**
 * Define the columns that are going to be used in the table.
 * @return array
 */
  public function get_columns() {
    return [
      'cb'         =&gt; '&lt;input type="checkbox" /&gt;',
      'note'       =&gt; 'Note',
      'created_at' =&gt; 'Created At',
      'actions'    =&gt; 'Actions',
    ];
  }

/**
 * Get bulk actions.
 * @return array
 */
  public function get_bulk_actions() {
    return [
      'bulk-delete' =&gt; 'Delete',
    ];
  }

/**
 * Process bulk actions.
 */
  public function process_bulk_action() {
    global $wpdb;
    $table_name = $wpdb-&gt;prefix . 'quick_notes';

    // Single delete
    if ($this-&gt;current_action() === 'delete' &amp;&amp; !empty($_GET['id'])) {
      $id = absint($_GET['id']);
      $wpdb-&gt;delete($table_name, ['id' =&gt; $id]);
    }

    // Bulk delete
    if (($this-&gt;current_action() === 'bulk-delete') &amp;&amp; !empty($_POST['note'])) {
      $ids = array_map('absint', (array) $_POST['note']);
      foreach ($ids as $id) {
        $wpdb-&gt;delete($table_name, ['id' =&gt; $id]);
      }
    }
  }

/**
 * Define sortable columns.
 * @return array
 */
  public function get_sortable_columns() {
    return [
      'created_at' =&gt; ['created_at', true],
      'note'       =&gt; ['note', false],
    ];
  }

/**
 * Prepare the items for the table to process (with search, sort, pagination).
 */
  public function prepare_items() {
    global $wpdb;
    $table_name = $wpdb-&gt;prefix . 'quick_notes';

    $per_page = 10;
    $columns  = $this-&gt;get_columns();
    $hidden   = [];
    $sortable = $this-&gt;get_sortable_columns();
    $this-&gt;_column_headers = [$columns, $hidden, $sortable];

    $this-&gt;process_bulk_action();

    $search = isset($_REQUEST['s']) ? wp_unslash($_REQUEST['s']) : '';
    $orderby = !empty($_REQUEST['orderby']) ? esc_sql($_REQUEST['orderby']) : 'created_at';
    $order = !empty($_REQUEST['order']) ? esc_sql($_REQUEST['order']) : 'DESC';

    $where = '';
    $params = [];
    if (!empty($search)) {
      $where = "WHERE note LIKE %s";
      $params[] = '%' . $wpdb-&gt;esc_like($search) . '%';
    }

    $sql = "SELECT * FROM $table_name $where ORDER BY $orderby $order";
    $data = $wpdb-&gt;get_results($wpdb-&gt;prepare($sql, ...$params), ARRAY_A);

    $current_page = $this-&gt;get_pagenum();
    $total_items  = count($data);
    $this-&gt;set_pagination_args([
      'total_items' =&gt; $total_items,
      'per_page'    =&gt; $per_page,
    ]);
    $this-&gt;items = array_slice($data, (($current_page - 1) * $per_page), $per_page);
  }

/**
 * Default column rendering.
 * @param array $item
 * @param string $column_name
 * @return mixed
 */
  public function column_default($item, $column_name) {
    switch ($column_name) {
    case 'created_at':
      return $item[$column_name];
    case 'note':
      return $this-&gt;column_note($item);
    case 'actions':
      $actions = [
        'delete' =&gt; sprintf('&lt;a href="?page=qnm-quick-notes&amp;action=delete&amp;id=%s" onclick="return confirm(\'Are you sure?\')"&gt;Delete&lt;/a&gt;', $item['id']),
      ];
      return sprintf('%s', implode(' | ', $actions));
    case 'cb':
      return $this-&gt;column_cb($item);
    default:
      return print_r($item, true);
    }
  }

/**
 * Render the 'note' column.
 * @param array $item
 * @return string
 */
  public function column_note($item) {
    return wp_trim_words(esc_html($item['note']), 25, '...');
  }

/**
 * Render the checkbox column.
 * @param array $item
 * @return string
 */
  public function column_cb($item) {
    return sprintf(
      '&lt;input type="checkbox" name="note" value="%s" /&gt;', $item['id']
    );
  }

/**
 * Add search box above the table.
 */
  public function search_box($text, $input_id) {
    echo '&lt;form method="get"&gt;';
    foreach ($_GET as $key =&gt; $value) {
      if ($key === 's') continue;
      echo '&lt;input type="hidden" name="' . esc_attr($key) . '" value="' . esc_attr($value) . '" /&gt;';
    }
    echo '&lt;p class="search-box"&gt;';
    echo '&lt;label class="screen-reader-text" for="' . esc_attr($input_id) . '"&gt;' . esc_html($text) . ':&lt;/label&gt;';
    echo '&lt;input type="search" id="' . esc_attr($input_id) . '" name="s" value="' . esc_attr(isset($_REQUEST['s']) ? $_REQUEST['s'] : '') . '" /&gt;';
    submit_button($text, '', '', false, ['id' =&gt; 'search-submit']);
    echo '&lt;/p&gt;';
    echo '&lt;/form&gt;';
  }

/**
 * Display the table with a custom ID for JS access.
 */
  public function display() {
    $singular = $this-&gt;_args['singular'];

    $this-&gt;display_tablenav('top');

    $this-&gt;screen-&gt;render_screen_reader_content('heading_list');
    ?&gt;
    &lt;table id="qnm-notes-table" class="wp-list-table &lt;?php echo implode(' ', $this-&gt;get_table_classes()); ?&gt;"&gt;
      &lt;?php $this-&gt;print_table_description(); ?&gt;
      &lt;thead&gt;
        &lt;tr&gt;
          &lt;?php $this-&gt;print_column_headers(); ?&gt;
        &lt;/tr&gt;
      &lt;/thead&gt;

      &lt;tbody id="the-list"
        &lt;?php
        if ($singular) {
          echo " data-wp-lists='list:$singular'";
        }
        ?&gt;
      &gt;
        &lt;?php $this-&gt;display_rows_or_placeholder(); ?&gt;
      &lt;/tbody&gt;

      &lt;tfoot&gt;
        &lt;tr&gt;
          &lt;?php $this-&gt;print_column_headers(false); ?&gt;
        &lt;/tr&gt;
      &lt;/tfoot&gt;
    &lt;/table&gt;
    &lt;?php
    $this-&gt;display_tablenav('bottom');
  }
}
</code></pre>
<ul>
<li style="font-weight: 400;" aria-level="1"><b>__construct()</b>: The constructor calls its parent to set basic properties like the singular and plural names for the items being listed.</li>
<li style="font-weight: 400;" aria-level="1"><b>get_columns()</b>: This method returns an associative array where keys are the column identifiers and values are the display labels for the table headers.21 The<br />
cb key is reserved for the bulk action checkbox.</li>
<li style="font-weight: 400;" aria-level="1"><b>prepare_items()</b>: This is the most critical method. It fetches data from the database, sets up pagination, and assigns the data to the $this-&gt;items property for rendering.</li>
<li style="font-weight: 400;" aria-level="1"><b>column_default()</b>: This is a fallback method for any column that doesn&#8217;t have a specific rendering method. It ensures all data is displayed.</li>
<li style="font-weight: 400;" aria-level="1"><b>column_{column_key}()</b>: Methods named in this format, like <code>column_note()</code> and <code>column_cb()</code>, provide custom rendering for specific columns. Here, <code>column_note()</code> uses <code>wp_trim_words()</code> to shorten long notes and <code>esc_html()</code> to ensure the output is safe. <code>column_cb()</code> renders the checkbox for each row.</li>
</ul>
<h3>Integrating the Table into the Admin Page</h3>
<p>To display the custom table on the admin page, we need to include the class file in the main plugin file. This is done by adding the following line at the top of <code>quick-note-manager.php</code>:</p>
<pre tabindex="0"><code class="language-php">
require_once plugin_dir_path(__FILE__) . 'includes/class-quick-notes-list-table.php';
</code></pre>
<p><img loading="lazy" decoding="async" class="alignnone size-full wp-image-792" src="/wp-content/uploads/2025/06/include-class-file.png" alt="" width="1215" height="432" /></p>
<p>Finally, the WP_List_Table instance must be created and displayed within the main admin page rendering function.</p>
<p>The <code>qnm_render_admin_page()</code> function is updated one last time:</p>
<pre tabindex="0"><code class="language-php">
function qnm_render_admin_page() {
// The form code from before remains here...

//... after the form...

&lt;hr&gt;

&lt;h2&gt;Existing Notes&lt;/h2&gt;
&lt;?php
$list_table = new Quick_Notes_List_Table();
$list_table-&gt;prepare_items();
$list_table-&gt;search_box('Search Notes', 'qnm-search');
$list_table-&gt;display();
?&gt;
&lt;/div&gt;
&lt;?php
}
</code></pre>
<p><img loading="lazy" decoding="async" class="alignnone wp-image-829 size-full" src="/wp-content/uploads/2025/06/display-the-notes-table-under-form.png" alt="" width="1656" height="701" /></p>
<p>This code instantiates the custom table class, prepares the data by calling <code>prepare_items()</code>, and then renders the entire table structure with <code>display()</code>. The result is a professional, fully-featured admin table for managing notes.</p>
<p>The next article, <a href="/blog/building-a-wordpress-plugin-implementing-ajax-for-dynamic-content/">Building a WordPress Plugin: Implementing AJAX for Dynamic Content</a>, updates Add Note to use AJAX so a successful submission can update the list without reloading the page.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://sajdoko.com/blog/building-a-wordpress-plugin-creating-the-admin-dashboard-page/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Basic On-Site SEO Optimizations for WordPress Template Sites</title>
		<link>https://sajdoko.com/blog/basic-on-site-seo-optimizations-for-wordpress-template-sites/</link>
					<comments>https://sajdoko.com/blog/basic-on-site-seo-optimizations-for-wordpress-template-sites/#respond</comments>
		
		<dc:creator><![CDATA[sajdoko]]></dc:creator>
		<pubDate>Mon, 05 May 2025 11:04:04 +0000</pubDate>
				<category><![CDATA[Optimization]]></category>
		<category><![CDATA[SEO]]></category>
		<category><![CDATA[WordPress]]></category>
		<guid isPermaLink="false">https://sajdoko.al/blog/basic-on-site-seo-optimizations-for-wordpress-template-sites/</guid>

					<description><![CDATA[A premade WordPress template gives you a design, but its SEO setup still needs checking. This guide covers page titles, headings, content, speed and the template&#8230;]]></description>
										<content:encoded><![CDATA[<p>A premade WordPress template gives you a design, but its SEO setup still needs checking. This guide covers page titles, headings, content, speed and the template features that affect search access.</p>
<h2>Fundamental On-Site SEO Principles</h2>
<p>On-site SEO (also known as on-page SEO) involves optimizing elements of your web pages to make them understandable to search engines and user-friendly for visitors. Common on-page factors include page titles, content, internal links, URLs, and other page elements. Below are some fundamental principles you should apply on every WordPress page or post:</p>
<p>&nbsp;</p>
<p><img loading="lazy" decoding="async" class="alignnone size-full wp-image-725" src="/wp-content/uploads/2025/05/bad-vs.-good-Google-search-snippet.webp" alt="Example of optimized meta title and description in search results" width="1920" height="1280" /></p>
<h3></h3>
<h3><strong>Unique Title Tags and Meta Descriptions</strong></h3>
<p>Ensure each page/post has a unique SEO title and meta description. The title tag (typically 50-60 characters) should concisely describe the page content and include relevant keywords, since it’s the “headline” that appears in search results. The meta description (~150-160 characters) should summarize the page and entice users to click. WordPress SEO plugins make it easy to set these for each page and even show a snippet preview of how it will look on Google. Optimizing your titles and descriptions can improve your click-through rate from the search results, so don’t skip this basic step.</p>
<h3><strong>Heading Structure and Content Organization</strong></h3>
<p>Use HTML heading tags (H1, H2, H3, etc.) to structure your content in a logical hierarchy. Generally, each page should have one H1 (often the post/page title) that clearly indicates the topic of that page, and subsequent subheadings (H2, H3&#8230;) to break content into sections. Properly structured headings make your content easier to read and navigate. Google’s own SEO Starter Guide emphasizes writing content that is well-organized and easy to follow, using headings to help users (and crawlers) navigate your pages. For example, in a blog post about “SEO Tips,” the H1 might be the title of the post, H2s could be subtopics like “On-Page SEO Fundamentals” or “Optimizing WordPress Templates,” and beneath those, H3s can further organize details. This hierarchy not only improves readability but also gives search engines clues about the main topics and subtopics of your content.</p>
<h3><strong>High-Quality, Original Content</strong></h3>
<p>Quality content is at the heart of on-site SEO. Avoid using placeholder text or boilerplate content that came with your template &#8211; every page’s content should be original and valuable. Google rewards content that is <strong>unique</strong> and useful to readers <a href="https://developers.google.com/search/docs/fundamentals/seo-starter-guide#:~:text=,rehash%20what%20others%20already%20published" target="_blank" rel="noopener">developers.google.com</a>. That means you shouldn’t just copy text from other sites (or leave the template’s demo text in place); instead, provide information or insights written in your own voice. Make sure each page fully covers its topic and offers something new or helpful. Thin content (very short pages with little info) or duplicate content across multiple pages can hurt your SEO. In fact, if the same text appears on multiple URLs of your site, search engines might consider it duplicate content and have to decide which page to index. To avoid internal duplicates, configure your blog listing pages to show excerpts instead of full posts (so your homepage or category pages don’t each display the entire article content). Overall, focus on delivering clear, informative content that is different from what’s already on your site (and elsewhere on the web) &#8211; this keeps both readers and search engines happy.</p>
<h3><strong>SEO-Friendly URL Slugs</strong></h3>
<p>Pay attention to your page URLs (permalinks). WordPress by default can use numeric or date-based URLs, but for SEO it’s best to use “Post name” or a custom structure that incorporates words. Edit the URL slug for each page/post to be short, descriptive, and keyword-rich (e.g. <a href="/services/web-development/"><code>/products/wordpress-websites</code></a> instead of <code>/p=123</code>). Descriptive URLs help search engines and users alike understand what a page is about. Moreover, parts of the URL may be shown as breadcrumb links in Google results, giving additional context. Avoid leaving auto-generated gibberish or very long strings in your URLs &#8211; a simpler URL with relevant words is more SEO-friendly. You can set your global permalink structure in <strong>Settings &gt; Permalinks</strong> (most sites choose the &#8220;<strong>Post name</strong>&#8221; option for clean URLs), and then customize individual slugs as needed when editing content.</p>
<h3><strong>Internal Linking</strong></h3>
<p>Link your pages to each other wherever it makes sense. Internal links (links from one page on your site to another) are crucial for SEO <strong>and</strong> usability. They help visitors discover related content and navigate your site, and they help search engine crawlers find all your pages and understand the structure of your site. A good internal linking strategy can establish a hierarchy of importance among your pages &#8211; for example, if many pages link to your “Services” page, that signals it’s important. When adding internal links, use descriptive anchor text (the clickable text of the link). Google’s guidelines note that anchor text should indicate what the linked page is about &#8211; so instead of writing “click here,” you might write “learn more on our SEO services page,” linking the words “<strong><a href="/services/seo/">SEO services</a>.</strong>” This way, users and search engines get context from the link itself. Make it a habit to interlink relevant posts/pages (e.g., link your blog post about keyword research to another post about on-page SEO if they relate). Over time, good internal linking will improve your site’s crawlability and can pass “link juice” (ranking power) to your most important pages.</p>
<p>Technical tip: Also ensure your site is <strong>indexable</strong> by search engines. In WordPress, under <strong>Settings &gt; Reading</strong>, there’s a checkbox labeled “Discourage search engines from indexing this site.” This should be <strong>unchecked</strong> on a live site. Many people enable that setting while developing a site to keep it off Google temporarily, but forget to disable it later. If left checked, it tells Google not to index you &#8211; essentially negating all your SEO efforts. So double-check this setting when launching your site.</p>
<p>&nbsp;</p>
<h2>Best Practices for Optimizing WordPress Templates for SEO</h2>
<p>Using a premade WordPress template (theme) saves design time, but you must tailor it for SEO. Many templates are built for visual appeal and demo content rather than search optimization. Here are some best practices to ensure your template-based site is optimized:</p>
<h3><strong>Replace or Remove Duplicate Content</strong></h3>
<p>One common issue with premade templates is <strong>duplicate content</strong>. This can happen in a few ways. First, if you imported sample pages or demo text from the template, make sure to replace all of it with your own content. Leaving template filler text (like the infamous “Lorem ipsum” paragraphs or placeholder images) not only looks unprofessional but could appear on many other websites using the same theme. Search engines favor unique content, so purge any duplicate or boilerplate text. Second, be mindful of WordPress generating duplicates &#8211; for instance, the same blog post appearing on the homepage, category archive, tag archive, etc. As noted earlier, showing full posts in multiple places can trigger duplicate content concerns. To mitigate this, configure your archives to show excerpts and/or use canonical URLs (which SEO plugins can set automatically) to signal the “main” version of content to Google. The goal is to ensure every piece of content on your site lives at a single URL. By using original text and proper settings, you’ll avoid confusing search engines with duplicates.</p>
<p><strong>Customize Default Template Elements:</strong> Templates often come with default settings that should be customized for your site’s SEO. For example, update the <strong>site title and tagline</strong> (found in <strong>Settings &gt; General</strong>) from something generic like “Just Another WordPress Site” to a tagline with your keywords or brand message. Ensure each page’s title is unique; some themes might by default use the same title for multiple sections if not configured. If your template came with pre-filled content blocks or sample pages (About pages, contact info, etc.), don’t just leave them as-is &#8211; rewrite them to reflect your business and target keywords. Also, check for any <strong>hard-coded headings</strong> in the template. Some premade layouts might use a specific phrase in headings across pages; if so, edit those in the page editor or theme settings to avoid having multiple pages with the same subheadings. Essentially, you want to make the template your own: all visible text, headings, and media should be tailored to your content strategy. This not only improves SEO with relevant keywords, but also provides a better user experience by delivering content that matches your niche rather than a generic template design.</p>
<h3><strong>Optimize Page Load Speed</strong></h3>
<p>Speed is a critical on-site factor &#8211; users are impatient, and Google’s algorithm favors fast-loading websites. Premade templates can sometimes be bloated with fancy scripts, sliders, and features that slow down load times. Optimize your site’s performance by doing a few things:</p>
<ul>
<li><strong>Enable Caching:</strong> Use a caching plugin (we’ll recommend some later) to generate static HTML versions of your pages, so visitors aren’t hitting the database for each page load. Caching can dramatically improve loading times by serving pre-built pages and reducing server work.</li>
<li><strong>Optimize Images:</strong> Large images are often the biggest contributors to slow pages. Compress your images and use appropriate sizes. You can install an image optimization plugin to automatically shrink file sizes (without noticeable quality loss) and even serve next-gen formats like WebP. This can make a huge difference &#8211; image optimizers help improve loading speed and thus search rankings by reducing image bloat <a href="https://elementor.com/blog/image-optimization-plugins/#:~:text=Image%20optimization%20plugins%20assist%20website,speed%20by%20efficiently%20compressing%20images" target="_blank" rel="noopener">elementor.com</a>.</li>
<li><strong>Minimize and Defer Assets:</strong> Many templates load multiple CSS and JavaScript files. Consider using a plugin like <strong>Autoptimize</strong> to minify (compress) CSS/JS and combine files, and defer non-critical JS to load later. Disable any template features or plugins you don’t need &#8211; for example, if your template has an optional slideshow but you aren’t using it, turn it off to avoid loading that code.</li>
<li><strong>Use a Fast Hosting and CDN:</strong> While not template-specific, it’s worth noting that good hosting improves speed. A lightweight theme on slow hosting will still struggle. Ensure your host is performant, and consider using a Content Delivery Network (CDN) to serve static resources quickly to global users.</li>
</ul>
<p>The payoff for speeding up your template can be significant: faster sites not only retain visitors better (lower bounce rates) but also rank higher on Google on average. Use free tools like Google PageSpeed Insights or GTmetrix to test your site’s speed and get specific recommendations.</p>
<ul>
<li><strong>Ensure Mobile-Friendliness (Responsive Design):</strong> Mobile optimization is non-negotiable today. Google has shifted to mobile-first indexing, meaning it predominantly uses your site’s mobile version to rank and index content <a href="https://developers.google.com/search/docs/crawling-indexing/mobile/mobile-sites-mobile-first-indexing#:~:text=Google%20uses%20the%20mobile%20version,first%20indexing" target="_blank" rel="noopener">developers.google.com</a>. If your template isn’t mobile-responsive, your SEO will suffer greatly. Most modern WordPress themes are labeled “responsive,” which means they automatically adjust the layout for smaller screens. You should test this: open your site on a phone or use Chrome’s device toolbar to simulate a mobile device. Check that text is readable without zooming, images/videos scale correctly, and navigation is easy to use on touch screens. Any elements that break or look bad on mobile need fixing &#8211; that could mean tweaking CSS or using different template options. If your theme has mobile-specific settings (some provide options to hide certain elements on mobile to improve performance or layout), utilize those. Google strongly recommends using responsive design as the best mobile setup because it serves the same content to all devices. In practice, this means avoid older approaches like separate “m.dot” mobile sites or excessive reliance on AMP (Accelerated Mobile Pages) unless you have a specific need. In summary, choose a template that advertises mobile-friendly design and test it. With mobile traffic now surpassing desktop in many industries, a poor mobile experience will not only hurt rankings but also drive away a huge portion of your potential audience.</li>
<li><strong>Mind Technical SEO Settings:</strong> Even with a template, there are a few technical settings you should verify. We already mentioned the importance of making sure the site isn’t hidden from search engines. Another setting to check is your <strong>URL structure</strong> (permalinks), which we covered &#8211; use SEO-friendly URLs rather than the default <code>?p=ID</code> format. Also, generate an XML sitemap (most SEO plugins can do this automatically) and submit it to Google Search Console so the search engine can easily find all your pages. If your site is new or using a new template, double-check your <strong>robots.txt</strong> file &#8211; by default WordPress’s robots.txt is fine, but ensure the template or a plugin didn’t add any unexpected disallow rules that might block content. Finally, be careful with categories and tags: if your WordPress template heavily uses tag clouds or category pages, be mindful that those archive pages might get indexed and potentially dilute your content’s SEO if they are thin. You can noindex certain thin pages (like tag archives) via SEO plugins if needed. The key is to think beyond just the visual template and make sure the underlying SEO settings are correct.</li>
</ul>
<p>By following the above best practices, you adapt your template to be not just a pretty face, but also an SEO-friendly site. Now let’s look at some useful tools that make these optimizations easier.</p>
<p>&nbsp;</p>
<h2>Free Tools and Plugins for SEO Optimization in WordPress</h2>
<p>One of the great advantages of WordPress is its plugin ecosystem. There are many free plugins that can help you implement SEO best practices without needing to code. Below is a list of recommended free tools and plugins to boost your on-site SEO:</p>
<p>&nbsp;</p>
<p><img loading="lazy" decoding="async" class="alignnone size-full wp-image-728" src="/wp-content/uploads/2025/05/collage.png" alt="Recommended free SEO and performance plugins for WordPress" width="1235" height="254" /></p>
<p>&nbsp;</p>
<h3><strong>Yoast SEO</strong></h3>
<p>Arguably the most popular SEO plugin for WordPress, <a href="https://wordpress.org/plugins/wordpress-seo/" target="_blank" rel="noopener"><strong>Yoast SEO</strong></a> has been around since 2010 and is practically synonymous with “WordPress SEO” for many. It provides an easy interface to set your meta titles and descriptions for each page, and gives you a readability and keyword optimization analysis while you write. Yoast will generate an XML sitemap for you, add canonical link tags to avoid duplicate content issues, and offer guidance like “you’ve used this keyword too many times” or “add an internal link.” The plugin also integrates with Google Search Console for insights. The free version is very robust; a premium version adds features like multiple focus keywords and internal link suggestions, but you can get very far with the free tool alone. Yoast is a great starting point to ensure your template’s pages each have solid SEO basics covered. (Pro tip: After installing Yoast, go to its settings and use the Setup Wizard &#8211; it will ask you questions about your site and configure many meta defaults for you.)</p>
<h3><strong>Rank Math</strong></h3>
<p><strong> <a href="https://wordpress.org/plugins/seo-by-rank-math/" target="_blank" rel="noopener">Rank Math</a></strong> is a newer SEO plugin (launched in 2018) that quickly rose in popularity to become a top Yoast competitor. Many site owners favor Rank Math for its generous free features &#8211; it includes built-in support for things like schema markup, multiple focus keywords analysis (up to 5 in the free version), and even a 404 error monitor and redirection manager. Rank Math’s interface is user-friendly, and it also adds an SEO meta box in the post editor similar to Yoast, where you can set titles, descriptions, and target keywords, and get an SEO score for your content. Choosing between Yoast and Rank Math often comes down to preference; both cover the essential on-page SEO needs for a WordPress site. Notably, Rank Math’s free version includes features (like local SEO and WooCommerce SEO tweaks) that Yoast might require a paid addon for. If you’re building multiple sites with templates, Rank Math’s import tool can even import settings from Yoast, making it easy to switch. In any case, using one of these SEO plugins is highly recommended &#8211; they handle a lot of behind-the-scenes optimization (like meta tags, canonical URLs, sitemaps) that a plain template won’t do on its own.</p>
<h3><strong>Caching Plugin (WP Super Cache or W3 Total Cache)</strong></h3>
<p>To improve site speed, install a caching plugin. Two long-standing free options are <a href="https://wordpress.org/plugins/wp-super-cache/" target="_blank" rel="noopener"><strong>WP Super Cache</strong></a> (by Automattic) and <a href="https://wordpress.org/plugins/w3-total-cache/" target="_blank" rel="noopener"><strong>W3 Total Cache</strong></a>. These plugins generate static HTML files of your pages and posts, which dramatically reduces page load times for repeat visitors and lowers server load. Essentially, caching stores a pre-built version of a page so that each user (or Googlebot) doesn’t have to wait for WordPress to query the database and build the page from scratch. As mentioned earlier, caching can help your pages load in under 2 seconds &#8211; which is a common target for good UX and SEO. WP Super Cache is very straightforward: you can turn caching on and it works in the background. W3 Total Cache is more advanced, with options for minification, object caching, CDN integration, etc. If you’re not sure, start with WP Super Cache for simplicity. The improvement in speed will not only please your visitors but also signal to search engines that your site is optimized for performance.</p>
<h3><strong>Image Optimization Plugins (Smush, EWWW Image Optimizer, etc.)</strong></h3>
<p>Large image files can slow your site immensely, so an image compression plugin is a must if your template is image-heavy. <a href="https://wordpress.org/plugins/wp-smushit/" target="_blank" rel="noopener"><strong>Smush</strong> </a>(by WPMU Dev) and <a href="https://wordpress.org/plugins/ewww-image-optimizer/" target="_blank" rel="noopener"><strong>EWWW Image Optimizer</strong></a> are two highly-rated free plugins that automatically compress images as you upload them to the media library. These plugins can also bulk-compress existing images. They remove unnecessary metadata and use compression algorithms to reduce file size without visibly harming image quality. The result is faster loading pages and improved core web vitals. According to tests, using image optimizers leads to quicker load times and can improve your search engine rankings by enhancing page speed and user experience. Many image optimizers also have options for lazy-loading images (so images below the fold load only when the user scrolls to them) and converting images to next-gen formats like WebP for supported browsers. By incorporating one of these plugins, you ensure that the beautiful photos or graphics in your template aren’t undermining your SEO with slow load times.</p>
<h3><strong>Yoast SEO (or Rank Math) Structured Data &amp; Schema</strong></h3>
<p>This isn’t a separate plugin, but it’s worth noting: both Yoast and Rank Math help with adding <strong>structured data</strong> (schema markup) to your pages, which can enhance how your listings appear in search results (for instance, adding star ratings, FAQ drop-downs, breadcrumbs, etc., known as rich snippets). For most users, the default schema that these SEO plugins add (like marking your pages as Articles, your business name, etc.) is enough. Just be aware of this feature &#8211; you usually just need to fill in your organization info in the plugin settings. If your template doesn’t explicitly support schema, the SEO plugin’s output will cover the basics. For more advanced schema (like recipes or events), you might consider a dedicated plugin, but that veers into advanced SEO. The takeaway: your SEO plugin can help your template communicate more context to Google through structured data, so make sure to configure those options.</p>
<h3><strong>Performance Optimization Plugins (Autoptimize, Asset Cleanup)</strong></h3>
<p>In addition to caching, you can use plugins like <a href="https://wordpress.org/plugins/autoptimize/" target="_blank" rel="noopener"><strong>Autoptimize</strong> </a>(free) to minify and combine your CSS/JS files, and <a href="https://wordpress.org/plugins/wp-asset-clean-up/" target="_blank" rel="noopener"><strong>Asset CleanUp</strong></a> to conditionally load certain scripts only on specific pages. These require a bit more understanding to configure, but they can further speed up a templated site by reducing bloat. For example, if your template loads a slideshow script on every page but you only use it on the homepage, Asset CleanUp can prevent it from loading elsewhere. This level of optimization can shave off seconds and improve your site’s <strong>Core Web Vitals</strong>, which are performance metrics Google considers for ranking.</p>
<h3><strong>SEO Audit and Analytics Tools</strong></h3>
<p>Lastly, leverage free tools from Google to monitor your site’s SEO progress. <a href="https://search.google.com/search-console/about" target="_blank" rel="noopener"><strong>Google Search Console</strong></a> is indispensable &#8211; it will show you which pages are indexed, alert you to any mobile usability issues or crawl errors, and let you know what search queries are leading people to your site. Be sure to submit your XML sitemap through Search Console for easier indexing. <strong>Google Analytics</strong> (or an alternative analytics tool) is also important for understanding your traffic and user behavior. While these aren’t WordPress plugins, Google offers a <strong>Site Kit</strong> plugin that can integrate Search Console, Analytics, and PageSpeed Insights right into your WordPress dashboard for convenience. Using these tools, you can track the impact of your on-site changes: for instance, if you optimize a page’s title and speed, Search Console can show if its average position or click-through rate improves over time.</p>
<p>All the above tools are free, and they address different aspects of on-site SEO &#8211; from content optimization to speed to monitoring. By combining an SEO plugin with performance plugins and Google’s free tools, you equip yourself to cover all bases. Remember not to go overboard with too many plugins (that can slow down your site); pick a well-rounded set that addresses your needs. For most template-based sites, an SEO plugin + caching + image optimization + Search Console is an excellent starting lineup.</p>
<p>&nbsp;</p>
<h2>Evaluating and Improving Your Template’s SEO-Friendliness</h2>
<p>Not all WordPress templates are created equal in terms of SEO. Some themes market themselves as “SEO-optimized” &#8211; typically meaning they have clean code, fast performance, and proper use of HTML tags &#8211; while others may look pretty but hide SEO flaws under the hood. As someone building with a premade template, you should evaluate your theme’s SEO-friendliness and be ready to adjust common flaws. Here are some tips:</p>
<h3><strong>Choose a Lightweight, Well-Coded Theme</strong></h3>
<p>The foundation of an SEO-friendly site is a theme that isn’t bloated or poorly coded. If you’re still in the process of selecting a template, favor those that advertise performance and simplicity (for example, themes known for being lightweight like GeneratePress, Astra, or Kadence). Bloated code can hinder crawlability &#8211; if a theme has tons of nested HTML, inline scripts, or unnecessary elements, search engines might struggle to efficiently crawl and understand the content. In contrast, a lean theme with valid HTML5 and semantic markup ensures Googlebot can parse your pages without trouble. Themes that follow WordPress coding standards and best practices for headings and meta tags are ideal. Some templates unfortunately limit your ability to edit certain SEO-relevant areas (like they might not output an H1 tag for the page title, or they hard-code a title across pages). Be cautious of those. If you suspect your theme’s code is affecting SEO, you can run a site audit using a tool like Screaming Frog or an online validator to see if there are structural issues. In short, <strong>theme matters</strong> &#8211; a bad theme can drag down an otherwise good site. As Google’s John Mueller has noted, the theme you choose can impact your SEO, especially via page speed and structured data. Choose one that gives you a solid technical baseline.</p>
<h3><strong>Test Page Speed and Clean Up Bloat</strong></h3>
<p>Even after choosing a theme, continually monitor how it performs. Use Google PageSpeed Insights or Pingdom to test your site. If you notice the template loads 20+ separate scripts/styles, see if you can disable some features. For instance, many multipurpose templates load scripts for sliders, portfolios, animations, etc., on every page regardless of use. Turn off features in the theme options that you don’t use. You might even dequeue scripts using a plugin or custom code if necessary. The goal is to have your site running only the essentials. Template developers often include lots of functionality to cover various user needs, but you don’t need all of it. By trimming the fat, you’ll improve load times and avoid any negative SEO impact from slow pages. Remember, <strong>faster sites rank better</strong> on Google on average, so squeezing better performance out of your template is worth the effort. If performance is still lagging, consider using a child theme to manually remove especially heavy elements, or in extreme cases, switching to a more performance-oriented theme.</p>
<h3><strong>Check Mobile Responsiveness Thoroughly</strong></h3>
<p>Don’t just take the theme developer’s word for it &#8211; test the responsiveness yourself. Use Google’s Mobile-Friendly Test or simply navigate your site on multiple devices (phone, tablet). Look for any glitches: text overflowing off screen, elements that are too close together to tap, images that don’t resize, etc. These are common issues in some older or poorly maintained templates. If your site fails Google’s mobile-friendly criteria, it can significantly hurt your rankings, since Google predominantly uses mobile-first indexing now. If you do encounter problems, check if the theme has updates (developers often release updates to fix such issues). If not, you may need to add custom CSS or use a plugin like WP Touch (though a responsive theme is preferable to using a mobile plugin). In worst case, if a template just isn’t mobile-friendly and cannot be easily fixed, it might be worth finding a new theme &#8211; the mobile usability of your site is that important. On the flip side, a template that excels in mobile UX is a big SEO win. Fast load on mobile, easy navigation, and well-sized content will keep mobile visitors engaged and satisfy Google’s criteria.</p>
<h3><strong>Audit the Template’s HTML Structure (Especially Headings)</strong></h3>
<p>It’s a good practice to inspect how your theme handles headings and other markup. For example, view the source of your homepage and see: is the site name wrapped in a &lt;h1&gt; on every page? (Some themes mistakenly put the logo or site title in an H1 on every page, which means your pages technically have two H1s &#8211; one for the logo and one for the page title &#8211; not ideal.) A well-coded template will use one H1 per page (usually the page or post title) and use subsequent heading levels properly in content areas. Also check things like: are navigation menus in proper lists, are sidebars using headings appropriately for section titles, etc. Proper HTML5 semantic structure helps SEO. If you find issues (like multiple H1s, or missing alt attributes on important images), you can often correct them by editing the theme files via a child theme or sometimes just by configuring the content differently in the editor. Search engines rely on HTML structure to understand page context. For instance, Google expects the H1 to reflect the main topic. If your template’s structure is confusing that (e.g., by having logo as H1), then consider adjusting it. Many SEO plugins will flag if a page has no H1 or multiple H1s, so their page analysis can help spot these problems too. Fixing a template’s HTML structure might be as simple as unchecking a setting (some themes let you change, say, the logo tag from H1 to div) or as involved as editing PHP templates &#8211; but it’s worth doing for the important pages. A clean structure gives your content the best chance to be properly indexed and understood.</p>
<h3><strong>Verify Meta Data &amp; Schema Handling</strong></h3>
<p>Some templates come with built-in SEO settings or outputs &#8211; for example, a theme might automatically put the site name at the end of your &lt;title&gt; tag, or it might include some schema JSON-LD data for breadcrumbs or product pages. It’s worth knowing what your theme does in this regard. In most cases, if you’re using a dedicated SEO plugin like Yoast or Rank Math, you’ll want to avoid duplicating meta tags. Ensure the theme isn’t generating a meta description that could conflict with the one from your SEO plugin (usually not an issue in modern themes &#8211; most defer to SEO plugins, but check). If the theme has an option like “SEO Settings,” decide if you want to use them or the plugin’s settings (generally, let the plugin handle it to keep things consolidated). Also, note that a poorly coded theme might fail to output basic things like the SEO title tag properly &#8211; by default, WordPress (since a few years ago) uses the theme’s template to output titles. Virtually all updated themes use the WordPress add_theme_support( &#8216;title-tag&#8217; ) feature which lets WP handle the title, but if you happen to use an older theme, you might need an SEO plugin to force rewrite titles. In summary, check that your template isn’t working against your SEO efforts: it should allow plugins to add necessary tags in the &lt;head&gt; and not double-output things. If your template is relatively popular and up-to-date, you likely won’t have issues here.</p>
<h3><strong>Address Common Template SEO Flaws</strong></h3>
<p>Some SEO issues aren’t obvious until you dig into Google Search Console or do an audit. For example, does your template create <strong>paginated</strong> <strong>pages</strong> or infinite scroll for posts? Ensure that Google can crawl those or that you use proper rel=“prev/next” or load more buttons. Does the template use large background images or video that slow the site? Maybe replace a heavy video background with a static image or optimize the video for web. Another example: templates with a lot of <strong>shortcode</strong> usage (from page builders) can sometimes clutter the actual HTML with divs and spans &#8211; not a huge SEO issue, but keep an eye on your rendered text-to-HTML ratio and make sure search engines can get to the real content without wading through 50 nested divs. If you encounter something like an HTML element that is hurting SEO (e.g., an important piece of content is only shown via JavaScript that Google might not render, or the theme uses an &lt;h2&gt; for every widget title which makes your sidebar look as important as your content), look for theme settings or reach out to the theme developer for guidance. Often, there are workarounds known in the theme’s support community for SEO tweaks. At the end of the day, remember that <strong>content is king</strong> &#8211; the template is a vehicle to deliver content. Make sure your template showcases your content in a way that search engines can easily access and interpret. If you suspect your beautiful template is holding your SEO back, don’t hesitate to make changes. The beauty of WordPress is you can switch themes or modify them; the content (which is what Google cares most about) stays in the database. Your goal is to have a site that both looks good to users and communicates effectively with search engines.</p>
<p>&nbsp;</p>
<h2>Conclusion</h2>
<p>Review page titles, headings, internal links and loading speed when you add content or change the theme. Use Search Console to find indexing problems and check whether the pages reach relevant searches.</p>
<p>Keep a record of your changes and check their results. A template provides the layout; the content and ongoing maintenance still need attention. <strong>Good luck, and happy optimizing!</strong></p>
]]></content:encoded>
					
					<wfw:commentRss>https://sajdoko.com/blog/basic-on-site-seo-optimizations-for-wordpress-template-sites/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
	</channel>
</rss>
