diff --git a/scripts/check-no-js.mjs b/scripts/check-no-js.mjs
index 7098320..051a252 100644
--- a/scripts/check-no-js.mjs
+++ b/scripts/check-no-js.mjs
@@ -57,6 +57,7 @@ const ANALYTICS_SCRIPT_SRC_PATTERN =
function isSafeScriptTag(tag) {
if (tag.includes('data-theme-script')) return true;
if (tag.includes('data-thumbnail-iframe-script')) return true;
+ if (tag.includes('data-video-thumbnail-script')) return true;
if (ANALYTICS_SCRIPT_SRC_PATTERN.test(tag)) return true;
const typeMatch = tag.match(/\btype=["']([^"']+)["']/i);
if (!typeMatch) return false;
diff --git a/src/components/PostThumbnail.astro b/src/components/PostThumbnail.astro
index 7723e23..bb74768 100644
--- a/src/components/PostThumbnail.astro
+++ b/src/components/PostThumbnail.astro
@@ -1,13 +1,15 @@
---
import { Picture } from 'astro:assets';
import type { CollectionEntry } from 'astro:content';
-import { absoluteUrl } from '../lib/site';
+import VideoThumbnail from './VideoThumbnail.astro';
+import { absoluteUrl, getHeaderVideo } from '../lib/site';
interface Props {
post: CollectionEntry<'posts'>;
}
const { post } = Astro.props;
+const headerVideo = getHeaderVideo(post);
const demoLink = post.data.links.find(
(link) => !link.download && link.label.trim().toLowerCase() === 'demo'
);
@@ -54,66 +56,81 @@ for (const root of document.querySelectorAll('.post-thumbnail--iframe')) {
`;
---
-
-
+{
+ headerVideo ? (
+
+ ) : (
+
+
- {
- iframeSrc && (
- <>
-
-
-
- >
- )
- }
-
+ {iframeSrc && (
+ <>
+
+
+
+ >
+ )}
+
+ )
+}
{
- iframeSrc && (
+ iframeSrc && !headerVideo && (
)
}
diff --git a/src/components/ProjectList.astro b/src/components/ProjectList.astro
index 622d214..6395ef1 100644
--- a/src/components/ProjectList.astro
+++ b/src/components/ProjectList.astro
@@ -2,8 +2,9 @@
import type { CollectionEntry } from 'astro:content';
import { getEntry } from 'astro:content';
import EntryThumbnail from './EntryThumbnail.astro';
-import ProjectLinks from './ProjectLinks.astro';
-import { PROJECT_THUMBNAIL, articlePath, entrySlug } from '../lib/site';
+import VideoThumbnail from './VideoThumbnail.astro';
+import type { HeaderVideo } from '../lib/site';
+import { PROJECT_THUMBNAIL, articlePath, entrySlug, getHeaderVideo } from '../lib/site';
interface Props {
projects: CollectionEntry<'projects'>[];
@@ -19,15 +20,36 @@ const {
eagerThumbnailCount = eagerFirstThumbnail ? 1 : 0,
} = Astro.props;
+function isExternal(url: string) {
+ return /^https?:\/\//.test(url);
+}
+
// The `essay` field is a `reference('posts')`, so when present it's always a
// `{ collection, id }` shape that `getEntry` resolves to a CollectionEntry.
-// Drafts are skipped because their article page is not built.
+// Drafts are skipped because their article page is not built. A project may
+// have no essay (no article) just as an article may have no project; the
+// relationship is optional in both directions.
const essayHrefs = new Map();
+// When the linked article has a header video, the card thumbnail becomes a
+// click-to-play poster (the card body still opens the project site).
+const essayVideos = new Map();
for (const project of projects) {
const essay = project.data.essay;
if (!essay) continue;
const resolved = await getEntry(essay);
- if (resolved && !resolved.data.draft) essayHrefs.set(project.id, articlePath(resolved));
+ if (!resolved || resolved.data.draft) continue;
+ essayHrefs.set(project.id, articlePath(resolved));
+ const headerVideo = getHeaderVideo(resolved);
+ if (headerVideo) essayVideos.set(project.id, headerVideo);
+}
+
+// The whole card opens the project's website: the first link that isn't a
+// download (Source / Live / Demo / Site / package page, in author order).
+// The Open button is that link, and its overlay makes the entire card
+// clickable. Projects without such a link have no Open button and are not
+// clickable; their article, if any, is reachable through the Article link.
+function websiteUrl(project: CollectionEntry<'projects'>) {
+ return project.data.links.find((link) => !link.download)?.url;
}
---
@@ -37,36 +59,71 @@ for (const project of projects) {
const anchor = entrySlug(project);
const titleId = `${anchor}-title`;
const essayHref = essayHrefs.get(project.id);
- const primaryHref = essayHref ?? project.data.links[0]?.url;
+ const headerVideo = essayVideos.get(project.id);
+ const website = websiteUrl(project);
+ const websiteExternal = website ? isExternal(website) : false;
const eager = index < eagerThumbnailCount;
return (