Skip to content

Commit 61b1001

Browse files
btholtclaude
andcommitted
switch SEO tool from OpenAI SDK to Vercel AI SDK
Refactored the summary/index.js to use the Vercel AI SDK instead of the OpenAI SDK directly. This allows easily swapping between AI providers: - Defaults to Claude (Anthropic) if ANTHROPIC_API_KEY is set - Falls back to GPT-5 mini if only OPENAI_API_KEY is available - Removed direct openai dependency, now uses @ai-sdk/openai - Added ai, @ai-sdk/anthropic, @ai-sdk/openai as devDependencies - Updated README to document the new provider options Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent fcf42da commit 61b1001

4 files changed

Lines changed: 185 additions & 51 deletions

File tree

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ Each of these lessons can have a [frontmatter](https://github.com/jonschlinkert/
8080
- _description_ – If you want to give individual lessons descriptions for SEO and for Frontend Masters, you can write a brief description here.
8181
- _keywords_ - If you want to give individual lessons keywords for SEO purposes, write a comma separated list
8282

83-
🤖: now the course starter can auto-generate the description and keywords for you using ChatGPT. See below how to.
83+
🤖: now the course starter can auto-generate the description and keywords for you using AI (Claude or GPT). See below how to.
8484

8585
Be aware because of how the numbers and letters are stripped out, it is possible to have ambigious paths. `01-welcome/A-intro.md` and `03-welcome/D-intro.md` would resolve to the same thing and only the first one would be visitable.
8686

@@ -132,7 +132,7 @@ _Future pushes to the main branch will automatically trigger a new deployment._
132132
- `npm run start` - Start an already-built server.
133133
- `npm run csv` – Will generate the CSV of the metadata from your course. Note you may have to run build first, depending on your csvPath.
134134
- `npm run llm-text` – Will generate the LLM full text of your course - this concatenates all your lessons together into one long text document that students can load into an LLM to get additional help. Also useful for editing - just ask your LLM to look at all the text and fix grammar/spelling/content errors.
135-
- `npm run seo` – Using ChatGPT, every file that does not have a description, ChatGPT will generate a description and keywords and write them to the file. Requires you to set a valid `OPENAI_API_KEY` (which means having a paid OpenAI account) using a [.env](https://github.com/motdotla/dotenv) or just by setting it in the environment. If a description already exists, this will skip it.
135+
- `npm run seo` – Using AI, every file that does not have a description will have a description and keywords generated and written to the file. Set either `ANTHROPIC_API_KEY` (for Claude, preferred) or `OPENAI_API_KEY` (for GPT-5 mini) using a [.env](https://github.com/motdotla/dotenv) file or by setting it in the environment. If both are set, Claude is used. If a description already exists, this will skip it.
136136

137137
## Analytics
138138

package-lock.json

Lines changed: 149 additions & 24 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,11 @@
2323
"title-case": "^4.3.2"
2424
},
2525
"devDependencies": {
26+
"@ai-sdk/anthropic": "^3.0.35",
27+
"@ai-sdk/openai": "^3.0.25",
28+
"ai": "^6.0.68",
2629
"convert-array-to-csv": "^2.0.0",
2730
"dotenv": "^17.2.3",
28-
"openai": "^6.17.0",
2931
"zod": "^4.3.6"
3032
}
3133
}

summary/index.js

Lines changed: 31 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
1-
import OpenAI from "openai";
1+
import { generateObject } from "ai";
2+
import { anthropic } from "@ai-sdk/anthropic";
3+
import { openai } from "@ai-sdk/openai";
24
import matter from "gray-matter";
35
import "dotenv/config";
46
import fs from "fs/promises";
57
import { getLesson, getLessons } from "../data/lesson.js";
6-
import { zodResponseFormat } from "openai/helpers/zod";
7-
import getPrompt from "./getSystemPrompt.js";
8+
import getPrompt from "./getPrompt.js";
89
import assert from "assert";
910
import path from "path";
1011
import { z } from "zod";
@@ -17,13 +18,27 @@ const configBuffer = await fs.readFile(
1718
);
1819
const config = JSON.parse(configBuffer);
1920

20-
const openai = new OpenAI();
21+
// Determine which provider to use based on available API keys
22+
// Defaults to Anthropic (Claude), falls back to OpenAI
23+
const useAnthropic = !!process.env.ANTHROPIC_API_KEY;
24+
const useOpenAI = !!process.env.OPENAI_API_KEY;
2125

2226
assert(
23-
process.env.OPENAI_API_KEY,
24-
"OPENAI_API_KEY must exist. Either pass it in via the environment or define it in the .env file"
27+
useAnthropic || useOpenAI,
28+
"Either ANTHROPIC_API_KEY or OPENAI_API_KEY must be set. Pass it via the environment or define it in a .env file"
2529
);
2630

31+
// Configure the model - prefer Anthropic if available
32+
const model = useAnthropic
33+
? anthropic("claude-sonnet-4-20250514")
34+
: openai("gpt-5-mini");
35+
36+
if (useAnthropic) {
37+
console.log("🤖 Using Claude (Anthropic)");
38+
} else {
39+
console.log("🤖 Using GPT-5 mini (OpenAI)");
40+
}
41+
2742
async function exec() {
2843
const list = await getLessons();
2944

@@ -43,27 +58,19 @@ async function summarize(section, lesson) {
4358
console.log(`⏺️ ${lesson.fullSlug}`);
4459
} else {
4560
try {
46-
const completion = await openai.beta.chat.completions.parse({
47-
model: "gpt-4o-2024-08-06",
48-
messages: [
49-
{ role: "system", content: getPrompt(config) },
50-
{
51-
role: "user",
52-
content: `The markdown content is: \n\n\n${rendered.markdown}`,
53-
},
54-
],
55-
response_format: zodResponseFormat(
56-
z.object({
57-
seoDescription: z.string(),
58-
seoKeywords: z.array(z.string()),
59-
}),
60-
"lesson"
61-
),
61+
const { object } = await generateObject({
62+
model,
63+
system: getPrompt(config),
64+
prompt: `The markdown content is: \n\n\n${rendered.markdown}`,
65+
schema: z.object({
66+
seoDescription: z.string(),
67+
seoKeywords: z.array(z.string()),
68+
}),
6269
});
6370

6471
const parsed = {
65-
description: completion.choices[0].message.parsed.seoDescription,
66-
keywords: completion.choices[0].message.parsed.seoKeywords,
72+
description: object.seoDescription,
73+
keywords: object.seoKeywords,
6774
};
6875

6976
const newData = Object.assign({}, data, parsed);

0 commit comments

Comments
 (0)