Make configs json and have light/dark images for socials
All checks were successful
Build and Deploy / build (push) Successful in 40s

This commit is contained in:
Justin Deal 2025-05-04 16:59:10 -07:00
parent e56520c9e8
commit a80b2a5e01
14 changed files with 448 additions and 256 deletions

1
.astro/types.d.ts vendored
View File

@ -1,2 +1 @@
/// <reference types="astro/client" />
/// <reference path="content.d.ts" />

56
config/README.md Normal file
View File

@ -0,0 +1,56 @@
# Site Configuration
This directory contains configuration files for the website. Instead of hardcoding values in the TypeScript files, these JSON files are used to make the site more configurable.
## Available Configuration Files
- **site.json**: Contains global site metadata, menu structure, and text strings
- **services.json**: Contains homelab services organized by category
- **socials.json**: Contains social media profile configurations (the single source of truth for all social profiles)
## How to Use
To modify any configuration values, simply edit the appropriate JSON file. The changes will be reflected in the application without having to modify any TypeScript code.
### Example: Updating Menu Items
To add or remove a menu item, edit the `menu` section in `site.json`:
```json
"menu": {
"home": "/",
"about": "/about",
"blog": "/blog",
"projects": "/projects",
"homelab": "/homelab",
"code": "https://code.justin.deal",
"new-page": "/new-page"
}
```
### Example: Adding a New Service
To add a new service, find the appropriate category in `services.json` and add a new item:
```json
"Media": [
{
"name": "Jellyfin",
"link": "https://watch.justin.deal",
"icon": "https://cdn.jsdelivr.net/gh/selfhst/icons/svg/jellyfin.svg",
"alt": "Jellyfin",
"tags": []
},
{
"name": "Plex",
"link": "https://plex.justin.deal",
"icon": "https://cdn.jsdelivr.net/gh/selfhst/icons/svg/plex.svg",
"alt": "Plex Media Server",
"tags": ["media", "streaming"]
}
]
```
## Technical Implementation
The configuration files are loaded from this directory using the `loadConfig` function in `src/lib/config.ts`. The function reads the JSON files, parses them, and caches the results to avoid reading the files multiple times.

153
config/services.json Normal file
View File

