# How this notebook is built

- Note: 1
- Author: Jack
- Published: 2026-10-30
- URL: https://undueattention.com/posts/how-this-site-works/

> The site is a static Astro build on S3 and CloudFront, with a Markdown twin of every note for agents and one small HTTP service that does semantic search and speaks MCP. This note follows a request through the system, explains why that service runs on Lambda Web Functions behind origin access control, and shows how one push to main deploys all of it.

## Two readers

Much of what reads this site will not be a person. Crawlers, coding agents and research tools fetch a page, strip the chrome, and try to recover the text. I would rather hand them the text. So every note exists twice: as the page you are reading, and as a plain Markdown file with the same content.[^twin] The rest of the design follows from taking that second reader seriously without making the first one pay for it.

[^twin]: Append `.md` to a note's path, or send `Accept: text/markdown` to the normal URL. [llms.txt](/llms.txt) lists every twin.

## The request path

*2.1 Static first.* The site is a static Astro build. Pages, Markdown twins, the feed and `/llms.txt` [1] are plain files in one S3 bucket behind CloudFront, and nothing renders on request. Table 1 lists every path and the origin that answers it.

| path | origin | serves |
| --- | --- | --- |
| `/`, `/posts/<slug>/` | S3 | HTML |
| `/posts/<slug>/`, Markdown accepted | S3 | the `.md` twin |
| `/posts/<slug>.md`, `/llms.txt` | S3 | Markdown for agents |
| `/api/search?q=` | Web Function | search results, JSON |
| `/mcp` | Web Function | MCP, Streamable HTTP |

*Table 1. Every route on the site and the origin behind it.*

*2.2 Negotiation at the edge.* A CloudFront Function runs on every viewer request to the static origin. When a request for a note asks for Markdown, it rewrites the path to the twin before the cache lookup, so the two representations are cached separately and neither needs a server. Listing 1 is the whole idea.

```js
function handler(event) {
  var req = event.request;
  var uri = req.uri;
  var accept = req.headers.accept ? req.headers.accept.value : '';

  // Agents asking for markdown get the post's .md twin
  if (uri.startsWith('/posts/') && accept.includes('text/markdown') && !uri.endsWith('.md')) {
    req.uri = uri.replace(/\/$/, '') + '.md';
    return req;
  }

  // Pretty URLs map to index.html; the S3 origin has no index documents
  if (uri.endsWith('/')) req.uri = uri + 'index.html';
  else if (!uri.includes('.')) req.uri = uri + '/index.html';
  return req;
}
```

*Listing 1. The viewer-request router, excerpted. Line 7 is the content negotiation.*

## One service, two doors

The dynamic part is small: a search endpoint for the page's own tools, and a remote MCP server [2] with two tools, `search_posts` and `get_post_markdown`. Both live in one Express app. CloudFront sends `/api/*` and `/mcp` to it, as Figure 1 shows.

*[Diagram: Request flow: a reader or agent reaches CloudFront. Static paths go to the S3 site bucket. /api and /mcp go, signed with origin access control, to a Lambda Web Function, which queries a Bedrock knowledge base stored in S3 Vectors.]*

*Figure 1. The request path. CloudFront answers static paths from S3 and signs everything bound for the Web Function, which is the only part that talks to the knowledge base.*

*3.1 Why Web Functions.* The app runs on Lambda Web Functions [3], which give an ordinary HTTP server requests directly, on Node.js 24 and Arm64, with no event-handler rewrite and no adapter in front of Express. What matters most here is that the endpoint can require IAM auth and sit behind CloudFront origin access control. CloudFront signs every request it forwards, and signs POST bodies as `UNSIGNED-PAYLOAD`,[^payload] so an MCP client sends an ordinary JSON-RPC POST and nothing more.

[^payload]: Behind origin access control, a Lambda function URL needs the client to send the body's SHA-256 in `x-amz-content-sha256`. MCP clients don't, which is what pushed earlier versions of this design to API Gateway.

That removed three things an earlier version of this design needed: an API Gateway in front of the function, the Lambda Web Adapter inside a container image, and a shared-secret header that CloudFront added and the app checked. The function's resource policy now names the distribution, and Lambda refuses any request that doesn't come through it before my code runs.

*3.2 Search.* Each deploy copies the Markdown twins to a second bucket and starts an ingestion job on a Bedrock knowledge base. It embeds chunks with Titan Text Embeddings V2 at 1024 dimensions and keeps them in S3 Vectors, which has no always-on minimum the way a search cluster does. Retrieval is purely semantic, so an exact library name can rank below a paraphrase of it.

## Deploys

Everything above is declared in OpenTofu: DNS, buckets, the distribution, the function, the knowledge base and every role. State is encrypted on the runner with a key from KMS before it is written anywhere. GitHub Actions reaches AWS through OIDC with two roles: a read-only one that plans pull requests and checks for drift every night, and an apply role that only jobs in the protected `production` environment can assume.

A push to `main` builds the service into a zip named by the git tree hash of `services/api`, applies the infrastructure, publishes the site, re-ingests the knowledge base, and runs smoke tests against the live domain. When only prose changes, the tree hash stays the same and the function is left alone.

**Remark 1.** *Whatever changes on every commit is an artifact; everything else is infrastructure. Where that line falls is the main design decision.*

## In the page

Pages also register three tools through WebMCP in browsers that support it: `search_posts`, `get_post_markdown` and `list_posts`. They call the same endpoints a remote agent would, so an agent working in the tab and one working over MCP see the same notebook.

## References

1. J. Howard. The /llms.txt file. *llmstxt.org*, 2024. <https://llmstxt.org/>
2. Model Context Protocol. Transports, Streamable HTTP. *Specification*, revision 2025-03-26. <https://modelcontextprotocol.io/specification/2025-03-26/basic/transports>
3. Announcing AWS Lambda Web Functions. *AWS News Blog*, October 2026. <https://aws.amazon.com/blogs/aws/announcing-aws-lambda-web-functions-for-fast-consistent-web-app-performance-anywhere/>
