diff --git a/Makefile b/Makefile index 72929ec..e36a222 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,14 @@ -.PHONY: all css serve-dev +.PHONY: all css js serve-dev + +define update_cache_buster + hash=$$(sha256sum "$(1)" | cut -c1-8); \ + perl -0pi -e "s|(^[[:space:]]*$(2):.*?url: '/)[^']*(')|\$$1$(1)?v=$$hash\$$2|ms" _config.yml +endef + +define update_sri + hash=$$(openssl dgst -sha384 -binary "$(1)" | openssl base64 -A); \ + perl -0pi -e "s|(^[[:space:]]*$(2):.*?sri: ')[^']*(')|\$$1sha384-$$hash\$$2|ms" _config.yml +endef all: @echo '# These are the available make targets' @@ -6,7 +16,22 @@ all: css: npm install - npx less --clean-css="--s1 --advanced" styles/styles.less > styles/styles.min.css + npx lessc --clean-css="--s1 --advanced" styles/styles.less > styles/styles.min.css + $(call update_cache_buster,styles/styles.min.css,css) + $(call update_sri,styles/styles.min.css,css) + +js: + npm install + @for file in js/*.js; do \ + case "$$file" in \ + *.min.js) continue ;; \ + esac; \ + minified="$${file%.js}.min.js"; \ + name="$$(basename "$$file" .js)"; \ + npx uglifyjs "$$file" -c -m -o "$$minified"; \ + $(call update_cache_buster,$$minified,$$name); \ + $(call update_sri,$$minified,$$name); \ + done serve-dev: docker run -it -p 4000:4000 -v $(PWD):/site -w /site library/ruby:3 bash -c 'bundle install && bundle exec jekyll serve -H 0.0.0.0' diff --git a/README.md b/README.md index 02d9161..3e9ee7e 100644 --- a/README.md +++ b/README.md @@ -12,3 +12,7 @@ The pages are rendered with Jekyll and kindly hosted by github. ## Build CSS To build the stylesheets run `make css`. + +## Build JS + +To build the stylesheets run `make js`. diff --git a/_bin/update-jsdelivr-refs.php b/_bin/update-jsdelivr-refs.php new file mode 100755 index 0000000..6877b86 --- /dev/null +++ b/_bin/update-jsdelivr-refs.php @@ -0,0 +1,228 @@ +#!/usr/bin/env php + true, + CURLOPT_FOLLOWLOCATION => true, + CURLOPT_TIMEOUT => 15, + CURLOPT_HTTPHEADER => [ + 'Accept: application/json', + ], + ]); + + $response = curl_exec($ch); + + if ($response === false) { + $error = curl_error($ch); + curl_close($ch); + + throw new RuntimeException( + sprintf('Request failed: %s', $error) + ); + } + + $status = curl_getinfo($ch, CURLINFO_HTTP_CODE); + + curl_close($ch); + + if ($status < 200 || $status >= 300) { + throw new RuntimeException( + sprintf( + 'Request failed with HTTP %d: %s', + $status, + $url + ) + ); + } + + $data = json_decode($response, true); + + if (!is_array($data)) { + throw new RuntimeException( + sprintf( + 'Invalid JSON returned by %s', + $url + ) + ); + } + + return $data; +} + +/** + * Resolve a version constraint to a concrete version. + * + * jsDelivr handles the version/semver resolution. + */ +function resolveVersion($package, $constraint) +{ + $url = JSDELIVR_API . '/packages/npm/' . rawurlencode($package) . '/resolved?specifier=' . rawurlencode($constraint); + $data = apiRequest($url); + + if (!isset($data['version']) || !is_string($data['version'])) { + throw new RuntimeException( + sprintf( + 'Unable to resolve version "%s" for %s', + $constraint, + $package + ) + ); + } + + return $data['version']; +} + +/** + * Get the package metadata and file listing for a + * specific version. + */ +function getPackageFiles($package, $version) +{ + $url = JSDELIVR_API . '/packages/npm/' . rawurlencode($package) . '@' . rawurlencode($version); + $data = apiRequest($url); + + if (!isset($data['files']) || !is_array($data['files'])) { + throw new RuntimeException( + sprintf( + 'No file listing returned for %s@%s', + $package, + $version + ) + ); + } + + return $data['files']; +} + +/** + * Flatten the jsDelivr file tree into a path => metadata array. + */ +function flattenFiles($files, $prefix = '') +{ + $result = []; + + foreach ($files as $file) { + if (!isset($file['name'])) { + continue; + } + + $name = $file['name']; + $path = $prefix . '/' . ltrim($name, '/'); + + if (($file['type'] ?? null) === 'directory') { + if (isset($file['files']) && is_array($file['files'])) { + $result += flattenFiles($file['files'], $path); + } + + continue; + } + + $result[$path] = $file; + } + + return $result; +} + +/** + * Process all dependencies and return the resolved + * CDN configuration. + */ +function processDependencies($refs) +{ + $result = []; + + foreach ($refs as $name => $dependency) { + $package = $dependency['package']; + + if (!isset($dependency['version'])) { + throw new RuntimeException(sprintf('No version specified for %s', $package)); + } + + $requestedVersion = $dependency['version']; + $resolvedVersion = resolveVersion($package, $requestedVersion); + $files = getPackageFiles($package, $resolvedVersion); + $availableFiles = flattenFiles($files); + + foreach (['css', 'js'] as $type) { + if (empty($dependency[$type]) || !is_array($dependency[$type])) { + continue; + } + + $result[$name][$type] = []; + + foreach ($dependency[$type] as $file) { + if (!isset($availableFiles[$file])) { + throw new RuntimeException( + sprintf( + 'File does not exist: %s@%s%s', + $name, + $resolvedVersion, + $file + ) + ); + } + + $fileInfo = $availableFiles[$file]; + + if (!isset($fileInfo['hash'])|| !is_string($fileInfo['hash'])) { + throw new RuntimeException( + sprintf( + 'No hash available for: %s@%s%s', + $name, + $resolvedVersion, + $file + ) + ); + } + + $result[$name][$type][] = [ + 'url' => JSDELIVR_CDN . '/' . $package . '@' . $resolvedVersion . $file, + 'sri' => 'sha256-' . $fileInfo['hash'], + ]; + } + } + } + + return $result; +} + + +try { + $resolved = processDependencies($refs); + foreach ($resolved as $package => $types) { + echo " {$package}:" . PHP_EOL; + + foreach ($types as $type => $files) { + echo " {$type}:" . PHP_EOL; + + foreach ($files as $file) { + echo " url: '{$file['url']}'" . PHP_EOL; + echo " sri: '{$file['sri']}'" . PHP_EOL; + } + } + } +} catch (Throwable $e) { + fwrite(STDERR, 'ERROR: ' . $e->getMessage() . PHP_EOL); + exit(1); +} \ No newline at end of file diff --git a/_config.yml b/_config.yml index 09d8338..df0de88 100644 --- a/_config.yml +++ b/_config.yml @@ -1,38 +1,56 @@ safe: true encoding: UTF-8 url: https://roundcube.net -enforce_ssl: roundcube.net -hostname: "https://roundcube.net" permalink: news/:year/:month/:day/:title exclude: [vendor] plugins: [jekyll-paginate] paginate: 10 -paginate_path: "/news/page:num/" -environment: "production" +paginate_path: '/news/page:num/' +environment: 'production' num_related_items: 5 -libs: { - fonts: { - roboto: 'https://fonts.googleapis.com/css?family=Roboto&display=swap', - }, - less: { - url: 'https://cdn.jsdelivr.net/npm/less@4.6.4/dist/less.min.js', - sri: 'sha384-WR+iiQT+XSq8wDUutASKAXx1pZTJCLRyHfpwqBiL1ji4qd2BsJIPhn8k/s72NkPj' - }, - css: '/styles/styles.min.css?v=2026051000', - bootstrap: { - css: { - url: 'https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/css/bootstrap.min.css', - sri: 'sha384-sRIl4kxILFvY47J16cr9ZwB07vP4J8+LH7qKQnuqkuIAvNWLzeN8tE5YBujZqJLB' - }, - js: { - url: 'https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/js/bootstrap.bundle.min.js', - sri: 'sha384-FKyoEForCGlyvwx9Hj09JcYn3nv7wiPVlz7YYwJrWVcXK/BmnVDxM+D2scQbITxI' - } - }, - fontawesome: { - css: { - url: 'https://cdn.jsdelivr.net/npm/@fortawesome/fontawesome-free@7.2.0/css/all.min.css', - sri: 'sha384-EXatlQyrOJgDaM9/a74ArMzy7/2bTMSrZj8ID1IPeVmc3GncfCugefCFWSLj8JL/' - }, - } -} +exclude: + - Gemfile + - Gemfile.lock + - jsdelivr.json + - node_modules/ + - Makefile + - package.json + - package-lock.json + - README.md + - stylelint.config.mjs +libs: + css: + url: '/styles/styles.min.css?v=9bf32423' + sri: 'sha384-yb8/IfPlNNl8SDmdqLJ/CA5xbgQHXyHb2ehtoAamh+bLbMmu/tW+EBvF2BdzkzLv' + js: + banner: + url: '/js/banner.min.js?v=f44c2f56' + sri: 'sha384-zF0W4VZ06nGXgt2nFDiPoMhn5+UK+WaAIX1cprNGHkcURqccwGD+fXRZiu77TZ+b' + copy: + url: '/js/copy.min.js?v=ebb9b310' + sri: 'sha384-9TlbtUpnP3OoI5QMwNcfEMbkzjHOvlpeDEjVJCPWHJPxwCA5K2dXqlKZrb07rYVa' + theme: + url: '/js/theme.min.js?v=17d30fbf' + sri: 'sha384-VhNYv2z6f3ZriTjr7a9xelQKHZilrphb/xj36CtPT1McEdLSIPrMG4L+IJkgC1+T' + viewer: + url: '/js/viewer.min.js?v=df6dc21d' + sri: 'sha384-Gn/Zio1YIE6O7P0MXgTN8GECQMnPDd8MYeXQbJV5hk4+6O6aWhkBHcFUFs+DpNDL' + less: + js: + url: 'https://cdn.jsdelivr.net/npm/less@4.9.1/dist/less.min.js' + sri: 'sha256-QoOydTeDccOLjcHMHS1oOUe0QaqHXh2XFuki4s/zSXc=' + roboto: + css: + url: 'https://cdn.jsdelivr.net/npm/@fontsource/roboto@5.3.0/400.css' + sri: 'sha256-aYPEXwxOBquazL7uNzcM57whskXEvU0X84EZ2lI41yk=' + bootstrap: + css: + url: 'https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/css/bootstrap.min.css' + sri: 'sha256-2FMn2Zx6PuH5tdBQDRNwrOo60ts5wWPC9R8jK67b3t4=' + js: + url: 'https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/js/bootstrap.bundle.min.js' + sri: 'sha256-5P1JGBOIxI7FBAvT/mb1fCnI5n/NhQKzNUuW7Hq0fMc=' + fontawesome: + css: + url: 'https://cdn.jsdelivr.net/npm/@fortawesome/fontawesome-free@7.3.1/css/all.min.css' + sri: 'sha256-4Lad8m4ZWW1Lgb9+sMVLYEfnIh7BjV1NQMEe79Pviks=' diff --git a/_includes/footer.html b/_includes/footer.html index 9e7e268..bda5fad 100644 --- a/_includes/footer.html +++ b/_includes/footer.html @@ -13,9 +13,9 @@ Switch theme
@@ -41,91 +41,14 @@ - \ No newline at end of file +{% assign scripts = "theme" | split: "," %} +{% if page.scripts %}{% assign scripts = scripts | concat: page.scripts %}{% endif %} +{% for script in scripts %} + {% assign js = site.libs.js[script] %} + + {% if site.environment == "dev" %} + + {% else %} + + {% endif %} +{% endfor %} diff --git a/_includes/head.html b/_includes/head.html index 6c5fcb5..bd5c82b 100644 --- a/_includes/head.html +++ b/_includes/head.html @@ -5,20 +5,14 @@ - + {% if site.environment == "dev" %} - - + + {% else %} - + {% endif %} - diff --git a/_includes/viewer.html b/_includes/viewer.html new file mode 100644 index 0000000..15353b5 --- /dev/null +++ b/_includes/viewer.html @@ -0,0 +1,20 @@ +- gpg --verify roundcubemail-<version>.tar.gz{.asc,} + gpg --verify roundcubemail-<version>.tar.gz{.asc,}
-
+
The Nextcloud app "Roundcube Mail" integrates an existing Roundcube standalone email-webapp into Nextcloud, with optional SSO.
Roundcube for Enterprises - is a stand-alone, all-in-one email solution for service providers by Nextcloud. Your customers need a reliable, dependable and performant email service. The Nextcloud Roundcube Enterprise subscription helps you deliver exactly that. + is a stand-alone, all-in-one email solution for service providers by Nextcloud. Your customers need a reliable, dependable and performant email service. The Nextcloud Roundcube Enterprise subscription helps you deliver exactly that.
diff --git a/google2ae4ba011e8a69d4.html b/google2ae4ba011e8a69d4.html index e046b08..d24ab5a 100644 --- a/google2ae4ba011e8a69d4.html +++ b/google2ae4ba011e8a69d4.html @@ -1,4 +1,7 @@ + - + +