+ * ```
+ */
+interface Props {
+ /**
+ * The name of the service to display
+ */
+ name: string;
+
+ /**
+ * The URL to link to when the service card is clicked
+ */
+ href: string;
+
+ /**
+ * The URL of the service icon
+ */
+ img: string;
+
+ /**
+ * Alternative text for the service icon
+ */
+ alt: string;
+}
+
const { name, href, img, alt } = Astro.props;
---
-
- {name}
+
+

+
+ {name}
+
+
diff --git a/src/components/common/StyleControls.astro b/src/components/common/StyleControls.astro
new file mode 100644
index 0000000..cc5ab90
--- /dev/null
+++ b/src/components/common/StyleControls.astro
@@ -0,0 +1,121 @@
+---
+interface Props {
+ showSizeSelector?: boolean;
+ showViewSelector?: boolean;
+ className?: string;
+}
+
+const {
+ showSizeSelector = true,
+ showViewSelector = true,
+ className = "",
+} = Astro.props;
+---
+
+
+ {showSizeSelector && (
+
+
Size:
+
+
+
+
+
+
+ )}
+
+ {showViewSelector && (
+
+
View:
+
+
+
+
+ )}
+
+
+
diff --git a/src/components/common/search-client.js b/src/components/common/search-client.js
deleted file mode 100644
index 8ac46b6..0000000
--- a/src/components/common/search-client.js
+++ /dev/null
@@ -1,176 +0,0 @@
-/**
- * Client-side search functionality for filtering content across the site
- */
-
-/**
- * Initialize search functionality for any content type
- * @param {string} contentSelector - CSS selector for the items to filter
- * @param {Object} options - Configuration options
- * @returns {Object} Alpine.js data object with search functionality
- */
-function initializeSearch(contentSelector = '.searchable-item', options = {}) {
- const defaults = {
- nameAttribute: 'data-name',
- tagsAttribute: 'data-tags',
- categoryAttribute: 'data-category',
- additionalAttributes: [],
- noResultsMessage: 'No results found',
- allItemsMessage: 'Showing all items',
- resultCountMessage: (count) => `Found ${count} items`,
- itemLabel: 'items'
- };
-
- const config = { ...defaults, ...options };
-
- return {
- searchQuery: '',
- hasResults: true,
- visibleCount: 0,
-
- init() {
- // Initialize the visible count
- this.visibleCount = document.querySelectorAll(contentSelector).length;
- this.setupWatchers();
- this.setupKeyboardShortcuts();
- },
-
- setupWatchers() {
- this.$watch('searchQuery', (query) => {
- this.filterContent(query);
- });
- },
-
- setupKeyboardShortcuts() {
- document.addEventListener('keydown', (e) => {
- // '/' key focuses the search input
- if (e.key === '/' && document.activeElement.id !== 'app-search') {
- e.preventDefault();
- document.getElementById('app-search').focus();
- }
-
- // Escape key clears the search
- if (e.key === 'Escape' && this.searchQuery !== '') {
- this.searchQuery = '';
- document.getElementById('app-search').focus();
- }
- });
- },
-
- filterContent(query) {
- query = query.toLowerCase();
- let anyResults = false;
- let visibleCount = 0;
-
- // Process all content items
- document.querySelectorAll(contentSelector).forEach((item) => {
- // Get searchable attributes
- const name = (item.getAttribute(config.nameAttribute) || '').toLowerCase();
- const tags = (item.getAttribute(config.tagsAttribute) || '').toLowerCase();
- const category = (item.getAttribute(config.categoryAttribute) || '').toLowerCase();
-
- // Check additional attributes if specified
- const additionalMatches = config.additionalAttributes.some(attr => {
- const value = (item.getAttribute(attr) || '').toLowerCase();
- return value.includes(query);
- });
-
- const isMatch = query === '' ||
- name.includes(query) ||
- tags.includes(query) ||
- category.includes(query) ||
- additionalMatches;
-
- if (isMatch) {
- item.style.display = '';
- anyResults = true;
- visibleCount++;
- } else {
- item.style.display = 'none';
- }
- });
-
- // Update parent containers if needed
- this.updateContainerVisibility(query);
- this.updateResultsStatus(query, anyResults, visibleCount);
- },
-
- updateContainerVisibility(query) {
- // If there are container elements that should be hidden when empty
- const containers = document.querySelectorAll('.content-container');
- if (containers.length > 0) {
- containers.forEach((container) => {
- const hasVisibleItems = Array.from(
- container.querySelectorAll(contentSelector)
- ).some((item) => item.style.display !== 'none');
-
- if (query === '' || hasVisibleItems) {
- container.style.display = '';
- } else {
- container.style.display = 'none';
- }
- });
- }
- },
-
- updateResultsStatus(query, anyResults, count) {
- // Update results status
- this.hasResults = query === '' || anyResults;
- this.visibleCount = count;
-
- // Update screen reader status
- const statusEl = document.getElementById('search-status');
- if (statusEl) {
- if (query === '') {
- statusEl.textContent = config.allItemsMessage;
- this.visibleCount = document.querySelectorAll(contentSelector).length;
- } else if (this.hasResults) {
- statusEl.textContent = config.resultCountMessage(count);
- } else {
- statusEl.textContent = config.noResultsMessage;
- }
- }
- }
- };
-}
-
-// Register Alpine.js data components
-document.addEventListener('alpine:init', () => {
- // Homelab search
- window.Alpine.data('searchServices', () => {
- return initializeSearch('.app-card', {
- nameAttribute: 'data-app-name',
- tagsAttribute: 'data-app-tags',
- categoryAttribute: 'data-app-category',
- noResultsMessage: 'No services found',
- allItemsMessage: 'Showing all services',
- resultCountMessage: (count) => `Found ${count} services`,
- itemLabel: 'services'
- });
- });
-
- // Blog search
- window.Alpine.data('searchArticles', () => {
- return initializeSearch('.article-item', {
- nameAttribute: 'data-title',
- tagsAttribute: 'data-tags',
- additionalAttributes: ['data-description'],
- noResultsMessage: 'No articles found',
- allItemsMessage: 'Showing all articles',
- resultCountMessage: (count) => `Found ${count} articles`,
- itemLabel: 'articles'
- });
- });
-
- // Projects search
- window.Alpine.data('searchProjects', () => {
- return initializeSearch('.project-item', {
- nameAttribute: 'data-title',
- tagsAttribute: 'data-tags',
- additionalAttributes: ['data-description', 'data-github', 'data-live'],
- noResultsMessage: 'No projects found',
- allItemsMessage: 'Showing all projects',
- resultCountMessage: (count) => `Found ${count} projects`,
- itemLabel: 'projects'
- });
- });
-});
diff --git a/src/components/common/searchUtils.js b/src/components/common/searchUtils.js
deleted file mode 100644
index ccbddec..0000000
--- a/src/components/common/searchUtils.js
+++ /dev/null
@@ -1,132 +0,0 @@
-/**
- * Generic search utility for filtering content across the site
- */
-
-/**
- * Initialize search functionality for any content type
- * @returns {Object} Alpine.js data object with search functionality
- */
-export function initializeSearch(contentSelector = '.searchable-item', options = {}) {
- const defaults = {
- nameAttribute: 'data-name',
- tagsAttribute: 'data-tags',
- categoryAttribute: 'data-category',
- additionalAttributes: [],
- noResultsMessage: 'No results found',
- allItemsMessage: 'Showing all items',
- resultCountMessage: (count) => `Found ${count} items`,
- itemLabel: 'items'
- };
-
- const config = { ...defaults, ...options };
-
- return {
- searchQuery: '',
- hasResults: true,
- visibleCount: 0,
-
- init() {
- // Initialize the visible count
- this.visibleCount = document.querySelectorAll(contentSelector).length;
- this.setupWatchers();
- this.setupKeyboardShortcuts();
- },
-
- setupWatchers() {
- this.$watch('searchQuery', (query) => {
- this.filterContent(query);
- });
- },
-
- setupKeyboardShortcuts() {
- document.addEventListener('keydown', (e) => {
- // '/' key focuses the search input
- if (e.key === '/' && document.activeElement.id !== 'app-search') {
- e.preventDefault();
- document.getElementById('app-search').focus();
- }
-
- // Escape key clears the search
- if (e.key === 'Escape' && this.searchQuery !== '') {
- this.searchQuery = '';
- document.getElementById('app-search').focus();
- }
- });
- },
-
- filterContent(query) {
- query = query.toLowerCase();
- let anyResults = false;
- let visibleCount = 0;
-
- // Process all content items
- document.querySelectorAll(contentSelector).forEach((item) => {
- // Get searchable attributes
- const name = (item.getAttribute(config.nameAttribute) || '').toLowerCase();
- const tags = (item.getAttribute(config.tagsAttribute) || '').toLowerCase();
- const category = (item.getAttribute(config.categoryAttribute) || '').toLowerCase();
-
- // Check additional attributes if specified
- const additionalMatches = config.additionalAttributes.some(attr => {
- const value = (item.getAttribute(attr) || '').toLowerCase();
- return value.includes(query);
- });
-
- const isMatch = query === '' ||
- name.includes(query) ||
- tags.includes(query) ||
- category.includes(query) ||
- additionalMatches;
-
- if (isMatch) {
- item.style.display = '';
- anyResults = true;
- visibleCount++;
- } else {
- item.style.display = 'none';
- }
- });
-
- // Update parent containers if needed
- this.updateContainerVisibility(query);
- this.updateResultsStatus(query, anyResults, visibleCount);
- },
-
- updateContainerVisibility(query) {
- // If there are container elements that should be hidden when empty
- const containers = document.querySelectorAll('.content-container');
- if (containers.length > 0) {
- containers.forEach((container) => {
- const hasVisibleItems = Array.from(
- container.querySelectorAll(contentSelector)
- ).some((item) => item.style.display !== 'none');
-
- if (query === '' || hasVisibleItems) {
- container.style.display = '';
- } else {
- container.style.display = 'none';
- }
- });
- }
- },
-
- updateResultsStatus(query, anyResults, count) {
- // Update results status
- this.hasResults = query === '' || anyResults;
- this.visibleCount = count;
-
- // Update screen reader status
- const statusEl = document.getElementById('search-status');
- if (statusEl) {
- if (query === '') {
- statusEl.textContent = config.allItemsMessage;
- this.visibleCount = document.querySelectorAll(contentSelector).length;
- } else if (this.hasResults) {
- statusEl.textContent = config.resultCountMessage(count);
- } else {
- statusEl.textContent = config.noResultsMessage;
- }
- }
- }
- };
-}
diff --git a/src/components/home/Hero.astro b/src/components/home/Hero.astro
index 200322a..2a687ac 100644
--- a/src/components/home/Hero.astro
+++ b/src/components/home/Hero.astro
@@ -1,11 +1,15 @@
---
import { GLOBAL } from "../../lib/variables";
+import OptimizedImage from "../common/OptimizedImage.astro";
+import profileImage from "../../assets/images/pixel_avatar.png";
---
-
diff --git a/src/components/homelab/CategorySection.astro b/src/components/homelab/CategorySection.astro
index d302751..394ff82 100644
--- a/src/components/homelab/CategorySection.astro
+++ b/src/components/homelab/CategorySection.astro
@@ -1,17 +1,38 @@
---
+import { type Service } from "../../lib/types";
+import ServiceCard from "../common/ServiceCard.astro";
+
+/**
+ * CategorySection component displays a collapsible section of services grouped by category
+ * @component
+ * @example
+ * ```astro
+ *
+ * ```
+ */
interface Props {
+ /**
+ * The category name to display as the section title
+ */
category: string;
- apps: Array<{
- name: string;
- link: string;
- icon: string;
- alt: string;
- tags?: string[];
- }>;
+
+ /**
+ * Array of service objects to display in this category
+ */
+ apps: Service[];
}
const { category, apps } = Astro.props;
-import ServiceCard from "../common/ServiceCard.astro";
// Pre-compute values during server-side rendering
const categoryId = `category-${category.toLowerCase().replace(/\s+/g, '-')}`;
@@ -43,7 +64,7 @@ const categoryLower = category.toLowerCase();
x-transition
id={categoryId}
>
-
+
{apps.length > 0 ? (
apps.map(app => {
const appName = app.name.toLowerCase();
diff --git a/src/components/homelab/searchUtils.js b/src/components/homelab/searchUtils.js
deleted file mode 100644
index 7d25e87..0000000
--- a/src/components/homelab/searchUtils.js
+++ /dev/null
@@ -1,100 +0,0 @@
-/**
- * Initialize search functionality for the homelab page
- * This function sets up the search filtering, keyboard shortcuts,
- * and status updates for the search feature
- */
-function initializeSearch() {
- return {
- searchQuery: '',
- hasResults: true,
-
- init() {
- this.setupWatchers();
- this.setupKeyboardShortcuts();
- },
-
- setupWatchers() {
- this.$watch('searchQuery', (query) => {
- this.filterServices(query);
- });
- },
-
- setupKeyboardShortcuts() {
- document.addEventListener('keydown', (e) => {
- // '/' key focuses the search input
- if (e.key === '/' && document.activeElement.id !== 'app-search') {
- e.preventDefault();
- document.getElementById('app-search').focus();
- }
-
- // Escape key clears the search
- if (e.key === 'Escape' && this.searchQuery !== '') {
- this.searchQuery = '';
- document.getElementById('app-search').focus();
- }
- });
- },
-
- filterServices(query) {
- query = query.toLowerCase();
- let anyResults = false;
- let visibleCount = 0;
-
- // Process all service cards
- document.querySelectorAll('.app-card').forEach((card) => {
- const serviceName = card.getAttribute('data-app-name') || '';
- const serviceTags = card.getAttribute('data-app-tags') || '';
- const serviceCategory = card.getAttribute('data-app-category') || '';
-
- const isMatch = query === '' ||
- serviceName.includes(query) ||
- serviceTags.includes(query) ||
- serviceCategory.includes(query);
-
- if (isMatch) {
- card.style.display = '';
- anyResults = true;
- visibleCount++;
- } else {
- card.style.display = 'none';
- }
- });
-
- this.updateCategoryVisibility(query);
- this.updateResultsStatus(query, anyResults, visibleCount);
- },
-
- updateCategoryVisibility(query) {
- document.querySelectorAll('.category-section').forEach((category) => {
- const hasVisibleApps = Array.from(
- category.querySelectorAll('.app-card')
- ).some((card) => card.style.display !== 'none');
-
- if (query === '' || hasVisibleApps) {
- category.style.display = '';
- } else {
- category.style.display = 'none';
- }
- });
- },
-
- updateResultsStatus(query, anyResults, visibleCount) {
- // Update results status
- this.hasResults = query === '' || anyResults;
-
- // Update screen reader status
- const statusEl = document.getElementById('search-status');
- if (statusEl) {
- if (query === '') {
- statusEl.textContent = 'Showing all services';
- } else if (this.hasResults) {
- statusEl.textContent = 'Found ' + visibleCount + ' services matching ' + query;
- } else {
- statusEl.textContent = 'No services found matching ' + query;
- }
- }
- }
- };
-}
-
-export { initializeSearch };
diff --git a/src/env.d.ts b/src/env.d.ts
new file mode 100644
index 0000000..01a8c3d
--- /dev/null
+++ b/src/env.d.ts
@@ -0,0 +1,13 @@
+///
+
+/**
+ * Type definitions for environment variables
+ */
+interface ImportMetaEnv {
+ readonly PUBLIC_SITE_URL: string;
+ // Add other environment variables here as needed
+}
+
+interface ImportMeta {
+ readonly env: ImportMetaEnv;
+}
diff --git a/src/layouts/Layout.astro b/src/layouts/Layout.astro
index d93875e..12255e2 100644
--- a/src/layouts/Layout.astro
+++ b/src/layouts/Layout.astro
@@ -40,14 +40,63 @@ import "../styles/global.css";
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/lib/types.ts b/src/lib/types.ts
index 1f9c898..126f161 100644
--- a/src/lib/types.ts
+++ b/src/lib/types.ts
@@ -1,3 +1,6 @@
+/**
+ * Represents the frontmatter for a project
+ */
export type ProjectFrontmatter = {
/**
* The title of the project
@@ -43,6 +46,9 @@ export type ProjectFrontmatter = {
filename: string;
};
+/**
+ * Represents the frontmatter for an article
+ */
export type ArticleFrontmatter = {
/**
* The title of the article
@@ -50,7 +56,7 @@ export type ArticleFrontmatter = {
title: string;
/**
- * THe summary description of the article
+ * The summary description of the article
*/
description: string;
@@ -81,3 +87,40 @@ export type ArticleFrontmatter = {
*/
filename: string;
};
+
+/**
+ * Represents a service in the homelab
+ */
+export type Service = {
+ /**
+ * The name of the service
+ */
+ name: string;
+
+ /**
+ * The URL to the service
+ */
+ link: string;
+
+ /**
+ * The URL to the service icon
+ */
+ icon: string;
+
+ /**
+ * Alternative text for the service icon
+ */
+ alt: string;
+
+ /**
+ * Tags associated with the service for filtering and categorization
+ */
+ tags?: string[];
+};
+
+/**
+ * Represents a category of services in the homelab
+ */
+export type ServiceCategory = {
+ [category: string]: Service[];
+};
diff --git a/src/lib/variables.ts b/src/lib/variables.ts
index e2731f8..75c12a5 100644
--- a/src/lib/variables.ts
+++ b/src/lib/variables.ts
@@ -1,5 +1,28 @@
// 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} githubProfile - The GitHub profile URL
+ * @property {string} giteaProfile - The Gitea profile URL
+ * @property {string} linkedinProfile - The LinkedIn profile URL
+ * @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 {Object} menu - The menu items
+ */
export const GLOBAL = {
// Site metadata
username: "Justin Deal",
@@ -9,8 +32,8 @@ export const GLOBAL = {
// Social media links
githubProfile: "https://github.com/justindeal",
- twitterProfile: "https://twitter.com/",
- linkedinProfile: "https://www.linkedin.com/",
+ giteaProfile: "https://code.justin.deal/dealjus",
+ linkedinProfile: "https://www.linkedin.com/in/justin-deal/",
// Common text names used throughout the site
articlesName: "Articles",
diff --git a/src/pages/blog/index.astro b/src/pages/blog/index.astro
index 2e13d5c..40eacce 100644
--- a/src/pages/blog/index.astro
+++ b/src/pages/blog/index.astro
@@ -7,7 +7,6 @@ import Section from "../../components/common/Section.astro";
import SearchBar from "../../components/common/SearchBar.astro";
import { articles } from "../../lib/list";
import { countTags } from "../../lib/utils";
-import { initializeSearch } from "../../components/common/searchUtils.js";
const tagCounts = countTags(articles.map((article) => article.tags).flat().filter((tag): tag is string => tag !== undefined));
@@ -39,7 +38,7 @@ const tagCounts = countTags(articles.map((article) => article.tags).flat().filte
-
+
diff --git a/src/pages/homelab/index.astro b/src/pages/homelab/index.astro
index 7b4ca93..be002dd 100644
--- a/src/pages/homelab/index.astro
+++ b/src/pages/homelab/index.astro
@@ -3,42 +3,60 @@ import { GLOBAL } from "../../lib/variables";
import Layout from "../../layouts/Layout.astro";
import Section from "../../components/common/Section.astro";
import SearchBar from "../../components/common/SearchBar.astro";
+import StyleControls from "../../components/common/StyleControls.astro";
import CategorySection from "../../components/homelab/CategorySection.astro";
import ServiceCardSkeleton from "../../components/common/ServiceCardSkeleton.astro";
import SkeletonLoader from "../../components/common/SkeletonLoader.astro";
+import SEO from "../../components/SEO.astro";
+import StructuredData from "../../components/StructuredData.astro";
import { services } from "./services.ts";
-import { initializeSearch } from "../../components/common/searchUtils.js";
+
+// Count total services
+const totalServices = Object.values(services).reduce(
+ (count, serviceList) => count + serviceList.length,
+ 0
+);
+
+// Structured data for the homelab page
+const webpageData = {
+ name: "Homelab Dashboard",
+ description: `A collection of ${totalServices} self-hosted services and applications running on my personal homelab.`,
+ url: `${GLOBAL.rootUrl}/homelab`,
+ isPartOf: {
+ "@type": "WebSite",
+ "name": `${GLOBAL.username} • ${GLOBAL.shortDescription}`,
+ "url": GLOBAL.rootUrl
+ }
+};
---
-
- {GLOBAL.username} • {GLOBAL.shortDescription}
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
-
+
-
+
-
diff --git a/src/pages/homelab/services.ts b/src/pages/homelab/services.ts
index d145bb8..4820fa6 100644
--- a/src/pages/homelab/services.ts
+++ b/src/pages/homelab/services.ts
@@ -1,4 +1,9 @@
-export const services = {
+import { type Service, type ServiceCategory } from "../../lib/types";
+
+/**
+ * Services available in the homelab, organized by category
+ */
+export const services: ServiceCategory = {
Websites: [
{
name: "https://justin.deal",
diff --git a/src/pages/index.astro b/src/pages/index.astro
index 3a78a74..6a0f138 100644
--- a/src/pages/index.astro
+++ b/src/pages/index.astro
@@ -6,39 +6,37 @@ import Hero from "../components/home/Hero.astro";
import Section from "../components/common/Section.astro";
import FeaturedProjects from "../components/home/FeaturedProjects.astro";
import FeaturedArticles from "../components/home/FeaturedArticles.astro";
+import SEO from "../components/SEO.astro";
+import StructuredData from "../components/StructuredData.astro";
+
+// Structured data for the home page
+const websiteData = {
+ name: `${GLOBAL.username} • ${GLOBAL.shortDescription}`,
+ url: GLOBAL.rootUrl,
+ description: GLOBAL.longDescription,
+ potentialAction: {
+ "@type": "SearchAction",
+ "target": `${GLOBAL.rootUrl}/search?q={search_term_string}`,
+ "query-input": "required name=search_term_string"
+ }
+};
+
+const personData = {
+ name: GLOBAL.username,
+ url: GLOBAL.rootUrl,
+ jobTitle: "Software Developer",
+ sameAs: [
+ GLOBAL.githubProfile,
+ GLOBAL.linkedinProfile,
+ GLOBAL.giteaProfile
+ ]
+};
---
-
- {GLOBAL.username} • {GLOBAL.shortDescription}
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
diff --git a/src/pages/projects/index.astro b/src/pages/projects/index.astro
index c282184..0147c1a 100644
--- a/src/pages/projects/index.astro
+++ b/src/pages/projects/index.astro
@@ -6,7 +6,6 @@ import ProjectSnippetSkeleton from "../../components/ProjectSnippetSkeleton.astr
import SearchBar from "../../components/common/SearchBar.astro";
import Layout from "../../layouts/Layout.astro";
import { GLOBAL } from "../../lib/variables";
-import { initializeSearch } from "../../components/common/searchUtils.js";
---
@@ -35,7 +34,7 @@ import { initializeSearch } from "../../components/common/searchUtils.js";
-
+
diff --git a/src/pages/projects/this-site.md b/src/pages/projects/this-site.md
index ca0daeb..bf3c639 100644
--- a/src/pages/projects/this-site.md
+++ b/src/pages/projects/this-site.md
@@ -2,8 +2,8 @@
layout: ../../layouts/ProjectLayout.astro
title: This Website
description: My personal blog, portfolio, and homelab dashboard built with Astro, TypeScript, TailwindCSS, and Alpine.js featuring smart loading, advanced search, and accessibility features.
-tags: ["astro", "typescript", "tailwindcss", "alpinejs", "responsive-design", "accessibility", "dark-mode", "gruvbox-theme"]
-githubUrl: https://code.justin.deal
+tags: ["astro", "typescript", "tailwindcss", "alpinejs", "gruvbox-theme"]
+githubUrl: https://code.justin.deal/dealjus/justin.deal
timestamp: 2025-02-24T02:39:03+00:00
featured: true
filename: this-site
diff --git a/src/styles/global.css b/src/styles/global.css
index cb5338e..3284daf 100644
--- a/src/styles/global.css
+++ b/src/styles/global.css
@@ -13,6 +13,19 @@ html.theme-loaded body {
transition: background-color 0.3s ease, color 0.3s ease;
}
+/* Font loading states */
+html:not(.fonts-loaded) body {
+ /* Fallback font metrics that match your custom font */
+ font-family: monospace;
+}
+
+html.fonts-loaded body {
+ /* Your custom font */
+ font-family: var(--font-mono);
+ /* Add a subtle transition for font changes */
+ transition: font-family 0.1s ease-out;
+}
+
/* Hide elements with x-cloak until Alpine.js is loaded */
[x-cloak] {
display: none !important;
@@ -24,10 +37,11 @@ html.theme-loaded body {
outline-offset: 2px;
}
+/* Font declarations with optimized loading strategies */
@font-face {
font-family: "Literata Variable";
font-style: normal;
- font-display: swap;
+ font-display: swap; /* Use swap for text fonts */
font-weight: 200 900;
src: url(https://cdn.jsdelivr.net/fontsource/fonts/literata:vf@latest/latin-opsz-normal.woff2)
format("woff2-variations");
@@ -39,7 +53,7 @@ html.theme-loaded body {
@font-face {
font-family: "press-start-2p";
font-style: normal;
- font-display: swap;
+ font-display: optional; /* Use optional for decorative fonts */
font-weight: 400;
src: url(https://cdn.jsdelivr.net/fontsource/fonts/press-start-2p@latest/latin-400-normal.woff2)
format("woff2"),
diff --git a/tsconfig.json b/tsconfig.json
index 2e85c92..b9eced4 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -3,6 +3,16 @@
"include": [".astro/types.d.ts", "**/*"],
"exclude": ["dist"],
"compilerOptions": {
- "allowImportingTsExtensions": true
+ "allowImportingTsExtensions": true,
+ "exactOptionalPropertyTypes": false, // Temporarily disabled to fix component issues
+ "noFallthroughCasesInSwitch": true,
+ "noImplicitOverride": true,
+ "noImplicitReturns": true,
+ "noPropertyAccessFromIndexSignature": true,
+ "noUncheckedIndexedAccess": true,
+ "baseUrl": ".",
+ "paths": {
+ "@/*": ["src/*"]
+ }
}
}