Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ CONVERTKIT_FORM_ID="100000"
# to ensure the request originated from Vercel.
CRON_SECRET="local-cron-secret"

# Like it says on the label
PUBLIC_GOOGLE_MAPS_API_KEY=""

# Mock Resend API (WireMock container that backs transactional email tests
# local and on GitHub Actions, production on Vercel)
RESEND_HTTP_PORT="9011"
Expand Down
179 changes: 176 additions & 3 deletions .github/workflows/deployment.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,14 @@ jobs:
uses: actions/github-script@v8
with:
script: |
const runId = context.payload.workflow_run.id;
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 requiredJobs = ['Lint', 'Unit Tests'];
const { data } = await github.rest.actions.listJobsForWorkflowRun({
owner: context.repo.owner,
Expand Down Expand Up @@ -53,24 +60,133 @@ 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: always() && steps.vercel-preview.outcome == 'success'
if: ${{ !startsWith(github.event.workflow_run.head_branch, 'hotfix/') && always() && steps.vercel-preview.outcome == 'success' }}
uses: actions/github-script@v8
env:
VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }}
Expand Down Expand Up @@ -185,7 +301,7 @@ jobs:
}

- name: Comment preview failure on PR
if: always() && steps.vercel-preview.outcome != 'success'
if: ${{ !startsWith(github.event.workflow_run.head_branch, 'hotfix/') && always() && steps.vercel-preview.outcome != 'success' }}
uses: actions/github-script@v8
env:
VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }}
Expand Down Expand Up @@ -273,16 +389,73 @@ 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'
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ env:
CONVERTKIT_API_KEY: ${{ secrets.CONVERTKIT_API_KEY }}
CONVERTKIT_FORM_ID: ${{ secrets.CONVERTKIT_FORM_ID }}
CRON_SECRET: ${{ secrets.CRON_SECRET }}
PUBLIC_GOOGLE_MAPS_API_KEY: ${{ secrets.PUBLIC_GOOGLE_MAPS_API_KEY }}
RESEND_API_KEY: ${{ secrets.RESEND_API_KEY }}
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
Expand Down
80 changes: 4 additions & 76 deletions _TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -159,44 +159,9 @@ Needs to add real API key and test

## Astro 3rd-Party Integrations, Eleventy Migration

- **`eleventy-plugin-external-links`**
### **`eleventy-plugin-external-links`**

### Adds `target="_blank" rel="noreferrer"` to all external links

- **`eleventy-plugin-emoji`**

### Time to Read

Adds filter for analyzing content input into the filter and returning a time-to-read estimate to use in text like 'This will take 3 minutes to read'.

```typescript
{
speed: '200 words per minute',
/** 'long': 3 minutes and 10 seconds, 'short': 3 min & 10 sec, 'narrow': 3m, 10s */
style: 'narrow',src/layouts/BaseLayout.astro
/** Which time units to render */
hours: false,
minutes: true,
seconds: false,
/**
* Format returned string
*
* @param {object} data - An object with various keys, see docs
* @returns {string} Returns the formatted string to return from time-to-read shortcode
*/
output: function (data) {
return data.timing
},
}
```

## An accessible Emoji component. Wraps emojis in a `<span>` with `aria-label` or `aria-hidden`, and `role` attributes

[`astro-emoji`](https://github.com/seanmcp/astro-emoji#astro-emoji)

### Table of Contents (ToC) generator

[`astro-toc`](https://github.com/theisel/astro-toc#readme)
Adds `target="_blank" rel="noreferrer"` to all external links

### Astro wrapper for the `@github/clipboard-copy-element` web component. Copies element text content or input values to the clipboard

Expand All @@ -218,10 +183,6 @@ Adds filter for analyzing content input into the filter and returning a time-to-

## Miscellaneous

### [`astro-auto-import`](https://www.npmjs.com/package/astro-auto-import)

Allows you to auto-import components or other modules and access them in MDX files without importing them.

### [`astro-directives`](https://github.com/QuentinDutot/astro-directives)

Adds some custom directives:
Expand All @@ -245,7 +206,8 @@ Adds some custom directives:

Allows any Mastodon instance to discover your Mastodon profile directly from your own domain.

## Refactor social neworks in Authors collection to Contact collection format

## Refactor social networks in Authors collection to Contact collection format

The contact data collection uses an array of social networks, with keys:

Expand All @@ -260,36 +222,6 @@ The contact data collection uses an array of social networks, with keys:

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.

We should also make sure the avatar key in the authors collection is being output as a responsive image tag.

## Add Google Maps screenshot (or maps embed) to Contact Page

src/assets/images/map.webp

## Display a system font until font files load (Lighthouse improvements)

Display a system font until font files load to improve FCP (First Contentful Paint) with `font-display: swap`. Need to make sure that web font doesn't render larger or smaller than the system font fallback to avoid CLS (Cumulative Layout Shift) issues.

```css
@font-face {
font-family: 'Pacifico';
font-style: normal;
font-weight: 400;
src: local('Pacifico Regular'), local('Pacifico-Regular'),
url(https://fonts.gstatic.com/s/pacifico/v12/FwZY7-Qmy14u9lezJ-6H6MmBp0u-.woff2) format('woff2');
font-display: swap;
}
```

Preload fonts:

```html
<link rel="preload" as="font" />
```

TTI (Time to Interactive) measures time from when the page is painted until it becomes usefully interactive.
Interactive can only have two in-flight network requests.

## Stuff from ZMarkdown, a prepackaged Unified config

Repo is in root of Corporate Websites
Expand Down Expand Up @@ -331,10 +263,6 @@ Repo is in root of Corporate Websites

This plugin parses custom Markdown syntax to create new custom blocks.

- **remark-emoticons**

This plugins replaces ASCII emoticons with associated image. Compatible with [rehype][rehype]

- **remark-escape--escaped**

This plugin escapes HTML entities from Markdown input.
Expand Down
Loading