Compare commits

...

13 Commits

Author SHA1 Message Date
seb
ccceb8fe78 Add Mollie payment integration to CartTab and OrderProcessingService
- Introduced Mollie payment method in CartTab, allowing users to select it alongside existing payment options.
- Implemented loading and error handling for the Mollie component.
- Updated OrderProcessingService to create Mollie payment intents.
- Adjusted PaymentMethodSelector to switch to Mollie when specific delivery methods are selected.
- Enhanced CartTab to store cart items for Mollie payments and calculate total amounts accordingly.
2025-07-06 02:21:52 +02:00
seb
ea5ac762b2 Update .gitignore to exclude 404 page and enhance prerendering logic to support pagination for category-specific LLM files. Implemented generateAllCategoryLlmsPages function to create multiple paginated files, improving product catalog clarity and organization. 2025-07-05 16:20:00 +02:00
seb
40ec0287fd Refactor prerendering logic to include PrerenderSitemap component, enabling category data handling for the sitemap page. Update Sitemap component to initialize categories and loading state more efficiently, improving performance and clarity. 2025-07-05 16:13:38 +02:00
seb
47364d3ad8 Implement caching mechanism in Sitemap component to optimize category data retrieval. Added functions to check for cached product data and initialize categories from cache, improving performance for prerendered environments and reducing unnecessary socket requests. 2025-07-05 16:06:42 +02:00
seb
a6d7ed3e27 Refactor Sitemap component to improve context usage by replacing direct socket reference with context destructuring. This change enhances code clarity and maintains functionality for fetching category data. 2025-07-05 15:57:00 +02:00
seb
f8f03b45b8 Refactor SEO module to utilize a modular structure by re-exporting all SEO functions from a new index file. This change maintains backward compatibility while streamlining the codebase for future enhancements. 2025-07-05 15:52:34 +02:00
seb
eb0d5621e6 Enhance SEO text generation for product categories by implementing pagination support. Updated generateCategoryLlmsTxt to include page navigation and product count details, improving clarity for users accessing product catalogs. Added helper function generateAllCategoryLlmsPages to facilitate the creation of multiple category pages. 2025-07-05 15:41:36 +02:00
seb
f81b9d12df Refactor pagination visibility in ProductList component to prevent layout shifts when no products are available. Updated logic to use CSS visibility instead of conditional rendering. 2025-07-05 15:21:38 +02:00
seb
8ea3b1b6a3 Enhance ProductList component by adding conditional rendering for pagination and no products message based on active filters. Implement helper function for product count text to improve clarity in product display. 2025-07-05 15:18:55 +02:00
seb
fb3450aa23 Implement availability filters in Content and ProductList components to enhance product filtering functionality. Active filters for "auf Lager", "Neu", and "Bald verfügbar" are now dynamically displayed based on product availability, improving user experience and filtering accuracy. 2025-07-05 13:57:29 +02:00
seb
11f5b2cbfd Update package dependencies to latest versions, enhancing compatibility and performance. Adjust start scripts in package.json to include NODE_OPTIONS for deprecation warnings. Add new launch configuration in VSCode for easier development setup. Enhance product filtering logic in Content component to ensure new product filters are only applied when applicable. 2025-07-05 13:48:42 +02:00
seb
5fc0c3213b Enhance responsive design in profile components by adjusting padding and layout properties for improved mobile usability. Updated styles in ButtonGroup, CartTab, OrdersTab, SettingsTab, and ProfilePage to ensure better visual consistency across different screen sizes. 2025-07-04 05:01:39 +02:00
seb
8abf64ca38 Enhance responsive design across components by adjusting padding, margins, and layout properties for better mobile usability. Updated styles in Content, Header, Product, ProductFilters, ProductList, and SearchBar components to improve visual consistency and user experience on various screen sizes. 2025-07-04 04:51:15 +02:00
31 changed files with 2402 additions and 1336 deletions

1
.gitignore vendored
View File

@@ -27,6 +27,7 @@
/public/index.prerender.html
/public/Konfigurator
/public/profile
/public/404
/public/products.xml
/public/llms*

7
.vscode/launch.json vendored
View File

@@ -3,6 +3,7 @@
// This will install dependencies before starting the dev server
"version": "0.2.0",
"configurations": [
{
"type": "node-terminal",
"name": "Start with API propxy to seedheads.de (Install Deps)",
@@ -10,6 +11,12 @@
"command": "npm run start:seedheads",
"preLaunchTask": "npm: install",
"cwd": "${workspaceFolder}"
}, {
"type": "node-terminal",
"name": "Start",
"request": "launch",
"command": "npm run start",
"cwd": "${workspaceFolder}"
}
]
}

327
package-lock.json generated
View File

