Changeset 3791018
- Timestamp:
- 11/08/2026 01:14:36 PM (9 hours ago)
- Location:
- jhmg-ai-editor-for-divi-5/trunk
- Files:
-
- 2 added
- 11 edited
-
data/section-recipes.json (modified) (1 diff)
-
data/whats-new.php (modified) (1 diff)
-
jhmg-ai-editor-for-divi-5.php (modified) (2 diffs)
-
readme.txt (modified) (8 diffs)
-
src/LoopAdvisory.php (added)
-
src/LoopPreview.php (added)
-
src/SiteStructure.php (modified) (5 diffs)
-
src/StyleGuide.php (modified) (3 diffs)
-
src/StyleReferences.php (modified) (2 diffs)
-
src/ToolCatalog.php (modified) (1 diff)
-
src/autoload.php (modified) (1 diff)
-
validator/BlockParser.php (modified) (1 diff)
-
validator/Validator.php (modified) (3 diffs)
Legend:
- Unmodified
- Added
- Removed
-
jhmg-ai-editor-for-divi-5/trunk/data/section-recipes.json
r3766728 r3791018 2424 2424 }, 2425 2425 "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 -->" 2426 2594 } 2427 2595 ] -
jhmg-ai-editor-for-divi-5/trunk/data/whats-new.php
r3786407 r3791018 15 15 16 16 return [ 17 'version' => '4.1 7',18 'title' => __( 'What\'s new in 4.1 7', 'jhmg-ai-editor-for-divi-5' ),17 'version' => '4.18', 18 'title' => __( 'What\'s new in 4.18', 'jhmg-ai-editor-for-divi-5' ), 19 19 'bullets' => [ 20 __( 'D esign 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' ), 25 25 ], 26 26 'link' => [ -
jhmg-ai-editor-for-divi-5/trunk/jhmg-ai-editor-for-divi-5.php
r3786407 r3791018 3 3 * Plugin Name: JHMG AI Editor for Divi 5 4 4 * 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.1 7.05 * Version: 4.18.0 6 6 * Requires at least: 6.0 7 7 * Requires PHP: 8.1 … … 20 20 } 21 21 22 define('AI_EDITOR_DIVI5_VERSION', '4.1 7.0');22 define('AI_EDITOR_DIVI5_VERSION', '4.18.0'); 23 23 define('AI_EDITOR_DIVI5_MIN_PHP', '8.1'); 24 24 define('AI_EDITOR_DIVI5_MIN_WP', '6.0'); -
jhmg-ai-editor-for-divi-5/trunk/readme.txt
r3786407 r3791018 4 4 Requires at least: 6.0 5 5 Tested up to: 7.1 6 Stable tag: 4.1 7.06 Stable tag: 4.18.0 7 7 Requires PHP: 8.1 8 8 License: GPL-2.0-or-later … … 18 18 = Designed to work alongside Divi's upcoming MCP = 19 19 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 7 1proven 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.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 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. 21 21 22 22 = How it works = … … 82 82 * **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. 83 83 * **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.** 7 1ready-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. 85 85 * **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. 86 86 * **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. … … 104 104 * `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. 105 105 * `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). 107 107 * `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). 108 108 * `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). … … 126 126 * `get_landing_guide`: how to build a landing page that turns visitors into customers. 127 127 * `get_site_guide`: how to plan a site with several pages that link to each other. 128 * `get_section_recipes`: the 7 1ready-made section patterns.128 * `get_section_recipes`: the 75 ready-made section patterns. 129 129 * `get_image_guide`: how to pick the right image for each section, including the built-in image pack. 130 130 … … 220 220 = Can it read my ACF fields, custom post types or functions.php? = 221 221 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.222 It 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. 223 223 224 224 = Can it create global colors and presets? = … … 254 254 = Do I need this if I use Divi's MCP? = 255 255 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 7 1proven 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.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 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. 257 257 258 258 = Is this an MCP server for Divi? = … … 298 298 299 299 == 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. 300 309 301 310 = 4.17.0 = -
jhmg-ai-editor-for-divi-5/trunk/src/SiteStructure.php
r3750826 r3791018 15 15 * 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 16 16 * 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. 18 18 */ 19 19 final class SiteStructure 20 20 { 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; 22 26 public const MAX_ITEMS = 100; 23 27 public const MAX_FIELDS = 100; … … 38 42 { 39 43 if ( null === $value || '' === $value || [] === $value ) { 40 return self:: SECTIONS;44 return self::DEFAULT_SECTIONS; 41 45 } 42 46 if ( is_string( $value ) ) { … … 68 72 'custom_fields' => class_exists( CustomFieldReader::class ) ? [ CustomFieldReader::class, 'discover' ] : null, 69 73 '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, 70 76 ]; 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; 71 123 } 72 124 … … 121 173 public static function catalogWith( array $readers, ?array $include = null ): array 122 174 { 123 $include = $include ?? self:: SECTIONS;175 $include = $include ?? self::DEFAULT_SECTIONS; 124 176 $notes = []; 125 177 $truncated = false; … … 197 249 } 198 250 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 199 273 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; 200 321 } 201 322 -
jhmg-ai-editor-for-divi-5/trunk/src/StyleGuide.php
r3766728 r3791018 49 49 $topics[] = 'forms'; 50 50 $topics[] = 'composition'; 51 $topics[] = 'loops'; 51 52 $topics[] = 'style-kits'; 52 53 $topics[] = 'kit-rules'; … … 167 168 return self::formsTopic(); 168 169 } 170 if ( 'loops' === $topic ) { 171 return self::loopsTopic(); 172 } 169 173 if ( 'composition' === $topic ) { 170 174 return self::compositionTopic(); … … 191 195 } 192 196 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 210 Use a loop when the page should list things the site already holds: the latest posts, a product grid, the team, the manufacturers of 211 one 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 212 checker 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 213 slug, the taxonomy and the custom field keys (get_site_structure lists them), never invent a slug. 214 215 ## The settings 216 A 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 228 Inside 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 232 Both 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`). 239 Use 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 241 Where 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 ``` 265 Ready-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 267 Style 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. 269 MD; 193 270 } 194 271 -
jhmg-ai-editor-for-divi-5/trunk/src/StyleReferences.php
r3756982 r3791018 113 113 * Adds `advisories.style_references` to a response payload, only when the layout cites ids this 114 114 * 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. 116 116 * 117 117 * @param array<string,mixed> $payload … … 148 148 $advisories['unresolved_tokens'] = $tokens; 149 149 } 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 } 150 158 151 159 return [] === $advisories ? $payload : $payload + [ 'advisories' => $advisories ]; -
jhmg-ai-editor-for-divi-5/trunk/src/ToolCatalog.php
r3786407 r3791018 39 39 [ 40 40 '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.']], 45 45 'required' => [], 46 46 ], -
jhmg-ai-editor-for-divi-5/trunk/src/autoload.php
r3786407 r3791018 65 65 require_once __DIR__ . '/ComponentAdvisory.php'; 66 66 require_once __DIR__ . '/ContentLoss.php'; 67 require_once __DIR__ . '/LoopAdvisory.php'; 68 require_once __DIR__ . '/LoopPreview.php'; 67 69 require_once __DIR__ . '/DesignVariety.php'; 68 70 require_once __DIR__ . '/ViolationReport.php'; -
jhmg-ai-editor-for-divi-5/trunk/validator/BlockParser.php
r3727421 r3791018 84 84 $attrs = json_decode($jsonStr, associative: true); 85 85 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; 87 90 $attrs = []; 88 91 } -
jhmg-ai-editor-for-divi-5/trunk/validator/Validator.php
r3766728 r3791018 50 50 public const E_ORPHANED_REFERENCE = 'ORPHANED_REFERENCE'; 51 51 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 52 56 // Pass 6 — SEO / accessibility 53 57 public const E_MULTIPLE_H1 = 'MULTIPLE_H1'; … … 101 105 // Pass 5 — render-critical attributes 102 106 $this->passRenderCritical($tree, $violations); 107 108 // Pass 5b — Loop Builder settings and dynamic-content tokens 109 $this->passLoops($tree, $violations); 103 110 104 111 // Pass 6 — SEO / accessibility (at most one h1 per page) … … 415 422 }); 416 423 } 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 } 417 579 }
Note:
See TracChangeset
for help on using the changeset viewer.
![(please configure the [header_logo] section in trac.ini)](/chrome/site/your_project_logo.png)