diff --git wordpress.org/public_html/wp-content/plugins/wporg-meeting-ical/class-ical-generator.php wordpress.org/public_html/wp-content/plugins/wporg-meeting-ical/class-ical-generator.php
new file mode 100644
index 000000000..5e6540833
--- /dev/null
+++ wordpress.org/public_html/wp-content/plugins/wporg-meeting-ical/class-ical-generator.php
@@ -0,0 +1,158 @@
+<?php
+
+
+class Meeting_ICAL_Generator
+{
+
+    const NEWLINE = "\r\n";
+
+    /**
+     * Generate an iCalendar for the given set of meetings.
+     *
+     * @param WP_Post[] $posts
+     * @return string
+     */
+    public function generate($posts)
+    {
+        $ical = 'BEGIN:VCALENDAR' . self::NEWLINE;
+        $ical .= 'VERSION:2.0' . self::NEWLINE;
+        $ical .= 'PRODID:-//WPORG Make//Meeting Events Calendar//EN' . self::NEWLINE;
+        $ical .= 'METHOD:PUBLISH' . self::NEWLINE;
+        $ical .= 'CALSCALE:GREGORIAN' . self::NEWLINE;
+
+        foreach ($posts as $post) {
+            $ical .= $this->generate_event($post);
+        }
+
+        $ical .= 'END:VCALENDAR';
+        return $ical;
+    }
+
+    /**
+     * Generate an event for a meeting.
+     *
+     * @param WP_Post $post
+     * @return string
+     */
+    private function generate_event($post)
+    {
+        $id = $post->ID;
+        $title = $post->post_title;
+        $location = $post->location;
+        $link = $post->link;
+        $team = $post->team;
+        $recurring = $post->recurring;
+        $sequence = empty($post->sequence) ? 0 : intval($post->sequence);
+
+        $start_date = strftime('%Y%m%d', strtotime($post->next_date));
+        $start_time = strftime('%H%M%S', strtotime($post->time));
+        $start_date_time = "{$start_date}T{$start_time}Z";
+
+        $end_date = $start_date;
+        $end_time = strftime('%H%M%S', strtotime("{$post->time} +1 hour"));
+        $end_date_time = "{$end_date}T{$end_time}Z";
+
+        $description = '';
+        $slack_channel = null;
+
+        if ($location && preg_match('/^#([-\w]+)$/', trim($location), $match)) {
+            $slack_channel = '#' . sanitize_title($match[1]);
+            $location = "{$slack_channel} channel on Slack";
+        }
+
+        if ($link) {
+            if ($slack_channel) {
+                $description .= "Slack channel link: https://wordpress.slack.com/messages/{$slack_channel}\\n";
+            }
+
+            $description .= "For more information visit {$link}";
+        }
+
+        $frequency = $this->get_frequency($recurring, $post->next_date, $post->occurrence);
+
+        $event = "BEGIN:VEVENT" . self::NEWLINE;
+        $event .= "UID:{$id}" . self::NEWLINE;
+
+        $event .= "DTSTAMP:{$start_date_time}" . self::NEWLINE;
+        $event .= "DTSTART;VALUE=DATE:{$start_date_time}" . self::NEWLINE;
+        $event .= "DTEND;VALUE=DATE:{$end_date_time}" . self::NEWLINE;
+        $event .= "CATEGORIES:WordPress" . self::NEWLINE;
+        // Some calendars require the organizer's name and email address
+        $event .= "ORGANIZER;CN=WordPress {$team} Team:mailto:mail@example.com" . self::NEWLINE;
+        $event .= "SUMMARY:{$team}: {$title}" . self::NEWLINE;
+        // Incrementing the sequence number updates the specified event
+        $event .= "SEQUENCE:{$sequence}" . self::NEWLINE;
+        $event .= 'STATUS:CONFIRMED' . self::NEWLINE;
+        $event .= 'TRANSP:OPAQUE' . self::NEWLINE;
+
+        if (!empty($location)) {
+            $event .= "LOCATION:{$location}" . self::NEWLINE;
+        }
+
+        if (!empty($description)) {
+            $event .= "DESCRIPTION:{$description}" . self::NEWLINE;
+        }
+
+        if (!is_null($frequency)) {
+            $event .= "RRULE:FREQ={$frequency}" . self::NEWLINE;
+        }
+
+        $event .= "END:VEVENT" . self::NEWLINE;
+
+        return $event;
+    }
+
+    private function get_frequency($recurrence, $date, $occurrences)
+    {
+        switch ($recurrence) {
+            case 'weekly':
+                $frequency = 'WEEKLY';
+                break;
+            case 'biweekly':
+                $frequency = 'WEEKLY;INTERVAL=2';
+                break;
+            case 'monthly':
+                $frequency = 'MONTHLY';
+                break;
+            case 'occurrence':
+                $frequency = $this->get_frequencies_by_day($occurrences, $date);
+                break;
+            default:
+                $frequency = null;
+        }
+
+        return $frequency;
+    }
+
+    /**
+     * Returns a comma separated list of days in which the event should repeat for the month.
+     *
+     * For example, given:
+     *   $occurrences = array( 1, 3 ) // 1st and 3rd week in the month
+     *   $date = '2019-09-15' // the day is Sunday
+     * it will return: 'MONTHLY;BYDAY=1SU,3SU'
+     *
+     * @param array $occurrences
+     * @param string $date
+     * @return string
+     */
+    private function get_frequencies_by_day($occurrences, $date)
+    {
+        // Get the first two letters of the day of the start date in uppercase letters
+        $day = strtoupper(
+            substr(strftime('%a', strtotime($date)), 0, 2)
+        );
+
+        $by_days = array_reduce(array_keys($occurrences), function ($carry, $key) use ($day, $occurrences) {
+            $carry .= $occurrences[$key] . $day;
+
+            if ($key < count($occurrences) - 1) {
+                $carry .= ',';
+            }
+
+            return $carry;
+        });
+
+        return "MONTHLY;BYDAY={$by_days}";
+    }
+}
diff --git wordpress.org/public_html/wp-content/plugins/wporg-meeting-ical/wporg-meeting-ical.php wordpress.org/public_html/wp-content/plugins/wporg-meeting-ical/wporg-meeting-ical.php
new file mode 100644
index 000000000..4d9b98a78
--- /dev/null
+++ wordpress.org/public_html/wp-content/plugins/wporg-meeting-ical/wporg-meeting-ical.php
@@ -0,0 +1,202 @@
+<?php
+/*
+Plugin Name: WPORG Make Homepage Meeting iCalendar API
+Description: Provides API endpoints for iCalendar files that are generated dynamically from WPORG Meetings
+Version:     1.0
+License:     GPLv2 or later
+Author:      WordPress.org
+Author URI:  http://wordpress.org/
+Text Domain: wporg
+*/
+
+require __DIR__ . '/class-ical-generator.php';
+
+class Meeting_ICAL
+{
+
+    private static $instance = NULL;
+
+    const QUERY_KEY = 'meeting_ical';
+    const QUERY_TEAM_KEY = 'meeting_team';
+
+    public static function getInstance()
+    {
+        NULL === self::$instance && self::$instance = new self;
+        return self::$instance;
+    }
+
+    public static function init()
+    {
+        $mi = Meeting_ICAL::getInstance();
+
+        register_activation_hook(__FILE__, array($mi, 'on_activate'));
+        register_deactivation_hook(__FILE__, array($mi, 'on_deactivate'));
+
+        add_action('init', array($mi, 'add_rewrite_rules'));
+        add_action('parse_request', array($mi, 'parse_request'));
+
+        add_filter('query_vars', array($mi, 'query_vars'));
+    }
+
+    public function on_activate()
+    {
+        $this->add_rewrite_rules();
+        flush_rewrite_rules();
+    }
+
+    public function on_deactivate()
+    {
+        flush_rewrite_rules(); // remove custom rewrite rule
+        delete_option(self::QUERY_KEY); // remove cache
+    }
+
+    public function add_rewrite_rules()
+    {
+        add_rewrite_rule(
+            '^meetings/?([a-zA-Z\d\s_-]+)?/calendar\.ics$',
+            array(self::QUERY_KEY => 1, self::QUERY_TEAM_KEY => '$matches[1]'),
+            'top'
+        );
+    }
+
+    public function parse_request($request)
+    {
+        if (!array_key_exists(self::QUERY_KEY, $request->query_vars)) {
+            return;
+        }
+
+        $team = strtolower($request->query_vars[self::QUERY_TEAM_KEY]);
+
+        // Generate a calendar if such a team exists
+        if ($ical = $this->get_ical_contents($team)) {
+            /**
+             * If the calendar has a 'method' property, the 'Content-Type' header must also specify it
+             */
+            header('Content-Type: text/calendar; charset=utf-8; method=publish');
+            header('Content-Disposition: inline; filename=calendar.ics');
+            echo $ical;
+            exit;
+        }
+
+        return;
+    }
+
+    public function query_vars($query_vars)
+    {
+        array_push($query_vars, self::QUERY_KEY);
+        array_push($query_vars, self::QUERY_TEAM_KEY);
+        return $query_vars;
+    }
+
+    private function get_ical_contents($team)
+    {
+        $ttl = 60; // in seconds
+        $option = $team ? self::QUERY_KEY . "_{$team}" : self::QUERY_KEY;
+        $cache = get_option($option, false);
+
+        if (is_array($cache) && $cache['timestamp'] > time() - $ttl) {
+            return $cache['contents'];
+        }
+
+        if ($contents = $this->generate_ical_contents($team)) {
+            $cache = array('contents' => $contents, 'timestamp' => time());
+            delete_option($option);
+            add_option($option, $cache, false, false);
+
+            return $cache['contents'];
+        }
+
+        return null;
+    }
+
+    private function generate_ical_contents($team)
+    {
+        $posts = $this->get_meeting_posts($team);
+
+        // Don't generate a calendar if there are no meetings for that team
+        if (empty($posts)) {
+            return null;
+        }
+
+        $ical_generator = new Meeting_ICAL_Generator();
+        return $ical_generator->generate($posts);
+    }
+
+    /**
+     * Get all meetings for a team. If the 'team' parameter is empty, all meetings are returned.
+     *
+     * @param string $team Name of the team to fetch meetings for.
+     * @return array
+     */
+    private function get_meeting_posts($team = '')
+    {
+        // meta query to eliminate expired meetings from query
+        add_filter('get_meta_sql', function ($sql) {
+            return str_replace("'CURDATE()'", 'CURDATE()', $sql);
+        });
+
+        switch_to_blog(get_main_site_id());
+
+        $query = new WP_Query(
+            array(
+                'post_type' => 'meeting',
+                'nopaging' => true,
+                'meta_query' => array(
+                    'relation' => 'AND',
+                    array(
+                        'key' => 'team',
+                        'value' => $team,
+                        'compare' => empty($team) ? '!=' : '=',
+                    ),
+                    array(
+                        'relation' => 'OR',
+                        // not recurring  AND start_date >= CURDATE() = one-time meeting today or still in future
+                        array(
+                            'relation' => 'AND',
+                            array(
+                                'key' => 'recurring',
+                                'value' => array('weekly', 'biweekly', 'occurrence', 'monthly', '1'),
+                                'compare' => 'NOT IN',
+                            ),
+                            array(
+                                'key' => 'start_date',
+                                'type' => 'DATE',
+                                'compare' => '>=',
+                                'value' => 'CURDATE()',
+                            )
+                        ),
+                        // recurring = 1 AND ( end_date = '' OR end_date > CURDATE() ) = recurring meeting that has no end or has not ended yet
+                        array(
+                            'relation' => 'AND',
+                            array(
+                                'key' => 'recurring',
+                                'value' => array('weekly', 'biweekly', 'occurrence', 'monthly', '1'),
+                                'compare' => 'IN',
+                            ),
+                            array(
+                                'relation' => 'OR',
+                                array(
+                                    'key' => 'end_date',
+                                    'value' => '',
+                                    'compare' => '=',
+                                ),
+                                array(
+                                    'key' => 'end_date',
+                                    'type' => 'DATE',
+                                    'compare' => '>',
+                                    'value' => 'CURDATE()',
+                                )
+                            )
+                        ),
+                    )
+                )
+            )
+        );
+
+        restore_current_blog();
+
+        return $query->posts;
+    }
+}
+
+Meeting_ICAL::init();
diff --git wordpress.org/public_html/wp-content/plugins/wporg-meeting-posttype/wporg-meeting-posttype.php wordpress.org/public_html/wp-content/plugins/wporg-meeting-posttype/wporg-meeting-posttype.php
index 9dd64b010..ff3b2ed63 100644
--- wordpress.org/public_html/wp-content/plugins/wporg-meeting-posttype/wporg-meeting-posttype.php
+++ wordpress.org/public_html/wp-content/plugins/wporg-meeting-posttype/wporg-meeting-posttype.php
@@ -2,7 +2,7 @@
 /*
 Plugin Name: WPORG Make Homepage Meeting Post Type
 Description: Creates the meeting post type and assorted filters for https://make.wordpress.org/meetings
-Version:     1.0
+Version:     1.1
 License:     GPLv2 or later
 Author:      WordPress.org
 Author URI:  http://wordpress.org/
@@ -253,6 +253,7 @@ class Meeting_Post_Type {
 		$occurrence = isset( $meta['occurrence'][0] ) ? unserialize( $meta['occurrence'][0] ) : array();
 		$link       = isset( $meta['link'][0] ) ? $meta['link'][0] : '';
 		$location   = isset( $meta['location'][0] ) ? $meta['location'][0] : '';
+        $sequence   = isset( $meta['sequence'][0] ) ? $meta['sequence'][0] : 0;
 		wp_nonce_field( 'save_meeting_meta_'.$post->ID , 'meeting_nonce' );
 		?>
 
@@ -326,6 +327,7 @@ class Meeting_Post_Type {
 			<input type="text" name="location" id="location" class="regular-text wide" value="<?php echo esc_attr( $location ); ?>">
 		</label>
 		</p>
+        <input type="hidden" name="sequence" value="<?php echo esc_attr( $sequence ); ?>">
 		<script>
 		jQuery(document).ready( function($) {
 			$('.date').datepicker({
@@ -381,6 +383,7 @@ class Meeting_Post_Type {
 		                         ? array_map( 'intval', $_POST['occurrence'] ) : array() );
 		$meta['link']        = ( isset( $_POST['link'] ) ? esc_url( $_POST['link'] ) : '' );
 		$meta['location']    = ( isset( $_POST['location'] ) ? esc_textarea( $_POST['location'] ) : '' );
+        $meta['sequence']    = ( isset( $_POST['sequence'] ) ? intval( $_POST['sequence'] ) + 1 : 0 );
 
 		foreach ( $meta as $key => $value ) {
 			update_post_meta( $post->ID, $key, $value );
