diff --git a/app/assets/javascripts/discourse/app/components/modal/download-calendar.js b/app/assets/javascripts/discourse/app/components/modal/download-calendar.js
index 27daf1b957b..b289d864d64 100644
--- a/app/assets/javascripts/discourse/app/components/modal/download-calendar.js
+++ b/app/assets/javascripts/discourse/app/components/modal/download-calendar.js
@@ -22,12 +22,14 @@ export default class downloadCalendar extends Component {
     if (this.selectedCalendar === "ics") {
       downloadIcs(
         this.args.model.calendar.title,
-        this.args.model.calendar.dates
+        this.args.model.calendar.dates,
+        this.args.model.calendar.recurrenceRule
       );
     } else {
       downloadGoogle(
         this.args.model.calendar.title,
-        this.args.model.calendar.dates
+        this.args.model.calendar.dates,
+        this.args.model.calendar.recurrenceRule
       );
     }
     this.args.closeModal();
diff --git a/app/assets/javascripts/discourse/app/lib/download-calendar.js b/app/assets/javascripts/discourse/app/lib/download-calendar.js
index 9a9e7a5ad3a..238a8cf0dde 100644
--- a/app/assets/javascripts/discourse/app/lib/download-calendar.js
+++ b/app/assets/javascripts/discourse/app/lib/download-calendar.js
@@ -3,7 +3,7 @@ import User from "discourse/models/user";
 import { getOwnerWithFallback } from "discourse-common/lib/get-owner";
 import getURL from "discourse-common/lib/get-url";
 
-export function downloadCalendar(title, dates) {
+export function downloadCalendar(title, dates, recurrenceRule = null) {
   const currentUser = User.current();
 
   const formattedDates = formatDates(dates);
@@ -11,20 +11,20 @@ export function downloadCalendar(title, dates) {
 
   switch (currentUser.user_option.default_calendar) {
     case "none_selected":
-      _displayModal(title, formattedDates);
+      _displayModal(title, formattedDates, recurrenceRule);
       break;
     case "ics":
-      downloadIcs(title, formattedDates);
+      downloadIcs(title, formattedDates, recurrenceRule);
       break;
     case "google":
-      downloadGoogle(title, formattedDates);
+      downloadGoogle(title, formattedDates, recurrenceRule);
       break;
   }
 }
 
-export function downloadIcs(title, dates) {
+export function downloadIcs(title, dates, recurrenceRule) {
   const REMOVE_FILE_AFTER = 20_000;
-  const file = new File([generateIcsData(title, dates)], {
+  const file = new File([generateIcsData(title, dates, recurrenceRule)], {
     type: "text/plain",
   });
 
@@ -37,15 +37,23 @@ export function downloadIcs(title, dates) {
   setTimeout(() => window.URL.revokeObjectURL(file), REMOVE_FILE_AFTER); //remove file to avoid memory leaks
 }
 
-export function downloadGoogle(title, dates) {
+export function downloadGoogle(title, dates, recurrenceRule) {
   dates.forEach((date) => {
-    const encodedTitle = encodeURIComponent(title);
-    const link = getURL(`
-      https://www.google.com/calendar/event?action=TEMPLATE&text=${encodedTitle}&dates=${_formatDateForGoogleApi(
-      date.startsAt
-    )}/${_formatDateForGoogleApi(date.endsAt)}
-    `).trim();
-    window.open(link, "_blank", "noopener", "noreferrer");
+    const link = new URL("https://www.google.com/calendar/event");
+    link.searchParams.append("action", "TEMPLATE");
+    link.searchParams.append("text", title);
+    link.searchParams.append(
+      "dates",
+      `${_formatDateForGoogleApi(date.startsAt)}/${_formatDateForGoogleApi(
+        date.endsAt
+      )}`
+    );
+
+    if (recurrenceRule) {
+      link.searchParams.append("recur", `RRULE:${recurrenceRule}`);
+    }
+
+    window.open(getURL(link.href).trim(), "_blank", "noopener", "noreferrer");
   });
 }
 
@@ -60,7 +68,7 @@ export function formatDates(dates) {
   });
 }
 
-export function generateIcsData(title, dates) {
+export function generateIcsData(title, dates, recurrenceRule) {
   let data = "BEGIN:VCALENDAR\nVERSION:2.0\nPRODID:-//Discourse//EN\n";
   dates.forEach((date) => {
     const startDate = moment(date.startsAt);
@@ -72,6 +80,7 @@ export function generateIcsData(title, dates) {
         `DTSTAMP:${moment().utc().format("YMMDDTHHmmss")}Z\n` +
         `DTSTART:${startDate.utc().format("YMMDDTHHmmss")}Z\n` +
         `DTEND:${endDate.utc().format("YMMDDTHHmmss")}Z\n` +
+        (recurrenceRule ? `RRULE:${recurrenceRule}\n` : ``) +
         `SUMMARY:${title}\n` +
         "END:VEVENT\n"
     );
@@ -80,9 +89,11 @@ export function generateIcsData(title, dates) {
   return data;
 }
 
-function _displayModal(title, dates) {
+function _displayModal(title, dates, recurrenceRule) {
   const modal = getOwnerWithFallback(this).lookup("service:modal");
-  modal.show(downloadCalendarModal, { model: { calendar: { title, dates } } });
+  modal.show(downloadCalendarModal, {
+    model: { calendar: { title, dates, recurrenceRule } },
+  });
 }
 
 function _formatDateForGoogleApi(date) {
diff --git a/app/assets/javascripts/discourse/app/lib/plugin-api.js b/app/assets/javascripts/discourse/app/lib/plugin-api.js
index ca12d2c4e4c..70af87b8690 100644
--- a/app/assets/javascripts/discourse/app/lib/plugin-api.js
+++ b/app/assets/javascripts/discourse/app/lib/plugin-api.js
@@ -141,7 +141,7 @@ import { modifySelectKit } from "select-kit/mixins/plugin-api";
 // docs/CHANGELOG-JAVASCRIPT-PLUGIN-API.md whenever you change the version
 // using the format described at https://keepachangelog.com/en/1.0.0/.
 
-export const PLUGIN_API_VERSION = "1.15.0";
+export const PLUGIN_API_VERSION = "1.16.0";
 
 // This helper prevents us from applying the same `modifyClass` over and over in test mode.
 function canModify(klass, type, resolverName, changes) {
@@ -1835,7 +1835,7 @@ class PluginApi {
   }
 
   /**
-   * Download calendar modal which allow to pick between ICS and Google Calendar
+   * Download calendar modal which allow to pick between ICS and Google Calendar. Optionally, recurrence rule can be specified - https://datatracker.ietf.org/doc/html/rfc5545#section-3.3.10
    *
    * ```
    * api.downloadCalendar("title of the event", [
@@ -1843,12 +1843,14 @@ class PluginApi {
         startsAt: "2021-10-12T15:00:00.000Z",
         endsAt: "2021-10-12T16:00:00.000Z",
       },
-   * ]);
+   * ],
+   * "FREQ=DAILY;BYDAY=MO,TU,WE,TH,FR"
+   * );
    * ```
    *
    */
-  downloadCalendar(title, dates) {
-    downloadCalendar(title, dates);
+  downloadCalendar(title, dates, recurrenceRule = null) {
+    downloadCalendar(title, dates, recurrenceRule);
   }
 
   /**
diff --git a/app/assets/javascripts/discourse/tests/unit/lib/download-calendar-test.js b/app/assets/javascripts/discourse/tests/unit/lib/download-calendar-test.js
index c5a7b7d5c43..9ff25083d13 100644
--- a/app/assets/javascripts/discourse/tests/unit/lib/download-calendar-test.js
+++ b/app/assets/javascripts/discourse/tests/unit/lib/download-calendar-test.js
@@ -16,31 +16,73 @@ module("Unit | Utility | download-calendar", function (hooks) {
     sinon.stub(win, "focus");
   });
 
-  test("correct data for Ics", function (assert) {
+  test("correct data for ICS", function (assert) {
+    const now = moment.tz("2022-04-04 23:15", "Europe/Paris").valueOf();
+    sinon.useFakeTimers({
+      now,
+      toFake: ["Date"],
+      shouldAdvanceTime: true,
+      shouldClearNativeTimers: true,
+    });
     const data = generateIcsData("event test", [
       {
         startsAt: "2021-10-12T15:00:00.000Z",
         endsAt: "2021-10-12T16:00:00.000Z",
       },
     ]);
-    assert.ok(
+    assert.equal(
       data,
-      `
-BEGIN:VCALENDAR
+      `BEGIN:VCALENDAR
 VERSION:2.0
 PRODID:-//Discourse//EN
 BEGIN:VEVENT
 UID:1634050800000_1634054400000
-DTSTAMP:20213312T223320Z
-DTSTART:20210012T150000Z
-DTEND:20210012T160000Z
-SUMMARY:event2
+DTSTAMP:20220404T211500Z
+DTSTART:20211012T150000Z
+DTEND:20211012T160000Z
+SUMMARY:event test
 END:VEVENT
-END:VCALENDAR
-    `
+END:VCALENDAR`
     );
   });
 
+  test("correct data for ICS when recurring event", function (assert) {
+    const now = moment.tz("2022-04-04 23:15", "Europe/Paris").valueOf();
+    sinon.useFakeTimers({
+      now,
+      toFake: ["Date"],
+      shouldAdvanceTime: true,
+      shouldClearNativeTimers: true,
+    });
+    const data = generateIcsData(
+      "event test",
+      [
+        {
+          startsAt: "2021-10-12T15:00:00.000Z",
+          endsAt: "2021-10-12T16:00:00.000Z",
+        },
+      ],
+      "FREQ=DAILY;BYDAY=MO,TU,WE,TH,FR"
+    );
+    assert.equal(
+      data,
+      `BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//Discourse//EN
+BEGIN:VEVENT
+UID:1634050800000_1634054400000
+DTSTAMP:20220404T211500Z
+DTSTART:20211012T150000Z
+DTEND:20211012T160000Z
+RRULE:FREQ=DAILY;BYDAY=MO,TU,WE,TH,FR
+SUMMARY:event test
+END:VEVENT
+END:VCALENDAR`
+    );
+
+    sinon.restore();
+  });
+
   test("correct url for Google", function (assert) {
     downloadGoogle("event", [
       {
@@ -50,7 +92,28 @@ END:VCALENDAR
     ]);
     assert.ok(
       window.open.calledWith(
-        "https://www.google.com/calendar/event?action=TEMPLATE&text=event&dates=20211012T150000Z/20211012T160000Z",
+        "https://www.google.com/calendar/event?action=TEMPLATE&text=event&dates=20211012T150000Z%2F20211012T160000Z",
+        "_blank",
+        "noopener",
+        "noreferrer"
+      )
+    );
+  });
+
+  test("correct url for Google when recurring event", function (assert) {
+    downloadGoogle(
+      "event",
+      [
+        {
+          startsAt: "2021-10-12T15:00:00.000Z",
+          endsAt: "2021-10-12T16:00:00.000Z",
+        },
+      ],
+      "FREQ=DAILY;BYDAY=MO,TU,WE,TH,FR"
+    );
+    assert.ok(
+      window.open.calledWith(
+        "https://www.google.com/calendar/event?action=TEMPLATE&text=event&dates=20211012T150000Z%2F20211012T160000Z&recur=RRULE%3AFREQ%3DDAILY%3BBYDAY%3DMO%2CTU%2CWE%2CTH%2CFR",
         "_blank",
         "noopener",
         "noreferrer"
diff --git a/docs/CHANGELOG-JAVASCRIPT-PLUGIN-API.md b/docs/CHANGELOG-JAVASCRIPT-PLUGIN-API.md
index a6aab86a274..0d651279dfe 100644
--- a/docs/CHANGELOG-JAVASCRIPT-PLUGIN-API.md
+++ b/docs/CHANGELOG-JAVASCRIPT-PLUGIN-API.md
@@ -7,6 +7,12 @@ in this file.
 The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
 and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
 
+## [1.16.0] - 2023-11-17
+
+### Added
+
+- Added `recurrenceRule` option to `downloadCalendar`, this can be used to set recurring events in the calendar. Rule syntax can be found at https://datatracker.ietf.org/doc/html/rfc5545#section-3.3.10.
+
 ## [1.15.0] - 2023-10-18
 
 ### Added
diff --git a/plugins/discourse-local-dates/test/javascripts/acceptance/download-calendar-test.js b/plugins/discourse-local-dates/test/javascripts/acceptance/download-calendar-test.js
index b92932b7495..017a76e2b25 100644
--- a/plugins/discourse-local-dates/test/javascripts/acceptance/download-calendar-test.js
+++ b/plugins/discourse-local-dates/test/javascripts/acceptance/download-calendar-test.js
@@ -90,7 +90,7 @@ acceptance(
         assert.deepEqual(
           [...arguments],
           [
-            `https://www.google.com/calendar/event?action=TEMPLATE&text=title%20to%20trim&dates=${startDate}T180000Z/${startDate}T190000Z`,
+            `https://www.google.com/calendar/event?action=TEMPLATE&text=title+to+trim&dates=${startDate}T180000Z%2F${startDate}T190000Z`,
             "_blank",
             "noopener",
             "noreferrer",