@projectdochelp/s3te 3.2.1 → 3.2.3
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.
- package/README.md +470 -29
- package/package.json +3 -2
- package/packages/aws-adapter/src/deploy.mjs +4 -0
- package/packages/aws-adapter/src/features.mjs +13 -1
- package/packages/aws-adapter/src/manifest.mjs +5 -1
- package/packages/aws-adapter/src/package.mjs +10 -1
- package/packages/aws-adapter/src/runtime/common.mjs +3 -0
- package/packages/aws-adapter/src/runtime/sitemap-updater.mjs +221 -0
- package/packages/aws-adapter/src/template.mjs +68 -1
- package/packages/cli/bin/s3te.mjs +2 -0
- package/packages/cli/src/project.mjs +64 -0
- package/packages/core/src/config.mjs +64 -4
- package/packages/core/src/index.mjs +1 -0
package/README.md
CHANGED
|
@@ -17,7 +17,8 @@ This README is the user guide for the rewrite generation. The deeper implementat
|
|
|
17
17
|
- [Usage](#usage)
|
|
18
18
|
- [Daily Workflow](#daily-workflow)
|
|
19
19
|
- [CLI Commands](#cli-commands)
|
|
20
|
-
|
|
20
|
+
- [Template Commands](#template-commands)
|
|
21
|
+
- [Optional: Sitemap](#optional-sitemap)
|
|
21
22
|
- [Optional: Webiny CMS](#optional-webiny-cms)
|
|
22
23
|
|
|
23
24
|
## Motivation
|
|
@@ -65,6 +66,8 @@ That source sync is not limited to Lambda code. It includes your `.html`, `.part
|
|
|
65
66
|
|
|
66
67
|
The persistent environment stack contains the long-lived AWS resources such as buckets, Lambda functions, DynamoDB tables, CloudFront distributions and the runtime manifest parameter. The temporary deploy stack exists only so CloudFormation can consume the packaged Lambda artifacts cleanly.
|
|
67
68
|
|
|
69
|
+
If optional runtime features such as `sitemap` or Webiny are enabled in `s3te.config.json`, the same environment stack also carries those extra Lambdas and event bindings.
|
|
70
|
+
|
|
68
71
|
</details>
|
|
69
72
|
|
|
70
73
|
## Installation (AWS)
|
|
@@ -226,13 +229,18 @@ The most important fields for a first deployment are:
|
|
|
226
229
|
|
|
227
230
|
`route53HostedZoneId` is optional. Leave it out if you want to manage DNS yourself.
|
|
228
231
|
|
|
229
|
-
Use plain hostnames in `baseUrl` and `cloudFrontAliases`, not full URLs. If your config contains a `prod` environment plus additional environments such as `test` or `stage`, S3TE keeps the `prod` hostname unchanged and derives non-production hostnames
|
|
232
|
+
Use plain hostnames in `baseUrl` and `cloudFrontAliases`, not full URLs. If your config contains a `prod` environment plus additional environments such as `test` or `stage`, S3TE keeps the `prod` hostname unchanged and derives non-production hostnames like this:
|
|
233
|
+
|
|
234
|
+
- apex host: `example.com` -> `test.example.com`
|
|
235
|
+
- first-level subdomain: `app.example.com` -> `test-app.example.com`
|
|
236
|
+
- deeper host: `admin.app.example.com` -> `test-admin.app.example.com`
|
|
230
237
|
|
|
231
238
|
Your ACM certificate must cover the final derived aliases of the environment you deploy. Example:
|
|
232
239
|
|
|
233
240
|
- `*.example.com` covers `test.example.com`
|
|
234
|
-
- `*.example.com`
|
|
235
|
-
-
|
|
241
|
+
- `*.example.com` also covers `test-app.example.com`
|
|
242
|
+
- `*.example.com` does not cover `test-admin.app.example.com`
|
|
243
|
+
- for deeper aliases like `test-admin.app.example.com`, add a SAN such as `*.app.example.com`, the exact hostname, or use a different `certificateArn` for that environment
|
|
236
244
|
|
|
237
245
|
</details>
|
|
238
246
|
|
|
@@ -476,31 +484,86 @@ Once Webiny is installed and the stack is deployed with Webiny enabled, CMS cont
|
|
|
476
484
|
| `s3te sync --env <name>` | Uploads current project sources into the configured code buckets. |
|
|
477
485
|
| `s3te doctor --env <name>` | Checks local machine and AWS access before deploy. |
|
|
478
486
|
| `s3te deploy --env <name>` | Deploys or updates the AWS environment and syncs source files. |
|
|
479
|
-
| `s3te migrate` | Updates older project configs and can retrofit Webiny into an existing S3TE project. |
|
|
487
|
+
| `s3te migrate` | Updates older project configs and can retrofit optional features such as `sitemap` or Webiny into an existing S3TE project. |
|
|
480
488
|
|
|
481
489
|
</details>
|
|
482
490
|
|
|
483
|
-
|
|
491
|
+
## Template Commands
|
|
492
|
+
|
|
493
|
+
S3TE uses literal HTML-like tags inside your `.html` and `.part` files. The tags are case-sensitive, always lowercase, and never use attributes. JSON-based commands must contain valid JSON and reject unknown properties.
|
|
494
|
+
|
|
495
|
+
The commands below are grouped by purpose:
|
|
484
496
|
|
|
485
|
-
|
|
497
|
+
- `Core Features` work in every S3TE project.
|
|
498
|
+
- `Webiny Features` are the content-driven commands. Despite the name, they also work with local content files under `offline/content/` when you are not using Webiny yet.
|
|
499
|
+
|
|
500
|
+
### Core Features
|
|
486
501
|
|
|
487
502
|
<details>
|
|
488
|
-
<summary><code><part></code> - reuse a partial file</summary>
|
|
503
|
+
<summary><code><part></code> - reuse a partial file from <code>partDir</code></summary>
|
|
504
|
+
|
|
505
|
+
**Action**
|
|
506
|
+
|
|
507
|
+
Loads another template fragment from the current variant's `partDir`, renders it recursively, and inserts the result at the current position.
|
|
508
|
+
|
|
509
|
+
**Syntax**
|
|
489
510
|
|
|
490
511
|
```html
|
|
491
512
|
<part>head.part</part>
|
|
492
513
|
```
|
|
493
514
|
|
|
515
|
+
The payload must be a relative path inside `partDir`. Leading `/` and `..` are invalid.
|
|
516
|
+
|
|
517
|
+
**Example**
|
|
518
|
+
|
|
519
|
+
```html
|
|
520
|
+
<head>
|
|
521
|
+
<part>head.part</part>
|
|
522
|
+
</head>
|
|
523
|
+
```
|
|
524
|
+
|
|
494
525
|
</details>
|
|
495
526
|
|
|
496
527
|
<details>
|
|
497
528
|
<summary><code><if></code> - render inline HTML only when a condition matches</summary>
|
|
498
529
|
|
|
530
|
+
**Action**
|
|
531
|
+
|
|
532
|
+
Evaluates one inline JSON rule and renders its `template` only when the rule matches the current render target.
|
|
533
|
+
|
|
534
|
+
**Syntax**
|
|
535
|
+
|
|
536
|
+
```html
|
|
537
|
+
<if>{
|
|
538
|
+
"env": "prod",
|
|
539
|
+
"file": "index.html",
|
|
540
|
+
"not": false,
|
|
541
|
+
"template": "<meta name='robots' content='all'>"
|
|
542
|
+
}</if>
|
|
543
|
+
```
|
|
544
|
+
|
|
545
|
+
Supported JSON properties:
|
|
546
|
+
|
|
547
|
+
- `env` optional: matches the current environment name case-insensitively
|
|
548
|
+
- `file` optional: matches the current output filename, for example `index.html`
|
|
549
|
+
- `not` optional: inverts the final result when `true`
|
|
550
|
+
- `template` required: inline HTML to render when the rule matches
|
|
551
|
+
|
|
552
|
+
If both `env` and `file` are present, both must match.
|
|
553
|
+
|
|
554
|
+
If both conditions are omitted, the tag behaves like an inline template include and always renders its `template`.
|
|
555
|
+
|
|
556
|
+
**Example**
|
|
557
|
+
|
|
499
558
|
```html
|
|
500
559
|
<if>{
|
|
501
560
|
"env": "prod",
|
|
502
561
|
"template": "<meta name='robots' content='all'>"
|
|
503
562
|
}</if>
|
|
563
|
+
<if>{
|
|
564
|
+
"env": "test",
|
|
565
|
+
"template": "<meta name='robots' content='noindex'>"
|
|
566
|
+
}</if>
|
|
504
567
|
```
|
|
505
568
|
|
|
506
569
|
</details>
|
|
@@ -508,18 +571,55 @@ These are the core S3TE commands you will use even in a plain HTML-only project.
|
|
|
508
571
|
<details>
|
|
509
572
|
<summary><code><fileattribute></code> - print metadata of the current output file</summary>
|
|
510
573
|
|
|
574
|
+
**Action**
|
|
575
|
+
|
|
576
|
+
Prints metadata of the file currently being rendered.
|
|
577
|
+
|
|
578
|
+
**Syntax**
|
|
579
|
+
|
|
511
580
|
```html
|
|
512
581
|
<fileattribute>filename</fileattribute>
|
|
513
582
|
```
|
|
514
583
|
|
|
584
|
+
Currently supported values:
|
|
585
|
+
|
|
586
|
+
- `filename`: the output key relative to the target bucket, for example `news/article-one.html`
|
|
587
|
+
|
|
588
|
+
**Example**
|
|
589
|
+
|
|
590
|
+
```html
|
|
591
|
+
<link rel="canonical" href="https://<lang>baseurl</lang>/<fileattribute>filename</fileattribute>">
|
|
592
|
+
```
|
|
593
|
+
|
|
515
594
|
</details>
|
|
516
595
|
|
|
517
596
|
<details>
|
|
518
|
-
<summary><code><lang></code> - print
|
|
597
|
+
<summary><code><lang></code> - print current language metadata</summary>
|
|
598
|
+
|
|
599
|
+
**Action**
|
|
600
|
+
|
|
601
|
+
Prints language-related metadata of the current render target.
|
|
602
|
+
|
|
603
|
+
**Syntax**
|
|
604
|
+
|
|
605
|
+
```html
|
|
606
|
+
<lang>2</lang>
|
|
607
|
+
<lang>baseurl</lang>
|
|
608
|
+
```
|
|
609
|
+
|
|
610
|
+
Currently supported values:
|
|
611
|
+
|
|
612
|
+
- `2`: the current language code such as `en` or `de`
|
|
613
|
+
- `baseurl`: the resolved base hostname for the current language and environment
|
|
614
|
+
|
|
615
|
+
**Example**
|
|
519
616
|
|
|
520
617
|
```html
|
|
521
618
|
<html lang="<lang>2</lang>">
|
|
522
|
-
<
|
|
619
|
+
<head>
|
|
620
|
+
<link rel="canonical" href="https://<lang>baseurl</lang>">
|
|
621
|
+
</head>
|
|
622
|
+
</html>
|
|
523
623
|
```
|
|
524
624
|
|
|
525
625
|
</details>
|
|
@@ -527,6 +627,12 @@ These are the core S3TE commands you will use even in a plain HTML-only project.
|
|
|
527
627
|
<details>
|
|
528
628
|
<summary><code><switchlang></code> - choose inline content by language</summary>
|
|
529
629
|
|
|
630
|
+
**Action**
|
|
631
|
+
|
|
632
|
+
Selects the block whose tag name matches the current language and renders only that block.
|
|
633
|
+
|
|
634
|
+
**Syntax**
|
|
635
|
+
|
|
530
636
|
```html
|
|
531
637
|
<switchlang>
|
|
532
638
|
<de>Willkommen</de>
|
|
@@ -534,9 +640,327 @@ These are the core S3TE commands you will use even in a plain HTML-only project.
|
|
|
534
640
|
</switchlang>
|
|
535
641
|
```
|
|
536
642
|
|
|
643
|
+
There is no fallback to `defaultLanguage`. If the current language block is missing, S3TE renders an empty string and records a warning.
|
|
644
|
+
|
|
645
|
+
**Example**
|
|
646
|
+
|
|
647
|
+
```html
|
|
648
|
+
<p>
|
|
649
|
+
<switchlang>
|
|
650
|
+
<de>Dein ultimatives Website-Werkzeug</de>
|
|
651
|
+
<en>Your ultimate website tool</en>
|
|
652
|
+
</switchlang>
|
|
653
|
+
</p>
|
|
654
|
+
```
|
|
655
|
+
|
|
656
|
+
</details>
|
|
657
|
+
|
|
658
|
+
### Webiny Features
|
|
659
|
+
|
|
660
|
+
These commands read from the resolved content repository. With Webiny enabled, that means mirrored Webiny content. Without Webiny, the same commands can read from local JSON files under `offline/content/`.
|
|
661
|
+
|
|
662
|
+
<details>
|
|
663
|
+
<summary><code><dbpart></code> - insert one content fragment by <code>contentId</code></summary>
|
|
664
|
+
|
|
665
|
+
**Action**
|
|
666
|
+
|
|
667
|
+
Loads a single content item by `contentId` and inserts its content fragment.
|
|
668
|
+
|
|
669
|
+
S3TE first tries the language-specific field `content<lang>`, for example `contentde`, and falls back to `content` if the language-specific field does not exist.
|
|
670
|
+
|
|
671
|
+
**Syntax**
|
|
672
|
+
|
|
673
|
+
```html
|
|
674
|
+
<dbpart>impressum</dbpart>
|
|
675
|
+
```
|
|
676
|
+
|
|
677
|
+
The payload is the content ID, not the internal database record ID.
|
|
678
|
+
|
|
679
|
+
**Example**
|
|
680
|
+
|
|
681
|
+
```html
|
|
682
|
+
<body>
|
|
683
|
+
<dbpart>impressum</dbpart>
|
|
684
|
+
</body>
|
|
685
|
+
```
|
|
686
|
+
|
|
687
|
+
</details>
|
|
688
|
+
|
|
689
|
+
<details>
|
|
690
|
+
<summary><code><dbmulti></code> - render one inline template for multiple content items</summary>
|
|
691
|
+
|
|
692
|
+
**Action**
|
|
693
|
+
|
|
694
|
+
Queries matching content items and renders the given inline template once for each result.
|
|
695
|
+
|
|
696
|
+
**Syntax**
|
|
697
|
+
|
|
698
|
+
```html
|
|
699
|
+
<dbmulti>{
|
|
700
|
+
"filter": [
|
|
701
|
+
{"forWebsite": {"BOOL": true}}
|
|
702
|
+
],
|
|
703
|
+
"filtertype": "equals",
|
|
704
|
+
"limit": 3,
|
|
705
|
+
"template": "<article><h2><dbitem>headline</dbitem></h2></article>"
|
|
706
|
+
}</dbmulti>
|
|
707
|
+
```
|
|
708
|
+
|
|
709
|
+
Supported JSON properties:
|
|
710
|
+
|
|
711
|
+
- `filter` required: array of legacy DynamoDB-style filter clauses
|
|
712
|
+
- `filtertype` optional: `equals` or `contains`, default is `equals`
|
|
713
|
+
- `limit` optional: maximum number of items to render
|
|
714
|
+
- `template` required: inline template rendered once per match
|
|
715
|
+
|
|
716
|
+
Filter notes:
|
|
717
|
+
|
|
718
|
+
- every filter clause contains exactly one field
|
|
719
|
+
- multiple clauses are combined with logical `AND`
|
|
720
|
+
- `__typename` matches the content model, for example `article`
|
|
721
|
+
- supported legacy value wrappers are `S`, `N`, `BOOL`, `NULL`, and `L`
|
|
722
|
+
- results are sorted deterministically; numeric `order` comes first, then `contentId`, then `id`
|
|
723
|
+
|
|
724
|
+
**Example**
|
|
725
|
+
|
|
726
|
+
```html
|
|
727
|
+
<dbmulti>{
|
|
728
|
+
"filter": [
|
|
729
|
+
{"__typename": {"S": "article"}},
|
|
730
|
+
{"forWebsite": {"BOOL": true}}
|
|
731
|
+
],
|
|
732
|
+
"limit": 3,
|
|
733
|
+
"template": "<a href='article-<dbitem>slug</dbitem>.html'><h2><dbitem>headline</dbitem></h2></a>"
|
|
734
|
+
}</dbmulti>
|
|
735
|
+
```
|
|
736
|
+
|
|
737
|
+
</details>
|
|
738
|
+
|
|
739
|
+
<details>
|
|
740
|
+
<summary><code><dbmultifile></code> - generate one output file per content item</summary>
|
|
741
|
+
|
|
742
|
+
**Action**
|
|
743
|
+
|
|
744
|
+
Turns one source template into multiple output files. The content items are selected by filter, and each item produces one rendered file.
|
|
745
|
+
|
|
746
|
+
**Syntax**
|
|
747
|
+
|
|
748
|
+
```html
|
|
749
|
+
<dbmultifile>{
|
|
750
|
+
"filenamesuffix": "slug",
|
|
751
|
+
"filter": [
|
|
752
|
+
{"__typename": {"S": "article"}}
|
|
753
|
+
],
|
|
754
|
+
"limit": 10
|
|
755
|
+
}</dbmultifile>
|
|
756
|
+
<!doctype html>
|
|
757
|
+
<html>
|
|
758
|
+
<body>
|
|
759
|
+
<h1><dbmultifileitem>headline</dbmultifileitem></h1>
|
|
760
|
+
</body>
|
|
761
|
+
</html>
|
|
762
|
+
```
|
|
763
|
+
|
|
764
|
+
Supported JSON properties:
|
|
765
|
+
|
|
766
|
+
- `filenamesuffix` required: field whose value becomes the filename suffix
|
|
767
|
+
- `filter` required: array of legacy DynamoDB-style filter clauses
|
|
768
|
+
- `filtertype` optional: `equals` or `contains`, default is `equals`
|
|
769
|
+
- `limit` optional: maximum number of files to generate
|
|
770
|
+
|
|
771
|
+
Rules:
|
|
772
|
+
|
|
773
|
+
- `dbmultifile` must be the first non-whitespace construct in the file
|
|
774
|
+
- the control block itself is not part of the output
|
|
775
|
+
- generated filenames follow the pattern `<basename>-<suffix>.<ext>`
|
|
776
|
+
- the suffix must not be empty and must not contain `/`, `\\`, or `:`
|
|
777
|
+
- suffixes must be unique within that template
|
|
778
|
+
|
|
779
|
+
**Example**
|
|
780
|
+
|
|
781
|
+
If the source file is `article.html` and the current item has `"slug": "first-article"`, the generated output becomes `article-first-article.html`.
|
|
782
|
+
|
|
783
|
+
```html
|
|
784
|
+
<dbmultifile>{
|
|
785
|
+
"filenamesuffix": "slug",
|
|
786
|
+
"filter": [
|
|
787
|
+
{"__typename": {"S": "article"}}
|
|
788
|
+
]
|
|
789
|
+
}</dbmultifile>
|
|
790
|
+
<article>
|
|
791
|
+
<h1><dbmultifileitem>headline</dbmultifileitem></h1>
|
|
792
|
+
</article>
|
|
793
|
+
```
|
|
794
|
+
|
|
795
|
+
</details>
|
|
796
|
+
|
|
797
|
+
<details>
|
|
798
|
+
<summary><code><dbitem></code> - print one field of the current content item</summary>
|
|
799
|
+
|
|
800
|
+
**Action**
|
|
801
|
+
|
|
802
|
+
Reads one field from the current content item. This works inside `dbmulti` templates and inside `dbmultifile` bodies.
|
|
803
|
+
|
|
804
|
+
**Syntax**
|
|
805
|
+
|
|
806
|
+
```html
|
|
807
|
+
<dbitem>headline</dbitem>
|
|
808
|
+
```
|
|
809
|
+
|
|
810
|
+
Special field names:
|
|
811
|
+
|
|
812
|
+
- `__typename`
|
|
813
|
+
- `contentId`
|
|
814
|
+
- `id`
|
|
815
|
+
- `locale`
|
|
816
|
+
- `tenant`
|
|
817
|
+
- `_version`
|
|
818
|
+
- `_lastChangedAt`
|
|
819
|
+
|
|
820
|
+
For the field name `content`, S3TE again prefers `content<lang>` over `content`.
|
|
821
|
+
|
|
822
|
+
If the field value is a string array, S3TE serializes it as concatenated HTML links.
|
|
823
|
+
|
|
824
|
+
**Example**
|
|
825
|
+
|
|
826
|
+
```html
|
|
827
|
+
<dbmulti>{
|
|
828
|
+
"filter": [{"__typename": {"S": "article"}}],
|
|
829
|
+
"template": "<article><h2><dbitem>headline</dbitem></h2><div><dbitem>content</dbitem></div></article>"
|
|
830
|
+
}</dbmulti>
|
|
831
|
+
```
|
|
832
|
+
|
|
833
|
+
</details>
|
|
834
|
+
|
|
835
|
+
<details>
|
|
836
|
+
<summary><code><dbmultifileitem></code> - print or transform fields inside a <code>dbmultifile</code> body</summary>
|
|
837
|
+
|
|
838
|
+
**Action**
|
|
839
|
+
|
|
840
|
+
Reads one field from the current content item and can apply one transformation mode. It is primarily meant for `dbmultifile` bodies, but works wherever a current content item exists.
|
|
841
|
+
|
|
842
|
+
**Syntax**
|
|
843
|
+
|
|
844
|
+
Simple field output:
|
|
845
|
+
|
|
846
|
+
```html
|
|
847
|
+
<dbmultifileitem>headline</dbmultifileitem>
|
|
848
|
+
```
|
|
849
|
+
|
|
850
|
+
JSON command mode:
|
|
851
|
+
|
|
852
|
+
```html
|
|
853
|
+
<dbmultifileitem>{"field":"content","limit":160}</dbmultifileitem>
|
|
854
|
+
```
|
|
855
|
+
|
|
856
|
+
Supported JSON properties:
|
|
857
|
+
|
|
858
|
+
- `field` required
|
|
859
|
+
- `limit` optional: truncate text to a maximum length and append `...`
|
|
860
|
+
- `limitlow` optional: choose a random length between `limitlow` and `limit`
|
|
861
|
+
- `format` optional: currently only `date`
|
|
862
|
+
- `locale` optional: used with `format: "date"`
|
|
863
|
+
- `divideattag` optional: cut a section out of the field value
|
|
864
|
+
- `startnumber` optional: 1-based occurrence number for the divide start
|
|
865
|
+
- `endnumber` optional: 1-based occurrence number for the divide end
|
|
866
|
+
|
|
867
|
+
Only one transform mode is allowed at a time:
|
|
868
|
+
|
|
869
|
+
- limit mode: `limit` with optional `limitlow`
|
|
870
|
+
- date mode: `format: "date"`
|
|
871
|
+
- divide mode: `divideattag`
|
|
872
|
+
|
|
873
|
+
Date mode formats `de` as `dd.mm.yyyy`. All other locales currently format as `mm/dd/yyyy`.
|
|
874
|
+
|
|
875
|
+
**Examples**
|
|
876
|
+
|
|
877
|
+
Simple field output:
|
|
878
|
+
|
|
879
|
+
```html
|
|
880
|
+
<dbmultifileitem>headline</dbmultifileitem>
|
|
881
|
+
```
|
|
882
|
+
|
|
883
|
+
Truncated teaser text:
|
|
884
|
+
|
|
885
|
+
```html
|
|
886
|
+
<dbmultifileitem>{"field":"content","limit":160}</dbmultifileitem>
|
|
887
|
+
```
|
|
888
|
+
|
|
889
|
+
Date formatting:
|
|
890
|
+
|
|
891
|
+
```html
|
|
892
|
+
<dbmultifileitem>{"field":"publishedAt","format":"date","locale":"de"}</dbmultifileitem>
|
|
893
|
+
```
|
|
894
|
+
|
|
895
|
+
Extract one section from a larger HTML field:
|
|
896
|
+
|
|
897
|
+
```html
|
|
898
|
+
<dbmultifileitem>{
|
|
899
|
+
"field":"content",
|
|
900
|
+
"divideattag":"<h2>",
|
|
901
|
+
"startnumber":2,
|
|
902
|
+
"endnumber":3
|
|
903
|
+
}</dbmultifileitem>
|
|
904
|
+
```
|
|
905
|
+
|
|
906
|
+
</details>
|
|
907
|
+
|
|
908
|
+
## Optional: Sitemap
|
|
909
|
+
|
|
910
|
+
You do not need `sitemap.xml` automation to use S3TE. If you want it, S3TE can maintain one `sitemap.xml` per published output bucket through a dedicated Lambda, just like the older 2.x generation.
|
|
911
|
+
|
|
912
|
+
<details>
|
|
913
|
+
<summary>Enable sitemap maintenance</summary>
|
|
914
|
+
|
|
915
|
+
Add this block to `s3te.config.json`:
|
|
916
|
+
|
|
917
|
+
```json
|
|
918
|
+
"integrations": {
|
|
919
|
+
"sitemap": {
|
|
920
|
+
"enabled": true,
|
|
921
|
+
"environments": {
|
|
922
|
+
"dev": {
|
|
923
|
+
"enabled": false
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
```
|
|
929
|
+
|
|
930
|
+
The top-level `enabled` acts as the default. `integrations.sitemap.environments.<env>.enabled` can override that for a single environment.
|
|
931
|
+
|
|
932
|
+
If you prefer the CLI path, this does the same retrofit:
|
|
933
|
+
|
|
934
|
+
```bash
|
|
935
|
+
npx s3te migrate --enable-sitemap --write
|
|
936
|
+
npx s3te migrate --env test --enable-sitemap --write
|
|
937
|
+
```
|
|
938
|
+
|
|
939
|
+
After enabling or disabling `sitemap`, redeploy the affected environment once:
|
|
940
|
+
|
|
941
|
+
```bash
|
|
942
|
+
npx s3te deploy --env prod
|
|
943
|
+
```
|
|
944
|
+
|
|
537
945
|
</details>
|
|
538
946
|
|
|
539
|
-
|
|
947
|
+
<details>
|
|
948
|
+
<summary>What the sitemap feature does</summary>
|
|
949
|
+
|
|
950
|
+
When `sitemap` is enabled for an environment, S3TE adds one `sitemap-updater` Lambda to that environment stack and wires every output bucket to it for HTML create/delete events.
|
|
951
|
+
|
|
952
|
+
The Lambda maintains `sitemap.xml` directly inside the same output bucket:
|
|
953
|
+
|
|
954
|
+
- one sitemap per variant/language output bucket
|
|
955
|
+
- only published HTML files are tracked
|
|
956
|
+
- `404.html` is ignored
|
|
957
|
+
- `index.html` becomes `https://example.com/`
|
|
958
|
+
- nested `news/index.html` becomes `https://example.com/news/`
|
|
959
|
+
- regular pages such as `about.html` stay `https://example.com/about.html`
|
|
960
|
+
|
|
961
|
+
Because the trigger sits on the output bucket, the sitemap also stays correct when HTML is regenerated from Webiny content changes in AWS. Asset-only changes do not affect it.
|
|
962
|
+
|
|
963
|
+
</details>
|
|
540
964
|
|
|
541
965
|
## Optional: Webiny CMS
|
|
542
966
|
|
|
@@ -544,6 +968,8 @@ You do not need Webiny to use S3TE. Start with plain HTML first. Add Webiny only
|
|
|
544
968
|
|
|
545
969
|
The supported target for this optional path is Webiny 6.x on its standard AWS deployment model.
|
|
546
970
|
|
|
971
|
+
Important for the Webiny path: S3TE does not turn on DynamoDB Streams on your Webiny table for you. You must enable the stream manually on the Webiny source table. S3TE uses that stream as the trigger source for CMS-driven rerendering.
|
|
972
|
+
|
|
547
973
|

|
|
548
974
|
|
|
549
975
|
<details>
|
|
@@ -560,7 +986,10 @@ This section assumes that S3TE is already installed and deployed. The S3TE-speci
|
|
|
560
986
|
|
|
561
987
|
1. Install Webiny in AWS and finish the Webiny setup first.
|
|
562
988
|
2. Find the Webiny DynamoDB table that contains the CMS entries you want S3TE to mirror.
|
|
563
|
-
3.
|
|
989
|
+
3. Manually enable DynamoDB Streams on that Webiny table before the first S3TE deploy with Webiny enabled.
|
|
990
|
+
Use `NEW_AND_OLD_IMAGES`.
|
|
991
|
+
Without that stream, `s3te deploy --env <name>` cannot wire the Webiny trigger and fails because the table has no `LatestStreamArn`.
|
|
992
|
+
4. Upgrade your existing S3TE config for Webiny:
|
|
564
993
|
|
|
565
994
|
```bash
|
|
566
995
|
npx s3te migrate --enable-webiny --webiny-source-table webiny-1234567 --webiny-tenant root --webiny-model article --write
|
|
@@ -568,6 +997,22 @@ npx s3te migrate --enable-webiny --webiny-source-table webiny-1234567 --webiny-t
|
|
|
568
997
|
|
|
569
998
|
`staticContent` and `staticCodeContent` are kept automatically. Add `--webiny-model` once per custom model you want S3TE to mirror.
|
|
570
999
|
|
|
1000
|
+
`--webiny-model article` means:
|
|
1001
|
+
|
|
1002
|
+
- `article` is the technical Webiny model ID, not the human-readable label shown in the CMS UI.
|
|
1003
|
+
- S3TE adds that model ID to `integrations.webiny.relevantModels` in `s3te.config.json`.
|
|
1004
|
+
- Only Webiny stream records whose model is listed in `relevantModels` are mirrored into the S3TE content table and can trigger rerendering.
|
|
1005
|
+
- If you omit `--webiny-model`, only the built-in defaults `staticContent` and `staticCodeContent` are mirrored.
|
|
1006
|
+
- You can pass the flag multiple times for multiple models, for example `--webiny-model article --webiny-model news --webiny-model event`.
|
|
1007
|
+
|
|
1008
|
+
That makes the migration example above equivalent to a config that contains:
|
|
1009
|
+
|
|
1010
|
+
```json
|
|
1011
|
+
"relevantModels": ["article", "staticContent", "staticCodeContent"]
|
|
1012
|
+
```
|
|
1013
|
+
|
|
1014
|
+
Use this for every Webiny model whose entries should be available to S3TE template commands like `dbitem`, `dbmulti`, `dbmultifile`, `dbmultifileitem`, or `dbpart`.
|
|
1015
|
+
|
|
571
1016
|
If different environments should read from different Webiny installations or tenants, run the migration per environment:
|
|
572
1017
|
|
|
573
1018
|
```bash
|
|
@@ -575,16 +1020,18 @@ npx s3te migrate --env test --enable-webiny --webiny-source-table webiny-test-12
|
|
|
575
1020
|
npx s3te migrate --env prod --enable-webiny --webiny-source-table webiny-live-1234567 --webiny-tenant root --write
|
|
576
1021
|
```
|
|
577
1022
|
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
1023
|
+
5. Verify again that DynamoDB Streams are enabled on the Webiny source table with `NEW_AND_OLD_IMAGES`.
|
|
1024
|
+
You enable this manually in the AWS console on the Webiny DynamoDB table under `Exports and streams`.
|
|
1025
|
+
S3TE creates the Lambda event source mapping during deploy, but it does not create or enable the table stream itself.
|
|
1026
|
+
6. If your S3TE language keys are not identical to your Webiny locales, add `webinyLocale` per language in `s3te.config.json`, for example `"en": { "webinyLocale": "en-US" }`.
|
|
1027
|
+
7. If your Webiny installation hosts multiple tenants, keep `integrations.webiny.tenant` set so S3TE only mirrors the intended tenant.
|
|
1028
|
+
8. Check the project again:
|
|
582
1029
|
|
|
583
1030
|
```bash
|
|
584
1031
|
npx s3te doctor --env prod
|
|
585
1032
|
```
|
|
586
1033
|
|
|
587
|
-
|
|
1034
|
+
9. Redeploy the existing S3TE environment:
|
|
588
1035
|
|
|
589
1036
|
```bash
|
|
590
1037
|
npx s3te deploy --env prod
|
|
@@ -592,6 +1039,11 @@ npx s3te deploy --env prod
|
|
|
592
1039
|
|
|
593
1040
|
That deploy updates the existing environment stack and adds the Webiny mirror resources to it. You do not need a fresh S3TE installation. After that, Webiny content changes flow through the deployed AWS resources automatically; only template or asset changes still need `s3te sync --env <name>`.
|
|
594
1041
|
|
|
1042
|
+
Manual versus automatic responsibilities in this step:
|
|
1043
|
+
|
|
1044
|
+
- Manual: enable DynamoDB Streams on the Webiny source table
|
|
1045
|
+
- Automatic during `s3te deploy`: read `LatestStreamArn`, create the Lambda event source mapping, and wire the S3TE Webiny mirror Lambda into the environment stack
|
|
1046
|
+
|
|
595
1047
|
</details>
|
|
596
1048
|
|
|
597
1049
|
<details>
|
|
@@ -637,15 +1089,4 @@ For localized Webiny projects, the language block can also carry the mapping exp
|
|
|
637
1089
|
|
|
638
1090
|
</details>
|
|
639
1091
|
|
|
640
|
-
|
|
641
|
-
<summary>Content template commands</summary>
|
|
642
|
-
|
|
643
|
-
These commands are useful both with Webiny and with local JSON content files:
|
|
644
|
-
|
|
645
|
-
- `dbpart`
|
|
646
|
-
- `dbmulti`
|
|
647
|
-
- `dbmultifile`
|
|
648
|
-
- `dbitem`
|
|
649
|
-
- `dbmultifileitem`
|
|
650
|
-
|
|
651
|
-
</details>
|
|
1092
|
+
The content-driven tags are documented in [Template Commands](#template-commands), section [Webiny Features](#webiny-features).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@projectdochelp/s3te",
|
|
3
|
-
"version": "3.2.
|
|
3
|
+
"version": "3.2.3",
|
|
4
4
|
"description": "CLI, render core, AWS adapter, and testkit for S3TemplateEngine projects",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -51,7 +51,8 @@
|
|
|
51
51
|
"@aws-sdk/client-sfn": "^3.0.0",
|
|
52
52
|
"@aws-sdk/client-ssm": "^3.0.0",
|
|
53
53
|
"@aws-sdk/lib-dynamodb": "^3.0.0",
|
|
54
|
-
"@aws-sdk/util-dynamodb": "^3.0.0"
|
|
54
|
+
"@aws-sdk/util-dynamodb": "^3.0.0",
|
|
55
|
+
"fast-xml-parser": "^4.0.8"
|
|
55
56
|
},
|
|
56
57
|
"publishConfig": {
|
|
57
58
|
"access": "public"
|
|
@@ -344,6 +344,7 @@ function buildEnvironmentStackParameters({
|
|
|
344
344
|
`InvalidationSchedulerArtifactKey=${uploadedArtifacts.invalidationScheduler}`,
|
|
345
345
|
`InvalidationExecutorArtifactKey=${uploadedArtifacts.invalidationExecutor}`,
|
|
346
346
|
`ContentMirrorArtifactKey=${uploadedArtifacts.contentMirror}`,
|
|
347
|
+
`SitemapUpdaterArtifactKey=${uploadedArtifacts.sitemapUpdater}`,
|
|
347
348
|
`RuntimeManifestValue=${runtimeManifestValue}`,
|
|
348
349
|
`WebinySourceTableStreamArn=${webinyStreamArn}`
|
|
349
350
|
];
|
|
@@ -370,6 +371,9 @@ export async function deployAwsProject({
|
|
|
370
371
|
if (requestedFeatureSet.has("webiny") && !runtimeConfig.integrations.webiny.enabled) {
|
|
371
372
|
throw new S3teError("ADAPTER_ERROR", "Feature webiny was requested but is not enabled in s3te.config.json.");
|
|
372
373
|
}
|
|
374
|
+
if (requestedFeatureSet.has("sitemap") && !runtimeConfig.integrations.sitemap.enabled) {
|
|
375
|
+
throw new S3teError("ADAPTER_ERROR", "Feature sitemap was requested but is not enabled in s3te.config.json.");
|
|
376
|
+
}
|
|
373
377
|
|
|
374
378
|
await ensureAwsCliAvailable({ cwd: projectDir });
|
|
375
379
|
await ensureAwsCredentials({
|
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
resolveEnvironmentSitemapIntegration,
|
|
3
|
+
resolveEnvironmentWebinyIntegration
|
|
4
|
+
} from "../../core/src/index.mjs";
|
|
2
5
|
|
|
3
6
|
export function getConfiguredFeatures(config, environment) {
|
|
4
7
|
const features = [];
|
|
@@ -7,16 +10,25 @@ export function getConfiguredFeatures(config, environment) {
|
|
|
7
10
|
if (resolveEnvironmentWebinyIntegration(config, environment).enabled) {
|
|
8
11
|
features.push("webiny");
|
|
9
12
|
}
|
|
13
|
+
if (resolveEnvironmentSitemapIntegration(config, environment).enabled) {
|
|
14
|
+
features.push("sitemap");
|
|
15
|
+
}
|
|
10
16
|
return features;
|
|
11
17
|
}
|
|
12
18
|
|
|
13
19
|
const hasAnyEnvironmentWebiny = Object.keys(config.environments ?? {}).some((environmentName) => (
|
|
14
20
|
resolveEnvironmentWebinyIntegration(config, environmentName).enabled
|
|
15
21
|
));
|
|
22
|
+
const hasAnyEnvironmentSitemap = Object.keys(config.environments ?? {}).some((environmentName) => (
|
|
23
|
+
resolveEnvironmentSitemapIntegration(config, environmentName).enabled
|
|
24
|
+
));
|
|
16
25
|
|
|
17
26
|
if (hasAnyEnvironmentWebiny) {
|
|
18
27
|
features.push("webiny");
|
|
19
28
|
}
|
|
29
|
+
if (hasAnyEnvironmentSitemap) {
|
|
30
|
+
features.push("sitemap");
|
|
31
|
+
}
|
|
20
32
|
|
|
21
33
|
return features;
|
|
22
34
|
}
|
|
@@ -30,7 +30,8 @@ function buildFunctionNames(runtimeConfig) {
|
|
|
30
30
|
renderWorker: `${runtimeConfig.stackPrefix}_s3te_render_worker`,
|
|
31
31
|
invalidationScheduler: `${runtimeConfig.stackPrefix}_s3te_invalidation_scheduler`,
|
|
32
32
|
invalidationExecutor: `${runtimeConfig.stackPrefix}_s3te_invalidation_executor`,
|
|
33
|
-
contentMirror: runtimeConfig.integrations.webiny.enabled ? `${runtimeConfig.stackPrefix}_s3te_content_mirror` : ""
|
|
33
|
+
contentMirror: runtimeConfig.integrations.webiny.enabled ? `${runtimeConfig.stackPrefix}_s3te_content_mirror` : "",
|
|
34
|
+
sitemapUpdater: runtimeConfig.integrations.sitemap.enabled ? `${runtimeConfig.stackPrefix}_s3te_sitemap_updater` : ""
|
|
34
35
|
};
|
|
35
36
|
}
|
|
36
37
|
|
|
@@ -79,6 +80,9 @@ export function buildAwsRuntimeManifest({ config, environment, stackOutputs = {}
|
|
|
79
80
|
mirrorTableName: runtimeConfig.integrations.webiny.mirrorTableName,
|
|
80
81
|
relevantModels: [...runtimeConfig.integrations.webiny.relevantModels],
|
|
81
82
|
tenant: runtimeConfig.integrations.webiny.tenant
|
|
83
|
+
},
|
|
84
|
+
sitemap: {
|
|
85
|
+
enabled: runtimeConfig.integrations.sitemap.enabled
|
|
82
86
|
}
|
|
83
87
|
},
|
|
84
88
|
variants: runtimeConfig.variants
|
|
@@ -25,7 +25,8 @@ const RUNTIME_PACKAGE_DEPENDENCIES = [
|
|
|
25
25
|
"@aws-sdk/client-sfn",
|
|
26
26
|
"@aws-sdk/client-ssm",
|
|
27
27
|
"@aws-sdk/lib-dynamodb",
|
|
28
|
-
"@aws-sdk/util-dynamodb"
|
|
28
|
+
"@aws-sdk/util-dynamodb",
|
|
29
|
+
"fast-xml-parser"
|
|
29
30
|
];
|
|
30
31
|
const INTERNAL_RUNTIME_DIRECTORIES = [
|
|
31
32
|
{
|
|
@@ -226,6 +227,9 @@ export async function packageAwsProject({
|
|
|
226
227
|
if (features.includes("webiny") && !runtimeConfig.integrations.webiny.enabled) {
|
|
227
228
|
throw new S3teError("ADAPTER_ERROR", "Feature webiny was requested but is not enabled in s3te.config.json.");
|
|
228
229
|
}
|
|
230
|
+
if (features.includes("sitemap") && !runtimeConfig.integrations.sitemap.enabled) {
|
|
231
|
+
throw new S3teError("ADAPTER_ERROR", "Feature sitemap was requested but is not enabled in s3te.config.json.");
|
|
232
|
+
}
|
|
229
233
|
|
|
230
234
|
const resolvedFeatures = resolveRequestedFeatures(config, features, environment);
|
|
231
235
|
const packageDir = outDir
|
|
@@ -267,6 +271,11 @@ export async function packageAwsProject({
|
|
|
267
271
|
archive: path.join(lambdaDir, "content-mirror.zip"),
|
|
268
272
|
parameter: "ContentMirrorArtifactKey",
|
|
269
273
|
s3Key: `lambda/content-mirror.zip`
|
|
274
|
+
},
|
|
275
|
+
sitemapUpdater: {
|
|
276
|
+
archive: path.join(lambdaDir, "sitemap-updater.zip"),
|
|
277
|
+
parameter: "SitemapUpdaterArtifactKey",
|
|
278
|
+
s3Key: "lambda/sitemap-updater.zip"
|
|
270
279
|
}
|
|
271
280
|
};
|
|
272
281
|
|
|
@@ -258,6 +258,9 @@ export function buildCoreConfigFromEnvironment(manifest, environmentName) {
|
|
|
258
258
|
mirrorTableName: environment.integrations.webiny.mirrorTableName,
|
|
259
259
|
relevantModels: [...environment.integrations.webiny.relevantModels],
|
|
260
260
|
tenant: environment.integrations.webiny.tenant
|
|
261
|
+
},
|
|
262
|
+
sitemap: {
|
|
263
|
+
enabled: environment.integrations.sitemap?.enabled ?? false
|
|
261
264
|
}
|
|
262
265
|
}
|
|
263
266
|
};
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import { XMLBuilder, XMLParser } from "fast-xml-parser";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
createAwsClients,
|
|
5
|
+
decodeS3Key,
|
|
6
|
+
loadEnvironmentManifest
|
|
7
|
+
} from "./common.mjs";
|
|
8
|
+
|
|
9
|
+
const SITEMAP_XML_NAMESPACE = "http://www.sitemaps.org/schemas/sitemap/0.9";
|
|
10
|
+
const parser = new XMLParser({
|
|
11
|
+
ignoreAttributes: false,
|
|
12
|
+
isArray: (name) => name === "url"
|
|
13
|
+
});
|
|
14
|
+
const builder = new XMLBuilder({
|
|
15
|
+
ignoreAttributes: false,
|
|
16
|
+
format: true
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
function createEmptySitemapDocument() {
|
|
20
|
+
return {
|
|
21
|
+
"?xml": {
|
|
22
|
+
"@_version": "1.0",
|
|
23
|
+
"@_encoding": "UTF-8"
|
|
24
|
+
},
|
|
25
|
+
urlset: {
|
|
26
|
+
"@_xmlns": SITEMAP_XML_NAMESPACE,
|
|
27
|
+
url: []
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async function bodyToUtf8(body) {
|
|
33
|
+
if (typeof body?.transformToString === "function") {
|
|
34
|
+
return body.transformToString("utf8");
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (Buffer.isBuffer(body)) {
|
|
38
|
+
return body.toString("utf8");
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (body instanceof Uint8Array) {
|
|
42
|
+
return Buffer.from(body).toString("utf8");
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return String(body ?? "");
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function normalizeSitemapDocument(document) {
|
|
49
|
+
const candidate = document?.urlset ? document : createEmptySitemapDocument();
|
|
50
|
+
const urls = candidate?.urlset?.url;
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
"?xml": {
|
|
54
|
+
"@_version": candidate?.["?xml"]?.["@_version"] ?? "1.0",
|
|
55
|
+
"@_encoding": candidate?.["?xml"]?.["@_encoding"] ?? "UTF-8"
|
|
56
|
+
},
|
|
57
|
+
urlset: {
|
|
58
|
+
"@_xmlns": candidate?.urlset?.["@_xmlns"] ?? SITEMAP_XML_NAMESPACE,
|
|
59
|
+
url: Array.isArray(urls)
|
|
60
|
+
? urls.filter((entry) => entry?.loc)
|
|
61
|
+
: (urls?.loc ? [urls] : [])
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async function loadCurrentSitemap(s3, bucketName) {
|
|
67
|
+
try {
|
|
68
|
+
const response = await s3.getObject({
|
|
69
|
+
Bucket: bucketName,
|
|
70
|
+
Key: "sitemap.xml"
|
|
71
|
+
}).promise();
|
|
72
|
+
return normalizeSitemapDocument(parser.parse(await bodyToUtf8(response.Body)));
|
|
73
|
+
} catch (error) {
|
|
74
|
+
const errorCode = error?.name ?? error?.Code ?? error?.code;
|
|
75
|
+
if (errorCode === "NoSuchKey" || errorCode === "NoSuchBucket" || errorCode === "NotFound") {
|
|
76
|
+
return createEmptySitemapDocument();
|
|
77
|
+
}
|
|
78
|
+
throw error;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function findLanguageTargetByBucket(environmentManifest, bucketName) {
|
|
83
|
+
for (const [variantName, variantConfig] of Object.entries(environmentManifest.variants ?? {})) {
|
|
84
|
+
for (const [languageCode, languageConfig] of Object.entries(variantConfig.languages ?? {})) {
|
|
85
|
+
if (languageConfig.targetBucket === bucketName) {
|
|
86
|
+
return {
|
|
87
|
+
variantName,
|
|
88
|
+
variantConfig,
|
|
89
|
+
languageCode,
|
|
90
|
+
languageConfig
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function encodePathSegments(key) {
|
|
100
|
+
return String(key)
|
|
101
|
+
.split("/")
|
|
102
|
+
.filter((segment) => segment.length > 0)
|
|
103
|
+
.map((segment) => encodeURIComponent(segment))
|
|
104
|
+
.join("/");
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function buildSitemapUrl({ baseUrl, key, indexDocument, notFoundDocument }) {
|
|
108
|
+
const normalizedKey = String(key ?? "").replace(/^\/+/, "");
|
|
109
|
+
if (!normalizedKey || normalizedKey === "sitemap.xml" || normalizedKey === notFoundDocument) {
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const normalizedBaseUrl = String(baseUrl ?? "").replace(/^https?:\/\//i, "").replace(/\/+$/, "");
|
|
114
|
+
if (!normalizedBaseUrl) {
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (normalizedKey === indexDocument) {
|
|
119
|
+
return `https://${normalizedBaseUrl}/`;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (normalizedKey.endsWith(`/${indexDocument}`)) {
|
|
123
|
+
const directoryKey = normalizedKey.slice(0, -(indexDocument.length + 1));
|
|
124
|
+
const encodedDirectory = encodePathSegments(directoryKey);
|
|
125
|
+
return `https://${normalizedBaseUrl}/${encodedDirectory}/`;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return `https://${normalizedBaseUrl}/${encodePathSegments(normalizedKey)}`;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function applySitemapRecords(sitemapDocument, sitemapRecords = []) {
|
|
132
|
+
const normalizedDocument = normalizeSitemapDocument(sitemapDocument);
|
|
133
|
+
const entries = new Map((normalizedDocument.urlset.url ?? []).map((entry) => [entry.loc, {
|
|
134
|
+
loc: entry.loc,
|
|
135
|
+
lastmod: entry.lastmod
|
|
136
|
+
}]));
|
|
137
|
+
|
|
138
|
+
for (const record of sitemapRecords) {
|
|
139
|
+
if (!record?.loc) {
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const lastmod = String(record.lastmod ?? new Date().toISOString()).slice(0, 10);
|
|
144
|
+
if (record.action === "delete") {
|
|
145
|
+
entries.delete(record.loc);
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
entries.set(record.loc, {
|
|
150
|
+
loc: record.loc,
|
|
151
|
+
lastmod
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
normalizedDocument.urlset.url = [...entries.values()].sort((left, right) => left.loc.localeCompare(right.loc));
|
|
156
|
+
return normalizedDocument;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export async function handler(event) {
|
|
160
|
+
const environmentName = process.env.S3TE_ENVIRONMENT;
|
|
161
|
+
const runtimeParameter = process.env.S3TE_RUNTIME_PARAMETER;
|
|
162
|
+
|
|
163
|
+
const clients = createAwsClients();
|
|
164
|
+
const { environment: environmentManifest } = await loadEnvironmentManifest(
|
|
165
|
+
clients.ssm,
|
|
166
|
+
runtimeParameter,
|
|
167
|
+
environmentName
|
|
168
|
+
);
|
|
169
|
+
|
|
170
|
+
const updatesByBucket = new Map();
|
|
171
|
+
|
|
172
|
+
for (const record of event.Records ?? []) {
|
|
173
|
+
const bucketName = record.s3?.bucket?.name;
|
|
174
|
+
const key = decodeS3Key(record.s3?.object?.key ?? "");
|
|
175
|
+
const target = findLanguageTargetByBucket(environmentManifest, bucketName);
|
|
176
|
+
if (!bucketName || !key || !target) {
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const loc = buildSitemapUrl({
|
|
181
|
+
baseUrl: target.languageConfig.baseUrl,
|
|
182
|
+
key,
|
|
183
|
+
indexDocument: target.variantConfig.routing.indexDocument,
|
|
184
|
+
notFoundDocument: target.variantConfig.routing.notFoundDocument
|
|
185
|
+
});
|
|
186
|
+
if (!loc) {
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (!updatesByBucket.has(bucketName)) {
|
|
191
|
+
updatesByBucket.set(bucketName, []);
|
|
192
|
+
}
|
|
193
|
+
updatesByBucket.get(bucketName).push({
|
|
194
|
+
action: String(record.eventName).startsWith("ObjectRemoved:") ? "delete" : "upsert",
|
|
195
|
+
loc,
|
|
196
|
+
lastmod: record.eventTime
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
let updatedBuckets = 0;
|
|
201
|
+
|
|
202
|
+
for (const [bucketName, updates] of updatesByBucket.entries()) {
|
|
203
|
+
const sitemapDocument = applySitemapRecords(
|
|
204
|
+
await loadCurrentSitemap(clients.s3, bucketName),
|
|
205
|
+
updates
|
|
206
|
+
);
|
|
207
|
+
|
|
208
|
+
await clients.s3.putObject({
|
|
209
|
+
Bucket: bucketName,
|
|
210
|
+
Key: "sitemap.xml",
|
|
211
|
+
Body: builder.build(sitemapDocument),
|
|
212
|
+
ContentType: "application/xml; charset=utf-8"
|
|
213
|
+
}).promise();
|
|
214
|
+
|
|
215
|
+
updatedBuckets += 1;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return {
|
|
219
|
+
updatedBuckets
|
|
220
|
+
};
|
|
221
|
+
}
|
|
@@ -35,6 +35,26 @@ function lambdaCode(keyParameter) {
|
|
|
35
35
|
};
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
+
function buildSitemapNotificationConfigurations(functionLogicalId) {
|
|
39
|
+
const suffixes = [".html", ".htm"];
|
|
40
|
+
const events = ["s3:ObjectCreated:*", "s3:ObjectRemoved:*"];
|
|
41
|
+
|
|
42
|
+
return events.flatMap((eventName) => suffixes.map((suffix) => ({
|
|
43
|
+
Event: eventName,
|
|
44
|
+
Function: { "Fn::GetAtt": [functionLogicalId, "Arn"] },
|
|
45
|
+
Filter: {
|
|
46
|
+
S3Key: {
|
|
47
|
+
Rules: [
|
|
48
|
+
{
|
|
49
|
+
Name: "suffix",
|
|
50
|
+
Value: suffix
|
|
51
|
+
}
|
|
52
|
+
]
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
})));
|
|
56
|
+
}
|
|
57
|
+
|
|
38
58
|
function lambdaRuntimeProperties(runtimeConfig, roleRef, name, keyParameter, handlerName, extra = {}) {
|
|
39
59
|
return {
|
|
40
60
|
Type: "AWS::Lambda::Function",
|
|
@@ -117,7 +137,8 @@ export function buildCloudFormationTemplate({ config, environment, features = []
|
|
|
117
137
|
renderWorker: `${runtimeConfig.stackPrefix}_s3te_render_worker`,
|
|
118
138
|
invalidationScheduler: `${runtimeConfig.stackPrefix}_s3te_invalidation_scheduler`,
|
|
119
139
|
invalidationExecutor: `${runtimeConfig.stackPrefix}_s3te_invalidation_executor`,
|
|
120
|
-
contentMirror: `${runtimeConfig.stackPrefix}_s3te_content_mirror
|
|
140
|
+
contentMirror: `${runtimeConfig.stackPrefix}_s3te_content_mirror`,
|
|
141
|
+
sitemapUpdater: `${runtimeConfig.stackPrefix}_s3te_sitemap_updater`
|
|
121
142
|
};
|
|
122
143
|
|
|
123
144
|
const parameters = {
|
|
@@ -140,6 +161,10 @@ export function buildCloudFormationTemplate({ config, environment, features = []
|
|
|
140
161
|
Type: "String",
|
|
141
162
|
Default: ""
|
|
142
163
|
},
|
|
164
|
+
SitemapUpdaterArtifactKey: {
|
|
165
|
+
Type: "String",
|
|
166
|
+
Default: ""
|
|
167
|
+
},
|
|
143
168
|
RuntimeManifestValue: {
|
|
144
169
|
Type: "String",
|
|
145
170
|
Default: "{}"
|
|
@@ -369,6 +394,26 @@ export function buildCloudFormationTemplate({ config, environment, features = []
|
|
|
369
394
|
};
|
|
370
395
|
}
|
|
371
396
|
|
|
397
|
+
if (featureSet.has("sitemap") && runtimeConfig.integrations.sitemap.enabled) {
|
|
398
|
+
resources.SitemapUpdater = lambdaRuntimeProperties(
|
|
399
|
+
runtimeConfig,
|
|
400
|
+
"ExecutionRole",
|
|
401
|
+
functionNames.sitemapUpdater,
|
|
402
|
+
"SitemapUpdaterArtifactKey",
|
|
403
|
+
"sitemap-updater",
|
|
404
|
+
{
|
|
405
|
+
Timeout: 300,
|
|
406
|
+
MemorySize: 512,
|
|
407
|
+
Environment: {
|
|
408
|
+
Variables: {
|
|
409
|
+
S3TE_ENVIRONMENT: environment,
|
|
410
|
+
S3TE_RUNTIME_PARAMETER: runtimeConfig.runtimeParameterName
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
);
|
|
415
|
+
}
|
|
416
|
+
|
|
372
417
|
outputs.StackName = { Value: runtimeConfig.stackName };
|
|
373
418
|
outputs.RuntimeManifestParameterName = {
|
|
374
419
|
Value: runtimeConfig.runtimeParameterName
|
|
@@ -384,6 +429,9 @@ export function buildCloudFormationTemplate({ config, environment, features = []
|
|
|
384
429
|
if (resources.ContentMirror) {
|
|
385
430
|
outputs.ContentMirrorFunctionName = { Value: functionNames.contentMirror };
|
|
386
431
|
}
|
|
432
|
+
if (resources.SitemapUpdater) {
|
|
433
|
+
outputs.SitemapUpdaterFunctionName = { Value: functionNames.sitemapUpdater };
|
|
434
|
+
}
|
|
387
435
|
|
|
388
436
|
for (const [variantName, variantConfig] of Object.entries(runtimeConfig.variants)) {
|
|
389
437
|
const codeBucketLogicalId = cfName(sanitizeLogicalId(variantName), "CodeBucket");
|
|
@@ -417,12 +465,20 @@ export function buildCloudFormationTemplate({ config, environment, features = []
|
|
|
417
465
|
|
|
418
466
|
resources[outputBucketLogicalId] = {
|
|
419
467
|
Type: "AWS::S3::Bucket",
|
|
468
|
+
...(resources.SitemapUpdater ? { DependsOn: ["SitemapUpdaterPermission"] } : {}),
|
|
420
469
|
Properties: {
|
|
421
470
|
BucketName: languageConfig.targetBucket,
|
|
422
471
|
WebsiteConfiguration: {
|
|
423
472
|
IndexDocument: variantConfig.routing.indexDocument,
|
|
424
473
|
ErrorDocument: variantConfig.routing.notFoundDocument
|
|
425
474
|
},
|
|
475
|
+
...(resources.SitemapUpdater
|
|
476
|
+
? {
|
|
477
|
+
NotificationConfiguration: {
|
|
478
|
+
LambdaConfigurations: buildSitemapNotificationConfigurations("SitemapUpdater")
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
: {}),
|
|
426
482
|
PublicAccessBlockConfiguration: {
|
|
427
483
|
BlockPublicAcls: false,
|
|
428
484
|
BlockPublicPolicy: false,
|
|
@@ -544,6 +600,17 @@ export function buildCloudFormationTemplate({ config, environment, features = []
|
|
|
544
600
|
}
|
|
545
601
|
};
|
|
546
602
|
|
|
603
|
+
if (resources.SitemapUpdater) {
|
|
604
|
+
resources.SitemapUpdaterPermission = {
|
|
605
|
+
Type: "AWS::Lambda::Permission",
|
|
606
|
+
Properties: {
|
|
607
|
+
Action: "lambda:InvokeFunction",
|
|
608
|
+
FunctionName: { Ref: "SitemapUpdater" },
|
|
609
|
+
Principal: "s3.amazonaws.com"
|
|
610
|
+
}
|
|
611
|
+
};
|
|
612
|
+
}
|
|
613
|
+
|
|
547
614
|
return {
|
|
548
615
|
AWSTemplateFormatVersion: "2010-09-09",
|
|
549
616
|
Description: `S3TE environment stack for ${config.project.name} (${environment})`,
|
|
@@ -385,6 +385,8 @@ async function main() {
|
|
|
385
385
|
environment: asArray(options.env)[0],
|
|
386
386
|
enableWebiny: Boolean(options["enable-webiny"]),
|
|
387
387
|
disableWebiny: Boolean(options["disable-webiny"]),
|
|
388
|
+
enableSitemap: Boolean(options["enable-sitemap"]),
|
|
389
|
+
disableSitemap: Boolean(options["disable-sitemap"]),
|
|
388
390
|
webinySourceTable: options["webiny-source-table"],
|
|
389
391
|
webinyTenant: options["webiny-tenant"],
|
|
390
392
|
webinyModels: asArray(options["webiny-model"])
|
|
@@ -335,6 +335,23 @@ function schemaTemplate() {
|
|
|
335
335
|
}
|
|
336
336
|
}
|
|
337
337
|
}
|
|
338
|
+
},
|
|
339
|
+
sitemap: {
|
|
340
|
+
type: "object",
|
|
341
|
+
additionalProperties: false,
|
|
342
|
+
properties: {
|
|
343
|
+
enabled: { type: "boolean" },
|
|
344
|
+
environments: {
|
|
345
|
+
type: "object",
|
|
346
|
+
additionalProperties: {
|
|
347
|
+
type: "object",
|
|
348
|
+
additionalProperties: false,
|
|
349
|
+
properties: {
|
|
350
|
+
enabled: { type: "boolean" }
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
}
|
|
338
355
|
}
|
|
339
356
|
}
|
|
340
357
|
}
|
|
@@ -986,6 +1003,8 @@ export async function migrateProject(configPath, rawConfig, writeChanges) {
|
|
|
986
1003
|
|
|
987
1004
|
const enableWebiny = Boolean(options.enableWebiny);
|
|
988
1005
|
const disableWebiny = Boolean(options.disableWebiny);
|
|
1006
|
+
const enableSitemap = Boolean(options.enableSitemap);
|
|
1007
|
+
const disableSitemap = Boolean(options.disableSitemap);
|
|
989
1008
|
const targetEnvironment = options.environment ? String(options.environment).trim() : "";
|
|
990
1009
|
const webinySourceTable = options.webinySourceTable ? String(options.webinySourceTable).trim() : "";
|
|
991
1010
|
const webinyTenant = options.webinyTenant ? String(options.webinyTenant).trim() : "";
|
|
@@ -994,6 +1013,9 @@ export async function migrateProject(configPath, rawConfig, writeChanges) {
|
|
|
994
1013
|
if (enableWebiny && disableWebiny) {
|
|
995
1014
|
throw new S3teError("CONFIG_CONFLICT_ERROR", "migrate does not allow --enable-webiny and --disable-webiny at the same time.");
|
|
996
1015
|
}
|
|
1016
|
+
if (enableSitemap && disableSitemap) {
|
|
1017
|
+
throw new S3teError("CONFIG_CONFLICT_ERROR", "migrate does not allow --enable-sitemap and --disable-sitemap at the same time.");
|
|
1018
|
+
}
|
|
997
1019
|
|
|
998
1020
|
const touchesWebiny = enableWebiny || disableWebiny || Boolean(webinySourceTable) || Boolean(webinyTenant) || webinyModels.length > 0;
|
|
999
1021
|
if (touchesWebiny) {
|
|
@@ -1076,6 +1098,48 @@ export async function migrateProject(configPath, rawConfig, writeChanges) {
|
|
|
1076
1098
|
}
|
|
1077
1099
|
}
|
|
1078
1100
|
|
|
1101
|
+
const touchesSitemap = enableSitemap || disableSitemap;
|
|
1102
|
+
if (touchesSitemap) {
|
|
1103
|
+
if (targetEnvironment && !nextConfig.environments?.[targetEnvironment]) {
|
|
1104
|
+
throw new S3teError("CONFIG_CONFLICT_ERROR", `Unknown environment for migrate: ${targetEnvironment}.`);
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
const existingIntegrations = nextConfig.integrations ?? {};
|
|
1108
|
+
const existingSitemap = existingIntegrations.sitemap ?? {};
|
|
1109
|
+
const existingEnvironmentOverrides = existingSitemap.environments ?? {};
|
|
1110
|
+
const existingTargetSitemap = targetEnvironment
|
|
1111
|
+
? (existingEnvironmentOverrides[targetEnvironment] ?? {})
|
|
1112
|
+
: existingSitemap;
|
|
1113
|
+
const nextEnabled = disableSitemap
|
|
1114
|
+
? false
|
|
1115
|
+
: (enableSitemap || Boolean(targetEnvironment
|
|
1116
|
+
? (existingTargetSitemap.enabled ?? existingSitemap.enabled)
|
|
1117
|
+
: existingSitemap.enabled));
|
|
1118
|
+
|
|
1119
|
+
nextConfig.integrations = {
|
|
1120
|
+
...existingIntegrations,
|
|
1121
|
+
sitemap: targetEnvironment
|
|
1122
|
+
? {
|
|
1123
|
+
...existingSitemap,
|
|
1124
|
+
environments: {
|
|
1125
|
+
...existingEnvironmentOverrides,
|
|
1126
|
+
[targetEnvironment]: {
|
|
1127
|
+
...existingTargetSitemap,
|
|
1128
|
+
enabled: nextEnabled
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
: {
|
|
1133
|
+
...existingSitemap,
|
|
1134
|
+
enabled: nextEnabled,
|
|
1135
|
+
environments: existingEnvironmentOverrides
|
|
1136
|
+
}
|
|
1137
|
+
};
|
|
1138
|
+
|
|
1139
|
+
const scopeLabel = targetEnvironment ? ` for environment ${targetEnvironment}` : "";
|
|
1140
|
+
changes.push(nextEnabled ? `Enabled sitemap integration${scopeLabel}.` : `Disabled sitemap integration${scopeLabel}.`);
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1079
1143
|
if (options.writeChanges) {
|
|
1080
1144
|
await writeTextFile(configPath, JSON.stringify(nextConfig, null, 2) + "\n");
|
|
1081
1145
|
}
|
|
@@ -75,12 +75,27 @@ function environmentHostPrefix(config, environmentName) {
|
|
|
75
75
|
}
|
|
76
76
|
|
|
77
77
|
function prefixHostForEnvironment(config, host, environmentName) {
|
|
78
|
-
|
|
79
|
-
if (!prefix) {
|
|
78
|
+
if (!hasProductionEnvironment(config) || isProductionEnvironment(environmentName)) {
|
|
80
79
|
return host;
|
|
81
80
|
}
|
|
82
81
|
|
|
83
|
-
|
|
82
|
+
const normalizedHost = String(host).trim();
|
|
83
|
+
if (!normalizedHost) {
|
|
84
|
+
return normalizedHost;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (normalizedHost.startsWith(`${environmentName}.`) || normalizedHost.startsWith(`${environmentName}-`)) {
|
|
88
|
+
return normalizedHost;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const labels = normalizedHost.split(".");
|
|
92
|
+
if (labels.length <= 2) {
|
|
93
|
+
const prefix = environmentHostPrefix(config, environmentName);
|
|
94
|
+
return `${prefix}${normalizedHost}`;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const [firstLabel, ...remainingLabels] = labels;
|
|
98
|
+
return `${environmentName}-${firstLabel}.${remainingLabels.join(".")}`;
|
|
84
99
|
}
|
|
85
100
|
|
|
86
101
|
function isValidConfiguredHost(value) {
|
|
@@ -143,6 +158,12 @@ function resolveWebinyConfigDefaults(webinyConfig = {}) {
|
|
|
143
158
|
};
|
|
144
159
|
}
|
|
145
160
|
|
|
161
|
+
function resolveSitemapConfigDefaults(sitemapConfig = {}) {
|
|
162
|
+
return {
|
|
163
|
+
enabled: sitemapConfig.enabled ?? false
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
146
167
|
function resolveProjectWebinyConfig(projectConfig) {
|
|
147
168
|
const baseConfig = resolveWebinyConfigDefaults(projectConfig.integrations?.webiny ?? {});
|
|
148
169
|
const environmentConfigs = Object.fromEntries(Object.entries(projectConfig.integrations?.webiny?.environments ?? {}).map(([environmentName, webinyConfig]) => ([
|
|
@@ -162,6 +183,21 @@ function resolveProjectWebinyConfig(projectConfig) {
|
|
|
162
183
|
};
|
|
163
184
|
}
|
|
164
185
|
|
|
186
|
+
function resolveProjectSitemapConfig(projectConfig) {
|
|
187
|
+
const baseConfig = resolveSitemapConfigDefaults(projectConfig.integrations?.sitemap ?? {});
|
|
188
|
+
const environmentConfigs = Object.fromEntries(Object.entries(projectConfig.integrations?.sitemap?.environments ?? {}).map(([environmentName, sitemapConfig]) => ([
|
|
189
|
+
environmentName,
|
|
190
|
+
{
|
|
191
|
+
enabled: sitemapConfig.enabled
|
|
192
|
+
}
|
|
193
|
+
])));
|
|
194
|
+
|
|
195
|
+
return {
|
|
196
|
+
...baseConfig,
|
|
197
|
+
environments: environmentConfigs
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
165
201
|
export async function loadProjectConfig(configPath) {
|
|
166
202
|
const raw = await fs.readFile(configPath, "utf8");
|
|
167
203
|
try {
|
|
@@ -247,7 +283,8 @@ export function resolveProjectConfig(projectConfig) {
|
|
|
247
283
|
};
|
|
248
284
|
|
|
249
285
|
const integrations = {
|
|
250
|
-
webiny: resolveProjectWebinyConfig(projectConfig)
|
|
286
|
+
webiny: resolveProjectWebinyConfig(projectConfig),
|
|
287
|
+
sitemap: resolveProjectSitemapConfig(projectConfig)
|
|
251
288
|
};
|
|
252
289
|
|
|
253
290
|
for (const [variantName, variantConfig] of Object.entries(variants)) {
|
|
@@ -317,6 +354,15 @@ export function resolveEnvironmentWebinyIntegration(config, environmentName) {
|
|
|
317
354
|
};
|
|
318
355
|
}
|
|
319
356
|
|
|
357
|
+
export function resolveEnvironmentSitemapIntegration(config, environmentName) {
|
|
358
|
+
const baseConfig = resolveSitemapConfigDefaults(config.integrations?.sitemap ?? {});
|
|
359
|
+
const environmentOverride = config.integrations?.sitemap?.environments?.[environmentName] ?? {};
|
|
360
|
+
|
|
361
|
+
return {
|
|
362
|
+
enabled: environmentOverride.enabled ?? baseConfig.enabled
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
|
|
320
366
|
export function resolveTableNames(config, environmentName) {
|
|
321
367
|
const context = createPlaceholderContext(config, environmentName);
|
|
322
368
|
const webinyConfig = resolveEnvironmentWebinyIntegration(config, environmentName);
|
|
@@ -340,6 +386,7 @@ export function resolveStackName(config, environmentName) {
|
|
|
340
386
|
export function buildEnvironmentRuntimeConfig(config, environmentName, stackOutputs = {}) {
|
|
341
387
|
const environmentConfig = config.environments[environmentName];
|
|
342
388
|
const webinyConfig = resolveEnvironmentWebinyIntegration(config, environmentName);
|
|
389
|
+
const sitemapConfig = resolveEnvironmentSitemapIntegration(config, environmentName);
|
|
343
390
|
const tables = resolveTableNames(config, environmentName);
|
|
344
391
|
const runtimeParameterName = resolveRuntimeManifestParameterName(config, environmentName);
|
|
345
392
|
const stackName = resolveStackName(config, environmentName);
|
|
@@ -388,6 +435,9 @@ export function buildEnvironmentRuntimeConfig(config, environmentName, stackOutp
|
|
|
388
435
|
webiny: {
|
|
389
436
|
...webinyConfig,
|
|
390
437
|
mirrorTableName: tables.webinyMirror
|
|
438
|
+
},
|
|
439
|
+
sitemap: {
|
|
440
|
+
...sitemapConfig
|
|
391
441
|
}
|
|
392
442
|
},
|
|
393
443
|
variants
|
|
@@ -502,6 +552,7 @@ export async function validateAndResolveProjectConfig(projectConfig, options = {
|
|
|
502
552
|
}
|
|
503
553
|
|
|
504
554
|
const configuredWebiny = projectConfig.integrations?.webiny;
|
|
555
|
+
const configuredSitemap = projectConfig.integrations?.sitemap;
|
|
505
556
|
for (const [environmentName] of environmentEntries) {
|
|
506
557
|
const environmentWebinyConfig = resolveEnvironmentWebinyIntegration(resolveProjectConfig({
|
|
507
558
|
...projectConfig,
|
|
@@ -524,6 +575,15 @@ export async function validateAndResolveProjectConfig(projectConfig, options = {
|
|
|
524
575
|
}
|
|
525
576
|
}
|
|
526
577
|
|
|
578
|
+
for (const environmentName of Object.keys(configuredSitemap?.environments ?? {})) {
|
|
579
|
+
if (!projectConfig.environments?.[environmentName]) {
|
|
580
|
+
errors.push({
|
|
581
|
+
code: "CONFIG_CONFLICT_ERROR",
|
|
582
|
+
message: `integrations.sitemap.environments.${environmentName} does not match a configured environment.`
|
|
583
|
+
});
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
|
|
527
587
|
for (const [variantName, pattern] of Object.entries(projectConfig.aws?.codeBuckets ?? {})) {
|
|
528
588
|
ensureKnownPlaceholders(pattern, `aws.codeBuckets.${variantName}`, errors);
|
|
529
589
|
}
|