<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: ByteStay</title>
    <description>The latest articles on DEV Community by ByteStay (@bytestay).</description>
    <link>https://dev.to/bytestay</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4208806%2F121c0d9a-a5d5-4a52-b6b8-7c6475cb1cbf.png</url>
      <title>DEV Community: ByteStay</title>
      <link>https://dev.to/bytestay</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/bytestay"/>
    <language>en</language>
    <item>
      <title>Lint your Dockerfile and K8s manifests against Pod Security Standards in one call</title>
      <dc:creator>ByteStay</dc:creator>
      <pubDate>Tue, 03 Nov 2026 14:00:05 +0000</pubDate>
      <link>https://dev.to/bytestay/lint-your-dockerfile-and-k8s-manifests-against-pod-security-standards-in-one-call-29lh</link>
      <guid>https://dev.to/bytestay/lint-your-dockerfile-and-k8s-manifests-against-pod-security-standards-in-one-call-29lh</guid>
      <description>&lt;p&gt;&lt;em&gt;Disclosure: we're ByteStay and we built Shipshape, the API in this post. There's a free plan and a no-signup demo, so you can check everything below yourself.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Take the nginx Deployment from the Kubernetes docs (&lt;a href="https://k8s.io/examples/application/deployment.yaml" rel="noopener noreferrer"&gt;&lt;code&gt;deployment.yaml&lt;/code&gt;&lt;/a&gt;), the one most of us copied at some point. Run it through Shipshape and it gets a &lt;strong&gt;D, 42/100&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;13 findings&lt;/strong&gt; in total: 5 medium, 6 low and 2 info.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;4 of the mediums are Pod Security Standards "restricted" checks:&lt;/strong&gt; capabilities not dropped, privilege escalation allowed, runAsNonRoot not set, no seccomp profile.&lt;/li&gt;
&lt;li&gt;The fifth medium is the &lt;strong&gt;missing memory limit&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That's not a criticism of the docs: the example is there to show a Deployment, not a hardened one. But it's what a lot of real manifests look like.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Shipshape checks
&lt;/h2&gt;

&lt;p&gt;You POST a Dockerfile or one or more Kubernetes manifests (YAML or JSON, multi-document is fine). You get back findings, a score and a pass/fail flag.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;59 rules:&lt;/strong&gt; 26 for Dockerfiles and 33 for Kubernetes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Kubernetes:&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;Pod Security Standards, both baseline and restricted.&lt;/li&gt;
&lt;li&gt;Resources and probes.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;:latest&lt;/code&gt; images.&lt;/li&gt;
&lt;li&gt;Service-account token automount.&lt;/li&gt;
&lt;li&gt;Secrets in env vars.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dockerfiles:&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;Root in the final stage, following &lt;code&gt;FROM &amp;lt;stage&amp;gt;&lt;/code&gt; inheritance.&lt;/li&gt;
&lt;li&gt;Unpinned base images.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;curl | sh&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;apt/apk/pip cache hygiene.&lt;/li&gt;
&lt;li&gt;Secrets in ENV/ARG.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ADD&lt;/code&gt; vs &lt;code&gt;COPY&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Shell-form &lt;code&gt;CMD&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Every finding&lt;/strong&gt; has a line number, the object and container it applies to, a fix, and the standard behind it: PSS, the NSA/CISA Kubernetes Hardening Guide, the CIS Docker Benchmark, or the hadolint id.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One finding from the nginx example, as the production API returns it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"K8S-009"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"severity"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"medium"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Capabilities do not drop ALL"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"line"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"object"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Deployment/nginx-deployment"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"container"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"nginx"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"fix"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Add `securityContext.capabilities.drop: [&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;ALL&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;]` and add back only NET_BIND_SERVICE if needed."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"reference"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"PSS Restricted: Capabilities; NSA/CISA hardening"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"pss"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"restricted"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  From D to A
&lt;/h2&gt;

