Skip to main content

At Zeenko, we often tackle exciting challenges involving API integrations and custom WordPress development. Recently,we built a plugin to seamlessly fetch and display real estate listings from the Propstack API directly within a WordPress site. Today, we're pulling back the curtain to show you the core components and logic behind creating such a plugin.

Whether you're a budding WordPress developer or just curious about what goes into plugin development, this guide will walk you through the essential building blocks.

Disclaimer: This guide assumes some familiarity with PHP, WordPress plugin development concepts, CSS, and JavaScript (jQuery). The Propstack API requires an API key for access.


I. The Plugin Foundation: PHP Structure & WordPress Hooks

Our plugin is encapsulated in a primary PHP file. Let's call it propstack-integration.php.

1. Plugin Header & Security: Every WordPress plugin starts with a standard header comment providing metadata. We also include a security check to prevent direct access.

 <?php
/**
 * Plugin Name: Propstack Integration
 * Plugin URI: https://yourwebsite.com // Change to your actual URI
 * Description: Integrates Propstack API to display property listings on WordPress
 * Version: 1.0.0
 * Author: Zeenko (Your Name) // Or your name/company
 * License: GPL v2 or later
 */

// Prevent direct access
if (!defined('ABSPATH')) {
    exit;
}

// Define plugin constants for easy path/URL management
define('PROPSTACK_PLUGIN_URL', plugin_dir_url(__FILE__));
define('PROPSTACK_PLUGIN_PATH', plugin_dir_path(__FILE__));
define('PROPSTACK_API_BASE_URL', 'https://api.propstack.de/v1'); // Propstack API Base
 

2. The Main Plugin Class: PropstackIntegration We'll use a class to organize our plugin's functionality.

PHP
class PropstackIntegration {
private $api_key;

public function __construct() {
// Load API key from WordPress options
$this->api_key = get_option('propstack_api_key');

// WordPress Action Hooks
add_action('init', array($this, 'init_plugin')); // Changed from 'init' to avoid conflict
add_action('wp_enqueue_scripts', array($this, 'enqueue_assets')); // Changed for clarity
add_action('admin_menu', array($this, 'add_admin_menu_page')); // Changed for clarity
add_action('admin_init', array($this, 'register_plugin_settings')); // Changed for clarity

// Shortcode
add_shortcode('propstack_listings', array($this, 'display_listings_shortcode'));

// AJAX Actions for frontend and admin
add_action('wp_ajax_load_more_properties', array($this, 'ajax_load_more_properties')); // Changed
add_action('wp_ajax_nopriv_load_more_properties', array($this, 'ajax_load_more_properties')); // Changed
add_action('wp_ajax_test_propstack_connection', array($this, 'ajax_test_api_connection')); // Changed
}

public function init_plugin() {
// Future initializations, like custom post types if needed
}

// ... other methods will be discussed below ...
}

// Initialize the plugin
new PropstackIntegration();

In the constructor, we hook various methods into WordPress's execution flow using add_action and register our shortcode with add_shortcode.


II. Enqueueing Assets: CSS & JavaScript

To make our listings look good and behave dynamically, we need CSS and JavaScript.

PHP
    public function enqueue_assets() {
// Enqueue stylesheet
wp_enqueue_style(
'propstack-css',
PROPSTACK_PLUGIN_URL . 'assets/propstack.css',
array(),
'1.0.0'
);

// Enqueue JavaScript (jQuery dependency, loaded in footer)
wp_enqueue_script(
'propstack-js',
PROPSTACK_PLUGIN_URL . 'assets/propstack.js',
array('jquery'),
'1.0.0',
true // Load in footer
);

// Pass data to JavaScript (e.g., AJAX URL, nonce for security)
wp_localize_script('propstack-js', 'propstack_ajax', array(
'ajax_url' => admin_url('admin-ajax.php'),
'nonce' => wp_create_nonce('propstack_nonce') // For general AJAX
));
}

wp_enqueue_style and wp_enqueue_script are the standard WordPress functions for adding assets. wp_localize_script is crucial for passing PHP variables (like the AJAX URL and a security nonce) to our JavaScript file.


III. Admin Settings Page

