Serve iCalendar (.ics) as JSON for websites https://chaostreff-backnang.de
Find a file
2026-01-20 00:59:56 +01:00
cache Add handler to serve filtered ics file 2026-01-20 00:32:39 +01:00
include Move ICal module to include folder, export config to separate config file 2026-01-20 00:32:08 +01:00
.gitignore Add cache ics files to gitignore 2026-01-20 00:30:57 +01:00
index.php Move ICal module to include folder, export config to separate config file 2026-01-20 00:32:08 +01:00
LICENSE Initial commit 2021-10-13 14:08:58 +02:00
README.md Add nginx sample 2026-01-20 00:59:56 +01:00

Hackcal

This script is using PHP ICS Parser to parse ics files, e.g. from Nextcloud. The output is an very simple json for embedding the calender into html websites. Its presorted by days and preformatted to use less js on client side for date interpretation. The server part requires php-intl


Installation

Just copy the files into your php capable webserver directory. Be sure, that the cache folder is writeable by the script and the locale set in the index.php is available on the system.

Usage

Just call the url. You can filter by categories by adding /?filter=category to the request. You can expand the period of the events by adding /?period= to the request. The number represents the amount of future months as an integer between 1..12.

Example integration using vanilla js

You will need to include a <div id="hackcal"></div> placeholder in your html page.

document.addEventListener("DOMContentLoaded", function () {

    var cal_uri = "https://YOURDOMAIN/hackcal/";
    var uri_regex = /(https?:\/\/([-\w\.]+)+(:\d+)?(\/([\w\/\-_\.\:]*(\?\S+)?)?)?)/ig

    fetch(cal_uri).then(res => res.json()).then(data => {
        var items = [];
        Object.keys(data).forEach(function (date) {
            var day = new Date(date);
            items.push("<div data-date='" + date + "'><span>" + day.toLocaleDateString(true, {weekday:'short', day:'numeric', month:'long', year: 'numeric'}) + "</span></div>");
            Object.keys(data[date]).forEach(function (uid) {
                var event = data[date][uid];
                var location = (event.location)?event.location.replace(/\n/g,"<br>").replace(uri_regex, "<a href='$1' target='_blank'>$1</a>"):'';
                var description = (event.description)?event.description.replace(/\n/g,"<br>").replace(uri_regex, (url) => {event.summary = `<a href='${url}' target='_blank'>${event.summary}</a>`; return "";}):'';
                var categories = (event.categories)?"<i>" + event.categories.replace(",", "</i> <i>") + "</i>":'';
                items.push("<div data-uid='" + uid + "'><span>" + event.datestr + "</span><span><b>" + event.summary + "</b> | " + location + "<br/>" + description + categories + "</span></div>");
            });
        });
        document.querySelector('#hackcal').innerHTML = items.join( "" );
    });

});

Sample CSS

#hackcal {border: 1px solid #CCC}
#hackcal div {display: flex; padding: 8px 14px}
#hackcal [data-date] {background: #CCC; font-weight: bold}
#hackcal [data-uid] {font-size: .9em; gap: .5em; line-height: 1.5em}
#hackcal [data-uid] span:first-child {flex: 0 0 3em; font-weight: bold}

Sample nginx config

server {
    listen 80;
    listen [::]:80;
    server_name hackcal.mydomain.example;

    # Path to the root of your installation
    root /var/www/hackcal;

    # Charset
    charset UTF-8;

    # Enable gzip but do not remove ETag headers
    gzip on;
    gzip_vary on;
    gzip_comp_level 4;
    gzip_min_length 256;
    gzip_proxied expired no-cache no-store private no_last_modified no_etag auth;
    gzip_types application/atom+xml application/javascript application/json application/ld+json application/manifest+json application/rss+xml application/vnd.geo+json application/vnd.ms-fontobject application/wasm application/x-font-ttf application/x-web-app-manifest+json application/xhtml+xml application/xml font/opentype image/bmp image/svg+xml image/x-icon text/cache-manifest text/css text/plain text/vcard text/vnd.rim.location.xloc text/vtt text/x-component text/x-cross-domain-policy;

    # Remove X-Powered-By, which is an information leak
    fastcgi_hide_header X-Powered-By;

    index index.php;

    location ~ \.php$ {
        include fastcgi_params;
        fastcgi_index index.php;
        fastcgi_param SCRIPT_FILENAME $request_filename;
        fastcgi_pass php-handler;

        fastcgi_cache phpcache;
        fastcgi_cache_valid 1m;
        fastcgi_cache_use_stale error timeout updating invalid_header http_500 http_503;
        fastcgi_cache_min_uses 1;
        fastcgi_cache_lock on;
        add_header X-FastCGI-Cache $upstream_cache_status;
    }

    location = /robots.txt {
        allow all;
        log_not_found off;
        access_log off;
    }

    # Serve filtered calendar.ics
    location = /mycalendar.ics {
        rewrite ^ /cache/index.php last;
    }

}