Skip to content

feat(ntfy): add custom title and message templates for notifications#6804

Merged
CommanderStorm merged 8 commits intolouislam:masterfrom
epifeny:feature/ntfy-custom-templates
Jan 27, 2026
Merged

feat(ntfy): add custom title and message templates for notifications#6804
CommanderStorm merged 8 commits intolouislam:masterfrom
epifeny:feature/ntfy-custom-templates

Conversation

@epifeny
Copy link
Copy Markdown
Contributor

@epifeny epifeny commented Jan 24, 2026

Add optional custom templates for ntfy notification titles and messages using the existing LiquidJS templating system. Supports both notification-level templates (in ntfy settings) and per-monitor overrides (in monitor edit page).

  • Notification-level templates in ntfy notification settings
  • Per-monitor template overrides in monitor edit page
  • Uses existing renderTemplate() with LiquidJS
  • Priority: monitor-level > notification-level > default format
  • Backward compatible: empty templates use default Uptime Kuma format
  • Database migration adds ntfy_custom_title and ntfy_custom_message to monitor table
  • Notification-level templates stored in JSON config field (no schema change needed)

Related: #6797

Summary

In this pull request, the following changes are made:

This adds optional custom templates for ntfy notification titles and messages. Users can set a custom title and message in the ntfy notification settings, and optionally override them per monitor on the edit monitor page. Both use the existing LiquidJS templating system, so it fits the approach in #646.

Related to feature request #6797

Please follow this checklist to avoid unnecessary back and forth (click to expand)
  • ⚠️ If there are Breaking change (a fix or feature that alters existing functionality in a way that could cause issues) I have called them out
  • 🧠 I have disclosed any use of LLMs/AI in this contribution and reviewed all generated content.
    I understand that I am responsible for and able to explain every line of code I submit.
  • 🔍 Any UI changes adhere to visual style of this project.
  • 🛠️ I have self-reviewed and self-tested my code to ensure it works as expected.
  • 📝 I have commented my code, especially in hard-to-understand areas (e.g., using JSDoc for methods).
  • 🤖 I added or updated automated tests where appropriate.
  • 📄 Documentation updates are included (if applicable).
  • [] 🧰 Dependency updates are listed and explained.
  • ⚠️ CI passes and is green.

Screenshots for Visual Changes

Notification service edit page

Default collapsed (when both input fields are empty) toggler
image

Expanded toggler
image

Monitor type edit page

Default collapsed (when both input fields are empty) toggler
image

Expanded toggler
image

These are pretty self-explanatory I hope
image

To get the Group "phones" message, I used this title:

📱 {{ name }} - {{ status }}

and message:

{% if status == "✅ Up" %}
✅ All {{ monitorJSON.childrenIDs | size }} phones are operational!
{% elsif status == "🔴 Down" %}
{% assign cleanMsg = msg | replace: "[phones] [🔴 Down] ", "" %}
{% assign downPart = cleanMsg | split: "Child monitors down: " | last | split: ";" | first %}
{% assign downMonitors = downPart | split: ", " %}
{% assign downCount = downMonitors | size %}
{% assign totalCount = monitorJSON.childrenIDs | size %}
{% if downCount == 1 %}
⚠️ 1 out of {{ totalCount }} phones is down
{% else %}
⚠️ {{ downCount }} out of {{ totalCount }} phones are down
{% endif %}
Down: {{ downPart }}
{% else %}
{% assign cleanMsg = msg | replace: "[phones] ", "" | replace: "[⏳ Pending] ", "" %}
⏳ {{ cleanMsg }}

Monitoring {{ monitorJSON.childrenIDs | size }} phones
{% endif %}

Copy link
Copy Markdown
Collaborator

@CommanderStorm CommanderStorm left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Contains a bunch of unrelated changes that I would like to see removed, but once they are LGTM.