&lt;p&gt;Here's the same Deployment after working through the findings. The image is the unprivileged nginx build, so it listens on 8080 and runs as non-root.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;apps/v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Deployment&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;nginx-deployment&lt;/span&gt;
  &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;web&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;selector&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;matchLabels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;nginx&lt;/span&gt;
  &lt;span class="na"&gt;replicas&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;2&lt;/span&gt;
  &lt;span class="na"&gt;template&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;labels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;nginx&lt;/span&gt;
    &lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;automountServiceAccountToken&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
      &lt;span class="na"&gt;securityContext&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;runAsNonRoot&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
        &lt;span class="na"&gt;seccompProfile&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;RuntimeDefault&lt;/span&gt;
      &lt;span class="na"&gt;containers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;nginx&lt;/span&gt;
        &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;nginxinc/nginx-unprivileged:1.27.2&lt;/span&gt;
        &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;containerPort&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;8080&lt;/span&gt;
        &lt;span class="na"&gt;securityContext&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;allowPrivilegeEscalation&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
          &lt;span class="na"&gt;readOnlyRootFilesystem&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
          &lt;span class="na"&gt;capabilities&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;drop&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ALL"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
        &lt;span class="na"&gt;resources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;requests&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;cpu&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;100m&lt;/span&gt;
            &lt;span class="na"&gt;memory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;64Mi&lt;/span&gt;
          &lt;span class="na"&gt;limits&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;memory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;128Mi&lt;/span&gt;
        &lt;span class="na"&gt;livenessProbe&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;httpGet&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;/&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;port&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;8080&lt;/span&gt;&lt;span class="pi"&gt;}&lt;/span&gt;
        &lt;span class="na"&gt;readinessProbe&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;httpGet&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;/&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;port&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;8080&lt;/span&gt;&lt;span class="pi"&gt;}&lt;/span&gt;
        &lt;span class="na"&gt;volumeMounts&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;tmp&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;mountPath&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;/tmp&lt;/span&gt;&lt;span class="pi"&gt;}&lt;/span&gt;
      &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;tmp&lt;/span&gt;
        &lt;span class="na"&gt;emptyDir&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Result: &lt;strong&gt;A, 100/100&lt;/strong&gt;. One info finding is left, "No CPU limit", and it's info on purpose: CPU limits cause throttling, and plenty of teams choose not to set them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Grade and &lt;code&gt;passed&lt;/code&gt; are different things
&lt;/h2&gt;

&lt;p&gt;The score is overall hygiene: 100, minus 20 per high, 8 per medium and 3 per low (info is free). &lt;code&gt;passed&lt;/code&gt; is your CI gate: it's false when any finding is at or above &lt;code&gt;fail_on&lt;/code&gt;, which is &lt;code&gt;high&lt;/code&gt; by default.&lt;/p&gt;

&lt;p&gt;So they can disagree, and that's deliberate:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The nginx Deployment:&lt;/strong&gt; D, but &lt;code&gt;passed: true&lt;/code&gt;. Nothing in it is high, so it doesn't break the build by default.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Docker's own &lt;a href="https://github.com/docker/welcome-to-docker" rel="noopener noreferrer"&gt;welcome-to-docker&lt;/a&gt; Dockerfile:&lt;/strong&gt; B (80/100), but &lt;code&gt;passed: false&lt;/code&gt;. It has one high finding: the final stage never sets &lt;code&gt;USER&lt;/code&gt;, so the container runs as root.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Adding &lt;code&gt;USER node&lt;/code&gt; before &lt;code&gt;CMD&lt;/code&gt; takes it to &lt;strong&gt;A, 100/100&lt;/strong&gt; and &lt;code&gt;passed: true&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  On every pull request: the GitHub Action
&lt;/h2&gt;

&lt;p&gt;We published an open-source &lt;a href="https://github.com/marketplace/actions/bytestay-shipshape-dockerfile-kubernetes-lint" rel="noopener noreferrer"&gt;GitHub Action&lt;/a&gt; for this. With your RapidAPI key stored as the secret &lt;code&gt;RAPIDAPI_KEY&lt;/code&gt;, it's one step:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;shipshape&lt;/span&gt;
&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pull_request&lt;/span&gt;
&lt;span class="na"&gt;permissions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;contents&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;read&lt;/span&gt;
  &lt;span class="na"&gt;pull-requests&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;read&lt;/span&gt;
