Comment créer une fonctionnalité « Poser une question » personnalisée pour les produits dans Magento 2
Adding an "Ask a Question" fonctionnalité directly to page produits is a small change that can boost conversion, reduce returns, and increase engagement. In this post I’ll walk you through building a lightweight, maintainable custom module for Magento 2 that: stores product questions in the database, exposes an admin interface to manage them, renders a performant frontend form on page produits, sends notifications to admins and automated replies to clients, and adds FAQ structured data for SEO.
What we’ll build — quick aperçu
- A simple schéma de base de données for questions and answers (db_schema.xml).
- Models, resource models and collections to persist questions.
- An grille d'administration + edit form so store staff can answer questions.
- A frontend block and template to add an AJAX "Ask a Question" form on the page produit without hurting performance.
- Controllers and an e-mail/notification flux de travail (admin alerts + templated auto-reply to clients when an answer is published).
- JSON-LD FAQ markup generation for answered Q&As so Google can show rich snippets.
- Notes on extension vs custom development and the potential commercial angle for Magefine.
C'est a practical, copy-paste-friendly guide. I’ll use concise code exemples and explain où put things. I’ll follow Magento 2 bonnes pratiques (db_schema.xml, injection de dépendances, UI composants for grille d'administrations, AJAX endpoints, and e-mail templates).
Module skeleton
Create a module called Magefine_ProductQuestions (namespace Magefine). Folder structure (simplified):
app/code/Magefine/ProductQuestions/
├── registration.php
├── etc
│ ├── module.xml
│ ├── frontend/routes.xml
│ └── adminhtml
│ └── routes.xml
├── Controller
├── Model
├── Setup
├── view
│ ├── frontend
│ │ ├── layout
│ │ └── templates
│ └── adminhtml
│ └── ui_composant
└── etc
└── db_schema.xml
registration.php
<?php
use Magento\Framework\Component\ComponentRegistrar;
ComponentRegistrar::register(
ComponentRegistrar::MODULE,
'Magefine_ProductQuestions',
__DIR__
);
etc/module.xml
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd">
<module name="Magefine_ProductQuestions" setup_version="1.0.0"/>
</config>
Database architecture
We want a simple structure: product_question table with champs to relate to product, client (optional), question text, answer text, status, timestamps, visibility. Store answers in same table to keep it simple; you can split into question and answer tables for versioning later.
etc/db_schema.xml (important: use correct types and indexes)
<?xml version="1.0"?>
<schema xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Setup/Declaration/Schema/etc/schema.xsd">
<table name="magefine_product_question" resource="default" engine="innodb" comment="Product questions">
<colonne xsi:type="int" name="question_id" nullable="false" unsigned="true" identity="true" comment="Question ID" />
<colonne xsi:type="int" name="product_id" nullable="false" unsigned="true" comment="Product ID" />
<colonne xsi:type="int" name="client_id" nullable="true" unsigned="true" comment="Customer ID" />
<colonne xsi:type="text" name="question_text" nullable="false" comment="Question" />
<colonne xsi:type="text" name="answer_text" nullable="true" comment="Answer" />
<colonne xsi:type="smallint" name="status" nullable="false" default="0" comment="0=Pending,1=Answered,2=Hidden" />
<colonne xsi:type="timestamp" name="created_at" nullable="false" default="CURRENT_TIMESTAMP" />
<colonne xsi:type="timestamp" name="updated_at" nullable="true" on_update="CURRENT_TIMESTAMP" />
<constraint xsi:type="primary" referenceId="PRIMARY">
<colonne name="question_id"/>
</constraint>
<index referenceId="MAGEFINE_PRODUCT_ID" indexType="btree">
<colonne name="product_id"/>
</index>
</table>
</schema>
Why keep it this simple? It’s easier to manage in admin and to render JSON-LD. Si vous need complex moderation history, move to separate tables.
Models and resource models
Create Model/Question.php, Model/ResourceModel/Question.php and Model/ResourceModel/Question/Collection.php. Key méthodes are simple getters/setters; Magento will handle the rest.
// Model/Question.php
namespace Magefine\ProductQuestions\Model;
use Magento\Framework\Model\AbstractModel;
class Question extends AbstractModel
{
protected fonction _construct()
{
$this->_init('Magefine\ProductQuestions\Model\ResourceModel\Question');
}
}
// Model/ResourceModel/Question.php
namespace Magefine\ProductQuestions\Model\ResourceModel;
use Magento\Framework\Model\ResourceModel\Db\AbstractDb;
class Question extends AbstractDb
{
protected fonction _construct()
{
$this->_init('magefine_product_question', 'question_id');
}
}
// Model/ResourceModel/Question/Collection.php
namespace Magefine\ProductQuestions\Model\ResourceModel\Question;
use Magento\Framework\Model\ResourceModel\Db\Collection\AbstractCollection;
class Collection extends AbstractCollection
{
protected fonction _construct()
{
$this->_init('Magefine\ProductQuestions\Model\Question', 'Magefine\ProductQuestions\Model\ResourceModel\Question');
}
}
Admin UI: grid and form
Use UI Components for the grille d'administration. Vous allez need an admin route, ACL entries and UI composant XML for the listing and form. I’ll show the listing composant fichier and a simplified form composant. Place listing at view/adminhtml/ui_composant/magefine_product_question_listing.xml and a form at magefine_product_question_form.xml. Also add an admin contrôleur for edit/save.
ui_composant listing (simplified)
<?xml version="1.0" encoding="UTF-8"?>
<listing xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Ui:etc/ui_configuration.xsd">
<dataSource name="product_questions_data_source">
<argument name="dataProvider" xsi:type="configurableObject">
<argument name="class" xsi:type="chaîne">Magefine\ProductQuestions\Model\QuestionDataProvider</argument>
<argument name="name" xsi:type="chaîne">product_questions_data_source</argument>
<argument name="primaryFieldName" xsi:type="chaîne">question_id</argument>
<argument name="requestFieldName" xsi:type="chaîne">question_id</argument>
</argument>
</dataSource>
<colonnes>
<colonne name="question_id"/>
<colonne name="product_id"/>
<colonne name="question_text"/>
<colonne name="status"/>
<actionsColumn name="actions"/>
</colonnes>
</listing>
The DataProvider is a lightweight class extending Magento\Ui\DataProvider\AbstractDataProvider and wires the collection.
For the admin form you’ll provide an editor champ for answer_text and a status selector. When admin saves with answer_text and sets status=1 (Answered), we’ll trigger an e-mail flux de travail (see later).
Frontend integration — display the form without slowing pages
Key goals:
- Render minimal HTML server-side so page produits cache nicely (Full Page Cache must not be violated).
- Use an asynchronous endpoint to submit questions.
- Only load JS when needed and keep payload small.
Approach: add a layout update catalog_product_view.xml that inserts a small container block. The block HTML is static and cached; the form itself is injected client-side using a tiny RequireJS module that renders the form and handles AJAX submissions. That avoids FPC busting and removes heavy server work from product rendering.
view/frontend/layout/catalog_product_view.xml
<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd">
<body>
<referenceContainer name="product.info.additional">
<classe de bloc="Magento\Framework\View\Element\Template" name="magefine.product.question.container" template="Magefine_ProductQuestions::product/question-container.phtml"/>
</referenceContainer>
</body>
</page>
product/question-container.phtml (very light)
<?php /** minimal markup so FPC can cache it */ ?>
<div id="magefine-ask-question" data-product-id="<?= $block->getProduct()->getId(); ?>">
<button id="magefine-ask-toggle" class="action secondary">Ask a question</button>
<!-- The real form is injected by JS to avoid FPC problèmes -->
</div>
Now a small JS module view/frontend/web/js/ask-question.js loaded with RequireJS when the container exists. It will render the form HTML in the client, handle form_clé, send AJAX to productquestions/ajax/save, and show success states. Parce que it's client-side, the form doesn’t affect page cache and only executes for visiteurs who interact with the widget.
define(['jquery', 'mage/url', 'mage/storage'], fonction($, urlBuilder, storage){
'use strict';
return fonction(config){
var container = $('#magefine-ask-question');
if (!container.length) return;
var productId = container.data('product-id');
var formHtml = '\n <form id="magefine-ask-form" class="magefine-ask-form">' +
'' +
'' +
'' +
'';
container.append(formHtml);
container.on('submit', '#magefine-ask-form', fonction(e){
e.preventDefault();
var data = $(this).serialize();
var url = urlBuilder.build('productquestions/ajax/save');
$.ajax({
url: url,
data: data,
type: 'POST',
dataType: 'json'
}).done(fonction(res){
if (res.success) {
container.html('Thanks! Your question is submitted and sera avised.
');
} else {
container.find('.magefine-erreur').remove();
container.prepend('' + (res.message || 'Error') + '
');
}
}).fail(fonction(){
container.prepend('Network erreur
');
});
});
}
});
Load this module conditionally in the container template via x-magento-init to keep static assets minimal. Example snippet to add after the container div in question-container.phtml:
<script type="text/x-magento-init">
{
"#magefine-ask-question": {"Magefine_ProductQuestions/js/ask-question":{}}
}
</script>
That way, JS only initializes when DOM contains the container, and page HTML remains cacheable.
AJAX contrôleur and validation
Create Controller/Ajax/Save.php in frontend. Use POST and form_clé validation. On save you create a new Question model, set status=0 (pending), created_at etc., and return JSON. Also sanitize input and throttle by IP or client to avoid spam.
// Controller/Ajax/Save.php (simplified)
namespace Magefine\ProductQuestions\Controller\Ajax;
use Magento\Framework\App\Action\Action;
use Magento\Framework\App\Action\Context;
use Magento\Framework\Controller\Result\JsonFactory;
use Magefine\ProductQuestions\Model\QuestionFactory;
class Save extends Action
{
protected $resultJsonFactory;
protected $questionFactory;
public fonction __construct(Context $context, JsonFactory $resultJsonFactory, QuestionFactory $questionFactory)
{
parent::__construct($context);
$this->resultJsonFactory = $resultJsonFactory;
$this->questionFactory = $questionFactory;
}
public fonction execute()
{
$result = $this->resultJsonFactory->create();
$post = $this->getRequest()->getPostValue();
if (!$this->getRequest()->isPost()) {
return $result->setData(['success'=>false,'message'=>'Invalid request']);
}
$questionText = trim($post['question_text'] ?? '');
$productId = (int)($post['product_id'] ?? 0);
if (!$questionText || !$productId) {
return $result->setData(['success'=>false,'message'=>'Please provide a question.']);
}
// simple anti-spam: limit length
if (strlen($questionText) > 2000) {
return $result->setData(['success'=>false,'message'=>'Question too long.']);
}
try {
$question = $this->questionFactory->create();
$question->setData([
'product_id' => $productId,
'question_text' => $questionText,
'status' => 0
]);
$question->save();
return $result->setData(['success'=>true]);
} catch (\Exception $e) {
return $result->setData(['success'=>false,'message'=>'Server erreur']);
}
}
}
Note: Using $model->save() is fine for small fonctionnalités; for higher scale, use repositories or resource model save to avoid legacy behavior. Also consider reCAPTCHA and rate limiting for production.
Notification flux de travail — admin alerts and auto-replies
When an admin answers a question in the admin form (sets answer_text and status=1), we want to:
- Send an e-mail to a configured admin/support e-mail (or mulconseille e-mails) to alert them so they can moderate or respond if they didn't answer inline.
- If a question came from a logged-in client or includes an e-mail input, send an automated templated e-mail when the answer is published.
Implementation notes:
- Use events/observateurs: Observe model save (catalog_product_save_after is not relevant) — we can discorrectif a custom event in the model resource or use a plugin on ResourceModel::save to detect status change from pending to answered. Another simpler approche is to send notification in the admin contrôleur after saving the form.
- Create e-mail templates in etc/e-mail and load them by identifier in the code.
- Use Magento\Framework\Mail\Template\TransportBuilder to build and send e-mails.
Example: after admin saves, if ($oldStatus == 0 && $newStatus == 1) send notifications.
// inside admin contrôleur save after successfully saving the model
if ($question->getStatus() == 1 && $oldStatus == 0) {
// notify admins
$this->notifyAdmin($question);
// notify client if e-mail provided
if ($question->getCustomerEmail()) {
$this->sendAnswerToCustomer($question);
}
}
// sendAnswerToCustomer pseudo-code
$transport = $this->transportBuilder
->setTemplateIdentifier('magefine_question_answered_template')
->setTemplateOptions(['area' => 'frontend','store' => $storeId])
->setTemplateVars(['question' => $question, 'product' => $product])
->setFromByScope('general')
->addTo($question->getCustomerEmail())
->getTransport();
$transport->sendMessage();
Si vous want asynchronous sending (recommended), push e-mail sending to a queue or a tâche cron to avoid blocking the admin save operation.
Spam, moderation, and data privacy
Considerations for real sites:
- Anti-spam: Google reCAPTCHA v2/v3, honeypot champs, rate limiting per IP or per product/cookie, and contenu filtreing.
- GDPR: Si vous store personal data (e-mails, names), make sure to allow deletion and to document retention policy — add admin tools to export/delete a utilisateur's questions.
- Moderation: Add statuses (pending, approved, hidden) and optionally a moderation queue for high-traffic stores.
Genenote SEO-friendly FAQ rich snippets
Une fois a question is answered and made public (status = 1 and visibility flag), you can output a JSON-LD block on the page produit that follows Google’s FAQ schema. Keep generation server-side but only for cached public contenu (answered Q&As). Parce que contenu is fairly static once answered, adding this to the page produit is safe for caching and beneficial for SEO.
Example JSON-LD generator in the product block template (render only if there are answered Q&As):
<?php
$answered = $questionCollection->addFieldToFilter('product_id', $product->getId())->addFieldToFilter('status', 1);
if ($answered->getSize()) {
$faq = [
'@context' => 'https://schema.org',
'@type' => 'FAQPage',
'mainEntity' => []
];
foreach ($answered as $q) {
$faq['mainEntity'][] = [
'@type' => 'Question',
'name' => strip_tags($q->getQuestionText()),
'acceptedAnswer' => [
'@type' => 'Answer',
'text' => strip_tags($q->getAnswerText())
]
];
}
echo '<script type="application/ld+json">' . json_encode($faq) . '</script>';
}
?>
Notes:
- Only include answered and public Q&As. Si vous add private or utilisateur-only answers, exclude them from JSON-LD.
- Keep JSON-LD size reasonable (Google has limits). If a product has dozens of Q&As, limit to the top N (e.g., 10) most useful answers.
- Assurez-vous text is cleaned (no script or markup) before output.
Performance considerations
Key points to keep your page produits fast and cache-friendly:
- Do not include dynamic per-visiteur contenu inside the FPC. Inject interactive parts with client-side code.
- Limit server-side work: only load answered Q&As for JSON-LD and for public lists. Avoid heavy joins and collection processing in the layout render phase.
- Cache admin-loaded data where appropriate, but admin pages don’t need FPC; still keep DB queries efficient with proper indexation on product_id and status.
- Use asynchronous e-mail sending (queue or cron) so admins don't wait for e-mails to be sent.
- Compress and minify the small JS module and keep its payload tiny; the code above is deliberately small.
Example admin-to-client auto-reply template
<!-- etc/e-mail/magefine_question_answered_template.html -->
<h3>Your question has been answered</h3>
<p>Hello {{var client_name}},</p>
<p>Thanks for your question about <strong>{{var product.name}}</strong>. Voici the answer:</p>
<p><em>{{var question.question_text}}</em></p>
<p>{{var question.answer_text}}</p>
<p>Si vous need more help, reply to this e-mail or visit the page produit: <a href="{{var product.url}}">View product</a>.</p>
<p>Best, <br/>The {{var store.getFrontendName()}} team</p>
Extension vs custom development — pros and cons
Let’s be practical. Vous pouvez either install a tiers extension (if reputable) or build this custom fonctionnalité in-house. Voici the trade-offs I’d discuss with product and ops teams.
Extensions (Pros):
- Fast to déployer — often configurable and ready to go.
- May include advanced fonctionnalités tel que moderation flux de travails, analytics, and spam protection out-of-the-box.
- Updates supported by vendor (but check compatibility with your Magento version).
Extensions (Cons):
- May include fonctionnalités you don’t need — increasing complexity and possible performance overhead.
- Sometimes poorly maintained; sécurité and compatibility problèmes are risks.
- Less flexible for custom integrations with internal systems (CRM, support tools, custom e-mail flows) unless they provide APIs.
Custom development (Pros):
- Fully tailored to your flux de travails, modèle de données, and performance constraints.
- Easier to integrate with internal tooling (CRM, support desks, analytics).
- Smaller surface area — you only build what you need, keeping it lightweight.
Custom development (Cons):
- Requires développeur time up front and for maintenance.
- If poorly implemented, it can cause performance problèmes or sécurité holes.
Business case for Magefine to offer this as an extension or service:
- Small, well-built "Ask a Question" module peut être monetized with tiers: basic free version (store Q&A storage, admin tools) and premium fonctionnalités (reCAPTCHA integration, multi-language templates, automatic FAQ publishing, CRM integration, analytics, moderation tools).
- Offer installation and hosting as a service bundle — Magefine already sells extensions and hosting, so providing a curated, performance-optimized Q&A module plus managed hosting serait attractive to commerçants who want to offload complexity.
Commercial fonctionnalités to consider for a premium offering
- Auto-publish rules: publish answers automatically when pattern-matching approved templates.
- Integration with Zendesk / Freshdesk / Intercom to create tickets from questions.
- Analytics tableau de bord: most asked questions, unanswered questions per product, conversion impact.
- Bulk moderation tools and CSV export/import.
- Multi-store and multi-language support including translation flux de travails for answers.
- Rich snippet verification tool to ensure FAQ JSON-LD is valid and limited to allowed number of entries.
Testing and déploiement checklist
- Unit test models and resource models for CRUD operations.
- Manual QA: submit questions as guest and logged-in utilisateurs; verify grille d'administration and answer flux de travail.
- Security test: XSS in question/answer champs, validate outputs and sanitize when genenote JSON-LD.
- Performance test: ensure page load time not impacted; check FPC hits/misses before and after déployer.
- Functional tests on e-mail sending (dev-mode logs or staging SMTP) and rate limiting.
Extra conseils from experience
- Keep the question textarea simple — avoid heavy WYSIWYG editors in public forms; admins can format answers in admin if needed.
- Keep analytics events for when utilisateurs click "Ask a question" or submit — useful metric to prioritize product contenu improvements.
- Make answers public automatically only after avis unless you trust staff flux de travail.
- Expose a small badge on page produits like "Questions answered: 3" to improve credibility and encourage clients to look for answers.
- Consider caching the JSON-LD separately in a small block cache so it doesn’t run DB queries on every product view when many questions exist.
Wrap-up
That’s the full picture. The implémentation is intentionally lightweight but production-ready in structure: db_schema.xml for a stable database, models and resource models to persist data, admin UI via UI Components, client-side injected form to preserve FPC and performance, asynchronous contrôleurs and e-mail flux de travails, and JSON-LD for SEO.
Si vous want, I can:
- Generate a downloadable sample module repository with all fichiers and minimal dependencies for test on a dev environment.
- Create a premium fonctionnalité list and tarification suggestions for Magefine to package this as an extension + hosting offering.
Tell me which one you prefer and I’ll produce the repo or the go-to-market plan next.