@creaditor/web-starterkit 1.0.31 → 1.0.32

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.
@@ -0,0 +1,136 @@
1
+ # Publish hook — `onPublish`
2
+
3
+ On the **web** channel, clicking **Publish** runs a fixed sequence:
4
+
5
+ ```
6
+ SEO modal → loader up → editor publishes → onPublish(...) → loader down → confetti
7
+ └── your work happens here ──┘
8
+ ```
9
+
10
+ The **editor owns the publish**. `onPublish` is the notification that it
11
+ succeeded, fired while the loader is still up, so you can do your own follow-up
12
+ before the user sees the success card. Nothing is celebrated until you say you
13
+ are finished.
14
+
15
+ If you do not supply `onPublish`, the editor simply publishes and celebrates.
16
+
17
+ ## Signature
18
+
19
+ ```js
20
+ onPublish(details, { loading, success, confetti, error, commands })
21
+ ```
22
+
23
+ ### `details` — what the user typed in the SEO modal
24
+
25
+ ```js
26
+ { title: string, description: string, keywords: string[] }
27
+ ```
28
+
29
+ The editor has already saved this via `patchSite` before calling you; it is
30
+ passed along so you can mirror it into your own system.
31
+
32
+ ### Closing the loop — required
33
+
34
+ The loader stays up until you signal. Do exactly one of:
35
+
36
+ | Signal | Effect |
37
+ |---|---|
38
+ | `success()` or `confetti()` | Loader down, success card + confetti, `creaditor:publish` fires. |
39
+ | `error(message?)` | Loader down, failure snackbar, `creaditor:publish-error` fires. |
40
+ | return a promise | Resolution counts as `success()`, rejection as `error()`. |
41
+
42
+ `success()` and `confetti()` are the same call — invoking both yields one card.
43
+ Whichever signal lands first wins; the rest are ignored, so exactly one terminal
44
+ event fires per publish.
45
+
46
+ **If you never signal**, the editor gives up after 60 seconds, drops the loader
47
+ and celebrates anyway, with a console warning. Do not rely on this.
48
+
49
+ ### `loading(on = true)`
50
+
51
+ Mounts / dismisses the loader. You rarely need it: the loader is already up when
52
+ you are called, and the editor drops it on your first completion signal. A
53
+ trailing `loading(false)` after `success()` is a harmless no-op.
54
+
55
+ ### `commands` — offline hosts only
56
+
57
+ The editor can only publish when it has an access token (online mode). Hosts
58
+ running offline/embedded must publish themselves:
59
+
60
+ | Command | What it does |
61
+ |---|---|
62
+ | `patchSite(props)` | PATCH the site record. Pass `{ seo }` to store the SEO. |
63
+ | `publishSite()` | Publish the saved state. No argument needed — the endpoint takes the site id from the URL and has no body schema. |
64
+ | `toHTML()` / `toJSON()` | The same serializers handed to `onNextStep`. |
65
+
66
+ `patchSite` must land **before** `publishSite`. Online hosts should ignore
67
+ `commands` entirely — calling them there publishes a second time.
68
+
69
+ ## Example
70
+
71
+ ```js
72
+ onPublish: (details, { confetti, success, loading, error }) => {
73
+ // the site is already live; rebuild it on our side, then celebrate
74
+ fetch(`${myHost}/CreV3.ashx?mn=rebuildPage&pageLink=${showInMyDomain}`)
75
+ .then(() => { success(); confetti(); loading(false); })
76
+ .catch(() => { error(); loading(false); });
77
+ }
78
+ ```
79
+
80
+ Or, equivalently, by returning a promise:
81
+
82
+ ```js
83
+ onPublish: async () => {
84
+ await fetch(`${myHost}/CreV3.ashx?mn=rebuildPage&pageLink=${showInMyDomain}`);
85
+ }
86
+ ```
87
+
88
+ ## Testing it without a host page
89
+
90
+ `packages/core/publish-harness.html` boots the starterkit with an `onPublish`
91
+ handler and prints a timestamped timeline of the whole flow, so the hook can be
92
+ exercised without the ASPX host:
93
+
94
+ ```bash
95
+ cd packages/core
96
+ npm run build:web:starterkit
97
+ python3 -m http.server 8899 --directory .
98
+ # open http://localhost:8899/publish-harness.html
99
+ ```
100
+
101
+ Pick what the host does — `success()`, `error()`, return a promise, or signal
102
+ nothing (which exercises the 60s guard) — and watch the loader/card state at
103
+ each step. Leave the token box empty to run offline: everything except the
104
+ network publish still happens.
105
+
106
+ To exercise the real thing, paste an edit token. The production bundle targets
107
+ `api.creaditor.ai` regardless of where the page is served from (`isLocalhost()`
108
+ is a build-time `NODE_ENV` flag, not the hostname), so a local page plus a real
109
+ token is a genuine publish — **use a throwaway site**. The page documents both
110
+ ways to get a token.
111
+
112
+ ## Window events
113
+
114
+ Fired whether or not `onPublish` is wired, for hosts that would rather listen:
115
+
116
+ ```js
117
+ window.addEventListener("creaditor:publish", (e) => {
118
+ e.detail.seo; // { title, description, keywords }
119
+ e.detail.url; // live site URL, when the editor knows it
120
+ e.detail.channel; // "web"
121
+ e.detail.selfPublish; // true when the editor did the publishing
122
+ e.detail.toHTML; // serializers
123
+ e.detail.toJSON;
124
+ });
125
+
126
+ window.addEventListener("creaditor:publish-error", (e) => e.detail.error);
127
+ ```
128
+
129
+ ## Gotchas
130
+
131
+ - **Web channel only.** Newsletter/email keeps `onNextStep`; no SEO modal.
132
+ - **Cancelling the SEO modal aborts the publish.** `onPublish` is not called.
133
+ - **`onPublish` wins over `onNextStep`** on the web channel — if you supply
134
+ both, only `onPublish` runs.
135
+ - **Do not call `commands.publishSite()` from an online host.** The editor
136
+ already published before calling you.
package/README.md CHANGED
@@ -75,6 +75,14 @@ The key differences are:
75
75
  > sections). `sections` is currently empty for web and will be populated as
