Compare commits

...

4 commits

Author SHA1 Message Date
Sam Carlton
81b2179d0f Keep the raw worker URL pointed at the canonical site
Some checks failed
Deploy to Cloudflare Workers with Wrangler / Deploy (push) Has been cancelled
Run Node 24 Checks / build (24.x) (push) Has been cancelled
The default worker is published to workers.dev, but fetching that hostname as the origin returns Cloudflare's placeholder page. Redirect the raw workers.dev host to doesitarm.com while keeping the existing pass-through behavior for routed custom hosts.

Constraint: workers.dev has no real site origin behind the default worker
Rejected: Proxy workers.dev through doesitarm.com | redirect is simpler and avoids presenting duplicate public hosts
Confidence: high
Scope-risk: narrow
Directive: Keep this redirect host-specific so custom-domain worker routes can continue pass-through behavior
Tested: npm --prefix doesitarm-default run build
Not-tested: Post-deploy live redirect, pending GitHub Actions deployment
2026-04-26 12:53:35 -05:00
Sam Carlton
d1f49267c0 Keep device listings available in clean builds
The Netlify validation job imports site listings before generated static device JSON exists in a clean checkout, so keep the small device table in tracked source and use it as the route fallback.

Constraint: static/device-list.json is ignored generated output and is absent in GitHub-hosted clean runners
Rejected: Force-add static/device-list.json | keeps generated output under source control
Confidence: high
Scope-risk: narrow
Directive: Keep disabled device rows in the fallback source but filter them from routable listings
Tested: pnpm run with-env vitest ./test/prebuild/site-listings.test.js
Tested: pnpm netlify-build reached Astro build after passing prebuild tests
Not-tested: Full local netlify-build completion blocked by unrelated uncommitted scanner worker build failure
2026-04-25 15:46:39 -05:00
ThatGuySam
3d19c60670 Keep YouTube fallbacks small enough to review
The first baseline committed the raw YouTube API dump, which fixed clean builds but carried unused API metadata. The route fallback only needs prebuilt listing objects, so this replaces the raw seed with slug-keyed listing fallbacks and adds a refresh script that fetches the latest source data before projecting it.

Constraint: helpers/site-listings.js must work in a fresh checkout before generated static/api output exists

Rejected: Commit the full raw YouTube JSON | unnecessarily large and mostly unused by the fallback path

Rejected: Drop descriptions only | smaller but would weaken app-link and tag matching semantics

Confidence: high

Scope-risk: narrow

Reversibility: clean

Directive: Refresh static/api/youtube-video-listings.json via scripts/update-youtube-video-listing-fallbacks.js, not by committing static/api/youtube-videos.json

Tested: pnpm run with-env vite-node scripts/update-youtube-video-listing-fallbacks.js

Tested: pnpm run with-env vitest test/prebuild/site-listings.test.js

Tested: pnpm run netlify-prebuild:test-prebuild-functions

Tested: pnpm run netlify-prebuild
2026-04-25 15:13:29 -05:00
ThatGuySam
eaeff51cc9 Keep YouTube listing data available in clean builds
Netlify prebuild tests import the YouTube listing JSON at module load time, so a fresh checkout needs a baseline copy of that data before any generated API step runs. The fetched JSON is committed and explicitly unignored while the broader static/api output remains generated.

Constraint: Netlify runs test-prebuild before Astro generation creates generated API files

Rejected: Move all API generation before tests | broader build-order change with more external dependencies

Confidence: high

Scope-risk: narrow

Directive: Keep static/api/youtube-videos.json tracked unless helpers/site-listings.js stops importing it at module load time

Tested: pnpm run with-env vite-node /tmp/fetch-youtube-videos.mjs

Tested: pnpm run with-env vitest test/prebuild/site-listings.test.js

Tested: pnpm run netlify-prebuild:test-prebuild-functions
2026-04-25 14:44:49 -05:00
7 changed files with 223 additions and 34 deletions

View file

