diff --git a/README.md b/README.md index f24b84c..2785508 100644 --- a/README.md +++ b/README.md @@ -44,6 +44,10 @@ npx tsc --noEmit # type check npm run build # static export build ``` +## Intentionally unlinked pages + +- `/creator` (`src/app/creator/page.tsx`) is the creator profile. It is indexable (200, self-referencing canonical, listed in `sitemap.xml`) but deliberately absent from the header, sidebar, footer, and search index, and nothing links to it internally. That is a product decision, not an oversight. Keep it that way unless the decision changes. + ## Machine-readable endpoints - `/changelog.xml`: RSS feed for releases. diff --git a/content/docs/core-concepts/agent-loop.md b/content/docs/core-concepts/agent-loop.md index 973775f..b44d7ca 100644 --- a/content/docs/core-concepts/agent-loop.md +++ b/content/docs/core-concepts/agent-loop.md @@ -3,6 +3,8 @@ title: The Agent Loop slug: agent-loop description: One async generator drives every run, one iteration at a time. order: 1 +metaTitle: "The agent loop: one generator drives every run" +metaDescription: "How one generator runs each iteration: render the prompt, stream the model, run tool calls, record the step, and stop at a final answer or the step limit." --- The whole framework hangs off a single async generator, `AgentSession._run_stream`. `run()` is a thin dispatcher: in streaming mode it hands back that generator, and in blocking mode it drains the generator and returns the final answer. There is no second implementation to keep in sync. diff --git a/content/docs/core-concepts/memory.md b/content/docs/core-concepts/memory.md index 1a791d2..648581d 100644 --- a/content/docs/core-concepts/memory.md +++ b/content/docs/core-concepts/memory.md @@ -3,6 +3,8 @@ title: Memory slug: memory description: A run is a list of typed steps that serialize, replay, and resume. order: 3 +metaTitle: "Memory: typed steps that persist and resume" +metaDescription: "A run is a list of TaskStep, ActionStep, and FinalStep objects. Each renders into model messages and saves as JSON for persistence, replay, and resuming." --- A run is recorded as a list of typed steps rather than a flat list of messages. Steps know how to render themselves back into model messages and how to serialize to JSON, which gives you persistence, replay, and multi-turn continuation without extra machinery. @@ -41,7 +43,7 @@ await restored.run("A follow-up", reset=False) Loading validates the payload as it rebuilds the steps. A malformed or incompatible payload raises `MemoryLoadError` rather than silently producing a broken session. See [Errors](/docs/reference/errors) for the full exception hierarchy. -## What this unlocks +## What this enables Because the full run state lives in one serializable object, several features fall out for free: diff --git a/content/docs/core-concepts/skills.md b/content/docs/core-concepts/skills.md index d437689..a2a7005 100644 --- a/content/docs/core-concepts/skills.md +++ b/content/docs/core-concepts/skills.md @@ -3,6 +3,8 @@ title: Skills slug: skills description: Instruction folders the model loads on demand, catalog first. order: 5 +metaTitle: "Skills: instruction folders loaded on demand" +metaDescription: "Put a SKILL.md folder in your project. The model sees its name and summary in a catalog and loads the full instructions only when it decides it needs them." --- A skill is a folder containing a `SKILL.md` file: YAML frontmatter followed by a markdown body of instructions. Skills let you package expertise, and progressive disclosure keeps them cheap until the model actually needs them. diff --git a/content/docs/core-concepts/streaming-events.md b/content/docs/core-concepts/streaming-events.md index 35a56b1..e53cac7 100644 --- a/content/docs/core-concepts/streaming-events.md +++ b/content/docs/core-concepts/streaming-events.md @@ -3,6 +3,8 @@ title: Streaming Events slug: streaming-events description: A small set of typed events reports everything the loop does. order: 4 +metaTitle: "Streaming events: typed events from every run" +metaDescription: "The small set of typed events the loop emits while it runs: text deltas, tool calls, tool results, and the final event, and how to consume them." --- The loop communicates progress through a small set of frozen event types. Pass `stream=True` to `run()` and you get an async iterator of these events; leave it off and the same generator is drained for you. One code path serves both modes. diff --git a/content/docs/core-concepts/tools.md b/content/docs/core-concepts/tools.md index 5b52cd4..b1bfd1f 100644 --- a/content/docs/core-concepts/tools.md +++ b/content/docs/core-concepts/tools.md @@ -3,6 +3,8 @@ title: Tools slug: tools description: Plain Python functions become schema-validated tools with @tool. order: 2 +metaTitle: "Tools: plain Python functions with @tool" +metaDescription: "Turn a typed Python function into a schema-validated tool with @tool. Docstrings become descriptions, exceptions become observations, timeouts per call." --- A tool is any Python function wrapped with `@tool`. The function name becomes the tool name, the docstring summary becomes the description, and the type hints plus a Google-style `Args:` section become the JSON Schema the model sees. diff --git a/content/docs/getting-started/installation.md b/content/docs/getting-started/installation.md index 0ae7b75..4174ca7 100644 --- a/content/docs/getting-started/installation.md +++ b/content/docs/getting-started/installation.md @@ -3,6 +3,8 @@ title: Installation slug: installation description: Install agentling with pip or uv and configure your provider key. order: 2 +metaTitle: "Install Agentling with pip or uv" +metaDescription: "Install agentling with pip or uv on Python 3.11 or newer, set an API key for OpenAI or any compatible provider, and check the install in one command." --- Install agentling from PyPI: diff --git a/content/docs/getting-started/introduction.md b/content/docs/getting-started/introduction.md index 5b4c255..1028db3 100644 --- a/content/docs/getting-started/introduction.md +++ b/content/docs/getting-started/introduction.md @@ -3,6 +3,8 @@ title: Introduction slug: introduction description: What Agentling is, the idea behind it, and when to reach for it. order: 1 +metaTitle: "What Agentling is and when to use it" +metaDescription: "What Agentling is, the loop it is built on, what you get (typed memory, streamed events, self-healing tools, skills), and when to use something else." --- Agentling is a small async Python framework for building tool-using agents. It gives you a clean ReAct loop, typed memory, streaming events, and recoverable failures in an auditable codebase. diff --git a/content/docs/getting-started/quickstart.md b/content/docs/getting-started/quickstart.md index aff62d0..a22bab1 100644 --- a/content/docs/getting-started/quickstart.md +++ b/content/docs/getting-started/quickstart.md @@ -3,6 +3,8 @@ title: Quickstart slug: quickstart description: Build, run, and stream your first agent in a few lines of Python. order: 3 +metaTitle: "Quickstart: build and stream your first agent" +metaDescription: "Define a tool with @tool, create an Agent with a model, run it, then stream events from the same loop. About 30 lines of Python from start to finish." --- This page takes you from an empty file to a streaming agent with one tool. diff --git a/content/docs/guides/_section.md b/content/docs/guides/_section.md index 6de8c75..5fc2884 100644 --- a/content/docs/guides/_section.md +++ b/content/docs/guides/_section.md @@ -1,7 +1,7 @@ --- title: Guides slug: guides -description: Task focused guides for building production ready agents. +description: "How to run agents in production: sessions, persistence, interruption, error handling, and other providers." icon: compass order: 3 --- diff --git a/content/docs/guides/error-handling.md b/content/docs/guides/error-handling.md index f657ae3..b66be60 100644 --- a/content/docs/guides/error-handling.md +++ b/content/docs/guides/error-handling.md @@ -3,6 +3,8 @@ title: Error Handling slug: error-handling description: Self-healing tool errors, timeouts, redaction, output truncation, and the retry behavior of the model layer. order: 4 +metaTitle: "Error handling: self-healing tools and retries" +metaDescription: "How tool exceptions become observations the model can fix, per-call timeouts, output truncation, error redaction, and the retry policy of the model layer." --- agentling treats failure as something the model can recover from, not something that kills the run. This guide covers the layers of that design: self-healing tool errors, timeouts, redaction, and how the model layer retries. diff --git a/content/docs/guides/interruption.md b/content/docs/guides/interruption.md index 3166443..22ab361 100644 --- a/content/docs/guides/interruption.md +++ b/content/docs/guides/interruption.md @@ -3,6 +3,8 @@ title: Interruption slug: interruption description: Stop a running agent gracefully with session.interrupt() and resume it exactly where it paused. order: 3 +metaTitle: "Interrupt a running agent and resume it" +metaDescription: "Stop a running agent gracefully with session.interrupt(), what the loop does at the next safe point, and how to resume from the step where it paused." --- Sometimes you need to stop an agent mid-run: a user hits a stop button, a deadline passes, or your process receives a shutdown signal. agentling supports graceful interruption that preserves the run so you can resume it later. diff --git a/content/docs/guides/other-providers.md b/content/docs/guides/other-providers.md index dc31333..bc3751b 100644 --- a/content/docs/guides/other-providers.md +++ b/content/docs/guides/other-providers.md @@ -3,6 +3,8 @@ title: Using Other Providers slug: other-providers description: Point OpenAIModel at any OpenAI-compatible endpoint, or implement the Model protocol for a custom provider. order: 5 +metaTitle: "Use other providers with OpenAIModel or Model" +metaDescription: "Point OpenAIModel at any OpenAI-compatible endpoint, or implement the small Model protocol to plug in a provider of your own." --- agentling ships one provider adapter, `OpenAIModel`, but it is not tied to OpenAI. The adapter speaks to any OpenAI-compatible chat-completions endpoint, and the `Model` protocol lets you write your own adapter for anything else. diff --git a/content/docs/guides/persistence-and-resuming.md b/content/docs/guides/persistence-and-resuming.md index 4189ffc..e6ee8d8 100644 --- a/content/docs/guides/persistence-and-resuming.md +++ b/content/docs/guides/persistence-and-resuming.md @@ -3,6 +3,8 @@ title: Persistence and Resuming slug: persistence-and-resuming description: Serialize a session's memory to JSON, restore it later, and continue runs with reset=False. order: 2 +metaTitle: "Persist a session to JSON and resume it later" +metaDescription: "Serialize a session's memory to JSON, restore it in a new process, and continue the run with reset=False so the model picks up exactly where it stopped." --- Every session records its run as typed memory that serializes to JSON. That gives you persistence, replay, and multi-turn continuation without any extra infrastructure. diff --git a/content/docs/guides/sessions-and-concurrency.md b/content/docs/guides/sessions-and-concurrency.md index 2fccfc0..8f74197 100644 --- a/content/docs/guides/sessions-and-concurrency.md +++ b/content/docs/guides/sessions-and-concurrency.md @@ -3,6 +3,8 @@ title: Sessions and Concurrency slug: sessions-and-concurrency description: How Agent and AgentSession split configuration from run state, and how to run agents safely under concurrency. order: 1 +metaTitle: "Sessions and concurrency: Agent vs AgentSession" +metaDescription: "How Agent holds configuration and AgentSession holds per-run state, and the rules for sharing agents across tasks and running sessions concurrently." --- agentling separates what an agent is from what a run does. An `Agent` is immutable configuration, and an `AgentSession` holds the state of one conversation. Understanding that split is the key to running agents safely under concurrency. diff --git a/content/docs/reference/agent-api.md b/content/docs/reference/agent-api.md index cf6f7d3..f1afb21 100644 --- a/content/docs/reference/agent-api.md +++ b/content/docs/reference/agent-api.md @@ -3,6 +3,8 @@ title: Agent API slug: agent-api description: Every Agent constructor parameter, the run method, and the AgentSession surface. order: 1 +metaTitle: "Agent API: constructor, run(), and AgentSession" +metaDescription: "Every Agent constructor parameter and its default, what run() and start() do, and the AgentSession surface for multi-turn, interruptible runs." --- `Agent` is immutable configuration: a model, some tools, and settings. Build one once, share it freely, and let sessions carry the per-run state. This page lists every knob on the constructor and the semantics of `run()` and `start()`. diff --git a/content/docs/reference/errors.md b/content/docs/reference/errors.md index 46c5c7a..3c42b42 100644 --- a/content/docs/reference/errors.md +++ b/content/docs/reference/errors.md @@ -3,6 +3,8 @@ title: Errors slug: errors description: The exception hierarchy, which failures the model recovers from, and what redact_errors changes. order: 4 +metaTitle: "Errors reference: the exception hierarchy" +metaDescription: "Every exception under AgentlingError, which failures the loop recovers from on its own, and what redact_errors changes about what the model sees." --- Every exception agentling raises lives under a single base class, `AgentlingError`, so one `except` clause can catch anything the framework produces. Just as important is what does not raise: most tool failures are converted into observations the model can recover from instead of crashing the run. diff --git a/content/docs/reference/events.md b/content/docs/reference/events.md index 37ea399..58276c3 100644 --- a/content/docs/reference/events.md +++ b/content/docs/reference/events.md @@ -3,6 +3,8 @@ title: Events slug: events description: Every streaming event type, what it carries, and how to consume the stream. order: 3 +metaTitle: "Events reference: every streaming event type" +metaDescription: "Every event the loop can emit, the fields each one carries, and how to consume the stream in blocking and streaming modes." --- When you call `run(task, stream=True)`, the loop communicates progress through a small set of frozen event types. This page is the reference for each one and for `print_events`, the ready-made consumer. diff --git a/content/docs/reference/openai-model.md b/content/docs/reference/openai-model.md index 770c896..a2659fb 100644 --- a/content/docs/reference/openai-model.md +++ b/content/docs/reference/openai-model.md @@ -3,6 +3,8 @@ title: OpenAIModel slug: openai-model description: The built-in provider adapter, its retry policy, and the Model protocol for custom adapters. order: 2 +metaTitle: "OpenAIModel reference and the Model protocol" +metaDescription: "The built-in provider adapter: constructor options, its retry policy, how it streams, and the Model protocol a custom adapter must implement." --- `OpenAIModel` is an adapter for any OpenAI-compatible chat-completions endpoint. It is the only place in agentling that knows OpenAI's wire format: it converts framework-neutral `ChatMessage` lists into provider payloads on the way out and converts responses back on the way in. diff --git a/content/pages/contributing.md b/content/pages/contributing.md index 310a12d..05a30d4 100644 --- a/content/pages/contributing.md +++ b/content/pages/contributing.md @@ -2,6 +2,8 @@ title: Contributing slug: contributing description: How to set up a development environment, run the checks, and contribute to agentling. +metaTitle: "Contributing: setup, checks, pull requests" +metaDescription: "Set up a dev environment with uv, run the test, lint, format, and type checks that CI runs, and open a pull request against the agentling repository." --- Thanks for your interest in improving agentling. The framework stays deliberately small, so thoughtful contributions matter more than big ones. diff --git a/content/pages/privacy.md b/content/pages/privacy.md index 2502236..28efb7f 100644 --- a/content/pages/privacy.md +++ b/content/pages/privacy.md @@ -2,6 +2,8 @@ title: Privacy Policy slug: privacy description: What data this website collects, what it does not, and your choices. +metaTitle: "Privacy policy: what this site collects" +metaDescription: "What this website collects, what it does not, which third parties are involved, and the choices you have." --- Last updated: 7 July 2026 diff --git a/content/pages/terms.md b/content/pages/terms.md index 3add1d1..5f3d273 100644 --- a/content/pages/terms.md +++ b/content/pages/terms.md @@ -2,6 +2,8 @@ title: Terms of Use slug: terms description: The terms that apply to this website and to the agentling open source software. +metaTitle: "Terms of use for this site and the software" +metaDescription: "The terms that apply to using this website and to the agentling open source software, which is MIT licensed." --- Last updated: 7 July 2026 diff --git a/next.config.ts b/next.config.ts index b873185..6022dfd 100644 --- a/next.config.ts +++ b/next.config.ts @@ -12,6 +12,15 @@ const nextConfig: NextConfig = { outputFileTracingIncludes: { "/**": ["./content/**/*", "./src/assets/fonts/**"], }, + // URLs that used to be linked from the site and still receive requests. + async redirects() { + return [ + { source: "/feed.xml", destination: "/changelog.xml", permanent: true }, + { source: "/blog", destination: "/changelog", permanent: true }, + { source: "/blog/:path*", destination: "/changelog", permanent: true }, + { source: "/roadmap", destination: "/changelog", permanent: true }, + ]; + }, }; export default nextConfig; diff --git a/package-lock.json b/package-lock.json index c6748e5..5573e9f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,7 +14,6 @@ "gray-matter": "^4.0.3", "lucide-react": "^1.23.0", "minisearch": "^7.2.0", - "motion": "^12.42.2", "next": "16.2.10", "next-themes": "^0.4.6", "react": "19.2.4", @@ -38,6 +37,7 @@ "@types/react-dom": "^19", "eslint": "^9", "eslint-config-next": "16.2.10", + "sharp": "^0.34.4", "tailwindcss": "^4", "typescript": "^5" } @@ -542,19 +542,20 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==", + "devOptional": true, "license": "MIT", - "optional": true, "engines": { "node": ">=18" } }, "node_modules/@img/sharp-darwin-arm64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz", - "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==", + "version": "0.34.4", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.4.tgz", + "integrity": "sha512-sitdlPzDVyvmINUdJle3TNHl+AG9QcwiAMsXmccqsCOMZNIdW2/7S26w0LyU8euiLVzFBL3dXPwVCq/ODnf2vA==", "cpu": [ "arm64" ], + "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -567,16 +568,17 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-darwin-arm64": "1.2.4" + "@img/sharp-libvips-darwin-arm64": "1.2.3" } }, "node_modules/@img/sharp-darwin-x64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz", - "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==", + "version": "0.34.4", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.4.tgz", + "integrity": "sha512-rZheupWIoa3+SOdF/IcUe1ah4ZDpKBGWcsPX6MT0lYniH9micvIU7HQkYTfrx5Xi8u+YqwLtxC/3vl8TQN6rMg==", "cpu": [ "x64" ], + "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -589,16 +591,17 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-darwin-x64": "1.2.4" + "@img/sharp-libvips-darwin-x64": "1.2.3" } }, "node_modules/@img/sharp-libvips-darwin-arm64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz", - "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.3.tgz", + "integrity": "sha512-QzWAKo7kpHxbuHqUC28DZ9pIKpSi2ts2OJnoIGI26+HMgq92ZZ4vk8iJd4XsxN+tYfNJxzH6W62X5eTcsBymHw==", "cpu": [ "arm64" ], + "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -609,12 +612,13 @@ } }, "node_modules/@img/sharp-libvips-darwin-x64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz", - "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.3.tgz", + "integrity": "sha512-Ju+g2xn1E2AKO6YBhxjj+ACcsPQRHT0bhpglxcEf+3uyPY+/gL8veniKoo96335ZaPo03bdDXMv0t+BBFAbmRA==", "cpu": [ "x64" ], + "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -625,12 +629,13 @@ } }, "node_modules/@img/sharp-libvips-linux-arm": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz", - "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.3.tgz", + "integrity": "sha512-x1uE93lyP6wEwGvgAIV0gP6zmaL/a0tGzJs/BIDDG0zeBhMnuUPm7ptxGhUbcGs4okDJrk4nxgrmxpib9g6HpA==", "cpu": [ "arm" ], + "dev": true, "libc": [ "glibc" ], @@ -644,12 +649,13 @@ } }, "node_modules/@img/sharp-libvips-linux-arm64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz", - "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.3.tgz", + "integrity": "sha512-I4RxkXU90cpufazhGPyVujYwfIm9Nk1QDEmiIsaPwdnm013F7RIceaCc87kAH+oUB1ezqEvC6ga4m7MSlqsJvQ==", "cpu": [ "arm64" ], + "dev": true, "libc": [ "glibc" ], @@ -663,12 +669,13 @@ } }, "node_modules/@img/sharp-libvips-linux-ppc64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz", - "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.3.tgz", + "integrity": "sha512-Y2T7IsQvJLMCBM+pmPbM3bKT/yYJvVtLJGfCs4Sp95SjvnFIjynbjzsa7dY1fRJX45FTSfDksbTp6AGWudiyCg==", "cpu": [ "ppc64" ], + "dev": true, "libc": [ "glibc" ], @@ -701,12 +708,13 @@ } }, "node_modules/@img/sharp-libvips-linux-s390x": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz", - "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.3.tgz", + "integrity": "sha512-RgWrs/gVU7f+K7P+KeHFaBAJlNkD1nIZuVXdQv6S+fNA6syCcoboNjsV2Pou7zNlVdNQoQUpQTk8SWDHUA3y/w==", "cpu": [ "s390x" ], + "dev": true, "libc": [ "glibc" ], @@ -720,12 +728,13 @@ } }, "node_modules/@img/sharp-libvips-linux-x64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz", - "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.3.tgz", + "integrity": "sha512-3JU7LmR85K6bBiRzSUc/Ff9JBVIFVvq6bomKE0e63UXGeRw2HPVEjoJke1Yx+iU4rL7/7kUjES4dZ/81Qjhyxg==", "cpu": [ "x64" ], + "dev": true, "libc": [ "glibc" ], @@ -739,12 +748,13 @@ } }, "node_modules/@img/sharp-libvips-linuxmusl-arm64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz", - "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.3.tgz", + "integrity": "sha512-F9q83RZ8yaCwENw1GieztSfj5msz7GGykG/BA+MOUefvER69K/ubgFHNeSyUu64amHIYKGDs4sRCMzXVj8sEyw==", "cpu": [ "arm64" ], + "dev": true, "libc": [ "musl" ], @@ -758,12 +768,13 @@ } }, "node_modules/@img/sharp-libvips-linuxmusl-x64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz", - "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.3.tgz", + "integrity": "sha512-U5PUY5jbc45ANM6tSJpsgqmBF/VsL6LnxJmIf11kB7J5DctHgqm0SkuXzVWtIY90GnJxKnC/JT251TDnk1fu/g==", "cpu": [ "x64" ], + "dev": true, "libc": [ "musl" ], @@ -777,12 +788,13 @@ } }, "node_modules/@img/sharp-linux-arm": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz", - "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==", + "version": "0.34.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.4.tgz", + "integrity": "sha512-Xyam4mlqM0KkTHYVSuc6wXRmM7LGN0P12li03jAnZ3EJWZqj83+hi8Y9UxZUbxsgsK1qOEwg7O0Bc0LjqQVtxA==", "cpu": [ "arm" ], + "dev": true, "libc": [ "glibc" ], @@ -798,16 +810,17 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-arm": "1.2.4" + "@img/sharp-libvips-linux-arm": "1.2.3" } }, "node_modules/@img/sharp-linux-arm64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz", - "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==", + "version": "0.34.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.4.tgz", + "integrity": "sha512-YXU1F/mN/Wu786tl72CyJjP/Ngl8mGHN1hST4BGl+hiW5jhCnV2uRVTNOcaYPs73NeT/H8Upm3y9582JVuZHrQ==", "cpu": [ "arm64" ], + "dev": true, "libc": [ "glibc" ], @@ -823,16 +836,17 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-arm64": "1.2.4" + "@img/sharp-libvips-linux-arm64": "1.2.3" } }, "node_modules/@img/sharp-linux-ppc64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz", - "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==", + "version": "0.34.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.4.tgz", + "integrity": "sha512-F4PDtF4Cy8L8hXA2p3TO6s4aDt93v+LKmpcYFLAVdkkD3hSxZzee0rh6/+94FpAynsuMpLX5h+LRsSG3rIciUQ==", "cpu": [ "ppc64" ], + "dev": true, "libc": [ "glibc" ], @@ -848,7 +862,7 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-ppc64": "1.2.4" + "@img/sharp-libvips-linux-ppc64": "1.2.3" } }, "node_modules/@img/sharp-linux-riscv64": { @@ -877,12 +891,13 @@ } }, "node_modules/@img/sharp-linux-s390x": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz", - "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==", + "version": "0.34.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.4.tgz", + "integrity": "sha512-qVrZKE9Bsnzy+myf7lFKvng6bQzhNUAYcVORq2P7bDlvmF6u2sCmK2KyEQEBdYk+u3T01pVsPrkj943T1aJAsw==", "cpu": [ "s390x" ], + "dev": true, "libc": [ "glibc" ], @@ -898,16 +913,17 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-s390x": "1.2.4" + "@img/sharp-libvips-linux-s390x": "1.2.3" } }, "node_modules/@img/sharp-linux-x64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz", - "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==", + "version": "0.34.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.4.tgz", + "integrity": "sha512-ZfGtcp2xS51iG79c6Vhw9CWqQC8l2Ot8dygxoDoIQPTat/Ov3qAa8qpxSrtAEAJW+UjTXc4yxCjNfxm4h6Xm2A==", "cpu": [ "x64" ], + "dev": true, "libc": [ "glibc" ], @@ -923,16 +939,17 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-x64": "1.2.4" + "@img/sharp-libvips-linux-x64": "1.2.3" } }, "node_modules/@img/sharp-linuxmusl-arm64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz", - "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==", + "version": "0.34.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.4.tgz", + "integrity": "sha512-8hDVvW9eu4yHWnjaOOR8kHVrew1iIX+MUgwxSuH2XyYeNRtLUe4VNioSqbNkB7ZYQJj9rUTT4PyRscyk2PXFKA==", "cpu": [ "arm64" ], + "dev": true, "libc": [ "musl" ], @@ -948,16 +965,17 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" + "@img/sharp-libvips-linuxmusl-arm64": "1.2.3" } }, "node_modules/@img/sharp-linuxmusl-x64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz", - "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==", + "version": "0.34.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.4.tgz", + "integrity": "sha512-lU0aA5L8QTlfKjpDCEFOZsTYGn3AEiO6db8W5aQDxj0nQkVrZWmN3ZP9sYKWJdtq3PWPhUNlqehWyXpYDcI9Sg==", "cpu": [ "x64" ], + "dev": true, "libc": [ "musl" ], @@ -973,20 +991,21 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-x64": "1.2.4" + "@img/sharp-libvips-linuxmusl-x64": "1.2.3" } }, "node_modules/@img/sharp-wasm32": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz", - "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==", + "version": "0.34.4", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.4.tgz", + "integrity": "sha512-33QL6ZO/qpRyG7woB/HUALz28WnTMI2W1jgX3Nu2bypqLIKx/QKMILLJzJjI+SIbvXdG9fUnmrxR7vbi1sTBeA==", "cpu": [ "wasm32" ], + "dev": true, "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", "optional": true, "dependencies": { - "@emnapi/runtime": "^1.7.0" + "@emnapi/runtime": "^1.5.0" }, "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" @@ -996,12 +1015,13 @@ } }, "node_modules/@img/sharp-win32-arm64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz", - "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==", + "version": "0.34.4", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.4.tgz", + "integrity": "sha512-2Q250do/5WXTwxW3zjsEuMSv5sUU4Tq9VThWKlU2EYLm4MB7ZeMwF+SFJutldYODXF6jzc6YEOC+VfX0SZQPqA==", "cpu": [ "arm64" ], + "dev": true, "license": "Apache-2.0 AND LGPL-3.0-or-later", "optional": true, "os": [ @@ -1015,12 +1035,13 @@ } }, "node_modules/@img/sharp-win32-ia32": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz", - "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==", + "version": "0.34.4", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.4.tgz", + "integrity": "sha512-3ZeLue5V82dT92CNL6rsal6I2weKw1cYu+rGKm8fOCCtJTR2gYeUfY3FqUnIJsMUPIH68oS5jmZ0NiJ508YpEw==", "cpu": [ "ia32" ], + "dev": true, "license": "Apache-2.0 AND LGPL-3.0-or-later", "optional": true, "os": [ @@ -1034,12 +1055,13 @@ } }, "node_modules/@img/sharp-win32-x64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz", - "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==", + "version": "0.34.4", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.4.tgz", + "integrity": "sha512-xIyj4wpYs8J18sVN3mSQjwrw7fKUqRw+Z5rnHNCy5fYTxigBz81u5mOMPmFumwjcn8+ld1ppptMBCLic1nz6ig==", "cpu": [ "x64" ], + "dev": true, "license": "Apache-2.0 AND LGPL-3.0-or-later", "optional": true, "os": [ @@ -4129,33 +4151,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/framer-motion": { - "version": "12.42.2", - "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.42.2.tgz", - "integrity": "sha512-5XY9luDiu0oHfHBjpDthFMh0ES+122w6p/papSJBweMkO8Sn+PW2QaEgRblQBpWFnuvZS5qvarpt/hO2pjGmnw==", - "license": "MIT", - "dependencies": { - "motion-dom": "^12.42.2", - "motion-utils": "^12.39.0", - "tslib": "^2.4.0" - }, - "peerDependencies": { - "@emotion/is-prop-valid": "*", - "react": "^18.0.0 || ^19.0.0", - "react-dom": "^18.0.0 || ^19.0.0" - }, - "peerDependenciesMeta": { - "@emotion/is-prop-valid": { - "optional": true - }, - "react": { - "optional": true - }, - "react-dom": { - "optional": true - } - } - }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -6602,47 +6597,6 @@ "integrity": "sha512-dqT2XBYUOZOiC5t2HRnwADjhNS2cecp9u+TJRiJ1Qp/f5qjkeT5APcGPjHw+bz89Ms8Jp+cG4AlE+QZ/QnDglg==", "license": "MIT" }, - "node_modules/motion": { - "version": "12.42.2", - "resolved": "https://registry.npmjs.org/motion/-/motion-12.42.2.tgz", - "integrity": "sha512-Atvv11yUKIid41cVrRBDVX5m8tF8kNpExRSlbpt6APClhDjtwQssgFHhQzejxw7/7YYbjHSPKBVbHo05BuJT5Q==", - "license": "MIT", - "dependencies": { - "framer-motion": "^12.42.2", - "tslib": "^2.4.0" - }, - "peerDependencies": { - "@emotion/is-prop-valid": "*", - "react": "^18.0.0 || ^19.0.0", - "react-dom": "^18.0.0 || ^19.0.0" - }, - "peerDependenciesMeta": { - "@emotion/is-prop-valid": { - "optional": true - }, - "react": { - "optional": true - }, - "react-dom": { - "optional": true - } - } - }, - "node_modules/motion-dom": { - "version": "12.42.2", - "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.42.2.tgz", - "integrity": "sha512-5gIMWLp/PycBtJRJWRgjxke5n8dlvkSn2DrYW+tr3XcqAZY1xZh6BJyooJXCM8wdfM7wfMjkBJNLge1CKPUIRA==", - "license": "MIT", - "dependencies": { - "motion-utils": "^12.39.0" - } - }, - "node_modules/motion-utils": { - "version": "12.39.0", - "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.39.0.tgz", - "integrity": "sha512-8nadJAJjTtqRkmRF36FoJTrywK9nnFmnPwnSMyxaOCU7GDjN9RTMJIxx9De8ErM+vpPhMccr/6fo5WciyQLnMQ==", - "license": "MIT" - }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -6753,97 +6707,615 @@ "react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc" } }, - "node_modules/next/node_modules/postcss": { - "version": "8.4.31", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", - "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } + "node_modules/next/node_modules/@img/sharp-darwin-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz", + "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" ], - "license": "MIT", - "dependencies": { - "nanoid": "^3.3.6", - "picocolors": "^1.0.0", - "source-map-js": "^1.0.2" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, - "node_modules/node-exports-info": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/node-exports-info/-/node-exports-info-1.6.2.tgz", - "integrity": "sha512-kXs9Go0cah0qHVV2v389IXQLdLCeE1xfFtjOAF+iobu0OIoG1pje8At2vMHyaPMiPMnG/LWP50twML21eMcAag==", - "dev": true, - "license": "MIT", - "dependencies": { - "array.prototype.flatmap": "^1.3.3", - "es-errors": "^1.3.0", - "object.entries": "^1.1.9", - "semver": "^6.3.1" - }, "engines": { - "node": ">= 0.4" + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/node-releases": { - "version": "2.0.50", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.50.tgz", - "integrity": "sha512-J6l92tKHX6w8Jy5nO1Vuc01NoIiRGi/d6qBKVxh+IQ8Cr3b6HbVNfKiF8ZpFKufTwpwxMmce2W3iQZ861ZRyTg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "node_modules/object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.2.4" } }, - "node_modules/object-inspect": { - "version": "1.13.4", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", - "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", - "dev": true, - "license": "MIT", + "node_modules/next/node_modules/@img/sharp-darwin-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz", + "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": ">= 0.4" + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object-keys": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", - "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.2.4" } }, - "node_modules/object.assign": { + "node_modules/next/node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz", + "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/next/node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz", + "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/next/node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz", + "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==", + "cpu": [ + "arm" + ], + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/next/node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz", + "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/next/node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz", + "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==", + "cpu": [ + "ppc64" + ], + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/next/node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz", + "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==", + "cpu": [ + "s390x" + ], + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/next/node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz", + "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/next/node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz", + "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/next/node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz", + "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/next/node_modules/@img/sharp-linux-arm": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz", + "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==", + "cpu": [ + "arm" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.2.4" + } + }, + "node_modules/next/node_modules/@img/sharp-linux-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz", + "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.2.4" + } + }, + "node_modules/next/node_modules/@img/sharp-linux-ppc64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz", + "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==", + "cpu": [ + "ppc64" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.2.4" + } + }, + "node_modules/next/node_modules/@img/sharp-linux-s390x": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz", + "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==", + "cpu": [ + "s390x" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.2.4" + } + }, + "node_modules/next/node_modules/@img/sharp-linux-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz", + "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.2.4" + } + }, + "node_modules/next/node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz", + "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" + } + }, + "node_modules/next/node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz", + "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.2.4" + } + }, + "node_modules/next/node_modules/@img/sharp-wasm32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz", + "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==", + "cpu": [ + "wasm32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.7.0" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/next/node_modules/@img/sharp-win32-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz", + "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/next/node_modules/@img/sharp-win32-ia32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz", + "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/next/node_modules/@img/sharp-win32-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz", + "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/next/node_modules/postcss": { + "version": "8.4.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", + "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.6", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/next/node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "license": "ISC", + "optional": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/next/node_modules/sharp": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", + "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", + "hasInstallScript": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@img/colour": "^1.0.0", + "detect-libc": "^2.1.2", + "semver": "^7.7.3" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.34.5", + "@img/sharp-darwin-x64": "0.34.5", + "@img/sharp-libvips-darwin-arm64": "1.2.4", + "@img/sharp-libvips-darwin-x64": "1.2.4", + "@img/sharp-libvips-linux-arm": "1.2.4", + "@img/sharp-libvips-linux-arm64": "1.2.4", + "@img/sharp-libvips-linux-ppc64": "1.2.4", + "@img/sharp-libvips-linux-riscv64": "1.2.4", + "@img/sharp-libvips-linux-s390x": "1.2.4", + "@img/sharp-libvips-linux-x64": "1.2.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", + "@img/sharp-libvips-linuxmusl-x64": "1.2.4", + "@img/sharp-linux-arm": "0.34.5", + "@img/sharp-linux-arm64": "0.34.5", + "@img/sharp-linux-ppc64": "0.34.5", + "@img/sharp-linux-riscv64": "0.34.5", + "@img/sharp-linux-s390x": "0.34.5", + "@img/sharp-linux-x64": "0.34.5", + "@img/sharp-linuxmusl-arm64": "0.34.5", + "@img/sharp-linuxmusl-x64": "0.34.5", + "@img/sharp-wasm32": "0.34.5", + "@img/sharp-win32-arm64": "0.34.5", + "@img/sharp-win32-ia32": "0.34.5", + "@img/sharp-win32-x64": "0.34.5" + } + }, + "node_modules/node-exports-info": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/node-exports-info/-/node-exports-info-1.6.2.tgz", + "integrity": "sha512-kXs9Go0cah0qHVV2v389IXQLdLCeE1xfFtjOAF+iobu0OIoG1pje8At2vMHyaPMiPMnG/LWP50twML21eMcAag==", + "dev": true, + "license": "MIT", + "dependencies": { + "array.prototype.flatmap": "^1.3.3", + "es-errors": "^1.3.0", + "object.entries": "^1.1.9", + "semver": "^6.3.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/node-releases": { + "version": "2.0.50", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.50.tgz", + "integrity": "sha512-J6l92tKHX6w8Jy5nO1Vuc01NoIiRGi/d6qBKVxh+IQ8Cr3b6HbVNfKiF8ZpFKufTwpwxMmce2W3iQZ861ZRyTg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { "version": "4.1.7", "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", @@ -7685,16 +8157,16 @@ } }, "node_modules/sharp": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", - "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", + "version": "0.34.4", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.4.tgz", + "integrity": "sha512-FUH39xp3SBPnxWvd5iib1X8XY7J0K0X7d93sie9CJg2PO8/7gmg89Nve6OjItK53/MlAushNNxteBYfM6DEuoA==", + "dev": true, "hasInstallScript": true, "license": "Apache-2.0", - "optional": true, "dependencies": { "@img/colour": "^1.0.0", - "detect-libc": "^2.1.2", - "semver": "^7.7.3" + "detect-libc": "^2.1.0", + "semver": "^7.7.2" }, "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" @@ -7703,38 +8175,36 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-darwin-arm64": "0.34.5", - "@img/sharp-darwin-x64": "0.34.5", - "@img/sharp-libvips-darwin-arm64": "1.2.4", - "@img/sharp-libvips-darwin-x64": "1.2.4", - "@img/sharp-libvips-linux-arm": "1.2.4", - "@img/sharp-libvips-linux-arm64": "1.2.4", - "@img/sharp-libvips-linux-ppc64": "1.2.4", - "@img/sharp-libvips-linux-riscv64": "1.2.4", - "@img/sharp-libvips-linux-s390x": "1.2.4", - "@img/sharp-libvips-linux-x64": "1.2.4", - "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", - "@img/sharp-libvips-linuxmusl-x64": "1.2.4", - "@img/sharp-linux-arm": "0.34.5", - "@img/sharp-linux-arm64": "0.34.5", - "@img/sharp-linux-ppc64": "0.34.5", - "@img/sharp-linux-riscv64": "0.34.5", - "@img/sharp-linux-s390x": "0.34.5", - "@img/sharp-linux-x64": "0.34.5", - "@img/sharp-linuxmusl-arm64": "0.34.5", - "@img/sharp-linuxmusl-x64": "0.34.5", - "@img/sharp-wasm32": "0.34.5", - "@img/sharp-win32-arm64": "0.34.5", - "@img/sharp-win32-ia32": "0.34.5", - "@img/sharp-win32-x64": "0.34.5" + "@img/sharp-darwin-arm64": "0.34.4", + "@img/sharp-darwin-x64": "0.34.4", + "@img/sharp-libvips-darwin-arm64": "1.2.3", + "@img/sharp-libvips-darwin-x64": "1.2.3", + "@img/sharp-libvips-linux-arm": "1.2.3", + "@img/sharp-libvips-linux-arm64": "1.2.3", + "@img/sharp-libvips-linux-ppc64": "1.2.3", + "@img/sharp-libvips-linux-s390x": "1.2.3", + "@img/sharp-libvips-linux-x64": "1.2.3", + "@img/sharp-libvips-linuxmusl-arm64": "1.2.3", + "@img/sharp-libvips-linuxmusl-x64": "1.2.3", + "@img/sharp-linux-arm": "0.34.4", + "@img/sharp-linux-arm64": "0.34.4", + "@img/sharp-linux-ppc64": "0.34.4", + "@img/sharp-linux-s390x": "0.34.4", + "@img/sharp-linux-x64": "0.34.4", + "@img/sharp-linuxmusl-arm64": "0.34.4", + "@img/sharp-linuxmusl-x64": "0.34.4", + "@img/sharp-wasm32": "0.34.4", + "@img/sharp-win32-arm64": "0.34.4", + "@img/sharp-win32-ia32": "0.34.4", + "@img/sharp-win32-x64": "0.34.4" } }, "node_modules/sharp/node_modules/semver": { "version": "7.8.5", "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, "license": "ISC", - "optional": true, "bin": { "semver": "bin/semver.js" }, diff --git a/package.json b/package.json index 5a27f90..2c8c826 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,6 @@ "gray-matter": "^4.0.3", "lucide-react": "^1.23.0", "minisearch": "^7.2.0", - "motion": "^12.42.2", "next": "16.2.10", "next-themes": "^0.4.6", "react": "19.2.4", @@ -40,6 +39,7 @@ "@types/react-dom": "^19", "eslint": "^9", "eslint-config-next": "16.2.10", + "sharp": "^0.34.4", "tailwindcss": "^4", "typescript": "^5" } diff --git a/public/icon-192.png b/public/icon-192.png new file mode 100644 index 0000000..5951d9e Binary files /dev/null and b/public/icon-192.png differ diff --git a/public/icon-512-maskable.png b/public/icon-512-maskable.png new file mode 100644 index 0000000..83fafc4 Binary files /dev/null and b/public/icon-512-maskable.png differ diff --git a/public/icon-512.png b/public/icon-512.png new file mode 100644 index 0000000..154adac Binary files /dev/null and b/public/icon-512.png differ diff --git a/scripts/generate-icons.mjs b/scripts/generate-icons.mjs new file mode 100644 index 0000000..44c4f20 --- /dev/null +++ b/scripts/generate-icons.mjs @@ -0,0 +1,106 @@ +#!/usr/bin/env node +/** + * Regenerates every raster icon from the mark's geometry so the set never + * drifts from src/app/icon.svg. + * + * node scripts/generate-icons.mjs + * + * Writes: + * src/app/favicon.ico 32 and 16, PNG-compressed entries + * src/app/apple-icon.png 180, solid paper ground, padded + * public/icon-192.png PWA + * public/icon-512.png PWA + * public/icon-512-maskable.png PWA, mark inside the 80% safe zone + * + * Colours are the light-theme tokens from globals.css. Rasters cannot + * follow the viewer's theme, so they use the paper ground, which is also + * the Open Graph card's ground. + */ +import fs from "node:fs"; +import path from "node:path"; +import sharp from "sharp"; + +const ROOT = path.resolve(import.meta.dirname, ".."); +const PAPER = "#fafaf7"; +const INK = "#1a1a18"; +const ACCENT = "#4338ca"; + +/** The mark on a 32-unit grid, with a separate 16-unit drawing for 16px. */ +function markSvg({ size, ground, pad = 0, radius = 0 }) { + const grid = size <= 16 ? 16 : 32; + const bars = + grid === 16 + ? ` + + ` + : ` + + `; + const inner = size - pad * 2; + const scale = inner / grid; + return ` + ${ground ? `` : ""} + ${bars} + `; +} + +async function png(svg, size) { + return sharp(Buffer.from(svg), { density: 384 }).resize(size, size).png().toBuffer(); +} + +/** Packs PNG images into a .ico container (ICONDIR + ICONDIRENTRY per image). */ +function ico(images) { + const header = Buffer.alloc(6); + header.writeUInt16LE(0, 0); + header.writeUInt16LE(1, 2); + header.writeUInt16LE(images.length, 4); + const entries = []; + const blobs = []; + let offset = 6 + 16 * images.length; + for (const { size, data } of images) { + const entry = Buffer.alloc(16); + entry.writeUInt8(size >= 256 ? 0 : size, 0); + entry.writeUInt8(size >= 256 ? 0 : size, 1); + entry.writeUInt8(0, 2); + entry.writeUInt8(0, 3); + entry.writeUInt16LE(1, 4); + entry.writeUInt16LE(32, 6); + entry.writeUInt32LE(data.length, 8); + entry.writeUInt32LE(offset, 12); + entries.push(entry); + blobs.push(data); + offset += data.length; + } + return Buffer.concat([header, ...entries, ...blobs]); +} + +function write(rel, data) { + const file = path.join(ROOT, rel); + fs.mkdirSync(path.dirname(file), { recursive: true }); + fs.writeFileSync(file, data); + console.log(` ${rel} (${data.length} bytes)`); +} + +console.log("Generating icons"); + +// favicon.ico cannot follow the colour scheme, so it sits on a paper tile +// that reads on both light and dark tab strips. Browsers that support SVG +// favicons use icon.svg instead, which does follow the scheme. +write( + "src/app/favicon.ico", + ico([ + { size: 32, data: await png(markSvg({ size: 32, ground: PAPER, radius: 6 }), 32) }, + { size: 16, data: await png(markSvg({ size: 16, ground: PAPER, radius: 3 }), 16) }, + ]), +); + +// Apple touch icon: iOS adds its own corner radius; solid ground, 12% padding. +write("src/app/apple-icon.png", await png(markSvg({ size: 180, ground: PAPER, pad: 22 }), 180)); + +// PWA icons on a solid ground. The maskable one keeps the mark inside the +// central 80% safe zone. +write("public/icon-192.png", await png(markSvg({ size: 192, ground: PAPER, pad: 24, radius: 36 }), 192)); +write("public/icon-512.png", await png(markSvg({ size: 512, ground: PAPER, pad: 64, radius: 96 }), 512)); +write("public/icon-512-maskable.png", await png(markSvg({ size: 512, ground: PAPER, pad: 128 }), 512)); + +console.log("Done"); diff --git a/scripts/validate-content.mjs b/scripts/validate-content.mjs index f02eb3d..9183c83 100644 --- a/scripts/validate-content.mjs +++ b/scripts/validate-content.mjs @@ -73,6 +73,17 @@ function checkOptional(file, data, fields) { } } +// Search-result lengths: the title template appends " | Agentling" (12 chars). +const TITLE_SUFFIX = 12; +function checkMetaLengths(file, data) { + if (typeof data.metaTitle === "string" && data.metaTitle.length + TITLE_SUFFIX > 60) { + fail(file, `metaTitle is ${data.metaTitle.length + TITLE_SUFFIX} chars with the suffix; keep it at 60 or under`); + } + if (typeof data.metaDescription === "string" && data.metaDescription.length > 155) { + fail(file, `metaDescription is ${data.metaDescription.length} chars; keep it at 155 or under`); + } +} + function checkSlug(file, slug) { if (typeof slug === "string" && !SLUG_PATTERN.test(slug)) { fail(file, `slug "${slug}" is not kebab-case`); @@ -149,7 +160,12 @@ for (const dirent of fs.readdirSync(path.join(CONTENT, "docs"), { slug: "string", order: "number", }); - checkOptional(file, page.data, { description: "string" }); + checkOptional(file, page.data, { + description: "string", + metaTitle: "string", + metaDescription: "string", + }); + checkMetaLengths(file, page.data); checkSlug(file, page.data.slug); checkUnique(file, docSlugs, page.data.slug, "doc page slug"); validRoutes.add(`/docs/${dirent.name}/${page.data.slug}`); @@ -174,7 +190,12 @@ for (const file of markdownFiles(path.join(CONTENT, "pages"))) { if (!page) continue; allFiles.push({ file, raw: page.content }); requireFields(file, page.data, { title: "string", slug: "string" }); - checkOptional(file, page.data, { description: "string" }); + checkOptional(file, page.data, { + description: "string", + metaTitle: "string", + metaDescription: "string", + }); + checkMetaLengths(file, page.data); checkSlug(file, page.data.slug); checkUnique(file, pageSlugs, page.data.slug, "site page slug"); validRoutes.add(`/${page.data.slug}`); diff --git a/src/app/[slug]/page.tsx b/src/app/[slug]/page.tsx index 1743730..1935a54 100644 --- a/src/app/[slug]/page.tsx +++ b/src/app/[slug]/page.tsx @@ -1,9 +1,10 @@ import type { Metadata } from "next"; import { notFound } from "next/navigation"; +import { JsonLd } from "@/components/json-ld"; import { Markdown } from "@/components/markdown"; import { getSitePage, getSitePageSlugs } from "@/lib/content"; -import { pageMetadata } from "@/lib/seo"; +import { pageMetadata, webPageGraph } from "@/lib/seo"; interface SitePageParams { params: Promise<{ slug: string }>; @@ -21,9 +22,11 @@ export async function generateMetadata({ const page = await getSitePage(slug); if (!page) return {}; return pageMetadata({ - title: page.title, - description: page.description, + kind: "page", + title: page.metaTitle ?? page.title, + description: page.metaDescription ?? page.description, path: `/${page.slug}`, + dates: { published: page.published, modified: page.modified }, }); } @@ -33,16 +36,26 @@ export default async function StandaloneSitePage({ params }: SitePageParams) { if (!page) notFound(); return ( -
-

