@gaulatti/giorgia 0.1.98 → 0.1.99

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 (42) hide show
  1. package/README.md +65 -3
  2. package/dist/cronkite-manifest.d.ts +51 -0
  3. package/dist/cronkite-manifest.js +115 -0
  4. package/dist/cronkite-manifest.json +130 -0
  5. package/dist/json-schema-formats.d.ts +2 -0
  6. package/dist/json-schema-formats.js +24 -0
  7. package/dist/layouts.d.ts +14 -0
  8. package/dist/layouts.js +13 -0
  9. package/dist/live-program-page/live-program.css +1 -1
  10. package/dist/live-program-page/live-program.js +1 -1
  11. package/dist/manifest-validation.d.ts +10 -0
  12. package/dist/manifest-validation.js +27 -0
  13. package/dist/outlet-config.d.ts +0 -1
  14. package/dist/outlet-config.js +1 -2
  15. package/dist/renderer.core.d.ts +2 -1
  16. package/dist/renderer.core.js +2 -1
  17. package/dist/renderer.d.ts +2 -0
  18. package/dist/renderer.js +1 -0
  19. package/dist/renderer.node.js +1 -12
  20. package/dist/schemas/canonical-document.schema.json +1071 -0
  21. package/dist/schemas/cronkite-manifest.schema.json +501 -0
  22. package/dist/schemas/search-index.schema.json +27 -0
  23. package/dist/schemas/search-manifest.schema.json +34 -0
  24. package/dist/types/canonical-article.d.ts +7 -254
  25. package/dist/types/canonical-article.js +21 -242
  26. package/dist/types/canonical-document-schema.generated.d.ts +1634 -0
  27. package/dist/types/canonical-document-schema.generated.js +1730 -0
  28. package/dist/types/canonical-document.generated.d.ts +541 -0
  29. package/dist/types/canonical-document.generated.js +3 -0
  30. package/dist/types/cronkite-manifest.generated.d.ts +339 -0
  31. package/dist/types/cronkite-manifest.generated.js +3 -0
  32. package/dist/video/Root.d.ts +1 -0
  33. package/dist/video/Root.js +40 -0
  34. package/dist/video/ShortVideo.d.ts +3 -0
  35. package/dist/video/ShortVideo.js +92 -0
  36. package/dist/video/index.d.ts +2 -0
  37. package/dist/video/index.js +4 -0
  38. package/dist/video/types.d.ts +37 -0
  39. package/dist/video/types.js +1 -0
  40. package/package.json +22 -9
  41. package/src/styles/compiled.css +1 -1
  42. package/src/templates/partials/components/coming-soon/main.hbs +1 -1
package/README.md CHANGED
@@ -5,8 +5,11 @@ Server-side renderer and Handlebars template bundle for ModoItaliano pages.
5
5
  ## What it does
6
6
 
7
7
  - Renders canonical content documents into HTML
8
- - Supports `article-page`, `homepage`, `category-page`, `live-story`, and `404` layouts
8
+ - Supports the layouts declared by the packaged Cronkite manifest, including
9
+ article, homepage, category, search, 404, coming-soon, live-story,
10
+ link-in-bio, media, and standalone pages
9
11
  - Ships reusable Handlebars templates, partial dependency metadata, and compiled CSS
12
+ - Ships provider-neutral social-image and Remotion short-video renderables
10
13
  - Preserves the masthead, footer, and radio player across same-language internal navigation
11
14
 
12
15
  ## Installation
@@ -18,18 +21,31 @@ npm install @gaulatti/giorgia
18
21
  ## Basic usage
19
22
 
20
23
  ```ts
21
- import { render } from '@gaulatti/giorgia';
24
+ import { render } from "@gaulatti/giorgia";
22
25
 
23
26
  const html = render(doc);
24
27
  ```
25
28
 
26
- `doc` must match the canonical schema used by the renderer (see [src/types/canonical-article.ts](src/types/canonical-article.ts)).
29
+ `doc` must match the normative canonical schema in
30
+ [src/schemas/canonical-document.schema.json](src/schemas/canonical-document.schema.json).
31
+ TypeScript declarations are generated from that schema; runtime rendering validates
32
+ against the same artifact.
27
33
 
28
34
  ## Exports
29
35
 
30
36
  - `@gaulatti/giorgia` -> renderer entrypoint