A settings page allows users to configure the plugin, primarily for entering their Propstack API key.

1. Adding the Menu Page:

PHP
    public function add_admin_menu_page() {
add_options_page(
'Propstack Settings', // Page Title
'Propstack', // Menu Title
'manage_options', // Capability required
'propstack-settings', // Menu Slug
array($this, 'render_settings_page_html') // Callback to render HTML
);
}

2. Registering Settings & Fields: We use the WordPress Settings API to handle saving and validating options.

PHP
    public function register_plugin_settings() {
// Register a setting group
register_setting('propstack_settings_group', 'propstack_api_key'); // Sanitize callback can be added
register_setting('propstack_settings_group', 'propstack_properties_per_page', 'intval');
register_setting('propstack_settings_group', 'propstack_default_image', 'esc_url_raw');

// Add a settings section
add_settings_section(
'propstack_api_section',
'API Configuration',
null, // Optional callback for section description
'propstack-settings' // Page slug
);

// Add fields to the section
add_settings_field(
'propstack_api_key',
'API Key',
array($this, 'render_api_key_field'),
'propstack-settings',
'propstack_api_section'
);
// ... similar add_settings_field calls for properties_per_page and default_image ...
}

Field rendering callbacks (render_api_key_field, etc.) simply echo the HTML for the input fields.

3. Rendering the Settings Page HTML (render_settings_page_html): This method outputs the form for our settings.

PHP
    public function render_settings_page_html() {
?>
<div class="wrap">
<h1>Propstack Settings</h1>
<form action="options.php" method="post">
<?php
settings_fields('propstack_settings_group'); // Matches register_setting
do_settings_sections('propstack-settings'); // Matches add_settings_section
submit_button();
?>
</form>

<?php if (!empty($this->api_key)): ?>
<div class="propstack-test-connection" style="margin-top: 20px;">
<h2>API Connection Test</h2>
<button type="button" id="test-propstack-connection" class="button button-secondary">Test API Connection</button>
<div id="propstack-test-results" style="margin-top: 10px;"></div>
<script>
// Inline JS for the test button (simplified here for brevity)
// Ideally, this would also be in an enqueued JS file
document.getElementById('test-propstack-connection').addEventListener('click', function()
{
// ... (Fetch API call to our test_api_connection AJAX action) ...
// Uses a unique nonce for this action, e.g., wp_create_nonce('propstack_test_nonce')
});
</script>
</div>
<?php endif; ?>
<h2>Usage</h2>
<p>Use the shortcode <code>[propstack_listings]</code> to display property listings.</p>
</div>
<?php
}

The "Test API Connection" button uses JavaScript to make an AJAX request to our ajax_test_api_connection method.


IV. Communicating with the Propstack API

1. Making API Requests (make_api_request): This is a helper function to centralize API calls.

PHP
    private function make_api_request($endpoint, $params = array()) {
if (empty($this->api_key)) {
return new WP_Error('no_api_key', 'Propstack API key not configured.');
}

$url = PROPSTACK_API_BASE_URL . '/' . ltrim($endpoint, '/');
// Propstack might require API key in headers or as a query param.
// This example assumes it's primarily in headers.
// Adjust $params as needed if API key is also a query param for GET.

$response = wp_remote_get(add_query_arg($params, $url), array(
'timeout' => 30,
'headers' => array(
'X-API-KEY' => $this->api_key // Common way to send API key
)
));

if (is_wp_error($response)) {
return $response;
}

$body = wp_remote_retrieve_body($response);
$data = json_decode($body, true);

if (wp_remote_retrieve_response_code($response) !== 200) {
return new WP_Error('api_error', 'API Request Failed: ' . ($data['message'] ?? $body));
}
return $data;
}

2. Fetching Properties & Caching (get_properties): This method fetches properties and implements caching using WordPress Transients.

PHP
    public function get_properties($params = array()) {
$default_params = array(
'page' => 1,
'per_page' => get_option('propstack_properties_per_page', 12),
'with_meta'=> 1 // Assuming Propstack uses this for metadata
);
$params = wp_parse_args($params, $default_params);

// Create a unique cache key based on parameters
$cache_key = 'propstack_units_' . md5(serialize($params));
$cached_data = get_transient($cache_key);

if (false !== $cached_data) {
return $cached_data; // Return cached data if available
}

// 'units' is assumed to be the Propstack endpoint for listings
$response = $this->make_api_request('units', $params);

if (!is_wp_error($response)) {
// Cache successful response for 5 minutes
set_transient($cache_key, $response, 5 * MINUTE_IN_SECONDS);
}
return $response;
}

