magendoo / module-faq
Magento 2 FAQ and Product Questions module — SEO-optimized FAQ pages, product Q&A tabs, ask-a-question forms, ratings, and admin knowledge base management.
Magendoo FAQ & Product Questions Module for Magento 2
A FAQ and Product Questions management system for Magento 2: SEO-friendly FAQ pages, a
product-page Q&A tab with an ask-a-question form, moderation workflow, ratings, search with
analytics, CMS widgets, CSV import/export and a REST API.
Screenshots
Product Page — FAQ tab with questions and "Ask a Question" form
[image: Product Page FAQ Tab]
Admin — Question editor with WYSIWYG answer, status workflow, and SEO fields
[image: Admin Question Editor]
Admin — FAQ Questions grid with filtering, mass actions, and status management
[image: Admin Questions Grid]
Features
- Hierarchical FAQ organization — categories, tags, and product associations
- Search with analytics — storefront FAQ search plus an admin Search Terms report
- Customer engagement — helpfulness voting (Yes/No or thumbs up/down) or a 1–5 star rating
- Product page integration — a "Product Questions" tab with an ask-a-question form
- REST API — 24 endpoints; anonymous storefront routes return a privacy-safe projection
- Moderation workflow — Pending → Answered / Rejected, with optional email notifications
- Compliance — GDPR consent recording (timestamp + wording snapshot), native reCAPTCHA support
- SEO — clean URLs with URL rewrites, FAQPage JSON-LD, sitemap integration, canonical links,
hreflang tags, per-entity robots meta - Multi-store — per-store-view assignment for categories and questions; customer-group
restrictions for B2B/B2C segmentation
A merchant-oriented walkthrough lives in docs/user-guide.md.
Requirements
| Requirement | Version |
|---|---|
| Magento Open Source / Adobe Commerce | 2.4.x (magento/framework >= 103.0.0) |
| PHP | 8.1+ |
Developed and tested against Magento Open Source 2.4.9 on PHP 8.4. Depends on the
Magento_Sitemap, Magento_Widget and reCAPTCHA modules, all declared in composer.json
(they ship with every standard Magento installation).
Installation
Composer
composer require magendoo/module-faq
bin/magento module:enable Magendoo_Faq
bin/magento setup:upgrade
bin/magento cache:flush
bin/magento magendoo:faq:reindex
Manual
cp -r Magendoo/Faq app/code/Magendoo/
bin/magento module:enable Magendoo_Faq
bin/magento setup:upgrade
bin/magento cache:flush
bin/magento magendoo:faq:reindex
In production mode also run bin/magento setup:di:compile and
bin/magento setup:static-content:deploy.
Post-installation setup
- Configure the module: Stores → Configuration → Magendoo Extensions → FAQ
- Set admin permissions: System → Permissions → User Roles — the FAQ resources appear
under Content → FAQ - Enable reCAPTCHA (recommended): Stores → Configuration → Security → Google reCAPTCHA
Storefront — set keys, then choose a type for Enable for FAQ Ask a Question Form - Create content: Content → FAQ → FAQ Categories and Content → FAQ → FAQ Questions
Uninstallation
The module does not ship a Setup/Uninstall class, and Magento does not process a disabled
module's schema, so neither module:disable nor composer remove removes any data. To remove
the module completely:
bin/magento module:disable Magendoo_Faq
composer remove magendoo/module-faq # or delete app/code/Magendoo/Faq
bin/magento setup:upgrade
bin/magento cache:flush
Then, only if you also want to delete all FAQ data, drop the tables and rows manually
(back up first):
DROP TABLE IF EXISTS magendoo_faq_question_tag, magendoo_faq_rating,
magendoo_faq_question_customer_group, magendoo_faq_question_product,
magendoo_faq_question_category, magendoo_faq_question_store,
magendoo_faq_category_customer_group, magendoo_faq_category_store,
magendoo_faq_search_log, magendoo_faq_question, magendoo_faq_category,
magendoo_faq_tag;
DELETE FROM core_config_data WHERE path LIKE 'magendoo_faq/%';
DELETE FROM url_rewrite WHERE entity_type IN ('faq-category', 'faq-question');
Configuration Reference
All settings live under Stores → Configuration → Magendoo Extensions → FAQ and can be set per
store view. Defaults below come from etc/config.xml; fields without an entry there start
empty/off.
magendoo_faq/general
| Field | Default | Description |
|---|---|---|
enabled |
1 | Module on/off |
title |
FAQ |
FAQ landing page title |
url_prefix |
faq |
URL segment for all FAQ pages |
allow_guest_questions |
1 | Allow guests to use the storefront ask form |
magendoo_faq/navigation
| Field | Default | Description |
|---|---|---|
show_breadcrumbs |
1 | Breadcrumb display on FAQ pages |
sort_categories_by |
position |
position, name or most_viewed |
sort_questions_by |
position |
position, name or most_viewed |
answer_length_limit |
250 | Character limit for answer previews in listings |
show_search_box |
1 | Search box on FAQ pages |
no_results_text |
(empty) | Message when a search finds nothing |
questions_per_category_page |
10 | Pagination on category pages |
questions_per_search_page |
10 | Pagination on search results |
short_answer_behavior |
short_answer |
Listing preview source: short_answer or cut_full_answer (truncated full answer) |
tags_limit |
20 | Maximum tags in the tag cloud (0 = no limit) |
magendoo_faq/product_page
| Field | Default | Description |
|---|---|---|
enabled |
1 | Show the questions tab on product pages |
tab_name |
Product Questions |
Tab label; {count} is replaced with the question count |
tab_position |
40 | Tab sort order |
show_ask_button |
1 | Show the ask-a-question form in the tab |
questions_limit |
10 | Maximum questions shown in the tab |
magendoo_faq/rating
| Field | Default | Description |
|---|---|---|
enabled |
1 | Rating widget on question pages |
type |
yes_no |
yes_no, voting (thumbs up/down) or average_rating (1–5 stars) |
allow_guest_rating |
1 | Allow guests to vote; when off, voting requires login |
magendoo_faq/social
| Field | Default | Description |
|---|---|---|
enabled |
0 | Social share buttons on question pages |
networks |
(empty) | Any of Facebook, Twitter, LinkedIn, Pinterest, Email |
magendoo_faq/seo
| Field | Default | Description |
|---|---|---|
url_suffix_enabled |
0 | Append a suffix to FAQ URLs |
url_suffix |
.html |
The suffix (when enabled) |
remove_trailing_slash |
0 | Redirect trailing-slash URLs to the canonical form |
use_canonical |
0 | Emit a <link rel="canonical"> on FAQ pages (per-entity override available) |
structured_data_enabled |
1 | FAQPage JSON-LD on category and question pages |
robots_search_results |
NOINDEX,FOLLOW |
Robots meta for the search results page |
add_to_sitemap |
1 | Include FAQ pages in the XML sitemap |
hreflang_enabled |
0 | Hreflang alternate links for multi-store setups |
sitemap_frequency |
(empty — falls back to weekly) |
Sitemap change frequency |
sitemap_priority |
(empty — falls back to 0.5) |
Sitemap priority |
magendoo_faq/user_notifications
| Field | Default | Description |
|---|---|---|
enabled |
0 | Email the asker when their question is answered |
email_sender |
general |
Sender identity |
email_template |
magendoo_faq_user_notifications_email_template |
Template |
magendoo_faq/admin_notifications
| Field | Default | Description |
|---|---|---|
enabled |
0 | Email the store owner when a new question is submitted |
send_to |
[email protected] |
Comma-separated recipient addresses |
email_template |
magendoo_faq_admin_notifications_email_template |
Template |
magendoo_faq/gdpr
| Field | Default | Description |
|---|---|---|
enabled |
0 | Require a consent checkbox on the ask form |
consent_text |
"I agree that my name and email address will be stored…" | Checkbox label. When consent is given, the timestamp and the wording shown are stored on the question |
reCAPTCHA
The ask form registers with Magento's native reCAPTCHA framework as
FAQ Ask a Question Form (recaptcha_frontend/type_for/magendoo_faq_question_submit) —
configure it under Stores → Configuration → Security → Google reCAPTCHA Storefront.
Storefront URLs
Implemented by a custom router (Magento\Framework\App\RouterInterface):
/faq/ → FAQ home (category listing + search)
/faq/{category-url-key} → Category page
/faq/{category-url-key}/{question-key} → Question page (membership is verified;
mismatched pairs redirect to the canonical URL)
/faq/{question-url-key} → Question page (via URL rewrite)
/faq/tag/{tag-url-key} → Tag page
/faq/search?q=keyword → Search results
/faq/question/suggest?q=keyword → AJAX autocomplete (JSON)
- Configurable URL prefix per store view; optional suffix.
- Suffixed/unsuffixed and trailing-slash variants redirect to the canonical form
(trailing-slash redirect gated byseo/remove_trailing_slash). - URL rewrites are generated on save, cleaned up on delete/unpublish, and validated for
uniqueness (url_keyis unique at the database level; collisions produce an actionable
error naming the conflicting path). bin/magento magendoo:faq:reindexpurges and regenerates all FAQ rewrites.
CLI Commands
# Purge and regenerate FAQ URL rewrites for all categories and questions
bin/magento magendoo:faq:reindex
# Export questions or categories to CSV (default file: var/export/faq-{entity}.csv)
bin/magento magendoo:faq:export -e questions -f var/export/faq-questions.csv
bin/magento magendoo:faq:export -e categories
# Import questions or categories from CSV
bin/magento magendoo:faq:import -e questions -f var/export/faq-questions.csv
bin/magento magendoo:faq:import -e categories -f var/export/faq-categories.csv
Export includes the relation columns (store_ids, category_ids, product_ids, tags,
customer_group_ids) and defuses spreadsheet formula injection. Import updates rows whose
question_id/category_id matches an existing record, creates rows with an empty id column,
and skips (with a report) rows whose id does not exist.
REST API
24 routes in etc/webapi.xml. Auth column: admin = admin/integration token with the named
ACL resource; customer = a logged-in customer token; anonymous = no token required.
| Endpoint | Method | Auth | Description |
|---|---|---|---|
/V1/faq/categories |
GET | admin (Magendoo_Faq::category) |
List categories (SearchCriteria) |
/V1/faq/categories/:id |
GET | admin (Magendoo_Faq::category) |
Get category |
/V1/faq/categories |
POST | admin (Magendoo_Faq::category_edit) |
Create category |
/V1/faq/categories/:id |
PUT | admin (Magendoo_Faq::category_edit) |
Update category |
/V1/faq/categories/:id |
DELETE | admin (Magendoo_Faq::category_delete) |
Delete category |
/V1/faq/questions |
GET | admin (Magendoo_Faq::question) |
List questions (SearchCriteria) |
/V1/faq/questions/:id |
GET | admin (Magendoo_Faq::question) |
Get question (full object) |
/V1/faq/questions |
POST | admin (Magendoo_Faq::question_edit) |
Create question |
/V1/faq/questions/:id |
PUT | admin (Magendoo_Faq::question_edit) |
Update question |
/V1/faq/questions/:id |
DELETE | admin (Magendoo_Faq::question_delete) |
Delete question |
/V1/faq/questions/submit |
POST | customer | Submit a question (see note below) |
/V1/faq/questions/:id/rate |
POST | anonymous | Vote: positive/negative, or "1"–"5" in star mode |
/V1/faq/products/:id/questions |
GET | anonymous | Product questions (public projection) |
/V1/faq/categories/:id/questions |
GET | anonymous | Category questions (public projection) |
/V1/faq/questions/search |
GET | anonymous | Full-text search (public projection) |
/V1/faq/categories/url-key/:key/store/:id |
GET | anonymous | Category lookup by URL key |
/V1/faq/questions/url-key/:key/store/:id |
GET | anonymous | Question lookup by URL key (public projection; 404 for non-public questions) |
/V1/faq/questions/:id/notify |
POST | admin (Magendoo_Faq::question_edit) |
Send answer notification email |
/V1/faq/questions/:id/view |
POST | anonymous | Increment view count |
/V1/faq/tags |
GET | admin (Magendoo_Faq::question) |
List tags (SearchCriteria) |
/V1/faq/tags/:id |
GET | admin (Magendoo_Faq::question) |
Get tag |
/V1/faq/tags |
POST | admin (Magendoo_Faq::question_edit) |
Create tag |
/V1/faq/tags/:id |
PUT | admin (Magendoo_Faq::question_edit) |
Update tag |
/V1/faq/tags/:id |
DELETE | admin (Magendoo_Faq::question_delete) |
Delete tag |
Notes:
- Anonymous routes return a public projection (
PublicQuestionInterface): title, url_key,
answers, position, rating counters, view count and SEO fields. They never include
sender_name,sender_email,customer_id,statusorvisibility. /V1/faq/questions/submitrequires an authenticated customer. The storefront form is
protected by reCAPTCHA through a frontend predispatch observer — an event REST never
dispatches — so an anonymous REST route would bypass the captcha entirely. Guests submit
through the storefront form. The submit service enforces the same rules on both entry
points: guest permission, e-mail validation, GDPR consent, slug sanitisation.- Rating identity (customer id / IP) is resolved server-side and cannot be supplied by the
caller.
For Developers
Service contracts
Api/
├── CategoryRepositoryInterface # Category CRUD + getByUrlKey()
├── QuestionRepositoryInterface # Question CRUD + getByUrlKey() (ACL-protected, full object)
├── QuestionManagementInterface # submit, rate, search, product/category listings,
│ # getQuestionByUrlKey() (public projection), notify, views
└── TagRepositoryInterface # Tag CRUD
Repositories support the SearchCriteria pattern. Storefront-safe reads go through
QuestionManagementInterface and return Api\Data\PublicQuestionInterface.
Database schema (12 tables)
magendoo_faq_category,magendoo_faq_question,magendoo_faq_tag— entities
(questions carry SEO fields, rating counters, view count and the GDPR consent record:
consent_given_at+consent_textsnapshot)magendoo_faq_category_store,magendoo_faq_question_store— store assignmentmagendoo_faq_category_customer_group,magendoo_faq_question_customer_group— group restrictionsmagendoo_faq_question_category,magendoo_faq_question_product,magendoo_faq_question_tag— M:N relationsmagendoo_faq_rating— individual votes (dedup by customer id / IP, server-resolved)magendoo_faq_search_log— one row per executed search
url_key is unique on both the category and question tables.
Frontend
Layout handles: faq_index_index, faq_category_view, faq_question_view,
faq_question_search, faq_tag_view, plus catalog_product_view for the product tab.
RequireJS components: faqAskForm, faqRating, faqAutocomplete, faqTabDeeplink.
All storefront blocks implement IdentityInterface, so saving, deleting or re-publishing FAQ
content invalidates the affected full-page-cache entries automatically. The search results
page is deliberately non-cacheable so the search log sees repeat searches.
Admin
Menu: Content → FAQ (FAQ Categories, FAQ Questions, FAQ Search Terms).
UI components: faq_category_listing, faq_category_form, faq_question_listing,
faq_question_form, faq_searchlog_listing. Mass actions: delete and change-status on both
grids, plus change-visibility for questions.
ACL (etc/acl.xml):
Magendoo_Faq::faq # Content → FAQ menu
├── Magendoo_Faq::category # Manage Categories (grid access)
│ ├── Magendoo_Faq::category_view
│ ├── Magendoo_Faq::category_edit
│ └── Magendoo_Faq::category_delete
├── Magendoo_Faq::question # Manage Questions (grid access)
│ ├── Magendoo_Faq::question_view
│ ├── Magendoo_Faq::question_edit
│ ├── Magendoo_Faq::question_delete
│ └── Magendoo_Faq::question_approve
└── Magendoo_Faq::search_log # Search Terms Report
Magendoo_Faq::config # System configuration section (under Stores)
CMS widgets
| Widget | Description |
|---|---|
FAQ Questions List (magendoo_faq_questions_list) |
Questions, optionally filtered by category; list or accordion template |
FAQ Categories List (magendoo_faq_categories_list) |
Category links with optional question counts |
FAQ Search Box (magendoo_faq_search_box) |
Search form with configurable placeholder |
SEO implementation
- JSON-LD: FAQPage schema on category and question pages, encoded with
JSON_HEX_TAG|JSON_HEX_AMP|JSON_HEX_APOS|JSON_HEX_QUOTso user-supplied titles cannot break
out of the script block. - Sitemap: two
Magento\Sitemap\Model\ItemProvider\ItemProviderInterfaceproviders
(categories + questions), honouring per-entityexclude_sitemap. - Canonical:
Block\Faq\Canonicalemits<link rel="canonical">on the FAQ home,
category, question and tag pages whenseo/use_canonicalis enabled; a per-entity
canonical_urlfield overrides the computed URL. - Robots: per-entity noindex/nofollow, and a configurable robots meta on search results.
- Hreflang: alternate links per assigned store when
seo/hreflang_enabledis on.
Extending
<!-- app/code/Vendor/Module/etc/di.xml -->
<type name="Magendoo\Faq\Api\QuestionManagementInterface">
<plugin name="vendor_custom_question_logic"
type="Vendor\Module\Plugin\QuestionManagementPlugin"/>
</type>
All service contracts are interfaces resolved through DI preferences, so they can be plugged
or overridden the standard Magento way.
Tests and quality gates
- 85 PHPUnit unit tests (
Test/Unit, PHPUnit 12):composer testor
vendor/bin/phpunit -c phpunit.xml.dist - PHPStan (level in
phpstan.neon.dist, pre-existing findings recorded in
phpstan-baseline.neon):composer analyse - phpcs with the Magento2 standard:
composer cs - CI runs all three on PHP 8.1–8.4.
Known limitations
Stated plainly so you don't have to discover them:
- The "Show Full Answer" and "Hide Direct URL" checkboxes on the question form are
stored but not yet consumed by any storefront code — they currently have no effect. - The Search Terms report logs one row per executed search; the Hits column is always 1.
Sort by Search Query to gauge frequency. - Only an
en_UStranslation ships (i18n/en_US.csv, all 269 phrases). - Anonymous REST submission is intentionally disabled (see the REST notes above).
FAQ
Q: Can customers submit questions without an account?
Yes — through the storefront form, when magendoo_faq/general/allow_guest_questions is on.
Enable reCAPTCHA for the form to keep bots out. The REST submit route, by contrast, requires a
logged-in customer token.
Q: Can I migrate FAQs from another platform?
Yes, via bin/magento magendoo:faq:import with a CSV whose header row matches the database
column names (run an export first to get a template), or via the admin REST API.
Q: Can I restrict FAQs to specific customer groups?
Yes. Both categories and questions have a Customer Groups multiselect; an empty selection means
visible to all groups. Restrictions are enforced on listings, question pages and the anonymous
REST reads.
Q: What rating modes are available?
yes_no ("Was this answer helpful?"), voting (thumbs up/down with counts) and
average_rating (a real 1–5 star input; average_rating stores the 0–5 mean).
Q: Does the module support multiple languages/stores?
Content is assignable per store view, and hreflang tags can be enabled. UI translations
currently ship for en_US only — add your own CSV under i18n/.
Troubleshooting
FAQ URLs return 404 — run bin/magento magendoo:faq:reindex (purges and rebuilds all FAQ
rewrites), check the URL prefix doesn't collide with a CMS page, and flush cache.
Questions missing on the storefront — the question must have status Answered, visibility
Public (or Logged In while testing as a customer), a store assignment matching the current
store view, and no excluding customer-group restriction.
Email notifications not sent — enable magendoo_faq/user_notifications/enabled (customer
"your question was answered" mail) and/or magendoo_faq/admin_notifications/enabled (new
question alert); both default to off. The "Save and Send Email" button reports exactly what
happened — including "notifications are disabled".
reCAPTCHA not validating — configure keys and pick a type for "Enable for FAQ Ask a
Question Form" under Stores → Configuration → Security → Google reCAPTCHA Storefront.
Changelog
See CHANGELOG.md.
License
MIT — see the LICENSE file.
Contributing
Pull requests are welcome. Please follow the Magento2 coding standard
(composer cs), keep PHPStan green against the baseline (composer analyse), and add unit
tests for behaviour changes (composer test).
Support & Resources
- Issue Tracker: https://github.com/magendooro/magento2-catalog-faq-geo/issues
- Implementation & Customization Services: [email protected]
Changelog
All notable changes to this project will be documented in this file.
The format follows Keep a Changelog.
This project adheres to Semantic Versioning.
Unreleased
[2.0.1] - 2026-07-25
Fixed
- Exported tag names can now be re-imported.
magendoo:faq:exportwrites tags as names,
but nothing mapped them back to ids, so the name reached the junction table, cast to0
and violated the foreign key — aborting the whole row. A plain export → import round trip
failed on any question that had tags. Import now resolves names to ids case-insensitively
and creates any tag that does not exist, which also gives merchants the only bulk way to
add tags, since the module has no admin screen for creating them. Generated URL keys are
deterministic and de-duplicated, so importing the same file twice changes nothing. - The junction writer now drops anything that is not a positive integer instead of turning it
into a row that cannot exist, so no future caller can reproduce this class of failure.
2.0.0 - 2026-07-25
A review-and-hardening release: 43 fixes across routing, security, caching, the admin surface,
email and the REST API, plus the first real test suite. Several 1.0.0 features that could not
work at all (ratings, tags, customer-group restrictions, admin notifications, canonical links,
grid delete, icon upload) now do.
Upgrade notes — required steps
bin/magento setup:upgrade # new columns, unique url_key, new foreign keys
bin/magento magendoo:faq:reindex # purge and regenerate ALL FAQ URL rewrites
bin/magento cache:flush
The reindex is mandatory: 1.0.0 wrote question rewrites against a literal store_id = 0
(which the router never matches — every question URL saved with "All Store Views" was a 404)
and never deleted rewrites for removed entities. The command now purges and rebuilds, which
repairs both. setup:upgrade will fail if existing rows violate the new url_key unique
constraint — deduplicate url_keys first on stores with hand-edited data.
Breaking changes
- API —
QuestionManagementInterface::rateQuestion()drops the$customerIdand
$ipAddressparameters (they let any caller stuff votes and attribute them to real
customers). New signature:rateQuestion(int $questionId, string $voteType). Voter identity
is resolved server-side; extra request fields are ignored on the REST route. - API —
submitQuestion()now takes abool $gdprConsentargument and returns the new
PublicQuestionInterfaceprojection. - REST — anonymous responses no longer leak asker PII.
getProductQuestions(),
getCategoryQuestions(),searchQuestions()and the question url-key lookup return the
public projection:sender_name,sender_email,customer_id,statusandvisibility
are gone from anonymous payloads. The url-key route is now served by
getQuestionByUrlKey()and returns 404 for non-public questions. - REST —
POST /V1/faq/questions/submitrequires an authenticated customer. The
anonymous route bypassed reCAPTCHA (and every other submit rule) entirely; guests submit
through the protected storefront form. average_ratingchanges meaning from percent-positive to a 0–5 star average. The
"Average Rating" mode is now a real 1–5 star input; on the wire the star value travels in
voteTypeas"1"–"5". (No stored data is affected in practice: 1.0.0 ratings always
failed against a nonexistent table.)- Removed config fields (they were read by no code):
general/add_to_toolbar,
general/add_to_footer, the entirehome_pagegroup (use_cms_page,cms_page_id,
layout),navigation/show_ask_button,navigation/include_categories_in_search,
product_page/short_answer_behaviorandseo/rich_breadcrumbs. - Schema:
url_keyis now unique on the category and question tables; the customer-group
junction tables gained foreign keys. - Licence headers corrected from OSL-3.0 to MIT, matching the LICENSE file and
composer.json the project has always shipped. A correction of a contradiction, not a
relicensing.composer.jsonno longer hardcodes aversion(tags are the source of truth).
Fixed — URLs and SEO
- Question URL rewrites are written against real store ids; saving with "All Store Views"
no longer produces deadstore_id = 0rows (every question URL 404'd on a stock install). magendoo:faq:reindexno longer un-scopes every rewrite (it now looks up each entity's
store assignment) and no longer aborts on the first url_key collision — it skips and reports.- URL rewrites are deleted when an entity is deleted or unpublished; url_key collisions
produce an actionable error naming the conflicting path instead of "Something went wrong". - The router verifies category/question membership on two-segment URLs (previously any
category paired with any question served a duplicate 200) and redirects suffix and
trailing-slash variants to the canonical form. - A canonical
<link>is actually emitted (gated byseo/use_canonical, with the per-entity
override) — in 1.0.0 the config and entity fields had no frontend consumer. - Hreflang alternates respect per-store prefixes, entity store assignment and module
enablement instead of gluing the current path onto every store's base URL. - JSON-LD is hex-escaped so a shopper-submitted question title containing
</script>can no
longer break out of the structured-data block (stored XSS).
Fixed — features that had no working write path
- Ratings: votes are recorded against the real
magendoo_faq_ratingtable — every vote in
1.0.0 failed with "Base table not found" and the raw SQL error was shown to the shopper.
Guest-rating enforcement now actually applies. - Tags: the question form gained a Tags multiselect and save path — the tag cloud, tag
pages and router tag branch previously read a junction nothing could populate. - Customer-group restrictions: both admin forms gained Customer Groups multiselects, and
the storefront listings, question page and anonymous REST reads all enforce them. - Admin notifications:
sendAdminNotification()is now called after a successful
submission (form and REST) — it previously had no caller. - GDPR consent is recorded:
consent_given_at(UTC) plus a snapshot of the exact consent
wording shown, displayed read-only on the admin form. The consent checkbox label also
renders again (it was written to one config path and read from another).
Fixed — caching
- Storefront blocks implement
IdentityInterface, so answering, editing or unpublishing a
question invalidates the affected full-page-cache pages (previously changes were invisible
until a manual flush). - Removed the hour-long block cache on the FAQ home block that served one customer group's
content to all groups. - The ask form no longer reads the customer session on a cacheable page (it gates
client-side), so "please log in" can no longer get baked into the logged-in cache variant. - Search results are non-cacheable so the search log counts repeat searches.
Fixed — admin surface
- Grid row Delete and the edit-page Delete buttons work (they issued GETs against POST-only
controllers and 404'd). - Grids are no longer empty for non-superuser roles (the listings referenced ACL ids that
did not exist). - Category icon upload works: the missing
faq/category/uploadcontroller now exists, the
form shows the stored image, saving without a new upload no longer wipes the icon, and the
storefront renders the proper media URL. - "Save and Send Email" reports what actually happened (sent / notifications disabled /
transport failure) instead of always claiming success. - A failed save restores everything the admin typed (the data persistor is now populated).
- Opening an edit form runs a constant number of queries instead of 3N+1 over the whole table.
Fixed — email and submission
- The answer notification sends to exactly one validated recipient (a shopper-controlled
field was previously split on commas into a recipient list), renders in the correct area,
and resolves the store from the question's own assignment. - The submit success redirect uses the validated referer helper instead of the raw
Refererheader (open redirect). - The reCAPTCHA observer takes request/response from DI — enabling reCAPTCHA (the module's
own recommendation) previously fataled every submission with HTTP 500. - Storefront form and REST submit share one server-side rule set (guest permission, email
validation, GDPR consent, slug sanitisation); caller-supplied answers, counters and
customer ids are ignored.
Fixed — data layer and performance
- Export carries the relation columns it silently dropped and defuses spreadsheet formula
injection; import reports unknown ids instead of quietly creating duplicates. - The tag cloud collapses from unbounded per-tag queries to one bounded query; the categories
widget from N+2 queries to one; search uses the declared FULLTEXT index instead of a
doubleLIKEscan.
Added
- Unit test suite: 85 PHPUnit 12 tests concentrated on the previously broken logic
(URL-rewrite store scoping, JSON-LD escaping, rating integrity, config getters). - CI now runs phpcs, PHPStan (with a maintained baseline) and the unit suite on PHP 8.1–8.4;
fork PRs no longer fail on unavailable marketplace credentials. - Complete
en_US.csvtranslation (269 phrases; 1.0.0 shipped 31). - Hard dependencies declared:
Magento_Sitemap,Magento_Widgetand the reCAPTCHA modules.
Removed
- Fabricated README/marketing claims (Playwright coverage, unreleased versions 1.1.0/1.2.0).
- The dead config fields listed under breaking changes.
1.0.0 - 2026-04-13
Added
- SEO-optimized FAQ home page, category pages, and question pages with configurable URL prefix and suffix
- Custom frontend router for
faq/{category-slug}/{question-slug}URL structure - Product Questions tab on product detail pages (configurable tab name and position)
- Ask-a-Question form on product pages with guest/logged-in support and GDPR consent checkbox
- Answer helpfulness rating system (Yes/No, Voting, and Average Rating modes)
- Social share buttons on question pages (Facebook, Twitter, LinkedIn, Pinterest, Email)
- Admin grids and forms for FAQ Categories and FAQ Questions with full CRUD
- WYSIWYG editor for full answers; short answer field for listing previews
- Question workflow: Pending → Answered → Rejected status transitions
- Question visibility: Public, Logged-in only, Hidden
- 12 database tables: categories, questions, tags, ratings, search log, and M:N junction tables for stores, products, customer groups
- REST API: category CRUD (
/V1/faq/categories), question CRUD (/V1/faq/questions), product questions, category questions, search, submit, rate - URL rewrites generated on category/question save for SEO tool compatibility
- FAQPage JSON-LD structured data on category and question pages
- XML sitemap integration via
ItemProviderInterface - Hreflang tag support for multi-store setups
- Breadcrumbs with Home > FAQ > Category > Question hierarchy
- FAQ search with search terms report in admin
- Tag system with tag cloud and tag pages
- Three FAQ Widgets: Questions List, Categories List, Search Box
- Per-entity robots meta tag (noindex/nofollow) override
- Email notifications: admin notified on new question, customer notified on answer
- Customer group visibility restrictions on categories and questions
- Admin system configuration under Stores > Magendoo Extensions > FAQ and Product Questions
- CLI command
magendoo:faq:reindexto regenerate URL rewrites - i18n/en_US.csv translation file
Requires 9
| Package | Constraint |
|---|---|
| magento/framework | >=103.0.0 |
| magento/module-backend | >=102.0.0 |
| magento/module-catalog | >=104.0.0 |
| magento/module-cms | >=104.0.0 |
| magento/module-customer | >=103.0.0 |
| magento/module-store | >=101.0.0 |
| magento/module-ui | >=101.2.0 |
| magento/module-url-rewrite | >=102.0.0 |
| php | >=8.1 |
Requires-dev 2
| Package | Constraint |
|---|---|
| phpstan/phpstan | ^1.10 || ^2.0 |
| phpunit/phpunit | ^10.5 |
Compatibility
Each Magento release line is installed on its supported PHP versions, then the module is built (DI compilation + static-content deploy) and its unit and integration suites are run. The matrix shows the lines and PHP versions the module is confirmed to install and run on. Code-quality results further down (phpstan, phpcs, …) are reported separately and never affect compatibility.
Code Quality
Advisory checks against the module's source. Static analysis runs once across the whole module; PHPStan re-runs per Magento + PHP version because resolvable symbols differ between releases. These NEVER affect the Compatibility badge. A phpcs finding can't make a module incompatible.
Static analysis
Coding standards (phpcs), mess detection (phpmd), copy-pasted code (cpd), PHP cross-version compatibility, composer.json validity. Each runs once for the whole module.
| Tool | Status | Findings | Summary |
|---|---|---|---|
| PHPCS | Warning | 139 | 139 warnings (ruleset: Magento2), 86 auto-fixable with phpcbf |
| PHPMD | Warning | 77 | 77 rule violations (UnusedPrivateField:77) |
| Cpd | Warning | 3 | 3 duplicated chunks spanning 257 total lines (min-lines=5, min-tokens=70) |
| Composer validate | Info | 9 | valid; 9 advisory notes (composer validate --strict) |
PHPStan
Type-checks the module's PHP against a real Magento install at the configured gate level. Re-runs per Magento and PHP version because resolvable symbols differ between releases.
Tests
Unit and integration suites, run for each applicable Magento and PHP version. A test failure speaks to the module's behaviour, not its compatibility with a Magento line, so it is reported here separately and never reddens the compatibility matrix.
Unit tests
| Magento | PHP 8.2 | PHP 8.3 | PHP 8.4 | PHP 8.5 |
|---|---|---|---|---|
| 2.4.7 | N/A | N/A | ||
| 2.4.8 | N/A | N/A | ||
| 2.4.9 | N/A | N/A |
Integration tests
| Magento | PHP 8.2 | PHP 8.3 | PHP 8.4 | PHP 8.5 |
|---|---|---|---|---|
| 2.4.7 | N/A | N/A | ||
| 2.4.8 | N/A | N/A | ||
| 2.4.9 | N/A | N/A |
Security
Security checks run directly against the module: an audit of its declared dependencies for known vulnerabilities (composer audit) and a scan of its source for malware and web-shell signatures. Each runs once. A malware detection fails the version outright.
More from magendoo
View vendorEU Base Price (Grundpreis) display for Magento 2. Shows price per reference unit (kg, litre, m) on product pages, category listings, search results and cart. Compliant with EU Price Indication Directive 98/6/EC and German PAngV.
Shipping Restrictions module
Product labels for Magento 2 - first-class label entities with stable codes, manual assignment via product attribute, rule-computed assignments materialized by an indexer, PLP/PDP badges and GraphQL exposure
EU Omnibus Directive (2019/2161) price tracker for Magento 2. Records price changes with timestamps, computes the lowest prior price in the disclosure window, and displays it on PDP and PLP, with async queue processing for bulk operations.
Turn an existing module into recurring revenue.
If you already maintain a Magento 2 module on GitHub or GitLab, listing it on Packagento takes about five minutes. We mirror your tags, handle distribution signing, and route paid licenses through Stripe Connect, so you can keep shipping the way you already do.