Skip to content
Open
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: 2 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -68,9 +68,10 @@ image-check: build ## Build site and check all images resolve
cd astro && PORT=9999 LINK_CHECK=1 LINK_CHECK_PREVIEW=1 npx playwright test link-check
.PHONY: image-check

validate-metadata: ## Validate news and events frontmatter schemas
validate-metadata: ## Validate news, events, and did-you-know schemas
python scripts/validate_news.py --be-strict-from 2026-02-24
python scripts/validate_events.py --be-strict-from 2026-02-24
python scripts/validate_did_you_know.py
.PHONY: validate-metadata

help:
Expand Down
52 changes: 48 additions & 4 deletions astro/src/build/preprocess.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ const PUBLIC_IMAGES_DIR = path.join(ASTRO_ROOT, 'public/images');
const PUBLIC_ASSETS_DIR = path.join(ASTRO_ROOT, 'public/assets');
const PUBLIC_MEDIA_DIR = path.join(ASTRO_ROOT, 'public/media');
const NAVBAR_DEST_DIR = path.join(ASTRO_CONTENT_DIR, 'navbars');
const DID_YOU_KNOW_DEST_DIR = path.join(ASTRO_CONTENT_DIR, 'did-you-know');

/**
* Shared glob ignore patterns for content file discovery.
Expand Down Expand Up @@ -622,6 +623,27 @@ async function processNavbar(filePath) {
return { source: filePath, destination: destPath, collection: 'navbars', slug: `${navName}/navbar` };
}

/**
* Process "Did you know" item YAML files from content/did-you-know/**.
* Files are copied verbatim (preserving relative path) into src/content/did-you-know/.
*/
async function processDidYouKnow(filePath) {
const relativePath = path.relative(CONTENT_DIR, filePath);
const relativeWithin = relativePath.replace(/^did-you-know\//, '');
const destPath = path.join(DID_YOU_KNOW_DEST_DIR, relativeWithin);

await fs.promises.mkdir(path.dirname(destPath), { recursive: true });
await fs.promises.copyFile(filePath, destPath);

return { source: filePath, destination: destPath, collection: 'did-you-know' };
}

/** True when a content file lives under the did-you-know/ directory. */
function isDidYouKnowFile(filePath) {
const relativePath = path.relative(CONTENT_DIR, filePath).replace(/\\/g, '/');
return relativePath.startsWith('did-you-know/');
}

/**
* Process items in batches to avoid file table overflow
*/
Expand Down Expand Up @@ -686,10 +708,19 @@ export async function preprocessContent(options = {}) {

// Only process YAML files that are true datasets, not platform-specific ones
// Platform-specific files (in use/*/) would overwrite each other since they have same basenames
const yamlFiles = await glob('**/*.{yml,yaml}', {
// Exclude did-you-know/** — those are handled by a dedicated collection below.
const yamlFiles = (
await glob('**/*.{yml,yaml}', {
cwd: CONTENT_DIR,
absolute: true,
ignore: CONTENT_IGNORE,
})
).filter((file) => !isDidYouKnowFile(file));

const didYouKnowFiles = await glob('did-you-know/**/*.{yml,yaml}', {
cwd: CONTENT_DIR,
absolute: true,
ignore: CONTENT_IGNORE,
ignore: ['**/node_modules/**'],
});

const navbarFiles = await glob('**/navbar.{yml,yaml}', {
Expand All @@ -703,6 +734,7 @@ export async function preprocessContent(options = {}) {
console.log(`Found ${markdownFiles.length} markdown files`);
console.log(`Found ${nonNavbarYamlFiles.length} YAML files`);
console.log(`Found ${navbarFiles.length} navbar files`);
console.log(`Found ${didYouKnowFiles.length} did-you-know files`);
console.log('');

// Process markdown files in batches
Expand All @@ -720,8 +752,11 @@ export async function preprocessContent(options = {}) {
console.log('Processing navbar files...');
const { results: navbarResults, errors: navbarErrors } = await processBatch(navbarFiles, processNavbar, 50);

const results = [...mdResults, ...yamlResults, ...navbarResults];
const errors = mdErrors + yamlErrors + navbarErrors;
console.log('Processing did-you-know files...');
const { results: dykResults, errors: dykErrors } = await processBatch(didYouKnowFiles, processDidYouKnow, 50);

const results = [...mdResults, ...yamlResults, ...navbarResults, ...dykResults];
const errors = mdErrors + yamlErrors + navbarErrors + dykErrors;

// Check for duplicate slugs within the same collection (skip datasets — they use filenames, not slugs)
const slugMap = new Map();
Expand Down Expand Up @@ -955,6 +990,8 @@ async function watchContent() {
}
} else if (path.basename(fullPath) === 'navbar.yml' || path.basename(fullPath) === 'navbar.yaml') {
await processNavbar(fullPath);
} else if (isDidYouKnowFile(fullPath)) {
await processDidYouKnow(fullPath);
} else {
await processDataset(fullPath);
}
Expand Down Expand Up @@ -999,6 +1036,13 @@ async function watchContent() {
const navName = relativeDir === '.' ? 'global' : relativeDir;
const dest = path.join(NAVBAR_DEST_DIR, navName, path.basename(fullPath));
await fs.promises.rm(dest, { force: true });
} else if (isDidYouKnowFile(fullPath)) {
const relativeWithin = path
.relative(CONTENT_DIR, fullPath)
.replace(/\\/g, '/')
.replace(/^did-you-know\//, '');
const dest = path.join(DID_YOU_KNOW_DEST_DIR, relativeWithin);
await fs.promises.rm(dest, { force: true });
} else {
const dest = path.join(ASTRO_CONTENT_DIR, 'datasets', path.basename(fullPath));
await fs.promises.rm(dest, { force: true });
Expand Down
175 changes: 175 additions & 0 deletions astro/src/components/did-you-know/DidYouKnowCard.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
<script setup lang="ts">
import { ref, onMounted } from 'vue';
import { renderMarkdown } from '@/utils/markdown';
import { Button } from '@/components/ui/button';

export interface DykImage {
url: string;
alt: string;
}

export interface DykLink {
url: string;
text: string;
}

export interface DykItem {
slug: string;
title: string;
tease?: string | null;
body: string;
subsites?: string[];
date?: string | null;
weight?: number | null;
images?: DykImage[] | null;
links?: DykLink[] | null;
}

const props = withDefaults(
defineProps<{
item: DykItem;
/** Open links in a new tab (used on bare/embed pages). */
newTab?: boolean;
}>(),
{ newTab: false }
);

const sampledImage = ref<DykImage | undefined>(undefined);
const sampledLink = ref<DykLink | undefined>(undefined);

function pickRandom<T>(arr: readonly T[] | null | undefined): T | undefined {
if (!arr || arr.length === 0) return undefined;
return arr[Math.floor(Math.random() * arr.length)];
}

onMounted(() => {
sampledImage.value = pickRandom(props.item.images);
sampledLink.value = pickRandom(props.item.links);
});

// Re-sample the image and link (e.g. a "shuffle" action).
function resample() {
sampledImage.value = pickRandom(props.item.images);
sampledLink.value = pickRandom(props.item.links);
}

defineExpose({ resample });

const bodyHtml = renderMarkdown(props.item.body);
const linkTarget = props.newTab ? '_blank' : undefined;
const linkRel = props.newTab ? 'noopener noreferrer' : undefined;
</script>

<template>
<article class="dyk-card">
<header class="dyk-header">
<h3 class="dyk-title">{{ item.title }}</h3>
<p v-if="item.tease" class="dyk-tease">{{ item.tease }}</p>
</header>

<figure v-if="sampledImage" class="dyk-figure">
<img :src="sampledImage.url" :alt="sampledImage.alt" :title="sampledImage.alt" loading="lazy" class="dyk-image" />
<figcaption class="dyk-caption">{{ sampledImage.alt }}</figcaption>
</figure>

<div class="dyk-body" v-html="bodyHtml" />

<footer v-if="sampledLink" class="dyk-footer">
<Button
as="a"
:href="sampledLink.url"
:target="linkTarget"
:rel="linkRel"
variant="default"
size="sm"
class="dyk-link-btn"
>
{{ sampledLink.text }}
</Button>
</footer>
</article>
</template>

<style scoped>
.dyk-card {
display: flex;
flex-direction: column;
gap: 0.75rem;
background: #fff;
border: 1px solid #e2e8f0;
border-radius: 0.5rem;
padding: 1.25rem;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
height: 100%;
}

.dyk-title {
font-size: 1.125rem;
font-weight: 600;
color: #25537b;
margin: 0;
line-height: 1.3;
}

.dyk-tease {
margin: 0.25rem 0 0;
font-size: 0.875rem;
color: #64748b;
line-height: 1.4;
}

.dyk-figure {
margin: 0;
}

.dyk-image {
width: 100%;
height: auto;
border-radius: 0.375rem;
border: 1px solid #e2e8f0;
display: block;
}

.dyk-caption {
margin-top: 0.375rem;
font-size: 0.75rem;
color: #64748b;
line-height: 1.3;
}

.dyk-body {
font-size: 0.9rem;
color: #334155;
line-height: 1.55;
}

.dyk-body :deep(p) {
margin: 0 0 0.5rem;
}

.dyk-body :deep(p:last-child) {
margin-bottom: 0;
}

.dyk-footer {
margin-top: auto;
padding-top: 0.5rem;
border-top: 1px solid #f1f5f9;
text-align: center;
}

.dyk-link-btn {
text-decoration: none;
}
</style>

<style>
/* Override prose link styling on bare pages where the card lives inside
article.prose-galaxy — without this, .prose-galaxy a { color: #25537b }
makes the button text the same color as its primary background. */
.prose-galaxy .dyk-link-btn,
.prose-galaxy .dyk-link-btn:hover {
color: var(--color-primary-foreground, #fff);
text-decoration: none;
}
</style>
Loading
Loading