Caching is vital to avoid hitting API rate limits and to speed up your site.


V. Displaying Listings: Shortcode & Rendering

1. The Shortcode Handler (display_listings_shortcode):

PHP
    public function display_listings_shortcode($atts) {
$atts = shortcode_atts(array(
'limit' => get_option('propstack_properties_per_page', 12),
'columns' => 3,
'show_pagination' => 'true',
'property_type' => '', // For filtering
'page' => 1 // Initial page for shortcode
), $atts, 'propstack_listings');

$params = array(
'per_page' => intval($atts['limit']),
'page' => intval($atts['page'])
);
if (!empty($atts['property_type'])) {
$params['type'] = sanitize_text_field($atts['property_type']);
}

$properties_data = $this->get_properties($params);

if (is_wp_error($properties_data)) {
return '<div class="propstack-error">Error: ' . esc_html($properties_data->get_error_message()) . '</div>';
}

ob_start();
// Pass $properties_data (API response) and $atts (shortcode attributes)
$this->render_properties_grid($properties_data, $atts);
return ob_get_clean();
}

2. Rendering Functions (render_properties_grid, render_property_card, render_pagination): These methods generate the HTML.

PHP
    private function render_properties_grid($api_response, $shortcode_atts) {
$properties = isset($api_response['data']) ? $api_response['data'] : array();
$meta = isset($api_response['meta']) ? $api_response['meta'] : array(); // For pagination
// Propstack's API might provide total count differently, e.g., $meta['total_count'] or $meta['total']
$total_properties = $meta['total_count'] ?? ($meta['total'] ?? count($properties));


if (empty($properties)) {
echo '<div class="propstack-no-results"><p>Keine Immobilien gefunden.</p></div>';
return;
}

// Data attributes for AJAX pagination in JS
echo '<div class="propstack-listings-container"
data-limit="'
. esc_attr($shortcode_atts['limit']) . '"
data-columns="'
. esc_attr($shortcode_atts['columns']) . '"
data-property-type="'
. esc_attr($shortcode_atts['property_type']) . '"
data-show-pagination="'
. esc_attr($shortcode_atts['show_pagination']) . '">';
echo '<div class="propstack-grid propstack-columns-' . esc_attr($shortcode_atts['columns']) . '">';
foreach ($properties as $property) {
// Column class can be handled purely by CSS grid, or calculated in PHP if needed
echo '<div class="propstack-property-card-wrapper">'; // Wrapper for column class
$this->render_property_card($property);
echo '</div>';
}
echo '</div>'; // .propstack-grid

if ($shortcode_atts['show_pagination'] === 'true') {
$current_page = intval($shortcode_atts['page']);
$per_page = intval($shortcode_atts['limit']);
$last_page = ceil($total_properties / $per_page);
if ($last_page > 1) {
$this->render_pagination($current_page, $last_page, $shortcode_atts);
}
}
echo '</div>'; // .propstack-listings-container
}

private function render_property_card($property) {
// Extract data: $title, $image_url, $address, $price, $status etc.
// Handle missing data gracefully and use esc_attr(), esc_html(), esc_url() for security.
// Example for image:
$default_image_url = get_option('propstack_default_image', PROPSTACK_PLUGIN_URL . 'assets/default-property.jpg');
$image_url = $default_image_url;
if (!empty($property['images']) && is_array($property['images'])) {
$first_image = $property['images'][0];
// Propstack API might offer different image sizes
$image_url = $first_image['medium'] ?? $first_image['big'] ?? $first_image['original'] ?? $default_image_url;
}
// ... HTML for the card structure (see your CSS for class names) ...
}

private function render_pagination($current_page, $last_page, $shortcode_atts) {
// ... HTML for pagination links, using data-page attributes for JS ...
}