&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;lint&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v7&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Bytestay/shipshape-action@v1&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;rapidapi-key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.RAPIDAPI_KEY }}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What it does on each pull request:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It lints only the Dockerfiles and manifests the pull request changed. Helm templates and YAML that isn't Kubernetes are skipped locally, so a pull request usually costs 1 to 3 requests.&lt;/li&gt;
&lt;li&gt;Findings show up as annotations on the diff: &lt;code&gt;high&lt;/code&gt; as errors, &lt;code&gt;medium&lt;/code&gt; as warnings, each with the fix.&lt;/li&gt;
&lt;li&gt;The job summary has a table per file with the grade, the score and every finding.&lt;/li&gt;
&lt;li&gt;The step fails on &lt;code&gt;high&lt;/code&gt; by default (&lt;code&gt;fail-on&lt;/code&gt;), or when a file grades worse than &lt;code&gt;min-grade&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;If the API is down or your quota runs out, you get a warning. Your build is never failed because of us.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The source is on &lt;a href="https://github.com/Bytestay/shipshape-action" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; (MIT).&lt;/p&gt;

&lt;p&gt;The Action takes a RapidAPI key for now. Support for api.market keys is planned, but it isn't there yet.&lt;/p&gt;

&lt;h3&gt;
  
  
  Other CI systems
&lt;/h3&gt;

&lt;p&gt;Prefer no third-party action, or using GitLab CI, Jenkins or a pre-commit hook? You only need &lt;code&gt;curl&lt;/code&gt; and &lt;code&gt;jq&lt;/code&gt;, one request per file. Here it is as a GitHub Actions step; the same commands work anywhere:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Shipshape&lt;/span&gt;
  &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;RAPIDAPI_KEY&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.RAPIDAPI_KEY }}&lt;/span&gt;
  &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
    &lt;span class="s"&gt;jq -Rs '{content: ., fail_on: "high"}' Dockerfile |&lt;/span&gt;
      &lt;span class="s"&gt;curl -sS -X POST https://shipshape-dockerfile-and-kubernetes-lint-api.p.rapidapi.com/v1/dockerfile \&lt;/span&gt;
        &lt;span class="s"&gt;-H "X-RapidAPI-Key: $RAPIDAPI_KEY" \&lt;/span&gt;
        &lt;span class="s"&gt;-H "X-RapidAPI-Host: shipshape-dockerfile-and-kubernetes-lint-api.p.rapidapi.com" \&lt;/span&gt;
        &lt;span class="s"&gt;-H "Content-Type: application/json" --data-binary @- &amp;gt; shipshape.json&lt;/span&gt;
    &lt;span class="s"&gt;jq -r '.findings[] | "::\(if .severity == "high" then "error" elif .severity == "medium" then "warning" else "notice" end) file=Dockerfile,line=\(.line // 1)::\(.id) \(.title)"' shipshape.json&lt;/span&gt;
    &lt;span class="s"&gt;jq -e '.passed' shipshape.json &amp;gt; /dev/null&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The last line makes the step fail when &lt;code&gt;passed&lt;/code&gt; is false. For manifests, use &lt;code&gt;/v1/kubernetes&lt;/code&gt;, or use &lt;code&gt;/v1/lint&lt;/code&gt;, which detects the type. To skip a rule you've decided to accept, add &lt;code&gt;"ignore": ["DF-017"]&lt;/code&gt; to the body.&lt;/p&gt;

&lt;h2&gt;
  
  
  In your coding agent: the MCP server
&lt;/h2&gt;

&lt;p&gt;If an AI coding agent writes your Dockerfiles and manifests, it can run the same check before handing them over.&lt;/p&gt;

