@exdst-sitecore-content-sdk/astro 0.0.16 → 0.0.19

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 (110) hide show
  1. package/README.md +3 -3
  2. package/package.json +41 -42
  3. package/src/client/index.ts +1 -1
  4. package/src/client/sitecore-astro-client.test.ts +41 -20
  5. package/src/client/sitecore-astro-client.ts +74 -57
  6. package/src/components/AstroImage.astro +2 -2
  7. package/src/components/AstroImage.astro.test.ts +542 -0
  8. package/src/components/Date.astro +5 -1
  9. package/src/components/Date.astro.test.ts +197 -0
  10. package/src/components/DefaultEmptyFieldEditingComponentImage.astro +4 -0
  11. package/src/components/DefaultEmptyFieldEditingComponentText.astro +4 -0
  12. package/src/components/EditingScripts.astro +2 -2
  13. package/src/components/EditingScripts.astro.test.ts +267 -0
  14. package/src/components/ErrorBoundary.astro +8 -9
  15. package/src/components/ErrorBoundary.astro.test.ts +252 -0
  16. package/src/components/ErrorComponent.astro +16 -0
  17. package/src/components/ErrorComponent.astro.test.ts +31 -0
  18. package/src/components/FieldMetadata.astro +1 -1
  19. package/src/components/FieldMetadata.astro.test.ts +40 -0
  20. package/src/components/File.astro +5 -1
  21. package/src/components/File.astro.test.ts +68 -0
  22. package/src/components/HiddenRendering.astro.test.ts +36 -0
  23. package/src/components/Image.astro +18 -4
  24. package/src/components/Image.astro.test.ts +438 -0
  25. package/src/components/Link.astro +13 -1
  26. package/src/components/Link.astro.test.ts +261 -0
  27. package/src/components/MissingComponent.astro.test.ts +21 -0
  28. package/src/components/Placeholder/Placeholder.astro +18 -23
  29. package/src/components/Placeholder/Placeholder.astro.test.ts +1088 -0
  30. package/src/components/Placeholder/PlaceholderMetadata.astro +24 -18
  31. package/src/components/Placeholder/PlaceholderMetadata.astro.test.ts +228 -0
  32. package/src/components/Placeholder/PlaceholderUtils.astro +21 -40
  33. package/src/components/Placeholder/PlaceholderUtils.astro.test.ts +149 -0
  34. package/src/components/Placeholder/models.ts +26 -17
  35. package/src/components/Placeholder/placeholder-utils.test.ts +153 -6
  36. package/src/components/Placeholder/placeholder-utils.ts +33 -11
  37. package/src/components/RichText.astro +9 -1
  38. package/src/components/RichText.astro.test.ts +205 -0
  39. package/src/components/Text.astro +15 -3
  40. package/src/components/Text.astro.test.ts +273 -0
  41. package/src/config/define-config.test.ts +5 -5
  42. package/src/config/define-config.ts +22 -42
  43. package/src/config-cli/define-cli-config.test.ts +5 -12
  44. package/src/config-cli/define-cli-config.ts +4 -8
  45. package/src/context.ts +42 -11
  46. package/src/debug.ts +13 -0
  47. package/src/editing/editing-config-middleware.test.ts +5 -7
  48. package/src/editing/editing-config-middleware.ts +11 -7
  49. package/src/editing/editing-render-middleware.test.ts +366 -24
  50. package/src/editing/editing-render-middleware.ts +34 -12
  51. package/src/editing/index.ts +2 -0
  52. package/src/editing/render-middleware.test.ts +1 -1
  53. package/src/editing/render-middleware.ts +1 -1
  54. package/src/editing/types.ts +39 -0
  55. package/src/editing/utils.test.ts +364 -4
  56. package/src/editing/utils.ts +82 -24
  57. package/src/enhancers/WithEmptyFieldEditingComponent.astro +1 -1
  58. package/src/enhancers/WithEmptyFieldEditingComponent.astro.test.ts +380 -0
  59. package/src/enhancers/WithFieldMetadata.astro.test.ts +113 -0
  60. package/src/index.ts +10 -7
  61. package/src/middleware/index.ts +4 -12
  62. package/src/middleware/middleware.test.ts +13 -0
  63. package/src/middleware/middleware.ts +12 -3
  64. package/src/middleware/multisite-middleware.test.ts +45 -50
  65. package/src/middleware/multisite-middleware.ts +33 -6
  66. package/src/middleware/robots-middleware.test.ts +20 -4
  67. package/src/middleware/robots-middleware.ts +10 -3
  68. package/src/middleware/sitemap-middleware.test.ts +35 -3
  69. package/src/middleware/sitemap-middleware.ts +7 -6
  70. package/src/services/component-props-service.ts +7 -6
  71. package/src/sharedTypes/component-props.ts +15 -4
  72. package/src/site/index.ts +1 -1
  73. package/src/tests/astro-helpers.ts +61 -0
  74. package/src/tests/test-components/CustomErrorComponent.astro +3 -0
  75. package/src/tests/test-components/CustomHiddenRendering.astro +10 -0
  76. package/src/tests/test-components/CustomMissingComponent.astro +9 -0
  77. package/src/tests/test-components/DownloadCallout.astro +12 -0
  78. package/src/tests/test-components/EmptyFieldEditingComponent.astro +5 -0
  79. package/src/tests/test-components/ErrorBoundaryWithError.astro +10 -0
  80. package/src/tests/test-components/Home.astro +12 -0
  81. package/src/tests/test-components/SxaRichText.astro +23 -0
  82. package/src/tests/test-components/SxaRichTextDefault.astro +7 -0
  83. package/src/tests/test-components/SxaRichTextWithTitle.astro +8 -0
  84. package/src/tests/test-components/TestComponent.astro +9 -0
  85. package/src/tests/test-components/TestComponentWithError.astro +4 -0
  86. package/src/tests/test-components/TestComponentWithField.astro +17 -0
  87. package/src/tests/test-components/TestHeader.astro +8 -0
  88. package/src/tests/test-components/TestLogo.astro +5 -0
  89. package/src/tests/test-components/TestParentWrapperComponent.astro +5 -0
  90. package/src/tests/test-components/TestWrapperComponent.astro +5 -0
  91. package/src/tests/test-data/metadata-data.ts +86 -0
  92. package/src/tests/test-data/normal-mode-data.ts +466 -0
  93. package/src/tests/vitest.setup.ts +4 -0
  94. package/src/tools/generate-map.ts +4 -3
  95. package/src/tools/index.ts +2 -4
  96. package/src/tools/templating/components.test.ts +100 -87
  97. package/src/tools/templating/components.ts +2 -1
  98. package/src/tools/templating/default-component.ts +3 -8
  99. package/src/utils/utils.ts +20 -2
  100. /package/src/{test-data → tests}/helpers.ts +0 -0
  101. /package/src/{test-data → tests}/personalizeData.ts +0 -0
  102. /package/src/{test-data/components → tests/test-components/map-components}/Bar.astro +0 -0
  103. /package/src/{test-data/components → tests/test-components/map-components}/Baz.astro +0 -0
  104. /package/src/{test-data/components → tests/test-components/map-components}/Foo.astro +0 -0
  105. /package/src/{test-data/components → tests/test-components/map-components}/Hero.variant.astro +0 -0
  106. /package/src/{test-data/components → tests/test-components/map-components}/NotComponent.bsx +0 -0
  107. /package/src/{test-data/components → tests/test-components/map-components}/Qux.astro +0 -0
  108. /package/src/{test-data/components → tests/test-components/map-components}/folded/Folded.astro +0 -0
  109. /package/src/{test-data/components → tests/test-components/map-components}/folded/random-file-2.docx +0 -0
  110. /package/src/{test-data/components → tests/test-components/map-components}/random-file.txt +0 -0