Note the use of data-* attributes on the container to pass shortcode settings to our JavaScript for AJAX pagination.


VI. AJAX Functionality

1. Handling "Load More" / Pagination (ajax_load_more_properties):

PHP
    public function ajax_load_more_properties() {
// Security check: Verify nonce
check_ajax_referer('propstack_nonce', 'nonce');

$page = isset($_POST['page']) ? intval($_POST['page']) : 1;
$limit = isset($_POST['limit']) ? intval($_POST['limit']) : get_option('propstack_properties_per_page', 12);
$property_type = isset($_POST['property_type']) ? sanitize_text_field($_POST['property_type']) : '';
// Get columns to pass it back to render_properties_grid if necessary or handle it via JS
$columns = isset($_POST['columns']) ? intval($_POST['columns']) : 3;


$params = ['page' => $page, 'per_page' => $limit];
if (!empty($property_type)) {
$params['type'] = $property_type;
}

$api_response = $this->get_properties($params);

if (is_wp_error($api_response)) {
wp_send_json_error($api_response->get_error_message());
}

// We need to reconstruct the shortcode attributes for render_properties_grid
$shortcode_atts_for_render = [
'limit' => $limit,
'columns' => $columns, // Assuming you might need this in render_properties_grid
'property_type' => $property_type,
'page' => $page, // Current page being loaded
'show_pagination' => 'true' // Or get this from POST if it can change
];

ob_start();
// Re-use render_properties_grid to generate the HTML for the new set of properties
// but only the grid part, not the full container and pagination controls.
// So, we might need a more specific rendering function here, or adjust render_properties_grid.

// For simplicity, let's assume we render just the cards.
// A better approach would be to have render_properties_grid output only the grid
// and then call a separate render_pagination function.
// The JS would then replace only the grid and update pagination separately.

$html_content = '';
$properties = isset($api_response['data']) ? $api_response['data'] : array();
if (!empty($properties)) {
foreach ($properties as $property) {
ob_start();
$this->render_property_card($property); // Render individual card
$html_content .= ob_get_clean();
}
} else {
$html_content = '<div class="propstack-no-results"><p>Keine weiteren Immobilien gefunden.</p></div>';
}


// Determine if there are more pages
$meta = $api_response['meta'] ?? [];
$total_properties = $meta['total_count'] ?? ($meta['total'] ?? 0);
$has_more = ($page * $limit) < $total_properties;

wp_send_json_success(array(
'html' => $html_content, // Send back only the new property cards
'has_more' => $has_more, // Let JS know if there are more pages
// Optionally, send back new pagination HTML if you regenerate it here
));
}

2. API Connection Test (ajax_test_api_connection): This is called by the button on the settings page.

PHP
    public function ajax_test_api_connection() {
// Use a specific nonce for this admin-only action
check_ajax_referer('propstack_test_nonce', 'nonce'); // Ensure this nonce is generated and passed in JS

if (!current_user_can('manage_options')) {
wp_send_json_error('Insufficient permissions.');
}

// Make a minimal API request, e.g., get 1 property
$response = $this->make_api_request('units', array('per_page' => 1, 'with_meta' => 1));

if (is_wp_error($response)) {
wp_send_json_error($response->get_error_message());
}

// Check response structure and content
$count = $response['meta']['total'] ?? ($response['meta']['total_count'] ?? 0);
$first_property_info = 'N/A';
if (!empty($response['data']) && is_array($response['data'])) {
$first_prop = $response['data'][0];
$first_property_info = ($first_prop['title'] ?? $first_prop['name'] ?? 'Unknown') . ' (ID: ' . ($first_prop['id'] ?? 'N/A') . ')';
}

wp_send_json_success(array(
'message' => 'Connection successful!',
'count' => $count,
'first_property' => $first_property_info
// 'raw_response' => $response // For debugging, but be careful with sensitive data
));
}

VII. Styling with CSS (assets/propstack.css)