&lt;p&gt;The open-source &lt;a href="https://github.com/Bytestay/shipshape-mcp" rel="noopener noreferrer"&gt;Shipshape MCP server&lt;/a&gt; runs on your machine and uses your own key, from RapidAPI or api.market:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"mcpServers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"shipshape"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"uvx"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"args"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"--from"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"bytestay-shipshape-mcp"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"shipshape-mcp"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"env"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"RAPIDAPI_KEY"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"your-rapidapi-key"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Its &lt;code&gt;lint&lt;/code&gt; tool takes the file's content or a local path, and returns the grade and every finding with its fix.&lt;/li&gt;
&lt;li&gt;With an api.market key, set &lt;code&gt;API_MARKET_KEY&lt;/code&gt; instead of &lt;code&gt;RAPIDAPI_KEY&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;It's listed in the official MCP Registry as &lt;code&gt;io.github.Bytestay/shipshape-mcp&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Each tool call is one request on your plan.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Limits
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Helm templates aren't parsed.&lt;/strong&gt; Render them first (&lt;code&gt;helm template … | …&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cross-object checks are out of scope.&lt;/strong&gt; NetworkPolicies, PodDisruptionBudgets and RBAC aren't checked.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Base images aren't inspected.&lt;/strong&gt; If a third-party base image sets &lt;code&gt;USER&lt;/code&gt; itself, we can't see it, and the finding says so.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Line numbers are best effort:&lt;/strong&gt; the instruction's first line in Dockerfiles, the key's line in YAML.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Privacy
&lt;/h2&gt;

&lt;p&gt;The file is parsed in memory and nothing is stored or fetched. Manifests and Dockerfiles are never logged.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No signup:&lt;/strong&gt; the demo on &lt;a href="https://shipshape.bytestay.pt/#try" rel="noopener noreferrer"&gt;shipshape.bytestay.pt&lt;/a&gt; runs the real API in your browser. Paste a Dockerfile or a manifest.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Free plan:&lt;/strong&gt; on RapidAPI, Basic is $0 for 15 requests a month, with a hard limit, so it never runs up a bill. Paid plans start at $9 for 10,000 requests.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Docs and plans:&lt;/strong&gt; &lt;a href="https://rapidapi.com/bytestay-bytestay-default/api/shipshape-dockerfile-and-kubernetes-lint-api" rel="noopener noreferrer"&gt;RapidAPI listing&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Also on api.market:&lt;/strong&gt; a 7-day free trial, once per customer. &lt;a href="https://api.market/store/bytestay/shipshape" rel="noopener noreferrer"&gt;Shipshape on api.market&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If a rule fires where it shouldn't, tell us. False positives are the bugs we fix first.&lt;/p&gt;

</description>
      <category>kubernetes</category>
      <category>docker</category>
      <category>devops</category>
      <category>security</category>
    </item>
    <item>
      <title>Google-Extended isn't a crawler. Why our fetcher honours it anyway</title>
      <dc:creator>ByteStay</dc:creator>
      <pubDate>Wed, 28 Oct 2026 15:10:33 +0000</pubDate>
      <link>https://dev.to/bytestay/google-extended-isnt-a-crawler-why-our-fetcher-honours-it-anyway-5ai7</link>
      <guid>https://dev.to/bytestay/google-extended-isnt-a-crawler-why-our-fetcher-honours-it-anyway-5ai7</guid>
      <description>&lt;p&gt;&lt;em&gt;Disclosure: we're ByteStay and we built Distill, the URL-to-Markdown API in this post. There's a free plan and a no-signup demo, so you can check everything below yourself.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Google-Extended&lt;/code&gt; shows up in robots.txt like any other user agent:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User-agent: Google-Extended
Disallow: /
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But no bot ever sends it. Googlebot keeps fetching the page as before. The token only tells Google whether the content may be used for Gemini. &lt;code&gt;Applebot-Extended&lt;/code&gt; does the same for Apple.&lt;/p&gt;

&lt;p&gt;So a fetcher that announces itself as &lt;code&gt;DistillBot&lt;/code&gt; could skip that group and be technically right: it isn't Google-Extended. We don't skip it. Here's why, with numbers.&lt;/p&gt;

