@orderlyshop/web-components 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/AGENTS.md +110 -0
  2. package/README.md +685 -0
  3. package/bin/orderly-build-category-pages.mjs +160 -0
  4. package/bin/orderly-generate-category-pages.mjs +308 -0
  5. package/bin/orderly-hydrate-static-pages.mjs +595 -0
  6. package/bin/orderly-init-navigation.mjs +327 -0
  7. package/bin/orderly-init-shop.mjs +876 -0
  8. package/bin/orderly-init-taxonomy.mjs +2 -0
  9. package/bin/orderly-publish-site.mjs +342 -0
  10. package/custom-elements.json +495 -0
  11. package/dist/browser/orderly-web-components.define.global.js +3085 -0
  12. package/dist/browser/orderly-web-components.define.global.js.map +1 -0
  13. package/dist/browser/orderly-web-components.global.js +3085 -0
  14. package/dist/browser/orderly-web-components.global.js.map +1 -0
  15. package/dist/default-shop-DWdB_MRd.d.ts +220 -0
  16. package/dist/default-shop.d.ts +6 -0
  17. package/dist/default-shop.js +762 -0
  18. package/dist/default-shop.js.map +1 -0
  19. package/dist/define-IAQk8OmQ.d.ts +9 -0
  20. package/dist/define.d.ts +2 -0
  21. package/dist/define.js +10266 -0
  22. package/dist/define.js.map +1 -0
  23. package/dist/index.d.ts +683 -0
  24. package/dist/index.js +10589 -0
  25. package/dist/index.js.map +1 -0
  26. package/dist/navigation.d.ts +51 -0
  27. package/dist/navigation.js +818 -0
  28. package/dist/navigation.js.map +1 -0
  29. package/dist/query.d.ts +31 -0
  30. package/dist/query.js +115 -0
  31. package/dist/query.js.map +1 -0
  32. package/dist/registry-CPDecU3g.d.ts +6 -0
  33. package/dist/shop-BnT1C6kG.d.ts +173 -0
  34. package/dist/shop-query.d.ts +8 -0
  35. package/dist/shop-query.js +100 -0
  36. package/dist/shop-query.js.map +1 -0
  37. package/dist/shop.d.ts +8 -0
  38. package/dist/shop.js +10359 -0
  39. package/dist/shop.js.map +1 -0
  40. package/dist/stores.d.ts +46 -0
  41. package/dist/stores.js +145 -0
  42. package/dist/stores.js.map +1 -0
  43. package/dist/taxonomy.d.ts +35 -0
  44. package/dist/taxonomy.js +247 -0
  45. package/dist/taxonomy.js.map +1 -0
  46. package/dist/types-CCQDd6Nd.d.ts +95 -0
  47. package/docs/components/README.md +610 -0
  48. package/docs/components/product-grid.md +176 -0
  49. package/docs/components/product-rail.md +174 -0
  50. package/examples/shop/README.md +71 -0
  51. package/examples/shop/package.json +28 -0
  52. package/examples/shop/src/category.html +20 -0
  53. package/examples/shop/src/checkout.html +21 -0
  54. package/examples/shop/src/forretningsbetingelser.html +80 -0
  55. package/examples/shop/src/includes/body-end.html +1 -0
  56. package/examples/shop/src/includes/body-start.html +2 -0
  57. package/examples/shop/src/includes/head.html +32 -0
  58. package/examples/shop/src/index.html +25 -0
  59. package/examples/shop/src/navigation.ts +154 -0
  60. package/examples/shop/src/product.html +24 -0
  61. package/examples/shop/src/templates/shop-footer.html +76 -0
  62. package/examples/shop/tsconfig.json +32 -0
  63. package/examples/shop/vite.config.mjs +184 -0
  64. package/html-custom-data.json +262 -0
  65. package/package.json +118 -0
  66. package/server/README.md +111 -0
  67. package/server/apache/.htaccess +18 -0
  68. package/server/nginx/orderly-products.conf +24 -0
  69. package/server/node/product-snapshot-server.mjs +133 -0
  70. package/server/php/orderly-product.php +204 -0