76
76
  > web section templates are added under `engines/web/sections`.
77
77
 
78
+ ### Publish
79
+
80
+ On the web channel the **Publish** button opens the SEO modal, publishes, then
81
+ calls your `onPublish` hook — with the loader still up — so you can run your own
82
+ follow-up before the user sees the success card. See
83
+ [PUBLISH-HOOK.md](./PUBLISH-HOOK.md) for the signature, how to signal
84
+ completion, and the `creaditor:publish` window event.
85
+
78
86
  ## 🏗️ How it's built
79
87
 
80
88
  This package is a thin wrapper. The bundle is produced from `@creaditor/core`:
@@ -582,7 +582,7 @@
582
582
  width: 15px;
583
583
  height: 15px;
584
584
  }
585
- `,u.properties={message:{type:String},sub:{type:String},url:{type:String},seoTitle:{type:String},seoDescription:{type:String},copied:{type:Boolean}},customElements.define("cdtr-publish-success",u);const f="creaditor:publish",g="creaditor:publish-error";class b extends o.WF{constructor(){super(),this.channel="",this.isLoading=!1,this.isLocked=!1,this.lockTimer=null}connectedCallback(){super.connectedCallback(),this.onNextStep||(this.onNextStep=s.M.get("config","onNextStep")),this.channel||(this.channel=s.M.get("config","channel")||"")}disconnectedCallback(){super.disconnectedCallback(),this.clearTimers()}clearTimers(){this.lockTimer&&(clearTimeout(this.lockTimer),this.lockTimer=null)}async handleClick(){if(this.isLocked||this.isLoading)return;const e=!!s.M.get("config","access"),t="web"===this.channel,i=e&&t;let o=null;if(!t||(o=await p.open(),o)){this.isLoading=!0,this.isLocked=!0,this.lockTimer=setTimeout(()=>{this.clearTimers(),this.isLoading=!1,this.isLocked=!1},1e4);try{if(t){const e=this.showBuildingLoader();try{i&&o&&(await(0,n.YE)({seo:o}),await(0,n.bK)({})),"function"==typeof this.onNextStep&&await this.onNextStep({toHTML:r.FE,toJSON:r.U2,seo:o??void 0})}finally{e()}return this.emitPublished(i,o),void this.celebratePublish(o)}"function"==typeof this.onNextStep&&await this.onNextStep({toHTML:r.FE,toJSON:r.U2}),this.emitPublished(i,o)}catch(e){console.error("Error in publish/onNextStep:",e),this.emitPublishError(i,o,e),this.snackbar(s.M.get("lang","publishError")||"Publish failed")}finally{this.clearTimers(),this.isLoading=!1,this.isLocked=!1}}}snackbar(e){const t=s.M.get("commands");t?.snackbar?.(e)}showBuildingLoader(){const e=(0,h.A)();document.body.appendChild(e);let t=!1;return()=>{t||(t=!0,e.style.transition="opacity 300ms ease",e.style.opacity="0",e.style.pointerEvents="none",setTimeout(()=>e.remove(),320))}}siteUrl(){const e=s.M.get("app","domains"),t=e?.[0]?.name;if(t)return/^https?:\/\//.test(t)?t:`https://${t}`}emitPublished(e,t){const i={channel:this.channel,selfPublish:e,seo:t??void 0,url:this.siteUrl(),toHTML:r.FE,toJSON:r.U2};window.dispatchEvent(new CustomEvent(f,{detail:i}))}emitPublishError(e,t,i){const o={channel:this.channel,selfPublish:e,seo:t??void 0,error:i};window.dispatchEvent(new CustomEvent(g,{detail:o}))}celebratePublish(e){const t=document.createElement("cdtr-publish-success");e&&(t.seoTitle=e.title,t.seoDescription=e.description);const i=this.siteUrl();i&&(t.url=i),document.body.appendChild(t)}render(){const e=!!s.M.get("config","access");if(!(this.onNextStep||"web"===this.channel&&e))return null;const t="web"===this.channel?"publish":"next";return o.qy`
585
+ `,u.properties={message:{type:String},sub:{type:String},url:{type:String},seoTitle:{type:String},seoDescription:{type:String},copied:{type:Boolean}},customElements.define("cdtr-publish-success",u);const f="creaditor:publish",g="creaditor:publish-error";class b extends o.WF{constructor(){super(),this.channel="",this.isLoading=!1,this.isLocked=!1,this.lockTimer=null}connectedCallback(){super.connectedCallback(),this.onNextStep||(this.onNextStep=s.M.get("config","onNextStep")),this.onPublish||(this.onPublish=s.M.get("config","onPublish")),this.channel||(this.channel=s.M.get("config","channel")||"")}disconnectedCallback(){super.disconnectedCallback(),this.clearTimers()}clearTimers(){this.lockTimer&&(clearTimeout(this.lockTimer),this.lockTimer=null)}async handleClick(){if(this.isLocked||this.isLoading)return;const e=!!s.M.get("config","access"),t="web"===this.channel,i=e&&t;let o=null;if(!t||(o=await p.open(),o)){this.isLoading=!0,this.isLocked=!0,this.lockTimer=setTimeout(()=>{this.clearTimers(),this.isLoading=!1,this.isLocked=!1},1e4);try{if(t){const e=this.showBuildingLoader();let t=!1;try{i&&o&&(await(0,n.YE)({seo:o}),await(0,n.bK)({})),"function"==typeof this.onPublish?t=await this.runHostPublish(this.onPublish,o,i,e):"function"==typeof this.onNextStep&&await this.onNextStep({toHTML:r.FE,toJSON:r.U2,seo:o??void 0})}finally{e()}return void(t||(this.emitPublished(i,o),this.celebratePublish(o)))}"function"==typeof this.onNextStep&&await this.onNextStep({toHTML:r.FE,toJSON:r.U2}),this.emitPublished(i,o)}catch(e){console.error("Error in publish/onNextStep:",e),this.emitPublishError(i,o,e),this.snackbar(s.M.get("lang","publishError")||"Publish failed")}finally{this.clearTimers(),this.isLoading=!1,this.isLocked=!1}}}runHostPublish(e,t,i,o){var a=this;return new Promise(l=>{let c=!1,d=o;const p=function(){arguments.length>0&&void 0!==arguments[0]&&!arguments[0]?(d?.(),d=null):d||(d=a.showBuildingLoader())},h=(e,o,r)=>{c||(c=!0,clearTimeout(u),p(!1),e?(this.celebratePublish(t),this.emitPublished(i,t)):(this.snackbar(r||s.M.get("lang","publishError")||"Publish failed"),this.emitPublishError(i,t,o)),l(!0))},u=setTimeout(()=>{c||(console.warn("config.onPublish never signalled completion — finishing the publish without it. Call success() or error() (or return a promise) so the loader can come down."),c=!0,p(!1),l(!1))},6e4),f={title:t.title,description:t.description,keywords:t.keywords},g={loading:p,success:()=>h(!0),confetti:()=>h(!0),error:e=>h(!1,new Error(e||"host publish failed"),e),commands:{patchSite:e=>(0,n.YE)(e??{}),publishSite:e=>(0,n.bK)(e??{}),toHTML:r.FE,toJSON:r.U2}};let b;try{b=e(f,g)}catch(e){return console.error("Error in config.onPublish:",e),void h(!1,e)}b&&"function"==typeof b.then&&Promise.resolve(b).then(()=>h(!0),e=>{console.error("Error in config.onPublish:",e),h(!1,e)})})}snackbar(e){const t=s.M.get("commands");t?.snackbar?.(e)}showBuildingLoader(){const e=(0,h.A)();document.body.appendChild(e);let t=!1;return()=>{t||(t=!0,e.style.transition="opacity 300ms ease",e.style.opacity="0",e.style.pointerEvents="none",setTimeout(()=>e.remove(),320))}}siteUrl(){const e=s.M.get("app","domains"),t=e?.[0]?.name;if(t)return/^https?:\/\//.test(t)?t:`https://${t}`}emitPublished(e,t){const i={channel:this.channel,selfPublish:e,seo:t??void 0,url:this.siteUrl(),toHTML:r.FE,toJSON:r.U2};window.dispatchEvent(new CustomEvent(f,{detail:i}))}emitPublishError(e,t,i){const o={channel:this.channel,selfPublish:e,seo:t??void 0,error:i};window.dispatchEvent(new CustomEvent(g,{detail:o}))}celebratePublish(e){const t=document.createElement("cdtr-publish-success");e&&(t.seoTitle=e.title,t.seoDescription=e.description);const i=this.siteUrl();i&&(t.url=i),document.body.appendChild(t)}render(){const e=!!s.M.get("config","access");if(!(this.onNextStep||"function"==typeof this.onPublish||"web"===this.channel&&e))return null;const t="web"===this.channel?"publish":"next";return o.qy`
586
586
  <paper-button
587
587
  @click=${this.handleClick}
588
588
  class="next-button"
@@ -648,4 +648,4 @@
648
648
  }
649
649
 
650
650
 
651
- `,b.properties={onNextStep:{type:Function},channel:{type:String},isLoading:{type:Boolean},isLocked:{type:Boolean},lockTimer:{type:Number}},customElements.define("cdtr-next-button",b)}}]);
651
+ `,b.properties={onNextStep:{type:Function},onPublish:{type:Function},channel:{type:String},isLoading:{type:Boolean},isLocked:{type:Boolean},lockTimer:{type:Number}},customElements.define("cdtr-next-button",b)}}]);
Binary file