&lt;h2&gt;
  
  
  What 37 robots.txt files say
&lt;/h2&gt;

&lt;p&gt;On 27 October 2026 we downloaded the robots.txt of 37 big sites:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;international news: BBC, The New York Times, The Guardian, the FT, Reuters, AP, The Verge and others;&lt;/li&gt;
&lt;li&gt;Portuguese news: Público, Expresso, Observador, JN, DN, RTP, SAPO;&lt;/li&gt;
&lt;li&gt;platforms: GitHub, Medium, Substack, Wikipedia.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We ran each one through the same parser Distill uses in production and asked about the home page (&lt;code&gt;/&lt;/code&gt;):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;36 of 37&lt;/strong&gt; let &lt;code&gt;User-agent: *&lt;/code&gt; in.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;29 of those 36&lt;/strong&gt; refuse at least one AI crawler by name.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;7&lt;/strong&gt; name no AI crawler at all: Wikipedia, Forbes, Substack, The Independent, The Telegraph, DN and RTP.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;How often each name is refused:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;token&lt;/th&gt;
&lt;th&gt;sites refusing it&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Bytespider&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;27&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;CCBot&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;26&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ClaudeBot&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;25&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Applebot-Extended&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;25&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Google-Extended&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;18&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;GPTBot&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;16&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The second bullet is the one that matters. A scraper that reads only the &lt;code&gt;*&lt;/code&gt; group would fetch 29 home pages whose owners said no to AI crawlers by name.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Distill decides
&lt;/h2&gt;

&lt;p&gt;The parser follows RFC 9309: the longest matching rule wins, &lt;code&gt;*&lt;/code&gt; and &lt;code&gt;$&lt;/code&gt; wildcards work, and groups for the same agent are merged. Then:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;If there's a &lt;code&gt;DistillBot&lt;/code&gt; group, it decides alone.&lt;/strong&gt; That's the site owner talking to us directly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Otherwise the path must be allowed by &lt;code&gt;*&lt;/code&gt; and by every group for a known AI crawler.&lt;/strong&gt; There are 15 of them:

&lt;ul&gt;
&lt;li&gt;OpenAI: &lt;code&gt;GPTBot&lt;/code&gt;, &lt;code&gt;ChatGPT-User&lt;/code&gt;, &lt;code&gt;OAI-SearchBot&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Anthropic: &lt;code&gt;ClaudeBot&lt;/code&gt;, &lt;code&gt;Claude-User&lt;/code&gt;, &lt;code&gt;Claude-SearchBot&lt;/code&gt;, &lt;code&gt;anthropic-ai&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Google: &lt;code&gt;Google-Extended&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Apple: &lt;code&gt;Applebot-Extended&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Perplexity: &lt;code&gt;PerplexityBot&lt;/code&gt;, &lt;code&gt;Perplexity-User&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Meta: &lt;code&gt;Meta-ExternalAgent&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Common Crawl: &lt;code&gt;CCBot&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Cohere: &lt;code&gt;cohere-ai&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;ByteDance: &lt;code&gt;Bytespider&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This is the core of the production code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;own&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;groups&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;settings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;BOT_TOKEN&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;own&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;allowed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;own&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="n"&gt;settings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;BOT_TOKEN&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;token&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;*&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;settings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AI_OPT_OUT_TOKENS&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;rules&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;groups&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;rules&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="nf"&gt;allowed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rules&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;token&lt;/span&gt;
&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And this is what a user gets back for the BBC's news page, from the production API:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://www.bbc.co.uk/news"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"robots_txt"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"detail"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"robots.txt disallows this path for GPTBot"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"markdown"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"took_ms"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;146&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Across the sample, Distill reads 7 of the 37 home pages and refuses 30: the 29 above, plus Reuters, whose &lt;code&gt;*&lt;/code&gt; group blocks &lt;code&gt;/&lt;/code&gt;. That's the product working as intended.&lt;/p&gt;

&lt;h2&gt;
  
  
  So why Google-Extended?
&lt;/h2&gt;