@@ -106,9 +106,9 @@
}
},
"node_modules/@babel/compat-data": {
"version": "7.27.5",
"resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.27.5.tgz",
"integrity": "sha512-KiRAp/VoJaWkkte84TvUd9qjdbZAdiqyvMxrGl1N6vzFogKmaLgoM3L1kgtLicp2HP5fBJS8JrZKLVIZGVJAVg==",
"version": "7.28.0",
"resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.0.tgz",
"integrity": "sha512-60X7qkglvrap8mn1lh2ebxXdZYtUcpd7gsmy9kLaBJ4i/WdY8PqTSdxyA8qraikqKQK5C1KRBKXqznrVapyNaw==",
"dev": true,
"license": "MIT",
"engines": {
@@ -116,22 +116,22 @@
}
},
"node_modules/@babel/core": {
"version": "7.27.4",
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.27.4.tgz",
"integrity": "sha512-bXYxrXFubeYdvB0NhD/NBB3Qi6aZeV20GOWVI47t2dkecCEoneR4NPVcb7abpXDEvejgrUfFtG6vG/zxAKmg+g==",
"version": "7.28.0",
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.0.tgz",
"integrity": "sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@ampproject/remapping": "^2.2.0",
"@babel/code-frame": "^7.27.1",
"@babel/generator": "^7.27.3",
"@babel/generator": "^7.28.0",
"@babel/helper-compilation-targets": "^7.27.2",
"@babel/helper-module-transforms": "^7.27.3",
"@babel/helpers": "^7.27.4",
"@babel/parser": "^7.27.4",
"@babel/helpers": "^7.27.6",
"@babel/parser": "^7.28.0",
"@babel/template": "^7.27.2",
"@babel/traverse": "^7.27.4",
"@babel/types": "^7.27.3",
"@babel/traverse": "^7.28.0",
"@babel/types": "^7.28.0",
"convert-source-map": "^2.0.0",
"debug": "^4.1.0",
"gensync": "^1.0.0-beta.2",
@@ -147,9 +147,9 @@
}
},
"node_modules/@babel/eslint-parser": {
"version": "7.27.5",
"resolved": "https://registry.npmjs.org/@babel/eslint-parser/-/eslint-parser-7.27.5.tgz",
"integrity": "sha512-HLkYQfRICudzcOtjGwkPvGc5nF1b4ljLZh1IRDj50lRZ718NAKVgQpIAUX8bfg6u/yuSKY3L7E0YzIV+OxrB8Q==",
"version": "7.28.0",
"resolved": "https://registry.npmjs.org/@babel/eslint-parser/-/eslint-parser-7.28.0.tgz",
"integrity": "sha512-N4ntErOlKvcbTt01rr5wj3y55xnIdx1ymrfIr8C2WnM1Y9glFgWaGDEULJIazOX3XM9NRzhfJ6zZnQ1sBNWU+w==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -166,15 +166,15 @@
}
},
"node_modules/@babel/generator": {
"version": "7.27.5",
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.27.5.tgz",
"integrity": "sha512-ZGhA37l0e/g2s1Cnzdix0O3aLYm66eF8aufiVteOgnwxgnRP8GoyMj7VWsgWnQbVKXyge7hqrFh2K2TQM6t1Hw==",
"version": "7.28.0",
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.0.tgz",
"integrity": "sha512-lJjzvrbEeWrhB4P3QBsH7tey117PjLZnDbLiQEKjQ/fNJTjuq4HSqgFA+UNSwZT8D7dxxbnuSBMsa1lrWzKlQg==",
"license": "MIT",
"dependencies": {
"@babel/parser": "^7.27.5",
"@babel/types": "^7.27.3",
"@jridgewell/gen-mapping": "^0.3.5",
"@jridgewell/trace-mapping": "^0.3.25",
"@babel/parser": "^7.28.0",
"@babel/types": "^7.28.0",
"@jridgewell/gen-mapping": "^0.3.12",
"@jridgewell/trace-mapping": "^0.3.28",
"jsesc": "^3.0.2"
},
"engines": {
@@ -252,22 +252,31 @@
}
},
"node_modules/@babel/helper-define-polyfill-provider": {
"version": "0.6.4",
"resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.4.tgz",
"integrity": "sha512-jljfR1rGnXXNWnmQg2K3+bvhkxB51Rl32QRaOTuwwjviGrHzIbSc8+x9CpraDtbT7mfyjXObULP4w/adunNwAw==",
"version": "0.6.5",
"resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.5.tgz",
"integrity": "sha512-uJnGFcPsWQK8fvjgGP5LZUZZsYGIoPeRjSF5PGwrelYgq7Q15/Ft9NGFp1zglwgIv//W0uG4BevRuSJRyylZPg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-compilation-targets": "^7.22.6",
"@babel/helper-plugin-utils": "^7.22.5",
"debug": "^4.1.1",
"@babel/helper-compilation-targets": "^7.27.2",
"@babel/helper-plugin-utils": "^7.27.1",
"debug": "^4.4.1",
"lodash.debounce": "^4.0.8",
"resolve": "^1.14.2"
"resolve": "^1.22.10"
},
"peerDependencies": {
"@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0"
}
},
"node_modules/@babel/helper-globals": {
"version": "7.28.0",
"resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz",
"integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==",
"license": "MIT",
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helper-member-expression-to-functions": {
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.27.1.tgz",
@@ -444,12 +453,12 @@
}
},
"node_modules/@babel/parser": {
"version": "7.27.5",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.5.tgz",
"integrity": "sha512-OsQd175SxWkGlzbny8J3K8TnnDD0N3lrIUtB92xwyRpzaenGZhxDvxN/JgU00U3CDZNj9tPuDJ5H0WS4Nt3vKg==",
"version": "7.28.0",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.0.tgz",
"integrity": "sha512-jVZGvOxOuNSsuQuLRTh13nU0AogFlw32w/MT+LV6D3sP5WdbW61E77RnkbaO2dUvmPAYrBDJXGn5gGS6tH4j8g==",
"license": "MIT",
"dependencies": {
"@babel/types": "^7.27.3"
"@babel/types": "^7.28.0"
},
"bin": {
"parser": "bin/babel-parser.js"
@@ -637,15 +646,15 @@
}
},
"node_modules/@babel/plugin-transform-async-generator-functions": {
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.27.1.tgz",
"integrity": "sha512-eST9RrwlpaoJBDHShc+DS2SG4ATTi2MYNb4OxYkf3n+7eb49LWpnS+HSpVfW4x927qQwgk8A2hGNVaajAEw0EA==",
"version": "7.28.0",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.28.0.tgz",
"integrity": "sha512-BEOdvX4+M765icNPZeidyADIvQ1m1gmunXufXxvRESy/jNNyfovIqUyE7MVgGBjWktCoJlzvFA1To2O4ymIO3Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.27.1",
"@babel/helper-remap-async-to-generator": "^7.27.1",
"@babel/traverse": "^7.27.1"
"@babel/traverse": "^7.28.0"
},
"engines": {
"node": ">=6.9.0"
@@ -689,9 +698,9 @@
}
},
"node_modules/@babel/plugin-transform-block-scoping": {
"version": "7.27.5",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.27.5.tgz",
"integrity": "sha512-JF6uE2s67f0y2RZcm2kpAUEbD50vH62TyWVebxwHAlbSdM49VqPz8t4a1uIjp4NIOIZ4xzLfjY5emt/RCyC7TQ==",
"version": "7.28.0",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.28.0.tgz",
"integrity": "sha512-gKKnwjpdx5sER/wl0WN0efUBFzF/56YZO0RJrSYP4CljXnP31ByY7fol89AzomdlLNzI36AvOTmYHsnZTCkq8Q==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -739,18 +748,18 @@
}
},
"node_modules/@babel/plugin-transform-classes": {
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.27.1.tgz",
"integrity": "sha512-7iLhfFAubmpeJe/Wo2TVuDrykh/zlWXLzPNdL0Jqn/Xu8R3QQ8h9ff8FQoISZOsw74/HFqFI7NX63HN7QFIHKA==",
"version": "7.28.0",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.28.0.tgz",
"integrity": "sha512-IjM1IoJNw72AZFlj33Cu8X0q2XK/6AaVC3jQu+cgQ5lThWD5ajnuUAml80dqRmOhmPkTH8uAwnpMu9Rvj0LTRA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-annotate-as-pure": "^7.27.1",
"@babel/helper-compilation-targets": "^7.27.1",
"@babel/helper-annotate-as-pure": "^7.27.3",
"@babel/helper-compilation-targets": "^7.27.2",
"@babel/helper-globals": "^7.28.0",
"@babel/helper-plugin-utils": "^7.27.1",
"@babel/helper-replace-supers": "^7.27.1",
"@babel/traverse": "^7.27.1",
"globals": "^11.1.0"
"@babel/traverse": "^7.28.0"
},
"engines": {
"node": ">=6.9.0"
@@ -759,16 +768,6 @@
"@babel/core": "^7.0.0-0"
}
},
"node_modules/@babel/plugin-transform-classes/node_modules/globals": {
"version": "11.12.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz",
"integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=4"
}
},
"node_modules/@babel/plugin-transform-computed-properties": {
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.27.1.tgz",
@@ -787,13 +786,14 @@
}
},
"node_modules/@babel/plugin-transform-destructuring": {
"version": "7.27.3",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.27.3.tgz",
"integrity": "sha512-s4Jrok82JpiaIprtY2nHsYmrThKvvwgHwjgd7UMiYhZaN0asdXNLr0y+NjTfkA7SyQE5i2Fb7eawUOZmLvyqOA==",
"version": "7.28.0",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.28.0.tgz",
"integrity": "sha512-v1nrSMBiKcodhsyJ4Gf+Z0U/yawmJDBOTpEB3mcQY52r9RIyPneGyAS/yM6seP/8I+mWI3elOMtT5dB8GJVs+A==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.27.1"
"@babel/helper-plugin-utils": "^7.27.1",
"@babel/traverse": "^7.28.0"
},
"engines": {
"node": ">=6.9.0"
@@ -868,6 +868,23 @@
"@babel/core": "^7.0.0-0"
}
},
"node_modules/@babel/plugin-transform-explicit-resource-management": {
"version": "7.28.0",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-explicit-resource-management/-/plugin-transform-explicit-resource-management-7.28.0.tgz",
"integrity": "sha512-K8nhUcn3f6iB+P3gwCv/no7OdzOZQcKchW6N389V6PD8NUWKZHzndOd9sPDVbMoBsbmjMqlB4L9fm+fEFNVlwQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.27.1",
"@babel/plugin-transform-destructuring": "^7.28.0"
},
"engines": {
"node": ">=6.9.0"
},
"peerDependencies": {
"@babel/core": "^7.0.0-0"
}
},
"node_modules/@babel/plugin-transform-exponentiation-operator": {
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.27.1.tgz",
@@ -1135,16 +1152,17 @@
}
},
"node_modules/@babel/plugin-transform-object-rest-spread": {
"version": "7.27.3",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.27.3.tgz",
"integrity": "sha512-7ZZtznF9g4l2JCImCo5LNKFHB5eXnN39lLtLY5Tg+VkR0jwOt7TBciMckuiQIOIW7L5tkQOCh3bVGYeXgMx52Q==",
"version": "7.28.0",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.28.0.tgz",
"integrity": "sha512-9VNGikXxzu5eCiQjdE4IZn8sb9q7Xsk5EXLDBKUYg1e/Tve8/05+KJEtcxGxAgCY5t/BpKQM+JEL/yT4tvgiUA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-compilation-targets": "^7.27.2",
"@babel/helper-plugin-utils": "^7.27.1",
"@babel/plugin-transform-destructuring": "^7.27.3",
"@babel/plugin-transform-parameters": "^7.27.1"
"@babel/plugin-transform-destructuring": "^7.28.0",
"@babel/plugin-transform-parameters": "^7.27.7",
"@babel/traverse": "^7.28.0"
},
"engines": {
"node": ">=6.9.0"
@@ -1204,9 +1222,9 @@
}
},
"node_modules/@babel/plugin-transform-parameters": {
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.27.1.tgz",
"integrity": "sha512-018KRk76HWKeZ5l4oTj2zPpSh+NbGdt0st5S6x0pga6HgrjBOJb24mMDHorFopOOd6YHkLgOZ+zaCjZGPO4aKg==",
"version": "7.27.7",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.27.7.tgz",
"integrity": "sha512-qBkYTYCb76RRxUM6CcZA5KRu8K4SM8ajzVeUgVdMVO9NN9uI/GaVmBg/WKJJGnNokV9SY8FxNOVWGXzqzUidBg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1340,9 +1358,9 @@
}
},
"node_modules/@babel/plugin-transform-regenerator": {
"version": "7.27.5",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.27.5.tgz",
"integrity": "sha512-uhB8yHerfe3MWnuLAhEbeQ4afVoqv8BQsPqrTv7e/jZ9y00kJL6l9a/f4OWaKxotmjzewfEyXE1vgDJenkQ2/Q==",
"version": "7.28.0",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.28.0.tgz",
"integrity": "sha512-LOAozRVbqxEVjSKfhGnuLoE4Kz4Oc5UJzuvFUhSsQzdCdaAQu06mG8zDv2GFSerM62nImUZ7K92vxnQcLSDlCQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1537,13 +1555,13 @@
}
},
"node_modules/@babel/preset-env": {
"version": "7.27.2",
"resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.27.2.tgz",
"integrity": "sha512-Ma4zSuYSlGNRlCLO+EAzLnCmJK2vdstgv+n7aUP+/IKZrOfWHOJVdSJtuub8RzHTj3ahD37k5OKJWvzf16TQyQ==",
"version": "7.28.0",
"resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.28.0.tgz",
"integrity": "sha512-VmaxeGOwuDqzLl5JUkIRM1X2Qu2uKGxHEQWh+cvvbl7JuJRgKGJSfsEF/bUaxFhJl/XAyxBe7q7qSuTbKFuCyg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/compat-data": "^7.27.2",
"@babel/compat-data": "^7.28.0",
"@babel/helper-compilation-targets": "^7.27.2",
"@babel/helper-plugin-utils": "^7.27.1",
"@babel/helper-validator-option": "^7.27.1",
@@ -1557,19 +1575,20 @@
"@babel/plugin-syntax-import-attributes": "^7.27.1",
"@babel/plugin-syntax-unicode-sets-regex": "^7.18.6",
"@babel/plugin-transform-arrow-functions": "^7.27.1",
"@babel/plugin-transform-async-generator-functions": "^7.27.1",
"@babel/plugin-transform-async-generator-functions": "^7.28.0",
"@babel/plugin-transform-async-to-generator": "^7.27.1",
"@babel/plugin-transform-block-scoped-functions": "^7.27.1",
"@babel/plugin-transform-block-scoping": "^7.27.1",
"@babel/plugin-transform-block-scoping": "^7.28.0",
"@babel/plugin-transform-class-properties": "^7.27.1",
"@babel/plugin-transform-class-static-block": "^7.27.1",
"@babel/plugin-transform-classes": "^7.27.1",
"@babel/plugin-transform-classes": "^7.28.0",
"@babel/plugin-transform-computed-properties": "^7.27.1",
"@babel/plugin-transform-destructuring": "^7.27.1",
"@babel/plugin-transform-destructuring": "^7.28.0",
"@babel/plugin-transform-dotall-regex": "^7.27.1",
"@babel/plugin-transform-duplicate-keys": "^7.27.1",
"@babel/plugin-transform-duplicate-named-capturing-groups-regex": "^7.27.1",
"@babel/plugin-transform-dynamic-import": "^7.27.1",
"@babel/plugin-transform-explicit-resource-management": "^7.28.0",
"@babel/plugin-transform-exponentiation-operator": "^7.27.1",
"@babel/plugin-transform-export-namespace-from": "^7.27.1",
"@babel/plugin-transform-for-of": "^7.27.1",
@@ -1586,15 +1605,15 @@
"@babel/plugin-transform-new-target": "^7.27.1",
"@babel/plugin-transform-nullish-coalescing-operator": "^7.27.1",
"@babel/plugin-transform-numeric-separator": "^7.27.1",
"@babel/plugin-transform-object-rest-spread": "^7.27.2",
"@babel/plugin-transform-object-rest-spread": "^7.28.0",
"@babel/plugin-transform-object-super": "^7.27.1",
"@babel/plugin-transform-optional-catch-binding": "^7.27.1",
"@babel/plugin-transform-optional-chaining": "^7.27.1",
"@babel/plugin-transform-parameters": "^7.27.1",
"@babel/plugin-transform-parameters": "^7.27.7",
"@babel/plugin-transform-private-methods": "^7.27.1",
"@babel/plugin-transform-private-property-in-object": "^7.27.1",
"@babel/plugin-transform-property-literals": "^7.27.1",
"@babel/plugin-transform-regenerator": "^7.27.1",
"@babel/plugin-transform-regenerator": "^7.28.0",
"@babel/plugin-transform-regexp-modifiers": "^7.27.1",
"@babel/plugin-transform-reserved-words": "^7.27.1",
"@babel/plugin-transform-shorthand-properties": "^7.27.1",
@@ -1607,10 +1626,10 @@
"@babel/plugin-transform-unicode-regex": "^7.27.1",
"@babel/plugin-transform-unicode-sets-regex": "^7.27.1",
"@babel/preset-modules": "0.1.6-no-external-plugins",
"babel-plugin-polyfill-corejs2": "^0.4.10",
"babel-plugin-polyfill-corejs3": "^0.11.0",
"babel-plugin-polyfill-regenerator": "^0.6.1",
"core-js-compat": "^3.40.0",
"babel-plugin-polyfill-corejs2": "^0.4.14",
"babel-plugin-polyfill-corejs3": "^0.13.0",
"babel-plugin-polyfill-regenerator": "^0.6.5",
"core-js-compat": "^3.43.0",
"semver": "^6.3.1"
},
"engines": {
@@ -1700,36 +1719,27 @@
}
},
"node_modules/@babel/traverse": {
"version": "7.27.4",
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.27.4.tgz",
"integrity": "sha512-oNcu2QbHqts9BtOWJosOVJapWjBDSxGCpFvikNR5TGDYDQf3JwpIoMzIKrvfoti93cLfPJEG4tH9SPVeyCGgdA==",
"version": "7.28.0",
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.0.tgz",
"integrity": "sha512-mGe7UK5wWyh0bKRfupsUchrQGqvDbZDbKJw+kcRGSmdHVYrv+ltd0pnpDTVpiTqnaBru9iEvA8pz8W46v0Amwg==",
"license": "MIT",
"dependencies": {
"@babel/code-frame": "^7.27.1",
"@babel/generator": "^7.27.3",
"@babel/parser": "^7.27.4",
"@babel/generator": "^7.28.0",
"@babel/helper-globals": "^7.28.0",
"@babel/parser": "^7.28.0",
"@babel/template": "^7.27.2",
"@babel/types": "^7.27.3",
"debug": "^4.3.1",
"globals": "^11.1.0"
"@babel/types": "^7.28.0",
"debug": "^4.3.1"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/traverse/node_modules/globals": {
"version": "11.12.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz",
"integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==",
"license": "MIT",
"engines": {
"node": ">=4"
}
},
"node_modules/@babel/types": {
"version": "7.27.6",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.6.tgz",
"integrity": "sha512-ETyHEk2VHHvl9b9jZP5IHPavHYk57EhanlRRuae9XCpb/j5bDCbPPMOBfCWhnl/7EDJz0jEMCi/RhccCE8r1+Q==",
"version": "7.28.0",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.0.tgz",
"integrity": "sha512-jYnje+JyZG5YThjHiF28oT4SIZLnYOcSBb6+SDaFIyzDVSkXQmQQYclJ2R+YxcdmK0AX6x1E5OQNtuh3jHDrUg==",
"license": "MIT",
"dependencies": {
"@babel/helper-string-parser": "^7.27.1",
@@ -2708,17 +2718,13 @@
}
},
"node_modules/@jridgewell/gen-mapping": {
"version": "0.3.8",
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz",
"integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==",
"version": "0.3.12",
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.12.tgz",
"integrity": "sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg==",
"license": "MIT",
"dependencies": {
"@jridgewell/set-array": "^1.2.1",
"@jridgewell/sourcemap-codec": "^1.4.10",
"@jridgewell/sourcemap-codec": "^1.5.0",
"@jridgewell/trace-mapping": "^0.3.24"
},
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/@jridgewell/resolve-uri": {
@@ -2730,15 +2736,6 @@
"node": ">=6.0.0"
}
},
"node_modules/@jridgewell/set-array": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz",
"integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==",
"license": "MIT",
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/@jridgewell/source-map": {
"version": "0.3.6",
"resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.6.tgz",
@@ -2757,9 +2754,9 @@
"license": "MIT"
},
"node_modules/@jridgewell/trace-mapping": {
"version": "0.3.25",
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz",
"integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==",
"version": "0.3.29",
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.29.tgz",
"integrity": "sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==",
"license": "MIT",
"dependencies": {
"@jridgewell/resolve-uri": "^3.1.0",
@@ -3118,9 +3115,9 @@
}
},
"node_modules/@pmmmwh/react-refresh-webpack-plugin": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/@pmmmwh/react-refresh-webpack-plugin/-/react-refresh-webpack-plugin-0.6.0.tgz",
"integrity": "sha512-AAc+QWfZ1KQ/e1C6OHWVlxU+ks6zFGOA44IJUlvju7RlDS8nsX6poPFOIlsg/rTofO9vKov12+WCjMhKkRKD5g==",
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/@pmmmwh/react-refresh-webpack-plugin/-/react-refresh-webpack-plugin-0.6.1.tgz",
"integrity": "sha512-95DXXJxNkpYu+sqmpDp7vbw9JCyiNpHuCsvuMuOgVFrKQlwEIn9Y1+NNIQJq+zFL+eWyxw6htthB5CtdwJupNA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -4188,14 +4185,14 @@
}
},
"node_modules/babel-plugin-polyfill-corejs2": {
"version": "0.4.13",
"resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.13.tgz",
"integrity": "sha512-3sX/eOms8kd3q2KZ6DAhKPc0dgm525Gqq5NtWKZ7QYYZEv57OQ54KtblzJzH1lQF/eQxO8KjWGIK9IPUJNus5g==",
"version": "0.4.14",
"resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.14.tgz",
"integrity": "sha512-Co2Y9wX854ts6U8gAAPXfn0GmAyctHuK8n0Yhfjd6t30g7yvKjspvvOo9yG+z52PZRgFErt7Ka2pYnXCjLKEpg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/compat-data": "^7.22.6",
"@babel/helper-define-polyfill-provider": "^0.6.4",
"@babel/compat-data": "^7.27.7",
"@babel/helper-define-polyfill-provider": "^0.6.5",
"semver": "^6.3.1"
},
"peerDependencies": {
@@ -4203,27 +4200,27 @@
}
},
"node_modules/babel-plugin-polyfill-corejs3": {
"version": "0.11.1",
"resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.11.1.tgz",
"integrity": "sha512-yGCqvBT4rwMczo28xkH/noxJ6MZ4nJfkVYdoDaC/utLtWrXxv27HVrzAeSbqR8SxDsp46n0YF47EbHoixy6rXQ==",
"version": "0.13.0",
"resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.13.0.tgz",
"integrity": "sha512-U+GNwMdSFgzVmfhNm8GJUX88AadB3uo9KpJqS3FaqNIPKgySuvMb+bHPsOmmuWyIcuqZj/pzt1RUIUZns4y2+A==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-define-polyfill-provider": "^0.6.3",
"core-js-compat": "^3.40.0"
"@babel/helper-define-polyfill-provider": "^0.6.5",
"core-js-compat": "^3.43.0"
},
"peerDependencies": {
"@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0"
}
},
"node_modules/babel-plugin-polyfill-regenerator": {
"version": "0.6.4",
"resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.4.tgz",
"integrity": "sha512-7gD3pRadPrbjhjLyxebmx/WrFYcuSjZ0XbdUujQMZ/fcE9oeewk2U/7PCvez84UeuK3oSjmPZ0Ch0dlupQvGzw==",
"version": "0.6.5",
"resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.5.tgz",
"integrity": "sha512-ISqQ2frbiNU9vIJkzg7dlPpznPZ4jOiUQ1uSmB0fEHeowtN3COYRsXr/xexn64NpU13P06jc/L5TgiJXOgrbEg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-define-polyfill-provider": "^0.6.4"
"@babel/helper-define-polyfill-provider": "^0.6.5"
},
"peerDependencies": {
"@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0"
@@ -4317,9 +4314,9 @@
"license": "ISC"
},
"node_modules/brace-expansion": {
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
"version": "1.1.12",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -4341,9 +4338,9 @@
}
},
"node_modules/browserslist": {
"version": "4.24.4",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.4.tgz",
"integrity": "sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==",
"version": "4.25.1",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.1.tgz",
"integrity": "sha512-KGj0KoOMXLpSNkkEI6Z6mShmQy0bc1I+T7K9N81k4WWMrfz+6fQ6es80B/YLAeRoKvjYE1YSHHOW1qe9xIVzHw==",
"dev": true,
"funding": [
{
@@ -4361,10 +4358,10 @@
],
"license": "MIT",
"dependencies": {
"caniuse-lite": "^1.0.30001688",
"electron-to-chromium": "^1.5.73",
"caniuse-lite": "^1.0.30001726",
"electron-to-chromium": "^1.5.173",
"node-releases": "^2.0.19",
"update-browserslist-db": "^1.1.1"
"update-browserslist-db": "^1.1.3"
},
"bin": {
"browserslist": "cli.js"
@@ -4477,9 +4474,9 @@
}
},
"node_modules/caniuse-lite": {
"version": "1.0.30001715",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001715.tgz",
"integrity": "sha512-7ptkFGMm2OAOgvZpwgA4yjQ5SQbrNVGdRjzH0pBdy1Fasvcr+KAeECmbCAECzTuDuoX0FCY8KzUxjf9+9kfZEw==",
"version": "1.0.30001726",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001726.tgz",
"integrity": "sha512-VQAUIUzBiZ/UnlM28fSp2CRF3ivUn1BWEvxMcVTNwpw91Py1pGbPIyIKtd+tzct9C3ouceCVdGAXxZOpZAsgdw==",
"dev": true,
"funding": [
{
@@ -4815,13 +4812,13 @@
"license": "MIT"
},
"node_modules/core-js-compat": {
"version": "3.41.0",
"resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.41.0.tgz",
"integrity": "sha512-RFsU9LySVue9RTwdDVX/T0e2Y6jRYWXERKElIjpuEOEnxaXffI0X7RUwVzfYLfzuLXSNJDYoRYUAmRUcyln20A==",
"version": "3.43.0",
"resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.43.0.tgz",
"integrity": "sha512-2GML2ZsCc5LR7hZYz4AXmjQw8zuy2T//2QntwdnpuYI7jteT6GVYJL7F6C2C57R7gSYrcqVW3lAALefdbhBLDA==",
"dev": true,
"license": "MIT",
"dependencies": {
"browserslist": "^4.24.4"
"browserslist": "^4.25.0"
},
"funding": {
"type": "opencollective",
@@ -5141,9 +5138,9 @@
"license": "MIT"
},
"node_modules/debug": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
"integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==",
"version": "4.4.1",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
"integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==",
"license": "MIT",
"dependencies": {
"ms": "^2.1.3"
@@ -5478,9 +5475,9 @@
"license": "MIT"
},
"node_modules/electron-to-chromium": {
"version": "1.5.139",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.139.tgz",
"integrity": "sha512-GGnRYOTdN5LYpwbIr0rwP/ZHOQSvAF6TG0LSzp28uCBb9JiXHJGmaaKw29qjNJc5bGnnp6kXJqRnGMQoELwi5w==",
"version": "1.5.179",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.179.tgz",
"integrity": "sha512-UWKi/EbBopgfFsc5k61wFpV7WrnnSlSzW/e2XcBmS6qKYTivZlLtoll5/rdqRTxGglGHkmkW0j0pFNJG10EUIQ==",
"dev": true,
"license": "ISC"
},

View File

@@ -4,8 +4,8 @@
"type": "module",
"main": "index.js",
"scripts": {
"start": "webpack serve --progress --mode development --no-open",
"start:seedheads": "cross-env PROXY_TARGET=https://seedheads.de webpack serve --progress --mode development --no-open",
"start": "cross-env NODE_OPTIONS=\"--no-deprecation\" webpack serve --progress --mode development --no-open",
"start:seedheads": "cross-env PROXY_TARGET=https://seedheads.de NODE_OPTIONS=\"--no-deprecation\" webpack serve --progress --mode development --no-open",
"prod": "webpack serve --progress --mode production --no-client-overlay --no-client --no-web-socket-server --no-open --no-live-reload --no-hot --compress --no-devtool",
"build:client": "cross-env NODE_ENV=production webpack --progress --mode production && shx cp dist/index.html dist/index_template.html",
"build": "npm run build:client",

View File

@@ -50,6 +50,7 @@ const {
generateProductsXml,
generateLlmsTxt,
generateCategoryLlmsTxt,
generateAllCategoryLlmsPages,
} = require("./prerender/seo.cjs");
const {
fetchCategoryProducts,
@@ -71,6 +72,7 @@ const Batteriegesetzhinweise =
require("./src/pages/Batteriegesetzhinweise.js").default;
const Widerrufsrecht = require("./src/pages/Widerrufsrecht.js").default;
const Sitemap = require("./src/pages/Sitemap.js").default;
const PrerenderSitemap = require("./src/PrerenderSitemap.js").default;
const AGB = require("./src/pages/AGB.js").default;
const NotFound404 = require("./src/pages/NotFound404.js").default;
@@ -361,10 +363,11 @@ const renderApp = async (categoryData, socket) => {
description: "Widerrufsrecht page",
},
{
component: Sitemap,
component: PrerenderSitemap,
path: "/sitemap",
filename: "sitemap",
description: "Sitemap page",
needsCategoryData: true,
},
{ component: AGB, path: "/agb", filename: "agb", description: "AGB page" },
{ component: NotFound404, path: "/404", filename: "404", description: "404 Not Found page" },
@@ -384,7 +387,9 @@ const renderApp = async (categoryData, socket) => {
let staticPagesRendered = 0;
for (const page of staticPages) {
const pageComponent = React.createElement(page.component, null);
// Pass category data as props if needed
const pageProps = page.needsCategoryData ? { categoryData } : null;
const pageComponent = React.createElement(page.component, pageProps);
let metaTags = "";
// Special handling for Sitemap page to include category data
@@ -657,27 +662,39 @@ const renderApp = async (categoryData, socket) => {
productsByCategory[categoryId].push(product);
});
// Generate category-specific LLM files
// Generate category-specific LLM files with pagination
let categoryFilesGenerated = 0;
let totalCategoryProducts = 0;
let totalPaginatedFiles = 0;
for (const category of allCategories) {
if (category.seoName) {
const categoryProducts = productsByCategory[category.id] || [];
const categorySlug = category.seoName.toLowerCase().replace(/[^a-z0-9]/g, '-');
const categoryLlmsTxt = generateCategoryLlmsTxt(category, categoryProducts, shopConfig.baseUrl, shopConfig);
const categoryLlmsTxtPath = path.resolve(__dirname, config.outputDir, `llms-${categorySlug}.txt`);
// Generate all paginated files for this category
const categoryPages = generateAllCategoryLlmsPages(category, categoryProducts, shopConfig.baseUrl, shopConfig);
fs.writeFileSync(categoryLlmsTxtPath, categoryLlmsTxt, { encoding: 'utf8' });
// Write each paginated file
for (const page of categoryPages) {
const pagePath = path.resolve(__dirname, config.outputDir, page.fileName);
fs.writeFileSync(pagePath, page.content, { encoding: 'utf8' });
totalPaginatedFiles++;
}
console.log(` ✅ llms-${categorySlug}.txt - ${categoryProducts.length} products (${Math.round(categoryLlmsTxt.length / 1024)}KB)`);
const pageCount = categoryPages.length;
const totalSize = categoryPages.reduce((sum, page) => sum + page.content.length, 0);
console.log(` ✅ llms-${categorySlug}-page-*.txt - ${categoryProducts.length} products across ${pageCount} pages (${Math.round(totalSize / 1024)}KB total)`);
categoryFilesGenerated++;
totalCategoryProducts += categoryProducts.length;
}
}
console.log(` 📄 Total paginated files generated: ${totalPaginatedFiles}`);
console.log(` 📦 Total products across all categories: ${totalCategoryProducts}`);
try {
const verification = fs.readFileSync(llmsTxtPath, 'utf8');
console.log(` - File verification: ✅ All files valid UTF-8`);

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,81 @@
const generateCategoryJsonLd = (category, products = [], baseUrl, config) => {
const categoryUrl = `${baseUrl}/Kategorie/${category.seoName}`;
const jsonLd = {
"@context": "https://schema.org/",
"@type": "CollectionPage",
name: category.name,
url: categoryUrl,
description: `${category.name} - Entdecken Sie unsere Auswahl an hochwertigen Produkten`,
breadcrumb: {
"@type": "BreadcrumbList",
itemListElement: [
{
"@type": "ListItem",
position: 1,
name: "Home",
item: baseUrl,
},
{
"@type": "ListItem",
position: 2,
name: category.name,
item: categoryUrl,
},
],
},
};
// Add product list if products are available
if (products && products.length > 0) {
jsonLd.mainEntity = {
"@type": "ItemList",
numberOfItems: products.length,
itemListElement: products.slice(0, 20).map((product, index) => ({
"@type": "ListItem",
position: index + 1,
item: {
"@type": "Product",
name: product.name,
url: `${baseUrl}/Artikel/${product.seoName}`,
image:
product.pictureList && product.pictureList.trim()
? `${baseUrl}/assets/images/prod${product.pictureList
.split(",")[0]
.trim()}.jpg`
: `${baseUrl}/assets/images/nopicture.jpg`,
description: product.description
? product.description.replace(/<[^>]*>/g, "").substring(0, 200)
: `${product.name} - Hochwertiges Growshop Produkt`,
sku: product.articleNumber || product.seoName,
brand: {
"@type": "Brand",
name: product.manufacturer || config.brandName,
},
offers: {
"@type": "Offer",
url: `${baseUrl}/Artikel/${product.seoName}`,
price: product.price && !isNaN(product.price) ? product.price.toString() : "0.00",
priceCurrency: config.currency,
availability: product.available
? "https://schema.org/InStock"
: "https://schema.org/OutOfStock",
seller: {
"@type": "Organization",
name: config.brandName,
},
itemCondition: "https://schema.org/NewCondition",
},
},
})),
};
}
return `<script type="application/ld+json">${JSON.stringify(
jsonLd
)}</script>`;
};
module.exports = {
generateCategoryJsonLd,
};

344
prerender/seo/feeds.cjs Normal file
View File

@@ -0,0 +1,344 @@
const generateRobotsTxt = (baseUrl) => {
// Ensure URLs are properly formatted
const canonicalUrl = baseUrl.endsWith('/') ? baseUrl.slice(0, -1) : baseUrl;
const robotsTxt = `User-agent: *
Allow: /
Sitemap: ${canonicalUrl}/sitemap.xml
Crawl-delay: 0
`;
return robotsTxt;
};
const generateProductsXml = (allProductsData = [], baseUrl, config) => {
const currentDate = new Date().toISOString();
// Validate input
if (!Array.isArray(allProductsData) || allProductsData.length === 0) {
throw new Error("No valid product data provided");
}
// Category mapping function
const getGoogleProductCategory = (categoryId) => {
const categoryMappings = {
// Seeds & Plants
689: "Home & Garden > Plants > Seeds",
706: "Home & Garden > Plants", // Stecklinge (cuttings)
376: "Home & Garden > Plants > Plant & Herb Growing Kits", // Grow-Sets
// Headshop & Accessories
709: "Arts & Entertainment > Hobbies & Creative Arts", // Headshop
711: "Arts & Entertainment > Hobbies & Creative Arts", // Bongs
714: "Arts & Entertainment > Hobbies & Creative Arts", // Zubehör
748: "Arts & Entertainment > Hobbies & Creative Arts", // Köpfe
749: "Arts & Entertainment > Hobbies & Creative Arts", // Chillums / Diffusoren / Kupplungen
896: "Electronics > Electronics Accessories", // Vaporizer
710: "Home & Garden > Kitchen & Dining > Kitchen Tools & Utensils", // Grinder
// Measuring & Packaging
186: "Business & Industrial", // Wiegen & Verpacken
187: "Business & Industrial > Science & Laboratory > Lab Equipment", // Waagen
346: "Home & Garden > Kitchen & Dining > Food Storage", // Vakuumbeutel
355: "Home & Garden > Kitchen & Dining > Food Storage", // Boveda & Integra Boost
407: "Home & Garden > Kitchen & Dining > Food Storage", // Grove Bags
449: "Home & Garden > Kitchen & Dining > Food Storage", // Cliptütchen
539: "Home & Garden > Kitchen & Dining > Food Storage", // Gläser & Dosen
// Lighting & Equipment
694: "Home & Garden > Lighting", // Lampen
261: "Home & Garden > Lighting", // Lampenzubehör
// Plants & Growing
691: "Home & Garden > Lawn & Garden > Fertilizers", // Dünger
692: "Home & Garden > Lawn & Garden > Fertilizers", // Dünger - Zubehör
693: "Sporting Goods > Outdoor Recreation > Camping & Hiking > Tents", // Zelte
// Pots & Containers
219: "Home & Garden > Decor > Planters & Pots", // Töpfe
220: "Home & Garden > Decor > Planters & Pots", // Untersetzer
301: "Home & Garden > Decor > Planters & Pots", // Stofftöpfe
317: "Home & Garden > Decor > Planters & Pots", // Air-Pot
364: "Home & Garden > Decor > Planters & Pots", // Kunststofftöpfe
292: "Home & Garden > Decor > Planters & Pots", // Trays & Fluttische
// Ventilation & Climate
703: "Home & Garden > Outdoor Power Tools", // Abluft-Sets
247: "Home & Garden > Outdoor Power Tools", // Belüftung
214: "Home & Garden > Outdoor Power Tools", // Umluft-Ventilatoren
308: "Home & Garden > Outdoor Power Tools", // Ab- und Zuluft
609: "Home & Garden > Outdoor Power Tools", // Schalldämpfer
248: "Home & Garden > Pool & Spa > Pool & Spa Filters", // Aktivkohlefilter
392: "Home & Garden > Pool & Spa > Pool & Spa Filters", // Zuluftfilter
658: "Home & Garden > Climate Control > Dehumidifiers", // Luftbe- und entfeuchter
310: "Home & Garden > Climate Control > Heating", // Heizmatten
379: "Home & Garden > Household Supplies > Air Fresheners", // Geruchsneutralisation
// Irrigation & Watering
221: "Home & Garden > Lawn & Garden > Watering Equipment", // Bewässerung
250: "Home & Garden > Lawn & Garden > Watering Equipment", // Schläuche
297: "Home & Garden > Lawn & Garden > Watering Equipment", // Pumpen
354: "Home & Garden > Lawn & Garden > Watering Equipment", // Sprüher
372: "Home & Garden > Lawn & Garden > Watering Equipment", // AutoPot
389: "Home & Garden > Lawn & Garden > Watering Equipment", // Blumat
405: "Home & Garden > Lawn & Garden > Watering Equipment", // Schläuche
425: "Home & Garden > Lawn & Garden > Watering Equipment", // Wassertanks
480: "Home & Garden > Lawn & Garden > Watering Equipment", // Tropfer
519: "Home & Garden > Lawn & Garden > Watering Equipment", // Pumpsprüher
// Growing Media & Soils
242: "Home & Garden > Lawn & Garden > Fertilizers", // Böden
243: "Home & Garden > Lawn & Garden > Fertilizers", // Erde
269: "Home & Garden > Lawn & Garden > Fertilizers", // Kokos
580: "Home & Garden > Lawn & Garden > Fertilizers", // Perlite & Blähton
// Propagation & Starting
286: "Home & Garden > Plants", // Anzucht
298: "Home & Garden > Plants", // Steinwolltrays
421: "Home & Garden > Plants", // Vermehrungszubehör
489: "Home & Garden > Plants", // EazyPlug & Jiffy
359: "Home & Garden > Outdoor Structures > Greenhouses", // Gewächshäuser
// Tools & Equipment
373: "Home & Garden > Tools > Hand Tools", // GrowTool
403: "Home & Garden > Kitchen & Dining > Kitchen Tools & Utensils", // Messbecher & mehr
259: "Home & Garden > Tools > Hand Tools", // Pressen
280: "Home & Garden > Tools > Hand Tools", // Erntescheeren
258: "Home & Garden > Tools", // Ernte & Verarbeitung
278: "Home & Garden > Tools", // Extraktion
302: "Home & Garden > Tools", // Erntemaschinen
// Hardware & Plumbing
222: "Hardware > Plumbing", // PE-Teile
374: "Hardware > Plumbing > Plumbing Fittings", // Verbindungsteile
// Electronics & Control
314: "Electronics > Electronics Accessories", // Steuergeräte
408: "Electronics > Electronics Accessories", // GrowControl
344: "Business & Industrial > Science & Laboratory > Lab Equipment", // Messgeräte
555: "Business & Industrial > Science & Laboratory > Lab Equipment > Microscopes", // Mikroskope
// Camping & Outdoor
226: "Sporting Goods > Outdoor Recreation > Camping & Hiking", // Zeltzubehör
// Plant Care & Protection
239: "Home & Garden > Lawn & Garden > Pest Control", // Pflanzenschutz
240: "Home & Garden > Plants", // Anbauzubehör
// Office & Media
424: "Office Supplies > Labels", // Etiketten & Schilder
387: "Media > Books", // Literatur
// General categories
705: "Home & Garden", // Set-Konfigurator
686: "Home & Garden", // Zubehör
741: "Home & Garden", // Zubehör
294: "Home & Garden", // Zubehör
695: "Home & Garden", // Zubehör
293: "Home & Garden", // Trockennetze
4: "Home & Garden", // Sonstiges
450: "Home & Garden", // Restposten
};
return categoryMappings[categoryId] || "Home & Garden > Plants";
};
let productsXml = `<?xml version="1.0" encoding="UTF-8"?>
<rss xmlns:g="http://base.google.com/ns/1.0" version="2.0">
<channel>
<title>${config.descriptions.short}</title>
<link>${baseUrl}</link>
<description>${config.descriptions.short}</description>
<lastBuildDate>${currentDate}</lastBuildDate>
<language>${config.language}</language>`;
// Helper function to clean text content of problematic characters
const cleanTextContent = (text) => {
if (!text) return "";
return text.toString()
// Remove HTML tags
.replace(/<[^>]*>/g, "")
// Remove non-printable characters and control characters
.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F-\x9F]/g, '')
// Remove BOM and other Unicode formatting characters
.replace(/[\uFEFF\u200B-\u200D\u2060]/g, '')
// Replace multiple whitespace with single space
.replace(/\s+/g, ' ')
// Remove leading/trailing whitespace
.trim();
};
// Helper function to properly escape XML content and remove invalid characters
const escapeXml = (unsafe) => {
if (!unsafe) return "";
// Convert to string and remove invalid XML characters
const cleaned = unsafe.toString()
// Remove control characters except tab (0x09), newline (0x0A), and carriage return (0x0D)
.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, '')
// Remove invalid Unicode characters and surrogates
.replace(/[\uD800-\uDFFF]/g, '')
// Remove other problematic characters
.replace(/[\uFFFE\uFFFF]/g, '')
// Normalize whitespace
.replace(/\s+/g, ' ')
.trim();
// Escape XML entities
return cleaned
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&apos;");
};
let processedCount = 0;
let skippedCount = 0;
// Category IDs to skip (seeds, plants, headshop items)
const skipCategoryIds = [689, 706, 709, 711, 714, 748, 749, 896, 710];
// Add each product as an item
allProductsData.forEach((product, index) => {
try {
// Skip products without essential data
if (!product || !product.seoName) {
skippedCount++;
return;
}
// Skip products from excluded categories
const productCategoryId = product.categoryId || product.category_id || product.category || null;
if (productCategoryId && skipCategoryIds.includes(parseInt(productCategoryId))) {
skippedCount++;
return;
}
// Skip products without GTIN
if (!product.gtin || !product.gtin.toString().trim()) {
skippedCount++;
return;
}
// Skip products without pictures
if (!product.pictureList || !product.pictureList.trim()) {
skippedCount++;
return;
}
// Clean description for feed (remove HTML tags and limit length)
const rawDescription = product.description
? cleanTextContent(product.description).substring(0, 500)
: `${product.name || 'Product'} - Art.-Nr.: ${product.articleNumber || 'N/A'}`;
const cleanDescription = escapeXml(rawDescription) || "Produktbeschreibung nicht verfügbar";
// Clean product name
const rawName = product.name || "Unnamed Product";
const cleanName = escapeXml(cleanTextContent(rawName)) || "Unnamed Product";
// Validate essential fields
if (!cleanName || cleanName.length < 2) {
skippedCount++;
return;
}
// Generate product URL
const productUrl = `${baseUrl}/Artikel/${encodeURIComponent(product.seoName)}`;
// Generate image URL
const imageUrl = product.pictureList && product.pictureList.trim()
? `${baseUrl}/assets/images/prod${product.pictureList.split(",")[0].trim()}.jpg`
: `${baseUrl}/assets/images/nopicture.jpg`;
// Generate brand (manufacturer)
const rawBrand = product.manufacturer || config.brandName;
const brand = escapeXml(cleanTextContent(rawBrand));
// Generate condition (always new for this type of shop)
const condition = "new";
// Generate availability
const availability = product.available ? "in stock" : "out of stock";
// Generate price (ensure it's a valid number)
const price = product.price && !isNaN(product.price)
? `${parseFloat(product.price).toFixed(2)} ${config.currency}`
: `0.00 ${config.currency}`;
// Skip products with price == 0
if (!product.price || parseFloat(product.price) === 0) {
skippedCount++;
return;
}
// Generate GTIN/EAN if available
const gtin = product.gtin ? escapeXml(product.gtin.toString().trim()) : null;
// Generate product ID (using articleNumber or seoName)
const rawProductId = product.articleNumber || product.seoName || `product_${Date.now()}_${Math.random().toString(36).substr(2, 5)}`;
const productId = escapeXml(rawProductId.toString().trim()) || `fallback_${Date.now()}_${Math.random().toString(36).substr(2, 5)}`;
// Get Google product category based on product's category ID
const categoryId = product.categoryId || product.category_id || product.category || null;
const googleCategory = getGoogleProductCategory(categoryId);
const escapedGoogleCategory = escapeXml(googleCategory);
// Build item XML with proper formatting
productsXml += `
<item>
<g:id>${productId}</g:id>
<g:title>${cleanName}</g:title>
<g:description>${cleanDescription}</g:description>
<g:link>${productUrl}</g:link>
<g:image_link>${imageUrl}</g:image_link>
<g:condition>${condition}</g:condition>
<g:availability>${availability}</g:availability>
<g:price>${price}</g:price>
<g:shipping>
<g:country>${config.country}</g:country>
<g:service>${config.shipping.defaultService}</g:service>
<g:price>${config.shipping.defaultCost}</g:price>
</g:shipping>
<g:brand>${brand}</g:brand>
<g:google_product_category>${escapedGoogleCategory}</g:google_product_category>
<g:product_type>Gartenbedarf</g:product_type>`;
// Add GTIN if available
if (gtin && gtin.trim()) {
productsXml += `
<g:gtin>${gtin}</g:gtin>`;
}
// Add weight if available
if (product.weight && !isNaN(product.weight)) {
productsXml += `
<g:shipping_weight>${parseFloat(product.weight).toFixed(2)} g</g:shipping_weight>`;
}
productsXml += `
</item>`;
processedCount++;
} catch (itemError) {
console.log(` ⚠️ Skipped product ${index + 1}: ${itemError.message}`);
skippedCount++;
}
});
productsXml += `
</channel>
</rss>`;
console.log(` 📊 Processing summary: ${processedCount} products included, ${skippedCount} skipped`);
return productsXml;
};
module.exports = {
generateRobotsTxt,
generateProductsXml,
};

215
prerender/seo/homepage.cjs Normal file
View File

@@ -0,0 +1,215 @@
const generateHomepageMetaTags = (baseUrl, config) => {
const description = config.descriptions.long;
const keywords = config.keywords;
const imageUrl = `${baseUrl}${config.images.logo}`;
// Ensure URLs are properly formatted
const canonicalUrl = baseUrl.endsWith('/') ? baseUrl.slice(0, -1) : baseUrl;
return `
<!-- SEO Meta Tags -->
<meta name="description" content="${description}">
<meta name="keywords" content="${keywords}">
<!-- Open Graph Meta Tags -->
<meta property="og:title" content="${config.descriptions.short}">
<meta property="og:description" content="${description}">
<meta property="og:image" content="${imageUrl}">
<meta property="og:url" content="${canonicalUrl}">
<meta property="og:type" content="website">
<meta property="og:site_name" content="${config.siteName}">
<!-- Twitter Card Meta Tags -->
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="${config.descriptions.short}">
<meta name="twitter:description" content="${description}">
<meta name="twitter:image" content="${imageUrl}">
<!-- Additional Meta Tags -->
<meta name="robots" content="index, follow">
<link rel="canonical" href="${canonicalUrl}">
`;
};
const generateHomepageJsonLd = (baseUrl, config, categories = []) => {
// Ensure URLs are properly formatted
const canonicalUrl = baseUrl.endsWith('/') ? baseUrl.slice(0, -1) : baseUrl;
const logoUrl = `${canonicalUrl}${config.images.logo}`;
const websiteJsonLd = {
"@context": "https://schema.org/",
"@type": "WebSite",
name: config.brandName,
url: canonicalUrl,
description: config.descriptions.long,
publisher: {
"@type": "Organization",
name: config.brandName,
url: canonicalUrl,
logo: {
"@type": "ImageObject",
url: logoUrl,
},
},
potentialAction: {
"@type": "SearchAction",
target: `${canonicalUrl}/search?q={search_term_string}`,
query: "required name=search_term_string"
},
mainEntity: {
"@type": "WebPage",
name: "Sitemap",
url: `${canonicalUrl}/sitemap`,
description: "Vollständige Sitemap mit allen Kategorien und Seiten",
},
sameAs: [
// Add your social media URLs here if available
],
};
// Organization/LocalBusiness Schema for rich results
const organizationJsonLd = {
"@context": "https://schema.org",
"@type": "LocalBusiness",
"name": config.brandName,
"alternateName": config.siteName,
"description": config.descriptions.long,
"url": canonicalUrl,
"logo": logoUrl,
"image": logoUrl,
"telephone": "015208491860",
"email": "service@growheads.de",
"address": {
"@type": "PostalAddress",
"streetAddress": "Trachenberger Strasse 14",
"addressLocality": "Dresden",
"postalCode": "01129",
"addressCountry": "DE",
"addressRegion": "Sachsen"
},
"geo": {
"@type": "GeoCoordinates",
"latitude": "51.083675",
"longitude": "13.727215"
},
"openingHours": [
"Mo-Fr 10:00:00-20:00:00",
"Sa 11:00:00-19:00:00"
],
"paymentAccepted": "Cash, Credit Card, PayPal, Bank Transfer",
"currenciesAccepted": "EUR",
"priceRange": "€€",
"areaServed": {
"@type": "Country",
"name": "Germany"
},
"contactPoint": [
{
"@type": "ContactPoint",
"telephone": "015208491860",
"contactType": "customer service",
"availableLanguage": "German",
"hoursAvailable": {
"@type": "OpeningHoursSpecification",
"dayOfWeek": ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday"],
"opens": "10:00:00",
"closes": "20:00:00"
}
},
{
"@type": "ContactPoint",
"email": "service@growheads.de",
"contactType": "customer service",
"availableLanguage": "German"
}
],
"sameAs": [
// Add social media URLs when available
// "https://www.facebook.com/growheads",
// "https://www.instagram.com/growheads"
]
};
// FAQPage Schema for common questions
const faqJsonLd = {
"@context": "https://schema.org",
"@type": "FAQPage",
"mainEntity": [
{
"@type": "Question",
"name": "Welche Zahlungsmethoden akzeptiert GrowHeads?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Wir akzeptieren Kreditkarten, PayPal, Banküberweisung, Nachnahme und Barzahlung bei Abholung in unserem Laden in Dresden."
}
},
{
"@type": "Question",
"name": "Liefert GrowHeads deutschlandweit?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Ja, wir liefern deutschlandweit. Zusätzlich haben wir einen Laden in Dresden (Trachenberger Strasse 14) für lokale Kunden."
}
},
{
"@type": "Question",
"name": "Welche Produkte bietet GrowHeads?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Wir bieten ein komplettes Sortiment für den Indoor-Anbau: Beleuchtung, Belüftung, Dünger, Töpfe, Zelte, Messgeräte und vieles mehr für professionelle Zuchtanlagen."
}
},
{
"@type": "Question",
"name": "Hat GrowHeads einen physischen Laden?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Ja, unser Laden befindet sich in Dresden, Trachenberger Strasse 14. Öffnungszeiten: Mo-Fr 10-20 Uhr, Sa 11-19 Uhr. Sie können auch online bestellen."
}
},
{
"@type": "Question",
"name": "Bietet GrowHeads Beratung zum Indoor-Anbau?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Ja, unser erfahrenes Team berät Sie gerne zu allen Aspekten des Indoor-Anbaus. Kontaktieren Sie uns telefonisch unter 015208491860 oder besuchen Sie unseren Laden."
}
}
]
};
// Generate ItemList for all categories (more appropriate for homepage)
const categoriesListJsonLd = {
"@context": "https://schema.org",
"@type": "ItemList",
"name": "Produktkategorien",
"description": "Alle verfügbaren Produktkategorien in unserem Online-Shop",
"numberOfItems": categories.filter(category => category.seoName).length,
"itemListElement": categories
.filter(category => category.seoName) // Only include categories with seoName
.map((category, index) => ({
"@type": "ListItem",
"position": index + 1,
"item": {
"@type": "Thing",
"name": category.name,
"url": `${canonicalUrl}/Kategorie/${category.seoName}`
}
}))
};
// Return all JSON-LD scripts
const websiteScript = `<script type="application/ld+json">${JSON.stringify(websiteJsonLd)}</script>`;
const organizationScript = `<script type="application/ld+json">${JSON.stringify(organizationJsonLd)}</script>`;
const faqScript = `<script type="application/ld+json">${JSON.stringify(faqJsonLd)}</script>`;
const categoriesScript = categories.length > 0
? `<script type="application/ld+json">${JSON.stringify(categoriesListJsonLd)}</script>`
: '';
return websiteScript + '\n' + organizationScript + '\n' + faqScript + (categoriesScript ? '\n' + categoriesScript : '');
};
module.exports = {
generateHomepageMetaTags,
generateHomepageJsonLd,
};

