Latest articles
No results for this search. Try another term.
Technical guides with real code, not empty promises. Each integration covers two things: showing Heurix results instead of native search, and keeping your catalog in sync.
Honesty first: these are integration guides meant for a developer, not "one-click install" apps from the Shopify/PrestaShop/etc. stores — that may come if demand justifies it, but we didn't want to claim it before it's true.
The relay (Cloudflare Worker):
// worker.js — déployé sur Cloudflare Workers (gratuit pour ce volume)
export default {
async fetch(request, env) {
const url = new URL(request.url);
const q = url.searchParams.get("q") || "";
const res = await fetch("https://api.heurix.fr/v1/index/" + env.HEURIX_CATALOG + "/search", {
method: "POST",
headers: {
"Authorization": "Bearer " + env.HEURIX_API_KEY, // secret Cloudflare, jamais exposé
"Content-Type": "application/json"
},
body: JSON.stringify({ q, limit: 20 })
});
return new Response(await res.text(), {
headers: { "Content-Type": "application/json", "Access-Control-Allow-Origin": "https://votre-boutique.myshopify.com" }
});
}
}
In the theme (Online Store 2.0), replace the rendering of sections/search-results.liquid with a call to this relay:
<div id="heurix-results"></div>
<script>
const params = new URLSearchParams(window.location.search);
fetch("https://votre-worker.workers.dev/search?q=" + encodeURIComponent(params.get("q") || ""))
.then(r => r.json())
.then(data => {
document.getElementById("heurix-results").innerHTML = data.hits.map(h =>
`<a href="/products/${h.product.handle}" class="heurix-hit">
<span>${h.product.name}</span><span>${h.product.price} €</span>
</a>`
).join("");
});
</script>
Register a Shopify webhook (Admin API or shopify.app.toml for a custom app) on the products/create, products/update, products/delete topics, pointed at the same Worker:
// Ajout dans worker.js : gestion du webhook Shopify
async function handleWebhook(request, env) {
const product = await request.json();
await fetch("https://api.heurix.fr/v1/index/" + env.HEURIX_CATALOG + "/items", {
method: "POST",
headers: { "Authorization": "Bearer " + env.HEURIX_API_KEY, "Content-Type": "application/json" },
body: JSON.stringify({
rulepack: "mode",
items: [{
id: String(product.id),
name: product.title,
ref: product.variants?.[0]?.sku || "",
description: product.body_html?.replace(/<[^>]+>/g, "") || "",
stock: product.variants?.reduce((s, v) => s + (v.inventory_quantity || 0), 0) || 0,
handle: product.handle,
price: product.variants?.[0]?.price
}]
})
});
}
Remember to verify the Shopify webhook's HMAC signature (X-Shopify-Hmac-Sha256) before processing the payload, to avoid accepting forged requests.
In a custom module, hook displaySearchResults or override the search controller:
<?php
// modules/heurixsearch/heurixsearch.php
class HeurixSearch extends Module
{
public function search($query)
{
$ch = curl_init('https://api.heurix.fr/v1/index/' . Configuration::get('HEURIX_CATALOG') . '/search');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . Configuration::get('HEURIX_API_KEY'),
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => json_encode(['q' => $query, 'limit' => 24]),
]);
$response = json_decode(curl_exec($ch), true);
curl_close($ch);
return $response['hits'] ?? [];
}
}
In the results template (search.tpl), loop over these results instead of PrestaShop's native search.
Three hooks are enough to stay in sync in real time:
<?php
public function install()
{
return parent::install()
&& $this->registerHook('actionProductAdd')
&& $this->registerHook('actionProductUpdate')
&& $this->registerHook('actionProductDelete');
}
public function hookActionProductUpdate($params)
{
$product = $params['product'];
$this->pushToHeurix([
'id' => (string) $product->id,
'name' => $product->name,
'ref' => $product->reference,
'description' => strip_tags($product->description_short),
'stock' => (int) StockAvailable::getQuantityAvailableByProduct($product->id),
]);
}
private function pushToHeurix($item)
{
$ch = curl_init('https://api.heurix.fr/v1/index/' . Configuration::get('HEURIX_CATALOG') . '/items');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . Configuration::get('HEURIX_API_KEY'),
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => json_encode(['rulepack' => 'mode', 'items' => [$item]]),
]);
curl_exec($ch);
curl_close($ch);
}
wp_remote_post(), WordPress's native HTTP function, rather than raw cURL.
<?php
// Dans functions.php ou un plugin dédié
add_filter('woocommerce_product_query_meta_query', function ($meta_query, $query) {
if (!is_search()) return $meta_query;
$response = wp_remote_post('https://api.heurix.fr/v1/index/' . HEURIX_CATALOG . '/search', [
'headers' => [
'Authorization' => 'Bearer ' . HEURIX_API_KEY,
'Content-Type' => 'application/json',
],
'body' => json_encode(['q' => get_search_query(), 'limit' => 24]),
]);
$hits = json_decode(wp_remote_retrieve_body($response), true)['hits'] ?? [];
$ids = array_map(fn($h) => (int) $h['product']['id'], $hits);
// Réordonne les résultats WooCommerce natifs selon le classement Heurix
$query->set('post__in', $ids ?: [0]);
$query->set('orderby', 'post__in');
return $meta_query;
}, 10, 2);
This approach has an advantage: it reuses WooCommerce's native display (product cards, existing filters), Heurix only reranks results — less template to rewrite.
<?php
add_action('woocommerce_update_product', 'heurix_sync_product');
add_action('woocommerce_new_product', 'heurix_sync_product');
function heurix_sync_product($product_id) {
$product = wc_get_product($product_id);
wp_remote_post('https://api.heurix.fr/v1/index/' . HEURIX_CATALOG . '/items', [
'headers' => [
'Authorization' => 'Bearer ' . HEURIX_API_KEY,
'Content-Type' => 'application/json',
],
'body' => json_encode([
'rulepack' => 'mode',
'items' => [[
'id' => (string) $product_id,
'name' => $product->get_name(),
'ref' => $product->get_sku(),
'description' => wp_strip_all_tags($product->get_short_description()),
'stock' => $product->get_stock_quantity() ?? 0,
]],
]),
]);
}
add_action('woocommerce_delete_product', function ($product_id) {
wp_remote_request('https://api.heurix.fr/v1/index/' . HEURIX_CATALOG . '/items/' . $product_id, [
'method' => 'DELETE',
'headers' => ['Authorization' => 'Bearer ' . HEURIX_API_KEY],
]);
});
A custom module with a plugin on the search results block:
<?php
// app/code/Heurix/Search/Model/SearchClient.php
namespace Heurix\Search\Model;
use Magento\Framework\HTTP\Client\Curl;
class SearchClient
{
public function __construct(private Curl $curl) {}
public function search(string $query): array
{
$this->curl->addHeader('Authorization', 'Bearer ' . HEURIX_API_KEY);
$this->curl->addHeader('Content-Type', 'application/json');
$this->curl->post(
'https://api.heurix.fr/v1/index/' . HEURIX_CATALOG . '/search',
json_encode(['q' => $query, 'limit' => 24])
);
$data = json_decode($this->curl->getBody(), true);
return $data['hits'] ?? [];
}
}
Inject this service into a plugin on Magento\CatalogSearch\Model\Layer\Filter, or directly replace the search results template with rendering based on this data.
An observer on the catalog's standard events:
<?php
// app/code/Heurix/Search/Observer/SyncProduct.php
namespace Heurix\Search\Observer;
use Magento\Framework\Event\ObserverInterface;
use Magento\Framework\Event\Observer;
class SyncProduct implements ObserverInterface
{
public function __construct(private \Magento\Framework\HTTP\Client\Curl $curl) {}
public function execute(Observer $observer)
{
$product = $observer->getEvent()->getProduct();
$this->curl->addHeader('Authorization', 'Bearer ' . HEURIX_API_KEY);
$this->curl->addHeader('Content-Type', 'application/json');
$this->curl->post('https://api.heurix.fr/v1/index/' . HEURIX_CATALOG . '/items', json_encode([
'rulepack' => 'mode',
'items' => [[
'id' => (string) $product->getId(),
'name' => $product->getName(),
'ref' => $product->getSku(),
'stock' => (int) $product->getExtensionAttributes()?->getStockItem()?->getQty(),
]],
]));
}
}
Declared in events.xml on catalog_product_save_after and catalog_product_delete_after.
These guides are a technical starting point, not a turnkey module. If your setup is out of the ordinary, get in touch.