&lt;p&gt;The honest finding first: in our sample, Google-Extended never decided anything on its own. All 18 sites that refuse it also refuse other AI crawlers by name. Taking it off our list would change none of these 37 answers today.&lt;/p&gt;

&lt;p&gt;We keep it anyway:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;It's the clearest statement of intent there is.&lt;/strong&gt; A site that writes &lt;code&gt;User-agent: Google-Extended&lt;/code&gt; / &lt;code&gt;Disallow: /&lt;/code&gt; is saying "index me, but don't feed me to an LLM". Distill fetches pages only to feed them to LLMs. That's exactly the use they refused.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The name of the bot is an accident.&lt;/strong&gt; Usage tokens exist because Google and Apple don't run a separate AI crawler. We go by what the page will be used for, not by who knocks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Owners can still let us in.&lt;/strong&gt; A &lt;code&gt;DistillBot&lt;/code&gt; group overrides everything else, so a site that blocks AI in general but is happy for Distill users to read its docs can say exactly that:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User-agent: DistillBot
Allow: /docs/
Disallow: /

User-agent: *
Disallow: /
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What being strict costs
&lt;/h2&gt;

&lt;p&gt;Some refused pages are ones the user may well have the right to read. When that happens:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The response says why: &lt;code&gt;status: "robots_txt"&lt;/code&gt; and, in &lt;code&gt;detail&lt;/code&gt;, the group that refused.&lt;/li&gt;
&lt;li&gt;We don't retry another way. Our Terms forbid fetching through other means a page that refused DistillBot.&lt;/li&gt;
&lt;li&gt;The same goes for &lt;code&gt;noai&lt;/code&gt; in a &lt;code&gt;&amp;lt;meta name="robots"&amp;gt;&lt;/code&gt; tag or an &lt;code&gt;X-Robots-Tag&lt;/code&gt; header, even when the user sends us the HTML directly.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A few more rules from the same RFC:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;robots.txt is checked on every host, including redirect targets, and cached for up to an hour.&lt;/li&gt;
&lt;li&gt;If it answers 5xx or 429, we treat everything as disallowed and check again after 5 minutes.&lt;/li&gt;
&lt;li&gt;A missing robots.txt (404) means no rules.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The fine print on these numbers
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;It's the home page only.&lt;/strong&gt; Article paths can have different rules, so a site can refuse &lt;code&gt;/&lt;/code&gt; and allow &lt;code&gt;/articles/&lt;/code&gt;, or the other way round.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It's 37 big publishers and platforms, not the web.&lt;/strong&gt; We didn't measure smaller sites, and they may look very different.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;robots.txt files change.&lt;/strong&gt; These are the files as served on 27 October 2026.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;For site owners:&lt;/strong&gt; &lt;a href="https://distill.bytestay.pt/bot.html#robots-txt" rel="noopener noreferrer"&gt;how DistillBot reads robots.txt&lt;/a&gt;, and how to opt out or in.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No signup:&lt;/strong&gt; the demo on &lt;a href="https://distill.bytestay.pt/#try" rel="noopener noreferrer"&gt;distill.bytestay.pt&lt;/a&gt; runs the real API in your browser. Try a site you know and see what its robots.txt says.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Free plan:&lt;/strong&gt; Basic is $0 for 15 requests a month, with a hard limit, so it never runs up a bill. Paid plans start at $9 for 10,000 requests.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Docs and plans:&lt;/strong&gt; &lt;a href="https://rapidapi.com/bytestay-bytestay-default/api/distill-html-to-markdown-api" rel="noopener noreferrer"&gt;RapidAPI listing&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you run a site and think we read your robots.txt wrong, tell us. That's a bug we want to fix first.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>ai</category>
      <category>llm</category>
      <category>webscraping</category>
    </item>
    <item>
      <title>Turn any web page into clean Markdown for your LLM, without ignoring robots.txt</title>
      <dc:creator>ByteStay</dc:creator>
      <pubDate>Tue, 27 Oct 2026 12:16:35 +0000</pubDate>
      <link>https://dev.to/bytestay/turn-any-web-page-into-clean-markdown-for-your-llm-without-ignoring-robotstxt-4pgm</link>
      <guid>https://dev.to/bytestay/turn-any-web-page-into-clean-markdown-for-your-llm-without-ignoring-robotstxt-4pgm</guid>
      <description>&lt;p&gt;&lt;em&gt;Disclosure: we're ByteStay and we built Distill, the API in this post. There's a free plan and a no-signup demo, so you can check everything below yourself.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;If you feed web pages to an LLM, most of what you send isn't content. Navigation, footers, cookie banners, inline scripts, tracking pixels and sidebars all cost tokens and add noise to your embeddings.&lt;/p&gt;

