Cómo crear una función «Hacer una pregunta» personalizada para productos en Magento 2
Adding an "Ask a Question" feature directly to página de productos 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 página de productos, sends notifications to admins and automated replies to customers, and adds Preguntas frecuentes structured data for SEO.
What we’ll build — quick overview
- A simple database schema for questions and answers (db_schema.xml).
- Models, resource models and collections to persist questions.
- An admin grid + edit form so store staff can answer questions.
- A frontend block and template to add an AJAX "Ask a Question" form on the página de producto without hurting performance.
- Controllers and an email/notification workflow (admin alerts + templated auto-reply to customers when an answer is published).
- JSON-LD Preguntas frecuentes 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.
Esto es a practical, copy-paste-friendly guide. I’ll use concise code examples and explain dónde put things. I’ll follow Magento 2 mejores prácticas (db_schema.xml, inyección de dependencias, UI components for admin grids, AJAX endpoints, and email 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_component
└── 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 fields to relate to product, customer (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">
<column xsi:type="int" name="question_id" nullable="false" unsigned="true" identity="true" comment="Question ID" />
<column xsi:type="int" name="product_id" nullable="false" unsigned="true" comment="Product ID" />
<column xsi:type="int" name="customer_id" nullable="true" unsigned="true" comment="Customer ID" />
<column xsi:type="text" name="question_text" nullable="false" comment="Question" />
<column xsi:type="text" name="answer_text" nullable="true" comment="Answer" />
<column xsi:type="smallint" name="status" nullable="false" default="0" comment="0=Pending,1=Answered,2=Hidden" />
<column xsi:type="timestamp" name="created_at" nullable="false" default="CURRENT_TIMESTAMP" />
<column xsi:type="timestamp" name="updated_at" nullable="true" on_update="CURRENT_TIMESTAMP" />
<constraint xsi:type="primary" referenceId="PRIMARY">
<column name="question_id"/>
</constraint>
<index referenceId="MAGEFINE_PRODUCT_ID" indexType="btree">
<column name="product_id"/>
</index>
</table>
</schema>
Why keep it this simple? It’s easier to manage in admin and to render JSON-LD. Si usted 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 methods 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 function _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 function _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 function _construct()
{
$this->_init('Magefine\ProductQuestions\Model\Question', 'Magefine\ProductQuestions\Model\ResourceModel\Question');
}
}
Admin UI: grid and form
Use UI Components for the admin grid. Usted va a need an admin route, ACL entries and UI component XML for the listing and form. I’ll show the listing component file and a simplified form component. Place listing at view/adminhtml/ui_component/magefine_product_question_listing.xml and a form at magefine_product_question_form.xml. Also add an admin controller for edit/save.
ui_component 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="string">Magefine\ProductQuestions\Model\QuestionDataProvider</argument>
<argument name="name" xsi:type="string">product_questions_data_source</argument>
<argument name="primaryFieldName" xsi:type="string">question_id</argument>
<argument name="requestFieldName" xsi:type="string">question_id</argument>
</argument>
</dataSource>
<columns>
<column name="question_id"/>
<column name="product_id"/>
<column name="question_text"/>
<column name="status"/>
<actionsColumn name="actions"/>
</columns>
</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 field for answer_text and a status selector. When admin saves with answer_text and sets status=1 (Answered), we’ll trigger an email workflow (see later).
Frontend integration — display the form without slowing pages
Key goals:
- Render minimal HTML server-side so página de productos 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">
<block class="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 issues -->
</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_key, send AJAX to productquestions/ajax/save, and show success states. Porque it's client-side, the form doesn’t affect page cache and only executes for visitors who interact with the widget.
define(['jquery', 'mage/url', 'mage/storage'], function($, urlBuilder, storage){
'use strict';
return function(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', function(e){
e.preventDefault();
var data = $(this).serialize();
var url = urlBuilder.build('productquestions/ajax/save');
$.ajax({
url: url,
data: data,
type: 'POST',
dataType: 'json'
}).done(function(res){
if (res.success) {
container.html('Thanks! Your question is submitted and será reviewed.
');
} else {
container.find('.magefine-error').remove();
container.prepend('' + (res.message || 'Error') + '
');
}
}).fail(function(){
container.prepend('Network error
');
});
});
}
});
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 controller and validation
Create Controller/Ajax/Save.php in frontend. Use POST and form_key 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 customer 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 function __construct(Context $context, JsonFactory $resultJsonFactory, QuestionFactory $questionFactory)
{
parent::__construct($context);
$this->resultJsonFactory = $resultJsonFactory;
$this->questionFactory = $questionFactory;
}
public function 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 error']);
}
}
}
Note: Using $model->save() is fine for small features; for higher scale, use repositories or resource model save to avoid legacy behavior. Also consider reCAPTCHA and rate limiting for production.
Notification workflow — 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 email to a configured admin/support email (or multiple emails) to alert them so they can moderate or respond if they didn't answer inline.
- If a question came from a logged-in customer or includes an email input, send an automated templated email when the answer is published.
Implementation notes:
- Use events/observers: Observe model save (catalog_product_save_after is not relevant) — we can disparche a custom event in the model resource or use a plugin on ResourceModel::save to detect status change from pending to answered. Another simpler approach is to send notification in the admin controller after saving the form.
- Create email templates in etc/email and load them by identifier in the code.
- Use Magento\Framework\Mail\Template\TransportBuilder to build and send emails.
Example: after admin saves, if ($oldStatus == 0 && $newStatus == 1) send notifications.
// inside admin controller save after successfully saving the model
if ($question->getStatus() == 1 && $oldStatus == 0) {
// notify admins
$this->notifyAdmin($question);
// notify customer if email 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 usted want asynchronous sending (recommended), push email sending to a queue or a tarea cron to avoid blocking the admin save operation.
Spam, moderation, and data privacy
Considerations for real sites:
- Anti-spam: Google reCAPTCHA v2/v3, honeypot fields, rate limiting per IP or per product/cookie, and content filtering.
- GDPR: Si usted store personal data (emails, names), make sure to allow deletion and to document retention policy — add admin tools to export/delete a user's questions.
- Moderation: Add statuses (pending, approved, hidden) and optionally a moderation queue for high-traffic stores.
Generating SEO-friendly Preguntas frecuentes rich snippets
Una vez a question is answered and made public (status = 1 and visibility flag), you can output a JSON-LD block on the página de producto that follows Google’s Preguntas frecuentes schema. Keep generation server-side but only for cached public content (answered Q&As). Porque content is fairly static once answered, adding this to the página de producto 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' => 'Preguntas frecuentesPage',
'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 usted add private or user-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.
- Asegúrese text is cleaned (no script or markup) before output.
Performance considerations
Key points to keep your página de productos fast and cache-friendly:
- Do not include dynamic per-visitor content 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 indexación on product_id and status.
- Use asynchronous email sending (queue or cron) so admins don't wait for emails to be sent.
- Compress and minify the small JS module and keep its payload tiny; the code above is deliberately small.
Example admin-to-customer auto-reply template
<!-- etc/email/magefine_question_answered_template.html -->
<h3>Your question has been answered</h3>
<p>Hello {{var customer_name}},</p>
<p>Thanks for your question about <strong>{{var product.name}}</strong>. Aquí está the answer:</p>
<p><em>{{var question.question_text}}</em></p>
<p>{{var question.answer_text}}</p>
<p>Si usted need more help, reply to this email or visit the página de producto: <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. Puede either install a de terceros extension (if reputable) or build this custom feature in-house. Aquí están the trade-offs I’d discuss with product and ops teams.
Extensions (Pros):
- Fast to deploy — often configurable and ready to go.
- May include advanced features como moderation workflows, analytics, and spam protection out-of-the-box.
- Updates supported by vendor (but check compatibility with your Magento version).
Extensions (Cons):
- May include features you don’t need — increasing complexity and possible performance overhead.
- Sometimes poorly maintained; security and compatibility issues are risks.
- Less flexible for custom integrations with internal systems (CRM, support tools, custom email flows) unless they provide APIs.
Custom development (Pros):
- Fully tailored to your workflows, data model, 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 developer time up front and for maintenance.
- If poorly implemented, it can cause performance issues or security holes.
Business case for Magefine to offer this as an extension or service:
- Small, well-built "Ask a Question" module puede ser monetized with tiers: basic free version (store Q&A storage, admin tools) and premium features (reCAPTCHA integration, multi-language templates, automatic Preguntas frecuentes 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 sería attractive to merchants who want to offload complexity.
Commercial features 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 dashboard: most asked questions, unanswered questions per product, conversion impact.
- Bulk moderation tools and CSV export/import.
- Multi-store and multi-language support including translation workflows for answers.
- Rich snippet verification tool to ensure Preguntas frecuentes JSON-LD is valid and limited to allowed number of entries.
Testing and despliegue checklist
- Unit test models and resource models for CRUD operations.
- Manual QA: submit questions as guest and logged-in users; verify admin grid and answer workflow.
- Security test: XSS in question/answer fields, validate outputs and sanitize when generating JSON-LD.
- Performance test: ensure page load time not impacted; check FPC hits/misses before and after deploy.
- Functional tests on email sending (dev-mode logs or staging SMTP) and rate limiting.
Extra tips 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 users click "Ask a question" or submit — useful metric to prioritize product content improvements.
- Make answers public automatically only after review unless you trust staff workflow.
- Expose a small badge on página de productos like "Questions answered: 3" to improve credibility and encourage customers 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 implementation 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 controllers and email workflows, and JSON-LD for SEO.
Si usted want, I can:
- Generate a downloadable sample module repository with all files and minimal dependencies for testing on a dev environment.
- Create a premium feature list and pricing 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.