<?xml version="1.0" encoding="UTF-8"?>
<?xml-stylesheet href="/vendor/feed/atom.xsl" type="text/xsl"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en-US">
                        <id>https://ohdear.app/feed</id>
                                <link href="https://ohdear.app/feed" rel="self"></link>
                                <title><![CDATA[ohdear.app]]></title>
                    
                                <subtitle>The description of the feed.</subtitle>
                                                    <updated>2026-03-27T08:31:47+00:00</updated>
                        <entry>
            <title><![CDATA[Enhancing our API for better agentic consumption]]></title>
            <link rel="alternate" href="https://ohdear.app/news-and-updates/enhancing-our-api-for-better-agentic-consumption" />
            <id>https://ohdear.app/183</id>
            <author>
                <name><![CDATA[Oh Dear]]></name>
            </author>
            <summary type="html">
                <![CDATA[<p>AI coding agents like Claude Code and Codex are becoming a real part of developer workflows. They don't just write code, they call APIs, interpret responses, and take action based on what they find. That means the quality of your API responses directly affects how useful an agent can be.</p>
<p>We've shipped a series of improvements to the Oh Dear API with this in mind. Every change helps humans too, but we specifically optimized for how agents consume and reason about data.</p>
<h2 id="historical-check-runs">Historical check runs <a href="#historical-check-runs" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#historical-check-runs">#</a></h2>
<p>Until recently, our API only returned the latest results for each check. That's fine for a quick status check, but it makes it impossible for an agent to answer questions like &quot;compare yesterday's broken links to today's, show me the diff.&quot;</p>
<p>Now you can list all completed runs for any check type:</p>
<pre data-lang="bash" class="notranslate"><span class="hl-keyword">curl</span> https://ohdear.app/api/monitors/1/checks/broken_links/runs \
    <span class="hl-attribute">-H</span> <span class="hl-value">&quot;Authorization: Bearer $OHDEAR_TOKEN&quot;</span> \
    <span class="hl-attribute">-H</span> <span class="hl-value">'Accept: application/json'</span>
</pre>
<pre data-lang="json" class="notranslate"><span class="hl-property">{</span>
  <span class="hl-keyword">&quot;data&quot;</span>: <span class="hl-property">[</span>
    <span class="hl-property">{</span>
      <span class="hl-keyword">&quot;id&quot;</span>: 98765,
      <span class="hl-keyword">&quot;check_type&quot;</span>: <span class="hl-value">&quot;broken_links&quot;</span>,
      <span class="hl-keyword">&quot;result&quot;</span>: <span class="hl-value">&quot;succeeded&quot;</span>,
      <span class="hl-keyword">&quot;started_at&quot;</span>: <span class="hl-value">&quot;2026-03-17T00:30:00+00:00&quot;</span>,
      <span class="hl-keyword">&quot;ended_at&quot;</span>: <span class="hl-value">&quot;2026-03-17T00:53:58+00:00&quot;</span>,
      <span class="hl-keyword">&quot;ohdear_url&quot;</span>: <span class="hl-value">&quot;https://ohdear.app/monitors/1234/check/broken-links/report/98765&quot;</span>
    <span class="hl-property">}</span>,
    <span class="hl-property">{</span>
      <span class="hl-keyword">&quot;id&quot;</span>: 98710,
      <span class="hl-keyword">&quot;check_type&quot;</span>: <span class="hl-value">&quot;broken_links&quot;</span>,
      <span class="hl-keyword">&quot;result&quot;</span>: <span class="hl-value">&quot;failed&quot;</span>,
      <span class="hl-keyword">&quot;started_at&quot;</span>: <span class="hl-value">&quot;2026-03-16T00:30:00+00:00&quot;</span>,
      <span class="hl-keyword">&quot;ended_at&quot;</span>: <span class="hl-value">&quot;2026-03-16T00:48:12+00:00&quot;</span>,
      <span class="hl-keyword">&quot;ohdear_url&quot;</span>: <span class="hl-value">&quot;https://ohdear.app/monitors/1234/check/broken-links/report/98710&quot;</span>
    <span class="hl-property">}</span>
  <span class="hl-property">]</span>
<span class="hl-property">}</span>
</pre>
<p>You can filter by date range and result status:</p>
<table>
<thead>
<tr>
<th>Parameter</th>
<th>Example</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>filter[started_after]</code></td>
<td><code>20260315000000</code></td>
<td>Runs started at or after this UTC timestamp</td>
</tr>
<tr>
<td><code>filter[started_before]</code></td>
<td><code>20260320000000</code></td>
<td>Runs started at or before this UTC timestamp</td>
</tr>
<tr>
<td><code>filter[result]</code></td>
<td><code>failed</code></td>
<td>Only <code>succeeded</code>, <code>warning</code>, or <code>failed</code> runs</td>
</tr>
</tbody>
</table>
<p>Once you have a run ID, pass it to the check endpoint to get the detailed results from that specific run:</p>
<pre data-lang="bash" class="notranslate"><span class="hl-keyword">curl</span> <span class="hl-value">&quot;https://ohdear.app/api/broken-links/1?run_id=98710&quot;</span> \
    <span class="hl-attribute">-H</span> <span class="hl-value">&quot;Authorization: Bearer $OHDEAR_TOKEN&quot;</span>
</pre>
<p>This works for broken links, mixed content, certificate health, and sitemap checks. Runs are kept for approximately 10 days.</p>
<p>For an agent, the workflow becomes straightforward: list runs, pick two, fetch both, diff the results. That's a powerful debugging tool when you're trying to figure out when a new broken link appeared or when your SSL certificate chain changed.</p>
<h2 id="dashboard-links-in-every-response">Dashboard links in every response <a href="#dashboard-links-in-every-response" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#dashboard-links-in-every-response">#</a></h2>
<p>When an agent fetches data from an API, it often needs to show the user where to look for more details. But constructing dashboard URLs from API data is error-prone. Different check types have different URL structures, and an agent would need to know our routing conventions.</p>
<p>We solved this by including an <code>ohdear_url</code> field in every response that links directly to the right dashboard page:</p>
<pre data-lang="json" class="notranslate"><span class="hl-property">{</span>
  <span class="hl-keyword">&quot;meta&quot;</span>: <span class="hl-property">{</span>
    <span class="hl-keyword">&quot;run_id&quot;</span>: 98765,
    <span class="hl-keyword">&quot;run_started_at&quot;</span>: <span class="hl-value">&quot;2026-03-17T00:30:00+00:00&quot;</span>,
    <span class="hl-keyword">&quot;run_ended_at&quot;</span>: <span class="hl-value">&quot;2026-03-17T00:53:58+00:00&quot;</span>,
    <span class="hl-keyword">&quot;ohdear_url&quot;</span>: <span class="hl-value">&quot;https://ohdear.app/monitors/1234/check/broken-links/report/98765&quot;</span>
  <span class="hl-property">}</span>
<span class="hl-property">}</span>
</pre>
<p>The URL points to the exact report page for that specific run. An agent can now say &quot;you have 3 new broken links since yesterday, <a href="https://ohdear.app/monitors/1234/check/broken-links/report/98765">view the full report</a>&quot; with a link that actually works.</p>
<p>This is a small addition, but it removes a whole category of guesswork. The API tells you exactly where to look.</p>
<h2 id="documentation-urls-and-markdown-friendly-docs">Documentation URLs and markdown-friendly docs <a href="#documentation-urls-and-markdown-friendly-docs" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#documentation-urls-and-markdown-friendly-docs">#</a></h2>
<p>Every API response now includes a <code>documentation_url</code> in the <code>links</code> section:</p>
<pre data-lang="json" class="notranslate"><span class="hl-property">{</span>
  <span class="hl-keyword">&quot;links&quot;</span>: <span class="hl-property">{</span>
    <span class="hl-keyword">&quot;documentation_url&quot;</span>: <span class="hl-value">&quot;https://ohdear.app/docs/features/broken-links-detection&quot;</span>
  <span class="hl-property">}</span>
<span class="hl-property">}</span>
</pre>
<p>When an agent encounters a response it doesn't fully understand, it can fetch the documentation URL to learn more. And to top it all off, our docs are served as clean markdown when you ask for it.</p>
<pre data-lang="bash" class="notranslate"><span class="hl-keyword">curl</span> <span class="hl-attribute">-H</span> <span class="hl-value">'Accept: text/markdown'</span> \
    https://ohdear.app/docs/features/broken-links-detection
</pre>
<p>Instead of getting a full HTML page (with navigation, footers, scripts, and all the chrome), the agent gets just the content as markdown. The token savings are significant:</p>
<table>
<thead>
<tr>
<th>Page</th>
<th>HTML</th>
<th>Markdown</th>
<th>Saving</th>
</tr>
</thead>
<tbody>
<tr>
<td><a href="/docs/api/monitors">Monitor API docs</a></td>
<td>386 KB</td>
<td>22.9 KB</td>
<td><strong>-94%</strong></td>
</tr>
<tr>
<td><a href="/docs/features/uptime-and-performance-monitoring">Uptime feature docs</a></td>
<td>370 KB</td>
<td>14.5 KB</td>
<td><strong>-96%</strong></td>
</tr>
</tbody>
</table>
<p>We wrote about <a href="/news-and-updates/making-our-docs-ai-friendly-a-tale-of-two-caches">the caching challenges we ran into</a> when building this feature. The short version: both Cloudflare and our Laravel response cache needed to learn that <code>Accept: text/markdown</code> requests should get different responses than regular browser requests.</p>
<p>The detection logic is simple. We support two ways to request markdown:</p>
<ul>
<li>Append <code>.md</code> to any docs URL: <code>https://ohdear.app/docs/api/monitors.md</code></li>
<li>Send the <code>Accept: text/markdown</code> header</li>
</ul>
<p>Both return plain markdown with a <code>text/markdown</code> content type and a <code>noindex</code> robots tag.</p>
<h2 id="full-list-of-crawled-urls">Full list of crawled URLs <a href="#full-list-of-crawled-urls" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#full-list-of-crawled-urls">#</a></h2>
<p>Our broken links endpoint has always returned the URLs that are broken. But sometimes you need the full picture: how many URLs were crawled, how many are internal vs. external, and what types of resources were found.</p>
<p>We've added two new endpoints for this.</p>
<p><strong>Summary</strong> gives you the counts without the weight of individual URLs:</p>
<pre data-lang="bash" class="notranslate"><span class="hl-keyword">curl</span> https://ohdear.app/api/crawled-urls/1/summary \
    <span class="hl-attribute">-H</span> <span class="hl-value">&quot;Authorization: Bearer $OHDEAR_TOKEN&quot;</span>
</pre>
<pre data-lang="json" class="notranslate"><span class="hl-property">{</span>
  <span class="hl-keyword">&quot;data&quot;</span>: <span class="hl-property">{</span>
    <span class="hl-keyword">&quot;total&quot;</span>: 1234,
    <span class="hl-keyword">&quot;internal&quot;</span>: 800,
    <span class="hl-keyword">&quot;external&quot;</span>: 434,
    <span class="hl-keyword">&quot;by_type&quot;</span>: <span class="hl-property">{</span>
      <span class="hl-keyword">&quot;image&quot;</span>: 250,
      <span class="hl-keyword">&quot;link&quot;</span>: 900,
      <span class="hl-value">&quot;og:image&quot;</span>: 14,
      <span class="hl-keyword">&quot;script&quot;</span>: 40,
      <span class="hl-keyword">&quot;stylesheet&quot;</span>: 30
    <span class="hl-property">}</span>
  <span class="hl-property">}</span>
<span class="hl-property">}</span>
</pre>
<p><strong>Details</strong> returns the full paginated list (100 per page) with the same fields as the broken links response:</p>
<pre data-lang="bash" class="notranslate"><span class="hl-keyword">curl</span> https://ohdear.app/api/crawled-urls/1/details \
    <span class="hl-attribute">-H</span> <span class="hl-value">&quot;Authorization: Bearer $OHDEAR_TOKEN&quot;</span>
</pre>
<p>Each entry now also includes <code>type</code> (link, image, script, stylesheet, og:image) and <code>error_message</code> fields, so you know exactly what kind of resource failed and why.</p>
<p>Both endpoints support <code>run_id</code> for historical data, just like broken links.</p>
<p>This is particularly useful for agents doing site audits. Instead of just knowing what's broken, they can understand the full scope of a crawl and reason about the ratio of broken to healthy URLs.</p>
<h2 id="what-this-unlocks">What this unlocks <a href="#what-this-unlocks" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#what-this-unlocks">#</a></h2>
<p>These changes aren't just incremental API improvements. Together, they create a feedback loop that makes agents more capable when working with Oh Dear.</p>
<p>An agent using Claude Code or Codex can now:</p>
<ol>
<li><strong>List historical runs</strong> and compare results across time periods</li>
<li><strong>Link users to the right dashboard page</strong> instead of generic &quot;check Oh Dear&quot; suggestions</li>
<li><strong>Read our documentation as markdown</strong> when it needs more context, without wasting tokens on HTML chrome</li>
<li><strong>Audit all crawled URLs</strong>, not just the broken ones</li>
</ol>
<p>The pattern we're following is: give the agent everything it needs to be self-sufficient. If it can answer its own questions by following links in the API response, it doesn't need hardcoded knowledge about our URL structure or documentation layout.</p>
<p>We also provide an <a href="/llms.txt">llms.txt</a> file and a <a href="/docs/tools-and-sdks/mcp-server">full MCP server</a> for AI assistants that want deeper integration.</p>
<h2 id="try-it-out">Try it out <a href="#try-it-out" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#try-it-out">#</a></h2>
<p>If you're building with AI agents and want to integrate site monitoring, here's how to get started:</p>
<pre data-lang="bash" class="notranslate"><span class="hl-comment"># List your monitors</span>
<span class="hl-keyword">curl</span> https://ohdear.app/api/monitors \
    <span class="hl-attribute">-H</span> <span class="hl-value">&quot;Authorization: Bearer $OHDEAR_TOKEN&quot;</span>

<span class="hl-comment"># Check historical broken link runs</span>
<span class="hl-keyword">curl</span> https://ohdear.app/api/monitors/1/checks/broken_links/runs \
    <span class="hl-attribute">-H</span> <span class="hl-value">&quot;Authorization: Bearer $OHDEAR_TOKEN&quot;</span>

<span class="hl-comment"># Get a crawl summary</span>
<span class="hl-keyword">curl</span> https://ohdear.app/api/crawled-urls/1/summary \
    <span class="hl-attribute">-H</span> <span class="hl-value">&quot;Authorization: Bearer $OHDEAR_TOKEN&quot;</span>

<span class="hl-comment"># Read our docs as markdown</span>
<span class="hl-keyword">curl</span> <span class="hl-attribute">-H</span> <span class="hl-value">'Accept: text/markdown'</span> https://ohdear.app/docs/api/monitors
</pre>
<p>All of these changes are live now and documented in our <a href="/docs/api/introduction">API docs</a>.</p>
<h2 id="feedback">Feedback? <a href="#feedback" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#feedback">#</a></h2>
<p>If you run into anything unexpected or have ideas for making the API more agent-friendly, reach out via <a href="mailto:mattias@ohdear.app">mattias@ohdear.app</a> or ping <a href="https://x.com/mattiasgeniar">@mattiasgeniar</a> or <a href="https://x.com/ohdearapp">@OhDearApp</a>. We'd love to hear how you're using these features!</p>
]]>
            </summary>
                                    <updated>2026-03-27T08:31:47+00:00</updated>
        </entry>
            <entry>
            <title><![CDATA[An Oh Dear skill for use in Claude Code or Codex]]></title>
            <link rel="alternate" href="https://ohdear.app/news-and-updates/an-oh-dear-skill-for-use-in-claude-code-or-codex" />
            <id>https://ohdear.app/182</id>
            <author>
                <name><![CDATA[Oh Dear]]></name>
            </author>
            <summary type="html">
                <![CDATA[<p>AI coding agents are getting good at calling tools. Claude Code, Codex, and others can run shell commands, parse JSON, and reason about the results. But they need to know what tools are available and how to use them.</p>
<p>That's what skills are for. A skill is a small package of documentation that teaches an AI agent how to use a specific tool. We've built one for Oh Dear.</p>
<h2 id="what-this-gives-you">What this gives you <a href="#what-this-gives-you" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#what-this-gives-you">#</a></h2>
<p>Once installed, your AI coding agent can do anything the Oh Dear CLI can do. That covers every endpoint in the Oh Dear API: listing monitors, checking uptime, reviewing broken links, managing maintenance windows, posting status page updates, and more.</p>
<p>You don't need to remember command names or look up parameter syntax. You describe what you want in plain language, and the agent figures out the right commands.</p>
<h2 id="installing-the-skill">Installing the skill <a href="#installing-the-skill" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#installing-the-skill">#</a></h2>
<p>Two steps. First, install the Oh Dear CLI if you haven't already:</p>
<pre data-lang="bash" class="notranslate">$ <span class="hl-keyword">composer</span> global require ohdearapp/ohdear-cli
$ ohdear login
</pre>
<p>You'll be prompted for your API token. Generate one at <a href="/user/api-tokens">your API token page</a>.</p>
<p>Then install the skill:</p>
<pre data-lang="bash" class="notranslate">$ ohdear install-skill
</pre>
<p>That's it. Your AI agent now knows how to work with Oh Dear.</p>
<h2 id="what-you-can-ask">What you can ask <a href="#what-you-can-ask" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#what-you-can-ask">#</a></h2>
<p>Here's where it gets interesting. These are real prompts, not hypotheticals.</p>
<h3 id="quotis-examplecom-up-right-nowquot">&quot;Is example.com up right now?&quot;</h3>
<p>The simplest question. The agent runs <code>ohdear get-monitor</code>, checks the status, and tells you. No need to open a browser or switch tabs.</p>
<h3 id="quotcompare-yesterdays-broken-links-to-todaysquot">&quot;Compare yesterday's broken links to today's&quot;</h3>
<p>This is the kind of task that used to require manual work: opening two reports side by side in the dashboard. Now the agent lists historical runs, fetches both, diffs the results, and tells you exactly which links are new, which got fixed, and which are still broken.</p>
<pre data-lang="txt" class="notranslate">&gt; Compare the broken links from March 17 to March 20 for ohdear.app

Tool calls:
  ohdear list-check-runs --monitor-id=42 --check-type=broken_links --json
  ohdear list-broken-links --monitor-id=42 --run-id=98765 --json
  ohdear list-broken-links --monitor-id=42 --run-id=98710 --json

Result:
  3 new broken links found
  2 links fixed since March 17
  7 persistent broken links across both runs
</pre>
<p>We built <a href="/docs/api/check-history">historical check run endpoints</a> specifically to make this kind of workflow possible.</p>
<h3 id="quotstart-a-maintenance-window-and-post-a-status-updatequot">&quot;Start a maintenance window and post a status update&quot;</h3>
<p>Deploying? Tell your agent to handle the communication:</p>
<pre data-lang="txt" class="notranslate">&gt; Start a 30-minute maintenance window on ohdear.app
  and post a status update: &quot;Deploying v2.4&quot;

Tool calls:
  ohdear start-maintenance --monitor-id=42
    --field stop_maintenance_after_seconds=1800
  ohdear create-status-page-update
    --field status_page_id=123
    --field title=&quot;Deploying v2.4&quot;

Result:
  Maintenance window started (30 min)
  Status page updated: &quot;Deploying v2.4&quot;
</pre>
<p>No false downtime alerts. Your users see the status update. You stay in your terminal.</p>
<h3 id="quotour-checkout-is-down-post-a-status-update-and-snooze-the-uptime-check-for-1-hourquot">&quot;Our checkout is down. Post a status update and snooze the uptime check for 1 hour.&quot;</h3>
<p>Incident response from your editor. The agent posts the update to your status page and snoozes the check so you're not getting pinged while you're actively fixing the problem.</p>
<h3 id="quotcross-reference-sitescsv-with-our-oh-dear-monitors-which-ones-are-missingquot">&quot;Cross-reference sites.csv with our Oh Dear monitors. Which ones are missing?&quot;</h3>
<p>This is one of my favorites. The agent reads your file, fetches your monitor list as JSON, compares the two, and tells you which sites aren't being monitored. Try doing that in a web dashboard. 😅</p>
<h3 id="quotwhat-do-the-latest-lighthouse-scores-look-like-for-my-homepagequot">&quot;What do the latest Lighthouse scores look like for my homepage?&quot;</h3>
<p>Quick performance check without leaving your editor. The agent fetches the latest Lighthouse report and summarizes the scores for performance, accessibility, best practices, and SEO.</p>
<pre data-lang="txt" class="notranslate">ohdear.app Lighthouse scores (March 22, desktop from Frankfurt):

┌────────────────┬───────┐
│    Category    │ Score │
├────────────────┼───────┤
│ Performance    │ 81    │
├────────────────┼───────┤
│ Accessibility  │ 96    │
├────────────────┼───────┤
│ Best Practices │ 100   │
├────────────────┼───────┤
│ SEO            │ 100   │
└────────────────┴───────┘

Core Web Vitals:
- FCP: 495ms, LCP: 862ms (both good)
- CLS: 0 (perfect)
- TBT: 415ms (the main drag on the performance score)
- TTI: 1.3s
</pre>
<h3 id="quotshow-me-the-certificate-health-for-all-my-monitorsquot">&quot;Show me the certificate health for all my monitors&quot;</h3>
<p>Bulk SSL overview. The agent loops through your monitors, checks certificate health on each, and flags anything expiring soon or with chain issues.</p>
<pre data-lang="txt" class="notranslate">All done. Here's the summary:

80 monitors with certificate health enabled. All clear.

- 79 monitors: All certificates healthy, no issues found
- 1 monitor (18953, uptime-checker-bahrain-1): Certificate check hasn't run yet, no data available

Nearest expirations to keep an eye on:
- uptime-checker-frankfurt-2 (11108): Apr 14 (3 weeks)
- uptime-checker-frankfurt-4 (13332): Apr 15
- uptime-checker-paris-4 (13335): Apr 17
- uptime-checker-toronto-2 (13333): Apr 15

These are all Let's Encrypt certs that auto-renew, so nothing to worry about.
</pre>
<p>The Bahrain node is interesting, it's hosted at the Bahrain AWS datacenter that's been down for a while due to bombing/collatoral damage near the UAE.</p>
<h2 id="how-the-cli-stays-in-sync-with-the-api">How the CLI stays in sync with the API <a href="#how-the-cli-stays-in-sync-with-the-api" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#how-the-cli-stays-in-sync-with-the-api">#</a></h2>
<p>One thing worth mentioning: the Oh Dear CLI is auto-generated from our <a href="https://ohdear.app/api-docs/ohdear-openapi.yml">OpenAPI specification</a>. When we add a new API endpoint, it automatically becomes a CLI command. No manual work, no lag between API and CLI.</p>
<p>This is powered by <a href="https://github.com/spatie/laravel-openapi-cli">spatie/laravel-openapi-cli</a>, which reads the OpenAPI spec at runtime and generates commands from it. The operation IDs in the spec map directly to CLI command names in kebab-case: <code>listMonitors</code> becomes <code>list-monitors</code>, <code>getCertificateHealth</code> becomes <code>get-certificate-health</code>.</p>
<p>For the skill, this means it always covers the full API surface. If we ship a new endpoint today, the CLI has it tomorrow and the skill can use it immediately.</p>
<h2 id="lessons-from-building-the-skill">Lessons from building the skill <a href="#lessons-from-building-the-skill" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#lessons-from-building-the-skill">#</a></h2>
<p>The skill went through several iterations before we landed on something that works well. The commit history tells the story:</p>
<p><strong>First attempt: document everything.</strong> We started with exhaustive documentation of every command, every parameter, every output format. The result was a massive prompt that burned through context window tokens.</p>
<p><strong>Second iteration: trim it down.</strong> We realized agents are good at discovery. Instead of documenting every flag, we document the patterns: how commands are named, how to use <code>--json</code> for structured output, how to use <code>--help</code> to learn more. The commit message says it well: <em>&quot;let agents auto-discover with enough hints on what's available.&quot;</em></p>
<p><strong>Third iteration: add workflows.</strong> Raw command documentation wasn't enough. We added step-by-step workflows for common tasks: investigating downtime, comparing historical data, managing maintenance windows. These give agents a playbook to follow.</p>
<p><strong>Final tweaks: enforce good habits.</strong> We added rules like &quot;always use <code>--json</code> when you need to analyze data&quot; and &quot;when you see an ohdear.app URL, use the CLI to fetch the data.&quot; These small nudges make the agent significantly more useful in practice.</p>
<p>The takeaway: good skill design isn't about completeness, it's about giving the agent the right level of guidance. Too little and it guesses wrong. Too much and you waste context tokens on information the agent can discover on its own.</p>
<p>You can see the <a href="https://github.com/ohdearapp/ohdear-cli/tree/main/skills/ohdear">entire SKILL.md and worfklows</a> on Github.</p>
<h2 id="skills-protocol">Skills protocol <a href="#skills-protocol" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#skills-protocol">#</a></h2>
<p>The Oh Dear skill uses the <a href="https://skills.sh">Skills protocol</a>, an open standard for teaching AI agents how to use tools. Any agent that supports the protocol can use our skill, not just Claude Code.</p>
<h2 id="try-it">Try it <a href="#try-it" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#try-it">#</a></h2>
<pre data-lang="bash" class="notranslate"><span class="hl-keyword">composer</span> global require ohdearapp/ohdear-cli
ohdear login
ohdear install-skill
</pre>
<p>Then ask your agent something like &quot;are there any broken links on my site?&quot; and watch it work.</p>
<p>The skill, CLI source, and all documentation are open source at <a href="https://github.com/ohdearapp/ohdear-cli">github.com/ohdearapp/ohdear-cli</a>.</p>
<h2 id="feedback">Feedback? <a href="#feedback" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#feedback">#</a></h2>
<p>If you run into anything unexpected or have ideas for the skill, reach out via <a href="mailto:support@ohdear.app">support@ohdear.app</a> or <a href="https://x.com/ohdearapp">@OhDearApp</a>. We'd love to hear how you're using this.</p>
]]>
            </summary>
                                    <updated>2026-03-25T09:00:03+00:00</updated>
        </entry>
            <entry>
            <title><![CDATA[Dark mode is now available for the Oh Dear dashboard]]></title>
            <link rel="alternate" href="https://ohdear.app/news-and-updates/dark-mode-is-now-available-for-the-oh-dear-dashboard" />
            <id>https://ohdear.app/181</id>
            <author>
                <name><![CDATA[Oh Dear]]></name>
            </author>
            <summary type="html">
                <![CDATA[<p>Oh Dear's dashboard now supports dark mode. You can choose between light, dark, or system-based theming, and your preference is saved to your profile so it follows you everywhere.</p>
<h2 id="three-ways-to-theme-your-dashboard">Three ways to theme your dashboard <a href="#three-ways-to-theme-your-dashboard" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#three-ways-to-theme-your-dashboard">#</a></h2>
<p>We've added three theme options to your profile settings:</p>
<ul>
<li><strong>Light</strong>: The classic Oh Dear look you know.</li>
<li><strong>Dark</strong>: A carefully tuned dark palette across the entire app.</li>
<li><strong>System</strong>: Follows your operating system's preference automatically.</li>
</ul>
<p><img src="/media/blog/8X1penS6JIk3P9YDywgJdkcn97LhkvPgL7E2vwnD.gif" alt="Oh Dear - configure dark mode" /></p>
<p>Your theme preference is saved to your account, not just your browser. That means it applies on every device, every session, and every tab you open.</p>
<p><img src="/media/blog/0uJgNHkAPIIy0cWQC1w3wN3AoKBgWjPWhFAQXtPo.png" alt="Oh Dear - Dashboard in dark mode" /></p>
<h2 id="no-more-flashbangs-at-3-am">No more flashbangs at 3 AM <a href="#no-more-flashbangs-at-3-am" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#no-more-flashbangs-at-3-am">#</a></h2>
<p>When your site goes down at 3 AM and you open the Oh Dear alert from Slack, the last thing you want is a blinding white screen burning your eyes.</p>
<p>If you've set your theme to &quot;dark&quot; or &quot;system&quot; (and your OS is in dark mode), your dashboard stays dark. Always. No matter how you got there, no matter which device you're on. Your eyes will thank you.</p>
<p>We specifically made sure the theme is saved on the profile, not just in a cookie or localStorage. When you click through from a Slack notification at midnight, your dark mode is already waiting for you.</p>
<p>Additional benefit: now that <a href="/news-and-updates/oh-dear-is-now-mobile-friendly">the dashboard is also mobile friendly</a>, you won't only get a good-looking dark mode, but a well-rendered version of any page you visit.</p>
<h2 id="it-just-looks-really-good">It just looks really good <a href="#it-just-looks-really-good" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#it-just-looks-really-good">#</a></h2>
<p>Dark mode isn't just a comfort feature. It looks great.</p>
<p><img src="/media/blog/7cy7FQzOc67u0MKTslzVrrmx7cU5WaLhCp9oNvTP.png" alt="Oh Dear - Uptime page in dark mode" /></p>
<p>Charts, tables, status indicators, alert boxes, everything got its own carefully chosen dark palette. We didn't just invert colors. We picked backgrounds, borders, text contrasts, and accent colors that feel right together.</p>
<p><img src="/media/blog/j9Cp8UY9cPWJTalJSyHTR3WZ2wSPyPPu9HR0j4J9.png" alt="Oh Dear - Broken links page in dark mode" /></p>
<p>Even the performance charts have dedicated dark mode color sets, so your timing breakdowns are just as readable at any hour.</p>
<h2 id="349-files-and-a-lot-of-dark-prefixes">349 files, and a lot of <code>dark:</code> prefixes <a href="#349-files-and-a-lot-of-dark-prefixes" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#349-files-and-a-lot-of-dark-prefixes">#</a></h2>
<p>This was a big update. The PR touched 349 files with 2,718 additions and 1,531 deletions across the entire app. Every template, every component, every form, every table needed dark mode variants.</p>
<p>That's the kind of work where agentic coding really shines.</p>
<h2 id="how-we-built-it-ai-for-the-repetitive-parts-humans-for-the-creative-parts">How we built it: AI for the repetitive parts, humans for the creative parts <a href="#how-we-built-it-ai-for-the-repetitive-parts-humans-for-the-creative-parts" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#how-we-built-it-ai-for-the-repetitive-parts-humans-for-the-creative-parts">#</a></h2>
<p>If you read our <a href="https://ma.ttias.be/mobile-friendly-oh-dear-ui-with-ai/">previous post about making Oh Dear mobile-friendly</a>, this will sound familiar. The pattern was the same: let AI handle the tedious, repetitive work, and focus human effort on the parts that actually matter.</p>
<h3 id="the-agentic-scaffold">The agentic scaffold</h3>
<p>We used Claude Code to generate the initial dark mode implementation across the entire codebase. The work was broken down into 8 user stories:</p>
<ol>
<li>Add theme preference to the user model</li>
<li>Serve the theme preference to the app layout</li>
<li>Build a theme switcher on the profile page</li>
<li>Dark mode styles for the app layout shell</li>
<li>Dark mode styles for content cards and panels</li>
<li>Dark mode styles for form elements</li>
<li>Dark mode styles for tables and data displays</li>
<li>Dark mode styles for check report pages</li>
</ol>
<p>Each of these was committed individually, with Claude as co-author. The agent worked through every Blade template, adding <code>dark:</code> Tailwind classes to backgrounds, text colors, borders, shadows, and interactive states. Hundreds of files, thousands of class additions, all done in a single session.</p>
<p>This is where AI is genuinely useful: applying a known pattern (<code>dark:bg-gray-900</code>, <code>dark:text-gray-200</code>, <code>dark:border-gray-700</code>) consistently across 349 files. It's not creative work. It's precise, repetitive, and easy to get wrong if you're doing it by hand.</p>
<h3 id="the-human-side-getting-the-colors-right">The human side: getting the colors right</h3>
<p>Adding <code>dark:</code> classes is the easy part. Choosing <em>which</em> colors, and making them feel cohesive across the entire app, that's where human judgment comes in. In our case, that human is called <a href="https://x.com/nckrtl">Nick</a>.</p>
<p>The initial AI pass gave us structurally correct dark mode. Every element had a dark variant. But the palette didn't feel right. Backgrounds were too dark or too light. Text contrast was inconsistent. Borders disappeared or felt too harsh. The charts looked washed out.</p>
<p>Getting the theme right required sitting with the app, clicking through every page, and fine-tuning values until the whole thing felt intentional. Not just technically correct, but aesthetically coherent.</p>
<p>That's design work, and it's the part AI can't (yet?) do for you. It doesn't know that a particular shade of gray makes a card feel heavy, or that reducing border opacity by 10% makes a table feel lighter. It doesn't have opinions about whether a status indicator should be muted or vibrant in dark mode. Those are judgment calls that need a human eye.</p>
<h3 id="the-technical-foundation">The technical foundation</h3>
<p>A few things we got right from the start:</p>
<p><strong>Tailwind's <code>class</code> strategy</strong>: We use <code>darkMode: 'class'</code> in our Tailwind config, which toggles dark mode by adding a <code>dark</code> class to the <code>&lt;html&gt;</code> element. This gives us full control and avoids relying solely on the system media query.</p>
<p><strong>No flash on page load</strong>: The theme script runs in the <code>&lt;head&gt;</code>, before any content renders. Whether you're in dark or light mode, you'll never see a white flash followed by a dark switch. It's instant.</p>
<p><strong>Real-time system detection</strong>: If you've chosen &quot;system&quot; mode, we listen for OS-level theme changes. Switch your Mac to dark mode, and the Oh Dear dashboard follows immediately, no refresh needed.</p>
<p><strong>Persistent preference</strong>: Your theme choice is saved to the database via your profile, with a localStorage fallback for immediate application. This means it works everywhere, instantly.</p>
<h2 id="give-it-a-try">Give it a try <a href="#give-it-a-try" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#give-it-a-try">#</a></h2>
<p>Dark mode is available now for all Oh Dear users. Head to your profile settings and pick the theme that works for you.</p>
<p><img src="/media/blog/t8WZWWImECAHrfluX5AlLBcp047cynHWNwqdHRnr.png" alt="Oh Dear - Theme switcher on mobile" /></p>
<p>And if you're the type who keeps their phone on dark mode 24/7, just set it to &quot;system&quot; and forget about it. Oh Dear will match your preference automatically.</p>
]]>
            </summary>
                                    <updated>2026-03-23T14:11:23+00:00</updated>
        </entry>
            <entry>
            <title><![CDATA[Oh Dear is now mobile-friendly]]></title>
            <link rel="alternate" href="https://ohdear.app/news-and-updates/oh-dear-is-now-mobile-friendly" />
            <id>https://ohdear.app/180</id>
            <author>
                <name><![CDATA[Oh Dear]]></name>
            </author>
            <summary type="html">
                <![CDATA[<p>Oh Dear has always been a desktop-first tool. If you checked your monitors on your phone, you'd get the full desktop layout squeezed into a tiny screen, with lots of horizontal scrolling and tiny tap targets. That's fixed now. Every page in the app works on mobile.</p>
<p><img src="/media/blog/npkyKRnXD3ZTkt8eiLZiYOmWBxJVXV46PE4D3dp0.png" alt="The Oh Dear app is optimized for mobile!" /></p>
<h2 id="what-changed">What changed <a href="#what-changed" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#what-changed">#</a></h2>
<p>We didn't just slap some media queries on the existing layout. Some parts of the app needed a completely different approach on small screens. Here's what we did:</p>
<ul>
<li><strong>New mobile navigation</strong> with a floating action button and full-screen menu overlay</li>
<li><strong>Dedicated mobile card layouts</strong> for monitor lists and status pages</li>
<li><strong>Scrollable tables</strong> with a visual hint that there's more to see</li>
<li><strong>Larger touch targets</strong> for toggles, checkboxes, and dropdown items</li>
<li><strong>Responsive check report pages</strong> for uptime, SSL, DNS, performance, broken links, and all other checks</li>
<li><strong>Mobile-friendly modals, forms, and settings pages</strong></li>
</ul>
<p>We touched over 160 Blade templates across the entire app.</p>
<p><img src="/media/blog/LCishW9OcaV4OqfbX2l6OZjkP9XW2rpPKaAfi4Gi.png" alt="Oh Dear - Dashboard" /></p>
<h2 id="optimize-mobile-views-when-you-need-them-the-most">Optimize mobile views when you need them the most <a href="#optimize-mobile-views-when-you-need-them-the-most" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#optimize-mobile-views-when-you-need-them-the-most">#</a></h2>
<p>We send you alerts when we notice something doesn't seem right. That can be website downtime, broken links, failing scheduled tasks, ... You're probably going to click through on these alerts from inside your Slack, Discord, Teams or any other notification type we offer.</p>
<p>The page you're landing on, likely on your mobile device, needs to be optimized for that <em>unhappy state</em>: you want all the details of the problem we're reporting, at your fingertips.</p>
<p>The new layout is optimized for exactly that: getting all the necessary details, clearly visible, on mobile.</p>
<ul>
<li>Tables with multiple columns are replaced by stacked, labeled cards, so nothing gets clipped or overlaps</li>
<li>Download and action buttons are always visible, never hidden behind menus</li>
<li>Page headers and action buttons stack naturally instead of wrapping awkwardly</li>
<li>&quot;Copy to clipboard&quot; icons are available where relevant, to easily share error messages, HTTP headers or reproduction steps with colleagues</li>
</ul>
<p>When you get that alert that something is down, you're already in a stressful moment. The last thing you need is to fight the UI to find the details. Everything you need to diagnose and communicate the problem is right there, on whatever device you happen to have in your hand.</p>
<p><img src="/media/blog/iL2wO4zveXhfG2bUymbkCWgX8jgB5OECSTeIqN3r.png" alt="Oh Dear - Before and after site dashboard" /></p>
<h2 id="the-floating-menu">The floating menu <a href="#the-floating-menu" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#the-floating-menu">#</a></h2>
<p>On desktop, Oh Dear has a horizontal navbar with dropdowns for switching teams, accessing settings, and navigating between monitors and status pages. That doesn't work on a phone.</p>
<p>Instead of cramming the navbar into a hamburger menu at the top, we went with a floating action button (FAB) fixed to the bottom-right corner. It's always within thumb reach.</p>
<pre data-lang="blade" class="notranslate">&lt;<span class="hl-keyword">button</span>
    <span class="hl-property">class</span>=&quot;fixed bottom-6 right-6 z-[60] flex items-center justify-center
           w-12 h-12 text-white rounded-full transform-gpu bg-primary
           md:hidden shadow-glow-primary&quot;
    <span class="hl-keyword">@click</span>=&quot;$store.appMobileMenu.isOpen = !$store.appMobileMenu.isOpen&quot;
&gt;
</pre>
<p><img src="/media/blog/XAKVxvPEAPuCI62Nv2AZHik6wUCejC5b9obNnnIX.png" alt="Oh Dear - Floating Menu Button" /></p>
<p>Tapping it opens a full-screen overlay with all the navigation options, organized vertically with generous padding for easy tapping. The menu uses an Alpine store (<code>$store.appMobileMenu</code>) as a shared state between the button and the overlay, which keeps things simple.</p>
<p>One nice detail: when the menu is open, we add <code>overflow-hidden</code> to the body so the page behind it doesn't scroll.</p>
<pre data-lang="blade" class="notranslate">&lt;<span class="hl-keyword">div</span>
    x-data
    <span class="hl-property">x-effect</span>=&quot;document.body.classList.toggle('overflow-hidden', $store.appMobileMenu.isOpen);
              document.body.classList.toggle('mobile-menu-open', $store.appMobileMenu.isOpen)&quot;
&gt;
</pre>
<p>The <code>mobile-menu-open</code> class also repositions our support bubble widget so it doesn't overlap with the menu button.</p>
<p><img src="/media/blog/Ep0RsggBrM9dccHUpDUzHdhln8kf76ySwwfCBDcx.png" alt="" /></p>
<h2 id="two-layouts-instead-of-one">Two layouts instead of one <a href="#two-layouts-instead-of-one" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#two-layouts-instead-of-one">#</a></h2>
<p>The monitor list is probably the most complex page in the app. On desktop, it's a multi-column table with performance sparklines, issue counts, and a hover-revealed actions menu.</p>
<p><img src="/media/blog/ybNvmkNldE3mojsM5b0cQJPBI63Y9ILzDC6d85cb.png" alt="The desktop monitor list showing the multi-column table with performance sparklines" /></p>
<p>Making that responsive with pure CSS would mean hiding columns, stacking cells, and losing the at-a-glance overview that makes the table useful. So we went with a different approach: render two completely separate layouts and show the right one based on screen size.</p>
<pre data-lang="blade" class="notranslate"><span class="hl-comment">{{-- Desktop table layout --}}</span>
&lt;<span class="hl-keyword">table</span> <span class="hl-property">class</span>=&quot;hidden md:table w-full tabular-nums table-fixed&quot;&gt;
    <span class="hl-keyword">@include</span>(<span class="hl-value">'app.monitors.list.components.monitorListRow'</span>)
&lt;/<span class="hl-keyword">table</span>&gt;

<span class="hl-comment">{{-- Mobile card layout --}}</span>
&lt;<span class="hl-keyword">div</span> <span class="hl-property">class</span>=&quot;md:hidden divide-y divide-gray-150&quot;&gt;
    <span class="hl-keyword">@foreach</span>(<span class="hl-variable">$monitorsInGroup</span> <span class="hl-keyword">as</span> <span class="hl-variable">$monitor</span>)
        <span class="hl-keyword">@include</span>(<span class="hl-value">'app.monitors.list.components.monitorListRowMobile'</span>)
    <span class="hl-keyword">@endforeach</span>
&lt;/<span class="hl-keyword">div</span>&gt;
</pre>
<p>The mobile card packs the essential info into a vertical layout: status indicator and name on top, tags inline, a mini performance chart below, and check summary messages as a list. Every row has a minimum height of 44px for reliable touch targets.</p>
<p>This is more code to maintain, but the result is clean on both screen sizes. No CSS hacks, no overflow issues, no squished text.</p>
<h2 id="scrollable-tables-with-a-fade-hint">Scrollable tables with a fade hint <a href="#scrollable-tables-with-a-fade-hint" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#scrollable-tables-with-a-fade-hint">#</a></h2>
<p>Not every table needed a mobile-specific layout. For simpler tables (DNS records, certificate chains, downtime history), horizontal scrolling works fine. But users need to know the table extends beyond what they can see.</p>
<p>We built a small <code>scrollable-table</code> component that wraps any table and adds a gradient fade on the right edge:</p>
<pre data-lang="blade" class="notranslate">&lt;<span class="hl-keyword">div</span>
    <span class="hl-property">x-data</span>=&quot;{
        canScroll: false,
        atEnd: false,
        check() {
            const <span class="hl-property">el</span> = this.$refs.scroller;
            this.<span class="hl-property">canScroll</span> = el.scrollWidth &gt; el.clientWidth;
            this.<span class="hl-property">atEnd</span> = el.scrollLeft + el.clientWidth &gt;= el.scrollWidth - 1;
        }
    }&quot;
    <span class="hl-property">x-init</span>=&quot;check()&quot;
    x-on:resize.<span class="hl-property">window</span>=&quot;check()&quot;
&gt;
    &lt;<span class="hl-keyword">div</span> <span class="hl-property">class</span>=&quot;scrollable-table-inner&quot; <span class="hl-property">x-ref</span>=&quot;scroller&quot; <span class="hl-property">x-on:scroll</span>=&quot;check()&quot;&gt;
        {{ <span class="hl-variable">$slot</span> }}
    &lt;/<span class="hl-keyword">div</span>&gt;
    &lt;<span class="hl-keyword">div</span> <span class="hl-property">class</span>=&quot;scrollable-table-fade&quot; <span class="hl-property">x-show</span>=&quot;canScroll &amp;&amp; !atEnd&quot; x-cloak&gt;&lt;/<span class="hl-keyword">div</span>&gt;
&lt;/<span class="hl-keyword">div</span>&gt;
</pre>
<p>The fade automatically disappears once you scroll to the end. It only shows on screens where the table actually overflows, and it's hidden entirely on desktop where tables fit comfortably.</p>
<h2 id="bigger-touch-targets">Bigger touch targets <a href="#bigger-touch-targets" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#bigger-touch-targets">#</a></h2>
<p>Small toggles and checkboxes are a constant frustration on mobile. We scaled up all form controls on small screens:</p>
<pre data-lang="css" class="notranslate"><span class="hl-keyword">@media (width &lt; theme(--breakpoint-mg)) </span>{<span class="hl-keyword">
    .check-toggle </span>{
        @apply w-11 h-6;
    }<span class="hl-keyword">

    [type='checkbox'],
    [type='radio'] </span>{
        <span class="hl-property">height</span>: 1.25rem;
        <span class="hl-property">width</span>: 1.25rem;
    }
}
</pre>
<p>Dropdown menu items also get extra vertical padding on mobile, bringing them to a comfortable 44px touch height.</p>
<p><img src="/media/blog/KSblWuG9WlEoNhsNfVYYT79ZGNYd3gU4omKDPjOO.png" alt="Oh Dear - Mobile view certificate health" /></p>
<h2 id="try-it-out">Try it out <a href="#try-it-out" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#try-it-out">#</a></h2>
<p>If you're an Oh Dear user, open the app on your phone. Everything should just work. Monitor lists, check reports, settings, notifications, status pages, all of it.</p>
<p>If something looks off on your device, let us know at <a href="mailto:support@ohdear.app">support@ohdear.app</a> or at <a href="https://x.com/ohdearapp">@OhDearApp</a> on X.</p>
]]>
            </summary>
                                    <updated>2026-03-06T08:47:50+00:00</updated>
        </entry>
            <entry>
            <title><![CDATA[DNS blocklist monitoring now available to all Oh Dear users]]></title>
            <link rel="alternate" href="https://ohdear.app/news-and-updates/dns-blocklist-monitoring-now-available-to-all-oh-dear-users" />
            <id>https://ohdear.app/179</id>
            <author>
                <name><![CDATA[Oh Dear]]></name>
            </author>
            <summary type="html">
                <![CDATA[<p><em>Your domain is on a spam blocklist. Password reset emails aren't arriving, order confirmations land in spam, and customers are complaining that &quot;your site doesn't work.&quot; By the time you hear about it, the damage has been building for days.</em></p>
<p>We've shipped <strong>DNS blocklist monitoring</strong> to catch this early. Oh Dear now checks your domain against 11 major blocklists and notifies you the moment you're listed, with direct links to get removed.</p>
<h2 id="two-kinds-of-blocklists-two-kinds-of-pain">Two kinds of blocklists, two kinds of pain <a href="#two-kinds-of-blocklists-two-kinds-of-pain" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#two-kinds-of-blocklists-two-kinds-of-pain">#</a></h2>
<p>Not all blocklists are the same. We monitor two categories:</p>
<p><strong>Security blocklists (RBLs)</strong> are the ones that wreck your email deliverability. If your IP or domain appears on Barracuda, SpamCop, SURBL, or URLhaus, email servers will reject or spam-folder your messages. Password resets, order confirmations, support replies: silently gone.</p>
<p><strong>Content filter DNS services</strong> block your site entirely for users behind DNS-level filtering. If Quad9, Cloudflare Family, AdGuard, or OpenDNS blocks your domain, anyone using those services (directly, or through tools like Pi-hole and AdGuard Home) simply can't reach you. That's millions of privacy-conscious users, corporate networks, and families with parental controls.</p>
<h2 id="the-11-blocklists-we-check">The 11 blocklists we check <a href="#the-11-blocklists-we-check" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#the-11-blocklists-we-check">#</a></h2>
<table>
<thead>
<tr>
<th>Category</th>
<th>Provider</th>
<th>What it catches</th>
</tr>
</thead>
<tbody>
<tr>
<td>Security</td>
<td>Barracuda</td>
<td>IP reputation</td>
</tr>
<tr>
<td>Security</td>
<td>SpamCop</td>
<td>Real-time spam reports</td>
</tr>
<tr>
<td>Security</td>
<td>SURBL</td>
<td>Spam, phishing, malware domains</td>
</tr>
<tr>
<td>Security</td>
<td>URLhaus</td>
<td>Malware distribution URLs</td>
</tr>
<tr>
<td>Content filter</td>
<td>AdGuard DNS</td>
<td>Ads, trackers, malware</td>
</tr>
<tr>
<td>Content filter</td>
<td>AdGuard Family</td>
<td>Ads, trackers, adult content</td>
</tr>
<tr>
<td>Content filter</td>
<td>CleanBrowsing</td>
<td>Malware and phishing</td>
</tr>
<tr>
<td>Content filter</td>
<td>Cloudflare Family</td>
<td>Malware and adult content</td>
</tr>
<tr>
<td>Content filter</td>
<td>OpenDNS</td>
<td>Malware and phishing</td>
</tr>
<tr>
<td>Content filter</td>
<td>OpenDNS Family</td>
<td>Malware, phishing, adult content</td>
</tr>
<tr>
<td>Content filter</td>
<td>Quad9</td>
<td>Malware and phishing</td>
</tr>
</tbody>
</table>
<h2 id="how-it-works">How it works <a href="#how-it-works" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#how-it-works">#</a></h2>
<p>For security blocklists, we perform standard DNSBL lookups. IP-based lists like Barracuda get a reversed-IP query against their DNS zone. Domain-based lists like SURBL get a direct domain query. A DNS response means you're listed.</p>
<p>For content filter services, we resolve your domain through each provider's DNS server. If the response is <code>NXDOMAIN</code> or a known block-page IP (like <code>0.0.0.0</code> for AdGuard), your domain is blocked by that service.</p>
<p>Checks run daily by default. You can increase the frequency to every 4 hours if you want tighter coverage, or pick specific blocklists instead of monitoring all 11.</p>
<h2 id="what-happens-when-youre-listed">What happens when you're listed <a href="#what-happens-when-youre-listed" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#what-happens-when-youre-listed">#</a></h2>
<p>You get a notification through whichever channels you've configured: email, Slack, Discord, Teams, Telegram, PagerDuty, Opsgenie, Pushover, SMS, webhooks, ntfy, or Google Chat.</p>
<p>The notification tells you exactly <strong>which</strong> blocklist flagged you and why. Your dashboard shows direct links to each provider's delisting form, so you can start the removal process immediately instead of researching where to go. All the info you need, at your fingertips.</p>
<p>When you're removed from all blocklists, you'll get a recovery notification confirming the all-clear.</p>
<p>If you're working on a fix and don't want repeated alerts, you can snooze the check.</p>
<h2 id="its-probably-not-your-fault">It's probably not your fault <a href="#its-probably-not-your-fault" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#its-probably-not-your-fault">#</a></h2>
<p>Domains get blacklisted for reasons beyond your control:</p>
<ul>
<li><strong>Shared hosting</strong>: a neighbor on your IP sends spam, your IP gets flagged</li>
<li><strong>Domain history</strong>: you registered a domain previously owned by bad actors</li>
<li><strong>IP history</strong>: you provisioned a new VM and got an IP that was previously abusive</li>
<li><strong>Compromised sites</strong>: attackers inject content you don't even know about</li>
<li><strong>Email misconfiguration</strong>: missing SPF/DKIM records or an open relay</li>
</ul>
<p>Regardless of the cause, you need to know fast. The longer you stay listed, the more damage accumulates.</p>
<h2 id="api-access">API access <a href="#api-access" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#api-access">#</a></h2>
<p>DNS Blocklist history is also available through our API. Fetch all check results for a monitor:</p>
<pre data-lang="txt" class="notranslate">GET /api/monitors/{id}/dns-blocklist-history-items
</pre>
<p>Each result includes the checked domain, resolved IPs, and per-provider results with listing status, return codes, and reasons. Full details in our <a href="/docs/api/dns-blocklist">API documentation</a>, or use our <a href="/docs/tools-and-sdks/the-oh-dear-php-sdk">PHP-SDK</a> for convenience.</p>
<h2 id="ready-to-be-enabled-by-everyone">Ready to be enabled by everyone <a href="#ready-to-be-enabled-by-everyone" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#ready-to-be-enabled-by-everyone">#</a></h2>
<p>DNS blocklist monitoring is available for all monitors. Head to your monitor's settings, enable the DNS Blocklist check, and you're covered. All 11 blocklists are monitored by default.</p>
<p>Read the full <a href="/docs/features/dns-blocklist-monitoring">documentation</a> for configuration options and remediation guides.</p>
<p>Want to bulk-update this check for all your monitors? Just reach out <a href="/contact">via support</a>.</p>
]]>
            </summary>
                                    <updated>2026-02-23T09:00:02+00:00</updated>
        </entry>
            <entry>
            <title><![CDATA[Kickstarting our reseller program with Zone & Ploi Cloud]]></title>
            <link rel="alternate" href="https://ohdear.app/news-and-updates/kickstarting-our-reseller-program-with-zone-ploi-cloud" />
            <id>https://ohdear.app/178</id>
            <author>
                <name><![CDATA[Oh Dear]]></name>
            </author>
            <summary type="html">
                <![CDATA[<p>We've quietly been building a reseller program over the past months. The idea is simple: let hosting providers, deployment platforms, and agencies offer Oh Dear monitoring directly inside their own products. Not a stripped-down version, the full thing.</p>
<p>We're working with several partners to bring Oh Dear into more products and platforms. <strong>Zone</strong> and <strong>Ploi Cloud</strong> are the first two out the door.</p>
<h2 id="whats-the-reseller-program">What's the reseller program? <a href="#whats-the-reseller-program" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#whats-the-reseller-program">#</a></h2>
<p>If you run a hosting company, a deployment platform, or an agency managing client sites, you probably want monitoring baked into your product. Building that from scratch is a massive undertaking. <a href="/features/website-uptime-monitoring">Uptime checks</a> from multiple global locations, <a href="/features/continuous-certificate-monitoring">SSL monitoring</a>, <a href="/features/broken-page-and-mixed-content-detection">broken link detection</a>, <a href="/features/performance-monitoring">performance tracking</a>, <a href="/features/dns-monitoring">DNS change alerts</a>, <a href="/features/scheduled-task-monitoring">cron monitoring</a>... that's years of work.</p>
<p>Our reseller program lets you skip all of that. You integrate with our API, and your customers get the full Oh Dear experience inside your platform. You handle the billing and the relationship with your customer. We handle the monitoring infrastructure.</p>
<h2 id="zone-monitoring-as-a-native-hosting-feature">Zone: monitoring as a native hosting feature <a href="#zone-monitoring-as-a-native-hosting-feature" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#zone-monitoring-as-a-native-hosting-feature">#</a></h2>
<p><img src="/media/blog/WHELmtLHKPGKe4otH84lJNVwfPD6mSAUZCKpYNCs.webp" alt="" /></p>
<p><a href="https://www.zone.eu/">Zone</a> is one of the largest domain registration and hosting providers in the Baltics, operating in <a href="https://www.zone.ee/et/">Estonia</a>, <a href="https://www.speedzone.fi/fi/">Finland</a> and <a href="https://www.zone.lv/lv/">Latvia</a>. They now offer Oh Dear monitoring as a native service to all their customers, covering uptime, SSL certificates, broken links, performance, domain expiry, DNS changes, and cron job monitoring.</p>
<p>Their approach to <a href="/features/notifications">notifications</a> is seamless: Zone customers get a one-click login into the full Oh Dear dashboard, where they can configure exactly which alerts they want and where they should go. Email, SMS, Slack, webhooks, you name it. The customer controls their own notification preferences directly in Oh Dear, without ever needing to create a separate account.</p>
<p>What we love about Zone's integration: they don't just resell it, they use Oh Dear internally too. When your own hosting provider trusts the same monitoring tool they're offering you, that says something.</p>
<blockquote>
<p>&quot;Oh Dear is the easy one to implement, because the API makes sense and is easy to work with.&quot;</p>
<p>Kaarel Urva, Chief Project Officer at Zone Media</p>
</blockquote>
<p>You can read more about their offering in <a href="https://www.zone.eu/blog/oh-dear-a-simple-way-to-truly-understand-how-your-website-is-doing/">Zone's announcement post</a>.</p>
<h2 id="ploi-cloud-three-clicks-to-monitoring">Ploi Cloud: three clicks to monitoring <a href="#ploi-cloud-three-clicks-to-monitoring" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#ploi-cloud-three-clicks-to-monitoring">#</a></h2>
<p><img src="/media/blog/eYJtt6XwqpJhMC5dQYvOzgMVnPxeOZrucAyXngDD.png" alt="" /></p>
<p><a href="https://ploi.cloud">Ploi Cloud</a> is a deployment platform built for developers. Their Oh Dear integration is beautifully simple: navigate to the Monitoring tab, click &quot;Enable monitoring&quot;, and you're done. Uptime checks, SSL monitoring, performance metrics, broken link detection, and mixed content scanning, all running within seconds.</p>
<p>Ploi took a different approach to notifications. Instead of sending users to the Oh Dear dashboard, they receive all monitoring alerts through our <a href="/docs/integrations/webhooks">webhooks</a> and pipe them into their existing notification system. If you've already set up Slack, Discord, Google Chat, or email notifications in Ploi, your monitoring alerts just show up there too. Nothing extra to configure.</p>
<p>Their team described it well: &quot;This isn't a third-party plugin or a workaround, it's a first-class integration built together.&quot;</p>
<p>Check out <a href="https://ploi.cloud/blog/introducing-uptime-monitoring-powered-by-oh-dear">Ploi Cloud's announcement</a> for the full details.</p>
<h2 id="built-on-our-reseller-api">Built on our reseller API <a href="#built-on-our-reseller-api" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#built-on-our-reseller-api">#</a></h2>
<p>Both integrations are powered by our <a href="/docs/api/reseller">reseller API</a>. It gives partners everything they need to build a seamless experience:</p>
<ul>
<li><strong>Team provisioning</strong>: create and manage separate client teams programmatically</li>
<li><strong>User management</strong>: add users with roles (admin, member, guest) to each team</li>
<li><strong>Secure login links</strong>: generate time-limited login links so customers can access the full Oh Dear dashboard without creating separate credentials</li>
<li><strong>Monitor management</strong>: create, configure, and manage monitors across all client teams through the standard monitors API</li>
</ul>
<p>The beauty is in the flexibility. Zone gives their customers direct access to the Oh Dear dashboard through login links, so users can configure their own <a href="/features/notifications">notification preferences</a>. Ploi keeps everything inside their own platform and uses <a href="/docs/integrations/webhooks">webhooks</a> to feed alerts into their existing notification channels. Both approaches work, both are valid. You tailor it to what fits your product best.</p>
<h2 id="more-to-come">More to come <a href="#more-to-come" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#more-to-come">#</a></h2>
<p>Zone and Ploi Cloud are the first two reseller partners we can publicly share, but there are more in the making. We'll announce them as they go live.</p>
<p>If you run a hosting company, a deployment platform, or a service where website monitoring would be a natural fit for your customers, we'd love to talk. Reach out at <strong><a href="mailto:support@ohdear.app">support@ohdear.app</a></strong> and we'll walk you through the program.</p>
]]>
            </summary>
                                    <updated>2026-02-20T11:00:02+00:00</updated>
        </entry>
            <entry>
            <title><![CDATA[Making our docs AI-friendly: a tale of two caches]]></title>
            <link rel="alternate" href="https://ohdear.app/news-and-updates/making-our-docs-ai-friendly-a-tale-of-two-caches" />
            <id>https://ohdear.app/177</id>
            <author>
                <name><![CDATA[Oh Dear]]></name>
            </author>
            <summary type="html">
                <![CDATA[<p>Our documentation, FAQ, and blog posts can now be served as clean markdown to AI agents. Send <code>Accept: text/markdown</code> or append <code>.md</code> to the URL, and you get structured content instead of a full HTML page.</p>
<p>It worked great in development. Then we deployed, and two separate caching layers broke everything. Here's the journey.</p>
<h2 id="the-implementation-surprisingly-easy">The implementation (surprisingly easy) <a href="#the-implementation-surprisingly-easy" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#the-implementation-surprisingly-easy">#</a></h2>
<p>Just last week, we added <code>.md</code> suffixes to all our docs URLs and listed them in our <a href="/llms.txt"><code>/llms.txt</code></a>. That felt like a solid approach. A week later, the convention has already moved on. <code>Accept: text/markdown</code> is now the preferred way. It's cleaner HTTP and works with standard content negotiation. We're keeping the <code>.md</code> suffixes for tools that can't set custom headers.</p>
<p>The actual implementation is a small trait:</p>
<pre data-lang="php" class="notranslate"><span class="hl-keyword">trait</span> <span class="hl-type">DetectsMarkdownRequest</span>
{
    <span class="hl-keyword">protected</span> <span class="hl-keyword">function</span> <span class="hl-property">shouldReturnMarkdown</span>(): <span class="hl-type">bool</span>
    {
        <span class="hl-keyword">if</span> (<span class="hl-property">str_ends_with</span>(<span class="hl-property">request</span>()-&gt;<span class="hl-property">path</span>(), <span class="hl-value">'.md'</span>)) {
            <span class="hl-keyword">return</span> <span class="hl-keyword">true</span>;
        }

        <span class="hl-keyword">if</span> (<span class="hl-property">str_contains</span>(<span class="hl-property">request</span>()-&gt;<span class="hl-property">header</span>(<span class="hl-value">'Accept'</span>, <span class="hl-value">''</span>), <span class="hl-value">'text/markdown'</span>)) {
            <span class="hl-keyword">return</span> <span class="hl-keyword">true</span>;
        }

        <span class="hl-keyword">return</span> <span class="hl-keyword">false</span>;
    }
}
</pre>
<p>Each controller just checks this early and returns markdown when needed:</p>
<pre data-lang="php" class="notranslate"><span class="hl-keyword">public</span> <span class="hl-keyword">function</span> <span class="hl-property">__invoke</span>(<span class="hl-injection"><span class="hl-type">string</span> $slug = ''</span>): <span class="hl-type">View|RedirectResponse|Response</span>
{
    <span class="hl-keyword">if</span> (<span class="hl-property">str_ends_with</span>(<span class="hl-variable">$slug</span>, <span class="hl-value">'.md'</span>)) {
        <span class="hl-variable">$slug</span> = <span class="hl-type">Str</span>::<span class="hl-property">chopEnd</span>(<span class="hl-variable">$slug</span>, <span class="hl-value">'.md'</span>);
    }

    <span class="hl-keyword">if</span> (<span class="hl-variable">$this</span>-&gt;<span class="hl-property">shouldReturnMarkdown</span>()) {
        <span class="hl-keyword">return</span> <span class="hl-variable">$this</span>-&gt;<span class="hl-property">markdownResponse</span>(<span class="hl-variable">$slug</span>);
    }

    <span class="hl-comment">// ... regular HTML response</span>
}
</pre>
<p>That's it. Add the trait, add the early return, done. The <code>markdownResponse()</code> method returns the raw content with the right headers:</p>
<pre data-lang="php" class="notranslate"><span class="hl-keyword">return</span> <span class="hl-property">response</span>(<span class="hl-variable">$markdown</span>, 200, [
    <span class="hl-value">'Content-Type'</span> =&gt; <span class="hl-value">'text/markdown; charset=UTF-8'</span>,
    <span class="hl-value">'X-Robots-Tag'</span> =&gt; <span class="hl-value">'noindex'</span>,
]);
</pre>
<h2 id="the-savings">The savings <a href="#the-savings" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#the-savings">#</a></h2>
<p>Here's what our pages look like as HTML versus markdown:</p>
<table>
<thead>
<tr>
<th>Page</th>
<th>HTML</th>
<th>Markdown</th>
<th>Saved</th>
</tr>
</thead>
<tbody>
<tr>
<td><a href="/docs/api/monitors">Monitor API docs</a></td>
<td>386 KB<br/>~99K tokens</td>
<td>22.9 KB<br/>~5.9K tokens</td>
<td><strong>94%</strong></td>
</tr>
<tr>
<td><a href="/docs/features/uptime-and-performance-monitoring">Uptime feature docs</a></td>
<td>370 KB<br/>~95K tokens</td>
<td>14.5 KB<br/>~3.7K tokens</td>
<td><strong>96%</strong></td>
</tr>
<tr>
<td><a href="/news-and-updates/our-3-part-series-on-sql-performance-optimisations">SQL performance post</a></td>
<td>333 KB<br/>~85K tokens</td>
<td>16.1 KB<br/>~4.1K tokens</td>
<td><strong>95%</strong></td>
</tr>
<tr>
<td><a href="/docs/faq/which-payment-methods-do-you-accept">Payment methods FAQ</a></td>
<td>295 KB<br/>~76K tokens</td>
<td>0.9 KB<br/>~230 tokens</td>
<td><strong>99%</strong></td>
</tr>
</tbody>
</table>
<p>What that means in API costs, per request for the Monitor API docs page:</p>
<table>
<thead>
<tr>
<th>Model</th>
<th>HTML cost</th>
<th>Markdown cost</th>
<th>You save</th>
</tr>
</thead>
<tbody>
<tr>
<td>Claude Opus 4.6 ($5/1M)</td>
<td>$0.50</td>
<td>$0.030</td>
<td><strong>$0.47</strong></td>
</tr>
<tr>
<td>Claude Sonnet 4.5 ($3/1M)</td>
<td>$0.30</td>
<td>$0.018</td>
<td><strong>$0.28</strong></td>
</tr>
<tr>
<td>GPT-5.2 ($1.75/1M)</td>
<td>$0.17</td>
<td>$0.010</td>
<td><strong>$0.16</strong></td>
</tr>
</tbody>
</table>
<p>Per request, the difference is small. But agents crawling multiple docs pages per session add up, and there's no reason to ship navigation chrome and script tags to something that just wants the content.</p>
<p>And then production happened.</p>
<h2 id="cache-layer-1-cloudflare">Cache layer 1: Cloudflare <a href="#cache-layer-1-cloudflare" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#cache-layer-1-cloudflare">#</a></h2>
<p>Cloudflare caches by URL. It doesn't vary by <code>Accept</code> header. So the first browser visit cached the HTML, and every subsequent request, including <code>Accept: text/markdown</code>, got that cached HTML back.</p>
<p>First instinct: add <code>Vary: Accept</code>. Nope. Cloudflare Free/Pro doesn't respect <code>Vary: Accept</code> for non-image content.</p>
<p>There's a clever workaround using <strong>Transform Rules</strong> to append a query parameter to markdown requests, giving them a different cache key. Mike Olson wrote a <a href="https://mwolson.org/blog/2026-02-14-markdown-for-agents-on-cloudflare-free-plan/">great post about this approach</a> for the Cloudflare Free plan.</p>
<p>We went simpler: a <strong>Cloudflare Cache Rule</strong> that bypasses cache entirely for markdown requests.</p>
<ul>
<li><strong>Rule name</strong>: &quot;Do not cache text/markdown requests&quot;</li>
<li><strong>Expression</strong>: <code>any(http.request.headers[&quot;accept&quot;][*] contains &quot;text/markdown&quot;)</code></li>
<li><strong>Action</strong>: Bypass cache</li>
</ul>
<p>Available on all plans, including Free. Why not the Transform Rule? Markdown requests are a tiny fraction of traffic. Bypassing the CDN for those has zero noticeable impact, and the rule is dead simple. Sometimes the boring solution is the right one.</p>
<h2 id="cache-layer-2-spatielaravel-responsecache">Cache layer 2: spatie/laravel-responsecache <a href="#cache-layer-2-spatielaravel-responsecache" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#cache-layer-2-spatielaravel-responsecache">#</a></h2>
<p>Fixed Cloudflare. Tested again. Still HTML. <em>What.</em></p>
<p>We use <a href="https://github.com/spatie/laravel-responsecache">spatie/laravel-responsecache</a> on our docs routes. First attempt: override <code>shouldCacheRequest()</code> to return <code>false</code> for markdown. Still HTML.</p>
<p>Here's why. The middleware does this on every request:</p>
<pre data-lang="txt" class="notranslate">if enabled() &amp;&amp; hasBeenCached($request)
    → return cached response          // our check never runs

if enabled() &amp;&amp; shouldCacheRequest($request)
    → store the fresh response         // only controls STORING
</pre>
<p><code>shouldCacheRequest()</code> only controls storing, not serving. The cached HTML was already returned before our override ever ran.</p>
<p>The fix: vary the cache key using <code>useCacheNameSuffix()</code>, the documented way to differentiate requests:</p>
<pre data-lang="php" class="notranslate"><span class="hl-keyword">public</span> <span class="hl-keyword">function</span> <span class="hl-property">useCacheNameSuffix</span>(<span class="hl-injection"><span class="hl-type">Request</span> $request</span>): <span class="hl-type">string</span>
{
    <span class="hl-variable">$suffix</span> = <span class="hl-keyword">parent</span>::<span class="hl-property">useCacheNameSuffix</span>(<span class="hl-variable">$request</span>);

    <span class="hl-keyword">if</span> (<span class="hl-property">str_contains</span>(<span class="hl-variable">$request</span>-&gt;<span class="hl-property">header</span>(<span class="hl-value">'Accept'</span>, <span class="hl-value">''</span>), <span class="hl-value">'text/markdown'</span>)) {
        <span class="hl-keyword">return</span> <span class="hl-variable">$suffix</span>.<span class="hl-value">'-markdown'</span>;
    }

    <span class="hl-keyword">return</span> <span class="hl-variable">$suffix</span>;
}
</pre>
<p>HTML and markdown now get separate cache entries. Both cached, both served correctly.</p>
<p>We also added a <code>Vary: Accept</code> middleware on the docs routes. Cloudflare ignores it on our plan, but it's correct HTTP semantics for browsers and other proxies.</p>
<h2 id="what-we-learned">What we learned <a href="#what-we-learned" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#what-we-learned">#</a></h2>
<ol>
<li><strong><code>Vary: Accept</code> doesn't work on Cloudflare Free/Pro.</strong> Use Cache Rules with <code>any(http.request.headers[&quot;accept&quot;][*] contains &quot;text/markdown&quot;)</code> instead.</li>
<li><strong><code>shouldCacheRequest()</code> in spatie/laravel-responsecache only controls storing, not serving.</strong> Use <code>useCacheNameSuffix()</code> to vary the cache key.</li>
<li><strong>Test with all cache layers enabled.</strong> Our local dev had response cache disabled, masking the bug entirely. Worked in dev, failed in prod. Classic.</li>
<li><strong>Caching bugs are sneaky.</strong> Valid response, wrong variant. No errors, no 500s, nothing in the logs.</li>
</ol>
<h2 id="try-it-out">Try it out <a href="#try-it-out" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#try-it-out">#</a></h2>
<pre data-lang="bash" class="notranslate"><span class="hl-comment"># Regular HTML</span>
<span class="hl-keyword">curl</span> <span class="hl-attribute">-I</span> https://ohdear.app/docs/api/monitors

<span class="hl-comment"># Markdown for AI agents</span>
<span class="hl-keyword">curl</span> <span class="hl-attribute">-H</span> <span class="hl-value">'Accept: text/markdown'</span> https://ohdear.app/docs/api/monitors

<span class="hl-comment"># Also works with .md suffix</span>
<span class="hl-keyword">curl</span> https://ohdear.app/docs/api/monitors.md
</pre>
<p>Our <a href="/llms.txt"><code>llms.txt</code></a> lists all available documentation. This works for feature docs, API docs, FAQ items, and blog posts. Everything a coding agent needs when integrating with Oh Dear.</p>
<p>If you're running Cloudflare + Laravel response cache and want to serve different content based on headers, hopefully this saves you the debugging time we spent on it.</p>
]]>
            </summary>
                                    <updated>2026-02-16T09:01:41+00:00</updated>
        </entry>
            <entry>
            <title><![CDATA[Our 3-part series on SQL performance optimisations]]></title>
            <link rel="alternate" href="https://ohdear.app/news-and-updates/our-3-part-series-on-sql-performance-optimisations" />
            <id>https://ohdear.app/176</id>
            <author>
                <name><![CDATA[Oh Dear]]></name>
            </author>
            <summary type="html">
                <![CDATA[<p>A few weeks ago, we massively improved the performance of the Oh Dear dashboard and website by optimising our SQL queries. We documented everything we learned along the way in a 3-part series - from finding problematic queries, to fixing them with indexes, to preventing regressions with automated testing.</p>
<p>Here's what each part covers:</p>
<h2 id="part-1-finding-the-right-queries-to-fix">Part 1: Finding the right queries to fix <a href="#part-1-finding-the-right-queries-to-fix" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#part-1-finding-the-right-queries-to-fix">#</a></h2>
<p>Before you can optimise anything, you need to know <em>what</em> to optimise. This post covers the tools and techniques we use to identify slow queries: Laravel's debug bar for local development, MySQL's slow query log, logging queries that don't use indexes, inspecting currently running queries, and using <code>pt-query-digest</code> to analyse query patterns. We also cover Laravel's built-in lazy loading prevention for catching N+1 issues early.</p>
<p><a href="/news-and-updates/sql-performance-improvements-finding-the-right-queries-to-fix-part-1">Read part 1: Finding the right queries to fix →</a></p>
<h2 id="part-2-analysing-amp-fixing-the-slow-queries">Part 2: Analysing &amp; fixing the slow queries <a href="#part-2-analysing-amp-fixing-the-slow-queries" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#part-2-analysing-amp-fixing-the-slow-queries">#</a></h2>
<p>Once you've found the problematic queries, how do you actually fix them? This post dives deep into MySQL indexes: how <code>EXPLAIN</code> works, what each output field means, how to read query execution plans, and how to design effective composite indexes. We cover the difference between full table scans, index scans, and index seeks - and show real examples of queries going from seconds to milliseconds with the right index.</p>
<p><a href="/news-and-updates/sql-performance-improvements-analysing-fixing-the-slow-queries-part-2">Read part 2: Analysing &amp; fixing the slow queries →</a></p>
<h2 id="part-3-automatic-detection-amp-regression-testing">Part 3: Automatic detection &amp; regression testing <a href="#part-3-automatic-detection-amp-regression-testing" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#part-3-automatic-detection-amp-regression-testing">#</a></h2>
<p>Fixing performance issues is one thing - keeping them fixed is another. This post introduces our open-source package <code>phpunit-query-count-assertions</code> that lets you assert SQL performance in your test suite. Catch N+1 queries, duplicate queries, and missing indexes automatically in CI before they reach production. We've eliminated over 15% of our SQL queries by catching duplicates that slipped through code review.</p>
<p><a href="/news-and-updates/sql-performance-improvements-automatic-detection-regression-testing-part-3">Read part 3: Automatic detection &amp; regression testing →</a></p>
<hr />
<p>Whether you're just starting to think about database performance or looking to prevent regressions in an already-optimised application, there's something in this series for you.</p>
]]>
            </summary>
                                    <updated>2026-01-25T18:46:04+00:00</updated>
        </entry>
            <entry>
            <title><![CDATA[SQL performance improvements: automatic detection & regression testing (part 3)]]></title>
            <link rel="alternate" href="https://ohdear.app/news-and-updates/sql-performance-improvements-automatic-detection-regression-testing-part-3" />
            <id>https://ohdear.app/175</id>
            <author>
                <name><![CDATA[Oh Dear]]></name>
            </author>
            <summary type="html">
                <![CDATA[<p>This is the final part of our 3-part series on SQL performance improvements. In <a href="/news-and-updates/sql-performance-improvements-finding-the-right-queries-to-fix-part-1">part 1</a>, we covered how to identify slow queries. In <a href="/news-and-updates/sql-performance-improvements-analysing-fixing-the-slow-queries-part-2">part 2</a>, we explored how to fix them with indexes. In this post, we'll share how we prevent those performance issues from ever reaching production again.</p>
<p>A few weeks ago, we <a href="/news-and-updates/speed-improvements-to-the-dashboard-website-job-processing">massively improved the performance</a> of the dashboard &amp; website by optimizing our SQL queries. The improvements were significant - but how do we make sure we don't accidentally undo all that work with future code changes?</p>
<p>In this post, you'll see:</p>
<ul>
<li><a href="#the-problem-performance-regressions-slip-through">The problem: performance regressions slip through</a></li>
<li><a href="#the-solution-test-driven-sql-performance">The solution: test-driven SQL performance</a></li>
<li><a href="#installing-the-package">Installing the package</a></li>
<li><a href="#quick-start-the-one-assertion-you-need">Quick start: the one assertion you need</a></li>
<li><a href="#detecting-duplicate-queries">Detecting duplicate queries</a></li>
<li><a href="#catching-n1-lazy-loading-issues">Catching N+1 / lazy loading issues</a></li>
<li><a href="#verifying-index-usage">Verifying index usage</a></li>
<li><a href="#query-count-thresholds">Query count thresholds</a></li>
<li><a href="#multi-connection-support">Multi-connection support</a></li>
<li><a href="#the-paranoid-mode-automatic-checks-on-every-test">The paranoid mode: automatic checks on every test</a></li>
<li><a href="#real-world-results-from-oh-dear">Real-world results from Oh Dear</a></li>
<li><a href="#what-this-wont-catch">What this won't catch</a></li>
</ul>
<p>Let's go!</p>
<h2 id="what-these-results-look-like">What these results look like <a href="#what-these-results-look-like" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#what-these-results-look-like">#</a></h2>
<p>Before diving into the tooling, here's what we've achieved by implementing automated SQL performance testing:</p>
<p>We eliminated <strong>over 15% of our SQL read queries</strong> by catching duplicate queries that slipped through manual code review. These weren't obvious N+1 loops - they were subtle cases where the same query was executed in different parts of the request lifecycle.</p>
<p><img src="/media/blog/8NvZI5C5xmZ4MDOIJmMSmgTPOpQUb5N4tDpIfxNn.png" alt="" /></p>
<p>We've caught index regressions in CI <em>before</em> they hit production. When a developer removes an index in a migration or writes a query that can't use existing indexes, the tests fail with a clear message explaining why.</p>
<p>The feedback loop is immediate: write code, run tests, see if your queries are efficient. No more waiting for production metrics to reveal problems days or weeks later.</p>
<h2 id="the-problem-performance-regressions-slip-through">The problem: performance regressions slip through <a href="#the-problem-performance-regressions-slip-through" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#the-problem-performance-regressions-slip-through">#</a></h2>
<p>Here's a scenario we've all experienced: you spend time optimising queries, ship the improvements, celebrate the faster response times - and then a few weeks later, performance starts degrading again.</p>
<p>Why? Because nothing was preventing new inefficient queries from being introduced. Code review helps, but reviewers don't always catch:</p>
<ul>
<li>Duplicate queries executed in different service classes</li>
<li>Lazy loading that only triggers with certain data combinations</li>
<li>Missing indexes on new columns added in migrations</li>
<li>Queries that worked fine on small datasets but don't scale</li>
</ul>
<p>The problem isn't fixing performance issues - we covered that in parts 1 and 2. The problem is <em>keeping</em> them fixed.</p>
<h2 id="the-solution-test-driven-sql-performance">The solution: test-driven SQL performance <a href="#the-solution-test-driven-sql-performance" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#the-solution-test-driven-sql-performance">#</a></h2>
<p>We built a package that lets us assert SQL performance characteristics in our test suite. When tests run (locally or in CI), they fail if queries are inefficient.</p>
<p>The package is called <code>phpunit-query-count-assertions</code> and it's <a href="https://github.com/mattiasgeniar/phpunit-query-count-assertions">available on GitHub</a>. We've been using it internally at Oh Dear for months, and it's caught more issues than we'd like to admit.</p>
<p>Here's the core idea: wrap your code in a tracking block, then assert that the queries were efficient.</p>
<pre data-lang="php" class="notranslate"><span class="hl-variable">$this</span>-&gt;<span class="hl-property">trackQueries</span>();

<span class="hl-property">app</span>(<span class="hl-type">CertificateHealthChecker</span>::<span class="hl-keyword">class</span>)-&gt;<span class="hl-property">perform</span>(<span class="hl-variable">$run</span>);

<span class="hl-variable">$this</span>-&gt;<span class="hl-property">assertQueriesAreEfficient</span>();
</pre>
<p>That single assertion checks for N+1 queries, duplicate queries, and missing indexes - all at once.</p>
<h2 id="installing-the-package">Installing the package <a href="#installing-the-package" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#installing-the-package">#</a></h2>
<p>The package requires PHP 8.2+ and Laravel 11 or 12. Install it as a dev dependency:</p>
<pre data-lang="bash" class="notranslate">$ <span class="hl-keyword">composer</span> require <span class="hl-attribute">--dev</span> mattiasgeniar/phpunit-query-count-assertions
</pre>
<p>Then add the trait to your test class:</p>
<pre data-lang="php" class="notranslate"><span class="hl-keyword">use</span> <span class="hl-type">Mattiasgeniar\PhpunitQueryCountAssertions\AssertsQueryCounts</span>;

<span class="hl-keyword">class</span> <span class="hl-type">CertificateHealthCheckTest</span> <span class="hl-keyword">extends</span> <span class="hl-type">TestCase</span>
{
    <span class="hl-keyword">use</span> <span class="hl-type">AssertsQueryCounts</span>;
}
</pre>
<p>That's it. No configuration files, no service providers - just a trait.</p>
<h2 id="quick-start-the-one-assertion-you-need">Quick start: the one assertion you need <a href="#quick-start-the-one-assertion-you-need" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#quick-start-the-one-assertion-you-need">#</a></h2>
<p>For most cases, <code>assertQueriesAreEfficient()</code> is all you need. It combines three checks into one:</p>
<ol>
<li><strong>No lazy loading</strong> - catches N+1 queries</li>
<li><strong>No duplicate queries</strong> - same query shouldn't run twice</li>
<li><strong>All queries use indexes</strong> - no full table scans</li>
</ol>
<p>Here's a real example from our certificate health checker tests:</p>
<pre data-lang="php" class="notranslate"><span class="hl-keyword">public</span> <span class="hl-keyword">function</span> <span class="hl-property">test_health_checker_is_efficient</span>(): <span class="hl-type">void</span>
{
    <span class="hl-comment">// Setup - create test data (these queries aren't tracked)</span>
    <span class="hl-variable">$certificate</span> = <span class="hl-type">Certificate</span>::<span class="hl-property">factory</span>()-&gt;<span class="hl-property">expired</span>()-&gt;<span class="hl-property">create</span>();
    <span class="hl-variable">$run</span> = <span class="hl-keyword">new</span> <span class="hl-type">InMemoryRun</span>();

    <span class="hl-comment">// Track only the code under test</span>
    <span class="hl-variable">$this</span>-&gt;<span class="hl-property">trackQueries</span>();
    <span class="hl-property">app</span>(<span class="hl-type">CertificateHealthChecker</span>::<span class="hl-keyword">class</span>)-&gt;<span class="hl-property">perform</span>(<span class="hl-variable">$run</span>);
    <span class="hl-variable">$this</span>-&gt;<span class="hl-property">assertQueriesAreEfficient</span>();
}
</pre>
<p>The key insight: call <code>trackQueries()</code> <em>after</em> your test setup. Factory calls and seeders execute queries too - we don't want those triggering false positives.</p>
<p>When something fails, you get actionable output:</p>
<pre data-lang="txt" class="notranslate">Query efficiency issues detected:

Lazy loading violations detected:
Violations:
  1. App\Models\Certificate::$site
  2. App\Models\Certificate::$site

Duplicate queries detected:
  1. Executed 2 times: SELECT * FROM sites WHERE id = ?
     Bindings: [42]
     Locations:
       #1: app/Checkers/CertificateHealthChecker.php:67
       #2: app/Checkers/CertificateHealthChecker.php:89
</pre>
<p>The locations tell you exactly where each query was triggered - no more guessing.</p>
<h2 id="detecting-duplicate-queries">Detecting duplicate queries <a href="#detecting-duplicate-queries" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#detecting-duplicate-queries">#</a></h2>
<p>Duplicate query detection is surprisingly powerful. It catches cases that aren't traditional N+1 loops but still waste database resources.</p>
<pre data-lang="php" class="notranslate"><span class="hl-variable">$this</span>-&gt;<span class="hl-property">assertNoDuplicateQueries</span>(<span class="hl-keyword">function</span> () {
    <span class="hl-comment">// This code executes the same query twice</span>
    <span class="hl-variable">$user</span> = <span class="hl-type">User</span>::<span class="hl-property">find</span>(1);

    <span class="hl-comment">// ... 50 lines of other code ...</span>

    <span class="hl-variable">$sameUser</span> = <span class="hl-type">User</span>::<span class="hl-property">find</span>(1); <span class="hl-comment">// Duplicate!</span>
});
</pre>
<p>The assertion considers bindings: <code>User::find(1)</code> and <code>User::find(2)</code> are <em>different</em> queries. But <code>User::find(1)</code> called twice is a duplicate.</p>
<p>When we first enabled this at Oh Dear, we found duplicate queries we'd missed in code review. The same <code>Monitor</code> model was being loaded in three different places during a single request - each time fetching from the database instead of reusing the already-loaded instance.</p>
<p>Fixing these duplicates removed over 15% of our SELECT queries.</p>
<h2 id="catching-n1-lazy-loading-issues">Catching N+1 / lazy loading issues <a href="#catching-n1-lazy-loading-issues" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#catching-n1-lazy-loading-issues">#</a></h2>
<p>N+1 queries are the classic performance killer. You load a list of models, then loop through them accessing a relationship - triggering a new query for each iteration.</p>
<pre data-lang="php" class="notranslate"><span class="hl-comment">// This will fail</span>
<span class="hl-variable">$this</span>-&gt;<span class="hl-property">assertNoLazyLoading</span>(<span class="hl-keyword">function</span> () {
    <span class="hl-variable">$sites</span> = <span class="hl-type">Site</span>::<span class="hl-property">all</span>();

    <span class="hl-keyword">foreach</span> (<span class="hl-variable">$sites</span> <span class="hl-keyword">as</span> <span class="hl-variable">$site</span>) {
        <span class="hl-keyword">echo</span> <span class="hl-variable">$site</span>-&gt;<span class="hl-property">checks</span>-&gt;<span class="hl-property">count</span>(); <span class="hl-comment">// N+1 query!</span>
    }
});
</pre>
<p>The fix is eager loading:</p>
<pre data-lang="php" class="notranslate"><span class="hl-comment">// This will pass</span>
<span class="hl-variable">$this</span>-&gt;<span class="hl-property">assertNoLazyLoading</span>(<span class="hl-keyword">function</span> () {
    <span class="hl-variable">$sites</span> = <span class="hl-type">Site</span>::<span class="hl-property">with</span>(<span class="hl-value">'checks'</span>)-&gt;<span class="hl-property">get</span>();

    <span class="hl-keyword">foreach</span> (<span class="hl-variable">$sites</span> <span class="hl-keyword">as</span> <span class="hl-variable">$site</span>) {
        <span class="hl-keyword">echo</span> <span class="hl-variable">$site</span>-&gt;<span class="hl-property">checks</span>-&gt;<span class="hl-property">count</span>();
    }
});
</pre>
<p>The package uses Laravel's built-in lazy loading prevention under the hood, so it catches exactly what Laravel would catch - but only within your test assertion, not globally.</p>
<p>Ps; if you only need the count, <code>Site::withCount('checks')-&gt;get()</code> is even more efficient since it doesn't load the related models at all.</p>
<h2 id="verifying-index-usage">Verifying index usage <a href="#verifying-index-usage" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#verifying-index-usage">#</a></h2>
<p>This is where it gets interesting. The package runs <code>EXPLAIN</code> on your queries and checks for performance red flags.</p>
<pre data-lang="php" class="notranslate"><span class="hl-variable">$this</span>-&gt;<span class="hl-property">assertAllQueriesUseIndexes</span>(<span class="hl-keyword">function</span> () {
    <span class="hl-type">Site</span>::<span class="hl-property">where</span>(<span class="hl-value">'team_id'</span>, 1)-&gt;<span class="hl-property">get</span>();
});
</pre>
<p>If <code>team_id</code> doesn't have an index, you'll see:</p>
<pre data-lang="txt" class="notranslate">Queries with index issues detected:

  1. SELECT * FROM sites WHERE team_id = ?
     Bindings: [1]
     Issues:
       - [ERROR] Full table scan on 'sites'
     Locations:
       #1: tests/Feature/SiteTest.php:42
</pre>
<p>The severity levels help prioritise:</p>
<table>
<thead>
<tr>
<th>Severity</th>
<th>Meaning</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>[ERROR]</code></td>
<td>Critical: full table scans, unused available indexes</td>
</tr>
<tr>
<td><code>[WARNING]</code></td>
<td>Potential issue: filesort, temporary tables</td>
</tr>
<tr>
<td><code>[INFO]</code></td>
<td>Informational: low filter efficiency (non-failing)</td>
</tr>
</tbody>
</table>
<h3 id="what-gets-detected">What gets detected</h3>
<p>For MySQL and MariaDB:</p>
<ul>
<li>Full table scans (<code>type=ALL</code>)</li>
<li>Full index scans (<code>type=index</code>)</li>
<li>Index available but not used</li>
<li>Using filesort</li>
<li>Using temporary tables</li>
<li>Using join buffer (missing index for joins)</li>
</ul>
<p>For SQLite (useful for fast local tests):</p>
<ul>
<li>Full table scans (<code>SCAN table</code>)</li>
<li>Temporary B-tree usage</li>
<li>Foreign key constraint check scans</li>
</ul>
<h3 id="small-table-optimisation">Small table optimisation</h3>
<p>Caution: full table scans on tiny tables (&lt; 10 rows) are often <em>faster</em> than index lookups. The package ignores these by default - MySQL's optimiser knows what it's doing on small datasets.</p>
<p>If you need to adjust this threshold:</p>
<pre data-lang="php" class="notranslate"><span class="hl-keyword">use</span> <span class="hl-type">Mattiasgeniar\PhpunitQueryCountAssertions\QueryAnalysers\MySQLAnalyser</span>;

<span class="hl-type">self</span>::<span class="hl-property">registerQueryAnalyser</span>(
    (<span class="hl-keyword">new</span> <span class="hl-type">MySQLAnalyser</span>)-&gt;<span class="hl-property">withMinRowsForScanWarning</span>(100)
);
</pre>
<h2 id="query-count-thresholds">Query count thresholds <a href="#query-count-thresholds" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#query-count-thresholds">#</a></h2>
<p>Sometimes you want precise control over how many queries run:</p>
<pre data-lang="php" class="notranslate"><span class="hl-comment">// Exact count</span>
<span class="hl-variable">$this</span>-&gt;<span class="hl-property">assertQueryCountMatches</span>(3, <span class="hl-keyword">fn</span>() =&gt; <span class="hl-variable">$this</span>-&gt;<span class="hl-property">loadDashboard</span>());

<span class="hl-comment">// Upper bound - useful for &quot;don't make this worse&quot;</span>
<span class="hl-variable">$this</span>-&gt;<span class="hl-property">assertQueryCountLessThan</span>(10, <span class="hl-keyword">fn</span>() =&gt; <span class="hl-variable">$this</span>-&gt;<span class="hl-property">loadDashboard</span>());

<span class="hl-comment">// Range</span>
<span class="hl-variable">$this</span>-&gt;<span class="hl-property">assertQueryCountBetween</span>(5, 15, <span class="hl-keyword">fn</span>() =&gt; <span class="hl-variable">$this</span>-&gt;<span class="hl-property">complexOperation</span>());

<span class="hl-comment">// No queries at all (cached responses)</span>
<span class="hl-variable">$this</span>-&gt;<span class="hl-property">assertNoQueriesExecuted</span>(<span class="hl-keyword">fn</span>() =&gt; <span class="hl-variable">$this</span>-&gt;<span class="hl-property">getCachedData</span>());
</pre>
<p>When assertions fail, you see exactly what ran:</p>
<pre data-lang="txt" class="notranslate">Expected 3 queries, got 5.
Queries executed:
  1. [0.45ms] SELECT * FROM users WHERE id = ?
      Bindings: [1]
      Locations:
        #1: app/Services/Dashboard.php:42
  2. [0.32ms] SELECT * FROM sites WHERE user_id = ?
      ...
</pre>
<p>The timing information helps identify which queries are worth optimising - a 0.3ms query that runs once matters less than a 0.1ms query that runs 100 times.</p>
<h2 id="multi-connection-support">Multi-connection support <a href="#multi-connection-support" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#multi-connection-support">#</a></h2>
<p>If your application uses read replicas, separate analytics databases, or tenant-specific connections, you'll appreciate this: <code>trackQueries()</code> captures queries from <strong>all database connections</strong> by default.</p>
<pre data-lang="php" class="notranslate"><span class="hl-variable">$this</span>-&gt;<span class="hl-property">trackQueries</span>();

<span class="hl-type">DB</span>::<span class="hl-property">select</span>(<span class="hl-value">'SELECT 1'</span>);                         <span class="hl-comment">// Tracked</span>
<span class="hl-type">DB</span>::<span class="hl-property">connection</span>(<span class="hl-value">'replica'</span>)-&gt;<span class="hl-property">select</span>(<span class="hl-value">'SELECT 2'</span>);  <span class="hl-comment">// Also tracked</span>

<span class="hl-variable">$queries</span> = <span class="hl-type">self</span>::<span class="hl-property">getQueriesExecuted</span>();
<span class="hl-comment">// $queries[0]['connection'] === 'mysql'</span>
<span class="hl-comment">// $queries[1]['connection'] === 'replica'</span>
</pre>
<p>Each tracked query includes its connection name, so you can verify queries are hitting the right database.</p>
<h3 id="filtering-to-specific-connections">Filtering to specific connections</h3>
<p>Sometimes you want to focus on just one connection - maybe your test setup runs migrations on a different connection that you don't want to count:</p>
<pre data-lang="php" class="notranslate"><span class="hl-comment">// Track only the replica connection</span>
<span class="hl-variable">$this</span>-&gt;<span class="hl-property">trackQueries</span>(<span class="hl-value">'replica'</span>);

<span class="hl-comment">// Track multiple specific connections</span>
<span class="hl-variable">$this</span>-&gt;<span class="hl-property">trackQueries</span>([<span class="hl-value">'mysql'</span>, <span class="hl-value">'replica'</span>]);
</pre>
<p>This is useful when:</p>
<ul>
<li>Your test setup runs queries on different connections that you don't want to count</li>
<li>You want to verify that specific queries go to the right connection</li>
<li>You're debugging connection routing in read/write split setups</li>
</ul>
<p>At Oh Dear, we generally track <em>all</em> queries, across all connections.</p>
<h2 id="the-paranoid-mode-automatic-checks-on-every-test">The paranoid mode: automatic checks on every test <a href="#the-paranoid-mode-automatic-checks-on-every-test" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#the-paranoid-mode-automatic-checks-on-every-test">#</a></h2>
<p>If you want to enforce query efficiency across your entire test suite, you can enable automatic checking in <code>beforeEach</code> and <code>afterEach</code> hooks.</p>
<p>For Pest (in <code>tests/Pest.php</code>):</p>
<pre data-lang="php" class="notranslate"><span class="hl-keyword">use</span> <span class="hl-type">Mattiasgeniar\PhpunitQueryCountAssertions\AssertsQueryCounts</span>;

<span class="hl-property">pest</span>()-&gt;<span class="hl-property">extend</span>(<span class="hl-type">Tests\TestCase</span>::<span class="hl-keyword">class</span>)
    -&gt;<span class="hl-property">use</span>(<span class="hl-type">AssertsQueryCounts</span>::<span class="hl-keyword">class</span>)
    -&gt;<span class="hl-property">beforeEach</span>(<span class="hl-keyword">fn</span> () =&gt; <span class="hl-type">self</span>::<span class="hl-property">trackQueries</span>())
    -&gt;<span class="hl-property">afterEach</span>(<span class="hl-keyword">fn</span> () =&gt; <span class="hl-variable">$this</span>-&gt;<span class="hl-property">assertQueriesAreEfficient</span>())
    -&gt;<span class="hl-property">in</span>(<span class="hl-value">'Feature'</span>);
</pre>
<p>For PHPUnit (in your base test class):</p>
<pre data-lang="php" class="notranslate"><span class="hl-keyword">abstract</span> <span class="hl-keyword">class</span> <span class="hl-type">TestCase</span> <span class="hl-keyword">extends</span> <span class="hl-type">BaseTestCase</span>
{
    <span class="hl-keyword">use</span> <span class="hl-type">AssertsQueryCounts</span>;

    <span class="hl-keyword">protected</span> <span class="hl-keyword">function</span> <span class="hl-property">setUp</span>(): <span class="hl-type">void</span>
    {
        <span class="hl-keyword">parent</span>::<span class="hl-property">setUp</span>();
        <span class="hl-variable">$this</span>-&gt;<span class="hl-property">trackQueries</span>();
    }

    <span class="hl-keyword">protected</span> <span class="hl-keyword">function</span> <span class="hl-property">tearDown</span>(): <span class="hl-type">void</span>
    {
        <span class="hl-variable">$this</span>-&gt;<span class="hl-property">assertQueriesAreEfficient</span>();
        <span class="hl-keyword">parent</span>::<span class="hl-property">tearDown</span>();
    }
}
</pre>
<p>I know some will #YOLO this on their entire test suite - don't @ me when hundreds of tests suddenly fail. Consider starting with a subset of critical tests first. Keep in mind this might trigger warnings for queries executed purely during the <em>setup</em> and <em>seeding</em> of the tests, although you can make an argument efficiency matters then, too.</p>
<h2 id="real-world-results-from-oh-dear">Real-world results from Oh Dear <a href="#real-world-results-from-oh-dear" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#real-world-results-from-oh-dear">#</a></h2>
<p>Here's what we've caught since implementing this:</p>
<p><strong>Duplicate query in the Scheduled Task Checker</strong>: Our scheduled task checker called <code>-&gt;refresh()</code> twice on the main model, to ensure we operate on <em>all</em> the received pings in our checker. This was a legacy from our older setup, before we refactored it to our new setup more than a year ago. The <code>refresh()</code> was needed in the old system, not our new one, so we could safely clean it up. This is hard to spot, because the code never <em>broke</em>, it was just being a little too aggressive in refreshing state.</p>
<p><strong>N+1 in certificate expiry notifications</strong>: When sending expiry warnings, we were lazy-loading the team for each certificate instead of eager loading. Only triggered when multiple certificates expired simultaneously - exactly the kind of edge case that slips through manual testing.</p>
<p>The pattern is clear: these aren't bugs that crash the application. They're inefficiencies that slowly accumulate until someone notices &quot;the app feels slower than it used to.&quot;</p>
<h2 id="what-this-wont-catch">What this won't catch <a href="#what-this-wont-catch" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#what-this-wont-catch">#</a></h2>
<p>Let's be honest about the limitations. This package catches a lot, but it's not a silver bullet.</p>
<p><strong>Small dataset blindspot</strong>: By default, the package ignores full table scans on tables with fewer than 10 rows. MySQL's optimiser often <em>chooses</em> to scan tiny tables because it's faster than an index lookup. This means your tests - which typically have small datasets - might pass while production (with millions of rows) suffers.</p>
<p><strong>EXPLAIN isn't execution</strong>: The index analysis runs <code>EXPLAIN</code> on your queries, which shows MySQL's <em>plan</em>, not what actually happened. In rare cases, the plan and reality diverge - especially with complex joins or when statistics are stale.</p>
<p><strong>Timing is unreliable in tests</strong>: Query timing assertions (<code>assertMaxQueryTime</code>) work, but test databases are usually local SSDs with no contention. A query that runs in 5ms locally might take 500ms in production under load.</p>
<p><strong>You need representative test scenarios</strong>: If your test only creates one user with one site, you won't catch the N+1 that only triggers when a user has multiple sites. The package can only catch what your tests actually exercise.</p>
<p>That said, we've found it catches roughly 80% of performance issues before they reach production. The remaining 20% still need production monitoring - but that's a much smaller haystack to search through.&lt;@</p>
<h2 id="wrapping-up">Wrapping up <a href="#wrapping-up" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#wrapping-up">#</a></h2>
<p>The three-part journey is complete:</p>
<ol>
<li><strong>Part 1</strong>: <a href="/news-and-updates/sql-performance-improvements-finding-the-right-queries-to-fix-part-1">Find slow queries using debug bars, slow query logs, and process monitoring</a></li>
<li><strong>Part 2</strong>: <a href="/news-and-updates/sql-performance-improvements-analysing-fixing-the-slow-queries-part-2">Fix them with proper indexes</a></li>
<li><strong>Part 3</strong>: <a href="/news-and-updates/sql-performance-improvements-automatic-detection-regression-testing-part-3">Keep them fixed with automated testing</a></li>
</ol>
<p>The package is open source and available at <a href="https://github.com/mattiasgeniar/phpunit-query-count-assertions">github.com/mattiasgeniar/phpunit-query-count-assertions</a>. We'd love to hear how it works for your applications.</p>
<h2 id="feedback">Feedback? <a href="#feedback" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#feedback">#</a></h2>
<p>If this article contains any errors or if it should clarify certain sections more, do reach out via either mattias@ohdear.app or ping me via <a href="https://x.com/mattiasgeniar">@mattiasgeniar</a> or our <a href="https://x.com/ohdearapp">@OhDearApp</a> account. Any feedback is appreciated!</p>
<p>Curious to hear about performance issues you've caught with automated testing that we don't yet know about!</p>
]]>
            </summary>
                                    <updated>2026-01-25T18:39:28+00:00</updated>
        </entry>
            <entry>
            <title><![CDATA[The effects of the Cloudflare outage on Oh Dear]]></title>
            <link rel="alternate" href="https://ohdear.app/news-and-updates/the-effects-of-the-cloudflare-outage-on-oh-dear" />
            <id>https://ohdear.app/174</id>
            <author>
                <name><![CDATA[Oh Dear]]></name>
            </author>
            <summary type="html">
                <![CDATA[<p>Last night, Cloudflare <a href="https://www.cloudflarestatus.com/incidents/8gmgl950y3h7">experienced one of their biggest outages</a> in the past decade. Almost all websites behind Cloudflare's proxy/DDoS-protection service were offline for anywhere between 1–3 hours. That included our own <a href="https://ohdear.app">website &amp; application</a>, while our <a href="https://status.ohdear.app/">status pages</a> remained online the entire time.</p>
<p>This post examines some of our lessons learned, and we'll share our insights into what we saw during the outage.</p>
<h2 id="cloudflare-a-net-positive-for-our-industry">Cloudflare, a net positive for our industry <a href="#cloudflare-a-net-positive-for-our-industry" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#cloudflare-a-net-positive-for-our-industry">#</a></h2>
<p>Let me start by chiming in on the public debate: has the internet decided to centralize too much around a few key players (Cloudflare, AWS, Google, ...)? Probably.</p>
<p>Should we now all move away from these big players to <em>decentralize</em> the web again? Probably not.</p>
<p>Despite Cloudflare's outage yesterday, I firmly believe they're a net positive for our industry, advancing the protection, speed, and availability of websites globally. Any major provider will experience downtime. I'm confident there will be a solid and transparent post-mortem of this incident with many lessons learned to prevent this from happening again. (<strong>Update</strong>: Cloudflare's <a href="https://blog.cloudflare.com/18-november-2025-outage/">post-mortem on the incident is now available</a>)</p>
<p>So no, we won't suddenly move everything away from Cloudflare in a knee-jerk reaction, but we will be conscious to have procedures and workflows readily available to <em>temporarily</em> move away from Cloudflare when there is prolonged downtime.</p>
<p>Now, on to the real stuff!</p>
<h2 id="our-web-ui-was-offline-our-status-pages-amp-monitoring-werent">Our Web UI was offline, our status pages &amp; monitoring weren't <a href="#our-web-ui-was-offline-our-status-pages-amp-monitoring-werent" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#our-web-ui-was-offline-our-status-pages-amp-monitoring-werent">#</a></h2>
<p>Oh Dear itself sits behind Cloudflare. We've had the bad luck of experiencing some DDoS attacks a few years ago and have since opted into Cloudflare's Pro plan for better protection against this. As a result, during the outage, this was the availability of our services:</p>
<ul>
<li>Website: ❌</li>
<li>Dashboard: ❌</li>
<li>API: ❌</li>
<li>Monitoring engine: ✅</li>
<li>Notification engine: ✅</li>
<li>Status pages: ✅</li>
</ul>
<p>The problem we (and our clients) were facing: while status pages were online, our users couldn't post any updates to them because they couldn't log in to the main application that has the interface to manage those status pages.</p>
<p>This essentially turned status pages—unless users opted into the <a href="/docs/status-pages/create-your-first-oh-dear-status-page#auto-updates-based-on-uptime-monitoring">auto-updates based on our uptime monitors</a>—into a stale and outdated communication channel. Only when our dashboard services were restored could our users start posting updates.</p>
<p>All our actual monitoring &amp; notification engines remained online and performant at all times.</p>
<h2 id="temporarily-disabling-cloudflare">Temporarily disabling Cloudflare <a href="#temporarily-disabling-cloudflare" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#temporarily-disabling-cloudflare">#</a></h2>
<p>Because of this, we temporarily disabled Cloudflare for our website so that our users could access our service again.</p>
<p>This turned out to take slightly more time because the Cloudflare Dashboard itself was offline during their outage (because <code>challenges.cloudflare.com</code>, their &quot;I'm not a Robot&quot; service, was offline and prevented logins to their dashboard). Only once the CF dashboard was available again could we disable proxying through Cloudflare.</p>
<p><strong>Lesson learned</strong>: we need a readily available DNS backup at our domain registry, allowing us to more easily switch nameservers away from Cloudflare to a third party in the case of an emergency. We have backups of our DNS records, but there are <em>many</em> of them, and the process to restore those was still manual and too time-consuming.</p>
<h2 id="our-ping-endpoint-stays-behind-cloudflare">Our ping endpoint stays behind Cloudflare <a href="#our-ping-endpoint-stays-behind-cloudflare" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#our-ping-endpoint-stays-behind-cloudflare">#</a></h2>
<p>Users of our <a href="/docs/features/cron-job-monitoring">cron job/scheduled task monitoring</a> might have noticed notifications about missed scheduled tasks because the callback URL <code>ping.ohdear.app</code> is also routed through Cloudflare.</p>
<p>This was also unavailable during the outage. We do, however, have internal systems that prevented ~99% of the false alerts related to those cron job monitors from being sent.</p>
<p>Every 4 seconds, we ping our own endpoint and expect the result to be processed in the following seconds. This system caught the unavailability of the ping endpoint early on and muted most of the alerts for our clients. Only during the recovery/early downtime—when our system hadn't caught this yet—have we sent some notifications to users.</p>
<p><strong>Lessons learned</strong>: our internal system worked well for this particular use case, but we'll investigate options around service recovery times to ensure we don't send notifications that were <em>just</em> on the edge of the Cloudflare downtime.</p>
<h2 id="increased-system-load-for-downtime-verifications">Increased system load for downtime verifications <a href="#increased-system-load-for-downtime-verifications" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#increased-system-load-for-downtime-verifications">#</a></h2>
<p>The <em>happy path</em> of uptime monitoring is fairly straightforward: make an <a href="/features/website-uptime-monitoring">HTTP</a> (or <a href="/features/ping-monitoring">ping</a> or <a href="/features/tcp-port-monitoring">TCP</a>) request; if the service responds as expected, the service is considered up. All is well.</p>
<p>The <em>unhappy path</em>, where we detect downtime, is more resource-intensive: we'll do that <em>exact same</em> monitor request but route it via a different set of servers (technically: a different datacenter provider but within the same geographical continent) and expect that to <em>also</em> confirm the downtime.</p>
<p>This means that downtime detection <em>doubles our system load</em> because we can no longer just do a single check to confirm something is up; we need a second verification.</p>
<p>We have around 50+ servers worldwide, here's a screenshot of the network-throughput of one of our <em>satellite</em> servers:</p>
<p><img src="/media/blog/zM3c9x5439OjYdPKTneM3wUXikrRA1QTFcj1KUYA.png" alt="" /></p>
<p>In our capacity planning, we take this into account: our systems aren't running at their limits and always have the capacity to double their throughput at any moment in time, especially for these circumstances.</p>
<p><strong>Lessons learned</strong>: there's a reason we keep 50% spare capacity at all times—this incident shows why. We'll keep doing so.</p>
<h2 id="a-spike-in-notifications-sent">A spike in notifications sent <a href="#a-spike-in-notifications-sent" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#a-spike-in-notifications-sent">#</a></h2>
<p>It makes sense that when Cloudflare goes down, we'll start notifying <em>a lot</em> of our users that their website is down. Yesterday can be considered a stress test on our own systems as well, and I'm glad to see our choices in architecture have ensured that this was a smooth process.</p>
<p>At any given time, some of the websites we monitor are down, and we'll be notifying those users. Yesterday was a bit different, though.</p>
<p>For any of the following data, timestamps are shown <strong>in UTC</strong>.</p>
<p><img src="/media/blog/FmuAgKQxxPjrj0yyPPMq6A5Uwo9XQTXzuT5YqMAQ.png" alt="" /></p>
<p>At the peak, we were sending ~1,500 notifications <em>per second</em>. This is a mix of channels—some via email, some Slack, some Discord, etc.—depending on the settings of our users.</p>
<p>The downtime wasn't consistent either, so during the same period we also sent out a lot of recovery notifications (only to send new downtime notifications a few minutes later when the service was disrupted again):</p>
<p><img src="/media/blog/lJrIwNoUMGJAyKXu2klO2YuHRghB8ovERz1IaWBx.png" alt="" /></p>
<p>During our peak, we observed at most a 1- to 2-minute delay between <em>detecting</em> the downtime and <em>sending</em> the notification because our queues were being hit much harder than ever before. We quickly scaled up capacity for processing outbound notifications and swiftly processed all notifications in &lt; 1 minute again.</p>
<p>We also monitor our own service with some of our competitor tooling, and we observed some of their email notifications only arriving <em>more than an hour later</em> than the actual incident. We're proud to say that wasn't the case for us.</p>
<p><strong>Lessons learned</strong>: our architecture allows for sending out many notifications in a short period of time with minimal disruption.</p>
<h2 id="where-did-our-users-get-notified-the-most">Where did our users get notified the most? <a href="#where-did-our-users-get-notified-the-most" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#where-did-our-users-get-notified-the-most">#</a></h2>
<p>We support a myriad of notification options, so where did we send most notifications during this outage?</p>
<p><img src="/media/blog/Wd0JUD9VbG3fVXdftExXBc8F21dCxOWiwnN5Olc9.png" alt="" /></p>
<p>In order of popularity, we sent out most notifications over:</p>
<ul>
<li>mail: 54.0%</li>
<li>slackApi: 18.8%</li>
<li>sms: 5.1%</li>
<li>telegram: 4.4%</li>
<li>discord: 3.8%</li>
</ul>
<p>The others are in single digits. Email being the most popular makes sense, as it's the most accessible <em>and</em> it's the one we enable by default if someone signs up to try Oh Dear. The bump in email delivery is also clear from our email delivery stats:</p>
<p><img src="/media/blog/6PaoHwyAOGqHzLyagAxUw3RXRoJBx4KbwokUNdTH.png" alt="" /></p>
<h2 id="in-closing">In closing <a href="#in-closing" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#in-closing">#</a></h2>
<p>There are a few lessons learned from this incident, but we're proud to see how our service performed during a real-life stress test. We can implement some improvements over the coming weeks and prepare even better for a major outage. With the centralization of more of the web, we should prepare for downtime events to potentially have <em>much bigger ripple effects</em> that could affect more services at the same time.</p>
<p>Through our support channels, we heard some of our users request new features, such as:</p>
<ul>
<li>Mass-muting of all notifications during an outage like this (initiated by the user)</li>
<li>Out-of-band access to Status Pages for managing updates/incidents</li>
<li>An in-app banner highlighting a worldwide issue of downtime</li>
</ul>
<p>All of these are under consideration. If you missed crucial functionality during this outage, we'd love to hear from you!</p>
]]>
            </summary>
                                    <updated>2025-11-19T10:01:56+00:00</updated>
        </entry>
            <entry>
            <title><![CDATA[A different view for the performance timings of an uptime monitor]]></title>
            <link rel="alternate" href="https://ohdear.app/news-and-updates/a-different-view-for-the-performance-timings-of-an-uptime-monitor" />
            <id>https://ohdear.app/173</id>
            <author>
                <name><![CDATA[Oh Dear]]></name>
            </author>
            <summary type="html">
                <![CDATA[<p>When you monitor a website at Oh Dear, the monitoring also includes the historical performance insights that belong to that monitor. It gives you a historical overview of the speed of that monitor, allowing you to see anomalies and changes over time.</p>
<p>As of today, there's a second view available, one that matches the webbrowser visualisation of the timing of a single request.</p>
<p><img src="/media/blog/3xpYgynII5xHWYO9vRcJJRnzQk7BL971VxCtQLpG.png" alt="" /></p>
<p>This view shows the same waterfall information you'd find in Chrome or Firefox, providing a familiar view to developers worldwide.</p>
<p>The historical insights remain available as always, of course, allowing you to zoom out and look at the bigger picture.</p>
<p><img src="/media/blog/CYwWACsb4CTYN5W9rgLJabNqEMdI31uUIsXCrjDs.png" alt="" /></p>
<p>You'll find the new view under the &quot;<em>Show all details</em>&quot; section of an uptime monitor.</p>
]]>
            </summary>
                                    <updated>2025-11-04T14:21:55+00:00</updated>
        </entry>
            <entry>
            <title><![CDATA[AI monitoring is coming to Oh Dear]]></title>
            <link rel="alternate" href="https://ohdear.app/news-and-updates/ai-monitoring-is-coming-to-oh-dear" />
            <id>https://ohdear.app/172</id>
            <author>
                <name><![CDATA[Oh Dear]]></name>
            </author>
            <summary type="html">
                <![CDATA[<p>Would you know if your checkout form stopped working overnight? Or if a recent deploy broke your login flow? Traditional monitoring can't catch these issues - it only tells you if your site is up, not if it actually works.</p>
<p>AI monitoring lets you describe what should work in plain English, and we'll test it like a real user would - clicking buttons, filling forms, checking content. No scripts to maintain, no complex setup.</p>
<p>Here's an example where we let the AI submit a form and verifies that the submission was ok.</p>
<p><img src="/media/blog/SSZjKTCf98DMIXX281qItgnEAYjAwFgDbpBh9c8n.jpg" alt="" /></p>
<p>Our AI can control a real browser and click, type and submit forms on your site, and inspect results. Like you see above, it can also take screenshots. When something does not seem right, the AI can notify you via our powerful notification system.</p>
<p>There are lots of other use cases for AI checks. Here are some example prompts you can use:</p>
<ul>
<li>If there are any warnings or errors on the developer console of mysite.com, fail the check and send me a notification. You can ignore any messages from CloudFlare.</li>
<li>Make sure the homepage of mysite.com lists at least five products, includes a login link, and has a registration page link. Send me a notification if any of these elements are missing.</li>
<li>Check the spelling on https://example.com. If there are any spelling mistakes, send me a notification.</li>
</ul>
<p>You can already check out <a href="https://ohdear.app/docs/features/ai-monitoring">the AI monitoring documentation</a>, which contains more info on what you can do with our AI check.</p>
<p>A couple of users are currently helping us test and polish our AI check. Want to test it out too? <a href="https://tally.so/r/wbPDbE">Apply for the AI monitoring closed beta</a>.</p>
<p>We aim to fully launch this feature mid-end November.</p>
]]>
            </summary>
                                    <updated>2025-10-29T09:51:33+00:00</updated>
        </entry>
            <entry>
            <title><![CDATA[SQL performance improvements: analysing & fixing the slow queries (part 2)]]></title>
            <link rel="alternate" href="https://ohdear.app/news-and-updates/sql-performance-improvements-analysing-fixing-the-slow-queries-part-2" />
            <id>https://ohdear.app/167</id>
            <author>
                <name><![CDATA[Oh Dear]]></name>
            </author>
            <summary type="html">
                <![CDATA[<p><em>This is <a href="/news-and-updates/sql-performance-improvements-finding-the-right-queries-to-fix-part-1">part 2 of a 3-part series on SQL performance improvements</a>.</em></p>
<p>A few weeks ago, we <a href="/news-and-updates/speed-improvements-to-the-dashboard-website-job-processing">massively improved the performance</a> of the dashboard &amp; website by optimizing some of our SQL queries. In this post, we'll dive deeper into the optimisations of queries with indexes.</p>
<p>Table of contents:</p>
<ul>
<li><a href="#ways-to-improve-database-performance">Ways to improve database performance</a></li>
<li><a href="#how-database-indexes-work">How database indexes work</a></li>
<li><a href="#let-mysql-explain-why-the-query-is-slow">Let MySQL explain why a query is slow</a></li>
<li><a href="#different-explain-output">Different EXPLAIN output</a></li>
<li><a href="#adding-indexes">Adding indexes</a></li>
<li><a href="#multiple-indexes-vs-composite-indexes">Multiple indexes vs. composite indexes</a></li>
<li><a href="#the-order-of-the-index-vs-the-order-of-the-where-statement">The order of the index vs. the order of the WHERE statement</a></li>
<li><a href="#null-values-in-indexes">NULL values in indexes</a></li>
<li><a href="#what-performance-gains-to-expect">What performance gains to expect</a></li>
<li><a href="#random-trivia-on-indexes">Random trivia on indexes</a></li>
<li><a href="#the-ai-way-how-llms-can-help">The AI way: how LLMs can help</a></li>
</ul>
<p>Let's go!</p>
<h2 id="what-these-results-look-like">What these results look like <a href="#what-these-results-look-like" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#what-these-results-look-like">#</a></h2>
<p>As a reminder, this is the resulting performance gain for the dashboard &amp; some of our internal APIs:</p>
<p><img src="/media/blog/RF9LvvgCQU1skDedrUkm0l6CEfuG7qTJ8Bu0XklO.png" alt="" /></p>
<p><img src="/media/blog/1HEzbWpII2U7qyCcl9f708IdWFgFDeGUVcYhqNZ1.png" alt="" /></p>
<p>These graphs come from the <a href="/features/global-uptime-monitoring">Oh Dear uptime monitoring we perform</a>. Behind the scenes, we also use <a href="https://www.zabbix.com/">Zabbix</a> for low-level server monitoring, where we get extra insights into the performance of our servers.</p>
<p>The optimisations have lead to an increase in network bandwidth between our SQL server &amp; the rest of our infrastructure. Normally an increase like this would be cause for concern, but in our scenario it's because we're now able to perform all checks faster and complete our queues at a much faster pace, so that we're able to utilize the network bandwidth more.</p>
<p><img src="/media/blog/jMUegmXocf8CMVHYV05ewTJFqtGJeS6Wxsj4ZFrI.png" alt="" /></p>
<p>As a result of our improvements, we were able to almost double the SELECT's (green line) we're able to do concurrently.</p>
<p><img src="/media/blog/5ojyRfstZmp4602ROM5xog1EjCuvMm8gGzIwcSS8.png" alt="" /></p>
<p>This also gives us a bit of context: at a constant load of ~6,000 SELECT's per second, around 1,000 INSERT's, another 1,000 UPDATE's and ~600 DELETE's per second, we've got ourselves a busy system.</p>
<p>So then, what did we do to get these numbers?</p>
<h2 id="ways-to-improve-database-performance">Ways to improve database performance <a href="#ways-to-improve-database-performance" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#ways-to-improve-database-performance">#</a></h2>
<p>In general, speeding up SQL queries can be done in 4 main ways;</p>
<ol>
<li>Avoiding the query altogether (resolving N+1 loops, introducing caching, etc.)</li>
<li>Rewriting the query to be more performant (different joins, fewer subqueries, etc)</li>
<li>Get faster hardware</li>
<li>Adding indexes to the table to speed up data lookups</li>
</ol>
<p>This post focusses mostly on the last point: the indexes, the hidden weapon of any database system.</p>
<h2 id="how-database-indexes-work">How database indexes work <a href="#how-database-indexes-work" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#how-database-indexes-work">#</a></h2>
<p>For clarity, we run on MySQL 8.0, what we're explaining in this post is tested and applied on our systems, and <em>should</em> work on PostgreSQL and even SQLite - although the output of the <code>EXPLAIN</code> queries lower in this article will vary.</p>
<p>Imagine a table with 100,000,000 rows of data and multiple columns. If you ask your database to perform the following query:</p>
<pre data-lang="sql" class="notranslate">mysql&gt; <span class="hl-keyword">SELECT</span> * <span class="hl-keyword">FROM</span> <span class="hl-type">big_table</span>
       <span class="hl-keyword">WHERE</span> user_id = 1 <span class="hl-keyword">AND</span> deleted_at <span class="hl-keyword">IS NOT NULL</span>;
</pre>
<p>Without indexes, the database has to scan the entire table for rows that match your criteria. <em>With</em> indexes, it has a general idea of where the records you'll be asking might be. It's the difference between walking across a country with or without a map. You can get there, but it'll be a lot more efficient <em>with</em> that map.</p>
<p>When it comes to using indexes, you need to have a good idea on <em>what</em> data you want to filter on. Generally, that's everything in your <code>WHERE</code> clause. That's where the indexes come in.</p>
<p>In our example above, we're filtering data on both <code>user_id</code> and <code>deleted_at</code>, ideally we have an index that combines both of those data points for faster lookups, although even one index (say on the <code>user_id</code> column) would already make big differences.</p>
<p>There's a few gotcha's with that query straight-away (hint: <em>nullable</em> values can be tricky) which we'll cover deeper down.</p>
<h2 id="let-mysql-explain-why-the-query-is-slow">Let MySQL explain why the query is slow <a href="#let-mysql-explain-why-the-query-is-slow" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#let-mysql-explain-why-the-query-is-slow">#</a></h2>
<p>In our previous post, we explained the <a href="/news-and-updates/sql-performance-improvements-finding-the-right-queries-to-fix-part-1">techniques to identify which queries need improving</a>. If you've followed along, you might now have a list of queries that have been identified as <em>slow</em>. Now, it's time to analyse them. That's where MySQL's <code>EXPLAIN</code> keyword comes in.</p>
<p>Take your slow query, add in the keyword <code>EXPLAIN</code> just before your own query, and let MySQL tell you what it did during execution.</p>
<pre data-lang="sql" class="notranslate">mysql&gt; EXPLAIN <span class="hl-keyword">SELECT</span> * <span class="hl-keyword">FROM</span> ...
</pre>
<p>The output can be something like this:</p>
<pre data-lang="txt" class="notranslate">           id: 1
  select_type: SIMPLE
        table: &lt;your table-name&gt;
   partitions: NULL
         type: ref
possible_keys: &lt;table-name&gt;_created_at_index
          key: &lt;table-name&gt;_created_at_index
      key_len: 8
          ref: const
         rows: 1
     filtered: 100.00
        Extra: Using filesort
</pre>
<p>To start, the most important fields are:</p>
<ul>
<li><strong>possible_keys</strong>: if indexes already exist in your table, these are the ones that are under consideration for executing this query</li>
<li><strong>key</strong>: if there are multiple indexes, this is the one MySQL ended up chosing</li>
<li><strong>rows</strong>: MySQL estimates it will need to examine 1 row to satisfy the WHERE clause</li>
<li><strong>filtered</strong>: 100% of the examined rows are expected to pass the WHERE condition.</li>
<li><strong>extra</strong>: the biggest clue is here</li>
</ul>
<p>The <code>extra</code> column can contain a lot of value, typically you'll see values like these, sorted by the best possible output (aka the fastest queries) to the slowest.</p>
<ul>
<li>The best performance:
<ul>
<li><strong>(no extra info)</strong> - Optimal: using index efficiently with no additional operations</li>
<li><strong>Using index</strong> - Very fast: query satisfied entirely from index without accessing table data</li>
<li><strong>Using where; Using index</strong> - Fast: filtering done on index data only</li>
<li><strong>Using index condition</strong> - Good: index condition pushdown reduces data transfer</li>
</ul>
</li>
<li>Moderate performance:
<ul>
<li><strong>Using where</strong> - Acceptable: additional filtering applied after index lookup</li>
<li><strong>Distinct</strong> - Moderate: removing duplicates, but can use index</li>
<li><strong>Using index for group-by</strong> - Good for GROUP BY: grouping operations done via index</li>
</ul>
</li>
<li>Performance concerns:
<ul>
<li><strong>Using filesort</strong> - Slow: sorting requires additional memory/disk operations</li>
<li><strong>Using temporary</strong> - Slow: temporary table needed for complex operations</li>
<li><strong>Using temporary; Using filesort</strong> - Slower: both temporary table creation and sorting required</li>
</ul>
</li>
<li>The worst performance:
<ul>
<li><strong>Using join buffer</strong> - Slow: nested loop join without efficient index access</li>
<li><strong>Using where; Using join buffer</strong> - Very slow: inefficient joins with additional filtering</li>
<li><strong>Range checked for each record</strong> - Very slow: MySQL recalculates optimal index for each row</li>
<li><strong>Full scan on NULL key</strong> - Extremely slow: cannot use index due to NULL handling in subqueries</li>
</ul>
</li>
</ul>
<p>The key principle here is: anything involving filesorts, temporary tables, or full scans without indexes will significantly impact query performance and should be solved.</p>
<h2 id="different-explain-output">Different EXPLAIN output <a href="#different-explain-output" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#different-explain-output">#</a></h2>
<p>MySQL has several outputs for an <code>EXPLAIN</code> query, depending on your preferred tooling, these can be useful to you:</p>
<pre data-lang="sql" class="notranslate"><span class="hl-comment">-- output in tabular format, the default</span>
mysql&gt; EXPLAIN <span class="hl-keyword">SELECT</span> * <span class="hl-keyword">FROM</span> ...

<span class="hl-comment">-- output in JSON, also contains cost calculations</span>
<span class="hl-comment">-- from the query optimizer</span>
mysql&gt; EXPLAIN FORMAT=JSON <span class="hl-keyword">SELECT</span> * <span class="hl-keyword">FROM</span> ...

<span class="hl-comment">-- outputs a visual hierarchy of operations,</span>
<span class="hl-comment">-- useful with many joins</span>
mysql&gt; EXPLAIN FORMAT=TREE <span class="hl-keyword">SELECT</span> * <span class="hl-keyword">FROM</span> ...

<span class="hl-comment">-- performs the actual query and compares</span>
<span class="hl-comment">-- estimated vs. actual rows</span>
mysql&gt; EXPLAIN ANALYSE <span class="hl-keyword">SELECT</span> * <span class="hl-keyword">FROM</span> ...
</pre>
<p>I usually default to just the standard <code>EXPLAIN</code> output.</p>
<h2 id="adding-indexes">Adding indexes <a href="#adding-indexes" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#adding-indexes">#</a></h2>
<p>Most slow queries can be solved by adding indexes. This ensures MySQL can find the data efficiently, without having to scan the entire table.</p>
<p>Take the following query for instance:</p>
<pre data-lang="sql" class="notranslate"><span class="hl-keyword">SELECT</span>
	*
<span class="hl-keyword">FROM</span>
	`snooze_history_items`
<span class="hl-keyword">WHERE</span>
	`snooze_history_items`.`check_id` = 1
	<span class="hl-keyword">AND</span> `ends_at` &gt; '<span class="hl-value">2025-09-13 20:00:00</span>'
<span class="hl-keyword">ORDER BY</span>
	`ends_at` <span class="hl-keyword">DESC</span>
<span class="hl-keyword">LIMIT</span>
	1
</pre>
<p>We're querying a pretty big table to get some results. The two <code>WHERE</code> conditions are meant to limit the result, but without indexes, MySQL needs to scan and read the entire table, filter out the requested results, then return them.</p>
<p>To make matters worse, it also needs to then <em>sort</em> the result set in memory, before returning the result.</p>
<p>Both actions - reading the entire table to filter the results and then sorting them - are <em>incredibly</em> CPU intensive. If the query can't be simplified further, it's time to add an index so MySQL can consult its index for the results, efficiently gather <em>only</em> the data it needs, and instantly return it.</p>
<pre data-lang="sql" class="notranslate">mysql&gt; <span class="hl-keyword">CREATE INDEX</span> idx_check_ends_optimized
       <span class="hl-keyword">ON</span> snooze_history_items (check_id, <span class="hl-property">ends_at</span> <span class="hl-keyword">DESC</span>);
</pre>
<p>Or in a Laravel migration:</p>
<pre data-lang="php" class="notranslate"><span class="hl-type">Schema</span>::<span class="hl-property">table</span>(<span class="hl-value">'snooze_history_items'</span>, <span class="hl-keyword">function</span> (<span class="hl-injection"><span class="hl-type">Blueprint</span> $table</span>) {
    <span class="hl-variable">$table</span>-&gt;<span class="hl-property">index</span>([<span class="hl-value">'check_id'</span>, <span class="hl-value">'ends_at'</span>], <span class="hl-value">'idx_check_ends_optimized'</span>);
});
</pre>
<p>Now when we ask the <code>EXPLAIN</code> output, we'll see:</p>
<pre data-lang="sql" class="notranslate">mysql&gt; EXPLAIN <span class="hl-keyword">SELECT</span> ... 

possible_keys: idx_check_ends_optimized
          key: idx_check_ends_optimized
        Extra: Using <span class="hl-keyword">index</span> condition; Backward <span class="hl-keyword">index</span> scan
</pre>
<p>This saves a tremendous amount of CPU cycles in MySQL reading all data &amp; sorting the results. At our scale of close to 10k queries/s, this <em>seriously</em> adds up.</p>
<h2 id="multiple-indexes-vs-composite-indexes">Multiple indexes vs. composite indexes <a href="#multiple-indexes-vs-composite-indexes" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#multiple-indexes-vs-composite-indexes">#</a></h2>
<p>After a while, you might have multiple indexes on any given table, like these:</p>
<pre data-lang="sql" class="notranslate">KEY `table_user_id` (`user_id`) USING BTREE,
<span class="hl-property">KEY</span> `table_user_id_deleted_at` (`user_id`,`deleted_at`) USING BTREE,
<span class="hl-property">KEY</span> `table_user_id_deleted_at_client_type` (`user_id`,`deleted_at`,`client_type`) USING BTREE
</pre>
<p>Written differently, this table has 3 indexes:</p>
<ul>
<li>user_id (&quot;user_id_index&quot;)</li>
<li>user_id, deleted_at (&quot;user_id_deleted_at_index&quot;)</li>
<li>user_id, deleted_at, client_type (&quot;user_id_deleted_at_client_type_index&quot;)</li>
</ul>
<p>Indexes are used/read from left-to-right: an index that consists of 3 columns, like our <strong>user_id_deleted_at_client_type_index</strong> in the example above, can also be used by queries that only search for the left-most column of that index.</p>
<p>If you're performing a query like:</p>
<pre data-lang="sql" class="notranslate"><span class="hl-keyword">SELECT</span> * <span class="hl-keyword">FROM</span> <span class="hl-type">users</span> <span class="hl-keyword">WHERE</span> user_id = 1;
</pre>
<p>Any of the 3 indexes is capable of supporting this query, because the user_id is the left-most index. This actually makes the first index, <strong>user_id_index</strong>, obsolete and it can be dropped.</p>
<h2 id="the-order-of-the-index-vs-the-order-of-the-where-statement">The order of the index vs. the order of the WHERE statement <a href="#the-order-of-the-index-vs-the-order-of-the-where-statement" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#the-order-of-the-index-vs-the-order-of-the-where-statement">#</a></h2>
<p>Both of the following queries will use the same available index on the <strong>user_id, deleted_at</strong> fields. The order of the <code>WHERE</code> statements does not matter:</p>
<pre data-lang="sql" class="notranslate"><span class="hl-keyword">SELECT</span> * <span class="hl-keyword">FROM</span> <span class="hl-type">big_table</span>
<span class="hl-keyword">WHERE</span>
    user_id = 1
    <span class="hl-keyword">AND</span> deleted_at <span class="hl-keyword">IS NOT NULL</span>
</pre>
<p>and:</p>
<pre data-lang="sql" class="notranslate"><span class="hl-keyword">SELECT</span> * <span class="hl-keyword">FROM</span> <span class="hl-type">big_table</span>
<span class="hl-keyword">WHERE</span>
    deleted_at <span class="hl-keyword">IS NOT NULL</span>
    <span class="hl-keyword">AND</span> user_id = 1
</pre>
<p><strong>The order of the index, on the other hand, does matter!</strong></p>
<p>When creating indexes, it helps to visualize which data-filter would reduce the result set the most. In our query, if we can filter based on <code>user_id</code> first, we might already limit the scope of our query to 1% of the total table. This filter has the biggest impact. It's also the most likely field to be used in <code>WHERE</code> statements, so can probably be re-used by other queries, it makes sense for both of these reasons to keep <code>user_id</code> the first column in the index.</p>
<p>If we turned the indexes around, like so:</p>
<pre data-lang="sql" class="notranslate">KEY `table_deleted_at_user_id` (`deleted_at`, `user_id`) USING BTREE,
</pre>
<p>We'd now have an index that <em>starts</em> with the <code>deleted_at</code> column, followed by the <code>user_id</code> column. Now, any query that filters <em>only</em> on the <code>user_id</code> can no longer use this index and will have to fall back to a full table scan, even though it's part of another index.</p>
<h2 id="null-values-in-indexes">NULL values in indexes <a href="#null-values-in-indexes" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#null-values-in-indexes">#</a></h2>
<p>Some fields in your table might be nullable. A good example is usually a <code>deleted_at</code> soft-delete timestamp. It's only filled in with a date if a record is actually soft-deleted.</p>
<p>The main gotcha with nullable columns in MySQL indexes is that <code>NULL</code> values <em>are</em> indexed, but they can cause performance issues in your queries:</p>
<pre data-lang="sql" class="notranslate"><span class="hl-comment">-- This CAN use the index</span>
<span class="hl-keyword">WHERE</span> deleted_at <span class="hl-keyword">IS NULL</span>
<span class="hl-keyword">WHERE</span> deleted_at <span class="hl-keyword">IS NOT NULL</span>

<span class="hl-comment">-- These too, for either range or equality checks</span>
<span class="hl-keyword">WHERE</span> deleted_at = '<span class="hl-value">2025-10-06</span>'
<span class="hl-keyword">WHERE</span> deleted_at &gt;= '<span class="hl-value">2025-10-06</span>'
<span class="hl-keyword">WHERE</span> deleted_at &lt; '<span class="hl-value">2025-10-06</span>'
<span class="hl-keyword">WHERE</span> deleted_at <span class="hl-keyword">BETWEEN</span> '<span class="hl-value">2025-01-01</span>' <span class="hl-keyword">AND</span> '<span class="hl-value">2025-12-31</span>'

<span class="hl-comment">-- These CANNOT efficiently use the index</span>
<span class="hl-keyword">WHERE</span> deleted_at != '<span class="hl-value">2025-10-06</span>'  <span class="hl-comment">-- Inequality (requires scanning most of the index, inefficient range scan)</span>
<span class="hl-keyword">WHERE</span> deleted_at &lt;&gt; '<span class="hl-value">2025-10-06</span>'  <span class="hl-comment">-- Same, requires scanning most of the index</span>
<span class="hl-keyword">WHERE</span> <span class="hl-keyword">NOT</span> (deleted_at = '<span class="hl-value">2025-10-06</span>')  <span class="hl-comment">-- NOT conditions also require scanning most of the index here</span>
</pre>
<p>If you need consistent indexes on datetime fields, you could opt for a default timestamp (ie <code>1970-01-01</code>) on that column instead and removing the nullable property.</p>
<p>As a rule of thumb, where possible, put <em>nullable</em> columns as the last column in your composite indexes.</p>
<h2 id="what-performance-gains-to-expect">What performance gains to expect <a href="#what-performance-gains-to-expect" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#what-performance-gains-to-expect">#</a></h2>
<p>I define a fast query as one that completes under 15-20ms, depending on the size of the output. For single-row queries, it's reasonable to expect a response around 1ms to 2ms. Larger responses (ie many rows) will just take longer to travel over the network as well, let's set that upper bound to somewhere around 20ms.</p>
<p>Take the following example for instance:</p>
<pre data-lang="sql" class="notranslate"><span class="hl-keyword">SELECT</span> * <span class="hl-keyword">FROM</span> <span class="hl-type">big_table</span>
<span class="hl-keyword">WHERE</span> user_id = 1
<span class="hl-keyword">LIMIT</span> 10
</pre>
<p>The performance results are:</p>
<ul>
<li>Without an index on <code>user_id</code>: 32ms</li>
<li>With an index on <code>user_id</code>: 3ms</li>
</ul>
<p>That's a straight up 10x faster query, just with an index.</p>
<p>The results are a little more extreme with bigger tables or more complex queries, in our environment where we might have big queries with joins, subqueries &amp; lots of <code>WHERE</code> conditions, we've seen improvements from &gt; 5s down to &lt; 50ms, that's an almost <strong>100x faster query</strong>.</p>
<h2 id="random-trivia-on-indexes">Random trivia on indexes <a href="#random-trivia-on-indexes" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#random-trivia-on-indexes">#</a></h2>
<p>A few notes &amp; thoughts on indexes in general;</p>
<ul>
<li>They usually only matter for bigger tables, on very small tables, full scans <em>might</em> actually be faster than index lookups.</li>
<li>More indexes = slower write performance, so slower <code>INSERT</code>, <code>UPDATE</code> and <code>DELETE</code> queries. This can significantly slowdown the write performance, a table with 3+ indexes can see slower write performance by up to 20% and more. The larger (more columns) the index, the slower writes will become. Another reason to use a single, good, composite index vs. overlapping smaller ones.</li>
<li>Most applications benefit from more indexes, they often have a 95% SELECT vs. 5% WRITE ratio on database load anyway.</li>
<li>Primary keys &amp; foreign keys are automatically indexes as well, but are added as single-column indexes, and may be redundant with composite indexes.</li>
<li>Indexes consume disk space, but I've never realistically seen this have any noticeable impact.</li>
<li>Indexes also use RAM, but I've also not seen this become an issue.</li>
<li>You can always get a bigger and faster database server, often times, this is cheaper than spending weeks optimizing code or queries.</li>
<li>Using functions in queries means the index won't be used, ie <code>WHERE YEAR(created_at) = 2025</code> won't use an index on created_at, but <code>WHERE created_at &gt;= '2025-01-01' AND created_at &lt; '2026-01-01'</code> will</li>
<li>Leading wildcard searches break indexes: <code>WHERE name LIKE '%smith'</code> can't use an index, but <code>WHERE name LIKE 'smith%'</code> can.</li>
<li>In the past 20yrs of managing servers, I've had one weird instance where indexes became corrupt (queries were always slow, regardless of the indexes), where it needed an <code>OPTIMIZE TABLE &lt;table&gt;</code> to rebuild indexes. Since then, I run <code>OPTIMIZE TABLE</code> quite often when troubleshooting.</li>
</ul>
<p>Curious to hear trivia you might have on SQL performance that we don't yet know about!</p>
<h2 id="the-ai-way-how-llms-can-help">The AI way: how LLMs can help <a href="#the-ai-way-how-llms-can-help" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#the-ai-way-how-llms-can-help">#</a></h2>
<p>When you give your AI of choice enough context, the suggestions can be extremely powerful. You'll need to be diligent in reviewing the response, but if what you're looking for is a good first review of a slow query, this helps:</p>
<blockquote>
<p>Below is the EXPLAIN output of a slow query, followed by the result of the query analysis. The table structure is below that. Analyze the query and suggest which indexes to add, clarify why, and show the index creations in the Laravel migration format.</p>
</blockquote>
<p>Follow that with;</p>
<ul>
<li>The exact <code>EXPLAIN SELECT ...</code> query</li>
<li>The output of that EXPLAIN</li>
<li>The output of <code>SHOW CREATE TABLE &lt;tablename&gt;</code></li>
</ul>
<p>This gives the LLM the query that's slow, what the query planner did to execute it as well as the full table structure - including current indexes - to work with.</p>
<p>You should, however, review the suggestions and see if any of the indexes the LLM might suggest already exist or could be part of a composite index you added in earlier, to avoid duplicating indexes.</p>
<h2 id="feedback">Feedback? <a href="#feedback" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#feedback">#</a></h2>
<p>If this article contains any errors or if it should clarify certain sections more, do reach out via either <em>mattias@ohdear.app</em> or ping me via <a href="https://x.com/mattiasgeniar">@mattiasgeniar</a> or our <a href="https://x.com/ohdearapp">@OhDearApp</a> account. Any feedback is appreciated!</p>
]]>
            </summary>
                                    <updated>2025-10-06T12:26:26+00:00</updated>
        </entry>
            <entry>
            <title><![CDATA[Sending beers all across Belgium, a throwback to how we named Oh Dear]]></title>
            <link rel="alternate" href="https://ohdear.app/news-and-updates/sending-beers-all-across-belgium-a-throwback-to-how-we-named-oh-dear" />
            <id>https://ohdear.app/171</id>
            <author>
                <name><![CDATA[Oh Dear]]></name>
            </author>
            <summary type="html">
                <![CDATA[<p>We're obviously a little biased, but we believe we have one of the best website monitoring tools on the market today, leading in features compared to our competitors. We've already tried a <a href="/news-and-updates/heres-what-worked-and-what-didnt-when-marketing-saas-like-oh-dear">variety of marketing techniques</a> to promote our service, but none really had the impact we were looking for.</p>
<p>Maybe we're better at actually <em>building</em> good software than we are at marketing it? Or are we trying what everyone else is also doing, thus making it all harder? After all, you can only outbid your competitors in Google Ads for so long.</p>
<p>So we're trying something new: an analog marketing campaign for the digital world. 🍺</p>
<h2 id="the-origin-of-oh-dear">The origin of Oh Dear <a href="#the-origin-of-oh-dear" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#the-origin-of-oh-dear">#</a></h2>
<p>Back in 2017 we were brainstorming the idea of a better uptime monitor. We did this out of a local bar called <em>Paters Vaetje</em>, a small but cozy bar in Antwerp, near the Cathedral.</p>
<p>Belgium has many good beers, so it's no surprise that the bar we were sitting in was covered in beer advertisements. One of them was this one for a fine one called &quot;<em>Moeder Overste</em>&quot;, which translates to &quot;<em>Mother Superior</em>&quot;.</p>
<p><img src="/media/blog/D5E2ju8NK8DqnNZLS0GgkUjsRjSTZVQe8UKeGzpO.jpg" alt="" /></p>
<p>We were thinking: &quot;<em>What would Mother Superior Superior say if her website was down</em>&quot;. Immediately &quot;<strong>Oh Dear</strong>&quot; came to mind. We thought we would only keep it as a work-in-progress name, but over time we liked it so much that we used it as the final product name.</p>
<h2 id="sharing-is-caring">Sharing is caring <a href="#sharing-is-caring" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#sharing-is-caring">#</a></h2>
<p>So with the origin of our name clarified, we figured: why not let others experience the same taste while promoting Oh Dear at the same time?</p>
<p>So as an experiment, we're sending a specially branded beer gift to some select companies in Belgium. Obviously, with some <em>Moeder Overste</em> inside of it.</p>
<p><img src="/media/blog/12OfqQMNFSWgWxpJYL2lE9d3dt9FuFZgnuoDVuGS.jpg" alt="" /></p>
<p>The outside of the beer packages have our branding, with the backside a small text explaining what Oh Dear does.</p>
<p><img src="/media/blog/OjL3GuNa1hNeO8SNeXsIUe5cBgg4dgBfPGtTcZ0g.jpg" alt="" /></p>
<p>Inside the packaging, there's a personal and handwritten note for the recipients. With so many word-puns available, we couldn't resist the &quot;cold emails&quot; angle. 😁</p>
<p><img src="/media/blog/TBju34LAyOULnfjSlpNFL0Emv1YPXNxEy83qmCWM.jpg" alt="" /></p>
<p>Will this work and actually convert to Oh Dear users? We have no idea yet, but we're about to find out. 😎</p>
<h2 id="not-just-for-new-clients">Not just for new clients <a href="#not-just-for-new-clients" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#not-just-for-new-clients">#</a></h2>
<p>We're trying this as an experiment, but we've already heard from some of you that you'd love a package like this to be delivered. And you're right, why should we only try to convince new users and not treat loyal customers just the same?</p>
<p>We're looking into extending our beer-delivery-operation to some of our existing clients as well, as a token of gratitude for the trust placed in Oh Dear to keep watch over all your sites. Realistically though, these packages aren't cheap to make ... so ... the bigger your Oh Dear subscription, the more chance you'll have of receiving a package. <em>It's marketing after all.</em> 😇</p>
<p><em>(Ps; we can only do Belgium for now, turns out, shipping alcohol across the border comes with its own challenges)</em></p>
]]>
            </summary>
                                    <updated>2025-10-02T17:33:52+00:00</updated>
        </entry>
            <entry>
            <title><![CDATA[Apply for the AI Monitoring Closed Beta]]></title>
            <link rel="alternate" href="https://ohdear.app/news-and-updates/apply-for-the-ai-monitoring-closed-beta" />
            <id>https://ohdear.app/170</id>
            <author>
                <name><![CDATA[Oh Dear]]></name>
            </author>
            <summary type="html">
                <![CDATA[<p>We're currenlty developing a new exciting feature: <strong>AI monitoring</strong>. You will be able to describe any check using natural language, making monitoring more intuitive and powerful than ever. You can now <a href="https://tally.so/r/wbPDbE">apply for the AI monitoring closed beta</a>.</p>
<h2 id="what-ai-monitoring-can-do">What AI Monitoring Can Do <a href="#what-ai-monitoring-can-do" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#what-ai-monitoring-can-do">#</a></h2>
<p>Our AI monitoring system comes equipped with comprehensive capabilities:</p>
<p><strong>Internet Access and sending notications</strong>: The AI can check live websites in real-time. It is also able to use our notification system.</p>
<blockquote>
<p>Make sure the homepage of mysite.com lists at least five products, includes a login link, and has a registration page link. Send me a notification if any of these elements are missing.</p>
</blockquote>
<p><strong>DNS Record Analysis</strong>: Full access to all DNS records for comprehensive domain monitoring</p>
<blockquote>
<p>Verify that all MX records for spatie.be point to Google. If they don't, send me a notification and show all current MX records in the result.</p>
</blockquote>
<p><strong>API Testing</strong>: Complete support for POST requests with custom payloads and headers</p>
<blockquote>
<p>Send a POST request to mysite.com/api/my-endpoint with this payload:</p>
<ul>
<li>first_name: &quot;John&quot;</li>
<li>last_name: &quot;Doe&quot;</li>
</ul>
<p>Include this HTTP header:</p>
<ul>
<li>my-custom-header: my-custom-header-value</li>
</ul>
<p>The response code should be 200. Send me a notification if the response code is anything other than 200.</p>
</blockquote>
<p>These are just a couple of simple examples. We’re curious to know how you would use this feature.</p>
<h2 id="join-our-closed-beta">Join Our Closed Beta <a href="#join-our-closed-beta" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#join-our-closed-beta">#</a></h2>
<p>We're preparing to launch our closed beta program and need passionate users to help us test and refine this feature. If you're interested in being among the first to experience AI monitoring and want to help shape its development, we'd love to hear from you.</p>
<p><strong><a href="https://tally.so/r/wbPDbE">Apply for the AI monitoring closed beta</a></strong></p>
<p>Your feedback will be invaluable in making this feature the best it can be.</p>
<p>We're aiming to add people to our closed beta in October, and launch the feature for all users in November.</p>
]]>
            </summary>
                                    <updated>2025-09-26T15:06:47+00:00</updated>
        </entry>
            <entry>
            <title><![CDATA[Introducing Request Mirror: a free micro-service to reflect HTTP requests]]></title>
            <link rel="alternate" href="https://ohdear.app/news-and-updates/introducing-request-mirror-a-free-micro-service-to-reflect-http-requests" />
            <id>https://ohdear.app/169</id>
            <author>
                <name><![CDATA[Oh Dear]]></name>
            </author>
            <summary type="html">
                <![CDATA[<p>We have launched <a href="https://request-mirror.ohdear.app">Request Mirror</a>, a little free service to reflect HTTP requests. We've also open-sourced it: you can read the code in the <a href="https://github.com/ohdearapp/request-mirror.ohdear.app">ohdearapp/request-mirror.ohdear.app</a> repo on GitHub.</p>
<p>In this blog post I'd like to explain why we built it and how you can use it.</p>
<h2 id="why-we-needed-to-reflect-http-requests">Why we needed to reflect HTTP requests <a href="#why-we-needed-to-reflect-http-requests" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#why-we-needed-to-reflect-http-requests">#</a></h2>
<p>One of the many checks we offer is our uptime check. Like all our checks, we have an extensive test suite for our uptime monitoring functionality. We have tests that prove that when a site is down, we actually do warn our users.</p>
<p>So, in our end-to-end tests we need to have a site available that is considered down. Instead of bringing an actually service down, wouldn't it be nice if we could just tell a website what status code it should return?</p>
<p>That's exactly the sort of thing that Request Mirror can do. It has a route <code>/status/</code> that you can append with any status code you want. So a request to <code>https://request-mirror.ohdear.app/status/500</code> will respond with status code 500, mimicking a website that is down. Try it by visiting that URL in the browser and use your developer tools to see the status code.</p>
<p>In our tests we can use this:</p>
<pre data-lang="php" class="notranslate"><span class="hl-comment">// I've simplified the test for brevity</span>
<span class="hl-property">it</span>(<span class="hl-value">'can send a notification when the site is down'</span>, <span class="hl-keyword">function</span> () {
   <span class="hl-property">performUptimeCheck</span>(<span class="hl-value">'https://request-mirror.ohdear.app/status/500'</span>);
    
   <span class="hl-type">Notification</span>::<span class="hl-property">assertSent</span>(<span class="hl-type">UptimeCheckFailedNotification</span>::<span class="hl-keyword">class</span>);
});
</pre>
<h2 id="other-endpoints">Other endpoints <a href="#other-endpoints" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#other-endpoints">#</a></h2>
<p>Our uptime check has <a href="https://ohdear.app/docs/features/uptime-and-performance-monitoring#uptime-options">many options</a> to tailor it to your specific monitoring needs. To be able to test all these options, we added extra endpoints to Request Mirror.</p>
<p>Let's take a look at the ability to reflect headers:</p>
<pre data-lang="php" class="notranslate"><span class="hl-variable">$response</span> = <span class="hl-type">Http</span>::<span class="hl-property">withHeaders</span>([
    <span class="hl-value">'extra-header'</span> =&gt; <span class="hl-value">'extra-value'</span>,
])-&gt;<span class="hl-property">get</span>(<span class="hl-value">'https://request-mirror.ohdear.app/get'</span>);

<span class="hl-comment">// the json response will have a headers property that contains</span>
<span class="hl-comment">// the `extra-header` header.</span>
<span class="hl-variable">$response</span>-&gt;<span class="hl-property">json</span>(<span class="hl-value">'headers'</span>);
</pre>
<p>These are also available:</p>
<ul>
<li><a href="https://request-mirror.ohdear.app/ip">https://request-mirror.ohdear.app/ip</a>: returns your IP</li>
<li><a href="https://request-mirror.ohdear.app/user-agent">https://request-mirror.ohdear.app/user-agent</a>: returns your user agent</li>
<li><a href="https://request-mirror.ohdear.app/json">https://request-mirror.ohdear.app/json</a>: returns a bit of random JSON</li>
<li><a href="https://request-mirror.ohdear.app/xml">https://request-mirror.ohdear.app/xml</a>: returns a bit of random XML</li>
</ul>
<p>And there are a few more listed on <a href="https://request-mirror.ohdear.app">Request Mirror</a>.</p>
<h2 id="an-alternative-httpbin">An alternative: HTTPBin <a href="#an-alternative-httpbin" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#an-alternative-httpbin">#</a></h2>
<p>There's also an excellent alternative that's been around for a long time: <a href="https://httpbin.org">HTTP Bin</a>. Request Mirror was heavily inspired by HTTP Bin.</p>
<p>Even though HTTP Bin works well, I still took the time to build Request Mirror, because the uptime of HTTP Bin was a bit flaky in the past period, causing our tests to fail sometimes. And with Request Mirror being in my control, I can add any endpoint Oh Dear needs for its test suite.</p>
<h2 id="in-closing">In closing <a href="#in-closing" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#in-closing">#</a></h2>
<p>Granted, <a href="https://request-mirror.ohdear.app">Request Mirror</a> is not the most exciting project, but if you need this kind of functionality it can come quite handy.</p>
<p>Should you need another endpoint for your testing, I'm open for pull requests to <a href="https://github.com/ohdearapp/request-mirror.ohdear.app">the request mirror repo on GitHub</a>.</p>
]]>
            </summary>
                                    <updated>2025-09-22T16:47:53+00:00</updated>
        </entry>
            <entry>
            <title><![CDATA[You can now connect your AI to Oh Dear]]></title>
            <link rel="alternate" href="https://ohdear.app/news-and-updates/you-can-now-connect-your-ai-to-oh-dear" />
            <id>https://ohdear.app/168</id>
            <author>
                <name><![CDATA[Oh Dear]]></name>
            </author>
            <summary type="html">
                <![CDATA[<p>Today, we have launched <a href="https://ohdear.app/docs/tools-and-sdks/mcp-server">our MCP server</a>. MCP (Model Context Protocol) is a standardized way for AI models to connect with external data sources and tools.</p>
<p>If you use a tool like <a href="https://claude.com/product/claude-code">Claude Code</a>, then this is how you can connect Oh Dear to it (you can <a href="https://ohdear.app/user/api-tokens">create an API token</a> in your account settings)</p>
<pre data-lang="bash" class="notranslate">claude mcp add <span class="hl-attribute">--transport</span> http ohdear https://ohdear.app/mcp \
--header <span class="hl-value">&quot;Authorization: Bearer YOUR-API-TOKEN-HERE&quot;</span>
</pre>
<h2 id="some-examples">Some examples <a href="#some-examples" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#some-examples">#</a></h2>
<p>Here are a couple of example prompts</p>
<p>“Analyse the performance of spatie.be of the last week”</p>
<p><img src="/media/blog/40imjDgf0CR5P71mpr0NeE6mRwg0jOOjNaDFwyie.jpg" alt="" /></p>
<p>“Which DNS records changed recently at spatie.be”</p>
<p><img src="/media/blog/JzIQoHOy6L2WWs1tJMucQM3d7UBBQ42lAEGsoaU9.jpg" alt="" /></p>
<p>“Show me the trends of the lighthouse reports of spatie.be”</p>
<p><img src="/media/blog/GUzjpSSb0jWwNNW6pcKgteVuk7FL8aqg92Liv6OS.jpg" alt="" /></p>
<p>This is very powerful stuff. I’m curious which prompts you will try.</p>
<h2 id="in-closing">In closing <a href="#in-closing" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#in-closing">#</a></h2>
<p>Our MCP server can access all checks and their results, so you can ask questions about anything that Oh Dear is monitoring for you.</p>
<p>You learn how to get started an view a few more examples in on <a href="https://ohdear.app/docs/tools-and-sdks/mcp-server">the dedicted MCP server page in our docs</a>.</p>
<p>We’re still improving our MCP server. If you have some unexpected results, or think a result can be better, do reach out to <a href="mailto:ohdeear.">support@ohdear.app</a></p>
]]>
            </summary>
                                    <updated>2026-02-24T12:04:04+00:00</updated>
        </entry>
            <entry>
            <title><![CDATA[SQL performance improvements: finding the right queries to fix (part 1)]]></title>
            <link rel="alternate" href="https://ohdear.app/news-and-updates/sql-performance-improvements-finding-the-right-queries-to-fix-part-1" />
            <id>https://ohdear.app/165</id>
            <author>
                <name><![CDATA[Oh Dear]]></name>
            </author>
            <summary type="html">
                <![CDATA[<p>A few weeks ago, we <a href="/news-and-updates/speed-improvements-to-the-dashboard-website-job-processing">massively improved the performance</a> of the dashboard &amp; website by optimizing some of our SQL queries. In this post, we'll share how we identified the queries that needed work. In the next post, we'll explore <em>how</em> we fixed each of them.</p>
<p>We'll cover the basics and gradually work our way up to the more advanced/complex ways of identifying slow queries.</p>
<p>In this post, you'll see:</p>
<ul>
<li><a href="#enable-the-debug-bar-in-your-local-environment">Using a local debug-bar to identify queries</a></li>
<li><a href="#let-mysql-tell-you-which-queries-are-slow">Using MySQL slow query log</a></li>
<li><a href="#let-mysql-tell-you-which-queries-lack-indexes">Logging queries that don't use indexes</a></li>
<li><a href="#let-mysql-tell-you-which-queries-are-executed-right-now">Evaluating currently running queries live</a></li>
<li><a href="#let-mysql-tell-you-which-queries-get-executed-the-most">Analysing the global query log</a></li>
<li>2 MySQL bonusses: <a href="#mysql-bonus-1-sort-output-vertically-not-horizontally">better CLI output</a> &amp; <a href="#mysql-bonus-2-write-query-output-to-a-file">redirecting output to files</a></li>
<li><a href="#let-laravel-warn-you-for-n1-queries">Enforce eager loading in local Laravel environment</a></li>
</ul>
<p>Let's go!</p>
<h2 id="what-these-results-look-like">What these results look like <a href="#what-these-results-look-like" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#what-these-results-look-like">#</a></h2>
<p>As a reminder, this is the resulting performance gain for the dashboard &amp; some of our internal APIs:</p>
<p><img src="/media/blog/RF9LvvgCQU1skDedrUkm0l6CEfuG7qTJ8Bu0XklO.png" alt="" /></p>
<p><img src="/media/blog/1HEzbWpII2U7qyCcl9f708IdWFgFDeGUVcYhqNZ1.png" alt="" /></p>
<p>These graphs come from the <a href="/features/global-uptime-monitoring">Oh Dear uptime &amp; performance monitoring we perform</a>.</p>
<p>Now let's get started identifying which queries need optimising.</p>
<h2 id="enable-the-debug-bar-in-your-local-environment">Enable the debug-bar in your local environment <a href="#enable-the-debug-bar-in-your-local-environment" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#enable-the-debug-bar-in-your-local-environment">#</a></h2>
<p>The easiest place to start investigating queries is locally, in your development environment. Most frameworks have the concept of a &quot;debug bar&quot; - in the case of Laravel applications, the most widely used is the <a href="https://github.com/barryvdh/laravel-debugbar">barryvdh/laravel-debugbar</a> package that offers excellent insights.</p>
<p>Once enabled, you can see output similar to this:</p>
<p><img src="/media/blog/lpZ1GLA6rNmNyfkkmsq6CHq0uDNKV4RZJWNdTbKf.png" alt="" />
It contains, at a glance:</p>
<ul>
<li>The total time spent executing SQL queries (top-right)</li>
<li>The total amount of queries executed (top-left)</li>
<li>How many of those were duplicates (indicating potential N+1 loop issues)</li>
</ul>
<p>Before optimising any query, it makes sense to ask the question: can the query be avoided in the first place? Duplicate queries are worth investigating, as well as queries that don't add meaning to the page you're seeing (ie a Class being lazy-loaded whose data isn't needed on this page).</p>
<p>In our case, if we're looking at the data being loaded on an <a href="https://ohdear.app/features/global-uptime-monitoring">uptime monitor</a>, we wouldn't expect a SQL query to load data for a <a href="https://ohdear.app/features/broken-page-and-mixed-content-detection">broken link monitor</a>. For this, you need application-awareness to know what data makes sense to load on that page.</p>
<h2 id="let-mysql-tell-you-which-queries-are-slow">Let MySQL tell you which queries are slow <a href="#let-mysql-tell-you-which-queries-are-slow" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#let-mysql-tell-you-which-queries-are-slow">#</a></h2>
<p>MySQL has the ability to enable a &quot;slow query log&quot;, where you get to decide what qualifies as a slow query. This is the easiest step to get started, as MySQL will log to disk the SQL queries that exceeded your threshold.</p>
<p>First, create the file to be used as the log (as the <code>root</code> user):</p>
<pre data-lang="txt" class="notranslate">$ touch /var/log/mysql-slow-query.log
$ chown mysql:mysql /var/log/mysql-slow-query.log
</pre>
<p>This creates an empty file and allows MySQL to read &amp; write to it. If the file doesn't exist, MySQL won't create it for you, it just won't log anything.</p>
<p>Next, hop in your MySQL command-line and activate the <a href="https://dev.mysql.com/doc/refman/8.4/en/slow-query-log.html">Slow Query Log</a>.</p>
<pre data-lang="txt" class="notranslate">$ mysql
mysql&gt; SET GLOBAL slow_query_log_file = '/var/log/mysql-slow-query.log';
mysql&gt; SET GLOBAL long_query_time = 1;
mysql&gt; SET GLOBAL slow_query_log = 'ON';
</pre>
<p>From this point, MySQL will log every query that exceeded the 1s threshold in your log file. Tweak the &quot;long query time&quot; as you see fit.</p>
<pre data-lang="txt" class="notranslate">$ tail -f /var/log/mysql-slow-query.log

# Query_time: 2.547717  Lock_time: 0.000003 Rows_sent: 0  Rows_examined: 0
SET timestamp=1757095277;
select * from `runs` where `runs`.`check_id` = ...;
</pre>
<p>This will give you a list of your slow queries, ready to be optimized.</p>
<p>The example above will modify your currently running MySQL instance to log queries, but if you restart your MySQL server, the settings won't be persisted. If you want to have this enabled all the time, it needs to be added to your <code>my.cnf</code> config file:</p>
<pre data-lang="txt" class="notranslate">[mysqld]
slow_query_log = ON
slow_query_log_file = /var/log/mysql-slow-query.log
long_query_time = 1
</pre>
<p>You can gradually increase the slow query threshold, MySQL allows decimal values to log queries that are faster than 500ms, 300ms, etc.</p>
<pre data-lang="txt" class="notranslate">mysql&gt; SET GLOBAL long_query_time = 0.3;
</pre>
<p>This would let MySQL log all queries that are slower than 300ms.</p>
<p>If the results become too verbose, you can tweak how many queries get logged a bit more by setting a minimal amount of rows that a query should return, before it's logged.</p>
<pre data-lang="txt" class="notranslate">mysql&gt; SET GLOBAL min_examined_row_limit = 1000;
</pre>
<p>Queries that examine fewer than this number of rows will not be logged to the slow query log.</p>
<h2 id="let-mysql-tell-you-which-queries-lack-indexes">Let MySQL tell you which queries lack indexes <a href="#let-mysql-tell-you-which-queries-lack-indexes" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#let-mysql-tell-you-which-queries-lack-indexes">#</a></h2>
<p>Spoiler alert: a fast query <em>usually</em> has indexes on them that make retrieving the data blazing fast. We'll explore how to set those &amp; decide <em>which</em> ones to set in future posts.</p>
<p>MySQL can log all queries that are being executed that <em>don't</em> use an index for lookups. This can get a little noisy, especially if you haven't added indexes before, so this is a setting to enable once you've done the 2 tips shared above first, to trim down on the log-noise this might generate.</p>
<pre data-lang="txt" class="notranslate">mysql&gt; SET GLOBAL log_queries_not_using_indexes = ON;
</pre>
<p>This will log all sorts of queries, including queries like:</p>
<ul>
<li><code>SELECT * FROM users</code>: a full table scan (without a <code>WHERE</code> clause)</li>
<li><code>SELECT * FROM users WHERE email = 'mattias@ohdear.app'</code>: a <code>WHERE</code> clause on a non-index column</li>
<li><code>SELECT * FROM users WHERE UPPER(email) = 'MATTIAS@OHDEAR.APP</code>: a function call on an indexed column (this prevents index usage)</li>
<li><code>SELECT * FROM users WHERE name LIKE '%mattias%'</code>: using a leading wildcard in a LIKE statement</li>
</ul>
<p>I wouldn't recommend running <code>log_queries_not_using_indexes</code> all the time, just for debugging &amp; analysis purposes.</p>
<p>The <code>log_queries_not_using_indexes</code> is also compatible with the same <code>min_examined_row_limit</code> option we shared in the previous tip, so you can limit the logging of non-indexed queries by adding:</p>
<pre data-lang="txt" class="notranslate">mysql&gt; SET GLOBAL min_examined_row_limit = 1000;
</pre>
<p>If you have a high amount of queries without indexes <em>and</em> a busy MySQL server, the log activity can also become a bottleneck. Keep this in mind and disable it again once you're done with your analysis.</p>
<pre data-lang="txt" class="notranslate">mysql&gt; SET GLOBAL log_queries_not_using_indexes = OFF;
</pre>
<p>Ps; feels weird writing queries that set a value to what seems to be a string, right? The values for <code>OFF</code> and <code>ON</code> are reserved keywords, similar to <code>TRUE</code> or <code>FALSE</code>, so they don't need quotes.</p>
<h2 id="let-mysql-tell-you-which-queries-are-executed-right-now">Let MySQL tell you which queries are executed right now <a href="#let-mysql-tell-you-which-queries-are-executed-right-now" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#let-mysql-tell-you-which-queries-are-executed-right-now">#</a></h2>
<p>As a rule of thumb, I like to use: whenever I can see a query being executed <em>right now</em>, it probably can use some optimisations.</p>
<p>Think about that logic for a second: ideally, queries are finished under 10ms or faster. What are the odds that when I request the <em>current</em> processlist, a query will show up? It becomes such a narrow window that when you look at the current processlist a few times, and you see the same queries showing up over and over again, they're worth investigating.</p>
<p>Let's start with the basics:</p>
<pre data-lang="txt" class="notranslate">mysql&gt; SHOW FULL PROCESSLIST;
</pre>
<p>If this returns a lot of results, you can use the raw SQL query to be able to filter the results based on your own criteria. The <code>SHOW FULL PROCESSLIST</code> is essentially a shortcut for the following SQL query:</p>
<pre data-lang="txt" class="notranslate">mysql&gt; SELECT 
		ID,
		USER,
		HOST,
		DB,
		COMMAND,
		TIME,
		STATE,
		INFO
FROM performance_schema.processlist
ORDER BY ID;
</pre>
<p>So you're able to trim down the output a little by avoiding Sleeping connections or from databases or users you don't need (if you're hosting multiple databases on this system);</p>
<pre data-lang="txt" class="notranslate">mysql&gt; SELECT 
		ID,
		USER,
		HOST,
		DB,
		COMMAND,
		TIME,
		STATE,
		INFO
FROM performance_schema.processlist
WHERE DB = 'ohdear' AND COMMAND != 'Sleep';
</pre>
<p>If you run this command a few times in a row and you see the same types of queries showing up, even if they have indexes, they're worth noting for followup.</p>
<p>The output of <code>PROCESSLIST</code> and the content of the <code>performance_schema.processlist</code> have a limitation that the <code>TIME</code> only has seconds granularity, so it's hard to sort on. As a workaround, once you need more insights into &lt; 1 second queries, you can run the following query:</p>
<pre data-lang="txt" class="notranslate">mysql&gt; SELECT 
	t.processlist_id,
	t.processlist_user,
	t.processlist_host,
	t.processlist_db,
	t.processlist_command,
	t.processlist_state,
	t.processlist_info,
	ROUND(s.timer_wait/1000000, 2) as execution_time_ms
FROM performance_schema.threads t
JOIN performance_schema.events_statements_current s ON t.thread_id = s.thread_id
WHERE t.processlist_command != 'Sleep' 
  AND s.timer_wait IS NOT NULL
ORDER BY s.timer_wait DESC;
</pre>
<p>This will show the time spent in miliseconds in the <code>execution_time_ms</code> column.</p>
<h2 id="let-mysql-tell-you-which-queries-get-executed-the-most">Let MySQL tell you which queries get executed the most <a href="#let-mysql-tell-you-which-queries-get-executed-the-most" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#let-mysql-tell-you-which-queries-get-executed-the-most">#</a></h2>
<p><strong>Caution: on high-traffic MySQL servers, this will become a major bottleneck! Be careful.</strong></p>
<p>This one is best to enable locally, in your development environment. Sure, you can try to enable this on staging or production, but there's a very big chance your disk I/O won't survive that setting. Be careful when doing this on anything <em>but</em> your local development environment.</p>
<p>I know some will #YOLO this in production, don't @ me when your system breaks. :-)</p>
<p>MySQL can log <em>all</em> queries that are being executed, with the ability to then fire off some CLI tools on that big query log to identify the most recurring queries.</p>
<p>First, similar to the Slow Query Log, create the file MySQL can write to:</p>
<pre data-lang="txt" class="notranslate">$ touch /var/log/mysql-general-query.log
$ chown mysql:mysql /var/log/mysql-general-query.log
</pre>
<p>Next, instruct MySQL to log all queries to that file:</p>
<pre data-lang="txt" class="notranslate">mysql&gt; SET GLOBAL general_log_file = '/var/log/mysql-general-query.log';
mysql&gt; SET GLOBAL general_log = ON;
</pre>
<p>To disable the general log again:</p>
<pre data-lang="txt" class="notranslate">mysql&gt; SET GLOBAL general_log = OFF;
</pre>
<p>Now you can - again, locally - open your application, run your jobs &amp; scheduled tasks, run your CI tests, ... and every query will be logged to the <code>/var/log/mysql-general-query.log</code> file. The output will be something like this:</p>
<pre data-lang="txt" class="notranslate">$ tail -f /var/log/mysql-general-query.log
2025-09-16T11:40:04.736814Z	   94 Prepare	select * from `plan_prices` where `archived` = ? and `is_yearly` = ? and `currency` = ? order by `amount` asc limit 1
2025-09-16T11:40:04.736987Z	   94 Execute	select * from `plan_prices` where `archived` = 0 and `is_yearly` = 0 and `currency` = 'eur' order by `amount` asc limit 1
2025-09-16T11:40:04.737575Z	   94 Close stmt
2025-09-16T11:40:04.745635Z	   94 Prepare	select * from `transformation_results` where `url` = ? and `type` = ? limit 1
2025-09-16T11:40:04.745773Z	   94 Execute	select * from `transformation_results` where `url` = 'https://ohdear.app.test/pricing' and `type` = 'ldJson' limit 1
2025-09-16T11:40:04.746173Z	   94 Close stmt
2025-09-16T11:40:04.768509Z	   94 Prepare	select * from `users` where `id` = ? limit 1
2025-09-16T11:40:04.768763Z	   94 Execute	select * from `users` where `id` = 27765 limit 1
2025-09-16T11:40:04.769149Z	   94 Close stmt
</pre>
<p>This quickly becomes overwhelming &amp; you won't be able to parse this manually anymore. That's where the CLI comes in. Percona offers some amazing scripts for us to use, let's install those first:</p>
<pre data-lang="txt" class="notranslate">$ apt install percona-toolkit # for debian/ubuntu
$ brew install percona-toolkit # for Mac
</pre>
<p>The <code>pt-query-digest</code> tool allows us to analyse this general log and sort the queries by either count or time-spent in the database. We just need a quick fix in our log, because <code>pt-query-digest</code> only looks at the keyword <code>Query</code> in the log, and we're using Prepared Statements so we have a lot of entries with <code>Prepare</code> and <code>Execute</code> that the tool will happily ignore. Since each <code>Execute</code> is just a <code>Query</code> that was executed, we can simply rename them in our logfile.</p>
<pre data-lang="txt" class="notranslate">$ sed 's/Execute\t/Query\t/' /var/log/mysql-general-query.log &gt; /var/log/mysql-general-query-editted.log
</pre>
<p>Then, let's fire off the analysis:</p>
<pre data-lang="txt" class="notranslate">$ pt-query-digest \
	--type=genlog \
	--group-by fingerprint \
	--order-by Query_time:cnt,Query_time:sum \
	--filter '$event-&gt;{cmd} =~ /^(Query|Execute)$/' \
	--limit 25 \
	/var/log/mysql-general-query-editted.log
</pre>
<p>The output is a bit noisy, but tells you which queries were executed most (<code>Count</code> attribute) and which spent most time (<code>Exec time</code> attribute). Any performance gain that can be made to the most-occuring queries will yield better results.</p>
<p>Improving a query from 25ms to 20ms may sound negligible, but if it's performed millions of times a day, it adds up.</p>
<h2 id="mysql-bonus-1-sort-output-vertically-not-horizontally">MySQL bonus 1: sort output vertically, not horizontally <a href="#mysql-bonus-1-sort-output-vertically-not-horizontally" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#mysql-bonus-1-sort-output-vertically-not-horizontally">#</a></h2>
<p>If you're working via the CLI, some of the queries I shared above can have quite lengthy output that's not always easy to see on small screens. You can change some of the outputs with <em>modifiers</em> in MySQL.</p>
<p>One very useful modifier is the <code>\G</code> control character - that's backslash + G.</p>
<p>This is a normal query output via the CLI:</p>
<pre data-lang="txt" class="notranslate">mysql&gt; SELECT id, name FROM teams LIMIT 2;
+----+--------------+
| id | name         |
+----+--------------+
|  1 | Team Mattias |
|  2 | Spatie       |
+----+--------------+
2 rows in set (0.00 sec)
</pre>
<p>If you add in the <code>\G</code> suffix to a query, the results are shown vertically instead:</p>
<pre data-lang="txt" class="notranslate">mysql&gt; SELECT id, name FROM teams LIMIT 2 \G;
*************************  * 1. row ***************************
  id: 1
name: Team Mattias

*************************  * 2. row ***************************
  id: 2
name: Spatie
2 rows in set (0.00 sec)
</pre>
<p>That's a litteral <code>\G</code> (backslash + G) at the end of the query.</p>
<h2 id="mysql-bonus-2-write-query-output-to-a-file">MySQL bonus 2: write query output to a file <a href="#mysql-bonus-2-write-query-output-to-a-file" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#mysql-bonus-2-write-query-output-to-a-file">#</a></h2>
<p>If you miss using <code>grep</code>, <code>awk</code>, <code>sort</code>, ... in a MySQL shell, you're probably a greybeard Linux sysadmin and we should have some beers together. But good news, MySQLs' output can be redirected to a file for easier parsing!</p>
<pre data-lang="txt" class="notranslate">mysql&gt; \T /tmp/query-output.log
Logging to file '/tmp/query-output.log'

mysql&gt; SELECT * FROM teams;

mysql&gt; \t
Outfile disabled.
</pre>
<p>With <code>\T</code> you can specify which file this MySQL session should write all its output to. And with <code>\t</code> you can stop writing to that file. Afterwards, your file contains all the output of the queries you ran in between.</p>
<p><em>You can also use <code>INTO OUTFILE</code> but I find it a little cumbersome to tweak queries to achieve that, I'd rather mark a terminal session as &quot;log to file&quot;, do the queries I want, and then stop that logging.</em></p>
<h2 id="let-laravel-warn-you-for-n1-queries">Let Laravel warn you for N+1 queries <a href="#let-laravel-warn-you-for-n1-queries" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#let-laravel-warn-you-for-n1-queries">#</a></h2>
<p>The Laravel framework can warn you when you're (potentially) causing excessive queries by throwing an exception when you're lazy loading relationships.</p>
<p>You can enable this warning in your non-production environment by adding the following into your <code>boot()</code> method in your <code>AppServiceProvider</code>:</p>
<pre data-lang="txt" class="notranslate">public function boot(): void
{
    Model::preventLazyLoading(! $this-&gt;app-&gt;isProduction());
}
</pre>
<p>Now, every time you run a piece of code like this:</p>
<pre data-lang="txt" class="notranslate">$posts = Post::all();

foreach ($posts as $post) {
    echo $post-&gt;comments-&gt;count();
}
</pre>
<p>It'll throw an exception, because for every iteration through the loop, Laravel would perform an extra query to count the comments <em>of that specific $post</em> instead of loading them at once. The fix, in this example, would be to eager load the counts only - not all the comments.</p>
<pre data-lang="txt" class="notranslate">// Eager load the comment counts
$posts = Post::withCount('comments')-&gt;get();

foreach ($posts as $post) {
    echo $post-&gt;comments_count; // Note the _count suffix
}
</pre>
<p>This is an example that will optimise your query count with just minimal code changes.</p>
<h2 id="next-up-fixing-the-queries">Next up: fixing the queries <a href="#next-up-fixing-the-queries" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#next-up-fixing-the-queries">#</a></h2>
<p>In this post, we shared several ways of <em>identifying</em> which queries either get executed the most or which are the slowest. There's many ways to get this data, and you're able to pick whichever method you're most comfortable with or have access to.</p>
<p>In our next post, we'll deep-dive into the different ways of identifying what the bottleneck of a query is and how to resolve it.</p>
]]>
            </summary>
                                    <updated>2025-09-16T18:02:33+00:00</updated>
        </entry>
            <entry>
            <title><![CDATA[Speed improvements to the dashboard, website & job processing]]></title>
            <link rel="alternate" href="https://ohdear.app/news-and-updates/speed-improvements-to-the-dashboard-website-job-processing" />
            <id>https://ohdear.app/166</id>
            <author>
                <name><![CDATA[Oh Dear]]></name>
            </author>
            <summary type="html">
                <![CDATA[<p>The past month we dedicated time and resources into optimising the speed and experience of our public website, our dashboard and our behind-the-scenes uptime checks that we perform. Overall, our website and dashboard feels about <strong>2x to 3x faster</strong>.</p>
<p>The biggest gains are for our users that have &gt; 100 sites on their dashboard, they'll get a noticeably faster loading time. For those biggest users, the dashboard is quite litterally <em>10x</em> faster.</p>
<h2 id="what-these-results-look-like">What these results look like <a href="#what-these-results-look-like" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#what-these-results-look-like">#</a></h2>
<p>This is the historical performance of our homepage for instance:</p>
<p><img src="/media/blog/RF9LvvgCQU1skDedrUkm0l6CEfuG7qTJ8Bu0XklO.png" alt="" /></p>
<p>And one of our uptime location API endpoints:</p>
<p><img src="/media/blog/1HEzbWpII2U7qyCcl9f708IdWFgFDeGUVcYhqNZ1.png" alt="" /></p>
<p>These graphs come from the <a href="/features/global-uptime-monitoring">Oh Dear uptime monitoring we perform</a>.</p>
<p>Over the next few blogposts, we'll share the details on how we identified which areas to focus on and how we implemented the performance improvements across the board.</p>
]]>
            </summary>
                                    <updated>2025-09-16T09:31:37+00:00</updated>
        </entry>
            <entry>
            <title><![CDATA[You can now choose the frequency of checks]]></title>
            <link rel="alternate" href="https://ohdear.app/news-and-updates/you-can-now-choose-the-frequency-of-checks" />
            <id>https://ohdear.app/162</id>
            <author>
                <name><![CDATA[Oh Dear]]></name>
            </author>
            <summary type="html">
                <![CDATA[<p>As part of our big deploy that added <a href="/news-and-updates/introducing-ping-and-tcp-port-monitoring-and-lots-of-other-improvements">ping and TCP monitoring</a>, we’ve also shipped a small, but often requested feature: you can now choose the frequency of the check we run.</p>
<p>By default, we check your website for uptime every minute. The Lighthouse check runs daily. Using our new feature, you can now, for instance, choose that the uptime check should run every 2 minutes, and the Lighthouse check every 5 days.</p>
<p>You can choose the frequency at the settings of the check.</p>
<p><img src="/media/blog/zss3rg0i90NWkWsuNdBdgfsNzp4wk0El017pD1dJ.jpg" alt="" /></p>
<p>Most people won’t need this option. For people who host multiple sites on one server, all monitored by Oh Dear, you can decrease the check frequency to have less stress on the server.</p>
]]>
            </summary>
                                    <updated>2025-09-13T17:14:43+00:00</updated>
        </entry>
            <entry>
            <title><![CDATA[Introducing ping and TCP port monitoring (and lots of other improvements)]]></title>
            <link rel="alternate" href="https://ohdear.app/news-and-updates/introducing-ping-and-tcp-port-monitoring-and-lots-of-other-improvements" />
            <id>https://ohdear.app/164</id>
            <author>
                <name><![CDATA[Oh Dear]]></name>
            </author>
            <summary type="html">
                <![CDATA[<p>A couple months ago, we sent out <a href="https://tally.so/r/nPLODP">a survey</a> to all our users asking what they like about Oh Dear, how they use it, and how we could improve our service.</p>
<p>One of the things that was asked a lot was ping and TCP port monitoring. The past few months we worked hard to add this kind of monitoring to our service. And while building it, we touched upon other parts of our service and improved lots of little things.</p>
<p>And I'm proud to share that we now have shipped it all! Let's go through it!</p>
<h2 id="goodbye-sites-welcome-monitors">Goodbye sites, welcome monitors <a href="#goodbye-sites-welcome-monitors" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#goodbye-sites-welcome-monitors">#</a></h2>
<p>Let's take a look at the dashboard at Oh Dear.</p>
<p><img src="/media/blog/TfLyEMIT7PLEN0XzV9UBthhXiwsjvCbcbrefkElH.jpg" alt="" /></p>
<p>Long time Oh Dear users will probably notice that &quot;sites&quot; isn't in the main navigation anymore. It has been replaced by &quot;monitors&quot;.</p>
<p>Since Oh Dear started, we always had the concept of a &quot;Site&quot; you're monitoring. But with the introduction of Ping and TCP checks, we can now monitor more than just a site.</p>
<p>That's why we renamed the &quot;Site&quot; concept in Oh Dear to &quot;Monitor&quot;. Not only the main navigation has changed, but we updated all copy throughout our entire app and docs.</p>
<p>A monitor will have a type:</p>
<ul>
<li>HTTP: for checking websites</li>
<li>Ping: for checking servers via ICMP ping</li>
<li>TCP: for checking the connectivity of a port over TCP</li>
</ul>
<p>When you now create a monitor in Oh Dear, this new dialog pops up, which lets you choose the type of monitor.</p>
<p><img src="/media/blog/LXuYnULvwj4t8e2hRQoUI9zm4WYb3njBKi1UtcNt.jpg" alt="" /></p>
<h2 id="monitor-your-servers-via-ping">Monitor your servers via ping <a href="#monitor-your-servers-via-ping" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#monitor-your-servers-via-ping">#</a></h2>
<p>Let's explore the new ping monitoring type. With HTTP monitoring we're sending an HTTP request to your app to see if it works. With ping monitoring, we're sending an ICMP(v6) ping packet to your server, so this kind of monitoring is more low-level. Ping checks if your server is responding at the most basic network level. If ping fails, you know there's a fundamental connectivity issue before users even try to access your site.</p>
<p>Adding a ping monitor is easy, just select the ping type, enter the address (either a FQDN hostname or the IPv4 or IPv6 address) of your server and you're good to go.</p>
<p><img src="/media/blog/cUNwQbOsWXuiSeB9qD6mFaVC0ti1wkwrvlcF6fku.jpg" alt="" /></p>
<p>On the uptime report of a ping check, you can see the average response time, together with the raw ping output.</p>
<p><img src="/media/blog/4wSFwpTyLhhGj0VXYf7vKIPlyAY7QIqLT1v3xILO.jpg" alt="" /></p>
<p>When you scroll down a bit, you'll see a full graph of how the ping check performed over time.</p>
<p><img src="/media/blog/TlZiheQ1utkM2h1AJ7Ka05GwrcdpS0a7kekwambs.jpg" alt="" /></p>
<p>Needless to say, whenever the ping fails, we'll send you notifications, via one of our <a href="https://ohdear.app/docs/notifications/notifications">many available notification channels</a>.</p>
<p>The ping check can also be customized to your heart's content. You can choose how many packets should be sent, what their size should be, define the acceptable packet loss, and much more.</p>
<p><img src="/media/blog/99hVayy6C8XnXaa5L15n18JEMKQiSgryUIwrd4Z3.jpg" alt="" /></p>
<h2 id="monitor-tcp-ports">Monitor TCP ports <a href="#monitor-tcp-ports" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#monitor-tcp-ports">#</a></h2>
<p>Let's take a look at the second new monitoring type: TCP ports.</p>
<p>TCP port monitoring checks if specific services are running and accepting connections on their designated ports, giving you more targeted insight than basic ping monitoring: While ping only tells you the server is reachable, TCP port monitoring verifies that specific services (web server on port 80/443, database on 3306, SSH on 22, etc.) are actually listening and responding.</p>
<p>When creating a monitor, you can now choose the TCP type.</p>
<p><img src="/media/blog/4N9yjryvtywF3lKbxVUWfX1UB3AHsvgj5Yu5I1Fu.jpg" alt="" /></p>
<p>For this example we're going to monitor <code>smtp.google.com:587</code>, Gmail's mail server.</p>
<p>Let's maybe look at the settings of a TCP monitor first.</p>
<p><img src="/media/blog/15gWAwI8IaFn3LSdmwpK7b9hQKIVJS6LFt9CMjNx.jpg" alt="" /></p>
<p>On the image above you see that we can check the welcome message the server will send back. If the welcome message doesn't contain the string given, we'll regard the server as down. Optionally, you can also send a message to the server, and also check if the response we get for the message contains a substring. This way you can really test particular behavior of your server.</p>
<p>On the TCP result page, we'll show the welcome message (and also the response of the optional message when it has been set)</p>
<p><img src="/media/blog/UVIINETR1YUugPoVB659QcxZ82Ed3V8lOViQUJsL.jpg" alt="" /></p>
<h2 id="our-api-has-been-updated-as-well">Our API has been updated as well <a href="#our-api-has-been-updated-as-well" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#our-api-has-been-updated-as-well">#</a></h2>
<p>Across our UI, notifications and docs, we have changed the language so we'll talk about &quot;Monitors&quot;, instead of &quot;Sites&quot;. We've also updated our entire codebase. Internally our <code>Site</code> model is renamed to <code>Monitor</code>, together with all relationships, foreign keys, views, controllers... Together with the technical rename, we'll also pay off other technical debt that was accrued over the years.</p>
<p>This has also affected our API. At Oh Dear, we value backwards compatibility. In the eight years that we've been in operation, we've never made a breaking change to our API. We also considered keeping a v1 of our API, but we decided against it because the maintenance burden would be too big.</p>
<p>On <a href="https://ohdear.app/docs/faq/how-do-i-update-to-the-latest-version-of-the-oh-dear-api">this FAQ page</a>, you can read more info on which exact breaking changes we've made, and how you can upgrade.</p>
<h2 id="a-new-homepage">A new homepage <a href="#a-new-homepage" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#a-new-homepage">#</a></h2>
<p>To promote ping and TCP monitoring, we added two dedicated new pages, <a href="https://ohdear.app/features/ping-monitoring">one for ping</a>, and <a href="https://ohdear.app/features/tcp-port-monitoring">one for TCP</a>. These pages both have beautiful illustrations, made by our designer <a href="https://github.com/nckrtl">Nick</a>.</p>
<p>We also took the time to update our homepage to mention the new feature, and also to show some nice screenshots from our app.</p>
<p><img src="/media/blog/mhBCrdw8AZUY3AWuRIsGrVSswmhEGc9VGqZbko4G.jpg" alt="" /></p>
<p>The features and docs menu were revamped as well.</p>
<p><img src="/media/blog/aRxzMUOQPXXvh7H9XdY9c2hVz9Vzy9tN3xzp1wsV.jpg" alt="" /></p>
<p><img src="/media/blog/k898lJJtYDcqgvBBU89yHrIjTDOALCdUt9Vadkcx.jpg" alt="" /></p>
<h2 id="bonus-feature-customize-the-frequency-of-the-checks">Bonus feature: customize the frequency of the checks <a href="#bonus-feature-customize-the-frequency-of-the-checks" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#bonus-feature-customize-the-frequency-of-the-checks">#</a></h2>
<p>We've also shipped another small, but often requested feature: you can now choose the frequency of the check we run.</p>
<p>By default, we check your website for uptime every minute. The Lighthouse check runs daily. Using our new feature, you can now for instance choose that the uptime check should run every 2 minutes, and the Lighthouse check every 5 days.</p>
<p>You can choose the frequency at the settings of the check.</p>
<p><img src="/media/blog/zss3rg0i90NWkWsuNdBdgfsNzp4wk0El017pD1dJ.jpg" alt="" /></p>
<h2 id="in-closing">In closing <a href="#in-closing" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#in-closing">#</a></h2>
<p>We hope you'll like these updates we've made to Oh Dear.</p>
<p>We aren't done and are building more exciting features, but it's a bit too early to talk about those. Should you have any suggestions on how to improve Oh Dear, do let us know by <a href="https://tally.so/r/nPLODP">filling in our survey</a> or just emailing <a href="mailto:support@ohdear.app">support</a>.</p>
<p>We'll close off this post by highlighting some of the features that were requested through our survey:</p>
<ul>
<li>We now <a href="/news-and-updates/introducing-our-new-notification-logs">log sent notifications</a></li>
<li><a href="https://ohdear.app/news-and-updates/exploring-our-new-php-sdk-built-using-saloon">Our new SDK has been rebuilt from the ground up</a></li>
<li>People with a big number of sites in their team reported that the site list screen is slow. We made it faster, and are still optimizing it.</li>
<li>There's <a href="https://ohdear.app/team-settings/usage">a new usage screen</a> that shows your usage against your current plan</li>
<li>Maintenance periods can now have a name</li>
<li>We can now <a href="https://ohdear.app/news-and-updates/we-now-support-google-chat">send notifications via Google Chat</a></li>
<li>The certificate health check has a new &quot;Detected Certificates&quot; tab showing the history of certificates we detected for a site.</li>
<li>A graph was added to the Lighthouse result history</li>
<li><a href="https://ohdear.app/news-and-updates">Our blog</a> layout was simplified.</li>
<li>We made it more clear across our UI which things are clickable, by adding the cursor-pointer CSS class to those elements</li>
<li>You can now <a href="https://ohdear.app/news-and-updates/managing-monthly-reports-with-the-api">manage monthly reports with our API</a></li>
<li>Alert snoozing now has <a href="https://ohdear.app/news-and-updates/targeted-snoozes-with-full-history">full history</a></li>
<li>A new login method was added: <a href="https://ohdear.app/news-and-updates/you-can-now-log-in-using-passkeys">passkeys</a></li>
<li>Our broken links check has <a href="https://ohdear.app/news-and-updates/broken-links-troubleshooting-and-a-couple-of-extras">a new &quot;Troubleshooting&quot; tab</a>, showing you tips to fix broken links.</li>
<li>We <a href="https://ohdear.app/news-and-updates/introducing-our-improved-uptime-check">merged our uptime and performance check</a></li>
</ul>
]]>
            </summary>
                                    <updated>2025-08-26T22:38:07+00:00</updated>
        </entry>
            <entry>
            <title><![CDATA[Exploring our new PHP SDK, built using Saloon]]></title>
            <link rel="alternate" href="https://ohdear.app/news-and-updates/exploring-our-new-php-sdk-built-using-saloon" />
            <id>https://ohdear.app/163</id>
            <author>
                <name><![CDATA[Oh Dear]]></name>
            </author>
            <summary type="html">
                <![CDATA[<p>Today, next to <a href="https://ohdear.app/news-and-updates/introducing-ping-and-tcp-port-monitoring-and-lots-of-other-improvements">Ping and TCP monitoring</a>, we've also launched a <a href="https://github.com/ohdearapp/ohdear-php-sdk">new PHP SDK package</a>, which has been rebuilt from scratch using the wonderful <a href="https://docs.saloon.dev">Saloon</a> library. Using our new SDK, you can easily use the entire Oh Dear API.</p>
<p>In this blog post, I'd like to show you how you can use the new SDK and how it works under the hood.</p>
<h2 id="using-the-sdk">Using the SDK <a href="#using-the-sdk" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#using-the-sdk">#</a></h2>
<p>With the SDK package installed (you'll only have to require it using <code>composer require ohdearapp/ohdear-php-sdk</code>), you'll be able to instantiate the Oh Dear class like this:</p>
<pre data-lang="php" class="notranslate"><span class="hl-keyword">use</span> <span class="hl-type">OhDear\PhpSdk\OhDear</span>;

<span class="hl-variable">$ohDear</span> = <span class="hl-keyword">new</span> <span class="hl-type">OhDear</span>(<span class="hl-value">'your-api-token'</span>);
</pre>
<p>That API token can be created on <a href="https://ohdear.app/user/api-tokens">the API tokens screen</a>.</p>
<p>On that <code>OhDear</code> class you can use one of the <a href="https://github.com/ohdearapp/ohdear-php-sdk/blob/monitors/README.md#usage">many available API methods</a>.</p>
<p>Here's how you would create a simple monitor.</p>
<pre data-lang="php" class="notranslate"><span class="hl-variable">$monitor</span> = <span class="hl-variable">$ohDear</span>-&gt;<span class="hl-property">createMonitor</span>([
    <span class="hl-value">'url'</span> =&gt; <span class="hl-value">'https://example.com'</span>,
    <span class="hl-value">'type'</span> =&gt; <span class="hl-value">'http'</span>,
    <span class="hl-value">'team_id'</span> =&gt; 1,
]);

<span class="hl-keyword">echo</span> <span class="hl-variable">$monitor</span>-&gt;<span class="hl-property">url</span>; <span class="hl-comment">// returns https://example.com</span>
</pre>
<p>Of course, we'll start performing all of our checks on a new monitor right away.</p>
<p>Here's how you would list all broken links we detect on a site.</p>
<pre data-lang="php" class="notranslate"><span class="hl-variable">$brokenLinks</span> = <span class="hl-variable">$ohDear</span>-&gt;<span class="hl-property">brokenLinks</span>(<span class="hl-variable">$monitorId</span>);

<span class="hl-keyword">foreach</span> (<span class="hl-variable">$brokenLinks</span> <span class="hl-keyword">as</span> <span class="hl-variable">$brokenLink</span>) {
    <span class="hl-keyword">echo</span> <span class="hl-value">&quot;Broken link: {$brokenLink-&gt;crawledUrl}&quot;</span>;
    <span class="hl-keyword">echo</span> <span class="hl-value">&quot;Status: {$brokenLink-&gt;statusCode}&quot;</span>;
    <span class="hl-keyword">echo</span> <span class="hl-value">&quot;Found on: {$brokenLink-&gt;foundOnUrl}&quot;</span>;
    <span class="hl-keyword">echo</span> <span class="hl-value">&quot;Link text: {$brokenLink-&gt;linkText}&quot;</span>;
    <span class="hl-keyword">echo</span> <span class="hl-value">&quot;Internal link: &quot;</span> . (<span class="hl-variable">$brokenLink</span>-&gt;<span class="hl-property">internal</span> <span class="hl-operator">?</span> <span class="hl-value">'Yes'</span> : <span class="hl-value">'No'</span>) . <span class="hl-value">&quot;&quot;</span>;
}
</pre>
<p>Here's a cool tidbit, you can't know from looking at the code above: you don't need to care about pagination. The <code>brokenLinks</code> method (and all other methods in our SDK that return multiple results) doesn't return a regular array. Instead it returns an iterator.</p>
<p>When you loop over the iterator, we will automatically fetch more pages of results from our API. It's completely transparent. So no matter how many broken links we detect on your site, looping over the iterator returned by <code>brokenLinks</code> will handle them all.</p>
<h2 id="built-on-top-of-saloon">Built on top of Saloon <a href="#built-on-top-of-saloon" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#built-on-top-of-saloon">#</a></h2>
<p>This way of handling pagination (with an iterator) is powered by <a href="https://docs.saloon.dev">Saloon</a>, which is a wonderful package to build modern PHP SDKs. I really had a fun time developing our SDK because Saloon streamlines everything so well.</p>
<p>At the heart of a Saloon powered SDK is <code>Connector</code>. This is the class that has all of the basic information of how to connect to an API. Here's the <code>OhDear</code> connector from our package (redacted for brevity).</p>
<pre data-lang="php" class="notranslate"><span class="hl-keyword">&lt;?php</span>

<span class="hl-keyword">namespace</span> <span class="hl-type">OhDear\PhpSdk</span>;

<span class="hl-keyword">use</span> <span class="hl-type">Saloon\Http\Connector</span>;
<span class="hl-keyword">use</span> <span class="hl-type">Saloon\Http\Request</span>;
<span class="hl-keyword">use</span> <span class="hl-type">Saloon\Http\Response</span>;
<span class="hl-keyword">use</span> <span class="hl-type">Throwable</span>;

<span class="hl-keyword">class</span> <span class="hl-type">OhDear</span> <span class="hl-keyword">extends</span> <span class="hl-type">Connector</span> <span class="hl-keyword">implements</span><span class="hl-type"> HasPagination
</span>{
    <span class="hl-keyword">use</span> <span class="hl-type">AcceptsJson</span>;
    <span class="hl-keyword">use</span> <span class="hl-type">AlwaysThrowOnErrors</span>;
   
    <span class="hl-keyword">protected</span> <span class="hl-type">string</span> <span class="hl-property">$apiToken</span>;

    <span class="hl-keyword">protected</span> <span class="hl-type">string</span> <span class="hl-property">$baseUrl</span>;

    <span class="hl-keyword">protected</span> <span class="hl-type">int</span> <span class="hl-property">$timeoutInSeconds</span>;

    <span class="hl-keyword">public</span> <span class="hl-keyword">function</span> <span class="hl-property">__construct</span>(<span class="hl-injection">
        <span class="hl-type">string</span> $apiToken,
        <span class="hl-type">string</span> $baseUrl = 'https:<span class="hl-comment">//ohdear.app/api/',</span>
        <span class="hl-type">int</span> $timeoutInSeconds = 10,
    </span>) {
        <span class="hl-variable">$this</span>-&gt;<span class="hl-property">apiToken</span> = <span class="hl-variable">$apiToken</span>;
        <span class="hl-variable">$this</span>-&gt;<span class="hl-property">baseUrl</span> = <span class="hl-property">rtrim</span>(<span class="hl-variable">$baseUrl</span>, <span class="hl-value">'/'</span>);
        <span class="hl-variable">$this</span>-&gt;<span class="hl-property">timeoutInSeconds</span> = <span class="hl-variable">$timeoutInSeconds</span>;
    }

    <span class="hl-keyword">public</span> <span class="hl-keyword">function</span> <span class="hl-property">resolveBaseUrl</span>(): <span class="hl-type">string</span>
    {
        <span class="hl-keyword">return</span> <span class="hl-variable">$this</span>-&gt;<span class="hl-property">baseUrl</span>;
    }

    <span class="hl-keyword">protected</span> <span class="hl-keyword">function</span> <span class="hl-property">defaultAuth</span>(): <span class="hl-type">TokenAuthenticator</span>
    {
        <span class="hl-keyword">return</span> <span class="hl-keyword">new</span> <span class="hl-type">TokenAuthenticator</span>(<span class="hl-variable">$this</span>-&gt;<span class="hl-property">apiToken</span>);
    }

    <span class="hl-keyword">protected</span> <span class="hl-keyword">function</span> <span class="hl-property">defaultHeaders</span>(): <span class="hl-type">array</span>
    {
        <span class="hl-keyword">return</span> [
            <span class="hl-value">'Accept'</span> =&gt; <span class="hl-value">'application/json'</span>,
            <span class="hl-value">'Content-Type'</span> =&gt; <span class="hl-value">'application/json'</span>,
        ];
    }

    <span class="hl-keyword">protected</span> <span class="hl-keyword">function</span> <span class="hl-property">defaultConfig</span>(): <span class="hl-type">array</span>
    {
        <span class="hl-keyword">return</span> [
            <span class="hl-value">'timeout'</span> =&gt; <span class="hl-variable">$this</span>-&gt;<span class="hl-property">timeoutInSeconds</span>,
        ];
    }

    <span class="hl-comment">// other methods</span>
}
</pre>
<p>For each different API request, you can create a class that extends <code>Saloon\Http\Request</code>. In the class, you can define the URL that should be called, and the parameters it needs. Here's the <code>GetMonitorRequest</code> that is used to retrieve a single monitor.</p>
<pre data-lang="php" class="notranslate"><span class="hl-keyword">&lt;?php</span>

<span class="hl-keyword">namespace</span> <span class="hl-type">OhDear\PhpSdk\Requests\Monitors</span>;

<span class="hl-keyword">use</span> <span class="hl-type">OhDear\PhpSdk\Dto\Monitor</span>;
<span class="hl-keyword">use</span> <span class="hl-type">Saloon\Enums\Method</span>;
<span class="hl-keyword">use</span> <span class="hl-type">Saloon\Http\Request</span>;
<span class="hl-keyword">use</span> <span class="hl-type">Saloon\Http\Response</span>;

<span class="hl-keyword">class</span> <span class="hl-type">GetMonitorRequest</span> <span class="hl-keyword">extends</span> <span class="hl-type">Request</span>
{
    <span class="hl-keyword">protected</span> <span class="hl-type">Method</span> <span class="hl-property">$method</span> = <span class="hl-type">Method</span>::<span class="hl-property">GET</span>;

    <span class="hl-keyword">public</span> <span class="hl-keyword">function</span> <span class="hl-property">__construct</span>(<span class="hl-injection">
        <span class="hl-keyword">protected</span> <span class="hl-type">int</span> <span class="hl-property">$monitorId</span>
    </span>) {}

    <span class="hl-keyword">public</span> <span class="hl-keyword">function</span> <span class="hl-property">resolveEndpoint</span>(): <span class="hl-type">string</span>
    {
        <span class="hl-keyword">return</span> <span class="hl-value">&quot;/monitors/{$this-&gt;monitorId}&quot;</span>;
    }

    <span class="hl-keyword">public</span> <span class="hl-keyword">function</span> <span class="hl-property">createDtoFromResponse</span>(<span class="hl-injection"><span class="hl-type">Response</span> $response</span>): <span class="hl-type">Monitor</span>
    {
        <span class="hl-keyword">return</span> <span class="hl-type">Monitor</span>::<span class="hl-property">fromResponse</span>(<span class="hl-variable">$response</span>-&gt;<span class="hl-property">json</span>());
    }
}
</pre>
<p>That <code>Monitor</code> object is a DTO class that will transform the array response from our API to a real PHP object, which is nicer to handle.</p>
<pre data-lang="php" class="notranslate"><span class="hl-keyword">&lt;?php</span>

<span class="hl-keyword">namespace</span> <span class="hl-type">OhDear\PhpSdk\Dto</span>;

<span class="hl-keyword">class</span> <span class="hl-type">Monitor</span>
{
    <span class="hl-keyword">public</span> <span class="hl-keyword">function</span> <span class="hl-property">__construct</span>(<span class="hl-injection">
        <span class="hl-keyword">public</span> <span class="hl-type">int</span> <span class="hl-property">$id</span>,
        <span class="hl-keyword">public</span> <span class="hl-type">?int</span> <span class="hl-property">$teamId</span>,
        <span class="hl-keyword">public</span> <span class="hl-type">string</span> <span class="hl-property">$url</span>,
        
        <span class="hl-comment">// other properties omitted for brevity</span>
        
    </span>) {}

    <span class="hl-keyword">public</span> <span class="hl-keyword">static</span> <span class="hl-keyword">function</span> <span class="hl-property">fromResponse</span>(<span class="hl-injection"><span class="hl-type">array</span> $data</span>): <span class="hl-type">self</span>
    {
        <span class="hl-keyword">return</span> <span class="hl-keyword">new</span> <span class="hl-type">self</span>(
            <span class="hl-property">id</span>: <span class="hl-variable">$data</span>[<span class="hl-value">'id'</span>],
            <span class="hl-property">teamId</span>: <span class="hl-variable">$data</span>[<span class="hl-value">'team_id'</span>],
            <span class="hl-property">url</span>: <span class="hl-variable">$data</span>[<span class="hl-value">'url'</span>],
            
            <span class="hl-comment">// other properties omitted for brevity</span>
        );
    }
}
</pre>
<p>Using the connector and the request, you can get results from the API like this.</p>
<pre data-lang="php" class="notranslate"><span class="hl-keyword">use</span> <span class="hl-type">OhDear\PhpSdk\Requests\Monitors\GetMonitorsRequest</span>;

<span class="hl-variable">$request</span> = <span class="hl-keyword">new</span> <span class="hl-type">GetMonitorsRequest</span>();

<span class="hl-comment">// raw response from the Oh Dear API</span>
<span class="hl-variable">$response</span> = <span class="hl-variable">$ohDear</span>-&gt;<span class="hl-property">send</span>(<span class="hl-variable">$request</span>);

<span class="hl-variable">$monitor</span> = <span class="hl-variable">$response</span>-&gt;<span class="hl-property">dto</span>();
</pre>
<p>By using the connector/request this way, you have full control to customize requests as you see fit (you could for example use <a href="https://docs.saloon.dev/digging-deeper/concurrency-and-pools">Saloon's concurrency functionality</a> this way.)</p>
<p>Now, to make it easier for the package user, I've added methods to the connector class, that wrap up those requests, so it becomes easier to use.</p>
<pre data-lang="php" class="notranslate"><span class="hl-comment">// on the OhDear connector class</span>

<span class="hl-keyword">public</span> <span class="hl-keyword">function</span> <span class="hl-property">monitor</span>(<span class="hl-injection"><span class="hl-type">int</span> $monitorId</span>): <span class="hl-type">Monitor</span>
{
    <span class="hl-variable">$request</span> = <span class="hl-keyword">new</span> <span class="hl-type">GetMonitorRequest</span>(<span class="hl-variable">$monitorId</span>);

    <span class="hl-keyword">return</span> <span class="hl-variable">$this</span>-&gt;<span class="hl-property">send</span>(<span class="hl-variable">$request</span>)-&gt;<span class="hl-property">dto</span>();
}
</pre>
<p>With this in place, users can just get a monitor like this.</p>
<pre data-lang="php" class="notranslate"><span class="hl-variable">$monitor</span> = <span class="hl-variable">$ohDear</span>-&gt;<span class="hl-property">monitor</span>(1);

<span class="hl-keyword">echo</span> <span class="hl-variable">$monitor</span>-&gt;<span class="hl-property">url</span>;
</pre>
<p>Easy peasy!</p>
<p>One thing that I'd like to highlight about Saloon, is its <a href="https://docs.saloon.dev/the-basics/testing">amazing testing facilities</a>. Using the fixture recorder, you can easily test endpoints of the real SDK.</p>
<p>Here is the test of that endpoint to get a single monitor.</p>
<pre data-lang="php" class="notranslate"><span class="hl-property">it</span>(<span class="hl-value">'can get a single monitor'</span>, <span class="hl-keyword">function</span> () {
    <span class="hl-type">MockClient</span>::<span class="hl-keyword">global</span>([
        <span class="hl-type">GetMonitorRequest</span>::<span class="hl-keyword">class</span> =&gt; <span class="hl-type">MockResponse</span>::<span class="hl-property">fixture</span>(<span class="hl-value">'monitor'</span>),
    ]);

    <span class="hl-variable">$monitor</span> = <span class="hl-variable">$this</span>-&gt;<span class="hl-property">ohDear</span>-&gt;<span class="hl-property">monitor</span>(82063);

    <span class="hl-property">expect</span>(<span class="hl-variable">$monitor</span>-&gt;<span class="hl-property">url</span>)-&gt;<span class="hl-property">toBe</span>(<span class="hl-value">'https://laravel.com'</span>);
});
</pre>
<p>In the code above you can see that we use Saloon's <code>MockResponse::fixture</code> function. This works a bit like snapshot testing. The first time you run this test, it will call the actual API and store its response in a file in the <code>Fixtures</code> directory of the test suite. The second time this test is run, it will not call the actual API anymore, but use the response saved in the <code>Fixtures</code> directory. And of course, this is a much faster test.</p>
<p>Notice that the test above mentioned the actual id of the created monitor. I don't mind that, because this is from our staging environment, which will be cleaned up by the time you read this. If you have sensitive values returned by your API, Saloon has got your back too as it can <a href="https://docs.saloon.dev/the-basics/testing#redacting-recorded-responses-fixtures">redact fixtures</a>.</p>
<h2 id="in-closing">In closing <a href="#in-closing" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#in-closing">#</a></h2>
<p>When I decided to rebuild our SDK in a modern way, I dreaded it a bit, as it's always a bit of tedious work. But because of <a href="https://docs.saloon.dev">Saloon</a>, I really enjoyed the process. After creating the base setup and a couple of requests, I also used AI to complete most of the endpoints. Because Saloon divides all concerns of an SDK so nicely, the AI didn't have any problems discovering the structure. Be sure to check out <a href="https://github.com/ohdearapp/ohdear-php-sdk">the source code our package</a>, to learn more about Saloon can be used.</p>
<p>As a user of Oh Dear, I hope you'll enjoy using <a href="https://github.com/ohdearapp/ohdear-php-sdk">our new SDK</a>. All the most important API methods are covered by our SDK. Missing a method? Feel free to open a PR, or just reach out to support and we'll add it!</p>
]]>
            </summary>
                                    <updated>2025-08-27T08:29:59+00:00</updated>
        </entry>
            <entry>
            <title><![CDATA[Introducing our new notification logs]]></title>
            <link rel="alternate" href="https://ohdear.app/news-and-updates/introducing-our-new-notification-logs" />
            <id>https://ohdear.app/159</id>
            <author>
                <name><![CDATA[Oh Dear]]></name>
            </author>
            <summary type="html">
                <![CDATA[<p>One of the core features of Oh Dear is that we can notify you whenever we detect problems with one of your sites.</p>
<p>Our notification system is quite powerful. We support many different channels (like email, Slack, Telegram, ... and a whole bunch more), and have fine-grained control over which events should trigger a notification.</p>
<p>Today, we've added notification logs. If you take a look at your configured team notifications, you'll now see a new counter that indicates how many notifications we've sent you per configuration.</p>
<p><img src="/media/blog/rVE7rwRqHCZSC8VRynVCvCN4x9ZGfTqw8KpeWkPm.png" alt="" /></p>
<p>In the little menu on the right, you can now choose &quot;Show sent notifications&quot;...</p>
<p><img src="/media/blog/x9r6flmANBS3eSNF9ahjWqDbD1iYfVxTHocSE4LP.png" alt="" /></p>
<p>... that will show you all sent notifications that were sent to this configuration.</p>
<p><img src="/media/blog/JaZUOSZGpPi4L28eYzPvAjHF77lN3EPsBmmRWqUq.png" alt="" /></p>
<p>On the usage page, which we added a couple of weeks ago, we also list the total number of notifications we sent you across your account.</p>
<p><img src="/media/blog/bjxLdHU7pw6E7brDme5W3BksLl5K9tQpsGBbUXiA.jpg" alt="" /></p>
<p>Behind the scenes, this feature is powered by <a href="https://clickhouse.com">ClickHouse</a>, which is also used for storing all measured performance data we collect. Using ClickHouse will unlock more interesting features, which will be coming soon.</p>
<p>The new notification log feature was requested via the survey we recently sent to all our users. If you want to request a feature, feel free to <a href="https://tally.so/r/nPLODP">fill in the survey</a> as well.</p>
]]>
            </summary>
                                    <updated>2025-08-07T13:25:43+00:00</updated>
        </entry>
            <entry>
            <title><![CDATA[Here's how you can monitor your site's SEO performance]]></title>
            <link rel="alternate" href="https://ohdear.app/news-and-updates/heres-how-you-can-monitor-your-sites-seo-performance" />
            <id>https://ohdear.app/149</id>
            <author>
                <name><![CDATA[Oh Dear]]></name>
            </author>
            <summary type="html">
                <![CDATA[<p>SEO is in a weird place right now. About one in five LinkedIn posts in my feed currently claims that SEO is dead, or has been assimilated by LLMs. Do not be remiss, dearest reader, because even an LLM still uses search engines like Google and Bing for web crawling.</p>
<p>In other words, SEO still matters, a lot. Additionally, it's never a bad idea to keep tabs your website's SEO performance. SEO monitoring helps you identify exactly what needs improvement so that your website can get the attention it deserves, without directly emptying your pockets and bidding your life's savings on a couple of keywords.</p>
<h2 id="what-is-seo-monitoring">What is SEO monitoring? <a href="#what-is-seo-monitoring" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#what-is-seo-monitoring">#</a></h2>
<p>SEO monitoring refers to the continuous process of tracking website performance metrics that impact your SEO rankings, simple. It shows you the aspects of your website that could use improvement and what aspects are currently working well. You can gain further insights by comparing your metrics to competitor sites that are currently ranking for keywords you target.</p>
<p>Typically, SEO monitoring involves:</p>
<ul>
<li>Tracking keyword rankings</li>
<li>Analyzing organic traffic patterns</li>
<li>Evaluating technical SEO elements</li>
<li>Monitoring backlink profiles</li>
<li>Assessing page performance metrics</li>
<li>Checking for crawlability and indexation issues</li>
</ul>
<h2 id="why-does-seo-monitoring-matter">Why does SEO monitoring matter? <a href="#why-does-seo-monitoring-matter" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#why-does-seo-monitoring-matter">#</a></h2>
<p>Using SEO monitoring allows you to assign metrics to changes in your website’s rankings. That way, you can make continuous improvements backed by data as opposed to hoping something sticks. Identifying the most impactful improvements you can make allows you to zero in on what matters most. What gets measured, gets managed—you know what it is.</p>
<p>You can also use SEO monitoring to catch technical issues before they impact your rankings. Whether it’s <a href="https://developers.google.com/search/updates/core-updates">an update to Google’s algorithm</a> or a change in your CMS, monitoring SEO metrics allows you to make pre-emptive changes to keep your numbers up.</p>
<h2 id="what-are-some-key-seo-metrics-to-keep-an-eye-on">What are some key SEO metrics to keep an eye on? <a href="#what-are-some-key-seo-metrics-to-keep-an-eye-on" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#what-are-some-key-seo-metrics-to-keep-an-eye-on">#</a></h2>
<p>As with most things in marketing, there’s no shortage of metrics you can monitor. However, there are a few crucial metrics that you need to monitor. We’re going to group the most important metrics into categories so that you can more easily put your metrics to use based on the results you’re looking for.</p>
<h3 id="traffic-metrics">Traffic Metrics</h3>
<p>One of the main reasons to invest in SEO is to drive more traffic to your site. These are the three main metrics you should monitor when trying to increase your traffic:</p>
<ul>
<li><strong>Organic Sessions:</strong> These are the number of people who visit your site through results on search engines.</li>
<li><strong>New versus Returning Visitors:</strong> Higher levels of return visitors signal to search engines that your site consistently provides visitors with value.</li>
<li><strong>Bounce Rate:</strong> This refers to how many people leave shortly after being directed to your site and signals to search engines that your site is not delivering the content users seek.</li>
</ul>
<h3 id="engagement-metrics">Engagement Metrics</h3>
<p>You also want users to engage with your site, not just visit it. Here are likely the most important engagement metrics for SEO monitoring:</p>
<ul>
<li><strong>Pages Per Session:</strong> You want users that find your site through organic searches to explore your other pages. The number of pages per session tells you how many they visit on average.</li>
<li><strong>Scroll Depth:</strong> Users who read an entire page provide you with more value making it an important engagement metric.</li>
<li><strong>Click-through Rate:</strong> Every page comes with an intention for an action from the user. The click-through rate tells you the percentage of people who take that action.</li>
</ul>
<h3 id="technical-seo-metrics">Technical SEO Metrics</h3>
<p>For some sites, certain technical aspects can hold back your performance and therefore ranking, including:</p>
<ul>
<li><strong>Page Load Speed:</strong> If your site suffers from <a href="https://ohdear.app/docs/faq/why-do-you-need-website-performance-monitoring">performance issues</a> and takes too long to load, users bounce from your site before they see your content.</li>
<li><strong>Mobile-friendliness:</strong> Nowadays, most people browse the internet on their phone or tablet, so your site needs to work just as well for them as desktop users. Crazy how we still need to talk about this.</li>
<li><strong>Crawl Errors:</strong> Google can’t rank your site if its bots can’t crawl through it. Look out for crawl errors, such as <a href="https://ohdear.app/news-and-updates/detecting-and-resolving-broken-links-using-website-monitoring-software">broken links</a>, and fix them immediately. Getting on top of issues like these as quickly as possible is the name of the game here.</li>
</ul>
<h3 id="content-performance-metrics">Content Performance Metrics</h3>
<p>The most in-your-face SEO metrics are your content performance metrics, such as:</p>
<ul>
<li><strong>Keyword rankings:</strong> Most people investing in SEO aim to improve their keyword ranking, but that comes from improving other SEO metrics.</li>
<li><strong>Featured snippets:</strong> Oftentimes overlooked, featured snippets can drive more traffic than a high-ranking page.</li>
<li><strong>Backlink quality and quantity:</strong> The number and quality of sites linking to your website help search engine’s assess the trustworthiness and authority of your site.</li>
</ul>
<h2 id="cant-have-an-article-without-a-top-5-of-something">Can't have an article without a top 5 of something! <a href="#cant-have-an-article-without-a-top-5-of-something" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#cant-have-an-article-without-a-top-5-of-something">#</a></h2>
<p>We've talked at length about metrics, let's dive into some practicalities next. Martech vendors are kind of like a plague these days, so there's obviously no shortage of SEO tools around. These are the five most popular ones:</p>
<ol>
<li><strong>Google Search Console:</strong> Showing which queries bring users to your site, identifying indexing issues, and highlighting mobile usability problems makes <a href="https://search.google.com/search-console/about">Google Search Console</a> the core of most monitoring strategies.</li>
<li><strong>Google Analytics:</strong> It’s not specific for SEO, but <a href="https://developers.google.com/analytics">Google Analytics</a> provides tons of data that can help you monitor SEO metrics such as which channels drive traffic and the behavior flow of users.</li>
<li><strong>Ahrefs:</strong> As one of the most expensive SEO monitoring tools, <a href="https://ahrefs.com/">Ahrefs</a> provides a premium monitoring service for just about any metric you could want.</li>
<li><strong>SEMRush:</strong> For a budget-friendly alternative to Ahrefs, consider <a href="https://www.semrush.com/">SEMRush</a>. It’s another all-in-one tool, but it primarily focuses on content.</li>
<li><strong>Screaming Frog:</strong> To identify technical SEO errors, use <a href="https://www.screamingfrog.co.uk/seo-spider/">Screaming Frog</a>. It crawls your site the same way Google does to find issues.</li>
</ol>
<h2 id="dont-forget-about-google-lighthouse">Don't forget about Google Lighthouse <a href="#dont-forget-about-google-lighthouse" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#dont-forget-about-google-lighthouse">#</a></h2>
<p>Now, one can’t discuss SEO monitoring without mentioning <a href="https://ohdear.app/features/lighthouse-seo-monitoring">Google Lighthouse</a>. They provide a pretty complete overview of your site’s performance and then assign it a grade for different categories. It covers everything from mobile-friendliness and content quality to performance and crawlability.</p>
<p>After completing the test, Google Lighthouse gives you actionable tips on how to improve your site. However, their recommendations are fairly technical, so you'll need at least a modicum of SEO expertise to implement their suggestions. It’s a free tool, though, which makes it accessible to anyone and is an overall pretty valuable asset to have.</p>
<h2 id="seo-monitoring-made-even-simpler-with-oh-dear">SEO monitoring made even simpler with Oh Dear <a href="#seo-monitoring-made-even-simpler-with-oh-dear" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#seo-monitoring-made-even-simpler-with-oh-dear">#</a></h2>
<p>Running SEO tests across your site and recording your metrics takes a lot of time. <a href="https://ohdear.app/">Oh Dear</a> simplifies the entire process by integrating Google Lighthouse into our full-fledged website monitoring suite. It automatically tracks key SEO metrics so you can monitor the impact of your SEO efforts as you build or expand your site.</p>
<p>Automatically running daily Google Lighthouse scans also notifies you of issues before they impact your rankings. Whether you accidentally loaded a full-scale image instead of a compressed one or a plugin you used suddenly updated, causing a hit to your site’s performance, you’ll be notified immediately thanks to Oh Dear’s daily scans. Neat!</p>
<p>Lighthouse monitoring is natively integrated with Oh Dear’s broader website monitoring suite so you can truly keep track of every little aspect of your website’s health and performance.</p>
]]>
            </summary>
                                    <updated>2025-08-13T13:37:52+00:00</updated>
        </entry>
            <entry>
            <title><![CDATA[Performance degradation on Oh Dear, a post mortem]]></title>
            <link rel="alternate" href="https://ohdear.app/news-and-updates/performance-degradation-on-oh-dear-a-post-mortem" />
            <id>https://ohdear.app/158</id>
            <author>
                <name><![CDATA[Oh Dear]]></name>
            </author>
            <summary type="html">
                <![CDATA[<p>On Tuesday the 22nd of July around 09:00 UTC, our users have experienced a slower-than-usual website, API and application. This continued until the next day, July 23rd and was ultimately resolved around 17:30 UTC.</p>
<p>This post highlights the cause and the fix we've applied. There were several misleading metrics that caused this issue to last longer than we'd have expected that make for an interesting retrospective.</p>
<p>As is always the case when debugging: sometimes it's hard to distinguish <em>cause</em> from <em>effect</em> when looking at metrics.</p>
<h2 id="the-issue-slowdowns-in-application">The issue: slowdowns in application <a href="#the-issue-slowdowns-in-application" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#the-issue-slowdowns-in-application">#</a></h2>
<p>Without a seemingly clear cause, our website &amp; API started to experience signifant slowdowns. There were no deployments, configuration or infrastructure changes at that time - in fact, the team was in the middle of our weekly standup/huddle at the time it started.</p>
<p><img src="/media/blog/VJoGRvEKGEQMNF260jD4Xk2WYxWH67c8OwBuKuFG.png" alt="" />
We monitor several different endpoints, the problem was only noticeable on <em>authenticated</em> endpoints or routes that actually touched our database - the PHP-only endpoints (such as naked API calls without authentication) showed stable performance.</p>
<p><img src="/media/blog/qlUG3wohehKgNKwApq8s9td8PIExNK7lq4AHGLvX.png" alt="" /></p>
<h2 id="what-did-our-zabbix-monitor-tell-us">What did our Zabbix monitor tell us? <a href="#what-did-our-zabbix-monitor-tell-us" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#what-did-our-zabbix-monitor-tell-us">#</a></h2>
<p>Internally, next to using Oh Dear to monitor Oh Dear, we also use an isolated open source system called <a href="https://www.zabbix.com/">Zabbix</a>. This is our independent set of eyes on our infrastructure and looks at the internals, like CPU, memory &amp; disk usage, as well as details on MySQL throughput, Redis usage, etc.</p>
<p>This is where we were thrown off-track.</p>
<p>Below we show our CPU load, CPU usage and MySQL throughput of our MySQL server.</p>
<p><img src="/media/blog/VevnTpa9qyk8izwLuBaVxETk2LKwGMddeif7fQtM.png" alt="" /></p>
<p><img src="/media/blog/V7yAowxBcTGQYtoP9XEPT8FLPLihNbUaePm4RS2a.png" alt="" /></p>
<p><img src="/media/blog/KToEQAMXC8AMNTDjD1ktDle8fFDDQ5Ob4JBlYsyP.png" alt="" /></p>
<p>Clearly, we had an issue: MySQL throughput (the top green line = SELECT's per second) fell off a cliff. CPU load and CPU time showed a noticeable, but in comparison to the SQL throughput difference - rather small, drop.</p>
<p>Our first train of thought was: the cause is on the application side, we're sending less SQL queries to our database server, so the cause is either;</p>
<ul>
<li>Slower-than-usual processing of PHP jobs (looking at: Horizon, PHP-FPM)</li>
<li>Slower-than-usual network traffic between our main servers &amp; MySQL (looking at: network throughput, firewalling, connection limits)</li>
<li>Faulty hardware: a failing RAID setup, a failing memory bank, ...</li>
</ul>
<p>None of these proved to be the cause, there were no anomalies and we could confirm &amp; verify that we <em>tried</em> to process all requests at the same speed, but were being slowed down by MySQL queries.</p>
<h2 id="enter-cpu-jumps">Enter, CPU jumps <a href="#enter-cpu-jumps" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#enter-cpu-jumps">#</a></h2>
<p>Having excluded the most common suspects, we dived deeper and focussed the search entirely to MySQL itself. That's when this metric jumped out - pun intended.</p>
<p><img src="/media/blog/VSwbsr5dEj4dVj4t7Qf6AHT1vQzxkWCbneSVqm2P.png" alt="" /></p>
<p>On Linux, CPU jumps - typically referring to sudden spikes in context switches - can be caused by a variety of factors related to how the kernel schedules processes and handles interrupts.</p>
<p>Excessive I/O operations (disk or network), frequent system calls, or user-space programs making short-lived kernel interactions can force more context switches. Interrupts from hardware devices (IRQ) and soft interrupts (softirq), especially in high-throughput environments, further contribute to this, as they demand the CPU’s attention and force it to switch context from user processes to kernel handlers and back.</p>
<p>At first, this lead us back to the hardware side: rate limiting on the physical switch between the servers (causing time-waits on the network), faulty hardware, ... But these were all already excluded earlier.</p>
<h2 id="mysql-queries-impacting-cpu-jumps">MySQL queries impacting CPU jumps <a href="#mysql-queries-impacting-cpu-jumps" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#mysql-queries-impacting-cpu-jumps">#</a></h2>
<p>As we switched our own context back to MySQL itself, we noticed a single query taking ~1 second to execute. In and of itself not the biggest of issues (especially if they are one-offs), but these were recurring:</p>
<pre data-lang="txt" class="notranslate">SELECT
	*
FROM
	`ping_endpoint_monitor_uptime_periods`
WHERE
	`started_at` &lt;= '2025-07-23 15:29:01'
	AND `ended_at` &gt;= '2025-07-23 15:29:01'
LIMIT
	1
</pre>
<p>Now for a bit of context: when we're performing our <a href="https://ohdear.app/features/scheduled-task-monitoring">scheduled task monitoring</a>, we receive incoming <code>ping</code>'s to our <code>ping.ohdear.app</code> endpoint. This hits AWS, gets queued there, and we pick up the queue to process all incoming requests. AWS handles the peak load, we see a steady load of incoming messages via AWS SQS.</p>
<p>To avoid false alerts, we monitor this ping endpoint ourselves: besides all the usuals to monitor (like https, ping, tcp, ...) we also send a ping callback of our own every 4 seconds, and expect this to be processed and handled within the next 4 seconds. If that's not the case, we fire off some internal alerts and mark the ping endpoint as being down.</p>
<p>This way, we can keep the <em>uptime</em> of our ping endpoint in mind when processing and sending out alerts: if our ping endpoint was down, we shouldn't send a &quot;Your scheduled task didn't run&quot;-notification, it <em>might</em> have still run, but the pingback didn't receive us.</p>
<p>All that data is stored in in our <code>ping_endpoint_monitor_uptime_periods</code> table, with a simple structure:</p>
<pre data-lang="txt" class="notranslate">CREATE TABLE `ping_endpoint_monitor_uptime_periods` (
	`id` bigint UNSIGNED NOT NULL AUTO_INCREMENT,
	`started_at` TIMESTAMP NULL DEFAULT NULL,
	`ended_at` TIMESTAMP NULL DEFAULT NULL,
	`created_at` TIMESTAMP NULL DEFAULT NULL,
	`updated_at` TIMESTAMP NULL DEFAULT NULL,
	PRIMARY KEY (`id`),
	KEY `started_ended_index` (`started_at`, `ended_at`),
) ENGINE = InnoDB AUTO_INCREMENT = 1 DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci
</pre>
<p>Over time, this table grew - we didn't prune it - and it looks like MySQL can be stressed when executing date queries on a large table, to the point where it exceeds a certain threshold and becomes <em>incredibly</em> inefficient at executing that same query.</p>
<p>There were no sudden increases in the table size, but it did contain 50k+ rows accumulated over time. By all accounts, it looks like we exceeded a certain size that either prevented the table from fitting in memory (unlikely, as it's still super small) or it forced a different query execution logic (possible, but hard to verify).</p>
<p>The result was: a massive jump in <em>context switches</em> as measured by CPU jumps and a <em>very</em> noticeable decrease in SQL throughput, all caused by queries that went from ~25ms in execution to jumping to &gt; 1s. That longer query time didn't spike CPU usage (which you would expect), but spiked the <em>context switches</em> which slowed down all other CPU usage.</p>
<h2 id="the-fix">The fix <a href="#the-fix" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#the-fix">#</a></h2>
<p>In hindsight, the fix is easy: prune the table (we don't need records &gt; 1 day old).</p>
<pre data-lang="txt" class="notranslate">DELETE FROM `ohdear`.`ping_endpoint_monitor_uptime_periods`
WHERE
	(`started_at` &lt; '2025-07-20 00:00:00')
</pre>
<p>What we didn't expect was the real cause to be so hidden by misleading metrics.</p>
<h2 id="avoiding-this-issue-in-the-future">Avoiding this issue in the future <a href="#avoiding-this-issue-in-the-future" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#avoiding-this-issue-in-the-future">#</a></h2>
<p>Since this short-term fix, we've implemented daily pruning of the table to delete the old - unneeded - records.</p>
<h2 id="in-closing">In closing <a href="#in-closing" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#in-closing">#</a></h2>
<p>It can be very frustrating to see that a fix to a (for us at least) long period of performance issues can turn out to be so simple.</p>
<p>Like we mentioned at the top: when debugging, it can be hard to distinguish what is the <em>cause</em> of the problem vs. what is just a <em>side effect</em> of the problem.</p>
<p>I've never before experienced a MySQL server with lower CPU usage, lower CPU load and lower SQL throughput to be caused by a query. If anything, I'd assume the CPU usage and load to <em>increase</em> in such a scenario - which completely threw me off.</p>
<p>The fact that the problems appeared without clear human cause by us (a certain threshold in MySQL exceeded?) and that the initial metrics didn't make a lot of sense (slower SQL throughput, lower CPU usage on MySQL) lead us in the wrong direction first.</p>
]]>
            </summary>
                                    <updated>2025-07-26T18:16:48+00:00</updated>
        </entry>
            <entry>
            <title><![CDATA[Why we will be making breaking changes in our API soon]]></title>
            <link rel="alternate" href="https://ohdear.app/news-and-updates/why-we-will-be-making-breaking-changes-in-our-api-soon" />
            <id>https://ohdear.app/155</id>
            <author>
                <name><![CDATA[Oh Dear]]></name>
            </author>
            <summary type="html">
                <![CDATA[<p>In a few weeks, on Tuesday 26st August, we will be deploying breaking changes to our API. In this blog post we want to share why and how we'll do this.</p>
<p>If you're using our API, here are <a href="https://ohdear.app/docs/faq/how-do-i-update-to-the-latest-version-of-the-oh-dear-api">the upgrade instructions</a>.</p>
<h2 id="introducing-ping-and-tcp-port-monitoring">Introducing Ping and TCP Port monitoring <a href="#introducing-ping-and-tcp-port-monitoring" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#introducing-ping-and-tcp-port-monitoring">#</a></h2>
<p>A few weeks ago, we sent a survey out to all our users asking what they like about Oh Dear, how they use it, and how we could improve our service. If you didn't get it, you can still <a href="https://tally.so/r/nPLODP">fill out our survey</a>. It only takes a couple of minutes.</p>
<p>Currently, we can monitor the uptime of any HTTP(s) site. In our survey, our users mentioned that they'd like to use Oh Dear to also monitor MySQL, email, Redis, or any other kind of server.</p>
<p>That's why we're working on adding ping and TCP monitoring. Instead of performing a simple GET request, Oh Dear will be able to send a Unix ping command to a server, and to open up a TCP port to any server you'd like, and send a command through it.</p>
<p>When adding a site to monitor to Oh Dear you will be able to select the type (we'll make this choice much prettier than a simple dropdown when we ship this feature).</p>
<p><img src="/media/blog/y2A3jytKKZ1LWUmjGmWBU1fhTcSnj3O1PPSoiIUz.png" alt="" /></p>
<p>We already have ping/TCP checks running in our staging environment. Here's how ping results look like.</p>
<p><img src="/media/blog/F7K1VKgUfdwiur0PoN7opXlHIpClH2R4HBWEVRne.png" alt="" /></p>
<p>Of course, we'll also record performance data, which will be shown on that page as well when scrolling down.</p>
<p><img src="/media/blog/OVZSeOMji8jQR2u8wk9ZHfECp8cpmgEKwVq1OZtf.png" alt="" /></p>
<p>And here's an example of TCP results.</p>
<p><img src="/media/blog/AHqlZtgo65dlbcS1PeRIGTFiyKvntDBxKY5KMnJ7.png" alt="" /></p>
<p>For TCP checks, we'll also record connection time and dislay a graph for those times.</p>
<p><img src="/media/blog/KEezvvPq1B0MldgxBOUlZJQuEjUSw18zZmvQYOeW.png" alt="" /></p>
<h2 id="naming-things-is-hard-why-we-are-breaking-our-api">Naming things is hard (why we are breaking our API) <a href="#naming-things-is-hard-why-we-are-breaking-our-api" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#naming-things-is-hard-why-we-are-breaking-our-api">#</a></h2>
<p>Since Oh Dear started, we always had the concept of a &quot;Site&quot; you're monitoring. But the name &quot;Site&quot; is really weird when you change the checking type to ping or TCP. For example, when checking the uptime of a port of a server, you're not really checking a &quot;Site&quot;.</p>
<p>That's why we made the hard choice of renaming the &quot;Site&quot; concept in Oh Dear to &quot;Monitor&quot;. A monitor will have a type:</p>
<ul>
<li>HTTP: for checking websites</li>
<li>Ping: for checking websites and servers via Ping</li>
<li>TCP: for checking the connectivity of a port</li>
</ul>
<p>Here's a screenshot from our staging environment, where we run a &quot;Monitor&quot; for each type. Notice that in the main navigation, we don't have &quot;Sites&quot; anymore, but &quot;Monitors&quot;.</p>
<p><img src="/media/blog/K7savCYDbQxJTookmo45MN2DUlR9PNfeSD2etwPH.png" alt="" /></p>
<p>Across our UI, notifications and docs, we'll change the language so we'll talk about &quot;Monitors&quot;, instead of &quot;Sites&quot;. We're also busy with updating our entire codebase. Internally our <code>Site</code> model is renamed to <code>Monitor</code>, together with all relationships, foreign keys, views, controllers... Together with the technical rename, we'll also pay off other technical debt that was accrued over the years.</p>
<p>This will also affect our API. At Oh Dear, we value backwards compatibility. In the eight years that we've been in operation, we've never made a breaking change to our API. We also considered keeping a v1 of our API, but we decided against it because the maintenance burden would be too big.</p>
<h2 id="well-help-prepare-you">We'll help prepare you <a href="#well-help-prepare-you" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#well-help-prepare-you">#</a></h2>
<p>On Tuesday 26st August, we'll be making breaking changes to our API. The &quot;sites&quot; endpoints will be renamed to the &quot;monitors&quot; endpoint. We also rename and regroup some of the attributes.</p>
<p>To help you through these changes, we'll publish a page with instructions on how to update to the new API. We will also update our PHP SDK package and related packages. We'll also send you a dedicated mail with a reminder an upgrade instructions.</p>
<h2 id="more-features-are-coming">More features are coming <a href="#more-features-are-coming" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#more-features-are-coming">#</a></h2>
<p>Ping/TCP isn't the only new feature we have planned. Our uptime check runs once per minute by default. For most people this is fine, but a lot of users have asked for the ability to specify the frequency of checking. So we'll be adding that.</p>
<p>We also have other exciting things on the table, but it's a bit too early to talk about those. Should you have any suggestions on how to improve Oh Dear, do let us know by <a href="https://tally.so/r/nPLODP">filling in our survey</a>.</p>
<p>We'll close off this post by highlighting some of the features that were requested through our survey:</p>
<ul>
<li>People with a big number of sites in their team reported that the site list screen is slow. We made it faster, and are still optimizing stuff.</li>
<li>There's <a href="https://ohdear.app/team-settings/usage">a new usage screen</a> that shows your usage against your current plan</li>
<li>Maintenance periods can now have a name</li>
<li>We can now <a href="https://ohdear.app/news-and-updates/we-now-support-google-chat">send notifications via Google Chat</a></li>
<li>The certificate health check has a new &quot;Detected Certificates&quot; tab showing the history of certificates we detected for a site.</li>
<li>A graph was added to the lighthouse result history</li>
<li><a href="https://ohdear.app/news-and-updates">Our blog</a> layout was simplified.</li>
<li>We made it more clear across our UI which things are clickable, by adding the cursor-pointer CSS class to those elements</li>
<li>You can now <a href="https://ohdear.app/news-and-updates/managing-monthly-reports-with-the-api">manage monthly reports with our API</a></li>
<li>Alert snoozing now has <a href="https://ohdear.app/news-and-updates/targeted-snoozes-with-full-history">full history</a></li>
<li>A new login method was added: <a href="https://ohdear.app/news-and-updates/you-can-now-log-in-using-passkeys">passkeys</a></li>
<li>Our broken links check has <a href="https://ohdear.app/news-and-updates/broken-links-troubleshooting-and-a-couple-of-extras">a new &quot;Troubleshooting&quot; tab</a>, showing you tips to fix broken links.</li>
<li>We <a href="https://ohdear.app/news-and-updates/introducing-our-improved-uptime-check">merged our uptime and performance check</a></li>
</ul>
]]>
            </summary>
                                    <updated>2025-08-08T10:48:31+00:00</updated>
        </entry>
            <entry>
            <title><![CDATA[Oh Dear now supports Revolut for payments]]></title>
            <link rel="alternate" href="https://ohdear.app/news-and-updates/oh-dear-now-supports-revolut-for-payments" />
            <id>https://ohdear.app/148</id>
            <author>
                <name><![CDATA[Oh Dear]]></name>
            </author>
            <summary type="html">
                <![CDATA[<p>We're excited to announce that you can now pay for your Oh Dear subscription using <a href="https://www.revolut.com">Revolut</a>! This new payment option joins <a href="https://ohdear.app/docs/faq/which-payment-methods-do-you-accept">our existing payment methods</a>, making it even easier for teams around the world to monitor their websites with Oh Dear. For existing users who want to switch to Revolut, you can update your payment method anytime from your account settings. The change will take effect on your next billing cycle.</p>
<p>That being said, here's a quick overview of our currently accepted payment methods.</p>
<h2 id="worldwide-available-options">Worldwide available options <a href="#worldwide-available-options" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#worldwide-available-options">#</a></h2>
<p>We accept all major credit and debit cards, including:</p>
<ul>
<li>Visa</li>
<li>Mastercard</li>
<li>American Express</li>
<li>Other major international cards</li>
</ul>
<h2 id="european-payment-methods">European payment methods <a href="#european-payment-methods" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#european-payment-methods">#</a></h2>
<p>If you're paying in euros, additional regional payment options are available:</p>
<ul>
<li>SEPA Direct Debit</li>
<li>Sofort</li>
<li>iDEAL</li>
<li>Bancontact</li>
<li>Revolut Pay</li>
</ul>
]]>
            </summary>
                                    <updated>2025-07-23T13:00:30+00:00</updated>
        </entry>
            <entry>
            <title><![CDATA[How to monitor your Laravel app for critical vulnerabilities using Oh Dear]]></title>
            <link rel="alternate" href="https://ohdear.app/news-and-updates/how-to-monitor-your-laravel-app-for-critical-vulnerabilities-using-oh-dear" />
            <id>https://ohdear.app/154</id>
            <author>
                <name><![CDATA[Oh Dear]]></name>
            </author>
            <summary type="html">
                <![CDATA[<p>A critical security vulnerability was recently discovered in Livewire v3 that allows remote code execution, as <a href="https://securinglaravel.com/security-notice-livewire-v3-rce">Stephen Rees-Carter reported on Securing Laravel</a>. While patches are released quickly, many applications remain vulnerable because developers simply don't know about the issue yet. Oh Dear's Application Health monitoring solves this by continuously checking your production environment for security vulnerabilities and immediately notifying you when issues are detected.</p>
<p>For Laravel applications, this is incredibly easy to set up using the <a href="https://spatie.be/docs/laravel-health">spatie/laravel-health package</a>, which can automatically check your composer dependencies against known vulnerabilities. I wrote a detailed guide on my personal blog about <a href="https://freek.dev/2892-how-to-protect-your-laravel-app-against-critical-vulnerabilities-like-livewires-recent-remote-code-execution-issue">protecting your Laravel app against critical vulnerabilities</a> that walks through the entire setup process. Don't wait for the next zero-day - start monitoring your application's health today.</p>
]]>
            </summary>
                                    <updated>2025-07-18T16:37:31+00:00</updated>
        </entry>
            <entry>
            <title><![CDATA[Here's how you can build site templates for Oh Dear]]></title>
            <link rel="alternate" href="https://ohdear.app/news-and-updates/heres-how-you-can-build-site-templates-for-oh-dear" />
            <id>https://ohdear.app/143</id>
            <author>
                <name><![CDATA[Oh Dear]]></name>
            </author>
            <summary type="html">
                <![CDATA[<p>When you're managing a handful of client sites, setting things up manually is fine. Though if you're managing dozens of them, you're going to think twice about your approach.</p>
<p>For agencies, development teams and platforms who are responsible for loads of websites, having to repeat the same configuration over and over is not only inefficient but also more prone to errors. That’s where this blog post comes in handy.</p>
<p>While Oh Dear doesn’t offer templates via the UI (yet) you can achieve pretty powerful templating patterns using tags, notification channels and the API.</p>
<p>Here’s how you can achieve this:</p>
<h2 id="1-build-a-template-site">1. Build a Template Site <a href="#1-build-a-template-site" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#1-build-a-template-site">#</a></h2>
<p>Start by creating a site in Oh Dear that contains all your default settings. You can do this via the API but it's probably worth doing this via the UI and seeing what's available. Everything you can do in the UI you can do via the API.</p>
<p>For example, by default you might want to:</p>
<ul>
<li>Uptime and performance checks enabled</li>
<li>SSL certificate check enabled with a 30-day expiry threshold</li>
<li>Broken link crawler active</li>
</ul>
<p>Save the site and tag it with <code>template</code> and anything else you might want to identify it with if you have multiple types of sites to share config.</p>
<p>For example, you might have <code>template:marketing</code>, <code>template:app</code> and <code>template:internal</code>.</p>
<h2 id="2-create-notification-channels">2. Create Notification Channels <a href="#2-create-notification-channels" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#2-create-notification-channels">#</a></h2>
<p>Before we get too far into the API, let's consider how we want notification channels to work. We have multiple blog posts on this topic, so we won't go into too much detail here but tag-level notifications are great, extendible way to route client alerts and team-level tags are great for keeping your dev team informed across every site.</p>
<p>This decouples notification logic from individual sites and makes future updates super easy!</p>
<h2 id="3-use-the-api-to-clone-from-template">3. Use the API to Clone from Template <a href="#3-use-the-api-to-clone-from-template" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#3-use-the-api-to-clone-from-template">#</a></h2>
<p>When onboarding a new client site, skip the UI. Use the API to:</p>
<ul>
<li>Fetch your template site</li>
<li>Copy over settings (checks, thresholds, tags, notifications)</li>
<li>Apply any client-specific overrides (tags, server location, client report delivery address, etc.)</li>
</ul>
<p>This ensures consistency without requiring manual duplication. Let's get stuck in:</p>
<pre data-lang="js" class="notranslate"><span class="hl-keyword">const</span> response = <span class="hl-keyword">await</span> <span class="hl-property">fetch</span>(<span class="hl-value">'https://ohdear.app/api/sites'</span>, {
	<span class="hl-property">headers</span>: {
		<span class="hl-value">'Authorization'</span>: <span class="hl-value">'Bearer '</span> + process.<span class="hl-property">env</span>.<span class="hl-property">OH_DEAR_API_KEY</span>,
		<span class="hl-value">'Content-Type'</span>: <span class="hl-value">'application/json'</span>
	}
});

<span class="hl-keyword">const</span> sites = <span class="hl-keyword">await</span> response.<span class="hl-property">json</span>();

<span class="hl-keyword">const</span> templateSite = sites.<span class="hl-property">data</span>.<span class="hl-property">find</span>(site =&gt; site.<span class="hl-property">tags</span>.<span class="hl-property">includes</span>(<span class="hl-value">'template:marketing'</span>));


<span class="hl-keyword">const</span> keysToRemove = [
	<span class="hl-value">'id'</span>, <span class="hl-value">'team_id'</span>, <span class="hl-value">'url'</span>, <span class="hl-value">'label'</span>, <span class="hl-value">'checks'</span>,
];

<span class="hl-comment">// Remove keys that we don't want to copy over</span>
<span class="hl-keyword">const</span> rawData = <span class="hl-type">Object</span>.<span class="hl-property">fromEntries</span>(

  
<span class="hl-type">Object</span>.<span class="hl-property">entries</span>(templateSite).<span class="hl-property">filter</span>(([key]) =&gt; !keysToRemove.<span class="hl-property">includes</span>(key))
);

<span class="hl-comment">// Get the check types that are enabled for the template site</span>
<span class="hl-keyword">const</span> templateCheckTypes = templateSite.<span class="hl-property">checks</span>.<span class="hl-property">filter</span>(check =&gt; check.<span class="hl-property">enabled</span>).<span class="hl-property">map</span>(check =&gt; check.<span class="hl-property">type</span>);

<span class="hl-comment">// Check settings are found under each check instance in the response</span>
<span class="hl-comment">// Here's a way to go through each check and pluck the settings and merge them into a single object</span>
<span class="hl-keyword">const</span> checkSettings = templateSite.<span class="hl-property">checks</span>.<span class="hl-property">map</span>(check =&gt; check.<span class="hl-property">settings</span>).<span class="hl-property">reduce</span>((acc, obj) =&gt; ({ ...<span class="hl-property">acc</span>, ...<span class="hl-property">obj</span> }), {});

<span class="hl-comment">// Build the new site payload</span>
<span class="hl-keyword">const</span> urls = [
<span class="hl-value">&quot;https://example.com/from-template-1&quot;</span>,
<span class="hl-value">&quot;https://example.com/from-template-2&quot;</span>,
<span class="hl-value">&quot;https://example.com/from-template-3&quot;</span>,
]

  

<span class="hl-keyword">for</span> (<span class="hl-keyword">const</span> url <span class="hl-keyword">of</span> urls) {

<span class="hl-keyword">const</span> data = {
	<span class="hl-value">&quot;url&quot;</span>: url,
	<span class="hl-value">&quot;team_id&quot;</span>: templateSite.<span class="hl-property">team_id</span>,
	...<span class="hl-property">rawData</span>,
	...<span class="hl-property">checkSettings</span>,
	<span class="hl-property">checks</span>: templateCheckTypes
}

  

<span class="hl-keyword">const</span> created = <span class="hl-keyword">await</span> <span class="hl-property">fetch</span>(<span class="hl-value">'https://ohdear.app/api/sites'</span>, {
	<span class="hl-property">method</span>: <span class="hl-value">'POST'</span>,
	<span class="hl-property">headers</span>: {
		<span class="hl-value">'Authorization'</span>: <span class="hl-value">'Bearer '</span> + process.<span class="hl-property">env</span>.<span class="hl-property">OH_DEAR_API_KEY</span>,
		<span class="hl-value">'Content-Type'</span>: <span class="hl-value">'application/json'</span>
	},
	<span class="hl-property">body</span>: <span class="hl-type">JSON</span>.<span class="hl-property">stringify</span>(data)
})
</pre>
<p>Wrap this up into a function and you've got yourself a nice little script to clone a template site. Alternatively, you could store a local JSON object with all the default site settings and use the <a href="https://ohdear.test/docs/integrations/the-oh-dear-api#add-a-site-with-custom-settings">add-a-site-with-custom-settings</a> end point directly. On one hand this allow your non-tech team members to update the template site in Oh Dear but at the same time keeping the entire payload in a file means you can keep it in version control and share it with your team.</p>
<h2 id="4-mass-update-all-sites-via-the-api">4. Mass Update All Sites via the API <a href="#4-mass-update-all-sites-via-the-api" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#4-mass-update-all-sites-via-the-api">#</a></h2>
<p>Need to roll out a new policy across all sites (e.g. enable certificate checking with a new threshold)?</p>
<p>Use the API to:</p>
<ul>
<li>Fetch all sites (optionally filter by tag, URL, internal notes etc)</li>
<li>Update only the checks you care about</li>
<li>Apply the new certificate expiration threshold across the board</li>
</ul>
<p>This is especially helpful if you're enforcing compliance or want to roll out new monitoring best practices without point and click effort.</p>
<pre data-lang="js" class="notranslate"><span class="hl-keyword">const</span> response = <span class="hl-keyword">await</span> <span class="hl-property">fetch</span>(<span class="hl-value">'https://ohdear.app/api/sites'</span>, {
	<span class="hl-property">headers</span>: {
		<span class="hl-value">'Authorization'</span>: <span class="hl-value">'Bearer '</span> + process.<span class="hl-property">env</span>.<span class="hl-property">OH_DEAR_API_KEY</span>,
		<span class="hl-value">'Content-Type'</span>: <span class="hl-value">'application/json'</span>
	}
});

<span class="hl-keyword">const</span> sites = <span class="hl-keyword">await</span> response.<span class="hl-property">json</span>();

<span class="hl-keyword">for</span> (<span class="hl-keyword">const</span> site <span class="hl-keyword">of</span> sites.<span class="hl-property">data</span>){

<span class="hl-keyword">const</span> checks = site.<span class="hl-property">checks</span>
	.<span class="hl-property">filter</span>(check =&gt; check.<span class="hl-property">enabled</span>)
	.<span class="hl-property">map</span>(check =&gt; check.<span class="hl-property">type</span>)
	.<span class="hl-property">concat</span>([ 
		<span class="hl-value">&quot;certificate_health&quot;</span>, <span class="hl-comment">// enable certificate health check in all environments</span>
		site.<span class="hl-property">tags</span>.<span class="hl-property">includes</span>(<span class="hl-value">'env:production'</span>) ? <span class="hl-value">&quot;lighthouse&quot;</span> : <span class="hl-keyword">null</span> <span class="hl-comment">// enable lighthouse check for production sites</span>
])
.<span class="hl-property">filter</span>(Boolean);

<span class="hl-keyword">const</span> update = <span class="hl-keyword">await</span> <span class="hl-property">fetch</span>(<span class="hl-value">'https://ohdear.app/api/sites/'</span> + site.<span class="hl-property">id</span>, {
	<span class="hl-property">method</span>: <span class="hl-value">'PUT'</span>,
	<span class="hl-property">headers</span>: {
		<span class="hl-value">'Authorization'</span>: <span class="hl-value">'Bearer '</span> + process.<span class="hl-property">env</span>.<span class="hl-property">OH_DEAR_API_KEY</span>,
		<span class="hl-value">'Content-Type'</span>: <span class="hl-value">'application/json'</span>
	},
	
	<span class="hl-property">body</span>: <span class="hl-type">JSON</span>.<span class="hl-property">stringify</span>({
		<span class="hl-value">&quot;checks&quot;</span>: checks,
		<span class="hl-comment">// check settings</span>
		<span class="hl-value">&quot;certificate_health_check_expires_soon_threshold_in_days&quot;</span>: 10,
		<span class="hl-value">&quot;lighthouse_check_continent&quot;</span>: <span class="hl-value">&quot;europe&quot;</span>,
		<span class="hl-value">&quot;lighthouse_cpu_slowdown_modifier&quot;</span>: 0
	})
});
	
	console.<span class="hl-property">log</span>({
		<span class="hl-property">site</span>: site.<span class="hl-property">id</span>,
		<span class="hl-property">url</span>: site.<span class="hl-property">url</span>,
		<span class="hl-property">status</span>: update.<span class="hl-property">status</span>,
	});
}
</pre>
<p>Hopefully, this can help you get started with templating in Oh Dear. If you have any questions or feedback, please let us know.</p>
]]>
            </summary>
                                    <updated>2025-07-16T11:35:42+00:00</updated>
        </entry>
            <entry>
            <title><![CDATA[We now support Google Chat]]></title>
            <link rel="alternate" href="https://ohdear.app/news-and-updates/we-now-support-google-chat" />
            <id>https://ohdear.app/147</id>
            <author>
                <name><![CDATA[Oh Dear]]></name>
            </author>
            <summary type="html">
                <![CDATA[<p>I'm pleased to share that we've can now notify you via Google Chat.</p>
<p>Here's what that looks like:</p>
<p><img src="/media/blog/UGl3CnlGSwUs9nTGL9I668cNZwYZwB3YhHmE2Ub5.png" alt="" /></p>
<p>Our Google Chat notifications include:</p>
<ul>
<li>Visual status indicators with emojis (✅ for success, ⚠️ for warnings, ❌ for errors)</li>
<li>Clear, concise messaging that tells you exactly what happened</li>
<li>Quick action buttons to view full reports and investigate issues</li>
<li>Consistent formatting that matches the severity of each alert type</li>
</ul>
<p>You can read more on how to set up Google Chat notifications <a href="https://ohdear.app/docs/notifications/google-chat">in our docs</a>.</p>
<p>Of course, we also offer <a href="https://ohdear.app/features/notifications">numerous other channels</a> to notify you when something is wrong with your site.</p>
]]>
            </summary>
                                    <updated>2025-07-08T21:00:03+00:00</updated>
        </entry>
            <entry>
            <title><![CDATA[Recipes for automating Oh Dear]]></title>
            <link rel="alternate" href="https://ohdear.app/news-and-updates/recipes-for-automating-oh-dear" />
            <id>https://ohdear.app/150</id>
            <author>
                <name><![CDATA[Oh Dear]]></name>
            </author>
            <summary type="html">
                <![CDATA[<p>Today we are releasing <code>oh-dear-api-examples</code> a brand-new open-source repository on GitHub. It curates bite-size scripts, helpers and ideas for helping you bulk-manage your Oh Dear account via the API.</p>
<p><a href="https://github.com/ohdearapp/oh-dear-api-examples">github.com/ohdearapp/oh-dear-api-examples</a></p>
<h3 id="how-it-works">How it works</h3>
<p>Installation in under a minute just following the README.md or:</p>
<pre data-lang="bash" class="notranslate"><span class="hl-keyword">git</span> clone https://github.com/ohdearapp/oh-dear-api-examples.git
<span class="hl-keyword">cd</span> oh-dear-api-examples
<span class="hl-keyword">cp</span> .env.example .env
<span class="hl-keyword">npm</span> install
</pre>
<p>Just clone → npm install → start interacting with your account. This repository is a cookbook <em>not</em> a full SDK. It was created in JavaScript as it's easy to use, reference and port to another language. Copy a function, tweak it, add it to your workflows as needed. 💪</p>
<p>You will find a few different files</p>
<ul>
<li><code>./examples.js</code>: composable functions (create_site_from_existing, sync_status_pages_with_tagged_sites, generateReportSnapshot, …) you can import or run directly</li>
<li><code>report.js</code>: exposes a <code>generateReportSnapshot</code> function that returns a full site report for the month (up to the current day) - useful when recreating Oh Dear's monthly PDF reports</li>
<li><code>cache/</code>: responses are persisted locally so repeated runs are fast and you can inspect the raw JSON response when debugging</li>
<li><code>ohdear.js</code>: a thin wrapper around fetch to interact with the API</li>
<li><code>.env</code>: add you Oh Dear API key here (OH_DEAR_API_KEY=…)</li>
</ul>
<p>Let us know if you have any other use-cases, problems or automation suggestions. This could either be a product/service update or an addition to this repo. Just let us know!</p>
<h3 id="did-you-know">Did you know?</h3>
<p>We have several options to automate Oh Dear further;</p>
<ul>
<li>We have an <a href="https://ohdear.app/docs/integrations/the-oh-dear-php-sdk">official Php SDK</a></li>
<li>There's an <a href="https://ohdear.app/docs/integrations/our-cli-tool">Oh Dear CLI</a> command</li>
<li>And <a href="https://ohdear.app/docs/integrations/3rd-party-integrations-of-oh-dear">more 3rd party integrations</a></li>
</ul>
<h3 id="need-more">Need more?</h3>
<p>Leave an issue in the repo or contact support for suggestions or more information. We look forward to helping you navigate real world use cases when using Oh Dear API to manage you team and client monitoring.</p>
]]>
            </summary>
                                    <updated>2025-07-20T19:57:58+00:00</updated>
        </entry>
            <entry>
            <title><![CDATA[How to create dynamic status pages on demand]]></title>
            <link rel="alternate" href="https://ohdear.app/news-and-updates/how-to-create-dynamic-status-pages-on-demand" />
            <id>https://ohdear.app/142</id>
            <author>
                <name><![CDATA[Oh Dear]]></name>
            </author>
            <summary type="html">
                <![CDATA[<p>Clients expect transparency, especially when things go wrong. But manually updating a status page during an incident or maintenance window slows you down when speed matters most.</p>
<p>Oh Dear’s status pages are more than just a pretty uptime dashboard. They’re fully API-driven and designed to scale with your workflow. Whether you manage five client sites or five hundred, you can create, update and sync status pages as needed.</p>
<p>Before we get into it, you can find everything there's to know about our status pages in the <a href="https://ohdear.test/docs/integrations/the-oh-dear-api#status-pages">documentation</a>.</p>
<h2 id="creating-status-pages-via-the-api">Creating status pages via the API <a href="#creating-status-pages-via-the-api" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#creating-status-pages-via-the-api">#</a></h2>
<p>You can programmatically spin up a status page using the Oh Dear API. This includes:</p>
<ul>
<li>Name and title</li>
<li>Sites components to show</li>
<li>Create a status update</li>
</ul>
<p>This is perfect for automating client onboarding from your CRM - provision their site, then create a branded status page as part of the same flow.</p>
<p>Let's create a status page for all of our client sites.</p>
<pre data-lang="js" class="notranslate"><span class="hl-keyword">const</span> response = <span class="hl-keyword">await</span> <span class="hl-property">fetch</span>(<span class="hl-value">'https://ohdear.app/api/status-pages'</span>, {
    <span class="hl-property">headers</span>: {
        <span class="hl-value">'Authorization'</span>: `Bearer ${process.<span class="hl-property">env</span>.<span class="hl-property">OHDEAR_API_KEY</span>}`,
        <span class="hl-value">'Content-Type'</span>: <span class="hl-value">'application/json'</span>
    },
    <span class="hl-property">method</span>: <span class="hl-value">'POST'</span>,
    <span class="hl-property">body</span>: <span class="hl-type">JSON</span>.<span class="hl-property">stringify</span>({
        <span class="hl-value">&quot;team_id&quot;</span>: 1,
        <span class="hl-value">&quot;title&quot;</span>: <span class="hl-value">&quot;Clients&quot;</span>,
        <span class="hl-value">&quot;sites&quot;</span>: []
    })
});

<span class="hl-keyword">const</span> data = <span class="hl-keyword">await</span> response.<span class="hl-property">json</span>();

<span class="hl-keyword">const</span> statusPageId = data.<span class="hl-property">id</span>;
</pre>
<h2 id="syncing-sites-according-to-their-tags">Syncing sites according to their tags <a href="#syncing-sites-according-to-their-tags" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#syncing-sites-according-to-their-tags">#</a></h2>
<p>Once the page is created, use the API to attach monitored sites to it. To keep status pages in sync with your infrastructure, you can pull all sites matching <strong>tag</strong> and attach them to the relevant status page.</p>
<p>In this example we will sync sites tagged with <code>env:production</code> and <code>client:*</code>. This way, new sites added to your system are reflected in public reporting without extra effort. Add this in as a hook to your CRM or on a schedule.</p>
<p>Tip: you can monitor the scheduled task using Oh Dear's cron job monitoring service.</p>
<p>Let's see how we can do this:</p>
<pre data-lang="js" class="notranslate"><span class="hl-comment">// get the status page using the id from the previous step or find it by your custom domain:</span>

<span class="hl-keyword">const</span> statusPages = <span class="hl-keyword">await</span> (<span class="hl-keyword">await</span> <span class="hl-keyword">get</span>(<span class="hl-value">'status-pages'</span>)).<span class="hl-property">json</span>();

<span class="hl-keyword">const</span> statusPage = statusPages.<span class="hl-property">data</span>.<span class="hl-property">find</span>(page =&gt; page.<span class="hl-property">domain</span> === <span class="hl-value">'status.your-site.com'</span>);

<span class="hl-keyword">const</span> sites = <span class="hl-keyword">await</span> (<span class="hl-keyword">await</span> <span class="hl-keyword">get</span>(<span class="hl-value">'sites'</span>)).<span class="hl-property">json</span>();

<span class="hl-keyword">const</span> sitesToSync = sites.<span class="hl-property">data</span>.<span class="hl-property">filter</span>(site =&gt; site.<span class="hl-property">tags</span>.<span class="hl-property">includes</span>(<span class="hl-value">'env:production'</span>) &amp;&amp; site.<span class="hl-property">tags</span>.<span class="hl-property">includes</span>(<span class="hl-value">'client:*'</span>));

<span class="hl-keyword">const</span> response = <span class="hl-keyword">await</span> <span class="hl-property">post</span>(`<span class="hl-property">https</span>:<span class="hl-comment">//ohdear.app/api/status-pages/${statusPage.id}/sites`, {</span>
        <span class="hl-value">&quot;sync&quot;</span>: <span class="hl-keyword">true</span>,
        <span class="hl-value">&quot;sites&quot;</span>: sitesToSync.<span class="hl-property">map</span>(site =&gt; ({
            <span class="hl-value">&quot;id&quot;</span>: site.<span class="hl-property">id</span>,
            <span class="hl-value">&quot;clickable&quot;</span>: <span class="hl-keyword">true</span>
        }))
    })

console.<span class="hl-property">log</span>(response.<span class="hl-property">status</span>);
</pre>
<p>You should see a 200 response and all sites synced to the status page. You can also switch off the sync by setting <code>&quot;sync&quot;: false</code> which turns this endpoint into a append-only operation.</p>
<p>Status page sites are automatically grouped by the 'group name' in your site settings. Note: tags are not visible to the public.</p>
<h2 id="creating-reusable-status-page-templates">Creating reusable status page templates <a href="#creating-reusable-status-page-templates" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#creating-reusable-status-page-templates">#</a></h2>
<p>If you manage multiple clients with similar needs, you can define a <strong>status page template</strong> from the UI or API. This gives you the option to set a default title, text and/or severity with a label for quick selection.</p>
<p>Even though we are using the API one of the biggest benefits of status page templates is the ability to give non-technical team members access to pre-prepared messages to the public. No more back and forth with the team to get a message approved.</p>
<p>Let's see how we can create a new template. You might use this to control the list of messages from your CRM.</p>
<pre data-lang="js" class="notranslate"><span class="hl-keyword">const</span> response = <span class="hl-keyword">await</span> <span class="hl-property">post</span>(<span class="hl-value">'https://ohdear.app/api/status-page-update-templates'</span>, {
    <span class="hl-value">&quot;team_id&quot;</span>: 1,
    <span class="hl-value">&quot;name&quot;</span>: <span class="hl-value">&quot;Major issue template&quot;</span>,
    <span class="hl-value">&quot;title&quot;</span>: <span class="hl-value">&quot;Major issue&quot;</span>,
    <span class="hl-value">&quot;text&quot;</span>: <span class="hl-value">&quot;We are currently experiencing a major issue. Please check back later.&quot;</span>,
    <span class="hl-value">&quot;severity&quot;</span>: <span class="hl-value">&quot;major&quot;</span>
})
</pre>
<p>You might want to make the template more useful by providing more details about the issue. You can view some example templates we have prepared here: https://ohdear.app/news-and-updates/new-feature-status-page-update-templates</p>
<h2 id="setting-up-recurring-maintenance-periods">Setting up recurring maintenance periods <a href="#setting-up-recurring-maintenance-periods" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#setting-up-recurring-maintenance-periods">#</a></h2>
<p>Using the API, you can schedule a <strong>maintenance window</strong> and set the status page’s current status at the same time. This is ideal when deploying updates or rolling out infrastructure changes.</p>
<p>We recommend:</p>
<ol>
<li>Triggering the maintenance state just before deployment starts (if you are zero downtime it might be worth doing this for larger code changes, migrations and performance-impacting work)</li>
<li>Updating the status to &quot;Maintenance scheduled&quot;</li>
<li>Redirecting traffic to the status page during the window if there's downtime involved</li>
</ol>
<p>This improves transparency and helps set expectations regarding performance while you focus on the work.</p>
<p>We frequently get asked about recurring maintenance periods. We don't support it directly in the application as it can be confusing to manage - sometimes it's not obvious why your notifications have been supressed. This is especially important when a maintenance window is cancelled internally but Oh Dear is not aware - potentially causing some alerts to be silenced.</p>
<p>However, the API allow you to set a time in advance and duration for the maintenance window., This way you can schedule as many maintenance windows as you want in preparation scheduled releases. Similarly, status page updates can also be set in advance so you can co-ordinate your alert maintenance windows and public status updates.</p>
<p>Let's see how we can do this:</p>
<pre data-lang="js" class="notranslate"><span class="hl-keyword">const</span> response = <span class="hl-keyword">await</span> <span class="hl-property">post</span>(<span class="hl-value">'https://ohdear.app/api/maintenance-periods'</span>, {
    <span class="hl-value">&quot;site_id&quot;</span>: 1,
    <span class="hl-value">&quot;start_at&quot;</span>: <span class="hl-value">&quot;2025-01-01 09:00&quot;</span>,
    <span class="hl-value">&quot;end_at&quot;</span>: <span class="hl-value">&quot;2025-01-01 10:00&quot;</span>,
});

<span class="hl-keyword">const</span> response = <span class="hl-keyword">await</span> <span class="hl-property">post</span>(<span class="hl-value">'https://ohdear.app/api/status-page-updates'</span>, {
    <span class="hl-value">&quot;status_page_id&quot;</span>: 1,
    <span class="hl-value">&quot;title&quot;</span>: <span class="hl-value">&quot;Scheduled maintenance&quot;</span>,
    <span class="hl-value">&quot;text&quot;</span>: <span class="hl-value">&quot;We are working on it. Please check back later.&quot;</span>,
    <span class="hl-value">&quot;severity&quot;</span>: <span class="hl-value">&quot;maintenance&quot;</span>,
    <span class="hl-value">&quot;time&quot;</span>: <span class="hl-value">&quot;2025-01-01 09:00&quot;</span>,
    <span class="hl-value">&quot;pinned&quot;</span>: <span class="hl-keyword">false</span>
});

</pre>
<p>Now everything is nicely syncronized. Set up as many as you need in advance, trigger it via a cron job or by deployment or via your internal tools - your status page and alerts is only an API call away.</p>
<h2 id="you-can-automate-everything">You can automate everything <a href="#you-can-automate-everything" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#you-can-automate-everything">#</a></h2>
<p>By integrating status page management into your deployment pipeline and client onboarding you give your customers a better experience without extra overhead. And with Oh Dear’s flexible API you're always in control of what gets shared, and when.</p>
<p>The API works across your account (assuming you have scoped the API key appropriately) so you can easily transfer sites, status pages, message templates and notifications between teams.</p>
<p>Let us know what you would like to see in the API to make managing sites as easy as using the UI.</p>
]]>
            </summary>
                                    <updated>2025-07-18T13:19:46+00:00</updated>
        </entry>
            <entry>
            <title><![CDATA[Oh Dear now lets you verify redirect URLs during uptime checks]]></title>
            <link rel="alternate" href="https://ohdear.app/news-and-updates/oh-dear-now-lets-you-verify-redirect-urls-during-uptime-checks" />
            <id>https://ohdear.app/145</id>
            <author>
                <name><![CDATA[Oh Dear]]></name>
            </author>
            <summary type="html">
                <![CDATA[<p>We’ve just rolled out a helpful new setting to give you tighter control over how uptime is measured: Redirect URL Validation.</p>
<h3 id="why-url-validation-matters">Why URL validation matters</h3>
<p>By default, our uptime checks follow redirects and consider the site to be &quot;up&quot; as long as a 200 OK response is reached, regardless of what the final URL is.</p>
<p>But for some workflows, infrastructure setups or security models, ensuring the final destination matches a specific URL is critical. For example:</p>
<ul>
<li>You expect all traffic from https://example.com to land on https://www.example.com</li>
<li>You're routing through CDNs, reverse proxies or edge workers and want to verify the correct endpoint is reached</li>
<li>You're enforcing domain-level canonicalization</li>
</ul>
<h3 id="introducing-the-codeexpected-final-redirect-urlcode-setting">Introducing the <code>expected_final_redirect_url</code> setting</h3>
<p>Oh Dear has been able to verify the maximum number of redirects allowed before marking the site as &quot;down&quot;.</p>
<p>But with the new <code>expected_final_redirect_url</code> setting we will now inspect the full redirect chain during each uptime check. If the final URL doesn’t exactly match what you’ve configured, the site will be marked as down.</p>
<pre data-lang="json" class="notranslate"><span class="hl-keyword">&quot;expected_final_redirect_url&quot;</span>: <span class="hl-value">&quot;https://www.example.com&quot;</span>
</pre>
<p>If the final resolved URL is anything other than https://www.example.com the check fails.</p>
<h3 id="how-it-works">How it works</h3>
<p>You can configure this setting in the dashboard under your site's uptime check settings or via the API.</p>
<pre data-lang="txt" class="notranslate">PUT /api/sites/{site_id}

{
  &quot;uptime_check_redirect_url&quot;: &quot;https://www.example.com&quot;
}
</pre>
<p>What Happens If It Doesn't Match?</p>
<p>If the resolved URL doesn't match the configured redirect_url exactly:</p>
<ul>
<li>The site will be marked 'down'</li>
<li>The status page (if used) will reflect the downtime</li>
<li>Your configured notifications will be triggered just like with any other failure</li>
</ul>
<h3 id="whats-next">What's next?</h3>
<p>👉 Have questions or ideas for other advanced uptime rules? Reach out to us via support as we’d love to hear them.</p>
<p>Happy monitoring!</p>
]]>
            </summary>
                                    <updated>2025-07-18T13:14:41+00:00</updated>
        </entry>
            <entry>
            <title><![CDATA[Oh Dear becomes the preferred website monitoring tool for Dutch education and research institutions]]></title>
            <link rel="alternate" href="https://ohdear.app/news-and-updates/oh-dear-becomes-the-preferred-website-monitoring-tool-for-dutch-education-and-research-institutions" />
            <id>https://ohdear.app/146</id>
            <author>
                <name><![CDATA[Oh Dear]]></name>
            </author>
            <summary type="html">
                <![CDATA[<p><a href="https://www.surf.nl/">SURF</a> is a Duth IT cooperative in service of educational and research institutions. In essence, they help out these organizations by vetting and building software and other tools that best suit their needs. For years they offered a website monitoring service of their own, SURFopzichter, to affiliated institutions. As of February 1, 2025, the application has been sunset. Since there are plenty more modern alternatives on the market that offer the same services, SURF now refers its members to Oh Dear, among others, as a user-friendly and comprehensive solution for website monitoring. Affiliated universities, colleges, and research institutions can now enjoy all-in-one monitoring with the help of a Belgium-made tool, awesome!</p>
<p>For context, SURF's primary goal is to strengthen the Dutch education and research sector by collaborating on digital services, innovation, and knowledge sharing in ICT. In practice, they are a member organization that provides solutions, knowledge, and expertise in areas such as security, public values, access management, storage, and connectivity. For its members, SURF functions as an association, service provider, and innovation hub, negotiating contracts with suppliers on behalf of its members. Basically, the aim is to provide members with good deals and proper expertise under clear conditions—both in line with (inter)national laws and regulations and shared public values.</p>
<p>“The fact that a large-scale member organization like SURF now offers Oh Dear to its members is a very nice confirmation for us that we have been able to build a reliable and quality product,” says our co-founder <a href="https://x.com/mattiasgeniar">Mattias</a>. “Even though we're small potatoes, we're proud that with Oh Dear, we're supporting the educational landscape and digital learning environments in the Netherlands.”</p>
<p>More than 100 institutions are affiliated with the SURF cooperative, including all Dutch universities, colleges, medical centers, vocational schools (MBO), as well as research institutions like the Dutch Research Council (NWO) and the Royal Library (KB).</p>
<p>“I also love how conscious and diligent people have become when they're looking for a specific tool,” adds our other co-founder, <a href="https://x.com/freekmurze">Freek</a>. “The biggest ballers are not always first-choice anymore. Some people even deliberately avoid the large tech companies from the US and instead opt for a fully EU-based solution. Buy local, as they say!”</p>
<p>Speaking of being EU-based, <a href="https://european-alternatives.eu/product/ohdear">Oh Dear was recently recommended on European Alternatives</a>, a database of alternative solutions to well-known software and tools. This didn't happen overnight, as Oh Dear underwent a pretty thorough assessment in terms of privacy and security.</p>
<p>With SURF’s trust, our boys Freek and Mattias are optimistic about the future. “Partnerships like these give Oh Dear a lot of credibility and confirm that we certainly don’t have to fall short compared to our biggest competitors,” they say.</p>
<p>If, by some miracle, you landed on this page without any context whatsoever, Oh Dear is an all-in-one website monitoring tool. It's a complete solution for uptime, domain, and performance monitoring, with built-in status pages and extensive notification settings. With a focus on transparency, ease of use, and reliability, Oh Dear serves customers worldwide—from sole proprietors to government agencies and large agencies. We can handle anything you throw at us, and we're still working on improving our services on the daily!</p>
]]>
            </summary>
                                    <updated>2025-06-24T21:00:03+00:00</updated>
        </entry>
            <entry>
            <title><![CDATA[Typehinting Laravel validation rules using PHPStan's type aliases]]></title>
            <link rel="alternate" href="https://ohdear.app/news-and-updates/typehinting-laravel-validation-rules-using-phpstans-type-aliases" />
            <id>https://ohdear.app/144</id>
            <author>
                <name><![CDATA[Oh Dear]]></name>
            </author>
            <summary type="html">
                <![CDATA[<p>Static analysis is a powerful tool that examines your code without actually running it, catching bugs and type mismatches before they reach production. For PHP developers, <a href="https://phpstan.org/user-guide/getting-started">PHPStan</a> has become the go-to solution, and Laravel developers often use Larastan - a wrapper that adds Laravel-specific understanding to PHPStan. In this post, I'll share how I'm using PHPStan's type aliases feature to make validation rule typehints more readable and maintainable in our Laravel application.</p>
<h2 id="what-is-static-analysis-and-why-it-matters">What is static analysis and why it matters <a href="#what-is-static-analysis-and-why-it-matters" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#what-is-static-analysis-and-why-it-matters">#</a></h2>
<p>Static analysis tools like PHPStan analyze your codebase to find errors that might slip through testing. They check for type mismatches, undefined methods, incorrect return types, and more. While PHP has become more type-safe over the years, static analysis adds an extra layer of confidence, especially in large codebases where manual review becomes impractical.</p>
<h2 id="a-bit-of-history">A bit of history <a href="#a-bit-of-history" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#a-bit-of-history">#</a></h2>
<p>When we launched Oh Dear almost 8 years ago, we were using the cutting-edge tools of the time: PHP 7, Laravel 5, and the first version of Laravel Spark. Over the years, we've diligently kept our stack modern, upgrading annually to run on the latest versions. Our extensive test suite (over 2,000 tests) has been crucial in making these upgrades smooth and ensuring everything continues to work correctly.</p>
<p>Despite our commitment to code quality, there was one area I'd been putting off: static analysis.</p>
<p>At Spatie, where I also work, static analysis has been a standard practice for years. Starting new projects with PHPStan from day one is straightforward. But Oh Dear was different - it's a mature application with many many classes. The thought of retrofitting static analysis into such a large existing codebase felt overwhelming. I didn't want to just suppress all the errors in a baseline file; I wanted to fix them properly. But finding the time for such a massive undertaking seemed impossible.</p>
<p>Recently, I finally decided to tackle this technical debt. I started with PHPStan at level 1, methodically fixing every issue before moving to the next level. Now I'm working through level 6, where PHPStan becomes particularly strict about type definitions. The most common issues at this level involve missing or incomplete type hints - and that's where today's story begins.</p>
<h2 id="adding-typehints-to-laravel-form-requests">Adding typehints to Laravel form requests <a href="#adding-typehints-to-laravel-form-requests" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#adding-typehints-to-laravel-form-requests">#</a></h2>
<p>Before diving into the problem, let me briefly explain Laravel's FormRequest objects. In Laravel, FormRequests are classes that encapsulate validation logic for HTTP requests. They keep your controllers clean by moving validation rules to dedicated classes.</p>
<p>Oh Dear has grown into a substantial application with over 100 web and API endpoints, each with its own <code>FormRequest</code> object. Here's a typical example:</p>
<pre data-lang="php" class="notranslate"><span class="hl-keyword">namespace</span> <span class="hl-type">App\Http\Api\Requests</span>;

<span class="hl-keyword">use</span> <span class="hl-type">App\Domain\Site\Rules\SiteIdRule</span>;
<span class="hl-keyword">use</span> <span class="hl-type">App\Domain\Team\Rules\TeamMemberRule</span>;
<span class="hl-keyword">use</span> <span class="hl-type">Illuminate\Foundation\Http\FormRequest</span>;

<span class="hl-keyword">class</span> <span class="hl-type">StoreStatusPageRequest</span> <span class="hl-keyword">extends</span> <span class="hl-type">FormRequest</span>
{
    <span class="hl-keyword">public</span> <span class="hl-keyword">function</span> <span class="hl-property">rules</span>(): <span class="hl-type">array</span>
    {
        <span class="hl-keyword">return</span> [
            <span class="hl-value">'team_id'</span> =&gt; [<span class="hl-value">'required'</span>, <span class="hl-keyword">new</span> <span class="hl-type">TeamMemberRule</span>(<span class="hl-property">currentUser</span>())],
            <span class="hl-value">'title'</span> =&gt; [<span class="hl-value">'required'</span>, <span class="hl-value">'string'</span>, <span class="hl-value">'max:255'</span>],
            <span class="hl-value">'sites'</span> =&gt; [<span class="hl-value">'required'</span>, <span class="hl-value">'array'</span>],
            <span class="hl-value">'sites.*.id'</span> =&gt; [<span class="hl-value">'required'</span>, <span class="hl-keyword">new</span> <span class="hl-type">SiteIdRule</span>(<span class="hl-property">currentUser</span>())],
            <span class="hl-value">'sites.*.clickable'</span> =&gt; [<span class="hl-value">'bool'</span>],
        ];
    }
}
</pre>
<p>This looks clean and works perfectly. But when PHPStan examines it at level 6, it's not satisfied:</p>
<pre data-lang="txt" class="notranslate"> ------ --------------------------------------------------------------------------------------- 
  Line   StoreStatusPageRequest.php                                                             
 ------ --------------------------------------------------------------------------------------- 
  16     Method App\Http\Api\Requests\StoreStatusPageRequest::rules() return type has no value  
         type specified in iterable type array.                                                 
         🪪 missingType.iterableValue                                                           
         💡 See:                                                                                
            https://phpstan.org/blog/solving-phpstan-no-value-type-specified-in-iterable-type   
 ------ ---------------------------------------------------------------------------------------
</pre>
<p>PHPStan wants to know what's inside the array. Let's start with a simple fix:</p>
<pre data-lang="php" class="notranslate"><span class="hl-comment">// ...</span>

<span class="hl-comment">/** <span class="hl-value">@return</span> <span class="hl-type">array&lt;<span class="hl-generic">string</span>, array&lt;<span class="hl-generic">string</span>&gt;&gt; </span>*/</span>
<span class="hl-keyword">public</span> <span class="hl-keyword">function</span> <span class="hl-property">rules</span>(): <span class="hl-type">array</span>
{
    <span class="hl-comment">// ...</span>
}
</pre>
<p>This tells PHPStan we're returning an array with string keys and arrays of strings as values. But now we get a different error:</p>
<pre data-lang="txt" class="notranslate"> ------ ------------------------------------------------------------------------------------------
  Line   StoreStatusPageRequest.php
 ------ ------------------------------------------------------------------------------------------
  19     Method App\Http\Api\Requests\StoreStatusPageRequest::rules() should return array&lt;string,
         array&lt;string&gt;&gt; but returns array&lt;string,
         list&lt;App\Domain\Site\Rules\SiteIdRule|App\Domain\Team\Rules\TeamMemberRule|string&gt;&gt;.
         🪪 return.type
 ------ ------------------------------------------------------------------------------------------
</pre>
<p>PHPStan has analyzed our actual return value and found that we're not just returning strings - we're also returning custom rule objects. Let's be more specific:</p>
<pre data-lang="php" class="notranslate"><span class="hl-comment">/** <span class="hl-value">@return</span> <span class="hl-type">array&lt;string, array&lt;\Illuminate\Contracts\Validation\ValidationRule|string&gt;&gt; </span>*/</span>
<span class="hl-keyword">public</span> <span class="hl-keyword">function</span> <span class="hl-property">rules</span>(): <span class="hl-type">array</span>
{
    <span class="hl-comment">// ...</span>
}
</pre>
<p>Since both <code>SiteIdRule</code> and <code>TeamMemberRule</code> implement Laravel's <code>ValidationRule</code> interface, this type hint is more accurate and flexible. PHPStan is now satisfied:</p>
<pre data-lang="txt" class="notranslate"> [OK] No errors
</pre>
<p>But wait - Laravel's validation system is even more flexible than this. Rules don't always need to be wrapped in arrays, and there are multiple validation contracts. Here's the complete type hint that covers all possibilities:</p>
<pre data-lang="php" class="notranslate"><span class="hl-comment">/** <span class="hl-value">@return</span> <span class="hl-type">array&lt;string, array&lt;\Illuminate\Contracts\Validation\Rule|\Illuminate\Contracts\Validation\ValidationRule|string&gt;|string&gt; </span>*/</span>
<span class="hl-keyword">public</span> <span class="hl-keyword">function</span> <span class="hl-property">rules</span>(): <span class="hl-type">array</span>
{
    <span class="hl-comment">// ...</span>
}
</pre>
<p>This type hint is accurate but... it's quite a mouthful, isn't it?</p>
<h2 id="type-aliases-to-the-rescue">Type aliases to the rescue <a href="#type-aliases-to-the-rescue" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#type-aliases-to-the-rescue">#</a></h2>
<p>Imagine copying that lengthy type hint to over 100 FormRequest classes. Not only would it be tedious, but it would also create a maintenance nightmare. What if Laravel adds a new validation contract? We'd need to update every single docblock.</p>
<p>Fortunately, PHPStan offers an elegant solution: <a href="https://phpstan.org/config-reference#type-aliases">type aliases</a>. These allow you to define complex types once and reference them by a simple name throughout your codebase.</p>
<p>In your <code>phpstan.neon</code> configuration file, you can define a type alias like this:</p>
<pre data-lang="txt" class="notranslate">parameters:
    level: 6
    
    typeAliases:
        ValidationRules: 'array&lt;string, array&lt;\Illuminate\Contracts\Validation\Rule|\Illuminate\Contracts\Validation\ValidationRule|string&gt;|string&gt;'
</pre>
<p>Now, instead of that complex type hint, you can simply use:</p>
<pre data-lang="php" class="notranslate"><span class="hl-comment">/** <span class="hl-value">@return</span> <span class="hl-type">ValidationRules </span>*/</span>
<span class="hl-keyword">public</span> <span class="hl-keyword">function</span> <span class="hl-property">rules</span>(): <span class="hl-type">array</span>
{
    <span class="hl-comment">// ...</span>
}
</pre>
<h2 id="in-closing">In closing <a href="#in-closing" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#in-closing">#</a></h2>
<p>Adding static analysis to a large existing codebase can feel daunting, but it doesn't have to be an all-or-nothing endeavor. By starting at level 1 and gradually working your way up, you can improve your code quality incrementally. In our case, implementing PHPStan has already helped us catch several subtle bugs that our tests missed.</p>
<p>Type aliases, in particular, have made the process much more manageable. What could have been a tedious copy-paste exercise across 100+ files became a simple, maintainable solution. The readable type hints also serve as inline documentation, making our codebase more approachable for new team members.</p>
]]>
            </summary>
                                    <updated>2025-06-18T09:41:32+00:00</updated>
        </entry>
            <entry>
            <title><![CDATA[5 tips for managing client sites with Oh Dear]]></title>
            <link rel="alternate" href="https://ohdear.app/news-and-updates/5-tips-for-managing-client-sites-with-oh-dear" />
            <id>https://ohdear.app/139</id>
            <author>
                <name><![CDATA[Oh Dear]]></name>
            </author>
            <summary type="html">
                <![CDATA[<p>Managing dozens (or hundreds) of client sites can quickly become chaotic without the right tools. Whether you're running an agency, internal platform team or dev shop, visibility and control are everything. That's where Oh Dear comes in.</p>
<p>Oh Dear is an all-in-one monitoring service that gives you a unified dashboard for uptime checks, performance monitoring, broken link detection, SSL and domain expiry alerts, scheduled task validation and more. But beyond just running checks Oh Dear also gives you powerful tools for organizing and automating how you manage your client sites at scale.</p>
<p>Here are 5 practical ways to make that happen.</p>
<hr />
<h2 id="1-use-tags-to-organize-and-notify">1. Use Tags to Organize and Notify <a href="#1-use-tags-to-organize-and-notify" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#1-use-tags-to-organize-and-notify">#</a></h2>
<p>When you're managing a fleet of sites, grouping them by client, environment, region or team is essential. Oh Dear lets you apply <strong>tags</strong> in any format but we suggest something like:</p>
<ul>
<li><code>client:spatie</code></li>
<li><code>env:production</code></li>
<li><code>infra:cloudflare</code></li>
<li><code>region:eu-west-1</code></li>
<li><code>project:rebrand</code></li>
</ul>
<p>You are probably already aware that you can route notifications to tags or groups of tags.  Once your tags are set up, you can configure <strong>tag group notifications</strong>. This means when something goes wrong on any site tagged with <code>env:production</code> + <code>client:*</code> for example, you can notify a VIP Slack channel or OpsGenie team automatically - no manual routing needed while <code>env:staging</code> + <code>internal</code> can go to the email inbox that no one reads.</p>
<p>Tags also make onboarding easy. New production application for a <code>client</code>? Just tag it accordingly <code>client:spatie</code> and all the right checks and notifications apply.</p>
<hr />
<h2 id="2-find-sites-by-tag-via-global-search">2. Find Sites by Tag via Global Search <a href="#2-find-sites-by-tag-via-global-search" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#2-find-sites-by-tag-via-global-search">#</a></h2>
<p>Tags aren't just for routing notifications - they’re fully searchable. Open the global search menu in Oh Dear and type any tag (like <code>region:eu-west-1</code> or <code>team:seo</code>) to instantly filter your sites.</p>
<p>This is especially handy for large accounts where you need to quickly check in on just the production sites in Europe or just the ones belonging to a specific customer.</p>
<hr />
<h2 id="3-trigger-health-checks-on-deployment-via-the-api">3. Trigger Health Checks on Deployment via the API <a href="#3-trigger-health-checks-on-deployment-via-the-api" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#3-trigger-health-checks-on-deployment-via-the-api">#</a></h2>
<p>Oh Dear provides an API endpoint to <strong>trigger checks manually</strong>, which is perfect for CI/CD workflows.</p>
<p>This ensures you catch issues <em>before</em> the client does, and keeps your deployment pipeline tightly integrated with live monitoring. Here's an article explaining how you can run a new broken links check on-demand: https://ohdear.app/news-and-updates/trigger-an-on-demand-uptime-broken-links-check-after-a-deploy</p>
<p>But you can also apply the same principles to all of our other checks. My favourites are:</p>
<ul>
<li>Lighthouse</li>
<li>Sitemap</li>
</ul>
<pre data-lang="js" class="notranslate"><span class="hl-keyword">const</span> OHDEAR_TOKEN = <span class="hl-value">'your API token'</span>;

<span class="hl-comment">// you can find this via the /sites/{id} endpoint</span>
<span class="hl-comment">// or from the check settings in the app</span>
<span class="hl-keyword">const</span> checkId = 1; 

<span class="hl-property">fetch</span>(<span class="hl-value">'https://ohdear.app/api/checks/1/request-run'</span>, {
  <span class="hl-property">method</span>: <span class="hl-value">'POST'</span>,
  <span class="hl-property">headers</span>: {
    <span class="hl-value">'Authorization'</span>: `Bearer ${OHDEAR_TOKEN}`,
    <span class="hl-value">'Accept'</span>: <span class="hl-value">'application/json'</span>,
    <span class="hl-value">'Content-Type'</span>: <span class="hl-value">'application/json'</span>
  }
})
  .<span class="hl-property">then</span>(res =&gt; {
    <span class="hl-keyword">if</span> (!res.<span class="hl-property">ok</span>) <span class="hl-keyword">throw</span> <span class="hl-keyword">new</span> <span class="hl-type">Error</span>(`HTTP ${res.<span class="hl-property">status</span>}: ${res.<span class="hl-property">statusText</span>}`);
    <span class="hl-keyword">return</span> res.<span class="hl-property">json</span>();
  })
  .<span class="hl-property">then</span>(data =&gt; console.<span class="hl-property">log</span>(<span class="hl-value">'Check run requested:'</span>, data))
  .<span class="hl-property">catch</span>(err =&gt; console.<span class="hl-property">error</span>(<span class="hl-value">'Error triggering check:'</span>, err));
</pre>
<p>Our uptime checks and application health checks run every minute - so that's two extra things we don't have to worry about.</p>
<p>You could also add a maintenance window if you don't have zero-downtime deployments or you are expecting some temporary latency after go-live. Read more here: https://ohdear.app/docs/features/how-to-configure-maintenance-windows</p>
<hr />
<h2 id="4-automate-site-creation-using-templates">4. Automate Site Creation Using Templates <a href="#4-automate-site-creation-using-templates" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#4-automate-site-creation-using-templates">#</a></h2>
<p>When spinning up new client sites speed and consistency is key. Rather than configuring everything from scratch, you can:</p>
<ul>
<li>Assign one of your sites as a <strong>template site</strong> using tag: <code>template</code> with your base checks, tags and settings.</li>
<li>Use the API to <strong>clone</strong> that site when onboarding a new client.</li>
<li>We support all site settings from our API so you can customize the new site as needed (e.g. domain name, team assignments) at the same time.</li>
<li>Add the site to an internal status page and client-facing status page both with custom domains. (Yes you can have as many status pages, custom domains and sites as you need. Included in every plan!)</li>
</ul>
<p>This approach saves time, ensures consistency across all clients and reduces configuration drift.</p>
<hr />
<h2 id="5-control-access-with-fine-grained-permissions">5. Control Access with Fine-Grained Permissions <a href="#5-control-access-with-fine-grained-permissions" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#5-control-access-with-fine-grained-permissions">#</a></h2>
<p>Not every stakeholder needs to see everything. Oh Dear lets you <strong>invite clients or teammates</strong> with scoped access:</p>
<ul>
<li>Give dev teams access to performance and scheduled task checks.</li>
<li>Let clients see uptime and SSL status only.</li>
<li>Prevent accidental edits to monitoring settings.</li>
</ul>
<p>This keeps your workspace clean and secure while still being transparent with stakeholders.</p>
<p>You can also add clients to the monthly site reports so they will automatically be up to date. We run reports on the first day of every month.</p>
<p>Tip: client email address can be added to the monthly site report via sites API.</p>
<hr />
<h2 id="bonus-add-site-status-badges">Bonus: Add Site Status Badges <a href="#bonus-add-site-status-badges" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#bonus-add-site-status-badges">#</a></h2>
<p>Want to show the latest status on your client sites? Oh Dear provides a <strong>site badge</strong> you can embed showing current uptime status.</p>
<pre data-lang="js" class="notranslate">
<span class="hl-comment">// https://ohdear.app/api/sites/{id}</span>

{
   ...
   <span class="hl-value">&quot;badge_id&quot;</span>: <span class="hl-value">&quot;01jeb77smcek1f4dxys08vect5&quot;</span>,
   ...
}

</pre>
<p>Then update your content with:</p>
<pre data-lang="html" class="notranslate">&lt;<span class="hl-keyword">img</span> <span class="hl-property">src</span>=&quot;https://ohdear.app/badges/site/{badge_id}/uptime/normal&quot; <span class="hl-property">alt</span>=&quot;uptime status badge&quot; /&gt;
</pre>
<p>It's a small touch that builds trust with clients and surfaces real-time updates to visitors.</p>
<hr />
<h2 id="whats-next">What's Next? <a href="#whats-next" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#whats-next">#</a></h2>
<p>In the next few blog posts, we'll dive deeper into each of these tips with hands-on code examples using the Oh Dear API, tips for setting up automation and ideas for streamlining your agency workflows even further.</p>
<p>Stay tuned!</p>
]]>
            </summary>
                                    <updated>2025-07-16T11:57:53+00:00</updated>
        </entry>
            <entry>
            <title><![CDATA[Introducing our improved uptime check]]></title>
            <link rel="alternate" href="https://ohdear.app/news-and-updates/introducing-our-improved-uptime-check" />
            <id>https://ohdear.app/140</id>
            <author>
                <name><![CDATA[Oh Dear]]></name>
            </author>
            <summary type="html">
                <![CDATA[<p>The past few months, we’ve working on improving our uptime check. We proud to announce that this improved check is now available for all users. You don’t have to do anything to get it (unless you are not subscribed to Oh Dear, in that case your should subscribe to Oh Dear 😄), all our users now have it enabled by default.</p>
<p>In this blogpost, I’d like to give an overview of the changes and some background why we changed some things.</p>
<h2 id="a-bit-of-history">A bit of history <a href="#a-bit-of-history" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#a-bit-of-history">#</a></h2>
<p>When Oh Dear was initially built, we only had a handful of checks: uptime, certificate health, and broken links.</p>
<p>Over time we added more checks: DNS, application health, sitemap, <a href="https://ohdear.app/docs/features/what-checks-does-oh-dear-perform">and a couple more</a>. One of the checks we introduced after launch was the performance check. It displays a graph that shows the response times of your site. For each minute it has a data point.</p>
<p><img src="/media/blog/FsaJo0P4SMMKpEYEozKA8TfxbTuDopWLNelbQLGj.png" alt="" /></p>
<p>Under the hood the performance check is powered by the uptime check. Each time we ping your site to verify it is online, we collect response time data for the performance check. That's why for the performance check to work, you had to have the uptime check enabled.</p>
<p>Because the vast majority of our users have both the uptime and the performance check enabled for their sites, and both of these checks are technically so intertwined, we decided to merge them together.</p>
<h2 id="lets-take-a-look-at-new-uptime-check">Let’s take a look at new uptime check <a href="#lets-take-a-look-at-new-uptime-check" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#lets-take-a-look-at-new-uptime-check">#</a></h2>
<p>In our UI, you'll only see the uptime check, but the new results page now also includes the performance graph. Here's how that new page looks like.</p>
<p><img src="/media/blog/rPuRmxulQMWgErDgRw9KCkde3gnJrBDczi94UZR9.jpg" alt="" /></p>
<p>We also improved what you see when the site is down. The alert is now much more clear, and you can take certain actions right from the alert: request a new run of the check, view your site. You can even post an update directly to the status page that contains your site.</p>
<p><img src="/media/blog/mnhWcHQ6tDt45p6AnxXLz8aR8GGZXPeQAoqU1XvV.png" alt="" /></p>
<p>Behind the scenes, we performed an additional technical upgrade. Instead of storing the performance data in our local MySQL database, it now gets saved in a ClickHouse database. This is a database specialized in working with time-based data. This unlocks future improvements we have in mind.</p>
<h2 id="in-closing">In closing <a href="#in-closing" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#in-closing">#</a></h2>
<p>We do hope that you like our new uptime check. Should you have any remarks, questions or feature requests, <a href="email:support@ohdear.app">let us know</a>!</p>
]]>
            </summary>
                                    <updated>2025-06-03T17:44:43+00:00</updated>
        </entry>
            <entry>
            <title><![CDATA[Managing monthly reports with the API]]></title>
            <link rel="alternate" href="https://ohdear.app/news-and-updates/managing-monthly-reports-with-the-api" />
            <id>https://ohdear.app/138</id>
            <author>
                <name><![CDATA[Oh Dear]]></name>
            </author>
            <summary type="html">
                <![CDATA[<p>On the first of every month we generate an extensive PDF report for every site. This report contains a summary of all check results for the month and is a snapshot available to you and your team via email and the Oh Dear dashboard. We keep the report history so each month can be viewed in a browser or downloaded as a PDF.</p>
<p>This report can also be emailed to any email address - not just team members - perfect for keeping your customers informed.</p>
<h2 id="managing-reports-via-api">Managing reports via API <a href="#managing-reports-via-api" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#managing-reports-via-api">#</a></h2>
<p>Up until now you could enable this feature from the 'Monthly reports' menu within your site details dashboard where you can also find the history but you could not update these settings from the API.</p>
<p>We have been adding more and more support to our API recently including snoozing individual scheduled task and application health items, managing status pages, viewing sitemap reports as well as exposing all check settings.</p>
<p>We have now made it possible to create and update sites with <code>send_report_to_emails</code> and <code>include_check_types_in_report</code> parameters in the payload to control who receives an email and what appears on the report.</p>
<p>Here's an example where i'm updating <code>your_site_id</code> with just the new monthly report parameters</p>
<p>POST https://ohdear.app/api/sites/{your_site_id}</p>
<p>Payload</p>
<pre data-lang="json" class="notranslate"><span class="hl-property">{</span>
	<span class="hl-keyword">&quot;send_report_to_emails&quot;</span>: <span class="hl-property">[</span>
		<span class="hl-value">&quot;sean@ohdear.app&quot;</span>,
		<span class="hl-value">&quot;mattias@ohdear.app&quot;</span>,
		<span class="hl-value">&quot;freek@ohdear.app&quot;</span>
	<span class="hl-property">]</span>,
	<span class="hl-keyword">&quot;include_check_types_in_report&quot;</span>: <span class="hl-property">[</span>
	  <span class="hl-value">&quot;uptime&quot;</span>,
      <span class="hl-value">&quot;performance&quot;</span>,
      <span class="hl-value">&quot;broken_links&quot;</span>,
      <span class="hl-value">&quot;mixed_content&quot;</span>,
      <span class="hl-value">&quot;lighthouse&quot;</span>,
      <span class="hl-value">&quot;cron&quot;</span>,
      <span class="hl-value">&quot;application_health&quot;</span>,
      <span class="hl-value">&quot;sitemap&quot;</span>,
      <span class="hl-value">&quot;dns&quot;</span>,
      <span class="hl-value">&quot;domain&quot;</span>,
      <span class="hl-value">&quot;certificate_health&quot;</span>,
      <span class="hl-value">&quot;certificate_transparency&quot;</span>
	<span class="hl-property">]</span>
<span class="hl-property">}</span>
</pre>
<h2 id="whats-next">What's next? <a href="#whats-next" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#whats-next">#</a></h2>
<p>Have you got any feature requests or anything you would like to see in the API that we haven't covered yet?</p>
]]>
            </summary>
                                    <updated>2025-05-12T09:06:39+00:00</updated>
        </entry>
            <entry>
            <title><![CDATA[Here are 10 ways to prevent website downtime]]></title>
            <link rel="alternate" href="https://ohdear.app/news-and-updates/here-are-10-ways-to-prevent-website-downtime" />
            <id>https://ohdear.app/137</id>
            <author>
                <name><![CDATA[Oh Dear]]></name>
            </author>
            <summary type="html">
                <![CDATA[<p>Every minute of website downtime cost large organizations <a href="https://www.forbes.com/councils/forbestechcouncil/2024/04/10/the-true-cost-of-downtime-and-how-to-avoid-it/">an average of $9,000</a>. That’s half a million dollars every hour, damn. And that’s just the average. If your organization heavily relies on your website to do business, that cost can increase even further. Needless to say, preventing website downtime is a top priority.</p>
<h2 id="leading-causes-of-website-downtime">Leading causes of website downtime <a href="#leading-causes-of-website-downtime" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#leading-causes-of-website-downtime">#</a></h2>
<p>Before we jump into ways of preventing website downtime, let’s first cover the leading causes of website downtime. If you can diagnose the cause of the downtime, you’ll be in a much better spot to prevent it from happening again. You don't want the perpetual Whac-a-Mole. So, here are the eight leading causes of website downtime:</p>
<ul>
<li><strong>Server issues:</strong> Whether you run your own website servers or pay for third-party hosting, hardware or software issues with your website’s servers can cause your site to go down.</li>
<li><strong>Overwhelmed resources:</strong> A sudden spike in traffic to your website can cause your servers to become overwhelmed preventing additional traffic from accessing your site.</li>
<li><strong>Expired SSL certificates:</strong> Although this doesn’t cause your website to go down, an expired SSL certificate should be treated the same. Most users will navigate away from your site if they receive a warning saying your site is insecure which occurs with an expired SSL certificate.</li>
<li><strong>DNS configuration issues:</strong> Your DNS needs to be configured correctly so that users are directed to your site when they navigate to your site’s URL. A DNS issue prevents people from accessing your site through your domain name.</li>
<li><strong>Software conflicts:</strong> Most sites use software to handle the backend of their website. Any errors with the software, which can occur after an update or due to conflicts between multiple software running different parts of a site’s backend, can cause your site to go down.</li>
<li><strong>Cyberattacks:</strong> Malicious actors can bring your website down with cyberattacks such as <a href="https://www.sciencedirect.com/topics/computer-science/distributed-denial-of-service">DDoS attacks</a> which artificially overwhelm your hosting infrastructure.</li>
<li><strong>Human error:</strong> Mistakes happen more often than we think. Thankfully, if it was human error that took down your website, it usually means you can get it back up by reverting the action that caused the downtime.</li>
<li><strong>Maintenance gone awry:</strong> During scheduled maintenance, things can go wrong that cause the downtime to extend beyond the planned maintenance time.</li>
</ul>
<h2 id="10-ways-to-help-you-to-prevent-website-downtime">10 ways to help you to prevent website downtime <a href="#10-ways-to-help-you-to-prevent-website-downtime" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#10-ways-to-help-you-to-prevent-website-downtime">#</a></h2>
<p>So, how do you prevent those eight leading causes of website downtime from occurring? We’re going to cover the 10 most effective strategies to prevent website downtime and minimize its impact.</p>
<h3 id="implement-global-uptime-monitoring">Implement global uptime monitoring</h3>
<p>To prevent website downtime, you first need to know when it happens. That can help you identify issues causing your website to go down. It also allows you to take immediate action to bring your site back up when it goes down.</p>
<h3 id="monitor-your-ssl-certificates">Monitor your SSL certificates</h3>
<p>Like we mentioned, issues with your SSL certificate won’t cause website downtime, but it does divert traffic. When issues with your SSL certificate exist, anyone navigating to your website will be greeted with a big warning from their browser saying your site is not secure. Most people do not choose to proceed to your site after that warning. SSL certificate monitoring ensures you’re the first person to know about any issues with your certificate so you can fix it before they impact your users.</p>
<h3 id="set-up-performance-monitoring">Set up performance Monitoring</h3>
<p>Pages that load slowly can have just as big of an impact as a page being down. When a page takes over three seconds to load, nearly half of users will leave your site. Performance monitoring keeps an eye on how long it takes your site to load and tells you what’s bogging down your load times. That gives you the information you need to make your site faster.</p>
<h3 id="implement-dns-monitoring">Implement DNS monitoring</h3>
<p>Internal tests can easily miss DNS errors. Afterall, an issue with your DNS won’t prevent your server from sending data to a test environment. But, it does prevent your users from accessing your website. You need your DNS configured correctly to ensure your users can access your site through your domain name. DNS monitoring checks your configuration, ensures users can access your domain, and tells you where the error exists if there is an issue with your DNS records.</p>
<h3 id="schedule-regular-backups">Schedule regular backups</h3>
<p>Downtime is not 100% avoidable. There will be errors that happen whether they’re human errors, an issue with your hosting servers, or a software issue. Scheduling regular backups allows you to rollback your changes at any time while minimizing progress loss. This can immediately bring your website back up if you run into software issues or a human error that takes down your site.</p>
<h3 id="implement-load-balancing">Implement load balancing</h3>
<p>Without load balancing, a single server becoming overwhelmed can prevent some users from accessing your website. Load balancing spreads out the traffic to your website between multiple servers preventing any one server from becoming overwhelmed. This makes your website more scalable as you can add more servers to accommodate increases in traffic.</p>
<h3 id="monitor-scheduled-tasks">Monitor scheduled tasks</h3>
<p>Whether its database or server maintenance, keep track of scheduled tasks and monitor their progress. These tasks can fail and if not monitored, can cause cascading effects that create downtime for your website.</p>
<h3 id="check-for-mixed-content">Check for mixed content</h3>
<p>Mixed content doesn't cause downtime, but it does create a poor user experience. Browsers can identify mixed content and then send users a security warning just like with SSL certificate issues. Monitoring for this type of content tells you what pages it affects so that you can fix it.</p>
<h3 id="set-up-application-health-monitoring">Set Up application health monitoring</h3>
<p>Most modern websites use various applications to run all the aspects of their site. These applications need to work in tandem for every part of your site to work. Application health monitoring notifies you of issues within an application and with the integration of each application so that it can work cohesively.</p>
<h3 id="create-and-maintain-a-status-page">Create and maintain a status page</h3>
<p>When downtime occurs, communication is critical. You want your users to know that your website will be coming back up and, if you can, give them a timeframe of when they can expect your site to be back up. So, create and maintain a status page with all relevant information you can display during downtime.</p>
<h2 id="monitoring-your-website">Monitoring your website <a href="#monitoring-your-website" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#monitoring-your-website">#</a></h2>
<p>It can sound overwhelming to manage all this stuff just to prevent downtime and fix issues before they impact your users. But guess what, modern website monitoring solutions—like <a href="https://ohdear.app/register">Oh Dear</a> of course—can manage all of the above with minimal effort from your end. They offer you a single dashboard to track all relevant aspects of your website from your performance and uptime to your SSL certificate and DNS records. You can try OhDear for free for 30 days to see how website monitoring can help you better manage your sites.</p>
<h2 id="faqs-about-website-downtime">FAQs about website downtime <a href="#faqs-about-website-downtime" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#faqs-about-website-downtime">#</a></h2>
<p><strong>How can I quickly respond to website downtime?</strong>
Website monitoring is the best way to get notified of website downtime immediately so that you can respond to it as quickly as possible.</p>
<p><strong>Should I notify users about downtime?</strong>
Yes, proactive communication is the best way to minimize the impacts of downtime to your end user. Be open about the cause of the downtime, how long you expect it to impact users, and the extent of the impact.</p>
<p><strong>How do I diagnose the cause of website downtime?</strong>
You can start your diagnosis with the monitoring tool that notified you of the downtime. It should tell you the general cause of the error based on the HTTP status code the monitoring service received to identify the downtime. From there, you need to systematically test ever aspect related to the general cause until you find the point of failure.</p>
<p><strong>What should I include in my website downtime status page?</strong>
When displaying a status page notifying users that your website is currently down, we recommend including information related to when they can expect your website to come back up as well as a sign-up form that allows users to receive a notification when your website is back up.</p>
]]>
            </summary>
                                    <updated>2025-05-09T06:21:48+00:00</updated>
        </entry>
            <entry>
            <title><![CDATA[You can now log in using passkeys]]></title>
            <link rel="alternate" href="https://ohdear.app/news-and-updates/you-can-now-log-in-using-passkeys" />
            <id>https://ohdear.app/136</id>
            <author>
                <name><![CDATA[Oh Dear]]></name>
            </author>
            <summary type="html">
                <![CDATA[<p>We’ve added a new option to log in to Oh Dear: passkeys!</p>
<p>When logging in using a passkey, you don’t have to type an email or password, and you won’t be redirect to a third party for logging in. It’s also super fast.</p>
<h2 id="how-to-use-passkeys-at-oh-dear">How to use passkeys at Oh Dear <a href="#how-to-use-passkeys-at-oh-dear" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#how-to-use-passkeys-at-oh-dear">#</a></h2>
<p>Here’s me logging in to Oh Dear using a passkey.</p>
<video width="640" height="360" controls>
  <source src="https://ohdear.app/media/videos/passkey-login.mp4" type="video/mp4">
  Your browser does not support the video tag.
</video>
<p>Cool right?</p>
<p>If you want to use a passkey to login, just head over to <a href="https://ohdear.app/user/profile">your profile page</a> and scroll down to the passkeys section. There you can create a new passkey.</p>
<p><img src="/media/blog/8ttfdBYlLRuNp2mawuhxCu2S0EsWIhLe9oUndlaS.png" alt="" /></p>
<p>Please note that you need a modern browser and OS to use passkeys. For MacOS, the password app will be used to store your passkey. If you have 1Pass installed, it will offer to safe your passkey.</p>
<p>With the passkey stored in your password app of your choice, you can now use it. Just head over to the login page and click “Login with passkey” to login.</p>
<p>Easy peasy!</p>
<h2 id="learning-more">Learning more <a href="#learning-more" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#learning-more">#</a></h2>
<p>If you want to know more about passkeys, head over to the documentation of the <a href="https://spatie.be/docs/laravel-passkeys">spatie/laravel-passkeys</a> package, which we are using behind to scenes.</p>
]]>
            </summary>
                                    <updated>2025-05-06T18:49:17+00:00</updated>
        </entry>
            <entry>
            <title><![CDATA[Targeted snoozes with full history]]></title>
            <link rel="alternate" href="https://ohdear.app/news-and-updates/targeted-snoozes-with-full-history" />
            <id>https://ohdear.app/135</id>
            <author>
                <name><![CDATA[Oh Dear]]></name>
            </author>
            <summary type="html">
                <![CDATA[<p>No one likes to admit it but we all hit snooze on the morning alarm every now and then. The same goes for Oh Dear alerts - sometimes you know that link deep in the docs will get fixed <em>eventually</em> but right now you're busy working on something else. Getting reminded every hour isn’t always helpful.</p>
<p>Since April 2020, Oh Dear has allowed you to temporarily silence alerts for any check. Whether you’re managing a known outage or fighting some accessibility issues that is causing your Lighthouse score to drop, snoozing has given you breathing room across all notification channels at the site, team and tag level.</p>
<p>Today, we’re giving snoozing some more visibility.</p>
<p><img src="/media/blog/AefDzxDJL3k3UTyzokv8ynbJI5J1UHuEWz9jp26C.png" alt="" /></p>
<h2 id="introducing-snooze-history">Introducing Snooze History <a href="#introducing-snooze-history" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#introducing-snooze-history">#</a></h2>
<p>Ever wondered <em>“did I already snooze this?”</em> or <em>“who muted alerts last week?”</em></p>
<p>With <strong>Snooze History</strong> you can now view a complete log of snoozes for every check and every site with some additional information:</p>
<ul>
<li><strong>When</strong> the check was snoozed</li>
<li><strong>Who</strong> snoozed it</li>
<li><strong>How</strong> it was snoozed (UI, Slack, Email, API)</li>
<li><strong>Duration</strong> of the snooze</li>
<li><strong>Which check or scope</strong> it applied to</li>
</ul>
<p>This audit trail helps your team stay informed especially when multiple people manage the same infrastructure. No more expected alerts disappearing!</p>
<p>You’ll find this at the bottom of the <strong>Snoozes</strong> tab on each check and in your site menu.</p>
<p><img src="/media/blog/9CU5ogmm4I2DTmuJcHq3dEGS14YYJBU69LSBiw3V.png" alt="" /></p>
<h2 id="snooze-specific-check-types">Snooze Specific Check Types <a href="#snooze-specific-check-types" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#snooze-specific-check-types">#</a></h2>
<p>Not all checks are created equal. Sometimes one failing cron job can be ignored but you still want to know if the others stop running. This has been a requested feature from our customers for a while now and we are pleased to announce that you can now snooze <strong>individual scheduled task and application health check items</strong>.</p>
<ul>
<li>Snooze a <strong>specific scheduled task</strong> without muting all defined jobs.</li>
<li>Snooze one <strong>application health item</strong>, like a disk space warning, while keeping CPU and memory alerts active. But maybe you shouldn't ignore that disk space warning...just saying.</li>
</ul>
<p><img src="/media/blog/Um7HYfCddqQ1ZJQLcWfbgdpRnJ7RGPZQUuIzMQ2N.png" alt="" /></p>
<h2 id="the-only-time-snoozing-helps-devops">The Only Time Snoozing Helps DevOps <a href="#the-only-time-snoozing-helps-devops" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#the-only-time-snoozing-helps-devops">#</a></h2>
<p>Alert fatigue is real. We hope these new features help you mute what is necessary and keep your team in sync. Clear visibility into snooze actions helps reduce the risk of missing critical issues and tracking when and why.</p>
<p>Both available today for all subscribers.</p>
]]>
            </summary>
                                    <updated>2025-05-12T08:00:21+00:00</updated>
        </entry>
            <entry>
            <title><![CDATA[How does website monitoring even work?]]></title>
            <link rel="alternate" href="https://ohdear.app/news-and-updates/how-does-website-monitoring-even-work" />
            <id>https://ohdear.app/134</id>
            <author>
                <name><![CDATA[Oh Dear]]></name>
            </author>
            <summary type="html">
                <![CDATA[<p>Every website manager knows that feeling when you look at your inbox only to find a customer notifying you that a core page of your site is down. The worst part of it all, you don’t know how long that page has been down for. If you’ve yet to experience that, count your blessings. Well, unless you decide to opt for a website monitoring solution before it happens to you.</p>
<p>With website monitoring, you can ensure every page on your site is up and running at all times. And if any of them go down, you’ll be the first person to know. That way, you can fix the problem before it impacts any of your customers.</p>
<h2 id="the-technical-process-of-website-monitoring">The technical process of website monitoring <a href="#the-technical-process-of-website-monitoring" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#the-technical-process-of-website-monitoring">#</a></h2>
<p>Obviously, website monitoring monitors your website. But, how does that process actually work? It’s simpler than it sounds. Basically, the software uses an automated system to regularly check every page on your website and ensure an end-user could access them.</p>
<h3 id="the-request-response-cycle">The request-response cycle</h3>
<p>Let’s dig into the details of how that automated system works. It continuously sends your website server HTTP requests triggering the server to send an HTTP response back. That HTTP response includes a status code telling the monitoring service the current status of the page. Those HTTP status codes include:</p>
<ul>
<li><strong>HTTP 2xx:</strong> A response with a HTTP status code in the 200s is the expected response. It means the page is up and running.</li>
<li><strong>HTTP 3xx:</strong> This means the page is redirecting traffic to a different page. This could be expected, but not always.</li>
<li><strong>HTTP 4xx:</strong> When there’s a problem with the code in one of your pages that prevents it from being displayed.</li>
<li><strong>HTTP 5xx:</strong> When your server cannot fulfil an HTTP request, like an issue with your website host or servers.</li>
</ul>
<p>Receiving the status code for every page on your site is the basics of website monitoring. These services can do a lot more, though. For instance, they can also track how long it takes to fulfil a request so you can monitor the performance of your website.</p>
<p>That way, you can tell if a large uptick in traffic slows down your website which could mean you need to invest in faster server infrastructure.</p>
<h2 id="types-of-website-monitoring">Types of website monitoring <a href="#types-of-website-monitoring" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#types-of-website-monitoring">#</a></h2>
<p>The specialty tasks website monitoring can perform, like the performance testing we just mentioned, depends on the type of website monitoring provided. Here are the main types of website monitoring you should look for when signing up for a website monitoring service:</p>
<h3 id="uptime-monitoring">Uptime monitoring</h3>
<p>This is the main type of website monitoring. It uses the process we discussed earlier with making an HTTP request and then analyzing the response code. However, more advanced uptime monitoring doesn’t just check that your website is up, it can also check for specific features in your website. Other services allow you to check that your website is up in specific regions or globally by making requests from localized servers.</p>
<h3 id="ssl-certificate-monitoring">SSL certificate monitoring</h3>
<p>We’ve all gone to a site and saw the “your connection is not private” warning. That’s what happens when a site has an SSL certificate error. And just like you, that warning turns traffic away immediately. So, while receiving an HTTP response code, quality monitoring services also check the status of your SSL certificate and notify you of any errors. That way, you can fix any issues before your visitors notice them.</p>
<h3 id="broken-link-detection">Broken link detection</h3>
<p>Every website uses internal links and if you don’t, you should. But when you delete a page, it’s hard to find every single time you linked to that page on other pages. Or, if you change the URL of a page, finding every instance of a link to that URL can be a challenge. Broken link detection finds these issues for you while scouring your website. The service goes through each link on the page and makes sure it returns a valid HTTP response code as well.</p>
<h3 id="mixed-content-warnings">Mixed content warnings</h3>
<p>Just like an error with your SSL certificate can send your visitors a warning, mixed content on your pages does that too. That’s when your page has an SSL certificate and uses HTTPS, but content within your page uses insecure HTTP resources. This can cause your visitors to go running just like with an SSL certificate error. Monitoring this requires your website monitoring service to load all resources on the page and assess each one’s security status.</p>
<h3 id="performance-monitoring">Performance monitoring</h3>
<p>It’s easy to tell how quickly a website responds, but the information performance monitoring services can tell you a lot more than just how long it takes for a page to load from the time you enter the URL. It breaks down your page loading time into separate categories so you can identify the bottlenecks impeding your website’s performance.</p>
<h3 id="domain-monitoring">Domain monitoring</h3>
<p>There’s a lot that can go wrong with a domain. Whether it’s about to expire or someone fraudulently updated the WHOIS records, keeping tabs on your domain is just as important as keeping tabs on your website’s performance. That’s why most website monitoring services monitor your domain as well. They look for unexpected activity, warn you when your domain is about to expire, and verify all of your email-related records.</p>
<h2 id="what-kind-of-website-monitoring-do-you-need">What kind of website monitoring do you need? <a href="#what-kind-of-website-monitoring-do-you-need" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#what-kind-of-website-monitoring-do-you-need">#</a></h2>
<p>Clearly there's no shortage in different types of website monitoring, but in practice you usually don’t need to pick and choose. Tools like UptimeRobot, StatusCake, and <a href="https://ohdear.app/register">Oh Dear</a> are considered &quot;all-in-one&quot; monitoring solutions. All types of monitoring are included, coupled with a robust notification system. What's also cool and totally not self-promotional, is that Oh Dear offers unlimited status pages, Lighthouse SEO monitoring and broken links and content detection as well! I know what I'd choose!</p>
<p><img src="/media/blog/CPgVriz7TRT55j76AV8GyXLfqka5qIZtHWMkZmnD.png" alt="A banner promoting Oh Dear as an all-in-one website monitoring tool" /></p>
]]>
            </summary>
                                    <updated>2025-04-28T11:11:48+00:00</updated>
        </entry>
            <entry>
            <title><![CDATA[Lifespan of TLS certificates is getting reduced to 47 days]]></title>
            <link rel="alternate" href="https://ohdear.app/news-and-updates/lifespan-of-tls-certificates-is-getting-reduced-to-47-days" />
            <id>https://ohdear.app/133</id>
            <author>
                <name><![CDATA[Oh Dear]]></name>
            </author>
            <summary type="html">
                <![CDATA[<p>In a pretty significant shift for internet security and subsequently certificate management, the CA/Browser Forum has officially voted to reduce the maximum validity period of TLS certificates to just 47 days by March 15, 2029.</p>
<p>This move aims to enhance digital security and trust across the web. But as these changes approach, it'll become increasingly crucial for organizations to understand their implications and prepare accordingly. Automation will likely become mandatory.</p>
<h2 id="whats-the-deal-with-tls-certificates">What's the deal with TLS certificates? <a href="#whats-the-deal-with-tls-certificates" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#whats-the-deal-with-tls-certificates">#</a></h2>
<h3 id="what-are-they">What are they?</h3>
<p>TLS (Transport Layer Security) certificates—more commonly known as SSL certificates—are digital certificates that authenticate the identity of a website and enable encrypted connections like HTTPS.</p>
<h3 id="why-do-i-need-one">Why do I need one?</h3>
<p>Well, they are essential for securing data transmitted between a user's browser and a web server, ensuring privacy and data integrity. This is pretty important, as SSL certificates are now a requirement for all websites, as mandated by Google and Mozilla. In a nutshell, they enable:</p>
<ul>
<li><strong>Data Encryption:</strong> Protecting sensitive information from eavesdroppers.</li>
<li><strong>Authentication:</strong> Verifying that a website is legitimate.</li>
<li><strong>Trust:</strong> Your browser will mark all HTTP pages as &quot;Not Secure&quot;.</li>
</ul>
<h2 id="what-are-the-upcoming-changes-to-certificate-lifetimes">What are the upcoming changes to certificate lifetimes? <a href="#what-are-the-upcoming-changes-to-certificate-lifetimes" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#what-are-the-upcoming-changes-to-certificate-lifetimes">#</a></h2>
<p>To keep a short story even shorter, the maximum certificate lifetime is going down pretty drastically. The idea is, by reducing the lifetimes, you need more frequent validation ergo potentially compromised certificates won't stick around for too long. Here's the rundown.</p>
<ul>
<li>Until March 15, 2026, the maximum lifetime for a TLS certificate is 398 days.</li>
<li>March 15, 2026, the maximum lifetime for a TLS certificate will be 200 days.</li>
<li>March 15, 2027, the maximum lifetime for a TLS certificate will be 100 days.</li>
<li>March 15, 2029, the maximum lifetime for a TLS certificate will be 47 days.</li>
</ul>
<p>Some additional clarification, though. As of March 15, 2029, the maximum lifetime for a TLS certificate will indeed be 47 days, but the maximum period during which domain validation information may be reused is <strong>only 10 days</strong>. That's pretty short. Manual revalidation will still be possible, but this kind of sets you up for trouble if you're not diligent.</p>
<h2 id="what-does-this-mean-for-my-business-and-websites">What does this mean for my business and website(s)? <a href="#what-does-this-mean-for-my-business-and-websites" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#what-does-this-mean-for-my-business-and-websites">#</a></h2>
<p>The proposed changes are put there to improve overall security on the web. However, that is assuming everyone handles their certificate renewals precisely on schedule. While shorter lifetimes do improve security, they also increase the risk of certificate expiration without you realized it. Failing to renew your TLS/SSL certificate means browsers will immediately block access to your site.</p>
<p>We probably don't have to paint this picture, but if your site becomes inaccessible because of an expired TLS certificate, this can lead to:</p>
<ul>
<li>Lost revenue</li>
<li>Damaged brand trust</li>
<li>Search engine ranking drops</li>
<li>Internal panic and/or angry clients</li>
</ul>
<p>As a side note, according to the 2023 EMA report, 25% of certificates pose a security threat because they're either expired or self-signed. As lifetimes shrink, that number will go up considerably if counter measures aren't taken.</p>
<h2 id="how-do-i-prepare-for-these-changes">How do I prepare for these changes? <a href="#how-do-i-prepare-for-these-changes" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#how-do-i-prepare-for-these-changes">#</a></h2>
<p>The good news is that it's actually pretty easy to avoid certificates from expiring. There's plenty of time left and solutions are already readily available:</p>
<ul>
<li>The most obvious solution would be to <strong>automate TLS certificate renewals</strong>. Using services like Let’s Encrypt, cloud providers with automated certificate management, or something like CertBot you can deal with these shorter lifespans without breaking much of a sweat.</li>
<li>Another solution is to <strong>simply monitor your websites</strong>, which is just good practice either way. A website monitoring tool (like Oh Dear, obviously) can notify you in time of not only expired certificates, but of broken links, tasks, drops in performance, SEO...</li>
<li>Lastly, it's never a bad idea to <strong>keep your team informed</strong>. Make sure your developers, DevOps engineers, and sysadmins understand the timeline and implications of these changes.</li>
</ul>
<p><img src="/media/blog/HNNJsJujDfFDEcLQP8DeEnN1L0daDgdd5ilA85ZV.png" alt="" /></p>
<h2 id="closing-thoughts">Closing thoughts <a href="#closing-thoughts" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#closing-thoughts">#</a></h2>
<p>In a nutshell, the ballot argues that shorter lifetimes are necessary because the information contained in these certificates becomes less trustworthy over time, which can only be mitigated by revalidating said information more frequently.</p>
<p>DigiCert, the certificate authority where we got this information from, expects rapid adoption of automated certificate renewal well before the 2029 changes. This is the most likely course of events. But we wouldn't be Oh Dear if we didn't tell you to also use website monitoring :)</p>
]]>
            </summary>
                                    <updated>2025-04-23T14:26:13+00:00</updated>
        </entry>
            <entry>
            <title><![CDATA[Running our test suite in parallel on GitHub actions]]></title>
            <link rel="alternate" href="https://ohdear.app/news-and-updates/running-our-test-suite-in-parallel-on-github-actions" />
            <id>https://ohdear.app/132</id>
            <author>
                <name><![CDATA[Oh Dear]]></name>
            </author>
            <summary type="html">
                <![CDATA[<p>A couple of years ago, Laravel introduced a great feature which allows to run PHPUnit / Pest tests in parallel. This results in a big boost in performance.</p>
<p>By default, it determines the concurrency level by taking a look at the number of CPU cores your machine has. So, if you're using a modern Mac that has 10 CPU cores, it will run 10 tests at the same time, greatly cutting down on the time your testsuite needs to run completely.</p>
<p>A default runner on GitHub doesn't have that many cores, so you can't leverage parallel testing as good as in your typical local environments.</p>
<p>In this blog post, I'd like to show you a way of running your tests on GitHub, by splitting them up in small chunks that can run concurrently.vWe use this technique to cut down the running time of our vast testsuite from 16 minutes to only just 4. In this blog post all examples will come from our base.</p>
<!--more-->
<h2 id="what-were-going-to-do">What we're going to do <a href="#what-were-going-to-do" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#what-were-going-to-do">#</a></h2>
<p>Like already mentioned in the introduction, a typical test runner on GitHub hasn't got a lot of cores, meaning the default way of running tests in parallel that Laravel offers (running a test per CPU core) doesn't work well.</p>
<p>What you can do at GitHub however is running a lot of GitHub actions jobs in parallel. So what we are going to do is splitting our testsuite in equal chunks, and create a test job on GitHub action per chunk. These chunks can run in parallel, which will immensely decrease the total running time.</p>
<p>I'll tell you how to achieve this technically in the remainder of this blog post, but here's already how the end result will look like on GitHub.</p>
<p><img src="/media/blog/ahsGwZzeKjPu9lOG4L2ubjF8xAVOrmfDBWekhzkF.png" alt="" /></p>
<p>In the screenshot above you can see that our test suite is split up in 12 parts which will all run concurrently. Composer / NPM will only run once to build up the dependencies and assets and they will be used in all 12 testing parts.</p>
<h2 id="splitting-up-the-testsuite-in-equal-parts">Splitting up the testsuite in equal parts <a href="#splitting-up-the-testsuite-in-equal-parts" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#splitting-up-the-testsuite-in-equal-parts">#</a></h2>
<p>Let's first take a look at how we can split the testsuite in equal parts. We use Pest as a test runner, which offers a <code>--list-tests</code> option to output all tests.</p>
<p><img src="/media/blog/fCyxyPAg8N2HABEnL3io7DTLTpmruuTs9gnMvxWR.png" alt="" /></p>
<p>Here's a bit of code to get all test class names from that output.</p>
<pre data-lang="php" class="notranslate"> <span class="hl-variable">$process</span> = <span class="hl-keyword">new</span> <span class="hl-type">Process</span>([<span class="hl-property">__DIR__</span> . <span class="hl-value">'/../vendor/bin/pest'</span>, <span class="hl-value">'--list-tests'</span>]);

<span class="hl-variable">$process</span>-&gt;<span class="hl-property">mustRun</span>();

<span class="hl-variable">$index</span> = <span class="hl-variable">$shardNumber</span> - 1;

<span class="hl-variable">$allTestNames</span> = <span class="hl-type">Str</span>::<span class="hl-property">of</span>(<span class="hl-variable">$process</span>-&gt;<span class="hl-property">getOutput</span>())
    -&gt;<span class="hl-property">explode</span>(<span class="hl-value">&quot;\n&quot;</span>)
    -&gt;<span class="hl-property">filter</span>(<span class="hl-keyword">fn</span>(<span class="hl-injection"><span class="hl-type">string</span> $line</span>) =&gt; <span class="hl-property">str_contains</span>(<span class="hl-variable">$line</span>, <span class="hl-value">' - '</span>))
    -&gt;<span class="hl-property">map</span>(<span class="hl-keyword">function</span> (<span class="hl-injection"><span class="hl-type">string</span> $fullTestName</span>) {
        <span class="hl-variable">$testClassName</span> = <span class="hl-type">Str</span>::<span class="hl-property">of</span>(<span class="hl-variable">$fullTestName</span>)
            -&gt;<span class="hl-property">replace</span>(<span class="hl-value">'- '</span>, <span class="hl-value">''</span>)
            -&gt;<span class="hl-property">trim</span>()
            -&gt;<span class="hl-property">between</span>(<span class="hl-value">'\\\\', '</span>::')
            -&gt;<span class="hl-property">afterLast</span>(<span class="hl-value">'\\'</span>)
            -&gt;<span class="hl-property">toString</span>();

        <span class="hl-keyword">return</span> <span class="hl-variable">$testClassName</span>;
    })
    -&gt;<span class="hl-property">filter</span>()
    -&gt;<span class="hl-property">unique</span>();
</pre>
<p>In <code>$allTestNames</code> will be a collection containing all class names (= file names) that are inside the test suite.</p>
<p>To split the collection up in multiple parts, you can use the <code>split</code> function which accepts the number of parts you want. Here's how you would split up the tests in 12 parts, and get the first part</p>
<pre data-lang="php" class="notranslate"><span class="hl-variable">$testNamesOfFirstPart</span> = <span class="hl-variable">$allTestNames</span>
   -&gt;<span class="hl-property">split</span>(12) <span class="hl-comment">// split the collection in 12 equal parts</span>
   -&gt;<span class="hl-property">get</span>(<span class="hl-property">key</span>: 0) <span class="hl-comment">// get the first part (the index is 0 based)</span>
</pre>
<p>PHPUnit / Pest also offers a <code>--filter</code> option to only run specific tests. If you only want to run the tests in from the <code>ArchTest</code> class (which is displayed in the screenshot above), you could execute this.</p>
<pre data-lang="bash" class="notranslate"><span class="hl-comment"># Will only run the tests from the ArchTest file</span>
vendor/bin/pest <span class="hl-attribute">--filter</span>=ArchTest
</pre>
<p>You can use <code>|</code> to specify multiple patterns. Here's how you could execute the tests from multiple files</p>
<pre data-lang="bash" class="notranslate"><span class="hl-comment"># Will only run the tests from the ArchTest file</span>
vendor/bin/pest <span class="hl-attribute">--filter</span>=ArchTest|CheckSitesBeingMonitoredTest
</pre>
<p>Here's how you could use the <code>$testNamesOfFirstPart</code> from the previous snippet to run the first part of the tests programmatically.</p>
<pre data-lang="php" class="notranslate"><span class="hl-variable">$process</span> = <span class="hl-keyword">new</span> <span class="hl-type">Process</span>(
    <span class="hl-property">command</span>: [
       <span class="hl-value">'./vendor/bin/pest'</span>,
       <span class="hl-value">'--filter'</span>, 
       <span class="hl-variable">$testNamesOfFirstPart</span>-&gt;<span class="hl-property">join</span>(<span class="hl-value">'|'</span>)],
       <span class="hl-property">timeout</span>: <span class="hl-keyword">null</span> <span class="hl-comment">// take as much time as we need</span>
);

<span class="hl-variable">$process</span>-&gt;<span class="hl-property">start</span>();

<span class="hl-comment">/* pipe the Pest output to the console */</span>
<span class="hl-keyword">foreach</span> (<span class="hl-variable">$process</span> <span class="hl-keyword">as</span> <span class="hl-variable">$data</span>) {
    <span class="hl-keyword">echo</span> <span class="hl-variable">$data</span>;
}

<span class="hl-variable">$process</span>-&gt;<span class="hl-property">wait</span>();

<span class="hl-comment">// use the exit code of Pest as the exit code of the script</span>
<span class="hl-keyword">exit</span>(<span class="hl-variable">$process</span>-&gt;<span class="hl-property">getExitCode</span>());
</pre>
<h2 id="running-the-testsuite-parts-in-parallel-at-github">Running the testsuite parts in parallel at GitHub <a href="#running-the-testsuite-parts-in-parallel-at-github" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#running-the-testsuite-parts-in-parallel-at-github">#</a></h2>
<p>Now that you know how you could split up a test suite in equal parts, let's take a look at how we can run all these parts in parallel on GitHub actions.</p>
<p>GitHub actions support <a href="https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/running-variations-of-jobs-in-a-workflow">a <code>matrix</code> parameter</a>. Shortly said, this matrix parameter is used for testing variations of your test suite, and it will run those variations concurrently.</p>
<p>Here's the part of the Oh Dear GitHub workflow where the matrix is being set up. I've omitted several parts for brevity.</p>
<pre data-lang="yaml" class="notranslate"><span class="hl-comment"># .github/workflows/run-tests.yml</span>

<span class="hl-keyword">name</span><span class="hl-property">:</span> Run tests

<span class="hl-keyword">jobs</span><span class="hl-property">:</span>
    <span class="hl-keyword">run-tests</span><span class="hl-property">:</span>
        <span class="hl-keyword">name</span><span class="hl-property">:</span> Run Tests (Part $<span class="hl-value">{{</span><span class="hl-property"> matrix.shard_number </span><span class="hl-value">}}</span>/$<span class="hl-value">{{</span><span class="hl-property"> matrix.total_shard_count </span><span class="hl-value">}}</span>)
        <span class="hl-keyword">runs-on</span><span class="hl-property">:</span> ubuntu-latest
        <span class="hl-keyword">strategy</span><span class="hl-property">:</span>
            <span class="hl-keyword">matrix</span><span class="hl-property">:</span>
                <span class="hl-keyword">total_shard_count</span><span class="hl-property">:</span> <span class="hl-property">[</span>12<span class="hl-property">]</span>
                <span class="hl-keyword">shard_number</span><span class="hl-property">:</span> <span class="hl-property">[</span>'<span class="hl-value">01</span>', '<span class="hl-value">02</span>', '<span class="hl-value">03</span>', '<span class="hl-value">04</span>', '<span class="hl-value">05</span>', '<span class="hl-value">06</span>', '<span class="hl-value">07</span>', '<span class="hl-value">08</span>', '<span class="hl-value">09</span>', '<span class="hl-value">10</span>', '<span class="hl-value">11</span>', '<span class="hl-value">12</span>'<span class="hl-property">]</span>

        <span class="hl-keyword">steps</span><span class="hl-property">:</span>
           <span class="hl-comment">## </span>
           <span class="hl-comment">## multiple set up steps omitted for brevity</span>
           <span class="hl-comment">##</span>
            <span class="hl-property">-</span>   <span class="hl-keyword">name</span><span class="hl-property">:</span> Run tests
                <span class="hl-keyword">run</span><span class="hl-property">:</span> ./vendor/bin/pest
</pre>
<p>The matrix will create jobs per combination in the matrix. So it will run 1 (only one element in <code>total_shard_count</code>) * 12 (twelve elements in <code>shard_number</code>) = 12 times.</p>
<p>In the <code>Run tests</code> step, <code>pest is executed</code>. This will result in the whole testsuite being executed 12 times. Of course we don't want to execute the whole test suite 12 times, but only each separate 1/12 part of the testsuite.</p>
<p>We can achieve this by not running <code>/vendor/bin/pest</code> but a custom PHP script called <code>github_parallel_test_runner</code> that will receive the <code>total_shard_count</code> and the  <code>shard_number</code> as environment variables.</p>
<pre data-lang="yaml" class="notranslate"><span class="hl-comment"># .github/workflows/run-tests.yml</span>

<span class="hl-keyword">name</span><span class="hl-property">:</span> Run tests

<span class="hl-keyword">jobs</span><span class="hl-property">:</span>
    <span class="hl-keyword">run-tests</span><span class="hl-property">:</span>
        <span class="hl-keyword">name</span><span class="hl-property">:</span> Run Tests (Part $<span class="hl-value">{{</span><span class="hl-property"> matrix.shard_number </span><span class="hl-value">}}</span>/$<span class="hl-value">{{</span><span class="hl-property"> matrix.total_shard_count </span><span class="hl-value">}}</span>)
        <span class="hl-keyword">runs-on</span><span class="hl-property">:</span> ubuntu-latest
        <span class="hl-keyword">strategy</span><span class="hl-property">:</span>
            <span class="hl-keyword">matrix</span><span class="hl-property">:</span>
                <span class="hl-keyword">total_shard_count</span><span class="hl-property">:</span> <span class="hl-property">[</span>12<span class="hl-property">]</span>
                <span class="hl-keyword">shard_number</span><span class="hl-property">:</span> <span class="hl-property">[</span>'<span class="hl-value">01</span>', '<span class="hl-value">02</span>', '<span class="hl-value">03</span>', '<span class="hl-value">04</span>', '<span class="hl-value">05</span>', '<span class="hl-value">06</span>', '<span class="hl-value">07</span>', '<span class="hl-value">08</span>', '<span class="hl-value">09</span>', '<span class="hl-value">10</span>', '<span class="hl-value">11</span>', '<span class="hl-value">12</span>'<span class="hl-property">]</span>

        <span class="hl-keyword">steps</span><span class="hl-property">:</span>
           <span class="hl-comment">## </span>
           <span class="hl-comment">## multiple set up steps omitted for brevity</span>
           <span class="hl-comment">##</span>
            <span class="hl-property">-</span>   <span class="hl-keyword">name</span><span class="hl-property">:</span> Run tests
                <span class="hl-keyword">run</span><span class="hl-property">:</span> ./bin/github_parallel_test_runner
                <span class="hl-keyword">env</span><span class="hl-property">:</span>
                    <span class="hl-keyword">TOTAL_SHARD_COUNT</span><span class="hl-property">:</span> $<span class="hl-value">{{</span><span class="hl-property"> matrix.total_shard_count </span><span class="hl-value">}}</span>
                    <span class="hl-keyword">SHARD_NUMBER</span><span class="hl-property">:</span> $<span class="hl-value">{{</span><span class="hl-property"> matrix.shard_number </span><span class="hl-value">}}</span>
</pre>
<p>Here's the content of <code>./bin/github_parallel_test_runner</code> in our code base. It will read the environment variables, execute Pest using the <code>--list-files</code> and <code>--filter</code> flags to only run a part of the tests like explained in the previous section of this post.</p>
<pre data-lang="php" class="notranslate"><span class="hl-comment">#!/usr/bin/env php</span>
<span class="hl-keyword">&lt;?php</span>

<span class="hl-keyword">use</span> <span class="hl-type">Illuminate\Support\Collection</span>;
<span class="hl-keyword">use</span> <span class="hl-type">Illuminate\Support\Str</span>;
<span class="hl-keyword">use</span> <span class="hl-type">Symfony\Component\Process\Process</span>;

<span class="hl-keyword">require_once</span> <span class="hl-value">'vendor/autoload.php'</span>;

<span class="hl-variable">$shardNumber</span> = (<span class="hl-type">int</span>)<span class="hl-property">getenv</span>(<span class="hl-value">'SHARD_NUMBER'</span>);
<span class="hl-variable">$totalShardCount</span> = (<span class="hl-type">int</span>)<span class="hl-property">getenv</span>(<span class="hl-value">'TOTAL_SHARD_COUNT'</span>);

<span class="hl-keyword">if</span> (<span class="hl-variable">$shardNumber</span> === 0 <span class="hl-operator">||</span> <span class="hl-variable">$totalShardCount</span> === 0) {
    <span class="hl-keyword">echo</span> <span class="hl-value">&quot;SHARD_NUMBER and TOTAL_SHARD_COUNT must be set.&quot;</span> . <span class="hl-property">PHP_EOL</span>;
    <span class="hl-keyword">exit</span>(1);
}

<span class="hl-keyword">new</span> <span class="hl-type">ParallelTests</span>(<span class="hl-variable">$totalShardCount</span>)-&gt;<span class="hl-property">run</span>(<span class="hl-variable">$shardNumber</span>);

<span class="hl-keyword">class</span> <span class="hl-type">ParallelTests</span>
{
    <span class="hl-keyword">public</span> <span class="hl-keyword">function</span> <span class="hl-property">__construct</span>(<span class="hl-injection">
        <span class="hl-keyword">protected</span> <span class="hl-type">int</span> <span class="hl-property">$totalShardCount</span>,
    </span>)
    {
    }

    <span class="hl-keyword">public</span> <span class="hl-keyword">function</span> <span class="hl-property">run</span>(<span class="hl-injection"><span class="hl-type">int</span> $shardNumber</span>): <span class="hl-type">never</span>
    {
        <span class="hl-variable">$testNames</span> = <span class="hl-variable">$this</span>-&gt;<span class="hl-property">getTestNames</span>(<span class="hl-variable">$shardNumber</span>);

        <span class="hl-keyword">echo</span> <span class="hl-value">&quot;Running {$testNames-&gt;count()} tests on node {$shardNumber} of {$this-&gt;totalShardCount}...&quot;</span> . <span class="hl-property">PHP_EOL</span>;

        <span class="hl-variable">$exitCode</span> = <span class="hl-variable">$this</span>-&gt;<span class="hl-property">runPestTests</span>(<span class="hl-variable">$testNames</span>);

        <span class="hl-keyword">exit</span>(<span class="hl-variable">$exitCode</span>);
    }

    <span class="hl-comment">/** <span class="hl-value">@return</span> <span class="hl-type">Collection&lt;<span class="hl-generic">string</span>&gt; </span>*/</span>
    <span class="hl-keyword">protected</span> <span class="hl-keyword">function</span> <span class="hl-property">getTestNames</span>(<span class="hl-injection"><span class="hl-type">int</span> $shardNumber</span>): <span class="hl-type">Collection</span>
    {
        <span class="hl-variable">$process</span> = <span class="hl-keyword">new</span> <span class="hl-type">Process</span>([<span class="hl-property">__DIR__</span> . <span class="hl-value">'/../vendor/bin/pest'</span>, <span class="hl-value">'--list-tests'</span>]);

        <span class="hl-variable">$process</span>-&gt;<span class="hl-property">mustRun</span>();

        <span class="hl-variable">$index</span> = <span class="hl-variable">$shardNumber</span> - 1;

        <span class="hl-variable">$allTestNames</span> = <span class="hl-type">Str</span>::<span class="hl-property">of</span>(<span class="hl-variable">$process</span>-&gt;<span class="hl-property">getOutput</span>())
            -&gt;<span class="hl-property">explode</span>(<span class="hl-value">&quot;\n&quot;</span>)
            -&gt;<span class="hl-property">filter</span>(<span class="hl-keyword">fn</span>(<span class="hl-injection"><span class="hl-type">string</span> $line</span>) =&gt; <span class="hl-property">str_contains</span>(<span class="hl-variable">$line</span>, <span class="hl-value">' - '</span>))
            -&gt;<span class="hl-property">map</span>(<span class="hl-keyword">function</span> (<span class="hl-injection"><span class="hl-type">string</span> $fullTestName</span>) {
                <span class="hl-variable">$testClassName</span> = <span class="hl-type">Str</span>::<span class="hl-property">of</span>(<span class="hl-variable">$fullTestName</span>)
                    -&gt;<span class="hl-property">replace</span>(<span class="hl-value">'- '</span>, <span class="hl-value">''</span>)
                    -&gt;<span class="hl-property">trim</span>()
                    -&gt;<span class="hl-property">between</span>(<span class="hl-value">'\\\\', '</span>::')
                    -&gt;<span class="hl-property">afterLast</span>(<span class="hl-value">'\\'</span>)
                    -&gt;<span class="hl-property">toString</span>();

                <span class="hl-keyword">return</span> <span class="hl-variable">$testClassName</span>;
            })
            -&gt;<span class="hl-property">filter</span>()
            -&gt;<span class="hl-property">unique</span>();

        <span class="hl-keyword">echo</span> <span class="hl-value">&quot;Detected {$allTestNames-&gt;count()} tests:&quot;</span> . <span class="hl-property">PHP_EOL</span>;

        <span class="hl-keyword">return</span> <span class="hl-variable">$allTestNames</span>
            -&gt;<span class="hl-property">split</span>(<span class="hl-variable">$this</span>-&gt;<span class="hl-property">totalShardCount</span>)
            -&gt;<span class="hl-property">get</span>(<span class="hl-variable">$index</span>);
    }

    <span class="hl-keyword">protected</span> <span class="hl-keyword">function</span> <span class="hl-property">runPestTests</span>(<span class="hl-injection"><span class="hl-type">Collection</span> $testNames</span>): <span class="hl-type">?int</span>
    {
        <span class="hl-variable">$process</span> = <span class="hl-keyword">new</span> <span class="hl-type">Process</span>(
            <span class="hl-property">command</span>: [<span class="hl-value">'./vendor/bin/pest'</span>, <span class="hl-value">'--filter'</span>, <span class="hl-variable">$testNames</span>-&gt;<span class="hl-property">join</span>(<span class="hl-value">'|'</span>)],
            <span class="hl-property">timeout</span>: <span class="hl-keyword">null</span>
        );

        <span class="hl-variable">$process</span>-&gt;<span class="hl-property">start</span>();

        <span class="hl-comment">/* pipe the Pest output to the console */</span>
        <span class="hl-keyword">foreach</span> (<span class="hl-variable">$process</span> <span class="hl-keyword">as</span> <span class="hl-variable">$data</span>) {
            <span class="hl-keyword">echo</span> <span class="hl-variable">$data</span>;
        }

        <span class="hl-variable">$process</span>-&gt;<span class="hl-property">wait</span>();

        <span class="hl-keyword">return</span> <span class="hl-variable">$process</span>-&gt;<span class="hl-property">getExitCode</span>();
    }
}
</pre>
<p>To make this script executable you must execute this command and push the changes permissions...</p>
<pre data-lang="bash" class="notranslate"><span class="hl-keyword">chmod</span> +x ./bin/github_parallel_test_runner
</pre>
<h2 id="only-run-composer-and-npm-yarn-once">Only run composer and NPM / Yarn once <a href="#only-run-composer-and-npm-yarn-once" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#only-run-composer-and-npm-yarn-once">#</a></h2>
<p>In the screenshot above, you could see how there's a &quot;Composer and Yarn&quot; step that is executing only once before all the test parts run. Here's that screenshot again.</p>
<p><img src="/media/blog/CNKYnLy3HB8ynxEI90sHqHsKU1B0YpkaH4zV2LZy.png" alt="" /></p>
<p>A GitHub action workflow can contain multiple jobs, and you can define dependencies between them. In the snippet below you'll see the <code>setup-dependencies</code> job being defined (I've omitted all steps regarding to Yarn / NPM to keep things brief). We save the <code>vendor</code> directory as an artifact and use that saved directory in all of our test jobs. Finally, there's also a step to clean up any created artifacts.</p>
<p>You can see that in the <code>needs</code> key, you can define the steps that a job depends on.</p>
<pre data-lang="yaml" class="notranslate">
<span class="hl-comment">#</span>
<span class="hl-comment"># name, and concurrency setup omitted for brevity</span>
<span class="hl-comment">#</span>

<span class="hl-keyword">jobs</span><span class="hl-property">:</span>
    <span class="hl-keyword">setup-dependencies</span><span class="hl-property">:</span>
        <span class="hl-keyword">name</span><span class="hl-property">:</span> Composer and Yarn
        <span class="hl-keyword">runs-on</span><span class="hl-property">:</span> ubuntu-latest
        <span class="hl-keyword">steps</span><span class="hl-property">:</span>
            <span class="hl-property">-</span>   <span class="hl-keyword">uses</span><span class="hl-property">:</span> actions/checkout@v3
                <span class="hl-keyword">with</span><span class="hl-property">:</span>
                    <span class="hl-keyword">fetch-depth</span><span class="hl-property">:</span> 1

            <span class="hl-property">-</span>   <span class="hl-keyword">name</span><span class="hl-property">:</span> Setup PHP
                <span class="hl-keyword">uses</span><span class="hl-property">:</span> shivammathur/setup-php@v2
                <span class="hl-keyword">with</span><span class="hl-property">:</span>
                    <span class="hl-keyword">php-version</span><span class="hl-property">:</span> 8.4
                    <span class="hl-keyword">extensions</span><span class="hl-property">:</span> dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, bcmath, soap, intl, gd, exif, iconv, imagick
                    <span class="hl-keyword">coverage</span><span class="hl-property">:</span> none

            <span class="hl-property">-</span>   <span class="hl-keyword">name</span><span class="hl-property">:</span> Install composer dependencies
                <span class="hl-keyword">run</span><span class="hl-property">:</span> composer install --prefer-dist --no-scripts -q -o

            <span class="hl-property">-</span>   <span class="hl-keyword">name</span><span class="hl-property">:</span> Upload vendor directory
                <span class="hl-keyword">uses</span><span class="hl-property">:</span> actions/upload-artifact@v4
                <span class="hl-keyword">with</span><span class="hl-property">:</span>
                    <span class="hl-keyword">name</span><span class="hl-property">:</span> vendor-directory-$<span class="hl-value">{{</span><span class="hl-property"> github.run_id </span><span class="hl-value">}}</span>
                    <span class="hl-keyword">path</span><span class="hl-property">:</span> vendor
                    <span class="hl-keyword">retention-days</span><span class="hl-property">:</span> 1

    <span class="hl-keyword">run-tests</span><span class="hl-property">:</span>
        <span class="hl-keyword">needs</span><span class="hl-property">:</span> <span class="hl-property">[</span> setup-dependencies<span class="hl-property">]</span>
        <span class="hl-keyword">name</span><span class="hl-property">:</span> Run Tests (Part $<span class="hl-value">{{</span><span class="hl-property"> matrix.shard_number </span><span class="hl-value">}}</span>/$<span class="hl-value">{{</span><span class="hl-property"> matrix.total_shard_count </span><span class="hl-value">}}</span>)
        <span class="hl-keyword">runs-on</span><span class="hl-property">:</span> ubuntu-latest
        <span class="hl-keyword">strategy</span><span class="hl-property">:</span>
            <span class="hl-keyword">fail-fast</span><span class="hl-property">:</span> false
            <span class="hl-keyword">matrix</span><span class="hl-property">:</span>
                <span class="hl-keyword">total_shard_count</span><span class="hl-property">:</span> <span class="hl-property">[</span>12<span class="hl-property">]</span>
                <span class="hl-keyword">shard_number</span><span class="hl-property">:</span> <span class="hl-property">[</span>'<span class="hl-value">01</span>', '<span class="hl-value">02</span>', '<span class="hl-value">03</span>', '<span class="hl-value">04</span>', '<span class="hl-value">05</span>', '<span class="hl-value">06</span>', '<span class="hl-value">07</span>', '<span class="hl-value">08</span>', '<span class="hl-value">09</span>', '<span class="hl-value">10</span>', '<span class="hl-value">11</span>', '<span class="hl-value">12</span>'<span class="hl-property">]</span>
        <span class="hl-keyword">steps</span><span class="hl-property">:</span>
		        <span class="hl-comment">#</span>
		        <span class="hl-comment"># Some setup steps omitted for brevity</span>
		        <span class="hl-comment">#</span>
        
            <span class="hl-property">-</span>   <span class="hl-keyword">uses</span><span class="hl-property">:</span> actions/checkout@v3
                <span class="hl-keyword">with</span><span class="hl-property">:</span>
                    <span class="hl-keyword">fetch-depth</span><span class="hl-property">:</span> 1

            <span class="hl-property">-</span>   <span class="hl-keyword">name</span><span class="hl-property">:</span> Download vendor directory
                <span class="hl-keyword">uses</span><span class="hl-property">:</span> actions/download-artifact@v4
                <span class="hl-keyword">with</span><span class="hl-property">:</span>
                    <span class="hl-keyword">name</span><span class="hl-property">:</span> vendor-directory-$<span class="hl-value">{{</span><span class="hl-property"> github.run_id </span><span class="hl-value">}}</span>
                    <span class="hl-keyword">path</span><span class="hl-property">:</span> vendor

            <span class="hl-property">-</span>   <span class="hl-keyword">name</span><span class="hl-property">:</span> Run tests
                <span class="hl-keyword">run</span><span class="hl-property">:</span> ./bin/github-parallel-test-runner
                <span class="hl-keyword">env</span><span class="hl-property">:</span>
                    <span class="hl-keyword">TOTAL_SHARD_COUNT</span><span class="hl-property">:</span> $<span class="hl-value">{{</span><span class="hl-property"> matrix.total_shard_count </span><span class="hl-value">}}</span>
                    <span class="hl-keyword">SHARD_NUMBER</span><span class="hl-property">:</span> $<span class="hl-value">{{</span><span class="hl-property"> matrix.shard_number </span><span class="hl-value">}}</span>

    <span class="hl-keyword">cleanup-artifacts</span><span class="hl-property">:</span>
        <span class="hl-keyword">name</span><span class="hl-property">:</span> Clean up artifacts
        <span class="hl-keyword">needs</span><span class="hl-property">:</span> <span class="hl-property">[</span>setup-dependencies, run-tests<span class="hl-property">]</span>
        <span class="hl-keyword">runs-on</span><span class="hl-property">:</span> ubuntu-latest
        <span class="hl-keyword">if</span><span class="hl-property">:</span> always()
        <span class="hl-keyword">steps</span><span class="hl-property">:</span>
            <span class="hl-property">-</span>   <span class="hl-keyword">name</span><span class="hl-property">:</span> Delete artifacts
                <span class="hl-keyword">uses</span><span class="hl-property">:</span> geekyeggo/delete-artifact@v2
                <span class="hl-keyword">with</span><span class="hl-property">:</span>
                    <span class="hl-keyword">name</span><span class="hl-property">:</span> <span class="hl-property">|</span>
                        vendor-directory-$<span class="hl-value">{{</span><span class="hl-property"> github.run_id </span><span class="hl-value">}}</span>
                    <span class="hl-keyword">failOnError</span><span class="hl-property">:</span> false
</pre>
<h2 id="cancelling-stale-tests">Cancelling stale tests <a href="#cancelling-stale-tests" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#cancelling-stale-tests">#</a></h2>
<p>There's another neat little thing that we do in our GitHub action workflow to save some time. Whenever changes are pushed to a certain branch, we're not really interested in the results of the tests of any previous commits / pushes on that branch anymore.</p>
<p>Wouldn't it be nice if the test for any older commits on a branch were automatically cancelled, so the tests for the new commits / push would immediately start?</p>
<p>Well, with this snippet in your workflow, that's exactly what will happen.</p>
<pre data-lang="yaml" class="notranslate"><span class="hl-keyword">name</span><span class="hl-property">:</span> Run tests

<span class="hl-keyword">on</span><span class="hl-property">:</span>
    <span class="hl-keyword">push</span><span class="hl-property">:</span>
        <span class="hl-keyword">paths</span><span class="hl-property">:</span>
            <span class="hl-property">-</span> '<span class="hl-value">**.php</span>'
            <span class="hl-property">-</span> '<span class="hl-value">.github/workflows/run-tests.yml</span>'
            <span class="hl-property">-</span> '<span class="hl-value">phpunit.xml.dist</span>'
            <span class="hl-property">-</span> '<span class="hl-value">composer.json</span>'
            <span class="hl-property">-</span> '<span class="hl-value">composer.lock</span>'

<span class="hl-keyword">concurrency</span><span class="hl-property">:</span>
    <span class="hl-keyword">group</span><span class="hl-property">:</span> $<span class="hl-value">{{</span><span class="hl-property"> github.workflow </span><span class="hl-value">}}</span>-$<span class="hl-value">{{</span><span class="hl-property"> github.ref </span><span class="hl-value">}}</span>
    <span class="hl-keyword">cancel-in-progress</span><span class="hl-property">:</span> true
    
<span class="hl-keyword">jobs</span><span class="hl-property">:</span>
   <span class="hl-comment">#</span>
   <span class="hl-comment"># omitted for brevity</span>
   <span class="hl-comment">#</span>
</pre>
<h2 id="our-complete-test-workflow">Our complete test workflow <a href="#our-complete-test-workflow" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#our-complete-test-workflow">#</a></h2>
<p>To make things clear, here's our entire workflow file including the setup of all the services that our testsuite needs like MySQL, Redis, ClickHouse, Lighthouse and more.</p>
<pre data-lang="yaml" class="notranslate"><span class="hl-keyword">name</span><span class="hl-property">:</span> Run tests

<span class="hl-keyword">on</span><span class="hl-property">:</span>
    <span class="hl-keyword">push</span><span class="hl-property">:</span>
        <span class="hl-keyword">paths</span><span class="hl-property">:</span>
            <span class="hl-property">-</span> '<span class="hl-value">**.php</span>'
            <span class="hl-property">-</span> '<span class="hl-value">.github/workflows/run-tests.yml</span>'
            <span class="hl-property">-</span> '<span class="hl-value">phpunit.xml.dist</span>'
            <span class="hl-property">-</span> '<span class="hl-value">composer.json</span>'
            <span class="hl-property">-</span> '<span class="hl-value">composer.lock</span>'

<span class="hl-keyword">concurrency</span><span class="hl-property">:</span>
    <span class="hl-keyword">group</span><span class="hl-property">:</span> $<span class="hl-value">{{</span><span class="hl-property"> github.workflow </span><span class="hl-value">}}</span>-$<span class="hl-value">{{</span><span class="hl-property"> github.ref </span><span class="hl-value">}}</span>
    <span class="hl-keyword">cancel-in-progress</span><span class="hl-property">:</span> true

<span class="hl-keyword">jobs</span><span class="hl-property">:</span>
    <span class="hl-keyword">setup-dependencies</span><span class="hl-property">:</span>
        <span class="hl-keyword">name</span><span class="hl-property">:</span> Composer and Yarn
        <span class="hl-keyword">runs-on</span><span class="hl-property">:</span> ubuntu-latest
        <span class="hl-keyword">steps</span><span class="hl-property">:</span>
            <span class="hl-property">-</span>   <span class="hl-keyword">uses</span><span class="hl-property">:</span> actions/checkout@v3
                <span class="hl-keyword">with</span><span class="hl-property">:</span>
                    <span class="hl-keyword">fetch-depth</span><span class="hl-property">:</span> 1

            <span class="hl-property">-</span>   <span class="hl-keyword">name</span><span class="hl-property">:</span> Setup PHP
                <span class="hl-keyword">uses</span><span class="hl-property">:</span> shivammathur/setup-php@v2
                <span class="hl-keyword">with</span><span class="hl-property">:</span>
                    <span class="hl-keyword">php-version</span><span class="hl-property">:</span> 8.4
                    <span class="hl-keyword">extensions</span><span class="hl-property">:</span> dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, bcmath, soap, intl, gd, exif, iconv, imagick
                    <span class="hl-keyword">coverage</span><span class="hl-property">:</span> none

            <span class="hl-property">-</span>   <span class="hl-keyword">name</span><span class="hl-property">:</span> Get Composer Cache Directory
                <span class="hl-keyword">id</span><span class="hl-property">:</span> composer-cache
                <span class="hl-keyword">run</span><span class="hl-property">:</span> echo &quot;<span class="hl-value">dir=$(composer config cache-files-dir)</span>&quot; &gt;&gt; $GITHUB_OUTPUT

            <span class="hl-property">-</span>   <span class="hl-keyword">name</span><span class="hl-property">:</span> Cache Composer dependencies
                <span class="hl-keyword">uses</span><span class="hl-property">:</span> actions/cache@v3
                <span class="hl-keyword">with</span><span class="hl-property">:</span>
                    <span class="hl-keyword">path</span><span class="hl-property">:</span> $<span class="hl-value">{{</span> steps.composer-cache.outputs.dir <span class="hl-value">}}</span>
                    <span class="hl-keyword">key</span><span class="hl-property">:</span> $<span class="hl-value">{{</span><span class="hl-property"> runner.os </span><span class="hl-value">}}</span>-composer-$<span class="hl-value">{{</span> hashFiles('<span class="hl-value">**/composer.lock</span>') <span class="hl-value">}}</span>
                    <span class="hl-keyword">restore-keys</span><span class="hl-property">:</span> $<span class="hl-value">{{</span><span class="hl-property"> runner.os </span><span class="hl-value">}}</span>-composer-

            <span class="hl-property">-</span>   <span class="hl-keyword">name</span><span class="hl-property">:</span> Install composer dependencies
                <span class="hl-keyword">run</span><span class="hl-property">:</span> composer install --prefer-dist --no-scripts -q -o

            <span class="hl-property">-</span>   <span class="hl-keyword">name</span><span class="hl-property">:</span> Upload vendor directory
                <span class="hl-keyword">uses</span><span class="hl-property">:</span> actions/upload-artifact@v4
                <span class="hl-keyword">with</span><span class="hl-property">:</span>
                    <span class="hl-keyword">name</span><span class="hl-property">:</span> vendor-directory-$<span class="hl-value">{{</span><span class="hl-property"> github.run_id </span><span class="hl-value">}}</span>
                    <span class="hl-keyword">path</span><span class="hl-property">:</span> vendor
                    <span class="hl-keyword">retention-days</span><span class="hl-property">:</span> 1

            <span class="hl-property">-</span>   <span class="hl-keyword">name</span><span class="hl-property">:</span> Cache Node Modules
                <span class="hl-keyword">uses</span><span class="hl-property">:</span> actions/cache@v3
                <span class="hl-keyword">with</span><span class="hl-property">:</span>
                    <span class="hl-keyword">path</span><span class="hl-property">:</span> node_modules
                    <span class="hl-keyword">key</span><span class="hl-property">:</span> $<span class="hl-value">{{</span><span class="hl-property"> runner.os </span><span class="hl-value">}}</span>-node-$<span class="hl-value">{{</span> hashFiles('<span class="hl-value">**/yarn.lock</span>') <span class="hl-value">}}</span>
                    <span class="hl-keyword">restore-keys</span><span class="hl-property">:</span> $<span class="hl-value">{{</span><span class="hl-property"> runner.os </span><span class="hl-value">}}</span>-node-

            <span class="hl-property">-</span>   <span class="hl-keyword">name</span><span class="hl-property">:</span> Cache built assets
                <span class="hl-keyword">uses</span><span class="hl-property">:</span> actions/cache@v3
                <span class="hl-keyword">with</span><span class="hl-property">:</span>
                    <span class="hl-keyword">path</span><span class="hl-property">:</span> <span class="hl-property">|</span>
                        public/build
                        public/hot
                        public/css
                        public/js
                    <span class="hl-keyword">key</span><span class="hl-property">:</span> $<span class="hl-value">{{</span><span class="hl-property"> runner.os </span><span class="hl-value">}}</span>-assets-$<span class="hl-value">{{</span> hashFiles('<span class="hl-value">resources/**/*</span>') <span class="hl-value">}}</span>-$<span class="hl-value">{{</span> hashFiles('<span class="hl-value">**/yarn.lock</span>') <span class="hl-value">}}</span>
                    <span class="hl-keyword">restore-keys</span><span class="hl-property">:</span> $<span class="hl-value">{{</span><span class="hl-property"> runner.os </span><span class="hl-value">}}</span>-assets-

            <span class="hl-property">-</span>   <span class="hl-keyword">name</span><span class="hl-property">:</span> Compile assets
                <span class="hl-keyword">run</span><span class="hl-property">:</span> <span class="hl-property">|</span>
                    yarn install --pure-lockfile
                    yarn build
                <span class="hl-comment"># Only run build if node_modules or assets cache wasn't hit</span>
                <span class="hl-keyword">if</span><span class="hl-property">:</span> steps.node-cache.outputs.cache-hit != '<span class="hl-value">true</span>' || steps.assets-cache.outputs.cache-hit != '<span class="hl-value">true</span>'

            <span class="hl-property">-</span>   <span class="hl-keyword">name</span><span class="hl-property">:</span> Upload compiled assets
                <span class="hl-keyword">uses</span><span class="hl-property">:</span> actions/upload-artifact@v4
                <span class="hl-keyword">with</span><span class="hl-property">:</span>
                    <span class="hl-keyword">name</span><span class="hl-property">:</span> compiled-assets-$<span class="hl-value">{{</span><span class="hl-property"> github.run_id </span><span class="hl-value">}}</span>
                    <span class="hl-keyword">path</span><span class="hl-property">:</span> <span class="hl-property">|</span>
                        public/build
                        public/hot
                        public/css
                        public/js
                    <span class="hl-keyword">retention-days</span><span class="hl-property">:</span> 1

    <span class="hl-keyword">install-chrome-and-lighthouse</span><span class="hl-property">:</span>
        <span class="hl-keyword">name</span><span class="hl-property">:</span> Install Chrome and Lighthouse
        <span class="hl-keyword">runs-on</span><span class="hl-property">:</span> ubuntu-latest
        <span class="hl-keyword">steps</span><span class="hl-property">:</span>
            <span class="hl-property">-</span>   <span class="hl-keyword">uses</span><span class="hl-property">:</span> actions/checkout@v3
                <span class="hl-keyword">with</span><span class="hl-property">:</span>
                    <span class="hl-keyword">fetch-depth</span><span class="hl-property">:</span> 1

            <span class="hl-property">-</span>   <span class="hl-keyword">name</span><span class="hl-property">:</span> Setup PHP
                <span class="hl-keyword">uses</span><span class="hl-property">:</span> shivammathur/setup-php@v2
                <span class="hl-keyword">with</span><span class="hl-property">:</span>
                    <span class="hl-keyword">php-version</span><span class="hl-property">:</span> 8.4
                    <span class="hl-keyword">extensions</span><span class="hl-property">:</span> dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, bcmath, soap, intl, gd, exif, iconv, imagick
                    <span class="hl-keyword">coverage</span><span class="hl-property">:</span> none

            <span class="hl-property">-</span>   <span class="hl-keyword">name</span><span class="hl-property">:</span> Setup problem matchers
                <span class="hl-keyword">run</span><span class="hl-property">:</span> <span class="hl-property">|</span>
                    echo &quot;<span class="hl-value">::add-matcher::${{ runner.tool_cache }}/php.json</span>&quot;
                    echo &quot;<span class="hl-value">::add-matcher::${{ runner.tool_cache }}/phpunit.json</span>&quot;

            <span class="hl-property">-</span>   <span class="hl-keyword">name</span><span class="hl-property">:</span> Install Chrome Launcher
                <span class="hl-keyword">run</span><span class="hl-property">:</span> npm install chrome-launcher

            <span class="hl-property">-</span>   <span class="hl-keyword">name</span><span class="hl-property">:</span> Install Lighthouse
                <span class="hl-keyword">run</span><span class="hl-property">:</span> npm install lighthouse

            <span class="hl-property">-</span>   <span class="hl-keyword">name</span><span class="hl-property">:</span> Cache test environment
                <span class="hl-keyword">uses</span><span class="hl-property">:</span> actions/upload-artifact@v4
                <span class="hl-keyword">with</span><span class="hl-property">:</span>
                    <span class="hl-keyword">name</span><span class="hl-property">:</span> test-env-$<span class="hl-value">{{</span><span class="hl-property"> github.run_id </span><span class="hl-value">}}</span>
                    <span class="hl-keyword">path</span><span class="hl-property">:</span> <span class="hl-property">|</span>
                        node_modules
                    <span class="hl-keyword">retention-days</span><span class="hl-property">:</span> 1

    <span class="hl-keyword">run-tests</span><span class="hl-property">:</span>
        <span class="hl-keyword">needs</span><span class="hl-property">:</span> <span class="hl-property">[</span> setup-dependencies, install-chrome-and-lighthouse <span class="hl-property">]</span>
        <span class="hl-keyword">name</span><span class="hl-property">:</span> Run Tests (Part $<span class="hl-value">{{</span><span class="hl-property"> matrix.shard_number </span><span class="hl-value">}}</span>/$<span class="hl-value">{{</span><span class="hl-property"> matrix.total_shard_count </span><span class="hl-value">}}</span>)
        <span class="hl-keyword">runs-on</span><span class="hl-property">:</span> ubuntu-latest
        <span class="hl-keyword">strategy</span><span class="hl-property">:</span>
            <span class="hl-keyword">fail-fast</span><span class="hl-property">:</span> false
            <span class="hl-keyword">matrix</span><span class="hl-property">:</span>
                <span class="hl-keyword">total_shard_count</span><span class="hl-property">:</span> <span class="hl-property">[</span>12<span class="hl-property">]</span>
                <span class="hl-keyword">shard_number</span><span class="hl-property">:</span> <span class="hl-property">[</span>'<span class="hl-value">01</span>', '<span class="hl-value">02</span>', '<span class="hl-value">03</span>', '<span class="hl-value">04</span>', '<span class="hl-value">05</span>', '<span class="hl-value">06</span>', '<span class="hl-value">07</span>', '<span class="hl-value">08</span>', '<span class="hl-value">09</span>', '<span class="hl-value">10</span>', '<span class="hl-value">11</span>', '<span class="hl-value">12</span>'<span class="hl-property">]</span>

        <span class="hl-keyword">services</span><span class="hl-property">:</span>
            <span class="hl-keyword">mysql</span><span class="hl-property">:</span>
                <span class="hl-keyword">image</span><span class="hl-property">:</span> <span class="hl-keyword">mysql</span>:8.0
                <span class="hl-keyword">env</span><span class="hl-property">:</span>
                    <span class="hl-keyword">MYSQL_ALLOW_EMPTY_PASSWORD</span><span class="hl-property">:</span> yes
                    <span class="hl-keyword">MYSQL_DATABASE</span><span class="hl-property">:</span> ohdear_testing
                <span class="hl-keyword">ports</span><span class="hl-property">:</span>
                    <span class="hl-property">-</span> 3306
                <span class="hl-keyword">options</span><span class="hl-property">:</span> --health-cmd=&quot;<span class="hl-value">mysqladmin ping</span>&quot; --health-interval=10s --health-timeout=5s --health-retries=3
            <span class="hl-keyword">redis</span><span class="hl-property">:</span>
                <span class="hl-keyword">image</span><span class="hl-property">:</span> redis
                <span class="hl-keyword">ports</span><span class="hl-property">:</span>
                    <span class="hl-property">-</span> <span class="hl-keyword">6379</span><span class="hl-property">:</span>6379
                <span class="hl-keyword">options</span><span class="hl-property">:</span> --entrypoint redis-server
            <span class="hl-keyword">clickhouse</span><span class="hl-property">:</span>
                <span class="hl-keyword">image</span><span class="hl-property">:</span> clickhouse/clickhouse-server
                <span class="hl-keyword">options</span><span class="hl-property">:</span> &gt;-
                    <span class="hl-property">-</span>-health-cmd &quot;<span class="hl-value">clickhouse client -q 'SELECT 1'</span>&quot;
                    <span class="hl-property">-</span>-health-interval 10s
                    <span class="hl-property">-</span>-health-timeout 5s
                    <span class="hl-property">-</span>-health-retries 5
                <span class="hl-keyword">ports</span><span class="hl-property">:</span>
                    <span class="hl-property">-</span> <span class="hl-keyword">8123</span><span class="hl-property">:</span>8123
                    <span class="hl-property">-</span> <span class="hl-keyword">9000</span><span class="hl-property">:</span>9000
                    <span class="hl-property">-</span> <span class="hl-keyword">9009</span><span class="hl-property">:</span>9009
                <span class="hl-keyword">env</span><span class="hl-property">:</span>
                    <span class="hl-keyword">CLICKHOUSE_DB</span><span class="hl-property">:</span> ohdear
                    <span class="hl-keyword">CLICKHOUSE_DEFAULT_ACCESS_MANAGEMENT</span><span class="hl-property">:</span> 1

        <span class="hl-keyword">steps</span><span class="hl-property">:</span>
            <span class="hl-property">-</span>   <span class="hl-keyword">uses</span><span class="hl-property">:</span> actions/checkout@v3
                <span class="hl-keyword">with</span><span class="hl-property">:</span>
                    <span class="hl-keyword">fetch-depth</span><span class="hl-property">:</span> 1

            <span class="hl-property">-</span>   <span class="hl-keyword">name</span><span class="hl-property">:</span> create db
                <span class="hl-keyword">run</span><span class="hl-property">:</span> <span class="hl-property">|</span>
                    sudo /etc/init.d/mysql start
                    mysql  -u root -proot -e '<span class="hl-value">CREATE DATABASE IF NOT EXISTS ohdear_testing;</span>'

            <span class="hl-property">-</span>   <span class="hl-keyword">name</span><span class="hl-property">:</span> Setup PHP
                <span class="hl-keyword">uses</span><span class="hl-property">:</span> shivammathur/setup-php@v2
                <span class="hl-keyword">with</span><span class="hl-property">:</span>
                    <span class="hl-keyword">php-version</span><span class="hl-property">:</span> 8.4
                    <span class="hl-keyword">extensions</span><span class="hl-property">:</span> dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, bcmath, soap, intl, gd, exif, iconv, imagick
                    <span class="hl-keyword">coverage</span><span class="hl-property">:</span> none

            <span class="hl-property">-</span>   <span class="hl-keyword">name</span><span class="hl-property">:</span> Download test environment
                <span class="hl-keyword">uses</span><span class="hl-property">:</span> actions/download-artifact@v4
                <span class="hl-keyword">with</span><span class="hl-property">:</span>
                    <span class="hl-keyword">name</span><span class="hl-property">:</span> test-env-$<span class="hl-value">{{</span><span class="hl-property"> github.run_id </span><span class="hl-value">}}</span>
                    <span class="hl-keyword">path</span><span class="hl-property">:</span> .

            <span class="hl-property">-</span>   <span class="hl-keyword">name</span><span class="hl-property">:</span> Download vendor directory
                <span class="hl-keyword">uses</span><span class="hl-property">:</span> actions/download-artifact@v4
                <span class="hl-keyword">with</span><span class="hl-property">:</span>
                    <span class="hl-keyword">name</span><span class="hl-property">:</span> vendor-directory-$<span class="hl-value">{{</span><span class="hl-property"> github.run_id </span><span class="hl-value">}}</span>
                    <span class="hl-keyword">path</span><span class="hl-property">:</span> vendor

            <span class="hl-property">-</span>   <span class="hl-keyword">name</span><span class="hl-property">:</span> Download compiled assets
                <span class="hl-keyword">uses</span><span class="hl-property">:</span> actions/download-artifact@v4
                <span class="hl-keyword">with</span><span class="hl-property">:</span>
                    <span class="hl-keyword">name</span><span class="hl-property">:</span> compiled-assets-$<span class="hl-value">{{</span><span class="hl-property"> github.run_id </span><span class="hl-value">}}</span>
                    <span class="hl-keyword">path</span><span class="hl-property">:</span> public

            <span class="hl-property">-</span>   <span class="hl-keyword">name</span><span class="hl-property">:</span> Prepare Laravel Application
                <span class="hl-keyword">run</span><span class="hl-property">:</span> <span class="hl-property">|</span>
                    cp .env.example .env
                    php artisan <span class="hl-keyword">key</span><span class="hl-property">:</span>generate

            <span class="hl-property">-</span>   <span class="hl-keyword">name</span><span class="hl-property">:</span> Set permissions for vendor binaries
                <span class="hl-keyword">run</span><span class="hl-property">:</span> chmod -R +x vendor/bin/

            <span class="hl-property">-</span>   <span class="hl-keyword">name</span><span class="hl-property">:</span> Run tests
                <span class="hl-keyword">run</span><span class="hl-property">:</span> ./bin/github-parallel-test-runner
                <span class="hl-keyword">env</span><span class="hl-property">:</span>
                    <span class="hl-keyword">DB_PORT</span><span class="hl-property">:</span> $<span class="hl-value">{{</span> job.services.mysql.ports<span class="hl-property">[</span>3306<span class="hl-property">]</span> <span class="hl-value">}}</span>
                    <span class="hl-keyword">REDIS_PORT</span><span class="hl-property">:</span> $<span class="hl-value">{{</span> job.services.redis.ports<span class="hl-property">[</span>6379<span class="hl-property">]</span> <span class="hl-value">}}</span>
                    <span class="hl-keyword">CLICKHOUSE_HOST</span><span class="hl-property">:</span> localhost
                    <span class="hl-keyword">CLICKHOUSE_PORT</span><span class="hl-property">:</span> 8123
                    <span class="hl-keyword">CLICKHOUSE_DATABASE</span><span class="hl-property">:</span> ohdear
                    <span class="hl-keyword">CLICKHOUSE_USERNAME</span><span class="hl-property">:</span> default
                    <span class="hl-keyword">CLICKHOUSE_PASSWORD</span><span class="hl-property">:</span>
                    <span class="hl-keyword">CLICKHOUSE_HTTPS</span><span class="hl-property">:</span> false
                    <span class="hl-keyword">TOTAL_SHARD_COUNT</span><span class="hl-property">:</span> $<span class="hl-value">{{</span><span class="hl-property"> matrix.total_shard_count </span><span class="hl-value">}}</span>
                    <span class="hl-keyword">SHARD_NUMBER</span><span class="hl-property">:</span> $<span class="hl-value">{{</span><span class="hl-property"> matrix.shard_number </span><span class="hl-value">}}</span>

    <span class="hl-keyword">cleanup-artifacts</span><span class="hl-property">:</span>
        <span class="hl-keyword">name</span><span class="hl-property">:</span> Clean up artifacts
        <span class="hl-keyword">needs</span><span class="hl-property">:</span> <span class="hl-property">[</span>setup-dependencies, install-chrome-and-lighthouse, run-tests<span class="hl-property">]</span>
        <span class="hl-keyword">runs-on</span><span class="hl-property">:</span> ubuntu-latest
        <span class="hl-keyword">if</span><span class="hl-property">:</span> always()
        <span class="hl-keyword">steps</span><span class="hl-property">:</span>
            <span class="hl-property">-</span>   <span class="hl-keyword">name</span><span class="hl-property">:</span> Delete artifacts
                <span class="hl-keyword">uses</span><span class="hl-property">:</span> geekyeggo/delete-artifact@v2
                <span class="hl-keyword">with</span><span class="hl-property">:</span>
                    <span class="hl-keyword">name</span><span class="hl-property">:</span> <span class="hl-property">|</span>
                        vendor-directory-$<span class="hl-value">{{</span><span class="hl-property"> github.run_id </span><span class="hl-value">}}</span>
                        compiled-assets-$<span class="hl-value">{{</span><span class="hl-property"> github.run_id </span><span class="hl-value">}}</span>
                        test-env-$<span class="hl-value">{{</span><span class="hl-property"> github.run_id </span><span class="hl-value">}}</span>
                    <span class="hl-keyword">failOnError</span><span class="hl-property">:</span> false

</pre>
<h2 id="pros-and-cons">Pros and cons <a href="#pros-and-cons" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#pros-and-cons">#</a></h2>
<p>There are pros and cons for running tests in parallel on GitHub actions. Let's start with the most important pro first: your test suite will run significantly faster. It's also very easy to increase or decrease the level of parallelism. A faster testsuite means that your entire feedback cycle is faster, which is a big win.</p>
<p>On the cons side, there's certainly some more complexity involved: you need a script to split tests, the workflow becomes more complex.</p>
<p>There's also the risk of uneven test distribution. If your tests vary significantly in execution time, you might end up with some shards finishing much earlier than others, reducing the efficiency gain.</p>
<h2 id="in-closing">In closing <a href="#in-closing" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#in-closing">#</a></h2>
<p>The approach outlined in this post works exceptionally well for large test suites like we have, where we've seen significant reduction in test execution time. But even smaller projects can benefit from this technique, especially as they grow over time.</p>
]]>
            </summary>
                                    <updated>2025-04-23T09:09:09+00:00</updated>
        </entry>
            <entry>
            <title><![CDATA[Broken links troubleshooting and a couple of extras]]></title>
            <link rel="alternate" href="https://ohdear.app/news-and-updates/broken-links-troubleshooting-and-a-couple-of-extras" />
            <id>https://ohdear.app/131</id>
            <author>
                <name><![CDATA[Oh Dear]]></name>
            </author>
            <summary type="html">
                <![CDATA[<p>We are excited to announce a powerful new feature in our Broken Links monitor: <strong>Troubleshooting</strong>.</p>
<p>When we can’t crawl all your links, or you see unexpected gaps in the results, it can be tricky to know where to start. Our new Troubleshooting tab is here to help.</p>
<h3 id="quickly-identify-and-resolve-crawling-issues">Quickly identify and resolve crawling issues</h3>
<p>The Troubleshooting tab detects common issues that may affect link checking and gives you clear guidance to resolve them and in some instances allows you to resolve them with one click. Is <code>robots.txt</code>preventing us from accessing your site? Is your firewall rate-limiting us, or maybe you are linking to <code>www</code> when your site URL is different?</p>
<p><img src="/media/blog/Dp2uAtNN2lgnmatS9TybS7TH0csxCU6Klb8n9yk5.png" alt="" /></p>
<p>All broken link report pages will now have access to <code>suggested</code> and <code>all</code> troubleshooting filters where we either show you relevant actions based on your results or all possible issues and solutions. Think of it like interactive documentation and FAQs (which we have also linked to). There's even a short API snippet to trigger a new broken links - perfect for post-deployment hooks.</p>
<h3 id="why-it-matters">Why it matters</h3>
<p>Our goal is to make it easy for teams of all sizes to catch and fix broken links and quickly identify and resolve any issues. With this update, even the trickier edge cases become actionable and transparent. Take a look at what we have got so far - we will be keeping this up-to-date based on feedback so keep an eye out! And if you're still having trouble our support team is just a click away in the support bubble.</p>
<p><img src="/media/blog/dIJbH3wTgbY59dWPWtLxiCYzmDeOERdgn1mFZgU8.png" alt="" /></p>
<h3 id="new-robotstxt-controls">New robots.txt controls</h3>
<p>We have always allowed our users to 'ignore' broken links but sometimes you don't even want us to crawl your pages. Especially when you want us to respect your robots.txt file but where you may not have access to server config.</p>
<p>So we have added advanced settings for a couple of edge cases:</p>
<p>Force crawl specific URLs: If you can't or don’t want to modify your <code>robots.txt</code>, you can manually add specific pages to an allowlist that should always be crawled - even if disallowed.</p>
<p>Do not crawl specific URLs: Useful to exclude sensitive pages, admin areas, or known false positives.</p>
<p>These options give you just a little bit more control over how Oh Dear crawls your site.</p>
<p><img src="/media/blog/TRxIFlij1N1Gp7oIofllVBbB9lIyPscZd769JHfQ.png" alt="" /></p>
<h3 id="coming-soon">Coming soon</h3>
<p>We have more broken links updates coming to your notification settings soon! Tired of receiving the same alert every day for the same broken links? You will now be able to only get a follow-up notification when new broken links are found. Giving you a bit more time to get them fixed without constantly snoozing the notification.</p>
<p>Keep an eye on the Oh Dear to find out when this is released :)</p>
]]>
            </summary>
                                    <updated>2025-04-07T11:42:59+00:00</updated>
        </entry>
            <entry>
            <title><![CDATA[Why your business can’t afford to skip website monitoring]]></title>
            <link rel="alternate" href="https://ohdear.app/news-and-updates/why-your-business-cant-afford-to-skip-website-monitoring" />
            <id>https://ohdear.app/129</id>
            <author>
                <name><![CDATA[Oh Dear]]></name>
            </author>
            <summary type="html">
                <![CDATA[<h2 id="why-would-i-monitor-my-website-in-the-first-place">Why would I monitor my website in the first place? <a href="#why-would-i-monitor-my-website-in-the-first-place" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#why-would-i-monitor-my-website-in-the-first-place">#</a></h2>
<p>Your website is your business’ storefront, sales team, customer service department, and potentially even your primary revenue channel. Just like you’d protect the physical presence of these aspects of your business with a security system, you also need to protect the online aspects too. That means keeping an eye on your website with monitoring.</p>
<h2 id="how-much-does-website-downtime-cost">How much does website downtime cost? <a href="#how-much-does-website-downtime-cost" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#how-much-does-website-downtime-cost">#</a></h2>
<p>When figuring out the cost of website downtime, there are two factors at play - direct costs and indirect costs.</p>
<h3 id="direct-costs-of-website-downtime">Direct costs of website downtime</h3>
<p>You can calculate the direct costs. Figure out how much money your website makes you per hour or minute and then multiply that number by the amount of time your site was down.</p>
<p>Of course, the amount your website makes is a lot easier to calculate for e-commerce sites than other businesses such as consultants. But, you can still get a rough idea by considering the amount of inquiries you get on your website per hour or day and the rate that you turn those inquiries into sales.</p>
<h3 id="indirect-costs-of-website-downtime">Indirect costs of website downtime</h3>
<p>Then, there are the indirect costs. While they’re more difficult to account for, they cost your business all the same. Here are a few examples of the indirect costs to website downtime:</p>
<ul>
<li><strong>Brand reputation damage:</strong> Nowadays, people expect websites to always be working. When potential customers visit a website that’s down, they start off with a negative impression of the brand. They associate it with brands that cut corners and offer cheap services or products.</li>
<li><strong>SEO ranking penalties:</strong> Google constantly indexes websites so they have the most up-to-date information on your pages. If your site is down while Google is trying to crawl your site, it will negatively impact your rankings since it returns an <a href="https://developers.google.com/search/docs/essentials/technical">error code to the Googlebots</a>.</li>
<li><strong>Customers go elsewhere:</strong> According to a <a href="https://landing.adobe.com/en/na/products/creative-cloud/264699-state-of-content/index.html">study by Adobe</a>, if your site doesn’t load, 41 percent of people go elsewhere. This drives your potential customers to your competitors.</li>
<li><strong>Increased support costs:</strong> You likely have self-help articles on your website. When those go down, customers who need help must revert to calling your customer service line which increases your support costs.</li>
</ul>
<h3 id="real-world-examples-of-website-downtime-impacts">Real-world examples of website downtime impacts</h3>
<p>The costs of website downtime aren’t just hypothetical. They cause real-world impacts. Let’s go over a few of the many times website downtime has cost companies millions:</p>
<ul>
<li><strong>Amazon's Prime Day disaster:</strong> In 2018, Amazon went down for some users on the worst day for them, Prime Day. It’s one of their major shopping days and users couldn’t access the site. Amazon estimates this outage cost them <a href="https://www.businessinsider.com/amazon-prime-day-website-issues-cost-it-millions-in-lost-sales-2018-7">upwards of $99 million</a>.</li>
<li><strong>British Airways' internal chaos:</strong> Internal issues can arise from website downtime too. British Airways’ 2017 disaster is a perfect example. Their IT systems went down forcing them to cancel flights for <a href="https://www.theguardian.com/business/2017/may/31/ba-it-shutdown-caused-by-uncontrolled-return-of-power-after-outage">up to 75,000 passengers</a>.</li>
<li><strong>Facebook’s $90 million day:</strong> After just five hours of downtime, Facebook lost <a href="https://www.theguardian.com/technology/2021/oct/05/facebook-outage-what-went-wrong-and-why-did-it-take-so-long-to-fix">4.9 percent of its share value</a>. That resulted in Mark Zuckerberg losing roughly $6 billion in wealth.</li>
</ul>
<h2 id="why-startups-cant-afford-to-skip-website-monitoring">Why startups can't afford to skip website monitoring <a href="#why-startups-cant-afford-to-skip-website-monitoring" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#why-startups-cant-afford-to-skip-website-monitoring">#</a></h2>
<p>All too often, startups consider website monitoring a luxury to buy once they get bigger. The reality is that bigger companies have enough resources to survive through downtime issues whereas the same problems for startups can become fatal. Here are a few of the reasons why startups need website monitoring:</p>
<ul>
<li><strong>Myth of startup resilience:</strong> People think of startups as agile companies that can quickly adjust for any problems that arise. While it’s true that startups are agile, they also don’t have as much runway to adjust. That’s why startups need website monitoring that notifies them of downtime immediately so they can act right away. If it goes unnoticed, startups can run out of runway before they even get started recovering.</li>
<li><strong>First impressions are critical:</strong> We already touched on how website downtime can give customers a bad taste in their mouth. What we haven’t shared yet, though, is that according to <a href="https://in.nau.edu/experience-design/">Northern Arizona University</a>, 88 percent of users won’t return after a bad user experience. And your website being down is absolutely a bad first experience. Website monitoring helps identify downtime as the issue as opposed to a different issue such as a poorly designed website.</li>
<li><strong>Launch timing:</strong> There’s a cruel paradox where website downtime occurs when it’s least welcome, like the launch of your product or a feature. You’ve spent countless hours and thousands, if not millions, on your launch. If your website’s down, you can’t capitalize on it. Website monitoring can help you predict downtime to avoid these circumstances.</li>
<li><strong>Investor confidence:</strong> When pitching your startup to VCs, you usually need to present a demo and let them try your product. If your network goes down during these meetings, you’ll be hard-pressed to get a VC to come back another time, let alone invest in your startup. Your odds go up if you can cite your downtime statistics to show the VC that this is an outlier and not the norm.</li>
</ul>
<h2 id="what-else-should-you-monitor-on-your-website">What else should you monitor on your website? <a href="#what-else-should-you-monitor-on-your-website" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#what-else-should-you-monitor-on-your-website">#</a></h2>
<p>Monitoring the binary of whether your website is up or not leaves a lot of data off the table. That’s why alongside website downtime monitoring, you should also monitor:</p>
<h3 id="performance-metrics">Performance metrics</h3>
<p>Both users and Google care a lot about your performance metrics. Sure, your users don’t know how long it takes for your site to load, but they definitely know if it takes too long. And Google has figured out exactly what “too long” is. That’s why they use your website speed as a <a href="https://web.dev/articles/vitals">direct ranking factor</a>. You should apply this to all customer-facing online services. An overwhelmed server slowing down your online service or website is different from downtime, though, so it requires separate monitoring.</p>
<h3 id="functionality-testing">Functionality testing</h3>
<p>Website downtime monitoring won’t monitor for a broken user journey. That means when a user clicks a link or opens a menu, that link or menu doesn’t work. Services monitor for functionality by continuously running simulated user journeys identifying issues before they impact real users.</p>
<h3 id="third-party-service-integration">Third-party service integration</h3>
<p>Every third-party service you use introduces a potential point of failure for your website or online service. Now, that’s not to say you shouldn’t use third-party services. Every business depends on at least some third-party services. It only means you need to monitor their integration. APIs get updated all the time and those updates can break your integration. Third-party service monitoring ensures you know what’s working and what needs work at all times.</p>
<h3 id="security-monitoring">Security monitoring</h3>
<p>A <a href="https://newsroom.ibm.com/IBM-security?item=30567">study conducted by IBM</a> looked at how long it takes the average company to identify a security breach. The results are a shocking 197 days. That’s over half a year before a security breach gets identified. This is why security monitoring is crucial and why modern website monitoring services integrate security monitoring into their downtime monitoring. Any efforts to reduce that time to identify a breach help you minimize the damage it can cause.</p>
<h2 id="recap-why-you-need-website-monitoring">Recap: Why you need website monitoring <a href="#recap-why-you-need-website-monitoring" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#recap-why-you-need-website-monitoring">#</a></h2>
<p>Monitoring your website and online services is all about preventing you from jumping out of the frying pan and into the fire. That’s to say, without appropriately monitoring your website, you can incorrectly identify issues or not even realize an issue exists in the first place. Then, based on incorrect information, your attempts to fix the problem can make the problem worse.</p>
<p>For instance, if you notice a big uptick in your bounce rate after a marketing campaign, you might think the campaign flopped. But, it could be an issue with your site being overwhelmed causing it to slow down or go down completely for some users. With quality website monitoring, instead of scrapping an otherwise successful campaign, you can fix the core issue. That’s just one example of the value of website monitoring. Whether you’re a startup or an enterprise, website monitoring gives you the information you need to manage your website like the pros.</p>
]]>
            </summary>
                                    <updated>2025-03-12T09:14:26+00:00</updated>
        </entry>
            <entry>
            <title><![CDATA[Here's what worked and what didn't when marketing SaaS like Oh Dear]]></title>
            <link rel="alternate" href="https://ohdear.app/news-and-updates/heres-what-worked-and-what-didnt-when-marketing-saas-like-oh-dear" />
            <id>https://ohdear.app/128</id>
            <author>
                <name><![CDATA[Oh Dear]]></name>
            </author>
            <summary type="html">
                <![CDATA[<h2 id="where-were-at-in-the-scene">Where we're at in the scene <a href="#where-were-at-in-the-scene" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#where-were-at-in-the-scene">#</a></h2>
<p>Oh Dear is the underdog in the website monitoring software space. We’re small fries in a market dominated by big, well-funded competitors. Yet somehow, Freek and Mattias managed to build a solid user base and created a profitable SaaS business. But where do you go from there? What was the eventual marketing strategy? We thought it would be cool to share with you guys our approach for Oh Dear, our lessons learned, and the strategies that helped us grow.</p>
<h2 id="started-from-the-bottom-now-were-here">Started from the bottom, now we're here <a href="#started-from-the-bottom-now-were-here" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#started-from-the-bottom-now-were-here">#</a></h2>
<p>After launching <a href="https://ohdear.app/">Oh Dear</a>, the boys were able to get some solid traction going thanks to their personal network. Both Freek and Mattias are well-known in the <a href="https://laravel.com/">Laravel</a> community, each running their own successful businesses—the <a href="https://ma.ttias.be/cronweekly/">cron.weekly newsletter</a> (Mattias) and <a href="https://spatie.be/">Spatie</a> (Freek). Their developer-focused blogs and community presence helped spread the word about Oh Dear organically.</p>
<p>Thanks to word-of-mouth and strong community engagement, we scaled to about 2,000 users without spending much on marketing.</p>
<p>But they knew they had to expand their audience beyond their own network. This is when <a href="https://brandimpact.be/">Brandimpact</a> entered the picture, a teeny tiny marketing agency from Ghent, Belgium, to help the boys start experimenting and build a robust, overarching marketing strategy. Below is an overview of what we did to better market Oh Dear throughout 2024.</p>
<h2 id="phase-1-getting-the-ball-rolling">Phase 1: Getting the ball rolling <a href="#phase-1-getting-the-ball-rolling" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#phase-1-getting-the-ball-rolling">#</a></h2>
<p>The Brandimpact gang got work doing the following:</p>
<ul>
<li>Customer interviews to better understand Oh Dear's ideal users</li>
<li>Competitor analysis &amp; keyword research to refine our positioning</li>
<li>A strategy workshop to set the priorities for the next six months</li>
</ul>
<p>The goals? Increasing awareness, improving conversion rates, and exploring less obvious acquisition channels.
Initially, we kept the personal networks of Freek and Mattias out of the picture, to see how independently the marketing efforts could run.</p>
<p>Since all of this was still very experimental, we limited our monthly media budget to approximately $5.000 per month for PPC (pay-per-click) advertising.</p>
<h3 id="what-worked">What worked</h3>
<h4 id="1-google-search-ads-with-limitations">1. Google Search Ads (With Limitations)</h4>
<p><a href="https://ads.google.com/">Google Ads</a> helped generate some free trial sign-ups, but at a pretty high acquisition cost. Since many <a href="https://ohdear.app/oh-dear-vs-competition">competitors</a> are obviously bidding on the same keywords (“website monitoring software”, “uptime monitoring software”), we were often outspent. While it brought in leads, it wasn’t sustainable for a bootstrapped SaaS like ours.</p>
<h4 id="2-cro-conversion-rate-optimization">2. CRO (Conversion Rate Optimization)</h4>
<p>We improved our website and trial experience by:</p>
<ul>
<li>Creating and adding a product video to the homepage</li>
<li>Making our copy more engaging and clear for our target audience</li>
<li>Extending the free trial from 7 to 30 days</li>
<li>Collecting and adding more customer testimonials</li>
</ul>
<p>Here's the video we ended up with, by the way. This was actually a lot of fun to do:</p>
<iframe width="100%" height="350px" src="https://www.youtube-nocookie.com/embed/IbCC06sHRtk?si=XCJlJO-rkTWsT1cj&amp;autoplay=1&amp;loop=1&amp;modestbranding=1&amp;playsinline=1" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen=""></iframe>
<h4 id="3-reddit-ads-better-than-expected">3. Reddit Ads — Better than expected</h4>
<p>We didn’t expect much from <a href="https://ads.reddit.com/">Reddit Ads</a> to be perfectly honest, but given our target audience we figured it was worth a shot. But as it turned out, Reddit ended up being one of the best-performing paid channels for us in terms of acquiring new trial sign-ups. We tested three different campaigns on Reddit:</p>
<ul>
<li>A feature-based carousel ad</li>
<li>A product video featuring Mattias &amp; Freek with a fun hook</li>
<li>A series of memes using popular formats</li>
</ul>
<p>Though we will say the quality of the traffic might be debatable—lots of clicks but not much else.</p>
<h3 id="what-didnt-work">What didn’t work</h3>
<h4 id="1-linkedin-ads-too-expensive">1. LinkedIn ads – Too expensive</h4>
<p>We targeted CTOs and developers, but the cost per click was too high. LinkedIn might work for enterprise SaaS, but for our niche, it wasn’t worth the investment.</p>
<h4 id="2-meta-amp-youtube-ads-no-significant-impact">2. Meta &amp; YouTube ads – No significant impact</h4>
<p>We briefly tested Facebook (Meta) Ads and YouTube Ads, but they didn’t generate enough quality leads. Developers don’t typically discover technical tools through these platforms.</p>
<h4 id="3-twitter-ads-garbage-clicks">3. Twitter ads – Garbage clicks</h4>
<p>Arguably the worst advertising services among social media platforms. Lots of reach and clicks but absolutely abysmal quality. We're suspecting many among them were just bots.</p>
<h2 id="phase-2-tuning-our-strategy">Phase 2: Tuning our strategy <a href="#phase-2-tuning-our-strategy" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#phase-2-tuning-our-strategy">#</a></h2>
<p>All in all, we were driving traffic but conversions weren't exactly growing in a similar fashion, so we turned our attention to re-activating our audience, leveraging SaaS aggregators and content marketing.</p>
<h3 id="1-affiliate-marketing-activating-our-user-base">1. Affiliate marketing – Activating our user base</h3>
<p>We already had an affiliate program offering 25% commission per sale, but few affiliates were actively promoting us. To improve this, we:</p>
<ul>
<li>Reached out to existing customers</li>
<li>Created a press kit with ready-to-use content</li>
<li>Updated the affiliate landing page on the website</li>
<li>Optimized our affiliate onboarding emails</li>
</ul>
<h3 id="2-saas-review-platforms-leveraging-g2-amp-capterra">2. SaaS review platforms – Leveraging G2 &amp; Capterra</h3>
<p>Like most people, developers tend to trust their peers, so we focused on building a strong presence on platforms like G2 and Capterra. It's kind of an SEO effort as well, since these platforms work similary to Google in terms of ranking search results.</p>
<ul>
<li>We added review badges to our site</li>
<li>We launched campaigns to encourage users to leave reviews</li>
<li>We paid for a Capterra CPC campaign (~€40 per free trial)</li>
</ul>
<p>Results? So far, Capterra (Gartner) has been our best-performing paid &quot;channel&quot; for free trial sign-ups.</p>
<h3 id="3-seo-amp-content-marketing-playing-the-long-game">3. SEO &amp; content marketing – Playing the long game</h3>
<p>We invested in SEO-driven content, but competing for high-intent keywords (like “uptime monitoring software”) remains tough. To improve our rankings, we're:</p>
<ul>
<li>Publishing SEO-optimized blog posts (see our blog)</li>
<li>Focusing on getting backlinks from SaaS review sites</li>
<li>Reaching out to tech blogs that review monitoring tools</li>
</ul>
<p>Example outreach targets:</p>
<ul>
<li><a href="https://middleware.io/blog/website-monitoring-tools/">Middleware.io's blog on website monitoring tools</a></li>
<li><a href="https://www.pcmag.com/picks/the-best-website-monitoring-services">PCMag’s Best Website Monitoring Services</a></li>
</ul>
<h3 id="4-direct-outreach-with-gifting-taking-things-offline">4. Direct outreach with gifting – Taking things offline</h3>
<p>We’re currently planning a gifting campaign targeting digital agencies in Belgium. The idea is to send over a little care package—as in, a couple of Belgian beers—to a series of target accounts. Custom packaging, a personalized message, nothing too sales-y either. Freek and Mattias were actually at a bar drinking said beer when the idea for Oh Dear first came up, so it's a fun little tie-in to the product.</p>
<h2 id="final-thoughts-what-we-learned-about-marketing-a-saas">Final Thoughts: What We Learned About Marketing a SaaS <a href="#final-thoughts-what-we-learned-about-marketing-a-saas" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#final-thoughts-what-we-learned-about-marketing-a-saas">#</a></h2>
<ol>
<li>Personal networks &amp; word-of-mouth are powerful, especially in the developer community.</li>
<li>Paid ads (safe for Reddit &amp; Capterra) weren’t particularly effective.</li>
<li>SEO &amp; review platforms can drive sustainable growth.</li>
<li>Don't be afraid to try something you're not familiar with. Trust is key, though.</li>
<li>There's no silver bullet, every audience is different.</li>
</ol>
<p>Our marketing strategy is always evolving, and we’ll keep refining our approach as we learn. Keep you posted!</p>
]]>
            </summary>
                                    <updated>2025-03-14T15:43:37+00:00</updated>
        </entry>
            <entry>
            <title><![CDATA[Detecting and Resolving Broken Links Using Website Monitoring Software]]></title>
            <link rel="alternate" href="https://ohdear.app/news-and-updates/detecting-and-resolving-broken-links-using-website-monitoring-software" />
            <id>https://ohdear.app/127</id>
            <author>
                <name><![CDATA[Oh Dear]]></name>
            </author>
            <summary type="html">
                <![CDATA[<p>Broken links are more than minor annoyances—they can significantly harm your website’s user experience, SEO rankings, and overall credibility. Whether managing a small blog or a complex e-commerce platform, ensuring that your site functions flawlessly is essential to keeping visitors engaged and search engines satisfied. However, identifying and fixing broken links manually can be time-consuming and error-prone. That’s where website monitoring software comes into play.</p>
<p>These tools are especially relevant in today’s digital landscape, where user expectations and search engine algorithms demand seamless navigation and impeccable website health. Whether you're a website owner, SEO professional, or student learning about digital optimization, this guide will equip you with actionable insights and best practices for maintaining a robust online presence.</p>
<h2 id="how-do-broken-links-impact-website-seo-and-user-experience">How do broken links impact website SEO and user experience? <a href="#how-do-broken-links-impact-website-seo-and-user-experience" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#how-do-broken-links-impact-website-seo-and-user-experience">#</a></h2>
<p>Broken links negatively impact both website SEO and user experience. Firstly, they signal poor website maintenance to search engines, potentially lowering your search rankings. Secondly, they disrupt search engine crawlers, hindering their ability to index your content. Thirdly, broken links on pages with many inbound links diminish the value passed from other websites, reducing the page's authority.</p>
<p>Furthermore, broken links can frustrate users, leading to high bounce rates and short session durations, which send negative signals to search engines. From a user perspective, broken links create a frustrating experience, damaging your brand reputation and making your website appear unprofessional. This can disrupt user flow, prevent access to important information, and ultimately lead to missed opportunities for conversions and revenue.</p>
<h2 id="how-website-monitoring-software-helps-identify-broken-links">How website monitoring software helps identify broken links <a href="#how-website-monitoring-software-helps-identify-broken-links" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#how-website-monitoring-software-helps-identify-broken-links">#</a></h2>
<p><img src="/media/blog/h0IO8zxQmdMRFHmet2TBfHBgUPAuErRMK9RKPCEO.png" alt="" />
Website monitoring software is an essential tool for efficiently identifying and managing broken links. These tools streamline the process by automating detection, providing actionable insights, and ensuring proactive website maintenance. Here's how they help:</p>
<h3 id="automated-crawling-of-your-website">Automated Crawling of Your Website</h3>
<p>Website monitoring software scans your entire site, following internal and external links to identify any that are broken. This eliminates the need for manual checking, which can be time-consuming and prone to human error.</p>
<h3 id="real-time-error-detection">Real-Time Error Detection</h3>
<p>Many website monitoring software continuously tracks your website and alerts you in real-time when a link becomes broken. This allows you to address the issue immediately before it affects SEO or user experience.</p>
<h3 id="comprehensive-reporting">Comprehensive Reporting</h3>
<p>These tools generate detailed reports that categorize broken links by type (404 errors, redirect loops, timeout issues, etc.) and highlight their location. This makes it easier to prioritize and fix the most critical problems first.</p>
<h3 id="integration-with-seo-and-analytics-tools">Integration with SEO and Analytics Tools</h3>
<p>Many monitoring platforms integrate with SEO tools like Google Analytics and Google Search Console. This allows you to see how broken links are impacting site traffic and rankings, helping prioritize fixes that matter most to your audience and search engines.</p>
<h3 id="scalability-for-large-websites">Scalability for Large Websites</h3>
<p>For websites with thousands of pages, manual link checking is impractical. Website monitoring software is built to handle large-scale link audits efficiently, ensuring no broken links are overlooked.</p>
<h3 id="user-friendly-dashboards">User-Friendly Dashboards</h3>
<p>Most website monitoring software offers intuitive dashboards that make it easy to manage broken links without technical expertise. Filters and sorting options allow quick access to the most urgent issues.</p>
<h2 id="what-features-should-you-look-for-in-software-designed-to-monitor-links">What features should you look for in software designed to monitor links? <a href="#what-features-should-you-look-for-in-software-designed-to-monitor-links" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#what-features-should-you-look-for-in-software-designed-to-monitor-links">#</a></h2>
<p>When selecting a website monitoring software, it's important to choose a tool with the right features to ensure effective and efficient link management. Here are the essential features to look for:</p>
<ul>
<li><strong>Real-time Monitoring</strong>: The ability to monitor link performance in real-time, is crucial for detecting issues as they occur. This allows for swift responses to potential problems, such as downtime or performance degradation.</li>
<li><strong>Automated Alerts</strong>: Automated alerts should notify users of any anomalies or threshold breaches in link performance. This feature ensures that administrators are promptly informed of issues that require attention, enabling quick action to mitigate risks.</li>
</ul>
<h2 id="how-accurate-are-automated-tools-compared-to-manual-link-checking">How accurate are automated tools compared to manual link-checking? <a href="#how-accurate-are-automated-tools-compared-to-manual-link-checking" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#how-accurate-are-automated-tools-compared-to-manual-link-checking">#</a></h2>
<p>Automated <a href="/news-and-updates/enhanced-broken-links-monitoring-images-css-and-js">link-checking tools</a> generally offer high accuracy in identifying broken links on a large scale, significantly outperforming manual checks in terms of speed and efficiency. However, they may occasionally miss certain types of links or misinterpret complex situations, such as dynamic content or JavaScript-based links. Manual checks, while time-consuming, can provide a more nuanced understanding of link functionality and context, ensuring that all critical links are properly evaluated.</p>
<p>For optimal results, a combination of both approaches is often recommended. Automated tools can quickly identify the majority of broken links, while manual checks can be used to review critical links, investigate false positives, and ensure the accuracy of the automated results.</p>
<h2 id="what-common-mistakes-do-businesses-make-when-addressing-broken-links">What common mistakes do businesses make when addressing broken links? <a href="#what-common-mistakes-do-businesses-make-when-addressing-broken-links" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#what-common-mistakes-do-businesses-make-when-addressing-broken-links">#</a></h2>
<p><img src="/media/blog/SemN7HPJ4n3gndzywX5I5pU4kuZGOQ2CHk0KyFay.png" alt="" /></p>
<p>When businesses attempt to <a href="/news-and-updates/improving-our-broken-link-tests">address broken links</a>, they often make several common mistakes that can exacerbate the problem or lead to further issues. Here are the key mistakes to avoid:</p>
<ul>
<li><strong>Failure to Check Links Periodically</strong>: Many businesses do not regularly audit their websites for broken links. This oversight can lead to a poor user experience and negatively impact SEO, as broken links can diminish site credibility and authority.</li>
<li><strong>Ignoring Internal vs. External Links</strong>: Businesses sometimes treat internal and external links the same way. However, internal links can be controlled and fixed more easily, while external links may require different strategies, such as reaching out to the site owner for repairs.</li>
<li><strong>Failing to Revise Interlinking</strong>: When a website undergoes structural changes, such as URL updates, businesses often forget to update internal links accordingly. This can lead to numerous 404 errors if the old links are not redirected or corrected.</li>
</ul>
<h2 id="how-can-you-proactively-manage-internal-links-to-avoid-breaks">How can you proactively manage internal links to avoid breaks? <a href="#how-can-you-proactively-manage-internal-links-to-avoid-breaks" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#how-can-you-proactively-manage-internal-links-to-avoid-breaks">#</a></h2>
<p>Website monitoring software identifies and prevents time-consuming website issues, while <a href="https://research.com/software/best-time-tracking-software">time tracking software</a> measures the time spent resolving them. Both tools provide valuable data that can be used to optimize website performance and improve employee productivity. By combining data from both, businesses can make informed decisions about website investments and resource allocation.</p>
<p>Proactive management of internal links is crucial for maintaining a healthy website and avoiding broken links. This involves a multi-faceted approach. Firstly, establish and adhere to consistent naming conventions for all files and folders on your website, making it easier to track and update links. Secondly, a version control system should be implemented to track changes to website files and easily revert to previous versions if necessary.</p>
<p>You should also conduct regular website audits to identify and fix any broken internal links promptly. Fourthly, a content management system (CMS) with robust features for managing internal links, such as automated link checking and version history, should be used. Finally, all team members should be involved in the process by providing clear guidelines and training on best practices for creating and maintaining internal links. By implementing these strategies, you can significantly reduce the risk of broken internal links and ensure a seamless user experience across your entire website.</p>
<h2 id="future-proofing-your-website-against-broken-links">Future-proofing your website against broken links <a href="#future-proofing-your-website-against-broken-links" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#future-proofing-your-website-against-broken-links">#</a></h2>
<p>Future-proofing your website against broken links involves a proactive approach that anticipates and minimizes potential issues. This includes implementing robust internal linking structures with clear and consistent naming conventions. Utilizing a version control system and <a href="https://www.indeed.com/career-advice/career-development/web-traffic-monitoring">website monitoring software</a> allows for easy tracking and reversion of changes, minimizing the risk of unintended link breakage.</p>
<p>Regularly scheduled website audits and the use of a CMS with powerful link management features are essential for early detection and resolution of any broken links. Furthermore, fostering a company-wide understanding of the importance of proper link management through training and clear guidelines empowers all team members to contribute to a more resilient and user-friendly website.</p>
<h2 id="boost-your-websites-credibility">Boost Your Website’s Credibility <a href="#boost-your-websites-credibility" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#boost-your-websites-credibility">#</a></h2>
<p>Detecting and resolving broken links is not just about fixing errors—it’s about safeguarding your website’s credibility, user experience, and SEO performance. By leveraging website monitoring software, you can automate the detection process, gain valuable insights, and proactively address issues before they harm your site’s reputation or search rankings.</p>
<p>Coupled with best practices for link management and a future-proofing mindset, these tools enable you to maintain a seamless, reliable website that meets the demands of both users and search engines. By taking these steps, you can ensure that your site remains a trusted, high-performing resource in today’s competitive digital landscape.</p>
]]>
            </summary>
                                    <updated>2025-03-10T08:48:18+00:00</updated>
        </entry>
            <entry>
            <title><![CDATA[Status Page Customizations]]></title>
            <link rel="alternate" href="https://ohdear.app/news-and-updates/status-page-customizations" />
            <id>https://ohdear.app/122</id>
            <author>
                <name><![CDATA[Oh Dear]]></name>
            </author>
            <summary type="html">
                <![CDATA[<p>Your status page is an important tool for communicating incidents and building trust with your customers. It's also an opportunity to reinforce your brand's identity. That’s why we’re excited to introduce new customization options for your status pages at Oh Dear!</p>
<h2 id="whats-new">What’s New? <a href="#whats-new" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#whats-new">#</a></h2>
<p>With our latest update you can now customize your status page to match your brand's look and feel:</p>
<ul>
<li><strong>Theming</strong>: Choose between Light, Dark, or System Mode to make your status page visually align with your website or user preferences.</li>
<li><strong>Custom Logos</strong>: Add different logos for each theme ensuring your brand is always visible.</li>
<li><strong>Logo position</strong>: Left or centered. You choose.</li>
<li><strong>Borders</strong>: Customize the borders and corners of your status page elements for a classic or modern look.</li>
<li><strong>Backgrounds</strong>: Change the header and main backgrounds independently.</li>
<li><strong>Colors</strong>: Choose text, information and button colors that complement your brand and ensure readability.</li>
<li><strong>Uptime Chart</strong>: Prevent the uptime graph from clashing with your brand - choose colors for 'ok', 'warning' and 'crtitical' data points.</li>
</ul>
<h2 id="why-customize-your-status-page">Why Customize Your Status Page? <a href="#why-customize-your-status-page" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#why-customize-your-status-page">#</a></h2>
<p>Customizing your status page isn’t just about aesthetics - it also comes with several practical benefits:</p>
<ul>
<li><strong>Brand Consistency</strong>: A status page that matches your brand helps users recognize it as an official communication channel, reducing confusion and enhancing trust.</li>
<li><strong>Improve User Experience</strong>: Custom colors and themes can make your status page easier to read and navigate based on familiarity with your existing website.</li>
<li><strong>Professionalism</strong>: A well-designed, on-brand status page shows that you care about every aspect of your service, even when problems occur</li>
</ul>
<h2 id="how-it-works">How it works <a href="#how-it-works" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#how-it-works">#</a></h2>
<p>Customizing your status page is quick and easy:</p>
<ol>
<li>Click on the 'Status Page' tab in your Oh Dear dashboard.</li>
<li>Click on 'Display' in the left menu.</li>
<li>Select 'Logo' or 'Style' tabs.</li>
</ol>
<p><img src="/media/blog/ZRo6KEl46u7F1G5dF7Fh2UMWQ5LjW50EjsBZJJS3.png" alt="" /></p>
<p>You can manage light and dark mode colors under style &gt; theme settings where you can match your brand to your status page.
<img src="/media/blog/Gv4UMIDUIidtialPhQVMPHdw0yNkvCQqaQ8fbgPy.png" alt="" /></p>
<h2 id="show-it-off">Show it off <a href="#show-it-off" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#show-it-off">#</a></h2>
<p>Get started with the new customization features today and give your status page a fresh, on-brand look that users will recognize and trust. Head over to your settings and make it uniquely yours!</p>
<p>If you have any questions, need assistance or just want to let us know what you think, don’t hesitate to reach out to our support team.</p>
<p><img src="/media/blog/0jAdEzuSi9k1kD4ex69KmS4HHRLDhb1pEzVIeoWn.png" alt="" /></p>
<h2 id="did-you-know">Did you know? <a href="#did-you-know" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#did-you-know">#</a></h2>
<p>We also have status page update templates that you can use to quickly keep users informed while keeping your messages clear, consistent and on-brand.</p>
]]>
            </summary>
                                    <updated>2024-12-06T15:24:58+00:00</updated>
        </entry>
            <entry>
            <title><![CDATA[Enhanced Broken Links Monitoring: Images, CSS and JS]]></title>
            <link rel="alternate" href="https://ohdear.app/news-and-updates/enhanced-broken-links-monitoring-images-css-and-js" />
            <id>https://ohdear.app/123</id>
            <author>
                <name><![CDATA[Oh Dear]]></name>
            </author>
            <summary type="html">
                <![CDATA[<p>We are excited to announce an enhancement to one of our most unique features at Oh Dear: Broken Links!</p>
<p>Broken links monitoring crawls your entire website for internal and external links but we now take this a step further by expanding our monitoring to include CSS, JavaScript, and image links.</p>
<h2 id="whats-new">What’s New? <a href="#whats-new" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#whats-new">#</a></h2>
<p>With this update, our broken links check now scans:</p>
<ul>
<li><strong>CSS Links</strong>: Verify that all your stylesheets are loading correctly and aren’t returning 404 errors or other issues.</li>
<li><strong>JavaScript Links</strong>: Ensure that all your JavaScript files are accessible helping to prevent issues that can break functionality or affect the user experience.</li>
<li><strong>Images</strong>: Confirm that all images on your site are loading properly, avoiding missing images that can make your site look unprofessional or incomplete.</li>
<li><strong>Open Graph Images</strong>: Your social previews are important - make sure they are loading correctly.</li>
</ul>
<p><img src="/media/blog/hu7tzP47Enq2iVMHqHlofqAiLpQ6e6vGSMUgTps7.png" alt="" /></p>
<h2 id="why">Why? <a href="#why" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#why">#</a></h2>
<p>Here’s why these new checks are important for your website:</p>
<ul>
<li><strong>Comprehensive Monitoring</strong>: By checking CSS, JavaScript, and image links, you can catch issues that might not be immediately visible but could still impact your site's performance, SEO, and user experience.</li>
<li><strong>Prevent Styling and Functionality Errors</strong>: Broken CSS and JavaScript files can cause your website to look broken or behave incorrectly. Early detection helps you fix these issues before your users even notice.</li>
<li><strong>Maintain a Professional Appearance</strong>: Broken image links can make your website look unprofessional. Our enhanced check ensures that all visual elements are loading correctly, preserving your brand’s appearance.</li>
<li><strong>Boost SEO</strong>: Search engines penalize sites with broken links, including CSS, JS, and images. Keeping all your assets intact helps maintain your SEO ranking and keeps your site in good standing.</li>
</ul>
<h2 id="how-it-works">How it works <a href="#how-it-works" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#how-it-works">#</a></h2>
<p>The enhanced broken links check doesn't require any additional setup other than just enabling the options from your broken links settings. Here’s how it benefits you:</p>
<ol>
<li><strong>Automatic Scanning</strong>: As part of your regular monitoring, Oh Dear will automatically check all your website’s links, including CSS, JavaScript, and images.</li>
<li><strong>Detailed Reporting</strong>: If we find any broken links, we’ll notify you immediately with detailed reports specifying which files are affected and the exact URLs that are broken.</li>
<li><strong>Proactive Maintenance</strong>: Use the insights from our enhanced checks to quickly fix broken assets and keep your website running smoothly.</li>
</ol>
<h2 id="ready-to-check-your-entire-website">Ready to check your entire website? <a href="#ready-to-check-your-entire-website" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#ready-to-check-your-entire-website">#</a></h2>
<p>This enhancement makes Oh Dear’s monitoring more comprehensive and reliable, ensuring that every element of your website is working as intended. By proactively checking all types of links, you can stay ahead of issues and address them before they become a problem.</p>
<p>When paired with our other features such as Lighthouse you can get deep insights into how your application is performing from the user's perspective.</p>
]]>
            </summary>
                                    <updated>2024-12-06T14:51:44+00:00</updated>
        </entry>
            <entry>
            <title><![CDATA[In & outside business hour notifications]]></title>
            <link rel="alternate" href="https://ohdear.app/news-and-updates/in-outside-business-hour-notifications" />
            <id>https://ohdear.app/126</id>
            <author>
                <name><![CDATA[Oh Dear]]></name>
            </author>
            <summary type="html">
                <![CDATA[<p>Developers, marketing teams and business owners rely on Oh Dear to monitor their entire website and applications. We offer a feature-packed, all-in-one service with simple pricing - designed specifically for your peace of mind.</p>
<p>Oh Dear already has a flexible and powerful notification system and to date has performed over 38 billion checks and sent over 19 million notifications across Email, Slack, MS Teams, PagerDuty, webhook and more.</p>
<p>We respond to 30 different events across our services and all plans allow you to create an unlimited number of configurations (recipients) at the site, team and tag level!</p>
<h2 id="whats-new">What's new <a href="#whats-new" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#whats-new">#</a></h2>
<p>We are excited to introduce <strong>Business hours notifications</strong>! This feature allows teams to define specific business hours and tailor notification settings to ensure alerts are received at the right time by the right people. Whether you want to be notified during work hours, after-hours, or 24/7, you now have the flexibility to configure your notifications to fit your team's unique needs.</p>
<p><img src="/media/blog/a3PJS39gF8C5xDsLZDP6CxD3WOzxFuREHGE0rQcl.png" alt="" /></p>
<p>In this post we will dive into how it works, why it's useful and how it can help streamline your team's workflow.</p>
<h2 id="why">Why? <a href="#why" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#why">#</a></h2>
<p>Not every alert needs immediate attention, especially if it occurs outside of your team’s active hours. By using Business Hours settings you can:</p>
<p><strong>Reduce alert fatigue</strong>: by only receiving relevant notifications when needed.</p>
<p><strong>Improve work-life balance</strong>: by reducing disruptions outside of scheduled hours.</p>
<p><strong>Streamline on-call responsibilities</strong>: by distinguishing between notifications during business hours and those meant for after-hours or on-call teams.</p>
<p><strong>Improve response times for critical issues</strong>: ensure the right notifications reach the right people as quickly as possible.</p>
<p>This new feature empowers your team to stay focused during working hours while keeping the after-hours alert volume in check. It also provides peace of mind by ensuring that critical issues are still flagged when necessary — whether inside or outside of business hours.</p>
<h2 id="how-it-works">How it works <a href="#how-it-works" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#how-it-works">#</a></h2>
<p>When creating a new notification configuration you will now have the option for 'Notification timing'. You will see the following three options:</p>
<ol>
<li>Any time</li>
<li>Within business hours only</li>
<li>Outisde business hours only</li>
</ol>
<p><img src="/media/blog/QPPW62inwA8GtL1rVX7y86uQLVEQuOFbhR5Vkiyf.png" alt="" /></p>
<p>To give you a better idea of how business hours can be used across your team alongside existing features here are a few practical scenarios:</p>
<p>Your support team might be comfortable handling problems during work hours while an on-call team is responsible for handling critical issues outside these hours.</p>
<ul>
<li>Support team: email to a shared inbox only during business hours for broken link events only</li>
<li>On-call team: SMS outside of business hours with negative events only (e.g. Site down, scheduled task not executed on time)</li>
<li>Development team: all events enabled on low-priority Slack channel at all times</li>
</ul>
<h2 id="snooze">Snooze <a href="#snooze" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#snooze">#</a></h2>
<p>Are you receiving too many notifications? Are you working on fixing an issue and need to focus?</p>
<p>You can temporarily silence (or snooze) notifications. We will still perform all checks, but won't send any notifications when a snooze is active. A snooze is applied at the check-level (e.g. Lighthouse reports) for a particular site. It silences all notifications configured that would normally be sent.</p>
<p>If you decide a snooze isn't needed any more, you can end it from the 'snooze' tab on every check page.</p>
<h2 id="did-you-know">Did you know? <a href="#did-you-know" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#did-you-know">#</a></h2>
<p>We recently released support for <a href="https://ohdear.app/news-and-updates/tag-notifications">tag notifications</a>! This feature allows you to keep your notification settings under control by assigning tags to sites and configuring your notifications to tags (or groups of tags).</p>
<p>The new business hours notifications feature is all about control and customization for teams of all sizes. Together with tag notifications, 'business hours' gives your team a granular but maintainable level of control over your monitoring alerts.</p>
<p>We can’t wait for you to try it out and see how it improves your workflow. Let us know your feedback, and as always, happy monitoring!</p>
<p>Not registered? <a href="https://ohdear.app/register">Sign up for a free 30-day trial</a> (no payment card required)</p>
]]>
            </summary>
                                    <updated>2024-10-17T11:07:40+00:00</updated>
        </entry>
            <entry>
            <title><![CDATA[New Broken links UI]]></title>
            <link rel="alternate" href="https://ohdear.app/news-and-updates/new-broken-links-ui" />
            <id>https://ohdear.app/125</id>
            <author>
                <name><![CDATA[Oh Dear]]></name>
            </author>
            <summary type="html">
                <![CDATA[<p>Keeping your website free of broken links is essential for good user experience and SEO - that's why Oh Dear monitors your entire website! Our broken links crawler will find and test all the links on your site and we keep going until we have checked everything!</p>
<p>In addition to alerting you when you need it we have given our Broken Links feature a UI makeover! With this update it's now easier than ever to identify and resolve broken links across your site. Here’s a quick introduction of what you can expect from the new interface.</p>
<h2 id="whats-new">What's new? <a href="#whats-new" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#whats-new">#</a></h2>
<h3 id="streamlined-results">Streamlined results</h3>
<p>We now show an overview of the crawl process where you can quickly see the total number of pages found, internal/external broken links, pages fixed from the last run as well as any links that have been removed since the last report.</p>
<p><img src="/media/blog/zCrDymGtpzUwRJmAs9W8Oo9hWKYrXvqs40nrUn83.png" alt="" /></p>
<h3 id="enhanced-searching">Enhanced searching</h3>
<p>You could always download your results in Excel or CSV format but you can now search all links by error code (404, 500, etc.), URL or parent URL in the UI. It's one less thing to have to do to find the pages you need!
<img src="/media/blog/vkzLCaB2L90c7EPzM7NxNsO18e1rGobp0BNyqcuA.png" alt="" /></p>
<h3 id="link-diffs">Link Diffs</h3>
<p>Any links that were found on the current and previous runs will now be compared to show any changes to status code, link text, or parent URL. In the screenshot below we can see an 'About' page was found on the homepage but responded with a 404. The 'fixed' tab now shows the new successful response code along with the updated link text.</p>
<p><img src="/media/blog/w4TVQiV2zWker8c0gRnqSMvt1ex8QbUDZsDkgf9a.png" alt="" /></p>
<p>Managing broken links is now more intuitive and efficient which means you can focus on fixing issues and optimising SEO. Whether you’re a solo developer or running a large e-commerce platform our revamped Broken Links crawler is designed to help you maintain a smooth and error-free browsing experience for your audience.</p>
<h2 id="dont-forget-to-check-out-our-sitemap-service">Don’t forget to check out our sitemap service! <a href="#dont-forget-to-check-out-our-sitemap-service" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#dont-forget-to-check-out-our-sitemap-service">#</a></h2>
<p>In addition to our broken links crawler we also offer <a href="https://ohdear.app/features/sitemap-monitoring">powerful sitemap monitoring</a>. This feature ensures the format of your sitemap is valid and any other issues that might prevent search engines from properly indexing your site. We also scan every link to ensure they are valid and accessible.</p>
<p>If you're already using the broken links service then the sitemap monitoring is a perfect complement. It ensures that every link on your site - whether it’s in your sitemap or found organically - meets the same high standards.</p>
<p>Try it out today and keep your site working perfectly from every angle with a <a href="https://ohdear.app/register">free 30-day trial</a>. Let us know what you think!</p>
]]>
            </summary>
                                    <updated>2024-10-14T08:40:41+00:00</updated>
        </entry>
            <entry>
            <title><![CDATA[Tag notifications]]></title>
            <link rel="alternate" href="https://ohdear.app/news-and-updates/tag-notifications" />
            <id>https://ohdear.app/121</id>
            <author>
                <name><![CDATA[Oh Dear]]></name>
            </author>
            <summary type="html">
                <![CDATA[<p>At Oh Dear, we’re always looking for ways to make web monitoring easier and more efficient for our users. That’s why we’re excited to introduce Tag Notifications, a new feature that expands our notification options beyond site and team-level notifications.</p>
<h2 id="why">Why? <a href="#why" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#why">#</a></h2>
<p>For our larger customers using tags is an effective way to identify and manage multiple clients, projects, or environments. However, managing notifications by site or team can be too broad or too granular.</p>
<p>For example, some sites might be missed when creating/updating notification channels for a particular department or client meaning some sites might be notifying an out-of-date recipient.</p>
<p>With Tag Notifications you can now define notifications channels to a tag, ensuring new sites or updates to notification configurations are not missed. Your notifications will always be up-to-date.</p>
<h2 id="how-it-works">How It Works <a href="#how-it-works" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#how-it-works">#</a></h2>
<p>Setting up Tag Notifications is simple:</p>
<ol>
<li>Click 'Manage Tags' from the main user-menu.</li>
<li>You should see all tags defined for your team along with a summary showing the number of sites and notification channels associated with each tag.</li>
<li>Click on the 'Notifications' link to define the notification settings for each tag. You can specify the channels, recipients, and types of alerts for that tag just like you would for a site or team notification.</li>
</ol>
<p>Now when a site is tagged it automatically inherits the tag’s notification settings so nothing gets missed :)</p>
<p><img src="/media/blog/v511cCn0RX3De4Wev4CkqO8uqxuUrpgTY97YQBfO.png" alt="" /></p>
<h3 id="tag-groups">Tag groups</h3>
<p>You can also create 'tag groups'. Tag groups are perfect for larger team - apply notification settings to multiple related tags at once. Tag group notifications are applied when a site has all of the tags in the group, for example:</p>
<p>Label: 'Client production sites'
Tags: <code>client</code>, <code>production</code></p>
<p>Only sites with both the <code>client</code> and <code>production</code> tags will receive the configured notifications.</p>
<h3 id="wildcards">Wildcards</h3>
<p>Oh Dear also supports wildcard matching for tag groups. This is particularly useful when you have a naming convention for your tags and want to create rules that apply to a range of tags.</p>
<p>For example:</p>
<ul>
<li>A wildcard tag <code>region-*</code> would match tags like <code>region-eu</code>, <code>region-us</code>, etc.</li>
<li><code>*-production</code> would match tags like <code>client1-production</code>, <code>project2-production</code>, etc.</li>
</ul>
<p>To use a wildcard:</p>
<ol>
<li>When setting up a tag notification, enter your wildcard pattern (e.g., <code>production*</code>) in the tag selection field.</li>
<li>Oh Dear will interpret this as a wildcard and match it against your existing tags.</li>
</ol>
<p>Tag notifications provide a powerful way to organize and manage your alerts, especially when dealing with a large number of sites with different requirements. By effectively using tags, tag groups, and wildcards, you can create a notification system that's both flexible and precise.</p>
<h3 id="examples">Examples</h3>
<p>You can a arrange your tags in any way that suits your team's workflow. Here are some examples of how you might structure your tags, groups and notifications:</p>
<ul>
<li>Site 1: <code>production</code> <code>region-eu</code> <code>client</code> <code>spatie</code> <code>app</code></li>
<li>Site 2: <code>production</code> <code>region-us</code> <code>client</code> <code>laravel</code> <code>app</code></li>
<li>Site 3: <code>staging</code> <code>region-us</code> <code>internal</code> <code>marketing</code> <code>project-a</code></li>
<li>Site 4: <code>staging</code> <code>region-us</code> <code>internal</code> <code>app</code> <code>project-a</code></li>
<li>Site 5: <code>staging</code> <code>region-eu</code> <code>internal</code> <code>app</code> <code>project-a</code></li>
</ul>
<hr />
<p><strong>Client production</strong>: <code>production</code> <code>client</code></p>
<p>Defines all 'client production' sites to send alerts to a high-priority internal Slack channel, email the account manager and the customer point of contact. This ensures your team responds quickly to any critical issues.</p>
<hr />
<p><strong>Client non-prod</strong>: <code>development</code> <code>client</code></p>
<p>Set all 'non-prod client' sites to send alerts to an internal developer Slack channel only.</p>
<hr />
<p><strong>Marketing projects</strong>: <code>internal</code> <code>production</code> <code>marketing</code> <code>project-*</code></p>
<p>Delivers all marketing projects to your marketing group email.</p>
<hr />
<p><strong>Internal important</strong>: <code>internal</code> <code>app</code> <code>production</code></p>
<p>Send a PagerDuty message to your SRE team for internal production services and an SMS to your US-based on-call team.</p>
<h2 id="api">API <a href="#api" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#api">#</a></h2>
<p>We have added new API routes to manage tag notifications via the <a href="https://ohdear.app/docs/integrations/the-oh-dear-api#notification-destinations">Oh Dear API</a>.</p>
<h2 id="keep-notifications-up-to-date">Keep notifications up-to-date <a href="#keep-notifications-up-to-date" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#keep-notifications-up-to-date">#</a></h2>
<p>Tag Notifications provide a more flexible and efficient way to handle alerts, especially for teams managing multiple sites across various clients or environments. By defining notifications at the tag level, you save time, reduce the risk of missed alerts, and ensure the right people are always informed.</p>
<p>Ready to get started with Tag Notifications? Head over to the 'Manage Tags' section of your team settings to start.</p>
<p>If you have any questions or issues feel free to reach out to our support team.</p>
]]>
            </summary>
                                    <updated>2024-10-08T10:53:20+00:00</updated>
        </entry>
            <entry>
            <title><![CDATA[New feature: Status Page Update Templates]]></title>
            <link rel="alternate" href="https://ohdear.app/news-and-updates/new-feature-status-page-update-templates" />
            <id>https://ohdear.app/120</id>
            <author>
                <name><![CDATA[Oh Dear]]></name>
            </author>
            <summary type="html">
                <![CDATA[<p>We know how crucial it is to provide clear and timely communication during incidents or downtime. That’s why we’re excited to introduce Status Page Update Templates - a new feature that makes it even easier to keep your users informed while maintaining a consistent message.</p>
<h2 id="why-use-a-status-page">Why use a status page <a href="#why-use-a-status-page" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#why-use-a-status-page">#</a></h2>
<p>A status page is a dedicated, Oh Dear hosted site where you can communicate the current status of your website or service to your users by posting status updates. It's a simple yet powerful tool for transparency and trust-building. Here’s why you should consider having one.</p>
<ul>
<li><strong>Keep Users Informed</strong>: Communicate issues, such as downtime, slow performance, or scheduled maintenance, directly to your users. Instead of leaving them guessing or frustrated, they’ll know exactly what’s going on.</li>
<li><strong>Reduce Support Overhead</strong>: Proactively provide information and reduce the number of support tickets when issues arise.</li>
<li><strong>Build Trust</strong>: Transparency is key. Being open about issues builds trust with your users - a status page demonstrates that you are committed to keeping them in the loop.</li>
<li><strong>Improve User Experience</strong>: Users are more understanding of problems when they know what's happening and what to expect. By managing expectations you show that you’re on top of the situation.</li>
<li><strong>Centralised Communication</strong>: During an incident, a status page acts as a single source of truth, where all updates are published in real time. This ensures consistent messaging across all your communication channels.</li>
</ul>
<h2 id="why-use-status-page-update-templates">Why Use Status Page Update Templates? <a href="#why-use-status-page-update-templates" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#why-use-status-page-update-templates">#</a></h2>
<p>Things go wrong. While there is pressure to resolve the issue, there is also pressure to communicate quickly and accurately. With our new templates, you can:</p>
<ul>
<li><strong>Ensure Consistent Messaging</strong>: Keep your communications clear and consistent, reducing confusion for your users.</li>
<li><strong>Save Time During High-Pressure Situations</strong>: Apply a pre-made template, fill in the specifics, and publish your update in moments.</li>
<li><strong>Reduce Stress</strong>: No need to worry about drafting the perfect message; simply use a template and modify placeholders as needed.</li>
</ul>
<p><img src="/media/blog/O2KpXX9eDO07BRcEM8l9sh7o0naPqcZC1n9iVIyZ.png" alt="" /></p>
<h2 id="key-features">Key Features <a href="#key-features" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#key-features">#</a></h2>
<p>Here’s why we think Status Page Update Templates are awesome.</p>
<ul>
<li><strong>Named Templates</strong>: Create templates that are easy to find and apply.</li>
<li><strong>Flexible Fields</strong>: Not all fields are required—you can save a template with just a description or any other combination that suits your needs.</li>
<li><strong>Quick Apply</strong>: Simply select a template when creating a new update then make any necessary adjustments before publishing.</li>
<li><strong>API Support</strong>: Manage your templates with new API endpoints for listing, creating, updating and deleting templates.</li>
</ul>
<h2 id="create-your-first-template">Create your first template <a href="#create-your-first-template" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#create-your-first-template">#</a></h2>
<p>Getting started with Status Page Update Templates is easy:</p>
<ol>
<li>Go to the 'Status Pages' section in your Oh Dear dashboard menu and select a status page to edit.</li>
<li>Select 'Status updates' in the sidebar.</li>
<li>Select 'Templates' tab.</li>
<li>Select 'Create Template'.</li>
<li>Fill in the name and any title, description, text, and severity level you need to use.</li>
<li>Click 'Save Template'.</li>
</ol>
<p>When an incident occurs, you can select a template from the list while creating a status update, and modify any placeholders as needed.</p>
<p><img src="/media/blog/QMGeshDqDwj0P7rKEaOfZZviXmJoXHrNl1m3FXst.png" alt="" /></p>
<h2 id="api">API <a href="#api" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#api">#</a></h2>
<p>If you're looking to automate your status updates, we’ve got you covered! The Oh Dear API now supports managing status page templates with the following endpoints:</p>
<ul>
<li><strong>List Templates</strong>: Fetch all saved templates.</li>
<li><strong>Create Templates</strong>: Add a new template with the desired fields.</li>
<li><strong>Update Templates</strong>: Modify existing templates.</li>
<li><strong>Delete Templates</strong>: Remove templates you no longer need.</li>
</ul>
<p>Read the full <a href="/docs/integrations/the-oh-dear-api#status-pages">API documentation</a> to help you start managing your template integrations!</p>
<h2 id="examples">Examples <a href="#examples" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#examples">#</a></h2>
<p>View our <a href="https://ohdear.app/news-and-updates/incident-template-library">incident libray</a> to find example status updates to copy into your templates.</p>
<h2 id="a-more-user-friendly-approach">A More User-Friendly Approach <a href="#a-more-user-friendly-approach" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#a-more-user-friendly-approach">#</a></h2>
<p>We believe this new feature will make it easier for you to communicate effectively with your users. Whether you’re a small team managing one site or a large team overseeing multiple domains, having a set of templates ready can make all the difference during critical moments.</p>
]]>
            </summary>
                                    <updated>2024-10-16T09:32:14+00:00</updated>
        </entry>
            <entry>
            <title><![CDATA[Incident Template Library]]></title>
            <link rel="alternate" href="https://ohdear.app/news-and-updates/incident-template-library" />
            <id>https://ohdear.app/124</id>
            <author>
                <name><![CDATA[Oh Dear]]></name>
            </author>
            <summary type="html">
                <![CDATA[<p>We recently announced a new feature to enhance how you communicate with your users during maintenance, incidents, and general service updates. <a href="https://ohdear.app/news-and-updates/new-feature-status-page-update-templates">Status Page Templates</a> allows you to save and re-use status updates - but how do you know what incidents might happen or what updates you need to keep users informed about until it's too late?</p>
<p>We have put together a library of ready-to-use templates designed to keep your users informed with clear, concise and consistent messaging.</p>
<ul>
<li><a href="#maintenance">Maintenance</a></li>
<li><a href="#incidents">Incidents</a></li>
<li><a href="#performance">Performance</a></li>
<li><a href="#informational">Informational</a></li>
</ul>
<h2 id="maintenance">Maintenance <a href="#maintenance" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#maintenance">#</a></h2>
<h3 id="maintenance-1-scheduled">Maintenance - 1. Scheduled</h3>
<p>Title: Scheduled maintenance</p>
<p>We will be performing scheduled maintenance on [date] from [start time] to [end time] UTC. During this period [services] may experience [brief outages/slower performance/limited access] in the following regions: [regions]. We apologise for any inconvenience.</p>
<h3 id="maintenance-2-started">Maintenance - 2. Started</h3>
<p>Title: Maintenance In Progress</p>
<p>Scheduled maintenance has started as planned. [Service] may be temporarily unavailable during this time. We are working to complete the maintenance as quickly as possible. Thank you for your patience.</p>
<h3 id="maintenance-3-completed">Maintenance - 3. Completed</h3>
<p>Title: Maintenance Completed</p>
<p>The scheduled maintenance has been successfully completed. All services are now fully operational. Thank you for your patience.</p>
<h3 id="maintenance-4-delayed">Maintenance - 4. Delayed</h3>
<p>Title: Maintenance Delayed</p>
<p>The scheduled maintenance has been delayed and will now occur at [time] UTC. We apologize for any inconvenience this delay may cause.</p>
<h3 id="maintenance-5-extended">Maintenance - 5. Extended</h3>
<p>Title: Maintenance Extended</p>
<p>The ongoing maintenance is taking longer than expected and will be extended until [time] UTC. We are working diligently to complete the process as soon as possible. Thank you for your continued patience.</p>
<h2 id="incidents">Incidents <a href="#incidents" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#incidents">#</a></h2>
<h3 id="incident-1-under-investigation">Incident - 1. Under Investigation</h3>
<p>Title: Investigating Issues</p>
<p>We are aware of an issue impacting [service] and are currently investigating. Updates will be provided as soon as more information is available.</p>
<h3 id="incident-2-issues-detected">Incident - 2. Issues Detected</h3>
<p>Title: Service Issues Detected</p>
<p>We have detected issues affecting [service] and some users may be experiencing slower response times. Our team is working to identify the cause and will provide updates as more information becomes available.</p>
<h3 id="incident-3-identified">Incident - 3. Identified</h3>
<p>Title: Issue Identified</p>
<p>We have identified the cause of the issue affecting [service] and are working on a fix. We will provide an update once we have more information.</p>
<h3 id="incident-4-partial-outage">Incident - 4. Partial Outage</h3>
<p>Title: Partial Service Outage</p>
<p>We are experiencing a partial outage affecting [service]. Our team is actively working to restore full service. Thank you for your patience.</p>
<h3 id="incident-5-major-outage">Incident - 5. Major Outage</h3>
<p>Title: Major Service Outage</p>
<p>We are currently experiencing a major outage affecting [service] in [region]. Our team is working urgently to restore service. We will provide updates as soon as we have more information.</p>
<h3 id="incident-6-monitoring">Incident - 6. Monitoring</h3>
<p>Title: Monitoring the Situation</p>
<p>A fix has been implemented, and we are currently monitoring the system to ensure stability. [services] should be operating normally, but we will continue to observe performance closely.</p>
<h3 id="incident-7-resolved">Incident - 7. Resolved</h3>
<p>Title: Incident Resolved</p>
<p>The issue affecting [service] has been resolved as of [time] (UTC). All services are now operational. We apologize for any inconvenience caused and thank you for your patience.</p>
<p>You can view the full incident report <a href="https://">here</a>.</p>
<h2 id="performance">Performance <a href="#performance" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#performance">#</a></h2>
<h3 id="performance-1-degradation-detected">Performance - 1. Degradation Detected</h3>
<p>Title: Performance Degradation Detected</p>
<p>We are currently investigating reports of degraded performance affecting [service]. Our team is working to identify the cause and mitigate the impact. We will provide updates as soon as possible.</p>
<h3 id="performance-2-identified">Performance - 2. Identified</h3>
<p>Title: Performance Issue Identified</p>
<p>We have identified the cause of the performance degradation affecting [service] and are working on a fix. Updates will be provided as we make progress.</p>
<h3 id="performance-3-monitoring-improvement">Performance - 3. Monitoring Improvement</h3>
<p>Title: Monitoring Performance Improvement</p>
<p>A fix has been implemented to address the performance issues. We are monitoring the system to ensure performance is fully restored. Thank you for your patience.</p>
<h3 id="performance-4-resolved">Performance - 4. Resolved</h3>
<p>Title: Performance Restored</p>
<p>The performance issues affecting [service] have been resolved. All services are now operating normally. We appreciate your patience during this time.</p>
<h3 id="informational">Informational</h3>
<h3 id="info-1-service-update">Info - 1. Service Update</h3>
<p>Title: Service Update</p>
<p>We have rolled out an update to [affected services]. This update includes [brief description of changes or improvements]. All services are operating normally.</p>
<h3 id="info-2-new-feature">Info - 2. New Feature</h3>
<p>Title: New Feature Available</p>
<p>We are excited to announce a new feature for [services]: [feature name/description]. This feature will [briefly describe benefit]. Thank you for using our services!</p>
<p>You can view the full release notes in <a href="https://">our blog post</a>.</p>
<h3 id="info-3-announcement">Info - 3. Announcement</h3>
<p>Title: General Announcement</p>
<p>[Service] announcement: [description]. This will not impact service availability. Thank you for your attention.</p>
<hr />
<p>We hope these new status page templates improve your communication processes and enhance your users' experience during service updates, incidents, and more.</p>
<p>Ready to try out the new templates? Head over to your status page settings and explore the options available. As always, we’re here to help - if you have any questions or need assistance, feel free to reach out to our support team.</p>
<p>Happy communicating!</p>
]]>
            </summary>
                                    <updated>2024-09-03T09:46:57+00:00</updated>
        </entry>
            <entry>
            <title><![CDATA[Update to Microsoft Teams Notifications]]></title>
            <link rel="alternate" href="https://ohdear.app/news-and-updates/update-to-microsoft-teams-notifications" />
            <id>https://ohdear.app/118</id>
            <author>
                <name><![CDATA[Oh Dear]]></name>
            </author>
            <summary type="html">
                <![CDATA[<p>Oh Dear offers several ways to keep you updated on important events like downtime, performance and DNS changes, broken links, Lighthouse issues, and more.</p>
<p>By default, you will get email notifications to the email address you used to sign up. But you can also choose to receive alerts to your preferred platform. A popular choice for many of our users is Microsoft Teams.</p>
<p>But with the recent announcement from Microsoft about retiring the Office 365 Connectors within Microsoft Teams we had to provide a new way for you to connect with minimal effort.</p>
<h2 id="why-the-change">Why the Change? <a href="#why-the-change" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#why-the-change">#</a></h2>
<p>Microsoft is phasing out Office 365 Connectors in Teams, which many of you relied on to receive updates directly in your channels. To ensure you continue to receive real-time alerts and notifications about your website's status, we have deployed an updated integration tailored for Microsoft Teams workflow apps!</p>
<h2 id="how-to-set-up-the-new-integration">How to Set Up the New Integration <a href="#how-to-set-up-the-new-integration" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#how-to-set-up-the-new-integration">#</a></h2>
<p>This update does not contain any changes to Microsoft Teams notifications content but it will allow you to migrate easily from Office 365 connectors to workflow apps.</p>
<p>The new update is as simple as creating a new workflow app with a 'post to channel' webhook event and using that unique URL in your Oh Dear notification conifguration. That’s it! Now your team will receive all critical updates right where you work.</p>
<p>Your current Teams notifications will continue working until Microsoft removes support on <strong>December 31st, 2024</strong>. But you can configure a new notification channel in Oh Dear and run the two side-by-side until you are ready to switch completly. Then you can delete the old connector!</p>
<p>Head over to the <a href="/docs/notifications/microsoft-teams">documentation</a> for a step-by-step guide on how to handle the migration :)</p>
<h2 id="stay-informed">Stay Informed! <a href="#stay-informed" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#stay-informed">#</a></h2>
<p>We know how important it is for you to stay on top of any potential issues with your website. Our new integration ensures that even with the changes from Microsoft, you’ll continue to receive reliable, real-time updates.</p>
<p>Got questions or need help setting up the new integration? Reach out to our support team – we are happy to help!</p>
]]>
            </summary>
                                    <updated>2024-08-30T10:28:07+00:00</updated>
        </entry>
            <entry>
            <title><![CDATA[History Filters]]></title>
            <link rel="alternate" href="https://ohdear.app/news-and-updates/history-filters" />
            <id>https://ohdear.app/119</id>
            <author>
                <name><![CDATA[Oh Dear]]></name>
            </author>
            <summary type="html">
                <![CDATA[<p>We know that keeping track of downtime and other failures across multiple sites can be a challenge, especially for larger teams. That’s why we’ve added  filters to the 'History' page and introduced 'Team History' to make monitoring easier and more efficient!</p>
<h2 id="whats-new">What’s New? <a href="#whats-new" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#whats-new">#</a></h2>
<h3 id="site-history-filters">Site History Filters</h3>
<p>Our updated History page now includes powerful filters that allow you to filter by:</p>
<ul>
<li><strong>Time Period</strong>: Narrow down incidents to specific days, weeks, or months.</li>
<li><strong>Check Type</strong>: Focus on specific types of checks such as uptime, performance, sitemaps and more.</li>
<li><strong>Result</strong>: Quickly find all failures, warnings or successful runs.</li>
</ul>
<p>These filters give you the flexibility to quickly find the information you need, helping you identify patterns or recurring issues more effectively.</p>
<h3 id="team-history">Team History</h3>
<p>For larger teams, seeing the big picture across all monitored sites is crucial. Our new 'Team History' view provides an overview of <strong>all check failures</strong> across the sites that your team has access to along with the same filter options as site history.</p>
<p><img src="/media/blog/HFjm8IOwP40KN148Pu7P6zLfIeYpXf33sbhG2lvJ.png" alt="" /></p>
<h2 id="why">Why? <a href="#why" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#why">#</a></h2>
<ul>
<li><strong>Streamlined Monitoring</strong>: No more sifting through irrelevant data  - get to the information that matters quickly.</li>
<li><strong>Better Team Collaboration</strong>: By ensuring each user sees only the sites they have access to, teams can focus on their responsibilities without distractions.</li>
<li><strong>Improved Incident Response</strong>: Quickly identify which sites were affected and what issues are recurring, allowing for more effective problem-solving.</li>
</ul>
<h2 id="how-it-works">How it works <a href="#how-it-works" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#how-it-works">#</a></h2>
<p>Getting started with these new features is simple:</p>
<ol>
<li>Navigate to the <strong>History</strong> page from your Oh Dear dashboard to view team history, or the <strong>History</strong> component from a site's sidebar menu to view site history.</li>
<li>Use the new <strong>filter options</strong> at the top of the page to customize your view by time period, check type, or specific sites.</li>
<li>Copy the link to your team so you can share it with other members of the team.</li>
</ol>
<h2 id="built-with-larger-teams-in-mind">Built with Larger Teams in Mind <a href="#built-with-larger-teams-in-mind" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#built-with-larger-teams-in-mind">#</a></h2>
<p>These enhancements are particularly useful for teams managing multiple websites where staying on top of all incidents is essential. The new filters and Team History View make it easier to find and understand previous check runs.</p>
<p>We hope these new tools will help make monitoring even more effective for your team!</p>
<p>If you have any questions or feedback, don't hesitate to reach out to our support team.</p>
]]>
            </summary>
                                    <updated>2024-10-16T09:27:56+00:00</updated>
        </entry>
            <entry>
            <title><![CDATA[Sitemap monitoring is now available at Oh Dear]]></title>
            <link rel="alternate" href="https://ohdear.app/news-and-updates/sitemap-monitoring-is-now-available-at-oh-dear" />
            <id>https://ohdear.app/117</id>
            <author>
                <name><![CDATA[Oh Dear]]></name>
            </author>
            <summary type="html">
                <![CDATA[<p>Oh Dear can perform <a href="https://ohdear.app/docs/features/what-checks-does-oh-dear-perform">many checks</a>: uptime, broken links, scheduled jobs, DNS, and much more. We proud to announce that we’ve added a new check: sitemap monitoring. This check will make sure that the structure of your sitemap is correct. We’ll also check if each and every link it links to a page that returns a correct response.</p>
<p>Whenever we detect a problem, you’ll be notified via one of our many supported notification channels.</p>
<h2 id="what-is-a-sitemap">What is a sitemap? <a href="#what-is-a-sitemap" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#what-is-a-sitemap">#</a></h2>
<p>Sitemaps play an essential role in SEO. Having sitemaps with no errors helps to ensure search engines index your web pages correctly. With sitemap monitoring, you are assured you will be informed when your sitemap(s) condition degrades.</p>
<p>Technically, a sitemap is a file where you can list the web pages of your site to tell Google and other search engines about the organization of your site content. Search engine web crawlers like Googlebot read this file to more intelligently crawl your site.</p>
<h2 id="how-oh-dear-monitors-your-sitemap">How Oh Dear monitors your sitemap <a href="#how-oh-dear-monitors-your-sitemap" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#how-oh-dear-monitors-your-sitemap">#</a></h2>
<p>We can monitor your sitemap by checking if it's available and if the structure is valid. We will also visit each URL in the sitemap to check if it's available and if it returns a response code in the 200 or 300 range.</p>
<p>We will check the following things when monitoring your sitemap. We will check if:</p>
<ul>
<li>the sitemap is available at the specified URL</li>
<li>visit each URL in the sitemap to check if it's available and if it returns a response code in the 200 or 300 range</li>
<li>all URLs are unique</li>
<li>the sitemap doesn't contain more than 50,000 URLs</li>
<li>the priority of each item is between 0 and 1</li>
<li>the frequency of each item is one of these values: <code>always</code>, <code>hourly</code>, <code>daily</code>, <code>weekly</code>, <code>monthly</code>, <code>yearly</code>, <code>never</code></li>
<li>last modified of each item is not in the future</li>
</ul>
<p>If you are using <a href="https://developers.google.com/search/docs/crawling-indexing/sitemaps/large-sitemaps">a sitemap index</a>, we will perform all these checks for all sitemaps mentioned in the index file. We also support gzipped sitemap indexes and sitemaps</p>
<h2 id="how-to-configure-oh-dear-to-monitor-your-site-map">How to configure Oh Dear to monitor your site map <a href="#how-to-configure-oh-dear-to-monitor-your-site-map" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#how-to-configure-oh-dear-to-monitor-your-site-map">#</a></h2>
<p>When you create a new site in Oh Dear, you can toggle on the sitemap check. Let's try it out by monitoring the semrush.com domain</p>
<p><img src="/media/blog/cFle23ql1P9YqarlVwwX0giDs6CtNvxXk6RZNKtz.png" alt="" /></p>
<p>We’ll try to check the sitemap located at <code>&lt;your-domain&gt;/sitemap.xml</code>. If your sitemap is saved somewhere else, you can specify a custom URL in the sitemap check settings.</p>
<p>Oh Dear will now check the structure of your sitemap and crawl all links. When the checking process is finished, you’ll see a report like this one.</p>
<p><img src="/media/blog/VcFOA1GMZFK0l6pUlXMABcnLgZjAeNKWWriH24KB.png" alt="" /></p>
]]>
            </summary>
                                    <updated>2024-07-01T11:43:11+00:00</updated>
        </entry>
            <entry>
            <title><![CDATA[Improving our broken link tests]]></title>
            <link rel="alternate" href="https://ohdear.app/news-and-updates/improving-our-broken-link-tests" />
            <id>https://ohdear.app/116</id>
            <author>
                <name><![CDATA[Oh Dear]]></name>
            </author>
            <summary type="html">
                <![CDATA[<p>One of the most unique features Oh Dear offers is the broken links and mixed content crawler. We will crawl your site for all links, reporting any broken pages to your defined <a href="https://ohdear.app/docs/notifications/notifications">notification channels</a>.</p>
<p>Recently, we encountered degraded performance with our crawler service due to a breaking change in an underlying library called Guzzle. This caused HTTP 505 responses on the first page of each site, blocking further crawling and resulting in false positive reports. Although our uptime service showed everything was okay, some customers were incorrectly informed about unreachable pages in their broken links report.</p>
<h2 id="making-sure-this-problem-does-not-happen-again">Making sure this problem does not happen again <a href="#making-sure-this-problem-does-not-happen-again" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#making-sure-this-problem-does-not-happen-again">#</a></h2>
<p>We have over 1,200 tests that run on GitHub Actions with every pull request. To make sure all these tests are executed fast, we mocked all requests that happen inside our broken links and mixed content tests. Although this results in fast tests and  consistent results (no network or external conditions), it missed this particular issue because that particular issue only occurred when making real network requests.</p>
<p>We added tests that perform real HTTP requests using our crawler service to validate end-to-end functionality under real-world conditions. This way, we'll make sure we don't hit this particular bug again.</p>
<p>Additonally, we posted a status update and responded to support tickets to inform users of the issue and began our investigation.</p>
]]>
            </summary>
                                    <updated>2024-06-09T10:48:27+00:00</updated>
        </entry>
            <entry>
            <title><![CDATA[You can now manage notification preferences via our API]]></title>
            <link rel="alternate" href="https://ohdear.app/news-and-updates/you-can-now-manage-notification-preferences-via-our-api" />
            <id>https://ohdear.app/115</id>
            <author>
                <name><![CDATA[Oh Dear]]></name>
            </author>
            <summary type="html">
                <![CDATA[<p>Our service can detect various problems with your website: whenever it is down, or a broken link is detected, your cron job isn't running on time, and <a href="https://ohdear.app/docs/features/what-checks-does-oh-dear-perform">much much more</a>. Whenever we see a problem we can notify you via email, Slack, webhooks, <a href="https://ohdear.app/docs/notifications/notifications">and various other channels</a>.</p>
<p>Up until now, you could configure these notification channels in our UI, but now you can do this via our API as wel.</p>
<h2 id="whats-new">What's new? <a href="#whats-new" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#whats-new">#</a></h2>
<p>Our comprehensive API documentation provides all the details you need to start <a href="https://ohdear.app/docs/integrations/the-oh-dear-api#notification-destinations">managing your notification channels</a> along with example payloads for each type of channel and event.</p>
<p>With this update it is now easier than ever to automate the applications you manage. For example, you could ensure a client email address or phone number is added to each site created and trigger a test notification once setup is complete.</p>
<p>The request payload is the same as the response body which makes it easy to copy your existing configurations across sites.</p>
<h2 id="examples">Examples <a href="#examples" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#examples">#</a></h2>
<p>Create a new email configuration for site <code>siteId</code> and send a test notification. It will respond to only two events (site down and recovered).</p>
<p><code>/api/sites/{siteId}/notification-destinations</code></p>
<pre data-lang="json" class="notranslate"><span class="hl-property">{</span>
  <span class="hl-keyword">&quot;test&quot;</span>: true,
  <span class="hl-keyword">&quot;channel&quot;</span>: <span class="hl-value">&quot;mail&quot;</span>,
  <span class="hl-keyword">&quot;destination&quot;</span>: <span class="hl-property">{</span>
      <span class="hl-keyword">&quot;mail&quot;</span>: <span class="hl-value">&quot;example@example.com&quot;</span>
   <span class="hl-property">}</span>,
  <span class="hl-keyword">&quot;notification_types&quot;</span>: <span class="hl-property">[</span>
      <span class="hl-value">&quot;UptimeCheckRecoveredNotification&quot;</span>,
      <span class="hl-value">&quot;UptimeCheckFailedNotification&quot;</span>,
   <span class="hl-property">]</span>
<span class="hl-property">}</span>
</pre>
<p>Update an existing team-level configuration (<code>teamId</code> and <code>destinationId</code>) to now use webhooks and respond to all Oh Dear events. This example will <em>not</em> send a test notification.</p>
<p><code>/api/team-notification-destinations/{teamId}/destination/{destinationId}</code></p>
<pre data-lang="json" class="notranslate">  <span class="hl-keyword">&quot;channel&quot;</span>: <span class="hl-value">&quot;webhooks&quot;</span>,
  <span class="hl-keyword">&quot;destination&quot;</span>: <span class="hl-property">{</span>
    <span class="hl-keyword">&quot;url&quot;</span>: <span class="hl-value">&quot;https://your-webhook-url&quot;</span>
  <span class="hl-property">}</span>
</pre>
<h2 id="did-you-know">Did you know? <a href="#did-you-know" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#did-you-know">#</a></h2>
<p>You can also manage maintenance periods, post a status page message, enable/disable checks and much more <a href="https://ohdear.app/docs/integrations/the-oh-dear-api">using the API</a>. There is also a <a href="https://ohdear.app/docs/integrations/the-oh-dear-php-sdk">PHP SDK</a> and many <a href="https://ohdear.app/docs/integrations/3rd-party-integrations-of-oh-dear">third-party integrations</a> ready to help you get started.</p>
<p>We’re excited to see how you’ll leverage this new feature to enhance your monitoring experience. Let us know what you are building or if you have any questions.</p>
]]>
            </summary>
                                    <updated>2024-06-09T10:57:07+00:00</updated>
        </entry>
            <entry>
            <title><![CDATA[Enforcing Multi-Factor Authentication ]]></title>
            <link rel="alternate" href="https://ohdear.app/news-and-updates/enforcing-multi-factor-authentication" />
            <id>https://ohdear.app/113</id>
            <author>
                <name><![CDATA[Oh Dear]]></name>
            </author>
            <summary type="html">
                <![CDATA[<p>We are pleased to announce the introduction of team-level Multi-Factor Authentication (MFA). With our latest update, owners now have the ability to enforce MFA for all team members. This means you can ensure that every user accessing your account is taking the necessary steps to protect sensitive information.</p>
<p><img src="/media/blog/0RAMefdhvKlv0kLiwMcfQhOssufUiKFrO3mmzUxd.png" alt="" /></p>
<h2 id="how-it-works">How it works <a href="#how-it-works" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#how-it-works">#</a></h2>
<ol>
<li>As the team owner select 'Manage team' from the main menu</li>
<li>Navigate to the 'Security' tab and toggle 'Require two factor authentication' and hit 'save'</li>
<li>All users will now be required to enable MFA before continuing to your team's sites, status page and team admin pages</li>
</ol>
<p>If you have not yet enabled enabled two factor authentication you can still access your other teams that do not have this requirement as well as your personal settings.</p>
<h2 id="did-you-know">Did you know <a href="#did-you-know" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#did-you-know">#</a></h2>
<p>Even if you do not require MFA for your team you can still see which users have it enabled. See <a href="https://ohdear.app/news-and-updates/improving-our-multi-factor-authentication">our other recent updates</a> for more information.</p>
]]>
            </summary>
                                    <updated>2025-06-24T21:00:03+00:00</updated>
        </entry>
            <entry>
            <title><![CDATA[Improving our Multi-Factor Authentication]]></title>
            <link rel="alternate" href="https://ohdear.app/news-and-updates/improving-our-multi-factor-authentication" />
            <id>https://ohdear.app/111</id>
            <author>
                <name><![CDATA[Oh Dear]]></name>
            </author>
            <summary type="html">
                <![CDATA[<p>This week we introduced two new updates to enhance the user/team security administration. These updates focus on improving the Multi-Factor Authentication (MFA) setup and visibility for team management. Let’s take a look.</p>
<h3 id="1-strongview-your-mfa-secret-keystrong">1. <strong>View Your MFA Secret Key</strong></h3>
<p>Previously, users could set up MFA by scanning a QR code, which seamlessly integrates an authentication app. Now, in addition to the QR code, we provide the ability to view and copy the secret key directly.</p>
<p>This is especially useful if you prefer to manually input the secret into your app or need to back up your MFA details for safekeeping.</p>
<p><img src="/media/blog/kXly18qcEGLS3UwTqelltWtZpdMtN8XDbL3EBfKu.png" alt="" /></p>
<h3 id="2-strongcheck-mfa-status-of-team-membersstrong">2. <strong>Check MFA Status of Team Members</strong></h3>
<p>Team owners can now view a list of all team members with their MFA status clearly displayed. This feature allows for better security management and helps ensure that all accounts have an added layer of protection. This is a great tool for identifying compliance with internal security policies requiring MFA.</p>
<p><img src="/media/blog/VtY7Pm315QgypG1xYIlkFFhxq6xEjom3NuAgHRec.png" alt="" /></p>
<p>We hope you find these new features useful in managing your team’s security more effectively. As always, we are dedicated to improving your experience and welcome any feedback or suggestions you may have!</p>
]]>
            </summary>
                                    <updated>2024-04-22T14:38:38+00:00</updated>
        </entry>
            <entry>
            <title><![CDATA[Every second counts in our UI]]></title>
            <link rel="alternate" href="https://ohdear.app/news-and-updates/every-second-counts-in-our-ui" />
            <id>https://ohdear.app/112</id>
            <author>
                <name><![CDATA[Oh Dear]]></name>
            </author>
            <summary type="html">
                <![CDATA[<p>Downtime has always been shown in minutes, hours, and days but for shorter downtimes you would see &quot;0m&quot; even if the actual downtime was less than a minute. We've updated the UI to show downtime in seconds. This means no more manually calculating brief outages — you’ll see exactly how long the system was down :)</p>
<p><img src="/media/blog/2PONTUMhLYsttCfRq18bG56f9Y0ApQG6oCrxK95g.png" alt="" /></p>
<p>Did you know you can <a href="https://ohdear.app/news-and-updates/you-can-now-add-notes-to-downtime-periods">add notes to downtime periods</a>?</p>
]]>
            </summary>
                                    <updated>2024-04-22T14:38:24+00:00</updated>
        </entry>
            <entry>
            <title><![CDATA[Reducing PHP's CPU usage by nearly 40% by upgrading from Ubuntu 20.04 to 22.04]]></title>
            <link rel="alternate" href="https://ohdear.app/news-and-updates/reducing-phps-cpu-usage-by-nearly-40-by-upgrading-from-ubuntu-2004-to-2204" />
            <id>https://ohdear.app/110</id>
            <author>
                <name><![CDATA[Oh Dear]]></name>
            </author>
            <summary type="html">
                <![CDATA[<p>Over these past few weeks, we've updated <a href="/news-and-updates/how-to-upgrade-from-ubuntu-2004-lts-to-ubuntu-2204-lts">our Ubuntu 20.04 LTS uptime servers to Ubuntu 22.04 LTS</a>. The reason was mostly just staying up-to-date, but we had a surprising side-effect that was unexpected but very welcome.</p>
<h2 id="measuring-reduced-cpu-usage">Measuring reduced CPU usage <a href="#measuring-reduced-cpu-usage" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#measuring-reduced-cpu-usage">#</a></h2>
<p>An easy riddle: can you guess when we were running on Ubuntu 20.04 and when we were on 22.04? The spikes in the middle were during the upgrades (when this server was taken out of rotation).</p>
<p><img src="/media/blog/EAd6fAdGtiFZH7NliehPZUttW6lxgRkbeckl1Atp.png" alt="" /></p>
<p>For the exact same workload, here's how the numbers work out:</p>
<ul>
<li>Avg CPU (user) on Ubuntu 20.04 LTS: <strong>22.9%</strong></li>
<li>Avg CPU (user) on Ubuntu 22.04 LTS: <strong>13.2%</strong></li>
</ul>
<p>That's a whopping 42% reduction in CPU usage just by upgrading the OS. 🥳</p>
<p>And this wasn't an isolated server, this was measured across multiple servers, across multiple (independent) cloud providers.</p>
<h2 id="purposefully-simple-workload">Purposefully simple workload <a href="#purposefully-simple-workload" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#purposefully-simple-workload">#</a></h2>
<p>Our satellite servers are spread worldwide and act as mere <em>proxies</em> for HTTP(s) requests. They are, on purpose, simple by design.</p>
<p>Each server has Nginx &amp; PHP-FPM running, nothing more. It just takes in requests from our dispatcher, processes them by calling out to an external service (aka: our client website to monitor) and then returns the response.</p>
<p>After the upgrade;, we observered a significant increase in <em>context switches</em> for each server.</p>
<p><img src="/media/blog/VEyGAFjHLeFvP3Eno5psi1Bjg1x4Vlih2IZM7914.png" alt="" /></p>
<p>Because the workload has remained constant, I can only assume this dramatic speed up of context switches/interrupts is the direct cause of the performance improvements we're seeing. Happy to hear thoughts/theories on this!</p>
<h2 id="measured-across-dozens-of-servers">Measured across dozens of servers <a href="#measured-across-dozens-of-servers" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#measured-across-dozens-of-servers">#</a></h2>
<p>Just for dramatic effect, here's the CPU usage of a few other servers, but without the fixed y-axis scale. Sorry data scientists, I couldn't resist skewing the scale to our advantage here.</p>
<p><strong>Exhibit 1</strong>: a drop from ~16% CPU usage to ~10.3%.</p>
<p><img src="/media/blog/hjOde7cE7g2fkuRuj6nGUjASTKhdplbzpLp6LuO7.png" alt="" />
<strong>Exhibit 2</strong>: a noticeable reduction in the <em>CPU load</em> as well:</p>
<p><img src="/media/blog/f4wsKVRzSrXmnVPAeLTIRdhrU9rOgUO06rHTSXls.png" alt="" />
On average, the CPU load went from <strong>0.7</strong> to <strong>0.4</strong>.</p>
<h2 id="your-mileage-may-vary">Your mileage may vary <a href="#your-mileage-may-vary" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#your-mileage-may-vary">#</a></h2>
<p>Obviously, our setup isn't yours. We run <em>pure</em> PHP processes here, no databases/caches/file storage/... It may be a rather exceptional setup, as most environments would at least have some kind of persistence in the form of a database in the mix here.</p>
<p>But, the performance improvements are noticeable!</p>
<p>If you're still on Ubuntu 20.04 LTS - which gets support until 2025 - you might want to consider upgrading to the next LTS to get some noticeable speed improvements.</p>
]]>
            </summary>
                                    <updated>2024-02-28T10:44:10+00:00</updated>
        </entry>
            <entry>
            <title><![CDATA[How to upgrade from Ubuntu 20.04 LTS to Ubuntu 22.04 LTS]]></title>
            <link rel="alternate" href="https://ohdear.app/news-and-updates/how-to-upgrade-from-ubuntu-2004-lts-to-ubuntu-2204-lts" />
            <id>https://ohdear.app/109</id>
            <author>
                <name><![CDATA[Oh Dear]]></name>
            </author>
            <summary type="html">
                <![CDATA[<p>We're in the process of upgrading some of our older <em>uptime satellite</em> servers that power our <a href="https://ohdear.app/features/global-uptime-monitoring">Uptime Monitoring</a> from Ubuntu 20.04 to Ubuntu 22.04 LTS. This is mostly a smooth process, but in case anyone's ever in a similar position, these are the steps we're taking to complete the upgrades.</p>
<p>We should note: there's a lot of ways to upgrade servers, and the safest approach is to setup a brand new machine, move over your data, test things &amp; then migrate your DNS records to the new server. Our cloud providers however do not allow the existing public IP addresses to move from one server to another. Since a lot of clients have whitelisted <a href="https://ohdear.app/docs/faq/what-ips-does-oh-dear-monitor-from">our IPs</a> to get (unrestricted/un-ratelimited) access, we have to preserve them. For this reason, we're preferring <em>in-place upgrades</em>, where the IPs stay the same.</p>
<h2 id="backup-the-server-first">Backup the server first <a href="#backup-the-server-first" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#backup-the-server-first">#</a></h2>
<p>A lot of things can go wrong with in-place upgrades obviously, so it's best to be safe. You'll be installing a new Linux kernel, replace just about every package on the system (including <em>glibc</em> and <em>openssl</em>), and if anything goes wrong, your machine won't boot.</p>
<p>Ensure you have a back-up or a snapshot in place that you can restore from. This is also a good opportunity to test some of your disaster recovery practices, since you'll be taking snapshots &amp; rolling back to them.</p>
<h2 id="updates-first">Updates first <a href="#updates-first" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#updates-first">#</a></h2>
<p>I always like to ensure that the machine we'll be upgrading is in the best state possible, before the update. That's why I'll update &amp; reboot the machine first, to get a fresh system.</p>
<pre data-lang="txt" class="notranslate">$ apt update
$ apt upgrade
$ reboot
</pre>
<p>After that, you should be running the latest Kernel on your Ubuntu 20.04 LTS.</p>
<h2 id="perform-the-upgrade-to-ubuntu-2204-lts">Perform the upgrade to Ubuntu 22.04 LTS <a href="#perform-the-upgrade-to-ubuntu-2204-lts" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#perform-the-upgrade-to-ubuntu-2204-lts">#</a></h2>
<p>This is the type of step that is safest to perform <em>through a console screen or VNC display</em>. Ideally, you head over to your cloud provider of choice and open up the console there and login to the system as the local <em>root</em> user. This way, even if your SSH session(s) disconnect, you still have server access.</p>
<p>Because this process went incredibly smooth with our servers (and we have the fallbacks in place of snapshots/backups), we opted for the SSH approach instead.</p>
<h3 id="step-1-open-a-screentmux-via-ssh">Step 1: open a screen/tmux via SSH</h3>
<p>Your SSH session might disconnect throughout the upgrades, so it's safe to start the process in a separate shell that you can (try to) reconnect to later, should it disconnect.</p>
<pre data-lang="txt" class="notranslate">$ screen -S ubuntu_grade
</pre>
<p>You can detach yourself from the shell by hitting <code>CTRL+A</code> and then <code>D</code>.</p>
<h3 id="step-2-start-the-upgrade">Step 2: start the upgrade</h3>
<p>In your shell, type:</p>
<pre data-lang="txt" class="notranslate">$ cat /etc/issue
Ubuntu 20.04.6 LTS
</pre>
<p>This will confirm you're on Ubuntu 20.04 LTS.</p>
<p>Next, start the upgrade:</p>
<pre data-lang="txt" class="notranslate">$ do-release-upgrade
</pre>
<p>In this process, you'll be prompted to make several decisions. I'll mark some of these below with a little explainer to each.</p>
<pre data-lang="txt" class="notranslate">Third party sources disabled

Some third party entries in your sources.list were disabled. You can
re-enable them after the upgrade with the 'software-properties' tool
or your package manager.

To continue please press [ENTER]
</pre>
<p>You likely have 3rd party repos (ie: to get the latest PHP or Python versions). To avoid any interference of packages, those specific repos will be disabled during the upgrade. This is fine.</p>
<p>You might, as one of the first steps, encounter this message:</p>
<pre data-lang="txt" class="notranslate">Invalid package information

After updating your package information, the essential package
'ubuntu-minimal' could not be located. This may be because you have
no official mirrors listed in your software sources, or because of
excessive load on the mirror you are using. See /etc/apt/sources.list
for the current list of configured software sources.
In the case of an overloaded mirror, you may want to try the upgrade
again later.
</pre>
<p>This happened to us for the Digital Ocean VMs. I believe it's because Digital Ocean includes some of their own reposities with their mirrors in the distributions, and by default those get disabled during the upgrade. To avoid that, restart the command with:</p>
<pre data-lang="txt" class="notranslate">$ RELEASE_UPGRADER_ALLOW_THIRD_PARTY=1 do-release-upgrade
</pre>
<p>Once done, the upgrade will restart.</p>
<pre data-lang="txt" class="notranslate">14 packages are going to be removed. 145 new packages are going to be
installed. 893 packages are going to be upgraded.

You have to download a total of 826 M. This download will take about
2 minutes with your connection.

Installing the upgrade can take several hours. Once the download has
finished, the process cannot be canceled.

 Continue [yN]  Details [d]
</pre>
<p>Obviously, we'll install new packages, so hit <code>y</code> to continue.</p>
<pre data-lang="txt" class="notranslate"> Restart services during package upgrades without asking?                                                                                   
     &lt;Yes&gt;     &lt;no&gt;
</pre>
<p>In our case, each server we're upgrading gets taken out of rotation temporarily for us to perform the maintenance on. We're fine having the upgrader auto-restart services when needed, so we choose <code>&lt;Yes&gt;</code>.</p>
<p>Throughout the process you're going to get <em>a lot</em> of questions like these;</p>
<pre data-lang="txt" class="notranslate">Configuration file '/etc/crontab'
 ==&gt; Modified (by you or by a script) since installation.
 ==&gt; Package distributor has shipped an updated version.
   What would you like to do about it ?  Your options are:
    Y or I  : install the package maintainer's version
    N or O  : keep your currently-installed version
      D     : show the differences between the versions
      Z     : start a shell to examine the situation
 The default action is to keep your current version.
*** crontab (Y/I/N/O/D/Z) [default=N] ?
</pre>
<p>You made modifications to a file that the system upgrade would like to overwrite. These have to be decided upon on a case-by-case basis. After all, <em>you</em> know <em>your</em> system best. In the above example, we have custom cronjobs per server, so we'll choose <code>N</code> (the default value) to keep <em>our</em> version of the file.</p>
<p>Whenever possible, I'll opt for answer <code>I</code> where the default package version can get used, but you need to know the scenario's in which you can use that.</p>
<p>One of those scenario's for instance is <code>sysctl.conf</code>;</p>
<pre data-lang="txt" class="notranslate">Configuration file '/etc/sysctl.conf'
 ==&gt; Modified (by you or by a script) since installation.
 ==&gt; Package distributor has shipped an updated version.
   What would you like to do about it ?  Your options are:
    Y or I  : install the package maintainer's version
    N or O  : keep your currently-installed version
      D     : show the differences between the versions
      Z     : start a shell to examine the situation
 The default action is to keep your current version.
*** sysctl.conf (Y/I/N/O/D/Z) [default=N] ?
</pre>
<p>If you've not made any changes there (and it's unlikely you did), best to pick option <code>I</code> here for the package defaults. This is the file that is used to override default kernel parameter values. And since you'll be upgrading several kernel versions higher, I find it safest to take whatever the package maintainer opted to include in there. They probably know kernel tuning more than I do.</p>
<p><em>Note: some of these prompts will be pure text/cli, some will be in an ncurses-like UI. The UI might be different, but the questions/answers and logic to deal with them are the same in all scenario's.</em></p>
<pre data-lang="txt" class="notranslate">System upgrade is complete.

Restart required

To finish the upgrade, a restart is required.
If you select 'y' the system will be restarted.

Continue [yN]
</pre>
<p>Now's the moment of truth: hit <code>y</code> to reboot!</p>
<h3 id="step-3-verify-the-upgrade">Step 3: verify the upgrade</h3>
<p>Once the server has rebooted, log in and verify the upgrade is completed:</p>
<pre data-lang="txt" class="notranslate">$ cat /etc/issue
Ubuntu 22.04.4 LTS
</pre>
<h3 id="step-4-cleanup-obsolete-packages">Step 4: cleanup obsolete packages</h3>
<p>There might still be a few packages on the system that you can now cleanup.</p>
<pre data-lang="txt" class="notranslate">$ apt autoremove
</pre>
<p>This will prompt you to uninstall packages that <em>seem</em> obsolete, but the decision is ultimately yours.</p>
<p>Have a look at the installed packages and see if any of these are no longer needed (think: old PHP versions).</p>
<pre data-lang="txt" class="notranslate">$ apt list --installed
</pre>
<h2 id="youre-good">You're good! <a href="#youre-good" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#youre-good">#</a></h2>
<p>If all went well, you've now upgraded your machine successfully to the latest LTS version (at time of writing).</p>
<p>Now it's up to you to validate that your application(s) are still functional.</p>
]]>
            </summary>
                                    <updated>2024-02-20T21:03:27+00:00</updated>
        </entry>
            <entry>
            <title><![CDATA[Introducing our beautiful status badges]]></title>
            <link rel="alternate" href="https://ohdear.app/news-and-updates/introducing-our-beautiful-status-badges" />
            <id>https://ohdear.app/108</id>
            <author>
                <name><![CDATA[Oh Dear]]></name>
            </author>
            <summary type="html">
                <![CDATA[<p>For each site and status page in your account, we now offer badges that you can use to show the status of your site or service everywhere you want.</p>
<p>Technically these badges are svg's, so they are very lightweight to embed.</p>
<h2 id="the-default-badge">The default badge <a href="#the-default-badge" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#the-default-badge">#</a></h2>
<p>The default badge shows the name of a site and its uptime status.</p>
<p>Here's the actual badge for freek.dev</p>
<img src="https://ohdear.app/badges/site/01hn3vrtnjm3qr4xx72pwbazd7/uptime/normal" alt="uptime status badge" />
<p>Hopefully, freek.dev is up while you are reading this. Should freek.dev be down, the badge will be colored red.</p>
<h2 id="the-small-badge">The small badge <a href="#the-small-badge" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#the-small-badge">#</a></h2>
<p>The small badge is a smaller version of the default badge that only shows the status of a site.</p>
<p>Here's the actual small badge for freek.dev:</p>
<img src="https://ohdear.app/badges/site/01hn3vrtnjm3qr4xx72pwbazd7/uptime/small" alt="uptime status badge" />
<h2 id="where-to-find-the-badges">Where to find the badges <a href="#where-to-find-the-badges" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#where-to-find-the-badges">#</a></h2>
<p>For each site and status page in your account, you can find the badges in the &quot;Badges&quot; tab. On that screen, you can easily copy the HTML code to use the badge you want.</p>
<p><img src="/media/blog/g9kbSWuSqs7JAOvWrMGIR6Y0pRF18P8oF6vvERt2.jpg" alt="" /></p>
]]>
            </summary>
                                    <updated>2024-01-26T22:36:23+00:00</updated>
        </entry>
            <entry>
            <title><![CDATA[Making sure Laravel's debug mode is always disabled in production]]></title>
            <link rel="alternate" href="https://ohdear.app/news-and-updates/making-sure-laravels-debug-mode-is-always-disabled-in-production" />
            <id>https://ohdear.app/107</id>
            <author>
                <name><![CDATA[Oh Dear]]></name>
            </author>
            <summary type="html">
                <![CDATA[<p>Recently, people started talking about a malware called  “Androxgh0st” specifically targeting Laravel apps. In a recent edition of <a href="https://securinglaravel.com">Securing Laravel</a>,  Stephen Rees-Carter wrote <a href="https://securinglaravel.com/p/laravel-security-androxgh0st-malware">a good explanation</a> of how it works.</p>
<p>The malware targets apps with <code>APP_DEBUG</code> set to <code>true</code>. When enabled, Laravel will give detailed error messages, and some security features will be disabled. In production, you always want this value to be set to <code>false</code>.</p>
<p>You can make sure it's always set to' false' using Oh Dear’s <a href="https://ohdear.app/features/application-health-monitoring">application monitoring</a> feature. We can notify you whenever someone should set it to <code>true</code>. Let’s go through the steps required to set this up.</p>
<h3 id="installing-laravel-health-in-your-laravel-app">Installing Laravel Health in your Laravel app</h3>
<p>The <a href="https://spatie.be/docs/laravel-health">spatie/laravel-health package</a> can monitor the health of your application by registering one of <a href="https://spatie.be/docs/laravel-health/v1/available-checks/overview">the available checks</a>. Out of the box, it can monitor if your application is in debugging mode.</p>
<p>Using Laravel Health, you can check many other things, such as <a href="">used disk space</a>, whether or not <a href="">Horizon is running</a>, and [much more]!</p>
<p>You can install the package using composer.</p>
<pre data-lang="php" class="notranslate">composer <span class="hl-keyword">require</span> spatie/laravel-health
</pre>
<p>You’ll find full installation instructions <a href="https://spatie.be/docs/laravel-health/v1/installation-setup">here</a>.</p>
<p>To register the debug mode check, you can put this code in a service provider.</p>
<pre data-lang="php" class="notranslate"><span class="hl-comment">// typically, in a service provider</span>

<span class="hl-keyword">use</span> <span class="hl-type">Spatie\Health\Facades\Health</span>;
<span class="hl-keyword">use</span> <span class="hl-type">Spatie\Health\Checks\Checks\UsedDiskSpaceCheck</span>;

<span class="hl-type">Health</span>::<span class="hl-property">checks</span>([
   <span class="hl-type">DebugModeCheck</span>::<span class="hl-keyword">new</span>(),
   
   <span class="hl-comment">// other checks can come here</span>
]);
</pre>
<h3 id="adding-a-health-check-endpoint-to-your-laravel-app">Adding a health check endpoint to your Laravel app</h3>
<p>Oh Dear’s application health check works by sending an HTTP request to your application to a specific endpoint to get health check results. Your application should respond with JSON containing the result of health checks.</p>
<p>The spatie/laravel-health package can add such an endpoint to your Laravel app. To do this, must configure the <code>ohdear_endpoint_key</code> in the <code>health</code> config file.</p>
<p>You can publish that <code>health</code> with this command:</p>
<pre data-lang="bash" class="notranslate"><span class="hl-keyword">php</span> <span class="hl-keyword">artisan</span> vendor:publish <span class="hl-attribute">--tag</span>=<span class="hl-value">&quot;health-config&quot;</span>
</pre>
<p>These are some of the default values in the published <code>health</code> config file.</p>
<pre data-lang="php" class="notranslate"><span class="hl-comment">// in app/config/health.php</span>

<span class="hl-comment">/*
 * You can let Oh Dear monitor the results of all health checks. This way, you'll
 * get notified of any problems even if your application goes totally down. Via
 * Oh Dear, you can also have access to more advanced notification options.
 */</span>
<span class="hl-value">'oh_dear_endpoint'</span> =&gt; [
    <span class="hl-value">'enabled'</span> =&gt; <span class="hl-keyword">false</span>,

    <span class="hl-comment">/*
     * When this option is enabled, the checks will run before sending a response.
     * Otherwise, we'll send the results from the last time the checks have run.
     */</span>
    <span class="hl-value">'always_send_fresh_results'</span> =&gt; <span class="hl-keyword">true</span>,

    <span class="hl-comment">/*
     * The secret that is displayed at the Application Health settings at Oh Dear.
     */</span>
    <span class="hl-value">'secret'</span> =&gt; <span class="hl-property">env</span>(<span class="hl-value">'OH_DEAR_HEALTH_CHECK_SECRET'</span>),

    <span class="hl-comment">/*
     * The URL that should be configured in the Application health settings at Oh Dear.
     */</span>
    <span class="hl-value">'url'</span> =&gt; <span class="hl-value">'/oh-dear-health-check-results'</span>,
],
</pre>
<p>To get started:</p>
<ul>
<li>set the <code>enabled</code> config option to <code>true</code></li>
<li>add a <code>secret</code> (we recommend putting it in the <code>.env</code> file, just like you would do for any application secret or password)</li>
<li>optionally customize the <code>url</code> where the health check endpoint will be registered.</li>
</ul>
<h3 id="configuring-the-health-check-at-oh-dear">Configuring the health check at Oh Dear</h3>
<p>At Oh Dear, you can create a new site to monitor and enable the application health check.</p>
<p>In the application health check settings screen at Oh Dear, you should fill in the URL and secret that you specified in the <code>health</code> config file.</p>
<p><img src="/media/blog/u87FnLilDHErJuldBRJuhH838GnpFMHAqVyZqgW0.jpg" alt="" /></p>
<p>And with this set up, Oh Dear will send you a notification whenever somebody should set <code>APP_DEBUG</code> to <code>true</code>.</p>
<h2 id="in-closing">In closing <a href="#in-closing" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#in-closing">#</a></h2>
<p>Oh Dear’s <a href="https://ohdear.app/docs/features/application-health-monitoring">application health check</a> can be used to warn you whenever somebody turns on debugging mode of your app, but also a lot more other things can be checked:</p>
<ul>
<li>disk space is running low</li>
<li>the database is down</li>
<li>Redis cannot be reached</li>
<li>mails cannot be sent</li>
<li>a reboot of your app is required</li>
<li>...</li>
</ul>
<p>Next to this application health check, we also offer <a href="https://ohdear.app/docs/features/cron-job-monitoring">a scheduled jobs check</a>. You can sync your application's schedule to Oh Dear using the <a href="https://github.com/spatie/laravel-schedule-monitor">spatie/laravel-schedule-monitor</a> package. We can notify you whenever a scheduled task is not running on time or not running at all.</p>
]]>
            </summary>
                                    <updated>2024-02-06T20:26:26+00:00</updated>
        </entry>
            <entry>
            <title><![CDATA[Laravel Pulse cards to show response times, scheduled jobs, broken links]]></title>
            <link rel="alternate" href="https://ohdear.app/news-and-updates/laravel-pulse-cards-to-show-response-times-scheduled-jobs-broken-links" />
            <id>https://ohdear.app/106</id>
            <author>
                <name><![CDATA[Oh Dear]]></name>
            </author>
            <summary type="html">
                <![CDATA[<p>Today, we released the <a href="https://github.com/ohdearapp/ohdear-pulse">ohdearapp/ohdear-pulse</a> package, which contains Laravel Pulse cards to show you the status of your scheduled jobs, any broken links you have in your Laravel app, and uptime / HTTP performance stats. All of these cards use the Oh Dear API to fetch their data.</p>
<p><a href="https://pulse.laravel.com">Laravel Pulse</a> is a first party package that can display a dashboard with information surrounding usage and performance of your Laravel app. Here’s how a default installation looks like.</p>
<h2 id="discovering-the-cards">Discovering the cards <a href="#discovering-the-cards" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#discovering-the-cards">#</a></h2>
<p>The first Pulse card one displays if you site is up, and recent response times.</p>
<p><img src="/media/blog/zOWPnJ060mFu93jkB8mkupKa7afqEkMIjZAKsTbc.jpg" alt="" /></p>
<p>This card, and also the other two, also support dark mode.</p>
<p><img src="/media/blog/rlkIKUV6fKTvYU1uEwydEkWUaPXroVVIPLwfZcNK.jpg" alt="" /></p>
<p>Oh Dear can monitor if the scheduled jobs of a Laravel app run on time. Using the <a href="https://github.com/spatie/laravel-schedule-monitor">spatie/laravel-schedule-monitor</a> package, you can <a href="https://github.com/spatie/laravel-schedule-monitor#getting-notified-when-a-scheduled-task-doesnt-finish-in-time">sync the schedule</a> of your app to Oh Dear.</p>
<p>The cron Pulse card displays when your scheduled jobs have run for the last time, and if they ran on time. Very powerful stuff if you ask me.</p>
<p><img src="/media/blog/GAssL7jgFce2aMU8S5KPnsAU0lm6sfC467CQZYUG.jpg" alt="" /></p>
<p>The last card can display any broken links of your app. This card is powered by <a href="https://ohdear.app/docs/features/broken-links-detection">Oh Dear’s broken links check</a>, which crawls your entire site.</p>
<p><img src="/media/blog/5kqmKgkDbkMJLAL1Z7RSpmL4zXibq8MeOrpMiPWa.jpg" alt="" /></p>
<h2 id="installing-the-cards">Installing the cards <a href="#installing-the-cards" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#installing-the-cards">#</a></h2>
<p>To use these cards, you have to pull in the package in your app via Composer.</p>
<pre data-lang="bash" class="notranslate"><span class="hl-keyword">composer</span> require ohdearapp/ohdear-pulse
</pre>
<p>In your <code>config/services.php</code> file, add the following lines:</p>
<pre data-lang="php" class="notranslate"><span class="hl-value">'oh_dear'</span> =&gt; [
    <span class="hl-value">'pulse'</span> =&gt; [
        <span class="hl-value">'api_key'</span> =&gt; <span class="hl-property">env</span>(<span class="hl-value">'OH_DEAR_API_TOKEN'</span>),
        <span class="hl-value">'site_id'</span> =&gt; <span class="hl-property">env</span>(<span class="hl-value">'OH_DEAR_SITE_ID'</span>),
    ],
],
</pre>
<p>You can create an API token on the &quot;API Tokens&quot; page at Oh Dear. You'll find the site ID on the &quot;Settings&quot; page of a site on Oh Dear.</p>
<p>You can add the cards to your Pulse dashboard, by first publishing the Pulse's dashboard view:</p>
<pre data-lang="bash" class="notranslate"><span class="hl-keyword">php</span> <span class="hl-keyword">artisan</span> vendor:publish <span class="hl-attribute">--tag</span>=pulse-dashboard
</pre>
<p>Next, add the cards to the <code>resources/views/vendor/pulse/dashboard.blade.php</code> file:</p>
<pre data-lang="html" class="notranslate">&lt;<span class="hl-keyword">x-pulse</span>&gt;
    &lt;livewire:ohdear.pulse.uptime <span class="hl-property">cols</span>=&quot;4&quot; /&gt;
    
    &lt;livewire:ohdear.pulse.cron <span class="hl-property">cols</span>=&quot;8&quot; /&gt;

    &lt;livewire:ohdear.pulse.brokenLinks <span class="hl-property">cols</span>=&quot;8&quot; /&gt;
    
    {{-- Add more cards here --}}
&lt;/<span class="hl-keyword">x-pulse</span>&gt;
</pre>
<p>You’ll find the code in <a href="https://github.com/spatie/ohdear-pulse">this repo on GitHub</a>.</p>
]]>
            </summary>
                                    <updated>2024-01-11T08:22:56+00:00</updated>
        </entry>
            <entry>
            <title><![CDATA[Two smallish improvements to our DNS check]]></title>
            <link rel="alternate" href="https://ohdear.app/news-and-updates/two-smallish-improvements-to-our-dns-check" />
            <id>https://ohdear.app/105</id>
            <author>
                <name><![CDATA[Oh Dear]]></name>
            </author>
            <summary type="html">
                <![CDATA[<p>As you probably know, Oh Dear is run by a small but capable team. One of the advantages of being small is that we can implement stuff pretty quickly: there’s no red tape, and our code base is very healthy.</p>
<p>So, when our users have feature requests that make sense to add to Oh Dear, we can move fast. In the past month, we implemented two smallish feature requests for <a href="https://ohdear.app/docs/features/dns-monitoring">our DNS check</a> we got through support.</p>
<p>Here’s what our new DNS settings screens look like. The first option, monitoring different CNAMES, was already added a few months ago.</p>
<p><img src="/media/blog/LOUOOr7Je9i7uRPJfk2TWZsEZVjvRuq09fFqEzyu.jpg" alt="" /></p>
<p>We now added the ability to disable the check whether all of your nameservers are in sync. Under normal circumstances, DNS nameservers of a domain are in check. But in advanced setups, this might not be the case. That’s why we added the option to disable that aspect of our DNS check.</p>
<p>The second option is the ability to monitor the DNS of your main domain. This option is only available if the site you’re monitoring is on a subdomain. Some of our users wanted to simultaneously monitor the site on the subdomain and the DNS records of the main domain.</p>
<p>We hope you like this improvement. Should you have any ideas or feature requests to improve Oh Dear, <a href="mailto:support@ohdear.app">let us know</a>!</p>
]]>
            </summary>
                                    <updated>2024-01-11T08:21:47+00:00</updated>
        </entry>
            <entry>
            <title><![CDATA[Our Lighthouse check has been upgraded to Lighthouse v11]]></title>
            <link rel="alternate" href="https://ohdear.app/news-and-updates/our-lighthouse-check-has-been-upgraded-to-lighthouse-v11" />
            <id>https://ohdear.app/104</id>
            <author>
                <name><![CDATA[Oh Dear]]></name>
            </author>
            <summary type="html">
                <![CDATA[<p>We are happy to announce that we have upgraded our Lighthouse check from v9 to the latest version, Lighthouse v11. Lighthouse is an open-source tool by Google that helps developers improve the quality of their web pages.</p>
<p>Oh Dear can run this check frequently for your site, informing you when SEO-related problems arise. Our check may suggest optimizing images or minifying JavaScript to improve performance. By implementing these suggestions, a website can become faster, more accessible, and more secure, improving its ranking in search engine results.</p>
<p>In our UI, you can watch how your key metrics evolved over time.</p>
<p><img src="/media/blog/divEXkUCIniNLjgAvJ6Qbl88fTx0BmEumljZS5hc.jpg" alt="" /></p>
<p>By upgrading to v11, many more audits have been added. You can learn more in <a href="https://developer.chrome.com/blog/lighthouse-11-0">this announcement post of Lighthouse v11</a>.</p>
<p>We hope you like this improvement. Should you have any ideas or feature requests to improve Oh Dear, <a href="mailto:support@ohdear.app">let us know</a>!</p>
]]>
            </summary>
                                    <updated>2023-12-19T17:06:03+00:00</updated>
        </entry>
            <entry>
            <title><![CDATA[Our DNS check can now monitor hidden CNAME records]]></title>
            <link rel="alternate" href="https://ohdear.app/news-and-updates/our-dns-check-can-now-monitor-hidden-cname-records" />
            <id>https://ohdear.app/103</id>
            <author>
                <name><![CDATA[Oh Dear]]></name>
            </author>
            <summary type="html">
                <![CDATA[<p>Besides monitoring your site's uptime, Oh Dear offers <a href="https://ohdear.app/docs/features/what-checks-does-oh-dear-perform">many other checks</a> to monitor all kinds of aspects of your web app. One of those checks is <a href="https://ohdear.app/docs/features/dns-monitoring">our DNS check</a>.</p>
<p>Whenever we detect problems with your DNS records or when one of the DNS records changes, we can notify you. By default, we only monitor the DNS records of the domain you are monitoring. So when you're monitoring <code>example.com</code>, we'll only monitor the records of that hostname.</p>
<p>A CNAME record is a special kind of DNS record. It can be looked at as an alias or - in Linux terms - a symlink to another record. That record has a name, e.g., <code>mycname</code>, and a value it points to e.g. <code>someotherdomain.com</code>.</p>
<p>In the example above, the hostname of the record is not <code>example.com</code> but <code>mycname.example.com</code>. That is why we don't monitor it by default: it's not part of the main domain, and DNS query responses don't return these records by default. They are hard to discover.</p>
<p>We've now added the ability to specify the CNAMEs of your domain. You can do that in the DNS settings of your site. Here's an example where we want to monitor the <code>technical</code> CNAME record of <code>freek.dev</code></p>
<p><img src="/media/blog/0cNrLETR4UZLpxdxwAKNRBVXBmIGdfGhnCjP6ZVt.jpg" alt="" /></p>
<p>When you've added these CNAMEs, Oh Dear will monitor these records too, and notify you whenever they change or disappear.</p>
<p><img src="/media/blog/ay1zbdfaSQ79jSz93jsBXb0PunHZMgUBH1qdqGAr.jpg" alt="" /></p>
<p>CNAME records are vital to most modern web apps, and we are glad we can now monitor these as part of our DNS check.</p>
]]>
            </summary>
                                    <updated>2023-10-26T11:37:15+00:00</updated>
        </entry>
            <entry>
            <title><![CDATA[Our uptime check can now verify the absence of a string]]></title>
            <link rel="alternate" href="https://ohdear.app/news-and-updates/our-uptime-check-can-now-verify-the-absence-of-a-string" />
            <id>https://ohdear.app/102</id>
            <author>
                <name><![CDATA[Oh Dear]]></name>
            </author>
            <summary type="html">
                <![CDATA[<p>The most popular check that Oh Dear offers is, without a doubt, our uptime check. It's enabled for almost every site we monitor.</p>
<p>By default, this check will notify you when your site returns a non-2xx response, but you can <a href="https://ohdear.app/docs/features/uptime-monitoring">greatly customize that behavior</a>.</p>
<p>You can check if the response has certain headers, if the response contains a particular string, and more!</p>
<p><img src="/media/blog/QtrhxTNTE6vxmKKnTbUWoX46U0BOJmO2HTKbvR6B.jpg" alt="" /></p>
<p>Some of our users requested a new behavior: checking the absence of a string on the response. We're happy to share that we've now added that feature.</p>
<p><img src="/media/blog/3xx25XntAhAvn8EUMylgZeVff2yCb5YIB60RgKzk.jpg" alt="" /></p>
<p>This way, you can ensure that a string like &quot;An error occurred&quot; does not appear on your page. As soon as it does, we'll send you a notification.</p>
<p>We hope you like this little improvement. Should you have any ideas or feature requests to improve Oh Dear, <a href="mailto:support@ohdear.app">let us know</a>!</p>
]]>
            </summary>
                                    <updated>2023-10-01T11:52:39+00:00</updated>
        </entry>
            <entry>
            <title><![CDATA[Our API now allows you to read all notification settings]]></title>
            <link rel="alternate" href="https://ohdear.app/news-and-updates/our-api-now-allows-you-to-read-all-notification-settings" />
            <id>https://ohdear.app/99</id>
            <author>
                <name><![CDATA[Oh Dear]]></name>
            </author>
            <summary type="html">
                <![CDATA[<p>Oh Dear's robust API provides endpoints that enable programmatically accessing nearly all features available on the web UI.</p>
<p>One thing that needed to be added was the ability to read all notification settings in your account. A few of our users requested this; it made sense to add this, so we did just that. A benefit of choosing a smaller company like Oh Dear (we're only two people) is that we can move very fast and quickly add features when customer requests come in.</p>
<p>Because the results of these API calls
can contain sensitive values, only admins and team owners can use the new endpoints.</p>
<p>Let's try out one of the news endpoints. The <code>/API/team-notification-destinations</code> URL will return all notification destinations defined on the team level
for all your teams.</p>
<pre data-lang="json" class="notranslate"><span class="hl-property">{</span>
  <span class="hl-keyword">&quot;data&quot;</span>: <span class="hl-property">[</span>
    <span class="hl-property">{</span>
      <span class="hl-keyword">&quot;id&quot;</span>: 123,
      <span class="hl-keyword">&quot;team_id&quot;</span>: 1,
      <span class="hl-keyword">&quot;channel&quot;</span>: <span class="hl-value">&quot;mail&quot;</span>,
      <span class="hl-keyword">&quot;destination&quot;</span>: <span class="hl-property">{</span>
        <span class="hl-keyword">&quot;mail&quot;</span>: <span class="hl-value">&quot;john@example.com&quot;</span>
      <span class="hl-property">}</span>,
      <span class="hl-keyword">&quot;notification_types&quot;</span>: <span class="hl-property">[</span>
        <span class="hl-value">&quot;UptimeCheckFailedNotification&quot;</span>,
        <span class="hl-value">&quot;UptimeCheckRecoveredNotification&quot;</span>,
        ...
      <span class="hl-property">]</span>
    <span class="hl-property">}</span>
  <span class="hl-property">]</span>
<span class="hl-property">}</span>
</pre>
<p>If you want to know more about the new endpoints and our API in general, head over to <a href="https://ohdear.app/docs/integrations/the-oh-dear-api#notification-destinations">our extensive API docs</a>.</p>
]]>
            </summary>
                                    <updated>2025-06-24T21:00:03+00:00</updated>
        </entry>
            <entry>
            <title><![CDATA[Make money by referring customers through our new affiliate program]]></title>
            <link rel="alternate" href="https://ohdear.app/news-and-updates/make-money-by-referring-customers-through-our-new-affiliate-program" />
            <id>https://ohdear.app/101</id>
            <author>
                <name><![CDATA[Oh Dear]]></name>
            </author>
            <summary type="html">
                <![CDATA[<p>We're proud to announce that we started <a href="https://ohdear.app/affiliate-program">our affiliate program</a>.</p>
<p>Using this program, you can generate a link (like https://ohdear.app?via=your-name) that you can include in your blog posts, tweets, or anywhere on the web. If somebody clicks that link and subscribes to Oh Dear in the next 30 days, you'll get 25% of the revenue of the first year of that subscription. The more people subscribe via your link, the more money you earn.</p>
<p>When you create your affiliate account, you'll get access to a nice dashboard showing you a clear overview of all your clicks, conversions, and money your links generated.</p>
<p><img src="/media/blog/T454bSiRbCjoZOijqKXmNKA2PbKoq6D0GjydNLiA.png" alt="" /></p>
<p>You'll find more info on all of this on our brand new (and beautiful) <a href="https://ohdear.app/affiliate-program">affiliate program page</a>.</p>
]]>
            </summary>
                                    <updated>2023-07-29T10:48:05+00:00</updated>
        </entry>
            <entry>
            <title><![CDATA[Better handling of bounced emails]]></title>
            <link rel="alternate" href="https://ohdear.app/news-and-updates/better-handling-of-bounced-emails" />
            <id>https://ohdear.app/100</id>
            <author>
                <name><![CDATA[Oh Dear]]></name>
            </author>
            <summary type="html">
                <![CDATA[<p>Whenever we detects something wrong with your site it can send you a notification. We have multiple channels available: Slack, Telegram, webhooks, and many more. The most popular channel our users use is just a simple mail.</p>
<p>Behind the scenes, Oh Dear uses <a href="https://postmarkapp.com">Postmark</a> to send out mails. Postmark will inform us whenever a notification mail results in a hard bounce. A hard bounce means that the mail won't be delivered. The most common reason for this is that the mailbox doesn't exist (anymore). This can occur when somebody changed jobs and the work email address doesn't exist anymore.</p>
<p>Whenever Postmark informs us about a hard bounce, Oh Dear will determine which team that email belongs to. It wil send a mail to the owner of the Oh Dear team asking to correct the email address or to remove the member from the team.</p>
<p>For most cases this is fine, but what if it is the email address of the team owner itself that bounced? In this case we obviously can't mail the team owner anymore.</p>
<p>As of today, whenever an users logs in who's email has bounced (team owner and other members), we'll display a warning to update their email address. Here's how that looks.</p>
<p><img src="/media/blog/5BqUeqAqyLEixHr7Q9thLYV729MFeNTU2nNwJcCR.jpg" alt="" /></p>
<p>We hope that this will be let people understand faster why they didn't get an alert they were expecting.</p>
]]>
            </summary>
                                    <updated>2023-07-27T16:20:24+00:00</updated>
        </entry>
            <entry>
            <title><![CDATA[You can now log in faster using Google and GitHub]]></title>
            <link rel="alternate" href="https://ohdear.app/news-and-updates/you-can-now-log-in-faster-using-google-and-github" />
            <id>https://ohdear.app/98</id>
            <author>
                <name><![CDATA[Oh Dear]]></name>
            </author>
            <summary type="html">
                <![CDATA[<p>Since Oh Dear was launched, we offered a traditional login using the familiar email and password combination.</p>
<p>Today, we've launched our social login. This feature allows you to use your Google or GitHub account to log into Oh Dear.</p>
<p>You'll see these two new buttons on the registration and login page.</p>
<p><img src="/media/blog/eCVwJP12UV7VFwyCNrLyeqShVyrCaqLJZgHFi3e2.jpg" alt="" /></p>
<p>When clicking one, we'll use your Google or GitHub account to log in. When logging in, we'll search for an Oh Dear account whose email matches the email used for your Google / GitHub account.</p>
<p>Using this feature, you can log into your Oh Dear account with a single click.</p>
<p>If you're not using Oh Dear to monitor your site, now's the perfect time to get started. We monitor uptime, SSL certificates, broken links, scheduled tasks, application health, DNS, domain expiry, and more. We send notifications when something's wrong. All that paired with <a href="/docs/api/introduction" class="underline hover:no-underline">a developer-friendly API<a> and <a href="/docs" class="underline hover:no-underline">kick-ass documentation</a>.</p>
]]>
            </summary>
                                    <updated>2023-07-24T06:21:12+00:00</updated>
        </entry>
            <entry>
            <title><![CDATA[Our uptime check can now verify response headers]]></title>
            <link rel="alternate" href="https://ohdear.app/news-and-updates/our-uptime-check-can-now-verify-response-headers" />
            <id>https://ohdear.app/97</id>
            <author>
                <name><![CDATA[Oh Dear]]></name>
            </author>
            <summary type="html">
                <![CDATA[<p>When we make a request to your site to verify that your site is up, the response of your server will contain certain headers. We can verify that those headers contain the values you expect. If these expectations are not met, we'll consider your site as down.</p>
<p>In the &quot;Responses&quot; section of the uptime settings page, you can specify which headers we should verify.</p>
<p>You could add this expectation to ensure your page uses <code>gzip</code> compression.</p>
<p><img src="/media/blog/l7LzO9p1PBwa915YDU2PlN9PI8zRh398tAokfWIt.jpg" alt="" /></p>
<p>If you want to verify that a particular header is set on the response, regardless of its value, you can use the &quot;matches pattern&quot; condition and the <code>*</code> wildcard as the value. In this example, we'll verify that the response contains a header named <code>laravel-responsecache</code> with any value.</p>
<p><img src="/media/blog/kR0F2pku61NPYgNWpjgbfCkk2Ys7htQ6JcW0p8vg.jpg" alt="" /></p>
<p>If you're not using Oh Dear to monitor your site, now's the perfect time to get started. We monitor uptime, SSL certificates, broken links, scheduled tasks, application health, DNS, domain expiry, and more. We send notifications when something's wrong. All that paired with <a href="/docs/api/introduction" class="underline hover:no-underline">a developer-friendly API<a> and <a href="/docs" class="underline hover:no-underline">kick-ass documentation</a>.</p>
]]>
            </summary>
                                    <updated>2023-07-02T17:46:02+00:00</updated>
        </entry>
            <entry>
            <title><![CDATA[We can now notify you through PagerDuty]]></title>
            <link rel="alternate" href="https://ohdear.app/news-and-updates/we-can-now-notify-you-through-pagerduty" />
            <id>https://ohdear.app/96</id>
            <author>
                <name><![CDATA[Oh Dear]]></name>
            </author>
            <summary type="html">
                <![CDATA[<p>When we detect a problem with your site, we can notify you via mail, a Slack message, a webhook, or any of <a href="https://ohdear.app/docs/notifications/notifications">our other notifications channels</a>. This is enough for most of our users, but those who work in larger teams often need more flexibility.</p>
<p>Today, we are launching our <a href="https://www.pagerduty.com">PagerDuty</a> integration. PagerDuty is a cloud-based incident management platform that helps organizations improve operational reliability by providing real-time alerts, on-call scheduling, and incident tracking.</p>
<p>Once the PagerDuty integration is set up, Oh Dear will automatically open and close incidents at PagerDuty when we detect problems. PagerDuty will then notify the right people that are on call through email, SMS, phone calls, and iOS &amp; Android push notifications.</p>
<p>Getting started with our PagerDuty integration is straightforward. Go to the team or site notification screens, and create a new notification destination of type &quot;PagerDuty.&quot;</p>
<p><img src="/media/blog/Y6yxRHLtqfNEUv2vmGJqUMjPDONaKqmYTfdzDozH.jpg" alt="" /></p>
<p>You'll find all the details on getting the values to fill out the form <a href="https://ohdear.app/docs/notifications/pagerduty">in our docs</a>.</p>
<p>If you're not using Oh Dear to monitor your site, now's the perfect time to get started. We monitor uptime, SSL certificates, broken links, scheduled tasks, application health, DNS, domain expiry, and more. We send notifications when something's wrong. All that paired with <a href="/docs/api/introduction" class="underline hover:no-underline">a developer-friendly API<a> and <a href="/docs" class="underline hover:no-underline">kick-ass documentation</a>.</p>
]]>
            </summary>
                                    <updated>2023-06-13T08:05:12+00:00</updated>
        </entry>
            <entry>
            <title><![CDATA[Our redesigned status pages can now show uptime history]]></title>
            <link rel="alternate" href="https://ohdear.app/news-and-updates/our-redesigned-status-pages-can-now-show-uptime-history" />
            <id>https://ohdear.app/95</id>
            <author>
                <name><![CDATA[Oh Dear]]></name>
            </author>
            <summary type="html">
                <![CDATA[<p>Next to <a href="https://ohdear.app/docs/features/what-checks-does-oh-dear-perform">the many checks</a> we can perform, we can also render beautiful status pages to inform your audience about the health of your service.</p>
<p>Today, we've deployed a redesign of these status pages. In this iteration, everything is more polished. We picked a new font and colors and added some icons to make the status page a bit more visually interesting.</p>
<p>In addition to the cosmetic upgrade, we also added a significant new feature. We can now display 60 days of uptime history for your sites. Here's what that looks like (taken from <a href="https://status.flareapp.io">the Flare status page</a>).</p>
<p><img src="/media/blog/XI8Em4nKCosfPRT4M9624T8dAla868uTtuMEjJb0.jpg" alt="" /></p>
<p>Every bar in the graph is a day. It's colored green when uptime is above 99% and orange when it's below that threshold. Of course, you can configure that threshold for your particular status page.</p>
<p>When you hover over a bar, you can see the exact amount of downtime.</p>
<p><img src="/media/blog/W3xW69iwdhkfn0UQHVlPQxU4dmzMMYIfJGUyvNak.jpg" alt="" /></p>
<p>We'll show the uptime history by default for a newly created status page. For the old status page, you'll need to manually enable it in your status page's settings.</p>
<p><img src="/media/blog/UYhcnVNrQmrj2dXcw96copbOHki3gJAVI96t33hN.jpg" alt="" /></p>
<p>We hope that you like this addition to Oh Dear! If you're not using Oh Dear to monitor your site, now's the perfect time to get started. We monitor uptime, SSL certificates, broken links, scheduled tasks, application health, DNS, domain expiry and more. We send notifications when something's wrong. All that paired with <a href="/docs/api/introduction" class="underline hover:no-underline">a developer-friendly API<a> and <a href="/docs" class="underline hover:no-underline">kick-ass documentation</a>.</p>
]]>
            </summary>
                                    <updated>2023-06-11T22:23:55+00:00</updated>
        </entry>
            <entry>
            <title><![CDATA[Our broken links check has been improved]]></title>
            <link rel="alternate" href="https://ohdear.app/news-and-updates/our-broken-links-check-has-been-improved" />
            <id>https://ohdear.app/94</id>
            <author>
                <name><![CDATA[Oh Dear]]></name>
            </author>
            <summary type="html">
                <![CDATA[<p>One of our unique monitoring features is that we crawl your entire site to discover links that might be broken. When we discover a broken link, we'll send you a notification and display every broken link in our Broken Links Report.</p>
<p>We've made a nice quality-of-life improvement to that Broken Links Report. In addition to displaying the broken link URL and the page on which that broken link was found, we now also display the link text of that broken link.</p>
<p><img src="/media/blog/WIgn9r4tRPtRVhQ8fE4i07DYrt0m0tChF6HgjCzF.jpg" alt="" /></p>
<p>That link text will greatly help you to pinpoint the exact location of the broken link on the page where it was found.</p>
<p>This improvement was suggested by one of our users. We're always open for feature requests. If we think a feature might be useful for most of our users, it usually gets implemented pretty quickly.</p>
<p>If you're not using Oh Dear to monitor your site, now's the perfect time to get started. We monitor uptime, SSL certificates, broken links, scheduled tasks, application health, DNS, domain expiry and more. We send notifications when something's wrong. All that paired with <a href="/docs/api/introduction" class="underline hover:no-underline">a developer friendly API<a> and <a href="/docs" class="underline hover:no-underline">kick-ass documentation</a>.</p>
]]>
            </summary>
                                    <updated>2023-06-05T07:09:16+00:00</updated>
        </entry>
            <entry>
            <title><![CDATA[You can now add notes to downtime periods]]></title>
            <link rel="alternate" href="https://ohdear.app/news-and-updates/you-can-now-add-notes-to-downtime-periods" />
            <id>https://ohdear.app/93</id>
            <author>
                <name><![CDATA[Oh Dear]]></name>
            </author>
            <summary type="html">
                <![CDATA[<p>Oh Dear offers <a href="https://ohdear.app/docs/features/what-checks-does-oh-dear-perform">many checks</a> to ensure your website is healthy. The most popular check that is active for almost every site we monitor is <a href="https://ohdear.app/docs/features/uptime-monitoring">the uptime check</a>.</p>
<p>When the uptime check detects that your site is down, it will notify you via one of <a href="https://ohdear.app/docs/notifications/notifications">our many available channels</a>.</p>
<p>The check will also create a downtime period visible on the uptime check results page. Here's what those downtimes might look like.</p>
<p><img src="/media/blog/89wc1ys1K3BhSgVKCm9nH3HG703O6Ogjbk4TgTv3.jpg" alt="" /></p>
<p>The screenshot above shows the new little feature we introduced: you can now add notes to downtime periods. You could use these notes to add extra details on why the downtime happened or what actions you took. Examples could be: &quot;Process X was overloaded&quot; or &quot;We've contacted our hosting partner, and they said they had connectivity issues in their data center.&quot;</p>
<p>If you're not using Oh Dear to monitor your site, now's the perfect time to get started. We monitor uptime, SSL certificates, broken links, scheduled tasks, application health, DNS, domain expiry and more. We send notifications when something's wrong. All that paired with <a href="/docs/api/introduction" class="underline hover:no-underline">a developer friendly API<a> and <a href="/docs" class="underline hover:no-underline">kick-ass documentation</a>.</p>
]]>
            </summary>
                                    <updated>2023-05-13T07:50:13+00:00</updated>
        </entry>
            <entry>
            <title><![CDATA[Our Opsgenie integration is now available]]></title>
            <link rel="alternate" href="https://ohdear.app/news-and-updates/our-opsgenie-integration-is-now-available" />
            <id>https://ohdear.app/92</id>
            <author>
                <name><![CDATA[Oh Dear]]></name>
            </author>
            <summary type="html">
                <![CDATA[<p>When we detect a problem with your site we can notify you via mail, a slack message, a webhook, or any of <a href="https://ohdear.app/docs/notifications/notifications">our other notifications channels</a>. For most of our users this is enough, but those work in larger teams often need more flexibility.</p>
<p>Today, we are launching our <a href="https://www.atlassian.com/software/opsgenie">Opsgenie</a> integration, a modern incident management platform.</p>
<p>Opsgenie’s on-call scheduling allows you to set up escalation policies, so that if a notification isn’t handle within a certain timeframe, it will automatically be escalated to the next person in the on-call rotation. This ensures that critical notifications are always being addressed, even if the first person notified is unavailable or unable to resolve the issue.</p>
<p>Here's an example of such an escalation policy and on-call schedule.</p>
<p><img src="/media/blog/1DimlGmRM1T9Bi4g4WxIDKImf10CtneHVPSWbAuf.webp" alt="" /></p>
<p>Oh Dear can now automatically open and close alerts at Opsgenie when we detect problems with your site. Opsgenie will then notify the right people that are on call as specified by the escalation policy.</p>
<p>Here's how an alert that was created for an incoming Oh Dear notification looks like in Opsgenie. As you can see we send extra information about the particular issue we detected. In this case we added some information on the expired notification.</p>
<p><img src="/media/blog/YVMwkDBozOhsYrbVWQnBzm9Ts1kIGTXkx0ROrmvr.jpg" alt="" /></p>
<p>Configuring Oh Dear to send notifications is pretty easy. All you need to do is to <a href="https://ohdear.app/docs/notifications/opsgenie">get an Opsgenie API key</a> on the team level and use that in the a newly created notification destination.</p>
<p><img src="/media/blog/vPffmfDqmI1pjjlNyLTKNIIG2N6xMpEpi81vS8Na.jpg" alt="" /></p>
<p>If you're not using Oh Dear to monitor your site, now's the perfect time to get started. We monitor uptime, SSL certificates, broken links, scheduled tasks, application health, DNS, domain expiry and more. We send notifications when something's wrong. All that paired with <a href="/docs/api/introduction" class="underline hover:no-underline">a developer friendly API<a> and <a href="/docs" class="underline hover:no-underline">kick-ass documentation</a>.</p>
]]>
            </summary>
                                    <updated>2023-05-11T17:37:33+00:00</updated>
        </entry>
            <entry>
            <title><![CDATA[Our broken links check now highlights application errors ]]></title>
            <link rel="alternate" href="https://ohdear.app/news-and-updates/our-broken-links-check-now-highlights-application-errors" />
            <id>https://ohdear.app/91</id>
            <author>
                <name><![CDATA[Oh Dear]]></name>
            </author>
            <summary type="html">
                <![CDATA[<p>One of the unique features of Oh Dear is that we crawl your entire site and report any broken links.</p>
<p>Our broken links report had two main categories:</p>
<ul>
<li>external broken links: these are links on your site that point towards a page on another site</li>
<li>internal broken links: these are links on your site that point towards a non-existing page on your own site</li>
</ul>
<p>In both categories, the problem is caused by something related to the site's content. In most cases, a page you're linking to was removed or archived. The solution is often letting the content manager of the site fix this.</p>
<p>Today, we're introducing a third category in our report: internal broken links that resulted in a 5xx status code. This 5xx range of status codes signifies that the URL is correct but that the application could not respond appropriately because of an internal error. In most cases, this problem should be solved by a developer.</p>
<p>Because page visits that result in a 5xx response are more noteworthy, we decided to put these on top of our report.</p>
<p>Here's what our broken links report now looks like.</p>
<p><img src="/media/blog/00rLgRqEyuC1o7trlZdJ7f9QRP0fGiQeAwWFoRrR.jpg" alt="" /></p>
<p>We hope that you like this little addition to our UI. It might not be a revolutionary feature, but getting many of these small details right can make a big difference.</p>
<p>If you're not using Oh Dear to monitor your site, now's the perfect time to get started. We monitor uptime, SSL certificates, broken links, scheduled tasks, application health, DNS, domain expiry and more. We send notifications when something's wrong. All that paired with <a href="/docs/api/introduction" class="underline hover:no-underline">a developer friendly API<a> and <a href="/docs" class="underline hover:no-underline">kick-ass documentation</a>.</p>
]]>
            </summary>
                                    <updated>2023-04-14T16:10:29+00:00</updated>
        </entry>
            <entry>
            <title><![CDATA[You can now save notes on a site]]></title>
            <link rel="alternate" href="https://ohdear.app/news-and-updates/you-can-now-save-notes-on-a-site" />
            <id>https://ohdear.app/90</id>
            <author>
                <name><![CDATA[Oh Dear]]></name>
            </author>
            <summary type="html">
                <![CDATA[<p>We implemented a small, but valuable feature requested by some of our users. You can now store some free form notes on a site.</p>
<p>When heading over a site's settings, you'll see the new &quot;notes&quot; field.</p>
<p><img src="/media/blog/yArxYRDchqIRX3OzmncE4KMg6XMNBw8waAeQjsqY.jpg" alt="" /></p>
<p>Here you can add some important information to the site, for example, some details on the SLA or technical details, ...</p>
<p>When you saved notes on a site, we'll show it when hovering over the site on the site list.</p>
<p><img src="/media/blog/djaZFeBTrCmvEGpPzpZlzDXSYDLVZd3C68jpShg3.jpg" alt="" /></p>
<p>Of course, you can also get to these notes via the API.</p>
<p>If you're not using Oh Dear to monitor your site, now's the perfect time to get started. We monitor uptime, SSL certificates, broken links, scheduled tasks, application health, DNS, domain expiry and more. We send notifications when something's wrong. All that paired with <a href="/docs/api/introduction" class="underline hover:no-underline">a developer friendly API<a> and <a href="/docs" class="underline hover:no-underline">kick-ass documentation</a>.</p>
]]>
            </summary>
                                    <updated>2023-03-22T07:30:55+00:00</updated>
        </entry>
            <entry>
            <title><![CDATA[Redesigning Oh Dear: a case study]]></title>
            <link rel="alternate" href="https://ohdear.app/news-and-updates/redesigning-oh-dear-a-case-study" />
            <id>https://ohdear.app/89</id>
            <author>
                <name><![CDATA[Oh Dear]]></name>
            </author>
            <summary type="html">
                <![CDATA[<p>A few months ago, we totally redesigned our service. We didn't to this on our own, but got help from our friends at Digital With You.</p>
<p>On their site, they published an in-depth case study on how they rewrote marketing copy, chose new colours and redesigned entire pages.</p>
<p>Check it out: <a href="https://digitalwithyou.com/cases/oh-dear">https://digitalwithyou.com/cases/oh-dear</a></p>
]]>
            </summary>
                                    <updated>2023-03-21T09:05:35+00:00</updated>
        </entry>
            <entry>
            <title><![CDATA[Changing the owner of the team can now be done in our UI]]></title>
            <link rel="alternate" href="https://ohdear.app/news-and-updates/changing-the-owner-of-the-team-can-now-be-done-in-our-ui" />
            <id>https://ohdear.app/87</id>
            <author>
                <name><![CDATA[Oh Dear]]></name>
            </author>
            <summary type="html">
                <![CDATA[<p>Changing the owner of the team can now be done in our UI</p>
<p>In the past, we've seen users reach out to support to change the owner of a time for a variety of reasons.</p>
<p>You can now change the owner of the team without contacting support. Just head over to the team settings and scroll down. As the owner of a team, you can move ownership to another member of your team.</p>
<p><img src="/media/blog/qBhdXRYrXDaI44ZnuIbywwOzxZaoTHi0UFKCGXJT.jpg" alt="" /></p>
<p>Because this action cannot be reversed, it needs to be confirmed.</p>
<p><img src="/media/blog/7NSb9qpsZYGaR8P162yygnATt7WzGrP1w0LBrUmW.jpg" alt="" /></p>
<p>If you're not using Oh Dear to monitor your site, now's the perfect time to get started. We monitor uptime, SSL certificates, broken links, scheduled tasks, application health, DNS, domain expiry and more. We send notifications when something's wrong. All that paired with <a href="/docs/api/introduction" class="underline hover:no-underline">a developer friendly API<a> and <a href="/docs" class="underline hover:no-underline">kick-ass documentation</a>.</p>
]]>
            </summary>
                                    <updated>2023-03-10T08:42:48+00:00</updated>
        </entry>
            <entry>
            <title><![CDATA[Behind The Scenes Of Oh Dear]]></title>
            <link rel="alternate" href="https://ohdear.app/news-and-updates/behind-the-scenes-of-oh-dear" />
            <id>https://ohdear.app/88</id>
            <author>
                <name><![CDATA[Oh Dear]]></name>
            </author>
            <summary type="html">
                <![CDATA[<p>In this sponsored talk given at Laracon India 2023, I demo all major Oh Dear features. After that, I share how the Laravel app behind Oh Dear is structured using domains.</p>
<p>The audio quality is not perfect, but it should still be understandable.</p>
<iframe width="560" height="315" src="https://www.youtube.com/embed/Iq4L6m8SnFM" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe>
]]>
            </summary>
                                    <updated>2023-03-10T13:29:50+00:00</updated>
        </entry>
            <entry>
            <title><![CDATA[You can now add tags to your sites]]></title>
            <link rel="alternate" href="https://ohdear.app/news-and-updates/you-can-now-add-tags-to-your-sites" />
            <id>https://ohdear.app/86</id>
            <author>
                <name><![CDATA[Oh Dear]]></name>
            </author>
            <summary type="html">
                <![CDATA[<p>Some of our users have a lot of sites in their Oh Dear account. A feature often requested is the ability to add a little bit of meta information about each site.</p>
<p>To do that, we've introduced the ability to add tags to a site.</p>
<p>Tags can be used for instance how important it is to fix a problem with the site immediately. Possible tags could be <code>important</code> or <code>has support contract</code>. Another use case would be to add the technology used, tags could be named <code>wordpress</code>, <code>laravel</code>, <code>js</code>.</p>
<p>When adding a site to Oh Dear, you can now create or pick tags for that site.</p>
<p><img src="/media/blog/GnnRumpEMtrlyzXOYkMeeeSbILr24IDjuQdkLJKF.jpg" alt="" /></p>
<p>Picked tags will be visible across our UI.</p>
<p><img src="/media/blog/c537UiFX4tboyvql9fss2YY6pLaL8Tymh19JCSXu.jpg" alt="" /></p>
<p>We will also display the tags in all notifications for a site.</p>
<p>Finally, we'll also add the tags to the response of <a href="/docs/integrations/the-oh-dear-api#sites">any API call that returns a site</a>, and [all webhook notifications](You can use tags to for instance visually mark them as important.).</p>
<p>Should you want to change the tags of a site, head over to the site settings, and take a look at the &quot;Tags&quot; section.</p>
<p><img src="/media/blog/HFytQaqWVH2BTvD7PX4wjiFxtJ1lfY4wuvHot0Od.jpg" alt="" /></p>
<p>You'll also find a &quot;Tags&quot; page in the team settings.</p>
<p><img src="/media/blog/oELpqa153kNMMw4Di2FnRKS7LkmDq6LeSXZmEA3u.jpg" alt="" /></p>
<p>On this screen, you'll see an overview of all tags used across your sites. When you edit or delete tags here, they will be edited or deleted for all sites they are associated with.</p>
<h2 id="in-closing">In closing <a href="#in-closing" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#in-closing">#</a></h2>
<p>Tags is very handy when you want to add a little bit of information about a site.</p>
<p>We very much try to keep our UI simple. Tags are very easy to easy, and if you don't need tags, they won't get in your way.</p>
<p>If you're not using Oh Dear to monitor your site, now's the perfect time to get started. We monitor uptime, SSL certificates, broken links, scheduled tasks, application health, DNS, domain expiry and more. We send notifications when something's wrong. All that paired with <a href="/docs/api/introduction" class="underline hover:no-underline">a developer friendly API<a> and <a href="/docs" class="underline hover:no-underline">kick-ass documentation</a>.</p>
]]>
            </summary>
                                    <updated>2023-02-20T06:49:27+00:00</updated>
        </entry>
            <entry>
            <title><![CDATA[Status pages can now be displayed in multiple languages]]></title>
            <link rel="alternate" href="https://ohdear.app/news-and-updates/status-pages-can-now-be-displayed-in-multiple-languages" />
            <id>https://ohdear.app/85</id>
            <author>
                <name><![CDATA[Oh Dear]]></name>
            </author>
            <summary type="html">
                <![CDATA[<p>In addition to performing <a href="https://ohdear.app/docs/features/what-checks-does-oh-dear-perform">various checks</a> to monitor your site, Oh Dear also offers <a href="https://ohdear.app/features/status-pages">beautiful status pages</a>. Status pages can now use multiple languages. Using these status pages, you can inform your audience about the status of your service. Here's the beautiful Oh Dear powered <a href="https://status.laravel.com">status page of the Laravel team</a>.</p>
<p>Some of our users have a global, multi-lingual audience. That's why we now added support for a status page to be displayed in multiple languages.</p>
<p>If you want to translate your status page, all you need to do is activate some of the languages at the new &quot;Languages&quot; screen on your status page.</p>
<p><img src="/media/blog/5Jb7KbXbwIZ2Olp5K1SWaPguBC6Q3kdoboBtIwPR.jpg" alt="" /></p>
<p>As soon as a status page has more than one active language, it will display a drop down where a visitor can select a language.</p>
<p><img src="/media/blog/WSPGIdYFF3j5JK0FKp4QDXQQyr1gBSeYs0Zc4jMz.jpg" alt="" /></p>
<p>Of course, you can also add a message in multiple languages on your status page. When multiple languages are activated for a status page, you'll see a language dropdown when creating an update. This will allow you to add a translation for the new update in any language your status page supports.</p>
<p><img src="/media/blog/CiPLyLwml8Wku4DNCrvHOtcRTg9tHIB1AqeS1RlS.jpg" alt="" /></p>
<p>We hope that you like this new feature.</p>
<p>If you're not using Oh Dear to monitor your site, now's the perfect time to get started. We monitor uptime, SSL certificates, broken links, scheduled tasks, application health, DNS, domain expiry and more. We send notifications when something's wrong. All that paired with <a href="/docs/api/introduction" class="underline hover:no-underline">a developer friendly API<a> and <a href="/docs" class="underline hover:no-underline">kick-ass documentation</a>.</p>
]]>
            </summary>
                                    <updated>2023-02-18T12:02:58+00:00</updated>
        </entry>
            <entry>
            <title><![CDATA[Our API tokens can now be scoped by site or status page]]></title>
            <link rel="alternate" href="https://ohdear.app/news-and-updates/our-api-tokens-can-now-be-scoped-by-site-or-status-page" />
            <id>https://ohdear.app/84</id>
            <author>
                <name><![CDATA[Oh Dear]]></name>
            </author>
            <summary type="html">
                <![CDATA[<p>Oh Dear has <a href="https://ohdear.app/docs/integrations/the-oh-dear-api">an extensive API</a> that powers <a href="https://ohdear.app/docs/integrations/3rd-party-integrations-of-oh-dear">various powerful integrations</a>.</p>
<p>To use the API, you first need to create an API Token in the Oh Dear UI. Previously, such a token could be used to make API calls to any site or status page in your Oh Dear account.</p>
<p><img src="/media/blog/af7OVRCh1KHDMGVGXxMKlzBFjdj4dGHLvX9cecOU.jpg" alt="" /></p>
<p>We noticed that some of our users are agencies that use Oh Dear to monitor their clients' sites. When such an agency passes an Oh Dear API Token to one of their clients, then that client could potentially use the broadly scoped token to view the results and settings of other clients of that agency.</p>
<p>In general, it's best practice to scope down the abilities of the token to the bare minimum it needs to do in the integration where it will be used.</p>
<p>Today, we're launching the ability to scope an API Token by site or status page. When creating a token, you can pick the sites and status pages it should have access to.</p>
<p><img src="/media/blog/3JoJ7sxHXb0U7WY3VnOfuqM6l0BN8Yd42OKL2bBc.jpg" alt="" /></p>
<p>We hope that you like this nice addition.</p>
<p>If you're not using Oh Dear to monitor your site, now's the perfect time to get started. We monitor uptime, SSL certificates, broken links, scheduled tasks, application health, DNS, domain expiry and more. We send notifications when something's wrong. All that paired with <a href="/docs/api/introduction" class="underline hover:no-underline">a developer friendly API<a> and <a href="/docs" class="underline hover:no-underline">kick-ass documentation</a>.</p>
]]>
            </summary>
                                    <updated>2023-02-07T18:12:31+00:00</updated>
        </entry>
            <entry>
            <title><![CDATA[Lighthouse SEO monitoring is now available at Oh Dear]]></title>
            <link rel="alternate" href="https://ohdear.app/news-and-updates/lighthouse-seo-monitoring-is-now-available-at-oh-dear" />
            <id>https://ohdear.app/83</id>
            <author>
                <name><![CDATA[Oh Dear]]></name>
            </author>
            <summary type="html">
                <![CDATA[<p>We're proud to announce we have added a new check to our service: <strong>Lighthouse SEO</strong>. Using this check you can detect (and get solution suggestions) for SEO and performance problems.</p>
<h2 id="what-is-lighthouse">What is Lighthouse <a href="#what-is-lighthouse" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#what-is-lighthouse">#</a></h2>
<p>Lighthouse is a tool provided by Google for improving the quality of web pages. It has audits for performance, accessibility, progressive web apps, SEO, and more.</p>
<p>One of the key benefits of this check is that it provides specific, <em>actionable</em> suggestions for improving the quality of a website. For example, it may suggest optimizing images or minifying JavaScript to improve performance. By implementing these suggestions, a website can become faster, more accessible, and more secure, improving its ranking in search engine results.</p>
<p>When the Lighthouse check is enabled for your site, it will be run on a daily basis.</p>
<h2 id="seeing-lighthouse-results-at-oh-dear">Seeing Lighthouse results at Oh Dear <a href="#seeing-lighthouse-results-at-oh-dear" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#seeing-lighthouse-results-at-oh-dear">#</a></h2>
<p>When the Lighthouse SEO check is enabled for a site, you can see the results for your site on the Lighthouse check page. This page contains the scores, key metrics, and a graph picturing the trends of results</p>
<p>Here's how it might look like.</p>
<p><img src="/media/blog/adgYNMC5cBRbZBpK2N4wB8YKErSLA1zAXdlsaOh5.jpg" alt="" /></p>
<p>When you scroll a bit down, you'll see the full report as it was generated by Lighthouse, that contains the results and recommendations by all Lighthouse audits.</p>
<p><img src="/media/blog/679xPVBX8x5nMhJrx8SEzzGbZgXTpkSGWIsSchTK.jpg" alt="" /></p>
<h2 id="get-notified-when-lighthouse-detects-issues">Get notified when Lighthouse detects issues <a href="#get-notified-when-lighthouse-detects-issues" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#get-notified-when-lighthouse-detects-issues">#</a></h2>
<p>Oh Dear can send you a notification when one of the scores or metrics are not within expectations. On the settings page of the lighthouse check you have fine-grained control of when we should notify you.</p>
<p><img src="/media/blog/3Oz7rHwWulceudPQPoUJOjfMvhnKDmrCySjo3Tqc.jpg" alt="" /></p>
<p>Here's an example notification.</p>
<p><img src="/media/blog/KhaQxZhaTjWdy6CZdMMHsJyLPyzdCawBlwCj3e8Z.jpg" alt="" /></p>
<h2 id="in-closing">In closing <a href="#in-closing" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#in-closing">#</a></h2>
<p>We think that the Lighthouse SEO check is a very powerful addition to our feature set. It will help you rank better on Google and get notified whenever an update to your site hurts SEO ranking.</p>
<p>If you're not using Oh Dear to monitor your site, now's the perfect time to get started.  We monitor uptime, SSL certificates, broken links, scheduled tasks, application health, DNS, domain expiry and more. We send notifications when something's wrong. All that paired with <a href="/docs/api/introduction" class="underline hover:no-underline">a developer friendly API<a> and <a href="/docs" class="underline hover:no-underline">kick-ass documentation</a>.</p>
]]>
            </summary>
                                    <updated>2023-01-25T12:20:17+00:00</updated>
        </entry>
            <entry>
            <title><![CDATA[We have redesigned our entire service]]></title>
            <link rel="alternate" href="https://ohdear.app/news-and-updates/we-have-redesigned-our-entire-service" />
            <id>https://ohdear.app/82</id>
            <author>
                <name><![CDATA[Oh Dear]]></name>
            </author>
            <summary type="html">
                <![CDATA[<p>As of today, Oh Dear is in a brand new jacket. We've totally redesigned Oh Dear's UI. Our app doesn't only look better, but we've also made it much easier to use.</p>
<p>We feel that our new design should speak for itself, so we highly recommend visiting <a href="https://ohdear.app">the home page</a>, browsing a bit around, <a href="https://ohdear.app/register">register an account</a>, or <a href="https://ohdear.app/login">log in</a>, and discover the redesigned app yourself.</p>
<p>If you've been using Oh Dear before, you'll notice that we polished everything, and the UX should be much better.</p>
<p>To read more about why and how we redesigned the service and want to read some technical details, head over to <a href="https://freek.dev/2343-oh-dear-20-has-been-launched">this extensive blog post on Freek's blog</a>.</p>
<p>We hope you like the redesign as much as we do!</p>
]]>
            </summary>
                                    <updated>2022-10-03T14:08:10+00:00</updated>
        </entry>
            <entry>
            <title><![CDATA[A preview of our upcoming redesign]]></title>
            <link rel="alternate" href="https://ohdear.app/news-and-updates/a-preview-of-our-upcoming-redesign" />
            <id>https://ohdear.app/81</id>
            <author>
                <name><![CDATA[Oh Dear]]></name>
            </author>
            <summary type="html">
                <![CDATA[<p>Earlier this year, we announced that one of our goals for this year is to bring the UI of Oh Dear to the next level. Behind the scenes, our team is working hard on a complete rewrite of our marketing website and app.</p>
<p>We're currently targeting the end of September timeframe to launch our redesign. In this blog post, we'd like to give you a preview of the redesign.</p>
<h2 id="the-marketing-site">The marketing site <a href="#the-marketing-site" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#the-marketing-site">#</a></h2>
<p>One of the most important pages is, of course, the homepage. Here's what our current one looks like.</p>
<p><img src="/uploads/blogs/redesign-preview/current-homepage.png" alt="screenshot" /></p>
<p>And here's the redesigned one:</p>
<p><img src="/uploads/blogs/redesign-preview/beta-homepage.png" alt="screenshot" /></p>
<p>You can see that together with redesigning our site; we're also updating our logo.</p>
<p>On the redesigned homepage, there's a lot of content below the fold that's not shown in the screenshot above. We explain which features there are, show some testimonials and explain our pricing. Here are some more screenshots of the stuff below the fold.</p>
<p><img src="/uploads/blogs/redesign-preview/beta-homepage-2.png" alt="screenshot" /></p>
<p><img src="/uploads/blogs/redesign-preview/beta-homepage-3.png" alt="screenshot" /></p>
<p>You can't see in these screenshots that there are a lot of subtle animations.</p>
<p>We're also updating our docs section. Here's what it currently looks like.</p>
<p><img src="/uploads/blogs/redesign-preview/current-docs.png" alt="screenshot" /></p>
<p>And here's what the redesigned docs look like.</p>
<p><img src="/uploads/blogs/redesign-preview/beta-docs.png" alt="screenshot" /></p>
<p>This is just a small preview of the new marketing site. I hope it's clear that we're not just updating details but are rebuilding it from the ground up.</p>
<h2 id="the-oh-dear-application-ui">The Oh Dear application UI <a href="#the-oh-dear-application-ui" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#the-oh-dear-application-ui">#</a></h2>
<p>The public marketing pages are important for us as we hope they'll convince more people to use our service. But we didn't stop at the marketing site. We've also totally redesigning the app itself, solving long-standing usability issues in the process.</p>
<h3 id="the-sites-list">The sites list</h3>
<p>The current app UI is now almost five years old. We designed it when Oh Dear only offered its initial four checks. At the moment of writing, we offer nine checks.  Here's what our current site list looks like.</p>
<p><img src="/uploads/blogs/redesign-preview/current-sites.png" alt="screenshot" /></p>
<p>You can see that, with nine checks, it's a bit busy.</p>
<p>Here's what the redesigned list looks like. Don't mind those red dots at the performance checks, that's just because of seeded data.</p>
<p><img src="/uploads/blogs/redesign-preview/beta-sites.png" alt="screenshot" /></p>
<p>You can see that this list is much calmer. We only show the issues that we find. If no issues are found, the dot before the site will be green.</p>
<p>When you click those three dots at the end of a row, you'll see a little submenu that allows you to navigate to common pages for that site.</p>
<p><img src="/uploads/blogs/redesign-preview/site-menu.png" alt="screenshot" /></p>
<p>Let's take a look at the old site overview page.</p>
<p><img src="/uploads/blogs/redesign-preview/current-site-overview.png" alt="screenshot" /></p>
<p>Not too bad, but it's much better in our redesign. We'll show a lot more helpful information by default.</p>
<p><img src="/uploads/blogs/redesign-preview/beta-site-overview.png" alt="screenshot" /></p>
<p>Again, those three dots allow you to take relevant actions or pages for a check.</p>
<p><img src="/uploads/blogs/redesign-preview/check-menu.png" alt="screenshot" /></p>
<h3 id="notifications-preferences">Notifications preferences</h3>
<p>Let's go to another important part of our application. Oh Dear can send you notifications whenever we detect something wrong with your site. There are many notification channels: mail, Slack, Telegram, Discord, ... On the team notifications screen, you can configure for which events we should send on a particular channel.</p>
<p>Here's what the current screens look like. We have screens per channel, and on the list of a channel you can see all the possible notifications we can send.</p>
<p><img src="/uploads/blogs/redesign-preview/current-notifications.png" alt="screenshot" /></p>
<p>The problem with this setup is that it's hard to see which channels you have configured. To do that, you'll have to click &quot;Mail&quot;, &quot;Slack&quot;, &quot;Discord&quot;, ... in the sidebar and check if you have something configured.</p>
<p>In the redesign, we've revamped this screen. Instead of screens per channel, we only have one screen that shows all notification configurations.</p>
<p><img src="/uploads/blogs/redesign-preview/beta-notifications.png" alt="screenshot" /></p>
<p>On the list we don't show all those notifications toggles anymore. Instead, we show how many notifications are configured for the channel. If you want to see which notifications, you can edit a configuration.</p>
<p><img src="/uploads/blogs/redesign-preview/beta-notifications-detail.png" alt="screenshot" /></p>
<h3 id="configuring-status-pages">Configuring status pages</h3>
<p>In addition to monitoring sites and applications, Oh Dear also offers beautiful status pages. The status pages allow you to communicate the health of your service to your audience. Here are some Oh Dear powered status pages for <a href="https://status.laravel.com">the Laravel organization</a> and <a href="https://status.flareapp.io">Flare</a>.</p>
<p>We almost didn't dare to share a screenshot of how the UI now looks to set up a status page. It's kind of messy.</p>
<p><img src="/uploads/blogs/redesign-preview/current-status-pages.png" alt="screenshot" /></p>
<p>Sure, you can do a lot here, but it isn't pleasing to the eyes. In our redesign, we've vastly simplified this list. Here's what it looks like.</p>
<p><img src="/uploads/blogs/redesign-preview/beta-status-pages.png" alt="screenshot" /></p>
<p>If you click on of the status pages, you'll see this status page overview.</p>
<p><img src="/uploads/blogs/redesign-preview/beta-status-page-detail.png" alt="screenshot" /></p>
<p>Notice how similar this all looks to the site list and site overview. This is a benefit to us: if you are already familiar with sites, you'll immediately feel at home when working with status pages.</p>
<h2 id="in-closing">In closing <a href="#in-closing" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#in-closing">#</a></h2>
<p>We hope that you liked this preview of our upcoming redesign. Currently, we're styling all of the separate settings screens. It's still a lot of work, but we'll get there. As mentioned above, we aim to launch this around end-of-September.</p>
<p>At that time, we'll also write some blog posts with technical details on the redesign. I can already share that we've built this using Laravel, Livewire, Alpine and Tailwind.</p>
<p>Oh Dear is the all-in-one monitoring tool for your entire website. Now is the perfect time to start monitoring your site using <a href="/register">our ten-day free trial</a>. When we launch our redesign, we'll also increase our prices. Old customers will always keep the current prices.</p>
<p>We monitor uptime, SSL certificates, broken links, scheduled tasks, application health, DNS, domain expiry and more. We send notifications when something's wrong. All that paired with a developer-friendly API and kick-ass documentation.</p>
]]>
            </summary>
                                    <updated>2022-08-18T11:14:02+00:00</updated>
        </entry>
            <entry>
            <title><![CDATA[Making sure routes and config files are cached in a Laravel app]]></title>
            <link rel="alternate" href="https://ohdear.app/news-and-updates/making-sure-routes-and-config-files-are-cached-in-a-laravel-app" />
            <id>https://ohdear.app/80</id>
            <author>
                <name><![CDATA[Oh Dear]]></name>
            </author>
            <summary type="html">
                <![CDATA[<p>In a typical Laravel application, you'll likely to have many routes, config files and possible some events. In your development environment these routes and config files will loaded and registered in each request. The performance penalty for this is not too big.</p>
<p>In a production environment, you want to cache these things. Laravel makes this easy by offering a couple of Artisan commands that you can use in your deployment procedure.</p>
<pre data-lang="bash" class="notranslate"><span class="hl-keyword">php</span> <span class="hl-keyword">artisan</span> optimize <span class="hl-comment"># will cache routes and config</span>
<span class="hl-keyword">php</span> <span class="hl-keyword">artisan</span> event:cache <span class="hl-comment"># will cache events</span>
</pre>
<p>By caching these things, you'll improve the performance of your Laravel app</p>
<p>Using our <a href="https://ohdear.app/feature/application-health-monitoring">application health</a> check, you can get notified when things are not cached in production.</p>
<p>Oh Dear will not run any code inside your application or server. Instead, you should perform the checks yourself. Oh Dear will send an HTTP request to your application to a specific endpoint. Your application should respond with JSON containing the result of health checks.</p>
<p>Spatie's <a href="https://spatie.be/docs/laravel-health/v1/introduction">Laravel Health</a> package can build up the JSON the Oh Dear expects. Here are <a href="https://ohdear.app/docs/general/application-health-monitoring/laravel">the docs on how to do that</a>.</p>
<p><a href="https://spatie.be/docs/laravel-health/v1/introduction">Laravel Health</a> is a package that can detect various problems that are going on with your application and server. It can check disk space, cpu usage, if Horizon is running, and much more.</p>
<p>The package has <a href="https://spatie.be/docs/laravel-health/v1/available-checks/cached-config-routes-and-events">a new check</a> that makes sure wether routes, config and events are cached.</p>
<p>This is how you can use it. Check in the package can be registered via <code>Health::check()</code> function.</p>
<pre data-lang="php" class="notranslate"><span class="hl-comment">// typically in a service provider</span>

<span class="hl-keyword">use</span> <span class="hl-type">Spatie\Health\Facades\Health</span>;
<span class="hl-keyword">use</span> <span class="hl-type">Spatie\Health\Checks\Checks\OptimizedAppCheck</span>;

<span class="hl-type">Health</span>::<span class="hl-property">checks</span>([
		<span class="hl-comment">// other checks...</span>

    <span class="hl-type">OptimizedAppCheck</span>::<span class="hl-keyword">new</span>(),
]);
</pre>
<p>This check will pass if the config, routes and events are cached.</p>
<p>If you only want to check certain caches, you can call the checkConfig, checkRoutes and checkEvents methods. In this example, we'll only check for cached config and routes.</p>
<pre data-lang="php" class="notranslate"><span class="hl-keyword">use</span> <span class="hl-type">Spatie\Health\Facades\Health</span>;
<span class="hl-keyword">use</span> <span class="hl-type">Spatie\Health\Checks\Checks\OptimizedAppCheck</span>;

<span class="hl-type">Health</span>::<span class="hl-property">checks</span>([
		<span class="hl-comment">// other checks,</span>

    <span class="hl-type">OptimizedAppCheck</span>::<span class="hl-keyword">new</span>()
       -&gt;<span class="hl-property">checkConfig</span>()
       -&gt;<span class="hl-property">checkRoutes</span>(),
]);
</pre>
<p>With that <code>OptimizedAppCheck</code> in place and Oh Dear app configured, this is what it looks like when something is not cached.</p>
<p><img src="/uploads/blogs/cache-config/slack.png" alt="image" /></p>
<p>This is a Slack notification, but it will look similar on all notification channels we offer.</p>
]]>
            </summary>
                                    <updated>2022-07-16T15:16:39+00:00</updated>
        </entry>
            <entry>
            <title><![CDATA[You can now monitor your domain name using Oh Dear]]></title>
            <link rel="alternate" href="https://ohdear.app/news-and-updates/you-can-now-monitor-your-domain-name-using-oh-dear" />
            <id>https://ohdear.app/79</id>
            <author>
                <name><![CDATA[Oh Dear]]></name>
            </author>
            <summary type="html">
                <![CDATA[<p>When registering a domain name, you could assume it is yours forever. Unfortunately, this is false, and most domains must be renewed periodically. If you fail to do this, you risk losing your domain, and ownership could be transferred away from you.</p>
<p>Oh Dear's new Domain check can send you a notification days before your domain expires. This way, you still have time to renew it.</p>
<h2 id="introducing-rdap">Introducing RDAP <a href="#introducing-rdap" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#introducing-rdap">#</a></h2>
<p>To monitor domain names, we rely on <a href="https://www.icann.org/rdap">RDAP</a>, which stands for Registration Data Access Protocol. It is a standardized protocol managed by <a href="https://www.iana.org">IANA</a>, the non-profit organization that manages IP address allocation, DNS, and more... RDAP is the successor of WHOIS. The most significant improvement is that all information in RDAP is standardized, whereas information in WHOIS is available as unstructured text.</p>
<p>Registry operators of most TLDs (<code>.com</code>, <code>.net</code>, <code>.org</code>, ...) send information about the domains they manage to RDAP so anyone can query the information. Unfortunately, not all TLDs are supported.</p>
<p>Most notably: <code>.be</code> and <code>.nl</code> domains are not supported, but they will be in time.</p>
<h2 id="how-we-monitor-your-domain">How we monitor your domain <a href="#how-we-monitor-your-domain" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#how-we-monitor-your-domain">#</a></h2>
<p>Using RDAP, we can determine important dates of a domain, such as the registration and expiration date. When you turn on domain monitoring for your domain, we'll check RDAP several times a day. We'll send you a notification when your site is not listed in RDAP or if your domain expires in the next 10 days. The number of days can be configured in the domain settings of a site.</p>
<p>We will not spam you with domain check notifications: we'll only send you one per day.</p>
<p>Additionally, we'll also fetch the <em>domain status codes</em>. These are special attributes that can be set on a domain name. You'll find a list of all codes and their meaning on <a href="https://www.icann.org/resources/pages/epp-status-codes-2014-06-16-en">the EPP status codes page of Icann</a>.</p>
<p>One of the most important ones is <code>clientTransferProhibited</code>, which signifies that your registry will automatically reject domain transfer requests, significantly preventing domain theft.</p>
<p>Here's what the domain check results look like in our UI:</p>
<p><img src="/img/docs/domain/domain.png" alt="image" /></p>
<p>As mentioned above, our domain check is not available for all domains because not all domains are supported by RDAP. We expect each domain to be supported in the next couple of years.</p>
<p>You'll find an up to date list of each supported domain <a href="https://ohdear.app/docs/general/domain-monitoring">in our docs</a>.</p>
<h2 id="in-closing">In closing <a href="#in-closing" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#in-closing">#</a></h2>
<p>We hope that you will like this new check. If you already have an Oh Dear subscription, you can now enable it for any site that you are monitoring.</p>
<p>Behind the scenes, we're working on a couple more improvements to our service. And later this year, we'll launch a total redesign, of which you can already see a preview <a href="https://ohdear.app/blog/building-oh-dears-new-design-implementing-the-design">in this blogpost</a>.</p>
<p>If you're not using Oh Dear to monitor your site, now's the perfect time to get started.  We monitor uptime, SSL certificates, broken links, scheduled tasks, application health, DNS, domain expiry and more. We send notifications when something's wrong. All that paired with <a href="/docs/api/introduction" class="underline hover:no-underline">a developer friendly API<a> and <a href="/docs" class="underline hover:no-underline">kick-ass documentation</a>.</p>
]]>
            </summary>
                                    <updated>2022-06-15T07:06:50+00:00</updated>
        </entry>
            <entry>
            <title><![CDATA[Building Oh Dear’s new design: Creating a color system, why and how]]></title>
            <link rel="alternate" href="https://ohdear.app/news-and-updates/building-oh-dears-new-design-creating-a-color-system-why-and-how" />
            <id>https://ohdear.app/78</id>
            <author>
                <name><![CDATA[Oh Dear]]></name>
            </author>
            <summary type="html">
                <![CDATA[<p>In <a href="https://ohdear.app/blog/building-oh-dears-new-design-implementing-the-design">the previous blog post</a> I talked about how we are implementing the Oh Dear redesign. In this post I go into more detail on why and how we implemented a tailwind-like color system which supports all the colours used throughout the redesign.</p>
<h2 id="from-sketch-to-code">From Sketch to code <a href="#from-sketch-to-code" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#from-sketch-to-code">#</a></h2>
<p>One of the tasks that needs to be done to go from design to code is extracting the colours used in the design. In our case, the designer already put together a specific palette of colours and defined it within the Sketch file. We copied over those colours and renamed them a bit so they fit nicely in our tailwind.config.js.</p>
<pre data-lang="txt" class="notranslate">extend: {
	colors: {
		'bg-blue': '#...',
		'darkish-blue': '#...',
		divide: '#...',
		whiteish: '#...',
		green: '#...',
        ...
	},
	...
}
</pre>
<h3 id="not-all-colours-were-named">Not all colours were named</h3>
<p>While implementing the design, more and more colours kept popping up that were defined in the inspector as a plain hex code, instead of a named color.</p>
<p><img src="/uploads/blogs/redesign-2022-part-3/color-inspector.png" alt="image" /></p>
<p>As we already had to rename the tracked colours, I didn’t think it would be helpful to ask to designer to go through the document and give every specific color a name. We would have to know the desired naming pattern and we didn’t know what would be the best approach.</p>
<p>The design is also very complex with a lot of gradients and nuances created by use of different opacities. Going through all the layers and track all those colours would be very time consuming. We decided to leave the document as is and look for another alternative.</p>
<h3 id="tailwind-to-the-rescue-or-not">Tailwind to the rescue, or not?</h3>
<p>The first solution we ended up with was using tailwind classes with arbitrary values like <code>text-[#0C0515]</code> or <code>bg-[#0C0515]</code>, which is supported since [JIT mode]. This made it really easy to give an element a very specific color without the need to think of a name and having to define it in the tailwind config file. This felt like a real time-saver.</p>
<p>Although this worked well at the beginning we started to discover more and more colours that kind of looked the same but had a different hex code. As hex codes can vary a lot, while looking visually the same, and vice versa, it wasn’t easy to search occurrences of previously used hex codes that were close to a specific color.</p>
<p><img src="/uploads/blogs/redesign-2022-part-3/hex-codes.jpg" alt="image" /></p>
<p>This also resulted in code that was flooded with arbitrary values that didn’t make the use of color much consistent. Dark mode was also on the list of features to implement, and adding dark mode specific hex codes wouldn’t make it any cleaner.</p>
<p><img src="/uploads/blogs/redesign-2022-part-3/arbitrary-values-example.png" alt="image" /></p>
<h3 id="difficulty-of-going-a-shade-lighter-or-darker">Difficulty of going a shade lighter or darker</h3>
<p>Another difficulty we faced was the ability to play with a slightly lighter of darker shade. Sometimes a different colour would look better on the web compared to what was used in the design. To do that we adjusted the hex code, which then turned into yet another unique arbitrary value. We figured that having all those arbitrary values would lead to more inconsistencies and a bigger than necessary css bundle housing all those colours.</p>
<p>At this point we thought it would be great if we could use Tailwind’s colour system. Going a shade lighter or darker is as easy as lowering or increasing the number in the class. For example <code>text-blue-500</code> is lighter than <code>text-blue-600</code>. There was only one problem, the standard colours didn’t match the used colours in the design. So we had to extend the existing set of colours.</p>
<h2 id="creating-our-own-color-palettes">Creating our own color palettes <a href="#creating-our-own-color-palettes" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#creating-our-own-color-palettes">#</a></h2>
<p>We created our own color palettes through a series of easy to reproduce steps.</p>
<h3 id="1-index-all-colours">1. Index all colours</h3>
<p>The first step we took was index all colours used in the design. And put them in a separate art-board in Sketch. We ended up with something like this:</p>
<p><img src="/uploads/blogs/redesign-2022-part-3/collecting-colors.png" alt="image" /></p>
<h3 id="2-group-the-colours">2. Group the colours</h3>
<p>Next we grouped them by color, and we could already spot different colours that were almost similar and could be consolidated into a single color:</p>
<p><img src="/uploads/blogs/redesign-2022-part-3/group-colors.png" alt="image" /></p>
<h3 id="3-fill-in-the-blanks">3. Fill in the blanks</h3>
<p>After grouping them we started to order them from light to dark. As we didn’t have step-based shades for each colour yet we filled in the blanks where needed. We ended up with five base colours and a few specific colours with fewer shades:</p>
<p><img src="/uploads/blogs/redesign-2022-part-3/complete-color-system.jpg" alt="image" /></p>
<p>As you might notice we ended up with a higher resolution palette than the standard 9 shades per base color. This is because the design contained many colours that were slightly lighter or darker. We tried to replace a specific X50 for a X00 instead, but the difference in color was often too big. So we needed those subtle variations in shades and decided to create shades of the base colours in steps of 50 instead of 100.</p>
<h3 id="but-you-wont-be-using-all-those-colours">But you won’t be using all those colours!</h3>
<p>That’s true, but with Tailwind that’s not a problem. Thanks to the ability to purge all unused styles the final CSS bundle will be small no matter how much colours we define in the config.</p>
<p>The only downside is that your tailwind config can grow pretty large while having so many colours. If you would really dislike that you can always extract the colours to a separate file and then include that in the config file.</p>
<h3 id="4-import-and-replace">4. Import and replace</h3>
<p>After having all the hex codes imported in the tailwind config we replaced all the hex codes in the html with the newly available color classes. The code now looked cleaner as the color + a number was more familiar than a hex code. It’s now also easier to play with the shades as we only need to increase or lower the number in the color class at the end by 50.</p>
<h2 id="in-conclusion">In conclusion <a href="#in-conclusion" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#in-conclusion">#</a></h2>
<p>The best way of exporting colours from design to code is very dependant on the tools and frameworks you use. In our case we use Tailwind as a CSS framework and we were already familiar with its color system. If you don’t use Tailwind a different approach might be  better, although I could still see how the method described in this post can be applied on any project.</p>
<p>Creating the system was a bit tedious and time consuming. Most time was spent on ordering the colours and filling in the blanks which was done by hand. However, many weeks have been passed by now since implementing the color system and we are still very happy with it. It’s definitely an investment worth doing.</p>
<p>In the next post I will go into more detail on how we are implementing dark mode.</p>
<p>Do you have a question or feedback? Feel free to send a reply <a href="https://twitter.com/ohdearapp">via Twitter</a>.</p>
]]>
            </summary>
                                    <updated>2022-05-20T08:41:00+00:00</updated>
        </entry>
            <entry>
            <title><![CDATA[Easily navigate Oh Dear using the command palette]]></title>
            <link rel="alternate" href="https://ohdear.app/news-and-updates/easily-navigate-oh-dear-using-the-command-palette" />
            <id>https://ohdear.app/77</id>
            <author>
                <name><![CDATA[Oh Dear]]></name>
            </author>
            <summary type="html">
                <![CDATA[<p>When you log in Oh Dear, you'll notice a new little search field in the header.</p>
<p><img src="/uploads/blogs/spotlight/search.png" alt="image" /></p>
<p>When you click that, it opens up a nice command palette. You can use this to navigate to anywhere in our service quickly.</p>
<p><img src="/uploads/blogs/spotlight/palette.png" alt="image" /></p>
<p>If you want to go to the performance graph of your site, just type &quot;performance&quot;, ...</p>
<p><img src="/uploads/blogs/spotlight/command.png" alt="image" /></p>
<p>... and type the site's name you want to see the performance results of.</p>
<p><img src="/uploads/blogs/spotlight/site.png" alt="image" /></p>
<p><img src="/uploads/blogs/spotlight/site2.png" alt="image" /></p>
<p>When selecting the site, you'll get taken to the performance results.</p>
<p><img src="/uploads/blogs/spotlight/performance.png" alt="image" /></p>
<p>In many cases, this is much faster than clicking on the site and then going to the right check results yourself.</p>
<p>The command palette has commands to navigate to almost anywhere in our UI, and tasks like logging out, and switching teams.</p>
<p><img src="/uploads/blogs/spotlight/other.png" alt="image" /></p>
<p>If you want to know more details on how this works under the hood, check out <a href="https://freek.dev/2251-how-to-add-a-spotlight-like-search-field-to-your-laravel-app">this blog post on freek.dev</a>.</p>
<p>We hope that you'll like this addition to our UI.</p>
]]>
            </summary>
                                    <updated>2022-05-19T14:05:42+00:00</updated>
        </entry>
            <entry>
            <title><![CDATA[Status pages can now be viewed as JSON or XML]]></title>
            <link rel="alternate" href="https://ohdear.app/news-and-updates/status-pages-can-now-be-viewed-as-json-or-xml" />
            <id>https://ohdear.app/76</id>
            <author>
                <name><![CDATA[Oh Dear]]></name>
            </author>
            <summary type="html">
                <![CDATA[<p>Next to a large collection of checks, Oh Dear offers the ability to easily create beautiful status pages. This way, you can communicate the status of your sites and services to your users. Take a look at the Oh Dear powered status pages for <a href="https://status.flareapp.io">Flare</a> and <a href="https://status.laravel.com">Laravel</a>.</p>
<p><img src="/uploads/blogs/status-json/statuspage.png" alt="image" /></p>
<p>Today, we added the ability to view the status page as JSON (or XML if that is your thing). You just have to append <code>/json</code> (or <code>/xml</code> to the status page URL. So for the Laravel status page, you'll find the JSON at <a href="https://status.laravel.com/json">status.laravel.com/json</a>.</p>
<p>Here's what the response looks like:</p>
<pre data-lang="json" class="notranslate"><span class="hl-property">{</span>
  <span class="hl-keyword">&quot;title&quot;</span>: <span class="hl-value">&quot;Laravel Service Health Dashboard&quot;</span>,
  <span class="hl-keyword">&quot;timezone&quot;</span>: <span class="hl-value">&quot;America\/Chicago&quot;</span>,
  <span class="hl-keyword">&quot;pinnedUpdate&quot;</span>: null,
  <span class="hl-keyword">&quot;sites&quot;</span>: <span class="hl-property">{</span>
    <span class="hl-keyword">&quot;Ungrouped&quot;</span>: <span class="hl-property">[</span>
      <span class="hl-property">{</span>
        <span class="hl-keyword">&quot;label&quot;</span>: <span class="hl-value">&quot;envoyer.io&quot;</span>,
        <span class="hl-keyword">&quot;url&quot;</span>: <span class="hl-value">&quot;https:\/\/envoyer.io&quot;</span>,
        <span class="hl-keyword">&quot;status&quot;</span>: <span class="hl-value">&quot;up&quot;</span>
      <span class="hl-property">}</span>,
      <span class="hl-property">{</span>
        <span class="hl-keyword">&quot;label&quot;</span>: <span class="hl-value">&quot;forge.laravel.com&quot;</span>,
        <span class="hl-keyword">&quot;url&quot;</span>: <span class="hl-value">&quot;https:\/\/forge.laravel.com&quot;</span>,
        <span class="hl-keyword">&quot;status&quot;</span>: <span class="hl-value">&quot;up&quot;</span>
      <span class="hl-property">}</span>,
      <span class="hl-property">{</span>
        <span class="hl-keyword">&quot;label&quot;</span>: <span class="hl-value">&quot;laravel.com&quot;</span>,
        <span class="hl-keyword">&quot;url&quot;</span>: <span class="hl-value">&quot;https:\/\/laravel.com&quot;</span>,
        <span class="hl-keyword">&quot;status&quot;</span>: <span class="hl-value">&quot;up&quot;</span>
      <span class="hl-property">}</span>,
      <span class="hl-property">{</span>
        <span class="hl-keyword">&quot;label&quot;</span>: <span class="hl-value">&quot;vapor.laravel.com&quot;</span>,
        <span class="hl-keyword">&quot;url&quot;</span>: <span class="hl-value">&quot;https:\/\/vapor.laravel.com&quot;</span>,
        <span class="hl-keyword">&quot;status&quot;</span>: <span class="hl-value">&quot;up&quot;</span>
      <span class="hl-property">}</span>,
      <span class="hl-property">{</span>
        <span class="hl-keyword">&quot;label&quot;</span>: <span class="hl-value">&quot;nova.laravel.com&quot;</span>,
        <span class="hl-keyword">&quot;url&quot;</span>: <span class="hl-value">&quot;https:\/\/nova.laravel.com&quot;</span>,
        <span class="hl-keyword">&quot;status&quot;</span>: <span class="hl-value">&quot;up&quot;</span>
      <span class="hl-property">}</span>
    <span class="hl-property">]</span>
  <span class="hl-property">}</span>,
  <span class="hl-keyword">&quot;updatesPerDay&quot;</span>: <span class="hl-property">{</span>
    <span class="hl-keyword">&quot;1652245200&quot;</span>: <span class="hl-property">[</span><span class="hl-property">]</span>,
    <span class="hl-keyword">&quot;1652158800&quot;</span>: <span class="hl-property">[</span><span class="hl-property">]</span>,
    <span class="hl-keyword">&quot;1652072400&quot;</span>: <span class="hl-property">[</span><span class="hl-property">]</span>,
    <span class="hl-keyword">&quot;1651986000&quot;</span>: <span class="hl-property">[</span><span class="hl-property">]</span>,
    <span class="hl-keyword">&quot;1651899600&quot;</span>: <span class="hl-property">[</span><span class="hl-property">]</span>,
    <span class="hl-keyword">&quot;1651813200&quot;</span>: <span class="hl-property">[</span><span class="hl-property">]</span>,
    <span class="hl-keyword">&quot;1651726800&quot;</span>: <span class="hl-property">[</span><span class="hl-property">]</span>
  <span class="hl-property">}</span>
<span class="hl-property">}</span>
</pre>
<p>By offering JSON (and XML), your users can integrate the status of your service better in their systems. If you forget the exact URL, you can access the JSON (or XML) version via &quot;Subscribe to updates&quot; dropdown in the header of a status page.</p>
<p><img src="/uploads/blogs/status-json/dropdown.png" alt="screenshot" /></p>
<p>Alternatively, your users can also subscribe to updates of your status page via Slack. You'll find more info about that in <a href="https://ohdear.app/blog/status-page-subscriptions-are-now-available">this blog post</a>.</p>
]]>
            </summary>
                                    <updated>2022-05-11T07:01:11+00:00</updated>
        </entry>
            <entry>
            <title><![CDATA[Building Oh Dear’s new design: Implementing the design]]></title>
            <link rel="alternate" href="https://ohdear.app/news-and-updates/building-oh-dears-new-design-implementing-the-design" />
            <id>https://ohdear.app/75</id>
            <author>
                <name><![CDATA[Oh Dear]]></name>
            </author>
            <summary type="html">
                <![CDATA[<p>In the <a href="/blog/building-oh-dears-new-design-project-setup">previous blog post</a> I gave an introduction about the project setup for the redesign of the new Oh Dear frontend. In this blog post I would like to show you how we are implementing the redesign of the Oh Dear frontend. Feel free to provide feedback on the design choices and statements made in this and future blog posts. We’d love to hear what you think of it.</p>
<h2 id="it-all-started-with-sketch">It all started with Sketch <a href="#it-all-started-with-sketch" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#it-all-started-with-sketch">#</a></h2>
<p>Before we started the build process we had every page redesigned upfront by the talented team of <a href="https://digitalwithyou.com">DWY</a>. They have been instructed to design something unique that would make Oh Dear look and feel next level. We think they did a perfect job. The result was a Sketch file containing approximately 25 pixel perfect page designs. Each containing about 5 to 10 sections that exist out of multiple components and unique design elements.</p>
<p><img src="/uploads/blogs/redesign-2022-part-2/dns-section.jpg" alt="DNS section" /></p>
<p><img src="/uploads/blogs/redesign-2022-part-2/customize-looks-to-match-your-brand.jpg" alt="Customize the looks to match your brand" /></p>
<p><img src="/uploads/blogs/redesign-2022-part-2/powerful-notifications.jpg" alt="Powerful notifications" /></p>
<p>This resulted in approximately 100 components to be build. Many of those components contain a unique visual sub-component which ideally should become interactive in many cases.</p>
<p>That’s not all, besides light mode all pages should also support dark mode and of course look perfect and be interactive on any screen size. It’s lot of work but nevertheless very exciting to see if we can pull it off!</p>
<p><img src="/uploads/blogs/redesign-2022-part-2/dark-mode-homepage-section.jpg" alt="Dark mode homepage section" /></p>
<h2 id="setting-up-the-project">Setting up the project <a href="#setting-up-the-project" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#setting-up-the-project">#</a></h2>
<p>After setting up the project we started scaffolding the page layouts one by one. We had to decide with which one to start. We chose to start with the most complex page, the homepage. The idea behind this was that we had to overcome the hardest challenges first. By doing the hard things first we would gain knowledge on how each component could be structured best, making the lesser complex pages easier to build.</p>
<h3 id="using-the-traditional-approach">Using the traditional approach</h3>
<p>Each page has the traditional setup of a base layout, a header, a main and a footer section. All public facing pages use the same base layout. They are built with blade, and are structured through blade components as much as possible. Therefore our base layout is located in <code>views/components/front/layout</code> as <code>index.blade.php</code>. This allows us to start each page with <code>&lt;x-front.layout&gt;</code>.</p>
<p><quote>a note: the <code>front</code> folder contains all public facing components. We also have an <code>app</code> folder which contains all application frontend components. You can read more about this in our <a href="/blog/building-oh-dears-new-design-project-setup">previous blog post</a>.</quote></p>
<p>The base layout contains two sub components a <code>&lt;x-front.layout.header&gt;</code> and <code>&lt;x-front.layout.footer&gt;</code> with a <code>$slot</code> variable in between. Resulting in something like this:</p>
<pre data-lang="blade" class="notranslate">&lt;!DOCTYPE html&gt;
&lt;<span class="hl-keyword">html</span> <span class="hl-property">lang</span>=&quot;en&quot;&gt;
	&lt;<span class="hl-keyword">head</span>&gt;
	... head tags
	&lt;/<span class="hl-keyword">head</span>&gt;
&lt;<span class="hl-keyword">body</span>&gt;
	&lt;<span class="hl-keyword">x-front.layout.header</span>&gt;
	
	{{<span class="hl-variable">$slot</span>}}

	&lt;<span class="hl-keyword">x-front.layout.footer</span>&gt;
&lt;/<span class="hl-keyword">body</span>&gt;

</pre>
<h3 id="using-blade-components">Using blade components</h3>
<p>The result of using this approach combined with blade components is a very clean markup which makes it perfect for reuse. For example this is how the homepage is structured:</p>
<pre data-lang="blade" class="notranslate">&lt;<span class="hl-keyword">x-front.layout</span>
    <span class="hl-property">title</span>=&quot;Oh Dear - The all-in-one monitoring tool for your entire website&quot;
&gt;
	&lt;<span class="hl-keyword">x-slot</span> <span class="hl-property">name</span>=&quot;header&quot;&gt;
		... all custom header things specific to the homepage 
	&lt;/<span class="hl-keyword">x-slot</span>&gt;
	
	<span class="hl-comment">&lt;!-- homepage sections --&gt;</span>
	...

&lt;/<span class="hl-keyword">x-front.layout</span>&gt;
</pre>
<p>Blade components and slots provide handy means to customise the components for each specific use case. Note the <code>title</code> attribute which sets the <code>&lt;title&gt;</code> within the base layout. Or the header slot which we use to add specific markup for the hero section of the homepage.</p>
<h2 id="lets-build">Lets build <a href="#lets-build" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#lets-build">#</a></h2>
<p>Now we have the basics covered it was time to start building. But how do you approach building 25 pages consisting of 100 components. Well, you do it one bit at a time. Before we started with the pages we built the base layout, including the header and the footer first.</p>
<p>Next we started with the homepage. We first determined the homepage sections:
<img src="/uploads/blogs/redesign-2022-part-2/homepage-sections.jpg" alt="Homepage sections" /></p>
<p>Next, we took the first section and split that section into smaller components:
<img src="/uploads/blogs/redesign-2022-part-2/homepage-section.jpg" alt="Homepage section" /></p>
<p>We then start building the components one by one. Building each component goes through several stages. They basically come down to:</p>
<h3 id="1-deconstruct-the-design">1. Deconstruct the design</h3>
<p>First we need to understand the design before we can build it. For us it meant diving into the Sketch file and checkout all layers and their properties like colours, fonts and font sizes. Once we identified the ingredients we continued with the markup.</p>
<h3 id="2-start-building">2. Start building</h3>
<p>Next we replicate each layer and its properties. We write the markup and style it at same time thanks to Tailwind CSS. This combined with <a href="https://browsersync.io">Browsersync</a> or a tool like <a href="https://sizzy.co">Sizzy</a> is seeing magic happening in front of you.</p>
<p>It allows us to have the design, the code and the end result in front of us without having to toggle between windows or files too much.</p>
<h3 id="3-refine">3. Refine</h3>
<p>When having built a specific section we constantly refine them which basically means repeating step 1 and 2 up to a point where the end result matches the design.</p>
<h4 id="4-add-interactivity">4. Add interactivity</h4>
<p>We try to go the next mile by adding interactivity to many components. As we build most visuals in html we can make them interactive with Javascript. To make our lives a lot easier we use help of AlpineJS.</p>
<p>AlpineJS fits perfectly with the strategy of using blade components as much as possible. For example on the homepage we have this counter component:</p>
<p><img src="/uploads/blogs/redesign-2022-part-2/counter.jpg" alt="Counter" /></p>
<p>Of which the markup in home.blade.php looks like:
<code>&lt;x-front.home.counter :count=&quot;$publicMetrics['runCount']&quot; /&gt;</code></p>
<p>We often pass an initial values via the component attributes to reuse them in the component itself.</p>
<p>When we take a look at the counter component it is structured as follows:</p>
<pre data-lang="blade" class="notranslate">&lt;<span class="hl-keyword">div</span> <span class="hl-property">x-data</span>=&quot;counter&quot;&gt;
	... all counter markup
&lt;/<span class="hl-keyword">div</span>&gt;

<span class="hl-keyword">@once</span>
    <span class="hl-keyword">@push</span>(<span class="hl-value">'scripts'</span>)
        &lt;<span class="hl-keyword">script</span>&gt;
            document.<span class="hl-property">addEventListener</span>(<span class="hl-value">'alpine:init'</span>, () =&gt; {
                <span class="hl-type">Alpine</span>.<span class="hl-property">data</span>(<span class="hl-value">'counter'</span> =&gt; ({
                    <span class="hl-property">count</span>: {{ <span class="hl-variable">$count</span> }},
                    <span class="hl-property">digits</span>: [],
                    <span class="hl-property">init</span>() {
                        <span class="hl-keyword">this</span>.<span class="hl-property">recalculateDigits</span>();
                        <span class="hl-property">setInterval</span>(() =&gt; {
                            <span class="hl-property">fetch</span>(<span class="hl-value">'/api/public-metrics'</span>)
                                .<span class="hl-property">then</span>(response =&gt; response.<span class="hl-property">json</span>())
                                .<span class="hl-property">then</span>((data) =&gt; {                      
                                    <span class="hl-keyword">let</span> key = `${<span class="hl-keyword">this</span>.<span class="hl-property">type</span>}_count`
                                    <span class="hl-keyword">this</span>.<span class="hl-property">count</span> = data[key];
                                });
                        }, 5000);    
                    },
                }));
            });
        &lt;/<span class="hl-keyword">script</span>&gt;
    <span class="hl-keyword">@endpush</span>
<span class="hl-keyword">@endonce</span>
</pre>
<p>As you can see we set the x-data property to what is defined below the markup. We create a specific script which is scoped to this component. This setup allows us to set initial values and get it all going in the init() hook. In this case it fetches an API endpoint every five seconds and updates the count.</p>
<p>By using this approach together with toggling tailwind classes we can create slick animations and add interactivity. Big shoutout to <a href="https://twitter.com/sebdedeyne">Seb</a> who initially set up this structure as well as the whole redesign structure. It turned out to be very effective for all other components as well.</p>
<h3 id="5-make-it-responsive">5. Make it responsive</h3>
<p>We often build the design for the desktop viewport first. The reason we don’t develop mobile first (which is a popular approach) is because the designs have been made (mostly) for desktop only. So in many cases we don’t know upfront what the mobile version should look like of a specific page. Therefore making the desktop version first and then see what needs to be adjusted when making the browser smaller is a better approach.</p>
<h2 id="rinse-and-repeat">Rinse and repeat <a href="#rinse-and-repeat" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#rinse-and-repeat">#</a></h2>
<p>When having a specific component or section built we then cleaned it up as much as possible. Refine the code, cleanup any log statements, rename variables and components if needed. Then we just repeat the whole process for the next section or component.</p>
<h2 id="using-components-and-make-them-easy-to-work-with">Using components and make them easy to work with <a href="#using-components-and-make-them-easy-to-work-with" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#using-components-and-make-them-easy-to-work-with">#</a></h2>
<p>As mentioned before, working with components can cleanup your markup a lot. Another benefit of using components is its flexibility. For example variables within a component can be either populated through setting the attribute on the parent, like so:</p>
<pre data-lang="blade" class="notranslate"><span class="hl-comment">&lt;!-- counter component --&gt;</span>
&lt;<span class="hl-keyword">div</span>&gt;
	{{ <span class="hl-variable">$counter</span> }}
&lt;/<span class="hl-keyword">div</span>&gt;
</pre>
<p>Use it like: <code>&lt;x-counter count=&quot;12&quot; /&gt;</code></p>
<p>Or you can add specific markup within the component tag:</p>
<pre data-lang="blade" class="notranslate">&lt;<span class="hl-keyword">x-counter</span>&gt;
	&lt;<span class="hl-keyword">x-slot:count</span>&gt;
		&lt;<span class="hl-keyword">span</span> <span class="hl-property">class</span>=&quot;text-white font-mono&quot;&gt;12&lt;/<span class="hl-keyword">span</span>&gt; notifications
	&lt;/<span class="hl-keyword">x-slot</span>&gt;
&lt;<span class="hl-keyword">x-counter</span>&gt;
</pre>
<p>This comes in very handy for components that must show a description. In some cases the description can be a single paragraph. It can then be passed to the component as follows:</p>
<pre data-lang="blade" class="notranslate">&lt;<span class="hl-keyword">x-feature-highlight</span>
	<span class="hl-property">title</span>=&quot;Have piece of mind&quot;
	<span class="hl-property">subtitle</span>=&quot;that things are running smooth&quot;
	<span class="hl-property">description</span>=&quot;This is what your dashboard looks like when all your websites are up. Oh Dear is running checks every minute, so you can sleep on both ears. Happy days!
/&gt;
</pre>
<p>However, we can’t use this approach if we need to pass more than one paragraph and expect it to look good in the browser.</p>
<p>We can then do:</p>
<pre data-lang="blade" class="notranslate">&lt;<span class="hl-keyword">x-feature-highlight</span>
	<span class="hl-property">title</span>=&quot;Have piece of mind&quot;
	<span class="hl-property">subtitle</span>=&quot;that things are running smooth&quot;
&gt;
	&lt;<span class="hl-keyword">x-slot:description</span>&gt;
		&lt;<span class="hl-keyword">p</span>&gt;This is what your dashboard looks like when all your websites are up. Oh Dear is running checks every minute, so you can sleep on both ears. Happy days!&lt;/<span class="hl-keyword">p</span>&gt;

		&lt;<span class="hl-keyword">p</span>&gt;Another paragraph can go here&lt;/<span class="hl-keyword">p</span>&gt;
	&lt;/<span class="hl-keyword">x-slot</span>&gt;
&lt;<span class="hl-keyword">x-counter</span>&gt;
</pre>
<h2 id="in-conclusion">In conclusion <a href="#in-conclusion" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#in-conclusion">#</a></h2>
<p>Implementing the design has been challenging, but having a process like described above felt being key in what we completed so far.</p>
<p>The homepage, the docs and almost all feature pages have been completed. So far there are 71,307 lines to be added to the main branch while we still need to start with the work the app side. We’ve done a crazy amount of work and yet there is still a lot to be done.</p>
<p>We keep on building and aim to provide our users with a renewed kick-ass product somewhere this year. Would you like to see more frequent product updates make sure to follow us on <a href="https://twitter.com/OhDearApp">Twitter</a>.</p>
<p>Check out the next post in this series on <a href="https://ohdear.app/blog/building-oh-dears-new-design-creating-a-color-system-why-and-how">why and how we created a color sytem</a>.</p>
]]>
            </summary>
                                    <updated>2022-05-20T08:38:57+00:00</updated>
        </entry>
            <entry>
            <title><![CDATA[Status page subscriptions are now available]]></title>
            <link rel="alternate" href="https://ohdear.app/news-and-updates/status-page-subscriptions-are-now-available" />
            <id>https://ohdear.app/74</id>
            <author>
                <name><![CDATA[Oh Dear]]></name>
            </author>
            <summary type="html">
                <![CDATA[<p>Using Oh Dear you can create beautiful status pages. As from today, you can let your users also subscribe to updates on those status pages. In this blog post we'll tell you all about it.</p>
<h2 id="subscribing-to-status-pages-updates">Subscribing to status pages updates <a href="#subscribing-to-status-pages-updates" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#subscribing-to-status-pages-updates">#</a></h2>
<p>In addition to monitoring your site, Oh Dear also provides status page. Here's how such a status page looks like. <a href="https://status.flareapp.io">Click here</a> to view it in your browser.</p>
<p><img src="/uploads/blogs/status-page-subscriptions/status-page.png" alt="image" /></p>
<p>Don't let that URL fool you: &quot;status.flareapp.io&quot; could make you think it's served by the Flare, but that domain points in fact to Oh Dear. We use <a href="https://ohdear.app/blog/how-we-used-caddy-and-laravels-subdomain-routing-to-serve-our-status-pages">some fancy hosting setup</a> to map subdomains to routes in Oh Dear.</p>
<p>Using such a status page you can inform the users of events happening at your service: things like up and down notices, but also things like scheduled maintenance.</p>
<p>When you click the new &quot;Subscribe to updates&quot; button on top of the page, you'll see that we now offer two ways of subscribing: Slack and RSS. We might add more channels in the future.</p>
<p><img src="/uploads/blogs/status-page-subscriptions/subscribe.png" alt="image" /></p>
<p>That dropdown is powered by <a href="https://alpinejs.dev">Alpine JS</a>. More specifically, we used Alpine's <a href="https://alpinejs.dev/component/dropdown">drop down component</a> as a basis. Do check out the <a href="https://alpinejs.dev/components">other Alpine components</a> too.</p>
<p>When you click &quot;Via Slack&quot;, you'll get redirected to a Slack authorization screen, on which you can choose the channel in your workspace where we should send notifications.</p>
<p><img src="/uploads/blogs/status-page-subscriptions/install.png" alt="image" /></p>
<p>To demonstrate that the Slack connection is working, we send immediately send a welcome notification to your Slack channel. This is what it looks like:</p>
<p><img src="/uploads/blogs/status-page-subscriptions/notification.png" alt="image" /></p>
<p>In your browser, you'll get redirected to the notification preferences screen. Where you can customise that message that you'd like to receive.</p>
<p><img src="/uploads/blogs/status-page-subscriptions/preferences.png" alt="image" /></p>
<p>An Oh Dear status page can display the status of multiple sites and updates can concerns different severities. If you only want to receive &quot;high&quot; and &quot;resolved&quot; severity updates of the Flare API you can configure it like this.</p>
<p><img src="/uploads/blogs/status-page-subscriptions/selection.png" alt="image" /></p>
<p>Here's how those notification will look in your Slack channel whenever something goes wrong with Flare's API.</p>
<p><img src="/uploads/blogs/status-page-subscriptions/notification-2.png" alt="image" /></p>
<p>Should too many notifications come in for your taste, then you can even decide to snooze notifications for a certain amount of time.</p>
<p><img src="/uploads/blogs/status-page-subscriptions/snooze.png" alt="image" /></p>
<p><img src="/uploads/blogs/status-page-subscriptions/snoozed.png" alt="image" /></p>
<h2 id="in-closing">In closing <a href="#in-closing" class="heading-permalink" aria-label="Permalink" data-clipboard-text="https://ohdear.app/feed#in-closing">#</a></h2>
<p>A while ago, we asked publicly <a href="https://twitter.com/freekmurze/status/1513968509755269130">on Twitter</a> which features we should add to our service.  <a href="https://twitter.com/DustinGTaylor/status/1513977141909630976">Status page updates</a> was one of the nicest suggestions we got. We'll also implement a few others soon.</p>
<p>If you have a feature request, <a href="mailto:support@ohdear.app">get in touch</a>! We're listening!</p>
]]>
            </summary>
                                    <updated>2022-05-05T13:15:08+00:00</updated>
        </entry>
    </feed>