Comment on lines +108 to +141
<div class="mb-2">
<i18n-t tag="span" keypath="liquidIntroduction">
<a href="https://liquidjs.com/" target="_blank">{{ $t("documentation") }}</a>
</i18n-t>
</div>
<div class="mb-2">
<strong>{{ $t("templateAvailableVariables") }}:</strong>
<code v-pre>{{ status }}</code>
,
<code v-pre>{{ name }}</code>
,
<code v-pre>{{ hostnameOrURL }}</code>
,
<code v-pre>{{ msg }}</code>
,
<code v-pre>{{ monitorJSON }}</code>
,
<code v-pre>{{ heartbeatJSON }}</code>
</div>
<div class="mt-3 p-2" style="background-color: rgba(100, 100, 100, 0.1); border-radius: 4px">
<div class="mb-1">
<strong>{{ $t("example") }}:</strong>
</div>
<div class="mb-2">
<code v-pre style="font-size: 0.85em; word-break: break-all">
{% for tag in monitorJSON.tags %}{{ tag.name }}{% unless tag.value == blank %}:
{{ tag.value }}{% endunless %}{% unless forloop.last %}, {% endunless %}{% endfor %}
</code>
</div>
<div style="font-size: 0.9em">
<strong>{{ $t("Result") }}:</strong>
<span style="opacity: 0.9">nightly, phone: fbal</span>
</div>
</div>
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please use the components that the other notification providers use for this

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure if I'm suppose to respond here and "resolve conversation", but I've updated the code and the PR, so you can review it now.

@epifeny
Copy link
Copy Markdown
Contributor Author

epifeny commented Jan 26, 2026

Contains a bunch of unrelated changes that I would like to see removed, but once they are LGTM.

Thank you for taking the time to review my PR.
I read, and to my best understanding, you would like me to completely remove my feature of allowing per-monitor notification override to the notification service provider, which was a substantial part of the PR in the first place.
It's a feature I would like to introduce, if not as a core idea to all monitors types and notification service in uptime kuma, at the very least, as per my current PR provides, to the NTFY provider.
Why would you not want it?

@CommanderStorm
Copy link
Copy Markdown
Collaborator

Tangling monitor and notification logic is very hard to maintain/reason about and I don't think this is a good idea.
Please remove this part.

Notifications can still be templated differently for some monitors, just via different notification configurations, not what you are trying to add.

Add optional custom templates for ntfy notification titles and messages
using the existing LiquidJS templating system. Supports both
notification-level templates (in ntfy settings) and per-monitor overrides
(in monitor edit page).

- Notification-level templates in ntfy notification settings
- Per-monitor template overrides in monitor edit page
- Uses existing renderTemplate() with LiquidJS (same as SMTP, Telegram)
- Priority: monitor-level > notification-level > default format
- Backward compatible: empty templates use default Uptime Kuma format
- Database migration adds ntfy_custom_title and ntfy_custom_message to monitor table
- Notification-level templates stored in JSON config field (no schema change needed)

Related: louislam#6797
Remove per-monitor template functionality and simplify to
notification-level templates only, as requested by maintainer.

Changes based on maintainer feedback:
- Remove database migration entirely
- Remove all monitor-level template fields and logic
- Change UI to checkbox approach (matching Telegram pattern)
- Use standard TemplatedInput and TemplatedTextarea components
- Revert ToggleSection auto-expand feature
- Update translation keys for checkbox UI
- Simplify ntfy.js to only use notification.ntfyUseTemplate

This keeps notification and monitor logic cleanly separated.
Users can create multiple ntfy notifications with different
templates for different monitors.

Related: louislam#6797, louislam#6804
Simplify the custom template UI based on maintainer feedback:
- Replace TemplatedInput/TemplatedTextarea with plain HTML elements
  to avoid duplicate help text
- Add shared help text explaining LiquidJS templating and available
  variables above both input fields
- Change from v-if to v-show for better performance
- Add auto-enable logic for template checkbox when fields have content

This makes the UI cleaner and follows the DRY principle by showing
template documentation once instead of repeating it for each field.
@epifeny epifeny force-pushed the feature/ntfy-custom-templates branch from 344c905 to cc17d4c Compare January 26, 2026 10:19
@epifeny
Copy link
Copy Markdown
Contributor Author

epifeny commented Jan 26, 2026

Again, thanks for the response. I've made the changes you asked for.

Tangling monitor and notification logic is very hard to maintain/reason about and I don't think this is a good idea. Please remove this part.

In efforts to learn and understand more, please, can you explain why?

Notifications can still be templated differently for some monitors, just via different notification configurations, not what you are trying to add.

Yea, I got it working with some advanced conditional templates, like this
title

{%- if status contains "Up" -%} {{ name }} Up {%- elsif status contains "Down" -%} {{ name }} Down {%- else -%} {{ name }} {{ status }} {%- endif -%}

message

{%- comment -%} Determine alert level {%- endcomment -%}
{%- assign alert_level = "INFO" -%}
{%- assign should_escalate = false -%}