&lt;p&gt;A real example: the Python tutorial page on &lt;a href="https://docs.python.org/3/tutorial/datastructures.html" rel="noopener noreferrer"&gt;Data Structures&lt;/a&gt; is &lt;strong&gt;111,008 bytes&lt;/strong&gt; of HTML. The main content as Markdown is &lt;strong&gt;28,453 characters&lt;/strong&gt;, about &lt;strong&gt;7,100 tokens&lt;/strong&gt;, and keeps the headings, the code blocks and the 31 links.&lt;/p&gt;

&lt;p&gt;That's roughly a quarter of the size, and none of what's gone is anything your model needed.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Distill does
&lt;/h2&gt;

&lt;p&gt;You send a URL, or HTML you already have. You get back:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the page's main content as &lt;strong&gt;Markdown&lt;/strong&gt;: headings, lists, GFM tables, fenced code blocks with their language;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;metadata&lt;/strong&gt;: title, author, published date, language and site name;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;counts&lt;/strong&gt;: words, characters and an approximate token count;&lt;/li&gt;
&lt;li&gt;the &lt;strong&gt;links&lt;/strong&gt; list, with relative URLs already made absolute.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here's the GitHub Blog's "What is Git?" post, as returned by the production API today (trimmed):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ok"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"What is Git? Our beginner’s guide to version control"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"author"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Kedasha Kerr"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"published_date"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2024-05-27"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"language"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"en-US"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"site_name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"The GitHub Blog"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"markdown"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"What is Git? Our beginner’s guide to version control&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s2"&gt;Let’s get you started on your Git journey…"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"truncated"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"took_ms"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1825&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Quick start
&lt;/h2&gt;

&lt;p&gt;Through RapidAPI, with your key:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-G&lt;/span&gt; &lt;span class="s2"&gt;"https://distill-html-to-markdown-api.p.rapidapi.com/v1/markdown"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--data-urlencode&lt;/span&gt; &lt;span class="s2"&gt;"url=https://en.wikipedia.org/wiki/Markdown"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nv"&gt;include_links&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;false&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nv"&gt;max_chars&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;4000 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"X-RapidAPI-Key: YOUR_RAPIDAPI_KEY"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"X-RapidAPI-Host: distill-html-to-markdown-api.p.rapidapi.com"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The options you'll actually use:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;max_chars&lt;/code&gt; cuts at a paragraph or word boundary, closes any open code fence and sets &lt;code&gt;truncated: true&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;include_links&lt;/code&gt; (default on) and &lt;code&gt;include_images&lt;/code&gt; (default off);&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;include_tables&lt;/code&gt; (default on) keeps tables as GFM.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Already have the HTML, for example from your own crawler or a headless browser? &lt;code&gt;POST&lt;/code&gt; it with a &lt;code&gt;base_url&lt;/code&gt;, and the relative links come back resolved.&lt;/p&gt;