@@ -1,4 +1,4 @@
1
- /* eslint-disable dot-notation */
1
+ /* eslint-disable dot-notation */
2
2
  /* eslint-disable no-unused-expressions */
3
3
  /* eslint-disable @typescript-eslint/no-explicit-any */
4
4
  import { expect, use } from 'chai';
@@ -7,11 +7,11 @@ import {
7
7
  QUERY_PARAM_EDITING_SECRET,
8
8
  EditingRenderQueryParams,
9
9
  DesignLibraryMode,
10
- } from '@sitecore-content-sdk/core/editing';
10
+ } from '@sitecore-content-sdk/content/editing';
11
11
  import { EditingRenderMiddleware } from './editing-render-middleware';
12
12
  import sinonChai from 'sinon-chai';
13
13
  import sinon from 'sinon';
14
- import { mockRequest as MockRequest, Query } from '../test-data/helpers';
14
+ import { mockRequest as MockRequest, Query } from '../tests/helpers';
15
15
  import {
16
16
  QUERY_PARAM_VERCEL_PROTECTION_BYPASS,
17
17
  QUERY_PARAM_VERCEL_SET_BYPASS_COOKIE,
@@ -49,7 +49,7 @@ const toQuery = (params: Query | EditingRenderQueryParams): Query => {
49
49
 
50
50
  Object.entries(params).forEach(([key, value]) => {
51
51
  if (value !== undefined && value !== null) {
52
- query[key] = String(value);
52
+ query[key] = Array.isArray(value) ? value.map(String) : String(value);
53
53
  }
54
54
  });
55
55
 
@@ -106,15 +106,17 @@ describe('EditingRenderMiddleware', () => {
106
106
  expect(res.body).to.equal(null);
107
107
 
108
108
  expect(res.headers.has('Access-Control-Allow-Origin')).to.be.true;
109
- expect(res.headers.get('Access-Control-Allow-Origin')).to.equal(allowedOrigin);
109
+ expect(res.headers.get('Access-Control-Allow-Origin')).to.include(allowedOrigin);
110
110
 
111
111
  expect(res.headers.has('Access-Control-Allow-Methods')).to.be.true;
112
- expect(res.headers.get('Access-Control-Allow-Methods')).to.equal(
112
+ expect(res.headers.get('Access-Control-Allow-Methods')).to.include(
113
113
  'GET, POST, OPTIONS, DELETE, PUT, PATCH'
114
114
  );
115
115
 
116
116
  expect(res.headers.has('Access-Control-Allow-Headers')).to.be.true;
117
- expect(res.headers.get('Access-Control-Allow-Headers')).to.equal('Content-Type, Authorization');
117
+ expect(res.headers.get('Access-Control-Allow-Headers')).to.include(
118
+ 'Content-Type, Authorization'
119
+ );
118
120
  });
119
121
 
120
122
  it('should respond with 401 for invalid secret', async () => {
@@ -295,6 +297,7 @@ describe('EditingRenderMiddleware', () => {
295
297
  expect(res.headers.get('Content-Security-Policy')).to.equal(
296
298
  `frame-ancestors 'self' https://allowed.com ${EDITING_ALLOWED_ORIGINS.join(' ')}`
297
299
  );
300
+ expect(res.headers.get('Content-Type')).to.equal('text/html; charset=utf-8');
298
301
  });
299
302
 
300
303
  it('should use custom resolvePageUrl', async () => {
@@ -377,6 +380,7 @@ describe('EditingRenderMiddleware', () => {
377
380
  expect(res.headers.get('Content-Security-Policy')).to.equal(
378
381
  `frame-ancestors 'self' https://allowed.com ${EDITING_ALLOWED_ORIGINS.join(' ')}`
379
382
  );
383
+ expect(res.headers.get('Content-Type')).to.equal('text/html; charset=utf-8');
380
384
  });
381
385
 
382
386
  it('should response with 400 for missing query params', async () => {
@@ -440,6 +444,332 @@ describe('EditingRenderMiddleware', () => {
440
444
  expect(fetchRequestUrl.includes('someOtherParam=shouldNotBeIncluded')).to.be.false;
441
445
  });
442
446
 
447
+ describe('allowedQueryParams configuration', () => {
448
+ it('should not include additional query params when allowedQueryParams is not configured', async () => {
449
+ const customQuery = {
450
+ ...query,
451
+ customParam1: 'value1',
452
+ customParam2: 'value2',
453
+ };
454
+ const req = mockRequest({ query: customQuery });
455
+
456
+ const middleware = new EditingRenderMiddleware();
457
+ const handler = middleware.getHandler();
458
+
459
+ sinon
460
+ .stub(middleware['dataFetcher'], 'get')
461
+ .resolves({ status: 200, statusText: 'success', data: '<div>some html</div>' });
462
+
463
+ const getPreviewDataCookiesSpy = sinon.spy(middleware as any, 'getPreviewDataCookies');
464
+
465
+ await handler(req);
466
+
467
+ expect(getPreviewDataCookiesSpy).to.have.been.calledWith({
468
+ site: 'website',
469
+ itemId: '{11111111-1111-1111-1111-111111111111}',
470
+ language: 'en',
471
+ variantIds: ['dev'],
472
+ version: 'latest',
473
+ mode: 'edit',
474
+ layoutKind: 'shared',
475
+ });
476
+ });
477
+
478
+ it('should include allowed query params when configured as array (objects and strings)', async () => {
479
+ const customQuery = {
480
+ ...query,
481
+ customParam1: 'value1',
482
+ customParam2: 'value2',
483
+ stringParam: 'string-value',
484
+ notAllowed: 'shouldNotBeIncluded',
485
+ };
486
+ const req = mockRequest({ query: customQuery });
487
+
488
+ const middleware = new EditingRenderMiddleware({
489
+ allowedQueryParams: [
490
+ { name: 'customParam1' },
491
+ { name: 'customParam2' },
492
+ 'stringParam',
493
+ 'missingStringParam',
494
+ ],
495
+ });
496
+
497
+ const handler = middleware.getHandler();
498
+
499
+ sinon
500
+ .stub(middleware['dataFetcher'], 'get')
501
+ .resolves({ status: 200, statusText: 'success', data: '<div>some html</div>' });
502
+
503
+ const getPreviewDataCookiesSpy = sinon.spy(middleware as any, 'getPreviewDataCookies');
504
+
505
+ await handler(req);
506
+
507
+ expect(getPreviewDataCookiesSpy).to.have.been.calledWith({
508
+ site: 'website',
509
+ itemId: '{11111111-1111-1111-1111-111111111111}',
510
+ language: 'en',
511
+ variantIds: ['dev'],
512
+ version: 'latest',
513
+ mode: 'edit',
514
+ layoutKind: 'shared',
515
+ customParam1: 'value1',
516
+ customParam2: 'value2',
517
+ stringParam: 'string-value',
518
+ });
519
+ });
520
+
521
+ it('should return 400 when required allowed query param is missing (mixed types)', async () => {
522
+ const customQuery = {
523
+ ...query,
524
+ customParam1: 'value1',
525
+ stringParam: 'value',
526
+ };
527
+ const req = mockRequest({ query: customQuery });
528
+
529
+ const middleware = new EditingRenderMiddleware({
530
+ allowedQueryParams: [
531
+ { name: 'customParam1', required: true },
532
+ { name: 'customParam2', required: true },
533
+ 'stringParam',
534
+ ],
535
+ });
536
+ const handler = middleware.getHandler();
537
+
538
+ const res = await handler(req);
539
+
540
+ const body = await res.json();
541
+
542
+ expect(res.status).to.equal(400);
543
+ expect(body).to.deep.equal({
544
+ html: '<html><body>Missing required query parameters: customParam2</body></html>',
545
+ });
546
+ });
547
+
548
+ it('should handle optional allowed query params correctly', async () => {
549
+ const customQuery = {
550
+ ...query,
551
+ requiredParam: 'required-value',
552
+ // optionalParam is not provided
553
+ };
554
+ const req = mockRequest({ query: customQuery });
555
+
556
+ const middleware = new EditingRenderMiddleware({
557
+ allowedQueryParams: [
558
+ { name: 'requiredParam', required: true },
559
+ { name: 'optionalParam', required: false },
560
+ ],
561
+ });
562
+ const handler = middleware.getHandler();
563
+
564
+ sinon
565
+ .stub(middleware['dataFetcher'], 'get')
566
+ .resolves({ status: 200, statusText: 'success', data: '<div>some html</div>' });
567
+
568
+ const getPreviewDataCookiesSpy = sinon.spy(middleware as any, 'getPreviewDataCookies');
569
+
570
+ const res = await handler(req);
571
+
572
+ expect(getPreviewDataCookiesSpy).to.have.been.calledWith({
573
+ site: 'website',
574
+ itemId: '{11111111-1111-1111-1111-111111111111}',
575
+ language: 'en',
576
+ variantIds: ['dev'],
577
+ version: 'latest',
578
+ mode: 'edit',
579
+ layoutKind: 'shared',
580
+ requiredParam: 'required-value',
581
+ });
582
+ expect(res.status).to.equal(200);
583
+ });
584
+
585
+ it('should use resolver function returning strings and objects', async () => {
586
+ const customQuery = {
587
+ ...query,
588
+ prefixedParam1: 'value1',
589
+ prefixedParam2: 'value2',
590
+ requiredParam: 'required-value',
591
+ otherParam: 'shouldNotBeIncluded',
592
+ };
593
+ const req = mockRequest({ query: customQuery });
594
+
595
+ const resolver = (queryParamKeys: string[]) => {
596
+ const result: Array<string | { name: string; required?: boolean }> = [];
597
+ queryParamKeys
598
+ .filter((key) => key.startsWith('prefixed'))
599
+ .forEach((key) => result.push(key));
600
+
601
+ if (queryParamKeys.includes('requiredParam')) {
602
+ result.push({ name: 'requiredParam', required: true });
603
+ }
604
+ return result;
605
+ };
606
+
607
+ const middleware = new EditingRenderMiddleware({
608
+ allowedQueryParams: resolver,
609
+ });
610
+ const handler = middleware.getHandler();
611
+
612
+ sinon
613
+ .stub(middleware['dataFetcher'], 'get')
614
+ .resolves({ status: 200, statusText: 'success', data: '<div>some html</div>' });
615
+
616
+ const getPreviewDataCookiesSpy = sinon.spy(middleware as any, 'getPreviewDataCookies');
617
+
618
+ await handler(req);
619
+
620
+ expect(getPreviewDataCookiesSpy).to.have.been.calledWith({
621
+ site: 'website',
622
+ itemId: '{11111111-1111-1111-1111-111111111111}',
623
+ language: 'en',
624
+ variantIds: ['dev'],
625
+ version: 'latest',
626
+ mode: 'edit',
627
+ layoutKind: 'shared',
628
+ prefixedParam1: 'value1',
629
+ prefixedParam2: 'value2',
630
+ requiredParam: 'required-value',
631
+ });
632
+ });
633
+
634
+ it('should return 400 when resolver returns mixed types with missing required param', async () => {
635
+ const customQuery = {
636
+ ...query,
637
+ presentParam: 'value1',
638
+ stringParam: 'string-value',
639
+ };
640
+ const req = mockRequest({ query: customQuery });
641
+
642
+ const resolver = () => {
643
+ return [
644
+ 'stringParam',
645
+ { name: 'presentParam', required: true },
646
+ { name: 'missingRequiredParam', required: true },
647
+ ];
648
+ };
649
+
650
+ const middleware = new EditingRenderMiddleware({
651
+ allowedQueryParams: resolver,
652
+ });
653
+ const handler = middleware.getHandler();
654
+
655
+ const res = await handler(req);
656
+
657
+ const body = await res.json();
658
+
659
+ expect(res.status).to.equal(400);
660
+ expect(body).to.deep.equal({
661
+ html: '<html><body>Missing required query parameters: missingRequiredParam</body></html>',
662
+ });
663
+ });
664
+
665
+ it('should handle resolver function returning empty array', async () => {
666
+ const customQuery = {
667
+ ...query,
668
+ customParam: 'value',
669
+ };
670
+ const req = mockRequest({ query: customQuery });
671
+
672
+ const resolver = () => {
673
+ return []; // No additional params allowed
674
+ };
675
+
676
+ const middleware = new EditingRenderMiddleware({
677
+ allowedQueryParams: resolver,
678
+ });
679
+ const handler = middleware.getHandler();
680
+
681
+ sinon
682
+ .stub(middleware['dataFetcher'], 'get')
683
+ .resolves({ status: 200, statusText: 'success', data: '<div>some html</div>' });
684
+
685
+ const getPreviewDataCookiesSpy = sinon.spy(middleware as any, 'getPreviewDataCookies');
686
+
687
+ const res = await handler(req);
688
+
689
+ expect(getPreviewDataCookiesSpy).to.have.been.calledWith({
690
+ site: 'website',
691
+ itemId: '{11111111-1111-1111-1111-111111111111}',
692
+ language: 'en',
693
+ variantIds: ['dev'],
694
+ version: 'latest',
695
+ mode: 'edit',
696
+ layoutKind: 'shared',
697
+ });
698
+ expect(res.status).to.equal(200);
699
+ });
700
+
701
+ it('should handle various data types in allowed query params', async () => {
702
+ const customQuery = {
703
+ ...query,
704
+ stringParam: 'string-value',
705
+ numberParam: '123',
706
+ booleanParam: 'true',
707
+ arrayParam: ['val1', 'val2'],
708
+ };
709
+ const req = mockRequest({ query: customQuery });
710
+
711
+ const middleware = new EditingRenderMiddleware({
712
+ allowedQueryParams: [
713
+ { name: 'stringParam' },
714
+ { name: 'numberParam' },
715
+ { name: 'booleanParam' },
716
+ { name: 'arrayParam' },
717
+ ],
718
+ });
719
+ const handler = middleware.getHandler();
720
+
721
+ sinon
722
+ .stub(middleware['dataFetcher'], 'get')
723
+ .resolves({ status: 200, statusText: 'success', data: '<div>some html</div>' });
724
+
725
+ const getPreviewDataCookiesSpy = sinon.spy(middleware as any, 'getPreviewDataCookies');
726
+
727
+ await handler(req);
728
+
729
+ expect(getPreviewDataCookiesSpy).to.have.been.calledWith({
730
+ site: 'website',
731
+ itemId: '{11111111-1111-1111-1111-111111111111}',
732
+ language: 'en',
733
+ variantIds: ['dev'],
734
+ version: 'latest',
735
+ mode: 'edit',
736
+ layoutKind: 'shared',
737
+ stringParam: 'string-value',
738
+ numberParam: '123',
739
+ booleanParam: 'true',
740
+ arrayParam: ['val1', 'val2'],
741
+ });
742
+ });
743
+
744
+ it('should combine missing required editing params and missing required allowed params in error message', async () => {
745
+ const customQuery = {
746
+ sc_site: 'website',
747
+ secret: secret,
748
+ // missing: sc_itemid, sc_lang, route, mode
749
+ // missing: requiredAllowedParam
750
+ };
751
+ const req = mockRequest({ query: customQuery });
752
+
753
+ const middleware = new EditingRenderMiddleware({
754
+ allowedQueryParams: [{ name: 'requiredAllowedParam', required: true }],
755
+ });
756
+ const handler = middleware.getHandler();
757
+
758
+ const res = await handler(req);
759
+
760
+ const body = await res.json();
761
+ const html = body.html;
762
+
763
+ expect(res.status).to.equal(400);
764
+ expect(html).to.include('Missing required query parameters:');
765
+ expect(html).to.include('sc_itemid');
766
+ expect(html).to.include('sc_lang');
767
+ expect(html).to.include('route');
768
+ expect(html).to.include('mode');
769
+ expect(html).to.include('requiredAllowedParam');
770
+ });
771
+ });
772
+
443
773
  it('should issue intrnal request propagating allowed headers', async () => {
444
774
  const req = mockRequest({
445
775
  query,
@@ -485,6 +815,7 @@ describe('EditingRenderMiddleware', () => {
485
815
  const res = await handler(req);
486
816
 
487
817
  expect(res.status).to.equal(200);
818
+ expect(res.headers.get('Content-Type')).to.equal('text/html; charset=utf-8');
488
819
  });
489
820
 
490
821
  it('should remove preview cookies before responding to browser', async () => {
@@ -503,32 +834,42 @@ describe('EditingRenderMiddleware', () => {
503
834
  expect(res.status).to.equal(200);
504
835
  });
505
836
 
506
- it('should respondWith 500 if rendered html empty', async () => {
507
- const req = mockRequest({ query });
837
+ describe('error handling', () => {
838
+ beforeEach(() => {
839
+ sinon.stub(console, 'error');
840
+ });
508
841
 
509
- const middleware = new EditingRenderMiddleware();
510
- const handler = middleware.getHandler();
842
+ afterEach(() => {
843
+ sinon.restore();
844
+ });
511
845
 
512
- sinon
513
- .stub(middleware['dataFetcher'], 'get')
514
- .resolves({ status: 200, statusText: 'success', data: '' });
846
+ it('should respondWith 500 if rendered html empty', async () => {
847
+ const req = mockRequest({ query });
515
848
 
516
- const res = await handler(req);
849
+ const middleware = new EditingRenderMiddleware();
850
+ const handler = middleware.getHandler();
517
851
 
518
- expect(res.status).to.equal(500);
519
- });
852
+ sinon
853
+ .stub(middleware['dataFetcher'], 'get')
854
+ .resolves({ status: 200, statusText: 'success', data: '' });
520
855
 
521
- it('should respondWith 500 if internal request fails', async () => {
522
- const req = mockRequest({ query });
856
+ const res = await handler(req);
523
857
 
524
- const middleware = new EditingRenderMiddleware();
525
- const handler = middleware.getHandler();
858
+ expect(res.status).to.equal(500);
859
+ });
526
860
 
527
- sinon.stub(middleware['dataFetcher'], 'get').throws(new Error('Request failed'));
861
+ it('should respondWith 500 if internal request fails', async () => {
862
+ const req = mockRequest({ query });
528
863
 
529
- const res = await handler(req);
864
+ const middleware = new EditingRenderMiddleware();
865
+ const handler = middleware.getHandler();
866
+
867
+ sinon.stub(middleware['dataFetcher'], 'get').throws(new Error('Request failed'));
530
868
 
531
- expect(res.status).to.equal(500);
869
+ const res = await handler(req);
870
+
871
+ expect(res.status).to.equal(500);
872
+ });
532
873
  });
533
874
 
534
875
  describe('Design Library handling', () => {
@@ -708,6 +1049,7 @@ describe('EditingRenderMiddleware', () => {
708
1049
 
709
1050
  expect(res.status).to.equal(200);
710
1051
  expect(body).to.equal('<div>some html</div>');
1052
+ expect(res.headers.get('Content-Type')).to.equal('text/html; charset=utf-8');
711
1053
  });
712
1054
  });
713
1055
 
@@ -1,12 +1,14 @@
1
- import { debug, NativeDataFetcher } from '@sitecore-content-sdk/core';
1
+ import { NativeDataFetcher } from '@sitecore-content-sdk/core';
2
2
  import {
3
3
  QUERY_PARAM_EDITING_SECRET,
4
4
  EDITING_ALLOWED_ORIGINS,
5
- } from '@sitecore-content-sdk/core/editing';
5
+ INVALID_SECRET_HTML_MESSAGE,
6
+ } from '@sitecore-content-sdk/content/editing';
6
7
  import { getEditingSecret } from '../utils';
7
- import { getEnforcedCorsHeaders } from '@sitecore-content-sdk/core/utils';
8
- import { LayoutServicePageState } from '@sitecore-content-sdk/core/layout';
8
+ import { getEnforcedCorsHeaders } from '@sitecore-content-sdk/core/tools';
9
+ import { LayoutServicePageState } from '@sitecore-content-sdk/content/layout';
9
10
  import { RenderMiddlewareBase } from './render-middleware';
11
+ import debug from '../debug';
10
12
  import {
11
13
  cleanupPreviewCookies,
12
14
  getCSPHeader,
@@ -19,11 +21,14 @@ import {
19
21
  mapEditingParams,
20
22
  PreviewCookies,
21
23
  resolveServerUrl,
24
+ getAllowedQueryParams,
22
25
  } from './utils';
26
+ import type { AllowedQueryParams } from './types';
23
27
  import * as cookie from 'cookie';
24
28
 
25
29
  /**
26
30
  * Configuration for the Editing Render Middleware.
31
+ * @public
27
32
  */
28
33
  export type EditingRenderMiddlewareConfig = {
29
34
  /**
@@ -38,11 +43,18 @@ export type EditingRenderMiddlewareConfig = {
38
43
  * The internal host URL for the application, used for server-side requests for page rendering during editing.
39
44
  */
40
45
  sitecoreInternalEditingHostUrl?: string;
46
+ /**
47
+ * Query string parameters to allow and include in the preview data.
48
+ * - Array: each item is a parameter name (string) or an object `{ name, required? }`.
49
+ * - Function: receives the request's query parameter names and returns the list of allowed parameters.
50
+ */
51
+ allowedQueryParams?: AllowedQueryParams;
41
52
  };
42
53
 
43
54
  /**
44
55
  * Middleware / handler for use in the editing render API route (e.g. '/api/editing/render')
45
56
  * which is required for Sitecore editing support.
57
+ * @public
46
58
  */
47
59
  export class EditingRenderMiddleware extends RenderMiddlewareBase {
48
60
  private dataFetcher: NativeDataFetcher;
@@ -85,7 +97,7 @@ export class EditingRenderMiddleware extends RenderMiddlewareBase {
85
97
 
86
98
  private handler = async (_req: Request): Promise<Response> => {
87
99
  const { method, headers } = _req;
88
- const url = new URL(_req.url.toLowerCase());
100
+ const url = new URL(_req.url);
89
101
  const query = getEditingRenderQueryParams(url.searchParams);
90
102
 
91
103
  debug.editing('editing render middleware start: %o', {
@@ -132,7 +144,7 @@ export class EditingRenderMiddleware extends RenderMiddlewareBase {
132
144
 
133
145
  return new Response(
134
146
  JSON.stringify({
135
- html: '<html><body>Missing or invalid secret</body></html>',
147
+ html: INVALID_SECRET_HTML_MESSAGE,
136
148
  }),
137
149
  {
138
150
  status: 401,
@@ -144,7 +156,7 @@ export class EditingRenderMiddleware extends RenderMiddlewareBase {
144
156
  if (_req.method === 'OPTIONS') {
145
157
  debug.editing('preflight request');
146
158
 
147
- // CORS headers are set by enforceCors
159
+ // CORS headers are set by getEnforcedCorsHeaders
148
160
  return new Response(null, {
149
161
  status: 204,
150
162
  headers: _res.headers,
@@ -175,15 +187,24 @@ export class EditingRenderMiddleware extends RenderMiddlewareBase {
175
187
 
176
188
  const missingQueryParams = requiredQueryParams.filter((param) => !query[param]);
177
189
 
190
+ const { allowedQueryParams, missingAllowedParams } = getAllowedQueryParams(
191
+ query,
192
+ this.config?.allowedQueryParams
193
+ );
194
+
178
195
  // Validate query parameters
179
- if (missingQueryParams.length) {
180
- debug.editing('missing required query parameters: %o', missingQueryParams);
196
+ if (missingQueryParams.length || missingAllowedParams.length) {
197
+ debug.editing('missing required query parameters: %o', [
198
+ ...missingQueryParams,
199
+ ...missingAllowedParams,
200
+ ]);
181
201
 
182
202
  return new Response(
183
203
  JSON.stringify({
184
- html: `<html><body>Missing required query parameters: ${missingQueryParams.join(
185
- ', '
186
- )}</body></html>`,
204
+ html: `<html><body>Missing required query parameters: ${[
205
+ ...missingQueryParams,
206
+ ...missingAllowedParams,
207
+ ].join(', ')}</body></html>`,
187
208
  }),
188
209
  {
189
210
  status: 400,
@@ -196,6 +217,7 @@ export class EditingRenderMiddleware extends RenderMiddlewareBase {
196
217
 
197
218
  const previewDataCookies = this.getPreviewDataCookies({
198
219
  ...previewDataParams,
220
+ ...allowedQueryParams,
199
221
  variantIds: previewDataParams.variantIds?.split(','),
200
222
  });
201
223
  _res.headers.append('Set-Cookie', previewDataCookies);
@@ -14,3 +14,5 @@ export {
14
14
  getHeadersForPropagation,
15
15
  PreviewCookies,
16
16
  } from './utils';
17
+
18
+ export type { AllowedQueryParam, AllowedQueryParamsResolver, AllowedQueryParams } from './types';
@@ -1,7 +1,7 @@
1
1
  /* eslint-disable dot-notation */
2
2
  import chai from 'chai';
3
3
  import chaiString from 'chai-string';
4
- import { QUERY_PARAM_EDITING_SECRET } from '@sitecore-content-sdk/core/editing';
4
+ import { QUERY_PARAM_EDITING_SECRET } from '@sitecore-content-sdk/content/editing';
5
5
  import { RenderMiddlewareBase } from './render-middleware';
6
6
  import {
7
7
  QUERY_PARAM_VERCEL_PROTECTION_BYPASS,
@@ -6,7 +6,7 @@ import {
6
6
 
7
7
  /**
8
8
  * Base class for middleware that handles pages and components rendering in Sitecore Editors.
9
- * @deprecated getQueryParamsForPropagation and getHeadersForPropagation methods have been moved to separate exports
9
+ * @internal
10
10
  */
11
11
  export abstract class RenderMiddlewareBase {
12
12
  /**
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Represents an allowed query parameter.
3
+ * @public
4
+ */
5
+ export interface AllowedQueryParam {
6
+ /**
7
+ * The name of the query parameter to allow.
8
+ */
9
+ name: string;
10
+ /**
11
+ * Whether the query parameter is required.
12
+ */
13
+ required?: boolean;
14
+ }
15
+
16
+ /**
17
+ * Resolver function for allowed query parameters, which can be used to extract additional parameters from the query string beyond the required editing parameters.
18
+ * @param {string[]} queryParams Array of query parameters from incoming URL.
19
+ * @returns {Array<AllowedQueryParam | string>} Allowed query editing parameters.
20
+ * @public
21
+ */
22
+ export type AllowedQueryParamsResolver = (
23
+ queryParams: string[]
24
+ ) => Array<AllowedQueryParam | string>;
25
+
26
+ /**
27
+ * Allowed query parameters which can be defined as an array of parameter names or objects, or a resolver function which can be used to extract additional parameters from the query string beyond the required editing parameters.
28
+ * @public
29
+ */
30
+ export type AllowedQueryParams = Array<AllowedQueryParam | string> | AllowedQueryParamsResolver;
31
+
32
+ /**
33
+ * Result of processing allowed query parameters, including any missing required parameters and the set of allowed parameters that were extracted from the query string.
34
+ * @internal
35
+ */
36
+ export interface GetAllowedQueryParamsResult {
37
+ missingAllowedParams: string[];
38
+ allowedQueryParams: { [key: string]: unknown };
39
+ }