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
5 changes: 5 additions & 0 deletions .github/workflows/docs-checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ on:
- 'scripts/check-stable-ids.ts'
- 'scripts/ensure-stable-ids.ts'
- 'scripts/redirects-sync.ts'
- 'scripts/tutorial-cards.ts'
- 'public/img/tutorials/**'
- 'package.json'
- 'pnpm-lock.yaml'
- '.github/workflows/docs-checks.yml'
Expand Down Expand Up @@ -46,6 +48,9 @@ jobs:
- name: Check stable IDs
run: pnpm stable-ids:check

- name: Check tutorial cards
run: pnpm tutorial-cards:check

# Redirect check diffs against origin/main, so it only makes sense as a
# pre-merge gate on PRs. On push to main the merge already landed and
# origin/main points at the pushed commit (self-comparison), so skip it.
Expand Down
6 changes: 2 additions & 4 deletions app/components/TutorialCard.vue
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,8 @@ const props = defineProps<{
};
}>();

const imageSrc = computed(() => {
const technologies = props.article.technologies?.length ? props.article.technologies : ['directus'];
return `/docs/api/tutorialimg?logos=${technologies.join(', ')}`;
});
// Cards are baked by `pnpm tutorial-cards`.
const imageSrc = computed(() => `/docs/img/tutorials/cards/${props.article.technologies?.[0] ?? 'directus'}.jpg`);
</script>

<template>
Expand Down
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
"postinstall": "node scripts/generate-api-reference.ts && nuxt prepare",
"stable-ids:ensure": "node scripts/ensure-stable-ids.ts",
"stable-ids:check": "node scripts/check-stable-ids.ts",
"tutorial-cards": "node scripts/tutorial-cards.ts",
"tutorial-cards:check": "node scripts/tutorial-cards.ts --check",
"redirects:sync": "node scripts/redirects-sync.ts --write-deterministic --fail-on-unresolved",
"redirects:check": "node scripts/redirects-sync.ts --fail-on-unresolved --no-write",
"typecheck:scripts": "tsc -p scripts/tsconfig.json",
Expand Down Expand Up @@ -53,7 +55,6 @@
"openapi3-ts": "4.5.0",
"posthog-js": "1.371.2",
"posthog-node": "5.29.7",
"sharp": "^0.34.5",
"shiki": "4.0.2",
"shiki-stream": "^0.1.4",
"tailwindcss": "^4.2.4",
Expand All @@ -79,6 +80,7 @@
"remark": "^15.0.1",
"remark-mdc": "^3.11.0",
"remark-parse": "^11.0.0",
"sharp": "^0.34.5",
"typescript": "6.0.3",
"vitest": "^4.1.7",
"vue-tsc": "^3.2.7"
Expand Down
6 changes: 3 additions & 3 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Binary file added public/img/tutorials/cards/airbyte.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/img/tutorials/cards/algolia.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/img/tutorials/cards/angular.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/img/tutorials/cards/astro.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/img/tutorials/cards/aws.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/img/tutorials/cards/azure.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/img/tutorials/cards/chrome.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/img/tutorials/cards/clarifai.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/img/tutorials/cards/clay.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/img/tutorials/cards/clearbit.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/img/tutorials/cards/crowdin.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/img/tutorials/cards/db.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/img/tutorials/cards/deepgram.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/img/tutorials/cards/digitalocean.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/img/tutorials/cards/directus.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/img/tutorials/cards/django.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/img/tutorials/cards/elasticsearch.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/img/tutorials/cards/eleventy.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/img/tutorials/cards/flask.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/img/tutorials/cards/flutter.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/img/tutorials/cards/framer.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/img/tutorials/cards/gcp.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/img/tutorials/cards/github.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/img/tutorials/cards/inngest.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/img/tutorials/cards/javascript.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/img/tutorials/cards/kotlin.jpg
Binary file added public/img/tutorials/cards/kubernetes.jpg
Binary file added public/img/tutorials/cards/meilisearch.jpg
Binary file added public/img/tutorials/cards/n8n.jpg
Binary file added public/img/tutorials/cards/netlify.jpg
Binary file added public/img/tutorials/cards/nextjs.jpg
Binary file added public/img/tutorials/cards/northflank.jpg
Binary file added public/img/tutorials/cards/notion.jpg
Binary file added public/img/tutorials/cards/nuxt.jpg
Binary file added public/img/tutorials/cards/okta.jpg
Binary file added public/img/tutorials/cards/openai.jpg
Binary file added public/img/tutorials/cards/railway.jpg
Binary file added public/img/tutorials/cards/react.jpg
Binary file added public/img/tutorials/cards/render.jpg
Binary file added public/img/tutorials/cards/sentry.jpg
Binary file added public/img/tutorials/cards/solidstart.jpg
Binary file added public/img/tutorials/cards/spring-boot.jpg
Binary file added public/img/tutorials/cards/stripe.jpg
Binary file added public/img/tutorials/cards/supabase.jpg
Binary file added public/img/tutorials/cards/sveltekit.jpg
Binary file added public/img/tutorials/cards/swift.jpg
Binary file added public/img/tutorials/cards/twilio.jpg
Binary file added public/img/tutorials/cards/ubuntu.jpg
Binary file added public/img/tutorials/cards/vercel.jpg
Binary file added public/img/tutorials/cards/vonage.jpg
Binary file added public/img/tutorials/cards/vue.jpg
Binary file added public/img/tutorials/cards/wordpress.jpg
Binary file added public/img/tutorials/cards/zapier.jpg
File renamed without changes
File renamed without changes
102 changes: 102 additions & 0 deletions scripts/tutorial-cards.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
#!/usr/bin/env node

