From 4eef4a3c2a5e624077e34c1da8072f15e2636112 Mon Sep 17 00:00:00 2001 From: Richard Poelderl Date: Mon, 18 Nov 2024 15:28:59 +0100 Subject: [PATCH] fix(glossary): various workflow fixes & latest content (#2664) * fix: move statelessness.mdx to glossary * fix: frontmatter indentation & missing fields * feat: add `categories` & `takeaways` workflows * feat: add `categories` & `takeaways` from frontmatter * takeaways is required * feat: add seo optimized h1 * feat(glossary): Add API Security.mdx to glossary * fixing duplicate db row creation * refactor(glossary): align takeaways schema across apps - Create strongly-typed Zod schema for takeaways in both billing and www apps - Add documentation indicating billing as source of truth - Update YAML frontmatter generation to match schema structure - Ensure type safety from LLM generation through to content collection BREAKING CHANGE: Takeaways schema is now strictly typed and validated. Existing content may need to be updated to match the new schema structure. TODO: Extract schema into shared package to avoid duplication * updates to content workflow * content * content * sitemap * updated content * `pnpm fmt` * update trigger * - fix workflow to create new PRs if a previous one was closed - add slug to frontmatter * update content for SSO * no diff? * final updated content * [autofix.ci] apply automated fixes --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: chronark --- apps/billing/package.json | 8 +- .../src/lib/db-marketing/schemas/entries.ts | 30 ++- .../lib/db-marketing/schemas/searchQuery.ts | 4 +- .../db-marketing/schemas/takeaways-schema.ts | 36 ++++ apps/billing/src/lib/search-query.ts | 84 +++----- .../glossary/_generate-glossary-entry.ts | 35 +++- .../src/trigger/glossary/content-takeaways.ts | 109 ++++++++++ .../billing/src/trigger/glossary/create-pr.ts | 75 +++++-- .../src/trigger/glossary/draft-sections.ts | 21 +- .../src/trigger/glossary/generate-faqs.ts | 95 +++++++++ .../src/trigger/glossary/keyword-research.ts | 4 +- .../trigger/glossary/linking-categories.ts | 86 ++++++++ .../src/trigger/glossary/seo-meta-tags.ts | 188 +++++++++++++----- apps/www/app/glossary/[slug]/page.tsx | 19 +- apps/www/app/glossary/sitemap.ts | 22 ++ apps/www/content-collections.ts | 33 +-- apps/www/content/glossary/mime-types.mdx | 131 ++++++------ apps/www/content/glossary/single-sign-on.mdx | 162 +++++++++++++++ apps/www/content/glossary/statelessness.mdx | 96 +++++++++ .../glossary/transport-layer-security.mdx | 117 +++++++++++ apps/www/content/statelessness.mdx | 49 ----- apps/www/lib/schemas/faq-schema.ts | 16 ++ apps/www/lib/schemas/takeaways-schema.ts | 37 ++++ pnpm-lock.yaml | 128 ++++++++---- 24 files changed, 1240 insertions(+), 345 deletions(-) create mode 100644 apps/billing/src/lib/db-marketing/schemas/takeaways-schema.ts create mode 100644 apps/billing/src/trigger/glossary/content-takeaways.ts create mode 100644 apps/billing/src/trigger/glossary/generate-faqs.ts create mode 100644 apps/billing/src/trigger/glossary/linking-categories.ts create mode 100644 apps/www/app/glossary/sitemap.ts create mode 100644 apps/www/content/glossary/single-sign-on.mdx create mode 100644 apps/www/content/glossary/statelessness.mdx create mode 100644 apps/www/content/glossary/transport-layer-security.mdx delete mode 100644 apps/www/content/statelessness.mdx create mode 100644 apps/www/lib/schemas/faq-schema.ts create mode 100644 apps/www/lib/schemas/takeaways-schema.ts diff --git a/apps/billing/package.json b/apps/billing/package.json index 7756fbe18..f351e1bc5 100644 --- a/apps/billing/package.json +++ b/apps/billing/package.json @@ -22,9 +22,9 @@ "@mendable/firecrawl-js": "^1.5.2", "@octokit/rest": "^21.0.2", "@planetscale/database": "^1.16.0", - "@trigger.dev/nextjs": "3.1.2", - "@trigger.dev/sdk": "3.1.2", - "@trigger.dev/slack": "3.1.2", + "@trigger.dev/nextjs": "3.2.0", + "@trigger.dev/sdk": "3.2.0", + "@trigger.dev/slack": "3.2.0", "@unkey/billing": "workspace:^", "@unkey/clickhouse": "workspace:^", "@unkey/db": "workspace:^", @@ -35,6 +35,8 @@ "ai": "^3.4.7", "drizzle-orm": "^0.33.0", "drizzle-zod": "^0.5.1", + "github-slugger": "^2.0.0", + "js-yaml": "^4.1.0", "react-dom": "^18", "stripe": "^14.23.0", "zod": "^3.23.5" diff --git a/apps/billing/src/lib/db-marketing/schemas/entries.ts b/apps/billing/src/lib/db-marketing/schemas/entries.ts index ae0e4b73d..da7d14e04 100644 --- a/apps/billing/src/lib/db-marketing/schemas/entries.ts +++ b/apps/billing/src/lib/db-marketing/schemas/entries.ts @@ -1,9 +1,30 @@ import { relations } from "drizzle-orm"; -import { index, int, mysqlTable, text, timestamp, varchar } from "drizzle-orm/mysql-core"; +import { + index, + int, + json, + mysqlEnum, + mysqlTable, + text, + timestamp, + varchar, +} from "drizzle-orm/mysql-core"; import { createInsertSchema, createSelectSchema } from "drizzle-zod"; -import type { z } from "zod"; +import { z } from "zod"; import { searchQueries } from "./searchQuery"; import { sections } from "./sections"; +import type { Takeaways } from "./takeaways-schema"; + +export const entryStatus = ["ARCHIVED", "PUBLISHED"] as const; +export type EntryStatus = (typeof entryStatus)[number]; +export const faqSchema = z.array( + z.object({ + question: z.string(), + answer: z.string(), + }), +); + +export type FAQ = z.infer; export const entries = mysqlTable( "entries", @@ -14,6 +35,11 @@ export const entries = mysqlTable( dynamicSectionsContent: text("dynamic_sections_content"), metaTitle: varchar("meta_title", { length: 255 }), metaDescription: varchar("meta_description", { length: 255 }), + metaH1: varchar("meta_h1", { length: 255 }), + categories: json("linking_categories").$type().default([]), + status: mysqlEnum("status", entryStatus), + takeaways: json("content_takeaways").$type(), + faq: json("content_faq").$type(), createdAt: timestamp("created_at").notNull().defaultNow(), updatedAt: timestamp("updated_at") .notNull() diff --git a/apps/billing/src/lib/db-marketing/schemas/searchQuery.ts b/apps/billing/src/lib/db-marketing/schemas/searchQuery.ts index dd1c997a4..f795f3222 100644 --- a/apps/billing/src/lib/db-marketing/schemas/searchQuery.ts +++ b/apps/billing/src/lib/db-marketing/schemas/searchQuery.ts @@ -39,12 +39,12 @@ export type NewSearchQueryParams = z.infer; // every searchQuery can have an optional 1:1 serperResult searchResponses associated with it // because the fk is stored in the serperResult table, the searchQueries relation have neither fields nor references export const searchQueryRelations = relations(searchQueries, ({ one, many }) => ({ - searchResponses: one(serperSearchResponses, { + searchResponse: one(serperSearchResponses, { fields: [searchQueries.inputTerm], references: [serperSearchResponses.inputTerm], }), firecrawlResponses: many(firecrawlResponses), - entries: one(entries, { + entry: one(entries, { fields: [searchQueries.inputTerm], references: [entries.inputTerm], }), diff --git a/apps/billing/src/lib/db-marketing/schemas/takeaways-schema.ts b/apps/billing/src/lib/db-marketing/schemas/takeaways-schema.ts new file mode 100644 index 000000000..b6f5b0d8d --- /dev/null +++ b/apps/billing/src/lib/db-marketing/schemas/takeaways-schema.ts @@ -0,0 +1,36 @@ +import { z } from "zod"; + +/** + * @description Schema for glossary entry takeaways + * @sourceOfTruth This is the source of truth for the takeaways schema as it's used for database storage + * @todo Extract this schema into a shared package to avoid duplication with apps/www + */ +export const takeawaysSchema = z.object({ + tldr: z.string(), + definitionAndStructure: z.array( + z.object({ + key: z.string(), + value: z.string(), + }), + ), + historicalContext: z.array( + z.object({ + key: z.string(), + value: z.string(), + }), + ), + usageInAPIs: z.object({ + tags: z.array(z.string()), + description: z.string(), + }), + bestPractices: z.array(z.string()), + recommendedReading: z.array( + z.object({ + title: z.string(), + url: z.string(), + }), + ), + didYouKnow: z.string(), +}); + +export type Takeaways = z.infer; diff --git a/apps/billing/src/lib/search-query.ts b/apps/billing/src/lib/search-query.ts index 7ff8fd619..588a8dfae 100644 --- a/apps/billing/src/lib/search-query.ts +++ b/apps/billing/src/lib/search-query.ts @@ -1,30 +1,33 @@ import { db } from "@/lib/db-marketing/client"; import { openai } from "@ai-sdk/openai"; import { generateObject } from "ai"; -import { eq, sql } from "drizzle-orm"; +import { eq } from "drizzle-orm"; import { entries, insertSearchQuerySchema, searchQueries } from "@/lib/db-marketing/schemas"; +import type { CacheStrategy } from "@/trigger/glossary/_generate-glossary-entry"; +import { AbortTaskRunError } from "@trigger.dev/sdk/v3"; -export async function getOrCreateSearchQuery(args: { term: string }) { - const { term } = args; +export async function getOrCreateSearchQuery({ + term, + onCacheHit = "stale", +}: { term: string; onCacheHit: CacheStrategy }) { // Try to find existing search query - const existingQuery = await db.query.searchQueries.findFirst({ - where: eq(searchQueries.inputTerm, term), + const existing = await db.query.entries.findFirst({ + where: eq(entries.inputTerm, term), + with: { + searchQuery: true, + }, + orderBy: (searchQueries, { asc }) => [asc(searchQueries.createdAt)], }); - if (existingQuery) { - // Ensure entry exists even for existing query - await db - .insert(entries) - .values({ - inputTerm: term, - }) - .onDuplicateKeyUpdate({ - set: { - updatedAt: sql`now()`, - }, - }); - return existingQuery; + if (existing?.searchQuery && onCacheHit === "revalidate") { + return existing; + } + + if (!existing) { + throw new AbortTaskRunError( + `Entry not found for term: ${term}. It's likely that the keyword-research task failed.`, + ); } // Generate new search query @@ -48,40 +51,19 @@ Keep the search query as short and as simple as possible, don't use quotes aroun schema: insertSearchQuerySchema.omit({ createdAt: true, updatedAt: true }), }); - // Create both search query and entry in a transaction - await db.transaction(async (tx) => { - // Insert search query - await tx - .insert(searchQueries) - .values({ - ...generatedQuery.object, - }) - .onDuplicateKeyUpdate({ - set: { - updatedAt: sql`now()`, - }, - }); - - // Insert entry - await tx - .insert(entries) - .values({ - inputTerm: term, - }) - .onDuplicateKeyUpdate({ - set: { - updatedAt: sql`now()`, - }, - }); - }); - - const insertedQuery = await db.query.searchQueries.findFirst({ - where: eq(searchQueries.inputTerm, generatedQuery.object.inputTerm), - }); + // create the search query in the database & connect it to the entry: + const [insertedQueryId] = await db + .insert(searchQueries) + .values(generatedQuery.object) + .$returningId(); - if (!insertedQuery) { + if (!insertedQueryId) { throw new Error("Failed to insert or update search query"); } - - return insertedQuery; + return db.query.entries.findFirst({ + where: eq(entries.inputTerm, term), + with: { + searchQuery: true, + }, + }); } diff --git a/apps/billing/src/trigger/glossary/_generate-glossary-entry.ts b/apps/billing/src/trigger/glossary/_generate-glossary-entry.ts index 3bd0c0977..5b5c5d999 100644 --- a/apps/billing/src/trigger/glossary/_generate-glossary-entry.ts +++ b/apps/billing/src/trigger/glossary/_generate-glossary-entry.ts @@ -3,8 +3,10 @@ import { entries } from "@/lib/db-marketing/schemas"; import { task } from "@trigger.dev/sdk/v3"; import { AbortTaskRunError } from "@trigger.dev/sdk/v3"; import { eq } from "drizzle-orm"; +import { contentTakeawaysTask } from "./content-takeaways"; import { createPrTask } from "./create-pr"; import { draftSectionsTask } from "./draft-sections"; +import { generateFaqsTask } from "./generate-faqs"; import { generateOutlineTask } from "./generate-outline"; import { keywordResearchTask } from "./keyword-research"; import { seoMetaTagsTask } from "./seo-meta-tags"; @@ -18,9 +20,10 @@ export type CacheStrategy = "revalidate" | "stale"; * This workflow runs multiple steps sequentially: * 1. Keyword Research * 2. Generate Outline - * 3. Draft Sections + * 3. Draft Sections & Content Takeaways (in parallel) * 4. Generate SEO Meta Tags - * 5. Create PR + * 5. Generate FAQs + * 6. Create PR * * Each workflow step generates output that's stored in the database (with the exception of create PR, which stores the MDX output in the GitHub repository). * The default behaviour of every task is to always return a cached output if available. @@ -68,6 +71,11 @@ export const generateGlossaryEntryTask = task({ }; } + if (!existing) { + // create the entry in the database if it doesn't exist, so that all other tasks can rely on it existing: + await db.insert(entries).values({ inputTerm: term }); + } + // Step 1: Keyword Research console.info("1/5 - Starting keyword research..."); const keywordResearch = await keywordResearchTask.triggerAndWait({ term, onCacheHit }); @@ -86,13 +94,20 @@ export const generateGlossaryEntryTask = task({ } console.info("✓ Outline generated"); - // Step 3: Draft Sections - console.info("3/5 - Drafting sections..."); - const draftSections = await draftSectionsTask.triggerAndWait({ term, onCacheHit }); + // Step 3: Draft Sections & Content Takeaways (in parallel) + console.info("3/5 - Drafting sections and generating takeaways..."); + const [draftSections, contentTakeaways] = await Promise.all([ + draftSectionsTask.triggerAndWait({ term, onCacheHit }), + contentTakeawaysTask.triggerAndWait({ term, onCacheHit }), + ]); + if (!draftSections.ok) { throw new AbortTaskRunError(`Section drafting failed for term: ${term}`); } - console.info("✓ All sections drafted"); + if (!contentTakeaways.ok) { + throw new AbortTaskRunError(`Content takeaways generation failed for term: ${term}`); + } + console.info("✓ All sections drafted and takeaways generated"); // Step 4: Generate SEO Meta Tags console.info("4/5 - Generating SEO meta tags..."); @@ -102,6 +117,14 @@ export const generateGlossaryEntryTask = task({ } console.info("✓ SEO meta tags generated"); + // Step 4.5: Generate FAQs + console.info("4.5/5 - Generating FAQs..."); + const faqs = await generateFaqsTask.triggerAndWait({ term, onCacheHit }); + if (!faqs.ok) { + throw new AbortTaskRunError(`FAQ generation failed for term: ${term}`); + } + console.info("✓ FAQs generated"); + // Step 5: Create PR console.info("5/5 - Creating PR..."); const pr = await createPrTask.triggerAndWait({ input: term, onCacheHit }); diff --git a/apps/billing/src/trigger/glossary/content-takeaways.ts b/apps/billing/src/trigger/glossary/content-takeaways.ts new file mode 100644 index 000000000..1de5fb50e --- /dev/null +++ b/apps/billing/src/trigger/glossary/content-takeaways.ts @@ -0,0 +1,109 @@ +import { db } from "@/lib/db-marketing/client"; +import { takeawaysSchema } from "@/lib/db-marketing/schemas/takeaways-schema"; +import { openai } from "@ai-sdk/openai"; +import { task } from "@trigger.dev/sdk/v3"; +import { generateObject } from "ai"; +import { eq } from "drizzle-orm"; +import { entries, firecrawlResponses } from "../../lib/db-marketing/schemas"; +import type { CacheStrategy } from "./_generate-glossary-entry"; + +export const contentTakeawaysTask = task({ + id: "content_takeaways", + retry: { + maxAttempts: 3, + }, + run: async ({ + term, + onCacheHit = "stale" as CacheStrategy, + }: { + term: string; + onCacheHit?: CacheStrategy; + }) => { + const existing = await db.query.entries.findFirst({ + where: eq(entries.inputTerm, term), + columns: { + id: true, + inputTerm: true, + takeaways: true, + }, + }); + + if (existing?.takeaways && onCacheHit === "stale") { + return existing; + } + + // Get scraped content for context + const scrapedContent = await db.query.firecrawlResponses.findMany({ + where: eq(firecrawlResponses.inputTerm, term), + columns: { + markdown: true, + summary: true, + }, + }); + + const takeaways = await generateObject({ + model: openai("gpt-4"), + system: ` + You are an API documentation expert. Create comprehensive takeaways for API-related terms. + Focus on practical, accurate, and developer-friendly content. + Each section should be concise but informative. + For best practices, include only the 3 most critical and widely-adopted practices. + For usage in APIs, provide a maximum of 3 short, focused sentences highlighting key terms and primary use cases. + `, + prompt: ` + Term: "${term}" + + Scraped content summaries: + ${scrapedContent.map((content) => content.summary).join("\n\n")} + + Create structured takeaways covering: + 1. TLDR (brief, clear definition) + 2. Definition and structure - follow these rules: + - Each value must be instantly recognizable (think of it as a memory aid) in one word or short expression + - Maximum 1-3 words per value + - Must be instantly understandable without explanation + - Examples: + Bad: "An API gateway is a server that acts as an intermediary..." + Good: "Client-Server Bridge" or "API Request-Response Routing" + - Focus on core concepts that can be expressed in minimal words + + 3. Historical context - follow these exact formats: + Introduced: + - Use exact year if known: "1995" + - Use decade if exact year unknown: "Early 1990s" + - If truly uncertain: "Est. ~YYYY" with best estimate + - Never use explanatory sentences + + Origin: + - Format must be: "[Original Context] (${term})" + - Example: "Web Services (${term})" or "Cloud Computing (${term})" + - Keep [Original Context] to 1-2 words maximum + - Never include explanations or evolution + + Evolution: + - Format must be: "[Current State] ${term}" + - Example: "Standardized ${term}" or "Enterprise ${term}" + - Maximum 2-3 words + - Focus on current classification/status only + + 4. Usage in APIs (max 3 concise sentences covering essential terms and main use cases) + 5. Best practices (only the 3 most important and widely-used practices) + 6. Recommended reading (key resources) + 7. Interesting fact (did you know) + `, + schema: takeawaysSchema, + temperature: 0.2, + }); + + await db + .update(entries) + .set({ + takeaways: takeaways.object, + }) + .where(eq(entries.inputTerm, term)); + + return db.query.entries.findFirst({ + where: eq(entries.inputTerm, term), + }); + }, +}); diff --git a/apps/billing/src/trigger/glossary/create-pr.ts b/apps/billing/src/trigger/glossary/create-pr.ts index f057af001..bdde73278 100644 --- a/apps/billing/src/trigger/glossary/create-pr.ts +++ b/apps/billing/src/trigger/glossary/create-pr.ts @@ -3,6 +3,8 @@ import { entries } from "@/lib/db-marketing/schemas"; import { Octokit } from "@octokit/rest"; import { AbortTaskRunError, task } from "@trigger.dev/sdk/v3"; import { eq } from "drizzle-orm"; +import GithubSlugger from "github-slugger"; +import yaml from "js-yaml"; // install @types/js-yaml? import type { CacheStrategy } from "./_generate-glossary-entry"; export const createPrTask = task({ @@ -21,8 +23,9 @@ export const createPrTask = task({ id: true, inputTerm: true, githubPrUrl: true, + takeaways: true, }, - orderBy: (entries, { desc }) => [desc(entries.createdAt)], + orderBy: (entries, { asc }) => [asc(entries.createdAt)], }); if (existing?.githubPrUrl && onCacheHit === "stale") { @@ -35,20 +38,54 @@ export const createPrTask = task({ // ==== 1. Prepare MDX file ==== const entry = await db.query.entries.findFirst({ where: eq(entries.inputTerm, input), - orderBy: (entries, { desc }) => [desc(entries.createdAt)], + orderBy: (entries, { asc }) => [asc(entries.createdAt)], }); if (!entry?.dynamicSectionsContent) { throw new AbortTaskRunError( `Unable to create PR: The markdown content for the dynamic sections are not available for the entry to term: ${input}. It's likely that draft-sections.ts didn't run as expected .`, ); } - // add meta tags to content in .mdx format - const frontmatter = `--- - title: "${entry.metaTitle}" - description: "${entry.metaDescription}" - --- - `; - const mdxContent = frontmatter + entry.dynamicSectionsContent; + if (!entry.takeaways) { + throw new AbortTaskRunError( + `Unable to create PR: The takeaways are not available for the entry to term: ${input}. It's likely that content-takeaways.ts didn't run as expected.`, + ); + } + const slugger = new GithubSlugger(); + // Convert the object to YAML, ensuring the structure matches our schema + const yamlString = yaml.dump( + { + title: entry.metaTitle, + description: entry.metaDescription, + h1: entry.metaH1, + term: entry.inputTerm, + categories: entry.categories, + takeaways: { + tldr: entry.takeaways.tldr, + definitionAndStructure: entry.takeaways.definitionAndStructure, + historicalContext: entry.takeaways.historicalContext, + usageInAPIs: { + tags: entry.takeaways.usageInAPIs.tags, + description: entry.takeaways.usageInAPIs.description, + }, + bestPractices: entry.takeaways.bestPractices, + recommendedReading: entry.takeaways.recommendedReading, + didYouKnow: entry.takeaways.didYouKnow, + }, + faq: entry.faq, + updatedAt: entry.updatedAt, + slug: slugger.slug(entry.inputTerm), + }, + { + lineWidth: -1, + noRefs: true, + quotingType: '"', + }, + ); + + // Create frontmatter + const frontmatter = `---\n${yamlString}---\n`; + + const mdxContent = `${frontmatter}${entry.dynamicSectionsContent}`; const blob = new Blob([mdxContent], { type: "text/markdown" }); // Create a File object from the Blob @@ -67,7 +104,7 @@ export const createPrTask = task({ const owner = "p6l-richard"; const repo = "unkey"; const branch = `richard/add-${input.replace(/\s+/g, "-").toLowerCase()}`; - const path = `apps/www/content/${input.replace(/\s+/g, "-").toLowerCase()}.mdx`; + const path = `apps/www/content/glossary/${input.replace(/\s+/g, "-").toLowerCase()}.mdx`; // Create a new branch const mainRef = await octokit.git.getRef({ @@ -89,12 +126,16 @@ export const createPrTask = task({ if (branchExists) { console.info("2.2.1 ⚠️ Duplicate branch found, deleting it"); - await octokit.git.deleteRef({ - owner, - repo, - ref: `heads/${branch}`, - }); - console.info("2.2.2 ⌫ Branch deleted"); + try { + await octokit.git.deleteRef({ + owner, + repo, + ref: `heads/${branch}`, + }); + console.info("2.2.2 ⌫ Branch deleted"); + } catch (error) { + console.error(`2.2.3 ❌ Error deleting branch: ${error}`); + } } console.info("2.4 🛣️ Creating the new branch"); @@ -141,7 +182,7 @@ export const createPrTask = task({ githubPrUrl: true, }, where: eq(entries.inputTerm, input), - orderBy: (entries, { desc }) => [desc(entries.createdAt)], + orderBy: (entries, { asc }) => [asc(entries.createdAt)], }); return { diff --git a/apps/billing/src/trigger/glossary/draft-sections.ts b/apps/billing/src/trigger/glossary/draft-sections.ts index 8d95137a7..4133c9a1f 100644 --- a/apps/billing/src/trigger/glossary/draft-sections.ts +++ b/apps/billing/src/trigger/glossary/draft-sections.ts @@ -78,21 +78,24 @@ export const draftSectionsTask = task({ }); console.info(`Optimized dynamic sections for ${entry.inputTerm}: ${optimizedContent}`); - const [inserted] = await db - .insert(entries) - .values({ - inputTerm: entry.inputTerm, - dynamicSectionsContent: optimizedContent, + // Strip any leading single # header if present + const finalContent = optimizedContent.replace(/^#\s+[^\n]+\n/, ""); + + await db + .update(entries) + .set({ + dynamicSectionsContent: finalContent, }) - .$returningId(); - return db.query.entries.findFirst({ + .where(eq(entries.inputTerm, entry.inputTerm)); + + return await db.query.entries.findFirst({ columns: { id: true, inputTerm: true, dynamicSectionsContent: true, }, - where: eq(entries.id, inserted.id), - orderBy: (entries, { desc }) => [desc(entries.createdAt)], + where: eq(entries.id, entry.id), + orderBy: (entries, { asc }) => [asc(entries.createdAt)], }); }, }); diff --git a/apps/billing/src/trigger/glossary/generate-faqs.ts b/apps/billing/src/trigger/glossary/generate-faqs.ts new file mode 100644 index 000000000..417dc80f0 --- /dev/null +++ b/apps/billing/src/trigger/glossary/generate-faqs.ts @@ -0,0 +1,95 @@ +import { db } from "@/lib/db-marketing/client"; +import { entries } from "@/lib/db-marketing/schemas"; +import { faqSchema } from "@/lib/db-marketing/schemas/entries"; +import { openai } from "@ai-sdk/openai"; +import { AbortTaskRunError, task } from "@trigger.dev/sdk/v3"; +import { generateObject } from "ai"; +import { eq } from "drizzle-orm"; +import { z } from "zod"; +import type { CacheStrategy } from "./_generate-glossary-entry"; + +export const generateFaqsTask = task({ + id: "generate_faqs", + retry: { + maxAttempts: 3, + }, + run: async ({ + term, + onCacheHit = "stale" as CacheStrategy, + }: { + term: string; + onCacheHit?: CacheStrategy; + }) => { + const existing = await db.query.entries.findFirst({ + where: eq(entries.inputTerm, term), + with: { + searchQuery: { + with: { + searchResponse: { + with: { + serperPeopleAlsoAsk: true, + }, + }, + }, + }, + }, + orderBy: (entries, { asc }) => [asc(entries.createdAt)], + }); + + if (existing?.faq && existing.faq.length > 0 && onCacheHit === "stale") { + return existing; + } + + if (!existing?.searchQuery?.searchResponse?.serperPeopleAlsoAsk) { + throw new AbortTaskRunError(`No 'People Also Ask' data found for term: ${term}`); + } + + const peopleAlsoAsk = existing.searchQuery.searchResponse.serperPeopleAlsoAsk; + + // Generate comprehensive answers for each question + const faqs = await generateObject({ + model: openai("gpt-4"), + system: `You are an API documentation expert. Your task is to provide clear, accurate, and comprehensive answers to frequently asked questions about API-related concepts. + + Guidelines for answers: + 1. Be technically accurate and precise + 2. Use clear, concise language + 3. Include relevant examples where appropriate + 4. Focus on practical implementation details + 5. Keep answers focused and relevant to API development + 6. Maintain a professional, technical tone + 7. Ensure answers are complete but not overly verbose`, + prompt: ` + Term: "${term}" + + Generate comprehensive answers for these questions from "People Also Ask": + ${peopleAlsoAsk + .map( + (q) => ` + Question: ${q.question} + Current snippet: ${q.snippet} + Source: ${q.link} + `, + ) + .join("\n\n")} + + Provide clear, accurate answers that improve upon the existing snippets while maintaining technical accuracy. + `, + schema: z.object({ faq: faqSchema }), + temperature: 0.2, + }); + + // Update the database with the generated FAQs + await db + .update(entries) + .set({ + faq: faqs.object.faq, + }) + .where(eq(entries.inputTerm, term)); + + return db.query.entries.findFirst({ + where: eq(entries.inputTerm, term), + orderBy: (entries, { asc }) => [asc(entries.createdAt)], + }); + }, +}); diff --git a/apps/billing/src/trigger/glossary/keyword-research.ts b/apps/billing/src/trigger/glossary/keyword-research.ts index 837356941..fe33b43d8 100644 --- a/apps/billing/src/trigger/glossary/keyword-research.ts +++ b/apps/billing/src/trigger/glossary/keyword-research.ts @@ -33,7 +33,8 @@ export const keywordResearchTask = task({ }; } - const searchQuery = await getOrCreateSearchQuery({ term: term }); + const entryWithSearchQuery = await getOrCreateSearchQuery({ term, onCacheHit }); + const searchQuery = entryWithSearchQuery?.searchQuery; console.info(`1/6 - SEARCH QUERY: ${searchQuery?.query}`); if (!searchQuery) { @@ -110,6 +111,7 @@ export const keywordResearchTask = task({ message: `Keyword Research for ${term} completed`, term: searchQuery.inputTerm, keywords: [...keywordsFromTitles, ...keywordsFromHeaders, ...insertedRelatedSearches], + entry: entryWithSearchQuery, }; }, }); diff --git a/apps/billing/src/trigger/glossary/linking-categories.ts b/apps/billing/src/trigger/glossary/linking-categories.ts new file mode 100644 index 000000000..be9363367 --- /dev/null +++ b/apps/billing/src/trigger/glossary/linking-categories.ts @@ -0,0 +1,86 @@ +import { db } from "@/lib/db-marketing/client"; +import { openai } from "@ai-sdk/openai"; +import { task } from "@trigger.dev/sdk/v3"; +import { generateObject } from "ai"; +import { eq } from "drizzle-orm"; +import { z } from "zod"; +import { entries } from "../../lib/db-marketing/schemas"; +import type { CacheStrategy } from "./_generate-glossary-entry"; + +// NB: These are hardcoded for now, but we should have them be dicatated by search volume as it allows for clusterization +const API_CATEGORIES = [ + "Authentication & Authorization", + "API Design", + "Security", + "Performance", + "Protocols", + "Data Formats", + "Error Handling", + "Versioning", + "Testing", + "Documentation", + "Monitoring", + "Integration", +] as const; + +export const linkingCategoriesTask = task({ + id: "linking_categories", + retry: { + maxAttempts: 3, + }, + run: async ({ + term, + onCacheHit = "stale" as CacheStrategy, + }: { + term: string; + onCacheHit?: CacheStrategy; + }) => { + const existing = await db.query.entries.findFirst({ + where: eq(entries.inputTerm, term), + columns: { + id: true, + inputTerm: true, + categories: true, + }, + }); + + if (existing?.categories && existing.categories.length > 0 && onCacheHit === "stale") { + return existing; + } + + const categories = await generateObject({ + model: openai("gpt-4o-mini"), + system: ` + You are an API documentation expert. Categorize API-related terms into relevant categories. + Only use categories from the predefined list. A term can belong to multiple categories if relevant. + Consider the term's primary purpose, related concepts, and practical applications. + `, + prompt: ` + Term: "${term}" + Available categories: ${API_CATEGORIES.join(", ")} + + Analyze the term and assign it to the most relevant categories. Consider: + 1. Primary function and purpose + 2. Related API concepts + 3. Implementation context + 4. Technical domain + `, + schema: z.object({ + categories: z.array(z.enum(API_CATEGORIES)), + reasoning: z.string(), + }), + temperature: 0.3, + }); + + await db + .update(entries) + .set({ + categories: categories.object.categories, + }) + .where(eq(entries.inputTerm, term)); + + return db.query.entries.findFirst({ + where: eq(entries.inputTerm, term), + }); + }, +}); diff --git a/apps/billing/src/trigger/glossary/seo-meta-tags.ts b/apps/billing/src/trigger/glossary/seo-meta-tags.ts index 38682bb61..dc6ec6e3f 100644 --- a/apps/billing/src/trigger/glossary/seo-meta-tags.ts +++ b/apps/billing/src/trigger/glossary/seo-meta-tags.ts @@ -25,11 +25,20 @@ export const seoMetaTagsTask = task({ inputTerm: true, metaTitle: true, metaDescription: true, + metaH1: true, + }, + with: { + dynamicSections: true, }, orderBy: (entries, { desc }) => [desc(entries.createdAt)], }); - if (existing?.metaTitle && existing?.metaDescription && onCacheHit === "stale") { + if ( + existing?.metaTitle && + existing?.metaDescription && + existing?.metaH1 && + onCacheHit === "stale" + ) { return existing; } @@ -51,74 +60,153 @@ export const seoMetaTagsTask = task({ // Step 3: Craft SEO-optimized title and description const craftedMetaTags = await generateObject({ - model: openai("gpt-4o"), + model: openai("gpt-4"), system: ` - You are an SEO expert specializing in technical content, particularly API development. You are given an API-related term and need to craft an SEO-optimized title and description for a glossary entry about this term. - - Follow these best practices when crafting the title and description: - - For the title: - - Keep it concise, ideally between 50-60 characters. - - Include the API term at the beginning of the title. - - If the term is an acronym, consider including both the acronym and its full form. - - Make it informative and clear, indicating that this is a definition or explanation. - - Include "API Glossary" or your brand name at the end if space allows. - - Use a pipe (|) or dash (-) to separate title elements if needed. - - For the description: - - Aim for 150-160 characters for optimal display in search results. - - Start with a clear, concise definition of the API term. - - Include the phrase "Learn about [term]" or similar to indicate the educational nature. - - Mention that this is part of an API development glossary. - - If relevant, briefly mention a key benefit or use case of the term. - - Use technical language appropriately, but ensure it's understandable for developers. - - Include a call-to-action like "Explore our API glossary for more terms." - - Additional guidelines: - - Ensure accuracy in technical terms and concepts. - - Balance SEO optimization with educational value. - - Consider the context of API development when explaining terms. - - For complex terms, focus on clarity over comprehensiveness in the meta description. - - If the term is commonly confused with another, briefly differentiate it. - - Example format: - Title: "HATEOAS in REST APIs | Unkey API Glossary" - Description: "What is HATEOAS in REST APIs? Learn about HATEOAS in REST APIs. Discover how it enhances API navigation and discoverability. Explore our API development glossary for more terms." - - Remember, the goal is to create meta tags that are both SEO-friendly and valuable to developers seeking to understand API terminology. - `, + You are three specialized experts collaborating on creating meta tags for an API documentation glossary: + + TITLE EXPERT (aim for 45-50 chars, strict max 55) + Primary goal: Create clear, informative titles that drive clicks + Structure outline: + 1. Keyword placement (start) - max 25 chars + 2. Concise value proposition - max 20 chars + 3. "Guide" identifier - max 10 chars + Best practices: + - Target 45-50 characters total + - Focus on clarity over attention-grabbing + - Use simple punctuation (colon, dash) + - Count characters before submitting + Example: "JWT Auth: Complete Guide" (27 chars) + + DESCRIPTION EXPERT (aim for 140-145 chars, strict max 150) + Primary goal: Convert visibility into clicks + Structure outline: + 1. Hook with main benefit (20-25 chars) + 2. Core value props - pick TWO only: + - "Learn essentials" + - "Key takeaways" + - "Expert examples" + - "Info & best practices" + 3. ONE secondary keyword (15-20 chars) + 4. Brief call-to-action (10 chars) + - omit the punctuatoin at the end to save a character + Best practices: + - Front-load main benefit + - Use short power words (learn, master) + - Count characters before submitting + - Leave roughly 10 char buffer + + Example: "Master JWT Auth essentials with expert guidance. Learn core concepts and implementation best practices for secure API authentication. Start now." (134 chars) + + H1 EXPERT (aim for 45-50 chars, strict max 60) + Primary goal: Validate click & preview content value + Structure outline: + 1. Main concept (20-25 chars) + 2. Value bridge (20-25 chars) + Best practices: + - Keep it shorter than title + - Use fewer modifiers + - Count characters before submitting + - Leave 10 char buffer + Example: "JWT Authentication: Core Concepts & Implementation" (49 chars) + + COLLABORATION RULES: + 1. Each element builds upon the previous + 2. Maintain keyword presence across all elements + 3. Create a narrative arc: Promise → Value → Delivery + 4. Technical accuracy is non-negotiable + 5. Consider search intent progression + `, prompt: ` Term: ${term} - List of related keywords: - - ${relatedKeywords.map((keyword) => keyword.keyword).join("\n- ")} + Content outline: + ${existing?.dynamicSections.map((section) => `- ${section.heading}`).join("\n")} + + Related keywords: + ${relatedKeywords.map((keyword) => keyword.keyword).join("\n")} - A markdown table of the title & description of the top 10 ranking pages along with their position: - \`\`\` - | Position | Title | Description | - | -------- | ----- | ----------- | + Top ranking pages: ${topRankingPages .map( - (page) => `${page.serperOrganicResult?.position} | ${page.title} | ${page.description}`, + (page) => + `- [${page.serperOrganicResult?.position}] ${page.title}\n ${page.description}`, ) .join("\n")} - \`\`\` - The title and description should be SEO-optimized for the keywords provided. + Create two meta tags and an H1 that form a compelling journey from search result to page content. + Focus on standing out in search results while maintaining accuracy and user value. `, schema: z.object({ - title: z.string(), - description: z.string(), + title: z.string().max(60), + description: z.string().max(190), + h1: z.string().max(80), + reasoning: z.object({ + titleStrategy: z.string(), + descriptionStrategy: z.string(), + h1Strategy: z.string(), + cohesion: z.string(), + }), }), - temperature: 0.5, + temperature: 0.3, }); + // Step 4: Validate and optimize lengths + const validatedMetaTags = await generateObject({ + model: openai("gpt-4"), + system: ` + You are an expert SEO consultant with 10 years of experience optimizing content for search engines. + Your task is to validate and optimize meta tags to ensure they meet strict character limits while + maintaining their SEO value and readability. + + Key requirements: + 1. Title: Max 60 chars (aim for 50-55) + 2. Description: Max 160 chars (aim for 145-155) + 3. H1: Max 80 chars (aim for 45-50) + + Best practices: + - Front-load important keywords + - Maintain readability and natural language + - Preserve core message and intent + - Keep primary keyword visible in truncated versions + - Use punctuation strategically to create natural breaks + + If tags exceed limits: + 1. Remove unnecessary words while preserving meaning + 2. Replace longer words with shorter synonyms + 3. Restructure for conciseness + 4. Ensure truncation occurs at natural breaks + `, + prompt: ` + Original tags: + Title: ${craftedMetaTags.object.title} + Description: ${craftedMetaTags.object.description} + H1: ${craftedMetaTags.object.h1} + + Optimize these tags to meet character limits while maintaining SEO value. + If they already meet the limits, return them unchanged. + `, + schema: z.object({ + title: z.string().max(60), + description: z.string().max(160), + h1: z.string().max(80), + reasoning: z.object({ + titleChanges: z.string(), + descriptionChanges: z.string(), + h1Changes: z.string(), + }), + }), + temperature: 0.1, // Low temperature for consistent, focused outputs + }); + + // Update database with validated meta tags await db .update(entries) .set({ - metaTitle: craftedMetaTags.object.title, - metaDescription: craftedMetaTags.object.description, + metaTitle: validatedMetaTags.object.title, + metaDescription: validatedMetaTags.object.description, + metaH1: validatedMetaTags.object.h1, }) .where(eq(entries.inputTerm, term)); + return db.query.entries.findFirst({ where: eq(entries.inputTerm, term), orderBy: (entries, { desc }) => [desc(entries.createdAt)], diff --git a/apps/www/app/glossary/[slug]/page.tsx b/apps/www/app/glossary/[slug]/page.tsx index 9de4b1e86..283da1f85 100644 --- a/apps/www/app/glossary/[slug]/page.tsx +++ b/apps/www/app/glossary/[slug]/page.tsx @@ -184,24 +184,7 @@ const GlossaryTermWrapper = async ({ params }: { params: { slug: string } }) =>
{ + // NB: I initally tried to import allGlossaries from content-collections but it wasn't available + const glossaryDir = path.join(process.cwd(), "content", "glossary"); + const files = await fs.readdir(glossaryDir); + + // NB: Google's limit is 50,000 URLs per sitemap, split up if necessary: https://nextjs.org/docs/app/api-reference/functions/generate-sitemaps + return files + .filter((file) => file.endsWith(".mdx")) + .map((file) => ({ + url: `${env().NEXT_PUBLIC_BASE_URL}/glossary/${path.basename(file, ".mdx")}`, + lastModified: new Date(), // TODO: Get the actual last modified date of the file -- if content-collections doesn't work from marketing-db? + })); +} diff --git a/apps/www/content-collections.ts b/apps/www/content-collections.ts index 13f8f483a..28970089b 100644 --- a/apps/www/content-collections.ts +++ b/apps/www/content-collections.ts @@ -3,6 +3,8 @@ import { compileMDX } from "@content-collections/mdx"; import { remarkGfm, remarkHeading, remarkStructure } from "fumadocs-core/mdx-plugins"; import GithubSlugger from "github-slugger"; import { categoryEnum } from "./app/glossary/data"; +import { faqSchema } from "./lib/schemas/faq-schema"; +import { takeawaysSchema } from "./lib/schemas/takeaways-schema"; const posts = defineCollection({ name: "posts", @@ -115,33 +117,10 @@ const glossary = defineCollection({ h1: z.string(), term: z.string(), categories: z.array(categoryEnum), - takeaways: z.object({ - tldr: z.string(), - definitionAndStructure: z.array( - z.object({ - key: z.string(), - value: z.string(), - }), - ), - historicalContext: z.array( - z.object({ - key: z.string(), - value: z.string(), - }), - ), - usageInAPIs: z.object({ - tags: z.array(z.string()), - description: z.string(), - }), - bestPractices: z.array(z.string()), - recommendedReading: z.array( - z.object({ - title: z.string(), - url: z.string(), - }), - ), - didYouKnow: z.string(), - }), + takeaways: takeawaysSchema, + faq: faqSchema, + updatedAt: z.string(), + slug: z.string(), }), transform: async (document, context) => { const mdx = await compileMDX(context, document, { diff --git a/apps/www/content/glossary/mime-types.mdx b/apps/www/content/glossary/mime-types.mdx index ec7f1c70c..c174ed86c 100644 --- a/apps/www/content/glossary/mime-types.mdx +++ b/apps/www/content/glossary/mime-types.mdx @@ -1,97 +1,82 @@ --- -title: "MIME Types Explained" -description: "Learn about MIME types in API development. Understand their role in defining file formats like images and PDFs. Explore our glossary for more insights." -term: "MIME Types" -h1: "What are MIME Types? Format IDs Explained" -categories: ["api-specification"] +title: "MIME Types: Comprehensive API Guide" +description: Unlock MIME Types world. Learn from API development examples. Dive into image/video MIME types. Start now. +h1: "MIME Types: Essentials & API Development" +term: MIME types +categories: [] takeaways: - tldr: "Crucial identifiers in digital communication, especially APIs. They specify data format, ensuring correct interpretation and interoperability." + tldr: MIME Types are identifiers used to specify the nature and format of data. definitionAndStructure: - - key: "Format" - value: "type/subtype" - - key: "Example" - value: "image/jpeg" - - key: "Optional" - value: "charset=UTF-8" + - key: Definition + value: Data Identifier + - key: Structure + value: type/subtype + - key: Examples + value: application/json, text/html historicalContext: - - key: "Introduced" - value: "1996" - - key: "Origin" - value: "Email (MIME)" - - key: "Evolution" - value: "HTTP & Web" + - key: Introduced + value: 1990s + - key: Origin + value: Web Services (MIME Types) + - key: Evolution + value: Standardized MIME Types usageInAPIs: tags: - - "Content-Type Header" - - "HTTP Requests" - - "File Uploads" - description: "In API responses, MIME types are included in HTTP headers (e.g., 'Content-Type: application/json') to inform clients about the data format." + - consumes + - produces + - Content-Type + description: MIME Types are used in APIs to define the data formats that can be consumed and produced. They are specified in the 'consumes' and 'produces' fields of API specifications and in the 'Content-Type' HTTP header. bestPractices: - - "Always set the correct MIME type in API responses" - - "Be aware of security implications (e.g., XSS risks with incorrect MIME types)" - - "Use standardized MIME types when possible" + - Always specify MIME types in API responses to ensure correct data handling by clients. + - Use the 'consumes' and 'produces' fields in API specifications to define the data formats the API can handle. + - Prevent MIME sniffing by using the 'X-Content-Type-Options' header. recommendedReading: - - title: "RFC 6838: Media Type Specifications and Registration Procedures" - url: "https://tools.ietf.org/html/rfc6838" - - title: "MDN Web Docs: MIME types (IANA media types)" - url: "https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types" - - title: "RESTful Web Services by Leonard Richardson & Sam Ruby" - url: "https://www.oreilly.com/library/view/restful-web-services/9780596529260/" - didYouKnow: '"application/octet-stream" is the catch-all for unknown binary files.' + - url: https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types + title: Understanding MIME Types + - url: https://swagger.io/docs/specification/media-types/ + title: MIME Types in APIs + - url: https://www.iana.org/assignments/media-types/media-types.xhtml + title: IANA MIME Types Registry + didYouKnow: MIME stands for Multipurpose Internet Mail Extensions, originally designed for email to support sending of different types of files. +faq: + - answer: In the context of APIs, MIME types are used to define the format of the data that is being exchanged between the client and the server. They are specified in the 'Content-Type' and 'Accept' HTTP headers. The 'Content-Type' header tells the server the format of the data being sent by the client, while the 'Accept' header tells the server the format in which the client wants the response. Common MIME types used in APIs include 'application/json' for JSON data and 'application/xml' for XML data. + question: What is the MIME type in API? + - answer: The question seems to be confused with the types of mime performance art. In the context of APIs and web technology, MIME types are not categorized into three types. Instead, they are classified based on the type of data they represent. For example, 'text/html' for HTML documents, 'application/json' for JSON data, 'image/jpeg' for JPEG images, and so on. + question: What are the 3 types of MIME? + - answer: MIME content types, also known as media types, are used to describe the nature and format of data. They consist of a type and a subtype, separated by a slash. For example, in 'text/html', 'text' is the type and 'html' is the subtype. Some common MIME content types include 'text/html' for HTML documents, 'application/json' for JSON data, 'image/jpeg' for JPEG images, and 'text/javascript' for JavaScript files. + question: What are the MIME content types? + - answer: MIME types themselves are not being deprecated and continue to be an essential part of the web, used in HTTP headers to inform about the type of data being transmitted. However, certain methods or properties related to MIME types in specific web APIs may be deprecated. It's always recommended to check the latest documentation for the specific API or feature you're using. + question: Is MIME type being deprecated? +updatedAt: 2024-11-15T14:16:31.000Z +slug: mime-types --- -## Understanding MIME Types in Web and API Development - -MIME types are widely used in web development and APIs to specify the format of the content being transferred. In the context of APIs, particularly RESTful APIs, MIME types are included in HTTP headers to inform the client about the format of the returned data, whether it is JSON, XML, or another format. For example, when a server returns JSON data, it uses the MIME type `application/json`. This helps client applications understand how to parse and handle the data effectively. - -In web development, specifying the correct MIME type is vital for ensuring that browsers know how to display or handle the served file. For instance, when a server sends a CSS file, it uses the MIME type `text/css`, indicating to the browser that it should interpret it as a stylesheet. +MIME types, or Multipurpose Internet Mail Extensions, are essential in defining the nature of files exchanged over the Internet. They inform web browsers and email clients how to handle various file types. For API developers, understanding and correctly using MIME types is crucial to ensure that data is properly accepted and interpreted across different systems. -### Examples of MIME Types in HTTP Headers - -Here are some common MIME type examples used in HTTP headers: - -```http -Content-Type: application/json -``` -This header in an API response indicates that the data is in JSON format. +## Understanding MIME Types in Web and API Development -```http -Content-Type: text/html -``` -This header in a web response tells the browser to interpret the response as HTML. +MIME types specify the nature of the content being transmitted over the web. Each MIME type consists of a type and a subtype, such as `text/html`. In API development, MIME types are vital for defining the content type of request and response bodies. For example, APIs often utilize `application/json` for JSON payloads, making it a common MIME type in API development. -### MIME Type List +## Detailed Structure of MIME Types -A comprehensive MIME type list includes various formats, such as: +A MIME type is structured as `type/subtype`. The `type` represents the general category of the data (e.g., `text`, `image`, `application`), while the `subtype` specifies the exact kind of data (e.g., `html` for text or `png` for image). Additional parameters may be included after the subtype, such as `charset=UTF-8` for text types, which is important for ensuring proper encoding. -- **Image MIME Types**: - - `image/jpeg` for JPEG images - - `image/png` for PNG images - - `image/gif` for GIF images +## Common and Lesser-Known MIME Types in Web Development -- **Video MIME Types**: - - `video/mp4` for MP4 videos - - `video/x-msvideo` for AVI videos - - `video/webm` for WebM videos +Common MIME types include `text/html`, `application/json`, and `image/jpeg`. Lesser-known MIME types, such as `application/vnd.api+json`, are used in specific scenarios, particularly in APIs that adhere to the JSON API specification. For a comprehensive **MIME type list**, developers can refer to various online resources. -- **Document MIME Types**: - - `application/pdf` for PDF documents - - `application/msword` for Microsoft Word documents +## Image and Video MIME Types Explained -### Detailed Structure of MIME Types +For images, common MIME types include `image/jpeg`, `image/png`, and `image/gif`. For video content, MIME types like `video/mp4` and `video/webm` are prevalent. These MIME types ensure that browsers and APIs handle media content correctly, providing the necessary information to render or process files appropriately. -A MIME type consists of two main parts: a type and a subtype, separated by a slash (`/`). The type represents the general category of the data, while the subtype specifies the specific kind of data. For example, in `image/jpeg`, `image` is the type, and `jpeg` is the subtype, indicating that the file is an image in JPEG format. Similarly, `application/json` indicates that the data is in the application-specific format of JSON. +## PDF MIME Types in Web and API Development -The structure can also include additional parameters, such as `charset`, which defines the character set used in text data. For example: +The MIME type for PDF files is `application/pdf`. This MIME type is crucial in both web and API contexts where PDF files are transmitted or received. For instance, an API might generate PDF reports based on data, requiring the correct MIME type to ensure proper handling and viewing by the client. Understanding the **MIME type PDF** is essential for developers working with document generation. -```http -Content-Type: text/html; charset=UTF-8 -``` -This indicates that the HTML content should be interpreted using the UTF-8 character set. +## MIME Types in API Development -### Importance of MIME Types in API Development +In API development, MIME types define the format of the data being exchanged. Common MIME types in RESTful APIs include `application/xml` and `application/json`. Specifying the correct MIME type in API requests and responses is vital for the data to be accurately interpreted and processed by the consuming application or service. For practical examples, developers can explore **MIME types API development examples** on platforms like GitHub. -Understanding the detailed structure and usage of MIME types is crucial for API developers. Properly specifying MIME types enhances compatibility and efficiency across different systems and platforms. It ensures that data is correctly processed in web applications and when interacting with APIs, ultimately improving the user experience. +## Conclusion -For further exploration, developers can refer to the official IANA Media Types registry (https://www.iana.org/assignments/media-types/) or the resources listed in the recommended reading section above. -By mastering MIME types, API developers can ensure that their applications handle data correctly, leading to more robust and user-friendly web services. +MIME types are a fundamental aspect of web and API development, ensuring that data is transmitted and interpreted correctly. By understanding the structure and usage of MIME types, developers can enhance their API's functionality and ensure seamless data exchange. For further exploration, consider checking out resources on **MIME types API development GitHub** and **MIME types API development PDF** for additional insights and examples. \ No newline at end of file diff --git a/apps/www/content/glossary/single-sign-on.mdx b/apps/www/content/glossary/single-sign-on.mdx new file mode 100644 index 000000000..cbaedbc09 --- /dev/null +++ b/apps/www/content/glossary/single-sign-on.mdx @@ -0,0 +1,162 @@ +--- +title: "Single Sign-On API: Comprehensive Guide" +description: Unlock SSO API power. Learn REST API SSO authentication, AWS SSO API implementation. Real-world examples. Start today. +h1: "Single Sign-On API: Understanding and Implementation" +term: single-sign-on +categories: [] +takeaways: + tldr: Single-Sign-On (SSO) is an authentication scheme that allows a user to log in with a single ID and password to any of several related, yet independent, software systems. + definitionAndStructure: + - key: Authentication Flow + value: Token-Based + - key: OAuth 2.0 + value: Access Tokens + - key: Token Management + value: Refresh Tokens + - key: Unattended Authentication + value: IdP Managed + - key: SCIM + value: Account Synchronization + historicalContext: + - key: Introduced + value: Late 1990s + - key: Origin + value: Web Services (Single-Sign-On) + - key: Evolution + value: Enterprise Single-Sign-On + usageInAPIs: + tags: + - rest + - oauth + - single-sign-on + - saml-2.0 + description: Single-Sign-On is used in APIs to provide seamless user authentication across multiple systems. It is often implemented using OAuth 2.0 for token-based authentication and SCIM for account synchronization. SSO is particularly useful in SaaS applications and REST APIs. + bestPractices: + - Use OAuth 2.0 for token-based authentication in SSO. + - Manage authentication through the Identity Provider (IdP) to maintain security. + - Use SCIM for account synchronization between the service provider and the IdP. + recommendedReading: + - url: https://auth0.com/learn/single-sign-on/ + title: Understanding Single Sign-On (SSO) + - url: https://www.youtube.com/watch?v=996OiexHze0 + title: OAuth 2.0 and OpenID Connect (in plain English) + - url: https://tools.ietf.org/html/rfc7642 + title: "SCIM: System for Cross-domain Identity Management" + didYouKnow: Single-Sign-On not only improves user experience by reducing password fatigue, but also enhances security by minimizing the risk of password phishing. +faq: + - answer: Single Sign-On (SSO) in API context refers to a user authentication process that allows a user to use one set of login credentials (like username and password) to access multiple applications. The service authenticates the end-user for all the applications they have been given rights to and eliminates further prompts when the user switches applications during the same session. In the context of APIs, SSO can be used to provide a unified login mechanism across different systems or services, enhancing user experience and security. + question: What is SSO in API? + - answer: Creating your own Single Sign-On (SSO) involves several steps. First, identify the applications you want to connect to SSO. Second, integrate with an Identity Provider (IdP), which will handle the authentication process. Third, verify the data in your identity directory to ensure it's accurate and up-to-date. Fourth, evaluate user privileges to determine what each user should have access to. Finally, ensure the SSO system is highly available and secure. This involves implementing security measures such as encryption, regular audits, and monitoring to detect and respond to any security incidents. + question: How do I create my own SSO? + - answer: The Single Sign-On (SSO) method is an authentication scheme that allows users to log in with a single ID and password to any of several related, yet independent, software systems. It is a common procedure in enterprises, where a client accesses multiple resources connected to a local area network (LAN). With SSO, a user logs in once and gains access to all systems without being prompted to log in again for each of them. + question: What is the single sign-on SSO method? + - answer: OAuth 2.0 works in a REST API by providing a process for end-users to authorize third-party access to their server resources without sharing their credentials. It uses token-based authentication and authorization. When a user authenticates, the authorization server issues an access token that the application can use for authentication when making requests to the resource server on behalf of the user. The access token defines the scope and duration of the access. The application doesn't need to know the user's identity, which makes OAuth a secure and powerful method for handling access control. + question: How does OAuth work in the rest API? +updatedAt: 2024-11-15T12:58:49.000Z +slug: single-sign-on +--- + +**Single Sign-On (SSO)** is a user authentication process that allows users to access multiple applications with a single set of login credentials, such as a username and password. This approach is particularly beneficial in environments where users need to access various applications or systems, as it enhances security and user experience by reducing password fatigue and minimizing the time spent re-entering credentials across different platforms. + +## Understanding Single Sign-On (SSO) Concepts + +Single Sign-On (SSO) enables users to authenticate with multiple systems and applications using one set of credentials managed by a central service. This central service authenticates the user and provides a token or similar credential that is trusted by all participating applications. SSO is widely adopted in enterprise environments to streamline user access to applications across different platforms and enhance security by centralizing user authentication. + +## API vs SSO: Key Differences Explained + +Understanding the differences between **APIs (Application Programming Interfaces)** and **SSO (Single Sign-On)** is crucial for developers. APIs allow software applications to interact with each other by sending requests and receiving responses. In contrast, SSO is a security process that facilitates a single user authentication process across multiple applications. While APIs can be utilized to implement SSO, the primary function of SSO is to authenticate users, not to facilitate direct application interaction. + +## Implementing AWS SSO API for Authentication + +To implement SSO using the **AWS SSO API**, developers can leverage the AWS SDK. Below is a simple example in Python using Boto3, AWS's SDK for Python: + +```python +import boto3 + +# Initialize a session using your AWS credentials +session = boto3.Session( + aws_access_key_id='YOUR_KEY', + aws_secret_access_key='YOUR_SECRET', + region_name='YOUR_REGION' +) + +# Create an SSO client +sso_client = session.client('sso') + +# List the available SSO instances +instances = sso_client.list_instances() +print(instances) +``` +This code snippet initializes a session with AWS and creates an SSO client to list all SSO instances, demonstrating a practical **AWS SSO API** implementation. + +## REST API SSO Authentication: A Practical Guide + +When implementing SSO in a **REST API**, developers typically use OAuth2 or OpenID Connect protocols. Here’s a straightforward example using OAuth2 with Python’s requests library: + +```python +import requests + +# Define the endpoint and your access credentials +token_url = "https://your-auth-server.com/oauth/token" +client_id = 'your-client-id' +client_secret = 'your-client-secret' + +# Request token +response = requests.post(token_url, data={ + 'grant_type': 'client_credentials', + 'client_id': client_id, + 'client_secret': client_secret +}) + +# Extract the token from the response +token = response.json().get('access_token') +``` +This script requests an access token from an OAuth2 server, showcasing a practical **REST API SSO authentication** example. + +## Single Sign-On API Development in JavaScript + +Developing SSO in **JavaScript** often involves integrating with a third-party SSO provider like Auth0. Here’s how to set up a simple SSO using Auth0: + +```javascript +const { auth } = require('express-openid-connect'); + +const config = { + authRequired: false, + auth0Logout: true, + secret: 'a long, randomly-generated string stored in env', + baseURL: 'http://your-application.com', + clientID: 'your-client-id', + issuerBaseURL: 'https://your-domain.auth0.com' +}; + +app.use(auth(config)); +``` +This code configures an Express.js application to use Auth0 for authentication, illustrating a **single sign-on API development in JavaScript**. + +## Single Sign-On API Development in Python + +For Python developers, implementing SSO can be achieved using libraries such as Flask and Flask-Dance. Here’s a basic setup for Google login using Flask-Dance: + +```python +from flask import Flask, redirect, url_for +from flask_dance.contrib.google import make_google_blueprint, google + +app = Flask(__name__) +app.secret_key = "supersekrit" +blueprint = make_google_blueprint(client_id="your-client-id", client_secret="your-client-secret") +app.register_blueprint(blueprint, url_prefix="/login") + +@app.route("/") +def index(): + if not google.authorized: + return redirect(url_for("google.login")) + resp = google.get("/oauth2/v1/userinfo") + assert resp.ok, resp.text + return "You are {email} on Google".format(email=resp.json()["email"]) + +if __name__ == "__main__": + app.run() +``` +This example sets up a Flask application with Google SSO, redirecting to Google's login if not already authenticated, providing a clear **single sign-on API development example in Python**. + +By understanding and implementing these concepts, developers can effectively utilize **Single Sign-On (SSO)** to enhance user authentication across multiple applications, improving both security and user experience. \ No newline at end of file diff --git a/apps/www/content/glossary/statelessness.mdx b/apps/www/content/glossary/statelessness.mdx new file mode 100644 index 000000000..4508351bd --- /dev/null +++ b/apps/www/content/glossary/statelessness.mdx @@ -0,0 +1,96 @@ +--- +title: "Statelessness in APIs: Comprehensive Guide" +description: Understand statelessness in APIs. Learn differences between stateless and stateful APIs, with examples. Explore REST API design. +h1: "Statelessness in API Design: Understanding & Examples" +term: Statelessness +categories: [] +takeaways: + tldr: Statelessness in APIs refers to the server not retaining any client-specific data between requests, making each request independent and self-contained. + definitionAndStructure: + - key: Stateless API + value: Independent Transactions + - key: Stateful API + value: Server-Side Memory + - key: REST API + value: Typically Stateless + historicalContext: + - key: Introduced + value: "2000" + - key: Origin + value: Web Services (statelessness) + - key: Evolution + value: Standardized statelessness + usageInAPIs: + tags: + - Stateless API + - REST API + - Scalability + - Reliability + - Performance + description: Statelessness is a fundamental principle in REST API design, enhancing scalability and reliability by treating each request as an independent transaction. It simplifies server-side operations as no session or state information is retained. This also improves performance as responses can be effectively cached. + bestPractices: + - Ensure each request contains all necessary information for processing, eliminating the need for server-side session management. + - Use stateless authentication methods, such as token-based authentication, to maintain security without violating statelessness. + - Leverage the benefits of statelessness for scalability, reliability, and caching. + recommendedReading: + - url: https://medium.com/@tiokachiu/stateful-vs-stateless-how-about-rest-api-8b7e4e8b6c0a + title: Stateful vs Stateless, How about REST API? + - url: https://howtodoinjava.com/rest/statelessness-in-rest-api/ + title: Statelessness in REST API + - url: https://stackoverflow.com/questions/3105296/if-rest-applications-are-supposed-to-be-stateless-how-do-you-manage-sessions + title: Understanding Statelessness in REST APIs + didYouKnow: Despite the stateless nature of REST APIs, they can incorporate stateful components if necessary, such as databases for session management. +faq: + - answer: Statelessness in an API refers to the design principle where the API does not store any information about the client session or context between requests. Each request is treated as an isolated transaction, independent of any previous requests. This means that all the necessary information must be included in each request, such as user authentication and data required to perform the operation. This approach enhances scalability as the server does not need to maintain and manage session information. + question: What is statelessness in API? + - answer: Web APIs, particularly those based on the HTTP protocol, are typically designed to be stateless. This means they do not retain any client session information between requests. However, there are exceptions such as WebSocket APIs, which provide a stateful, full-duplex communication channel between the client and server. In a stateful API, the server maintains client session information, allowing for continuous interaction over the same connection. + question: Is Web API stateful or stateless? + - answer: To maintain statelessness while implementing authentication in a REST API, token-based authentication methods are commonly used. One popular method is using JSON Web Tokens (JWT). In this approach, when a user logs in, the server generates a token that encapsulates the user's identity and other relevant attributes. This token is sent back to the client, which includes it in subsequent requests to authenticate itself. Since the token carries all necessary information, the server does not need to maintain session state, preserving the statelessness of the API. + question: How would you implement authentication for a REST API while maintaining statelessness? + - answer: Statelessness in HTTP refers to the protocol's design principle where each request is treated independently and does not rely on any information from previous requests. This means that the server does not store any session data or context between requests. Each request must contain all the necessary information for the server to understand and process it. This design principle contributes to the scalability and simplicity of the HTTP protocol. + question: What is statelessness in HTTP? +updatedAt: 2024-11-15T14:15:49.000Z +slug: statelessness +--- + +**Statelessness** is a fundamental concept in API development, significantly influencing how web services and client applications interact. In a **stateless API**, each request from a client to the server must contain all the information necessary for the server to understand and respond to the request. This approach contrasts with **stateful APIs**, where the server retains previous interactions and state information relevant to future requests. + +## Understanding Statelessness in APIs + +Statelessness in APIs means that every HTTP request occurs in complete isolation. When the server processes a request, it does not rely on any information stored from previous interactions. This design principle enhances reliability and scalability, as the server does not need to maintain, update, or communicate session state. + +## Stateless vs Stateful APIs: Key Differences + +| Feature | Stateless API | Stateful API | +|---------|---------------|--------------| +| Memory Consumption | Low, as no session data is stored | High, as session data needs to be stored and managed | +| Scalability | High, easier to scale as each request is independent | Lower, as the server must manage and synchronize session state across requests | +| Performance | Generally faster, due to the lack of need for session management | Can be slower, especially with large volumes of session data | +| Complexity | Simpler in design, as it does not require session management | More complex, due to the need for session tracking and management | +| Use Case | Ideal for public APIs and services where sessions are not necessary | Suitable for applications where user state needs to be preserved across requests | + +## Examples of Stateless APIs in Practice + +1. **HTTP Web Services**: Most **RESTful APIs** are stateless. Each request contains all necessary information, such as user authentication and query parameters. +2. **Microservices**: In a microservices architecture, services are often stateless to ensure they can scale independently without relying on shared state. +3. **Serverless Architectures**: Functions as a Service (FaaS) platforms like AWS Lambda are inherently stateless, executing code in response to events without maintaining any server or application state. + +## Stateful API Examples for Contrast + +1. **Web-based Applications**: Applications like online shopping carts or personalized user dashboards maintain state to track user sessions and preferences. +2. **Enterprise Applications**: Systems that require complex transactions, such as banking or booking systems, often rely on stateful APIs to ensure data consistency across multiple operations. +3. **Gaming and Social Media Platforms**: These platforms maintain user state to provide a continuous and personalized experience across multiple sessions. + +## Statelessness in REST API Design + +In **REST API design**, statelessness ensures that each client-server interaction is independent of previous ones, adhering to one of the core constraints of REST. This constraint simplifies server design, improves scalability, and increases system reliability by eliminating the server-side state's impact on behavior. + +## Common Misconceptions about Statelessness + +- **Statelessness Implies No Storage**: While stateless APIs do not store state between requests, they can still access stateful resources like databases or external services to retrieve necessary data. +- **Statelessness Reduces Functionality**: Some believe that statelessness limits API functionality. However, stateless APIs can offer rich functionalities as long as each request is self-contained with all necessary context. +- **Statelessness and Stateless are the Same**: The term 'stateless' refers to the lack of server-side state between requests, whereas 'statelessness' is a design approach that emphasizes this characteristic in API development. + +By adhering to the principle of **statelessness in API development**, developers can create more robust, scalable, and maintainable APIs. Understanding whether a **REST API is stateless or stateful** is crucial for making informed design decisions that align with application requirements. + +In summary, whether you are exploring **stateless API examples** or contrasting them with **stateful API examples**, grasping the concept of statelessness is essential for effective API design and implementation. \ No newline at end of file diff --git a/apps/www/content/glossary/transport-layer-security.mdx b/apps/www/content/glossary/transport-layer-security.mdx new file mode 100644 index 000000000..2de9a2859 --- /dev/null +++ b/apps/www/content/glossary/transport-layer-security.mdx @@ -0,0 +1,117 @@ +--- +title: "TLS in API: Secure Communication Guide" +description: Unlock TLS in API. Learn how it works, its differences with SSL, HTTPS. Dive in now. +h1: "TLS in API: Implementation & Challenges" +term: transport-layer-security +categories: [] +takeaways: + tldr: Transport Layer Security (TLS) is a protocol that provides authentication and encryption for secure data transmission, often used in APIs to prevent unauthorized data tampering and Man-in-the-Middle attacks. + definitionAndStructure: + - key: TLS + value: Authentication and Encryption Protocol + - key: SSL + value: TLS Predecessor + - key: TLS Handshake + value: Secure Connection Establishment + - key: mTLS + value: Dual Authentication + - key: API Gateway + value: Security Management + - key: Certificates + value: Trust and Keystores + historicalContext: + - key: Introduced + value: "1999" + - key: Origin + value: Web Security (transport-layer-security) + - key: Evolution + value: Standardized transport-layer-security + usageInAPIs: + tags: + - TLS + - SSL + - mTLS + - API Gateway + - Certificates + description: Transport Layer Security (TLS) is integral to secure API communication, preventing unauthorized data tampering and Man-in-the-Middle attacks. Mutual TLS (mTLS) enhances security by authenticating both client and server. API gateways manage security, and certificates from trusted authorities are essential for establishing trust and keystores. + bestPractices: + - Always use the latest version of TLS for optimal security. + - Implement mutual TLS (mTLS) for enhanced security in API transactions. + - Manage keystores effectively with secure passwords and regular key rotation. + recommendedReading: + - url: https://www.example.com/transport-layer-security-for-rest-services + title: Transport Layer Security (TLS) for REST Services + - url: https://www.example.com/understanding-the-tls-handshake-process + title: Understanding the TLS Handshake Process + - url: https://www.example.com/securing-apis-with-mutual-tls + title: Securing APIs with Mutual TLS + didYouKnow: TLS 1.3, the latest version of Transport Layer Security, has reduced the handshake process from two round-trips to only one, making it faster and more efficient than its predecessor, TLS 1.2. +faq: + - answer: Transport Layer Security (TLS) in API refers to a protocol used to secure communication between the API server and the client. It encrypts the data being transmitted, protecting it from interception or tampering during transit. Additionally, TLS can be used for mutual authentication, which verifies both the client and the server's identities to prevent unauthorized access. + question: What is TLS in API? + - answer: Secure Transport API is a part of Apple's security services that provides access to their implementation of various security protocols. These include Secure Sockets Layer version 3.0 (SSLv3), Transport Layer Security (TLS) versions 1.0 through 1.2, and Datagram Transport Layer Security (DTLS) version 1.0. The API is designed to be transport layer independent, meaning it can be used with any transport protocol. + question: What is secure transport API? + - answer: Yes, REST APIs often use Transport Layer Security (TLS) for securing the communication between the client and the server. By implementing TLS, the data transmitted in requests and responses is encrypted, ensuring its confidentiality and integrity. This is particularly important when sensitive data, such as personal information or payment details, is being exchanged. + question: Do rest APIs use TLS? + - answer: Transport Layer Security (TLS) is a protocol standard established by the Internet Engineering Task Force (IETF). It provides authentication, privacy, and data integrity in the communication between two computer applications. This is achieved through encryption, which protects data from being read or modified during transit, and authentication, which verifies the identities of the communicating parties. + question: What is the transport layer security? +updatedAt: 2024-11-15T14:10:41.000Z +slug: transport-layer-security +--- + +**Transport Layer Security (TLS)** is a critical protocol for securing communications over computer networks, particularly in web browsing, email, and API development. Understanding TLS is essential for API developers to ensure data integrity and privacy between client-server applications. + +## Understanding Transport Layer Security (TLS) + +TLS is a cryptographic protocol that provides secure communication across networks. As the successor to Secure Sockets Layer (SSL), TLS enhances the security of data transmitted over the internet through encryption, authentication, and integrity. It is widely used in web browsers and servers to prevent eavesdropping, tampering, and message forgery, making it a fundamental component in API development. + +## How Does TLS Work? A Technical Breakdown + +TLS operates between the transport layer and the application layer in the OSI model, ensuring that data remains encrypted and secure throughout its journey. The protocol employs a combination of symmetric and asymmetric cryptography. Symmetric encryption ensures the privacy and integrity of messages, while asymmetric encryption is utilized during the TLS handshake to securely exchange keys for symmetric encryption. + +## The TLS Handshake Process Explained + +The **TLS handshake** is a crucial process that establishes a secure connection between the client and server before data transfer begins. The handshake involves several steps: + +1. **ClientHello**: The client sends a message to the server, indicating supported TLS versions, cipher suites, and a randomly generated number. +2. **ServerHello**: The server responds with its chosen protocol version, cipher suite, and a randomly generated number. +3. **Certificate Exchange**: The server sends its digital certificates to the client for authentication. +4. **Key Exchange**: The client and server exchange keys to establish a symmetric key for encrypting subsequent communications. +5. **Finished**: Both parties confirm the established security settings and begin the secure session. + +Understanding the TLS handshake is vital for API developers to implement secure communications effectively. + +## Comparing TLS and SSL: Key Differences + +While TLS and SSL are often used interchangeably, they are distinct protocols. SSL is the predecessor to TLS and is considered less secure. Key differences include: + +- **Protocol Version**: SSL versions are deemed insecure, whereas TLS provides enhanced security features. +- **Encryption Algorithms**: TLS supports newer and more secure algorithms. +- **Handshake Process**: TLS features a more secure handshake process that offers better protection against attacks. + +## TLS vs HTTPS: Understanding the Relationship + +**HTTPS** (Hypertext Transfer Protocol Secure) is an extension of HTTP that utilizes TLS to encrypt data. While HTTPS incorporates TLS for security, TLS itself is a protocol that can secure any data transmitted over a network, not just HTTP. This distinction is crucial for API developers implementing secure communication across various applications. + +## Implementing TLS in API Development + +Incorporating TLS in API development is vital for protecting sensitive data and ensuring secure communications between clients and servers. Here’s a basic example of how to enforce TLS in a Node.js API: + +```javascript +const https = require('https'); +const fs = require('fs'); + +const options = { + key: fs.readFileSync('server-key.pem'), + cert: fs.readFileSync('server-cert.pem') +}; + +https.createServer(options, (req, res) => { + res.writeHead(200); + res.end('Hello secure world!\n'); +}).listen(443); +``` + +This example demonstrates how to create an HTTPS server in Node.js that listens on port 443, using TLS to secure all communications. Implementing TLS not only helps in compliance with security standards but also builds trust with users by protecting their data. + +By understanding **transport layer security** and its implementation in API development, developers can ensure robust security measures are in place, safeguarding sensitive information and enhancing user trust. \ No newline at end of file diff --git a/apps/www/content/statelessness.mdx b/apps/www/content/statelessness.mdx deleted file mode 100644 index 4f73c8374..000000000 --- a/apps/www/content/statelessness.mdx +++ /dev/null @@ -1,49 +0,0 @@ ---- - title: "Statelessness in APIs: Definition & Examples" - description: "Statelessness in APIs means no client data is stored between requests. Learn about stateless REST API examples and benefits. Explore our API glossary." - --- - # Statelessness in API Development: A Comprehensive Guide - -Statelessness is a fundamental concept in API development, significantly influencing how client-server interactions are designed and implemented. In a stateless architecture, each request from a client to a server must contain all the information the server needs to understand and process the request. The server does not store any state about the client session between requests. This approach offers several advantages, including scalability, reliability, and simplicity in managing server interactions. - -## Understanding Statelessness in API Development - -Statelessness in API development means that every HTTP request occurs in complete isolation. When the server processes a request, it does not rely on any information stored from previous requests. This design principle is crucial for creating scalable web services, as it simplifies server design by eliminating the need to maintain, update, or communicate session state. - -## Stateless vs Stateful APIs: Key Differences - -| Feature | Stateless API | Stateful API | -|---------|---------------|--------------| -| **Session Storage** | No session information is stored on the server. | Session information is stored on the server or in a session management system. | -| **Scalability** | High, as less server memory is used and each request is independent. | Lower, as the server must manage and synchronize session state across requests and possibly across different servers. | -| **Complexity** | Lower, as each request is treated as new, with no dependency on past interactions. | Higher, due to the need for maintaining session state and ensuring consistency. | -| **Performance** | Can be higher due to the simplicity of not managing state. | Can be impacted by the overhead of state management. | -| **Use Cases** | Ideal for public APIs and services where the interaction model is request-response based. | Suitable for applications where the user's state needs to be preserved across multiple interactions, such as in web applications. | - -## Stateless API Examples in Practice - -1. **HTTP Web APIs**: Most RESTful APIs are stateless. Each request contains all necessary information, such as user authentication tokens and input parameters. -2. **Microservices**: In a microservices architecture, services communicate with each other statelessly to ensure loose coupling and independence. -3. **Serverless Architectures**: Functions as a Service (FaaS) platforms like AWS Lambda are inherently stateless, executing code in response to events without maintaining any server state. - -## Understanding Stateful APIs with Examples - -1. **Web-based Applications**: Traditional web applications often rely on stateful interactions using sessions and cookies to track user authentication and activity across multiple requests. -2. **Real-time Applications**: Applications like chat apps maintain state to provide a continuous user experience where the server remembers previous interactions. -3. **E-commerce Platforms**: These platforms maintain user state to manage shopping carts and user preferences throughout a session. - -## Statelessness in REST API Design - -In REST API design, statelessness ensures that each client-server interaction is independent of the others. This design principle is one of the constraints of REST, which stands for Representational State Transfer. By adhering to statelessness, RESTful APIs improve visibility, reliability, and scalability. Servers do not need to manage resource state across requests, simplifying architecture and reducing resource consumption. - -## Common Misconceptions about Statelessness - -1. **Statelessness Implies No Storage**: While stateless APIs do not store state on the server between requests, they can still interact with databases or storage services to retrieve or save data needed for processing requests. -2. **Statelessness Reduces Security**: Some believe that statelessness might reduce security because authentication must be performed with each request. However, this can actually enhance security by reducing the attack surface (e.g., no session hijacking). -3. **Statelessness is Always Better**: While statelessness offers many benefits, it's not universally the best choice. Stateful APIs can be more appropriate for applications requiring complex transactions and user sessions. - -## Conclusion - -Understanding statelessness in API development is essential for creating efficient and scalable web services. By recognizing the differences between stateless and stateful APIs, developers can make informed decisions about which architecture best suits their application needs. Whether you're working with REST APIs or exploring SOAP's state management, grasping the concept of statelessness will enhance your API design and implementation skills. - -For further exploration, consider the implications of statelessness in REST API design and how it compares to stateful interactions in various applications. \ No newline at end of file diff --git a/apps/www/lib/schemas/faq-schema.ts b/apps/www/lib/schemas/faq-schema.ts new file mode 100644 index 000000000..335e71db9 --- /dev/null +++ b/apps/www/lib/schemas/faq-schema.ts @@ -0,0 +1,16 @@ +import { z } from "zod"; + +/** + * @description Schema for glossary entry FAQs + * @warning This is a duplicate of apps/billing/src/lib/db-marketing/schemas/entries.ts + * @todo Extract this schema into a shared package to ensure consistency with the billing app + * @see apps/billing/src/lib/db-marketing/schemas/entries.ts for the source of truth + */ +export const faqSchema = z.array( + z.object({ + question: z.string(), + answer: z.string(), + }), +); + +export type FAQ = z.infer; diff --git a/apps/www/lib/schemas/takeaways-schema.ts b/apps/www/lib/schemas/takeaways-schema.ts new file mode 100644 index 000000000..823b4b1b4 --- /dev/null +++ b/apps/www/lib/schemas/takeaways-schema.ts @@ -0,0 +1,37 @@ +import { z } from "zod"; + +/** + * @description Schema for glossary entry takeaways + * @warning This is a duplicate of apps/billing/src/lib/db-marketing/schemas/takeaways-schema.ts + * @todo Extract this schema into a shared package to ensure consistency with the billing app + * @see apps/billing/src/lib/db-marketing/schemas/takeaways-schema.ts for the source of truth + */ +export const takeawaysSchema = z.object({ + tldr: z.string(), + definitionAndStructure: z.array( + z.object({ + key: z.string(), + value: z.string(), + }), + ), + historicalContext: z.array( + z.object({ + key: z.string(), + value: z.string(), + }), + ), + usageInAPIs: z.object({ + tags: z.array(z.string()), + description: z.string(), + }), + bestPractices: z.array(z.string()), + recommendedReading: z.array( + z.object({ + title: z.string(), + url: z.string(), + }), + ), + didYouKnow: z.string(), +}); + +export type Takeaways = z.infer; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 93a73b465..2c37f736d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -168,14 +168,14 @@ importers: specifier: ^1.16.0 version: 1.18.0 '@trigger.dev/nextjs': - specifier: 3.1.2 - version: 3.1.2(@trigger.dev/sdk@3.1.2)(next@14.2.10) + specifier: 3.2.0 + version: 3.2.0(@trigger.dev/sdk@3.2.0)(next@14.2.10) '@trigger.dev/sdk': - specifier: 3.1.2 - version: 3.1.2 + specifier: 3.2.0 + version: 3.2.0 '@trigger.dev/slack': - specifier: 3.1.2 - version: 3.1.2 + specifier: 3.2.0 + version: 3.2.0 '@unkey/billing': specifier: workspace:^ version: link:../../internal/billing @@ -206,6 +206,12 @@ importers: drizzle-zod: specifier: ^0.5.1 version: 0.5.1(drizzle-orm@0.33.0)(zod@3.23.8) + github-slugger: + specifier: ^2.0.0 + version: 2.0.0 + js-yaml: + specifier: ^4.1.0 + version: 4.1.0 react-dom: specifier: ^18 version: 18.3.1(react@18.3.1) @@ -906,13 +912,13 @@ importers: version: 1.18.0 '@trigger.dev/nextjs': specifier: ^3.1.0 - version: 3.1.2(@trigger.dev/sdk@3.1.2)(next@14.2.10) + version: 3.2.0(@trigger.dev/sdk@3.2.0)(next@14.2.10) '@trigger.dev/sdk': specifier: ^3.1.0 - version: 3.1.2 + version: 3.2.0 '@trigger.dev/slack': specifier: ^3.1.0 - version: 3.1.2 + version: 3.2.0 '@unkey/billing': specifier: workspace:^ version: link:../../internal/billing @@ -10225,8 +10231,8 @@ packages: zod: 3.23.8 dev: false - /@trigger.dev/core@3.1.2: - resolution: {integrity: sha512-a9kQK20zehyVuXxWm2j0st+ydmNU4SB+4Ye+rC4ZwwoTFFUb4D9p2uiEjqYrKKUAJxExj/mixgcldOlznfdJiw==} + /@trigger.dev/core@3.2.0: + resolution: {integrity: sha512-V0YpClYeYP1zgEiH6Mhz9IRN+nwhTsXqWt3b6DBsJq9OWT4zFHuOW14lmGD0jq7tdDfqWffjnE119i4NLTXZCg==} engines: {node: '>=18.20.0'} dependencies: '@electric-sql/client': 0.6.3 @@ -10259,28 +10265,28 @@ packages: - utf-8-validate dev: false - /@trigger.dev/nextjs@3.1.2(@trigger.dev/sdk@3.1.2)(next@14.2.10): - resolution: {integrity: sha512-uwdQE1vM7Do7xCbqpw4PKw8/YbW3mjwpeTK7so5hsrndcKDxhr5rarPrjQaAKt0saa7mhxwmp+wFnmeCeRdTvQ==} + /@trigger.dev/nextjs@3.2.0(@trigger.dev/sdk@3.2.0)(next@14.2.10): + resolution: {integrity: sha512-bsHbG0VDCRjp9vncwMIhc2hGgY5Xz0mO3TAEhNFNUT9WQzGQkzVjex06gZlFJv5S4jQorpPvazZCSeZJrW0cyw==} engines: {node: '>=18.0.0'} peerDependencies: '@trigger.dev/sdk': ~2.3.0 || ^3.0.0 next: '>=12.0.0' dependencies: - '@trigger.dev/sdk': 3.1.2 + '@trigger.dev/sdk': 3.2.0 debug: 4.3.7(supports-color@8.1.1) next: 14.2.10(@babel/core@7.26.0)(@opentelemetry/api@1.4.1)(react-dom@18.3.1)(react@18.3.1) transitivePeerDependencies: - supports-color dev: false - /@trigger.dev/sdk@3.1.2: - resolution: {integrity: sha512-5G1mvZgTTFHaNzXV+wL5V6o2mfCd9gyH2jTehJVGDak2PbFTcXXLYHMY+TOWG0G2eAAdUm6kCGpCZCNYiY+StQ==} + /@trigger.dev/sdk@3.2.0: + resolution: {integrity: sha512-gZqR88+vA04VHYruFqdRhttL7g3WKqeQk3EcAx2Q0mubDyoWCcMklBja+q1bLNi2P4/tKK4Z7WK90Y33RVVWcQ==} engines: {node: '>=18.20.0'} dependencies: '@opentelemetry/api': 1.4.1 '@opentelemetry/api-logs': 0.52.1 '@opentelemetry/semantic-conventions': 1.13.0 - '@trigger.dev/core': 3.1.2 + '@trigger.dev/core': 3.2.0 chalk: 5.3.0 cronstrue: 2.51.0 debug: 4.3.7(supports-color@8.1.1) @@ -10297,12 +10303,12 @@ packages: - utf-8-validate dev: false - /@trigger.dev/slack@3.1.2: - resolution: {integrity: sha512-PjZ5grV18LEZNw6UhnaAA7n3TVjFpSqxQx/o4D2qUEItqWhtcojwjEkvVE3Qi740rgIsO9FMl+sD3iZRfOVF2g==} + /@trigger.dev/slack@3.2.0: + resolution: {integrity: sha512-eep1YHWXulIOo2tD4nYqQbdenis2ld844yF8TuZhpzw3lvrcZ4uJWmoas9L6IHRR6XbBeKOa1bC7qHsf6uKYVw==} engines: {node: '>=16.8.0'} dependencies: '@slack/web-api': 6.13.0 - '@trigger.dev/sdk': 3.1.2 + '@trigger.dev/sdk': 3.2.0 zod: 3.22.3 transitivePeerDependencies: - bufferutil @@ -13187,8 +13193,8 @@ packages: which: 1.3.1 dev: true - /cross-spawn@7.0.5: - resolution: {integrity: sha512-ZVJrKKYunU38/76t0RMOulHOnUcbU9GbpWKAOZ0mhjr7CX6FVrH+4FrAapSOekrgFQ3f/8gwMEuIft0aKq6Hug==} + /cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} dependencies: path-key: 3.1.1 @@ -13924,7 +13930,7 @@ packages: resolution: {integrity: sha512-fE1aywjRrWGxV3miaiUr3d2zC/VAiuzEGghi+QzgIA9fEf/M5hLMaRSXb4IxbUAwGmaLi0IozdZddnVU96acag==} hasBin: true dependencies: - cross-spawn: 7.0.5 + cross-spawn: 7.0.6 dotenv: 16.4.5 dotenv-expand: 10.0.0 minimist: 1.2.8 @@ -14968,7 +14974,7 @@ packages: '@types/json-schema': 7.0.15 ajv: 6.12.6 chalk: 4.1.2 - cross-spawn: 7.0.5 + cross-spawn: 7.0.6 debug: 4.3.7(supports-color@8.1.1) escape-string-regexp: 4.0.0 eslint-scope: 8.2.0 @@ -15167,7 +15173,7 @@ packages: resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==} engines: {node: '>=10'} dependencies: - cross-spawn: 7.0.5 + cross-spawn: 7.0.6 get-stream: 6.0.1 human-signals: 2.1.0 is-stream: 2.0.1 @@ -15182,7 +15188,7 @@ packages: resolution: {integrity: sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==} engines: {node: '>=16.17'} dependencies: - cross-spawn: 7.0.5 + cross-spawn: 7.0.6 get-stream: 8.0.1 human-signals: 5.0.0 is-stream: 3.0.0 @@ -15197,7 +15203,7 @@ packages: engines: {node: ^18.19.0 || >=20.5.0} dependencies: '@sindresorhus/merge-streams': 4.0.0 - cross-spawn: 7.0.5 + cross-spawn: 7.0.6 figures: 6.1.0 get-stream: 9.0.1 human-signals: 7.0.0 @@ -15215,7 +15221,7 @@ packages: engines: {node: ^18.19.0 || >=20.5.0} dependencies: '@sindresorhus/merge-streams': 4.0.0 - cross-spawn: 7.0.5 + cross-spawn: 7.0.6 figures: 6.1.0 get-stream: 9.0.1 human-signals: 8.0.0 @@ -15593,7 +15599,7 @@ packages: resolution: {integrity: sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==} engines: {node: '>=14'} dependencies: - cross-spawn: 7.0.5 + cross-spawn: 7.0.6 signal-exit: 4.1.0 /form-data-encoder@1.7.2: @@ -15819,7 +15825,7 @@ packages: dependencies: '@mdx-js/mdx': 3.1.0(acorn@8.14.0) chokidar: 3.6.0 - cross-spawn: 7.0.5 + cross-spawn: 7.0.6 esbuild: 0.23.1 estree-util-value-to-estree: 3.2.1 fast-glob: 3.3.2 @@ -20432,7 +20438,7 @@ packages: resolution: {integrity: sha512-HkrjG2aJlvF0t2BMH0e2LB/EHf3Lcq3fNMzy4GYHcQblAvOl+QQji1Lx7WRBMqpVK8p+KR7bCg7oqAMXtdgqyw==} dependencies: ansi-escapes: 4.3.2 - cross-spawn: 7.0.5 + cross-spawn: 7.0.6 dev: true /path-exists@3.0.0: @@ -20598,6 +20604,17 @@ packages: postcss-selector-parser: 6.1.2 dev: false + /postcss-import@15.1.0(postcss@8.4.45): + resolution: {integrity: sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==} + engines: {node: '>=14.0.0'} + peerDependencies: + postcss: ^8.0.0 + dependencies: + postcss: 8.4.45 + postcss-value-parser: 4.2.0 + read-cache: 1.0.0 + resolve: 1.22.8 + /postcss-import@15.1.0(postcss@8.4.49): resolution: {integrity: sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==} engines: {node: '>=14.0.0'} @@ -20608,6 +20625,16 @@ packages: postcss-value-parser: 4.2.0 read-cache: 1.0.0 resolve: 1.22.8 + dev: false + + /postcss-js@4.0.1(postcss@8.4.45): + resolution: {integrity: sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==} + engines: {node: ^12 || ^14 || >= 16} + peerDependencies: + postcss: ^8.4.21 + dependencies: + camelcase-css: 2.0.1 + postcss: 8.4.45 /postcss-js@4.0.1(postcss@8.4.49): resolution: {integrity: sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==} @@ -20617,6 +20644,24 @@ packages: dependencies: camelcase-css: 2.0.1 postcss: 8.4.49 + dev: false + + /postcss-load-config@4.0.2(postcss@8.4.45)(ts-node@10.9.2): + resolution: {integrity: sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==} + engines: {node: '>= 14'} + peerDependencies: + postcss: '>=8.0.9' + ts-node: '>=9.0.0' + peerDependenciesMeta: + postcss: + optional: true + ts-node: + optional: true + dependencies: + lilconfig: 3.1.2 + postcss: 8.4.45 + ts-node: 10.9.2(@types/node@20.14.9)(typescript@5.5.3) + yaml: 2.6.0 /postcss-load-config@4.0.2(postcss@8.4.49)(ts-node@10.9.2): resolution: {integrity: sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==} @@ -20635,6 +20680,15 @@ packages: ts-node: 10.9.2(@types/node@20.14.9)(typescript@5.5.3) yaml: 2.6.0 + /postcss-nested@6.2.0(postcss@8.4.45): + resolution: {integrity: sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==} + engines: {node: '>=12.0'} + peerDependencies: + postcss: ^8.2.14 + dependencies: + postcss: 8.4.45 + postcss-selector-parser: 6.1.2 + /postcss-nested@6.2.0(postcss@8.4.49): resolution: {integrity: sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==} engines: {node: '>=12.0'} @@ -20643,6 +20697,7 @@ packages: dependencies: postcss: 8.4.49 postcss-selector-parser: 6.1.2 + dev: false /postcss-selector-parser@6.0.10: resolution: {integrity: sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==} @@ -20693,7 +20748,6 @@ packages: nanoid: 3.3.7 picocolors: 1.1.1 source-map-js: 1.2.1 - dev: true /postcss@8.4.49: resolution: {integrity: sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==} @@ -23334,11 +23388,11 @@ packages: normalize-path: 3.0.0 object-hash: 3.0.0 picocolors: 1.1.1 - postcss: 8.4.49 - postcss-import: 15.1.0(postcss@8.4.49) - postcss-js: 4.0.1(postcss@8.4.49) - postcss-load-config: 4.0.2(postcss@8.4.49)(ts-node@10.9.2) - postcss-nested: 6.2.0(postcss@8.4.49) + postcss: 8.4.45 + postcss-import: 15.1.0(postcss@8.4.45) + postcss-js: 4.0.1(postcss@8.4.45) + postcss-load-config: 4.0.2(postcss@8.4.45)(ts-node@10.9.2) + postcss-nested: 6.2.0(postcss@8.4.45) postcss-selector-parser: 6.1.2 resolve: 1.22.8 sucrase: 3.35.0