<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>Alencar Tech</title>
    <link>https://alencar.tech/</link>
    <description>Thoughts, stories, and ideas about technology and life — deep dives on AI, cloud, and software engineering.</description>
    <language>en</language>
    <atom:link href="https://alencar.tech/feed.xml" rel="self" type="application/rss+xml" />
    <lastBuildDate>Tue, 14 Jul 2026 00:00:00 GMT</lastBuildDate>
    <item>
      <title>The Fleet — How to Actually Use AI as a Software Engineer</title>
      <link>https://alencar.tech/posts/becoming-a-heavy-ai-user/</link>
      <guid isPermaLink="true">https://alencar.tech/posts/becoming-a-heavy-ai-user/</guid>
      <pubDate>Tue, 14 Jul 2026 00:00:00 GMT</pubDate>
      <description>A practical playbook for going from &quot;AI as autocomplete&quot; to running a fleet — work loops, workflows, parallel subagents, and multiple Claude Code sessions across tmux panes.</description>
      <content:encoded xmlns:content="http://purl.org/rss/1.0/modules/content/"><![CDATA[<p>In my <a href="/posts/agentic-ai-on-bedrock-with-agentcore-and-mcp/">post on building agents with Bedrock AgentCore</a>, the whole point was giving a model <em>hands</em> — tools it can call and a loop that decides when to call them. This post is about the other side of that idea: giving <strong>yourself</strong> a fleet of those loops.</p>
<p>Most engineers use AI the way they'd use a smarter autocomplete. One chat window. One task at a time. You type, you wait, you watch it type back, you copy the result. That's a fine place to start, and it's also where most people stop. Heavy users work differently — not because they type faster, but because they stop babysitting a single conversation and start <strong>orchestrating many</strong>.</p>
<p>Here's the mental shift: a chat is something you <em>attend</em>; a fleet is something you <em>dispatch</em>. The rest of this post is a ladder from one to the other, in four levels, using <a href="https://claude.com/claude-code">Claude Code</a> as the running example. Each level is worth adopting on its own.</p>
<h2>Level 1 — Loops: stop re-typing the same prompt</h2>
<p>The first habit that separates heavy users is that they almost never do the same thing twice by hand. If a task is worth doing on an interval — poll a deploy, keep tests green, watch a PR for review comments — it should run on a loop, not on your attention.</p>
<p>In Claude Code, that's <code>/loop</code>:</p>
<pre><code>/loop 5m /run-tests
</code></pre>
<p>That runs your <code>/run-tests</code> command every five minutes. Drop the interval and the model self-paces — it decides when to check back based on what it's waiting for:</p>
<pre><code>/loop keep checking the PR for new review comments and address them
</code></pre>
<p>The point isn't the syntax. The point is a change in posture: <strong>work that repeats should not consume you.</strong> A loop babysitting a CI run frees you to think about the next thing instead of alt-tabbing to a dashboard every few minutes. The first time you set a loop running and walk away, the &quot;one chat window&quot; model starts to feel very small.</p>
<p>A good rule: any sentence you find yourself starting with &quot;every few minutes, check…&quot; is a loop.</p>
<h2>Level 2 — Workflows: when a script beats a conversation</h2>
<p>A loop repeats <em>one</em> task. A <strong>workflow</strong> orchestrates <em>many</em>, deterministically. The distinction matters: a conversation is model-driven — the model decides what to do next — while a workflow is code-driven. You write the control flow (the fan-out, the loop, the verify step) and the model fills in the intelligent parts.</p>
<p>The classic shape is <strong>find → verify → synthesize</strong>. Say you want to review a branch across several dimensions — correctness, security, performance — and you don't want a single pass that half-covers each. A workflow fans out one agent per dimension, then spawns a skeptic to adversarially verify each finding <em>before</em> it reaches you:</p>
<pre><code>review each changed file for [correctness, security, perf]
  → for every finding, spawn a verifier that tries to REFUTE it
  → keep only findings that survive
</code></pre>
<p>Why bother instead of just asking? Three reasons:</p>
<ol>
<li><strong>Determinism</strong> — the same structure runs the same way every time. No &quot;it forgot to check the security angle.&quot;</li>
<li><strong>Parallelism</strong> — the dimensions run concurrently, so wall-clock time is the slowest single chain, not the sum.</li>
<li><strong>Adversarial rigor</strong> — a separate agent whose only job is to <em>disprove</em> a finding kills the plausible-but-wrong results that a single optimistic pass waves through.</li>
</ol>
<p>You reach for a workflow when the task has structure you can name — a set of files to sweep, a pipeline of stages, a thing that must be checked N independent ways. When the task is exploratory and you don't know the shape yet, stay in a conversation. Heavy users move fluidly between the two: scout in a chat to discover the work-list, then hand the list to a workflow to grind through it.</p>
<h2>Level 3 — Subagents: parallel work, isolated context</h2>
<p>Inside a single session, you don't have to do everything in your own context window. You can <strong>dispatch subagents</strong> — independent Claude instances that go off, do a focused job, and return only the conclusion.</p>
<p>This is the antidote to context bloat. When you need to answer &quot;where is X handled across these forty files?&quot;, you don't want forty files dumped into your working memory. You want an agent to sweep them and hand back the two-line answer:</p>
<pre><code>Explore agent: find every place we validate auth tokens; return file:line + a one-line note each
</code></pre>
<p>The key techniques:</p>
<ul>
<li><strong>Fan out for independent work.</strong> If you have three unrelated tasks — update the docs, fix a flaky test, add a config flag — dispatch three agents in a <em>single message</em> so they run concurrently instead of one after another.</li>
<li><strong>Use the right agent for the job.</strong> A read-only <code>Explore</code> agent for searching, a <code>Plan</code> agent for designing an approach, a general agent for multi-step execution. Specialized agents keep each job's context clean.</li>
<li><strong>Keep the conclusion, not the file dump.</strong> The whole win is that the subagent reads broadly and you keep only what matters. Your context stays lean; theirs absorbs the noise.</li>
</ul>
<p>The mental model: you are less the person writing every line and more the <strong>tech lead</strong> handing out well-scoped tickets and integrating what comes back.</p>
<h2>Level 4 — Multiple sessions and tmux: run the fleet in parallel</h2>
<p>The final level is spatial. One session, however well-orchestrated, is still one throat to choke. Heavy users run <strong>several Claude Code sessions at once</strong>, each on its own slice of the work, laid out where they can watch them all.</p>
<p>That's what <a href="https://github.com/tmux/tmux">tmux</a> is for. Split your terminal into panes and put a session in each:</p>
<pre><code class="language-bash"># a 2x2 grid of independent Claude sessions
tmux new-session -d -s fleet
tmux split-window -h
tmux split-window -v
tmux select-pane -t 0
tmux split-window -v
tmux attach -t fleet
</code></pre>
<p>Now you have four panes. One session is refactoring a module, another is writing tests for it, a third is drafting a migration, a fourth is on a <code>/loop</code> watching CI. You glance across the grid the way an ops engineer watches a wall of monitors — dipping into whichever pane needs a decision, letting the rest run.</p>
<p>There's one trap to avoid: <strong>parallel sessions editing the same files will collide.</strong> The fix is isolation. Give each parallel line of work its own <a href="https://git-scm.com/docs/git-worktree">git worktree</a> — a separate checkout of the same repo sharing one history:</p>
<pre><code class="language-bash">git worktree add ../blog-feature-a feature-a
git worktree add ../blog-feature-b feature-b
</code></pre>
<p>Point one session at each worktree and they can hammer away simultaneously without stepping on each other. Merge when each is done. Some agent tools will even spin up a throwaway worktree per agent for you when parallel edits would otherwise conflict — but knowing the primitive underneath it is what lets you scale past a handful of sessions.</p>
<h2>A day in the fleet</h2>
<p>Put the four levels together and an ordinary morning looks like this:</p>
<ul>
<li><strong>Pane 1</strong> is running a review <em>workflow</em> on last night's branch — fan out, verify, and by the time I've had coffee it's handed me three confirmed bugs and discarded eleven false alarms.</li>
<li><strong>Pane 2</strong> is a session in a worktree, implementing the fix for the first bug while I read the writeup for the second.</li>
<li><strong>Pane 3</strong> is a <code>/loop</code> babysitting the deploy pipeline; it pings me only if something turns red.</li>
<li><strong>Pane 4</strong> is where I'm actually thinking — sketching the next feature with a <code>Plan</code> agent, dispatching <code>Explore</code> agents to answer &quot;how do we do X today?&quot; without polluting my context.</li>
</ul>
<p>I'm not typing faster than anyone else. I'm just not the bottleneck for four things at once.</p>
<h2>Where to start</h2>
<p>You don't get here by flipping a switch. You get here by climbing one rung at a time:</p>
<ol>
<li><strong>This week:</strong> replace one repetitive check with a <code>/loop</code>. Feel what it's like to walk away from it.</li>
<li><strong>Next:</strong> the first time you catch yourself doing the same review by hand twice, write it as a workflow.</li>
<li><strong>Then:</strong> stop reading forty files yourself — dispatch an <code>Explore</code> agent and keep the answer.</li>
<li><strong>Finally:</strong> open a second session in a worktree and let two things happen at once.</li>
</ol>
<p>The autocomplete model asks <em>&quot;what can this AI do for my current line of code?&quot;</em> The fleet model asks <em>&quot;what are the six things that could be running right now, and which one actually needs me?&quot;</em> That second question is the whole job. Learn to ask it and the tools stop being assistants — they become the team you lead.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Building Company-Aware AI Agents on AWS Bedrock with AgentCore and MCP</title>
      <link>https://alencar.tech/posts/agentic-ai-on-bedrock-with-agentcore-and-mcp/</link>
      <guid isPermaLink="true">https://alencar.tech/posts/agentic-ai-on-bedrock-with-agentcore-and-mcp/</guid>
      <pubDate>Mon, 13 Jul 2026 00:00:00 GMT</pubDate>
      <description>How to build an agent on Bedrock AgentCore that connects to your company&#39;s tools over MCP, grounds itself in internal documents with a Knowledge Base, and ships as a quick Python chatbot.</description>
      <content:encoded xmlns:content="http://purl.org/rss/1.0/modules/content/"><![CDATA[<p>In my <a href="/posts/building-an-llm-pipeline-with-sagemaker/">post on building an LLM pipeline with SageMaker</a>, we trained and served a model. But a served model just answers what you send it — it has no hands. It can't look up an order, query your internal wiki, or open a ticket. <strong>Agentic AI</strong> is the layer that gives a model those hands: tools it can call, documents it can read, and a loop that decides when to do which.</p>
<p>On AWS, the clean managed path for this is <strong>Bedrock AgentCore</strong> paired with the <strong>Model Context Protocol (MCP)</strong>. In this post I'll build an agent that connects to my company's internal tools over MCP, grounds its answers in internal documents through a Bedrock Knowledge Base, and ships as a small chatbot you can run in a terminal. Let me walk you through it.</p>
<h2>From Models to Agents</h2>
<p>An &quot;agent&quot; is three things wired together:</p>
<ol>
<li>A <strong>model</strong> to reason (here, Claude on Bedrock)</li>
<li>A set of <strong>tools</strong> it can call (your company's APIs, exposed as MCP tools)</li>
<li>A <strong>loop</strong> that lets the model decide: answer directly, call a tool, read a doc, then continue</li>
</ol>
<p>The hard parts have always been hosting that loop reliably and connecting it to real company systems without hand-rolling an integration per API. That's exactly what AgentCore handles. <strong>AgentCore Runtime</strong> hosts the agent as a managed, serverless endpoint. <strong>AgentCore Gateway</strong> turns your existing APIs into MCP tools the agent can discover and call. And a <strong>Bedrock Knowledge Base</strong> gives it retrieval over your documents.</p>
<h2>The Architecture at a Glance</h2>
<p>Here's how the pieces fit together:</p>
<ol>
<li><strong>Bedrock Knowledge Base</strong> indexes internal docs sitting in S3 (managed RAG)</li>
<li><strong>AgentCore Gateway</strong> exposes company APIs (order lookup, ticketing, etc.) as MCP tools</li>
<li><strong>The agent</strong> — a model plus a tool-use loop — connects to the Gateway over MCP and to the Knowledge Base for retrieval</li>
<li><strong>AgentCore Runtime</strong> hosts that agent behind a stable, session-aware endpoint</li>
<li><strong>A CLI chatbot</strong> invokes the endpoint and streams responses back</li>
</ol>
<p>Each layer has one job and can be built and tested on its own. Let's go through them.</p>
<h2>Grounding the Agent in Internal Docs</h2>
<p>Before the agent can answer questions about internal policies, runbooks, or specs, it needs to read them. A <strong>Bedrock Knowledge Base</strong> handles the whole RAG pipeline for you: point it at an S3 bucket of documents, pick an embeddings model, and Bedrock chunks, embeds, and indexes everything into a vector store.</p>
<p>Once the Knowledge Base is synced, you can query it directly with <code>retrieve_and_generate</code> — this both retrieves the relevant chunks and generates a grounded answer in one call:</p>
<pre><code class="language-python">import boto3

agent_runtime = boto3.client(&quot;bedrock-agent-runtime&quot;)

response = agent_runtime.retrieve_and_generate(
    input={&quot;text&quot;: &quot;What's our on-call escalation policy for Sev-1 incidents?&quot;},
    retrieveAndGenerateConfiguration={
        &quot;type&quot;: &quot;KNOWLEDGE_BASE&quot;,
        &quot;knowledgeBaseConfiguration&quot;: {
            &quot;knowledgeBaseId&quot;: &quot;KB1234567890&quot;,
            &quot;modelArn&quot;: &quot;arn:aws:bedrock:us-east-1::foundation-model/anthropic.claude-3-5-sonnet-20241022-v2:0&quot;,
        },
    },
)

print(response[&quot;output&quot;][&quot;text&quot;])
</code></pre>
<p>That's a useful building block on its own. But we don't want a separate call path — we want the <em>agent</em> to reach for the docs on its own when a question calls for it. We'll wire the Knowledge Base in as a tool in a moment.</p>
<h2>Exposing Company Tools Through the MCP Gateway</h2>
<p>This is the heart of it. Your company already has APIs — an order service, a ticketing system, a customer database. <strong>AgentCore Gateway</strong> takes those and exposes them as MCP tools, so any MCP-speaking agent can discover and call them through one standardized endpoint. No per-API glue code in the agent itself.</p>
<p>First, create the gateway. It speaks MCP and authorizes inbound callers with a JWT (from Cognito or any OIDC provider):</p>
<pre><code class="language-python">import boto3

gateway_client = boto3.client(&quot;bedrock-agentcore-control&quot;)

auth_config = {
    &quot;customJWTAuthorizer&quot;: {
        &quot;allowedClients&quot;: [&quot;&lt;cognito_client_id&gt;&quot;],
        &quot;discoveryUrl&quot;: &quot;&lt;cognito_oidc_discovery_url&gt;&quot;,
    }
}

gateway = gateway_client.create_gateway(
    name=&quot;CompanyToolsGateway&quot;,
    roleArn=&quot;arn:aws:iam::123456789012:role/AgentCoreGatewayRole&quot;,
    protocolType=&quot;MCP&quot;,
    authorizerType=&quot;CUSTOM_JWT&quot;,
    authorizerConfiguration=auth_config,
    description=&quot;Internal company tools exposed as MCP&quot;,
)

gateway_id = gateway[&quot;gatewayId&quot;]
gateway_url = gateway[&quot;gatewayUrl&quot;]
</code></pre>
<p>Then add <strong>targets</strong> — each target is a backend the gateway turns into tools. The simplest is a Lambda function, where you declare the tool schema inline. Here's an &quot;order status&quot; tool backed by a Lambda:</p>
<pre><code class="language-python">lambda_target_config = {
    &quot;mcp&quot;: {
        &quot;lambda&quot;: {
            &quot;lambdaArn&quot;: &quot;arn:aws:lambda:us-east-1:123456789012:function:get-order-status&quot;,
            &quot;toolSchema&quot;: {
                &quot;inlinePayload&quot;: [
                    {
                        &quot;name&quot;: &quot;get_order_status&quot;,
                        &quot;description&quot;: &quot;Look up the current status of a customer order by ID&quot;,
                        &quot;inputSchema&quot;: {
                            &quot;type&quot;: &quot;object&quot;,
                            &quot;properties&quot;: {&quot;orderId&quot;: {&quot;type&quot;: &quot;string&quot;}},
                            &quot;required&quot;: [&quot;orderId&quot;],
                        },
                    }
                ]
            },
        }
    }
}

gateway_client.create_gateway_target(
    gatewayIdentifier=gateway_id,
    name=&quot;OrderStatusTool&quot;,
    description=&quot;Order status lookup&quot;,
    targetConfiguration=lambda_target_config,
    credentialProviderConfigurations=[{&quot;credentialProviderType&quot;: &quot;GATEWAY_IAM_ROLE&quot;}],
)
</code></pre>
<p>If your service already has an OpenAPI spec, you can skip the inline schema entirely — point the target at the spec in S3 and the gateway generates a tool per operation:</p>
<pre><code class="language-python">openapi_target_config = {
    &quot;mcp&quot;: {&quot;openApiSchema&quot;: {&quot;s3&quot;: {&quot;uri&quot;: &quot;s3://my-bucket/specs/ticketing.yaml&quot;}}}
}

gateway_client.create_gateway_target(
    gatewayIdentifier=gateway_id,
    name=&quot;TicketingTools&quot;,
    description=&quot;Ticketing API from OpenAPI spec&quot;,
    targetConfiguration=openapi_target_config,
    credentialProviderConfigurations=[
        {
            &quot;credentialProviderType&quot;: &quot;API_KEY&quot;,
            &quot;credentialProvider&quot;: {
                &quot;apiKeyCredentialProvider&quot;: {
                    &quot;credentialParameterName&quot;: &quot;api_key&quot;,
                    &quot;providerArn&quot;: &quot;&lt;api-key-credential-provider-arn&gt;&quot;,
                    &quot;credentialLocation&quot;: &quot;HEADER&quot;,
                }
            },
        }
    ],
)
</code></pre>
<p>When you create a target, the gateway immediately calls the MCP <code>tools/list</code> capability behind the scenes and indexes the tools, so they're discoverable right away. On the outbound side, the gateway handles auth to your backend for you — IAM (SigV4), OAuth, or an API key — so your credentials never live in the agent.</p>
<h2>Assembling the Agent</h2>
<p>Now we bring the model, the tools, and the docs together. I'll use the <a href="https://strandsagents.com/">Strands Agents SDK</a> for the agent loop — it's what AgentCore's toolkit is built around — and wrap it in <code>BedrockAgentCoreApp</code> so it can be deployed to the Runtime.</p>
<p>The agent connects to the Gateway as an <strong>MCP client</strong>: it opens a streamable HTTP connection to the gateway URL with a bearer token, lists the available tools, and hands them to the model. For documents, Strands ships a built-in <code>retrieve</code> tool that queries a Bedrock Knowledge Base (it reads the <code>KNOWLEDGE_BASE_ID</code> environment variable), so the model can pull from internal docs the same way it calls any other tool.</p>
<pre><code class="language-python">import os

from bedrock_agentcore import BedrockAgentCoreApp
from strands import Agent
from strands.models import BedrockModel
from strands_tools import retrieve
from strands.tools.mcp import MCPClient
from mcp.client.streamable_http import streamablehttp_client

app = BedrockAgentCoreApp()

GATEWAY_URL = os.environ[&quot;GATEWAY_URL&quot;]
MODEL_ID = &quot;anthropic.claude-3-5-sonnet-20241022-v2:0&quot;

# The Strands `retrieve` tool reads this to know which Knowledge Base to query.
os.environ.setdefault(&quot;KNOWLEDGE_BASE_ID&quot;, &quot;KB1234567890&quot;)

model = BedrockModel(model_id=MODEL_ID)


def gateway_transport():
    token = get_gateway_token()  # fetch an OAuth token for the gateway's JWT authorizer
    return streamablehttp_client(
        GATEWAY_URL, headers={&quot;Authorization&quot;: f&quot;Bearer {token}&quot;}
    )


mcp_client = MCPClient(gateway_transport)

SYSTEM_PROMPT = (
    &quot;You are a helpful internal assistant. Use the company tools to look up live &quot;
    &quot;data, and use the retrieve tool to ground answers in internal documentation. &quot;
    &quot;Cite what you looked up.&quot;
)


@app.entrypoint
def invoke(payload):
    user_message = payload.get(&quot;prompt&quot;, &quot;&quot;)
    with mcp_client:
        tools = mcp_client.list_tools_sync() + [retrieve]
        agent = Agent(model=model, system_prompt=SYSTEM_PROMPT, tools=tools)
        result = agent(user_message)
    return {&quot;result&quot;: result.message}


if __name__ == &quot;__main__&quot;:
    app.run()
</code></pre>
<p>That's the whole agent. The <code>@app.entrypoint</code> decorator is what makes this deployable — <code>BedrockAgentCoreApp</code> wraps your function in the HTTP contract AgentCore Runtime expects, so you never touch the server plumbing. Inside, the model now has the company's MCP tools <em>and</em> the Knowledge Base at its disposal, and the loop decides which to reach for.</p>
<h2>Deploying to AgentCore Runtime</h2>
<p>With the agent written, the starter toolkit deploys it in two commands. <code>configure</code> inspects your entrypoint and generates the runtime config; <code>launch</code> builds the container, pushes it, and stands up the managed endpoint:</p>
<pre><code class="language-bash">pip install bedrock-agentcore bedrock-agentcore-starter-toolkit strands-agents strands-agents-tools

agentcore configure -e agent.py
agentcore launch
</code></pre>
<p>When <code>launch</code> finishes, it prints the <strong>agent runtime ARN</strong>. That ARN is the stable address other services — including our chatbot — use to invoke the agent. AgentCore handles scaling, session isolation, and keeping the endpoint warm.</p>
<p>You can smoke-test it right from the CLI:</p>
<pre><code class="language-bash">agentcore invoke '{&quot;prompt&quot;: &quot;What is the status of order 4815 and what does our returns policy say?&quot;}'
</code></pre>
<p>If the tools and Knowledge Base are wired correctly, the agent will call <code>get_order_status</code> for the live data and <code>retrieve</code> for the policy, then combine both into one answer.</p>
<h2>A Quick Chatbot</h2>
<p>The payoff: a small terminal chatbot that talks to the deployed agent. The key detail is the <strong>session id</strong> — reuse the same <code>runtimeSessionId</code> across turns and AgentCore keeps the conversation context, so the agent remembers what you said earlier.</p>
<pre><code class="language-python">import json
import uuid

import boto3

AGENT_ARN = &quot;arn:aws:bedrock-agentcore:us-east-1:123456789012:runtime/company-assistant-XXXX&quot;

client = boto3.client(&quot;bedrock-agentcore&quot;)
session_id = str(uuid.uuid4())  # one id for the whole conversation

print(&quot;Company assistant ready. Ctrl-C to quit.\n&quot;)

while True:
    try:
        prompt = input(&quot;you › &quot;).strip()
    except (EOFError, KeyboardInterrupt):
        print(&quot;\nbye&quot;)
        break

    if not prompt:
        continue

    response = client.invoke_agent_runtime(
        agentRuntimeArn=AGENT_ARN,
        runtimeSessionId=session_id,
        payload=json.dumps({&quot;prompt&quot;: prompt}).encode(),
        qualifier=&quot;DEFAULT&quot;,
    )

    # The response streams back as byte chunks — collect and decode them.
    chunks = [chunk.decode(&quot;utf-8&quot;) for chunk in response.get(&quot;response&quot;, [])]
    answer = json.loads(&quot;&quot;.join(chunks))
    print(f&quot;bot › {answer['result']}\n&quot;)
</code></pre>
<p>Run it, and you've got a working internal assistant:</p>
<pre><code>you › what's the status of order 4815?
bot › Order 4815 shipped yesterday and is out for delivery, expected today.

you › and if it arrives damaged, what should the customer do?
bot › Per our returns policy, damaged items can be reported within 30 days for a
      full refund or replacement — here's the process...
</code></pre>
<p>The first answer came from the MCP order tool; the second came from the Knowledge Base. The agent decided which to use, and the session id tied the two turns together.</p>
<h2>Putting It All Together</h2>
<p>Step back and the whole thing is just a few well-bounded layers:</p>
<ul>
<li>A <strong>Knowledge Base</strong> grounds the agent in internal documents</li>
<li>A <strong>Gateway</strong> exposes company APIs as MCP tools, handling auth on both sides</li>
<li>The <strong>agent</strong> wires the model to those tools and docs with a decision loop</li>
<li><strong>AgentCore Runtime</strong> hosts it serverlessly behind a session-aware endpoint</li>
<li>A <strong>CLI chatbot</strong> consumes it like any other API</li>
</ul>
<p>The nice part is that none of this required managing a server, and each layer is swappable — add a new company API by creating one more gateway target, and every agent connected to the gateway can use it immediately. No redeploys, no glue code.</p>
<p>If you've already got models running on Bedrock, this is how you give them hands. Next time someone asks your assistant a question that spans a live system <em>and</em> your internal docs, it'll just handle it.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Using LLM Embeddings to Transform Risk Data Analysis</title>
      <link>https://alencar.tech/posts/llm-embeddings-for-risk-analysis/</link>
      <guid isPermaLink="true">https://alencar.tech/posts/llm-embeddings-for-risk-analysis/</guid>
      <pubDate>Sat, 21 Feb 2026 00:00:00 GMT</pubDate>
      <description>How embeddings work under the hood and how to leverage them to build intelligent, user-friendly outputs from your trained models and risk datasets.</description>
      <content:encoded xmlns:content="http://purl.org/rss/1.0/modules/content/"><![CDATA[<p>In my <a href="/posts/building-an-llm-pipeline-with-sagemaker/">previous post on building an LLM pipeline with SageMaker</a>, we covered training and deploying models. But there's a powerful technique that sits between raw data and user-facing insights: <strong>embeddings</strong>. They're the bridge that lets you take complex risk data or trained outputs and turn them into something genuinely useful for end users.</p>
<h2>What Are Embeddings?</h2>
<p>At their core, embeddings are dense vector representations of data — typically text, but they work for images, audio, and structured data too. Instead of representing a word or sentence as a sparse one-hot vector, embeddings map it into a continuous, high-dimensional space where semantic similarity becomes geometric proximity.</p>
<p>Think of it like this: if you embed the phrases &quot;high credit risk&quot; and &quot;elevated default probability,&quot; they'll end up close together in vector space. Meanwhile, &quot;low volatility&quot; will be far away. This property is what makes embeddings so powerful for analysis.</p>
<pre><code class="language-python"># Example: Two semantically similar phrases will have similar embeddings
embedding_1 = model.encode(&quot;high credit risk&quot;)
embedding_2 = model.encode(&quot;elevated default probability&quot;)
embedding_3 = model.encode(&quot;low volatility&quot;)

# Cosine similarity between 1 and 2 will be high
# Cosine similarity between 1 and 3 will be low
</code></pre>
<p>Modern LLMs generate these embeddings as part of their architecture. The same model you trained in SageMaker can produce them directly.</p>
<h2>How Embeddings Work</h2>
<p>When you pass text through an LLM, the model processes it through multiple transformer layers. Each layer refines the representation, capturing increasingly abstract patterns. The final hidden states — or sometimes a pooled version of them — become the embedding.</p>
<p>Here's a simplified view of the process:</p>
<ol>
<li><strong>Tokenization</strong>: Break text into tokens (subwords or characters)</li>
<li><strong>Token Embeddings</strong>: Map each token to a learned vector</li>
<li><strong>Positional Encoding</strong>: Add information about token position</li>
<li><strong>Transformer Layers</strong>: Process tokens through attention and feed-forward layers</li>
<li><strong>Pooling</strong>: Aggregate token representations into a single vector (mean, max, or CLS token)</li>
</ol>
<p>The result is a fixed-size vector (often 768 or 1024 dimensions for models like BERT, or up to 4096+ for larger models) that encodes the meaning of your input.</p>
<pre><code class="language-python">import torch
from transformers import AutoTokenizer, AutoModel

# Load a pre-trained model
tokenizer = AutoTokenizer.from_pretrained(&quot;sentence-transformers/all-MiniLM-L6-v2&quot;)
model = AutoModel.from_pretrained(&quot;sentence-transformers/all-MiniLM-L6-v2&quot;)

# Generate embedding
text = &quot;Customer exhibits high transaction volatility&quot;
inputs = tokenizer(text, return_tensors=&quot;pt&quot;, padding=True, truncation=True)

with torch.no_grad():
    outputs = model(**inputs)
    # Use mean pooling to get sentence embedding
    embedding = outputs.last_hidden_state.mean(dim=1)

print(embedding.shape)  # torch.Size([1, 384])
</code></pre>
<h2>Using Embeddings for Risk Data Analysis</h2>
<p>Now, let's connect this to something practical: analyzing risk data from your SageMaker pipeline. Suppose you've trained a model on financial transaction data, and you want to surface insights to users in a way that's intuitive and actionable.</p>
<h3>Problem: Making Sense of Thousands of Risk Signals</h3>
<p>You have thousands of risk events — fraud alerts, credit score changes, compliance flags. Users can't scroll through raw logs. They need a way to:</p>
<ul>
<li>Find similar incidents quickly</li>
<li>Cluster related risks</li>
<li>Ask questions in natural language</li>
<li>Get summarized, ranked results</li>
</ul>
<h3>Solution: Embed Everything</h3>
<p>Step 1: Generate embeddings for all your risk events.</p>
<pre><code class="language-python">import boto3
import json
from sentence_transformers import SentenceTransformer

# Initialize embedding model
embed_model = SentenceTransformer('all-MiniLM-L6-v2')

# Fetch risk data from S3 (output from your SageMaker pipeline)
s3 = boto3.client('s3')
response = s3.get_object(Bucket='my-bucket', Key='risk-events/latest.json')
risk_events = json.loads(response['Body'].read())

# Generate embeddings for each event description
embeddings = []
for event in risk_events:
    description = event['description']
    embedding = embed_model.encode(description)
    embeddings.append({
        'event_id': event['id'],
        'embedding': embedding.tolist(),
        'metadata': event
    })

# Store embeddings in a vector database or S3
</code></pre>
<p>Step 2: Build a semantic search layer.</p>
<p>When a user searches for &quot;suspicious wire transfers,&quot; you embed their query and find the closest risk events in vector space.</p>
<pre><code class="language-python">import numpy as np
from sklearn.metrics.pairwise import cosine_similarity

# User query
query = &quot;suspicious wire transfers&quot;
query_embedding = embed_model.encode(query)

# Compute similarity to all stored embeddings
event_embeddings = np.array([e['embedding'] for e in embeddings])
similarities = cosine_similarity([query_embedding], event_embeddings)[0]

# Get top 5 most similar events
top_indices = similarities.argsort()[-5:][::-1]
results = [embeddings[i]['metadata'] for i in top_indices]

for result in results:
    print(f&quot;Event: {result['description']} | Score: {similarities[top_indices[0]]:.3f}&quot;)
</code></pre>
<h3>Using a Vector Database</h3>
<p>For production, you'll want a proper vector database like <strong>Pinecone</strong>, <strong>Weaviate</strong>, or <strong>OpenSearch with k-NN</strong>. They handle indexing, approximate nearest neighbor search, and scaling.</p>
<pre><code class="language-python">from pinecone import Pinecone, ServerlessSpec

# Initialize Pinecone
pc = Pinecone(api_key=&quot;your-api-key&quot;)

# Create index
index = pc.Index(&quot;risk-embeddings&quot;)

# Upsert embeddings
vectors = [
    (e['event_id'], e['embedding'], e['metadata'])
    for e in embeddings
]
index.upsert(vectors=vectors)

# Query
query_embedding = embed_model.encode(&quot;high credit risk customers&quot;).tolist()
results = index.query(vector=query_embedding, top_k=10, include_metadata=True)

for match in results['matches']:
    print(f&quot;{match['metadata']['description']} | Score: {match['score']:.3f}&quot;)
</code></pre>
<h2>Building User-Friendly Outputs</h2>
<p>Once you have embeddings in place, you can build interfaces that feel intelligent:</p>
<h3>1. Semantic Search</h3>
<p>Users type natural language queries and get relevant risk events instantly, ranked by semantic similarity.</p>
<h3>2. Automatic Clustering</h3>
<p>Group similar risks together without manual tagging. Run k-means or HDBSCAN on your embeddings to discover patterns.</p>
<pre><code class="language-python">from sklearn.cluster import KMeans

# Cluster risk events
kmeans = KMeans(n_clusters=10)
clusters = kmeans.fit_predict(event_embeddings)

# Add cluster labels to metadata
for i, event in enumerate(embeddings):
    event['metadata']['cluster'] = int(clusters[i])
</code></pre>
<h3>3. Anomaly Detection</h3>
<p>Identify outlier events by measuring distance to cluster centroids or using isolation forests on embeddings.</p>
<h3>4. Conversational Queries</h3>
<p>Let users ask complex questions like &quot;Show me credit risks from Q4 that are similar to this flagged account.&quot; Embed the question, filter by metadata, and return results.</p>
<h2>Integrating with Your SageMaker Pipeline</h2>
<p>Here's how this fits into the pipeline we built before:</p>
<ol>
<li><strong>Training</strong> — Your SageMaker job produces a model that can generate embeddings</li>
<li><strong>Batch Transform</strong> — Run batch inference to embed your entire risk dataset</li>
<li><strong>Storage</strong> — Write embeddings to a vector database or S3</li>
<li><strong>Endpoint</strong> — Deploy a SageMaker endpoint that embeds user queries in real time</li>
<li><strong>Application</strong> — Your front-end calls the endpoint and searches the vector store</li>
</ol>
<pre><code class="language-python">import sagemaker

# Deploy embedding model as SageMaker endpoint
estimator = sagemaker.estimator.Estimator(
    image_uri=&quot;123456789012.dkr.ecr.us-east-1.amazonaws.com/embedding-model:latest&quot;,
    role=role,
    instance_count=1,
    instance_type=&quot;ml.m5.large&quot;
)

predictor = estimator.deploy(
    initial_instance_count=1,
    instance_type=&quot;ml.m5.xlarge&quot;
)

# Query the endpoint
import boto3
client = boto3.client(&quot;sagemaker-runtime&quot;)

response = client.invoke_endpoint(
    EndpointName=predictor.endpoint_name,
    ContentType=&quot;application/json&quot;,
    Body=json.dumps({&quot;text&quot;: &quot;high credit risk&quot;})
)

embedding = json.loads(response[&quot;Body&quot;].read())
</code></pre>
<h2>Real-World Example: Risk Dashboard</h2>
<p>Imagine a dashboard where analysts can:</p>
<ul>
<li>Type &quot;show me fraud cases similar to case #12345&quot;</li>
<li>See a ranked list of similar incidents</li>
<li>Click through to see full details</li>
<li>Get an AI-generated summary of common patterns</li>
</ul>
<p>Behind the scenes:</p>
<ol>
<li>The query gets embedded via your SageMaker endpoint</li>
<li>A vector database returns the top matches</li>
<li>Metadata filters narrow down results (date range, severity, etc.)</li>
<li>Another LLM call summarizes the findings</li>
</ol>
<p>The entire flow is powered by embeddings, and users interact with it like it's magic.</p>
<h2>Best Practices</h2>
<p><strong>1. Choose the Right Model</strong>
For domain-specific data (finance, healthcare), fine-tune an embedding model on your data. Generic models like <code>all-MiniLM-L6-v2</code> work well out of the box, but custom training improves relevance.</p>
<p><strong>2. Normalize and Store Metadata</strong>
Embeddings capture semantics, but you still need filters (date, category, user ID). Store metadata alongside embeddings.</p>
<p><strong>3. Monitor Drift</strong>
As your data evolves, re-embed periodically. Embeddings from old models can become stale.</p>
<p><strong>4. Hybrid Search</strong>
Combine semantic search (embeddings) with keyword search (BM25) for best results. Tools like OpenSearch support this natively.</p>
<h2>Conclusion</h2>
<p>Embeddings turn messy, high-dimensional data into something you can reason about geometrically. They're not just a trick for similarity search — they're a fundamental tool for building intelligent systems that meet users where they are.</p>
<p>If you've already got a SageMaker pipeline running, adding embeddings is a natural next step. You go from serving predictions to serving <strong>understanding</strong>. And that's where the real value is.</p>
<p>Next time a user asks, &quot;find risks like this one,&quot; you'll be ready.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Building an End-to-End LLM Pipeline with AWS SageMaker</title>
      <link>https://alencar.tech/posts/building-an-llm-pipeline-with-sagemaker/</link>
      <guid isPermaLink="true">https://alencar.tech/posts/building-an-llm-pipeline-with-sagemaker/</guid>
      <pubDate>Wed, 04 Feb 2026 00:00:00 GMT</pubDate>
      <description>How to train and deploy a large language model using SageMaker, Glue, and Athena — from raw data extraction all the way to live inference endpoints.</description>
      <content:encoded xmlns:content="http://purl.org/rss/1.0/modules/content/"><![CDATA[<p>I've been putting together a pipeline on AWS that takes raw data, trains a large language model, and serves it for inference — all without managing a single server. The setup spans a few services, but each piece has a clear job. Let me walk you through how it all fits together.</p>
<h2>The Pipeline at a Glance</h2>
<p>The data flows like this:</p>
<ol>
<li><strong>AWS Glue</strong> extracts raw data and writes it as Parquet to S3</li>
<li><strong>Amazon Athena</strong> queries and validates that data on demand</li>
<li><strong>SageMaker Training Jobs</strong> consume the data and train the LLM</li>
<li><strong>SageMaker Batch Transform</strong> runs batch inference for evaluation</li>
<li><strong>SageMaker Endpoints</strong> serve the trained model in production</li>
<li>Downstream services call those endpoints for real-time inference</li>
</ol>
<p>Each layer does one thing well. Let's go through them.</p>
<h2>Extracting Data with AWS Glue</h2>
<p>Glue is where the pipeline starts. A Glue Job pulls raw data from the source, applies transformations, and writes it out as Parquet files to an S3 bucket. Parquet is the right format here — it's columnar, compressed, and works seamlessly with everything downstream.</p>
<pre><code class="language-python">from awsglue.job import Job
from awsglue.context import GlueContext
from pyspark import SparkContext

sc = SparkContext()
glueContext = GlueContext(sc)
job = Job(glueContext)

raw_data = glueContext.create_dynamic_frame_from_catalog(
    database=&quot;my_database&quot;,
    table_name=&quot;raw_events&quot;
)

glueContext.write_dynamic_frame_from_options(
    frame=raw_data,
    connection_type=&quot;s3&quot;,
    connection_options={
        &quot;path&quot;: &quot;s3://my-bucket/training-data/&quot;,
        &quot;partitionKeys&quot;: [&quot;date&quot;]
    },
    format=&quot;parquet&quot;
)

job.commit()
</code></pre>
<p>The job runs on a schedule, so the S3 bucket stays fresh without any manual intervention.</p>
<h2>Querying with Amazon Athena</h2>
<p>Once the Parquet files are in S3, Athena lets me query them with plain SQL — no cluster to spin up, no provisioning. I use it for two things: validating that the Glue transformation produced the right data, and as a lightweight source for feeding into training.</p>
<pre><code class="language-sql">SELECT COUNT(*), MIN(date), MAX(date)
FROM training_data
WHERE date &gt;= '2025-06-01';
</code></pre>
<p>Athena query results can be exported back to S3, which keeps the handoff to SageMaker clean and decoupled.</p>
<h2>Training the LLM with SageMaker</h2>
<p>This is the core of it. SageMaker Training Jobs handle the actual model training — provisioning GPU instances, running distributed training, and saving checkpoints. I use a custom container image that has the training script and dependencies baked in, and point it at the Parquet data in S3.</p>
<pre><code class="language-python">import sagemaker

session = sagemaker.Session()
role = sagemaker.get_execution_role()

estimator = sagemaker.estimator.Estimator(
    image_uri=&quot;123456789012.dkr.ecr.us-east-1.amazonaws.com/llm-trainer:latest&quot;,
    role=role,
    instance_count=2,
    instance_type=&quot;ml.p3.16xlarge&quot;,
    output_path=&quot;s3://my-bucket/model-output/&quot;
)

estimator.fit({
    &quot;train&quot;: &quot;s3://my-bucket/training-data/&quot;,
    &quot;validation&quot;: &quot;s3://my-bucket/validation-data/&quot;
})
</code></pre>
<p>When the job finishes, the trained model artifacts end up in <code>output_path</code> on S3. SageMaker takes care of the rest — scaling, monitoring, and logging.</p>
<h2>Evaluating with Batch Transform</h2>
<p>Before anything goes to production, I run the model against a held-out evaluation set. <strong>Batch Transform</strong> is perfect for this — it runs inference over an entire dataset in one pass, no persistent endpoint needed.</p>
<pre><code class="language-python">transformer = estimator.transformer(
    instance_count=1,
    instance_type=&quot;ml.m5.4xlarge&quot;,
    output_path=&quot;s3://my-bucket/batch-output/&quot;
)

transformer.transform(
    data=&quot;s3://my-bucket/evaluation-data/&quot;,
    content_type=&quot;application/json&quot;,
    split_type=&quot;Line&quot;
)
transformer.wait()
</code></pre>
<p>The predictions land in S3. I pull them into a notebook or Athena to compute metrics and decide whether the model is ready to deploy.</p>
<h2>Deploying with SageMaker Endpoints</h2>
<p>Once the model passes evaluation, I deploy it as a real-time endpoint. This gives other services a stable, managed URL to call for inference.</p>
<pre><code class="language-python">predictor = estimator.deploy(
    initial_instance_count=1,
    instance_type=&quot;ml.m5.xlarge&quot;
)

print(predictor.endpoint_name)
# e.g. llm-trainer-2026-02-04-12-30-00-000
</code></pre>
<p>SageMaker handles auto-scaling, health checks, and traffic management under the hood. The endpoint just stays up and ready.</p>
<h2>Consuming the Endpoint</h2>
<p>Other services in the stack call the endpoint directly over HTTPS. From their perspective, it's just an API — they don't need to know anything about the training setup or the underlying infrastructure.</p>
<pre><code class="language-python">import boto3

client = boto3.client(&quot;sagemaker-runtime&quot;)

response = client.invoke_endpoint(
    EndpointName=&quot;llm-trainer-2026-02-04-12-30-00-000&quot;,
    ContentType=&quot;application/json&quot;,
    Body=b'{&quot;prompt&quot;: &quot;Summarize the following text: ...&quot;}'
)

result = response[&quot;Body&quot;].read().decode(&quot;utf-8&quot;)
</code></pre>
<p>That's it. The consuming service gets back the model's response and can do whatever it needs with it.</p>
<h2>Putting It All Together</h2>
<p>The full pipeline runs with minimal babysitting:</p>
<ul>
<li>Glue keeps the training data fresh in S3</li>
<li>Athena validates and explores it</li>
<li>SageMaker trains new model versions as the data evolves</li>
<li>Batch Transform gates what goes to production</li>
<li>Endpoints serve the live model</li>
<li>Downstream services consume it like any other API</li>
</ul>
<p>It's a clean way to go from raw data to a production LLM on AWS. Each layer is independently testable, and the whole thing scales without you touching a single server. Definitely worth exploring if you're looking to get ML workloads running on AWS.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Deploying to Netlify - A Beginner&#39;s Guide</title>
      <link>https://alencar.tech/posts/deploying-to-netlify/</link>
      <guid isPermaLink="true">https://alencar.tech/posts/deploying-to-netlify/</guid>
      <pubDate>Sat, 10 Feb 2024 00:00:00 GMT</pubDate>
      <description>Step-by-step guide to deploying your static site to Netlify for free.</description>
      <content:encoded xmlns:content="http://purl.org/rss/1.0/modules/content/"><![CDATA[<p>Netlify makes deploying static sites incredibly simple. Let me walk you through the process.</p>
<h2>What is Netlify?</h2>
<p>Netlify is a hosting platform designed specifically for static sites and Jamstack applications. It offers continuous deployment, global CDN, and many other features - all with a generous free tier.</p>
<h2>Prerequisites</h2>
<p>Before we begin, make sure you have:</p>
<ul>
<li>Your static site in a Git repository (GitHub, GitLab, or Bitbucket)</li>
<li>A Netlify account (it's free!)</li>
</ul>
<h2>Deployment Steps</h2>
<h3>1. Connect Your Repository</h3>
<p>Log into Netlify and click &quot;Add new site&quot; → &quot;Import an existing project&quot;. Connect your Git provider and select your repository.</p>
<h3>2. Configure Build Settings</h3>
<p>Netlify will often auto-detect your build settings, but you'll want to verify:</p>
<ul>
<li><strong>Build command</strong>: <code>npm run build</code> (or your specific build command)</li>
<li><strong>Publish directory</strong>: <code>_site</code> (or wherever your build outputs)</li>
</ul>
<h3>3. Deploy!</h3>
<p>Click &quot;Deploy site&quot; and watch the magic happen. Netlify will:</p>
<ol>
<li>Clone your repository</li>
<li>Install dependencies</li>
<li>Run your build command</li>
<li>Deploy the output to their global CDN</li>
</ol>
<h2>Continuous Deployment</h2>
<p>The best part? Every time you push to your main branch, Netlify automatically rebuilds and deploys your site. No manual intervention needed!</p>
<h2>Custom Domain</h2>
<p>Once deployed, you can:</p>
<ul>
<li>Use the free <code>.netlify.app</code> subdomain</li>
<li>Add your own custom domain</li>
<li>Get free SSL certificates automatically</li>
</ul>
<h2>Additional Features</h2>
<p>Netlify offers many powerful features:</p>
<ul>
<li><strong>Form handling</strong> - Collect form submissions without backend code</li>
<li><strong>Functions</strong> - Add serverless functions to your static site</li>
<li><strong>Split testing</strong> - A/B test different versions</li>
<li><strong>Deploy previews</strong> - Every PR gets its own preview URL</li>
</ul>
<h2>Tips for Success</h2>
<ol>
<li>Make sure your <code>.gitignore</code> excludes <code>node_modules</code> and build output</li>
<li>Set up environment variables in Netlify's dashboard if needed</li>
<li>Use <code>netlify.toml</code> for advanced configuration</li>
</ol>
<h2>Conclusion</h2>
<p>Netlify has made web deployment accessible to everyone. Within minutes, you can have your site live on a global CDN with HTTPS. Give it a try - you'll wonder how you ever deployed sites any other way!</p>
]]></content:encoded>
    </item>
    <item>
      <title>Why I Chose Eleventy for My Blog</title>
      <link>https://alencar.tech/posts/why-eleventy/</link>
      <guid isPermaLink="true">https://alencar.tech/posts/why-eleventy/</guid>
      <pubDate>Thu, 01 Feb 2024 00:00:00 GMT</pubDate>
      <description>My experience choosing and working with Eleventy as a static site generator.</description>
      <content:encoded xmlns:content="http://purl.org/rss/1.0/modules/content/"><![CDATA[<p>After researching various static site generators, I decided to go with Eleventy (11ty) for my blog. Here's why.</p>
<h2>Simplicity and Flexibility</h2>
<p>Eleventy doesn't force you into a specific template language or structure. You can use Markdown, Nunjucks, Liquid, JavaScript, or even mix and match. This flexibility was a big selling point for me.</p>
<h2>Zero Configuration to Start</h2>
<p>You can literally get started with just a markdown file. As your needs grow, you can add configuration, but you're not forced to deal with complex setup from day one.</p>
<pre><code class="language-javascript">// .eleventy.js - simple config example
module.exports = function(eleventyConfig) {
  eleventyConfig.addPassthroughCopy(&quot;css&quot;);
  return {
    dir: {
      input: &quot;src&quot;,
      output: &quot;_site&quot;
    }
  };
};
</code></pre>
<h2>Performance</h2>
<p>Eleventy is incredibly fast. Build times are measured in milliseconds for small to medium sites. This makes the development experience smooth and enjoyable.</p>
<h2>JavaScript Ecosystem</h2>
<p>Since Eleventy is built on Node.js, I can tap into the entire npm ecosystem for plugins and tools. This is a huge advantage if you're already familiar with JavaScript.</p>
<h2>Great Documentation</h2>
<p>The <a href="https://www.11ty.dev/docs/">Eleventy docs</a> are comprehensive and well-organized. There's also a helpful community and lots of starter projects to learn from.</p>
<h2>Perfect for Blogs</h2>
<p>With built-in support for collections, pagination, and data processing, Eleventy is particularly well-suited for blogs and content-heavy sites.</p>
<h2>Conclusion</h2>
<p>If you're looking for a static site generator that's powerful yet simple, I highly recommend giving Eleventy a try. It's been a pleasure to work with, and I'm excited to keep building with it.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Getting Started with Static Site Generators</title>
      <link>https://alencar.tech/posts/getting-started-with-ssg/</link>
      <guid isPermaLink="true">https://alencar.tech/posts/getting-started-with-ssg/</guid>
      <pubDate>Mon, 15 Jan 2024 00:00:00 GMT</pubDate>
      <description>An introduction to static site generators and why they&#39;re great for modern web development.</description>
      <content:encoded xmlns:content="http://purl.org/rss/1.0/modules/content/"><![CDATA[<p>Static site generators have revolutionized the way we build websites. In this post, we'll explore what they are and why you might want to use one for your next project.</p>
<h2>What is a Static Site Generator?</h2>
<p>A static site generator is a tool that generates a full static HTML website based on raw data and templates. Unlike dynamic content management systems, static sites are pre-built and served as-is to users.</p>
<h2>Benefits of Static Sites</h2>
<h3>Speed</h3>
<p>Static sites are incredibly fast because there's no database queries or server-side processing needed. The HTML is pre-generated and ready to serve.</p>
<h3>Security</h3>
<p>With no database or server-side code execution, there's a much smaller attack surface. Static sites are inherently more secure.</p>
<h3>Scalability</h3>
<p>Static files can be easily cached and distributed through CDNs, making them highly scalable without complex infrastructure.</p>
<h3>Cost-Effective</h3>
<p>Hosting static files is cheap. Services like Netlify, Vercel, and GitHub Pages even offer free hosting for static sites.</p>
<h2>Popular Static Site Generators</h2>
<p>Some popular options include:</p>
<ul>
<li><strong>Eleventy</strong> - Flexible and JavaScript-based</li>
<li><strong>Hugo</strong> - Extremely fast, written in Go</li>
<li><strong>Gatsby</strong> - React-based with a rich plugin ecosystem</li>
<li><strong>Next.js</strong> - Can generate static sites with React</li>
</ul>
<h2>Getting Started</h2>
<p>The best way to learn is by doing. Pick a static site generator that fits your tech stack and start building. Most have excellent documentation and starter templates to help you get up and running quickly.</p>
<p>Happy building!</p>
]]></content:encoded>
    </item>
  </channel>
</rss>
