<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/">
    <channel>
        <title>Pglet Blog</title>
        <link>https://pglet.io/blog</link>
        <description>Pglet Blog</description>
        <lastBuildDate>Sun, 13 Feb 2022 00:00:00 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <item>
            <title><![CDATA[Pglet v0.6 release]]></title>
            <link>https://pglet.io/blog/pglet-0-6</link>
            <guid>pglet-0-6</guid>
            <pubDate>Sun, 13 Feb 2022 00:00:00 GMT</pubDate>
            <description><![CDATA[We are thrilled to announce Pglet v0.6.0 and updated Python client!]]></description>
            <content:encoded><![CDATA[<p>We are thrilled to announce <a href="https://github.com/pglet/pglet/releases/tag/v0.6.0" target="_blank" rel="noopener noreferrer">Pglet v0.6.0</a> and <a href="https://github.com/pglet/pglet-python/releases/tag/v0.6.0" target="_blank" rel="noopener noreferrer">updated Python client</a>!</p><p>Notable changes:</p><ul><li>Added <code>focused</code> property, <code>on_focus</code> and <code>on_blur</code> events to all input controls - paving the way to a proper validation support.</li><li>New <a href="https://developer.microsoft.com/en-us/fluentui#/controls/web/persona" target="_blank" rel="noopener noreferrer"><code>Persona</code></a> control.</li><li>New <a href="https://developer.microsoft.com/en-us/fluentui#/controls/web/combobox" target="_blank" rel="noopener noreferrer"><code>ComboBox</code></a> control.</li><li>New <code>page</code> events: <code>connect</code> and <code>disconnect</code> for real-time chat-like experiences.</li><li>Harmonization of border styling propeties across <code>Stack</code>, <code>Image</code>, <code>IFrame</code> and <code>Text</code> controls: HTML-ish <code>border</code> property with mixed and confusing to non-web devs semantics (<code>1px solid black</code> or <code>solid 1px black</code>?) replaced with clean and simple <code>border_style</code>, <code>border_width</code> and <code>border_color</code> properties.</li><li>All boolean and enum-like properties are protected with <a href="https://github.com/beartype/beartype" target="_blank" rel="noopener noreferrer"><code>beartype</code></a>.</li><li>Fixed all Python tests to ensure <a href="https://ci.appveyor.com/project/pglet/pglet-python" target="_blank" rel="noopener noreferrer">Pglet works nice with Python 3.7 and above</a>. Big shout-out to <a href="https://github.com/mikaelho" target="_blank" rel="noopener noreferrer">@mikaelho</a> for helping with that!</li><li><a href="https://github.com/psf/black" target="_blank" rel="noopener noreferrer">Black</a> and <a href="https://pycqa.github.io/isort/" target="_blank" rel="noopener noreferrer">isort</a> was adopted as official Python formatting tools.</li><li>Generating <a href="https://pypi.org/project/pglet/#files" target="_blank" rel="noopener noreferrer">platform-specific wheels</a> (<code>.whl</code>) with one <code>pglet</code> executable inside only: smaller wheels - faster installation!</li></ul>]]></content:encoded>
            <category>release notes</category>
        </item>
        <item>
            <title><![CDATA[Building email signup form for Docusaurus with hCaptcha, Cloudflare Pages and Mailgun]]></title>
            <link>https://pglet.io/blog/email-sign-form-for-docusaurus-with-hcaptcha-cloudflare-pages-and-mailgun</link>
            <guid>email-sign-form-for-docusaurus-with-hcaptcha-cloudflare-pages-and-mailgun</guid>
            <pubDate>Tue, 11 Jan 2022 00:00:00 GMT</pubDate>
            <description><![CDATA[Introduction]]></description>
            <content:encoded><![CDATA[<h2 class="anchor anchorWithHideOnScrollNavbar_R0VQ" id="introduction">Introduction<a class="hash-link" href="#introduction" title="Direct link to heading">​</a></h2><p>Staying in touch with your users via email is still an effective and reliable communication channel. In this tutorial we are going to implement <a href="https://pglet.io/#signup" target="_blank" rel="noopener noreferrer">email signup form</a> for a React-based static website that allows users to submit their email address and subscribe to a project mailing list. We are going to implement "double opt-in" process where upon signup an email is sent to the user which includes a link to click and confirm the subscription.</p><p><a href="https://pglet.io" target="_blank" rel="noopener noreferrer">Pglet website</a> is made with <a href="https://docusaurus.io/" target="_blank" rel="noopener noreferrer">Docusaurus</a> and hosted on <a href="https://pages.cloudflare.com/" target="_blank" rel="noopener noreferrer">Cloudflare Pages</a>. However, the following solution could be easily adopted for other React-based website frameworks such as <a href="https://nextjs.org/" target="_blank" rel="noopener noreferrer">Next.js</a> and use a different backend for server-side logic such as <a href="https://vercel.com/docs/concepts/functions/introduction" target="_blank" rel="noopener noreferrer">Vercel Functions</a> or <a href="https://deno.com/deploy/docs" target="_blank" rel="noopener noreferrer">Deno Deploy</a>.</p><p>Project requirements:</p><ul><li>The form must be as simple as possible: just "email" field and "submit" button.</li><li>The form must protected by CAPTCHA.</li><li>Double opt-in subscription process should be implemented: after submitting the form a user receives an email with a confirmation link to complete the process.</li></ul><p>For CAPTCHA we are going to use <a href="https://www.hcaptcha.com/" target="_blank" rel="noopener noreferrer">hCaptcha</a>, which is a great alternative to Google's reCAPTCHA and has a similar API.</p><p>A signup form requires server-side processing and for that we re going to use <a href="https://developers.cloudflare.com/pages/platform/functions" target="_blank" rel="noopener noreferrer">Cloudflare Pages Functions</a> which are a part of Cloudflare Pages platform.</p><p>For maintaining mailing list and sending email messages we are going to use <a href="https://www.mailgun.com/" target="_blank" rel="noopener noreferrer">Mailgun</a>. Mailgun offers great functionality, first-class API at a flexible pricing, plus we have a lot of experience with it.</p><p>All code samples in this article can be found in:</p><ul><li><a href="https://github.com/pglet/website" target="_blank" rel="noopener noreferrer">Pglet website GitHub repository</a></li><li><a href="https://github.com/pglet/website/tree/master/functions/api" target="_blank" rel="noopener noreferrer"><code>functions/api</code> directory with server-side logic</a></li><li><a href="https://github.com/pglet/website/blob/master/src/components/signup-form.js" target="_blank" rel="noopener noreferrer"><code>&lt;SignupForm/&gt;</code> React component</a></li></ul><h2 class="anchor anchorWithHideOnScrollNavbar_R0VQ" id="email-signup-form">Email signup form<a class="hash-link" href="#email-signup-form" title="Direct link to heading">​</a></h2><p>Signup form is implemented as a React component and includes an email entry form with hCaptcha and two messages:</p><p style="text-align:center"><img src="/img/blog/email-signup-form/email-signup-form.png" width="70%"></p><p>The official <a href="https://codesandbox.io/s/react-hcaptchaform-example-invisible-f7ekt?file=/src/Form.jsx" target="_blank" rel="noopener noreferrer">hCaptcha demo React app</a> with invisible captcha was a perfect starting point for making our own Docusaurus component.</p><p>Add hCaptcha component to your project:</p><div class="codeBlockContainer_I0IT theme-code-block"><div class="codeBlockContent_wNvx"><pre tabindex="0" class="prism-code language-text codeBlock_jd64 thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_mRuA"><span class="token-line" style="color:#393A34"><span class="token plain">yarn add @hcaptcha/react-hcaptcha --save</span><br></span></code></pre><button type="button" aria-label="Copy code to clipboard" class="copyButton_wuS7 clean-btn">Copy</button></div></div><p>Create <code>src/components/signup-form.js</code> with the following contents:</p><div class="codeBlockContainer_I0IT language-javascript theme-code-block"><div class="codeBlockContent_wNvx javascript"><pre tabindex="0" class="prism-code language-javascript codeBlock_jd64 thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_mRuA"><span class="token-line" style="color:#393A34"><span class="token keyword module" style="color:#00009f">import</span><span class="token plain"> </span><span class="token imports maybe-class-name">React</span><span class="token imports punctuation" style="color:#393A34">,</span><span class="token imports"> </span><span class="token imports punctuation" style="color:#393A34">{</span><span class="token imports"> useEffect</span><span class="token imports punctuation" style="color:#393A34">,</span><span class="token imports"> useRef</span><span class="token imports punctuation" style="color:#393A34">,</span><span class="token imports"> useState </span><span class="token imports punctuation" style="color:#393A34">}</span><span class="token plain"> </span><span class="token keyword module" style="color:#00009f">from</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"react"</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword module" style="color:#00009f">import</span><span class="token plain"> </span><span class="token imports maybe-class-name">BrowserOnly</span><span class="token plain"> </span><span class="token keyword module" style="color:#00009f">from</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'@docusaurus/BrowserOnly'</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword module" style="color:#00009f">import</span><span class="token plain"> </span><span class="token imports maybe-class-name">HCaptcha</span><span class="token plain"> </span><span class="token keyword module" style="color:#00009f">from</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"@hcaptcha/react-hcaptcha"</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword module" style="color:#00009f">export</span><span class="token plain"> </span><span class="token keyword module" style="color:#00009f">default</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">function</span><span class="token plain"> </span><span class="token function maybe-class-name" style="color:#d73a49">SignupForm</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">[</span><span class="token plain">token</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> setToken</span><span class="token punctuation" style="color:#393A34">]</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">useState</span><span class="token punctuation" style="color:#393A34">(</span><span class="token keyword null nil" style="color:#00009f">null</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">[</span><span class="token plain">email</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> setEmail</span><span class="token punctuation" style="color:#393A34">]</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">useState</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">""</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> captchaRef </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">useRef</span><span class="token punctuation" style="color:#393A34">(</span><span class="token keyword null nil" style="color:#00009f">null</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> </span><span class="token function-variable function" style="color:#d73a49">onSubmit</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token parameter">event</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token arrow operator" style="color:#393A34">=&gt;</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        event</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">preventDefault</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        captchaRef</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">current</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">execute</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token function" style="color:#d73a49">useEffect</span><span class="token punctuation" style="color:#393A34">(</span><span class="token keyword" style="color:#00009f">async</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token arrow operator" style="color:#393A34">=&gt;</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token keyword control-flow" style="color:#00009f">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">token</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">            </span><span class="token keyword" style="color:#00009f">var</span><span class="token plain"> data </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">                email</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> email</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">                captchaToken</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> token</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">            </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">            </span><span class="token comment" style="color:#999988;font-style:italic">// send message</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">            </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> response </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword control-flow" style="color:#00009f">await</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">fetch</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"/api/email-signup"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">                method</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'POST'</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">                headers</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'Content-Type'</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'application/json'</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">                body</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token known-class-name class-name">JSON</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">stringify</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">data</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">              </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">[</span><span class="token plain">token</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> email</span><span class="token punctuation" style="color:#393A34">]</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword control-flow" style="color:#00009f">return</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token operator" style="color:#393A34">&lt;</span><span class="token plain">div id</span><span class="token operator" style="color:#393A34">=</span><span class="token string" style="color:#e3116c">"signup"</span><span class="token plain"> className</span><span class="token operator" style="color:#393A34">=</span><span class="token string" style="color:#e3116c">"signup-form"</span><span class="token operator" style="color:#393A34">&gt;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">            </span><span class="token operator" style="color:#393A34">&lt;</span><span class="token maybe-class-name">BrowserOnly</span><span class="token plain"> fallback</span><span class="token operator" style="color:#393A34">=</span><span class="token punctuation" style="color:#393A34">{</span><span class="token operator" style="color:#393A34">&lt;</span><span class="token plain">div</span><span class="token operator" style="color:#393A34">&gt;</span><span class="token maybe-class-name">Loading</span><span class="token spread operator" style="color:#393A34">...</span><span class="token operator" style="color:#393A34">&lt;</span><span class="token operator" style="color:#393A34">/</span><span class="token plain">div</span><span class="token operator" style="color:#393A34">&gt;</span><span class="token punctuation" style="color:#393A34">}</span><span class="token operator" style="color:#393A34">&gt;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">                </span><span class="token punctuation" style="color:#393A34">{</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token arrow operator" style="color:#393A34">=&gt;</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">                    </span><span class="token keyword control-flow" style="color:#00009f">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">token</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">                        </span><span class="token comment" style="color:#999988;font-style:italic">// signup submitted</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">                        </span><span class="token keyword control-flow" style="color:#00009f">return</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">&lt;</span><span class="token plain">div</span><span class="token operator" style="color:#393A34">&gt;</span><span class="token maybe-class-name">Thank</span><span class="token plain"> you</span><span class="token operator" style="color:#393A34">!</span><span class="token plain"> </span><span class="token maybe-class-name">You</span><span class="token plain"> will receive the confirmation email shortly</span><span class="token punctuation" style="color:#393A34">.</span><span class="token operator" style="color:#393A34">&lt;</span><span class="token operator" style="color:#393A34">/</span><span class="token plain">div</span><span class="token operator" style="color:#393A34">&gt;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">                    </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"> </span><span class="token keyword control-flow" style="color:#00009f">else</span><span class="token plain"> </span><span class="token keyword control-flow" style="color:#00009f">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token dom variable" style="color:#36acaa">window</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">location</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">href</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">endsWith</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">'?signup-confirmed'</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">                        </span><span class="token comment" style="color:#999988;font-style:italic">// signup confirmed</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">                        </span><span class="token keyword control-flow" style="color:#00009f">return</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">&lt;</span><span class="token plain">div</span><span class="token operator" style="color:#393A34">&gt;</span><span class="token operator" style="color:#393A34">&lt;</span><span class="token plain">span style</span><span class="token operator" style="color:#393A34">=</span><span class="token punctuation" style="color:#393A34">{</span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain">fontSize</span><span class="token operator" style="color:#393A34">:</span><span class="token string" style="color:#e3116c">'25px'</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> marginRight</span><span class="token operator" style="color:#393A34">:</span><span class="token string" style="color:#e3116c">'10px'</span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">}</span><span class="token operator" style="color:#393A34">&gt;</span><span class="token plain">🎉</span><span class="token operator" style="color:#393A34">&lt;</span><span class="token operator" style="color:#393A34">/</span><span class="token plain">span</span><span class="token operator" style="color:#393A34">&gt;</span><span class="token maybe-class-name">Congratulations</span><span class="token operator" style="color:#393A34">!</span><span class="token plain"> </span><span class="token maybe-class-name">You</span><span class="token plain"> have successfully subscribed to </span><span class="token maybe-class-name">Pglet</span><span class="token plain"> newsletter</span><span class="token punctuation" style="color:#393A34">.</span><span class="token operator" style="color:#393A34">&lt;</span><span class="token operator" style="color:#393A34">/</span><span class="token plain">div</span><span class="token operator" style="color:#393A34">&gt;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">                    </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"> </span><span class="token keyword control-flow" style="color:#00009f">else</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">                        </span><span class="token comment" style="color:#999988;font-style:italic">// signup form</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">                        </span><span class="token keyword control-flow" style="color:#00009f">return</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">&lt;</span><span class="token plain">form onSubmit</span><span class="token operator" style="color:#393A34">=</span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain">onSubmit</span><span class="token punctuation" style="color:#393A34">}</span><span class="token operator" style="color:#393A34">&gt;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">                            </span><span class="token operator" style="color:#393A34">&lt;</span><span class="token plain">h3</span><span class="token operator" style="color:#393A34">&gt;</span><span class="token maybe-class-name">Subscribe</span><span class="token plain"> to </span><span class="token maybe-class-name">Pglet</span><span class="token plain"> newsletter </span><span class="token keyword control-flow" style="color:#00009f">for</span><span class="token plain"> project updates and tutorials</span><span class="token operator" style="color:#393A34">!</span><span class="token operator" style="color:#393A34">&lt;</span><span class="token operator" style="color:#393A34">/</span><span class="token plain">h3</span><span class="token operator" style="color:#393A34">&gt;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">                            </span><span class="token operator" style="color:#393A34">&lt;</span><span class="token plain">input</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">                                type</span><span class="token operator" style="color:#393A34">=</span><span class="token string" style="color:#e3116c">"email"</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">                                value</span><span class="token operator" style="color:#393A34">=</span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain">email</span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">                                placeholder</span><span class="token operator" style="color:#393A34">=</span><span class="token string" style="color:#e3116c">"Your email address"</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">                                onChange</span><span class="token operator" style="color:#393A34">=</span><span class="token punctuation" style="color:#393A34">{</span><span class="token punctuation" style="color:#393A34">(</span><span class="token parameter">evt</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token arrow operator" style="color:#393A34">=&gt;</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">setEmail</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">evt</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">target</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">value</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">                            </span><span class="token operator" style="color:#393A34">/</span><span class="token operator" style="color:#393A34">&gt;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">                            </span><span class="token operator" style="color:#393A34">&lt;</span><span class="token plain">input type</span><span class="token operator" style="color:#393A34">=</span><span class="token string" style="color:#e3116c">"submit"</span><span class="token plain"> value</span><span class="token operator" style="color:#393A34">=</span><span class="token string" style="color:#e3116c">"Submit"</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">/</span><span class="token operator" style="color:#393A34">&gt;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">                            </span><span class="token operator" style="color:#393A34">&lt;</span><span class="token maybe-class-name">HCaptcha</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">                                sitekey</span><span class="token operator" style="color:#393A34">=</span><span class="token string" style="color:#e3116c">"{YOUR-HCAPTCHA-SITE-KEY}"</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">                                size</span><span class="token operator" style="color:#393A34">=</span><span class="token string" style="color:#e3116c">"invisible"</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">                                onVerify</span><span class="token operator" style="color:#393A34">=</span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain">setToken</span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">                                ref</span><span class="token operator" style="color:#393A34">=</span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain">captchaRef</span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">                            </span><span class="token operator" style="color:#393A34">/</span><span class="token operator" style="color:#393A34">&gt;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">                        </span><span class="token operator" style="color:#393A34">&lt;</span><span class="token operator" style="color:#393A34">/</span><span class="token plain">form</span><span class="token operator" style="color:#393A34">&gt;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">                    </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">                </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">            </span><span class="token operator" style="color:#393A34">&lt;</span><span class="token operator" style="color:#393A34">/</span><span class="token maybe-class-name">BrowserOnly</span><span class="token operator" style="color:#393A34">&gt;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token operator" style="color:#393A34">&lt;</span><span class="token operator" style="color:#393A34">/</span><span class="token plain">div</span><span class="token operator" style="color:#393A34">&gt;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><br></span></code></pre><button type="button" aria-label="Copy code to clipboard" class="copyButton_wuS7 clean-btn">Copy</button></div></div><p>It's simply <code>&lt;form&gt;</code> element with "email" and "submit" inputs - except hCaptcha, no other 3rd-party components or hooks were used. </p><p>Replace <code>{YOUR-HCAPTCHA-SITE-KEY}</code> with your own hCaptcha site key.</p><p>Captcha is verified on <code>form.onSubmit</code> event which supports submitting form with ENTER and triggers built-in form validators.
The result of captcha verification is stored in <code>token</code> state variable which is sent to <code>/api/email-signup</code> server function along with entered email for further verification and processing.</p><p>Add <code>signup-form.js</code> component to <code>src/pages/index.js</code> page:</p><div class="codeBlockContainer_I0IT language-js theme-code-block"><div class="codeBlockContent_wNvx js"><pre tabindex="0" class="prism-code language-js codeBlock_jd64 thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_mRuA"><span class="token-line" style="color:#393A34"><span class="token keyword module" style="color:#00009f">import</span><span class="token plain"> </span><span class="token imports maybe-class-name">SignupForm</span><span class="token plain"> </span><span class="token keyword module" style="color:#00009f">from</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'@site/src/components/signup-form'</span><br></span></code></pre><button type="button" aria-label="Copy code to clipboard" class="copyButton_wuS7 clean-btn">Copy</button></div></div><p>and then put <code>&lt;SignupForm/&gt;</code> inside <code>&lt;main&gt;</code> element:</p><div class="codeBlockContainer_I0IT language-javascript theme-code-block"><div class="codeBlockContent_wNvx javascript"><pre tabindex="0" class="prism-code language-javascript codeBlock_jd64 thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_mRuA"><span class="token-line" style="color:#393A34"><span class="token operator" style="color:#393A34">&lt;</span><span class="token plain">main</span><span class="token operator" style="color:#393A34">&gt;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token operator" style="color:#393A34">&lt;</span><span class="token maybe-class-name">SignupForm</span><span class="token operator" style="color:#393A34">/</span><span class="token operator" style="color:#393A34">&gt;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token spread operator" style="color:#393A34">...</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token operator" style="color:#393A34">&lt;</span><span class="token operator" style="color:#393A34">/</span><span class="token plain">main</span><span class="token operator" style="color:#393A34">&gt;</span><br></span></code></pre><button type="button" aria-label="Copy code to clipboard" class="copyButton_wuS7 clean-btn">Copy</button></div></div><p>When you run Docusaurus site with <code>yarn start</code> and navigate to a page with captcha at http://localhost:3000 you'll get "blocked by CORS policy" JavaScript errors. To make captcha work locally you should browse with a domain instead of "localhost".</p><p>Add a new mapping <code>127.0.0.1  mysite.local</code> to <code>sudo nano /private/etc/hosts</code> and then you can open <a href="http://mysite.local:3000" target="_blank" rel="noopener noreferrer">http://mysite.local:3000</a> with working captcha.</p><div class="admonition admonition-note alert alert--secondary"><div class="admonition-heading"><h5><span class="admonition-icon"><svg xmlns="http://www.w3.org/2000/svg" width="14" height="16" viewBox="0 0 14 16"><path fill-rule="evenodd" d="M6.3 5.69a.942.942 0 0 1-.28-.7c0-.28.09-.52.28-.7.19-.18.42-.28.7-.28.28 0 .52.09.7.28.18.19.28.42.28.7 0 .28-.09.52-.28.7a1 1 0 0 1-.7.3c-.28 0-.52-.11-.7-.3zM8 7.99c-.02-.25-.11-.48-.31-.69-.2-.19-.42-.3-.69-.31H6c-.27.02-.48.13-.69.31-.2.2-.3.44-.31.69h1v3c.02.27.11.5.31.69.2.2.42.31.69.31h1c.27 0 .48-.11.69-.31.2-.19.3-.42.31-.69H8V7.98v.01zM7 2.3c-3.14 0-5.7 2.54-5.7 5.68 0 3.14 2.56 5.7 5.7 5.7s5.7-2.55 5.7-5.7c0-3.15-2.56-5.69-5.7-5.69v.01zM7 .98c3.86 0 7 3.14 7 7s-3.14 7-7 7-7-3.12-7-7 3.14-7 7-7z"></path></svg></span>note</h5></div><div class="admonition-content"><p>A part of form component is wrapped with <code>&lt;BrowserOnly&gt;</code> element which tells Docusaurus that the contents inside <code>&lt;BrowserOnly&gt;</code> is not suitable for server-side rendering because of client-side API used, in our case <code>window.location.ref</code>. You can read more about <code>&lt;BrowserOnly&gt;</code> <a href="https://docusaurus.io/docs/docusaurus-core#browseronly" target="_blank" rel="noopener noreferrer">here</a>.</p></div></div><h2 class="anchor anchorWithHideOnScrollNavbar_R0VQ" id="configuring-mailgun">Configuring Mailgun<a class="hash-link" href="#configuring-mailgun" title="Direct link to heading">​</a></h2><p><a href="https://www.mailgun.com/" target="_blank" rel="noopener noreferrer">Mailgun</a> is a transactional email service that offers first-class APIs for sending, receiving and tracking email messages.</p><div class="admonition admonition-note alert alert--secondary"><div class="admonition-heading"><h5><span class="admonition-icon"><svg xmlns="http://www.w3.org/2000/svg" width="14" height="16" viewBox="0 0 14 16"><path fill-rule="evenodd" d="M6.3 5.69a.942.942 0 0 1-.28-.7c0-.28.09-.52.28-.7.19-.18.42-.28.7-.28.28 0 .52.09.7.28.18.19.28.42.28.7 0 .28-.09.52-.28.7a1 1 0 0 1-.7.3c-.28 0-.52-.11-.7-.3zM8 7.99c-.02-.25-.11-.48-.31-.69-.2-.19-.42-.3-.69-.31H6c-.27.02-.48.13-.69.31-.2.2-.3.44-.31.69h1v3c.02.27.11.5.31.69.2.2.42.31.69.31h1c.27 0 .48-.11.69-.31.2-.19.3-.42.31-.69H8V7.98v.01zM7 2.3c-3.14 0-5.7 2.54-5.7 5.68 0 3.14 2.56 5.7 5.7 5.7s5.7-2.55 5.7-5.7c0-3.15-2.56-5.69-5.7-5.69v.01zM7 .98c3.86 0 7 3.14 7 7s-3.14 7-7 7-7-3.12-7-7 3.14-7 7-7z"></path></svg></span>note</h5></div><div class="admonition-content"><p>We are not affiliated with Mailgun - we just like their service and have a lot of experience with it.</p></div></div><p>Some advice before creating a mailing list in Mailgun:</p><ul><li><strong>Start with a free "Flex" plan</strong> - it allows sending 5,000 messages per month and includes custom domains.</li><li><strong>Configure custom domain</strong> - of course, you can test everything on a built-in <code>{something}.mailgun.org</code> domain, but messages sent from it will be trapped in recipient's Junk folder. Custom domain is included with a free plan and setting it up is just a matter of adding a few records to your DNS zone.</li><li><strong>Get dedicated IP address</strong> - if you require even greater email deliverability, assign your domain to a dedicated IP address. Dedicated IP is part of <a href="https://www.mailgun.com/pricing/" target="_blank" rel="noopener noreferrer">"Foundation" plan</a> which starts at $35/month.</li></ul><h2 class="anchor anchorWithHideOnScrollNavbar_R0VQ" id="cloudflare-pages-functions">Cloudflare Pages Functions<a class="hash-link" href="#cloudflare-pages-functions" title="Direct link to heading">​</a></h2><p><a href="https://developers.cloudflare.com/pages/platform/functions" target="_blank" rel="noopener noreferrer">Cloudflare Page Functions</a> are based on <a href="https://developers.cloudflare.com/workers/" target="_blank" rel="noopener noreferrer">Cloudflare Workers</a>.</p><p>Be aware that Functions runtime environment is different from Node.js - you can't use Node.js built-in modules, you can't install anything from NPM. It's more like JavaScript in a headless browser with <a href="https://developer.mozilla.org/en-US/docs/Web/API/fetch" target="_blank" rel="noopener noreferrer"><code>fetch()</code></a>, <a href="https://developer.mozilla.org/en-US/docs/Web/API/WebSocket" target="_blank" rel="noopener noreferrer">WebSocket</a>, <a href="https://developer.mozilla.org/en-US/docs/Web/API/Crypto" target="_blank" rel="noopener noreferrer">Crypto</a> and other <a href="https://developers.cloudflare.com/workers/runtime-apis/web-standards" target="_blank" rel="noopener noreferrer">Web APIs</a>.</p><p>For signup form, we are going to add two functions:</p><ul><li><code>POST /api/email-signup</code> - for initial form processing and signup</li><li><code>GET /api/confirm-subscription?email={email}&amp;code={code}</code> - for confirming subscription</li></ul><p>To generate routes above, we need to create two files: <code>/functions/api/email-signup.js</code> and <code>/functions/api/confirm-subscription.js</code> in the project repository.</p><div class="admonition admonition-caution alert alert--warning"><div class="admonition-heading"><h5><span class="admonition-icon"><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path fill-rule="evenodd" d="M8.893 1.5c-.183-.31-.52-.5-.887-.5s-.703.19-.886.5L.138 13.499a.98.98 0 0 0 0 1.001c.193.31.53.501.886.501h13.964c.367 0 .704-.19.877-.5a1.03 1.03 0 0 0 .01-1.002L8.893 1.5zm.133 11.497H6.987v-2.003h2.039v2.003zm0-3.004H6.987V5.987h2.039v4.006z"></path></svg></span>caution</h5></div><div class="admonition-content"><p><code>/functions</code> directory must be in the root of your repository, not in <code>/static</code> directory, and must be published along with the site.</p></div></div><p>You can glance through <a href="https://developers.cloudflare.com/pages/platform/functions" target="_blank" rel="noopener noreferrer">Functions docs</a> to become familiar with the technology. Here I'll only cover some tricky issues which could arise while you develop.</p><p>First, it's possible to run and debug your functions locally. A beta version of <a href="https://developers.cloudflare.com/pages/platform/functions#develop-and-preview-locally" target="_blank" rel="noopener noreferrer"><code>Wrangler</code></a> tool should be installed for that:</p><div class="codeBlockContainer_I0IT theme-code-block"><div class="codeBlockContent_wNvx"><pre tabindex="0" class="prism-code language-text codeBlock_jd64 thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_mRuA"><span class="token-line" style="color:#393A34"><span class="token plain">yarn add wrangler@beta --save-dev</span><br></span></code></pre><button type="button" aria-label="Copy code to clipboard" class="copyButton_wuS7 clean-btn">Copy</button></div></div><div class="admonition admonition-caution alert alert--warning"><div class="admonition-heading"><h5><span class="admonition-icon"><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path fill-rule="evenodd" d="M8.893 1.5c-.183-.31-.52-.5-.887-.5s-.703.19-.886.5L.138 13.499a.98.98 0 0 0 0 1.001c.193.31.53.501.886.501h13.964c.367 0 .704-.19.877-.5a1.03 1.03 0 0 0 .01-1.002L8.893 1.5zm.133 11.497H6.987v-2.003h2.039v2.003zm0-3.004H6.987V5.987h2.039v4.006z"></path></svg></span>caution</h5></div><div class="admonition-content"><p>Disregard scary deprecation warning while looking for <a href="https://www.npmjs.com/package/wrangler" target="_blank" rel="noopener noreferrer">wrangler package</a> on npmjs.com and don't install <code>@cloudflare/wrangler</code> as it suggests.
Apparently, Cloudflare team is actively <a href="https://github.com/cloudflare/wrangler2" target="_blank" rel="noopener noreferrer">working on Wrangler v2</a> and publishes it as <code>wrangler</code> package.</p></div></div><p>Run Wrangler as a proxy for your local Docusaurus run:</p><div class="codeBlockContainer_I0IT theme-code-block"><div class="codeBlockContent_wNvx"><pre tabindex="0" class="prism-code language-text codeBlock_jd64 thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_mRuA"><span class="token-line" style="color:#393A34"><span class="token plain">npx wrangler pages dev -- yarn start</span><br></span></code></pre><button type="button" aria-label="Copy code to clipboard" class="copyButton_wuS7 clean-btn">Copy</button></div></div><p>For configurable settings in functions we use environment variables. In contrast with Cloudflare Workers, environment variables are not set as globals in your functions, however they can be accessed via handler's <code>context</code>, like that:</p><div class="codeBlockContainer_I0IT language-javascript theme-code-block"><div class="codeBlockContent_wNvx javascript"><pre tabindex="0" class="prism-code language-javascript codeBlock_jd64 thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_mRuA"><span class="token-line" style="color:#393A34"><span class="token comment" style="color:#999988;font-style:italic">// handler function</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword module" style="color:#00009f">export</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">async</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">function</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">onRequestPost</span><span class="token punctuation" style="color:#393A34">(</span><span class="token parameter">context</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> request</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> env </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> context</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> apiKey </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> env</span><span class="token punctuation" style="color:#393A34">.</span><span class="token constant" style="color:#36acaa">API_KEY</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><br></span></code></pre><button type="button" aria-label="Copy code to clipboard" class="copyButton_wuS7 clean-btn">Copy</button></div></div><p>where <code>API_KEY</code> is the name of environment variable.</p><p>For Workers environment variables can be configured in <code>wrangler.toml</code>, but <code>wrangler.toml</code> is not supported by Functions, so the only way to test with environment variables locally is to pass them via command line with <code>-b</code> switch:</p><div class="codeBlockContainer_I0IT theme-code-block"><div class="codeBlockContent_wNvx"><pre tabindex="0" class="prism-code language-text codeBlock_jd64 thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_mRuA"><span class="token-line" style="color:#393A34"><span class="token plain">npx wrangler pages dev -b API_KEY=123! -b MY_VAR2=some_value ... -- yarn start</span><br></span></code></pre><button type="button" aria-label="Copy code to clipboard" class="copyButton_wuS7 clean-btn">Copy</button></div></div><p>For your Cloudflare Pages website, you can configure <code>Production</code> and <code>Preview</code> environment variables on <strong>Settings <!-- -->→<!-- --> Environment variables</strong> page:</p><p style="text-align:center"><img src="/img/blog/email-signup-form/cloudflare-pages-environment-variables.png" width="80%"></p><div class="admonition admonition-info alert alert--info"><div class="admonition-heading"><h5><span class="admonition-icon"><svg xmlns="http://www.w3.org/2000/svg" width="14" height="16" viewBox="0 0 14 16"><path fill-rule="evenodd" d="M7 2.3c3.14 0 5.7 2.56 5.7 5.7s-2.56 5.7-5.7 5.7A5.71 5.71 0 0 1 1.3 8c0-3.14 2.56-5.7 5.7-5.7zM7 1C3.14 1 0 4.14 0 8s3.14 7 7 7 7-3.14 7-7-3.14-7-7-7zm1 3H6v5h2V4zm0 6H6v2h2v-2z"></path></svg></span>info</h5></div><div class="admonition-content"><p>Environment variables are immutable. If you update/add/delete environment variable and then call the function using it again, it won't work - once variables have changed, the <strong>website must be re-built to pick up new values</strong>.</p></div></div><div class="admonition admonition-danger alert alert--danger"><div class="admonition-heading"><h5><span class="admonition-icon"><svg xmlns="http://www.w3.org/2000/svg" width="12" height="16" viewBox="0 0 12 16"><path fill-rule="evenodd" d="M5.05.31c.81 2.17.41 3.38-.52 4.31C3.55 5.67 1.98 6.45.9 7.98c-1.45 2.05-1.7 6.53 3.53 7.7-2.2-1.16-2.67-4.52-.3-6.61-.61 2.03.53 3.33 1.94 2.86 1.39-.47 2.3.53 2.27 1.67-.02.78-.31 1.44-1.13 1.81 3.42-.59 4.78-3.42 4.78-5.56 0-2.84-2.53-3.22-1.25-5.61-1.52.13-2.03 1.13-1.89 2.75.09 1.08-1.02 1.8-1.86 1.33-.67-.41-.66-1.19-.06-1.78C8.18 5.31 8.68 2.45 5.05.32L5.03.3l.02.01z"></path></svg></span>danger</h5></div><div class="admonition-content"><p>Do not put real secrets into "Preview" environment variables if your project in a public repository. Any pull request to the repository publishes "preview" website to a temp URL which is visible to everyone in <a href="https://github.com/pglet/website/runs/4754500508" target="_blank" rel="noopener noreferrer">commit status</a>. Therefore, it's possible for the attacker to submit malicious PR with a function printing all environment variables and then run it via temp URL.</p></div></div><h2 class="anchor anchorWithHideOnScrollNavbar_R0VQ" id="form-submit-handler">Form submit handler<a class="hash-link" href="#form-submit-handler" title="Direct link to heading">​</a></h2><p>Email signup form <code>POST</code>s entered email and hCaptcha response to <a href="https://github.com/pglet/website/blob/master/functions/api/email-signup.js" target="_blank" rel="noopener noreferrer"><code>/api/email-signup</code></a> function, which performs the following:</p><ol><li>Parses request body as JSON and validates its <code>email</code> and <code>captchaToken</code> fields.</li><li>Performs hCaptcha response validation and aborts the request if validation fails.</li><li>Tries adding a new email (member) into Mailgun mailing list and exits if it's already added.</li><li>Sends email with confirmation link via Mailgun to a newly added email address.</li></ol><h2 class="anchor anchorWithHideOnScrollNavbar_R0VQ" id="validating-hcaptcha-response">Validating hCaptcha response<a class="hash-link" href="#validating-hcaptcha-response" title="Direct link to heading">​</a></h2><p><a href="https://docs.hcaptcha.com/#verify-the-user-response-server-side" target="_blank" rel="noopener noreferrer">Validating hCaptcha response on the server</a> is just a <code>POST</code> request to <code>https://hcaptcha.com/siteverify</code> with hCaptcha response received from browser and hCaptcha site key secret in the body:</p><div class="codeBlockContainer_I0IT language-javascript theme-code-block"><div class="codeBlockContent_wNvx javascript"><pre tabindex="0" class="prism-code language-javascript codeBlock_jd64 thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_mRuA"><span class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">async</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">function</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">validateCaptcha</span><span class="token punctuation" style="color:#393A34">(</span><span class="token parameter">token</span><span class="token parameter punctuation" style="color:#393A34">,</span><span class="token parameter"> secret</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> data </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    response</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> token</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    secret</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> secret</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> encData </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">urlEncodeObject</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">data</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> captchaResponse </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword control-flow" style="color:#00009f">await</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">fetch</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token template-string template-punctuation string" style="color:#e3116c">`</span><span class="token template-string string" style="color:#e3116c">https://hcaptcha.com/siteverify</span><span class="token template-string template-punctuation string" style="color:#e3116c">`</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">      method</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'POST'</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">      headers</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token string" style="color:#e3116c">'Content-Type'</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'application/x-www-form-urlencoded'</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token string" style="color:#e3116c">'Content-Length'</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> encData</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">length</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">toString</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">      body</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> encData</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> captchaBody </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword control-flow" style="color:#00009f">await</span><span class="token plain"> captchaResponse</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">json</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword control-flow" style="color:#00009f">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token operator" style="color:#393A34">!</span><span class="token plain">captchaBody</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">success</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword control-flow" style="color:#00009f">throw</span><span class="token plain"> captchaBody</span><span class="token punctuation" style="color:#393A34">[</span><span class="token string" style="color:#e3116c">"error-codes"</span><span class="token punctuation" style="color:#393A34">]</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><br></span></code></pre><button type="button" aria-label="Copy code to clipboard" class="copyButton_wuS7 clean-btn">Copy</button></div></div><p>Thanks to <a href="https://github.com/sitepoint-editors/cloudflare-form-service/blob/master/email-service.js" target="_blank" rel="noopener noreferrer">this great example</a> on how to send a form request with <code>fetch()</code> method.</p><h2 class="anchor anchorWithHideOnScrollNavbar_R0VQ" id="adding-email-to-a-mailing-list">Adding email to a mailing list<a class="hash-link" href="#adding-email-to-a-mailing-list" title="Direct link to heading">​</a></h2><p>In <code>utils.js</code> we implemented a helper method for calling <a href="https://documentation.mailgun.com/en/latest/api_reference.html" target="_blank" rel="noopener noreferrer">Mailgun API</a>:</p><div class="codeBlockContainer_I0IT language-javascript theme-code-block"><div class="codeBlockContent_wNvx javascript"><pre tabindex="0" class="prism-code language-javascript codeBlock_jd64 thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_mRuA"><span class="token-line" style="color:#393A34"><span class="token keyword module" style="color:#00009f">export</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">function</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">callMailgunApi</span><span class="token punctuation" style="color:#393A34">(</span><span class="token parameter">mailgunApiKey</span><span class="token parameter punctuation" style="color:#393A34">,</span><span class="token parameter"> method</span><span class="token parameter punctuation" style="color:#393A34">,</span><span class="token parameter"> url</span><span class="token parameter punctuation" style="color:#393A34">,</span><span class="token parameter"> data</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> encData </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">urlEncodeObject</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">data</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword control-flow" style="color:#00009f">return</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">fetch</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">      url</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        method</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> method</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        headers</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">          </span><span class="token maybe-class-name">Authorization</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'Basic '</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">+</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">btoa</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">'api:'</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">+</span><span class="token plain"> mailgunApiKey</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">          </span><span class="token string" style="color:#e3116c">'Content-Type'</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'application/x-www-form-urlencoded'</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">          </span><span class="token string" style="color:#e3116c">'Content-Length'</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> encData</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">length</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">toString</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        body</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> encData</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword module" style="color:#00009f">export</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">function</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">urlEncodeObject</span><span class="token punctuation" style="color:#393A34">(</span><span class="token parameter">obj</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword control-flow" style="color:#00009f">return</span><span class="token plain"> </span><span class="token known-class-name class-name">Object</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">keys</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">obj</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">map</span><span class="token punctuation" style="color:#393A34">(</span><span class="token parameter">k</span><span class="token plain"> </span><span class="token arrow operator" style="color:#393A34">=&gt;</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">encodeURIComponent</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">k</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">+</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'='</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">+</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">encodeURIComponent</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">obj</span><span class="token punctuation" style="color:#393A34">[</span><span class="token plain">k</span><span class="token punctuation" style="color:#393A34">]</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">join</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">'&amp;'</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><br></span></code></pre><button type="button" aria-label="Copy code to clipboard" class="copyButton_wuS7 clean-btn">Copy</button></div></div><p>Request parameters are passed in URL-encoded form in the body.</p><p>Requests require Basic authentication header with <code>api</code> and <a href="https://help.mailgun.com/hc/en-us/articles/203380100-Where-Can-I-Find-My-API-Key-and-SMTP-Credentials-" target="_blank" rel="noopener noreferrer">Mailgun primary account API key</a> as username and password respectively.</p><p>With <code>callMailgunApi()</code> helper function adding a new member into Mailgun mailing lists becomes trivial:</p><div class="codeBlockContainer_I0IT language-javascript theme-code-block"><div class="codeBlockContent_wNvx javascript"><pre tabindex="0" class="prism-code language-javascript codeBlock_jd64 thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_mRuA"><span class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">async</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">function</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">addMailingListMember</span><span class="token punctuation" style="color:#393A34">(</span><span class="token parameter">mailgunApiKey</span><span class="token parameter punctuation" style="color:#393A34">,</span><span class="token parameter"> listName</span><span class="token parameter punctuation" style="color:#393A34">,</span><span class="token parameter"> memberAddress</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> data </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    address</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> memberAddress</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    subscribed</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'no'</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    upsert</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'no'</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> response </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword control-flow" style="color:#00009f">await</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">callMailgunApi</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">mailgunApiKey</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token string" style="color:#e3116c">'POST'</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token template-string template-punctuation string" style="color:#e3116c">`</span><span class="token template-string string" style="color:#e3116c">https://api.mailgun.net/v3/lists/</span><span class="token template-string interpolation interpolation-punctuation punctuation" style="color:#393A34">${</span><span class="token template-string interpolation">listName</span><span class="token template-string interpolation interpolation-punctuation punctuation" style="color:#393A34">}</span><span class="token template-string string" style="color:#e3116c">/members</span><span class="token template-string template-punctuation string" style="color:#e3116c">`</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> data</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword control-flow" style="color:#00009f">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">response</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">status</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">===</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">200</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword control-flow" style="color:#00009f">return</span><span class="token plain"> </span><span class="token boolean" style="color:#36acaa">true</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"> </span><span class="token comment" style="color:#999988;font-style:italic">// member has been added</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"> </span><span class="token keyword control-flow" style="color:#00009f">else</span><span class="token plain"> </span><span class="token keyword control-flow" style="color:#00009f">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">response</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">status</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">===</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">400</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword control-flow" style="color:#00009f">return</span><span class="token plain"> </span><span class="token boolean" style="color:#36acaa">false</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"> </span><span class="token comment" style="color:#999988;font-style:italic">// member already added</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"> </span><span class="token keyword control-flow" style="color:#00009f">else</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> responseBody </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword control-flow" style="color:#00009f">await</span><span class="token plain"> response</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">json</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword control-flow" style="color:#00009f">throw</span><span class="token plain"> </span><span class="token template-string template-punctuation string" style="color:#e3116c">`</span><span class="token template-string string" style="color:#e3116c">Error adding mailing list member: </span><span class="token template-string interpolation interpolation-punctuation punctuation" style="color:#393A34">${</span><span class="token template-string interpolation">responseBody</span><span class="token template-string interpolation punctuation" style="color:#393A34">.</span><span class="token template-string interpolation">message</span><span class="token template-string interpolation interpolation-punctuation punctuation" style="color:#393A34">}</span><span class="token template-string template-punctuation string" style="color:#e3116c">`</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><br></span></code></pre><button type="button" aria-label="Copy code to clipboard" class="copyButton_wuS7 clean-btn">Copy</button></div></div><p>It tries to add a new member into mailing list and returns <code>true</code> if it was successfully added; otherwise returns <code>false</code>.</p><h2 class="anchor anchorWithHideOnScrollNavbar_R0VQ" id="sending-confirmation-email">Sending confirmation email<a class="hash-link" href="#sending-confirmation-email" title="Direct link to heading">​</a></h2><p>The function for sending confirmation email message to a user via Mailgun is just a few lines:</p><div class="codeBlockContainer_I0IT language-javascript theme-code-block"><div class="codeBlockContent_wNvx javascript"><pre tabindex="0" class="prism-code language-javascript codeBlock_jd64 thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_mRuA"><span class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">async</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">function</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">sendEmail</span><span class="token punctuation" style="color:#393A34">(</span><span class="token parameter">mailgunApiKey</span><span class="token parameter punctuation" style="color:#393A34">,</span><span class="token parameter"> mailDomain</span><span class="token parameter punctuation" style="color:#393A34">,</span><span class="token parameter"> </span><span class="token parameter keyword module" style="color:#00009f">from</span><span class="token parameter punctuation" style="color:#393A34">,</span><span class="token parameter"> to</span><span class="token parameter punctuation" style="color:#393A34">,</span><span class="token parameter"> subject</span><span class="token parameter punctuation" style="color:#393A34">,</span><span class="token parameter"> htmlBody</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> data </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword module" style="color:#00009f">from</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token keyword module" style="color:#00009f">from</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    to</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> to</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    subject</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> subject</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    html</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> htmlBody</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> response </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword control-flow" style="color:#00009f">await</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">callMailgunApi</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">mailgunApiKey</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token string" style="color:#e3116c">'POST'</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token template-string template-punctuation string" style="color:#e3116c">`</span><span class="token template-string string" style="color:#e3116c">https://api.mailgun.net/v3/</span><span class="token template-string interpolation interpolation-punctuation punctuation" style="color:#393A34">${</span><span class="token template-string interpolation">mailDomain</span><span class="token template-string interpolation interpolation-punctuation punctuation" style="color:#393A34">}</span><span class="token template-string string" style="color:#e3116c">/messages</span><span class="token template-string template-punctuation string" style="color:#e3116c">`</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> data</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain">  </span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword control-flow" style="color:#00009f">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">response</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">status</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">!==</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">200</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> responseBody </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword control-flow" style="color:#00009f">await</span><span class="token plain"> response</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">text</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword control-flow" style="color:#00009f">throw</span><span class="token plain"> </span><span class="token template-string template-punctuation string" style="color:#e3116c">`</span><span class="token template-string string" style="color:#e3116c">Error sending email message: </span><span class="token template-string interpolation interpolation-punctuation punctuation" style="color:#393A34">${</span><span class="token template-string interpolation">responseBody</span><span class="token template-string interpolation interpolation-punctuation punctuation" style="color:#393A34">}</span><span class="token template-string template-punctuation string" style="color:#e3116c">`</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><br></span></code></pre><button type="button" aria-label="Copy code to clipboard" class="copyButton_wuS7 clean-btn">Copy</button></div></div><p>An interesting part here is how confirmation URL is built, which is sent in the message and should be clicked by a user to confirm subscription.</p><p>Confirmation URL contains two parameters: <strong>email</strong> and <strong>confirmation code</strong>. Email is just recipient's email address which is, obviously, not a secret. Confirmation code is calculated as <code>sha1(email + secret)</code>, with <code>secret</code> known to the server only.</p><p>When the server receives a request with email and confirmation code, it calculates a new confirmation code for the received email and compares it with the code from the request.</p><p>The algorithm could be further improved by implementing expiring confirmation code, but we want to keep it simple for now.</p><h2 class="anchor anchorWithHideOnScrollNavbar_R0VQ" id="verifying-email-and-completing-signup-process">Verifying email and completing signup process<a class="hash-link" href="#verifying-email-and-completing-signup-process" title="Direct link to heading">​</a></h2><p><a href="https://github.com/pglet/website/blob/master/functions/api/confirm-subscription.js" target="_blank" rel="noopener noreferrer"><code>/api/confirm-subscription</code></a> function has a single <code>onRequestGet()</code> handler which performs the following:</p><ul><li>Validates <code>email</code> and <code>code</code> request parameters.</li><li>Calculates confirmation code and compares it to the one from the request.</li><li>If both codes match, updates Mailgun mailing list member's <code>subscribed</code> status to <code>yes</code>.</li><li>Redirects to a home page with <code>?signup-confirmed</code> appended to the URL.</li></ul><div class="codeBlockContainer_I0IT language-javascript theme-code-block"><div class="codeBlockContent_wNvx javascript"><pre tabindex="0" class="prism-code language-javascript codeBlock_jd64 thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_mRuA"><span class="token-line" style="color:#393A34"><span class="token keyword module" style="color:#00009f">export</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">async</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">function</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">onRequestGet</span><span class="token punctuation" style="color:#393A34">(</span><span class="token parameter">context</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> request</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> env </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> context</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token comment" style="color:#999988;font-style:italic">// get request params</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> searchParams </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">new</span><span class="token plain"> </span><span class="token class-name">URL</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">request</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">url</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> email </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> searchParams</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">get</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">'email'</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> code </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> searchParams</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">get</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">'code'</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword control-flow" style="color:#00009f">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token operator" style="color:#393A34">!</span><span class="token plain">code </span><span class="token operator" style="color:#393A34">||</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">!</span><span class="token plain">email</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword control-flow" style="color:#00009f">throw</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"Invalid request parameters"</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token comment" style="color:#999988;font-style:italic">// validate confirmation code</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> calculatedCode </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword control-flow" style="color:#00009f">await</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">sha1</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">email </span><span class="token operator" style="color:#393A34">+</span><span class="token plain"> env</span><span class="token punctuation" style="color:#393A34">.</span><span class="token constant" style="color:#36acaa">CONFIRM_SECRET</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword control-flow" style="color:#00009f">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">calculatedCode </span><span class="token operator" style="color:#393A34">!==</span><span class="token plain"> code</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword control-flow" style="color:#00009f">throw</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"Invalid email or confirmation code"</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token comment" style="color:#999988;font-style:italic">// update subscription status</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword control-flow" style="color:#00009f">await</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">subscribeMailingListMember</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">env</span><span class="token punctuation" style="color:#393A34">.</span><span class="token constant" style="color:#36acaa">MAILGUN_API_KEY</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> env</span><span class="token punctuation" style="color:#393A34">.</span><span class="token constant" style="color:#36acaa">MAILGUN_MAILING_LIST</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> email</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token comment" style="color:#999988;font-style:italic">// redirect to a home page</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword control-flow" style="color:#00009f">return</span><span class="token plain"> </span><span class="token maybe-class-name">Response</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">redirect</span><span class="token punctuation" style="color:#393A34">(</span><span class="token keyword" style="color:#00009f">new</span><span class="token plain"> </span><span class="token class-name">URL</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">request</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">url</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">origin</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">+</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"?signup-confirmed"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">302</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><br></span></code></pre><button type="button" aria-label="Copy code to clipboard" class="copyButton_wuS7 clean-btn">Copy</button></div></div><h2 class="anchor anchorWithHideOnScrollNavbar_R0VQ" id="conclusion">Conclusion<a class="hash-link" href="#conclusion" title="Direct link to heading">​</a></h2><p>In this article we created an email signup form for Docusaurus website protected with hCaptcha. The form allows user to submit their email address and subscribe to a project mailing list. We implemented "double opt-in" process, where upon signup an email is sent to the user which includes a link to click and confirm the subscription. We used Cloudflare Pages Functions to implement all server-side logic. Mailgun service was used to send email messages and maintain mailing list.</p><p>In the next article we will build an interactive Python app using <a href="https://pglet.io/docs/tutorials/python" target="_blank" rel="noopener noreferrer">Pglet</a> for sending newsletter to Mailgun mailing lists. Make sure to subscribe to <a href="https://pglet.io/#signup" target="_blank" rel="noopener noreferrer">Pglet mailing list</a> not to miss it!</p>]]></content:encoded>
            <category>Tutorial</category>
            <category>Docusaurus</category>
            <category>React</category>
            <category>hCaptcha</category>
            <category>Cloudflare</category>
            <category>Mailgun</category>
        </item>
        <item>
            <title><![CDATA[Pglet v0.5 release]]></title>
            <link>https://pglet.io/blog/pglet-0-5</link>
            <guid>pglet-0-5</guid>
            <pubDate>Fri, 31 Dec 2021 00:00:00 GMT</pubDate>
            <description><![CDATA[We've just released Pglet v0.5!]]></description>
            <content:encoded><![CDATA[<p>We've just released <a href="https://github.com/pglet/pglet/releases/tag/v0.5.5" target="_blank" rel="noopener noreferrer">Pglet v0.5</a>!</p><p>While working on this release <a href="https://news.ycombinator.com/item?id=29349945" target="_blank" rel="noopener noreferrer">Pglet made HN home page</a>, got included into <a href="https://messaged.com/tldr/" target="_blank" rel="noopener noreferrer">TLDR newsletter</a> and topped <a href="https://github.com/pglet/pglet" target="_blank" rel="noopener noreferrer">500 stars on GitHub</a> (hey, if you enjoy using Pglet give it a ⭐️)! That was exciting - we received great feedback from real users! Many of those comments/requests were incorporated in the release.</p><h2 class="anchor anchorWithHideOnScrollNavbar_R0VQ" id="faster-websocket-based-clients">Faster WebSocket-based clients<a class="hash-link" href="#faster-websocket-based-clients" title="Direct link to heading">​</a></h2><p>Originally, Pglet was using Redis-like text-based protocol to modify web page controls. There was a proxy process running between a client library and Pglet server translating text commands to WebSocket messages understood by the server. A client library was communitcating with a proxy via Unix named pipes (or named pipes on Windows). Such design was chosen because initially Pglet was made to work with Bash and named pipes is what natively supported there. By adding clients for other languages such as <a href="https://github.com/pglet/pglet-python" target="_blank" rel="noopener noreferrer">Python</a> and <a href="https://github.com/pglet/pglet-powershell" target="_blank" rel="noopener noreferrer">C#/PowerShell</a> it became obvious that the layer of named pipes, command parser/translator and proxy process leads to increased complexity.</p><p>With the release of Pglet v0.5, client libraries for Python and C#/PowerShell were re-written to communicate directly with Pglet server via WebSocket protocol. Proxy process is no longer required. As a bonus, when a user application is exiting (CTRL+C), a client library is now sending correct WebSocket closing command to the server, so the app instantly becomes "inactive" and is removed from the server.</p><h3 class="anchor anchorWithHideOnScrollNavbar_R0VQ" id="versioning-for-pglet-and-clients">Versioning for Pglet and clients<a class="hash-link" href="#versioning-for-pglet-and-clients" title="Direct link to heading">​</a></h3><p>Versions of Pglet Server and its clients follow <a href="https://semver.org/" target="_blank" rel="noopener noreferrer">SemVer</a> format: <code>MAJOR.MINOR.PATCH</code>.</p><p><code>MAJOR</code> part of the version will be changed when Pglet Server API changes break backward compatibility. For now it's <code>0</code> meaning there are no drastic changes to the API and it's not mature enough to become <code>1</code>. All client libraries will follow the same <code>MAJOR</code> number.</p><p>Pglet clients could have <code>MINOR</code> version part greater or equal than Pglet server. When the next <code>0.6</code> version of Pglet server is released, all client libraries will be bumped to <code>0.6</code> as well.</p><p><code>PATCH</code> version part is completely independent for Pglet server and all client libraries.</p><h2 class="anchor anchorWithHideOnScrollNavbar_R0VQ" id="license-changed-to-apache-20">License changed to Apache 2.0<a class="hash-link" href="#license-changed-to-apache-20" title="Direct link to heading">​</a></h2><p>It looks like initially licensing Pglet under AGPL was "premature optimization". Yes, we may want to monetize Pglet with a hosted service and AGPL would stop big cloud providers from forking our service and incorporating it into their cloud offerings, but Pglet is very young project and, honestly, right now we don't foresee Microsoft copying it :)</p><p>On the other hand (A)GPL is used to be corporate-unfriendly license which reduces Pglet audience and slows adoption. We want every company using Pglet to build their internal tools!</p><p>So, we listened to your feedback and changed Pglet license to <a href="https://github.com/pglet/pglet/blob/master/LICENSE" target="_blank" rel="noopener noreferrer">Apache License 2.0</a>.</p><h2 class="anchor anchorWithHideOnScrollNavbar_R0VQ" id="local-mode-is-now-default">Local mode is now default<a class="hash-link" href="#local-mode-is-now-default" title="Direct link to heading">​</a></h2><p>Streaming app UI to the web via hosted Pglet service (aka "Web" mode) is a primary use case for Pglet, so in the previous v0.4 release we made "web" mode default. However, after receiving feedback from users, we realized that an app needs to be tested locally before making it public. Therefore, in Pglet v0.5 we are reverting back "local" mode as default. As before Pglet Server is conveniently bundled into all client libraries to make local app development smooth and pleasant.</p><h2 class="anchor anchorWithHideOnScrollNavbar_R0VQ" id="we-need-your-feedback">We need your feedback!<a class="hash-link" href="#we-need-your-feedback" title="Direct link to heading">​</a></h2><p>Give Pglet a try and let us know what you think! If you have a question or feature request please <a href="https://github.com/pglet/pglet/issues" target="_blank" rel="noopener noreferrer">submit an issue</a>, <a href="https://github.com/pglet/pglet/discussions" target="_blank" rel="noopener noreferrer">start a new discussion</a> or <a href="mailto:hello@pglet.io" target="_blank" rel="noopener noreferrer">drop an email</a>.</p><h2 class="anchor anchorWithHideOnScrollNavbar_R0VQ" id="where-does-pglet-go-next">Where does Pglet go next?<a class="hash-link" href="#where-does-pglet-go-next" title="Direct link to heading">​</a></h2><p>This is a brief roadmap for Pglet project:</p><ul><li>PowerShell guide and examples.</li><li>Node.js/Deno client (it's still based on Pglet v0.4), guide and examples.</li><li>Go (Golang) client, guide and examples.</li><li>Pglet hosted service into production:<ul><li>performance optimization,</li><li>storage cleanup,</li><li>CPU/memory profiling,</li><li>metrics,</li><li>monitoring.</li></ul></li></ul><p>Happy New Year! 🎄🤗</p>]]></content:encoded>
            <category>release notes</category>
        </item>
        <item>
            <title><![CDATA[Built-in authentication in a new Pglet release]]></title>
            <link>https://pglet.io/blog/pglet-0-4-6-authentication</link>
            <guid>pglet-0-4-6-authentication</guid>
            <pubDate>Tue, 08 Jun 2021 00:00:00 GMT</pubDate>
            <description><![CDATA[The major feature of Pglet 0.4.6 release is built-in authentication and authorization. Pglet now allows creating protected pages and apps which require users to sign in with any of three methods]]></description>
            <content:encoded><![CDATA[<p>The major feature of Pglet 0.4.6 release is built-in authentication and authorization. Pglet now allows creating protected pages and apps which require users to sign in with any of three methods: GitHub, Google or Microsoft account (Azure AD):</p><p style="text-align:center"><img src="/img/blog/auth/pglet-signin-example.png" width="70%"></p><p>Just imagine, you can instantly add authentication to any of your backend scripts!</p><p>For example, in Python to create a page accessible to GitHub user with username <code>ExampleUser</code> and all users in <code>myorg/Developers</code> team:</p><div class="codeBlockContainer_I0IT language-python theme-code-block"><div class="codeBlockContent_wNvx python"><pre tabindex="0" class="prism-code language-python codeBlock_jd64 thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_mRuA"><span class="token-line" style="color:#393A34"><span class="token plain">page </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> pglet</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">page</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">permissions</span><span class="token operator" style="color:#393A34">=</span><span class="token string" style="color:#e3116c">"github:ExampleUser, github:myorg/Developers"</span><span class="token punctuation" style="color:#393A34">)</span><br></span></code></pre><button type="button" aria-label="Copy code to clipboard" class="copyButton_wuS7 clean-btn">Copy</button></div></div><p>To allow access to specific Azure AD groups/roles you need to specify their full name including tenant ID, for example:</p><div class="codeBlockContainer_I0IT language-python theme-code-block"><div class="codeBlockContent_wNvx python"><pre tabindex="0" class="prism-code language-python codeBlock_jd64 thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_mRuA"><span class="token-line" style="color:#393A34"><span class="token plain">page </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> pglet</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">page</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">permissions</span><span class="token operator" style="color:#393A34">=</span><span class="token string" style="color:#e3116c">"{tenant-guid}/GroupA"</span><span class="token punctuation" style="color:#393A34">)</span><br></span></code></pre><button type="button" aria-label="Copy code to clipboard" class="copyButton_wuS7 clean-btn">Copy</button></div></div><p>To give access to users authenticating with any method and <code>@custom-domain.com</code> email domain:</p><div class="codeBlockContainer_I0IT language-python theme-code-block"><div class="codeBlockContent_wNvx python"><pre tabindex="0" class="prism-code language-python codeBlock_jd64 thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_mRuA"><span class="token-line" style="color:#393A34"><span class="token plain">page </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> pglet</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">page</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">permissions</span><span class="token operator" style="color:#393A34">=</span><span class="token string" style="color:#e3116c">"*@custom-domain.com"</span><span class="token punctuation" style="color:#393A34">)</span><br></span></code></pre><button type="button" aria-label="Copy code to clipboard" class="copyButton_wuS7 clean-btn">Copy</button></div></div><h2 class="anchor anchorWithHideOnScrollNavbar_R0VQ" id="other-changes-and-improvements">Other changes and improvements<a class="hash-link" href="#other-changes-and-improvements" title="Direct link to heading">​</a></h2><h3 class="anchor anchorWithHideOnScrollNavbar_R0VQ" id="web-mode-by-default">Web mode by default<a class="hash-link" href="#web-mode-by-default" title="Direct link to heading">​</a></h3><p>Starting from this relase when you start a new page or multi-user app with default parameters its UI will be streamed to <a href="https://console.pglet.io." target="_blank" rel="noopener noreferrer">https://console.pglet.io.</a> To create a local page add <code>local=True</code> parameter (Python), for example:</p><div class="codeBlockContainer_I0IT language-python theme-code-block"><div class="codeBlockContent_wNvx python"><pre tabindex="0" class="prism-code language-python codeBlock_jd64 thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_mRuA"><span class="token-line" style="color:#393A34"><span class="token plain">page </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> pglet</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">page</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"my-app"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> local</span><span class="token operator" style="color:#393A34">=</span><span class="token boolean" style="color:#36acaa">True</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token comment" style="color:#999988;font-style:italic"># this page will start a local Pglet server</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">page</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">add</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">Text</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">'Hello, localhost!'</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">)</span><br></span></code></pre><button type="button" aria-label="Copy code to clipboard" class="copyButton_wuS7 clean-btn">Copy</button></div></div><h3 class="anchor anchorWithHideOnScrollNavbar_R0VQ" id="consolepgletio">console.pglet.io<a class="hash-link" href="#consolepgletio" title="Direct link to heading">​</a></h3><p>Pglet hosted service was moved from <code>app.pglet.io</code> domain to <code>console.pglet.io</code> to emphasize the fact that Pglet is a secure web "console" where your backend apps can output reach progress.</p><h3 class="anchor anchorWithHideOnScrollNavbar_R0VQ" id="dark-theme-added-and-is-now-default">Dark theme added and is now default<a class="hash-link" href="#dark-theme-added-and-is-now-default" title="Direct link to heading">​</a></h3><p>Pglet provides two built-in themes: <code>dark</code> and <code>light</code> and you can configure custom theme. To set page theme change its <code>theme</code> property, for example:</p><div class="codeBlockContainer_I0IT language-python theme-code-block"><div class="codeBlockContent_wNvx python"><pre tabindex="0" class="prism-code language-python codeBlock_jd64 thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_mRuA"><span class="token-line" style="color:#393A34"><span class="token plain">page </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> pglet</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">page</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"my-app"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">page</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">theme </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'light'</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">page</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">update</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><br></span></code></pre><button type="button" aria-label="Copy code to clipboard" class="copyButton_wuS7 clean-btn">Copy</button></div></div><h3 class="anchor anchorWithHideOnScrollNavbar_R0VQ" id="re-connecting-client">Re-connecting client<a class="hash-link" href="#re-connecting-client" title="Direct link to heading">​</a></h3><p>A serious stabilization work has been done to make Pglet client more resilient to network fluctuations. Now your Pglet app will stay online for many days, reliably connected to Pglet service.</p><h3 class="anchor anchorWithHideOnScrollNavbar_R0VQ" id="python-package-includes-executables">Python package includes executables<a class="hash-link" href="#python-package-includes-executables" title="Direct link to heading">​</a></h3><p><a href="https://pypi.org/project/pglet/" target="_blank" rel="noopener noreferrer">Pglet package</a> now includes <code>pglet</code> executables for all platforms, so they are downloaded during the package installation - that better works for corporate or k8s environments without outbound internet access.</p>]]></content:encoded>
            <category>release</category>
            <category>security</category>
        </item>
        <item>
            <title><![CDATA[Web app to run PowerShell commands on a computer from anywhere]]></title>
            <link>https://pglet.io/blog/web-app-to-run-powershell-commands-on-a-computer-from-anywhere</link>
            <guid>web-app-to-run-powershell-commands-on-a-computer-from-anywhere</guid>
            <pubDate>Fri, 30 Apr 2021 00:00:00 GMT</pubDate>
            <description><![CDATA[After publishing this post on Reddit PowerShell community we received great feedback about security. Currently Pglet service is in preview and is not recommended for use in production. We are working on built-in authentication/authorization functionality at the moment. It's going to be "Login with GitHub/Google/Microsoft" OAuth at first plus OpenID for any other providers.]]></description>
            <content:encoded><![CDATA[<div class="admonition admonition-info alert alert--info"><div class="admonition-heading"><h5><span class="admonition-icon"><svg xmlns="http://www.w3.org/2000/svg" width="14" height="16" viewBox="0 0 14 16"><path fill-rule="evenodd" d="M7 2.3c3.14 0 5.7 2.56 5.7 5.7s-2.56 5.7-5.7 5.7A5.71 5.71 0 0 1 1.3 8c0-3.14 2.56-5.7 5.7-5.7zM7 1C3.14 1 0 4.14 0 8s3.14 7 7 7 7-3.14 7-7-3.14-7-7-7zm1 3H6v5h2V4zm0 6H6v2h2v-2z"></path></svg></span>info</h5></div><div class="admonition-content"><p>After publishing this post on <a href="https://www.reddit.com/r/PowerShell/comments/n22dzm/i_wrote_a_script_that_allows_running_powershell/" target="_blank" rel="noopener noreferrer">Reddit PowerShell community</a> we received great feedback about security. Currently Pglet service is in preview and is not recommended for use in production. We are working on built-in authentication/authorization functionality at the moment. It's going to be "Login with GitHub/Google/Microsoft" OAuth at first plus OpenID for any other providers.</p></div></div><p>Normally, to access computer via PowerShell you need to configure PowerShell remoting, open WinRM ports on firewall and, the most unpleasant part, add NAT rule on your router to expose a computer to the entire Internet.</p><p>So, how can I securely make a web UI for my script without any port opened on the firewall? I used Pglet - a free open-source service for adding remote UI to your scripts. Pglet acts as a relay between your script and a web browser. Your script "dials" the service and sends UI state updates while web users receive live page updates via WebSockets. Kind of Phoenix LiveView for PowerShell :)</p><p>To run the app you need to install <a href="https://www.powershellgallery.com/packages/pglet" target="_blank" rel="noopener noreferrer">pglet module</a>:</p><div class="codeBlockContainer_I0IT theme-code-block"><div class="codeBlockContent_wNvx"><pre tabindex="0" class="prism-code language-text codeBlock_jd64 thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_mRuA"><span class="token-line" style="color:#393A34"><span class="token plain">Install-Module pglet -Scope CurrentUser -Force</span><br></span></code></pre><button type="button" aria-label="Copy code to clipboard" class="copyButton_wuS7 clean-btn">Copy</button></div></div><p>The module works on Windows PowerShell on Windows 10 and PowerShell Core 6+ on Windows, Linux and Mac.</p><p>Here is how the app looks like:</p><div style="text-align:center"><img src="/img/blog/remote-powershell/remote-powershell-console.png" width="70%"></div><p>Below is the entire program (<a href="https://github.com/pglet/examples/blob/main/powershell/remote-console/remote-console.ps1" target="_blank" rel="noopener noreferrer">GitHub</a>):</p><div class="codeBlockContainer_I0IT language-powershell theme-code-block"><div class="codeBlockContent_wNvx powershell"><pre tabindex="0" class="prism-code language-powershell codeBlock_jd64 thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_mRuA"><span class="token-line" style="color:#393A34"><span class="token plain">Import-Module pglet</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">Connect-PgletApp -Name "ps-console/*" -Web -ScriptBlock {</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    $ErrorActionPreference = 'stop'</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    $page = $PGLET_PAGE</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    $page.Title = "PowerShell Remote Console"</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    $page.HorizontalAlign = 'stretch'</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    # Textbox with a command entered</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    $cmd = TextBox -Placeholder "Type PowerShell command and click Run or press ENTER..." -Width '100%'</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    # Event handler to call when "Run" button is clicked or Enter pressed</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    $run_on_click = {</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        $cmd_text = $cmd.value</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        if ([string]::IsNullOrWhitespace($cmd_text)) {</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">            return</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        }</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        # disable textbox and Run button, add spinner while the command is evaluating</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        $cmd.value = ''</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        $command_panel.disabled = $true</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        $results.controls.insert(0, (Text $cmd_text -BgColor 'neutralLight' -Padding 10))</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        $results.controls.insert(1, (Spinner))</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        $page.update()</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        try {</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">            # run the command</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">            $result = Invoke-Expression $cmd_text</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">            # if result is Array present it as Grid; otherwise Text</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">            if ($result -is [System.Array]) {</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">                $result_control = Grid -Compact -Items $result</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">            } else {</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">                $result_control = Text -Value ($result | Out-String) -Pre -Padding 10</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">            }</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        } catch {</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">            $result_control = Text -Value "$_" -Pre -Padding 10 -Color 'red'</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        }</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        # re-enable controls and replace spinner with the results</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        $command_panel.disabled = $false</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        $results.controls.removeAt(1)</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        $results.controls.insert(1, $result_control)</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        $page.update()</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    }</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    # container for command textbox and Run button</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    $command_panel = Stack -Horizontal -OnSubmit $run_on_click -Controls @(</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        $cmd</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        Button -Text "Run" -Primary -Icon 'Play' -OnClick $run_on_click</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    )</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    # results container</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    $results = Stack</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    # "main" view combining all controls together</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    $view = @(</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        $command_panel</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        Stack -Controls @(</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">            Stack -Horizontal -VerticalAlign Center -Controls @(</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">                Text 'Results' -Size large</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">                Button -Icon 'Clear' -Title 'Clear results' -OnClick {</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">                    $results.controls.clear()</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">                    $results.update()</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">                }</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">            )</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">            $results</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        )</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    )</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    # display the "main" view onto the page</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    $page.add($view)</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">}</span><br></span></code></pre><button type="button" aria-label="Copy code to clipboard" class="copyButton_wuS7 clean-btn">Copy</button></div></div><p>In the program I used just a few Pglet controls: Textbox, Button, Spinner and Stack for the layout. Controls are organized into DOM and every time <code>page.update()</code> is called its state is sent to Pglet service.</p><p>When you run the script, a new random URL is generated for your app and printed to the console:</p><div style="text-align:center"><img src="/img/blog/remote-powershell/windows-console-output.png" width="90%"></div><p>At the very top of the program there is main entry point for the app:</p><div class="codeBlockContainer_I0IT language-powershell theme-code-block"><div class="codeBlockContent_wNvx powershell"><pre tabindex="0" class="prism-code language-powershell codeBlock_jd64 thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_mRuA"><span class="token-line" style="color:#393A34"><span class="token plain">Connect-PgletApp -Name "ps-console/*" -Web -ScriptBlock {</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    ...</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">}</span><br></span></code></pre><button type="button" aria-label="Copy code to clipboard" class="copyButton_wuS7 clean-btn">Copy</button></div></div><p><code>-Name</code> parameter is a page URL and <code>*</code> means a randomly-generated string. You can add your own prefix to the random string or use another namespace, e.g. <code>my-pages/ps-example-*</code>.</p><p>If you want to tweak the app and test it locally, remove <code>-Web</code> parameter and a local Pglet server will be started. There is also <a href="https://pglet.io/docs/pglet-server/installation" target="_blank" rel="noopener noreferrer">self-hosted Pglet server</a> if you need more control.</p><p>That's it! More great examples are coming!</p>]]></content:encoded>
            <category>examples</category>
        </item>
        <item>
            <title><![CDATA[Pglet 0.2.3]]></title>
            <link>https://pglet.io/blog/pglet-0-2-3</link>
            <guid>pglet-0-2-3</guid>
            <pubDate>Mon, 01 Mar 2021 00:00:00 GMT</pubDate>
            <description><![CDATA[Pglet 0.2.3 adds more chart controls:]]></description>
            <content:encoded><![CDATA[<p>Pglet 0.2.3 adds more chart controls:</p><ul><li>Pie Chart - <a href="https://repl.it/@pglet/bash-piechart-example" target="_blank" rel="noopener noreferrer">demo</a></li><li>Line Chart - <a href="https://repl.it/@pglet/bash-linechart-example" target="_blank" rel="noopener noreferrer">demo</a></li><li>Horizontal Bar Chart - <a href="https://repl.it/@pglet/bash-barchart-example" target="_blank" rel="noopener noreferrer">demo</a></li></ul><h2 class="anchor anchorWithHideOnScrollNavbar_R0VQ" id="new-controls">New controls<a class="hash-link" href="#new-controls" title="Direct link to heading">​</a></h2><ul><li>Callout - <a href="https://repl.it/@pglet/bash-callout-example" target="_blank" rel="noopener noreferrer">demo</a></li><li>IFrame - <a href="https://repl.it/@pglet/bash-iframe-example" target="_blank" rel="noopener noreferrer">demo</a></li><li>Header navigation menu (inverted toolbar) - <a href="https://repl.it/@pglet/bash-header-menu-example" target="_blank" rel="noopener noreferrer">demo</a></li></ul><h2 class="anchor anchorWithHideOnScrollNavbar_R0VQ" id="deep-linking">Deep linking<a class="hash-link" href="#deep-linking" title="Direct link to heading">​</a></h2><p>It's now possible to switch between application states (views, areas) by means of URL hash. See how to use it in <a href="/docs/deep-linking">Deep linking guide</a>.</p><h2 class="anchor anchorWithHideOnScrollNavbar_R0VQ" id="other-fixes-and-improvements">Other fixes and improvements<a class="hash-link" href="#other-fixes-and-improvements" title="Direct link to heading">​</a></h2><ul><li><a href="https://github.com/pglet/pglet/issues/13" target="_blank" rel="noopener noreferrer">#13</a> Deep linking</li><li><a href="https://github.com/pglet/pglet/issues/56" target="_blank" rel="noopener noreferrer">#56</a> Fixed: VerticalBarChart control: theme colors are not supported in data points</li><li><a href="https://github.com/pglet/pglet/issues/59" target="_blank" rel="noopener noreferrer">#59</a> iframe control</li><li><a href="https://github.com/pglet/pglet/issues/62" target="_blank" rel="noopener noreferrer">#62</a> Enhancement - Application Installation Conforms to XDG Specification</li><li><a href="https://github.com/pglet/pglet/issues/65" target="_blank" rel="noopener noreferrer">#65</a> Line Chart (LineChart) control</li><li><a href="https://github.com/pglet/pglet/issues/67" target="_blank" rel="noopener noreferrer">#67</a> Horizontal Bar Chart (BarChart) control</li><li><a href="https://github.com/pglet/pglet/issues/69" target="_blank" rel="noopener noreferrer">#69</a> Pie Chart (PieChart) control</li><li><a href="https://github.com/pglet/pglet/issues/70" target="_blank" rel="noopener noreferrer">#70</a> Callout control</li><li><a href="https://github.com/pglet/pglet/issues/71" target="_blank" rel="noopener noreferrer">#71</a> Inverted toolbar button for use in app header</li><li><a href="https://github.com/pglet/pglet/issues/73" target="_blank" rel="noopener noreferrer">#73</a> Enhancement: configure pglet server options via config file</li><li><a href="https://github.com/pglet/pglet/issues/74" target="_blank" rel="noopener noreferrer">#74</a> Add host clients authentication with a token</li><li><a href="https://github.com/pglet/pglet/issues/75" target="_blank" rel="noopener noreferrer">#75</a> Stack control: overflow properties</li></ul><p><a href="/docs/">Give Pglet a try</a> and let us know what you think! There are multiple feedback channels available:</p><ul><li><a href="https://github.com/pglet/pglet/issues" target="_blank" rel="noopener noreferrer">Submit an issue in Pglet repository</a></li><li><a href="https://discord.gg/rWjf7xx" target="_blank" rel="noopener noreferrer">Joing a chat in our Discord channel</a></li><li><a href="https://twitter.com/pgletio" target="_blank" rel="noopener noreferrer">Follow us on Twitter</a></li><li><a href="mailto:hello@pglet.io" target="_blank" rel="noopener noreferrer">Drop us email</a></li></ul>]]></content:encoded>
            <category>release</category>
        </item>
        <item>
            <title><![CDATA[Pglet 0.2.2]]></title>
            <link>https://pglet.io/blog/pglet-0-2-2</link>
            <guid>pglet-0-2-2</guid>
            <pubDate>Wed, 17 Feb 2021 00:00:00 GMT</pubDate>
            <description><![CDATA[Pglet 0.2.2 adds the first chart control - VerticalBarChart:]]></description>
            <content:encoded><![CDATA[<p>Pglet 0.2.2 adds the first chart control - VerticalBarChart:</p><div style="text-align:center"><img src="/img/blog/pglet-0-2-2/vertical-bar-chart-example.png" width="70%"></div><p><a href="https://repl.it/@pglet/bash-vertical-bar-chart-example" target="_blank" rel="noopener noreferrer">View live demo of VerticalBarChart control</a></p><h2 class="anchor anchorWithHideOnScrollNavbar_R0VQ" id="theming-improvements">Theming improvements<a class="hash-link" href="#theming-improvements" title="Direct link to heading">​</a></h2><p>All <code>color</code>-like attributes in all controls can now accept Fluent UI theme slot colors and "shared" colors.</p><p>The list of theme colors can be found on <a href="https://developer.microsoft.com/en-us/fluentui#/styles/web/colors/theme-slots" target="_blank" rel="noopener noreferrer">Fluent UI Theme Slots</a> page.</p><p>The list of shared colors can be found on <a href="https://developer.microsoft.com/en-us/fluentui#/styles/web/colors/shared" target="_blank" rel="noopener noreferrer">Fluent UI Shared Colors</a> page.</p><p>For example, you can add an icon with <code>themePrimary</code> color:</p><div class="codeBlockContainer_I0IT theme-code-block"><div class="codeBlockContent_wNvx"><pre tabindex="0" class="prism-code language-text codeBlock_jd64 thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_mRuA"><span class="token-line" style="color:#393A34"><span class="token plain">add icon name=shop color=themePrimary</span><br></span></code></pre><button type="button" aria-label="Copy code to clipboard" class="copyButton_wuS7 clean-btn">Copy</button></div></div><p>Color is being searched in the following order:</p><ol><li>Theme slot color</li><li>Shared color</li><li>Fallback to a <a href="https://www.w3schools.com/colors/colors_names.asp" target="_blank" rel="noopener noreferrer">named web color</a> or color hex value.</li></ol><h2 class="anchor anchorWithHideOnScrollNavbar_R0VQ" id="other-fixes-and-improvements">Other fixes and improvements<a class="hash-link" href="#other-fixes-and-improvements" title="Direct link to heading">​</a></h2><ul><li><a href="https://github.com/pglet/pglet/issues/43" target="_blank" rel="noopener noreferrer">#43</a> Nav control: unify expanded/collapsed for groups and items</li><li><a href="https://github.com/pglet/pglet/issues/45" target="_blank" rel="noopener noreferrer">#45</a> ChoiceGroup control: Add "iconColor" property to Option</li><li><a href="https://github.com/pglet/pglet/issues/46" target="_blank" rel="noopener noreferrer">#46</a> Stack control: add "wrap" property</li><li><a href="https://github.com/pglet/pglet/issues/47" target="_blank" rel="noopener noreferrer">#47</a> Text control: Markdown mode</li><li><a href="https://github.com/pglet/pglet/issues/48" target="_blank" rel="noopener noreferrer">#48</a> Chart control: VerticalBarChart</li><li><a href="https://github.com/pglet/pglet/issues/49" target="_blank" rel="noopener noreferrer">#49</a> Add "trim" attribute to "add" command</li><li><a href="https://github.com/pglet/pglet/issues/52" target="_blank" rel="noopener noreferrer">#52</a> All color attributes accept theme slot colors and shared colors</li><li><a href="https://github.com/pglet/pglet/issues/53" target="_blank" rel="noopener noreferrer">#53</a> New control: Image</li><li><a href="https://github.com/pglet/pglet/issues/54" target="_blank" rel="noopener noreferrer">#54</a> Link control: can contain child controls and other improvements</li><li><a href="https://github.com/pglet/pglet/issues/55" target="_blank" rel="noopener noreferrer">#55</a> Text control: new "border*" properties</li></ul><p><a href="/docs/">Give Pglet a try</a> and let us know what you think! There are multiple feedback channels available:</p><ul><li><a href="https://github.com/pglet/pglet/issues" target="_blank" rel="noopener noreferrer">Submit an issue in Pglet repository</a></li><li><a href="https://discord.gg/rWjf7xx" target="_blank" rel="noopener noreferrer">Joing a chat in our Discord channel</a></li><li><a href="https://twitter.com/pgletio" target="_blank" rel="noopener noreferrer">Follow us on Twitter</a></li><li><a href="mailto:hello@pglet.io" target="_blank" rel="noopener noreferrer">Drop us email</a></li></ul>]]></content:encoded>
            <category>release</category>
        </item>
        <item>
            <title><![CDATA[Pglet 0.2.0]]></title>
            <link>https://pglet.io/blog/pglet-0-2-0</link>
            <guid>pglet-0-2-0</guid>
            <pubDate>Fri, 05 Feb 2021 00:00:00 GMT</pubDate>
            <description><![CDATA[We've just released Pglet 0.2.0!]]></description>
            <content:encoded><![CDATA[<p>We've just released Pglet 0.2.0!</p><p>A ton of new controls were added such as navigation menu, toolbar, grid, tabs, dialog and panel. Now we feel confident Pglet allows to fully unleash your creativity and build a user interface of any complexity!</p><h2 class="anchor anchorWithHideOnScrollNavbar_R0VQ" id="new-features">New features<a class="hash-link" href="#new-features" title="Direct link to heading">​</a></h2><h3 class="anchor anchorWithHideOnScrollNavbar_R0VQ" id="arm-support">ARM support<a class="hash-link" href="#arm-support" title="Direct link to heading">​</a></h3><p>This release adds binaries for Linux ARM and Apple Silicon M1 - Go 1.16 made that possible. Now you can add remote web UI to your Raspberry PI apps or control what's going on in any IoT device.</p><h3 class="anchor anchorWithHideOnScrollNavbar_R0VQ" id="docker-image">Docker image<a class="hash-link" href="#docker-image" title="Direct link to heading">​</a></h3><p>Docker image <a href="https://hub.docker.com/r/pglet/server" target="_blank" rel="noopener noreferrer"><code>pglet/server</code></a> with self-hosted Pglet Server is now available - run it on a VPS server or drop into your Kubernetes cluster.</p><h3 class="anchor anchorWithHideOnScrollNavbar_R0VQ" id="theming">Theming<a class="hash-link" href="#theming" title="Direct link to heading">​</a></h3><p>Theming in Pglet takes similar approach as in <a href="https://aka.ms/themedesigner" target="_blank" rel="noopener noreferrer">Fluent UI Theme Designer</a> - you choose primary, text, background colors and the theme is auto-magically generated from those colors. To change page theme in Pglet set <code>page</code> control properties: <code>themePrimaryColor</code>, <code>themeTextColor</code>, <code>themeBackgroundColor</code>. <a href="https://repl.it/@pglet/bash-theme-example" target="_blank" rel="noopener noreferrer">Check out Theme demo</a>!</p><h2 class="anchor anchorWithHideOnScrollNavbar_R0VQ" id="new-controls">New controls<a class="hash-link" href="#new-controls" title="Direct link to heading">​</a></h2><ul><li>Nav - <a href="https://repl.it/@pglet/bash-nav-example" target="_blank" rel="noopener noreferrer">demo</a></li><li>Toolbar - <a href="https://repl.it/@pglet/bash-toolbar-example" target="_blank" rel="noopener noreferrer">demo</a></li><li>Tabs - <a href="https://repl.it/@pglet/bash-tabs-example" target="_blank" rel="noopener noreferrer">demo</a></li><li>Grid - <a href="https://repl.it/@pglet/bash-grid-example" target="_blank" rel="noopener noreferrer">demo</a></li><li>Dialog - <a href="https://repl.it/@pglet/bash-dialogs-example" target="_blank" rel="noopener noreferrer">demo</a></li><li>Panel - <a href="https://repl.it/@pglet/bash-panel-example" target="_blank" rel="noopener noreferrer">demo</a></li><li>Message - <a href="https://repl.it/@pglet/bash-messages-example" target="_blank" rel="noopener noreferrer">demo</a></li><li>Toggle - <a href="https://repl.it/@pglet/bash-toggle-example" target="_blank" rel="noopener noreferrer">demo</a></li><li>Slider - <a href="https://repl.it/@pglet/bash-slider-example" target="_blank" rel="noopener noreferrer">demo</a></li><li>ChoiceGroup - <a href="https://repl.it/@pglet/bash-choicegroup-example" target="_blank" rel="noopener noreferrer">demo</a></li><li>Searchbox - <a href="https://repl.it/@pglet/bash-searchbox-example" target="_blank" rel="noopener noreferrer">demo</a></li><li>SpinButton - <a href="https://repl.it/@pglet/bash-spinbuttons-example" target="_blank" rel="noopener noreferrer">demo</a></li><li>Link - <a href="https://repl.it/@pglet/bash-link-example" target="_blank" rel="noopener noreferrer">demo</a></li><li>Icon - <a href="https://repl.it/@pglet/bash-icon-example" target="_blank" rel="noopener noreferrer">demo</a></li></ul><h2 class="anchor anchorWithHideOnScrollNavbar_R0VQ" id="improved-controls">Improved controls<a class="hash-link" href="#improved-controls" title="Direct link to heading">​</a></h2><p><strong>Button</strong> - additional types of Button control were implemented: compound buttons, icon buttons, toolbar, action and link buttons, buttons with context menus and split buttons. <a href="https://repl.it/@pglet/bash-buttons-example" target="_blank" rel="noopener noreferrer">Check out Buttons demo</a>!</p><p><strong>Text</strong> - You can now control the styling of Text control such as color and background as well as border properties and text alignment within it (vertical alignment in center works too!). <a href="https://repl.it/@pglet/bash-text-example" target="_blank" rel="noopener noreferrer">Check out Text demo</a>!</p><h2 class="anchor anchorWithHideOnScrollNavbar_R0VQ" id="other-fixes-and-improvements">Other fixes and improvements<a class="hash-link" href="#other-fixes-and-improvements" title="Direct link to heading">​</a></h2><ul><li>Controls are based on Fluent UI 8.</li><li><code>replace</code> command.</li><li>Event ticker to avoid hanging event loops.</li><li>Pglet Server now does not allow remote host clients by default. Remote hosts clients can be enabled with <code>ALLOW_REMOTE_HOST_CLIENTS=true</code> environment variable. <a href="https://hub.docker.com/r/pglet/server" target="_blank" rel="noopener noreferrer">Pglet Server Docker image</a> set this variable by default.</li></ul><p><a href="/docs/">Give Pglet a try</a> and let us know what you think! There are multiple feedback channels available:</p><ul><li><a href="https://github.com/pglet/pglet/issues" target="_blank" rel="noopener noreferrer">Submit an issue in Pglet repository</a></li><li><a href="https://discord.gg/rWjf7xx" target="_blank" rel="noopener noreferrer">Joing a chat in our Discord channel</a></li><li><a href="https://twitter.com/pgletio" target="_blank" rel="noopener noreferrer">Follow us on Twitter</a></li><li><a href="mailto:hello@pglet.io" target="_blank" rel="noopener noreferrer">Drop us email</a></li></ul>]]></content:encoded>
            <category>release</category>
        </item>
        <item>
            <title><![CDATA[Launching Pglet]]></title>
            <link>https://pglet.io/blog/launching-pglet</link>
            <guid>launching-pglet</guid>
            <pubDate>Fri, 15 Jan 2021 00:00:00 GMT</pubDate>
            <description><![CDATA[Today we are officially launching Pglet!]]></description>
            <content:encoded><![CDATA[<p>Today we are officially launching Pglet!</p><p>This is not a groundbreaking event shaking the World and it won't make any ripples on the Internet, but it's still very important milestone for us as it's a good chance to make functionality cut off and present Pglet to the public.</p><h2 class="anchor anchorWithHideOnScrollNavbar_R0VQ" id="what-weve-got">What we've got<a class="hash-link" href="#what-weve-got" title="Direct link to heading">​</a></h2><p>You probably won't be able to do a real app with Pglet yet, but we believe it's quite the MVP in a Technical Preview state. The core Pglet functionality is there: pages can be created, controls can be added, modified and removed with live updates streamed to users via WebSockets, page control events triggered by users are broadcasted back to your program - the entire concept's working. We've got basic layout (<a href="/docs/controls/stack">Stack</a>) and data entry controls (<a href="/docs/controls/textbox">Textbox</a>, <a href="/docs/controls/button">Button</a>, etc.) to do simple apps and dashboards, but <a href="https://developer.microsoft.com/en-us/fluentui#/controls/web" target="_blank" rel="noopener noreferrer">Fluent UI library</a>, Pglet is based on, is huge and it's a lot more controls to do!</p><p>We've got Pglet client bindinds for 4 languages: <a href="/docs/tutorials/python">Python</a>, <a href="/docs/tutorials/bash">Bash</a>, <a href="/docs/tutorials/powershell">PowerShell</a> and <a href="/docs/tutorials/node">Node.js</a>. We chose these languages for MVP to have a good sense of what might be involved in supporting another language. These are scriping non-typed environments mostly, but probably the next binding we do would be Go or C#. Python bindings is the most complete implementation with <a href="/docs/tutorials/python#control-classes">classes</a> for every control and <a href="/docs/tutorials/python#event-handlers">control event handlers</a>.</p><p>We've got <a href="/docs/pglet-service">Pglet Service</a> which is a hosted Pglet server which you can use right away to bring your web experiences to the web. For technical preview it's just a basic deployment on GAE (will do a separate blog post about that), but quite enough to play with.</p><h2 class="anchor anchorWithHideOnScrollNavbar_R0VQ" id="the-experience">The experience<a class="hash-link" href="#the-experience" title="Direct link to heading">​</a></h2><p>It's been really exciting to work on Pglet during the last 6 months and we learned a lot. Being a C# and mostly Windows developer for more than 15 years it was an absolute pleasure to develop in Go: clean and simple language syntax, goroutines and channels, everything async by design, explicit exceptions management - I'll probably do another post about that experience! Making Pglet UI in React with TypeScript was fun as well: both are fantastic technologies! There is also a challenge to support multiple platforms. Pglet works on Windows, Linux and macOS and you need to constantly think about the experience on all 3 platforms and do a triple amount of tests, CI workflows and other chore things.</p><h2 class="anchor anchorWithHideOnScrollNavbar_R0VQ" id="whats-next">What's next<a class="hash-link" href="#whats-next" title="Direct link to heading">​</a></h2><p>For year 2021 our goal is being able to build and run full-blown backend apps in production. Therefore we are going to work in multiple directions:</p><h3 class="anchor anchorWithHideOnScrollNavbar_R0VQ" id="controls">Controls<a class="hash-link" href="#controls" title="Direct link to heading">​</a></h3><p>We are going to add more controls and improve existing ones. Pglet is still missing navigation controls like <a href="https://developer.microsoft.com/en-us/fluentui#/controls/web/nav" target="_blank" rel="noopener noreferrer">menus</a>, <a href="https://developer.microsoft.com/en-us/fluentui#/controls/web/commandbar" target="_blank" rel="noopener noreferrer">toolbars</a> and <a href="https://developer.microsoft.com/en-us/fluentui#/controls/web/pivot" target="_blank" rel="noopener noreferrer">tabs</a>. <a href="https://developer.microsoft.com/en-us/fluentui#/controls/web/detailslist" target="_blank" rel="noopener noreferrer">Grid</a> is on top priority, for sure, and it's going to be the huge! Charts will be added as well, so you can build beautiful dashboards.</p><h3 class="anchor anchorWithHideOnScrollNavbar_R0VQ" id="pglet-service">Pglet Service<a class="hash-link" href="#pglet-service" title="Direct link to heading">​</a></h3><p>This year we are going to bring Pglet Service into production mode with a proper persistence, authentication and account/profile dashboards. All Pglet backend UI is going to be implemented with Pglet.</p><h3 class="anchor anchorWithHideOnScrollNavbar_R0VQ" id="more-docs-and-examples">More docs and examples<a class="hash-link" href="#more-docs-and-examples" title="Direct link to heading">​</a></h3><p>We'll be working on providing more <a href="https://github.com/pglet/examples" target="_blank" rel="noopener noreferrer">Pglet examples</a>, we'll write deployment guides for standalone Pglet apps and self-hosted Pglet Server.</p><h2 class="anchor anchorWithHideOnScrollNavbar_R0VQ" id="conclusion">Conclusion<a class="hash-link" href="#conclusion" title="Direct link to heading">​</a></h2><p>At this stage we are actively looking for any feedback to understand if the project idea is moving in the right direction. We'd be happy to know what would be your requirements, what's missing in Pglet, what's nice or inconvenient, what could be implemented with higher priority.</p><p>Feel free to <a href="/docs/">give Pglet a try</a> and let us know what you think! There are multiple feedback channels available:</p><ul><li><a href="https://github.com/pglet/pglet/issues" target="_blank" rel="noopener noreferrer">Submit an issue in Pglet repository</a></li><li><a href="https://discord.gg/rWjf7xx" target="_blank" rel="noopener noreferrer">Joing a chat in our Discord channel</a></li><li><a href="https://twitter.com/pgletio" target="_blank" rel="noopener noreferrer">Follow us on Twitter</a></li><li><a href="mailto:hello@pglet.io" target="_blank" rel="noopener noreferrer">Drop us email</a></li></ul>]]></content:encoded>
            <category>pglet</category>
            <category>launch</category>
            <category>roadmap</category>
        </item>
        <item>
            <title><![CDATA[Introducing Pglet]]></title>
            <link>https://pglet.io/blog/introducing-pglet</link>
            <guid>introducing-pglet</guid>
            <pubDate>Mon, 05 Oct 2020 00:00:00 GMT</pubDate>
            <description><![CDATA[Pglet empowers DevOps to easily add rich user interface into their internal apps and utilities without any knowledge of HTML, CSS and JavaScript.]]></description>
            <content:encoded><![CDATA[<div style="font-size:16pt"><b>Pglet</b> empowers DevOps to easily add rich user interface into their internal apps and utilities without any knowledge of HTML, CSS and JavaScript.</div><h2 class="anchor anchorWithHideOnScrollNavbar_R0VQ" id="the-problems-of-internal-apps">The problems of internal apps<a class="hash-link" href="#the-problems-of-internal-apps" title="Direct link to heading">​</a></h2><p>Hi, I'm Feodor, the founder of <a href="https://www.appveyor.com" target="_blank" rel="noopener noreferrer">AppVeyor</a> - CI/CD service for software developers.</p><p>At AppVeyor, as any other startup, we do a lot of <strong>internal apps supporting the core business</strong>. Our users don't see those apps. These could be scripts for processing database data, monitoring dashboards, background apps for housekeeping, scheduled scripts for reporting, backend web apps for account management and billing.</p><p><strong>Internal apps need User Interface</strong> (UI) to present progress/results and grab user input. The simplest form of UI is text console output. Console output can be easily produced from any program or script.</p><p><strong>Text output has limitations</strong>. It could be hard to present complex structures like hierarhies or visualize the progress of multiple concurrent processes (e.g. copying files in parallel). There is no easy way to fill out the form. Plain text cannot be grouped into collapsible areas. <strong>We need rich UI</strong> for our internal apps.</p><p>Console output can be logged and studied later or you can sit in front of your computer and stare at log "tail". But we want to be mobile. <strong>We want to control internal apps from anywhere</strong> and any device. We want to share the progress of long-running process with colleagues or send a link to a realt-time dashboard to a manager. Maybe have "Approve" button in the middle of the script to proceed with irreversible actions. We want to collaborate on the results in a real-time. Does it mean we need to build another web app?</p><p><strong>Building web apps is hard</strong>. Our small team is mostly DevOps. We all do development, deployment and maintenance. <strong>We are all good in doing backend coding in different languages</strong>: Bash, PowerShell, Python, C#, TypeScript. However, not every team member is a full-stack developer being able to create a web app. Frontend development is a completely different beast: HTTP, TLS, CGI, HTML, CSS, JavaScript, React, Vue, Angular, Svelte, WebPack and so on. Web development today has a steep learning curve.</p><p><strong>Building secure web apps is even harder</strong>. Internal app works with sensitive company data (databases, keys, credentials, etc.) and presumably hosted in DMZ. You simply can't allow any developer being able to deploy web app with an access to internal resources and available to the whole world.</p><h2 class="anchor anchorWithHideOnScrollNavbar_R0VQ" id="pglet-to-the-rescue">Pglet to the rescue<a class="hash-link" href="#pglet-to-the-rescue" title="Direct link to heading">​</a></h2><p>Let's say you are the developer responsible for deployment and maintenance of backend services and database - DevOp. You mostly work with Golang and use Bash or Python for writing scripts and tools. Your team asked you to implement a simple real-time dashboard with some metrics from backend services. Dashboard should be accessible outside your org.</p><p>Should you do a web app? You don't have much experience of writing web apps. Alright, you know the basics of HTML/CSS, you know how to use StackOverflow, but how do you start with the app? Should it be IIS + FastCGI or Flask, plain HTML + jQuery or React, or something else?</p><p style="font-size:16pt">Pglet gives you a <b>page</b>, a set of nice-looking <b>controls</b> and the <b>API</b> to arrange those controls on a page and query their state.</p><p>Pglet is the tool that hosts virtual pages for you. Think of a page as a "canvas", a "hub", an "app" where both your programs and users collaborate. <strong>Programs use an API to control page contents</strong> and listen to the events generated by users. <strong>Users view page</strong> in their browsers and continuosly receive real-time page updates. When in- API is just plain-text commands like "add column A", "add text B to column A", "get the value of textbox C", "wait until button D is clicked" - it's easy to format/parse strings in any programming language.</p><div style="text-align:center"><img src="/img/blog/pglet-introduction/pglet-highlevel-design.png"></div><p>Bash-like pseudo-code for a simple app greeting user by the name could look like the following:</p><div class="codeBlockContainer_I0IT language-bash theme-code-block"><div class="codeBlockContent_wNvx bash"><pre tabindex="0" class="prism-code language-bash codeBlock_jd64 thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_mRuA"><span class="token-line" style="color:#393A34"><span class="token comment" style="color:#999988;font-style:italic"># create/connect to a page and return its "handle" (named pipe)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token variable" style="color:#36acaa">$p</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">pglet page connect </span><span class="token string" style="color:#e3116c">"myapp"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic"># display entry form</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token builtin class-name">echo</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'add row'</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">&gt;</span><span class="token plain"> </span><span class="token variable" style="color:#36acaa">$p</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token builtin class-name">echo</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'add col id=form to=row'</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">&gt;</span><span class="token plain"> </span><span class="token variable" style="color:#36acaa">$p</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token builtin class-name">echo</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'add textbox id=yourName to=form'</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">&gt;</span><span class="token plain"> </span><span class="token variable" style="color:#36acaa">$p</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token builtin class-name">echo</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'add button id=submit to=form'</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">&gt;</span><span class="token plain"> </span><span class="token variable" style="color:#36acaa">$p</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic"># listen for events coming from a page</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">while</span><span class="token plain"> </span><span class="token builtin class-name">read</span><span class="token plain"> eventName eventTarget </span><span class="token operator" style="color:#393A34">&lt;</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"</span><span class="token string variable" style="color:#36acaa">$p</span><span class="token string" style="color:#e3116c">.events"</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">do</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token comment" style="color:#999988;font-style:italic"># user fills out the form and clicks "submit" button</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">[</span><span class="token punctuation" style="color:#393A34">[</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"</span><span class="token string variable" style="color:#36acaa">$eventTarget</span><span class="token string" style="color:#e3116c">"</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">==</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"submit"</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">&amp;&amp;</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"</span><span class="token string variable" style="color:#36acaa">$eventName</span><span class="token string" style="color:#e3116c">"</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">==</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"click"</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">]</span><span class="token punctuation" style="color:#393A34">]</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">then</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token comment" style="color:#999988;font-style:italic"># read textbox value</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token builtin class-name">echo</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'get yourName value'</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">&gt;</span><span class="token plain"> </span><span class="token variable" style="color:#36acaa">$p</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token builtin class-name">read</span><span class="token plain"> </span><span class="token variable" style="color:#36acaa">$yourName</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">&lt;</span><span class="token plain"> </span><span class="token variable" style="color:#36acaa">$p</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token comment" style="color:#999988;font-style:italic"># replace forms contents with the greeting</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token builtin class-name">echo</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'clear form'</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">&gt;</span><span class="token plain"> </span><span class="token variable" style="color:#36acaa">$p</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token builtin class-name">echo</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"add text value='Thank you, </span><span class="token string variable" style="color:#36acaa">$yourName</span><span class="token string" style="color:#e3116c">!' to=form"</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">&gt;</span><span class="token plain"> </span><span class="token variable" style="color:#36acaa">$p</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">fi</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">done</span><br></span></code></pre><button type="button" aria-label="Copy code to clipboard" class="copyButton_wuS7 clean-btn">Copy</button></div></div><p>You can build a web app in Bash! No HTML, no templates, no spaghetti code. You don't need to care about the design of your internal app - you get fully-featured controls with "standard" look-n-feel. What you care about is the time you need to deliver the required functionality.</p><h3 class="anchor anchorWithHideOnScrollNavbar_R0VQ" id="highlights">Highlights<a class="hash-link" href="#highlights" title="Direct link to heading">​</a></h3><ul><li>Imperatively program UI with commands.</li><li><strong>Standard controls</strong>: layout, data, form. Skins supported.</li><li><strong>Fast and simple API</strong> via named pipes - call from Bash, PowerShell and any other language.</li><li><strong>Secure by design</strong>. Program makes calls to Pglet to update/query UI. Pglet doesn't have access and knows nothing about internal resources located behind the firewall. Pglet keeps no sensitive data such as connection strings, credentials or certificates.</li><li>Two types of pages can be hosted:<ul><li><strong>Shared page</strong>: multiple programs/scripts can connect to the same page and multiple users can view/interact with the same page.</li><li><strong>App</strong>: a new session is created for every connected user; multiple programs/scripts can serve user sessions (load-balancing).</li></ul></li></ul>]]></content:encoded>
            <category>internal apps</category>
            <category>pglet</category>
            <category>introduction</category>
            <category>design</category>
        </item>
    </channel>
</rss>