@@ -0,0 +1,133 @@
1
+ #!/usr/bin/env node
2
+ import { createServer } from "node:http";
3
+ import { create } from "@bufbuild/protobuf";
4
+ import { anyUnpack } from "@bufbuild/protobuf/wkt";
5
+ import {
6
+ SearchObjectSchema,
7
+ SearchQuerySchema
8
+ } from "@orderlyshop/core-client";
9
+ import { createOrderlyNodeClient } from "@orderlyshop/core-client/node";
10
+
11
+ const ORDERLY_SHARE_URL_PREFIX = "https://orderly.shop/";
12
+ const DEFAULT_IMAGE_BASE_URL = "https://orderlyproduction.azureedge.net/";
13
+
14
+ const port = Number.parseInt(process.env.PORT ?? process.env.ORDERLY_PRODUCT_SNAPSHOT_PORT ?? "4107", 10);
15
+ const baseUrl = process.env.ORDERLY_BACKEND_URL ?? process.env.VITE_ORDERLY_BASE_URL ?? "https://service.orderly.shop";
16
+ const protocol = process.env.ORDERLY_BACKEND_PROTOCOL === "grpc" ? "grpc" : "grpc-web";
17
+ const imageBaseUrl = process.env.ORDERLY_IMAGE_BASE_URL ?? process.env.VITE_ORDERLY_IMAGE_BASE_URL ?? DEFAULT_IMAGE_BASE_URL;
18
+ const client = createOrderlyNodeClient({ baseUrl, protocol });
19
+
20
+ const server = createServer(async (request, response) => {
21
+ const url = new URL(request.url ?? "/", `http://${request.headers.host ?? "localhost"}`);
22
+ if (url.pathname !== "/orderly-product-snapshot") {
23
+ writeJson(response, 404, { error: "Not found" });
24
+ return;
25
+ }
26
+
27
+ const shareUrl = normalizeShareUrl(url.searchParams.get("url") ?? "");
28
+ if (!shareUrl) {
29
+ writeJson(response, 400, { error: "Missing url query parameter" });
30
+ return;
31
+ }
32
+
33
+ try {
34
+ const result = await client.services.searchService.search(create(SearchQuerySchema, {
35
+ ShareUrl: shareUrl
36
+ }));
37
+ const product = result.data
38
+ .map((item) => anyUnpack(item, SearchObjectSchema))
39
+ .filter(Boolean)[0];
40
+
41
+ if (!product) {
42
+ writeJson(response, 404, { error: "Product not found", shareUrl });
43
+ return;
44
+ }
45
+
46
+ writeJson(response, 200, productSnapshot(product, shareUrl));
47
+ } catch (error) {
48
+ const message = error instanceof Error ? error.message : String(error);
49
+ writeJson(response, 502, { error: "Could not load product", detail: message });
50
+ }
51
+ });
52
+
53
+ server.listen(port, () => {
54
+ console.log(`Orderly product snapshot server listening on http://localhost:${port}/orderly-product-snapshot`);
55
+ });
56
+
57
+ function productSnapshot(product, fallbackShareUrl) {
58
+ return {
59
+ shareUrl: product.ShareURL?.URL || fallbackShareUrl,
60
+ title: product.Title || "Produkt",
61
+ brand: normalizedBrand(product.Brand),
62
+ description: product.Description || "",
63
+ priceText: formatCredit(product.Price),
64
+ imageUrl: storedImageUrl(product.Images?.[0])
65
+ };
66
+ }
67
+
68
+ function normalizeShareUrl(value) {
69
+ const trimmed = value.trim();
70
+ if (!trimmed || trimmed.startsWith(ORDERLY_SHARE_URL_PREFIX) || /^https?:\/\//i.test(trimmed)) {
71
+ return trimmed;
72
+ }
73
+ return `${ORDERLY_SHARE_URL_PREFIX}${trimmed.replace(/^\/+/, "")}`;
74
+ }
75
+
76
+ function normalizedBrand(brand) {
77
+ const value = brand?.trim();
78
+ if (!value || value.toLowerCase() === "ukendt" || value.toUpperCase() === "DVD") {
79
+ return "";
80
+ }
81
+ return value;
82
+ }
83
+
84
+ function formatCredit(credit) {
85
+ if (!credit) {
86
+ return "";
87
+ }
88
+ const amount = Number(credit.Amount ?? 0);
89
+ const currency = credit.Currency || "";
90
+ const prefix = currency.toUpperCase() === "DKK" ? "kr." : currency.trim();
91
+ const value = amount.toLocaleString("da-DK", {
92
+ minimumFractionDigits: 2,
93
+ maximumFractionDigits: 2
94
+ });
95
+ return `${prefix ? `${prefix} ` : ""}${value}`.trim();
96
+ }
97
+
98
+ function storedImageUrl(image) {
99
+ if (!image) {
100
+ return "";
101
+ }
102
+ if (image.ImageDataBase64) {
103
+ return `data:image/jpeg;base64,${image.ImageDataBase64}`;
104
+ }
105
+ if (/^https?:\/\//i.test(image.Name) || image.Name.startsWith("/")) {
106
+ return image.Name;
107
+ }
108
+ if (!image.Name) {
109
+ return "";
110
+ }
111
+ const imageName = image.Name.toLowerCase();
112
+ const path = image.IsAsset || imageName.includes("cms") || imageName.startsWith("images")
113
+ ? "objects"
114
+ : "thumbnails";
115
+ return joinUrl(imageBaseUrl, path, image.Name);
116
+ }
117
+
118
+ function joinUrl(baseUrl, ...parts) {
119
+ const root = String(baseUrl).endsWith("/") ? String(baseUrl) : `${baseUrl}/`;
120
+ return new URL(parts
121
+ .filter(Boolean)
122
+ .map((part) => String(part).replace(/^\/+|\/+$/g, ""))
123
+ .join("/"), root).toString();
124
+ }
125
+
126
+ function writeJson(response, status, value) {
127
+ const body = `${JSON.stringify(value)}\n`;
128
+ response.writeHead(status, {
129
+ "content-type": "application/json; charset=utf-8",
130
+ "cache-control": status === 200 ? "public, max-age=300" : "no-store"
131
+ });
132
+ response.end(body);
133
+ }
@@ -0,0 +1,204 @@
1
+ <?php
2
+ declare(strict_types=1);
3
+
4
+ /*
5
+ * Orderly product SSR helper for PHP hosts.
6
+ *
7
+ * Deploy this file next to the built static shop output, including product.html.
8
+ * The script rewrites clean product URLs to a real HTML response, injects
9
+ * declarative SSR fallback content into <orderly-product-detail-page>, and then
10
+ * lets the browser web component hydrate the live product view.
11
+ *
12
+ * Optional environment variables:
13
+ * - ORDERLY_PRODUCT_TEMPLATE: path to built product.html. Defaults to ./product.html.
14
+ * - ORDERLY_PRODUCT_ORIGIN: prefix for compact product slugs. Defaults to https://orderly.shop/.
15
+ * - ORDERLY_PRODUCT_SNAPSHOT_ENDPOINT: internal JSON endpoint returning product snapshot data.
16
+ * - ORDERLY_PRODUCT_SNAPSHOT_TIMEOUT: snapshot endpoint timeout in seconds. Defaults to 2.
17
+ */
18
+
19
+ $shareUrl = orderly_product_share_url();
20
+ $snapshot = orderly_load_product_snapshot($shareUrl);
21
+ $templatePath = getenv('ORDERLY_PRODUCT_TEMPLATE') ?: __DIR__ . '/product.html';
22
+ $html = is_file($templatePath)
23
+ ? file_get_contents($templatePath)
24
+ : orderly_minimal_product_template();
25
+
26
+ if ($html === false) {
27
+ http_response_code(500);
28
+ header('Content-Type: text/plain; charset=UTF-8');
29
+ echo 'Could not read product template.';
30
+ exit;
31
+ }
32
+
33
+ $html = orderly_inject_product_page($html, $shareUrl, $snapshot);
34
+ $html = orderly_inject_meta($html, $shareUrl, $snapshot);
35
+
36
+ header('Content-Type: text/html; charset=UTF-8');
37
+ echo $html;
38
+
39
+ function orderly_product_share_url(): string
40
+ {
41
+ $value = $_GET['url'] ?? '';
42
+ if (!is_string($value) || $value === '') {
43
+ $path = parse_url($_SERVER['REQUEST_URI'] ?? '', PHP_URL_PATH);
44
+ $value = is_string($path) ? ltrim($path, '/') : '';
45
+ }
46
+
47
+ $value = trim(rawurldecode($value));
48
+ if ($value === '') {
49
+ return '';
50
+ }
51
+
52
+ if (preg_match('~^https?://~i', $value) === 1) {
53
+ return $value;
54
+ }
55
+
56
+ $origin = getenv('ORDERLY_PRODUCT_ORIGIN') ?: 'https://orderly.shop/';
57
+ return rtrim($origin, '/') . '/' . ltrim($value, '/');
58
+ }
59
+
60
+ function orderly_load_product_snapshot(string $shareUrl): ?array
61
+ {
62
+ $endpoint = getenv('ORDERLY_PRODUCT_SNAPSHOT_ENDPOINT') ?: '';
63
+ if ($endpoint === '' || $shareUrl === '') {
64
+ return null;
65
+ }
66
+
67
+ $separator = str_contains($endpoint, '?') ? '&' : '?';
68
+ $url = $endpoint . $separator . 'url=' . rawurlencode($shareUrl);
69
+ $timeout = (float) (getenv('ORDERLY_PRODUCT_SNAPSHOT_TIMEOUT') ?: '2');
70
+ $context = stream_context_create([
71
+ 'http' => [
72
+ 'method' => 'GET',
73
+ 'timeout' => $timeout,
74
+ 'header' => "Accept: application/json\r\n",
75
+ ],
76
+ ]);
77
+ $json = @file_get_contents($url, false, $context);
78
+ if ($json === false || $json === '') {
79
+ return null;
80
+ }
81
+
82
+ $data = json_decode($json, true);
83
+ return is_array($data) ? $data : null;
84
+ }
85
+
86
+ function orderly_inject_product_page(string $html, string $shareUrl, ?array $snapshot): string
87
+ {
88
+ $fallback = orderly_render_product_fallback($shareUrl, $snapshot);
89
+ $component = '<orderly-product-detail-page data-orderly-ssr share-url="' . orderly_e($shareUrl) . '">' . "\n"
90
+ . $fallback . "\n"
91
+ . '</orderly-product-detail-page>';
92
+
93
+ $result = preg_replace(
94
+ '~<orderly-product-detail-page\b[^>]*>.*?</orderly-product-detail-page>~is',
95
+ $component,
96
+ $html,
97
+ 1,
98
+ $count
99
+ );
100
+
101
+ if ($result !== null && $count > 0) {
102
+ return $result;
103
+ }
104
+
105
+ return str_replace('</body>', $component . "\n</body>", $html);
106
+ }
107
+
108
+ function orderly_render_product_fallback(string $shareUrl, ?array $snapshot): string
109
+ {
110
+ $title = orderly_snapshot_value($snapshot, 'title') ?: orderly_title_from_share_url($shareUrl);
111
+ $brand = orderly_snapshot_value($snapshot, 'brand');
112
+ $price = orderly_snapshot_value($snapshot, 'priceText');
113
+ $description = orderly_snapshot_value($snapshot, 'description');
114
+ $imageUrl = orderly_snapshot_value($snapshot, 'imageUrl');
115
+ $titleMarkup = orderly_e($title);
116
+
117
+ $image = $imageUrl !== ''
118
+ ? '<div class="orderly-ssr-product-detail__image"><img src="' . orderly_e($imageUrl) . '" alt="' . orderly_e($title) . '" loading="eager"></div>'
119
+ : '';
120
+ $brandMarkup = $brand !== '' ? '<p data-orderly-field="brand">' . orderly_e($brand) . '</p>' : '';
121
+ $priceMarkup = $price !== '' ? '<p data-orderly-field="price">' . orderly_e($price) . '</p>' : '';
122
+ $descriptionMarkup = $description !== '' ? '<p data-orderly-field="description">' . orderly_e($description) . '</p>' : '';
123
+
124
+ return <<<HTML
125
+ <article slot="content-before" class="orderly-ssr-product-detail" data-orderly-ssr-fallback data-orderly-product>
126
+ {$image}
127
+ <section class="orderly-ssr-product-detail__text">
128
+ {$brandMarkup}
129
+ <h1 data-orderly-field="title">{$titleMarkup}</h1>
130
+ {$priceMarkup}
131
+ {$descriptionMarkup}
132
+ </section>
133
+ </article>
134
+ HTML;
135
+ }
136
+
137
+ function orderly_inject_meta(string $html, string $shareUrl, ?array $snapshot): string
138
+ {
139
+ $title = orderly_snapshot_value($snapshot, 'title') ?: orderly_title_from_share_url($shareUrl);
140
+ $description = orderly_snapshot_value($snapshot, 'description');
141
+ $imageUrl = orderly_snapshot_value($snapshot, 'imageUrl');
142
+
143
+ $html = preg_replace('~<title>.*?</title>~is', '<title>' . orderly_e($title) . '</title>', $html, 1) ?? $html;
144
+ $html = orderly_upsert_head_tag($html, '~<meta\s+name=["\']description["\'][^>]*>~i', '<meta name="description" content="' . orderly_e($description ?: $title) . '">');
145
+ $html = orderly_upsert_head_tag($html, '~<link\s+rel=["\']canonical["\'][^>]*>~i', '<link rel="canonical" href="' . orderly_e($shareUrl) . '">');
146
+ $html = orderly_upsert_head_tag($html, '~<meta\s+property=["\']og:title["\'][^>]*>~i', '<meta property="og:title" content="' . orderly_e($title) . '">');
147
+ $html = orderly_upsert_head_tag($html, '~<meta\s+property=["\']og:url["\'][^>]*>~i', '<meta property="og:url" content="' . orderly_e($shareUrl) . '">');
148
+ if ($description !== '') {
149
+ $html = orderly_upsert_head_tag($html, '~<meta\s+property=["\']og:description["\'][^>]*>~i', '<meta property="og:description" content="' . orderly_e($description) . '">');
150
+ }
151
+ if ($imageUrl !== '') {
152
+ $html = orderly_upsert_head_tag($html, '~<meta\s+property=["\']og:image["\'][^>]*>~i', '<meta property="og:image" content="' . orderly_e($imageUrl) . '">');
153
+ }
154
+ return $html;
155
+ }
156
+
157
+ function orderly_upsert_head_tag(string $html, string $pattern, string $tag): string
158
+ {
159
+ if (preg_match($pattern, $html) === 1) {
160
+ return preg_replace($pattern, $tag, $html, 1) ?? $html;
161
+ }
162
+ return str_replace('</head>', ' ' . $tag . "\n </head>", $html);
163
+ }
164
+
165
+ function orderly_snapshot_value(?array $snapshot, string $key): string
166
+ {
167
+ $value = $snapshot[$key] ?? '';
168
+ return is_scalar($value) ? trim((string) $value) : '';
169
+ }
170
+
171
+ function orderly_title_from_share_url(string $shareUrl): string
172
+ {
173
+ $path = parse_url($shareUrl, PHP_URL_PATH);
174
+ $name = is_string($path) ? basename($path, '.html') : 'Produkt';
175
+ $name = preg_replace('~-[a-z0-9]{8,}$~i', '', $name) ?? $name;
176
+ $name = str_replace(['-', '_'], ' ', $name);
177
+ return ucwords($name ?: 'Produkt');
178
+ }
179
+
180
+ function orderly_minimal_product_template(): string
181
+ {
182
+ return <<<'HTML'
183
+ <!doctype html>
184
+ <html lang="da">
185
+ <head>
186
+ <meta charset="UTF-8">
187
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
188
+ <title>Produkt</title>
189
+ <script src="/node_modules/@orderlyshop/web-components/dist/browser/orderly-web-components.global.js"></script>
190
+ <script>
191
+ OrderlyWebComponents.configureShop({
192
+ defaultShop: { baseUrl: "https://service.orderly.shop" }
193
+ });
194
+ </script>
195
+ </head>
196
+ <body></body>
197
+ </html>
198
+ HTML;
199
+ }
200
+
201
+ function orderly_e(string $value): string
202
+ {
203
+ return htmlspecialchars($value, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
204
+ }