@ -0,0 +1,153 @@
{
"Websites": [
{
"name": "justin.deal",
"link": "https://justin.deal",
"icon": "/pixel_avatar.png",
"alt": "Personal Website"
}
],
"Utilities": [
{
"name": "Silverbullet",
"link": "https://notes.justin.deal",
"icon": "https://cdn.jsdelivr.net/gh/selfhst/icons/png/silverbullet.png",
"alt": "Silverbullet",
"tags": ["notes", "markdown", "knowledge base"]
},
{
"name": "Vikunja",
"link": "https://todo.justin.deal",
"icon": "https://cdn.jsdelivr.net/gh/selfhst/icons/svg/vikunja.svg",
"alt": "Vikunja",
"tags": ["todo", "tasks", "productivity"]
},
{
"name": "Actual",
"link": "https://budget.justin.deal",
"icon": "https://cdn.jsdelivr.net/gh/selfhst/icons/svg/actual-budget.svg",
"alt": "Actual",
"tags": ["finance", "budget", "money"]
},
{
"name": "Searxng",
"link": "https://search.justin.deal",
"icon": "https://cdn.jsdelivr.net/gh/selfhst/icons/svg/searxng.svg",
"alt": "Searxng",
"tags": ["search", "privacy", "metasearch"]
},
{
"name": "BaiKal",
"link": "https://dav.justin.deal",
"icon": "https://cdn.jsdelivr.net/gh/selfhst/icons/png/baikal.png",
"alt": "BaiKal",
"tags": []
},
{
"name": "Cryptpad",
"link": "https://docs.justin.deal",
"icon": "https://cdn.jsdelivr.net/gh/selfhst/icons/svg/cryptpad.svg",
"alt": "Cryptpad",
"tags": []
}
],
"Development": [
{
"name": "Gitea",
"link": "https://code.justin.deal",
"icon": "https://cdn.jsdelivr.net/gh/selfhst/icons/svg/gitea.svg",
"alt": "Gitea",
"tags": []
},
{
"name": "OpenGist",
"link": "https://snippets.justin.deal",
"icon": "https://cdn.jsdelivr.net/gh/selfhst/icons/svg/opengist.svg",
"alt": "OpenGist",
"tags": []
},
{
"name": "IT-Tools",
"link": "https://tools.justin.deal",
"icon": "https://cdn.jsdelivr.net/gh/selfhst/icons/svg/it-tools.svg",
"alt": "IT-Tools",
"tags": []
}
],
"Media": [
{
"name": "Jellyfin",
"link": "https://watch.justin.deal",
"icon": "https://cdn.jsdelivr.net/gh/selfhst/icons/svg/jellyfin.svg",
"alt": "Jellyfin",
"tags": []
},
{
"name": "Calibre-Web",
"link": "https://books.justin.deal",
"icon": "https://cdn.jsdelivr.net/gh/selfhst/icons/svg/calibre-web.svg",
"alt": "Calibre-Web",
"tags": []
}
],
"Analytics": [
{
"name": "Uptime Kuma",
"link": "https://status.justin.deal",
"icon": "https://cdn.jsdelivr.net/gh/selfhst/icons/svg/uptime-kuma.svg",
"alt": "Uptime Kuma",
"tags": []
},
{
"name": "Umami",
"link": "https://analytics.justin.deal",
"icon": "https://cdn.jsdelivr.net/gh/selfhst/icons/svg/umami.svg",
"alt": "Umami",
"tags": []
},
{
"name": "TeslaMate",
"link": "https://tesla.justin.deal",
"icon": "https://cdn.jsdelivr.net/gh/selfhst/icons/svg/teslamate.svg",
"alt": "TeslaMate",
"tags": []
}
],
"Infrastructure": [
{
"name": "Pi-hole",
"link": "https://pihole.justin.deal",
"icon": "https://cdn.jsdelivr.net/gh/selfhst/icons/svg/pi-hole.svg",
"alt": "Pi-hole",
"tags": []
},
{
"name": "Ntfy",
"link": "https://ntfy.justin.deal",
"icon": "https://cdn.jsdelivr.net/gh/selfhst/icons/svg/ntfy.svg",
"alt": "Ntfy",
"tags": []
},
{
"name": "Vaultwarden",
"link": "https://passwords.justin.deal",
"icon": "https://cdn.jsdelivr.net/gh/selfhst/icons/svg/vaultwarden.svg",
"alt": "Vaultwarden",
"tags": []
},
{
"name": "Authentik",
"link": "https://auth.justin.deal",
"icon": "https://cdn.jsdelivr.net/gh/selfhst/icons/svg/authentik.svg",
"alt": "Authentik",
"tags": []
},
{
"name": "Traefik",
"link": "https://proxy.justin.deal:8080",
"icon": "https://cdn.jsdelivr.net/gh/selfhst/icons/svg/traefik.svg",
"alt": "Traefik",
"tags": []
}
]
}

32
config/site.json Normal file
View File

@ -0,0 +1,32 @@
{
"username": "Justin Deal",
"rootUrl": "https://justin.deal",
"shortDescription": "My personal slice of the internet",
"longDescription": "My personal blog, portfolio, and homelab dashboard built with Astro, TypeScript, TailwindCSS, and Alpine.js.",
"articlesName": "Articles",
"projectsName": "Projects",
"viewAll": "View All",
"noArticles": "No featured articles yet.",
"noProjects": "No featured projects yet.",
"blogTitle": "My Thoughts & Takes",
"blogShortDescription": "Practical wisdom, unfiltered thoughts, and hot takes.",
"blogLongDescription": "Web development, tech trends, and the occasional programming mishap.",
"projectTitle": "Projects and Code",
"projectShortDescription": "A list of my web development projects and developer tools.",
"projectLongDescription": "All of my projects, including both frontend and full-stack applications.",
"profileImage": "pixel_avatar.png",
"menu": {
"home": "/",
"about": "/about",
"blog": "/blog",
"projects": "/projects",
"homelab": "/homelab",
"code": "https://code.justin.deal"
}
}

