Plugin Directory

Changeset 3791018


Ignore:
Timestamp:
11/08/2026 01:14:36 PM (9 hours ago)
Author:
lucaslopvet
Message:

Release 4.18.0: Loop Builder (guide topic, recipes, loop result report, validator rules)

Location:
jhmg-ai-editor-for-divi-5/trunk
Files:
2 added
11 edited

Legend:

Unmodified
Added
Removed
  • jhmg-ai-editor-for-divi-5/trunk/data/section-recipes.json

    r3766728 r3791018  
    24242424        },
    24252425        "markup": "<!-- wp:divi/section {\"module\":{\"decoration\":{\"background\":{\"desktop\":{\"value\":{\"color\":\"{{aied:color:bg}}\"}}},\"spacing\":{\"desktop\":{\"value\":{\"padding\":{\"top\":\"0px\",\"bottom\":\"112px\"}}},\"tablet\":{\"value\":{\"padding\":{\"top\":\"0px\",\"bottom\":\"88px\"}}},\"phone\":{\"value\":{\"padding\":{\"top\":\"0px\",\"bottom\":\"64px\"}}}}}},\"builderVersion\":\"5.8.0\"} -->\n<!-- wp:divi/row {\"module\":{\"advanced\":{\"columnStructure\":{\"desktop\":{\"value\":\"4_4\"}}},\"decoration\":{\"background\":{\"desktop\":{\"value\":{\"color\":\"{{aied:color:text}}\"}}},\"sizing\":{\"desktop\":{\"value\":{\"width\":\"100%\",\"maxWidth\":\"100%\"}}},\"spacing\":{\"desktop\":{\"value\":{\"padding\":{\"top\":\"112px\",\"bottom\":\"208px\",\"left\":\"24px\",\"right\":\"24px\"}}},\"tablet\":{\"value\":{\"padding\":{\"top\":\"88px\",\"bottom\":\"176px\",\"left\":\"24px\",\"right\":\"24px\"}}},\"phone\":{\"value\":{\"padding\":{\"top\":\"64px\",\"bottom\":\"144px\",\"left\":\"20px\",\"right\":\"20px\"}}}}}},\"builderVersion\":\"5.8.0\"} -->\n<!-- wp:divi/column {\"module\":{\"advanced\":{\"type\":{\"desktop\":{\"value\":\"4_4\"}}}},\"builderVersion\":\"5.8.0\"} -->\n<!-- wp:divi/heading {\"title\":{\"innerContent\":{\"desktop\":{\"value\":\"[Section name]\"}},\"decoration\":{\"font\":{\"font\":{\"desktop\":{\"value\":{\"headingLevel\":\"h4\",\"color\":\"{{aied:color:bg}}\",\"family\":\"{{aied:font:eyebrow}}\",\"size\":\"13px\",\"textAlign\":\"center\"}}}}}},\"builderVersion\":\"5.8.0\",\"aied\":{\"type\":\"eyebrow\"}} /-->\n<!-- wp:divi/group {\"module\":{\"decoration\":{\"sizing\":{\"desktop\":{\"value\":{\"width\":\"100%\",\"maxWidth\":\"720px\",\"alignment\":\"center\"}}}}},\"builderVersion\":\"5.8.0\"} -->\n<!-- wp:divi/heading {\"title\":{\"innerContent\":{\"desktop\":{\"value\":\"[A heading that leads into the detail]\"}},\"decoration\":{\"font\":{\"font\":{\"desktop\":{\"value\":{\"headingLevel\":\"h2\",\"color\":\"{{aied:color:bg}}\",\"family\":\"{{aied:font:display}}\",\"size\":\"44px\",\"lineHeight\":\"1.15em\",\"textWrap\":\"balance\",\"textAlign\":\"center\"}},\"tablet\":{\"value\":{\"size\":\"36px\"}},\"phone\":{\"value\":{\"size\":\"30px\"}}}}}},\"builderVersion\":\"5.8.0\",\"aied\":{\"type\":\"display\"}} /-->\n<!-- /wp:divi/group -->\n<!-- wp:divi/group {\"module\":{\"decoration\":{\"sizing\":{\"desktop\":{\"value\":{\"width\":\"100%\",\"maxWidth\":\"640px\",\"alignment\":\"center\"}}}}},\"builderVersion\":\"5.8.0\"} -->\n<!-- wp:divi/text {\"content\":{\"innerContent\":{\"desktop\":{\"value\":\"<p>[One line that frames the three points below.]</p>\"}},\"decoration\":{\"bodyFont\":{\"body\":{\"font\":{\"desktop\":{\"value\":{\"color\":\"{{aied:color:bg}}\",\"family\":\"{{aied:font:body}}\",\"size\":\"18px\",\"lineHeight\":\"1.6em\"}}}}}}},\"module\":{\"advanced\":{\"text\":{\"text\":{\"desktop\":{\"value\":{\"orientation\":\"center\"}}}}}},\"builderVersion\":\"5.8.0\"} /-->\n<!-- /wp:divi/group -->\n<!-- /wp:divi/column -->\n<!-- /wp:divi/row -->\n<!-- wp:divi/row {\"module\":{\"advanced\":{\"columnStructure\":{\"desktop\":{\"value\":\"4_4\"}}},\"decoration\":{\"spacing\":{\"desktop\":{\"value\":{\"margin\":{\"top\":\"-160px\"}}},\"tablet\":{\"value\":{\"margin\":{\"top\":\"-136px\"}}},\"phone\":{\"value\":{\"margin\":{\"top\":\"-112px\"}}}}}},\"builderVersion\":\"5.8.0\"} -->\n<!-- wp:divi/column {\"module\":{\"advanced\":{\"type\":{\"desktop\":{\"value\":\"4_4\"}}}},\"builderVersion\":\"5.8.0\"} -->\n<!-- wp:divi/group {\"module\":{\"decoration\":{\"spacing\":{\"desktop\":{\"value\":{\"padding\":{\"top\":\"48px\",\"bottom\":\"48px\",\"left\":\"48px\",\"right\":\"48px\"}}},\"tablet\":{\"value\":{\"padding\":{\"top\":\"40px\",\"bottom\":\"40px\",\"left\":\"36px\",\"right\":\"36px\"}}},\"phone\":{\"value\":{\"padding\":{\"top\":\"32px\",\"bottom\":\"32px\",\"left\":\"24px\",\"right\":\"24px\"}}}},\"layout\":{\"desktop\":{\"value\":{\"flexDirection\":\"column\",\"rowGap\":\"16px\"}}}}},\"builderVersion\":\"5.8.0\",\"aied\":{\"kit\":\"panel\",\"on\":\"light\"}} -->\n<!-- wp:divi/group {\"module\":{\"decoration\":{\"layout\":{\"desktop\":{\"value\":{\"display\":\"grid\",\"gridColumnCount\":\"3\",\"columnGap\":\"40px\",\"rowGap\":\"28px\"}},\"phone\":{\"value\":{\"gridColumnCount\":\"1\"}}}}},\"builderVersion\":\"5.8.0\"} -->\n<!-- wp:divi/group {\"module\":{\"decoration\":{\"layout\":{\"desktop\":{\"value\":{\"flexDirection\":\"column\",\"rowGap\":\"8px\"}}}}},\"builderVersion\":\"5.8.0\"} -->\n<!-- wp:divi/heading {\"title\":{\"innerContent\":{\"desktop\":{\"value\":\"[Point 1, in a few words]\"}},\"decoration\":{\"font\":{\"font\":{\"desktop\":{\"value\":{\"headingLevel\":\"h3\",\"color\":\"{{aied:color:text}}\",\"family\":\"{{aied:font:display}}\",\"size\":\"22px\",\"lineHeight\":\"1.3em\"}},\"tablet\":{\"value\":{\"size\":\"20px\"}},\"phone\":{\"value\":{\"size\":\"19px\"}}}}}},\"builderVersion\":\"5.8.0\",\"aied\":{\"type\":\"title\"}} /-->\n<!-- wp:divi/text {\"content\":{\"innerContent\":{\"desktop\":{\"value\":\"<p>[One line that explains it.]</p>\"}},\"decoration\":{\"bodyFont\":{\"body\":{\"font\":{\"desktop\":{\"value\":{\"color\":\"{{aied:color:muted}}\",\"family\":\"{{aied:font:body}}\",\"size\":\"16px\",\"lineHeight\":\"1.6em\"}}}}}}},\"builderVersion\":\"5.8.0\"} /-->\n<!-- /wp:divi/group -->\n<!-- wp:divi/group {\"module\":{\"decoration\":{\"layout\":{\"desktop\":{\"value\":{\"flexDirection\":\"column\",\"rowGap\":\"8px\"}}}}},\"builderVersion\":\"5.8.0\"} -->\n<!-- wp:divi/heading {\"title\":{\"innerContent\":{\"desktop\":{\"value\":\"[Point 2, in a few words]\"}},\"decoration\":{\"font\":{\"font\":{\"desktop\":{\"value\":{\"headingLevel\":\"h3\",\"color\":\"{{aied:color:text}}\",\"family\":\"{{aied:font:display}}\",\"size\":\"22px\",\"lineHeight\":\"1.3em\"}},\"tablet\":{\"value\":{\"size\":\"20px\"}},\"phone\":{\"value\":{\"size\":\"19px\"}}}}}},\"builderVersion\":\"5.8.0\",\"aied\":{\"type\":\"title\"}} /-->\n<!-- wp:divi/text {\"content\":{\"innerContent\":{\"desktop\":{\"value\":\"<p>[One line that explains it.]</p>\"}},\"decoration\":{\"bodyFont\":{\"body\":{\"font\":{\"desktop\":{\"value\":{\"color\":\"{{aied:color:muted}}\",\"family\":\"{{aied:font:body}}\",\"size\":\"16px\",\"lineHeight\":\"1.6em\"}}}}}}},\"builderVersion\":\"5.8.0\"} /-->\n<!-- /wp:divi/group -->\n<!-- wp:divi/group {\"module\":{\"decoration\":{\"layout\":{\"desktop\":{\"value\":{\"flexDirection\":\"column\",\"rowGap\":\"8px\"}}}}},\"builderVersion\":\"5.8.0\"} -->\n<!-- wp:divi/heading {\"title\":{\"innerContent\":{\"desktop\":{\"value\":\"[Point 3, in a few words]\"}},\"decoration\":{\"font\":{\"font\":{\"desktop\":{\"value\":{\"headingLevel\":\"h3\",\"color\":\"{{aied:color:text}}\",\"family\":\"{{aied:font:display}}\",\"size\":\"22px\",\"lineHeight\":\"1.3em\"}},\"tablet\":{\"value\":{\"size\":\"20px\"}},\"phone\":{\"value\":{\"size\":\"19px\"}}}}}},\"builderVersion\":\"5.8.0\",\"aied\":{\"type\":\"title\"}} /-->\n<!-- wp:divi/text {\"content\":{\"innerContent\":{\"desktop\":{\"value\":\"<p>[One line that explains it.]</p>\"}},\"decoration\":{\"bodyFont\":{\"body\":{\"font\":{\"desktop\":{\"value\":{\"color\":\"{{aied:color:muted}}\",\"family\":\"{{aied:font:body}}\",\"size\":\"16px\",\"lineHeight\":\"1.6em\"}}}}}}},\"builderVersion\":\"5.8.0\"} /-->\n<!-- /wp:divi/group -->\n<!-- /wp:divi/group -->\n<!-- /wp:divi/group -->\n<!-- /wp:divi/column -->\n<!-- /wp:divi/row -->\n<!-- /wp:divi/section -->"
     2426    },
     2427    {
     2428        "name": "loop-posts-cards",
     2429        "title": "Loop / the latest posts as cards",
     2430        "description": "A centred intro over a grid of cards that Divi's Loop Builder fills from the site's own newest posts: a picture, the post title, its excerpt and a Read more button, one card per post; three columns, two on tablets, one on phones.",
     2431        "when": "A journal, news or resources section that should always show the newest posts without being rewritten by hand. The cards are the site's own posts (a site with none shows Divi's \"No Results Found\"); the intro words are placeholders. One copy per page: the loop id is fixed.",
     2432        "stage": "Resources (supporting)",
     2433        "tags": {
     2434            "role": "blog",
     2435            "layout": "grid",
     2436            "mood": [
     2437                "editorial",
     2438                "corporate-trust",
     2439                "soft-saas"
     2440            ],
     2441            "density": "balanced",
     2442            "background": "solid",
     2443            "needs_images": true,
     2444            "pairs": [
     2445                [
     2446                    "text",
     2447                    "bg"
     2448                ],
     2449                [
     2450                    "muted",
     2451                    "bg"
     2452                ],
     2453                [
     2454                    "text",
     2455                    "surface"
     2456                ],
     2457                [
     2458                    "muted",
     2459                    "surface"
     2460                ],
     2461                [
     2462                    "accent-text",
     2463                    "accent"
     2464                ]
     2465            ]
     2466        },
     2467        "markup": "<!-- wp:divi/section {\"module\":{\"decoration\":{\"background\":{\"desktop\":{\"value\":{\"color\":\"{{aied:color:bg}}\"}}}}},\"aied\":{\"rhythm\":\"section\"},\"builderVersion\":\"5.8.0\"} -->\n<!-- wp:divi/row {\"module\":{\"advanced\":{\"columnStructure\":{\"desktop\":{\"value\":\"1_2,1_2\"}},\"flexColumnStructure\":{\"desktop\":{\"value\":\"equal-columns_2\"}}},\"decoration\":{\"layout\":{\"desktop\":{\"value\":{\"flexWrap\":\"nowrap\",\"columnGap\":\"24px\",\"alignItems\":\"flex-end\"}},\"phone\":{\"value\":{\"flexWrap\":\"wrap\"}}},\"spacing\":{\"desktop\":{\"value\":{\"padding\":{\"bottom\":\"24px\"}}}}}},\"builderVersion\":\"5.8.0\"} -->\n<!-- wp:divi/column {\"module\":{\"advanced\":{\"type\":{\"desktop\":{\"value\":\"1_2\"}}},\"decoration\":{\"sizing\":{\"desktop\":{\"value\":{\"flexType\":\"12_24\"}},\"phone\":{\"value\":{\"flexType\":\"24_24\"}}},\"layout\":{\"desktop\":{\"value\":{\"rowGap\":\"14px\"}}}}},\"builderVersion\":\"5.8.0\"} -->\n<!-- wp:divi/heading {\"title\":{\"innerContent\":{\"desktop\":{\"value\":\"Journal\"}},\"decoration\":{\"font\":{\"font\":{\"desktop\":{\"value\":{\"headingLevel\":\"h4\",\"color\":\"{{aied:color:muted}}\",\"family\":\"{{aied:font:eyebrow}}\",\"size\":\"13px\"}}}}}},\"aied\":{\"type\":\"eyebrow\"},\"builderVersion\":\"5.8.0\"} /-->\n<!-- wp:divi/heading {\"title\":{\"innerContent\":{\"desktop\":{\"value\":\"Latest from the journal\"}},\"decoration\":{\"font\":{\"font\":{\"desktop\":{\"value\":{\"headingLevel\":\"h2\",\"color\":\"{{aied:color:text}}\",\"family\":\"{{aied:font:display}}\",\"size\":\"44px\",\"lineHeight\":\"1.15em\",\"textAlign\":\"left\",\"textWrap\":\"balance\"}},\"tablet\":{\"value\":{\"size\":\"36px\"}},\"phone\":{\"value\":{\"size\":\"30px\"}}}}}},\"aied\":{\"type\":\"display\"},\"builderVersion\":\"5.8.0\"} /-->\n<!-- /wp:divi/column -->\n<!-- wp:divi/column {\"module\":{\"advanced\":{\"type\":{\"desktop\":{\"value\":\"1_2\"}}},\"decoration\":{\"sizing\":{\"desktop\":{\"value\":{\"flexType\":\"12_24\"}},\"phone\":{\"value\":{\"flexType\":\"24_24\"}}},\"layout\":{\"desktop\":{\"value\":{\"justifyContent\":\"flex-end\"}}}}},\"builderVersion\":\"5.8.0\"} -->\n<!-- wp:divi/text {\"content\":{\"innerContent\":{\"desktop\":{\"value\":\"<p>Placeholder: one line on what readers find here.</p>\"}},\"decoration\":{\"bodyFont\":{\"body\":{\"font\":{\"desktop\":{\"value\":{\"color\":\"{{aied:color:muted}}\",\"family\":\"{{aied:font:body}}\",\"size\":\"18px\",\"lineHeight\":\"1.6em\"}}}}}}},\"builderVersion\":\"5.8.0\"} /-->\n<!-- wp:divi/button {\"button\":{\"innerContent\":{\"desktop\":{\"value\":{\"text\":\"[All posts]\"}}},\"decoration\":{\"font\":{\"font\":{\"desktop\":{\"value\":{\"family\":\"{{aied:font:body}}\"}}}}}},\"aied\":{\"kit\":\"control-secondary\"},\"builderVersion\":\"5.8.0\"} /-->\n<!-- /wp:divi/column -->\n<!-- /wp:divi/row -->\n<!-- wp:divi/row {\"module\":{\"advanced\":{\"columnStructure\":{\"desktop\":{\"value\":\"4_4\"}},\"flexColumnStructure\":{\"desktop\":{\"value\":\"equal-columns_1\"}}},\"decoration\":{\"spacing\":{\"desktop\":{\"value\":{\"padding\":{\"top\":\"24px\"}}}}}},\"builderVersion\":\"5.8.0\"} -->\n<!-- wp:divi/column {\"module\":{\"advanced\":{\"type\":{\"desktop\":{\"value\":\"4_4\"}}}},\"builderVersion\":\"5.8.0\"} -->\n<!-- wp:divi/group {\"module\":{\"decoration\":{\"layout\":{\"desktop\":{\"value\":{\"display\":\"grid\",\"gridColumnCount\":\"3\",\"columnGap\":\"24px\",\"rowGap\":\"24px\"}},\"tablet\":{\"value\":{\"gridColumnCount\":\"2\"}},\"phone\":{\"value\":{\"gridColumnCount\":\"1\"}}}}},\"builderVersion\":\"5.8.0\"} -->\n<!-- wp:divi/group {\"module\":{\"advanced\":{\"loop\":{\"desktop\":{\"value\":{\"enable\":\"on\",\"loopId\":\"loop-p0stcards1\",\"queryType\":\"post_types\",\"subTypes\":[{\"value\":\"post\",\"label\":\"Posts\"}],\"orderBy\":\"date\",\"order\":\"descending\",\"postPerPage\":\"3\"}}}},\"decoration\":{\"layout\":{\"desktop\":{\"value\":{\"flexDirection\":\"column\",\"rowGap\":\"14px\"}}}}},\"aied\":{\"kit\":\"card\",\"on\":\"light\"},\"builderVersion\":\"5.8.0\"} -->\n<!-- wp:divi/image {\"module\":{\"advanced\":{\"sizing\":{\"desktop\":{\"value\":{\"width\":\"100%\",\"aspectRatio\":{\"width\":\"16\",\"height\":\"9\"}}}}}},\"image\":{\"innerContent\":{\"desktop\":{\"value\":{\"src\":\"$variable({\\u0022type\\u0022:\\u0022content\\u0022,\\u0022value\\u0022:{\\u0022name\\u0022:\\u0022loop_post_featured_image\\u0022,\\u0022settings\\u0022:{}}})$\",\"alt\":\"$variable({\\u0022type\\u0022:\\u0022content\\u0022,\\u0022value\\u0022:{\\u0022name\\u0022:\\u0022loop_post_title\\u0022,\\u0022settings\\u0022:{}}})$\"}}},\"decoration\":{\"border\":{\"desktop\":{\"value\":{\"radius\":\"{{aied:kit:radius-media}}\"}}},\"fit\":{\"desktop\":{\"value\":{\"objectFit\":\"cover\"}}}}},\"builderVersion\":\"5.8.0\"} /-->\n<!-- wp:divi/heading {\"title\":{\"innerContent\":{\"desktop\":{\"value\":\"$variable({\\u0022type\\u0022:\\u0022content\\u0022,\\u0022value\\u0022:{\\u0022name\\u0022:\\u0022loop_post_title\\u0022,\\u0022settings\\u0022:{}}})$\"}},\"decoration\":{\"font\":{\"font\":{\"desktop\":{\"value\":{\"headingLevel\":\"h3\",\"color\":\"{{aied:color:text}}\",\"family\":\"{{aied:font:display}}\",\"size\":\"22px\",\"lineHeight\":\"1.25em\"}}}}}},\"aied\":{\"type\":\"title\"},\"builderVersion\":\"5.8.0\"} /-->\n<!-- wp:divi/text {\"content\":{\"innerContent\":{\"desktop\":{\"value\":\"$variable({\\u0022type\\u0022:\\u0022content\\u0022,\\u0022value\\u0022:{\\u0022name\\u0022:\\u0022loop_post_excerpt\\u0022,\\u0022settings\\u0022:{}}})$\"}},\"decoration\":{\"bodyFont\":{\"body\":{\"font\":{\"desktop\":{\"value\":{\"color\":\"{{aied:color:muted}}\",\"family\":\"{{aied:font:body}}\",\"size\":\"16px\",\"lineHeight\":\"1.55em\"}}}}}}},\"builderVersion\":\"5.8.0\"} /-->\n<!-- wp:divi/button {\"button\":{\"innerContent\":{\"desktop\":{\"value\":{\"text\":\"Read more\",\"linkUrl\":\"$variable({\\u0022type\\u0022:\\u0022content\\u0022,\\u0022value\\u0022:{\\u0022name\\u0022:\\u0022loop_post_link\\u0022,\\u0022settings\\u0022:{}}})$\"}}},\"decoration\":{\"font\":{\"font\":{\"desktop\":{\"value\":{\"family\":\"{{aied:font:body}}\"}}}}}},\"aied\":{\"kit\":\"control\"},\"builderVersion\":\"5.8.0\"} /-->\n<!-- /wp:divi/group -->\n<!-- /wp:divi/group -->\n<!-- /wp:divi/column -->\n<!-- /wp:divi/row -->\n<!-- /wp:divi/section -->"
     2468    },
     2469    {
     2470        "name": "loop-products-grid",
     2471        "title": "Loop / the newest products as cards",
     2472        "description": "A centred intro over a grid of product cards that Divi's Loop Builder fills from the shop's own newest products: a picture, the product name, its current price and a View product button, one card per product; four columns, two on tablets, one on phones.",
     2473        "when": "A shop landing page or a home page that should always show the newest WooCommerce products. It needs WooCommerce active and published products (otherwise Divi prints \"No Results Found\"); the intro words are placeholders. One copy per page: the loop id is fixed.",
     2474        "stage": "Product / catalog",
     2475        "tags": {
     2476            "role": "shop",
     2477            "layout": "grid",
     2478            "mood": [
     2479                "playful-pastel",
     2480                "bold-vibrant",
     2481                "soft-saas"
     2482            ],
     2483            "density": "balanced",
     2484            "background": "solid",
     2485            "needs_images": true,
     2486            "pairs": [
     2487                [
     2488                    "text",
     2489                    "bg"
     2490                ],
     2491                [
     2492                    "muted",
     2493                    "bg"
     2494                ],
     2495                [
     2496                    "text",
     2497                    "surface"
     2498                ],
     2499                [
     2500                    "muted",
     2501                    "surface"
     2502                ],
     2503                [
     2504                    "accent-text",
     2505                    "accent"
     2506                ]
     2507            ]
     2508        },
     2509        "markup": "<!-- wp:divi/section {\"module\":{\"decoration\":{\"background\":{\"desktop\":{\"value\":{\"color\":\"{{aied:color:bg}}\"}}}}},\"aied\":{\"rhythm\":\"section\"},\"builderVersion\":\"5.8.0\"} -->\n<!-- wp:divi/row {\"module\":{\"advanced\":{\"columnStructure\":{\"desktop\":{\"value\":\"4_4\"}},\"flexColumnStructure\":{\"desktop\":{\"value\":\"equal-columns_1\"}}},\"decoration\":{\"spacing\":{\"desktop\":{\"value\":{\"padding\":{\"bottom\":\"24px\"}}}}}},\"builderVersion\":\"5.8.0\"} -->\n<!-- wp:divi/column {\"module\":{\"advanced\":{\"type\":{\"desktop\":{\"value\":\"4_4\"}}},\"decoration\":{\"layout\":{\"desktop\":{\"value\":{\"rowGap\":\"14px\",\"alignItems\":\"center\"}}}}},\"builderVersion\":\"5.8.0\"} -->\n<!-- wp:divi/heading {\"title\":{\"innerContent\":{\"desktop\":{\"value\":\"Fresh from the shop\"}},\"decoration\":{\"font\":{\"font\":{\"desktop\":{\"value\":{\"headingLevel\":\"h2\",\"color\":\"{{aied:color:text}}\",\"family\":\"{{aied:font:display}}\",\"size\":\"44px\",\"lineHeight\":\"1.15em\",\"textAlign\":\"center\",\"textWrap\":\"balance\"}},\"tablet\":{\"value\":{\"size\":\"36px\"}},\"phone\":{\"value\":{\"size\":\"30px\"}}}}}},\"aied\":{\"type\":\"display\"},\"builderVersion\":\"5.8.0\"} /-->\n<!-- wp:divi/text {\"content\":{\"innerContent\":{\"desktop\":{\"value\":\"<p>Placeholder: one line on what the shop sells.</p>\"}},\"decoration\":{\"bodyFont\":{\"body\":{\"font\":{\"desktop\":{\"value\":{\"color\":\"{{aied:color:muted}}\",\"family\":\"{{aied:font:body}}\",\"size\":\"18px\",\"lineHeight\":\"1.6em\"}}}}}}},\"builderVersion\":\"5.8.0\"} /-->\n<!-- wp:divi/button {\"button\":{\"innerContent\":{\"desktop\":{\"value\":{\"text\":\"[Shop all]\"}}},\"decoration\":{\"font\":{\"font\":{\"desktop\":{\"value\":{\"family\":\"{{aied:font:body}}\"}}}}}},\"aied\":{\"kit\":\"control-secondary\"},\"builderVersion\":\"5.8.0\"} /-->\n<!-- /wp:divi/column -->\n<!-- /wp:divi/row -->\n<!-- wp:divi/row {\"module\":{\"advanced\":{\"columnStructure\":{\"desktop\":{\"value\":\"4_4\"}},\"flexColumnStructure\":{\"desktop\":{\"value\":\"equal-columns_1\"}}},\"decoration\":{\"spacing\":{\"desktop\":{\"value\":{\"padding\":{\"top\":\"24px\"}}}}}},\"builderVersion\":\"5.8.0\"} -->\n<!-- wp:divi/column {\"module\":{\"advanced\":{\"type\":{\"desktop\":{\"value\":\"4_4\"}}}},\"builderVersion\":\"5.8.0\"} -->\n<!-- wp:divi/group {\"module\":{\"decoration\":{\"layout\":{\"desktop\":{\"value\":{\"display\":\"grid\",\"gridColumnCount\":\"4\",\"columnGap\":\"24px\",\"rowGap\":\"24px\"}},\"tablet\":{\"value\":{\"gridColumnCount\":\"2\"}},\"phone\":{\"value\":{\"gridColumnCount\":\"1\"}}}}},\"builderVersion\":\"5.8.0\"} -->\n<!-- wp:divi/group {\"module\":{\"advanced\":{\"loop\":{\"desktop\":{\"value\":{\"enable\":\"on\",\"loopId\":\"loop-pr0ducts01\",\"queryType\":\"post_types\",\"subTypes\":[{\"value\":\"product\",\"label\":\"Products\"}],\"orderBy\":\"date\",\"order\":\"descending\",\"postPerPage\":\"4\"}}}},\"decoration\":{\"layout\":{\"desktop\":{\"value\":{\"flexDirection\":\"column\",\"rowGap\":\"14px\"}}}}},\"aied\":{\"kit\":\"card\",\"on\":\"light\"},\"builderVersion\":\"5.8.0\"} -->\n<!-- wp:divi/image {\"module\":{\"advanced\":{\"sizing\":{\"desktop\":{\"value\":{\"width\":\"100%\",\"aspectRatio\":{\"width\":\"16\",\"height\":\"9\"}}}}}},\"image\":{\"innerContent\":{\"desktop\":{\"value\":{\"src\":\"$variable({\\u0022type\\u0022:\\u0022content\\u0022,\\u0022value\\u0022:{\\u0022name\\u0022:\\u0022loop_product_post_featured_image\\u0022,\\u0022settings\\u0022:{}}})$\",\"alt\":\"$variable({\\u0022type\\u0022:\\u0022content\\u0022,\\u0022value\\u0022:{\\u0022name\\u0022:\\u0022loop_product_title\\u0022,\\u0022settings\\u0022:{}}})$\"}}},\"decoration\":{\"border\":{\"desktop\":{\"value\":{\"radius\":\"{{aied:kit:radius-media}}\"}}},\"fit\":{\"desktop\":{\"value\":{\"objectFit\":\"cover\"}}}}},\"builderVersion\":\"5.8.0\"} /-->\n<!-- wp:divi/heading {\"title\":{\"innerContent\":{\"desktop\":{\"value\":\"$variable({\\u0022type\\u0022:\\u0022content\\u0022,\\u0022value\\u0022:{\\u0022name\\u0022:\\u0022loop_product_title\\u0022,\\u0022settings\\u0022:{}}})$\"}},\"decoration\":{\"font\":{\"font\":{\"desktop\":{\"value\":{\"headingLevel\":\"h3\",\"color\":\"{{aied:color:text}}\",\"family\":\"{{aied:font:display}}\",\"size\":\"20px\",\"lineHeight\":\"1.25em\"}}}}}},\"aied\":{\"type\":\"title\"},\"builderVersion\":\"5.8.0\"} /-->\n<!-- wp:divi/text {\"content\":{\"innerContent\":{\"desktop\":{\"value\":\"$variable({\\u0022type\\u0022:\\u0022content\\u0022,\\u0022value\\u0022:{\\u0022name\\u0022:\\u0022loop_product_price_current\\u0022,\\u0022settings\\u0022:{}}})$\"}},\"decoration\":{\"bodyFont\":{\"body\":{\"font\":{\"desktop\":{\"value\":{\"color\":\"{{aied:color:text}}\",\"family\":\"{{aied:font:body}}\",\"size\":\"18px\",\"lineHeight\":\"1.4em\",\"weight\":\"700\"}}}}}}},\"builderVersion\":\"5.8.0\"} /-->\n<!-- wp:divi/button {\"button\":{\"innerContent\":{\"desktop\":{\"value\":{\"text\":\"View product\",\"linkUrl\":\"$variable({\\u0022type\\u0022:\\u0022content\\u0022,\\u0022value\\u0022:{\\u0022name\\u0022:\\u0022loop_product_post_link_url\\u0022,\\u0022settings\\u0022:{}}})$\"}}},\"decoration\":{\"font\":{\"font\":{\"desktop\":{\"value\":{\"family\":\"{{aied:font:body}}\"}}}}}},\"aied\":{\"kit\":\"control\"},\"builderVersion\":\"5.8.0\"} /-->\n<!-- /wp:divi/group -->\n<!-- /wp:divi/group -->\n<!-- /wp:divi/column -->\n<!-- /wp:divi/row -->\n<!-- /wp:divi/section -->"
     2510    },
     2511    {
     2512        "name": "loop-posts-paged",
     2513        "title": "Loop / all posts, six per page with Next and Previous",
     2514        "description": "A centred intro over a three-column grid of post cards that Divi's Loop Builder fills from the site's own posts, six per page, with Divi's Post Navigation underneath: Next and Previous links that move this list through the pages (the address gets ?loop-p0stpaged1=2).",
     2515        "when": "A blog or news archive page that lists every post in pages, newest first. The cards are the site's own posts and the navigation shows links only when there is more than one page; the intro words are placeholders. One copy per page: the loop id is fixed, and the navigation is aimed at it.",
     2516        "stage": "Resources (supporting)",
     2517        "tags": {
     2518            "role": "blog",
     2519            "layout": "grid",
     2520            "mood": [
     2521                "editorial",
     2522                "corporate-trust",
     2523                "soft-saas"
     2524            ],
     2525            "density": "balanced",
     2526            "background": "solid",
     2527            "needs_images": true,
     2528            "pairs": [
     2529                [
     2530                    "text",
     2531                    "bg"
     2532                ],
     2533                [
     2534                    "muted",
     2535                    "bg"
     2536                ],
     2537                [
     2538                    "text",
     2539                    "surface"
     2540                ],
     2541                [
     2542                    "muted",
     2543                    "surface"
     2544                ],
     2545                [
     2546                    "accent-text",
     2547                    "accent"
     2548                ]
     2549            ]
     2550        },
     2551        "markup": "<!-- wp:divi/section {\"module\":{\"decoration\":{\"background\":{\"desktop\":{\"value\":{\"color\":\"{{aied:color:bg}}\"}}}}},\"aied\":{\"rhythm\":\"section\"},\"builderVersion\":\"5.8.0\"} -->\n<!-- wp:divi/row {\"module\":{\"advanced\":{\"columnStructure\":{\"desktop\":{\"value\":\"4_4\"}},\"flexColumnStructure\":{\"desktop\":{\"value\":\"equal-columns_1\"}}},\"decoration\":{\"spacing\":{\"desktop\":{\"value\":{\"padding\":{\"bottom\":\"24px\"}}}}}},\"builderVersion\":\"5.8.0\"} -->\n<!-- wp:divi/column {\"module\":{\"advanced\":{\"type\":{\"desktop\":{\"value\":\"4_4\"}}},\"decoration\":{\"layout\":{\"desktop\":{\"value\":{\"rowGap\":\"14px\",\"alignItems\":\"center\"}}}}},\"builderVersion\":\"5.8.0\"} -->\n<!-- wp:divi/heading {\"title\":{\"innerContent\":{\"desktop\":{\"value\":\"All posts\"}},\"decoration\":{\"font\":{\"font\":{\"desktop\":{\"value\":{\"headingLevel\":\"h4\",\"color\":\"{{aied:color:muted}}\",\"family\":\"{{aied:font:eyebrow}}\",\"size\":\"13px\"}}}}}},\"aied\":{\"type\":\"eyebrow\"},\"builderVersion\":\"5.8.0\"} /-->\n<!-- wp:divi/heading {\"title\":{\"innerContent\":{\"desktop\":{\"value\":\"Everything we have written\"}},\"decoration\":{\"font\":{\"font\":{\"desktop\":{\"value\":{\"headingLevel\":\"h2\",\"color\":\"{{aied:color:text}}\",\"family\":\"{{aied:font:display}}\",\"size\":\"44px\",\"lineHeight\":\"1.15em\",\"textAlign\":\"center\",\"textWrap\":\"balance\"}},\"tablet\":{\"value\":{\"size\":\"36px\"}},\"phone\":{\"value\":{\"size\":\"30px\"}}}}}},\"aied\":{\"type\":\"display\"},\"builderVersion\":\"5.8.0\"} /-->\n<!-- /wp:divi/column -->\n<!-- /wp:divi/row -->\n<!-- wp:divi/row {\"module\":{\"advanced\":{\"columnStructure\":{\"desktop\":{\"value\":\"4_4\"}},\"flexColumnStructure\":{\"desktop\":{\"value\":\"equal-columns_1\"}}},\"decoration\":{\"spacing\":{\"desktop\":{\"value\":{\"padding\":{\"top\":\"24px\"}}}}}},\"builderVersion\":\"5.8.0\"} -->\n<!-- wp:divi/column {\"module\":{\"advanced\":{\"type\":{\"desktop\":{\"value\":\"4_4\"}}}},\"builderVersion\":\"5.8.0\"} -->\n<!-- wp:divi/group {\"module\":{\"decoration\":{\"layout\":{\"desktop\":{\"value\":{\"display\":\"grid\",\"gridColumnCount\":\"3\",\"columnGap\":\"24px\",\"rowGap\":\"24px\"}},\"tablet\":{\"value\":{\"gridColumnCount\":\"2\"}},\"phone\":{\"value\":{\"gridColumnCount\":\"1\"}}}}},\"builderVersion\":\"5.8.0\"} -->\n<!-- wp:divi/group {\"module\":{\"advanced\":{\"loop\":{\"desktop\":{\"value\":{\"enable\":\"on\",\"loopId\":\"loop-p0stpaged1\",\"queryType\":\"post_types\",\"subTypes\":[{\"value\":\"post\",\"label\":\"Posts\"}],\"orderBy\":\"date\",\"order\":\"descending\",\"postPerPage\":\"6\"}}}},\"decoration\":{\"layout\":{\"desktop\":{\"value\":{\"flexDirection\":\"column\",\"rowGap\":\"14px\"}}}}},\"aied\":{\"kit\":\"card\",\"on\":\"light\"},\"builderVersion\":\"5.8.0\"} -->\n<!-- wp:divi/image {\"module\":{\"advanced\":{\"sizing\":{\"desktop\":{\"value\":{\"width\":\"100%\",\"aspectRatio\":{\"width\":\"16\",\"height\":\"9\"}}}}}},\"image\":{\"innerContent\":{\"desktop\":{\"value\":{\"src\":\"$variable({\\u0022type\\u0022:\\u0022content\\u0022,\\u0022value\\u0022:{\\u0022name\\u0022:\\u0022loop_post_featured_image\\u0022,\\u0022settings\\u0022:{}}})$\",\"alt\":\"$variable({\\u0022type\\u0022:\\u0022content\\u0022,\\u0022value\\u0022:{\\u0022name\\u0022:\\u0022loop_post_title\\u0022,\\u0022settings\\u0022:{}}})$\"}}},\"decoration\":{\"border\":{\"desktop\":{\"value\":{\"radius\":\"{{aied:kit:radius-media}}\"}}},\"fit\":{\"desktop\":{\"value\":{\"objectFit\":\"cover\"}}}}},\"builderVersion\":\"5.8.0\"} /-->\n<!-- wp:divi/heading {\"title\":{\"innerContent\":{\"desktop\":{\"value\":\"$variable({\\u0022type\\u0022:\\u0022content\\u0022,\\u0022value\\u0022:{\\u0022name\\u0022:\\u0022loop_post_title\\u0022,\\u0022settings\\u0022:{}}})$\"}},\"decoration\":{\"font\":{\"font\":{\"desktop\":{\"value\":{\"headingLevel\":\"h3\",\"color\":\"{{aied:color:text}}\",\"family\":\"{{aied:font:display}}\",\"size\":\"22px\",\"lineHeight\":\"1.25em\"}}}}}},\"aied\":{\"type\":\"title\"},\"builderVersion\":\"5.8.0\"} /-->\n<!-- wp:divi/text {\"content\":{\"innerContent\":{\"desktop\":{\"value\":\"$variable({\\u0022type\\u0022:\\u0022content\\u0022,\\u0022value\\u0022:{\\u0022name\\u0022:\\u0022loop_post_excerpt\\u0022,\\u0022settings\\u0022:{}}})$\"}},\"decoration\":{\"bodyFont\":{\"body\":{\"font\":{\"desktop\":{\"value\":{\"color\":\"{{aied:color:muted}}\",\"family\":\"{{aied:font:body}}\",\"size\":\"16px\",\"lineHeight\":\"1.55em\"}}}}}}},\"builderVersion\":\"5.8.0\"} /-->\n<!-- wp:divi/button {\"button\":{\"innerContent\":{\"desktop\":{\"value\":{\"text\":\"Read more\",\"linkUrl\":\"$variable({\\u0022type\\u0022:\\u0022content\\u0022,\\u0022value\\u0022:{\\u0022name\\u0022:\\u0022loop_post_link\\u0022,\\u0022settings\\u0022:{}}})$\"}}},\"decoration\":{\"font\":{\"font\":{\"desktop\":{\"value\":{\"family\":\"{{aied:font:body}}\"}}}}}},\"aied\":{\"kit\":\"control\"},\"builderVersion\":\"5.8.0\"} /-->\n<!-- /wp:divi/group -->\n<!-- /wp:divi/group -->\n<!-- /wp:divi/column -->\n<!-- /wp:divi/row -->\n<!-- wp:divi/row {\"module\":{\"advanced\":{\"columnStructure\":{\"desktop\":{\"value\":\"4_4\"}},\"flexColumnStructure\":{\"desktop\":{\"value\":\"equal-columns_1\"}}},\"decoration\":{\"spacing\":{\"desktop\":{\"value\":{\"padding\":{\"top\":\"8px\"}}}}}},\"builderVersion\":\"5.8.0\"} -->\n<!-- wp:divi/column {\"module\":{\"advanced\":{\"type\":{\"desktop\":{\"value\":\"4_4\"}}}},\"builderVersion\":\"5.8.0\"} -->\n<!-- wp:divi/post-nav {\"module\":{\"advanced\":{\"targetLoop\":{\"desktop\":{\"value\":\"loop-p0stpaged1\"}}}},\"builderVersion\":\"5.8.0\"} /-->\n<!-- /wp:divi/column -->\n<!-- /wp:divi/row -->\n<!-- /wp:divi/section -->"
     2552    },
     2553    {
     2554        "name": "loop-categories-grid",
     2555        "title": "Loop / the site's categories as cards",
     2556        "description": "An intro column beside a grid of category cards that Divi's Loop Builder fills from the site's own categories, most used first: the category name, how many articles it holds and a Browse button to its archive; two columns beside the intro, one on phones.",
     2557        "when": "A blog home or resources page that should always list the site's categories without being rewritten by hand. Divi lists only categories that have posts; the intro words are placeholders. One copy per page: the loop id is fixed.",
     2558        "stage": "Resources (supporting)",
     2559        "tags": {
     2560            "role": "blog",
     2561            "layout": "grid",
     2562            "mood": [
     2563                "editorial",
     2564                "corporate-trust",
     2565                "soft-saas"
     2566            ],
     2567            "density": "balanced",
     2568            "background": "solid",
     2569            "needs_images": false,
     2570            "pairs": [
     2571                [
     2572                    "text",
     2573                    "bg"
     2574                ],
     2575                [
     2576                    "muted",
     2577                    "bg"
     2578                ],
     2579                [
     2580                    "text",
     2581                    "surface"
     2582                ],
     2583                [
     2584                    "muted",
     2585                    "surface"
     2586                ],
     2587                [
     2588                    "accent-text",
     2589                    "accent"
     2590                ]
     2591            ]
     2592        },
     2593        "markup": "<!-- wp:divi/section {\"module\":{\"decoration\":{\"background\":{\"desktop\":{\"value\":{\"color\":\"{{aied:color:bg}}\"}}}}},\"aied\":{\"rhythm\":\"section\"},\"builderVersion\":\"5.8.0\"} -->\n<!-- wp:divi/row {\"module\":{\"advanced\":{\"columnStructure\":{\"desktop\":{\"value\":\"1_3,2_3\"}},\"flexColumnStructure\":{\"desktop\":{\"value\":\"equal-columns_2\"}}},\"decoration\":{\"layout\":{\"desktop\":{\"value\":{\"flexWrap\":\"nowrap\",\"columnGap\":\"48px\",\"alignItems\":\"flex-start\"}},\"phone\":{\"value\":{\"flexWrap\":\"wrap\"}}}}},\"builderVersion\":\"5.8.0\"} -->\n<!-- wp:divi/column {\"module\":{\"advanced\":{\"type\":{\"desktop\":{\"value\":\"1_3\"}}},\"decoration\":{\"sizing\":{\"desktop\":{\"value\":{\"flexType\":\"8_24\"}},\"phone\":{\"value\":{\"flexType\":\"24_24\"}}},\"layout\":{\"desktop\":{\"value\":{\"rowGap\":\"14px\"}}}}},\"builderVersion\":\"5.8.0\"} -->\n<!-- wp:divi/heading {\"title\":{\"innerContent\":{\"desktop\":{\"value\":\"Topics\"}},\"decoration\":{\"font\":{\"font\":{\"desktop\":{\"value\":{\"headingLevel\":\"h4\",\"color\":\"{{aied:color:muted}}\",\"family\":\"{{aied:font:eyebrow}}\",\"size\":\"13px\"}}}}}},\"aied\":{\"type\":\"eyebrow\"},\"builderVersion\":\"5.8.0\"} /-->\n<!-- wp:divi/heading {\"title\":{\"innerContent\":{\"desktop\":{\"value\":\"Browse by topic\"}},\"decoration\":{\"font\":{\"font\":{\"desktop\":{\"value\":{\"headingLevel\":\"h2\",\"color\":\"{{aied:color:text}}\",\"family\":\"{{aied:font:display}}\",\"size\":\"44px\",\"lineHeight\":\"1.15em\",\"textAlign\":\"left\",\"textWrap\":\"balance\"}},\"tablet\":{\"value\":{\"size\":\"36px\"}},\"phone\":{\"value\":{\"size\":\"30px\"}}}}}},\"aied\":{\"type\":\"display\"},\"builderVersion\":\"5.8.0\"} /-->\n<!-- wp:divi/text {\"content\":{\"innerContent\":{\"desktop\":{\"value\":\"<p>Placeholder: one line on how the topics are organised.</p>\"}},\"decoration\":{\"bodyFont\":{\"body\":{\"font\":{\"desktop\":{\"value\":{\"color\":\"{{aied:color:muted}}\",\"family\":\"{{aied:font:body}}\",\"size\":\"18px\",\"lineHeight\":\"1.6em\"}}}}}}},\"builderVersion\":\"5.8.0\"} /-->\n<!-- wp:divi/button {\"button\":{\"innerContent\":{\"desktop\":{\"value\":{\"text\":\"[All topics]\"}}},\"decoration\":{\"font\":{\"font\":{\"desktop\":{\"value\":{\"family\":\"{{aied:font:body}}\"}}}}}},\"aied\":{\"kit\":\"control-secondary\"},\"builderVersion\":\"5.8.0\"} /-->\n<!-- /wp:divi/column -->\n<!-- wp:divi/column {\"module\":{\"advanced\":{\"type\":{\"desktop\":{\"value\":\"2_3\"}}},\"decoration\":{\"sizing\":{\"desktop\":{\"value\":{\"flexType\":\"16_24\"}},\"phone\":{\"value\":{\"flexType\":\"24_24\"}}}}},\"builderVersion\":\"5.8.0\"} -->\n<!-- wp:divi/group {\"module\":{\"decoration\":{\"layout\":{\"desktop\":{\"value\":{\"display\":\"grid\",\"gridColumnCount\":\"2\",\"columnGap\":\"24px\",\"rowGap\":\"24px\"}},\"tablet\":{\"value\":{\"gridColumnCount\":\"2\"}},\"phone\":{\"value\":{\"gridColumnCount\":\"1\"}}}}},\"builderVersion\":\"5.8.0\"} -->\n<!-- wp:divi/group {\"module\":{\"advanced\":{\"loop\":{\"desktop\":{\"value\":{\"enable\":\"on\",\"loopId\":\"loop-categ0ries1\",\"queryType\":\"post_taxonomies\",\"subTypes\":[{\"value\":\"category\",\"label\":\"Categories\"}],\"orderBy\":\"count\",\"order\":\"descending\",\"postPerPage\":\"8\"}}}},\"decoration\":{\"layout\":{\"desktop\":{\"value\":{\"flexDirection\":\"column\",\"rowGap\":\"14px\"}}}}},\"aied\":{\"kit\":\"card\",\"on\":\"light\"},\"builderVersion\":\"5.8.0\"} -->\n<!-- wp:divi/heading {\"title\":{\"innerContent\":{\"desktop\":{\"value\":\"$variable({\\u0022type\\u0022:\\u0022content\\u0022,\\u0022value\\u0022:{\\u0022name\\u0022:\\u0022loop_term_name\\u0022,\\u0022settings\\u0022:{}}})$\"}},\"decoration\":{\"font\":{\"font\":{\"desktop\":{\"value\":{\"headingLevel\":\"h3\",\"color\":\"{{aied:color:text}}\",\"family\":\"{{aied:font:display}}\",\"size\":\"22px\",\"lineHeight\":\"1.25em\"}}}}}},\"aied\":{\"type\":\"title\"},\"builderVersion\":\"5.8.0\"} /-->\n<!-- wp:divi/text {\"content\":{\"innerContent\":{\"desktop\":{\"value\":\"<p>$variable({\\u0022type\\u0022:\\u0022content\\u0022,\\u0022value\\u0022:{\\u0022name\\u0022:\\u0022loop_term_count\\u0022,\\u0022settings\\u0022:{}}})$ articles</p>\"}},\"decoration\":{\"bodyFont\":{\"body\":{\"font\":{\"desktop\":{\"value\":{\"color\":\"{{aied:color:muted}}\",\"family\":\"{{aied:font:body}}\",\"size\":\"16px\",\"lineHeight\":\"1.55em\"}}}}}}},\"builderVersion\":\"5.8.0\"} /-->\n<!-- wp:divi/button {\"button\":{\"innerContent\":{\"desktop\":{\"value\":{\"text\":\"Browse\",\"linkUrl\":\"$variable({\\u0022type\\u0022:\\u0022content\\u0022,\\u0022value\\u0022:{\\u0022name\\u0022:\\u0022loop_term_permalink\\u0022,\\u0022settings\\u0022:{}}})$\"}}},\"decoration\":{\"font\":{\"font\":{\"desktop\":{\"value\":{\"family\":\"{{aied:font:body}}\"}}}}}},\"aied\":{\"kit\":\"control\"},\"builderVersion\":\"5.8.0\"} /-->\n<!-- /wp:divi/group -->\n<!-- /wp:divi/group -->\n<!-- /wp:divi/column -->\n<!-- /wp:divi/row -->\n<!-- /wp:divi/section -->"
    24262594    }
    24272595]
  • jhmg-ai-editor-for-divi-5/trunk/data/whats-new.php

    r3786407 r3791018  
    1515
    1616return [
    17     'version' => '4.17',
    18     'title'   => __( 'What\'s new in 4.17', 'jhmg-ai-editor-for-divi-5' ),
     17    'version' => '4.18',
     18    'title'   => __( 'What\'s new in 4.18', 'jhmg-ai-editor-for-divi-5' ),
    1919    'bullets' => [
    20         __( 'Design languages and section recipes are now abilities any AI can call (WordPress 6.9 and later).', 'jhmg-ai-editor-for-divi-5' ),
    21         __( 'check-design: an advisory look at readability, Divi\'s stock look and repeated sections. It never blocks a save.', 'jhmg-ai-editor-for-divi-5' ),
    22         __( 'Undo for Divi page changes from any source, with the source shown in Recent AI edits.', 'jhmg-ai-editor-for-divi-5' ),
    23         __( 'Designed to work alongside Divi\'s upcoming MCP.', 'jhmg-ai-editor-for-divi-5' ),
    24         __( 'A new "Instructions for your AI" card in Settings, ready to paste into your assistant.', 'jhmg-ai-editor-for-divi-5' ),
     20        __( 'Divi\'s Loop Builder: your AI can build lists of your posts, products, categories or menu items.', 'jhmg-ai-editor-for-divi-5' ),
     21        __( 'A list can be paged with Next and Previous links.', 'jhmg-ai-editor-for-divi-5' ),
     22        __( 'Four new ready-made patterns: newest posts, all posts in pages, categories and newest products.', 'jhmg-ai-editor-for-divi-5' ),
     23        __( 'Before a page is saved, your AI sees what each loop will list on your site.', 'jhmg-ai-editor-for-divi-5' ),
     24        __( 'The checker refuses loop settings that would break the page.', 'jhmg-ai-editor-for-divi-5' ),
    2525    ],
    2626    'link'    => [
  • jhmg-ai-editor-for-divi-5/trunk/jhmg-ai-editor-for-divi-5.php

    r3786407 r3791018  
    33 * Plugin Name:       JHMG AI Editor for Divi 5
    44 * Description:       Let your AI assistant (Claude, ChatGPT, Cursor) read and edit Divi 5 pages with natural language. Every AI edit is checked before it is saved, so a broken page is never saved by an AI edit.
    5  * Version:           4.17.0
     5 * Version:           4.18.0
    66 * Requires at least: 6.0
    77 * Requires PHP:      8.1
    … …  
    2020}
    2121
    22 define('AI_EDITOR_DIVI5_VERSION', '4.17.0');
     22define('AI_EDITOR_DIVI5_VERSION', '4.18.0');
    2323define('AI_EDITOR_DIVI5_MIN_PHP', '8.1');
    2424define('AI_EDITOR_DIVI5_MIN_WP',  '6.0');
  • jhmg-ai-editor-for-divi-5/trunk/readme.txt

    r3786407 r3791018  
    44Requires at least: 6.0
    55Tested up to:      7.1
    6 Stable tag:        4.17.0
     6Stable tag:        4.18.0
    77Requires PHP:      8.1
    88License:           GPL-2.0-or-later
    … …  
    1818= Designed to work alongside Divi's upcoming MCP =
    1919
    20 The plugin is designed to work alongside Divi's upcoming MCP and any other tool that builds Divi pages. It has not been tested against Divi's MCP yet, because that has not been released. It adds design guidance before the build (10 design languages and 71 proven section patterns), a fixed-rule check after it, and undo for every change, whichever tool made it. These are offered as WordPress abilities on WordPress 6.9 and later. It never wraps, blocks or changes another tool's abilities.
     20The plugin is designed to work alongside Divi's upcoming MCP and any other tool that builds Divi pages. It has not been tested against Divi's MCP yet, because that has not been released. It adds design guidance before the build (10 design languages and 75 proven section patterns), a fixed-rule check after it, and undo for every change, whichever tool made it. These are offered as WordPress abilities on WordPress 6.9 and later. It never wraps, blocks or changes another tool's abilities.
    2121
    2222= How it works =
    … …  
    8282* **Preview before saving.** Your AI can ask what a change would do before it saves anything. It gets a list of the blocks and settings that would change, and the checker's verdict. Nothing is saved until it repeats the request for real.
    8383* **Matches your site.** Your AI can look up your site's own Divi colors and presets and reuse them, and it is told when a page uses one your site does not have. It cannot create or change presets or colors: that stays in Divi's Visual Builder.
    84 * **Proven building blocks.** 71 ready-made section patterns (heroes, feature grids, testimonials, contact forms and more), each one tested against the checker, plus landing-page guidance on the order of sections, headlines and calls to action.
     84* **Proven building blocks.** 75 ready-made section patterns (heroes, feature grids, testimonials, contact forms and more), each one tested against the checker, plus landing-page guidance on the order of sections, headlines and calls to action.
    8585* **Consistent looks.** Ten design languages (editorial, glass dark, organic warm, tech mono and more) and 40 style kits set the exact look of buttons, cards, accordions, toggles, contact forms and image corners, so a whole page matches.
    8686* **Sensible images.** The AI looks in your Media Library first. When nothing there fits, it uses a built-in pack of 44 original images that ships with the plugin.
    … …  
    104104* `get_page_layout`: read the current layout of a page or Divi Project. For a big page your assistant can read just an outline (every block with its place, size and the start of its text) or one block, which costs a fraction of reading the whole page.
    105105* `list_global_styles`: list your site's own Divi colors and presets so the AI can reuse them (read-only).
    106 * `get_site_structure`: list your site's custom post types, taxonomies, image sizes, Divi 5.15 components (name, kind, Slots, how many pages use each) and Advanced Custom Fields field groups so pages fit your site (read-only: field names and the keys your posts use, never values here or theme files; available over MCP and the REST API, not in the ChatGPT Action).
     106* `get_site_structure`: list your site's custom post types, taxonomies, image sizes, Divi 5.15 components (name, kind, Slots, how many pages use each), on request the names and ids of your site's navigation lists and of your most used categories and tags (so a list on a page can name them), and Advanced Custom Fields field groups so pages fit your site (read-only: field names and the keys your posts use, never values here or theme files; available over MCP and the REST API, not in the ChatGPT Action).
    107107* `get_custom_fields`: read the custom fields of one page or Divi Project you can edit, with their values, so your assistant can write copy that fits and use the right field names (read-only: keys WordPress protects, or that look like passwords, API keys or other secrets, are never shown; available over MCP and the REST API, not in the ChatGPT Action).
    108108* `list_products`: list and count your WooCommerce products with their prices, stock status and categories (read-only; needs the permission to edit products; available over MCP and the REST API, not in the ChatGPT Action).
    … …  
    126126* `get_landing_guide`: how to build a landing page that turns visitors into customers.
    127127* `get_site_guide`: how to plan a site with several pages that link to each other.
    128 * `get_section_recipes`: the 71 ready-made section patterns.
     128* `get_section_recipes`: the 75 ready-made section patterns.
    129129* `get_image_guide`: how to pick the right image for each section, including the built-in image pack.
    130130
    … …  
    220220= Can it read my ACF fields, custom post types or functions.php? =
    221221
    222 It can read how your site is set up: custom post types, taxonomies, image sizes and, when Advanced Custom Fields is active, its field groups and fields (names, labels, types and choices). It reads custom field values only when your assistant asks about one specific page or Project you can edit, never ones WordPress protects or that look like passwords, API keys or other secrets, and it never reads your theme files or `functions.php`. This is available to assistants that connect over MCP or the REST API, not to the ChatGPT Action.
     222It can read how your site is set up: custom post types, taxonomies, image sizes, on request the names and ids of your site's navigation lists (never their links) and of your most used categories, tags and other public taxonomy terms, and, when Advanced Custom Fields is active, its field groups and fields (names, labels, types and choices). It reads custom field values only when your assistant asks about one specific page or Project you can edit, never ones WordPress protects or that look like passwords, API keys or other secrets, and it never reads your theme files or `functions.php`. This is available to assistants that connect over MCP or the REST API, not to the ChatGPT Action.
    223223
    224224= Can it create global colors and presets? =
    … …  
    254254= Do I need this if I use Divi's MCP? =
    255255
    256 Divi has announced its own MCP, which is not released yet, so this plugin has not been tested against it. A builder builds pages. This plugin is designed to add what a builder does not: ten design languages and 71 proven section patterns to design from, a deterministic checker that refuses a broken layout with the exact reason, an advisory design check (readability, Divi's stock look, repeated sections), and undo for every change, even ones made by other tools. The idea is to use both: ask your assistant to read this plugin's design guidance before it builds, build with any tool, then run the design check and fix what it finds.
     256Divi has announced its own MCP, which is not released yet, so this plugin has not been tested against it. A builder builds pages. This plugin is designed to add what a builder does not: ten design languages and 75 proven section patterns to design from, a deterministic checker that refuses a broken layout with the exact reason, an advisory design check (readability, Divi's stock look, repeated sections), and undo for every change, even ones made by other tools. The idea is to use both: ask your assistant to read this plugin's design guidance before it builds, build with any tool, then run the design check and fix what it finds.
    257257
    258258= Is this an MCP server for Divi? =
    … …  
    298298
    299299== Changelog ==
     300
     301= 4.18.0 =
     302* New: Divi's Loop Builder. Your AI can build lists that repeat once per post, product, category, user or navigation item, and page through them with Next and Previous links. The style guide has a new `loops` topic, and there are four ready-made patterns: the newest posts, all posts in pages, the site's categories and the newest products.
     303* New: the checker refuses the loop settings that make Divi fail the whole page (HTTP 500) or quietly list the wrong things, and says exactly which setting to fix.
     304* New: when your AI saves, checks or previews a page with a loop, it also gets a short report of what each loop will show on your site (how many items and the first titles). It flags a post type, category, term ID or navigation list your site does not have, and mistakes such as a post token in a navigation loop or two loops sharing one ID. Advice only: it never blocks a save.
     305* New: `get_site_structure` can also list the names and IDs of your site's navigation lists and of your most used categories and tags (ask for them with `include`). Read-only.
     306* Improved: four new loop patterns join the ready-made library.
     307* Improved: when a dynamic-content token is written with plain quotes inside a block, the refusal now says how to write it.
     308* Tested on Divi 5.15.1. Not covered yet: ACF repeater loops with a real repeater field, loops inside site-wide templates, and Divi's Post Filter module.
    300309
    301310= 4.17.0 =
  • jhmg-ai-editor-for-divi-5/trunk/src/SiteStructure.php

    r3750826 r3791018  
    1515 * Components (name, kind, Slot ids and how many pages use each; never their content). Everything comes from WordPress's and ACF's own public read functions, each behind a
    1616 * guard: a missing or throwing reader means an empty section plus a note. Nothing here writes anything,
    17  * and nothing reads theme files or menus.
     17 * and nothing reads theme files. The opt-in `menus` section lists menu names and ids, never their items.
    1818 */
    1919final class SiteStructure
    2020{
    21     public const SECTIONS    = [ 'post_types', 'taxonomies', 'image_sizes', 'acf', 'custom_fields', 'components' ];
     21    public const SECTIONS    = [ 'post_types', 'taxonomies', 'image_sizes', 'acf', 'custom_fields', 'components', 'menus', 'terms' ];
     22    /** What a call without `include` returns; `menus` and `terms` can be long, so they are asked for by name. */
     23    public const DEFAULT_SECTIONS = [ 'post_types', 'taxonomies', 'image_sizes', 'acf', 'custom_fields', 'components' ];
     24    public const MAX_TERMS_PER_TAXONOMY = 50;
     25    public const MAX_TAXONOMIES_WITH_TERMS = 20;
    2226    public const MAX_ITEMS   = 100;
    2327    public const MAX_FIELDS  = 100;
    … …  
    3842    {
    3943        if ( null === $value || '' === $value || [] === $value ) {
    40             return self::SECTIONS;
     44            return self::DEFAULT_SECTIONS;
    4145        }
    4246        if ( is_string( $value ) ) {
    … …  
    6872            'custom_fields' => class_exists( CustomFieldReader::class ) ? [ CustomFieldReader::class, 'discover' ] : null,
    6973            'components'  => class_exists( ComponentReader::class ) ? [ ComponentReader::class, 'discover' ] : null,
     74            'menus'       => function_exists( 'wp_get_nav_menus' ) ? [ self::class, 'readMenus' ] : null,
     75            'terms'       => function_exists( 'get_terms' ) && function_exists( 'get_taxonomies' ) ? [ self::class, 'readTerms' ] : null,
    7076        ];
     77    }
     78
     79    /** @return list<array{id:string,name:string,slug:string,items:int,locations:list<string>}> the site's navigation menus (names and ids only, never their items) */
     80    public static function readMenus(): array
     81    {
     82        $locations = function_exists( 'get_nav_menu_locations' ) ? (array) get_nav_menu_locations() : [];
     83        $out       = [];
     84        foreach ( (array) wp_get_nav_menus() as $menu ) {
     85            if ( ! is_object( $menu ) ) {
     86                continue;
     87            }
     88            $id    = (int) ( $menu->term_id ?? 0 );
     89            $where = [];
     90            foreach ( $locations as $slug => $menuId ) {
     91                if ( (int) $menuId === $id ) {
     92                    $where[] = (string) $slug;
     93                }
     94            }
     95            $out[] = [ 'id' => (string) $id, 'name' => (string) ( $menu->name ?? '' ), 'slug' => (string) ( $menu->slug ?? '' ), 'items' => (int) ( $menu->count ?? 0 ), 'locations' => $where ];
     96        }
     97
     98        return $out;
     99    }
     100
     101    /** @return array<string,list<array{id:string,name:string,slug:string,count:int,parent:string}>> taxonomy => its most used terms */
     102    public static function readTerms(): array
     103    {
     104        $out = [];
     105        foreach ( array_keys( (array) get_taxonomies( [ 'public' => true ] ) ) as $taxonomy ) {
     106            if ( count( $out ) >= self::MAX_TAXONOMIES_WITH_TERMS ) {
     107                break;
     108            }
     109            $terms = get_terms( [ 'taxonomy' => (string) $taxonomy, 'hide_empty' => false, 'number' => self::MAX_TERMS_PER_TAXONOMY + 1, 'orderby' => 'count', 'order' => 'DESC' ] );
     110            if ( ! is_array( $terms ) || [] === $terms ) {
     111                continue;
     112            }
     113            $rows = [];
     114            foreach ( $terms as $term ) {
     115                if ( is_object( $term ) ) {
     116                    $rows[] = [ 'id' => (string) ( $term->term_id ?? 0 ), 'name' => (string) ( $term->name ?? '' ), 'slug' => (string) ( $term->slug ?? '' ), 'count' => (int) ( $term->count ?? 0 ), 'parent' => (string) ( $term->parent ?? 0 ) ];
     117                }
     118            }
     119            $out[ (string) $taxonomy ] = $rows;
     120        }
     121
     122        return $out;
    71123    }
    72124
    … …  
    121173    public static function catalogWith( array $readers, ?array $include = null ): array
    122174    {
    123         $include   = $include ?? self::SECTIONS;
     175        $include   = $include ?? self::DEFAULT_SECTIONS;
    124176        $notes     = [];
    125177        $truncated = false;
    … …  
    197249        }
    198250
     251        if ( in_array( 'menus', $include, true ) ) {
     252            $raw = self::read( $readers['menus'] ?? null );
     253            if ( null === $raw ) {
     254                $notes[] = 'Menu data is not available on this site.';
     255            }
     256            $out['menus'] = [
     257                'items' => self::menuRows( $raw ?? [], $truncated ),
     258                'note'  => 'Navigation menus: id (a string, what a menu loop takes as its subTypes value), name, slug, how many items and the theme locations that show it. The items themselves are not listed.',
     259            ];
     260        }
     261
     262        if ( in_array( 'terms', $include, true ) ) {
     263            $raw = self::read( $readers['terms'] ?? null );
     264            if ( null === $raw ) {
     265                $notes[] = 'Term data is not available on this site.';
     266            }
     267            $out['terms'] = [
     268                'taxonomies' => self::termRows( $raw ?? [], $truncated ),
     269                'note'       => 'The most used terms of each public taxonomy: id (a string, what a loop filter takes as its selected value, not the slug), name, slug, how many items use it and the parent term id ("0" for none). At most ' . self::MAX_TERMS_PER_TAXONOMY . ' per taxonomy.',
     270            ];
     271        }
     272
    199273        return $out + [ 'truncated' => $truncated, 'notes' => $notes ];
     274    }
     275
     276    /** @return list<array{id:string,name:string,slug:string,items:int,locations:list<string>}> */
     277    private static function menuRows( mixed $raw, bool &$truncated ): array
     278    {
     279        $list = [];
     280        foreach ( is_array( $raw ) ? $raw : [] as $m ) {
     281            if ( ! is_array( $m ) ) {
     282                continue;
     283            }
     284            if ( count( $list ) >= self::MAX_ITEMS ) {
     285                $truncated = true;
     286                break;
     287            }
     288            $list[] = [ 'id' => self::text( $m['id'] ?? '' ), 'name' => self::text( $m['name'] ?? '' ), 'slug' => self::text( $m['slug'] ?? '' ), 'items' => max( 0, (int) ( $m['items'] ?? 0 ) ), 'locations' => self::strings( $m['locations'] ?? [] ) ];
     289        }
     290
     291        return $list;
     292    }
     293
     294    /** @return list<array{name:string,terms:list<array{id:string,name:string,slug:string,count:int,parent:string}>}> */
     295    private static function termRows( mixed $raw, bool &$truncated ): array
     296    {
     297        $list = [];
     298        foreach ( is_array( $raw ) ? $raw : [] as $taxonomy => $terms ) {
     299            if ( ! is_array( $terms ) ) {
     300                continue;
     301            }
     302            if ( count( $list ) >= self::MAX_TAXONOMIES_WITH_TERMS ) {
     303                $truncated = true;
     304                break;
     305            }
     306            $rows = [];
     307            foreach ( $terms as $t ) {
     308                if ( ! is_array( $t ) ) {
     309                    continue;
     310                }
     311                if ( count( $rows ) >= self::MAX_TERMS_PER_TAXONOMY ) {
     312                    $truncated = true;
     313                    break;
     314                }
     315                $rows[] = [ 'id' => self::text( $t['id'] ?? '' ), 'name' => self::text( $t['name'] ?? '' ), 'slug' => self::text( $t['slug'] ?? '' ), 'count' => max( 0, (int) ( $t['count'] ?? 0 ) ), 'parent' => self::text( $t['parent'] ?? '0' ) ];
     316            }
     317            $list[] = [ 'name' => self::text( (string) $taxonomy ), 'terms' => $rows ];
     318        }
     319
     320        return $list;
    200321    }
    201322
  • jhmg-ai-editor-for-divi-5/trunk/src/StyleGuide.php

    r3766728 r3791018  
    4949        $topics[] = 'forms';
    5050        $topics[] = 'composition';
     51        $topics[] = 'loops';
    5152        $topics[] = 'style-kits';
    5253        $topics[] = 'kit-rules';
    … …  
    167168            return self::formsTopic();
    168169        }
     170        if ( 'loops' === $topic ) {
     171            return self::loopsTopic();
     172        }
    169173        if ( 'composition' === $topic ) {
    170174            return self::compositionTopic();
    … …  
    191195        }
    192196        return implode( "\n", $lines ) . "\n";
     197    }
     198
     199    /**
     200     * Topic `loops`: the Loop Builder (a module that repeats once per post, product, term or user). Every setting and token named
     201     * here was rendered on the real Divi 5.15.1 (`make verify-loop-builder`, docs/loop-builder-evidence-5.15.1.md); the validator
     202     * refuses the shapes that break the page or switch the loop off (`INVALID_LOOP_SETTING`, `MALFORMED_DYNAMIC_TOKEN`).
     203     * At most 10,000 bytes.
     204     */
     205    private static function loopsTopic(): string
     206    {
     207        return <<<'MD'
     208# Loops: a module that repeats once per post, product, term or user
     209
     210Use a loop when the page should list things the site already holds: the latest posts, a product grid, the team, the manufacturers of
     211one state. Do not copy the same card five times by hand. Every setting and token below was rendered on the real Divi 5.15.1; the
     212checker refuses the shapes that break the page or quietly switch the loop off. The loop shows what the site has: ask for the post type
     213slug, the taxonomy and the custom field keys (get_site_structure lists them), never invent a slug.
     214
     215## The settings
     216A loop is a set of settings on the module that should repeat, at `module.advanced.loop.desktop.value`. The module repeats, with everything inside it.
     217- `enable`: the string "on" (or "off"). Anything else leaves the loop off.
     218- `loopId`: a string, unique on the page, "loop-" and ten lowercase letters or digits ("loop-k3m9q2x7ab"). Two loops on one page need two ids.
     219- `queryType`: "post_types" (posts, pages, products, any custom post type), "post_taxonomies" (the terms of a taxonomy), "user_roles" (users), "menus" (menu items), "current_page" (the list of the archive page it sits on; on an ordinary page it lists nothing), or "repeater_" plus an ACF repeater field name in lowercase ("repeater_team"; needs ACF's repeater field, not proven here).
     220- `subTypes`: what to list, a list of objects: `[{"value":"post","label":"Posts"}]`. The value is the slug: a post type ("post", "page", "product", "manufacturer"), a taxonomy ("category"), a user role ("administrator") or a menu slug. Always write it; any other shape, or none, reads every post type, media files included.
     221- `orderBy` (Divi's own lists): posts "date", "title", "name" (the slug), "modified", "rand", "menu_order", "comment_count", "ID"; products also "price", "popularity", "rating"; terms "name", "slug", "count", "term_id"; users "display_name", "registered", "post_count"; menu items "menu_order", "title". `order`: "ascending" or "descending" (not "asc" or "desc").
     222- `postPerPage`: a whole number of 1 or more written as a string ("6"); to list everything write a big number such as "100" (the checker refuses "0" and text: they load every item). `postOffset`: a whole number as a string ("0" is fine). For a menu loop `postPerPage` caps the items, sub-items included.
     223- `excludeCurrentPost`, `ignoreStickysPost`: "on" or "off".
     224- `includePostWithSpecificTerms` / `excludePostWithSpecificTerms`: only the posts that have (or lack) a term: `[{"categoryId":"state","categoryName":"States","selectedOptions":[{"value":"3","label":"North Carolina"}]}]`. `categoryId` is the taxonomy slug, `value` the term ID as a string (the ID, not the slug). Term IDs are site data: get_site_structure with include=terms lists them (and include=menus lists the menu IDs); a wrong ID or taxonomy is quietly ignored, so the loop is not filtered at all. The builder writes "" for an empty list; leave an unused list out.
     225- `metaQuery`: a list of `{"id","metaKey","metaValue","compare","type"}` objects, or leave it out. Never a string.
     226
     227## What goes inside the loop
     228Inside a looping module, write the content as loop tokens. A token is a string in the field that should change per item:
     229
     230`$variable({"type":"content","value":{"name":"loop_post_title","settings":{}}})$`
     231
     232Both ends matter: `$variable(` at the start and `)$` at the end. Without the closing `$` the page prints the raw text. **In a block comment every quote of the token MUST be written as `\u0022`** (copy the example): plain quotes break the JSON and the whole block is refused as BLOCK_PARSE_ERROR.
     233- Posts: `loop_post_title`, `loop_post_excerpt`, `loop_post_date`, `loop_post_link` (a web address), `loop_post_featured_image` (a picture), `loop_post_author`, `loop_post_terms`, `loop_post_id`.
     234- Products (queryType "post_types", subTypes "product"): `loop_product_title`, `loop_product_price_current`, `loop_product_price_regular`, `loop_product_price_sale`, `loop_product_short_description`, `loop_product_post_featured_image`, `loop_product_post_link_url`, `loop_product_sku`.
     235- Terms: `loop_term_name`, `loop_term_description`, `loop_term_count`, `loop_term_permalink`, `loop_term_featured_image`.
     236- Users: `loop_user_name`, `loop_user_username`, `loop_user_description`, `loop_user_avatar`, `loop_user_url`.
     237- Menu items: `loop_menu_text`, `loop_menu_link`, `loop_menu_description`.
     238- A custom field of each item: `loop_post_meta_key_manual_custom_field` with `"settings":{"select_loop_meta_key":"loop_post_meta_key_<field key>"}` (key "logo" is `loop_post_meta_key_logo`).
     239Use a `loop_` token, never `post_title` and its relatives: those show the page's own title in every item. A `loop_` token outside a loop shows nothing.
     240
     241Where a token goes (each rendered): a heading's `title.innerContent.desktop.value`; a text module's `content.innerContent.desktop.value`, inside the HTML ("<h3>token</h3><p>token</p>"); an image's `image.innerContent.desktop.value.src`, `.alt` (a token such as `loop_post_title` works) and `.linkUrl`; a button's `button.innerContent.desktop.value.linkUrl` (the label can be a token too).
     242
     243## Which module repeats
     244- A group repeats with everything in it: one card per item. Put the card's modules inside the group and the loop on the group. For a grid of cards, put that looped group inside a plain grid group (`module.decoration.layout.desktop.value`: `display` "grid", `gridColumnCount` "3", `columnGap`; the same path under `tablet` and `phone` for fewer columns); each repeated card becomes one grid cell.
     245- An image grid: the grid is a group with `display` "grid" and `gridColumnCount` "4" (tablet "2", phone "1") and NO loop of its own; the loop is on the image inside it.
     246- A row repeats too (one row per item). A loop inside a loop multiplies the items: use it only on purpose.
     247- An empty result prints Divi's own "No Results Found."; say in your answer which filter produced it.
     248
     249## Pages of results and menus
     250- Next and Previous links: put a Post Navigation module (`divi/post-nav`) after the looped module, in the same column, and point it at the loop: `module.advanced.targetLoop.desktop.value` is the loop's `loopId` string, for example `{"module":{"advanced":{"targetLoop":{"desktop":{"value":"loop-k3m9q2x7ab"}}}},"builderVersion":"5.8.0"}`. Set the loop's `postPerPage` to the page size. The page is `?loop-k3m9q2x7ab=2` in the address, so each loop pages on its own. The links are Divi's own (Next and Previous, shown only when there is another page); no other setting is needed. Never write a list or object as the `targetLoop` value: the whole page fails.
     251- A menu loop (queryType "menus", `subTypes` `[{"value":"<menu ID>","label":"Main menu"}]`, the ID a string from get_site_structure or the owner) lists every item of the menu, sub-items included, one after the other. The order is inverted: "descending" (the default) is the menu's own order and "ascending" is backwards (checked on Divi 5.15.1; a later Divi may fix it). Use `loop_menu_text` for the label and `loop_menu_link` for the address; the post tokens (`loop_post_title` and the rest) are empty there.
     252
     253## Example: the three latest posts as cards
     254```
     255<!-- wp:divi/section {"builderVersion":"5.8.0"} -->
     256<!-- wp:divi/row {"builderVersion":"5.8.0"} -->
     257<!-- wp:divi/column {"module":{"advanced":{"type":{"desktop":{"value":"4_4"}}}},"builderVersion":"5.8.0"} -->
     258<!-- wp:divi/group {"module":{"advanced":{"loop":{"desktop":{"value":{"enable":"on","loopId":"loop-k3m9q2x7ab","queryType":"post_types","subTypes":[{"value":"post","label":"Posts"}],"orderBy":"date","order":"descending","postPerPage":"3","postOffset":"0"}}}}},"builderVersion":"5.8.0"} -->
     259<!-- wp:divi/heading {"title":{"innerContent":{"desktop":{"value":"$variable({\u0022type\u0022:\u0022content\u0022,\u0022value\u0022:{\u0022name\u0022:\u0022loop_post_title\u0022,\u0022settings\u0022:{}}})$"}}},"builderVersion":"5.8.0"} /-->
     260<!-- /wp:divi/group -->
     261<!-- /wp:divi/column -->
     262<!-- /wp:divi/row -->
     263<!-- /wp:divi/section -->
     264```
     265Ready-made: `get_section_recipes` with name `loop-posts-cards` (the newest posts as cards), `loop-posts-paged` (all posts, six per page with Next and Previous), `loop-categories-grid` (the site's categories) or `loop-products-grid` (the newest WooCommerce products); each already follows the design language and the style kit. Use one copy of a recipe per page: its loop id is fixed. A save, `validate_layout` or `dry_run` answer carries `advisories.loops` when a `loop_` token sits outside a loop that is switched on, does not match the loop around it, or a `post_` token sits inside one, when two loops share an id, or when a Post Navigation points at no loop on the page, and `advisories.loop_results`: what each loop will list on this site (how many items, the first titles, and a problem such as a post type, menu or taxonomy this site does not have; an unknown post type makes the loop list every post type).
     266
     267Style the cards with the modules' own settings, as on any page (the design language and the style kit apply as usual). Check the page with
     268`validate_layout` or `dry_run` before saving; the answer lists the first wrong setting.
     269MD;
    193270    }
    194271
  • jhmg-ai-editor-for-divi-5/trunk/src/StyleReferences.php

    r3756982 r3791018  
    113113     * Adds `advisories.style_references` to a response payload, only when the layout cites ids this
    114114     * site does not have, `advisories.components` (ComponentAdvisory) when it does something with a Divi 5.15
    115      * component that does nothing, `advisories.content_loss` (ContentLoss) when a save removes most of the stored page, `advisories.design_variety` (DesignVariety) when a page of three or more sections looks repetitive, and `advisories.unresolved_tokens` (RecipeTokens) when an {{aied:...}} token is still in the content. Otherwise the payload is returned unchanged, byte for byte.
     115     * component that does nothing, `advisories.content_loss` (ContentLoss) when a save removes most of the stored page, `advisories.design_variety` (DesignVariety) when a page of three or more sections looks repetitive, `advisories.unresolved_tokens` (RecipeTokens) when an {{aied:...}} token is still in the content, and `advisories.loops` (LoopAdvisory) when a loop_ token sits outside a loop or a post_ token inside one. Otherwise the payload is returned unchanged, byte for byte.
    116116     *
    117117     * @param array<string,mixed> $payload
    … …  
    148148            $advisories['unresolved_tokens'] = $tokens;
    149149        }
     150        $loops = LoopAdvisory::advise( $content );
     151        if ( null !== $loops ) {
     152            $advisories['loops'] = $loops;
     153        }
     154        $results = LoopPreview::advise( $content );
     155        if ( null !== $results ) {
     156            $advisories['loop_results'] = $results;
     157        }
    150158
    151159        return [] === $advisories ? $payload : $payload + [ 'advisories' => $advisories ];
  • jhmg-ai-editor-for-divi-5/trunk/src/ToolCatalog.php

    r3786407 r3791018  
    3939            [
    4040                'name'        => 'get_site_structure',
    41                 'description' => 'List how this site is set up (read-only): public post types, each with how many items it has (published), taxonomies, image sizes and, when Advanced Custom Fields is active, its field groups, where each group applies (location rules) and its fields (names, labels, types and choices; never field values). Call it before building a page that lists custom post types, uses a given image size or should refer to ACF fields or other custom fields. Pass "include" to get only some sections: post_types, taxonomies, image_sizes, acf, custom_fields, components. The custom_fields section lists the custom field KEYS your recent posts use (a sample, never values); use get_custom_fields to read one post\'s values. The components section lists the site\'s Divi 5.15 components (name, kind, the ids of their Slots, how many pages use each): a component is shared by every page that uses it, so read it before editing a page that holds one.',
    42                 'inputSchema' => [
    43                     'type'       => 'object',
    44                     'properties' => ['include' => ['type' => 'array', 'items' => ['type' => 'string', 'enum' => ['post_types', 'taxonomies', 'image_sizes', 'acf', 'custom_fields', 'components']], 'description' => 'Optional. Sections to return; default all.']],
     41                'description' => 'List how this site is set up (read-only): public post types, each with how many items it has (published), taxonomies, image sizes and, when Advanced Custom Fields is active, its field groups, where each group applies (location rules) and its fields (names, labels, types and choices; never field values). Call it before building a page that lists custom post types, uses a given image size or should refer to ACF fields or other custom fields. Pass "include" to get only some sections: post_types, taxonomies, image_sizes, acf, custom_fields, components, or menus and terms (not in the default answer: the menu ids and the term ids a loop needs). The custom_fields section lists the custom field KEYS your recent posts use (a sample, never values); use get_custom_fields to read one post\'s values. The components section lists the site\'s Divi 5.15 components (name, kind, the ids of their Slots, how many pages use each): a component is shared by every page that uses it, so read it before editing a page that holds one.',
     42                'inputSchema' => [
     43                    'type'       => 'object',
     44                    'properties' => ['include' => ['type' => 'array', 'items' => ['type' => 'string', 'enum' => ['post_types', 'taxonomies', 'image_sizes', 'acf', 'custom_fields', 'components', 'menus', 'terms']], 'description' => 'Optional. Sections to return; default all.']],
    4545                    'required'   => [],
    4646                ],
  • jhmg-ai-editor-for-divi-5/trunk/src/autoload.php

    r3786407 r3791018  
    6565require_once __DIR__ . '/ComponentAdvisory.php';
    6666require_once __DIR__ . '/ContentLoss.php';
     67require_once __DIR__ . '/LoopAdvisory.php';
     68require_once __DIR__ . '/LoopPreview.php';
    6769require_once __DIR__ . '/DesignVariety.php';
    6870require_once __DIR__ . '/ViolationReport.php';
  • jhmg-ai-editor-for-divi-5/trunk/validator/BlockParser.php

    r3727421 r3791018  
    8484                    $attrs = json_decode($jsonStr, associative: true);
    8585                    if ($attrs === null && json_last_error() !== JSON_ERROR_NONE) {
    86                         $errors[] = "Invalid JSON in block wp:$name at offset $start: " . json_last_error_msg();
     86                        $hint = str_contains($jsonStr, '$variable({"')
     87                            ? ' (a dynamic-content token inside a block comment must write each of its quotes as \\u0022, not "; see get_style_guide topic=loops)'
     88                            : '';
     89                        $errors[] = "Invalid JSON in block wp:$name at offset $start: " . json_last_error_msg() . $hint;
    8790                        $attrs = [];
    8891                    }
  • jhmg-ai-editor-for-divi-5/trunk/validator/Validator.php

    r3766728 r3791018  
    5050    public const E_ORPHANED_REFERENCE      = 'ORPHANED_REFERENCE';
    5151
     52    // Pass 5b — Loop Builder settings and dynamic-content tokens (live-proven on Divi 5.15.1, docs/loop-builder-evidence-5.15.1.md)
     53    public const E_INVALID_LOOP_SETTING     = 'INVALID_LOOP_SETTING';
     54    public const E_MALFORMED_DYNAMIC_TOKEN  = 'MALFORMED_DYNAMIC_TOKEN';
     55
    5256    // Pass 6 — SEO / accessibility
    5357    public const E_MULTIPLE_H1             = 'MULTIPLE_H1';
    … …  
    101105        // Pass 5 — render-critical attributes
    102106        $this->passRenderCritical($tree, $violations);
     107
     108        // Pass 5b — Loop Builder settings and dynamic-content tokens
     109        $this->passLoops($tree, $violations);
    103110
    104111        // Pass 6 — SEO / accessibility (at most one h1 per page)
    … …  
    415422        });
    416423    }
     424
     425    // ---------------------------------------------------------------
     426    // Pass 5b — Loop Builder (module.advanced.loop) and dynamic tokens
     427    // ---------------------------------------------------------------
     428
     429    /**
     430     * @return string[] Query types the Loop Builder offers (Divi 5.15.1 QueryTypeController), plus "terms" (Divi reads it as
     431     * post_taxonomies; live-rendered) and "repeater_<acf field name>" (matched by isLoopQueryType()).
     432     */
     433    private function loopQueryTypes(): array
     434    {
     435        return ['post_types', 'post_taxonomies', 'terms', 'user_roles', 'menus', 'current_page'];
     436    }
     437
     438    /**
     439     * An ACF repeater loop is any queryType "repeater_" plus the field name (DynamicContentACFUtils::is_repeater_query). Divi runs
     440     * the type through sanitize_key(), so only lowercase letters, digits, "_" and "-" survive. Source-grounded and rendered on
     441     * Divi 5.15.1 without ACF (nothing listed, nothing thrown); not rendered with a real ACF Pro repeater.
     442     */
     443    private function isLoopQueryType(mixed $type): bool
     444    {
     445        return is_string($type)
     446            && (in_array($type, $this->loopQueryTypes(), true) || preg_match('/^repeater_[a-z0-9_-]+$/', $type) === 1);
     447    }
     448
     449    /**
     450     * Every shape below was rendered on the real Divi 5.15.1 (docs/loop-builder-evidence-5.15.1.md):
     451     * some answer HTTP 500 for the whole page (a non-list metaQuery, an array order or loopId), the others make the loop
     452     * silently switch off or query every post type, which is what a saved layout must never do. The builder itself only
     453     * writes the shapes accepted here (fixtures/valid/loop-builder-5.3.json), including "" for an empty include/exclude list.
     454     *
     455     * @param Violation[] $violations
     456     */
     457    private function passLoops(Block $root, array &$violations): void
     458    {
     459        $root->walk(function (Block $block, string $path) use (&$violations): void {
     460            $type = $block->name();
     461            $loop = $block->attr('module.advanced.loop');
     462            $base = "$path.module.advanced.loop";
     463
     464            if ($loop !== null && $loop !== []) {
     465                $fail = static function (string $what) use (&$violations, $type, $base): void {
     466                    $violations[] = new Violation(
     467                        self::E_INVALID_LOOP_SETTING,
     468                        "Block '$type': loop setting $what",
     469                        $base
     470                    );
     471                };
     472                $value = is_array($loop) ? ($loop['desktop']['value'] ?? null) : null;
     473                if (!is_array($loop) || !is_array($loop['desktop'] ?? null) || !is_array($value)) {
     474                    $fail("must be an object {\"desktop\":{\"value\":{...}}} (anything else switches the loop off without a message).");
     475                } else {
     476                    $this->checkLoopValue($value, $fail);
     477                }
     478            }
     479
     480            // A Post Navigation module connected to a loop: an array or object here is a TypeError for the whole page
     481            // (LoopQueryRegistry::get_query() wants a string; docs/loop-builder-evidence-5.15.1.md, probe 2026-11-08).
     482            if ($type === 'divi/post-nav' && is_array($block->attr('module.advanced.targetLoop.desktop.value'))) {
     483                $violations[] = new Violation(
     484                    self::E_INVALID_LOOP_SETTING,
     485                    "Block '$type': 'module.advanced.targetLoop.desktop.value' must be the loopId string of the loop it pages "
     486                        . '(an array or object makes Divi answer HTTP 500 for the whole page).',
     487                    "$path.module.advanced.targetLoop.desktop.value"
     488                );
     489            }
     490
     491            $this->checkTokens($block->attrs(), $path, $type, $violations);
     492        });
     493    }
     494
     495    /** @param callable(string):void $fail */
     496    private function checkLoopValue(array $v, callable $fail): void
     497    {
     498        $isDigits = static fn($x, int $min): bool => (is_int($x) && $x >= $min)
     499            || (is_string($x) && preg_match('/^\d+$/', $x) === 1 && (int) $x >= $min);
     500
     501        if (array_key_exists('enable', $v) && !in_array($v['enable'], ['on', 'off'], true)) {
     502            $fail("'enable' must be the string \"on\" or \"off\" (true, \"yes\" or a missing value leave the loop off).");
     503        }
     504        if (array_key_exists('loopId', $v) && !is_string($v['loopId'])) {
     505            $fail("'loopId' must be a string such as \"loop-ab12cd34ef\" (an array makes Divi answer HTTP 500 for the whole page).");
     506        }
     507        if (array_key_exists('queryType', $v) && !$this->isLoopQueryType($v['queryType'])) {
     508            $fail("'queryType' must be one of " . implode(', ', $this->loopQueryTypes()) . ', or repeater_<lowercase ACF field name>.');
     509        }
     510        if (array_key_exists('order', $v) && !in_array($v['order'], ['ascending', 'descending'], true)) {
     511            $fail("'order' must be the string \"ascending\" or \"descending\" (an array makes Divi answer HTTP 500, \"desc\" is read as ascending).");
     512        }
     513        if (array_key_exists('orderBy', $v) && !is_string($v['orderBy'])) {
     514            $fail("'orderBy' must be a string such as \"date\", \"name\" or \"title\".");
     515        }
     516        if (array_key_exists('postPerPage', $v) && !$isDigits($v['postPerPage'], 1)) {
     517            $fail("'postPerPage' must be a whole number of 1 or more, written as a string such as \"6\" (\"abc\" and \"0\" load every item).");
     518        }
     519        if (array_key_exists('postOffset', $v) && !$isDigits($v['postOffset'], 0)) {
     520            $fail("'postOffset' must be a whole number written as a string such as \"0\".");
     521        }
     522        foreach (['excludeCurrentPost', 'ignoreStickysPost'] as $key) {
     523            if (array_key_exists($key, $v) && !in_array($v[$key], ['on', 'off'], true)) {
     524                $fail("'$key' must be the string \"on\" or \"off\".");
     525            }
     526        }
     527        if (array_key_exists('subTypes', $v)) {
     528            $ok = is_array($v['subTypes']) && $this->isList($v['subTypes']);
     529            foreach ($ok ? $v['subTypes'] : [] as $item) {
     530                if (!is_array($item) || !is_string($item['value'] ?? null) || $item['value'] === '') {
     531                    $ok = false;
     532                }
     533            }
     534            if (!$ok) {
     535                $fail("'subTypes' must be a list of {\"value\":\"<slug>\",\"label\":\"<name>\"} objects, for example [{\"value\":\"post\",\"label\":\"Posts\"}] (any other shape makes the loop read EVERY post type, media files included).");
     536            }
     537        }
     538        if (array_key_exists('metaQuery', $v) && !(is_array($v['metaQuery']) && $this->isList($v['metaQuery']))) {
     539            $fail("'metaQuery' must be a list of {\"id\",\"metaKey\",\"metaValue\",\"compare\",\"type\"} objects, or left out (a string, even an empty one, makes Divi answer HTTP 500 for the whole page).");
     540        }
     541    }
     542
     543    /** A JSON array (not an object): keys 0..n-1 (array_is_list() needs WordPress 6.5). */
     544    private function isList(array $a): bool
     545    {
     546        return $a === [] || array_keys($a) === range(0, count($a) - 1);
     547    }
     548
     549    /**
     550     * A dynamic-content token is written $variable({...})$. Without the closing "$" Divi prints the raw text on the page.
     551     *
     552     * @param Violation[] $violations
     553     */
     554    private function checkTokens(mixed $node, string $path, string $type, array &$violations): void
     555    {
     556        if (is_array($node)) {
     557            foreach ($node as $key => $child) {
     558                $this->checkTokens($child, $path . '.' . $key, $type, $violations);
     559            }
     560            return;
     561        }
     562        if (!is_string($node) || !str_contains($node, '$variable(')) {
     563            return;
     564        }
     565        $offset = 0;
     566        while (($at = strpos($node, '$variable(', $offset)) !== false) {
     567            if (preg_match('/\G\$variable\(\{.*?\}\)\$/s', $node, $m, 0, $at) !== 1) {
     568                $violations[] = new Violation(
     569                    self::E_MALFORMED_DYNAMIC_TOKEN,
     570                    "Block '$type': a dynamic-content token must read \$variable({...})\$ with both the opening \$variable( and the closing )\$; "
     571                        . 'otherwise Divi prints the raw text on the page.',
     572                    $path
     573                );
     574                return;
     575            }
     576            $offset = $at + strlen($m[0]);
     577        }
     578    }
    417579}
Note: See TracChangeset for help on using the changeset viewer.