64
prerender/seo/index.cjs Normal file
View File

@@ -0,0 +1,64 @@
// Import all SEO functions from their respective modules
const {
generateProductMetaTags,
generateProductJsonLd,
} = require('./product.cjs');
const {
generateCategoryJsonLd,
} = require('./category.cjs');
const {
generateHomepageMetaTags,
generateHomepageJsonLd,
} = require('./homepage.cjs');
const {
generateSitemapJsonLd,
generateXmlSitemap,
} = require('./sitemap.cjs');
const {
generateKonfiguratorMetaTags,
} = require('./konfigurator.cjs');
const {
generateRobotsTxt,
generateProductsXml,
} = require('./feeds.cjs');
const {
generateLlmsTxt,
generateCategoryLlmsTxt,
generateAllCategoryLlmsPages,
} = require('./llms.cjs');
// Export all functions for use in the main application
module.exports = {
// Product functions
generateProductMetaTags,
generateProductJsonLd,
// Category functions
generateCategoryJsonLd,
// Homepage functions
generateHomepageMetaTags,
generateHomepageJsonLd,
// Sitemap functions
generateSitemapJsonLd,
generateXmlSitemap,
// Konfigurator functions
generateKonfiguratorMetaTags,
// Feed/Export functions
generateRobotsTxt,
generateProductsXml,
// LLMs/AI functions
generateLlmsTxt,
generateCategoryLlmsTxt,
generateAllCategoryLlmsPages,
};

