Creating categories and subcategories in Magento
A parent category and a subcategory with products, step by step in the admin: every field from the video, a REST API script that builds the same tree, and the fixes for when a category doesn’t show up.
Downloads
The script uses the Luma sample data SKUs; change SKUS, the names and the URL keys to your own.
The category tree
├─ Women menu item
├─ Men menu item
└─ Travel Gear parent · menu item · anchor
└─ Backpacks subcategory · drop-down · 4 products
Every store view has one root category (Stores › All Stores shows which). Its children are the main menu, and their children open as drop-downs. A new category always goes under the category that is selected in the tree.
1. Parent category: Travel Gear
Catalog › Categories, select Default Category, then Add Subcategory.
| Section | Field | Value | Why |
|---|---|---|---|
| Enable Category | Yes | No hides it everywhere. | |
| Include in Menu | Yes | No keeps the page, but takes it out of the main menu. | |
| Category Name | Travel Gear | Menu text, page title, breadcrumb. | |
| Display Settings | Anchor | Yes | The page also lists the products of all subcategories, with layered navigation. |
| Search Engine Optimization | URL Key | travel-gear | The page is /travel-gear.html. |
| Search Engine Optimization | Meta Title | Travel Gear: Bags and Backpacks | The title in Google results. |
Save. Travel Gear appears in the tree with its ID, for example Travel Gear (ID: 41) (0).
2. Subcategory: Backpacks
With Travel Gear selected, Add Subcategory again and name it Backpacks. Open Products in Category, type Backpack in the Name filter, press Enter and tick the products. Save.
| Field | Value |
|---|---|
| Category Name | Backpacks |
| URL Key | backpacks (filled in from the name), so the page is /travel-gear/backpacks.html |
| Products in Category | Driven Backpack, Endeavor Daytrip Backpack, Fusion Backpack, Crown Summit Backpack |
Backpacks (ID: 42) (4).3. On the storefront
Travel Gear is in the main menu, Backpacks opens under it, and the breadcrumb reads Home › Travel Gear › Backpacks. Travel Gear has no products of its own, but as an anchor it lists the four backpacks.
On a live store the cron job updates the indexes and caches within a minute or two. To see the result right away:
cd /var/www/magento # are the indexers on "Update by Schedule", and is anything waiting? sudo -u magento bin/magento indexer:status # the cron job does the work on a live store; to see the result right now: sudo -u magento bin/magento indexer:reindex catalog_category_product catalog_product_category catalogsearch_fulltext sudo -u magento bin/magento cache:clean block_html full_page
The URLs Magento created:
# the URLs Magento made for the new categories mysql magento -e "SELECT request_path, target_path FROM url_rewrite WHERE entity_type = 'category' AND request_path LIKE 'travel-gear%';"
The same tree by REST API
Useful when you set up many stores or move a category structure from a test store. The script logs in with the admin user and the code from your authenticator app, creates both categories and adds the products by SKU. Needs curl and jq.
#!/usr/bin/env bash
# Build the lesson's category tree through the Magento REST API:
# Default Category (root, ID 2) > Travel Gear (anchor) > Backpacks (+ products by SKU)
# usage: STORE=https://www.example.com ADMIN_USER=youradmin ./magento-create-categories.sh
# Needs curl and jq. Asks for the admin password and the 6-digit code from your authenticator app
# (Magento 2.4 admin accounts use two-factor authentication).
set -euo pipefail
STORE=${STORE:?set STORE, e.g. https://www.example.com}
ADMIN_USER=${ADMIN_USER:?set ADMIN_USER}
ROOT_ID=${ROOT_ID:-2} # Default Category
SKUS=${SKUS:-"24-WB03 24-WB06 24-MB02 24-MB03"} # Luma sample backpacks
CURL=(curl -sS --fail-with-body ${INSECURE:+-k}) # INSECURE=1 for a self-signed test certificate
read -rsp "Admin password: " PASS; echo
read -rp "Authenticator code: " OTP
TOKEN=$("${CURL[@]}" -X POST "$STORE/rest/V1/tfa/provider/google/authenticate" -H 'Content-Type: application/json' \
-d "$(jq -n --arg u "$ADMIN_USER" --arg p "$PASS" --arg o "$OTP" '{username:$u,password:$p,otp:$o}')" | jq -r .)
API() { "${CURL[@]}" -X "$1" "$STORE/rest/all/V1/$2" -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' ${3:+-d "$3"}; }
new_category() { # name parent url_key [meta_title] -> prints the new ID
API POST categories "$(jq -n --arg n "$1" --argjson p "$2" --arg k "$3" --arg m "${4:-}" '{category:{
name:$n, parent_id:$p, is_active:true, include_in_menu:true,
custom_attributes:([{attribute_code:"url_key",value:$k},{attribute_code:"is_anchor",value:"1"}]
+ (if $m == "" then [] else [{attribute_code:"meta_title",value:$m}] end))}}')" | jq -r .id
}
PARENT=$(new_category "Travel Gear" "$ROOT_ID" travel-gear "Travel Gear: Bags and Backpacks")
echo "Travel Gear: ID $PARENT"
CHILD=$(new_category "Backpacks" "$PARENT" backpacks)
echo "Backpacks: ID $CHILD"
for sku in $SKUS; do
API POST "categories/$CHILD/products" "$(jq -n --arg s "$sku" --argjson c "$CHILD" '{productLink:{sku:$s,category_id:$c,position:0}}')" >/dev/null
echo " + $sku"
done
echo "Done. Open $STORE/travel-gear/backpacks.html (the menu shows it after the next cron run or a cache flush)."
Run: STORE=https://www.example.com ADMIN_USER=youradmin bash magento-create-categories.sh. Add INSECURE=1 for a self-signed test certificate.
Good to know
- Move a category by drag and drop in the tree. Magento rewrites the URLs and, with “Create Permanent Redirect for URLs if URL Key Changed” on, adds 301 redirects.
- Keep the tree shallow. Two or three levels are enough for most stores; deep trees make menus and breadcrumbs long.
- Hide instead of delete. Enable Category = No, or Include in Menu = No. Deleting a category removes its URL rewrites and position data for good.
- Anchor for parent categories, off only when a parent should show its own hand-picked products.
Troubleshooting
| Symptom | Cause and fix |
|---|---|
| New category missing from the menu | Cached menu block or full page cache. Wait for cron, or run cache:clean block_html full_page. Check Include in Menu. |
| Category page is empty | Indexers on schedule and cron not running: indexer:status, then fix cron. Products must be enabled, in stock and visible in Catalog. |
| Parent page shows no products | Anchor is No and the parent has no products of its own: set Anchor = Yes. |
| 404 on the category URL | URL key changed or rewrite missing: check the url_rewrite query above; save the category again to regenerate. |
| Category not in the tree you expect | It was created under the selected category. Drag it to the right parent. |
▶ Watch every MageBooster lesson on YouTube: all lessons in order · subscribe to the channel

