diff --git a/apps/www/app/glossary/[slug]/faq.tsx b/apps/www/app/glossary/[slug]/faq.tsx new file mode 100644 index 0000000000..3a73e6b0ba --- /dev/null +++ b/apps/www/app/glossary/[slug]/faq.tsx @@ -0,0 +1,38 @@ +import { + Accordion, + AccordionContent, + AccordionItem, + AccordionTrigger, +} from "@/components/ui/accordion"; + +export function FAQ(props: { + epigraph?: string; + title: string; + description: string; + items: Array<{ question: string; answer: string }>; +}) { + return ( + <> +
+

+ {props.title} +

+

+ {props.description} +

+
+
+ + {props.items.map((item) => ( + + + {item.question} + + {item.answer} + + ))} + +
+ + ); +} diff --git a/apps/www/app/glossary/[slug]/page.tsx b/apps/www/app/glossary/[slug]/page.tsx new file mode 100644 index 0000000000..0afcf55c21 --- /dev/null +++ b/apps/www/app/glossary/[slug]/page.tsx @@ -0,0 +1,283 @@ +import { CTA } from "@/components/cta"; +import { Frame } from "@/components/frame"; + +import TermsStepperMobile from "@/components/glossary/terms-stepper-mobile"; +import { MDX } from "@/components/mdx-content"; +import { TopLeftShiningLight, TopRightShiningLight } from "@/components/svg/background-shiny"; +import { Card, CardContent, CardHeader } from "@/components/ui/card"; +import { MeteorLinesAngular } from "@/components/ui/meteorLines"; +import { cn } from "@/lib/utils"; +import { allGlossaries } from "content-collections"; +import { Zap } from "lucide-react"; +import type { Metadata } from "next"; +import Link from "next/link"; +import { notFound } from "next/navigation"; +import { FAQ } from "./faq"; +import Takeaways from "./takeaways"; +import TermsRolodexDesktop from "@/components/glossary/terms-rolodex-desktop"; +import { FilterableCommand } from "@/components/glossary/search"; + +export const generateStaticParams = async () => + allGlossaries.map((term) => ({ + slug: term.slug, + })); + +export function generateMetadata({ + params, +}: { + params: { slug: string }; +}): Metadata { + const term = allGlossaries.find((term) => term.slug === params.slug); + if (!term) { + notFound(); + } + return { + title: `${term.title} | Unkey Glossary`, + description: term.description, + openGraph: { + title: `${term.title} | Unkey Glossary`, + description: term.description, + url: `https://unkey.com/glossary/${term.slug}`, + siteName: "unkey.com", + type: "article", + }, + twitter: { + card: "summary_large_image", + title: `${term.title} | Unkey Glossary`, + description: term.description, + site: "@unkeydev", + creator: "@unkeydev", + }, + icons: { + shortcut: "/images/landing/unkey.png", + }, + }; +} + +const GlossaryTermWrapper = async ({ params }: { params: { slug: string } }) => { + const term = allGlossaries.find((term) => term.slug === params.slug); + if (!term) { + notFound(); + } + + const relatedTerms: { + slug: string; + term: string; + tldr: string; + }[] = []; + return ( + <> +
+
+ +
+
+ + + + + + + + +
+
+ +
+
+
+ {/* Left Sidebar */} +
+
+

+ Find a term +

+ +
+

Terms

+ ({ slug: term.slug, title: term.title }))} + /> + ({ slug: term.slug, title: term.title }))} + /> +
+
+
+ {/* Main Content */} +
+
+
+ + + Glossary + + + / + + {term.term} + +
+

+ {term.h1} +

+

+ {term.intro} +

+
+
+ +
+
+ +
+
+ +
+
+ {/* Right Sidebar */} +
+
+ {term.tableOfContents?.length !== 0 && ( +
+

Contents

+
    + {term.tableOfContents.map((heading) => ( +
  • + 2, + "ml-4": heading?.level && heading.level === 3, + "ml-8": heading?.level && heading.level === 4, + })} + > + {heading?.text} + +
  • + ))} +
+
+ )} + {/* Related Blogs */} +
+

Related Terms