View File

@@ -0,0 +1,36 @@
const generateKonfiguratorMetaTags = (baseUrl, config) => {
const description = "Unser interaktiver Growbox Konfigurator hilft dir dabei, das perfekte Indoor Growing Setup zusammenzustellen. Wähle aus verschiedenen Growbox-Größen, Beleuchtung, Belüftung und Extras. Bundle-Rabatte bis 36%!";
const keywords = "Growbox Konfigurator, Indoor Growing, Growzelt, Beleuchtung, Belüftung, Growbox Setup, Indoor Garden";
// Ensure URLs are properly formatted
const canonicalUrl = baseUrl.endsWith('/') ? baseUrl.slice(0, -1) : baseUrl;
const imageUrl = `${canonicalUrl}${config.images.placeholder}`; // Placeholder image
return `
<!-- SEO Meta Tags -->
<meta name="description" content="${description}">
<meta name="keywords" content="${keywords}">
<!-- Open Graph Meta Tags -->
<meta property="og:title" content="Growbox Konfigurator - Stelle dein perfektes Indoor Grow Setup zusammen">
<meta property="og:description" content="${description}">
<meta property="og:image" content="${imageUrl}">
<meta property="og:url" content="${canonicalUrl}/Konfigurator">
<meta property="og:type" content="website">
<meta property="og:site_name" content="${config.siteName}">
<!-- Twitter Card Meta Tags -->
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="Growbox Konfigurator - Indoor Grow Setup">
<meta name="twitter:description" content="${description}">
<meta name="twitter:image" content="${imageUrl}">
<!-- Additional Meta Tags -->
<meta name="robots" content="index, follow">
<link rel="canonical" href="${canonicalUrl}/Konfigurator">
`;
};
module.exports = {
generateKonfiguratorMetaTags,
};