@ -16,10 +16,33 @@ addEventListener('fetch', event => {
event.respondWith(handleRequest(event.request))
})
const canonicalHost = 'doesitarm.com'
const workersDevHost = 'doesitarm-default.samcarlton.workers.dev'
const workerHeaderName = 'x-workers-hello'
const workerHeaderValue = 'Hello from Cloudflare Workers'
function redirectToCanonicalHost(url) {
url.protocol = 'https:'
url.hostname = canonicalHost
return new Response(null, {
headers: {
Location: url.toString(),
[workerHeaderName]: workerHeaderValue
},
status: 302
})
}
// Alter Headers - https://developers.cloudflare.com/workers/examples/alter-headers
async function handleRequest(request) {
// const visitor = ua( process.env.GA_TRACKING_ID )
const url = new URL(request.url)
if (url.hostname === workersDevHost) {
return redirectToCanonicalHost(url)
}
const response = await fetch(request)
@ -28,7 +51,7 @@ async function handleRequest(request) {
const newResponse = new Response(response.body, response)
// Add a custom header with a value
newResponse.headers.append("x-workers-hello", "Hello from Cloudflare Workers")
newResponse.headers.append(workerHeaderName, workerHeaderValue)
// // Delete headers
// newResponse.headers.delete("x-header-to-delete")

View file

@ -0,0 +1,136 @@
const rawDeviceList = [
{
name: '2023 M3 Macbook Pros',
enabled: 'yes',
type: 'mac-apple-silicon',
description: '16 inch and 14 inch M3 Max Macbook Pro designed for pros. ',
amazonUrl: 'https://www.apple.com/macbook-pro/',
bhUrl: '',
adoramaUrl: '',
slug: '2023-m3-macbook-pros',
endpoint: '/device/2023-m3-macbook-pros'
},
{
name: '2023 M2 Macbook Pros',
enabled: 'yes',
type: 'mac-apple-silicon',
description: '16 inch and 14 inch M2 Max Macbook Pro designed for pros. ',
amazonUrl: 'https://amzn.to/3FEuUs1',
bhUrl: '',
adoramaUrl: '',
slug: '2023-m2-macbook-pros',
endpoint: '/device/2023-m2-macbook-pros'
},
{
name: '2023 M3 iMac',
enabled: 'yes',
type: 'mac-apple-silicon',
description: 'M3 iMac is the new Apple Silicon desktop optimized with the Apple M3 processor. ',
amazonUrl: 'https://www.apple.com/imac/',
bhUrl: '',
adoramaUrl: '',
slug: '2023-m3-imac',
endpoint: '/device/2023-m3-imac'
},
{
name: '2023 M2 Pro Mac Mini',
enabled: 'yes',
type: 'mac-apple-silicon',
description: 'M3 Mac Mini is the second generation Apple Silicon iMac for desktop.',
amazonUrl: 'https://amzn.to/40yxCsV',
bhUrl: '',
adoramaUrl: '',
slug: '2023-m2-pro-mac-mini',
endpoint: '/device/2023-m2-pro-mac-mini'
},
{
name: 'Intel Macs',
enabled: 'yes',
type: 'intel',
description: 'Intel Macs are the classic Macs that succeed the PowerPC Macs and preceeded Apple Silicon. ',
amazonUrl: 'https://amzn.to/3h3LQwR',
bhUrl: '',
adoramaUrl: '',
slug: 'intel-macs',
endpoint: '/device/intel-macs'
},
{
name: 'iPad',
enabled: 'no',
type: 'ios',
description: '',
amazonUrl: 'https://amzn.to/3b67Inz',
bhUrl: '',
adoramaUrl: '',
slug: 'ipad',
endpoint: '/device/ipad'
},
{
name: 'iPhone',
enabled: 'no',
type: 'ios',
description: '',
amazonUrl: 'https://amzn.to/3uovRxs',
bhUrl: '',
adoramaUrl: '',
slug: 'iphone',
endpoint: '/device/iphone'
},
{
name: '2021 M1 Macbook Pros',
enabled: 'no',
type: 'mac-apple-silicon',
description: 'M1 Pro Macbook Pro and M1 Max Macbook Pro are the new Apple Silicon notebooks optimized with the Apple M1 processor and the first Apple silicon designed for pros. ',
amazonUrl: 'https://amzn.to/3jiwNQh',
bhUrl: '',
adoramaUrl: '',
slug: '2021-m1-macbook-pros',
endpoint: '/device/2021-m1-macbook-pros'
},
{
name: 'M1 Macbook Pro',
enabled: 'no',
type: 'mac-apple-silicon',
description: 'M1 Macbook Pro is the portable first generation Apple Silicon Mac for professionals.',
amazonUrl: 'https://amzn.to/3h3BkG1',
bhUrl: '',
adoramaUrl: '',
slug: 'm1-macbook-pro',
endpoint: '/device/m1-macbook-pro'
},
{
name: 'M1 Macbook Air',
enabled: 'no',
type: 'mac-apple-silicon',
description: 'M1 Macbook Air is the portable first generation Apple Silicon Mac for users needing a lighter computer.',
amazonUrl: 'https://amzn.to/3elKVX5',
bhUrl: '',
adoramaUrl: '',
slug: 'm1-macbook-air',
endpoint: '/device/m1-macbook-air'
},
{
name: 'M1 iPad Pro',
enabled: 'no',
type: 'ios',
description: 'M1 iPad Pro is the first mobile Apple device featuring the M1 Apple processor. ',
amazonUrl: 'https://amzn.to/3nQY3qe',
bhUrl: '',
adoramaUrl: '',
slug: 'm1-ipad-pro',
endpoint: '/device/m1-ipad-pro'
},
{
name: 'M1 iMac',
enabled: 'no',
type: 'mac-apple-silicon',
description: 'M1 iMac is the new Apple Silicon desktop optimized with the Apple M1 processor and the first of the second generation of Apple Silicon Macs. ',
amazonUrl: 'https://amzn.to/3vMUnbA',
bhUrl: '',
adoramaUrl: '',
slug: 'm1-imac',
endpoint: '/device/m1-imac'
}
]
export const deviceListingFallbacks = rawDeviceList.filter( device => device.enabled !== 'no' )

View file

@ -1,37 +1,13 @@
import youtubeVideosText from '~/static/api/youtube-videos.json?raw'
import appListText from '~/static/app-list.json?raw'
import deviceListText from '~/static/device-list.json?raw'
import gameListText from '~/static/game-list.json?raw'
import videoListingsText from '~/static/api/youtube-video-listings.json?raw'
import {
buildVideoListingFromFetchedVideo,
makeVideoSlug
} from '~/helpers/build-video-list.js'
const trailingCommaPattern = /,\s*([\]}])/g
const parsedDeviceList = JSON.parse( deviceListText.replace( trailingCommaPattern, '$1' ) )
const parsedAppList = JSON.parse( appListText.replace( trailingCommaPattern, '$1' ) )
const parsedGameList = JSON.parse( gameListText.replace( trailingCommaPattern, '$1' ) )
const parsedYoutubeVideos = JSON.parse( youtubeVideosText )
import { deviceListingFallbacks } from './device-list-fallbacks.js'
const parsedVideoListings = JSON.parse( videoListingsText )
export function getDeviceListingBySlug ( slug ) {
return parsedDeviceList.find( device => device.slug === slug ) || null
}
function getAllVideoAppsList () {
return [
...parsedAppList,
...parsedGameList
]
return deviceListingFallbacks.find( device => device.slug === slug ) || null
}
export async function getVideoListingBySlug ( slug ) {
const allVideoAppsList = getAllVideoAppsList()
for ( const [ videoId, fetchedVideo ] of Object.entries( parsedYoutubeVideos ) ) {
if ( makeVideoSlug( fetchedVideo.title, videoId ) !== slug ) continue
return await buildVideoListingFromFetchedVideo( fetchedVideo, videoId, allVideoAppsList )
}
return null
return parsedVideoListings[slug] || null
}