37
+ - `@gaulatti/giorgia/cronkite-manifest.json` -> data-only CLS capability manifest
38
+ - `@gaulatti/giorgia/video` -> bundleable Remotion short-video entrypoint
39
+ - `@gaulatti/giorgia/schemas/canonical-document.schema.json` -> canonical input contract
40
+ - `@gaulatti/giorgia/schemas/search-manifest.schema.json` and
41
+ `@gaulatti/giorgia/schemas/search-index.schema.json` -> static-search resource contract
31
42
  - `@gaulatti/giorgia/partial-deps.json` -> partial-to-layout dependency map
32
43
 
44
+ The root module also exports `cronkiteManifest`, `outletConfig`, `version`,
45
+ `render`, `fontFiles`, `assetFiles`, and `buildInstagramImageHtml`. See
46
+ [docs/cronkite-compatibility.md](docs/cronkite-compatibility.md) for ownership,
47
+ validation, search, and asset-rendering details.
48
+
33
49
  ## Development
34
50
 
35
51
  ```bash
@@ -37,6 +53,7 @@ npm install
37
53
  npm run typecheck
38
54
  npm run test:unit
39
55
  npm run build
56
+ npm run verify:packed-remotion
40
57
  npm run storybook
41
58
  ```
42
59
 
@@ -50,3 +67,48 @@ Page-local scripts that must run after a content swap use `data-swup-reload-scri
50
67
 
51
68
  - CI validates typecheck, unit tests, and package build on pull requests and `main` pushes.
52
69
  - Package publish is triggered by pushing a `v*` tag.
70
+ - Storybook pull requests build an immutable artifact but receive no AWS token.
71
+ A push or manual run on `main` deploys that exact artifact to
72
+ `https://ui.modoitaliano.fm` through the `giorgia-storybook` GitHub
73
+ environment and short-lived AWS OIDC role
74
+ `giorgia-storybook-github-deploy`. Storybook deployment never runs
75
+ `npm publish` or changes the package version.
76
+
77
+ ### Storybook infrastructure and rollback
78
+
79
+ The Modo Italiano `Loredana` CloudFormation stack owns the dedicated Storybook
80
+ bucket and CloudFront distribution. The workflow resolves outputs
81
+ `LoredanaGiorgiaStorybookBucketName` and
82
+ `LoredanaGiorgiaStorybookDistributionId`; it must never target the Fifthbell
83
+ `ui.fifthbell.com` resources used by Brokaw.
84
+
85
+ Each build carries `deployment.json` with its repository, package version, run,
86
+ and full Git SHA. Deployment uploads to `/releases/<sha>` and changes the
87
+ CloudFront origin path only after the complete immutable prefix is present.
88
+ The previous origin path is retained and restored automatically if the public
89
+ identity check fails after promotion. A failed build or upload therefore
90
+ cannot partially overwrite the last known-good documentation.
91
+
92
+ The deployment log records the previous origin path. For an
93
+ operator-authorized manual rollback, fetch the current distribution config and
94
+ ETag, restore only the dedicated S3 origin's `OriginPath` to that recorded
95
+ `/releases/<sha>` value, submit the update with the ETag, wait for CloudFront to
96
+ deploy, and invalidate `/*`. Confirm the public `deployment.json` reports the
97
+ expected prior SHA. Do not delete release prefixes until a separately reviewed
98
+ retention policy exists.
99
+
100
+ The deployment role must trust only audience `sts.amazonaws.com` and the
101
+ repository's current immutable GitHub OIDC subject:
102
+
103
+ ```text
104
+ repo:modoitaliano@327696306/giorgia@1304750193:environment:giorgia-storybook
105
+ ```
106
+
107
+ Its permissions are limited to `cloudformation:DescribeStacks` for Loredana;
108
+ `s3:ListBucket` and `s3:GetBucketLocation` on the dedicated bucket;
109
+ `s3:GetObject` and `s3:PutObject` on that bucket's `releases/*`; and
110
+ `cloudfront:GetDistribution`,
111
+ `cloudfront:GetDistributionConfig`, `cloudfront:UpdateDistribution`,
112
+ `cloudfront:CreateInvalidation`, and `cloudfront:GetInvalidation` on the one
113
+ Storybook distribution. It has no delete permission. No long-lived AWS key
114
+ belongs in GitHub secrets.
@@ -0,0 +1,51 @@
1
+ export declare const cronkiteManifest: {
2
+ readonly contract: "cronkite.cls";
3
+ readonly contractVersion: 1;
4
+ readonly package: "@gaulatti/giorgia";
5
+ readonly version: string;
6
+ readonly layouts: ["article-page" | "homepage" | "category-page" | "search-page" | "404" | "coming-soon" | "live-story" | "link-in-bio" | "media-page" | "standalone-page", ...("article-page" | "homepage" | "category-page" | "search-page" | "404" | "coming-soon" | "live-story" | "link-in-bio" | "media-page" | "standalone-page")[]];
7
+ readonly languages: ["es", "en", "it"];
8
+ readonly collections: {
9
+ readonly "article-page": "json/articles";
10
+ readonly "standalone-page": "json/pages";
11
+ };
12
+ readonly systemPages: import("./types/cronkite-manifest.generated.js").SystemPage[];
13
+ readonly renderables: {
14
+ readonly "search-page": {
15
+ readonly engine: "handlebars";
16
+ readonly export: "render";
17
+ readonly outputMode: "single";
18
+ readonly lifecycle: "request";
19
+ readonly contentType: "text/html; charset=utf-8";
20
+ readonly resources: {
21
+ readonly "search-manifest": {
22
+ readonly key: "search-manifest-{language}.json";
23
+ readonly schema: "dist/schemas/search-manifest.schema.json";
24
+ readonly contentType: "application/json";
25
+ readonly perLanguage: true;
26
+ };
27
+ };
28
+ };
29
+ readonly "social-image": {
30
+ readonly engine: "html-raster";
31
+ readonly export: "buildInstagramImageHtml";
32
+ readonly contentType: "image/jpeg";
33
+ readonly width: 1080;
34
+ readonly height: 1350;
35
+ };
36
+ readonly "short-video": {
37
+ readonly engine: "remotion";
38
+ readonly entry: "dist/video/index.js";
39
+ readonly compositionId: "ModoItalianoShort";
40
+ readonly contentType: "video/mp4";
41
+ };
42
+ };
43
+ readonly capabilities: {
44
+ readonly fonts: {
45
+ readonly export: "fontFiles";
46
+ };
47
+ readonly assets: {
48
+ readonly export: "assetFiles";
49
+ };
50
+ };
51
+ };
@@ -0,0 +1,115 @@
1
+ import { layoutNames } from "./layouts.js";
2
+ import { outletConfig } from "./outlet-config.js";
3
+ import { version } from "./version.js";
4
+ const systemPages = [
5
+ {
6
+ layout: "404",
7
+ route: "/{language}/404",
8
+ key: "html/{language}/404/index.html",
9
+ cacheControl: "public, max-age=0, must-revalidate",
10
+ perLanguage: true,
11
+ copy: {
12
+ es: {
13
+ title: "Página no encontrada",
14
+ description: "La página que buscas no existe.",
15
+ },
16
+ en: {
17
+ title: "Page Not Found",
18
+ description: "The page you are looking for does not exist.",
19
+ },
20
+ it: {
21
+ title: "Pagina non trovata",
22
+ description: "La pagina che stai cercando non esiste.",
23
+ },
24
+ },
25
+ },
26
+ {
27
+ layout: "search-page",
28
+ route: "/{language}/search",
29
+ key: "html/{language}/search/index.html",
30
+ cacheControl: "public, max-age=0, must-revalidate",
31
+ perLanguage: true,
32
+ copy: {
33
+ es: {
34
+ title: "Buscar",
35
+ description: outletConfig.searchDescriptions.es,
36
+ },
37
+ en: {
38
+ title: "Search",
39
+ description: outletConfig.searchDescriptions.en,
40
+ },
41
+ it: {
42
+ title: "Cerca",
43
+ description: outletConfig.searchDescriptions.it,
44
+ },
45
+ },
46
+ },
47
+ {
48
+ layout: "coming-soon",
49
+ route: "/{language}/coming-soon",
50
+ key: "html/{language}/coming-soon/index.html",
51
+ cacheControl: "public, max-age=0, must-revalidate",
52
+ perLanguage: true,
53
+ copy: {
54
+ es: {
55
+ title: "Próximamente",
56
+ description: "Estamos preparando algo especial. Vuelve pronto para descubrirlo.",
57
+ },
58
+ en: {
59
+ title: "Coming soon",
60
+ description: "We are preparing something special. Come back soon.",
61
+ },
62
+ it: {
63
+ title: "Prossimamente",
64
+ description: "Stiamo preparando qualcosa di speciale. Torna presto.",
65
+ },
66
+ },
67
+ },
68
+ ];
69
+ export const cronkiteManifest = {
70
+ contract: "cronkite.cls",
71
+ contractVersion: 1,
72
+ package: "@gaulatti/giorgia",
73
+ version,
74
+ layouts: [...layoutNames],
75
+ languages: [...outletConfig.supportedLanguages],
76
+ collections: {
77
+ "article-page": "json/articles",
78
+ "standalone-page": "json/pages",
79
+ },
80
+ systemPages,
81
+ renderables: {
82
+ "search-page": {
83
+ engine: "handlebars",
84
+ export: "render",
85
+ outputMode: "single",
86
+ lifecycle: "request",
87
+ contentType: "text/html; charset=utf-8",
88
+ resources: {
89
+ "search-manifest": {
90
+ key: "search-manifest-{language}.json",
91
+ schema: "dist/schemas/search-manifest.schema.json",
92
+ contentType: "application/json",
93
+ perLanguage: true,
94
+ },
95
+ },
96
+ },
97
+ "social-image": {
98
+ engine: "html-raster",
99
+ export: "buildInstagramImageHtml",
100
+ contentType: "image/jpeg",
101
+ width: 1080,
102
+ height: 1350,
103
+ },
104
+ "short-video": {
105
+ engine: "remotion",
106
+ entry: "dist/video/index.js",
107
+ compositionId: "ModoItalianoShort",
108
+ contentType: "video/mp4",
109
+ },
110
+ },
111
+ capabilities: {
112
+ fonts: { export: "fontFiles" },
113
+ assets: { export: "assetFiles" },
114
+ },
115
+ };
@@ -0,0 +1,130 @@
1
+ {
2
+ "contract": "cronkite.cls",
3
+ "contractVersion": 1,
4
+ "package": "@gaulatti/giorgia",
5
+ "version": "0.1.99",
6
+ "layouts": [
7
+ "404",
8
+ "article-page",
9
+ "homepage",
10
+ "category-page",
11
+ "search-page",
12
+ "coming-soon",
13
+ "live-story",
14
+ "link-in-bio",
15
+ "media-page",
16
+ "standalone-page"
17
+ ],
18
+ "languages": [
19
+ "es",
20
+ "en",
21
+ "it"
22
+ ],
23
+ "collections": {
24
+ "article-page": "json/articles",
25
+ "standalone-page": "json/pages"
26
+ },
27
+ "systemPages": [
28
+ {
29
+ "layout": "404",
30
+ "route": "/{language}/404",
31
+ "key": "html/{language}/404/index.html",
32
+ "cacheControl": "public, max-age=0, must-revalidate",
33
+ "perLanguage": true,
34
+ "copy": {
35
+ "es": {
36
+ "title": "Página no encontrada",
37
+ "description": "La página que buscas no existe."
38
+ },
39
+ "en": {
40
+ "title": "Page Not Found",
41
+ "description": "The page you are looking for does not exist."
42
+ },
43
+ "it": {
44
+ "title": "Pagina non trovata",
45
+ "description": "La pagina che stai cercando non esiste."
46
+ }
47
+ }
48
+ },
49
+ {
50
+ "layout": "search-page",
51
+ "route": "/{language}/search",
52
+ "key": "html/{language}/search/index.html",
53
+ "cacheControl": "public, max-age=0, must-revalidate",
54
+ "perLanguage": true,
55
+ "copy": {
56
+ "es": {
57
+ "title": "Buscar",
58
+ "description": "Busca noticias de ModoItaliano."
59
+ },
60
+ "en": {
61
+ "title": "Search",
62
+ "description": "Search ModoItaliano news."
63
+ },
64
+ "it": {
65
+ "title": "Cerca",
66
+ "description": "Cerca le notizie di ModoItaliano."
67
+ }
68
+ }
69
+ },
70
+ {
71
+ "layout": "coming-soon",
72
+ "route": "/{language}/coming-soon",
73
+ "key": "html/{language}/coming-soon/index.html",
74
+ "cacheControl": "public, max-age=0, must-revalidate",
75
+ "perLanguage": true,
76
+ "copy": {
77
+ "es": {
78
+ "title": "Próximamente",
79
+ "description": "Estamos preparando algo especial. Vuelve pronto para descubrirlo."
80
+ },
81
+ "en": {
82
+ "title": "Coming soon",
83
+ "description": "We are preparing something special. Come back soon."
84
+ },
85
+ "it": {
86
+ "title": "Prossimamente",
87
+ "description": "Stiamo preparando qualcosa di speciale. Torna presto."
88
+ }
89
+ }
90
+ }
91
+ ],
92
+ "renderables": {
93
+ "search-page": {
94
+ "engine": "handlebars",
95
+ "export": "render",
96
+ "outputMode": "single",
97
+ "lifecycle": "request",
98
+ "contentType": "text/html; charset=utf-8",
99
+ "resources": {
100
+ "search-manifest": {
101
+ "key": "search-manifest-{language}.json",
102
+ "schema": "dist/schemas/search-manifest.schema.json",
103
+ "contentType": "application/json",
104
+ "perLanguage": true
105
+ }
106
+ }
107
+ },
108
+ "social-image": {
109
+ "engine": "html-raster",
110
+ "export": "buildInstagramImageHtml",
111
+ "contentType": "image/jpeg",
112
+ "width": 1080,
113
+ "height": 1350
114
+ },
115
+ "short-video": {
116
+ "engine": "remotion",
117
+ "entry": "dist/video/index.js",
118
+ "compositionId": "ModoItalianoShort",
119
+ "contentType": "video/mp4"
120
+ }
121
+ },
122
+ "capabilities": {
123
+ "fonts": {
124
+ "export": "fontFiles"
125
+ },
126
+ "assets": {
127
+ "export": "assetFiles"
128
+ }
129
+ }
130
+ }
@@ -0,0 +1,2 @@
1
+ import type { Ajv2020 } from "ajv/dist/2020.js";
2
+ export declare function addContractFormats(ajv: Ajv2020): void;
@@ -0,0 +1,24 @@
1
+ const rfc3339 = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/;
2
+ const uuid = /^(?:[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$/i;
3
+ export function addContractFormats(ajv) {
4
+ ajv.addFormat("date-time", {
5
+ type: "string",
6
+ validate: (value) => rfc3339.test(value) && !Number.isNaN(Date.parse(value)),
7
+ });
8
+ ajv.addFormat("uri", {
9
+ type: "string",
10
+ validate: (value) => {
11
+ try {
12
+ const parsed = new URL(value);
13
+ return Boolean(parsed.protocol);
14
+ }
15
+ catch {
16
+ return false;
17
+ }
18
+ },
19
+ });
20
+ ajv.addFormat("uuid", {
21
+ type: "string",
22
+ validate: (value) => uuid.test(value),
23
+ });
24
+ }
@@ -0,0 +1,14 @@
1
+ export declare const layoutFiles: {
2
+ readonly "article-page": "article-page.hbs";
3
+ readonly homepage: "homepage.hbs";
4
+ readonly "category-page": "category-page.hbs";
5
+ readonly "search-page": "search-page.hbs";
6
+ readonly "404": "404.hbs";
7
+ readonly "coming-soon": "coming-soon.hbs";
8
+ readonly "live-story": "live-story.hbs";
9
+ readonly "link-in-bio": "link-in-bio.hbs";
10
+ readonly "media-page": "media-page.hbs";
11
+ readonly "standalone-page": "standalone-page.hbs";
12
+ };
13
+ export type LayoutName = keyof typeof layoutFiles;
14
+ export declare const layoutNames: readonly ["article-page" | "homepage" | "category-page" | "search-page" | "404" | "coming-soon" | "live-story" | "link-in-bio" | "media-page" | "standalone-page", ...("article-page" | "homepage" | "category-page" | "search-page" | "404" | "coming-soon" | "live-story" | "link-in-bio" | "media-page" | "standalone-page")[]];
@@ -0,0 +1,13 @@
1
+ export const layoutFiles = {
2
+ "article-page": "article-page.hbs",
3
+ homepage: "homepage.hbs",
4
+ "category-page": "category-page.hbs",
5
+ "search-page": "search-page.hbs",
6
+ "404": "404.hbs",
7
+ "coming-soon": "coming-soon.hbs",
8
+ "live-story": "live-story.hbs",
9
+ "link-in-bio": "link-in-bio.hbs",
10
+ "media-page": "media-page.hbs",
11
+ "standalone-page": "standalone-page.hbs",
12
+ };
13
+ export const layoutNames = Object.freeze(Object.keys(layoutFiles));