29
config/socials.json Normal file
View File

@ -0,0 +1,29 @@
[
{
"name": "Gitea",
"url": "https://code.justin.deal/dealjus",
"iconDark": "https://cdn.jsdelivr.net/gh/selfhst/icons/svg/gitea-dark.svg",
"iconLight": "https://cdn.jsdelivr.net/gh/selfhst/icons/svg/gitea-light.svg",
"alt": "Gitea Profile",
"showInFooter": true,
"showInAbout": true
},
{
"name": "LinkedIn",
"url": "https://www.linkedin.com/in/justin-deal/",
"iconDark": "https://cdn.jsdelivr.net/gh/selfhst/icons/svg/linkedin-dark.svg",
"iconLight": "https://cdn.jsdelivr.net/gh/selfhst/icons/svg/linkedin-light.svg",
"alt": "LinkedIn Profile",
"showInFooter": true,
"showInAbout": true
},
{
"name": "GitHub",
"url": "https://github.com/justintdeal",
"iconDark": "https://cdn.jsdelivr.net/gh/selfhst/icons/svg/github-dark.svg",
"iconLight": "https://cdn.jsdelivr.net/gh/selfhst/icons/svg/github-light.svg",
"alt": "GitHub Profile",
"showInFooter": true,
"showInAbout": true
}
]

View File

@ -20,7 +20,8 @@ const footerSocials = socials.filter(social => social.showInFooter);
<SocialIcon
name={social.name}
url={social.url}
icon={social.icon}
iconDark={social.iconDark}
iconLight={social.iconLight}
alt={social.alt}
/>
))}

View File