+
+ +

{page.title}

{page.description && ( -

- {page.description} -

+

{page.description}

)} -
+
diff --git a/src/app/apple-icon.png b/src/app/apple-icon.png new file mode 100644 index 0000000..b89eac8 Binary files /dev/null and b/src/app/apple-icon.png differ diff --git a/src/app/apple-icon.tsx b/src/app/apple-icon.tsx deleted file mode 100644 index f773171..0000000 --- a/src/app/apple-icon.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import { ImageResponse } from "next/og"; - -export const size = { width: 180, height: 180 }; -export const contentType = "image/png"; - -/** iOS home-screen icon; iOS ignores SVG favicons, so this renders a PNG. */ -export default function AppleIcon() { - return new ImageResponse( - ( -
- - - - -
- ), - size, - ); -} diff --git a/src/app/changelog.xml/route.ts b/src/app/changelog.xml/route.ts index fbbec9b..6e4f661 100644 --- a/src/app/changelog.xml/route.ts +++ b/src/app/changelog.xml/route.ts @@ -25,6 +25,7 @@ export async function GET() { renderRss({ title: "agentling changelog", path: "/changelog", + feedPath: "/changelog.xml", description: "Every agentling release, documented.", items, }), diff --git a/src/app/changelog/page.tsx b/src/app/changelog/page.tsx index f91dfaf..5c4d310 100644 --- a/src/app/changelog/page.tsx +++ b/src/app/changelog/page.tsx @@ -1,79 +1,96 @@ import type { Metadata } from "next"; +import { JsonLd } from "@/components/json-ld"; import { Markdown } from "@/components/markdown"; -import { getChangelog } from "@/lib/content"; -import { pageMetadata } from "@/lib/seo"; +import { getChangelog, getContentModified } from "@/lib/content"; +import { pageMetadata, webPageGraph } from "@/lib/seo"; import { GITHUB_URL } from "@/lib/site"; import { formatDate } from "@/lib/utils"; +const TITLE = "Changelog: every release, with an RSS feed"; +const DESCRIPTION = + "What shipped in each Agentling release, following Keep a Changelog and semantic versioning, with an RSS feed at /changelog.xml."; + export const metadata: Metadata = pageMetadata({ - title: "Changelog", - description: - "Every Agentling release, documented. Follows Keep a Changelog and semantic versioning.", + kind: "changelog", + title: TITLE, + description: DESCRIPTION, path: "/changelog", }); export default async function ChangelogPage() { - const entries = await getChangelog(); + const [entries, modified] = await Promise.all([ + getChangelog(), + getContentModified("changelog"), + ]); return ( -
-

+
+ +

Changelog

-

+

What shipped, release by release. Releases are tagged on{" "} GitHub .

-
+
{entries.map((entry) => (
-

+

v{entry.version}

-

- {entry.tags.map((tag) => ( - - {tag} - - ))} -

+ {entry.tags.length > 0 && ( +

+ {entry.tags.map((tag) => ( + + {tag} + + ))} +

+ )}
-
+
{entry.title && ( -

- {entry.title} -

+

{entry.title}

)} -
- +
+
diff --git a/src/app/creator/opengraph-image.tsx b/src/app/creator/opengraph-image.tsx new file mode 100644 index 0000000..c75c192 --- /dev/null +++ b/src/app/creator/opengraph-image.tsx @@ -0,0 +1,17 @@ +import { renderOgImage, OG_CONTENT_TYPE, OG_SIZE } from "@/lib/og-image"; +import { AUTHOR } from "@/lib/site"; + +export const alt = `${AUTHOR.name}, AI Engineer and Creator of Agentling`; +export const size = OG_SIZE; +export const contentType = OG_CONTENT_TYPE; + +/** Typographic card: the name and the role line, no imagery. */ +export default function Image() { + return renderOgImage({ + eyebrow: "AI Engineer · Creator of Agentling", + title: AUTHOR.name, + subtitle: + "Builds agentic systems and AI-powered products. Creator of Agentling, an open-source Python framework for building AI agents.", + footerNote: "folarin.dev", + }); +} diff --git a/src/app/creator/page.tsx b/src/app/creator/page.tsx new file mode 100644 index 0000000..b4399fc --- /dev/null +++ b/src/app/creator/page.tsx @@ -0,0 +1,400 @@ +import type { Metadata } from "next"; +import Link from "next/link"; + +import { JsonLd } from "@/components/json-ld"; +import { getFileDates } from "@/lib/content/dates"; +import { absoluteUrl, pageMetadata } from "@/lib/seo"; +import { AUTHOR, GITHUB_URL, PERSON_ID, PYPI_URL, SITE_URL } from "@/lib/site"; + +/** + * Creator profile. Indexable on purpose and unlinked on purpose: + * + * - It returns 200, has a self-referencing canonical, and is listed in the + * sitemap (src/app/sitemap.ts), so search engines can find and index it. + * - It is deliberately absent from the header, sidebar, footer, and the + * search index. Both search indexes are built from /content, and this is + * a code route, so nothing links here unless someone adds a link. + * - It is a static page served identically to every visitor. There is no + * crawler-specific content and no conditional redirect. + * + * If you add an internal link to this page, that is a product decision, + * not a bug fix. Every factual claim below is drawn from folarin.dev, the + * GitHub profile, or the Agentling repository and docs. + */ + +const PATH = "/creator"; +const TITLE = "Folarin Akinloye - AI Engineer & Agentling Creator"; +const DESCRIPTION = + "Folarin Akinloye is an AI engineer building agentic systems and AI-powered products, and the creator of the open-source Agentling framework."; +const BLOG_URL = `${AUTHOR.url}/blog`; + +export const metadata: Metadata = pageMetadata({ + kind: "page", + title: TITLE, + absoluteTitle: true, + description: DESCRIPTION, + path: PATH, +}); + +const WRITING = [ + { + title: "The Reference Architecture for a Production AI App", + href: `${AUTHOR.url}/blog/the-reference-architecture-for-a-production-ai-app`, + note: "Why the model call is a small part of a production AI app, and what the surrounding components have to do.", + }, + { + title: "Skills vs MCP vs Tools vs Subagents: Stop Conflating Them", + href: `${AUTHOR.url}/blog/skills-vs-mcp-vs-tools-vs-subagents`, + note: "Four different ways to extend an agent, what each one is for, and how they fit together.", + }, + { + title: "Agent Memory: Short-Term vs Long-Term, and How to Wire It Up", + href: `${AUTHOR.url}/blog/agent-memory-short-term-vs-long-term`, + note: "The two kinds of memory an agent needs and how to implement each without overbuilding.", + }, + { + title: "Designing UX for a Confidently-Wrong Intern", + href: `${AUTHOR.url}/blog/designing-ux-for-a-confidently-wrong-intern`, + note: "Interface design for AI features that can be wrong or slow, unlike conventional software.", + }, +]; + +const PROJECTS = [ + { + title: "OpenFocus", + href: "https://github.com/folathecoder/study-agent", + live: "https://study-agent-production.up.railway.app", + note: "A study companion built as a multi-agent app, with five agents that each teach in a different style. The problem is coordinating several specialised agents behind one conversation without the seams showing.", + }, + { + title: "finops-agent", + href: "https://github.com/folathecoder/finops-agent", + note: "A multi-agent personal finance app for budgeting. The problem is splitting a money question across agents that each own one job and getting one coherent answer back.", + }, + { + title: "adzuna-job-search-mcp", + href: "https://github.com/folathecoder/adzuna-job-search-mcp", + note: "An MCP server that lets an agent search jobs, analyse salaries, and research employers across 12 countries through the Adzuna API. The problem is exposing an external dataset to agents as typed, bounded tools.", + }, + { + title: "uk-visa-sponsor-mcp", + href: "https://github.com/folathecoder/uk-visa-sponsor-mcp", + note: "An MCP server that lets an agent check whether an employer is a licensed UK visa sponsor. The problem is turning a government register into a single reliable yes-or-no tool call.", + }, + { + title: "UK Visa Sponsor Search", + href: "https://github.com/folathecoder/uk-visa-sponsor-search", + live: "https://uksponsorsearch.co.uk", + note: "A daily-updated, searchable list of UK visa sponsors sourced from gov.uk. The problem is making a large, awkward public dataset fast to search and keep current without manual work.", + }, +]; + +const PROFILES = [ + { label: "Website", href: AUTHOR.url, text: "folarin.dev" }, + { label: "Writing", href: BLOG_URL, text: "folarin.dev/blog" }, + { label: "GitHub", href: AUTHOR.github, text: "github.com/folathecoder" }, + { label: "LinkedIn", href: AUTHOR.linkedin, text: "linkedin.com/in/akinloye-folarin" }, +]; + +function ExternalLink({ href, children }: { href: string; children: React.ReactNode }) { + return ( + + {children} + + ); +} + +export default function CreatorPage() { + const dates = getFileDates("src/app/creator/page.tsx"); + + return ( +
+ + +

Creator

+

+ Folarin Akinloye, AI Engineer and Creator of Agentling +

+

+ I build agentic systems and AI-powered products, and I created + Agentling, an open-source Python framework for building AI agents. +

+ +
+

+ My work sits where agent architectures, AI infrastructure, and + product engineering meet. I design and build systems that reason + through a task, call tools, retrieve what they need, coordinate the + steps of a workflow, and behave predictably once real people depend + on them. Agentling is the part of that work I released as open + source: the loop, memory, and event model I kept wanting and could + not find small enough to read in one sitting. +

+ +

What I work on

+ +

Agentic AI systems

+

+ I design agents and the workflows around them: tool-calling loops, + multi-agent backends that route work by complexity and cost, and + pipelines that carry a task through several steps without a person + holding each one together. On the AI platform at Cuvama I built a + tiered multi-agent backend in LangGraph that routes tasks across + OpenAI, Anthropic, and Bedrock models by complexity, cost, and token + budget. The interesting decisions are rarely about the model. They + are about which tools an agent gets, how its subtasks are split and + coordinated, and what happens when a step fails halfway through. +

+ +

AI product engineering

+

+ A prototype only has to work once, in front of its author. A product + has to work for people who did not build it, on inputs nobody + predicted, inside an interface that stays honest about what the + model is doing. I own that whole surface end to end: model and tool + integration, backend services, the workflow design in between, and + the interface on top. That has included streaming chat, live + visualisation of tool calls and agent activity, progressive rendering + over WebSockets, and plan-approval flows where an agent proposes a + multi-step edit and a person reviews it, with versioning, before + anything changes. +

+ +

AI infrastructure and reliability

+

+ Retrieval, context management, evaluation, observability, and + recovery are not finishing touches. They are what make a system + usable at all. I have built version-aware retrieval with Agentic RAG + on MongoDB Atlas Vector Search, replacing bulk context injection with + typed tool calls, and I have wired LangSmith evaluation pipelines + into GitHub Actions so that a regression in agent behaviour blocks a + merge rather than reaching users. Reliability, predictable behaviour, + and quality you can measure are the difference between a system that + demos well and one a team will keep. +

+ +

Working with startups

+

+ I work on practical AI problems in startup environments, where a + small team has to turn a new capability into something useful without + building more than the problem needs. That means working across + product and engineering at once, making the architecture calls, and + delivering the whole thing, from the data layer to the pixels. The + judgement I bring is about what to build and what to leave out, not + only how. +

+ +

Why I built Agentling

+

+ Most agent frameworks hide the loop. When something goes wrong at + three in the morning, that is exactly the part you need to read. I + wanted a framework where the whole execution model fits in your head: + one async generator drives every run, every step is a typed object, + and every event the loop emits is one you can see. Agentling is that + framework, built Python-first for asyncio, and kept small enough that + the source is the documentation of last resort. +

+
    +
  • + Async, Python-first execution. The loop, the + tools, and the model calls are all async. Tool calls within one + step run concurrently by default, so an agent that needs three + lookups does not wait three times. +
  • +
  • + A ReAct-style loop with one code path. Blocking + and streaming modes share the same generator, so what you observe + in the stream is exactly what happened in a blocking run. +
  • +
  • + Typed memory. A run is a list of typed steps, not + a pile of message dictionaries. Steps render themselves into model + messages and serialize to JSON, which is what makes persistence, + replay, and resuming possible without extra machinery. +
  • +
  • + Streaming events. A small set of typed events + reports everything the loop does, which is what an interface + needs to show progress honestly and what an operator needs to + debug a run. +
  • +
  • + Recoverable failures and resumable runs. A tool + that raises becomes an observation the model can act on. Per-call + timeouts, output truncation, and error redaction are built in. A + run can be interrupted and resumed from the step where it paused. +
  • +
  • + Provider abstraction. The built-in adapter speaks + to any OpenAI-compatible endpoint, and the Model protocol is small + enough to implement for a provider of your own. +
  • +
  • + Runs without a key. The repository ships examples + that run offline against a scripted model, so you can see failure + recovery and memory persistence work before you configure a + provider. +
  • +
+

+ Source on GitHub,{" "} + documentation,{" "} + installation,{" "} + examples, and{" "} + the package on PyPI. MIT + licensed, Python 3.11 or newer. +

+ +

Engineering principles

+

+ None of these are original. They are the rules I keep coming back to + when a system is on the line. +

+
    +
  • + Agents should be useful, not just impressive.{" "} + Design around a real task and an outcome you can measure, not + around the demo. +
  • +
  • + Reliability is part of the architecture. Failures, + retries, observability, and recovery are designed in at the start, + not patched on after the first incident. +
  • +
  • + Evaluation matters. Agent behaviour is tested + systematically and regressions are caught in CI, not discovered in + a demo that happened to go well. +
  • +
  • + Abstractions should solve real problems. A + framework earns its abstractions by removing work, and it must + never hide the execution details you need when something breaks. +
  • +
  • + AI belongs in products. The model, the tools, the + interface, the infrastructure, and the user's workflow are one + system, and they are designed together. +
  • +
+ +

Selected AI engineering work

+
    +
  • + + Agentling + + . An async Python framework for tool-using agents with typed memory, + streamed events, concurrent tool calls, recoverable failures, and + resumable runs. The problem it addresses is execution you can read, + replay, and trust. +
  • +
  • + + From Demo to Product + + . A written series on what it takes to turn an AI prototype into a + product: architecture, unit economics, risk, quality ownership, and + interface design. Selected pieces: +
      + {WRITING.map((item) => ( +
    • + {item.title}.{" "} + {item.note} +
    • + ))} +
    +
  • + {PROJECTS.map((project) => ( +
  • + + {project.title} + + . {project.note} + {project.live && ( + <> + {" "} + Live app. + + )} +
  • + ))} +
+ +

Professional background

+

+ I have been building software for nine years, the last two mostly on + agent systems. Since 2024 I have been a Senior Product Engineer on + the AI platform at Cuvama, where I own the frontend platform and the + product's AI surface end to end. Before that I spent five years + at lead level building real-time financial interfaces: crypto + portfolio analytics and live equities trading, leading frontend + teams of seven and five. The tools I reach for most are Python and + TypeScript, LangGraph and LangSmith, MCP servers for giving agents + access to external systems, retrieval over vector search, and + evaluation pipelines that run in CI. +

+ +

Contact and profiles

+
    + {PROFILES.map((profile) => ( +
  • + {profile.label}:{" "} + {profile.text} +
  • + ))} +
+

+ For security reports about Agentling, see the{" "} + contributing page. +

+ +

Short biography

+

+ Folarin Akinloye is an AI engineer who builds agentic systems and + AI-powered products. He is the creator of Agentling, an open-source + Python framework for building AI agents. His work focuses on agent + architectures, AI product engineering, and the infrastructure needed + to build reliable AI systems. +

+
+
+ ); +} diff --git a/src/app/docs/[section]/[slug]/page.tsx b/src/app/docs/[section]/[slug]/page.tsx index 84270c4..3838c1a 100644 --- a/src/app/docs/[section]/[slug]/page.tsx +++ b/src/app/docs/[section]/[slug]/page.tsx @@ -1,27 +1,26 @@ -import { ArrowLeft, ArrowRight } from "lucide-react"; import type { Metadata } from "next"; import Link from "next/link"; import { notFound } from "next/navigation"; import { CopyMarkdownButton } from "@/components/copy-markdown-button"; import { FeedbackWidget } from "@/components/docs/feedback-widget"; -import { Toc } from "@/components/docs/toc"; +import { Toc, TocInline } from "@/components/docs/toc"; import { JsonLd } from "@/components/json-ld"; import { Markdown } from "@/components/markdown"; import { getDocPage, getDocSections } from "@/lib/content"; -import { absoluteUrl, pageMetadata } from "@/lib/seo"; -import { AUTHOR_JSON_LD } from "@/lib/site"; +import { docArticleGraph, pageMetadata } from "@/lib/seo"; import { extractToc } from "@/lib/toc"; interface DocPageParams { params: Promise<{ section: string; slug: string }>; } -const TEMPLATE_BADGES: Record = { - "getting-started": { label: "Tutorial", color: "bg-emerald-500/10 text-emerald-600 dark:text-emerald-400 border-emerald-500/20" }, - "core-concepts": { label: "Concept", color: "bg-indigo-500/10 text-indigo-600 dark:text-indigo-400 border-indigo-500/20" }, - guides: { label: "How-To Guide", color: "bg-blue-500/10 text-blue-600 dark:text-blue-400 border-blue-500/20" }, - reference: { label: "API Reference", color: "bg-amber-500/10 text-amber-600 dark:text-amber-400 border-amber-500/20" }, +/** Diátaxis mode per section, shown as one neutral eyebrow. */ +const PAGE_KIND: Record = { + "getting-started": "Tutorial", + "core-concepts": "Concept", + guides: "Guide", + reference: "Reference", }; export async function generateStaticParams() { @@ -38,10 +37,11 @@ export async function generateMetadata({ const page = await getDocPage(slug); if (!page) return {}; return pageMetadata({ - title: page.title, - description: page.description, + kind: "doc", + title: page.metaTitle ?? page.title, + description: page.metaDescription ?? page.description, path: `/docs/${page.sectionSlug}/${page.slug}`, - ogType: "article", + dates: { published: page.published, modified: page.modified }, }); } @@ -66,124 +66,92 @@ export default async function DocPage({ params }: DocPageParams) { const next = index < flat.length - 1 ? flat[index + 1] : null; const toc = extractToc(page.body); - const pageUrl = absoluteUrl(`/docs/${page.sectionSlug}/${page.slug}`); - const templateBadge = TEMPLATE_BADGES[page.sectionSlug] ?? { label: "Guide", color: "bg-muted text-muted-foreground border-border" }; + const pagePath = `/docs/${page.sectionSlug}/${page.slug}`; + const section = sections.find((entry) => entry.slug === page.sectionSlug); + const sectionFirst = section?.pages[0]; return ( -
+
- {/* Centre Column: max-w-[720px] for prose, code breaks wider up to 860px */} -
- {/* Breadcrumb + Diataxis badge */} -
-
); diff --git a/src/app/docs/[section]/page.tsx b/src/app/docs/[section]/page.tsx new file mode 100644 index 0000000..c8788ee --- /dev/null +++ b/src/app/docs/[section]/page.tsx @@ -0,0 +1,16 @@ +import { notFound, permanentRedirect } from "next/navigation"; + +import { getDocSections } from "@/lib/content"; + +/** /docs/
is a natural URL to type; send it to the section's first page. */ +export default async function SectionIndexPage({ + params, +}: { + params: Promise<{ section: string }>; +}) { + const { section: sectionSlug } = await params; + const section = (await getDocSections()).find((s) => s.slug === sectionSlug); + const first = section?.pages[0]; + if (!section || !first) notFound(); + permanentRedirect(`/docs/${section.slug}/${first.slug}`); +} diff --git a/src/app/docs/layout.tsx b/src/app/docs/layout.tsx index 524e32c..1ea7b58 100644 --- a/src/app/docs/layout.tsx +++ b/src/app/docs/layout.tsx @@ -9,9 +9,11 @@ export default async function DocsLayout({ const sections = await getDocSections(); return ( -
+ // Column flow below lg so the sticky page selector stacks above the + // content instead of competing with it for width. +
-
+
{children}
diff --git a/src/app/docs/page.tsx b/src/app/docs/page.tsx index b384c24..a3b5c60 100644 --- a/src/app/docs/page.tsx +++ b/src/app/docs/page.tsx @@ -1,5 +1,5 @@ -import { redirect } from "next/navigation"; +import { permanentRedirect } from "next/navigation"; export default function DocsIndexPage() { - redirect("/docs/getting-started/introduction"); + permanentRedirect("/docs/getting-started/introduction"); } diff --git a/src/app/favicon.ico b/src/app/favicon.ico new file mode 100644 index 0000000..6c9e70a Binary files /dev/null and b/src/app/favicon.ico differ diff --git a/src/app/globals.css b/src/app/globals.css index 03ae851..7b41142 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -4,82 +4,140 @@ @custom-variant dark (&:where(.dark, .dark *)); /* - * The Typeset Book & IDE Precision System - * Light: Warm book paper (#FAFAF7), near-black ink (#1A1A1A), deep indigo accent (#4338CA) - * Dark: Soft charcoal (#0F1012), lifted lavender accent (#818CF8), muted text (#9CA3AF) - * Zero gradients, zero decorative glow blobs, hairline 1px borders. + * Design tokens. Light and dark are separate tuned palettes, not an + * inversion. Greys carry a warm bias to sit on the paper background. + * Every text token clears WCAG AA (4.5:1) on every surface it is used on; + * the measured ratios live in docs/design-direction.md. */ :root { - --background: #fafaf7; - --foreground: #1a1a1a; - --card: #ffffff; - --card-foreground: #1a1a1a; - --muted: #f2f2ed; - --muted-foreground: #6b7280; - --faint: #9ca3af; - --border: #e8e8e3; - --border-strong: #d1d1ca; + --bg-base: #fafaf7; + --bg-raised: #ffffff; + --bg-sunken: #f1f1ec; + --bg-code: #141518; + + --border-subtle: #e7e7e1; + --border-default: #c4c4bc; + + --fg: #1a1a18; + --fg-secondary: #5f6059; + --fg-tertiary: #6d6e67; + --fg-inverse: #fafaf7; --accent: #4338ca; - --accent-strong: #3730a3; - --accent-foreground: #ffffff; - --accent-soft: rgba(67, 56, 202, 0.08); + --accent-hover: #3730a3; + --accent-fg: #ffffff; + --accent-subtle: rgba(67, 56, 202, 0.08); + + --success: #1f7a4d; + --warning: #9a5b00; + --danger: #b42318; - --code-bg: #141518; - --code-fg: #f3f4f6; - --code-border: #23252a; - --terminal-bg: #141518; + --code-fg: #e6e6e3; + --code-muted: #8b8e98; + --code-title: #a6a8b0; + --code-border: #26272c; - --ring: #4338ca; + --shadow-popover: 0 4px 12px rgba(26, 26, 24, 0.1), 0 0 0 1px var(--border-subtle); + --shadow-dialog: 0 16px 40px rgba(26, 26, 24, 0.18), 0 0 0 1px var(--border-subtle); } .dark { - --background: #0f1012; - --foreground: #ededed; - --card: #151619; - --card-foreground: #ededed; - --muted: #1c1d22; - --muted-foreground: #9ca3af; - --faint: #6b7280; - --border: #23252a; - --border-strong: #373942; + --bg-base: #0f1012; + --bg-raised: #151619; + --bg-sunken: #1b1c21; - --accent: #818cf8; - --accent-strong: #a5b4fc; - --accent-foreground: #0f1012; - --accent-soft: rgba(129, 140, 248, 0.12); + --border-subtle: #26272c; + --border-default: #3b3c44; - --code-bg: #141518; - --code-fg: #ededed; - --code-border: #23252a; - --terminal-bg: #141518; + --fg: #ededeb; + --fg-secondary: #a4a6ae; + --fg-tertiary: #858891; + --fg-inverse: #0f1012; - --ring: #818cf8; + --accent: #8b93f8; + --accent-hover: #a5acfb; + --accent-fg: #0f1012; + --accent-subtle: rgba(139, 147, 248, 0.12); + + --success: #5fcf8a; + --warning: #e5a83e; + --danger: #f2786e; + + --shadow-popover: 0 4px 12px rgba(0, 0, 0, 0.35), 0 0 0 1px var(--border-default); + --shadow-dialog: 0 16px 40px rgba(0, 0, 0, 0.55), 0 0 0 1px var(--border-default); } @theme inline { - --color-background: var(--background); - --color-foreground: var(--foreground); - --color-card: var(--card); - --color-card-foreground: var(--card-foreground); - --color-muted: var(--muted); - --color-muted-foreground: var(--muted-foreground); - --color-faint: var(--faint); - --color-border: var(--border); - --color-border-strong: var(--border-strong); + /* Drop Tailwind's stock palette so only semantic colours are available. */ + --color-*: initial; + + --color-base: var(--bg-base); + --color-raised: var(--bg-raised); + --color-sunken: var(--bg-sunken); + --color-code: var(--bg-code); + + --color-border-subtle: var(--border-subtle); + --color-border-default: var(--border-default); + + --color-fg: var(--fg); + --color-fg-secondary: var(--fg-secondary); + --color-fg-tertiary: var(--fg-tertiary); + --color-fg-inverse: var(--fg-inverse); + --color-accent: var(--accent); - --color-accent-strong: var(--accent-strong); - --color-accent-foreground: var(--accent-foreground); - --color-accent-soft: var(--accent-soft); + --color-accent-hover: var(--accent-hover); + --color-accent-fg: var(--accent-fg); + --color-accent-subtle: var(--accent-subtle); + + --color-success: var(--success); + --color-warning: var(--warning); + --color-danger: var(--danger); - --color-code-bg: var(--code-bg); --color-code-fg: var(--code-fg); + --color-code-muted: var(--code-muted); + --color-code-title: var(--code-title); --color-code-border: var(--code-border); - --color-terminal: var(--terminal-bg); - --font-sans: var(--font-sans), ui-sans-serif, system-ui, -apple-system, sans-serif; + --color-transparent: transparent; + --color-current: currentColor; + + /* Two radii: controls and panels. */ + --radius-*: initial; + --radius-control: 4px; + --radius-panel: 8px; + + --shadow-*: initial; + --shadow-popover: var(--shadow-popover); + --shadow-dialog: var(--shadow-dialog); + + --ease-out-strong: cubic-bezier(0.23, 1, 0.32, 1); + + --font-sans: var(--font-sans), ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif; --font-mono: var(--font-mono), ui-monospace, "SF Mono", Menlo, monospace; + + /* Type scale. */ + --text-*: initial; + --text-2xs: 11px; + --text-2xs--line-height: 16px; + --text-xs: 12px; + --text-xs--line-height: 16px; + --text-ui: 13px; + --text-ui--line-height: 20px; + --text-sm: 14px; + --text-sm--line-height: 22px; + --text-base: 16px; + --text-base--line-height: 26px; + --text-lede: 17px; + --text-lede--line-height: 26px; + --text-h3: 17px; + --text-h3--line-height: 24px; + --text-h2: 22px; + --text-h2--line-height: 28px; + --text-h1: 32px; + --text-h1--line-height: 36px; + --text-code: 13.5px; + --text-code--line-height: 22px; } html { @@ -88,9 +146,11 @@ html { } body { - background-color: var(--background); - color: var(--foreground); + background-color: var(--bg-base); + color: var(--fg); font-family: var(--font-sans); + font-size: 13px; + line-height: 20px; text-rendering: optimizeLegibility; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; @@ -98,7 +158,7 @@ body { ::selection { background: var(--accent); - color: var(--accent-foreground); + color: var(--accent-fg); } :focus-visible:not([data-focus-ring="none"]) { @@ -106,57 +166,114 @@ body { outline-offset: 2px; } -/* ---------- Prose Specification (720px cap, 17px/1.65 line height) ---------- */ +/* ---------- Small shared recipes ---------- */ + +/* Mono uppercase label. The only uppercase text in the product. */ +.eyebrow { + font-family: var(--font-mono); + font-size: 11px; + line-height: 16px; + letter-spacing: 0.06em; + text-transform: uppercase; + color: var(--fg-tertiary); +} + +.chip { + display: inline-flex; + align-items: center; + height: 20px; + padding: 0 6px; + border: 1px solid var(--border-subtle); + border-radius: var(--radius-control); + background: var(--bg-sunken); + font-family: var(--font-mono); + font-size: 11px; + line-height: 16px; + color: var(--fg-secondary); + white-space: nowrap; +} + +kbd { + font-family: var(--font-mono); + font-size: 11px; + line-height: 16px; + padding: 0 5px; + border: 1px solid var(--border-subtle); + border-radius: var(--radius-control); + background: var(--bg-sunken); + color: var(--fg-tertiary); +} + +/* ---------- Prose: 16/26, 680px measure ---------- */ .prose { - --tw-prose-body: var(--foreground); - --tw-prose-headings: var(--foreground); + --tw-prose-body: var(--fg); + --tw-prose-headings: var(--fg); + --tw-prose-lead: var(--fg-secondary); --tw-prose-links: var(--accent); - --tw-prose-bold: var(--foreground); - --tw-prose-code: var(--foreground); - --tw-prose-quotes: var(--muted-foreground); - --tw-prose-quote-borders: var(--border-strong); - --tw-prose-counters: var(--muted-foreground); - --tw-prose-bullets: var(--border-strong); - --tw-prose-hr: var(--border); - --tw-prose-th-borders: var(--border-strong); - --tw-prose-td-borders: var(--border); - --tw-prose-captions: var(--muted-foreground); + --tw-prose-bold: var(--fg); + --tw-prose-counters: var(--fg-secondary); + --tw-prose-bullets: var(--border-default); + --tw-prose-hr: var(--border-subtle); + --tw-prose-quotes: var(--fg-secondary); + --tw-prose-quote-borders: var(--border-default); + --tw-prose-captions: var(--fg-secondary); + --tw-prose-kbd: var(--fg); + --tw-prose-code: var(--fg); --tw-prose-pre-code: var(--code-fg); - --tw-prose-pre-bg: var(--code-bg); - --tw-prose-invert-pre-code: var(--code-fg); - --tw-prose-invert-pre-bg: var(--code-bg); - font-size: 1.0625rem; /* 17px */ - line-height: 1.65; - max-width: 720px; + --tw-prose-pre-bg: var(--bg-code); + --tw-prose-th-borders: var(--border-default); + --tw-prose-td-borders: var(--border-subtle); + font-size: 16px; + line-height: 26px; + max-width: 680px; +} + +.prose :where(p) { + margin: 0 0 18px; } .prose :where(h1) { - font-size: 2.25rem; - font-weight: 700; - letter-spacing: -0.035em; - line-height: 1.2; - margin-bottom: 0.75rem; + font-size: 32px; + line-height: 36px; + font-weight: 600; + letter-spacing: -0.02em; + margin: 0 0 12px; } .prose :where(h2) { - font-size: 1.5rem; - font-weight: 650; - letter-spacing: -0.025em; - margin-top: 2.5rem; - margin-bottom: 0.75rem; + font-size: 22px; + line-height: 28px; + font-weight: 600; + letter-spacing: -0.015em; + margin: 40px 0 12px; scroll-margin-top: 5rem; } .prose :where(h3) { - font-size: 1.2rem; + font-size: 17px; + line-height: 24px; font-weight: 600; - letter-spacing: -0.02em; - margin-top: 1.75rem; - margin-bottom: 0.5rem; + margin: 28px 0 8px; scroll-margin-top: 5rem; } +.prose :where(h4) { + font-size: 15px; + line-height: 22px; + font-weight: 600; + margin: 24px 0 6px; +} + +.prose :where(ul, ol) { + margin: 0 0 18px; + padding-left: 20px; +} + +.prose :where(li) { + margin: 6px 0; +} + .prose :where(a):not(:where(.not-prose *)) { font-weight: 500; color: var(--accent); @@ -171,14 +288,14 @@ body { } .prose :where(code):not(:where(pre code)) { - background: var(--muted); - border: 1px solid var(--border); - border-radius: 4px; - padding: 0.15rem 0.35rem; - font-size: 0.85em; - font-weight: 500; + background: var(--bg-sunken); + border: 1px solid var(--border-subtle); + border-radius: var(--radius-control); + padding: 1px 5px; + font-size: 13.5px; + font-weight: 400; font-family: var(--font-mono); - color: var(--foreground); + color: var(--fg); } .prose :where(code)::before, @@ -187,35 +304,49 @@ body { } .prose :where(table) { - width: 100%; - border-collapse: collapse; - font-size: 0.875rem; - margin: 1.75rem 0; - border: 1px solid var(--border); display: block; - overflow-x: auto; + width: 100%; max-width: 100%; + overflow-x: auto; + margin: 24px 0; + border: 1px solid var(--border-subtle); + border-radius: var(--radius-panel); + border-collapse: separate; + border-spacing: 0; + font-size: 14px; + line-height: 22px; } -.prose :where(thead th) { +.prose :where(table) :where(thead th) { font-family: var(--font-sans); font-weight: 600; - font-size: 0.8125rem; - background: var(--muted); - color: var(--foreground); - padding: 0.65rem 0.85rem; - border-bottom: 1px solid var(--border-strong); + font-size: 13px; + background: var(--bg-sunken); + color: var(--fg); + padding: 10px 14px; + border-bottom: 1px solid var(--border-default); text-align: left; } -.prose :where(tbody td) { - padding: 0.65rem 0.85rem; - border-bottom: 1px solid var(--border); +.prose :where(table) :where(tbody td) { + padding: 10px 14px; + border-bottom: 1px solid var(--border-subtle); + vertical-align: top; +} + +.prose :where(table) :where(tbody tr:last-child td) { + border-bottom: 0; } .prose :where(h2, h3, h4) .heading-anchor { - margin-left: 0.4em; - color: var(--faint); + display: inline-flex; + align-items: center; + justify-content: center; + min-width: 24px; + min-height: 24px; + margin-left: 0.25em; + vertical-align: middle; + color: var(--fg-tertiary); font-weight: 400; text-decoration: none; opacity: 0; @@ -231,59 +362,74 @@ body { color: var(--accent); } -/* Callouts: Note (grey), Warning (amber), Deprecated (red) */ +/* ---------- Callouts ---------- */ + .callout { - margin: 1.75rem 0; - padding: 0.9rem 1.2rem; - border: 1px solid var(--border); - border-left: 3px solid var(--border-strong); - background: var(--card); - border-radius: 4px; + margin: 24px 0; + padding: 12px 16px; + border: 1px solid var(--border-subtle); + border-left: 3px solid var(--accent); + border-radius: var(--radius-panel); + background: var(--bg-raised); } -.callout[data-callout="note"], -.callout[data-callout="tip"] { - border-left-color: var(--accent); +.callout :where(p) { + margin: 0; + font-size: 15px; + line-height: 24px; +} + +.callout :where(p + p) { + margin-top: 8px; +} + +.callout-title { + font-family: var(--font-mono); + font-size: 11px; + line-height: 16px; + letter-spacing: 0.06em; + text-transform: uppercase; + color: var(--accent); + margin: 0 0 4px; } .callout[data-callout="warning"] { - border-left-color: #d97706; + border-left-color: var(--warning); +} + +.callout[data-callout="warning"] .callout-title { + color: var(--warning); } .callout[data-callout="caution"], .callout[data-callout="important"] { - border-left-color: #dc2626; + border-left-color: var(--danger); } -.callout-title { - display: flex; - align-items: center; - gap: 0.5rem; - margin: 0; - font-size: 0.8125rem; - font-weight: 600; - text-transform: uppercase; - letter-spacing: 0.04em; - color: var(--foreground); +.callout[data-callout="caution"] .callout-title, +.callout[data-callout="important"] .callout-title { + color: var(--danger); } -/* Code block bleeds out up to 860px */ +/* ---------- Code blocks: dark in both themes ---------- */ + [data-rehype-pretty-code-figure] { - margin: 1.75rem 0; + margin: 24px 0; position: relative; border: 1px solid var(--code-border); - background: var(--code-bg); - border-radius: 6px; + background: var(--bg-code); + border-radius: var(--radius-panel); overflow: hidden; max-width: 100%; } [data-rehype-pretty-code-figure] pre { - background: var(--code-bg) !important; - padding: 1rem 0; + margin: 0; + background: var(--bg-code) !important; + padding: 14px 0; overflow-x: auto; - font-size: 0.875rem; /* 14px */ - line-height: 1.65; + font-size: 13.5px; + line-height: 22px; color: var(--code-fg); font-family: var(--font-mono); } @@ -291,26 +437,65 @@ body { [data-rehype-pretty-code-figure] pre code { display: grid; font-family: var(--font-mono); + font-size: inherit; background: transparent; border: none; padding: 0; } [data-rehype-pretty-code-figure] pre code [data-line] { - padding: 0 1.25rem; -} - -[data-rehype-pretty-code-figure] code span { - color: var(--shiki-dark, inherit); + padding: 0 16px; } [data-rehype-pretty-code-title] { + display: flex; + align-items: center; + height: 36px; + padding: 0 16px; font-family: var(--font-mono); - font-size: 0.75rem; - color: rgba(255, 255, 255, 0.6); - background: rgba(255, 255, 255, 0.04); + font-size: 12px; + color: var(--code-title); border-bottom: 1px solid var(--code-border); - padding: 0.5rem 1.25rem; +} + +[data-copy-button] { + position: absolute; + right: 8px; + top: 8px; + display: inline-flex; + width: 32px; + height: 32px; + align-items: center; + justify-content: center; + border-radius: var(--radius-control); + color: var(--code-title); + opacity: 0; + cursor: pointer; + transition: opacity 150ms ease, background-color 150ms ease, color 150ms ease, transform 100ms var(--ease-out-strong); +} + +[data-rehype-pretty-code-figure]:hover [data-copy-button], +[data-copy-button]:focus-visible { + opacity: 1; +} + +[data-copy-button]:hover { + background: rgba(255, 255, 255, 0.06); + color: var(--code-fg); +} + +[data-copy-button]:active { + transform: scale(0.95); +} + +/* Hover-revealed controls have no hover on touch devices: keep them visible. */ +@media (hover: none) { + .prose :where(h2, h3, h4) .heading-anchor { + opacity: 0.6; + } + [data-copy-button] { + opacity: 1; + } } @media (prefers-reduced-motion: reduce) { diff --git a/src/app/icon.svg b/src/app/icon.svg index 032100e..1e6e26b 100644 --- a/src/app/icon.svg +++ b/src/app/icon.svg @@ -1,5 +1,13 @@ - - - - + + + + + diff --git a/src/app/layout.tsx b/src/app/layout.tsx index f300893..21a21f6 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,30 +1,41 @@ import type { Metadata, Viewport } from "next"; -import { Inter, JetBrains_Mono } from "next/font/google"; +import { IBM_Plex_Sans, JetBrains_Mono } from "next/font/google"; import type { ReactNode } from "react"; +import { JsonLd } from "@/components/json-ld"; +import { SiteFooter } from "@/components/site-footer"; import { SiteHeader } from "@/components/site-header"; import { ThemeProvider } from "@/components/theme-provider"; import type { SearchItem } from "@/components/search"; import { getChangelog, getDocSections } from "@/lib/content"; +import { DEFAULT_TITLE, siteGraph } from "@/lib/seo"; import { SITE_NAME, SITE_TAGLINE, SITE_URL } from "@/lib/site"; +import { THEME_COLORS } from "@/lib/theme"; import "./globals.css"; -const inter = Inter({ +// Both faces are SIL Open Font License 1.1. next/font downloads them at +// build time, subsets to Latin, self-hosts the files, and sets +// font-display: swap. +const plexSans = IBM_Plex_Sans({ variable: "--font-sans", subsets: ["latin"], + weight: ["400", "500", "600"], + display: "swap", }); const jetbrainsMono = JetBrains_Mono({ variable: "--font-mono", subsets: ["latin"], + weight: ["400", "500"], + display: "swap", }); export const metadata: Metadata = { metadataBase: new URL(SITE_URL), title: { - default: `${SITE_NAME} Documentation`, - template: `%s · ${SITE_NAME}`, + default: DEFAULT_TITLE, + template: `%s | ${SITE_NAME}`, }, description: SITE_TAGLINE, applicationName: SITE_NAME, @@ -43,17 +54,6 @@ export const metadata: Metadata = { "LLM", "open source", ], - robots: { - index: true, - follow: true, - googleBot: { - index: true, - follow: true, - "max-image-preview": "large", - "max-snippet": -1, - "max-video-preview": -1, - }, - }, alternates: { types: { "application/rss+xml": [ @@ -74,8 +74,8 @@ export const metadata: Metadata = { export const viewport: Viewport = { themeColor: [ - { media: "(prefers-color-scheme: dark)", color: "#000000" }, - { media: "(prefers-color-scheme: light)", color: "#ffffff" }, + { media: "(prefers-color-scheme: dark)", color: THEME_COLORS.dark }, + { media: "(prefers-color-scheme: light)", color: THEME_COLORS.light }, ], }; @@ -113,20 +113,22 @@ export default async function RootLayout({ lang="en" suppressHydrationWarning data-scroll-behavior="smooth" - className={`${inter.variable} ${jetbrainsMono.variable} h-full antialiased`} + className={`${plexSans.variable} ${jetbrainsMono.variable} h-full antialiased`} > - + Skip to content + -
+
{children} -
+ +
diff --git a/src/app/manifest.ts b/src/app/manifest.ts index a7deccb..bbe4db4 100644 --- a/src/app/manifest.ts +++ b/src/app/manifest.ts @@ -1,6 +1,7 @@ import type { MetadataRoute } from "next"; import { SITE_NAME, SITE_TAGLINE } from "@/lib/site"; +import { THEME_COLORS } from "@/lib/theme"; export default function manifest(): MetadataRoute.Manifest { return { @@ -9,13 +10,17 @@ export default function manifest(): MetadataRoute.Manifest { description: SITE_TAGLINE, start_url: "/", display: "standalone", - background_color: "#0b0e14", - theme_color: "#0b0e14", + background_color: THEME_COLORS.light, + theme_color: THEME_COLORS.light, icons: [ + { src: "/icon.svg", sizes: "any", type: "image/svg+xml" }, + { src: "/icon-192.png", sizes: "192x192", type: "image/png" }, + { src: "/icon-512.png", sizes: "512x512", type: "image/png" }, { - src: "/icon.svg", - sizes: "any", - type: "image/svg+xml", + src: "/icon-512-maskable.png", + sizes: "512x512", + type: "image/png", + purpose: "maskable", }, ], }; diff --git a/src/app/not-found.tsx b/src/app/not-found.tsx index bc52a0d..1bf6a36 100644 --- a/src/app/not-found.tsx +++ b/src/app/not-found.tsx @@ -1,30 +1,32 @@ +import type { Metadata } from "next"; import Link from "next/link"; +import { buttonClass } from "@/components/ui/button"; +import { pageMetadata } from "@/lib/seo"; + +export const metadata: Metadata = pageMetadata({ + kind: "notFound", + title: "Page not found", + description: "There is no page at this address.", +}); + export default function NotFound() { return ( -
-

404

-

- This page took a different tool call +
+

404

+

+ There is no page at this address.

-

- The page you are looking for does not exist or has moved. Try the - docs, or head back home. +

+ It may have moved, or the link may be out of date. The documentation + index lists every page, and search is one keystroke away.

-
- - Browse the docs - - - Go home - -
+ + Open the documentation +
); } diff --git a/src/app/page.tsx b/src/app/page.tsx index 102bc3e..36a834d 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,5 +1,5 @@ -import { redirect } from "next/navigation"; +import { permanentRedirect } from "next/navigation"; export default function HomePage() { - redirect("/docs/getting-started/introduction"); + permanentRedirect("/docs/getting-started/introduction"); } diff --git a/src/app/sitemap.ts b/src/app/sitemap.ts index e61cd5b..a2f1f4b 100644 --- a/src/app/sitemap.ts +++ b/src/app/sitemap.ts @@ -1,52 +1,54 @@ import type { MetadataRoute } from "next"; -import { getDocSections, getSitePageSlugs } from "@/lib/content"; +import path from "node:path"; + +import { + getContentModified, + getDocSections, + getSitePage, + getSitePageSlugs, +} from "@/lib/content"; +import { getFileDates } from "@/lib/content/dates"; import { SITE_URL } from "@/lib/site"; +/** + * Lists only indexable, canonical, 200-status URLs. The root and /docs are + * redirects and are left out. lastModified comes from git per file. + */ export default async function sitemap(): Promise { - const [sections, sitePageSlugs] = await Promise.all([ + const [sections, sitePageSlugs, changelogModified] = await Promise.all([ getDocSections(), getSitePageSlugs(), + getContentModified("changelog"), ]); - const now = new Date(); - - const staticRoutes: MetadataRoute.Sitemap = [ - { - url: SITE_URL, - lastModified: now, - changeFrequency: "weekly", - priority: 1.0, - }, - { - url: `${SITE_URL}/docs`, - lastModified: now, - changeFrequency: "weekly", - priority: 0.9, - }, - { - url: `${SITE_URL}/changelog`, - lastModified: now, - changeFrequency: "weekly", - priority: 0.7, - }, - ]; - const docRoutes: MetadataRoute.Sitemap = sections.flatMap((section) => section.pages.map((page) => ({ url: `${SITE_URL}/docs/${section.slug}/${page.slug}`, - lastModified: now, - changeFrequency: "weekly" as const, + lastModified: page.modified, priority: 0.9, })), ); - const standaloneRoutes: MetadataRoute.Sitemap = sitePageSlugs.map((slug) => ({ - url: `${SITE_URL}/${slug}`, - lastModified: now, - changeFrequency: "monthly" as const, - priority: slug === "contributing" ? 0.6 : 0.3, - })); + const sitePages = await Promise.all(sitePageSlugs.map((slug) => getSitePage(slug))); + const standaloneRoutes: MetadataRoute.Sitemap = sitePages + .filter((page): page is NonNullable => page !== null) + .map((page) => ({ + url: `${SITE_URL}/${page.slug}`, + lastModified: page.modified, + priority: page.slug === "contributing" ? 0.6 : 0.3, + })); + + // /creator is indexable but intentionally unlinked from navigation; the + // sitemap is how crawlers find it. See src/app/creator/page.tsx. + const creatorModified = getFileDates( + path.join(process.cwd(), "src/app/creator/page.tsx"), + ).modified; - return [...staticRoutes, ...docRoutes, ...standaloneRoutes]; + return [ + ...docRoutes, + { url: `${SITE_URL}/changelog`, lastModified: changelogModified, priority: 0.7 }, + ...standaloneRoutes, + { url: `${SITE_URL}/creator`, lastModified: creatorModified, priority: 0.5 }, + ]; } diff --git a/src/components/copy-code-buttons.tsx b/src/components/copy-code-buttons.tsx index fc304a4..829c49c 100644 --- a/src/components/copy-code-buttons.tsx +++ b/src/components/copy-code-buttons.tsx @@ -2,13 +2,12 @@ import { useEffect } from "react"; -const CHECK_ICON = ``; -const COPY_ICON = ``; +const CHECK_ICON = ``; +const COPY_ICON = ``; /** - * Progressively enhances rendered markdown with a copy button on every code - * block. The markdown itself is server-rendered HTML, so the button has to - * be attached after hydration. + * Adds a copy button to every server-rendered code block after hydration. + * The button is styled by the [data-copy-button] rules in globals.css. */ export function CopyCodeButtons() { useEffect(() => { @@ -23,11 +22,10 @@ export function CopyCodeButtons() { if (!pre) return; const button = document.createElement("button"); + button.type = "button"; button.setAttribute("data-copy-button", ""); button.setAttribute("aria-label", "Copy code"); button.setAttribute("title", "Copy code"); - button.className = - "absolute right-2.5 top-2.5 flex size-8 cursor-pointer items-center justify-center rounded-md border border-code-border bg-code-bg text-muted-foreground opacity-0 transition-opacity duration-150 hover:text-foreground focus-visible:opacity-100 group-hover:opacity-100"; button.innerHTML = COPY_ICON; let timeout: ReturnType | undefined; @@ -35,9 +33,11 @@ export function CopyCodeButtons() { try { await navigator.clipboard.writeText(pre.innerText.trimEnd()); button.innerHTML = CHECK_ICON; + button.setAttribute("aria-label", "Copied"); if (timeout) clearTimeout(timeout); timeout = setTimeout(() => { button.innerHTML = COPY_ICON; + button.setAttribute("aria-label", "Copy code"); }, 1600); } catch { // Clipboard access denied; leave the button as-is. @@ -45,7 +45,6 @@ export function CopyCodeButtons() { }; button.addEventListener("click", onClick); - figure.classList.add("group"); figure.appendChild(button); cleanups.push(() => { diff --git a/src/components/copy-markdown-button.tsx b/src/components/copy-markdown-button.tsx index b9db31c..fbc5f41 100644 --- a/src/components/copy-markdown-button.tsx +++ b/src/components/copy-markdown-button.tsx @@ -1,12 +1,11 @@ "use client"; -import { Check, FileCode } from "lucide-react"; +import { Check, Copy } from "lucide-react"; import { useState } from "react"; -/** - * Copies the page's raw markdown source, handy for pasting a doc page - * into an LLM conversation. - */ +import { Button } from "@/components/ui/button"; + +/** Copies the page's raw markdown, for pasting a doc page into an LLM. */ export function CopyMarkdownButton({ markdown }: { markdown: string }) { const [copied, setCopied] = useState(false); @@ -21,17 +20,13 @@ export function CopyMarkdownButton({ markdown }: { markdown: string }) { } return ( - + ); } diff --git a/src/components/docs/feedback-widget.tsx b/src/components/docs/feedback-widget.tsx index d455136..9481f00 100644 --- a/src/components/docs/feedback-widget.tsx +++ b/src/components/docs/feedback-widget.tsx @@ -1,87 +1,65 @@ "use client"; -import { Check, ThumbsDown, ThumbsUp } from "lucide-react"; import { useState } from "react"; -export function FeedbackWidget() { - const [voted, setVoted] = useState<"yes" | "no" | null>(null); - const [comment, setComment] = useState(""); - const [submitted, setSubmitted] = useState(false); +import { Button } from "@/components/ui/button"; +import { GITHUB_DOCS_URL } from "@/lib/site"; - function handleVote(choice: "yes" | "no") { - setVoted(choice); - } +/** + * "Was this page helpful?" There is no feedback backend, so a "no" hands + * the reader a pre-filled GitHub issue instead of pretending to collect a + * comment. + */ +export function FeedbackWidget({ + pageTitle, + pagePath, +}: { + pageTitle: string; + pagePath: string; +}) { + const [voted, setVoted] = useState<"yes" | "no" | null>(null); - function handleSubmit(e: React.FormEvent) { - e.preventDefault(); - setSubmitted(true); - } - - if (submitted) { - return ( -
- - Thank you for your feedback. -
- ); - } + const issueUrl = `${GITHUB_DOCS_URL}/issues/new?${new URLSearchParams({ + title: `Docs feedback: ${pageTitle}`, + body: `Page: ${pagePath}\n\nWhat was missing or unclear?\n\n`, + }).toString()}`; return ( -
-
- - Was this page helpful? +
+
+ + {voted === "yes" ? "Glad it helped." : "Was this page helpful?"} -
- - -
+ {voted !== "yes" && ( + <> + + + + )}
- {voted && ( -
- -
- setComment(e.target.value)} - placeholder="Your thoughts..." - className="flex-1 rounded border border-border bg-background px-2.5 py-1 text-xs text-foreground placeholder:text-faint focus:border-accent focus:outline-none" - /> - -
-
+ {voted === "no" && ( +

+ Tell us what was missing in a{" "} + + GitHub issue + + , pre-filled with this page. +

)}
); diff --git a/src/components/docs/sidebar.tsx b/src/components/docs/sidebar.tsx index e19c3c8..b99c2ae 100644 --- a/src/components/docs/sidebar.tsx +++ b/src/components/docs/sidebar.tsx @@ -1,114 +1,77 @@ "use client"; -import { ChevronDown, FileText, X } from "lucide-react"; +import { ChevronDown, X } from "lucide-react"; import Link from "next/link"; import { usePathname } from "next/navigation"; -import { useState } from "react"; +import { useEffect, useRef, useState, useSyncExternalStore } from "react"; +import { createPortal } from "react-dom"; +import { buttonClass } from "@/components/ui/button"; import type { DocSection } from "@/lib/content/types"; import { GITHUB_URL } from "@/lib/site"; import { cn } from "@/lib/utils"; -// Diátaxis classification tags for section headers -const DIATAXIS_HINTS: Record = { - "getting-started": "Tutorials", - "core-concepts": "Concepts", - guides: "How-To", - reference: "Reference", -}; +const emptySubscribe = () => () => {}; -function SectionNav({ sections, onNavigate }: { sections: DocSection[]; onNavigate?: () => void }) { +const ROW = + "flex h-8 items-center rounded-control px-2 text-ui transition-[background-color,color] duration-150"; +const ROW_IDLE = "text-fg-secondary hover:bg-sunken hover:text-fg"; +const ROW_ACTIVE = "bg-sunken font-medium text-fg shadow-[inset_2px_0_0_var(--accent)]"; + +function SectionNav({ + sections, + onNavigate, +}: { + sections: DocSection[]; + onNavigate?: () => void; +}) { const pathname = usePathname(); return ( - - {/* Center: Search Field */} -
+ {/* Below lg the search sits with the right-hand controls; from lg it + takes the centre column. */} +
- {/* Right Controls */} -
-
- Python - 3.11+ -
- +
@@ -126,10 +88,12 @@ export function SiteHeader({ searchItems }: { searchItems: SearchItem[] }) { @@ -138,25 +102,38 @@ export function SiteHeader({ searchItems }: { searchItems: SearchItem[] }) { {menuOpen && ( )} diff --git a/src/components/theme-toggle.tsx b/src/components/theme-toggle.tsx index c24e5ef..552875a 100644 --- a/src/components/theme-toggle.tsx +++ b/src/components/theme-toggle.tsx @@ -4,6 +4,7 @@ import { Monitor, Moon, Sun } from "lucide-react"; import { useTheme } from "next-themes"; import { useSyncExternalStore } from "react"; +import { buttonClass } from "@/components/ui/button"; import { cn } from "@/lib/utils"; const emptySubscribe = () => () => {}; @@ -16,78 +17,60 @@ function useMounted(): boolean { ); } +const OPTIONS = [ + { value: "light", label: "Light theme", Icon: Sun }, + { value: "dark", label: "Dark theme", Icon: Moon }, + { value: "system", label: "System theme", Icon: Monitor }, +] as const; + export function ThemeToggle({ className }: { className?: string }) { - const { theme, setTheme } = useTheme(); + const { theme, resolvedTheme, setTheme } = useTheme(); const mounted = useMounted(); - const isDark = mounted ? theme === "dark" : true; - - function toggle() { - setTheme(isDark ? "light" : "dark"); - } + // resolvedTheme accounts for "system", so the compact toggle always flips + // what the visitor is actually looking at. + const isDark = mounted ? resolvedTheme === "dark" : true; return ( <> - {/* Mobile / Tablet compact 1-button toggle (32x32px) */} + {/* Below sm: one button that flips the resolved theme. */} - {/* Desktop 3-option toggle pill */} + {/* From sm: segmented control. */}
- - - + {OPTIONS.map(({ value, label, Icon }) => { + const checked = mounted ? theme === value : value === "dark"; + return ( + + ); + })}
); diff --git a/src/components/ui/button.tsx b/src/components/ui/button.tsx new file mode 100644 index 0000000..8b05dfc --- /dev/null +++ b/src/components/ui/button.tsx @@ -0,0 +1,50 @@ +import type { ButtonHTMLAttributes } from "react"; + +import { cn } from "@/lib/utils"; + +export type ButtonVariant = "ghost" | "outline" | "primary"; + +const BASE = + "inline-flex h-8 cursor-pointer items-center justify-center gap-1.5 whitespace-nowrap rounded-control text-ui font-medium transition-[background-color,color,border-color,transform] duration-150 ease-out active:scale-[0.97] active:duration-100 disabled:pointer-events-none disabled:opacity-45"; + +const VARIANTS: Record = { + ghost: "px-2.5 text-fg-secondary hover:bg-sunken hover:text-fg", + outline: + "border border-border-default bg-raised px-2.5 text-fg-secondary hover:bg-sunken hover:text-fg", + primary: "bg-accent px-3 text-accent-fg hover:bg-accent-hover", +}; + +/** + * Class recipe for anything that looks like a button, so links and real + * buttons share one set of states. + */ +export function buttonClass({ + variant = "ghost", + icon = false, + className, +}: { + variant?: ButtonVariant; + icon?: boolean; + className?: string; +} = {}): string { + return cn(BASE, VARIANTS[variant], icon && "w-8 px-0", className); +} + +export function Button({ + variant = "ghost", + icon = false, + className, + type = "button", + ...props +}: ButtonHTMLAttributes & { + variant?: ButtonVariant; + icon?: boolean; +}) { + return ( +