277
prerender/seo/llms.cjs Normal file
View File

@@ -0,0 +1,277 @@
const generateLlmsTxt = (allCategories = [], allProductsData = [], baseUrl, config) => {
const currentDate = new Date().toISOString().split("T")[0]; // YYYY-MM-DD format
// Group products by category for statistics
const productsByCategory = {};
allProductsData.forEach((product) => {
const categoryId = product.categoryId || 'uncategorized';
if (!productsByCategory[categoryId]) {
productsByCategory[categoryId] = [];
}
productsByCategory[categoryId].push(product);
});
// Find category names for organization
const categoryMap = {};
allCategories.forEach((cat) => {
categoryMap[cat.id] = cat.name;
});
let llmsTxt = `# ${config.siteName} - Site Map for LLMs
Generated: ${currentDate}
Base URL: ${baseUrl}
## About ${config.brandName}
GrowHeads.de is a German online shop and local store in Dresden specializing in high-quality seeds, plants, and gardening supplies for cannabis cultivation.
## Site Structure
### Static Pages
- **Home** - ${baseUrl}/
- **Datenschutz (Privacy Policy)** - ${baseUrl}/datenschutz
- **Impressum (Legal Notice)** - ${baseUrl}/impressum
- **AGB (Terms & Conditions)** - ${baseUrl}/agb
- **Widerrufsrecht (Right of Withdrawal)** - ${baseUrl}/widerrufsrecht
- **Batteriegesetzhinweise (Battery Law Notice)** - ${baseUrl}/batteriegesetzhinweise
- **Sitemap** - ${baseUrl}/sitemap
- **Growbox Konfigurator** - ${baseUrl}/Konfigurator - Interactive tool to configure grow box setups with bundle discounts
- **Profile** - ${baseUrl}/profile - User account and order management
### Site Features
- **Language**: German (${config.language})
- **Currency**: ${config.currency} (Euro)
- **Shipping**: ${config.country}
- **Payment Methods**: Credit Cards, PayPal, Bank Transfer, Cash on Delivery, Cash on Pickup
### Product Categories (${allCategories.length} categories)
`;
// Add categories with links to their detailed LLM files
allCategories.forEach((category) => {
if (category.seoName) {
const productCount = productsByCategory[category.id]?.length || 0;
const categorySlug = category.seoName.toLowerCase().replace(/[^a-z0-9]/g, '-');
const productsPerPage = 50;
const totalPages = Math.ceil(productCount / productsPerPage);
llmsTxt += `#### ${category.name} (${productCount} products)`;
if (totalPages > 1) {
llmsTxt += `
- **Product Catalog**: ${totalPages} pages available
- **Page 1**: ${baseUrl}/llms-${categorySlug}-page-1.txt (Products 1-${Math.min(productsPerPage, productCount)})`;
if (totalPages > 2) {
llmsTxt += `
- **Page 2**: ${baseUrl}/llms-${categorySlug}-page-2.txt (Products ${productsPerPage + 1}-${Math.min(productsPerPage * 2, productCount)})`;
}
if (totalPages > 3) {
llmsTxt += `
- **...**: Additional pages available`;
}
if (totalPages > 2) {
llmsTxt += `
- **Page ${totalPages}**: ${baseUrl}/llms-${categorySlug}-page-${totalPages}.txt (Products ${((totalPages - 1) * productsPerPage) + 1}-${productCount})`;
}
llmsTxt += `
- **Access Pattern**: Replace "page-X" with desired page number (1-${totalPages})`;
} else if (productCount > 0) {
llmsTxt += `
- **Product Catalog**: ${baseUrl}/llms-${categorySlug}-page-1.txt`;
} else {
llmsTxt += `
- **Product Catalog**: No products available`;
}
llmsTxt += `
`;
}
});
llmsTxt += `
---
*This sitemap is automatically generated during the site build process and includes all publicly accessible content. For technical inquiries, please refer to our contact information in the Impressum.*
`;
return llmsTxt;
};
const generateCategoryLlmsTxt = (category, categoryProducts = [], baseUrl, config, pageNumber = 1, productsPerPage = 50) => {
const currentDate = new Date().toISOString().split("T")[0]; // YYYY-MM-DD format
const categorySlug = category.seoName.toLowerCase().replace(/[^a-z0-9]/g, '-');
// Calculate pagination
const totalProducts = categoryProducts.length;
const totalPages = Math.ceil(totalProducts / productsPerPage);
const startIndex = (pageNumber - 1) * productsPerPage;
const endIndex = Math.min(startIndex + productsPerPage, totalProducts);
const pageProducts = categoryProducts.slice(startIndex, endIndex);
let categoryLlmsTxt = `# ${category.name} - Product Catalog (Page ${pageNumber} of ${totalPages})
Generated: ${currentDate}
Base URL: ${baseUrl}
Category: ${category.name} (ID: ${category.id})
Category URL: ${baseUrl}/Kategorie/${category.seoName}
## Category Overview
This file contains products ${startIndex + 1}-${endIndex} of ${totalProducts} in the "${category.name}" category from ${config.siteName}.
**Statistics:**
- **Total Products in Category**: ${totalProducts}
- **Products on This Page**: ${pageProducts.length}
- **Current Page**: ${pageNumber} of ${totalPages}
- **Category ID**: ${category.id}
- **Category URL**: ${baseUrl}/Kategorie/${category.seoName}
- **Back to Main Sitemap**: ${baseUrl}/llms.txt
`;
// Add navigation hints for LLMs
if (totalPages > 1) {
categoryLlmsTxt += `## Navigation for LLMs
**How to access other pages in this category:**
`;
if (pageNumber > 1) {
categoryLlmsTxt += `- **Previous Page**: ${baseUrl}/llms-${categorySlug}-page-${pageNumber - 1}.txt
`;
}
if (pageNumber < totalPages) {
categoryLlmsTxt += `- **Next Page**: ${baseUrl}/llms-${categorySlug}-page-${pageNumber + 1}.txt
`;
}
categoryLlmsTxt += `- **First Page**: ${baseUrl}/llms-${categorySlug}-page-1.txt
- **Last Page**: ${baseUrl}/llms-${categorySlug}-page-${totalPages}.txt
**All pages in this category:**
`;
for (let i = 1; i <= totalPages; i++) {
categoryLlmsTxt += `- **Page ${i}**: ${baseUrl}/llms-${categorySlug}-page-${i}.txt (Products ${((i-1) * productsPerPage) + 1}-${Math.min(i * productsPerPage, totalProducts)})
`;
}
categoryLlmsTxt += `
`;
}
if (pageProducts.length > 0) {
pageProducts.forEach((product, index) => {
if (product.seoName) {
// Clean description for markdown (remove HTML tags and limit length)
const cleanDescription = product.description
? product.description
.replace(/<[^>]*>/g, "")
.replace(/\n/g, " ")
.trim()
.substring(0, 300)
: "";
const globalIndex = startIndex + index + 1;
categoryLlmsTxt += `## ${globalIndex}. ${product.name}
- **Product URL**: ${baseUrl}/Artikel/${product.seoName}
- **Article Number**: ${product.articleNumber || 'N/A'}
- **Price**: €${product.price || '0.00'}
- **Brand**: ${product.manufacturer || config.brandName}
- **Availability**: ${product.available ? 'In Stock' : 'Out of Stock'}`;
if (product.gtin) {
categoryLlmsTxt += `
- **GTIN**: ${product.gtin}`;
}
if (product.weight && !isNaN(product.weight)) {
categoryLlmsTxt += `
- **Weight**: ${product.weight}g`;
}
if (cleanDescription) {
categoryLlmsTxt += `
**Description:**
${cleanDescription}${product.description && product.description.length > 300 ? '...' : ''}`;
}
categoryLlmsTxt += `
---
`;
}
});
} else {
categoryLlmsTxt += `## No Products Available
This category currently contains no products.
`;
}
// Add footer navigation for convenience
if (totalPages > 1) {
categoryLlmsTxt += `## Page Navigation
`;
if (pageNumber > 1) {
categoryLlmsTxt += `← [Previous Page](${baseUrl}/llms-${categorySlug}-page-${pageNumber - 1}.txt) | `;
}
categoryLlmsTxt += `[Category Overview](${baseUrl}/llms-${categorySlug}-page-1.txt)`;
if (pageNumber < totalPages) {
categoryLlmsTxt += ` | [Next Page](${baseUrl}/llms-${categorySlug}-page-${pageNumber + 1}.txt) →`;
}
categoryLlmsTxt += `
`;
}
categoryLlmsTxt += `---
*This category product list is automatically generated during the site build process. Product availability and pricing are updated in real-time on the main website.*
`;
return categoryLlmsTxt;
};
// Helper function to generate all pages for a category
const generateAllCategoryLlmsPages = (category, categoryProducts = [], baseUrl, config, productsPerPage = 50) => {
const totalProducts = categoryProducts.length;
const totalPages = Math.ceil(totalProducts / productsPerPage);
const pages = [];
for (let pageNumber = 1; pageNumber <= totalPages; pageNumber++) {
const pageContent = generateCategoryLlmsTxt(category, categoryProducts, baseUrl, config, pageNumber, productsPerPage);
const categorySlug = category.seoName.toLowerCase().replace(/[^a-z0-9]/g, '-');
const fileName = `llms-${categorySlug}-page-${pageNumber}.txt`;
pages.push({
fileName,
content: pageContent,
pageNumber,
totalPages
});
}
return pages;
};
module.exports = {
generateLlmsTxt,
generateCategoryLlmsTxt,
generateAllCategoryLlmsPages,
};

135
prerender/seo/product.cjs Normal file
View File

@@ -0,0 +1,135 @@
const generateProductMetaTags = (product, baseUrl, config) => {
const productUrl = `${baseUrl}/Artikel/${product.seoName}`;
const imageUrl =
product.pictureList && product.pictureList.trim()
? `${baseUrl}/assets/images/prod${product.pictureList
.split(",")[0]
.trim()}.jpg`
: `${baseUrl}/assets/images/nopicture.jpg`;
// Clean description for meta (remove HTML tags and limit length)
const cleanDescription = product.description
? product.description
.replace(/<[^>]*>/g, "")
.replace(/\n/g, " ")
.substring(0, 160)
: `${product.name} - Art.-Nr.: ${product.articleNumber}`;
return `
<!-- SEO Meta Tags -->
<meta name="description" content="${cleanDescription}">
<meta name="keywords" content="${product.name}, ${
product.manufacturer || ""
}, ${product.articleNumber}">
<!-- Open Graph Meta Tags -->
<meta property="og:title" content="${product.name}">
<meta property="og:description" content="${cleanDescription}">
<meta property="og:image" content="${imageUrl}">
<meta property="og:url" content="${productUrl}">
<meta property="og:type" content="product">
<meta property="og:site_name" content="${config.siteName}">
<meta property="product:price:amount" content="${product.price}">
<meta property="product:price:currency" content="${config.currency}">
<meta property="product:availability" content="${
product.available ? "in stock" : "out of stock"
}">
${product.gtin ? `<meta property="product:gtin" content="${product.gtin}">` : ''}
${product.articleNumber ? `<meta property="product:retailer_item_id" content="${product.articleNumber}">` : ''}
${product.manufacturer ? `<meta property="product:brand" content="${product.manufacturer}">` : ''}
<!-- Twitter Card Meta Tags -->
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="${product.name}">
<meta name="twitter:description" content="${cleanDescription}">
<meta name="twitter:image" content="${imageUrl}">
<!-- Additional Meta Tags -->
<meta name="robots" content="index, follow">
<link rel="canonical" href="${productUrl}">
`;
};
const generateProductJsonLd = (product, baseUrl, config, categoryInfo = null) => {
const productUrl = `${baseUrl}/Artikel/${product.seoName}`;
const imageUrl =
product.pictureList && product.pictureList.trim()
? `${baseUrl}/assets/images/prod${product.pictureList
.split(",")[0]
.trim()}.jpg`
: `${baseUrl}/assets/images/nopicture.jpg`;
// Clean description for JSON-LD (remove HTML tags)
const cleanDescription = product.description
? product.description.replace(/<[^>]*>/g, "").replace(/\n/g, " ")
: product.name;
// Calculate price valid date (current date + 3 months)
const priceValidDate = new Date();
priceValidDate.setMonth(priceValidDate.getMonth() + 3);
const jsonLd = {
"@context": "https://schema.org/",
"@type": "Product",
name: product.name,
image: [imageUrl],
description: cleanDescription,
sku: product.articleNumber,
...(product.gtin && { gtin: product.gtin }),
brand: {
"@type": "Brand",
name: product.manufacturer || "Unknown",
},
offers: {
"@type": "Offer",
url: productUrl,
priceCurrency: config.currency,
price: product.price.toString(),
priceValidUntil: priceValidDate.toISOString().split("T")[0],
itemCondition: "https://schema.org/NewCondition",
availability: product.available
? "https://schema.org/InStock"
: "https://schema.org/OutOfStock",
seller: {
"@type": "Organization",
name: config.brandName,
},
},
};
// Add breadcrumb if category information is available
if (categoryInfo && categoryInfo.name && categoryInfo.seoName) {
jsonLd.breadcrumb = {
"@type": "BreadcrumbList",
itemListElement: [
{
"@type": "ListItem",
position: 1,
name: "Home",
item: baseUrl,
},
{
"@type": "ListItem",
position: 2,
name: categoryInfo.name,
item: `${baseUrl}/Kategorie/${categoryInfo.seoName}`,
},
{
"@type": "ListItem",
position: 3,
name: product.name,
item: productUrl,
},
],
};
}
return `<script type="application/ld+json">${JSON.stringify(
jsonLd
)}</script>`;
};
module.exports = {
generateProductMetaTags,
generateProductJsonLd,
};

117
prerender/seo/sitemap.cjs Normal file
View File