@ -4,12 +4,18 @@ import Anchor from "./Anchor.astro";
export interface Props {
name: string;
url: string;
icon: string;
iconDark?: string;
iconLight?: string;
icon?: string; // For backward compatibility
alt: string;
size?: "sm" | "md" | "lg";
}
const { name, url, icon, alt, size = "md" } = Astro.props;
const { name, url, iconDark, iconLight, icon, alt, size = "md" } = Astro.props;
// Use provided icons or fallback to the legacy icon prop
const darkIcon = iconDark || icon;
const lightIcon = iconLight || icon;
const sizeClasses = {
sm: "w-6 h-6",
@ -19,9 +25,28 @@ const sizeClasses = {
---
<Anchor url={url} aria-label={alt}>
<!-- Dark icon shown in light theme -->
<img
src={icon}
src={darkIcon}
alt={alt}
class={`${sizeClasses[size]} zag-transition`}
class={`${sizeClasses[size]} light-theme-only zag-transition`}
/>
<!-- Light icon shown in dark theme -->
<img
src={lightIcon}
alt={alt}
class={`${sizeClasses[size]} dark-theme-only zag-transition`}
/>
</Anchor>
<style>
/* Hide dark icon in dark theme */
:global(.dark-theme) .light-theme-only {
display: none;
}
/* Hide light icon in light theme */
:global(.light-theme) .dark-theme-only {
display: none;
}
</style>

97
src/lib/config.ts Normal file
View File

@ -0,0 +1,97 @@
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
// Get the directory of the current module
const __dirname = path.dirname(fileURLToPath(import.meta.url));
// Path to the config directory (two levels up from lib directory)
const configDir = path.resolve(__dirname, '../../config');
/**
* Cache to avoid loading config files multiple times
*/
const configCache = new Map<string, any>();
/**
* Load and parse a JSON configuration file
* @param name The name of the config file without extension
* @returns The parsed configuration object
*/
export function loadConfig<T>(name: string): T {
// If the config is already in cache, return it
if (configCache.has(name)) {
return configCache.get(name) as T;
}
try {
// Load the config file
const configPath = path.join(configDir, `${name}.json`);
const configData = fs.readFileSync(configPath, 'utf-8');
const config = JSON.parse(configData);
// Cache the result
configCache.set(name, config);
return config as T;
} catch (error) {
console.error(`Error loading config '${name}':`, error);
throw new Error(`Failed to load config: ${name}`);
}
}
/**
* Site configuration type
*/
export interface SiteConfig {
username: string;
rootUrl: string;
shortDescription: string;
longDescription: string;
articlesName: string;
projectsName: string;
viewAll: string;
noArticles: string;
noProjects: string;
blogTitle: string;
blogShortDescription: string;
blogLongDescription: string;
projectTitle: string;
projectShortDescription: string;
projectLongDescription: string;
profileImage: string;
menu: Record<string, string>;
}
/**
* Service category type (from types.ts)
*/
export type ServiceCategory = Record<string, Service[]>;
/**
* Service type (based on types.ts)
*/
export interface Service {
name: string;
link: string;
icon: string;
alt: string;
tags?: string[];
}
/**
* Social media type (based on types.ts)
*/
export interface SocialMedia {
name: string;
url: string;
iconDark: string;
iconLight: string;
alt: string;
showInFooter: boolean;
showInAbout: boolean;
}
// Convenience exports for commonly used configs
export const site = loadConfig<SiteConfig>('site');
export const services = loadConfig<ServiceCategory>('services');
export const socials = loadConfig<SocialMedia[]>('socials');

View File

@ -1,31 +1,12 @@
import { type SocialMedia } from "./types";
import { socials as socialsConfig } from "./config";
/**
* Social media profiles
* This is now loaded from a JSON configuration file
* located at /config/socials.json
*
* To modify social profiles, edit that JSON file rather than
* modifying the values here.
*/
export const socials: SocialMedia[] = [
{
name: "Gitea",
url: "https://code.justin.deal/dealjus",
icon: "https://cdn.jsdelivr.net/gh/selfhst/icons/svg/gitea-dark.svg",
alt: "Gitea Profile",
showInFooter: true,
showInAbout: true
},
{
name: "LinkedIn",
url: "https://www.linkedin.com/in/justin-deal/",
icon: "https://cdn.jsdelivr.net/gh/selfhst/icons/svg/linkedin-dark.svg",
alt: "LinkedIn Profile",
showInFooter: true,
showInAbout: true
},
{
name: "GitHub",
url: "https://github.com/justintdeal",
icon: "https://cdn.jsdelivr.net/gh/selfhst/icons/svg/github-dark.svg",
alt: "GitHub Profile",
showInFooter: true,
showInAbout: true
}
];
export const socials: SocialMedia[] = socialsConfig;

View File

@ -140,9 +140,16 @@ export type SocialMedia = {
url: string;
/**
* The URL to the icon from selfh.st/icons
* The URL to the dark version of the icon from selfh.st/icons
* Used in light theme
*/
icon: string;
iconDark: string;
/**
* The URL to the light version of the icon from selfh.st/icons
* Used in dark theme
*/
iconLight: string;
/**
* Alternative text for the icon

View File

@ -1,72 +1,12 @@
// Set any item to undefined to remove it from the site or to use the default value
/**
* Global variables used throughout the site
* @property {string} username - The username displayed on the site
* @property {string} rootUrl - The root URL of the site
* @property {string} shortDescription - A short description of the site
* @property {string} longDescription - A longer description of the site
* @property {string} articlesName - The name used for articles
* @property {string} projectsName - The name used for projects
* @property {string} viewAll - The text used for "View All" links
* @property {string} noArticles - The text used when there are no articles
* @property {string} noProjects - The text used when there are no projects
* @property {string} blogTitle - The title of the blog section
* @property {string} blogShortDescription - A short description of the blog
* @property {string} blogLongDescription - A longer description of the blog
* @property {string} projectTitle - The title of the projects section
* @property {string} projectShortDescription - A short description of the projects
* @property {string} projectLongDescription - A longer description of the projects
* @property {string} profileImage - The profile image filename
* @property {string} githubProfile - The URL to the GitHub profile
* @property {string} linkedinProfile - The URL to the LinkedIn profile
* @property {string} giteaProfile - The URL to the Gitea profile
* @property {Object} menu - The menu items
* This is now loaded from a JSON configuration file
* located at /config/site.json
*
* To modify site configuration, edit that JSON file
* rather than modifying the values here.
*/
export const GLOBAL = {
// Site metadata
username: "Justin Deal",
rootUrl: "https://justin.deal",
shortDescription: "My personal slice of the internet",
longDescription: "My personal blog, portfolio, and homelab dashboard built with Astro, TypeScript, TailwindCSS, and Alpine.js.",
import { site } from './config';
// Common text names used throughout the site
articlesName: "Articles",
projectsName: "Projects",
viewAll: "View All",
// Common descriptions used throughout the site
noArticles: "No featured articles yet.",
noProjects: "No featured projects yet.",
// Blog metadata
blogTitle: "My Thoughts & Takes",
blogShortDescription: "Practical wisdom, unfiltered thoughts, and hot takes.",
blogLongDescription: "Web development, tech trends, and the occasional programming mishap.",
// Project metadata
projectTitle: "Projects and Code",
projectShortDescription: "A list of my web development projects and developer tools.",
projectLongDescription: "All of my projects, including both frontend and full-stack applications.",
// Profile image
profileImage: "pixel_avatar.png",
// Social media profiles
githubProfile: "https://github.com/justindeal",
linkedinProfile: "https://www.linkedin.com/in/justin-deal/",
giteaProfile: "https://code.justin.deal/dealjus",
// Menu items
menu: {
home: "/",
about: "/about",
blog: "/blog",
projects: "/projects",
homelab: "/homelab",
code: "https://code.justin.deal",
// videos: "https://www.youtube.com/@justin_deal",
// homelab: "https://homelab.justin.deal",
// contact: "/contact",
}
};
// Re-export site configuration as GLOBAL for backward compatibility
export const GLOBAL = site;

View File

@ -299,7 +299,10 @@ const aboutSocials = socials.filter(social => social.showInAbout);
<div class="flex flex-wrap gap-4">
{aboutSocials.map((social) => (
<a href={social.url} target="_blank" rel="noopener noreferrer" class="flex items-center gap-2 zag-bg-alt p-4 rounded-lg shadow-sm hover:zag-bg-accent-light transition-colors">
<img src={social.icon} alt={social.alt} class="w-6 h-6 zag-text" />
<!-- Dark icon for light theme -->
<img src={social.iconDark} alt={social.alt} class="w-6 h-6 zag-text light-theme-only" />
<!-- Light icon for dark theme -->
<img src={social.iconLight} alt={social.alt} class="w-6 h-6 zag-text dark-theme-only" />
<span>{social.name}</span>
</a>
))}

View File

@ -1,158 +1,12 @@
import { type Service, type ServiceCategory } from "../../lib/types";
import { services as servicesConfig } from "../../lib/config";
/**
* Services available in the homelab, organized by category
* This is now loaded from a JSON configuration file
* located at /config/services.json
*
* To modify services, edit that JSON file rather than
* modifying the values here.
*/
export const services: ServiceCategory = {
Websites: [
{
name: "justin.deal",
link: "https://justin.deal",
icon: "/pixel_avatar.png",
alt: "Personal Website"
}
],
Utilities: [
{
name: "Silverbullet",
link: "https://notes.justin.deal",
icon: "https://cdn.jsdelivr.net/gh/selfhst/icons/png/silverbullet.png",
alt: "Silverbullet",
tags: ["notes", "markdown", "knowledge base"]
},
{
name: "Vikunja",
link: "https://todo.justin.deal",
icon: "https://cdn.jsdelivr.net/gh/selfhst/icons/svg/vikunja.svg",
alt: "Vikunja",
tags: ["todo", "tasks", "productivity"]
},
{
name: "Actual",
link: "https://budget.justin.deal",
icon: "https://cdn.jsdelivr.net/gh/selfhst/icons/svg/actual-budget.svg",
alt: "Actual",
tags: ["finance", "budget", "money"]
},
{
name: "Searxng",
link: "https://search.justin.deal",
icon: "https://cdn.jsdelivr.net/gh/selfhst/icons/svg/searxng.svg",
alt: "Searxng",
tags: ["search", "privacy", "metasearch"]
},
{
name: "BaiKal",
link: "https://dav.justin.deal",
icon: "https://cdn.jsdelivr.net/gh/selfhst/icons/png/baikal.png",
alt: "BaiKal",
tags: []
},
{
name: "Cryptpad",
link: "https://docs.justin.deal",
icon: "https://cdn.jsdelivr.net/gh/selfhst/icons/svg/cryptpad.svg",
alt: "Cryptpad",
tags: []
}
],
Development: [
{
name: "Gitea",
link: "https://code.justin.deal",
icon: "https://cdn.jsdelivr.net/gh/selfhst/icons/svg/gitea.svg",
alt: "Gitea",
tags: []
},
{
name: "OpenGist",
link: "https://snippets.justin.deal",
icon: "https://cdn.jsdelivr.net/gh/selfhst/icons/svg/opengist.svg",
alt: "OpenGist",
tags: []
},
{
name: "IT-Tools",
link: "https://tools.justin.deal",
icon: "https://cdn.jsdelivr.net/gh/selfhst/icons/svg/it-tools.svg",
alt: "IT-Tools",
tags: []
}
],
Media: [
{
name: "Jellyfin",
link: "https://watch.justin.deal",
icon: "https://cdn.jsdelivr.net/gh/selfhst/icons/svg/jellyfin.svg",
alt: "Jellyfin",
tags: []
},
{
name: "Calibre-Web",
link: "https://books.justin.deal",
icon: "https://cdn.jsdelivr.net/gh/selfhst/icons/svg/calibre-web.svg",
alt: "Calibre-Web",
tags: []
}
],
Analytics: [
{
name: "Uptime Kuma",
link: "https://status.justin.deal",
icon: "https://cdn.jsdelivr.net/gh/selfhst/icons/svg/uptime-kuma.svg",
alt: "Uptime Kuma",
tags: []
},
{
name: "Umami",
link: "https://analytics.justin.deal",
icon: "https://cdn.jsdelivr.net/gh/selfhst/icons/svg/umami.svg",
alt: "Umami",
tags: []
},
{
name: "TeslaMate",
link: "https://tesla.justin.deal",
icon: "https://cdn.jsdelivr.net/gh/selfhst/icons/svg/teslamate.svg",
alt: "TeslaMate",
tags: []
},
],
Infrastructure: [
{
name: "Pi-hole",
link: "https://pihole.justin.deal",
icon: "https://cdn.jsdelivr.net/gh/selfhst/icons/svg/pi-hole.svg",
alt: "Pi-hole",
tags: []
},
{
name: "Ntfy",
link: "https://ntfy.justin.deal",
icon: "https://cdn.jsdelivr.net/gh/selfhst/icons/svg/ntfy.svg",
alt: "Ntfy",
tags: []
},
{
name: "Vaultwarden",
link: "https://passwords.justin.deal",
icon: "https://cdn.jsdelivr.net/gh/selfhst/icons/svg/vaultwarden.svg",
alt: "Vaultwarden",
tags: []
},
{
name: "Authentik",
link: "https://auth.justin.deal",
icon: "https://cdn.jsdelivr.net/gh/selfhst/icons/svg/authentik.svg",
alt: "Authentik",
tags: []
},
{
name: "Traefik",
link: "https://proxy.justin.deal:8080",
icon: "https://cdn.jsdelivr.net/gh/selfhst/icons/svg/traefik.svg",
alt: "Traefik",
tags: []
}
]
};
export const services: ServiceCategory = servicesConfig;

View File

@ -394,6 +394,21 @@
}
@layer utilities {
/* Theme-specific visibility utilities */
.light-theme-only {
display: block;
:where(.dark, .dark *) & {
display: none;
}
}
.dark-theme-only {
display: none;
:where(.dark, .dark *) & {
display: block;
}
}
/* Base backgrounds and text */
.zag-bg {
background-color: var(--color-zag-light);