View file

@ -0,0 +1,52 @@
import fs from 'fs-extra'
import {
buildVideoListingFromFetchedVideo,
makeVideoSlug
} from '~/helpers/build-video-list.js'
import { saveYouTubeVideos, youtubeVideoPath } from '~/helpers/api/youtube/build.js'
const outputPath = './static/api/youtube-video-listings.json'
const trailingCommaPattern = /,\s*([\]}])/g
async function readJsonWithTrailingCommaFallback ( path ) {
return JSON.parse(
( await fs.readFile( path, 'utf8' ) ).replace( trailingCommaPattern, '$1' )
)
}
await saveYouTubeVideos()
const [
fetchedVideos,
appList,
gameList
] = await Promise.all([
fs.readJson( youtubeVideoPath ),
readJsonWithTrailingCommaFallback( './static/app-list.json' ),
readJsonWithTrailingCommaFallback( './static/game-list.json' )
])
const allVideoAppsList = [
...appList,
...gameList
]
const videoListingsBySlug = {}
for ( const [ videoId, fetchedVideo ] of Object.entries( fetchedVideos ) ) {
const videoListing = await buildVideoListingFromFetchedVideo(
fetchedVideo,
videoId,
allVideoAppsList
)
if ( videoListing === undefined ) continue
videoListingsBySlug[ makeVideoSlug( fetchedVideo.title, videoId ) ] = videoListing
}
await fs.outputJson( outputPath, videoListingsBySlug )
console.log(
`Wrote ${ Object.keys( videoListingsBySlug ).length } video listing fallbacks to ${ outputPath }`
)

View file

@ -1,2 +1,3 @@
*
!.gitignore
!youtube-video-listings.json

File diff suppressed because one or more lines are too long

View file

@ -7,9 +7,9 @@ import {
describe( 'site listing fallbacks', () => {
it( 'loads known devices from the bundled device list', () => {
expect( getDeviceListingBySlug( 'm1-imac' ) ).toMatchObject({
name: 'M1 iMac',
endpoint: '/device/m1-imac'
expect( getDeviceListingBySlug( '2023-m3-imac' ) ).toMatchObject({
name: '2023 M3 iMac',
endpoint: '/device/2023-m3-imac'
})
})