You can serve calendars in ICal format from your web application on-the fly or you can save it to the disk as an *.ical file and serve the static file. Then people can add it to their calendar application and see the events you list.

The modules in use:

This code generates two events. The first one has a beginning and ending date. The second one has a beginninng and a duration. It also includes a link to Zoom as the "location".

examples/generate_ical.pl

#!/usr/bin/perl

use strict;
use warnings;

use Data::ICal               ();
use Data::ICal::Entry::Event ();
use DateTime                 ();
use DateTime::Format::ICal   ();
use DateTime::Duration       ();

my $calendar = Data::ICal->new;
my $now = DateTime->now;

{
    my $begin = DateTime->new( year => 2021, month => 4, day => 5, hour => 15 );
    my $end   = DateTime->new( year => 2021, month => 4, day => 5, hour => 17 );
    my $event = Data::ICal::Entry::Event->new;
    $event->add_properties(
        summary     => 'Short title of the event',
        description => "A longer description that can also have ebedded newlines.\nThen it can continue on a second and\nthird line.",
        dtstart     => DateTime::Format::ICal->format_datetime($begin),
        location    => 'The local coffee shop',
        dtend       => DateTime::Format::ICal->format_datetime($end),
        dtstamp     => DateTime::Format::ICal->format_datetime($now),
        uid         => DateTime::Format::ICal->format_datetime($begin) . '-short',
    );
    $calendar->add_entry($event);
}

{
    my $begin = DateTime->new( year => 2021, month => 4, day => 6, hour => 6, time_zone => '+0200' );
    my $duration = DateTime::Duration->new(hours => 3, minutes => 15);
    my $event = Data::ICal::Entry::Event->new;
    $event->add_properties(
        summary     => 'Another event',
        description => "Description",
        dtstart     => DateTime::Format::ICal->format_datetime($begin),
        location    => 'https://zoom.us/',
        duration    => DateTime::Format::ICal->format_duration($duration),
        dtstamp     => DateTime::Format::ICal->format_datetime($now),
        uid         => DateTime::Format::ICal->format_datetime($begin) . '-other',
    );
    $calendar->add_entry($event);
}


print $calendar->as_string;

The output is this:

examples/example.ical

BEGIN:VCALENDAR
VERSION:2.0
PRODID:Data::ICal 0.24
BEGIN:VEVENT
DESCRIPTION:A longer description that can also have ebedded newlines.\nThen
  it can continue on a second and\nthird line.
DTEND:20210405T170000
DTSTAMP:20210405T055524Z
DTSTART:20210405T150000
LOCATION:The local coffee shop
SUMMARY:Short title of the event
UID:20210405T150000-short
END:VEVENT
BEGIN:VEVENT
DESCRIPTION:Description
DTSTAMP:20210405T055524Z
DTSTART:20210406T040000Z
DURATION:+PT3H15M
LOCATION:https://zoom.us/
SUMMARY:Another event
UID:20210406T040000Z-other
END:VEVENT
END:VCALENDAR

Real world examples

For example the Perl Weekly newsletter has a calendar of Perl events based on the list of Perl events.