+
+ {relatedTerms.length > 0 ? ( + relatedTerms.map((relatedTerm) => ( + + + + +
+

+ TL;DR +

+

{relatedTerm.tldr}

+
+ +
+ +

+ {relatedTerm.term} +

+
+
+ + )) + ) : ( +

No related terms found.

+ )} +
+
+
+
+
+ +
+
+ + ); +}; + +export default GlossaryTermWrapper; diff --git a/apps/www/app/glossary/[slug]/takeaways.tsx b/apps/www/app/glossary/[slug]/takeaways.tsx new file mode 100644 index 0000000000..6decc40bb5 --- /dev/null +++ b/apps/www/app/glossary/[slug]/takeaways.tsx @@ -0,0 +1,170 @@ +import type { Glossary } from "@/.content-collections/generated"; +import { Badge } from "@/components/ui/badge"; +import { Card, CardContent, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"; +import { + AlertTriangle, + BookOpen, + Clock, + Code, + Coffee, + ExternalLink, + FileText, + RefreshCcw, + Zap, +} from "lucide-react"; +import { z } from "zod"; + +const itemSchema = z.object({ + key: z.string(), + value: z.string(), +}); + +export const takeawaysSchema = z.object({ + tldr: z.string(), + definitionAndStructure: z.array(itemSchema), + historicalContext: z.array(itemSchema), + 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 default function Takeaways(props: Pick) { + return ( + +
+ + {props.term}: Key Takeaways + + +
+

+ TL;DR +

+

{props.takeaways.tldr}

+
+
+
} + title="Definition & Structure" + content={ +
+ {props.takeaways.definitionAndStructure.map((item) => ( +
+ {item.key} + + {item.value} + +
+ ))} +
+ } + /> +
} + title="Historical Context" + items={props.takeaways.historicalContext} + /> +
} + title="Usage in APIs" + content={ + <> +
+ {props.takeaways.usageInAPIs.tags.map((tag) => ( + + {tag} + + ))} +
+

{props.takeaways.usageInAPIs.description}

+ + } + /> +
} + title="Best Practices" + items={props.takeaways.bestPractices} + /> +
+
} + title="Recommended Reading" + content={ + + } + /> + + +
+
+ + Did You Know? +
+ {props.takeaways.didYouKnow} + +
+
+ + ); +} + +type SectionProps = { + icon: React.ReactNode; + title: string; +} & ( + | { items: Array>; content?: never } + | { items?: never; content: React.ReactNode } +); + +function Section(props: SectionProps) { + const { icon, title } = props; + return ( +
+

+ {icon} + {title} +

+ {props.content ? ( + props.content + ) : ( +
+ {props.items?.map((item) => + typeof item === "string" ? ( +
  • + {item} +
  • + ) : ( +
    + {item.key} + {item.value} +
    + ), + )} +
    + )} +
    + ); +} diff --git a/apps/www/app/glossary/client.tsx b/apps/www/app/glossary/client.tsx new file mode 100644 index 0000000000..1e6f16dd06 --- /dev/null +++ b/apps/www/app/glossary/client.tsx @@ -0,0 +1,219 @@ +"use client"; +import { CTA } from "@/components/cta"; +import { ChangelogLight } from "@/components/svg/changelog"; + +import { PrimaryButton } from "@/components/button"; +import { Container } from "@/components/container"; +import { FilterableCommand } from "@/components/glossary/search"; +import { MeteorLinesAngular } from "@/components/ui/meteorLines"; +import { LogIn } from "lucide-react"; +import Link from "next/link"; +import { allGlossaries, type Glossary } from "@/.content-collections/generated"; +import { Zap } from "lucide-react"; + +export function GlossaryClient() { + const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ".split(""); + + const groupedTerms = allGlossaries.reduce( + (acc, term) => { + const firstLetter = term.title[0].toUpperCase(); + if (!acc[firstLetter]) { + acc[firstLetter] = []; + } + acc[firstLetter].push(term); + return acc; + }, + {} as Record>, + ); + + return ( +
    +
    +
    + +
    +
    + + + + + + + + +
    +
    + +
    +

    + + API Glossary: A comprehensive guide to API terminology by Unkey + +

    +
    +

    + With clear definitions and helpful examples, Unkey's API Glossary is your go-to + resource for understanding the key concepts and terminology in API development. +

    +
    +
    +
    +

    Ready to protect your API?

    + + + +
    +
    +
    +
    + {/* Left Sidebar */} +
    +

    + Find a term +

    + +
    +
    +
    + {alphabet.map((letter) => + groupedTerms[letter]?.length > 0 ? ( + + {letter} + + ) : ( + + {letter} + + ), + )} +
    + {Object.entries(groupedTerms).map( + ([letter, letterTerms]) => + letterTerms.length > 0 && ( +
    +

    {letter}

    +
    + {letterTerms.map(({ slug, categories, takeaways, term }) => ( + +
    +
    +
    +

    + TL;DR +

    +

    {takeaways.tldr}

    +
    +
    +
    +
    +
    +
    + {categories.length > 0 + ? categories.map((categorySlug) => ( +
    + {categorySlug // unslugged + .replace(/-/g, " ") + .replace(/\b\w/g, (char) => char.toUpperCase())} +
    + )) + : null} +
    +
    +
    +
    +

    + {term} +

    +
    +
    +
    + + ))} +
    +
    + ), + )} +
    +
    +
    + +
    + ); +} + +// +// +// +// +//
    +//

    +// TL;DR +//

    +//

    {takeaways.tldr}

    +//
    +// +//
    +// +//

    {term}

    +//
    +//
    +// diff --git a/apps/www/app/glossary/data-client.tsx b/apps/www/app/glossary/data-client.tsx new file mode 100644 index 0000000000..209dde91ae --- /dev/null +++ b/apps/www/app/glossary/data-client.tsx @@ -0,0 +1,6 @@ +import { FileJson } from "lucide-react"; +import { categories } from "./data"; +// note this is a separate client-file to include the icons, so that we can load the typescript .ts file into our content-collection config +export const categoriesWithIcons = [ + ...categories.map((c) => ({ ...c, icon: })), +] as const; diff --git a/apps/www/app/glossary/data.ts b/apps/www/app/glossary/data.ts new file mode 100644 index 0000000000..855021f43a --- /dev/null +++ b/apps/www/app/glossary/data.ts @@ -0,0 +1,21 @@ +import { z } from "zod"; + +// note that this doesn't include the react icons, so that we can load the typescript .ts file into our content-collection config +export const categories = [ + { + slug: "api-specification", + title: "API Specification", + description: + "API & Web standards for defining data formats and interactions (e.g. OpenAPI, REST, HTTP Requests, etc.)", + }, +] as const; + +// Extract slug values to create a union type +type CategorySlug = (typeof categories)[number]["slug"]; + +// Create a Zod enum from the CategorySlug type +export const categoryEnum = z.enum( + categories.map((c) => c.slug) as [CategorySlug, ...Array], +); + +export type CategoryEnum = z.infer; diff --git a/apps/www/app/glossary/page.tsx b/apps/www/app/glossary/page.tsx new file mode 100644 index 0000000000..a200131bc1 --- /dev/null +++ b/apps/www/app/glossary/page.tsx @@ -0,0 +1,34 @@ +import { GlossaryClient } from "./client"; + +export const metadata = { + title: "Glossary | Unkey", + description: "Jumpstart your API development with our pre-built solutions.", + openGraph: { + title: "Glossary | Unkey", + description: "Jumpstart your API development with our pre-built solutions.", + url: "https://unkey.com/glossary", + siteName: "unkey.com", + images: [ + { + url: "https://unkey.com/images/landing/og.png", + width: 1200, + height: 675, + }, + ], + }, + twitter: { + title: "Glossary | Unkey", + card: "summary_large_image", + }, + icons: { + shortcut: "/images/landing/unkey.png", + }, +}; + +export default function GlossaryPage() { + return ( +
    + +
    + ); +} diff --git a/apps/www/components/glossary/search.tsx b/apps/www/components/glossary/search.tsx new file mode 100644 index 0000000000..8d31b8b9cb --- /dev/null +++ b/apps/www/components/glossary/search.tsx @@ -0,0 +1,60 @@ +"use client"; + +import * as React from "react"; +import { Command as CommandPrimitive } from "cmdk"; +import { + Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandList, +} from "@/components/ui/command"; +import { cn } from "@/lib/utils"; +import { useRouter } from "next/navigation"; +import type { Glossary } from "@/.content-collections/generated"; + +export function FilterableCommand(props: { + placeholder: string; + className?: string; + terms: Array; +}) { + const [open, setOpen] = React.useState(false); + const router = useRouter(); + const commandRef = React.useRef(null); + + return ( + div]:border-b-0", props.className)} ref={commandRef}> + setOpen(true)} + // The `onBlur` event checks if the new focus target (relatedTarget) is within the Command component: + // - If it's not (i.e., clicking outside), it closes the list. + // - If it is (i.e., selecting an item), it keeps the list open, allowing the `onSelect` to handle the navigation. + onBlur={(event: React.FocusEvent) => { + const relatedTarget = event.relatedTarget as Node | null; + if (!commandRef.current?.contains(relatedTarget)) { + setOpen(false); + } + }} + /> + + {open && ( + + No terms found. + + {props.terms.map((item) => ( + router.push(`/glossary/${item.slug}`)} + > + {item.title} + + ))} + + + )} + + ); +} diff --git a/apps/www/components/glossary/terms-rolodex-desktop.tsx b/apps/www/components/glossary/terms-rolodex-desktop.tsx new file mode 100644 index 0000000000..17af33a986 --- /dev/null +++ b/apps/www/components/glossary/terms-rolodex-desktop.tsx @@ -0,0 +1,105 @@ +"use client"; + +import { useState } from "react"; +import Link from "next/link"; +import { Button } from "@/components/ui/button"; +import { ChevronUpIcon, ChevronDownIcon } from "lucide-react"; +import { cn } from "@/lib/utils"; +import { useParams } from "next/navigation"; +import type { Glossary } from "@/.content-collections/generated"; + +export default function TermsRolodexDesktop({ + className, + terms, +}: { className?: string; terms: Array> }) { + const params = useParams(); + const currentSlug = params.slug; + if (typeof currentSlug !== "string") { + throw new Error("slug is not a string"); + } + + const [currentIndex, setCurrentIndex] = useState(() => { + const initialIndex = terms.findIndex((term) => term.slug === currentSlug); + return initialIndex >= 0 ? initialIndex : 0; + }); + + const getVisibleTerms = () => { + const totalVisible = Math.min(7, terms.length); + const halfVisible = Math.floor(totalVisible / 2); + + const start = currentIndex - halfVisible; + const end = currentIndex + halfVisible + 1; + + const wrappedTerms = [...terms, ...terms, ...terms]; + const centerOffset = terms.length; + + return wrappedTerms.slice(centerOffset + start, centerOffset + end); + }; + + const handleScroll = (direction: "up" | "down") => { + setCurrentIndex((prevIndex) => { + let newIndex = direction === "up" ? prevIndex - 1 : prevIndex + 1; + if (newIndex < 0) { + newIndex = terms.length - 1; + } + if (newIndex >= terms.length) { + newIndex = 0; + } + return newIndex; + }); + }; + + const visibleTerms = getVisibleTerms(); + + return ( +
    +
    + +
    + {visibleTerms.map((term, index) => ( + 2 && (index === 0 || index === visibleTerms.length - 1), + }, + )} + > + {term.title} + + ))} +
    + +
    +
    + ); +} diff --git a/apps/www/components/glossary/terms-stepper-mobile.tsx b/apps/www/components/glossary/terms-stepper-mobile.tsx new file mode 100644 index 0000000000..1a1b50b479 --- /dev/null +++ b/apps/www/components/glossary/terms-stepper-mobile.tsx @@ -0,0 +1,49 @@ +"use client"; + +import type { Glossary } from "@/.content-collections/generated"; +import { cn } from "@/lib/utils"; +import { ChevronLeftIcon, ChevronRightIcon } from "lucide-react"; +import Link from "next/link"; +import { useParams } from "next/navigation"; + +export default function TermsStepperMobile({ + className, + terms, +}: { className?: string; terms: Array> }) { + const params = useParams(); + const slug = params.slug as string; + const sortedTerms = terms.sort((a, b) => a.title.localeCompare(b.title)); + const slugIndex = sortedTerms.findIndex((term) => term.slug === slug); + const startIndex = slugIndex !== -1 ? slugIndex : 0; + const currentTerm = sortedTerms[startIndex]; + const previousTerm = sortedTerms[(startIndex - 1 + sortedTerms.length) % sortedTerms.length]; + const nextTerm = sortedTerms[(startIndex + 1) % sortedTerms.length]; + + return ( +
    +
    +
    + + + {previousTerm.title} + + +
    +

    {currentTerm.title}

    +
    + + + {nextTerm.title} + + +
    +
    +
    + ); +} diff --git a/apps/www/components/svg/glossary-page.tsx b/apps/www/components/svg/glossary-page.tsx new file mode 100644 index 0000000000..e2266f7dd9 --- /dev/null +++ b/apps/www/components/svg/glossary-page.tsx @@ -0,0 +1,18 @@ +export const KeyIcon = ({ className }: { className?: string }) => ( + + + +); diff --git a/apps/www/components/ui/badge.tsx b/apps/www/components/ui/badge.tsx new file mode 100644 index 0000000000..effc532bc4 --- /dev/null +++ b/apps/www/components/ui/badge.tsx @@ -0,0 +1,33 @@ +import { type VariantProps, cva } from "class-variance-authority"; +import type * as React from "react"; + +import { cn } from "@/lib/utils"; + +const badgeVariants = cva( + "inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2", + { + variants: { + variant: { + default: "border-transparent bg-primary text-primary-foreground hover:bg-primary/80", + secondary: + "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80", + destructive: + "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80", + outline: "text-foreground", + }, + }, + defaultVariants: { + variant: "default", + }, + }, +); + +export interface BadgeProps + extends React.HTMLAttributes, + VariantProps {} + +function Badge({ className, variant, ...props }: BadgeProps) { + return
    ; +} + +export { Badge, badgeVariants }; diff --git a/apps/www/components/ui/button.tsx b/apps/www/components/ui/button.tsx new file mode 100644 index 0000000000..f20e872485 --- /dev/null +++ b/apps/www/components/ui/button.tsx @@ -0,0 +1,49 @@ +import { Slot } from "@radix-ui/react-slot"; +import { type VariantProps, cva } from "class-variance-authority"; +import * as React from "react"; + +import { cn } from "@/lib/utils"; + +const buttonVariants = cva( + "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50", + { + variants: { + variant: { + default: "bg-primary text-primary-foreground hover:bg-primary/90", + destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90", + outline: "border border-input bg-background hover:bg-accent hover:text-accent-foreground", + secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80", + ghost: "hover:bg-accent hover:text-accent-foreground", + link: "text-primary underline-offset-4 hover:underline", + }, + size: { + default: "h-10 px-4 py-2", + sm: "h-9 rounded-md px-3", + lg: "h-11 rounded-md px-8", + icon: "h-10 w-10", + }, + }, + defaultVariants: { + variant: "default", + size: "default", + }, + }, +); + +export interface ButtonProps + extends React.ButtonHTMLAttributes, + VariantProps { + asChild?: boolean; +} + +const Button = React.forwardRef( + ({ className, variant, size, asChild = false, ...props }, ref) => { + const Comp = asChild ? Slot : "button"; + return ( + + ); + }, +); +Button.displayName = "Button"; + +export { Button, buttonVariants }; diff --git a/apps/www/components/ui/card.tsx b/apps/www/components/ui/card.tsx new file mode 100644 index 0000000000..117feb709d --- /dev/null +++ b/apps/www/components/ui/card.tsx @@ -0,0 +1,56 @@ +import * as React from "react"; + +import { cn } from "@/lib/utils"; + +const Card = React.forwardRef>( + ({ className, ...props }, ref) => ( +
    + ), +); +Card.displayName = "Card"; + +const CardHeader = React.forwardRef>( + ({ className, ...props }, ref) => ( +
    + ), +); +CardHeader.displayName = "CardHeader"; + +const CardTitle = React.forwardRef>( + ({ className, ...props }, ref) => ( +

    + ), +); +CardTitle.displayName = "CardTitle"; + +const CardDescription = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +

    +)); +CardDescription.displayName = "CardDescription"; + +const CardContent = React.forwardRef>( + ({ className, ...props }, ref) => ( +

    + ), +); +CardContent.displayName = "CardContent"; + +const CardFooter = React.forwardRef>( + ({ className, ...props }, ref) => ( +
    + ), +); +CardFooter.displayName = "CardFooter"; + +export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }; diff --git a/apps/www/components/ui/input.tsx b/apps/www/components/ui/input.tsx new file mode 100644 index 0000000000..84da2c01c0 --- /dev/null +++ b/apps/www/components/ui/input.tsx @@ -0,0 +1,24 @@ +import * as React from "react"; + +import { cn } from "@/lib/utils"; + +export interface InputProps extends React.InputHTMLAttributes {} + +const Input = React.forwardRef( + ({ className, type, ...props }, ref) => { + return ( + + ); + }, +); +Input.displayName = "Input"; + +export { Input }; diff --git a/apps/www/components/ui/label.tsx b/apps/www/components/ui/label.tsx new file mode 100644 index 0000000000..ef8f3bd7c4 --- /dev/null +++ b/apps/www/components/ui/label.tsx @@ -0,0 +1,21 @@ +"use client"; + +import * as LabelPrimitive from "@radix-ui/react-label"; +import { type VariantProps, cva } from "class-variance-authority"; +import * as React from "react"; + +import { cn } from "@/lib/utils"; + +const labelVariants = cva( + "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70", +); + +const Label = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & VariantProps +>(({ className, ...props }, ref) => ( + +)); +Label.displayName = LabelPrimitive.Root.displayName; + +export { Label }; diff --git a/apps/www/content-collections.ts b/apps/www/content-collections.ts index b92e3b0eee..5d075fb1de 100644 --- a/apps/www/content-collections.ts +++ b/apps/www/content-collections.ts @@ -2,6 +2,7 @@ import { defineCollection, defineConfig } from "@content-collections/core"; 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"; const posts = defineCollection({ name: "posts", @@ -104,6 +105,78 @@ const job = defineCollection({ }, }); +const glossary = defineCollection({ + name: "glossary", + directory: "content/glossary", + include: "*.mdx", + schema: (z) => ({ + title: z.string(), + description: z.string(), + intro: z.string(), + 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(), + }), + }), + transform: async (document, context) => { + const mdx = await compileMDX(context, document, { + remarkPlugins: [remarkGfm, remarkHeading, remarkStructure], + }); + const slugger = new GithubSlugger(); + // This regex is different from the one in the blog post. It matches the first header without requiring a newline as well (the h1 is provided in the frontmatter) + + const regXHeader = /(?:^|\n)(?#+)\s+(?.+)/g; + const tableOfContents = Array.from(document.content.matchAll(regXHeader)) + .map(({ groups }) => { + const flag = groups?.flag; + const content = groups?.content; + // Only include headers that are not the main title (h1) + if (flag && flag.length > 1) { + return { + level: flag.length, + text: content, + slug: content ? slugger.slug(content) : undefined, + }; + } + return null; + }) + .filter(Boolean); // Remove null entries + return { + ...document, + mdx, + slug: document._meta.path, + url: `/glossary/${document._meta.path}`, + tableOfContents, + }; + }, +}); + export default defineConfig({ - collections: [posts, changelog, policy, job], + collections: [posts, changelog, policy, job, glossary], }); diff --git a/apps/www/content/glossary/mime-types.mdx b/apps/www/content/glossary/mime-types.mdx new file mode 100644 index 0000000000..a42590ec45 --- /dev/null +++ b/apps/www/content/glossary/mime-types.mdx @@ -0,0 +1,98 @@ +--- +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" +intro: "MIME types are essential identifiers in digital communication, specifying the nature and format of data. They play a crucial role in web development and API interactions, ensuring that information is correctly interpreted and displayed across various platforms and applications." +categories: ["api-specification"] +takeaways: + tldr: "Crucial identifiers in digital communication, especially APIs. They specify data format, ensuring correct interpretation and interoperability." + definitionAndStructure: + - key: "Format" + value: "type/subtype" + - key: "Example" + value: "image/jpeg" + - key: "Optional" + value: "charset=UTF-8" + historicalContext: + - key: "Introduced" + value: "1996" + - key: "Origin" + value: "Email (MIME)" + - key: "Evolution" + value: "HTTP & Web" + 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." + 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" + 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.' +--- + +## 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. + +### 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. + +```http +Content-Type: text/html +``` +This header in a web response tells the browser to interpret the response as HTML. + +### MIME Type List + +A comprehensive MIME type list includes various formats, such as: + +- **Image MIME Types**: + - `image/jpeg` for JPEG images + - `image/png` for PNG images + - `image/gif` for GIF images + +- **Video MIME Types**: + - `video/mp4` for MP4 videos + - `video/x-msvideo` for AVI videos + - `video/webm` for WebM videos + +- **Document MIME Types**: + - `application/pdf` for PDF documents + - `application/msword` for Microsoft Word documents + +### Detailed Structure of MIME Types + +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. + +The structure can also include additional parameters, such as `charset`, which defines the character set used in text data. For example: + +```http +Content-Type: text/html; charset=UTF-8 +``` +This indicates that the HTML content should be interpreted using the UTF-8 character set. + +### Importance of MIME Types in API Development + +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. + +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.