# landonmiles.com - Full Content Export > Complete content export of landonmiles.com — the About page and every blog post in full. > Intended for AI/LLM indexing and retrieval. > Generated: 2026-04-25 ## Site Information - Author: Landon Miles - Role: Senior Technical Marketing Specialist at Automox - Location: Oklahoma City, OK, USA - Website: https://landonmiles.com - Email: hello@landonmiles.com - LinkedIn: https://www.linkedin.com/in/landonmiles - GitHub: https://github.com/jlmiles4 - Project: PDF Comments (https://pdfcomments.app) — browser-based PDF annotation extractor ## Summary Landon Miles is a technical marketer and developer. He writes for IT operations teams at Automox and publishes on technical marketing, AWS, AI tools, and command-line workflows. He also builds small side-project tools when he needs something the off-the-shelf options don't do well. --- ## About Page ### PHILOSOPHY I like problems more than I like any particular technology. Debugging a weird bug, figuring out how to explain something hard, picking the right tool for a job – that's the fun part. I'm a generalist by temperament. Python, JavaScript, MongoDB, PostgreSQL, React, Next.js – whatever fits. I don't have a favorite stack and I distrust people who do. The right answer to a problem is usually smaller than you think. A lot of what I build is a script, a Lambda, or a static page someone can load without thinking about it. ### WORK & PROJECTS **Technical Marketing:** Senior Technical Marketing Specialist at [Automox](https://www.automox.com). Most of my writing there is aimed at IT operations teams – the people who actually have to run the software. **Podcasting:** I host and produce the [Hands-On IT podcast](https://www.youtube.com/playlist?list=PLbBlhOg7rvKCTycMQraG0umaWB6GsqdKu). Practical topics, expert interviews, stuff that actually matters to operations teams. **Development:** A mix of things. Custom DAM systems, workflow scripts, the occasional side-project site when I have an itch to scratch. Outside of work I'm usually with my wife, dog, and kiddos. --- ## Content by Category ### Development - [PDFs Are Feedback Traps](https://landonmiles.com/blog/pdfcomments-app) - [Terminal Velocity: Accelerate Your Workflow with the Command Line](https://landonmiles.com/blog/terminal-velocity) - [Building Static Sites with AWS S3 and CloudFront](https://landonmiles.com/blog/static-sites-and-aws) ### Technical Marketing - [Avoiding Hype: How to Write Honestly About Complex Products](https://landonmiles.com/blog/avoiding-hype) - [The Key to Technical Marketing: Find Your Lego Level](https://landonmiles.com/blog/find-your-lego-level) --- ## Content by Tag ### aws - [Building Static Sites with AWS S3 and CloudFront](https://landonmiles.com/blog/static-sites-and-aws) ### cli - [Terminal Velocity: Accelerate Your Workflow with the Command Line](https://landonmiles.com/blog/terminal-velocity) ### communication - [Avoiding Hype: How to Write Honestly About Complex Products](https://landonmiles.com/blog/avoiding-hype) ### content-strategy - [The Key to Technical Marketing: Find Your Lego Level](https://landonmiles.com/blog/find-your-lego-level) ### deployment - [Building Static Sites with AWS S3 and CloudFront](https://landonmiles.com/blog/static-sites-and-aws) ### pdf - [PDFs Are Feedback Traps](https://landonmiles.com/blog/pdfcomments-app) ### product-marketing - [Avoiding Hype: How to Write Honestly About Complex Products](https://landonmiles.com/blog/avoiding-hype) - [The Key to Technical Marketing: Find Your Lego Level](https://landonmiles.com/blog/find-your-lego-level) ### productivity - [Terminal Velocity: Accelerate Your Workflow with the Command Line](https://landonmiles.com/blog/terminal-velocity) ### side-project - [PDFs Are Feedback Traps](https://landonmiles.com/blog/pdfcomments-app) ### static-sites - [Building Static Sites with AWS S3 and CloudFront](https://landonmiles.com/blog/static-sites-and-aws) ### technical-marketing - [Avoiding Hype: How to Write Honestly About Complex Products](https://landonmiles.com/blog/avoiding-hype) - [The Key to Technical Marketing: Find Your Lego Level](https://landonmiles.com/blog/find-your-lego-level) ### terminal - [Terminal Velocity: Accelerate Your Workflow with the Command Line](https://landonmiles.com/blog/terminal-velocity) ### tools - [PDFs Are Feedback Traps](https://landonmiles.com/blog/pdfcomments-app) ### workflow - [PDFs Are Feedback Traps](https://landonmiles.com/blog/pdfcomments-app) - [Terminal Velocity: Accelerate Your Workflow with the Command Line](https://landonmiles.com/blog/terminal-velocity) --- ## All Posts ## PDFs Are Feedback Traps - URL: https://landonmiles.com/blog/pdfcomments-app - Published: 2026-01-25 - Category: Development - Tags: tools, pdf, workflow, side-project ## The extraction problem You know the drill. You generate a PDF – an architectural diagram, a draft blog post, a technical spec – and you send it to stakeholders for review. They do what you asked and mark it up with comments. Generating the PDF is easy. Commenting on it is easy. Getting the feedback back out into something you can actually work from is the part that kills an afternoon. If you have 40+ comment bubbles, the workflow looks like: 1. Open the PDF on monitor one. 2. Open your ticket tracker or Markdown file on monitor two. 3. Click comment, copy, alt-tab, paste, alt-tab, back to the PDF. Repeat 39 more times. It's manual, it's error-prone, and you miss things. The feedback ends up trapped in a proprietary layer on top of the document, separate from anywhere you'd actually triage work. What I wanted wasn't a better PDF viewer. I wanted something that would take a marked-up PDF and give me back a list of tasks. --- ## Why Existing Tools Fail Before building, I looked for existing tools. The options were terrible. * **Adobe Acrobat:** It can export comments to FDF or XFDF, proprietary formats that no one actually wants to read. Exporting to Word or RTF results in a formatting nightmare. * **Online Converters:** Most "PDF to Text" tools ignore annotations entirely. The ones that don't usually require you to upload sensitive documents to a mysterious server. * **Python Scripts:** You can use `PyPDF2`, but it requires a dev environment and custom logic for every document structure. The gap was clear: drag, drop, copy, done – a utility that runs entirely in the browser. --- ## How it's built **Stack:** Next.js 14 (static export) :: TypeScript :: Tailwind :: pdfjs-dist. ### It all runs in the browser PDFs often contain sensitive data – contracts, internal memos, unreleased specs. Building this as a client-side app on top of `pdfjs-dist` means the file never leaves the browser. No server, no upload, no "please trust our processing endpoint." You could cut your wifi and the extraction would still work. That's also why it's fast. ### Reconstructing context from geometry This was the interesting part to build. When you highlight text in a PDF, the file doesn't store the words. It stores coordinates – "user drew a yellow rectangle at `[x, y]`." To get your data back, the tool has to perform a geometric intersection: 1. **Extract Geometry:** Get the quad points (corners) of every highlight. 2. **Map the Text:** Parse the page to get the bounding box of every text item. 3. **Intersect:** Run a collision detection loop. If a text item overlaps with the highlight, it belongs to that comment. 4. **Sort:** PDF text isn't always stored in reading order. The tool sorts items by Y then X coordinates to reconstruct the sentence naturally. The upshot is that what looked like a pile of coordinates on disk comes out the other side as readable sentences tied to the right page. --- ## Getting the output somewhere useful Two export paths: | Action | Use Case | | :--- | :--- | | **Copy Checklist** | Paste into Google Docs (formatted list) or GitHub/Notion (GFM checkboxes). | | **Copy / Download Markdown** | Deep work. Includes page numbers and full context for AI agents or your favorite markdown editor. | The new loop takes seconds: receive the PDF, drop it into **pdfcomments.app**, and paste the checklist into your Google Doc. It saves about 15 minutes of mindless copying per document. More importantly, it ensures every comment becomes a tickable box. --- ## Try It **[pdfcomments.app](https://pdfcomments.app)** I built this on a snowy Saturday in about the runtime of the new Tron movie. It works for my use case – your mileage may vary. Free and private. [View on GitHub](https://github.com/jlmiles4/pdfcomments.app). --- ## Terminal Velocity: Accelerate Your Workflow with the Command Line - URL: https://landonmiles.com/blog/terminal-velocity - Published: 2025-12-05 - Category: Development - Tags: terminal, cli, workflow, productivity ## Why bother with the terminal You can build a long career without ever opening a terminal. Plenty of people do. But at some point – usually when you're SSH'd into a server at 11pm or trying to script something a GUI will never do – the command line stops being optional. That's the argument for learning it early instead of cornered into it. The terminal isn't a faster way to do what you already do with a mouse. It's the interface you'll meet the rest of the computing world through: servers, containers, CI runners, cloud instances, old Unix tools that still run the internet. Every one of them expects you to talk to it in text. Most of what follows is the small amount of context and the small set of commands that I think are worth learning first, in roughly the order I'd introduce them to someone. --- ## A quick word on terminology People use "terminal" to mean three different things, which makes the whole topic more confusing than it needs to be. Originally, a terminal was a piece of hardware. A screen and a keyboard wired into a mainframe somewhere else in the building, with no processing power of its own. When personal computers took over, we kept the text-based interface and moved it into software. Today the pieces work like this: - **Terminal emulator.** The app you open (iTerm2, Alacritty, Windows Terminal, Ghostty, the default Terminal on macOS). It draws the window and handles input. - **Shell.** The program running inside that window (Bash, Zsh, Fish) that reads what you type, interprets it, and talks to the operating system. You can mix and match. Your prompt customizations, aliases, and most of the "look and feel" you associate with the terminal actually live in the shell, not the emulator. This distinction matters the first time you want to move your setup to a new machine. --- ## What you get out of it A few things that GUIs can't really match: ### The skill travels VS Code's menus move around between versions. Windows Settings looks nothing like macOS System Settings. Meanwhile `ls`, `cd`, `grep`, and `ssh` have worked the same way for decades and run on everything from your laptop to a Raspberry Pi to an EC2 instance. Time spent learning them compounds in a way that time spent memorizing a particular UI does not. ### Pipes change what's possible The pipe operator (`|`) takes the output of one command and hands it to the next. That one idea is what turns a small set of commands into a general-purpose toolkit. For example, finding every text file that contains "error" and moving it into a debug folder: ```bash grep -l "error" *.txt | xargs -I {} mv {} ./debug/ ``` In a GUI that's a manual click-and-drag exercise. Here it's one line, and you can stick it in a script and forget about it. ### Servers don't have monitors If you end up anywhere near DevOps, backend, or cloud infrastructure, you'll spend real time in a shell. You can't RDP into a Lambda. You can't right-click a running container on a remote host. The terminal is where that work happens, and the sooner you're comfortable there, the less painful the first outage is. ### You learn how the computer actually works Using the terminal tends to push you into the model the OS is already using underneath. You stop guessing why a file won't save and start thinking about permissions (`chmod`, `chown`). You stop force-quitting from a menu and start thinking about processes and signals (`ps`, `kill`). You stop checking wifi bars and start reaching for `ping`, `curl`, and `dig`. None of this is strictly necessary to get work done – until it is. --- ## How to actually get comfortable The hardest part of learning the terminal is the first couple of weeks, when it's slower than what you already know. Some things that helped me: ### Replace one workflow at a time Pick something you do with a mouse and commit to doing it in the terminal for a week. - **Git.** Close the Source Control tab. `git status`, `git add -p`, `git commit`, `git log --oneline` will cover most of what you need. - **File navigation.** Skip Finder/Explorer for anything inside a project. `cd`, `ls`, `tree`. - **Reading logs.** Stop opening log files in a text editor. `tail -f`, `less`, `grep`. Full replacement isn't the goal. Just build the reflex to try the terminal first for one kind of task. ### Don't try to memorize man pages You don't need to learn every flag to every command. You need the daily drivers and the confidence to look up the rest when you need them. The core set is small. ### Make the environment yours The default terminal on most systems is ugly and unhelpful. A few changes make a real difference: - A modern shell (Zsh with something like Oh My Zsh, or Fish). - A prompt that tells you the current directory, git branch, and whether the last command failed. - A readable monospace font (I like Geist Mono and JetBrains Mono). - Two or three aliases for things you type all day – mine include `gs` for `git status` and `..` for `cd ..`. You don't need a 400-line dotfiles repo. Start small and add things as they become obviously useful. --- ## About platforms Almost every server you'll ever touch runs Linux. SSH into an EC2 instance, a DigitalOcean droplet, a Pi, a friend's home server – you land in a shell. The commands below work natively on Linux and macOS, which is Unix under the hood. On Windows, they work inside WSL or Git Bash. If you're on Windows and planning to do any backend or cloud work, get WSL installed before you go any further. The experience is dramatically better than PowerShell for this kind of work, and everything in the rest of this post will apply. --- ## The cheat sheet The commands that come up again and again, roughly grouped by what they do. ### Navigation | Command | Description | Example | | :------ | :---------- | :------ | | `pwd` | Print working directory | `pwd` | | `ls` | List directory contents | `ls -l`, `ls -a` | | `cd` | Change directory | `cd ~`, `cd ..` | ### Files and directories | Command | Description | Example | | :------ | :---------- | :------ | | `mkdir` | Make a directory | `mkdir new_project` | | `touch` | Create an empty file / update its timestamp | `touch new_file.txt` | | `cp` | Copy files or directories | `cp file.txt backup/` | | `mv` | Move or rename files | `mv old.txt new.txt` | | `rm` | Remove files | `rm file.txt`, `rm -rf dir` (be careful) | | `cat` | Dump a file to stdout | `cat log.txt` | | `less` | Read a file interactively | `less large_log.txt` | | `head` | First N lines | `head -n 10 file.txt` | | `tail` | Last N lines, optionally following | `tail -f log.txt` | ### Text and search | Command | Description | Example | | :------ | :---------- | :------ | | `grep` | Search for patterns in files | `grep "ERROR" app.log` | | `find` | Search for files | `find . -name "*.js"` | | `sort` | Sort lines | `cat names.txt | sort` | | `uniq` | Collapse adjacent duplicate lines | `sort list.txt | uniq` | ### System and network | Command | Description | Example | | :------ | :---------- | :------ | | `ps` | Snapshot of running processes | `ps aux` | | `top` | Live process monitor | `top` | | `htop` | Interactive process viewer | `htop` | | `kill` | Send a signal to a process | `kill 12345` | | `ping` | Basic connectivity check | `ping google.com` | | `curl` | Make HTTP requests / download files | `curl -O https://site.com/file.zip` | | `ssh` | Secure shell to a remote host | `ssh user@server.com` | --- Everything in that table is older than most of the software you use daily, and none of it is going anywhere. Start with the five or six commands you'd use today. The rest sticks as soon as you have a reason to reach for it. --- ## Avoiding Hype: How to Write Honestly About Complex Products - URL: https://landonmiles.com/blog/avoiding-hype - Published: 2025-12-02 - Category: Technical Marketing - Tags: technical-marketing, product-marketing, communication ## Distrust is the default "Next-generation, AI-powered, enterprise-grade platform for synergy." If you work in tech, you've read some version of that sentence a thousand times. And if you're like most technical buyers, you stopped reading before you got to "synergy." When a product description is that dense with superlatives, it usually tells you one thing about the company writing it: they can't explain what the product actually does. The pressure in marketing is always to go bigger. Founders want disruption. Sales wants a silver bullet. But when your buyer is an engineer, a developer, or an IT admin, hype doesn't just fall flat – it costs you. They spend their days finding edge cases and breaking things on purpose. When you lead with "revolutionary," they read that as "something we can't back up." What works instead is boring: specifics, evidence, and plain description. --- ## Spotting the patterns Hype tends to hide in a few places. Once you start looking, you see it everywhere – including, sometimes, in your own drafts. - **Vague superlatives.** "Best-in-class," "unparalleled," "revolutionary." They take up space without adding information. - **Buzzword stacking.** "AI-driven blockchain synergy." It sounds expensive. It means nothing. - **Cherry-picked metrics.** "10x faster" – great, on what hardware, against what baseline, running what workload? ### What engineers actually hear Whenever I catch myself reaching for a stock marketing phrase, I try to imagine how it lands on a skeptical reader. This is roughly the translation layer running in their head: | **The Claim** | **What the Engineer Hears** | | :--- | :--- | | "Seamless integration" | "We haven't finished the API docs." | | "Zero-configuration" | "Good luck customizing this when it breaks." | | "Single pane of glass" | "An iframe that loads four dashboards slowly." | | "Unlimited scale" | "We haven't tested this past 1,000 users." | None of that is fair, exactly. But it's what technical buyers have learned to expect, because they've been burned before. --- ## Why it backfires Engineers verify claims. That's the job. If you promise "instant deployment" and it takes four hours to configure IAM, you haven't just annoyed a user. You've given them a reason to doubt everything else you've written, including the parts that are true. Once someone mentally files your content under "marketing fluff," it's hard to get out of that folder. Hype also attracts the wrong prospects. People who bought the dream churn when they meet the software. And every overpromise eventually shows up as a support ticket – the kind that starts with "your site said…" Writing honestly narrows the funnel, and that's usually what you want. You don't need every prospect. You need the ones who are going to succeed with the product. --- ## How to write it instead Honest copy doesn't have to be dry. It just means trading adjectives for specifics. ### Lead with the problem Don't open with "our AI tool." Open with "manually parsing 10,000 log lines is miserable." When you describe the user's pain accurately, they trust that you understand the solution. ### Anchor benefits to context "Fast" is a word. "Processes 1GB of data in 400ms on a t3.medium" is a fact. If you don't have the number yet, go find it before you publish. ### Say what it doesn't do This is the hardest one to get past leadership, and the single most effective thing for buyers. "Works best for teams managing fewer than 500 endpoints." "Supports AWS and Azure; GCP is on the roadmap." Admitting the edges of your product is the fastest way to get a technical reader to believe the center. ### Show your work Link the methodology. Publish the benchmark script. Offer a sandbox. If the product is good, the best marketing asset you have is the product itself running in front of someone. --- ## When the pressure comes You will get asked to jazz it up. Someone will want "more punch" on a draft that is fine the way it is. The reframe that works for me: when a founder asks for more punch, offer more proof. Replace "revolutionary" with a customer quote about a measurable outcome. Replace "blazing fast" with a benchmark chart. The emotional register goes up, but the content stays verifiable. It also helps to point at competitors who already do this well. Stripe, Vercel, Tailscale, Fly – their marketing reads more like documentation than advertising, and it works. Not because dry is good for its own sake, but because their audience reads it and thinks, "okay, these people understand what I do." And when a feature genuinely isn't there yet, don't fake it. Label it as roadmap. Engineers understand development cycles. What they don't forgive is being lied to. --- ## A few things I do before I ship - **Highlight every adjective.** For each one, ask whether a number or a concrete noun would do more work. Delete or replace the ones that don't survive. - **Get an engineer to read it.** Not for typos – for plausibility. "Would you believe this if a competitor wrote it?" is a surprisingly useful question. - **Keep a banned-word list.** Mine has "seamless," "revolutionary," and "synergy" on it by default. Yours can be different. The point is that you've decided in advance what you won't say. If a claim can't survive "prove it," cut it or rewrite it. What's left is usually shorter, more specific, and more useful to the people you actually want as customers. --- ## The Key to Technical Marketing: Find Your Lego Level - URL: https://landonmiles.com/blog/find-your-lego-level - Published: 2025-11-30 - Category: Technical Marketing - Tags: technical-marketing, content-strategy, product-marketing ## The gap you're trying to close There's a tension built into any technical product. R&D and marketing are good at different things, and both are necessary. Engineering produces the product; marketing is how anyone finds out it exists and why they should care. But the two groups naturally operate from different instincts. Engineers reach for architecture, internals, and precision. Marketers reach for clarity, reach, and narrative. Those instincts don't line up on their own. Deep technical explanations overwhelm most buyers. High-level pitches leave technical buyers suspicious. And in the middle is a gap between what the product is and how customers understand what it does for them. Closing that gap is roughly what technical marketing is for. Your job is to translate technical depth into explanations of how the product solves a real customer problem, without oversimplifying to the point where an engineer in the room rolls their eyes. The frame I find useful for doing that is what I call the Lego Level. --- ## What I mean by the Lego Level Think about how Lego sells their sets. The box doesn't show a pile of plastic bricks. It shows a race car, a spaceship, a castle. The pieces are present, but the thing you're being sold is what you can build with them. That split is the part worth stealing. A good Lego Level for a piece of content sits at the altitude where the reader can see what they're building and which pieces matter, without getting buried in how the pieces were manufactured. Too high and it's a marketing brochure. Too low and it's an architecture doc no buyer will finish. Three things help me find the altitude. ### Start from the outcome Lego instructions are organized around the finished model, not around the properties of the individual bricks. Features alone don't mean anything to a buyer; they mean something when they're attached to a problem the buyer recognizes. Before writing, pin down what "castle" you're showing. Pick a real scenario with a realistic environment and a clear before-and-after. Then work backwards from there. When you open with the outcome, everything you describe afterward has somewhere to land. ### Skip the injection-molding details When you sit down to build a Lego set, you don't want a lecture on the chemistry of ABS plastic or the calibration of the molding machines. Those details are true. They are also useless for the job at hand. The same thing is true of products. Stability, reasonable performance, working auth, basic compatibility – these are table stakes. If they're present, users barely notice them. Where technical marketing tends to get stuck is explaining architecture, dependency choices, or internal complexity that matters enormously to the team building the product and not at all to the buyer using it. My editing test for this: for every paragraph, ask whether the reader needs it to snap the next piece together. If not, cut it – or move it to a reference doc where someone who actually wants that detail can find it. Architecture posts are great; just don't disguise them as solution content. ### Match the builder Two kinds of buyers show up, and they want very different things. Some are building the 9,000-piece Death Star. The product is big, the use case is complicated, and if you hand them a bag of parts and wish them luck they'll close the tab. These readers want a sequenced blueprint – the screens, commands, and config they need to get from box to finished build, without being re-taught the fundamentals they already know. Others just want to play. They don't want a walkthrough. They want to know what pieces exist, how those pieces interconnect, and a handful of examples of what's possible. For them the Lego Level looks like a well-organized parts map: clean API docs, schemas, small recipes. Then you get out of their way. Most products need both kinds of content. Problems start when you try to serve one reader with content built for the other. --- ## Turning it into a habit Nothing about this is a one-time exercise. A few things I try to do every time: - **Decide on the finished build before writing.** A real problem, a realistic environment, a clear outcome. If I can't name those, I'm not ready to write yet. - **Use the customer's language for the pieces.** Internal feature names and project codenames creep into drafts. Replace them with whatever a new user would call the thing. - **Show how pieces connect.** Diagrams, short walkthroughs, and code samples that mirror a real workflow beat isolated feature descriptions almost every time. - **Move internals out of solution content.** If a paragraph is really about how something is built rather than how to use it, it belongs in a reference or architecture post. - **Write for more than one altitude.** Step-by-step guides for the first group; quickstart repos, API references, and diagrams for the second. The short version: stop describing the bricks. Start showing what gets built with them. --- ## Building Static Sites with AWS S3 and CloudFront - URL: https://landonmiles.com/blog/static-sites-and-aws - Published: 2025-11-29 - Category: Development - Tags: aws, static-sites, deployment ## You already know how to build. Let's talk about deploying it right. If you're a developer, running your own static site is one of the easiest ways to get something online while staying in your comfort zone – building, writing, and shipping. The harder part is deploying it somewhere fast, reliable, and cheap, without signing up to manage a server. There are plenty of hosted options (Squarespace, Wix, GitHub Pages), but I keep coming back to AWS S3 paired with CloudFront. It looks intimidating the first time, and it really isn't once you've done it once. You get a globally distributed site for a few dollars a month, full control over how it's served, and transferable AWS experience that comes in handy the next time you need to host anything. The tradeoff is about an hour of first-time setup. This guide walks through that hour end-to-end, regardless of which static site generator you're using. --- ## Why Static Sites – and Why CloudFront + S3? Static sites pair extremely well with AWS because the entire workflow is based on one simple output: a folder of static files. No matter which generator you use, the end result is the same – HTML, CSS, JavaScript, and assets that can be dropped directly into S3 and served globally through CloudFront. Most tools output this in a predictable directory: - Next.js (static export) → `out/` - Hugo → `public/` - Astro → `dist/` - Jekyll → `_site/` - Eleventy → `_site/` - Angular (static build) → `dist/` AWS does not know or care which tool produced them. The hosting process is completely **generator agnostic**. ### Advantages of static sites - **Fast page loads**: low latency and global caching. - **Minimal infrastructure**: no servers, databases, or runtime environment to manage. - **Low hosting cost**: pay for storage and bandwidth, which is cheap for personal sites. - **Simple, predictable deployments**: a single `sync` and `invalidate` command. - **Ideal for version control**: the entire site is deployable from your git repository. - **No backend to maintain**: less security patching and fewer sleepless nights. Static sites are perfect for blogs, documentation, personal sites, marketing pages, and lightweight product pages. Most importantly, static sites keep you focused on the code and content rather than backend infrastructure. You write your pages, run a build, and deploy a folder. That is the entire workflow. Your energy stays where it belongs – designing, writing, and building. Some generators emphasize content (Hugo, Jekyll, Eleventy). Others emphasize components or application structure (Next.js, Astro, Angular). But the AWS deployment workflow stays the same – build locally, upload to S3, and let CloudFront handle global delivery. --- ## Getting Started with AWS The first steps are all about security and setup. We'll harden your AWS account before we even touch S3. --- ### Step 1: Build Your Site Locally Start by building your site using whatever tool you are most comfortable with. All modern static site generators produce a directory of output files that can go straight into S3. Keep your workflow straightforward: - Write your content - Build locally - Verify everything looks right - Prepare the output folder for deployment Once your site builds cleanly, you are ready for the AWS part. --- ### Step 2: Sign Up for AWS and Create a Secure SSO Admin If you do not already have an AWS account, create one and sign in. This first login uses your root account, which has unrestricted access to everything in AWS. #### 1. Harden the Root Account The root account is too powerful for day-to-day work, and using it regularly increases the risk of accidental or malicious changes. - Store the root email address and password in a reputable password manager (e.g., 1Password). - Enable **MFA** on the root account (a hardware security key or an authenticator app is preferred). - Save any recovery codes or MFA backup methods in a secure place. Treat the root account as break-glass only. You'll need it if something goes badly wrong, but you shouldn't use it day to day. Everything that follows runs from the SSO admin user we're about to create. #### 2. Change the Default Region Before creating anything, set your default region. Choose whatever is physically closest to you – as long as it is **not** `us-east-1`. Friends do not let friends deploy in us-east-1. That region absorbs a huge share of AWS traffic and is known for occasional quirks. From a reliability and operational-sanity standpoint, you are usually better off in a less congested region. There are a few reasons to deploy there, but for a personal site, just choose something else. #### 3. Enable IAM Identity Center (SSO) AWS best practice is to avoid using the root account for everyday tasks – IAM Identity Center will become your primary login method going forward. - In the AWS console, use the search bar at the top to find **IAM Identity Center**. - Open it and enable it if prompted. #### 4. Create Permission Sets - In IAM Identity Center, go to **Permission sets**. - Choose **Predefined permission sets** and add **`AdministratorAccess`**. - Create a second permission set using the predefined **`Billing`** permissions. If you're a solo developer, combining an administrative role with a separate billing-permission set is usually the simplest option. In a larger team, you'll want to create more granular roles so not everyone has full admin or billing access. #### 5. Create Your SSO User - Still in IAM Identity Center, click **Users** then **Add user**. - Enter a username and an email address. (You can reuse the same email as the root account or use a different one – the root login remains separate). - Assign the new user the **AdministratorAccess** permission set. - Optionally (and recommended), assign the **Billing** permission set to this user as well so you can manage billing without using the root account. - Complete the setup and verify the email to activate your SSO user. #### 6. Save Your SSO Start URL and Stop Using Root - After setup, IAM Identity Center will display a **user portal / start URL**. Save this URL in your password manager and bookmarks. - Sign out of the root account. - Sign back in using the new SSO user you just created – this will be your primary way to access AWS from now on. --- ### Step 3: Create Your S3 Bucket Once you're logged in to your SSO admin account, search for **S3** in the AWS console. Click **Create bucket** and give it a name. This bucket will store your static build files. Most defaults are correct, but double-check these: - **Block all public access**: keep this **enabled** - **Server-side encryption**: choose **SSE-S3** - **Bucket key**: enable it - **Object lock**: disable it Static sites should always be served privately through CloudFront, not directly from S3. After the bucket is created, open it and use **Create folder** to organize the files for your site. This is optional but helps keep things tidy if you ever host multiple projects. --- ### Step 4: Create Your CloudFront Distribution CloudFront is the global CDN layer that makes your site fast everywhere, not just near the S3 region. Search for **CloudFront** in the AWS console. Click **Create distribution** and configure: - **Pricing class**: the free tier is fine, but pay-as-you-go is generally inexpensive. For most personal blogs, **pay-as-you-go ends up cheaper than AWS’s monthly pricing packages**. - **Name / description**: anything you want - **Application type**: **Single-page web app** is fine for almost all static sites Click **Next** when prompted for a domain name. We will add a custom domain later. #### Configure Your Origin - **Origin type:** Amazon S3 - **Bucket:** select your S3 bucket using **Browse** - **Origin path:** your folder name, starting with `/your-folder` (if you created one in S3) - **Access:** enable **Allow private S3 bucket access to CloudFront** - This step automatically creates an Origin Access Control (OAC) and applies the required bucket policy so **only CloudFront** can read from your bucket. Use the recommended origin and cache settings – they are tuned for static assets. #### Security Settings Enable: - **Security protections** - **Layer 7 DDoS attack protection** (optional – this adds $30/month to your bill and is more relevant for large distributed applications than personal blogs. You can leave this unchecked.) These give you AWS Shield-level protection by default. Click **Next**, review the settings, and **Create distribution**. CloudFront will take a few minutes to deploy. --- ### Step 5: Set Default Root Object and Enable Bot Protection Open your new CloudFront distribution. #### Set the Default Root Object Under General, click Edit. Scroll to Default root object and enter: `index.html` This ensures that hitting `/` loads your homepage correctly. #### Bot Protection Click the **Security** tab and enable Bot Protection. CloudFront groups automated traffic into categories. A strong starting point is: | **Bot Category** | **Recommended Action** | **Why** | | ---------------------------------- | ---------------------- | ----------------------------------------------------------------------------------------------------------- | | **Unverified bot action** | **Block** | High-risk automation with no legitimate purpose. | | **Signal: Bot data center** | **Block** | Known bot-heavy networks that frequently generate abusive traffic. | | **Signal: Automated browser** | **CAPTCHA** | Often scraping or scripted browsing – challenge to reduce noise without blocking legitimate tools. | | **Signal: Non-browser user agent** | **CAPTCHA** | Typically automation or scraping tools impersonating browsers. | Everything else can stay in **Monitor** mode. Watch your traffic and tighten categories individually when needed. We will enable logging, custom error pages, and additional hardening after the site is live. --- ### Step 6: Set Up the AWS CLI and Deploy Your Build We’re going to use the AWS CLI because it’s universal, scriptable, and works with every site generator. #### Install the AWS CLI - **macOS:** `brew install awscli` - **Linux:** `sudo apt install awscli` (or use your distro’s package manager) - **Windows:** Start with installing Linux. But if you’re sticking with Windows, download and run the official AWS CLI MSI installer. #### Configure SSO in the CLI Once it is installed, open your terminal and run: `aws configure sso` You will see prompts like these: | **Prompt** | **Explanation** | | --------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------- | | SSO session name (Recommended) | Put in a name for this account. This one doesn't matter too much if you only are logging in to this AWS instance. | | SSO start URL | This is the URL you got when you set up your SSO user. Typically something like `https://d-long-string.awsapps.com/start` | | SSO region | The region you chose in step 2. | | SSO registration scopes [sso:account:access]: | The default value is correct, so push enter. | Press enter, and your web browser will open and authenticate your session. You'll be asked a couple more questions: | **Prompt** | **Explanation** | | ------------------------- | --------------------------------------------------------------------------------------------------------------------- | | CLI default client Region | I typically just use the same region as above. | | CLI default output format | `None` is fine, just push enter. | | CLI profile name | If this is the only AWS account you'll be working in, type `default` and it makes the login commands a little easier. | #### Important CLI Commands Your SSO session will expire periodically. When it does, run the below commands before deploying or running other AWS CLI commands. - If you named the profile `default`: `aws sso login` - To log in and refresh your SSO session for a given profile: `aws sso login --profile your-profile-name` #### Deploying Your Build #### 1. Authenticate Make sure you are authenticated: `aws sso login` (If you used a non-default profile, add `--profile your-profile-name`.) #### 2. Push to S3 From the directory where your build folder lives, run the **sync** command. This command is fast because it only uploads new or modified files and removes files from S3 that are no longer in your local folder (due to the `--delete` flag): `aws s3 sync ./out s3://your-bucket/your-folder --delete` Adjust the folder name (`./out`) to match your generator's output. If you want to get fancy, you can also create a wrapper around this command – for example, `pnpm deploy:s3`. #### 3. Invalidate CloudFront Invalidating CloudFront is how you tell it to look for new files. Invalidation forces CloudFront to refresh its global cache so users see the latest version of your site. It doesn't delete anything, it just tells CloudFront to go check for new files in your S3 bucket. Here is the command to invalidate everything: ```bash title="Invalidate CloudFront Cache" aws cloudfront create-invalidation \ --distribution-id YOUR_DISTRIBUTION_ID \ --paths "/*" ``` Replace `YOUR_DISTRIBUTION_ID` with the ID from your CloudFront distribution page. It can be found at the top of the page or in the URL. --- ### Step 7: Test Your Site CloudFront will take a few minutes to propagate globally. Open the built-in CloudFront domain (e.g.: `abcdefg1234567.cloudfront.net`) and verify that your pages render correctly. If you type `/about` and it loads `/about.html`, then your static generator already handles folder-style indexing. If not, you will want **pretty URLs**. --- ### Step 8: Add a CloudFront Function for Pretty URLs Static sites often generate files like: `/about.html` and `/contact.html` But most people expect: `/about` and `/contact` S3 does not automatically map `/about` to `/about.html`. CloudFront can do this for you using something called a **CloudFront Function**. #### What is a CloudFront Function? CloudFront Functions are small pieces of JavaScript that run on CloudFront’s global edge network _before_ the request reaches your S3 bucket. They can: - Rewrite URLs - Add or remove headers - Redirect pages - Block automated traffic They run extremely fast, cost almost nothing, and do not require any backend infrastructure. They are ideal for simple logic like URL rewriting. For heavier logic or accessing the response body, AWS also provides Lambda@Edge, but you won’t need that for static sites. #### How to Create the Pretty URL Rewrite Function #### 1. Open CloudFront Functions In the AWS console search bar, type CloudFront and open it. From the left menu, select Functions. #### 2. Create the Function - Click **Create function** - Name it something descriptive like `pretty-urls` - Choose **CloudFront Function** - Click **Create function** #### 3. Add the Rewrite Logic Paste this code into the editor: ```javascript title="CloudFront Function - Pretty URLs" function handler(event) { var request = event.request; var uri = request.uri; // If the request already includes a file extension, leave it alone if (uri.includes('.')) { return request; } // If it ends with a slash, CloudFront will automatically try index.html if (uri.endsWith('/')) { return request; } // Otherwise append .html so /about becomes /about.html request.uri = uri + '.html'; return request; } ``` Click **Save**, then **Publish**. #### Attach the Function to Your Distribution 1. Open your CloudFront distribution 2. Go to the **Behaviors** tab 3. Edit the default behavior 4. Scroll to **Function associations** 5. Under **Viewer Request**, select your `pretty-urls` function 6. Save the changes Once deployed, CloudFront will rewrite clean URLs automatically. If a user visits: `https://your-site.cloudfront.net/about` CloudFront will serve: `/about.html` Your users never see the rewrite – they just get the clean URL. #### How Static Generators Differ Some generators output nested index pages: `/about/index.html` and `/blog/index.html`. CloudFront automatically maps `/about` and `/about/` to the correct file. Others output flat files: `/about.html` and `/blog.html`. For these, the CloudFront Function handles the rewrite. Either way, CloudFront ensures your URLs look clean and modern. --- ### Step 9: Turn On Logging Once everything works, enable CloudFront logging. To stay privacy-friendly: - Anonymize IPs - Avoid logging cookies - Store logs in a dedicated S3 bucket Logging costs pennies and gives you insight into performance and traffic patterns. --- ### Step 10: Add Your Domain Buy a domain (Route 53, Cloudflare, Namecheap, etc.), add it to your CloudFront distribution, and issue a certificate through **AWS Certificate Manager**. Certificates are free and auto-renew when used with CloudFront. Update your DNS records to point your domain to CloudFront. --- ### Step 11: Deploy Changes and Invalidate the Cache To update your site: 1. Rebuild locally 2. Upload new files to S3 3. Invalidate CloudFront’s cache Invalidation forces CloudFront to refresh its global cache so users see the latest version of your site. Here is the command to invalidate everything: ```bash aws cloudfront create-invalidation \ --distribution-id YOUR_DISTRIBUTION_ID \ --paths "/*" ``` Replace `YOUR_DISTRIBUTION_ID` with the ID from your CloudFront distribution page. --- ## Quick Reference: Commands You’ll Use Regularly A compact list of core commands you'll run as part of your normal workflow. You can also create pnpm or other wrapper commands around these to make your life easier. ```bash title="AWS SSO Login" aws sso login ``` ```bash title="Upload to S3" aws s3 sync ./dist s3://your-bucket/your-folder --delete ``` ```bash title="Invalidate CloudFront Cache" aws cloudfront create-invalidation \ --distribution-id YOUR_DISTRIBUTION_ID \ --paths "/*" ``` ```bash title="List CloudFront Distributions" aws cloudfront list-distributions ``` **List objects in your S3 deployment folder:** ```bash title="List S3 Bucket Files"" aws s3 ls s3://your-bucket/your-folder/ ``` Having these in one place makes updating your site as simple as rebuilding, syncing, and invalidating. --- ## Wrapping up Once this is in place, deploying a change is three commands: build, sync, invalidate. There's no server to patch, no runtime to monitor, and the whole setup tends to run a few dollars a month for a personal site. That's most of the appeal. Things you can layer on later, as you need them: - Analytics via CloudFront logs, or something privacy-friendly like Plausible or Fathom - Forms through Formspree, or a small Lambda if you'd rather keep it in-house - WAF rules or more elaborate CloudFront Functions for stricter security - A CI/CD pipeline (GitHub Actions works cleanly) so pushes to `main` deploy themselves The same pattern scales from a personal blog to documentation sites, marketing pages, and fairly involved Next.js apps. You end up with a setup you can reason about end-to-end, which is worth something. ---