diff --git a/README.md b/README.md index 479f94cb..74ba5e80 100644 --- a/README.md +++ b/README.md @@ -2,4 +2,90 @@ # selfh.st/icons -This repository is a data store for the collection of icons found at [selfh.st/icons](https://selfh.st/icons). For more information, please see the project's [About](https://selfh.st/icons-about/) page. +[selfh.st/icons](https://selfh.st/icons) is a collection of 4,400+ logos and icons for self-hosted (and non-self-hosted) software. + +The collection is available for browsing via the directory at [selfh.st/icons](https://selfh.st/icons) and served to users directly from this repo using the jsDelivr content delivery network. + +To self-host the collection, users can clone, download, or sync the repository with a tool like [git-sync](https://github.com/AkashRajpurohit/git-sync) and serve it with a web server of their choosing (Caddy, NGINX, etc.). + + +### Color Options + +By default, most SVG icons are available in three color formats: + +* **Standard**: The standard colors of an icon without any modifications. +* **Dark**: A modified version of an icon displayed entirely in black (```#000000```). +* **Light**: A modified version of an icon displayed entirely in white (```#FFFFFF```). + +(Toggles to view icons by color type are available in the [directory hosted on the selfh.st website](https://selfh.st/icons).) + +### Custom Colors + +Because the dark and light versions of each icon are monochromatic, CSS can theoretically be leveraged to apply custom colors to the icons. + +This only works, however, when the SVG code is embedded directly onto a webpage. Unfortunately, most [integrations](https://selfh.st/apps/?tag=selfh-st-icons) link to the icons via an `` tag, which prevents styling from being overridden via CSS. + +As a workaround, a lightweight self-hosted server has been published via Docker that utilizes a URL parameter for color conversion on the fly. Continue reading for further instructions. + + +#### Deploying the Custom Color Container + +* Introduction +* Deploying the container +* Configuring a reverse proxy (optional) +* Linking to a custom icon +* Changelog + +##### Introduction + +The Docker image below allows users to host a local server that acts as a proxy between requests and jsDelivr. When a color parameter is detected in the URL, the server will intercept the requests, fill the SVG file with that color, and serve it to the user. + +Once deployed, users can append ```?color=eeeeee``` to the end of a URL to specify a custom color (replacing ```eeeeee``` with any [hex color code](https://htmlcolorcodes.com/)). + +##### Deployment + +The container can be easily deployed via docker-compose with the following snippet: + +``` +selfhst-icons: + image: ghcr.io/selfhst/icons:latest + restart: unless-stopped + ports: + - 4050:4050 +``` + +No volume mounts or environment variables are currently required. + +##### Reverse Proxy + +While out of the scope of this guide, many applications will require users to leverage HTTPS when linking to icons served from the container. + +The process to proxy the container and icons is straightforward. A sample Caddyfile configuration has been provided for reference: + +``` +icons.selfh.st { + reverse_proxy selfhst-icons:4050 +} +``` + +##### Linking + +After the container has been deployed, users can easily link to any existing icon within the collection: + +* ```https://icons.selfh.st/bookstack.svg``` +* ```https://icons.selfh.st/bookstack.png``` +* ```https://icons.selfh.st/bookstack-dark.webp``` + +To customize the color, users **must** link to the *standard* version of an SVG icon that has available monochromatic (dark/light) versions. To do so, append a custom URL parameter referencing any [hex color code](https://htmlcolorcodes.com/): + +* ```https://icons.selfh.st/bookstack.svg?color=eeeeee``` +* ```https://icons.selfh.st/bookstack.svg?color=439b68``` + +**Note the following:** + +* Only the standard icons accept URL parameters (for example, ```bookstack-light.svg?color=fff000``` will not yield a different color. +* Only append the alpha-numeric portion of the hex color code to the URL. The server will append the ```#``` in the backend before passing it on for styling. + +##### Changelog + +* 2025-04-30: Initial release \ No newline at end of file diff --git a/build/dockerfile b/build/dockerfile new file mode 100755 index 00000000..0e94eff5 --- /dev/null +++ b/build/dockerfile @@ -0,0 +1,11 @@ +FROM node:18-alpine +WORKDIR /app + +COPY package.json ./ +RUN npm install + +COPY server.js . + +EXPOSE 4050 + +CMD ["node", "server.js"] \ No newline at end of file diff --git a/build/package.json b/build/package.json new file mode 100755 index 00000000..6ddc0425 --- /dev/null +++ b/build/package.json @@ -0,0 +1,13 @@ +{ + "name": "test-repo", + "version": "1.0.0", + "main": "server.js", + "scripts": { + "start": "node server.js" + }, + "type": "module", + "dependencies": { + "express": "^4.18.2", + "node-fetch": "^3.3.0" + } +} \ No newline at end of file diff --git a/build/server.js b/build/server.js new file mode 100755 index 00000000..5c812048 --- /dev/null +++ b/build/server.js @@ -0,0 +1,89 @@ +import express from 'express' +import fetch from 'node-fetch' +import path from 'path' + +const app = express() +const PORT = 4050 +const CDN_ROOT = 'https://cdn.jsdelivr.net/gh/selfhst/icons' +const CDN_PATH = 'svg' + +async function fileExists(url) { + try { + const resp = await fetch(url, { method: 'HEAD' }); + return resp.ok; + } catch { + return false; + } +} + +async function fetchAndPipe(url, res) { + const response = await fetch(url); + if (!response.ok) return res.status(404).send('File not found'); + res.type(path.extname(url).slice(1)); + response.body.pipe(res); +} + +app.get('/*', async (req, res) => { + const urlPath = req.path; + const extMatch = urlPath.match(/\.(\w+)$/); + if (!extMatch) + return res.status(404).send('File extension missing'); + + const ext = extMatch[1].toLowerCase(); + if (!['png', 'webp', 'svg'].includes(ext)) + return res.status(404).send('Format not supported'); + + const filename = urlPath.slice(1); + const lowerFilename = filename.toLowerCase(); + + const isSuffix = lowerFilename.endsWith('-light.svg') || lowerFilename.endsWith('-dark.svg'); + + if (isSuffix) { + return fetchAndPipe(`${CDN_ROOT}/${CDN_PATH}/${filename}`, res); + } + + let mainUrl; + if (ext === 'png') { + mainUrl = `${CDN_ROOT}/png/${filename}`; + } else if (ext === 'webp') { + mainUrl = `${CDN_ROOT}/webp/${filename}`; + } else if (ext === 'svg') { + mainUrl = `${CDN_ROOT}/svg/${filename}`; + } else { + mainUrl = null; + } + + const hasColor = !!req.query['color'] && req.query['color'].trim() !== ''; + + if (ext === 'svg') { + if (hasColor) { + const baseName = filename.replace(/\.(png|webp|svg)$/, ''); + const suffixUrl = `${CDN_ROOT}/${CDN_PATH}/${baseName}-light.svg`; + if (await fileExists(suffixUrl)) { + let svgContent = await fetch(suffixUrl).then(r => r.text()); + const color = req.query['color'].startsWith('#') ? req.query['color'] : `#${req.query['color']}`; + svgContent = svgContent + .replace(/style="[^"]*fill:\s*#fff[^"]*"/gi, (match) => { + console.log('Replacing style fill:', match); + return match.replace(/fill:\s*#fff/gi, `fill:${color}`); + }) + .replace(/fill="#fff"/gi, `fill="${color}"`); + return res.type('image/svg+xml').send(svgContent); + } else { + return fetchAndPipe(mainUrl, res); + } + } else { + return fetchAndPipe(mainUrl, res); + } + } else { + // PNG/WebP: serve directly + return fetchAndPipe(mainUrl, res); + } +}); + +app.get('/', (req, res) => { + res.send('Self-hosted icon server'); +}); +app.listen(PORT, () => { + console.log(`Listening on port ${PORT}`); +}); \ No newline at end of file diff --git a/png/deployarr-dark.png b/png/deployrr-dark.png similarity index 100% rename from png/deployarr-dark.png rename to png/deployrr-dark.png diff --git a/png/deployarr-light.png b/png/deployrr-light.png similarity index 100% rename from png/deployarr-light.png rename to png/deployrr-light.png diff --git a/png/deployarr.png b/png/deployrr.png similarity index 100% rename from png/deployarr.png rename to png/deployrr.png diff --git a/png/positive-intentions-dark.png b/png/positive-intentions-dark.png index 5cf8c77e..20cded79 100755 Binary files a/png/positive-intentions-dark.png and b/png/positive-intentions-dark.png differ diff --git a/png/positive-intentions-light.png b/png/positive-intentions-light.png index efc83ca5..3368d588 100755 Binary files a/png/positive-intentions-light.png and b/png/positive-intentions-light.png differ diff --git a/svg/deployarr-dark.svg b/svg/deployrr-dark.svg similarity index 100% rename from svg/deployarr-dark.svg rename to svg/deployrr-dark.svg diff --git a/svg/deployarr-light.svg b/svg/deployrr-light.svg similarity index 100% rename from svg/deployarr-light.svg rename to svg/deployrr-light.svg diff --git a/svg/deployarr.svg b/svg/deployrr.svg similarity index 100% rename from svg/deployarr.svg rename to svg/deployrr.svg diff --git a/svg/positive-intentions-dark.svg b/svg/positive-intentions-dark.svg index 978db1e6..ae57b2d6 100755 --- a/svg/positive-intentions-dark.svg +++ b/svg/positive-intentions-dark.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/svg/positive-intentions-light.svg b/svg/positive-intentions-light.svg index 1f1b8945..03a99db4 100755 --- a/svg/positive-intentions-light.svg +++ b/svg/positive-intentions-light.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/webp/deployarr-dark.webp b/webp/deployrr-dark.webp similarity index 100% rename from webp/deployarr-dark.webp rename to webp/deployrr-dark.webp diff --git a/webp/deployarr-light.webp b/webp/deployrr-light.webp similarity index 100% rename from webp/deployarr-light.webp rename to webp/deployrr-light.webp diff --git a/webp/deployarr.webp b/webp/deployrr.webp similarity index 100% rename from webp/deployarr.webp rename to webp/deployrr.webp diff --git a/webp/positive-intentions-dark.webp b/webp/positive-intentions-dark.webp index 0c960516..eb5201f2 100755 Binary files a/webp/positive-intentions-dark.webp and b/webp/positive-intentions-dark.webp differ diff --git a/webp/positive-intentions-light.webp b/webp/positive-intentions-light.webp index 3169fc97..082e1e27 100755 Binary files a/webp/positive-intentions-light.webp and b/webp/positive-intentions-light.webp differ