{%- if status contains "Down" -%}
  {%- assign alert_level = "WARNING" -%}
  
  {%- if name contains "epifeny" or name contains "critical" -%}
    {%- assign alert_level = "CRITICAL" -%}
    {%- assign should_escalate = true -%}
  {%- endif -%}
  
  {%- for tag in monitorJSON.tags -%}
    {%- if tag.name == "phone" and tag.value contains "fbal" -%}
      {%- assign alert_level = "CRITICAL" -%}
      {%- assign should_escalate = true -%}
    {%- endif -%}
  {%- endfor -%}
{%- endif -%}

{%- if alert_level == "CRITICAL" -%}
🚨 PHONE-FBAL ALERT 🚨
=========================
{%- elsif alert_level == "WARNING" -%}
⚠️ WARNING
=========================
{%- else -%}
ℹ️ INFO
{%- endif %}

Monitor: {{ name }}
Status: {{ status }}
{%- if monitorJSON.type != "ping" and msg -%}
Message: {{ msg }}
{%- endif -%}

{%- comment -%} Only show URL if it's not empty or default {%- endcomment -%}
{%- if monitorJSON.url and monitorJSON.url != "https://" and monitorJSON.url != "" %}
URL: {{ monitorJSON.url }}
{%- endif -%}

{%- if should_escalate %}
⚠️ IMMEDIATE ATTENTION REQUIRED
{%- endif -%}
image image

@CommanderStorm
Copy link
Copy Markdown
Collaborator

In efforts to learn and understand more, please, can you explain why

If you have roughly two systems in the codebase and then they start interacting like you have shown this just is very hard to maintain long term.
We want to have low coupling and high cohesion, coupling these systems like proposed is not a good idea.

Notification templating yes, per monitor notification template no

@CommanderStorm CommanderStorm marked this pull request as draft January 26, 2026 16:24
@epifeny
Copy link
Copy Markdown
Contributor Author

epifeny commented Jan 26, 2026

I see that you marked this PR as draft. Is there anything else that was required from me that I didn't do?

@CommanderStorm
Copy link
Copy Markdown
Collaborator

Yes, the PR does not quite seem done.

It contains a few changes which are leftover from previous versions (such as changing the toggler) and the frontend does not yet use the components we have for adding templatable inputs.

It is still a draft.

@epifeny
Copy link
Copy Markdown
Contributor Author

epifeny commented Jan 26, 2026

Ok, if you could point out what I missed, I'll fix it.

@CommanderStorm
Copy link
Copy Markdown
Collaborator

I thing the unrelated changes should be clear and where to use TemplatedTextarea and TemplatedInput instead should also be relatively clear.

I don't quite understand where what I posted is unclear.

@epifeny
Copy link
Copy Markdown
Contributor Author

epifeny commented Jan 26, 2026

Hello,

I'm sorry, I'm just trying to do my best, and this is really just my very 2nd pull request (ever, for any project anywhere), so I want to do the right thing and to do it right (code wise). I'm not a developer, but I can probably do some things correctly. Your guideness is very important to me.

so i updated the code to use TemplatedInput and TemplatedTextarea like you asked. but i noticed something - since ntfy has two inputs (title and message), the helper text about liquidjs templating shows up twice now (once above each input).

i checked and saw that smtp does the same thing (it has subject and body, both use the templated components, so helper text shows twice there too). so i'm wondering - is this the expected behavior? or should we do something different for providers that have multiple templatable inputs?

i can see telegram only has one input so it's fine there, but for ntfy with two inputs it does feel a bit redundant. let me know what you'd prefer!

here you can see the ntfy with your changes in-place
image

and smtp has this, what i consider, redundancy
image

but for telegram, this isn't an issue
image

…evert ToggleSection changes

- Revert ToggleSection.vue to match master (remove leftover changes)
- Replace plain input/textarea with TemplatedInput/TemplatedTextarea components
- Remove duplicate help text (components provide it automatically)
- Matches pattern used in SMTP and other notification providers
@CommanderStorm CommanderStorm marked this pull request as ready for review January 27, 2026 01:12
Copilot AI review requested due to automatic review settings January 27, 2026 01:12
@github-actions github-actions bot added the pr:needs review this PR needs a review by maintainers or other community members label Jan 27, 2026
@CommanderStorm CommanderStorm enabled auto-merge (squash) January 27, 2026 01:12
Copy link
Copy Markdown
Collaborator

@CommanderStorm CommanderStorm left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM now, thanks ❤️

I am fine with being a bit verbose in the UI if this checkbox is ticked.
It is not the default anyhow 👍🏻

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds optional Liquid-based templating for ntfy notification titles and messages, configurable from the ntfy notification settings UI and applied server-side when sending ntfy notifications.