import fs from 'node:fs';
import path from 'node:path';

import sharp from 'sharp';

import { getFrontmatterBlock, listRoutableContentFiles } from './_content-lib.ts';

const IMAGES_DIR = 'public/img/tutorials';
const CARDS_DIR = path.join(IMAGES_DIR, 'cards');
const FALLBACK_LOGO = 'directus';

const args = process.argv.slice(2);

if (args.includes('--help') || args.includes('-h')) {
console.log(`tutorial-cards

Bakes a card image for the first technology of every tutorial into ${CARDS_DIR}.
Technologies without a logo in ${IMAGES_DIR} get the Directus card.

Usage:
node scripts/tutorial-cards.ts Write missing cards
node scripts/tutorial-cards.ts --force Rewrite every card
node scripts/tutorial-cards.ts --check Exit 1 if any card is missing
`);
process.exit(0);
}

function firstTechnology(source: string): string | undefined {
const block = getFrontmatterBlock(source);
if (!block) return undefined;

const match = block.body.match(/^technologies:\s*(?:\[\s*([^\],]+)|\r?\n\s*-\s*(.+))/m);
const value = match?.[1] ?? match?.[2];
return value?.trim().replace(/^['"]|['"]$/g, '') || undefined;
}

function cardPath(technology: string): string {
return path.join(CARDS_DIR, `${technology}.jpg`);
}

async function writeCard(technology: string): Promise<void> {
const logoFile = path.join(IMAGES_DIR, `${technology}.png`);
const logo = fs.readFileSync(fs.existsSync(logoFile) ? logoFile : path.join(IMAGES_DIR, `${FALLBACK_LOGO}.png`));
const background = fs.readFileSync(path.join(IMAGES_DIR, 'background.png'));
const container = fs.readFileSync(path.join(IMAGES_DIR, 'logo-container.png'));

const [backgroundMeta, containerMeta, logoMeta] = await Promise.all([
sharp(background).metadata(),
sharp(container).metadata(),
sharp(logo).metadata(),
]);

const left = Math.floor((backgroundMeta.width - containerMeta.width) / 2);
const top = Math.floor((backgroundMeta.height - containerMeta.height) / 2);

const composed = await sharp(background)
.composite([
{ input: container, top, left },
{
input: logo,
top: Math.floor(top + (containerMeta.height - logoMeta.height - 72) / 2),
left: Math.floor(left + (containerMeta.width - logoMeta.width) / 2),
},
])
.png()
.toBuffer();

// Cards render at most ~400px wide, so half size is still sharp on 2x screens.
await sharp(composed).resize({ width: 960 }).jpeg({ quality: 82, mozjpeg: true }).toFile(cardPath(technology));
}

async function main(): Promise<void> {
const technologies = new Set<string>([FALLBACK_LOGO]);
for (const file of listRoutableContentFiles()) {
const technology = firstTechnology(fs.readFileSync(file, 'utf8'));
if (technology) technologies.add(technology);
}

const force = args.includes('--force');
const missing = [...technologies].filter(technology => force || !fs.existsSync(cardPath(technology)));

if (args.includes('--check')) {
if (missing.length) {
console.error(`Missing tutorial cards: ${missing.join(', ')}`);
console.error('Run `pnpm tutorial-cards` and commit the result.');
process.exit(1);
}
console.log(`All ${technologies.size} tutorial cards present.`);
return;
}

fs.mkdirSync(CARDS_DIR, { recursive: true });
for (const technology of missing) {
await writeCard(technology);
console.log(`Wrote ${cardPath(technology)}`);
}
console.log(`${missing.length} card(s) written, ${technologies.size} total.`);
}

await main();
77 changes: 0 additions & 77 deletions server/api/tutorialimg.ts

This file was deleted.

Loading