diff --git a/config/ste.medusa-starter.design.json b/config/ste.medusa-starter.design.json
new file mode 100644
index 0000000..879fb8c
--- /dev/null
+++ b/config/ste.medusa-starter.design.json
@@ -0,0 +1,68 @@
+[
+ {
+ "Nav": {
+ "props": {},
+ "children": [
+ {
+ "Div": {
+ "props": { "className": "flex items-center h-full" },
+ "children": [
+ {
+ "LocalizedClientLink": {
+ "props": {
+ "href": "/",
+ "label": "Medusa Store",
+ "className": "bg-black txt-compact-xlarge-plus hover:text-ui-fg-base uppercase",
+ "data-testid": "nav-store-link"
+ }
+ }
+ }
+ ]
+ }
+ },
+ {
+ "Div": {
+ "props": { "className": "flex items-center gap-x-6 h-full flex-1 basis-0 justify-end" },
+ "children": [
+ {
+ "LocalizedClientLink": {
+ "props": {
+ "href": "/account",
+ "label": "Account",
+ "className": "hover:text-ui-fg-base bg-black",
+ "data-testid": "nav-account-link"
+ }
+ }
+ },
+ {
+ "Suspense": {
+ "props": {
+ "fallback": [
+ {
+ "LocalizedClientLink": {
+ "props": {
+ "href": "/cart",
+ "label": "Cart (0)",
+ "className": "bg-black hover:text-ui-fg-base flex gap-2",
+ "data-testid": "nav-cart-link"
+ }
+ }
+ }
+ ]
+ },
+ "children": [
+ { "CartButton": {} }
+ ]
+ }
+ }
+ ]
+ }
+ }
+ ]
+ }
+ },
+ { "CartMismatchBanner": { "show": true } },
+ { "FreeShippingPriceNudge": { "variant": "popup" } },
+ { "PropsChildren" : {}},
+ { "Footer": { "copyrightText": "© 2025 MyShop" } }
+]
\ No newline at end of file
diff --git a/src/app/[countryCode]/(main)/layout.tsx b/src/app/[countryCode]/(main)/layout.tsx
index 5635222..6036614 100644
--- a/src/app/[countryCode]/(main)/layout.tsx
+++ b/src/app/[countryCode]/(main)/layout.tsx
@@ -4,10 +4,9 @@ import { listCartOptions, retrieveCart } from "@lib/data/cart"
import { retrieveCustomer } from "@lib/data/customer"
import { getBaseURL } from "@lib/util/env"
import { StoreCartShippingOption } from "@medusajs/types"
-import CartMismatchBanner from "@modules/layout/components/cart-mismatch-banner"
-import Footer from "@modules/layout/templates/footer"
-import Nav from "@modules/layout/templates/nav"
-import FreeShippingPriceNudge from "@modules/shipping/components/free-shipping-price-nudge"
+import { DynamicLayoutRenderer } from "../../../vibentec/renderer"
+import { LayoutContext, LayoutComponentNode, } from "../../../vibentec/component-map"
+import { loadDesignConfig } from "vibentec/configloader"
export const metadata: Metadata = {
metadataBase: new URL(getBaseURL()),
@@ -24,22 +23,14 @@ export default async function PageLayout(props: { children: React.ReactNode }) {
shippingOptions = shipping_options
}
- return (
- <>
-
- {customer && cart && (
-
- )}
+ const nodes: LayoutComponentNode[] = await loadDesignConfig()
+ const context: LayoutContext = {
+ customer,
+ cart,
+ shippingOptions,
+ contentChildren: props.children,
+ }
- {cart && (
-
- )}
- {props.children}
-
- >
- )
+
+ return
}
diff --git a/src/modules/layout/templates/vt-footer/index.tsx b/src/modules/layout/templates/vt-footer/index.tsx
new file mode 100644
index 0000000..3e5b34e
--- /dev/null
+++ b/src/modules/layout/templates/vt-footer/index.tsx
@@ -0,0 +1,158 @@
+import { listCategories } from "@lib/data/categories"
+import { listCollections } from "@lib/data/collections"
+import { Text, clx } from "@medusajs/ui"
+import { FooterProps } from "vibentec/component-props"
+import LocalizedClientLink from "@modules/common/components/localized-client-link"
+import MedusaCTA from "@modules/layout/components/medusa-cta"
+
+
+export default async function VtFooter({copyrightText}:FooterProps) {
+ const { collections } = await listCollections({
+ fields: "*products",
+ })
+ const productCategories = await listCategories()
+
+ return (
+
+ )
+}
diff --git a/src/modules/layout/templates/vt-nav/index.tsx b/src/modules/layout/templates/vt-nav/index.tsx
new file mode 100644
index 0000000..dac8cfd
--- /dev/null
+++ b/src/modules/layout/templates/vt-nav/index.tsx
@@ -0,0 +1,26 @@
+import { listRegions } from "@lib/data/regions"
+import { StoreRegion } from "@medusajs/types"
+import SideMenu from "@modules/layout/components/side-menu"
+import { DynamicLayoutRenderer, DynamicLayoutRendererProps } from "vibentec/renderer"
+
+export default async function VtNav({ nodes, context }: DynamicLayoutRendererProps) {
+ const regions = await listRegions().then((regions: StoreRegion[]) => regions)
+
+ return (
+
+
+
+
+
+ )
+}
diff --git a/src/vibentec/component-map.tsx b/src/vibentec/component-map.tsx
new file mode 100644
index 0000000..138c5d6
--- /dev/null
+++ b/src/vibentec/component-map.tsx
@@ -0,0 +1,76 @@
+import CartMismatchBanner from "@modules/layout/components/cart-mismatch-banner"
+import FreeShippingPriceNudge from "@modules/shipping/components/free-shipping-price-nudge"
+import LocalizedClientLink from "@modules/common/components/localized-client-link"
+import CartButton from "@modules/layout/components/cart-button"
+import { DynamicLayoutRenderer } from "./renderer"
+import React from "react"
+import VtNav from "@modules/layout/templates/vt-nav"
+import VtFooter from "@modules/layout/templates/vt-footer"
+
+export interface LayoutComponentDefinition {
+ props?: Record
+ children?: LayoutComponentNode[]
+}
+
+//maps key = componentName to value = props + children
+export type LayoutComponentNode = Record
+
+export interface LayoutContext {
+ customer: any;
+ cart: any;
+ shippingOptions: any[];
+ contentChildren: React.ReactNode;
+}
+
+export type ComponentRenderer = {
+ render: (entry: LayoutComponentDefinition, ctx: LayoutContext) => React.ReactNode
+}
+
+// Utility, wenn eine Komponente nur props hat und keine children
+const simple = (Component: React.ComponentType): ComponentRenderer => ({
+ render: (entry) =>
+})
+
+// Helper für Kinder-Rendering
+const renderChildren = (entry: LayoutComponentDefinition, ctx: LayoutContext) =>
+ entry.children ? : null
+
+
+// Component Map
+export const componentMap: Record = {
+ Nav: {
+ render: (entry: any, ctx: LayoutContext) => ( ),
+ },
+ Div: {
+ render: (entry: any, ctx: LayoutContext) => (
+
+ {entry.children ? : null}
+
+ )
+ },
+ LocalizedClientLink: {
+ render: (entry: any) => (
+ {entry.props.label}
+ )
+ },
+ CartButton: simple(CartButton),
+ Suspense: {
+ render: (entry: any, ctx: LayoutContext) => (
+
+ ) : null
+ }
+ >
+ {entry.children ? : null}
+
+ ),
+ },
+ CartMismatchBanner: simple(CartMismatchBanner),
+ FreeShippingPriceNudge: simple(FreeShippingPriceNudge),
+ PropsChildren: {
+ render: (_props, ctx) => ctx.contentChildren, // PageLayout's props.children
+ },
+ Footer: simple(VtFooter),
+}
\ No newline at end of file
diff --git a/src/vibentec/component-props.ts b/src/vibentec/component-props.ts
new file mode 100644
index 0000000..31c519a
--- /dev/null
+++ b/src/vibentec/component-props.ts
@@ -0,0 +1 @@
+export interface FooterProps { copyrightText?: string }
\ No newline at end of file
diff --git a/src/vibentec/configloader.ts b/src/vibentec/configloader.ts
new file mode 100644
index 0000000..bf427f4
--- /dev/null
+++ b/src/vibentec/configloader.ts
@@ -0,0 +1,10 @@
+import fs from "fs"
+import path from "path"
+
+const fileName = "ste.medusa-starter.design.json";
+
+export async function loadDesignConfig() {
+ const filePath = path.join(process.cwd(), "config", fileName)
+ const fileData = await fs.promises.readFile(filePath, "utf-8")
+ return JSON.parse(fileData)
+}
diff --git a/src/vibentec/renderer.tsx b/src/vibentec/renderer.tsx
new file mode 100644
index 0000000..f4f90ba
--- /dev/null
+++ b/src/vibentec/renderer.tsx
@@ -0,0 +1,16 @@
+import React from "react"
+import { LayoutComponentNode, LayoutContext, componentMap } from "./component-map"
+
+export interface DynamicLayoutRendererProps {
+ nodes: LayoutComponentNode[]
+ context: LayoutContext
+}
+
+export function DynamicLayoutRenderer({ nodes, context } : DynamicLayoutRendererProps) {
+ return nodes.map((entry, index) => {
+ const [key, value] = Object.entries(entry)[0] as [string, any]
+ const component = componentMap[key]
+ if (!component) return null
+ return {component.render(value, context)}
+ })
+}
\ No newline at end of file