Changes:

  • Updated English i18n strings for templating help text and new ntfy template UI labels.
  • Added ntfy notification settings UI to enable/disable templates and edit custom title/message templates.
  • Implemented server-side template rendering for ntfy title/message when templates are enabled.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.

File Description
src/lang/en.json Adds new ntfy template-related strings and adjusts the general Liquid introduction text.
src/components/notifications/Ntfy.vue Adds template toggle plus custom title/message template inputs to the ntfy notification editor UI.
server/notification-providers/ntfy.js Applies renderTemplate() to title/message when ntfyUseTemplate is enabled.

Comment on lines +262 to +265
"liquidIntroduction": "Templatability is achieved via the Liquid templating language. Please refer to the {0} for usage instructions.",
"templateAvailableVariables": "Available variables",
"example": "Example",
"Result": "Result",
Copy link

Copilot AI Jan 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The updated liquidIntroduction string no longer introduces the variable list, but TemplatedInput.vue/TemplatedTextarea.vue still render the variables directly beneath it, which makes the UI read abruptly. Also, the newly added keys templateAvailableVariables, example, and Result are not referenced anywhere in src/ (verified via search), so they appear to be dead translations. Consider either wiring these keys into the templated input components (e.g., add headings/labels) or removing them and restoring the original introduction text.

Suggested change
"liquidIntroduction": "Templatability is achieved via the Liquid templating language. Please refer to the {0} for usage instructions.",
"templateAvailableVariables": "Available variables",
"example": "Example",
"Result": "Result",
"liquidIntroduction": "Templatability is achieved via the Liquid templating language. The following variables are available; please refer to the {0} for full usage instructions.",

Copilot uses AI. Check for mistakes.
Comment on lines +90 to +105
// Default values
let title = monitorJSON.name + " " + status + " [Uptime-Kuma]";
let message = heartbeatJSON.msg;

// Apply custom templates from notification settings if enabled
if (notification.ntfyUseTemplate) {
const customTitle = notification.ntfyCustomTitle?.trim() || "";
const customMessage = notification.ntfyCustomMessage?.trim() || "";

if (customTitle !== "") {
title = await this.renderTemplate(customTitle, msg, monitorJSON, heartbeatJSON);
}
if (customMessage !== "") {
message = await this.renderTemplate(customMessage, msg, monitorJSON, heartbeatJSON);
}
}
Copy link

Copilot AI Jan 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The PR description mentions per-monitor template overrides and a database migration adding ntfy_custom_title/ntfy_custom_message, plus a precedence order (monitor-level > notification-level > default). In the current changes, only notification-level fields (ntfyUseTemplate, ntfyCustomTitle, ntfyCustomMessage) are implemented and there are no monitor-level fields/migrations in the codebase (verified via repo search). Please either implement the missing monitor-level behavior/migration or update the PR description and linked issue resolution accordingly.

Copilot uses AI. Check for mistakes.
Comment on lines +40 to +51
// Apply custom templates from notification settings if enabled
if (notification.ntfyUseTemplate) {
const customTitle = notification.ntfyCustomTitle?.trim() || "";
if (customTitle !== "") {
title = await this.renderTemplate(customTitle, msg, monitorJSON, heartbeatJSON);
}

const customMessage = notification.ntfyCustomMessage?.trim() || "";
if (customMessage !== "") {
message = await this.renderTemplate(customMessage, msg, monitorJSON, heartbeatJSON);
}
}
Copy link

Copilot AI Jan 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The custom template rendering logic is duplicated between the heartbeatJSON == null path and the normal heartbeat path. To reduce the chance of future drift (e.g., updating trimming rules or adding new context variables in only one branch), consider extracting a small helper that applies templates and returns the final title/message.

Copilot uses AI. Check for mistakes.
@CommanderStorm CommanderStorm merged commit f5578da into louislam:master Jan 27, 2026
32 checks passed
@epifeny
Copy link
Copy Markdown
Contributor Author

epifeny commented Jan 28, 2026

LGTM now, thanks ❤️

I am fine with being a bit verbose in the UI if this checkbox is ticked. It is not the default anyhow 👍🏻

Thank you for the explanations and assistance and ofc, for the merge :) cheers

@CommanderStorm CommanderStorm added this to the 2.1.0 milestone Jan 29, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

pr:needs review this PR needs a review by maintainers or other community members

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Custom template for ntfy (or any service provider?)

3 participants