Skip to content

Commit 11d0e47

Browse files
kek-Secg.petrakis
and
g.petrakis
authored
Feat/branding (#7)
* Feat: implement dynamic branding configuration with environment variables and API endpoint * Feat: add utility classes for dynamic branding colors and update components to use them * Feat: update README to include customization options for branding and environment variables * Bump application version to 1.0.2 * Feat: add error handling for JSON encoding in GetConfig and implement unit tests for configuration retrieval --------- Co-authored-by: g.petrakis <g.petrakis@natechbanking.com>
1 parent 891d8d6 commit 11d0e47

File tree

16 files changed

+438
-20
lines changed

16 files changed

+438
-20
lines changed

Dockerfile

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,19 @@ COPY nginx.conf /etc/nginx/nginx.conf
5454
# Copy supervisord configuration
5555
COPY supervisord.conf /etc/supervisord.conf
5656

57+
# Set environment variables with default values that can be overridden
58+
ENV BRAND_TITLE="GoShort - URL Shortener" \
59+
BRAND_DESCRIPTION="GoShort is a powerful and user-friendly URL shortener. Simplify, manage, and track your links with ease." \
60+
BRAND_KEYWORDS="URL shortener, GoShort, link management, shorten URLs, track links" \
61+
BRAND_AUTHOR="GoShort Team" \
62+
BRAND_THEME_COLOR="#4caf50" \
63+
BRAND_LOGO_TEXT="GoShort" \
64+
BRAND_PRIMARY_COLOR="#3b82f6" \
65+
BRAND_SECONDARY_COLOR="#10b981" \
66+
BRAND_HEADER_TITLE="GoShort - URL Shortener" \
67+
BRAND_FOOTER_TEXT="View the project on" \
68+
BRAND_FOOTER_LINK="https://github.com/kek-Sec/GoShort"
69+
5770
# Expose ports
5871
EXPOSE 80 8080
5972

README.md

Lines changed: 52 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
### Demo: [https://x.yup.gr](http://x.yup.gr)
88
### DockerHub Image: [petrakisg/goshort](https://hub.docker.com/r/petrakisg/goshort)
99

10-
GoShort is a fast and customizable URL shortener built with Go , Svelte and TailwindCSS. It is designed to be self-hosted and easy to deploy.
10+
GoShort is a fast and customizable URL shortener built with Go, Svelte and TailwindCSS. It is designed to be self-hosted and easy to deploy.
1111

1212
![GoShort](web/static/banner.png)
1313

@@ -17,9 +17,10 @@ GoShort is a fast and customizable URL shortener built with Go , Svelte and Tail
1717

1818
1. [Features](#-features)
1919
2. [Installation](#-installation)
20-
3. [Contributing](#-contributing)
21-
4. [License](#-license)
22-
5. [Security](#-security)
20+
3. [Customization](#-customization)
21+
4. [Contributing](#-contributing)
22+
5. [License](#-license)
23+
6. [Security](#-security)
2324
---
2425

2526
## 🚀 **Features**
@@ -29,6 +30,7 @@ GoShort is a fast and customizable URL shortener built with Go , Svelte and Tail
2930
- **Self-hosted**: You own your data and can deploy GoShort on your own server.
3031
- **Custom URLs**: You can set custom URLs for your short links.
3132
- **Expiration**: You can set expiration for your short links.
33+
- **White-labeling**: Customize branding elements without rebuilding the Docker image.
3234

3335
---
3436

@@ -38,6 +40,52 @@ GoShort is a fast and customizable URL shortener built with Go , Svelte and Tail
3840
3941
---
4042

43+
## 🎨 **Customization**
44+
45+
GoShort supports customizing the branding and appearance through environment variables, making it easy to white-label without rebuilding the Docker image.
46+
47+
### Available Customization Options
48+
49+
You can customize the following aspects of the UI by setting these environment variables:
50+
51+
| Environment Variable | Description | Default Value |
52+
|---------------------|-------------|---------------|
53+
| BRAND_TITLE | Browser tab title | GoShort - URL Shortener |
54+
| BRAND_DESCRIPTION | Meta description for SEO | GoShort is a powerful and user-friendly URL shortener... |
55+
| BRAND_KEYWORDS | Meta keywords for SEO | URL shortener, GoShort, link management... |
56+
| BRAND_AUTHOR | Author meta tag | GoShort Team |
57+
| BRAND_THEME_COLOR | Browser theme color | #4caf50 |
58+
| BRAND_LOGO_TEXT | Text logo displayed in the header | GoShort |
59+
| BRAND_PRIMARY_COLOR | Main accent color (buttons, links) | #3b82f6 |
60+
| BRAND_SECONDARY_COLOR | Secondary accent color | #10b981 |
61+
| BRAND_HEADER_TITLE | Main heading on the page | GoShort - URL Shortener |
62+
| BRAND_FOOTER_TEXT | Text shown in the footer | View the project on |
63+
| BRAND_FOOTER_LINK | URL for the footer link | https://github.com/kek-Sec/GoShort |
64+
65+
### Usage Example
66+
67+
Here's how to customize the branding in your docker-compose file:
68+
69+
```yaml
70+
services:
71+
goshort:
72+
image: petrakisg/goshort:1.0.1
73+
environment:
74+
# Database configuration
75+
DATABASE_URL: postgres://user:password@db:5432/goshort
76+
77+
# Branding customization
78+
BRAND_TITLE: "MyCompany URL Shortener"
79+
BRAND_LOGO_TEXT: "MyShort"
80+
BRAND_PRIMARY_COLOR: "#ff5722"
81+
BRAND_SECONDARY_COLOR: "#2196f3"
82+
BRAND_HEADER_TITLE: "MyCompany Link Shortener"
83+
BRAND_FOOTER_TEXT: "Powered by"
84+
BRAND_FOOTER_LINK: "https://mycompany.com"
85+
```
86+
87+
---
88+
4189
## 🤝 **Contributing**
4290
4391
1. Fork the repository.

cmd/server/router.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
package main
22

33
import (
4-
"GoShort/internal/api/v1"
4+
v1 "GoShort/internal/api/v1"
5+
56
"github.com/gorilla/mux"
67
)
78

@@ -12,6 +13,7 @@ func setupRouter() *mux.Router {
1213
// V1 Routes
1314
apiV1 := router.PathPrefix("/v1").Subrouter()
1415
apiV1.HandleFunc("/shorten", v1.ShortenURL).Methods("POST")
16+
apiV1.HandleFunc("/config", v1.GetConfig).Methods("GET") // Add config endpoint
1517

1618
// Redirect Route (catch-all)
1719
router.HandleFunc("/{shortURL}", v1.RedirectURL).Methods("GET")

docker-compose.prod.yml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,17 @@ services:
2121
container_name: goshort_app
2222
environment:
2323
DATABASE_URL: postgres://goshort:goshort_password@goshort-db:5432/goshort?sslmode=disable
24+
# Branding customization (uncomment and modify as needed)
25+
# BRAND_TITLE: "MyShort - URL Shortener"
26+
# BRAND_DESCRIPTION: "A fast and customizable URL shortener for your organization"
27+
# BRAND_AUTHOR: "Your Company Name"
28+
# BRAND_THEME_COLOR: "#2563eb"
29+
# BRAND_LOGO_TEXT: "MyShort"
30+
# BRAND_PRIMARY_COLOR: "#2563eb"
31+
# BRAND_SECONDARY_COLOR: "#10b981"
32+
# BRAND_HEADER_TITLE: "MyShort - Simplify Your URLs"
33+
# BRAND_FOOTER_TEXT: "Powered by"
34+
# BRAND_FOOTER_LINK: "https://yourcompany.com"
2435
depends_on:
2536
goshort-db:
2637
condition: service_healthy

docker-compose.yaml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,13 @@ services:
88
- "8081:80" # Frontend
99
environment:
1010
DATABASE_URL: postgres://goshort:goshort_password@database:5432/goshort?sslmode=disable
11+
# Branding customization (optional)
12+
BRAND_TITLE: "GoShort - URL Shortener"
13+
BRAND_DESCRIPTION: "A fast and customizable URL shortener"
14+
BRAND_THEME_COLOR: "#4caf50"
15+
BRAND_PRIMARY_COLOR: "#3b82f6"
16+
BRAND_SECONDARY_COLOR: "#10b981"
17+
BRAND_HEADER_TITLE: "GoShort - URL Shortener"
1118
depends_on:
1219
database:
1320
condition: service_healthy

internal/api/v1/config.go

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
package v1
2+
3+
import (
4+
"encoding/json"
5+
"net/http"
6+
"os"
7+
)
8+
9+
// Config represents the frontend configuration
10+
type Config struct {
11+
Title string `json:"title,omitempty"`
12+
Description string `json:"description,omitempty"`
13+
Keywords string `json:"keywords,omitempty"`
14+
Author string `json:"author,omitempty"`
15+
ThemeColor string `json:"themeColor,omitempty"`
16+
LogoText string `json:"logoText,omitempty"`
17+
PrimaryColor string `json:"primaryColor,omitempty"`
18+
SecondaryColor string `json:"secondaryColor,omitempty"`
19+
HeaderTitle string `json:"headerTitle,omitempty"`
20+
FooterText string `json:"footerText,omitempty"`
21+
FooterLink string `json:"footerLink,omitempty"`
22+
}
23+
24+
// GetConfig returns the frontend configuration
25+
func GetConfig(w http.ResponseWriter, r *http.Request) {
26+
config := Config{}
27+
28+
// Load config from environment variables
29+
envVars := map[string]*string{
30+
"BRAND_TITLE": &config.Title,
31+
"BRAND_DESCRIPTION": &config.Description,
32+
"BRAND_KEYWORDS": &config.Keywords,
33+
"BRAND_AUTHOR": &config.Author,
34+
"BRAND_THEME_COLOR": &config.ThemeColor,
35+
"BRAND_LOGO_TEXT": &config.LogoText,
36+
"BRAND_PRIMARY_COLOR": &config.PrimaryColor,
37+
"BRAND_SECONDARY_COLOR": &config.SecondaryColor,
38+
"BRAND_HEADER_TITLE": &config.HeaderTitle,
39+
"BRAND_FOOTER_TEXT": &config.FooterText,
40+
"BRAND_FOOTER_LINK": &config.FooterLink,
41+
}
42+
43+
for envVar, field := range envVars {
44+
if value := os.Getenv(envVar); value != "" {
45+
*field = value
46+
}
47+
}
48+
49+
// Set response headers
50+
w.Header().Set("Content-Type", "application/json")
51+
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate") // Prevent caching
52+
53+
// Encode config as JSON and send response with error handling
54+
if err := json.NewEncoder(w).Encode(config); err != nil {
55+
http.Error(w, "Failed to encode response", http.StatusInternalServerError)
56+
}
57+
}

internal/api/v1/config_test.go

Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
package v1
2+
3+
import (
4+
"encoding/json"
5+
"net/http"
6+
"net/http/httptest"
7+
"os"
8+
"testing"
9+
10+
"github.com/stretchr/testify/assert"
11+
)
12+
13+
func TestGetConfig(t *testing.T) {
14+
// Test cases
15+
tests := []struct {
16+
name string
17+
envVars map[string]string
18+
expectedKey string
19+
expectedVal string
20+
}{
21+
{
22+
name: "Default Config Returns Empty When No Env Vars",
23+
// No env vars set
24+
expectedKey: "",
25+
expectedVal: "",
26+
},
27+
{
28+
name: "Returns Title From Environment",
29+
envVars: map[string]string{
30+
"BRAND_TITLE": "Custom Title",
31+
},
32+
expectedKey: "title",
33+
expectedVal: "Custom Title",
34+
},
35+
{
36+
name: "Returns Primary Color From Environment",
37+
envVars: map[string]string{
38+
"BRAND_PRIMARY_COLOR": "#ff0000",
39+
},
40+
expectedKey: "primaryColor",
41+
expectedVal: "#ff0000",
42+
},
43+
{
44+
name: "Returns Multiple Config Values",
45+
envVars: map[string]string{
46+
"BRAND_TITLE": "Custom Title",
47+
"BRAND_DESCRIPTION": "Custom Description",
48+
"BRAND_PRIMARY_COLOR": "#ff0000",
49+
"BRAND_HEADER_TITLE": "Custom Header",
50+
"BRAND_SECONDARY_COLOR": "#00ff00",
51+
},
52+
expectedKey: "headerTitle", // We'll check just this one
53+
expectedVal: "Custom Header",
54+
},
55+
}
56+
57+
// Run tests
58+
for _, tt := range tests {
59+
t.Run(tt.name, func(t *testing.T) {
60+
// Clear environment variables
61+
os.Clearenv()
62+
63+
// Set environment variables for this test
64+
for k, v := range tt.envVars {
65+
os.Setenv(k, v)
66+
}
67+
68+
// Create request
69+
req, err := http.NewRequest("GET", "/v1/config", nil)
70+
if err != nil {
71+
t.Fatal(err)
72+
}
73+
74+
// Create response recorder
75+
rr := httptest.NewRecorder()
76+
handler := http.HandlerFunc(GetConfig)
77+
78+
// Serve request
79+
handler.ServeHTTP(rr, req)
80+
81+
// Check status code
82+
assert.Equal(t, http.StatusOK, rr.Code)
83+
84+
// Check Content-Type
85+
assert.Equal(t, "application/json", rr.Header().Get("Content-Type"))
86+
assert.Equal(t, "no-cache, no-store, must-revalidate", rr.Header().Get("Cache-Control"))
87+
88+
// If we're not expecting any specific value, just verify it's valid JSON
89+
if tt.expectedKey == "" {
90+
var result map[string]interface{}
91+
err = json.Unmarshal(rr.Body.Bytes(), &result)
92+
assert.NoError(t, err, "Response should be valid JSON")
93+
return
94+
}
95+
96+
// Parse the response
97+
var result Config
98+
err = json.Unmarshal(rr.Body.Bytes(), &result)
99+
assert.NoError(t, err)
100+
101+
// Check the specific field we're testing for
102+
switch tt.expectedKey {
103+
case "title":
104+
assert.Equal(t, tt.expectedVal, result.Title)
105+
case "description":
106+
assert.Equal(t, tt.expectedVal, result.Description)
107+
case "keywords":
108+
assert.Equal(t, tt.expectedVal, result.Keywords)
109+
case "author":
110+
assert.Equal(t, tt.expectedVal, result.Author)
111+
case "themeColor":
112+
assert.Equal(t, tt.expectedVal, result.ThemeColor)
113+
case "logoText":
114+
assert.Equal(t, tt.expectedVal, result.LogoText)
115+
case "primaryColor":
116+
assert.Equal(t, tt.expectedVal, result.PrimaryColor)
117+
case "secondaryColor":
118+
assert.Equal(t, tt.expectedVal, result.SecondaryColor)
119+
case "headerTitle":
120+
assert.Equal(t, tt.expectedVal, result.HeaderTitle)
121+
case "footerText":
122+
assert.Equal(t, tt.expectedVal, result.FooterText)
123+
case "footerLink":
124+
assert.Equal(t, tt.expectedVal, result.FooterLink)
125+
}
126+
})
127+
}
128+
}
129+
130+
func TestGetConfigMultipleValues(t *testing.T) {
131+
// Clear environment variables
132+
os.Clearenv()
133+
134+
// Set multiple environment variables
135+
os.Setenv("BRAND_TITLE", "Test Title")
136+
os.Setenv("BRAND_PRIMARY_COLOR", "#ff0000")
137+
os.Setenv("BRAND_SECONDARY_COLOR", "#00ff00")
138+
os.Setenv("BRAND_FOOTER_TEXT", "Custom Footer")
139+
140+
// Create request
141+
req, err := http.NewRequest("GET", "/v1/config", nil)
142+
if err != nil {
143+
t.Fatal(err)
144+
}
145+
146+
// Create response recorder
147+
rr := httptest.NewRecorder()
148+
handler := http.HandlerFunc(GetConfig)
149+
150+
// Serve request
151+
handler.ServeHTTP(rr, req)
152+
153+
// Check status code
154+
assert.Equal(t, http.StatusOK, rr.Code)
155+
156+
// Parse the response
157+
var result Config
158+
err = json.Unmarshal(rr.Body.Bytes(), &result)
159+
assert.NoError(t, err)
160+
161+
// Check all expected values
162+
assert.Equal(t, "Test Title", result.Title)
163+
assert.Equal(t, "#ff0000", result.PrimaryColor)
164+
assert.Equal(t, "#00ff00", result.SecondaryColor)
165+
assert.Equal(t, "Custom Footer", result.FooterText)
166+
167+
// Check that others are empty
168+
assert.Empty(t, result.Description)
169+
assert.Empty(t, result.Keywords)
170+
}

package-lock.json

Lines changed: 6 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

version.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
11
#Application version following https://semver.org/
2-
version: 1.0.1
2+
version: 1.0.2

0 commit comments

Comments
 (0)