&lt;h2&gt;
  
  
  A small RAG ingest loop in Node.js
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://distill-html-to-markdown-api.p.rapidapi.com/v1/markdown&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;search&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URLSearchParams&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://docs.python.org/3/library/json.html&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;include_links&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;false&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;X-RapidAPI-Key&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;RAPIDAPI_KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;X-RapidAPI-Host&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;distill-html-to-markdown-api.p.rapidapi.com&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Distill &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// 401, 422, 429…&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ok&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;warn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;detail&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// robots_txt, blocked…&lt;/span&gt;
&lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="nx"&gt;chunks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(...&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;markdown&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;## &lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;final_url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt; &lt;span class="p"&gt;})));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Statuses, not exceptions
&lt;/h2&gt;

&lt;p&gt;A page you can't use isn't an error in your code: it's an answer. Distill always returns HTTP 200 with a &lt;code&gt;status&lt;/code&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;status&lt;/th&gt;
&lt;th&gt;what happened&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ok&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Markdown extracted&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;no_content&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;the page loaded, but there's no main content (often a JavaScript-built page)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;robots_txt&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;robots.txt disallows the path&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;opted_out&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;the page says &lt;code&gt;noai&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;blocked&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;the site's bot protection said no&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;not_html&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;PDF, image, JSON…&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;http_error&lt;/code&gt; / &lt;code&gt;unreachable&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;the site errored or couldn't be reached&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;HTTP errors are kept for your mistakes (422 for a bad URL) and for limits (429).&lt;/p&gt;

&lt;h2&gt;
  
  
  What we refuse to do
&lt;/h2&gt;

&lt;p&gt;This is the part we care about most. Scraping for AI has a bad reputation, mostly earned.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;robots.txt is checked on every redirect hop&lt;/strong&gt;, and if it answers 5xx we treat the path as disallowed (RFC 9309).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AI-crawler groups are honoured too.&lt;/strong&gt; If a site blocks &lt;code&gt;GPTBot&lt;/code&gt;, &lt;code&gt;ClaudeBot&lt;/code&gt;, &lt;code&gt;CCBot&lt;/code&gt;, &lt;code&gt;Google-Extended&lt;/code&gt; or a dozen others, we don't fetch it either.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;noai&lt;/code&gt; opt-outs&lt;/strong&gt; in &lt;code&gt;X-Robots-Tag&lt;/code&gt; or &lt;code&gt;&amp;lt;meta name="robots"&amp;gt;&lt;/code&gt; are respected.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;We identify ourselves&lt;/strong&gt; as &lt;code&gt;DistillBot/1.0&lt;/code&gt; with a &lt;a href="https://distill.bytestay.pt/bot.html" rel="noopener noreferrer"&gt;bot page&lt;/a&gt;. We never pretend to be a browser, and we never retry a bot wall from somewhere else.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;At most 10 fresh fetches per minute per site&lt;/strong&gt;, across all customers. Results are cached for an hour, so popular pages don't get hammered.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Limits, honestly
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;JavaScript isn't run.&lt;/strong&gt; Pages that build their content in the browser come back as &lt;code&gt;no_content&lt;/code&gt;. If you already render them, &lt;code&gt;POST&lt;/code&gt; the HTML.&lt;/li&gt;
&lt;li&gt;Pages are read up to 5 MB, with a 20-second total budget per request.&lt;/li&gt;
&lt;li&gt;Extraction is heuristic (we build on &lt;a href="https://github.com/adbar/trafilatura" rel="noopener noreferrer"&gt;trafilatura&lt;/a&gt;), so some layouts come out imperfect.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No signup:&lt;/strong&gt; the demo on &lt;a href="https://distill.bytestay.pt/#try" rel="noopener noreferrer"&gt;distill.bytestay.pt&lt;/a&gt; runs the real API in your browser.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Free plan:&lt;/strong&gt; Basic is $0 for 15 requests a month, with a hard limit, so it never runs up a bill. Paid plans start at $9 for 10,000 requests.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Docs and plans:&lt;/strong&gt; &lt;a href="https://rapidapi.com/bytestay-bytestay-default/api/distill-html-to-markdown-api" rel="noopener noreferrer"&gt;RapidAPI listing&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If a page comes out wrong, send us the URL: those reports are how the extraction gets better.&lt;/p&gt;

</description>
      <category>llm</category>
      <category>rag</category>
      <category>webdev</category>
      <category>ai</category>
    </item>
  </channel>
</rss>