@@ -0,0 +1,117 @@
const generateSitemapJsonLd = (allCategories = [], baseUrl, config) => {
const jsonLd = {
"@context": "https://schema.org/",
"@type": "WebPage",
name: "Sitemap",
url: `${baseUrl}/sitemap`,
description: `Sitemap - Übersicht aller Kategorien und Seiten auf ${config.siteName}`,
breadcrumb: {
"@type": "BreadcrumbList",
itemListElement: [
{
"@type": "ListItem",
position: 1,
name: "Home",
item: baseUrl,
},
{
"@type": "ListItem",
position: 2,
name: "Sitemap",
item: `${baseUrl}/sitemap`,
},
],
},
};
// Add all categories as site navigation elements
if (allCategories && allCategories.length > 0) {
jsonLd.mainEntity = {
"@type": "SiteNavigationElement",
name: "Kategorien",
hasPart: allCategories.map((category) => ({
"@type": "SiteNavigationElement",
name: category.name,
url: `${baseUrl}/Kategorie/${category.seoName}`,
description: `${category.name} Kategorie`,
})),
};
}
return `<script type="application/ld+json">${JSON.stringify(
jsonLd
)}</script>`;
};
const generateXmlSitemap = (allCategories = [], allProducts = [], baseUrl) => {
const currentDate = new Date().toISOString().split("T")[0]; // YYYY-MM-DD format
let sitemap = `<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
`;
// Homepage
sitemap += ` <url>
<loc>${baseUrl}/</loc>
<lastmod>${currentDate}</lastmod>
<changefreq>daily</changefreq>
<priority>1.0</priority>
</url>
`;
// Static pages
const staticPages = [
{ path: "/datenschutz", changefreq: "monthly", priority: "0.3" },
{ path: "/impressum", changefreq: "monthly", priority: "0.3" },
{ path: "/batteriegesetzhinweise", changefreq: "monthly", priority: "0.3" },
{ path: "/widerrufsrecht", changefreq: "monthly", priority: "0.3" },
{ path: "/sitemap", changefreq: "weekly", priority: "0.5" },
{ path: "/agb", changefreq: "monthly", priority: "0.3" },
{ path: "/404", changefreq: "monthly", priority: "0.1" },
{ path: "/Konfigurator", changefreq: "weekly", priority: "0.8" },
];
staticPages.forEach((page) => {
sitemap += ` <url>
<loc>${baseUrl}${page.path}</loc>
<lastmod>${currentDate}</lastmod>
<changefreq>${page.changefreq}</changefreq>
<priority>${page.priority}</priority>
</url>
`;
});
// Category pages
allCategories.forEach((category) => {
if (category.seoName) {
sitemap += ` <url>
<loc>${baseUrl}/Kategorie/${category.seoName}</loc>
<lastmod>${currentDate}</lastmod>
<changefreq>weekly</changefreq>
<priority>0.8</priority>
</url>
`;
}
});
// Product pages
allProducts.forEach((productSeoName) => {
sitemap += ` <url>
<loc>${baseUrl}/Artikel/${productSeoName}</loc>
<lastmod>${currentDate}</lastmod>
<changefreq>weekly</changefreq>
<priority>0.6</priority>
</url>
`;
});
sitemap += `</urlset>`;
return sitemap;
};
module.exports = {
generateSitemapJsonLd,
generateXmlSitemap,
};

137
src/PrerenderSitemap.js Normal file
View File

@@ -0,0 +1,137 @@
const React = require('react');
const {
Box,
AppBar,
Toolbar,
Container,
Typography,
List,
ListItem,
ListItemText
} = require('@mui/material');
const Footer = require('./components/Footer.js').default;
const { Logo, CategoryList } = require('./components/header/index.js');
const LegalPage = require('./pages/LegalPage.js').default;
const PrerenderSitemap = ({ categoryData }) => {
// Process category data to flatten the hierarchy
const collectAllCategories = (categoryNode, categories = [], level = 0) => {
if (!categoryNode) return categories;
// Add current category (skip root category 209)
if (categoryNode.id !== 209 && categoryNode.seoName) {
categories.push({
id: categoryNode.id,
name: categoryNode.name,
seoName: categoryNode.seoName,
level: level
});
}
// Recursively add children
if (categoryNode.children) {
for (const child of categoryNode.children) {
collectAllCategories(child, categories, level + 1);
}
}
return categories;
};
const categories = categoryData ? collectAllCategories(categoryData) : [];
const sitemapLinks = [
{ title: 'Startseite', url: '/' },
{ title: 'Mein Profil', url: '/profile' },
{ title: 'Datenschutz', url: '/datenschutz' },
{ title: 'AGB', url: '/agb' },
{ title: 'Impressum', url: '/impressum' },
{ title: 'Batteriegesetzhinweise', url: '/batteriegesetzhinweise' },
{ title: 'Widerrufsrecht', url: '/widerrufsrecht' },
{ title: 'Growbox Konfigurator', url: '/Konfigurator' },
{ title: 'API', url: '/api/', route: false },
];
const content = React.createElement(
React.Fragment,
null,
React.createElement(
Typography,
{ variant: 'body1', paragraph: true },
'Hier finden Sie eine Übersicht aller verfügbaren Seiten unserer Website.'
),
// Static site links
React.createElement(
Typography,
{ variant: 'h6', sx: { mt: 3, mb: 2, fontWeight: 'bold' } },
'Seiten'
),
React.createElement(
List,
null,
sitemapLinks.map((link) =>
React.createElement(
ListItem,
{
key: link.url,
button: true,
component: link.route === false ? 'a' : 'a',
href: link.url,
sx: {
py: 1,
borderBottom: '1px solid',
borderColor: 'divider'
}
},
React.createElement(ListItemText, { primary: link.title })
)
)
),
// Category links
React.createElement(
Typography,
{ variant: 'h6', sx: { mt: 4, mb: 2, fontWeight: 'bold' } },
'Kategorien'
),
React.createElement(
List,
null,
categories.map((category) =>
React.createElement(
ListItem,
{
key: category.id,
button: true,
component: 'a',
href: `/Kategorie/${category.seoName}`,
sx: {
py: 1,
pl: 2 + (category.level * 2), // Indent based on category level
borderBottom: '1px solid',
borderColor: 'divider'
}
},
React.createElement(
ListItemText,
{
primary: category.name,
sx: {
'& .MuiTypography-root': {
fontSize: category.level === 0 ? '1rem' : '0.9rem',
fontWeight: category.level === 0 ? 'bold' : 'normal',
color: category.level === 0 ? 'primary.main' : 'text.primary'
}
}
}
)
)
)
)
);
return React.createElement(LegalPage, { title: 'Sitemap', content: content });
};
module.exports = { default: PrerenderSitemap };

View File

