diff --git a/.github/workflows/build-and-push.yml b/.github/workflows/build-and-push.yml
new file mode 100644
index 00000000..e2f14b99
--- /dev/null
+++ b/.github/workflows/build-and-push.yml
@@ -0,0 +1,98 @@
+name: Build and Push Docker Images
+
+on:
+ push:
+ branches: [ main ]
+ paths:
+ - 'build/**'
+ - '.github/workflows/build-and-push.yml'
+
+ release:
+ types: [ published ]
+
+ workflow_dispatch:
+ inputs:
+ version:
+ description: 'Version tag (e.g., 2.0.0)'
+ required: true
+ default: 'latest'
+
+env:
+ REGISTRY: ghcr.io
+ IMAGE_NAME: selfhst/icons
+
+jobs:
+ build-and-push:
+ runs-on: ubuntu-latest
+ permissions:
+ contents: read
+ packages: write
+
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v4
+
+ - name: Set up Docker Buildx
+ uses: docker/setup-buildx-action@v3
+
+ - name: Log in to Container Registry
+ uses: docker/login-action@v3
+ with:
+ registry: ${{ env.REGISTRY }}
+ username: ${{ github.actor }}
+ password: ${{ secrets.GITHUB_TOKEN }}
+
+ - name: Read version from file
+ id: version
+ run: |
+ VERSION=$(cat build/VERSION | tr -d '\n')
+ echo "version=$VERSION" >> $GITHUB_OUTPUT
+ echo "Version from file: $VERSION"
+
+ # Extract semantic version parts
+ MAJOR=$(echo $VERSION | cut -d. -f1)
+ MINOR=$(echo $VERSION | cut -d. -f1-2)
+
+ echo "major_version=$MAJOR" >> $GITHUB_OUTPUT
+ echo "minor_version=$MINOR" >> $GITHUB_OUTPUT
+ echo "Major: $MAJOR, Minor: $MINOR"
+
+ - name: Extract metadata
+ id: meta
+ uses: docker/metadata-action@v5
+ with:
+ images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
+ tags: |
+ # For releases, use the release tag
+ type=ref,event=tag
+ # For main branch pushes, create semantic version tags
+ type=raw,value=${{ steps.version.outputs.version }},enable={{is_default_branch}}
+ type=raw,value=${{ steps.version.outputs.major_version }},enable={{is_default_branch}}
+ type=raw,value=${{ steps.version.outputs.minor_version }},enable={{is_default_branch}}
+ type=raw,value=latest,enable={{is_default_branch}}
+ labels: |
+ org.opencontainers.image.title=selfh.st/icons
+ org.opencontainers.image.description=Self-hosted icon server with custom color support
+ org.opencontainers.image.version=${{ steps.version.outputs.version }}
+ org.opencontainers.image.source=https://github.com/${{ github.repository }}
+
+ - name: Build and push Docker image
+ uses: docker/build-push-action@v5
+ with:
+ context: ./build
+ platforms: linux/amd64,linux/arm64,linux/arm/v7
+ push: true
+ tags: ${{ steps.meta.outputs.tags }}
+ labels: ${{ steps.meta.outputs.labels }}
+ cache-from: type=gha
+ cache-to: type=gha,mode=max
+
+ - name: Output image info
+ run: |
+ echo "🐳 Images built and pushed:"
+ echo "${{ steps.meta.outputs.tags }}" | sed 's/^/ - /'
+ echo ""
+ echo "🏗️ Supported architectures:"
+ echo " - linux/amd64 (Intel/AMD x86_64)"
+ echo " - linux/arm64 (ARM 64-bit, Apple M1, Raspberry Pi 4)"
+ echo " - linux/arm/v7 (ARM 32-bit, Raspberry Pi 2/3)"
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index 2dc68ed6..515e8d71 100755
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,4 @@
.*
staging/
-!/.gitignore
\ No newline at end of file
+!/.gitignore
+!/.github
\ No newline at end of file
diff --git a/README.md b/README.md
index 06a59b06..fff01cb2 100644
--- a/README.md
+++ b/README.md
@@ -1,107 +1,18 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-

-
-
-[selfh.st/icons](https://selfh.st/icons) is a collection of 4,500+ logos and icons for self-hosted (and non-self-hosted) software.
+[selfh.st/icons](https://selfh.st/icons) is a collection of 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 such as [git-sync](https://github.com/AkashRajpurohit/git-sync) and serve it via a web server of their choosing (Caddy, NGINX, etc.).
+To self-host the collection, users should refer to the [repository's wiki](https://github.com/selfhst/icons/wiki).
-## Color Options
+#### Custom Colors
-By default, most SVG icons are available in three color formats:
+By default, the directory provides SVG icons (when available) in standard, dark, and light colors. Users looking to display the icons using colors of their choice can become a [paid member of selfh.st](https://selfh.st/perks) or [deploy the collection themselves](https://github.com/selfhst/icons/wiki).
-* **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```).
+#### Requests
-(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](https://github.com/selfhst/icons#introduction)
-* [Deploying the container](https://github.com/selfhst/icons#deployment)
-* [Configuring a reverse proxy (optional)](https://github.com/selfhst/icons#reverse-proxy)
-* [Linking to a custom icon](https://github.com/selfhst/icons#linking)
-* [Changelog](https://github.com/selfhst/icons#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
+Requests for new or updated icons can be made via the repository's [discussions](https://github.com/selfhst/icons/discussions). To ensure quality and consistency, pull requests will not be accepted from unapproved members.
\ No newline at end of file
diff --git a/build/dockerfile b/build/dockerfile
deleted file mode 100755
index 0e94eff5..00000000
--- a/build/dockerfile
+++ /dev/null
@@ -1,11 +0,0 @@
-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
deleted file mode 100755
index 6ddc0425..00000000
--- a/build/package.json
+++ /dev/null
@@ -1,13 +0,0 @@
-{
- "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
deleted file mode 100755
index 5c812048..00000000
--- a/build/server.js
+++ /dev/null
@@ -1,89 +0,0 @@
-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