<?xml version="1.0" encoding="utf-8"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Edge Apps library release notes | Screenly</title><description>@screenly/edge-apps, the library for building Edge Apps. What has shipped, newest first.</description><link>https://www.screenly.io/releases/edge-apps/</link><language>en</language><atom:link href="https://www.screenly.io/releases/edge-apps/rss.xml" rel="self" type="application/rss+xml"/><lastBuildDate>Tue, 01 Sep 2026 00:00:00 +0000</lastBuildDate><item><title>Edge Apps library 26.8.0: A new captureScreenshot helper takes an Edge App screenshot in 1 call</title><link>https://www.screenly.io/releases/edge-apps/26.8.0/capture-screenshot/</link><guid isPermaLink="true">https://www.screenly.io/releases/edge-apps/26.8.0/capture-screenshot/</guid><pubDate>Tue, 01 Sep 2026 00:00:00 +0000</pubDate><category>release</category><category>developers</category><description>&lt;p&gt;A screenshot spec for an Edge App does the same 7 things for every resolution: open a browser
context at the right size, fix the clock, mock &lt;code&gt;screenly.js&lt;/code&gt;, load the page, wait for the network
to go quiet, save the image, and close the context. &lt;code&gt;captureScreenshot()&lt;/code&gt; does all of it, and your
spec keeps only what&amp;rsquo;s specific to your app:&lt;/p&gt;
&lt;div class="code-block"&gt;
&lt;button type="button" class="code-copy" aria-label="Copy code" data-copy-code&gt;
&lt;svg data-copy viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"&gt;
&lt;rect x="9" y="9" width="11" height="11" rx="2"&gt;&lt;/rect&gt;
&lt;path d="M5 15V5a2 2 0 0 1 2-2h10"&gt;&lt;/path&gt;
&lt;/svg&gt;
&lt;svg data-check viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"&gt;
&lt;path d="M20 6 9 17l-5-5"&gt;&lt;/path&gt;
&lt;/svg&gt;
&lt;/button&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d0d0d0;background-color:#202020;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-typescript" data-lang="typescript"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6ab825;font-weight:bold"&gt;import&lt;/span&gt; { test } &lt;span style="color:#6ab825;font-weight:bold"&gt;from&lt;/span&gt; &lt;span style="color:#ed9d13"&gt;&amp;#39;@playwright/test&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6ab825;font-weight:bold"&gt;import&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; captureScreenshot,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; createMockScreenlyForScreenshots,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; RESOLUTIONS,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;} &lt;span style="color:#6ab825;font-weight:bold"&gt;from&lt;/span&gt; &lt;span style="color:#ed9d13"&gt;&amp;#39;@screenly/edge-apps/test/screenshots&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6ab825;font-weight:bold"&gt;const&lt;/span&gt; { screenlyJsContent } = createMockScreenlyForScreenshots()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6ab825;font-weight:bold"&gt;for&lt;/span&gt; (&lt;span style="color:#6ab825;font-weight:bold"&gt;const&lt;/span&gt; { width, height } &lt;span style="color:#6ab825;font-weight:bold"&gt;of&lt;/span&gt; RESOLUTIONS) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; test(&lt;span style="color:#ed9d13"&gt;`screenshot &lt;/span&gt;&lt;span style="color:#ed9d13"&gt;${&lt;/span&gt;width&lt;span style="color:#ed9d13"&gt;}&lt;/span&gt;&lt;span style="color:#ed9d13"&gt;x&lt;/span&gt;&lt;span style="color:#ed9d13"&gt;${&lt;/span&gt;height&lt;span style="color:#ed9d13"&gt;}&lt;/span&gt;&lt;span style="color:#ed9d13"&gt;`&lt;/span&gt;, &lt;span style="color:#6ab825;font-weight:bold"&gt;async&lt;/span&gt; ({ browser }) =&amp;gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#6ab825;font-weight:bold"&gt;await&lt;/span&gt; captureScreenshot(browser, {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; width,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; height,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; filenamePrefix: &lt;span style="color:#ed9d13"&gt;&amp;#39;menu-board&amp;#39;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; screenlyJsContent,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; setupMocks: &lt;span style="color:#6ab825;font-weight:bold"&gt;async&lt;/span&gt; (page) =&amp;gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#6ab825;font-weight:bold"&gt;await&lt;/span&gt; page.route(&lt;span style="color:#ed9d13"&gt;&amp;#39;**/api/prices**&amp;#39;&lt;/span&gt;, (route) =&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; route.fulfill({ status: &lt;span style="color:#6ab825;font-weight:bold"&gt;200&lt;/span&gt;, body: &lt;span style="color:#ed9d13"&gt;&amp;#39;{&amp;#34;items&amp;#34;:[]}&amp;#39;&lt;/span&gt; }),
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; )
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; },
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; })
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; })
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;Each run writes &lt;code&gt;screenshots/menu-board-1920x1080.png&lt;/code&gt; and so on, 1 file per resolution.
&lt;code&gt;setupMocks&lt;/code&gt; runs before the page loads, so your routes are in place for the first request. Leave
it out when there&amp;rsquo;s nothing app-specific to mock.&lt;/p&gt;
&lt;p&gt;Keep &lt;code&gt;test()&lt;/code&gt; in your own spec file, as above. That way Playwright reports failures against your
file and line, not the library&amp;rsquo;s.&lt;/p&gt;
&lt;p&gt;Apps made with &lt;code&gt;npx @screenly/edge-apps create&lt;/code&gt; from this release on get a spec that already uses
it.&lt;/p&gt;</description></item><item><title>Edge Apps library 26.8.0: The library's version numbers moved to CalVer</title><link>https://www.screenly.io/releases/edge-apps/26.8.0/calver-version-numbers/</link><guid isPermaLink="true">https://www.screenly.io/releases/edge-apps/26.8.0/calver-version-numbers/</guid><pubDate>Tue, 01 Sep 2026 00:00:00 +0000</pubDate><category>improvement</category><category>developers</category><description>&lt;p&gt;Library releases are numbered by year and month now, so the release after 1.5.0 is 26.8.0. That&amp;rsquo;s
the same scheme Screenly, ScreenlyOS, and the CLI use.&lt;/p&gt;
&lt;p&gt;Nothing about the library&amp;rsquo;s behavior changes with the numbering, and a version you&amp;rsquo;ve already
pinned keeps resolving to exactly what it did before.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;If this affects you:&lt;/strong&gt; a range like &lt;code&gt;^1.5.0&lt;/code&gt; in your Edge App&amp;rsquo;s &lt;code&gt;package.json&lt;/code&gt; won&amp;rsquo;t match a
release again. Change it to &lt;code&gt;^26.8.0&lt;/code&gt;, which covers releases through the end of 2026. The first
release of 2027 will be 27.1.0.&lt;/p&gt;
&lt;/blockquote&gt;</description></item><item><title>Edge Apps library 1.5.0: A new isLightColor helper picks readable text for any brand color</title><link>https://www.screenly.io/releases/edge-apps/1.5.0/is-light-color/</link><guid isPermaLink="true">https://www.screenly.io/releases/edge-apps/1.5.0/is-light-color/</guid><pubDate>Wed, 19 Aug 2026 00:00:00 +0000</pubDate><category>release</category><category>developers</category><description>&lt;p&gt;Your app doesn&amp;rsquo;t choose the accent color. Each customer sets their own in &lt;code&gt;screenly_color_accent&lt;/code&gt;,
and the same app has to put readable text on a pale yellow brand and on a navy one.
&lt;code&gt;isLightColor()&lt;/code&gt; tells you which you&amp;rsquo;re dealing with:&lt;/p&gt;
&lt;div class="code-block"&gt;
&lt;button type="button" class="code-copy" aria-label="Copy code" data-copy-code&gt;
&lt;svg data-copy viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"&gt;
&lt;rect x="9" y="9" width="11" height="11" rx="2"&gt;&lt;/rect&gt;
&lt;path d="M5 15V5a2 2 0 0 1 2-2h10"&gt;&lt;/path&gt;
&lt;/svg&gt;
&lt;svg data-check viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"&gt;
&lt;path d="M20 6 9 17l-5-5"&gt;&lt;/path&gt;
&lt;/svg&gt;
&lt;/button&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d0d0d0;background-color:#202020;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-typescript" data-lang="typescript"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6ab825;font-weight:bold"&gt;import&lt;/span&gt; { isLightColor, setupTheme } &lt;span style="color:#6ab825;font-weight:bold"&gt;from&lt;/span&gt; &lt;span style="color:#ed9d13"&gt;&amp;#39;@screenly/edge-apps&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6ab825;font-weight:bold"&gt;const&lt;/span&gt; { primary } = setupTheme()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#24909d"&gt;document&lt;/span&gt;.body.classList.toggle(&lt;span style="color:#ed9d13"&gt;&amp;#39;on-light-brand&amp;#39;&lt;/span&gt;, isLightColor(primary))&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;It returns &lt;code&gt;true&lt;/code&gt; when a color needs dark text and &lt;code&gt;false&lt;/code&gt; when it needs light text. The answer
comes from the WCAG relative luminance formula rather than an average of the red, green, and blue
values, so it ranks colors the way people see them. Pure yellow and pure blue have similar channel
totals and land on opposite sides.&lt;/p&gt;
&lt;p&gt;It accepts hex (&lt;code&gt;#abc&lt;/code&gt;, &lt;code&gt;#aabbcc&lt;/code&gt;, &lt;code&gt;#aabbccdd&lt;/code&gt;) and numeric &lt;code&gt;rgb()&lt;/code&gt; or &lt;code&gt;rgba()&lt;/code&gt; strings, and
ignores alpha. Named colors and percentage channels aren&amp;rsquo;t supported. Anything it can&amp;rsquo;t read
returns &lt;code&gt;false&lt;/code&gt; instead of throwing, so a malformed setting can&amp;rsquo;t stop a screen from rendering.&lt;/p&gt;</description></item><item><title>Edge Apps library 1.4.0: Edge Apps can keep showing the last good data when a fetch fails</title><link>https://www.screenly.io/releases/edge-apps/1.4.0/last-known-good-cache/</link><guid isPermaLink="true">https://www.screenly.io/releases/edge-apps/1.4.0/last-known-good-cache/</guid><pubDate>Sat, 01 Aug 2026 00:00:00 +0000</pubDate><category>release</category><category>developers</category><description>&lt;p&gt;A weather app on a lobby screen that loses its API for 10 minutes has 2 choices: show an error to
everyone walking past, or keep showing the last forecast it had. &lt;code&gt;writeEdgeAppCache()&lt;/code&gt; and
&lt;code&gt;readEdgeAppCache()&lt;/code&gt; make the second one a few lines of code:&lt;/p&gt;
&lt;div class="code-block"&gt;
&lt;button type="button" class="code-copy" aria-label="Copy code" data-copy-code&gt;
&lt;svg data-copy viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"&gt;
&lt;rect x="9" y="9" width="11" height="11" rx="2"&gt;&lt;/rect&gt;
&lt;path d="M5 15V5a2 2 0 0 1 2-2h10"&gt;&lt;/path&gt;
&lt;/svg&gt;
&lt;svg data-check viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"&gt;
&lt;path d="M20 6 9 17l-5-5"&gt;&lt;/path&gt;
&lt;/svg&gt;
&lt;/button&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d0d0d0;background-color:#202020;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-typescript" data-lang="typescript"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6ab825;font-weight:bold"&gt;import&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; getSettingWithDefault,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; readEdgeAppCache,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; writeEdgeAppCache,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;} &lt;span style="color:#6ab825;font-weight:bold"&gt;from&lt;/span&gt; &lt;span style="color:#ed9d13"&gt;&amp;#39;@screenly/edge-apps&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6ab825;font-weight:bold"&gt;async&lt;/span&gt; &lt;span style="color:#6ab825;font-weight:bold"&gt;function&lt;/span&gt; loadWeather() {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#6ab825;font-weight:bold"&gt;try&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#6ab825;font-weight:bold"&gt;const&lt;/span&gt; response = &lt;span style="color:#6ab825;font-weight:bold"&gt;await&lt;/span&gt; fetch(weatherApiUrl)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#6ab825;font-weight:bold"&gt;if&lt;/span&gt; (!response.ok) &lt;span style="color:#6ab825;font-weight:bold"&gt;throw&lt;/span&gt; &lt;span style="color:#6ab825;font-weight:bold"&gt;new&lt;/span&gt; &lt;span style="color:#24909d"&gt;Error&lt;/span&gt;(&lt;span style="color:#ed9d13"&gt;`Weather API returned &lt;/span&gt;&lt;span style="color:#ed9d13"&gt;${&lt;/span&gt;response.status&lt;span style="color:#ed9d13"&gt;}&lt;/span&gt;&lt;span style="color:#ed9d13"&gt;`&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#6ab825;font-weight:bold"&gt;const&lt;/span&gt; weather = &lt;span style="color:#6ab825;font-weight:bold"&gt;await&lt;/span&gt; response.json()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; writeEdgeAppCache(&lt;span style="color:#ed9d13"&gt;&amp;#39;weather-app&amp;#39;&lt;/span&gt;, &lt;span style="color:#ed9d13"&gt;&amp;#39;forecast&amp;#39;&lt;/span&gt;, weather)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#6ab825;font-weight:bold"&gt;return&lt;/span&gt; weather
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; } &lt;span style="color:#6ab825;font-weight:bold"&gt;catch&lt;/span&gt; (error) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#6ab825;font-weight:bold"&gt;if&lt;/span&gt; (getSettingWithDefault(&lt;span style="color:#ed9d13"&gt;&amp;#39;display_errors&amp;#39;&lt;/span&gt;, &lt;span style="color:#6ab825;font-weight:bold"&gt;false&lt;/span&gt;)) &lt;span style="color:#6ab825;font-weight:bold"&gt;throw&lt;/span&gt; error
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#6ab825;font-weight:bold"&gt;return&lt;/span&gt; readEdgeAppCache(&lt;span style="color:#ed9d13"&gt;&amp;#39;weather-app&amp;#39;&lt;/span&gt;, &lt;span style="color:#ed9d13"&gt;&amp;#39;forecast&amp;#39;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;Values are stored in the player&amp;rsquo;s &lt;code&gt;localStorage&lt;/code&gt; as JSON under &lt;code&gt;namespace:key&lt;/code&gt;, so 2 apps, or 2
caches in one app, don&amp;rsquo;t overwrite each other. Anything JSON can hold works as a value.&lt;/p&gt;
&lt;p&gt;The cache is for fallback, not freshness. Nothing expires, so read it only after a real failure,
and expect it to be empty after the player restarts. Both functions fail quietly:
&lt;code&gt;readEdgeAppCache()&lt;/code&gt; returns &lt;code&gt;null&lt;/code&gt; when there&amp;rsquo;s nothing usable, and &lt;code&gt;writeEdgeAppCache()&lt;/code&gt; does
nothing when storage is full or unavailable, so neither needs its own &lt;code&gt;try&lt;/code&gt;.&lt;/p&gt;</description></item><item><title>Edge Apps library 1.3.0: New Edge Apps come with Tailwind CSS set up</title><link>https://www.screenly.io/releases/edge-apps/1.3.0/tailwind-css/</link><guid isPermaLink="true">https://www.screenly.io/releases/edge-apps/1.3.0/tailwind-css/</guid><pubDate>Tue, 28 Jul 2026 00:00:00 +0000</pubDate><category>improvement</category><category>developers</category><description>&lt;p&gt;An Edge App made with &lt;code&gt;npx @screenly/edge-apps create&lt;/code&gt; now styles its page with Tailwind CSS
utility classes, so you can lay out a screen in the markup without writing a stylesheet first.
The Tailwind plugin was already part of the &lt;code&gt;edge-apps-scripts&lt;/code&gt; build, so an existing app needs
only the imports. Put these at the top of your stylesheet:&lt;/p&gt;
&lt;div class="code-block"&gt;
&lt;button type="button" class="code-copy" aria-label="Copy code" data-copy-code&gt;
&lt;svg data-copy viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"&gt;
&lt;rect x="9" y="9" width="11" height="11" rx="2"&gt;&lt;/rect&gt;
&lt;path d="M5 15V5a2 2 0 0 1 2-2h10"&gt;&lt;/path&gt;
&lt;/svg&gt;
&lt;svg data-check viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"&gt;
&lt;path d="M20 6 9 17l-5-5"&gt;&lt;/path&gt;
&lt;/svg&gt;
&lt;/button&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d0d0d0;background-color:#202020;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-css" data-lang="css"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;@&lt;span style="color:#6ab825;font-weight:bold"&gt;layer&lt;/span&gt; &lt;span style="color:#6ab825;font-weight:bold"&gt;theme&lt;/span&gt;, &lt;span style="color:#6ab825;font-weight:bold"&gt;base&lt;/span&gt;, &lt;span style="color:#6ab825;font-weight:bold"&gt;utilities&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;@&lt;span style="color:#6ab825;font-weight:bold"&gt;import&lt;/span&gt; &lt;span style="color:#ed9d13"&gt;&amp;#39;tailwindcss/theme.css&amp;#39;&lt;/span&gt; &lt;span style="color:#6ab825;font-weight:bold"&gt;layer&lt;/span&gt;(&lt;span style="color:#6ab825;font-weight:bold"&gt;theme&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;@&lt;span style="color:#6ab825;font-weight:bold"&gt;import&lt;/span&gt; &lt;span style="color:#ed9d13"&gt;&amp;#39;@screenly/edge-apps/styles&amp;#39;&lt;/span&gt; &lt;span style="color:#6ab825;font-weight:bold"&gt;layer&lt;/span&gt;(&lt;span style="color:#6ab825;font-weight:bold"&gt;base&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;@&lt;span style="color:#6ab825;font-weight:bold"&gt;import&lt;/span&gt; &lt;span style="color:#ed9d13"&gt;&amp;#39;tailwindcss/utilities.css&amp;#39;&lt;/span&gt; &lt;span style="color:#6ab825;font-weight:bold"&gt;layer&lt;/span&gt;(&lt;span style="color:#6ab825;font-weight:bold"&gt;utilities&lt;/span&gt;);&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;Keep the layer order line and the &lt;code&gt;layer(base)&lt;/code&gt; on the library&amp;rsquo;s styles. Without them, the
library&amp;rsquo;s base styles win over your utility classes, whatever their specificity.&lt;/p&gt;
&lt;p&gt;Don&amp;rsquo;t use &lt;code&gt;@import 'tailwindcss';&lt;/code&gt; on its own. It includes Tailwind&amp;rsquo;s Preflight reset, which
strips the borders and padding that components like &lt;code&gt;&amp;lt;app-header&amp;gt;&lt;/code&gt; set for themselves.&lt;/p&gt;
&lt;p&gt;Inside &lt;code&gt;&amp;lt;auto-scaler&amp;gt;&lt;/code&gt;, size things with &lt;code&gt;h-full&lt;/code&gt; and &lt;code&gt;w-full&lt;/code&gt; rather than &lt;code&gt;h-screen&lt;/code&gt; and
&lt;code&gt;w-screen&lt;/code&gt;. &lt;code&gt;&amp;lt;auto-scaler&amp;gt;&lt;/code&gt; draws your app at its reference size and scales the result to fit the
display, so viewport units measure the wrong box. The &lt;code&gt;portrait:&lt;/code&gt; and &lt;code&gt;landscape:&lt;/code&gt; variants work
as expected:&lt;/p&gt;
&lt;div class="code-block"&gt;
&lt;button type="button" class="code-copy" aria-label="Copy code" data-copy-code&gt;
&lt;svg data-copy viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"&gt;
&lt;rect x="9" y="9" width="11" height="11" rx="2"&gt;&lt;/rect&gt;
&lt;path d="M5 15V5a2 2 0 0 1 2-2h10"&gt;&lt;/path&gt;
&lt;/svg&gt;
&lt;svg data-check viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"&gt;
&lt;path d="M20 6 9 17l-5-5"&gt;&lt;/path&gt;
&lt;/svg&gt;
&lt;/button&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d0d0d0;background-color:#202020;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-html" data-lang="html"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&amp;lt;&lt;span style="color:#6ab825;font-weight:bold"&gt;main&lt;/span&gt; &lt;span style="color:#bbb"&gt;class&lt;/span&gt;=&lt;span style="color:#ed9d13"&gt;&amp;#34;flex h-full w-full items-center justify-center&amp;#34;&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#6ab825;font-weight:bold"&gt;h1&lt;/span&gt; &lt;span style="color:#bbb"&gt;class&lt;/span&gt;=&lt;span style="color:#ed9d13"&gt;&amp;#34;text-6xl portrait:text-4xl&amp;#34;&lt;/span&gt;&amp;gt;Today&amp;#39;s specials&amp;lt;/&lt;span style="color:#6ab825;font-weight:bold"&gt;h1&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&amp;lt;/&lt;span style="color:#6ab825;font-weight:bold"&gt;main&lt;/span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;/div&gt;</description></item><item><title>Edge Apps library 1.2.1: The app header shows the logo without the screen name</title><link>https://www.screenly.io/releases/edge-apps/1.2.1/fixes-and-improvements/</link><guid isPermaLink="true">https://www.screenly.io/releases/edge-apps/1.2.1/fixes-and-improvements/</guid><pubDate>Tue, 21 Jul 2026 00:00:00 +0000</pubDate><category>roundup</category><description>&lt;ul&gt;
&lt;li&gt;&lt;code&gt;&amp;lt;app-header&amp;gt;&lt;/code&gt; shows the logo without the screen name next to it. If your app needs the name on
screen, &lt;code&gt;&amp;lt;brand-logo show-name&amp;gt;&lt;/code&gt; still renders it.&lt;/li&gt;
&lt;/ul&gt;</description></item><item><title>Edge Apps library 1.2.0: A new create command scaffolds an Edge App in 1 step</title><link>https://www.screenly.io/releases/edge-apps/1.2.0/create-a-new-edge-app/</link><guid isPermaLink="true">https://www.screenly.io/releases/edge-apps/1.2.0/create-a-new-edge-app/</guid><pubDate>Sun, 19 Jul 2026 00:00:00 +0000</pubDate><category>release</category><category>developers</category><description>&lt;p&gt;Starting an Edge App used to mean cloning a template repository and replacing its placeholders.
Now 1 command gives you a project that builds:&lt;/p&gt;
&lt;div class="code-block"&gt;
&lt;button type="button" class="code-copy" aria-label="Copy code" data-copy-code&gt;
&lt;svg data-copy viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"&gt;
&lt;rect x="9" y="9" width="11" height="11" rx="2"&gt;&lt;/rect&gt;
&lt;path d="M5 15V5a2 2 0 0 1 2-2h10"&gt;&lt;/path&gt;
&lt;/svg&gt;
&lt;svg data-check viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"&gt;
&lt;path d="M20 6 9 17l-5-5"&gt;&lt;/path&gt;
&lt;/svg&gt;
&lt;/button&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d0d0d0;background-color:#202020;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;npx @screenly/edge-apps create my-edge-app
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#24909d"&gt;cd&lt;/span&gt; my-edge-app
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;npm run dev&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;With Bun, run &lt;code&gt;bunx @screenly/edge-apps create my-edge-app&lt;/code&gt; instead. The command notices which
package manager started it and writes the &lt;code&gt;package.json&lt;/code&gt; scripts and test setup to match. &lt;code&gt;--pm npm&lt;/code&gt;
or &lt;code&gt;--pm bun&lt;/code&gt; overrides that.&lt;/p&gt;
&lt;p&gt;The new directory has:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;a &lt;code&gt;screenly.yml&lt;/code&gt; manifest with a &lt;code&gt;message&lt;/code&gt; setting and a &lt;code&gt;sentry_dsn&lt;/code&gt; setting&lt;/li&gt;
&lt;li&gt;&lt;code&gt;index.html&lt;/code&gt; with &lt;code&gt;&amp;lt;auto-scaler&amp;gt;&lt;/code&gt; and &lt;code&gt;&amp;lt;app-header&amp;gt;&lt;/code&gt; already on the page&lt;/li&gt;
&lt;li&gt;&lt;code&gt;src/main.ts&lt;/code&gt;, which applies the theme, reads the &lt;code&gt;message&lt;/code&gt; setting, and signals ready&lt;/li&gt;
&lt;li&gt;scripts for &lt;code&gt;dev&lt;/code&gt; (with the CORS proxy running alongside), &lt;code&gt;build&lt;/code&gt;, &lt;code&gt;lint&lt;/code&gt;, &lt;code&gt;format&lt;/code&gt;,
&lt;code&gt;type-check&lt;/code&gt;, &lt;code&gt;screenshots&lt;/code&gt;, and &lt;code&gt;deploy&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;a Playwright spec under &lt;code&gt;e2e/&lt;/code&gt; for screenshots at every supported resolution&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Dependencies are installed for you, at the versions the library itself was released with.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Option&lt;/th&gt;
&lt;th&gt;What it does&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;--description &amp;lt;text&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Sets the description in &lt;code&gt;package.json&lt;/code&gt;, &lt;code&gt;screenly.yml&lt;/code&gt;, and &lt;code&gt;README.md&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;--author &amp;lt;text&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Sets the author in &lt;code&gt;package.json&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;--pm &amp;lt;npm|bun&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Picks the package manager instead of detecting it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;--force&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Writes into a directory that already has files in it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;--skip-install&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Skips installing dependencies&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Running &lt;code&gt;edge-apps-scripts create&lt;/code&gt; with no directory still does what it did before: it fills in
the &lt;code&gt;{{APP_NAME}}&lt;/code&gt;-style placeholders in the current project, for template repositories you&amp;rsquo;ve
already cloned.&lt;/p&gt;</description></item><item><title>Edge Apps library 1.1.2: The edge-apps-scripts commands run on Vite 8</title><link>https://www.screenly.io/releases/edge-apps/1.1.2/fixes-and-improvements/</link><guid isPermaLink="true">https://www.screenly.io/releases/edge-apps/1.1.2/fixes-and-improvements/</guid><pubDate>Thu, 16 Jul 2026 00:00:00 +0000</pubDate><category>roundup</category><description>&lt;h2 id="improvements"&gt;Improvements&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;edge-apps-scripts dev&lt;/code&gt;, &lt;code&gt;build&lt;/code&gt;, and &lt;code&gt;build:dev&lt;/code&gt; run on Vite 8. If your Edge App brings its own
&lt;code&gt;vite.config.ts&lt;/code&gt; or Vite plugins, check them against Vite 8 when you upgrade.&lt;/li&gt;
&lt;/ul&gt;</description></item><item><title>Edge Apps library 1.1.1: The README lists the error reporting helpers</title><link>https://www.screenly.io/releases/edge-apps/1.1.1/fixes-and-improvements/</link><guid isPermaLink="true">https://www.screenly.io/releases/edge-apps/1.1.1/fixes-and-improvements/</guid><pubDate>Sun, 21 Jun 2026 00:00:00 +0000</pubDate><category>roundup</category><description>&lt;h2 id="improvements"&gt;Improvements&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;The README now lists the error reporting helpers that came with
&lt;a href="https://www.screenly.io/releases/edge-apps/1.1.0/sentry-error-reporting/"&gt;Sentry support in 1.1.0&lt;/a&gt;: &lt;code&gt;setupSentry&lt;/code&gt;,
&lt;code&gt;scrubSensitiveData&lt;/code&gt;, and &lt;code&gt;reportError&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;</description></item><item><title>Edge Apps library 1.1.0: Edge Apps can send their errors to your own Sentry project</title><link>https://www.screenly.io/releases/edge-apps/1.1.0/sentry-error-reporting/</link><guid isPermaLink="true">https://www.screenly.io/releases/edge-apps/1.1.0/sentry-error-reporting/</guid><pubDate>Fri, 19 Jun 2026 00:00:00 +0000</pubDate><category>release</category><category>developers</category><description>&lt;p&gt;An Edge App that fails on a screen in another building usually fails quietly. Nobody is standing in
front of it with the developer tools open. With &lt;code&gt;setupSentry()&lt;/code&gt; and &lt;code&gt;reportError()&lt;/code&gt;, those errors
land in your own Sentry project instead, with enough context to tell which screen sent them.&lt;/p&gt;
&lt;p&gt;Add a &lt;code&gt;sentry_dsn&lt;/code&gt; setting to &lt;code&gt;screenly.yml&lt;/code&gt;, set it to your project&amp;rsquo;s DSN in Screenly, and call
&lt;code&gt;setupSentry()&lt;/code&gt; once when the app starts:&lt;/p&gt;
&lt;div class="code-block"&gt;
&lt;button type="button" class="code-copy" aria-label="Copy code" data-copy-code&gt;
&lt;svg data-copy viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"&gt;
&lt;rect x="9" y="9" width="11" height="11" rx="2"&gt;&lt;/rect&gt;
&lt;path d="M5 15V5a2 2 0 0 1 2-2h10"&gt;&lt;/path&gt;
&lt;/svg&gt;
&lt;svg data-check viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"&gt;
&lt;path d="M20 6 9 17l-5-5"&gt;&lt;/path&gt;
&lt;/svg&gt;
&lt;/button&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d0d0d0;background-color:#202020;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6ab825;font-weight:bold"&gt;settings&lt;/span&gt;:&lt;span style="color:#666"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#666"&gt; &lt;/span&gt;&lt;span style="color:#6ab825;font-weight:bold"&gt;sentry_dsn&lt;/span&gt;:&lt;span style="color:#666"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#666"&gt; &lt;/span&gt;&lt;span style="color:#6ab825;font-weight:bold"&gt;type&lt;/span&gt;:&lt;span style="color:#666"&gt; &lt;/span&gt;secret&lt;span style="color:#666"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#666"&gt; &lt;/span&gt;&lt;span style="color:#6ab825;font-weight:bold"&gt;title&lt;/span&gt;:&lt;span style="color:#666"&gt; &lt;/span&gt;Sentry Client Key&lt;span style="color:#666"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#666"&gt; &lt;/span&gt;&lt;span style="color:#6ab825;font-weight:bold"&gt;optional&lt;/span&gt;:&lt;span style="color:#666"&gt; &lt;/span&gt;&lt;span style="color:#6ab825;font-weight:bold"&gt;true&lt;/span&gt;&lt;span style="color:#666"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#666"&gt; &lt;/span&gt;&lt;span style="color:#6ab825;font-weight:bold"&gt;is_global&lt;/span&gt;:&lt;span style="color:#666"&gt; &lt;/span&gt;&lt;span style="color:#6ab825;font-weight:bold"&gt;true&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;div class="code-block"&gt;
&lt;button type="button" class="code-copy" aria-label="Copy code" data-copy-code&gt;
&lt;svg data-copy viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"&gt;
&lt;rect x="9" y="9" width="11" height="11" rx="2"&gt;&lt;/rect&gt;
&lt;path d="M5 15V5a2 2 0 0 1 2-2h10"&gt;&lt;/path&gt;
&lt;/svg&gt;
&lt;svg data-check viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"&gt;
&lt;path d="M20 6 9 17l-5-5"&gt;&lt;/path&gt;
&lt;/svg&gt;
&lt;/button&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d0d0d0;background-color:#202020;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-typescript" data-lang="typescript"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6ab825;font-weight:bold"&gt;import&lt;/span&gt; { setupSentry, reportError } &lt;span style="color:#6ab825;font-weight:bold"&gt;from&lt;/span&gt; &lt;span style="color:#ed9d13"&gt;&amp;#39;@screenly/edge-apps&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;setupSentry(&lt;span style="color:#ed9d13"&gt;&amp;#39;menu-board&amp;#39;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6ab825;font-weight:bold"&gt;try&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#6ab825;font-weight:bold"&gt;await&lt;/span&gt; refreshPrices()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;} &lt;span style="color:#6ab825;font-weight:bold"&gt;catch&lt;/span&gt; (error) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; reportError(error, { source: &lt;span style="color:#ed9d13"&gt;&amp;#39;price-feed&amp;#39;&lt;/span&gt; })
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;Every event is tagged with &lt;code&gt;edge_app&lt;/code&gt; (the name you pass) and the screen&amp;rsquo;s &lt;code&gt;hostname&lt;/code&gt;, which is
also set as the Sentry user, so one screen&amp;rsquo;s errors group together. The optional second argument
to &lt;code&gt;setupSentry()&lt;/code&gt; attaches contexts of your own, such as the URL a report was embedded from. The
second argument to &lt;code&gt;reportError()&lt;/code&gt; goes in as extra data on that event.&lt;/p&gt;
&lt;p&gt;Before an event leaves the screen, the value of any setting whose key contains &lt;code&gt;token&lt;/code&gt;, &lt;code&gt;secret&lt;/code&gt;,
&lt;code&gt;password&lt;/code&gt;, or &lt;code&gt;credential&lt;/code&gt; is replaced with &lt;code&gt;[REDACTED]&lt;/code&gt; wherever it appears in the event. If an
event can&amp;rsquo;t be checked that way, it isn&amp;rsquo;t sent. The same filter is exported as
&lt;code&gt;scrubSensitiveData()&lt;/code&gt; if you initialize Sentry yourself and want it as your &lt;code&gt;beforeSend&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Leave &lt;code&gt;sentry_dsn&lt;/code&gt; empty and &lt;code&gt;setupSentry()&lt;/code&gt; does nothing, so the same build runs with or without
error reporting. It reports errors only, with no performance tracing.&lt;/p&gt;</description></item><item><title>Edge Apps library 1.0.0: edge-apps-scripts runs on Node.js and npm</title><link>https://www.screenly.io/releases/edge-apps/1.0.0/runs-on-node-and-npm/</link><guid isPermaLink="true">https://www.screenly.io/releases/edge-apps/1.0.0/runs-on-node-and-npm/</guid><pubDate>Sat, 02 May 2026 00:00:00 +0000</pubDate><category>release</category><category>developers</category><description>&lt;p&gt;Until this release, &lt;code&gt;edge-apps-scripts&lt;/code&gt; only ran under Bun. If your team already had Node.js
everywhere, building an Edge App meant installing a second runtime first. Now the library installs
and runs with npm on Node.js 20.6.0 or later:&lt;/p&gt;
&lt;div class="code-block"&gt;
&lt;button type="button" class="code-copy" aria-label="Copy code" data-copy-code&gt;
&lt;svg data-copy viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"&gt;
&lt;rect x="9" y="9" width="11" height="11" rx="2"&gt;&lt;/rect&gt;
&lt;path d="M5 15V5a2 2 0 0 1 2-2h10"&gt;&lt;/path&gt;
&lt;/svg&gt;
&lt;svg data-check viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"&gt;
&lt;path d="M20 6 9 17l-5-5"&gt;&lt;/path&gt;
&lt;/svg&gt;
&lt;/button&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d0d0d0;background-color:#202020;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;npm install @screenly/edge-apps
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;npm run dev
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;npm run build&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;Every command works the same way it did under Bun: &lt;code&gt;dev&lt;/code&gt;, &lt;code&gt;build&lt;/code&gt;, &lt;code&gt;build:dev&lt;/code&gt;, &lt;code&gt;lint&lt;/code&gt;,
&lt;code&gt;type-check&lt;/code&gt;, &lt;code&gt;screenshots&lt;/code&gt;, and &lt;code&gt;create&lt;/code&gt;. The recommended scripts for your Edge App&amp;rsquo;s
&lt;code&gt;package.json&lt;/code&gt; are unchanged apart from &lt;code&gt;deploy&lt;/code&gt;, which now reads
&lt;code&gt;npm run build &amp;amp;&amp;amp; screenly edge-app deploy --path=dist/&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Bun still works. &lt;code&gt;bun add @screenly/edge-apps&lt;/code&gt; and &lt;code&gt;bun run dev&lt;/code&gt; read the same &lt;code&gt;package.json&lt;/code&gt;
scripts, so a Bun project can upgrade without changing anything except the CORS proxy script
covered in the next entry.&lt;/p&gt;
&lt;p&gt;To link a local copy of the library while you work on it, run &lt;code&gt;npm install&lt;/code&gt;, &lt;code&gt;npm run build&lt;/code&gt;, and
&lt;code&gt;npm link&lt;/code&gt; in the library, then &lt;code&gt;npm link @screenly/edge-apps&lt;/code&gt; in your Edge App.&lt;/p&gt;</description></item><item><title>Edge Apps library 1.0.0: The CORS proxy is an edge-apps-scripts command now</title><link>https://www.screenly.io/releases/edge-apps/1.0.0/cors-proxy-command/</link><guid isPermaLink="true">https://www.screenly.io/releases/edge-apps/1.0.0/cors-proxy-command/</guid><pubDate>Sat, 02 May 2026 00:00:00 +0000</pubDate><category>improvement</category><category>developers</category><description>&lt;p&gt;The local CORS proxy has its own command:&lt;/p&gt;
&lt;div class="code-block"&gt;
&lt;button type="button" class="code-copy" aria-label="Copy code" data-copy-code&gt;
&lt;svg data-copy viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"&gt;
&lt;rect x="9" y="9" width="11" height="11" rx="2"&gt;&lt;/rect&gt;
&lt;path d="M5 15V5a2 2 0 0 1 2-2h10"&gt;&lt;/path&gt;
&lt;/svg&gt;
&lt;svg data-check viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"&gt;
&lt;path d="M20 6 9 17l-5-5"&gt;&lt;/path&gt;
&lt;/svg&gt;
&lt;/button&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d0d0d0;background-color:#202020;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-json" data-lang="json"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#6ab825;font-weight:bold"&gt;&amp;#34;scripts&amp;#34;&lt;/span&gt;: {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#6ab825;font-weight:bold"&gt;&amp;#34;cors-proxy-server&amp;#34;&lt;/span&gt;: &lt;span style="color:#ed9d13"&gt;&amp;#34;edge-apps-scripts cors-proxy&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;It still listens on &lt;code&gt;0.0.0.0:8080&lt;/code&gt; by default, and &lt;code&gt;HOST&lt;/code&gt; and &lt;code&gt;PORT&lt;/code&gt; change that. Put the target
URL after the proxy&amp;rsquo;s address, as before: &lt;code&gt;http://localhost:8080/https://api.example.com/feed&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The proxy also follows redirects now. When an API answers a &lt;code&gt;GET&lt;/code&gt; or &lt;code&gt;HEAD&lt;/code&gt; request with a 301 to
308, the proxy fetches the new location and returns that response to your app, up to 10 redirects
deep. After 10 it stops and returns a 508 with &lt;code&gt;Too many redirects&lt;/code&gt;. Other methods get the redirect
response itself, unchanged.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;If this affects you:&lt;/strong&gt; the file &lt;code&gt;cors-proxy-server.ts&lt;/code&gt; is gone from the package. If your
&lt;code&gt;package.json&lt;/code&gt; runs it directly, replace that script with &lt;code&gt;edge-apps-scripts cors-proxy&lt;/code&gt; as shown
above.&lt;/p&gt;
&lt;/blockquote&gt;</description></item></channel></rss>