diff --git a/.github/workflows/deployment.yml b/.github/workflows/deployment.yml
index 41807fe5b..6084c7c99 100644
--- a/.github/workflows/deployment.yml
+++ b/.github/workflows/deployment.yml
@@ -24,14 +24,7 @@ jobs:
uses: actions/github-script@v8
with:
script: |
- const workflowRun = context.payload.workflow_run;
- const headBranch = workflowRun?.head_branch;
- if (typeof headBranch === 'string' && headBranch.startsWith('hotfix/')) {
- core.info('hotfix/* branch detected; skipping CI job verification.');
- return;
- }
-
- const runId = workflowRun.id;
+ const runId = context.payload.workflow_run.id;
const requiredJobs = ['Lint', 'Unit Tests'];
const { data } = await github.rest.actions.listJobsForWorkflowRun({
owner: context.repo.owner,
@@ -60,133 +53,24 @@ jobs:
github.event.workflow_run.event == 'pull_request'
steps:
- - name: Hotfix branch — skip preview deploy
- if: ${{ startsWith(github.event.workflow_run.head_branch, 'hotfix/') }}
- run: |
- echo "hotfix/* branch detected; skipping preview deployment (treat as success)."
-
- name: Checkout repository
- if: ${{ !startsWith(github.event.workflow_run.head_branch, 'hotfix/') }}
uses: actions/checkout@v6
with:
ref: ${{ github.event.workflow_run.head_sha }}
- - name: Create GitHub deployment (Preview)
- if: ${{ !startsWith(github.event.workflow_run.head_branch, 'hotfix/') }}
- id: github-deployment-preview
- uses: actions/github-script@v8
- with:
- script: |
- const workflowRun = context.payload.workflow_run;
- const ref = workflowRun?.head_sha;
- if (!ref) {
- core.setFailed('Missing workflow_run.head_sha; cannot create GitHub deployment.');
- return;
- }
-
- const { data } = await github.rest.repos.createDeployment({
- owner: context.repo.owner,
- repo: context.repo.repo,
- ref,
- auto_merge: false,
- required_contexts: [],
- environment: 'vercel-preview',
- transient_environment: true,
- production_environment: false,
- description: 'Vercel preview deployment'
- });
-
- core.setOutput('deployment_id', String(data.id));
-
- name: Deploy to Vercel (Preview)
- if: ${{ !startsWith(github.event.workflow_run.head_branch, 'hotfix/') }}
uses: amondnet/vercel-action@v41.1.4
id: vercel-preview
with:
vercel-token: ${{ secrets.VERCEL_TOKEN }}
- github-token: ${{ secrets.GITHUB_TOKEN }}
vercel-project-id: ${{ secrets.VERCEL_PROJECT_ID }}
vercel-org-id: ${{ secrets.VERCEL_ORG_ID }}
github-comment: false
env:
VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }}
- GITHUB_SHA: ${{ github.event.workflow_run.head_sha }}
- GITHUB_REF: refs/heads/${{ github.event.workflow_run.head_branch }}
-
- - name: Update GitHub deployment status (Preview)
- if: ${{ !startsWith(github.event.workflow_run.head_branch, 'hotfix/') && always() }}
- uses: actions/github-script@v8
- env:
- VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }}
- VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }}
- VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }}
- with:
- script: |
- const deploymentIdRaw = '${{ steps.github-deployment-preview.outputs.deployment_id }}';
- const deploymentId = Number(deploymentIdRaw);
- if (!deploymentIdRaw || Number.isNaN(deploymentId)) {
- core.warning('Missing deployment id; skipping GitHub deployment status update.');
- return;
- }
-
- const workflowRun = context.payload.workflow_run;
- const rawPreviewUrl = '${{ steps.vercel-preview.outputs.preview-url }}'.trim();
- const isVercelUrl = (url) => typeof url === 'string' && /vercel\.(app|com)/.test(url);
- const fallbackRunUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`;
-
- const fetchDeploymentUrl = async () => {
- if (!process.env.VERCEL_TOKEN || !process.env.VERCEL_PROJECT_ID) {
- return null;
- }
- if (typeof fetch !== 'function') {
- return null;
- }
- const query = new URLSearchParams({
- projectId: process.env.VERCEL_PROJECT_ID,
- 'meta-githubCommitSha': workflowRun?.head_sha ?? '',
- limit: '1'
- });
- if (process.env.VERCEL_ORG_ID) {
- query.set('teamId', process.env.VERCEL_ORG_ID);
- }
- const response = await fetch(`https://api.vercel.com/v6/deployments?${query.toString()}`, {
- headers: {
- Authorization: `Bearer ${process.env.VERCEL_TOKEN}`
- }
- });
- if (!response.ok) {
- return null;
- }
- const data = await response.json();
- const deployment = data?.deployments?.[0];
- if (deployment?.url) {
- return `https://${deployment.url}`;
- }
- if (deployment?.inspectorUrl) {
- return deployment.inspectorUrl.startsWith('http')
- ? deployment.inspectorUrl
- : `https://${deployment.inspectorUrl}`;
- }
- return null;
- };
-
- const previewUrl = isVercelUrl(rawPreviewUrl) ? rawPreviewUrl : await fetchDeploymentUrl();
- const state = '${{ steps.vercel-preview.outcome }}' === 'success' ? 'success' : 'failure';
- const targetUrl = isVercelUrl(previewUrl) ? previewUrl : fallbackRunUrl;
-
- await github.rest.repos.createDeploymentStatus({
- owner: context.repo.owner,
- repo: context.repo.repo,
- deployment_id: deploymentId,
- state,
- environment: 'vercel-preview',
- environment_url: targetUrl,
- log_url: fallbackRunUrl,
- description: state === 'success' ? 'Vercel preview is ready' : 'Vercel preview failed'
- });
- name: Comment preview URL on PR
- if: ${{ !startsWith(github.event.workflow_run.head_branch, 'hotfix/') && always() && steps.vercel-preview.outcome == 'success' }}
+ if: always() && steps.vercel-preview.outcome == 'success'
uses: actions/github-script@v8
env:
VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }}
@@ -301,7 +185,7 @@ jobs:
}
- name: Comment preview failure on PR
- if: ${{ !startsWith(github.event.workflow_run.head_branch, 'hotfix/') && always() && steps.vercel-preview.outcome != 'success' }}
+ if: always() && steps.vercel-preview.outcome != 'success'
uses: actions/github-script@v8
env:
VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }}
@@ -389,73 +273,16 @@ jobs:
with:
ref: ${{ github.event.workflow_run.head_sha }}
- - name: Create GitHub deployment (Production)
- id: github-deployment-production
- uses: actions/github-script@v8
- with:
- script: |
- const workflowRun = context.payload.workflow_run;
- const ref = workflowRun?.head_sha;
- if (!ref) {
- core.setFailed('Missing workflow_run.head_sha; cannot create GitHub deployment.');
- return;
- }
-
- const { data } = await github.rest.repos.createDeployment({
- owner: context.repo.owner,
- repo: context.repo.repo,
- ref,
- auto_merge: false,
- required_contexts: [],
- environment: 'production',
- transient_environment: false,
- production_environment: true,
- description: 'Vercel production deployment'
- });
-
- core.setOutput('deployment_id', String(data.id));
-
- name: Deploy to Vercel (Production)
uses: amondnet/vercel-action@v41.1.4
id: vercel-production
with:
vercel-token: ${{ secrets.VERCEL_TOKEN }}
- github-token: ${{ secrets.GITHUB_TOKEN }}
vercel-args: '--prod'
vercel-project-id: ${{ secrets.VERCEL_PROJECT_ID }}
vercel-org-id: ${{ secrets.VERCEL_ORG_ID }}
env:
VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }}
- GITHUB_SHA: ${{ github.event.workflow_run.head_sha }}
- GITHUB_REF: refs/heads/${{ github.event.workflow_run.head_branch }}
-
- - name: Update GitHub deployment status (Production)
- if: ${{ always() }}
- uses: actions/github-script@v8
- with:
- script: |
- const deploymentIdRaw = '${{ steps.github-deployment-production.outputs.deployment_id }}';
- const deploymentId = Number(deploymentIdRaw);
- if (!deploymentIdRaw || Number.isNaN(deploymentId)) {
- core.warning('Missing deployment id; skipping GitHub deployment status update.');
- return;
- }
-
- const state = '${{ steps.vercel-production.outcome }}' === 'success' ? 'success' : 'failure';
- const rawUrl = '${{ steps.vercel-production.outputs.preview-url }}'.trim();
- const environmentUrl = rawUrl ? rawUrl : undefined;
- const logUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`;
-
- await github.rest.repos.createDeploymentStatus({
- owner: context.repo.owner,
- repo: context.repo.repo,
- deployment_id: deploymentId,
- state,
- environment: 'production',
- environment_url: environmentUrl,
- log_url: logUrl,
- description: state === 'success' ? 'Production deploy completed' : 'Production deploy failed'
- });
- name: Log production deployment
if: always() && steps.vercel-production.outcome == 'success'
diff --git a/.vscode/settings.json b/.vscode/settings.json
index 67fbcbb99..791c694a1 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -69,6 +69,7 @@
"Neue",
"Niklas",
"noabbr",
+ "Noto",
"octocat",
"oden",
"oembed",
@@ -90,6 +91,7 @@
"repost",
"reposts",
"RGAA",
+ "Roboto",
"rodgtr",
"Ryuk",
"samp",
diff --git a/_TODO.md b/_TODO.md
index fb16dcc5c..4088ed156 100644
--- a/_TODO.md
+++ b/_TODO.md
@@ -202,26 +202,6 @@ Adds some custom directives:
1. Add `rel="prefetch"` to any `` tags to prefetch when visible
2. Add `rel="prefetch-intent"` to any `` links on your page to prefetch them only when they are hovered over, touched, or focused.
-### [astro-webfinger](https://www.npmjs.com/package/astro-webfinger)
-
-Allows any Mastodon instance to discover your Mastodon profile directly from your own domain.
-
-
-## Refactor social networks in Authors collection to Contact collection format
-
-The contact data collection uses an array of social networks, with keys:
-
-```
-{
- network: z.string(),
- name: z.string(),
- url: z.string().url(),
- order: z.number(),
-}
-```
-
-The authors collection is using named entries under a "social" property, like "twitter", "github", etc. This task is to refactor that to use an array like contact data collection. We also need to add a color for the social network icon, or some other approach to setting the color of it while enabling theming.
-
## Stuff from ZMarkdown, a prepackaged Unified config
Repo is in root of Corporate Websites
diff --git a/src/components/scripts/utils/environmentClient.ts b/src/components/scripts/utils/environmentClient.ts
index c9f9d18f1..b33f157b4 100644
--- a/src/components/scripts/utils/environmentClient.ts
+++ b/src/components/scripts/utils/environmentClient.ts
@@ -2,7 +2,11 @@
* This file cannot be used in API endpoint code. See
* src/lib/config/environmentServer.ts for details.
*/
-import { PACKAGE_RELEASE_VERSION, PRIVACY_POLICY_VERSION, PUBLIC_GOOGLE_MAPS_API_KEY } from 'astro:env/client'
+import {
+ PACKAGE_RELEASE_VERSION,
+ PRIVACY_POLICY_VERSION,
+ PUBLIC_GOOGLE_MAPS_API_KEY
+} from 'astro:env/client'
import { ClientScriptError } from '@components/scripts/errors'
/**
diff --git a/src/content.config.ts b/src/content.config.ts
index 8c6df3af7..c851bb9c6 100644
--- a/src/content.config.ts
+++ b/src/content.config.ts
@@ -58,6 +58,16 @@ const createBaseCollectionSchema = ({ image }: SchemaContext) =>
tags: z.array(z.enum(validTags)),
})
+const createSocialCollectionSchema = () =>
+ z.array(
+ z.object({
+ network: z.string(),
+ name: z.string(),
+ url: z.string().url(),
+ order: z.number(),
+ }),
+ )
+
/**
* =================================================================================
*
@@ -170,20 +180,7 @@ const authorsCollection = defineCollection({
name: z.string(),
email: z.string().email(),
avatar: z.string(),
- social: z.object({
- twitter: z.object({
- name: z.string(),
- url: z.string().url(),
- }),
- github: z.object({
- name: z.string(),
- url: z.string().url(),
- }),
- linkedin: z.object({
- name: z.string(),
- url: z.string().url(),
- }),
- }),
+ social: createSocialCollectionSchema(),
}),
})
@@ -202,14 +199,7 @@ const contactDataCollection = defineCollection({
telephoneLocal: z.string(),
telephoneMobile: z.string(),
telephoneTollFree: z.string(),
- social: z.array(
- z.object({
- network: z.string(),
- name: z.string(),
- url: z.string().url(),
- order: z.number(),
- }),
- ),
+ social: createSocialCollectionSchema(),
}),
})
diff --git a/src/content/authors/kevin-brown.mdx b/src/content/authors/kevin-brown.mdx
index 477f847e9..14971c0e5 100644
--- a/src/content/authors/kevin-brown.mdx
+++ b/src/content/authors/kevin-brown.mdx
@@ -4,13 +4,28 @@ name: "Kevin Brown"
email: "kevin@webstackbuilders.com"
avatar: "/images/avatars/kevin-brown.webp"
social:
- twitter:
+ - network: "x"
name: "WebstackDev"
url: "https://x.com/WebstackDev"
- github:
+ order: 1
+ - network: "github"
name: "webstackdev"
url: "https://github.com/webstackdev"
- linkedin:
+ order: 2
+ - network: "linkedin"
name: "kevin-brown-developer"
url: "https://www.linkedin.com/in/kevin-brown-developer/"
+ order: 3
+ - network: "codepen"
+ name: "WebstackDev"
+ url: "https://codepen.io/webstackdev"
+ order: 4
+ - network: "bluesky"
+ name: "webstackdev.bsky.social"
+ url: "https://bsky.app/profile/webstackdev.bsky.social"
+ order: 5
+ - network: "mastodon"
+ name: "@webstackdev"
+ url: "https://k8s.social/@webstackdev"
+ order: 6
---
diff --git a/src/pages/rss.xml.ts b/src/pages/rss.xml.ts
index fcfd1739e..573f79e1d 100644
--- a/src/pages/rss.xml.ts
+++ b/src/pages/rss.xml.ts
@@ -1,20 +1,33 @@
import rss from '@astrojs/rss'
import { getCollection } from 'astro:content'
import type { APIContext } from 'astro'
-import type { CollectionEntry } from 'astro:content'
+import { ApiFunctionError } from './api/_errors/ApiFunctionError'
export async function GET(context: APIContext) {
- const articles = await getCollection('articles')
+ if (!context.site) {
+ throw new ApiFunctionError('Missing site URL. Configure `site` in `astro.config.ts` to generate RSS links.', {
+ status: 500,
+ route: '/rss.xml',
+ operation: 'Generate RSS feed',
+ })
+ }
+
+ const now = new Date()
+ const articles = await getCollection('articles', ({ data }) => !data.isDraft && data.publishDate <= now)
+ const sortedArticles = articles.sort((a, b) => b.data.publishDate.valueOf() - a.data.publishDate.valueOf())
+
return rss({
title: 'Webstack Builders Website',
description:
'Webstack Builders is a solo software development agency specializing in platform engineering.',
- site: context.site ?? 'https://webstackbuilders.com',
- items: articles.map((article: CollectionEntry<'articles'>) => ({
+ site: context.site,
+ trailingSlash: false,
+ items: sortedArticles.map(article => ({
title: article.data.title,
pubDate: article.data.publishDate,
description: article.data.description,
link: `/articles/${article.id}`,
+ categories: article.data.tags,
})),
customData: `en-us`,
})
diff --git a/src/styles/fonts.css b/src/styles/fonts.css
index 0eb56ee5f..2ac941290 100644
--- a/src/styles/fonts.css
+++ b/src/styles/fonts.css
@@ -1,11 +1,40 @@
/**
- * Font Face Declarations
- * Modernized for Tailwind v4 compatibility
+ * Font Face Declarations, modernized for Tailwind v4 compatibility
+ *
+ * Displays a system font until font files load to improve FCP (First
+ * Contentful Paint) with `font-display: swap`. This setup adjusts system
+ * fonts to ensure that the web font doesn't render larger or smaller
+ * than the system font fallback to avoid CLS (Cumulative Layout Shift) issues.
+ *
+ * System font mappings (font families available by default per platform):
+ * Windows - Sans Serif: Arial, Serif: Times New Roman, Monospace: Courier New
+ * MacOS - Sans Serif: Helvetica, Serif: Georgia, Monospace: Courier
+ * Gnome/KDE - Sans Serif: DejaVu Sans, Serif: Liberation Serif, Monospace: DejaVu Sans Mono
+ * iOS - Sans Serif: Helvetica, Serif: Georgia, Monospace: Courier
+ * Android - Sans Serif: Roboto, Serif: Noto Serif, Monospace: Roboto Mono
*/
@theme {
- --font-family-sans: "Onest Regular", sans-serif;
- --font-family-serif: "Lora Regular", serif;
- --font-family-mono: "IBM Plex Mono Regular", monospace;
+ --font-family-sans:
+ "Onest Regular",
+ "Onest Fallback Helvetica",
+ "Onest Fallback DejaVu Sans",
+ "Onest Fallback Arial",
+ "Onest Fallback Roboto",
+ sans-serif;
+ --font-family-serif:
+ "Lora Regular",
+ "Lora Fallback Liberation Serif",
+ "Lora Fallback Times New Roman",
+ "Lora Fallback Georgia",
+ "Lora Fallback Noto Serif",
+ serif;
+ --font-family-mono:
+ "IBM Plex Mono Regular",
+ "IBM Plex Mono Fallback Courier",
+ "IBM Plex Mono Fallback DejaVu Sans Mono",
+ "IBM Plex Mono Fallback Courier New",
+ "IBM Plex Mono Fallback Roboto Mono",
+ monospace;
}
@font-face {
@@ -19,6 +48,45 @@
url("/fonts/OnestRegular1602-hint.woff2") format("woff2");
}
+@font-face {
+ ascent-override: 97.00%;
+ descent-override: 30.50%;
+ font-family: "Onest Fallback Helvetica";
+ line-gap-override: 0.00%;
+ size-adjust: 103.70%;
+ src:
+ local("Helvetica"),
+ local("Helvetica LT Std"),
+ local("Helvetica World");
+}
+
+@font-face {
+ ascent-override: 97.00%;
+ descent-override: 30.50%;
+ font-family: "Onest Fallback DejaVu Sans";
+ line-gap-override: 0.00%;
+ size-adjust: 91.41%;
+ src: local("DejaVu Sans");
+}
+
+@font-face {
+ ascent-override: 97.00%;
+ descent-override: 30.50%;
+ font-family: "Onest Fallback Arial";
+ line-gap-override: 0.00%;
+ size-adjust: 103.07%;
+ src: local("Arial");
+}
+
+@font-face {
+ ascent-override: 97.00%;
+ descent-override: 30.50%;
+ font-family: "Onest Fallback Roboto";
+ line-gap-override: 0.00%;
+ size-adjust: 105.30%;
+ src: local("Roboto");
+}
+
@font-face {
font-display: swap;
font-family: "Lora Regular";
@@ -30,6 +98,42 @@
url("/fonts/Lora-Regular.woff2") format("woff2");
}
+@font-face {
+ ascent-override: 100.60%;
+ descent-override: 27.40%;
+ font-family: "Lora Fallback Liberation Serif";
+ line-gap-override: 0.00%;
+ size-adjust: 108.56%;
+ src: local("Liberation Serif");
+}
+
+@font-face {
+ ascent-override: 100.60%;
+ descent-override: 27.40%;
+ font-family: "Lora Fallback Times New Roman";
+ line-gap-override: 0.00%;
+ size-adjust: 108.56%;
+ src: local("Times New Roman");
+}
+
+@font-face {
+ ascent-override: 100.60%;
+ descent-override: 27.40%;
+ font-family: "Lora Fallback Georgia";
+ line-gap-override: 0.00%;
+ size-adjust: 101.39%;
+ src: local("Georgia");
+}
+
+@font-face {
+ ascent-override: 100.60%;
+ descent-override: 27.40%;
+ font-family: "Lora Fallback Noto Serif";
+ line-gap-override: 0.00%;
+ size-adjust: 98.67%;
+ src: local("Noto Serif");
+}
+
@font-face {
font-display: swap;
font-family: "IBM Plex Mono Regular";
@@ -39,4 +143,44 @@
local("IBM Plex Mono Regular"),
local("IBMPlexMono-Regular"),
url("/fonts/IBMPlexMono-Regular.woff2") format("woff2");
+}
+
+@font-face {
+ ascent-override: 102.50%;
+ descent-override: 27.50%;
+ font-family: "IBM Plex Mono Fallback Courier";
+ line-gap-override: 0.00%;
+ size-adjust: 100.00%;
+ src:
+ local("Courier"),
+ local("Nimbus Mono PS");
+}
+
+@font-face {
+ ascent-override: 102.50%;
+ descent-override: 27.50%;
+ font-family: "IBM Plex Mono Fallback DejaVu Sans Mono";
+ line-gap-override: 0.00%;
+ size-adjust: 99.66%;
+ src: local("DejaVu Sans Mono");
+}
+
+@font-face {
+ ascent-override: 102.50%;
+ descent-override: 27.50%;
+ font-family: "IBM Plex Mono Fallback Courier New";
+ line-gap-override: 0.00%;
+ size-adjust: 99.98%;
+ src: local("Courier New");
+}
+
+@font-face {
+ ascent-override: 102.50%;
+ descent-override: 27.50%;
+ font-family: "IBM Plex Mono Fallback Roboto Mono";
+ line-gap-override: 0.00%;
+ size-adjust: 99.98%;
+ src:
+ local("Roboto Mono"),
+ local("Roboto Mono for Powerline");
}
\ No newline at end of file