The CSS file you provided (propstack.css) handles all the visual presentation. Key aspects include:

  • Main Container (.propstack-listings-container): Sets max-width and padding.
  • Grid Layout (.propstack-grid, .propstack-columns-*): Uses CSS Grid for responsive columns.
  • Property Cards (.propstack-card): Defines appearance, hover effects, shadows, borders.
    • Images (.propstack-card-image img): Object-fit for consistent image display, zoom on hover.
    • Status Badge (.propstack-status): Positioned absolutely, with dynamic background colors if provided by API, and fallback styles.
    • Content (.propstack-card-content): Padding, typography for title, address, ID.
    • Details (.propstack-details, .propstack-detail): Flexbox for aligning labels and values.
    • Price (.propstack-price): Highlighted section.
    • Actions (.propstack-btn-primary): Button styling.
  • Pagination (.propstack-pagination, .propstack-page-link): Styling for navigation links.
  • Feedback Messages (.propstack-error, .propstack-no-results): Clear display for users.
  • Loading Spinner (.propstack-spinner): Animation for AJAX loading.
  • Responsive Design: Media queries adjust layout for tablets and mobile devices.
  • Accessibility: Basic focus styles and high-contrast mode considerations.

VIII. Frontend Interactivity with JavaScript (assets/propstack.js)

Your propstack.js file (using jQuery) enhances the user experience:

  • PropstackListings Class: Organizes frontend logic.
  • Event Binding (bindEvents): Attaches click handlers to pagination links.
  • Pagination Handling (handlePagination, loadPage):
    • Prevents multiple rapid requests (this.isLoading).
    • Retrieves data-* attributes from the listing container (set by the PHP shortcode).
    • Makes an AJAX POST request to our ajax_load_more_properties PHP action.
    • Uses propstack_ajax.ajax_url and propstack_ajax.nonce (passed via wp_localize_script).
  • Updating the View (updatePropertyGrid, updatePagination):
    • On AJAX success, replaces the content of .propstack-grid with the new HTML.
    • Updates the 'current' class on pagination links.
    • Includes simple fade-in animation for new cards.
  • Utility Functions: The provided code also mentions initLazyLoading, initImageFallback, handleCardHover, trackPropertyClick, handleResize, showLoading, hideLoading, showError, scrollToTop, and debounce. These would need to be implemented to fully realize all described JS features. For example, initLazyLoadingcould be handled by the browser's native loading="lazy" attribute on <img> tags, which you've already included in the PHP rendering.

IX. Putting It All Together & Next Steps

This walkthrough covers the core architecture of a WordPress plugin that integrates with an external API like Propstack. The PHP handles server-side logic, settings, and initial rendering; CSS provides the visual appeal; and JavaScript adds dynamic client-side interactions like AJAX pagination.

Further Enhancements could include:

  • Single Property Pages: Creating a custom post type or a dynamic template to show detailed information for each property.
  • Advanced Filtering & Sorting: Adding more controls for users to refine listings.
  • Map Integrations: Displaying properties on a map.
  • More Robust Error Handling & Logging.
  • Unit & Integration Testing.

Building custom WordPress plugins like this allows for tailored solutions that perfectly fit specific needs. At Zeenko, we thrive on such projects!

If you're looking to develop a custom WordPress plugin or need expert assistance with API integrations, don't hesitate to contact us at Zeenko.com! We'd love to help bring your project to life.


Important Notes for your zeenko.com version:

  • Replace placeholders: Update URIs, author names, and links to your actual website.
  • Code Snippets: You might want to use a syntax highlighting plugin on your blog for better readability of the code.
  • Nonce for Test Connection: Ensure the inline JavaScript for the "Test API Connection" button correctly generates and uses a nonce specific to that action (e.g., wp_create_nonce('propstack_test_nonce') in PHP, and passing that to the JS or including it in the POST body). The provided plugin code uses wp_create_nonce('propstack_test') in the PHP for the settings page script, so that's good.
  • AJAX load_more_properties HTML: The way HTML is currently generated in ajax_load_more_properties(looping and rendering each card individually) is fine for sending back just the cards. The JavaScript would then append these to the grid or replace grid contents. Ensure the JS correctly handles the received HTML. The current PHP for ajax_load_more_properties sends back an array array('html' => $html, 'has_more' => ...) so the JS in updatePropertyGrid(response.data, $container) should expect response.data.html.
Claudia Uztzu
Post by Claudia Uztzu
May 25, 2025 11:26:17 AM

Comments