@@ -97,7 +97,12 @@ function getFilteredProducts(unfilteredProducts, attributes) {
let filteredProducts = (unfilteredProducts || []).filter(product => {
const availabilityFilter = sessionStorage.getItem('filter_availability');
let inStockMatch = availabilityFilter == 1 ? true : (product.available>0);
const isNewMatch = availabilityFilters.includes('2') ? isNew(product.neu) : true;
// Check if there are any new products in the entire set
const hasNewProducts = (unfilteredProducts || []).some(product => isNew(product.neu));
// Only apply the new filter if there are actually new products and the filter is active
const isNewMatch = availabilityFilters.includes('2') && hasNewProducts ? isNew(product.neu) : true;
let soonMatch = availabilityFilters.includes('3') ? !product.available && product.incoming : true;
const soon2Match = (availabilityFilter != 1)&&availabilityFilters.includes('3') ? (product.available) || (!product.available && product.incoming) : true;
@@ -133,7 +138,31 @@ function getFilteredProducts(unfilteredProducts, attributes) {
const manufacturer = uniqueManufacturersWithName.find(manufacturer => manufacturer.id === filter);
return {name: manufacturer.value, value: manufacturer.id};
});
return {filteredProducts,activeAttributeFilters:activeAttributeFiltersWithNames,activeManufacturerFilters:activeManufacturerFiltersWithNames};
// Extract active availability filters
const availabilityFilter = sessionStorage.getItem('filter_availability');
const activeAvailabilityFilters = [];
// Check if there are actually products with these characteristics
const hasNewProducts = (unfilteredProducts || []).some(product => isNew(product.neu));
const hasComingSoonProducts = (unfilteredProducts || []).some(product => !product.available && product.incoming);
// Check for "auf Lager" filter (in stock) - it's active when filter_availability is NOT set to '1'
if (availabilityFilter !== '1') {
activeAvailabilityFilters.push({id: '1', name: 'auf Lager'});
}
// Check for "Neu" filter (new) - only show if there are actually new products and filter is active
if (availabilityFilters.includes('2') && hasNewProducts) {
activeAvailabilityFilters.push({id: '2', name: 'Neu'});
}
// Check for "Bald verfügbar" filter (coming soon) - only show if there are actually coming soon products and filter is active
if (availabilityFilters.includes('3') && hasComingSoonProducts) {
activeAvailabilityFilters.push({id: '3', name: 'Bald verfügbar'});
}
return {filteredProducts,activeAttributeFilters:activeAttributeFiltersWithNames,activeManufacturerFilters:activeManufacturerFiltersWithNames,activeAvailabilityFilters};
}
function setCachedCategoryData(categoryId, data) {
if (!window.productCache) {
@@ -441,7 +470,7 @@ class Content extends Component {
return (
<Container maxWidth="xl" sx={{ py: 2, flexGrow: 1, display: 'grid', gridTemplateRows: '1fr' }}>
<Container maxWidth="xl" sx={{ py: { xs: 0, sm: 2 }, px: { xs: 0, sm: 3 }, flexGrow: 1, display: 'grid', gridTemplateRows: '1fr' }}>
{showCategoryBoxes ? (
// Show category boxes layout when no products but have child categories
@@ -491,30 +520,20 @@ class Content extends Component {
</Box>
{/* Subcategories Grid */}
<Box sx={{ flex: 1, minWidth: 0 }}>
<CategoryBoxGrid
categories={this.state.childCategories}
showTitle={false}
spacing={3}
/>
<Box sx={{ flexGrow: 1 }}>
<CategoryBoxGrid categories={this.state.childCategories} />
</Box>
</Box>
);
} else {
// Just show subcategories without parent
return (
<CategoryBoxGrid
categories={this.state.childCategories}
showTitle={false}
spacing={3}
/>
);
// No parent category, just show subcategories
return <CategoryBoxGrid categories={this.state.childCategories} />;
}
})()}
</Box>
)}
{/* Show parent category navigation when in 2nd or 3rd level but no subcategories */}
{/* Show standalone parent category navigation when there are only products */}
{this.state.loaded &&
this.props.params.categoryId &&
!(this.state.unfilteredProducts.length > 0 && this.state.childCategories.length > 0) && (() => {
@@ -558,7 +577,7 @@ class Content extends Component {
<Box sx={{
display: 'grid',
gridTemplateColumns: { xs: '1fr', sm: '1fr 2fr', md: '1fr 3fr', lg: '1fr 4fr', xl: '1fr 4fr' },
gap: 3
gap: { xs: 0, sm: 3 }
}}>
<Stack direction="row" spacing={0} sx={{
@@ -690,6 +709,7 @@ class Content extends Component {
products={this.state.filteredProducts || []}
activeAttributeFilters={this.state.activeAttributeFilters || []}
activeManufacturerFilters={this.state.activeManufacturerFilters || []}
activeAvailabilityFilters={this.state.activeAvailabilityFilters || []}
onFilterChange={()=>{this.filterProducts()}}
dataType={this.state.dataType}
dataParam={this.state.dataParam}

View File

@@ -42,8 +42,12 @@ class Header extends Component {
return (
<AppBar position="sticky" color="primary" elevation={0} sx={{ zIndex: 1100 }}>
<Toolbar sx={{ minHeight: 64 }}>
<Container maxWidth="lg" sx={{ display: 'flex', alignItems: 'center' }}>
<Toolbar sx={{ minHeight: 64, py: { xs: 0.5, sm: 0 } }}>
<Container maxWidth="lg" sx={{
display: 'flex',
alignItems: 'center',
px: { xs: 0, sm: 3 }
}}>
{/* First row: Logo and ButtonGroup on xs, all items on larger screens */}
<Box sx={{
display: 'flex',
@@ -56,23 +60,36 @@ class Header extends Component {
display: 'flex',
alignItems: 'center',
width: '100%',
justifyContent: { xs: 'space-between', sm: 'flex-start' }
justifyContent: { xs: 'space-between', sm: 'flex-start' },
minHeight: { xs: 52, sm: 'auto' },
px: { xs: 0, sm: 0 }
}}>
<Logo />
{/* SearchBar visible on sm and up */}
<Box sx={{ display: { xs: 'none', sm: 'block' }, flexGrow: 1 }}>
<SearchBar />
</Box>
<ButtonGroupWithRouter socket={socket}/>
<Box sx={{
display: 'flex',
alignItems: { xs: 'flex-end', sm: 'center' },
transform: { xs: 'translateY(4px) translateX(9px)', sm: 'none' },
ml: { xs: 0, sm: 0 }
}}>
<ButtonGroupWithRouter socket={socket}/>
</Box>
</Box>
{/* Second row: SearchBar only on xs */}
{/* Second row: SearchBar only on xs - make it wider */}
<Box sx={{
display: { xs: 'block', sm: 'none' },
width: '100%',
mt: 1,mb: 1
mt: { xs: 1, sm: 0 },
mb: { xs: 0.5, sm: 0 },
px: { xs: 0, sm: 0 }
}}>
<SearchBar />
<Box sx={{ width: '100%' }}>
<SearchBar />
</Box>
</Box>
</Box>
</Container>

381
src/components/Mollie.js Normal file
View File

@@ -0,0 +1,381 @@
import React, { Component, useState } from "react";
import { Button, Box, Typography, CircularProgress } from "@mui/material";
import config from "../config.js";
// Function to lazy load Mollie script
const loadMollie = () => {
return new Promise((resolve, reject) => {
// Check if Mollie is already loaded
if (window.Mollie) {
resolve(window.Mollie);
return;
}
// Create script element
const script = document.createElement('script');
script.src = 'https://js.mollie.com/v1/mollie.js';
script.async = true;
script.onload = () => {
if (window.Mollie) {
resolve(window.Mollie);
} else {
reject(new Error('Mollie failed to load'));
}
};
script.onerror = () => {
reject(new Error('Failed to load Mollie script'));
};
document.head.appendChild(script);
});
};
const CheckoutForm = ({ mollie }) => {
const [errorMessage, setErrorMessage] = useState(null);
const [isSubmitting, setIsSubmitting] = useState(false);
React.useEffect(() => {
if (!mollie) return;
let mountedComponents = {
cardNumber: null,
cardHolder: null,
expiryDate: null,
verificationCode: null
};
try {
// Create Mollie components
const cardNumber = mollie.createComponent('cardNumber');
const cardHolder = mollie.createComponent('cardHolder');
const expiryDate = mollie.createComponent('expiryDate');
const verificationCode = mollie.createComponent('verificationCode');
// Store references for cleanup
mountedComponents = {
cardNumber,
cardHolder,
expiryDate,
verificationCode
};
// Mount components
cardNumber.mount('#card-number');
cardHolder.mount('#card-holder');
expiryDate.mount('#expiry-date');
verificationCode.mount('#verification-code');
// Set up error handling
cardNumber.addEventListener('change', event => {
const errorElement = document.querySelector('#card-number-error');
if (errorElement) {
if (event.error && event.touched) {
errorElement.textContent = event.error;
} else {
errorElement.textContent = '';
}
}
});
cardHolder.addEventListener('change', event => {
const errorElement = document.querySelector('#card-holder-error');
if (errorElement) {
if (event.error && event.touched) {
errorElement.textContent = event.error;
} else {
errorElement.textContent = '';
}
}
});
expiryDate.addEventListener('change', event => {
const errorElement = document.querySelector('#expiry-date-error');
if (errorElement) {
if (event.error && event.touched) {
errorElement.textContent = event.error;
} else {
errorElement.textContent = '';
}
}
});
verificationCode.addEventListener('change', event => {
const errorElement = document.querySelector('#verification-code-error');
if (errorElement) {
if (event.error && event.touched) {
errorElement.textContent = event.error;
} else {
errorElement.textContent = '';
}
}
});
// Components are now mounted and ready
} catch (error) {
console.error('Error creating Mollie components:', error);
setErrorMessage('Failed to initialize payment form. Please try again.');
}
// Cleanup function
return () => {
try {
if (mountedComponents.cardNumber) mountedComponents.cardNumber.unmount();
if (mountedComponents.cardHolder) mountedComponents.cardHolder.unmount();
if (mountedComponents.expiryDate) mountedComponents.expiryDate.unmount();
if (mountedComponents.verificationCode) mountedComponents.verificationCode.unmount();
} catch (error) {
console.error('Error cleaning up Mollie components:', error);
}
};
}, [mollie]);
const handleSubmit = async (event) => {
event.preventDefault();
if (!mollie || isSubmitting) {
return;
}
setIsSubmitting(true);
setErrorMessage(null);
try {
const { token, error } = await mollie.createToken();
if (error) {
setErrorMessage(error.message || 'Payment failed. Please try again.');
setIsSubmitting(false);
return;
}
if (token) {
// Handle successful token creation
// Create a payment completion event similar to Stripe
const mollieCompletionData = {
mollieToken: token,
paymentMethod: 'mollie'
};
// Dispatch a custom event to notify the parent component
const completionEvent = new CustomEvent('molliePaymentComplete', {
detail: mollieCompletionData
});
window.dispatchEvent(completionEvent);
// For now, redirect to profile with completion data
const returnUrl = `${window.location.origin}/profile?complete&mollie_token=${token}`;
window.location.href = returnUrl;
}
} catch (error) {
console.error('Error creating Mollie token:', error);
setErrorMessage('Payment failed. Please try again.');
setIsSubmitting(false);
}
};
return (
<Box sx={{ maxWidth: 600, mx: 'auto', p: 3 }}>
<Typography variant="h6" gutterBottom>
Kreditkarte oder Sofortüberweisung
</Typography>
<form onSubmit={handleSubmit}>
<Box sx={{ mb: 3 }}>
<Typography variant="body2" gutterBottom>
Kartennummer
</Typography>
<Box
id="card-number"
sx={{
p: 2,
border: '1px solid #e0e0e0',
borderRadius: 1,
minHeight: 40,
backgroundColor: '#fff'
}}
/>
<Typography
id="card-number-error"
variant="caption"
sx={{ color: 'error.main', minHeight: 16, display: 'block' }}
/>
</Box>
<Box sx={{ mb: 3 }}>
<Typography variant="body2" gutterBottom>
Karteninhaber
</Typography>
<Box
id="card-holder"
sx={{
p: 2,
border: '1px solid #e0e0e0',
borderRadius: 1,
minHeight: 40,
backgroundColor: '#fff'
}}
/>
<Typography
id="card-holder-error"
variant="caption"
sx={{ color: 'error.main', minHeight: 16, display: 'block' }}
/>
</Box>
<Box sx={{ display: 'flex', gap: 2, mb: 3 }}>
<Box sx={{ flex: 1 }}>
<Typography variant="body2" gutterBottom>
Ablaufdatum
</Typography>
<Box
id="expiry-date"
sx={{
p: 2,
border: '1px solid #e0e0e0',
borderRadius: 1,
minHeight: 40,
backgroundColor: '#fff'
}}
/>
<Typography
id="expiry-date-error"
variant="caption"
sx={{ color: 'error.main', minHeight: 16, display: 'block' }}
/>
</Box>
<Box sx={{ flex: 1 }}>
<Typography variant="body2" gutterBottom>
Sicherheitscode
</Typography>
<Box
id="verification-code"
sx={{
p: 2,
border: '1px solid #e0e0e0',
borderRadius: 1,
minHeight: 40,
backgroundColor: '#fff'
}}
/>
<Typography
id="verification-code-error"
variant="caption"
sx={{ color: 'error.main', minHeight: 16, display: 'block' }}
/>
</Box>
</Box>
<Button
variant="contained"
disabled={!mollie || isSubmitting}
type="submit"
fullWidth
sx={{
mt: 2,
backgroundColor: '#2e7d32',
'&:hover': {
backgroundColor: '#1b5e20'
}
}}
>
{isSubmitting ? (
<>
<CircularProgress size={20} sx={{ mr: 1, color: 'white' }} />
Verarbeitung...
</>
) : (
'Bezahlung Abschließen'
)}
</Button>
{errorMessage && (
<Typography
variant="body2"
sx={{ color: 'error.main', mt: 2, textAlign: 'center' }}
>
{errorMessage}
</Typography>
)}
</form>
</Box>
);
};
class Mollie extends Component {
constructor(props) {
super(props);
this.state = {
mollie: null,
loading: true,
error: null,
};
this.molliePromise = loadMollie();
}
componentDidMount() {
this.molliePromise
.then((MollieClass) => {
try {
// Initialize Mollie with profile key
const mollie = MollieClass(config.mollieProfileKey, {
locale: 'de_DE',
testmode: true // Set to false for production
});
this.setState({ mollie, loading: false });
} catch (error) {
console.error('Error initializing Mollie:', error);
this.setState({
error: 'Failed to initialize payment system. Please try again.',
loading: false
});
}
})
.catch((error) => {
console.error('Error loading Mollie:', error);
this.setState({
error: 'Failed to load payment system. Please try again.',
loading: false
});
});
}
render() {
const { mollie, loading, error } = this.state;
if (loading) {
return (
<Box sx={{ textAlign: 'center', py: 4 }}>
<CircularProgress sx={{ color: '#2e7d32' }} />
<Typography variant="body1" sx={{ mt: 2 }}>
Zahlungskomponente wird geladen...
</Typography>
</Box>
);
}
if (error) {
return (
<Box sx={{ textAlign: 'center', py: 4 }}>
<Typography variant="body1" sx={{ color: 'error.main' }}>
{error}
</Typography>
<Button
variant="outlined"
onClick={() => window.location.reload()}
sx={{ mt: 2 }}
>
Seite neu laden
</Button>
</Box>
);
}
return <CheckoutForm mollie={mollie} />;
}
}
export default Mollie;

View File

@@ -180,18 +180,21 @@ class Product extends Component {
<Card
sx={{
width: { xs: 'calc(100vw - 48px)', sm: '250px' },
minWidth: { xs: 'calc(100vw - 48px)', sm: '250px' },
width: { xs: '100vw', sm: '250px' },
minWidth: { xs: '100vw', sm: '250px' },
height: '100%',
display: 'flex',
flexDirection: 'column',
transition: 'transform 0.3s ease, box-shadow 0.3s ease',
position: 'relative',
overflow: 'hidden',
borderRadius: '8px',
borderRadius: { xs: 0, sm: '8px' },
border: { xs: 'none', sm: 'inherit' },
boxShadow: { xs: 'none', sm: 'inherit' },
mx: { xs: 0, sm: 'auto' },
'&:hover': {
transform: 'translateY(-5px)',
boxShadow: '0px 10px 20px rgba(0,0,0,0.1)'
transform: { xs: 'none', sm: 'translateY(-5px)' },
boxShadow: { xs: 'none', sm: '0px 10px 20px rgba(0,0,0,0.1)' }
}
}}
>

View File

@@ -162,13 +162,17 @@ class ProductFilters extends Component {
return (
<Paper
id="filters-paper"
elevation={1}
elevation={window.innerWidth < 600 ? 0 : 1}
sx={{
p: 2,
borderRadius: 2,
p: { xs: 1, sm: 2 },
borderRadius: { xs: 0, sm: 2 },
bgcolor: 'background.paper',
display: 'flex',
flexDirection: 'column'
flexDirection: 'column',
border: { xs: 'none', sm: 'inherit' },
boxShadow: { xs: 'none', sm: 'inherit' },
mx: { xs: 0, sm: 'auto' },
width: { xs: '100%', sm: 'auto' }
}}
>

View File

@@ -122,9 +122,19 @@ class ProductList extends Component {
}
renderPagination = (pages, page) => {
// Make pagination invisible when there are zero products to avoid layout shifts
const hasProducts = this.props.products.length > 0;
return (
<Box sx={{ height: 64, display: 'flex', alignItems: 'center', justifyContent: 'left' }}>
{((this.state.itemsPerPage==='all')||(this.props.products.length<this.state.itemsPerPage))?null:
<Box sx={{
height: 64,
display: 'flex',
alignItems: 'center',
justifyContent: 'left',
width: '100%',
visibility: hasProducts ? 'visible' : 'hidden'
}}>
{(this.state.itemsPerPage==='all')?null:
<Pagination
count={pages}
page={page}
@@ -150,6 +160,57 @@ class ProductList extends Component {
);
}
// Check if filters are active
hasActiveFilters = () => {
return (
(this.props.activeAttributeFilters && this.props.activeAttributeFilters.length > 0) ||
(this.props.activeManufacturerFilters && this.props.activeManufacturerFilters.length > 0) ||
(this.props.activeAvailabilityFilters && this.props.activeAvailabilityFilters.length > 0)
);
}
// Render message when no products found but filters are active
renderNoProductsMessage = () => {
const hasFiltersActive = this.hasActiveFilters();
const hasUnfilteredProducts = this.props.totalProductCount > 0;
if (this.props.products.length === 0 && hasUnfilteredProducts && hasFiltersActive) {
return (
<Box sx={{
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
py: 4,
px: 2
}}>
<Typography variant="h6" color="text.secondary" sx={{ textAlign: 'center' }}>
Entferne Filter um Produkte zu sehen
</Typography>
</Box>
);
}
return null;
}
// Helper function for correct pluralization
getProductCountText = () => {
const filteredCount = this.props.products.length;
const totalCount = this.props.totalProductCount;
const isFiltered = totalCount !== filteredCount;
if (!isFiltered) {
// No filters applied
if (filteredCount === 0) return "0 Produkte";
if (filteredCount === 1) return "1 Produkt";
return `${filteredCount} Produkte`;
} else {
// Filters applied
if (totalCount === 0) return "0 Produkte";
if (totalCount === 1) return `${filteredCount} von 1 Produkt`;
return `${filteredCount} von ${totalCount} Produkten`;
}
}
render() {
//console.log('products',this.props.activeAttributeFilters,this.props.activeManufacturerFilters,window.currentSearchQuery,this.state.sortBy);
@@ -164,15 +225,56 @@ class ProductList extends Component {
const products = this.state.itemsPerPage==='all'?[...filteredProducts]:filteredProducts.slice((this.state.page - 1) * this.state.itemsPerPage , this.state.page * this.state.itemsPerPage);
return (
<Box sx={{ height: '100%' }}>
<Box sx={{ height: '100%', px: { xs: 0, sm: 0 } }}>
<Box sx={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center'
alignItems: 'center',
flexDirection: { xs: 'column', sm: 'row' },
gap: { xs: 1, sm: 0 },
px: { xs: 0, sm: 0 },
py: { xs: 1, sm: 0 },
bgcolor: { xs: '#e8f5e8', sm: 'transparent' },
mb: { xs: 0, sm: 0 }
}}>
<Box sx={{ display: 'flex', gap: 1, alignItems: 'center', flexWrap: 'wrap' }}>
<Box sx={{
display: 'flex',
gap: { xs: 0.5, sm: 1 },
alignItems: 'center',
flexWrap: 'wrap',
order: { xs: 2, sm: 1 },
px: { xs: 1, sm: 0 }
}}>
{this.props.activeAvailabilityFilters && this.props.activeAvailabilityFilters.map((filter,index) => (
<Chip
size="medium"
key={`availability-${index}`}
label={filter.name}
onClick={() => {
if (filter.id === '1') {
// Add "auf Lager" filter by setting the sessionStorage item to '1'
sessionStorage.setItem('filter_availability', '1');
} else {
// Remove "Neu" or "Bald verfügbar" filters
removeSessionSetting(`filter_availability_${filter.id}`);
}
this.props.onFilterChange();
}}
onDelete={() => {
if (filter.id === '1') {
// Add "auf Lager" filter by setting the sessionStorage item to '1'
sessionStorage.setItem('filter_availability', '1');
} else {
// Remove "Neu" or "Bald verfügbar" filters
removeSessionSetting(`filter_availability_${filter.id}`);
}
this.props.onFilterChange();
}}
clickable
/>
))}
{this.props.activeAttributeFilters.map((filter,index) => (
<Chip
size="medium"
@@ -205,11 +307,26 @@ class ProductList extends Component {
clickable
/>
))}
</Box>
<Box sx={{ display: 'flex', gap: 2, alignItems: 'center' }}>
<Box sx={{
display: 'flex',
gap: { xs: 1, sm: 2 },
alignItems: 'center',
order: { xs: 1, sm: 2 },
width: { xs: '100%', sm: 'auto' },
justifyContent: { xs: 'space-between', sm: 'flex-end' },
px: { xs: 1, sm: 0 }
}}>
{/* Sort Dropdown */}
<FormControl variant="outlined" size="small" sx={{ minWidth: 140 }}>
<FormControl
variant={window.innerWidth < 600 ? 'standard' : 'outlined'}
size="small"
sx={{
minWidth: { xs: 120, sm: 140 }
}}
>
<InputLabel id="sort-by-label">Sortierung</InputLabel>
<Select
size="small"
@@ -244,7 +361,13 @@ class ProductList extends Component {
</FormControl>
{/* Per Page Dropdown */}
<FormControl variant="outlined" size="small" sx={{ minWidth: 100 }}>
<FormControl
variant={window.innerWidth < 600 ? 'standard' : 'outlined'}
size="small"
sx={{
minWidth: { xs: 80, sm: 100 }
}}
>
<InputLabel id="products-per-page-label">pro Seite</InputLabel>
<Select
labelId="products-per-page-label"
@@ -278,39 +401,56 @@ class ProductList extends Component {
<MenuItem value="all">Alle</MenuItem>
</Select>
</FormControl>
{/* Product count info - mobile only */}
<Box sx={{
display: { xs: 'block', sm: 'none' },
ml: 1
}}>
<Typography variant="body2" color="text.secondary" sx={{ fontSize: '0.75rem', whiteSpace: 'nowrap' }}>
{this.getProductCountText()}
</Typography>
</Box>
</Box>
</Box>
<Box sx={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center'
alignItems: 'center',
px: { xs: 0, sm: 0 },
py: { xs: 1, sm: 0 },
bgcolor: { xs: '#e8f5e8', sm: 'transparent' },
mt: { xs: 0, sm: 0 }
}}>
{ this.renderPagination(Math.ceil(filteredProducts.length / this.state.itemsPerPage), this.state.page) }
<Stack direction="row" spacing={2}>
<Box sx={{ px: { xs: 1, sm: 0 }, width: '100%' }}>
{ this.renderPagination(Math.ceil(filteredProducts.length / this.state.itemsPerPage), this.state.page) }
</Box>
<Stack direction="row" spacing={2} sx={{ display: { xs: 'none', sm: 'flex' }, px: { xs: 1, sm: 0 } }}>
<Typography variant="body2" color="text.secondary">
{/*this.props.dataType == 'category' && (<>Kategorie: {this.props.dataParam}</>)}*/}
{this.props.dataType == 'search' && (<>Suchergebnisse für: "{this.props.dataParam}"</>)}
</Typography>
<Typography variant="body2" color="text.secondary">
{
this.props.totalProductCount==this.props.products.length && this.props.totalProductCount>0 ?
`${this.props.totalProductCount} Produkte`
:
`${this.props.products.length} von ${this.props.totalProductCount} Produkte`
}
<Typography variant="body2" color="text.secondary" sx={{ whiteSpace: 'nowrap' }}>
{this.getProductCountText()}
</Typography>
</Stack>
</Box>
<Grid container spacing={2}>
{products.map((product) => (
<Grid container spacing={{ xs: 0, sm: 2 }}>
{this.renderNoProductsMessage()}
{products.map((product, index) => (
<Grid
key={product.id}
key={product.id}
sx={{
display: 'flex',
justifyContent: { xs: 'stretch', sm: 'center' },
mb: 1
mb: { xs: 0, sm: 1 },
width: { xs: '100%', sm: 'auto' },
borderBottom: {
xs: index < products.length - 1 ? '16px solid #e8f5e8' : 'none',
sm: 'none'
}
}}
>
<Product
@@ -338,8 +478,18 @@ class ProductList extends Component {
</Grid>
))}
</Grid>
{this.renderPagination(Math.ceil(filteredProducts.length / this.state.itemsPerPage), this.state.page)}
{/* Bottom pagination */}
<Box sx={{
px: { xs: 0, sm: 0 },
py: { xs: 1, sm: 1 },
bgcolor: { xs: '#e8f5e8', sm: 'transparent' },
mt: { xs: 0, sm: 2 }
}}>
<Box sx={{ px: { xs: 1, sm: 0 } }}>
{this.renderPagination(Math.ceil(filteredProducts.length / this.state.itemsPerPage), this.state.page)}
</Box>
</Box>
</Box>
);
}

View File

@@ -142,7 +142,7 @@ class ButtonGroup extends Component {
onClose={this.toggleCart}
disableScrollLock={true}
>
<Box sx={{ width: 420, p: 2 }}>
<Box sx={{ width: { xs: '100vw', sm: 420 }, p: { xs: 1, sm: 2 } }}>
<Box
sx={{
display: 'flex',

View File

@@ -219,7 +219,7 @@ const SearchBar = () => {
onSubmit={handleSearch}
sx={{
flexGrow: 1,
mx: { xs: 1, sm: 2, md: 4 },
mx: { xs: 0, sm: 2, md: 4 },
position: "relative",
}}
>

View File

@@ -51,6 +51,9 @@ class CartTab extends Component {
showStripePayment: false,
StripeComponent: null,
isLoadingStripe: false,
showMolliePayment: false,
MollieComponent: null,
isLoadingMollie: false,
showPaymentConfirmation: false,
orderCompleted: false,
originalCartItems: []
@@ -116,7 +119,7 @@ class CartTab extends Component {
// Determine payment method - respect constraints
let prefillPaymentMethod = template.payment_method || "wire";
const paymentMethodMap = {
"credit_card": "stripe",
"credit_card": "mollie",//stripe
"bank_transfer": "wire",
"cash_on_delivery": "onDelivery",
"cash": "cash"
@@ -319,6 +322,27 @@ class CartTab extends Component {
}
};
loadMollieComponent = async () => {
this.setState({ isLoadingMollie: true });
try {
const { default: Mollie } = await import("../Mollie.js");
this.setState({
MollieComponent: Mollie,
showMolliePayment: true,
isCompletingOrder: false,
isLoadingMollie: false,
});
} catch (error) {
console.error("Failed to load Mollie component:", error);
this.setState({
isCompletingOrder: false,
isLoadingMollie: false,
completionError: "Failed to load payment component. Please try again.",
});
}
};
handleCompleteOrder = () => {
this.setState({ completionError: null }); // Clear previous errors
@@ -363,6 +387,25 @@ class CartTab extends Component {
this.orderService.createStripeIntent(totalAmount, this.loadStripeComponent);
return;
}
// Handle Mollie payment differently
if (paymentMethod === "mollie") {
// Store the cart items used for Mollie payment in sessionStorage for later reference
try {
sessionStorage.setItem('molliePaymentCart', JSON.stringify(cartItems));
} catch (error) {
console.error("Failed to store Mollie payment cart:", error);
}
// Calculate total amount for Mollie
const subtotal = cartItems.reduce(
(total, item) => total + item.price * item.quantity,
0
);
const totalAmount = Math.round((subtotal + deliveryCost) * 100); // Convert to cents
this.orderService.createMollieIntent(totalAmount, this.loadMollieComponent);
return;
}
// Handle regular orders
const orderData = {
@@ -398,6 +441,9 @@ class CartTab extends Component {
showStripePayment,
StripeComponent,
isLoadingStripe,
showMolliePayment,
MollieComponent,
isLoadingMollie,
showPaymentConfirmation,
orderCompleted,
} = this.state;
@@ -409,7 +455,7 @@ class CartTab extends Component {
const displayError = completionError || preSubmitError;
return (
<Box sx={{ p: 3 }}>
<Box sx={{ p: { xs: 1, sm: 3 } }}>
{/* Payment Confirmation */}
{showPaymentConfirmation && (
<PaymentConfirmationDialog
@@ -434,7 +480,7 @@ class CartTab extends Component {
<CartDropdown
cartItems={cartItems}
socket={this.context.socket}
showDetailedSummary={showStripePayment}
showDetailedSummary={showStripePayment || showMolliePayment}
deliveryMethod={deliveryMethod}
deliveryCost={deliveryCost}
/>
@@ -442,7 +488,7 @@ class CartTab extends Component {
{cartItems.length > 0 && (
<Box sx={{ mt: 3 }}>
{isLoadingStripe ? (
{isLoadingStripe || isLoadingMollie ? (
<Box sx={{ textAlign: 'center', py: 4 }}>
<Typography variant="body1">
Zahlungskomponente wird geladen...
@@ -468,9 +514,29 @@ class CartTab extends Component {
</Box>
<StripeComponent clientSecret={stripeClientSecret} />
</>
) : showMolliePayment && MollieComponent ? (
<>
<Box sx={{ mb: 2 }}>
<Button
variant="outlined"
onClick={() => this.setState({ showMolliePayment: false })}
sx={{
color: '#2e7d32',
borderColor: '#2e7d32',
'&:hover': {
backgroundColor: 'rgba(46, 125, 50, 0.04)',
borderColor: '#1b5e20'
}
}}
>
Zurück zur Bestellung
</Button>
</Box>
<MollieComponent />
</>
) : (
<CheckoutForm
paymentMethod={paymentMethod}
paymentMethod={paymentMethod}
invoiceAddress={invoiceAddress}
deliveryAddress={deliveryAddress}
useSameAddress={useSameAddress}
@@ -478,7 +544,7 @@ class CartTab extends Component {
addressFormErrors={addressFormErrors}
termsAccepted={termsAccepted}
note={note}
deliveryMethod={deliveryMethod}
deliveryMethod={deliveryMethod}
hasStecklinge={hasStecklinge}
isPickupOnly={isPickupOnly}
deliveryCost={deliveryCost}

View File

@@ -270,6 +270,10 @@ class OrderProcessingService {
});
}
}
// Create Mollie payment intent
createMollieIntent(totalAmount, loadMollieComponent) {
loadMollieComponent();
}
// Calculate delivery cost
getDeliveryCost() {

View File

@@ -139,7 +139,7 @@ const OrdersTab = ({ orderIdFromHash }) => {
if (loading) {
return (
<Box sx={{ p: 3, display: "flex", justifyContent: "center" }}>
<Box sx={{ p: { xs: 1, sm: 3 }, display: "flex", justifyContent: "center" }}>
<CircularProgress />
</Box>
);
@@ -147,14 +147,14 @@ const OrdersTab = ({ orderIdFromHash }) => {
if (error) {
return (
<Box sx={{ p: 3 }}>
<Box sx={{ p: { xs: 1, sm: 3 } }}>
<Alert severity="error">{error}</Alert>
</Box>
);
}
return (
<Box sx={{ p: 3 }}>
<Box sx={{ p: { xs: 1, sm: 3 } }}>
{orders.length > 0 ? (
<TableContainer component={Paper}>
<Table>

View File

@@ -24,7 +24,7 @@ const PaymentMethodSelector = ({ paymentMethod, onChange, deliveryMethod, onDeli
// Handle delivery method changes - auto-switch to stripe when DHL/DPD is selected
useEffect(() => {
if ((deliveryMethod === "DHL" || deliveryMethod === "DPD") && paymentMethod === "cash") {
handlePaymentMethodChange({ target: { value: "stripe" } });
handlePaymentMethodChange({ target: { value: "mollie" } });
}
}, [deliveryMethod, paymentMethod, handlePaymentMethodChange]);
@@ -42,8 +42,22 @@ const PaymentMethodSelector = ({ paymentMethod, onChange, deliveryMethod, onDeli
description: "Bezahlen Sie per Banküberweisung",
disabled: totalAmount === 0,
},
{
/*{
id: "stripe",
name: "Karte oder Sofortüberweisung (Stripe)",
description: totalAmount < 0.50 && totalAmount > 0
? "Bezahlen Sie per Karte oder Sofortüberweisung (Mindestbetrag: 0,50 €)"
: "Bezahlen Sie per Karte oder Sofortüberweisung",
disabled: totalAmount < 0.50 || (deliveryMethod !== "DHL" && deliveryMethod !== "DPD" && deliveryMethod !== "Abholung"),
icons: [
"/assets/images/giropay.png",
"/assets/images/maestro.png",
"/assets/images/mastercard.png",
"/assets/images/visa_electron.png",
],
},*/
{
id: "mollie",
name: "Karte oder Sofortüberweisung",
description: totalAmount < 0.50 && totalAmount > 0
? "Bezahlen Sie per Karte oder Sofortüberweisung (Mindestbetrag: 0,50 €)"

View File

@@ -235,8 +235,8 @@ class SettingsTab extends Component {
render() {
return (
<Box sx={{ p: 3 }}>
<Paper sx={{ p: 3}}>
<Box sx={{ p: { xs: 1, sm: 3 } }}>
<Paper sx={{ p: { xs: 2, sm: 3 } }}>
<Typography variant="subtitle1" fontWeight="bold" gutterBottom>
Passwort ändern
</Typography>
@@ -289,7 +289,7 @@ class SettingsTab extends Component {
<Divider sx={{ my: 4 }} />
<Paper sx={{ p: 3 }}>
<Paper sx={{ p: { xs: 2, sm: 3 } }}>
<Typography variant="subtitle1" fontWeight="bold" gutterBottom>
E-Mail-Adresse ändern
</Typography>
@@ -332,7 +332,7 @@ class SettingsTab extends Component {
<Divider sx={{ my: 4 }} />
<Paper sx={{ p: 3 }}>
<Paper sx={{ p: { xs: 2, sm: 3 } }}>
<Typography variant="subtitle1" fontWeight="bold" gutterBottom>
API-Schlüssel
</Typography>

View File

@@ -3,6 +3,7 @@ const config = {
apiBaseUrl: "",
googleClientId: "928121624463-jbgfdlgem22scs1k9c87ucg4ffvaik6o.apps.googleusercontent.com",
stripePublishableKey: "pk_test_51R7lltRtpe3h1vwJzIrDb5bcEigTLBHrtqj9SiPX7FOEATSuD6oJmKc8xpNp49ShpGJZb2GShHIUqj4zlSIz4olj00ipOuOAnu",
mollieProfileKey: "pfl_AtcRTimCff",
// SEO and Business Information
siteName: "Growheads.de",

View File

@@ -167,11 +167,16 @@ const ProfilePage = (props) => {
}
return (
<Container maxWidth="md" sx={{ py: 4 }}>
<Paper elevation={2} sx={{ borderRadius: 2, overflow: 'hidden' }}>
<Box sx={{ bgcolor: '#2e7d32', p: 3, color: 'white' }}>
<Container maxWidth="md" sx={{ py: { xs: 0, sm: 4 }, px: { xs: 0, sm: 3 } }}>
<Paper elevation={{ xs: 0, sm: 2 }} sx={{ borderRadius: { xs: 0, sm: 2 }, overflow: 'hidden' }}>
<Box sx={{ bgcolor: '#2e7d32', p: { xs: 2, sm: 3 }, color: 'white' }}>
<Typography variant="h5" fontWeight="bold">
Mein Profil
{window.innerWidth < 600 ?
(tabValue === 0 ? 'Bestellabschluss' :
tabValue === 1 ? 'Bestellungen' :
tabValue === 2 ? 'Einstellungen' : 'Mein Profil')
: 'Mein Profil'
}
</Typography>
{user && (
<Typography variant="body1" sx={{ mt: 1 }}>
@@ -185,7 +190,11 @@ const ProfilePage = (props) => {
value={tabValue}
onChange={handleTabChange}
variant="fullWidth"
sx={{ borderBottom: 1, borderColor: 'divider' }}
sx={{
borderBottom: 1,
borderColor: 'divider',
display: { xs: 'none', sm: 'flex' }
}}
TabIndicatorProps={{
style: { backgroundColor: '#2e7d32' }
}}

View File

@@ -33,10 +33,42 @@ const collectAllCategories = (categoryNode, categories = [], level = 0) => {
return categories;
};
// Check for cached data - handle both browser and prerender environments
const getProductCache = () => {
if (typeof window !== "undefined" && window.productCache) {
return window.productCache;
}
if (
typeof global !== "undefined" &&
global.window &&
global.window.productCache
) {
return global.window.productCache;
}
return null;
};
// Initialize categories from cache if available (for prerendering)
const initializeCategories = () => {
const productCache = getProductCache();
if (productCache && productCache["categoryTree_209"]) {
const cached = productCache["categoryTree_209"];
const cacheAge = Date.now() - cached.timestamp;
const tenMinutes = 10 * 60 * 1000;
if (cacheAge < tenMinutes && cached.categoryTree) {
return collectAllCategories(cached.categoryTree);
}
}
return [];
};
const Sitemap = () => {
const [categories, setCategories] = useState([]);
const [loading, setLoading] = useState(true);
const {socket} = useContext(SocketContext);
// Initialize categories and loading state together
const initialCategories = initializeCategories();
const [categories, setCategories] = useState(initialCategories);
const [loading, setLoading] = useState(initialCategories.length === 0);
const context = useContext(SocketContext);
const sitemapLinks = [
@@ -53,8 +85,14 @@ const Sitemap = () => {
useEffect(() => {
const fetchCategories = () => {
// Try cache first
if (window.productCache && window.productCache['categoryTree_209']) {
// If we already have categories from prerendering, we're done
if (categories.length > 0) {
setLoading(false);
return;
}
// Try cache first (for browser environment)
if (typeof window !== "undefined" && window.productCache && window.productCache['categoryTree_209']) {
const cached = window.productCache['categoryTree_209'];
const cacheAge = Date.now() - cached.timestamp;
const tenMinutes = 10 * 60 * 1000;
@@ -66,9 +104,9 @@ const Sitemap = () => {
}
}
// Otherwise, fetch from socket if available
if (socket) {
socket.emit('categoryList', { categoryId: 209 }, (response) => {
// Otherwise, fetch from socket if available (only in browser)
if (context && context.socket && context.socket.connected && typeof window !== "undefined") {
context.socket.emit('categoryList', { categoryId: 209 }, (response) => {
if (response && response.categoryTree) {
// Store in cache
try {
@@ -95,7 +133,7 @@ const Sitemap = () => {
};
fetchCategories();
}, [socket]);
}, [context, categories.length]);
const content = (
<>