From 5548aae0011b3c24bc4befc4c503f59b58eb4253 Mon Sep 17 00:00:00 2001 From: Kevin Brown Date: Mon, 15 Dec 2025 02:41:43 +0300 Subject: [PATCH] Revert deployment workflow, add swap fonts, change social network content type --- .github/workflows/deployment.yml | 179 +----------------- .vscode/settings.json | 2 + _TODO.md | 20 -- .../scripts/utils/environmentClient.ts | 6 +- src/content.config.ts | 34 ++-- src/content/authors/kevin-brown.mdx | 21 +- src/pages/rss.xml.ts | 21 +- src/styles/fonts.css | 154 ++++++++++++++- 8 files changed, 206 insertions(+), 231 deletions(-) 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