Complete architecture, class reference, hook system, database schema, and extension guide for FlowLinks 2.6.10.
FlowLinks follows a clean singleton-based architecture with a clear separation between admin and public concerns. All persistence goes through a single settings manager that caches results per-request for performance.
Every core class exposes get_instance(). No global state leaks.
Settings are fetched once per request and cached in a static array.
Every echo uses esc_html, esc_attr, or esc_url.
Posts/Pages and Products use independent rule sets merged through interleaving.
The bootstrap file loads classes in dependency order: settings โ renderer โ admin โ public. Admin and Public are instantiated on the plugins_loaded action.
Central configuration manager. Handles defaults, fetching, sanitizing, and saving settings.
Returns the singleton instance.
Returns fully merged settings for a group (defaults + saved options).
Returns the raw defaults array for the requested group.
Returns the sanitized current admin tab (internal or about).
Sanitizes and persists POST data. Called from the Admin layer after nonce verification.
Flushes the static settings cache.
Fetches related posts and produces escaped HTML for both frontend and preview contexts.
Renders a block for the given source post on the frontend.
Renders a preview block using temporary settings (no DB write).
Registers the admin menu, enqueues assets, handles settings saving, and serves AJAX tab content.
Registers the top-level FlowLinks menu page.
Loads CSS/JS only on the plugin's admin page.
Verifies nonce + capability, then calls Settings::save().
AJAX handler for live preview rendering.
AJAX handler for lazy loading admin tab content.
Hooks into the_content, injects rendered blocks, and enqueues frontend assets. Dynamic CSS is attached to the enqueued stylesheet via the WordPress style API.
Filter callback for the_content.
Reusable rendering logic for both singular views and custom contexts.
Enqueues the frontend stylesheet and attaches validated dynamic CSS custom properties with wp_add_inline_style(). Font-face rules are defined once in assets/css/frontend.css.
Settings are stored under the option key flowlinks_settings_internal. The public external-credit setting show_powered_by is controlled by an explicit, clearly labeled administrator switch and defaults to false. WordPress.org supplies locale translation files; the plugin does not bundle English self-translation files or manually load its text domain.
| Key | Type | Default | Validation |
|---|---|---|---|
enabled | bool | true | isset check |
show_powered_by | bool | false | isset check; explicit frontend opt-in |
display_matrix | array | ['post'=>['post'],'product'=>['product']] | sanitize_key + post_type_exists |
min_words | int | 0 | absint, 0โ50000 |
excerpt_length | int | 15 | absint, 1โ100 |
blocks_count | int | 1 | derived from positions |
paragraph_positions | string | '1' | comma-separated integers, max 10 |
position_1 โฆ position_10 | int | 0 | 0 or 1 |
section_title | string | 'Recommended Content' | sanitize_text_field |
items_per_block | int | 3 | absint, 1โ20 |
exclude_ids | array | [] | absint IDs, max 500 |
manual_ids | array | [] | absint IDs, max 500 |
order_by | string | 'date' | whitelisted set |
order | string | 'DESC' | ASC or DESC |
post_order_by | string | 'date' | whitelisted set |
post_order | string | 'DESC' | ASC or DESC |
post_manual_ids | array | [] | absint IDs, max 500 |
product_order_by | string | 'date' | whitelisted set |
product_order | string | 'DESC' | ASC or DESC |
product_manual_ids | array | [] | absint IDs, max 500 |
display_style | string | 'card' | card | list |
columns | int | 3 | absint, 1โ6 |
show_thumbnail | bool | true | isset check |
show_excerpt | bool | false | isset check |
background_color | string | '#f9f9f9' | sanitize_hex_color |
border_color | string | '#dddddd' | sanitize_hex_color |
title_color | string | '#111111' | sanitize_hex_color |
text_color | string | '#555555' | sanitize_hex_color |
item_border_color | string | '#005a87' | sanitize_hex_color |
content_analysis_mode | string | 'p' | p | elementor |
Legacy keys order_by, order, and manual_ids are still accepted and used as fallbacks for post_order_by, post_order, and post_manual_ids.
FlowLinks uses WP_Query for all standard sorts and a raw $wpdb query for WooCommerce best-sellers (joining on _total_sales). Each target content type is queried separately, then results are interleaved to produce a balanced mix.
// Simplified pseudo-code of get_related_posts_by_types() $by_type = []; foreach ( $post_types as $type ) { $rule = get_order_settings_for_type( $settings, $type ); $by_type[$type] = get_related_posts( $source_id, $rule, [$type] ); } // Interleave: take one from each type per round $merged = []; $offset = 0; while ( count( $merged ) < $limit ) { foreach ( $post_types as $type ) { $post = $by_type[$type][$offset] ?? null; if ( $post && ! isset( $seen[$post->ID] ) ) { $merged[] = $post; $seen[$post->ID] = true; } } $offset++; }
When display_style = 'card' and the number of rendered items is โค 6, the effective column count matches the item count. This guarantees all cards fit into a single row regardless of the configured columns setting.
FlowLinks registers a single top-level menu with slug pixel-flowlinks and the dashicons-admin-links icon at position 60.
Tab content is rendered via AJAX (flowlinks_load_tab) to keep initial page weight minimal. The active tab is determined server-side by FlowLinks_Settings::get_current_tab() and reflected in the URL ?tab= parameter.
| Asset | Loaded When |
|---|---|
wp-color-picker | Always on FlowLinks admin page |
flowlinks-admin (CSS) | Always on FlowLinks admin page |
flowlinks-frontend (CSS) | Only when Internal tab is active |
flowlinks-admin (JS) | Always, with jQuery + wp-color-picker + wp-util deps |
flowlinks_admin_obj = { ajax_url: '/wp-admin/admin-ajax.php', preview_nonce: '<nonce>', tabs_nonce: '<nonce>', current_tab: 'internal' };
| Action | wp_ajax_flowlinks_load_tab |
| Nonce | flowlinks_tabs_nonce |
| Capability | manage_options |
| Input | tab (internal | about) |
| Output | { success: true, data: { content: '<html>' } } |
| Action | wp_ajax_flowlinks_get_preview |
| Nonce | flowlinks_preview_nonce |
| Capability | manage_options |
| Input | settings (JSON string of FlowLinks[internal]) |
| Output | { success: true, data: { html: '<html>' } } |
The JS collects all form fields, serializes them into a nested object, then sends them as a JSON string. The PHP handler parses it with json_decode, normalizes booleans, and passes the array to FlowLinks_Renderer::render_preview_block().
FlowLinks hooks the_content at priority 20. In singular views, FlowLinks_Public::inject_links_into_content() builds the effective content.
// The frontend scans the original HTML structure. $paragraph_ends = $this->find_paragraph_end_offsets( $content, $analysis_mode ); // Only real <p>...</p> elements are counted. // Comments and raw-text/code areas are ignored. // Elementor mode recognizes text-editor/widget wrappers; without one, it falls back to all real <p> elements.
The paragraph positions string (e.g. "2, 5, 9") is parsed into an integer array, deduplicated, sorted, and capped at 10 values. Each matching real HTML paragraph is located by its byte offset, then a block is rendered immediately after that paragraph without reserializing the original content.
Every text-bearing element is emitted with inline direction:ltr !important;text-align:left !important. This guarantees correct alignment even on RTL-themed sites that aggressively override styles.
The current release does not expose public filters. The internal design follows WordPress coding standards, so filters can be added in a future version with deliberate compatibility review.
| Hook | Callback | Purpose |
|---|---|---|
admin_menu | Admin::add_admin_menu | Register top-level menu |
admin_enqueue_scripts | Admin::enqueue_admin_assets | Load admin CSS/JS |
admin_init | Admin::maybe_save_settings | Handle form submit |
admin_notices | Admin::show_admin_notices | Success notice |
wp_ajax_flowlinks_get_preview | Admin::ajax_get_preview | Preview render |
wp_ajax_flowlinks_load_tab | Admin::ajax_load_tab | Tab lazy-load |
wp_enqueue_scripts | Public::enqueue_frontend_assets | Enqueue frontend CSS and attach validated CSS variables with wp_add_inline_style() |
the_content | Public::inject_links_into_content | Frontend injection |
// Filter the final rendered HTML block $html = apply_filters( 'flowlinks/rendered_block', $html, $posts, $settings ); // Filter the related posts before rendering $posts = apply_filters( 'flowlinks/related_posts', $posts, $source_id, $settings ); // Filter paragraph positions $positions = apply_filters( 'flowlinks/paragraph_positions', $positions, $post_id );
All form submissions are protected by wp_nonce_field( 'flowlinks_save_settings', 'flowlinks_save_settings_nonce' ). AJAX handlers call check_ajax_referer() with the relevant nonce action.
Every mutating action requires current_user_can( 'manage_options' ). The menu itself is registered with the same capability.
| Field Type | Sanitizer |
|---|---|
| Integers | absint() with range clamps |
| Booleans | isset() check |
| Keys / enums | sanitize_key() + whitelist |
| Text | sanitize_text_field() |
| URLs | esc_url_raw() + url_to_postid() |
| Colors | sanitize_hex_color() |
| Post types | post_type_exists() |
All HTML output goes through esc_html(), esc_attr(), or esc_url(). No raw user input is ever echoed.
The single raw SQL query (WooCommerce best-sellers) uses $wpdb->prepare() with placeholder arrays. All other queries use WP_Query.
Static array caches settings per request โ DB hit happens only once.
All WP_Query calls set no_found_rows = true to skip COUNT queries.
Sticky post merging is bypassed for recommendation queries.
Hard limit of 20 items per query prevents runaway loads.
The best-sellers query is capped at 500 rows and uses a single JOIN.
Frontend CSS loads only when is_singular() and plugin is enabled.
// Register a CPT, then extend the matrix on save add_filter( 'flowlinks/supported_post_types', function( $types ) { $types[] = 'recipe'; return $types; } );
add_filter( 'flowlinks/allowed_order_by', function( $allowed ) { $allowed[] = 'menu_order'; return $allowed; } ); add_filter( 'flowlinks/related_posts', function( $posts, $source_id, $settings ) { if ( $settings['order_by'] === 'menu_order' ) { usort( $posts, fn($a,$b) => $a->menu_order <=> $b->menu_order ); } return $posts; }, 10, 3 );
add_filter( 'flowlinks/style_vars', function( $vars, $settings ) { $vars[] = '--lf-custom-shadow: 0 4px 12px rgba(0,0,0,.08)'; return $vars; }, 10, 2 );
add_filter( 'flowlinks/item_html', function( $html, $post, $settings ) { return '<article class="custom-item">' . esc_html( $post->post_title ) . '</article>'; }, 10, 3 );
| Version | Highlights |
|---|---|
| 2.6.10 | Enqueued dynamic CSS and admin tab JavaScript; removed raw style/script output, manual text-domain loading, and bundled en_US translation files; tightened CSS-value validation; clarified explicit opt-in for the optional public credit link; hardened structural paragraph-position detection; reduced repeated related-content queries. |
| 2.6.9 | Equal width Style & Colors cards; About page refinement. |
| 2.6.6 | Paragraph positions field consolidates position_1 โฆ position_10. |
| 2.6.3 | Authoritative UI system; Vazirmatn + Shabnam typography. |
| 2.6.0 | Independent post/product ordering profiles. |
| 2.5.0 | Card / List layout switch; color pickers. |
| 2.0.0 | Content Type Matrix; AJAX tabs. |
| 1.0.0 | Initial public release. |
Follow releases and documentation at pixelplugins.top.