<?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: appierun</title>
    <description>The latest articles on DEV Community by appierun (@appierun_66).</description>
    <link>https://dev.to/appierun_66</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%2F4191473%2F65dcdc9f-d62d-4fcb-b837-5ddd6bb14bae.png</url>
      <title>DEV Community: appierun</title>
      <link>https://dev.to/appierun_66</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/appierun_66"/>
    <language>en</language>
    <item>
      <title>Convert CSV and JSON to Markdown Tables for LLM Prompts and Chatbot Knowledge With Python (Escaping, Ragged Rows, Nested Fields)</title>
      <dc:creator>appierun</dc:creator>
      <pubDate>Sun, 01 Nov 2026 16:01:50 +0000</pubDate>
      <link>https://dev.to/appierun_66/convert-csv-and-json-to-markdown-tables-for-llm-prompts-and-chatbot-knowledge-with-python-1jda</link>
      <guid>https://dev.to/appierun_66/convert-csv-and-json-to-markdown-tables-for-llm-prompts-and-chatbot-knowledge-with-python-1jda</guid>
      <description>&lt;p&gt;Pasting a spreadsheet export into a prompt, a README or a chatbot knowledge file usually goes one of two ways: raw CSV that is hard for a person to review, or a hand-made Markdown table that breaks the moment a cell contains a pipe character. This post walks through a small, dependency-free Python script that turns CSV files and JSON arrays into Markdown tables and handles the cases that break naive converters.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Disclosure: Appie.run publishes this post. Appie has free CSV and JSON to Markdown converters, mentioned near the end. The script below has no dependency on them, and it uses only the Python standard library.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Earlier posts in this series cover &lt;a href="https://dev.to/appierun_66/how-to-convert-pdfs-to-markdown-for-rag-and-chatbots-and-when-not-to-307d"&gt;converting PDFs to Markdown for RAG&lt;/a&gt; and &lt;a href="https://dev.to/appierun_66/how-to-convert-notion-to-markdown-export-public-pages-and-the-api-with-a-cleanup-script-3gmc"&gt;converting Notion to Markdown&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What breaks naive converters
&lt;/h2&gt;

&lt;p&gt;The one-liner &lt;code&gt;"| " + " | ".join(row) + " |"&lt;/code&gt; works until your data meets the real world:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Pipes inside cells.&lt;/strong&gt; A value like &lt;code&gt;has a | pipe&lt;/code&gt; adds a column and shifts everything after it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Newlines inside quoted CSV cells.&lt;/strong&gt; A multi-line note ends the table row early.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ragged rows.&lt;/strong&gt; Exports often have trailing rows with fewer fields, or completely empty rows.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Different delimiters.&lt;/strong&gt; European spreadsheet exports often use semicolons.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A byte order mark.&lt;/strong&gt; Excel's "CSV UTF-8" export starts with a BOM, which ends up glued to your first header.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;JSON records with different keys.&lt;/strong&gt; The second object has a field the first one lacks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Nested values.&lt;/strong&gt; A list of tags or an address object inside a record.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The script
&lt;/h2&gt;

&lt;p&gt;Save this as &lt;code&gt;to_md.py&lt;/code&gt;. It was tested with Python 3.13 on the cases below.&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="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;csv&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pathlib&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;escape_cell&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Make one value safe for a Markdown table cell.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="ow"&gt;is&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="sh"&gt;""&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;)):&lt;/span&gt;
        &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ensure_ascii&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\\\\&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;replace&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="sh"&gt;"&lt;/span&gt;&lt;span class="se"&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="k"&gt;return&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="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&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="c1"&gt;# newlines and tabs become single spaces
&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;rows_to_markdown&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;header&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;lines&lt;/span&gt; &lt;span class="o"&gt;=&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="o"&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="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;escape_cell&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;header&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;| &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="o"&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="nf"&gt;join&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="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;header&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&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="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;cells&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&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="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;header&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;  &lt;span class="c1"&gt;# pad short rows
&lt;/span&gt;        &lt;span class="n"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&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="o"&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="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;escape_cell&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;cells&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;header&lt;/span&gt;&lt;span class="p"&gt;)])&lt;/span&gt; &lt;span class="o"&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="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;csv_to_markdown&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="n"&gt;max_rows&lt;/span&gt;&lt;span class="o"&gt;=&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;with&lt;/span&gt; &lt;span class="nf"&gt;open&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="n"&gt;newline&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;encoding&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8-sig&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;sample&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;4096&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;seek&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;dialect&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;csv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Sniffer&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;sniff&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sample&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;delimiters&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;,;&lt;/span&gt;&lt;span class="se"&gt;\t&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;csv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;dialect&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;csv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;excel&lt;/span&gt;
        &lt;span class="n"&gt;reader&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;csv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dialect&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;header&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;reader&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;rows&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;enumerate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;reader&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;max_rows&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="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;max_rows&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;break&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;any&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cell&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;cell&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
                &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;rows_to_markdown&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;header&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;json_to_markdown&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="n"&gt;max_rows&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Path&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="nf"&gt;read_text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;encoding&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="c1"&gt;# a dict of lists: render each list under its own heading
&lt;/span&gt;        &lt;span class="n"&gt;parts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
            &lt;span class="n"&gt;parts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;## &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
                &lt;span class="n"&gt;parts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;records_to_markdown&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_rows&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;parts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;escape_cell&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="n"&gt;parts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;parts&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;records_to_markdown&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_rows&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;records_to_markdown&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;records&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_rows&lt;/span&gt;&lt;span class="o"&gt;=&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;if&lt;/span&gt; &lt;span class="n"&gt;max_rows&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="n"&gt;records&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;records&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt;&lt;span class="n"&gt;max_rows&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;header&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;rec&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;records&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="c1"&gt;# union of keys, first-seen order
&lt;/span&gt;        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;rec&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;key&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;header&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;header&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;rows&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[[&lt;/span&gt;&lt;span class="n"&gt;rec&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;k&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;header&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;rec&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;records&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;rows_to_markdown&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;header&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rows&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;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;src&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;argv&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;limit&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;argv&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;argv&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;src&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="nf"&gt;endswith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;.json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;json_to_markdown&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;src&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;csv_to_markdown&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;src&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run it with a file and an optional row limit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python3 to_md.py plans.csv
python3 to_md.py leads.json 25
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  How each problem is handled
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Pipes and backslashes&lt;/strong&gt; are escaped in &lt;code&gt;escape_cell&lt;/code&gt;, so &lt;code&gt;a | b&lt;/code&gt; renders as one cell. Backslashes are escaped first so an existing &lt;code&gt;\&lt;/code&gt; does not swallow the escape.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Newlines and tabs&lt;/strong&gt; collapse to single spaces with &lt;code&gt;" ".join(text.split())&lt;/code&gt;. Markdown table cells cannot hold line breaks without HTML, and a prompt rarely needs them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ragged rows&lt;/strong&gt; are padded with empty cells up to the header length and trimmed if they are longer. Rows where every cell is blank are skipped.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Delimiters&lt;/strong&gt; are detected by &lt;code&gt;csv.Sniffer&lt;/code&gt; from the first 4 KB, limited to comma, semicolon and tab, with a fallback to standard CSV if detection fails.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The BOM&lt;/strong&gt; is removed by opening the file with &lt;code&gt;encoding="utf-8-sig"&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Different keys&lt;/strong&gt; in JSON records are merged into one header in first-seen order, and missing values become empty cells.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Nested values&lt;/strong&gt; (lists and objects) are written as compact JSON inside the cell. A top-level JSON object of lists becomes one &lt;code&gt;##&lt;/code&gt; heading per key, with a table under each list of records.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Test it on the awkward cases
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'plan,price_usd,notes\nStarter,19,"2,000 credits"\nStandard,49,"has a | pipe\nand a newline"\n,,\nFree,0\n'&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; plans.csv
python3 to_md.py plans.csv
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;| plan | price_usd | notes |
| --- | --- | --- |
| Starter | 19 | 2,000 credits |
| Standard | 49 | has a \| pipe and a newline |
| Free | 0 |  |
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The quoted comma stayed in its cell, the pipe was escaped, the newline became a space, the blank row was dropped and the short &lt;code&gt;Free&lt;/code&gt; row was padded.&lt;/p&gt;

&lt;p&gt;A semicolon file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'name;score\nA;1\nB;2\n'&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; semi.csv
python3 to_md.py semi.csv
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;| name | score |
| --- | --- |
| A | 1 |
| B | 2 |
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;JSON records with different keys and a nested list:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;'[{"name":"Ana","company":"Acme","tags":["saas","eu"]},{"name":"Ben","title":"PM"}]'&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; leads.json
python3 to_md.py leads.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;| name | company | tags | title |
| --- | --- | --- | --- |
| Ana | Acme | ["saas", "eu"] |  |
| Ben |  |  | PM |
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A JSON object of lists:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;'{"plans":[{"name":"Starter","credits":2000},{"name":"Standard","credits":5000}],"currency":"USD"}'&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; nested.json
python3 to_md.py nested.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;## plans

| name | credits |
| --- | --- |
| Starter | 2000 |
| Standard | 5000 |

## currency

USD
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Is Markdown the right format for a prompt?
&lt;/h2&gt;

&lt;p&gt;It depends on what you are optimising. We measured one case on our own machine: 50 records with four short fields came to 3,280 characters as JSON, 1,202 as CSV and 1,686 as a Markdown table. You can reproduce it with this snippet next to &lt;code&gt;to_md.py&lt;/code&gt;:&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="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;csv&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;io&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;to_md&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;records_to_markdown&lt;/span&gt;

&lt;span class="n"&gt;rows&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;plan&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Starter&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;credits&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2000&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;span class="n"&gt;buf&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;io&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;StringIO&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;w&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;csv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;DictWriter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fieldnames&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;writeheader&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;writerows&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getvalue&lt;/span&gt;&lt;span class="p"&gt;()),&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;records_to_markdown&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So for this data, CSV was the most compact and JSON the largest. Characters are not tokens, and your numbers will differ with your data and tokenizer, so measure your own case. Practical rules we use:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Use Markdown&lt;/strong&gt; when the table sits inside prose (a README, a doc page, a chatbot knowledge file a person will review), or when you want the model and the reader to see the same thing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use CSV&lt;/strong&gt; when the table is large and you only care about size.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keep JSON&lt;/strong&gt; when values are deeply nested or when the model must return the same structure.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Limit rows.&lt;/strong&gt; The optional row limit exists because the right answer for a 20,000-row export is usually a sample plus a summary, not the whole file.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  When you do not want to run a script
&lt;/h2&gt;

&lt;p&gt;Appie has two free browser tools for one-off conversions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://appie.run/tools/convert-csv-to-markdown" rel="noopener noreferrer"&gt;Convert CSV to Markdown&lt;/a&gt;: paste a CSV export or upload the file, and the first row becomes the table header.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://appie.run/tools/convert-json-to-markdown" rel="noopener noreferrer"&gt;Convert JSON to Markdown&lt;/a&gt;: paste JSON or upload a &lt;code&gt;.json&lt;/code&gt; file. Arrays of objects become tables and nested objects become headings.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Both ask for a Cloudflare Turnstile check before each run, and the pages say the file or paste is converted for that request and not saved to an Appie account. For anything repeatable, or for data you would rather not paste into a website, use the script.&lt;/p&gt;

&lt;p&gt;If the Markdown is headed for a chatbot, Appie's AI Chatbots app can also take CSV files, text and URLs as knowledge directly, and you can add content from Claude Code or Cursor through the &lt;code&gt;add_chatbot_content&lt;/code&gt; tool on &lt;a href="https://appie.run/docs/mcp" rel="noopener noreferrer"&gt;Appie's MCP server&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Honest limits of Appie's converters
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;One file per run, by hand.&lt;/strong&gt; There is no batch mode on the tool pages, and each run needs a fresh captcha check.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No control over escaping or row limits&lt;/strong&gt; is described on the pages. If you need a specific output format, the script gives you that control.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Why not use pandas &lt;code&gt;to_markdown()&lt;/code&gt;?&lt;/strong&gt;&lt;br&gt;
It works well if pandas is already installed; it needs the extra &lt;code&gt;tabulate&lt;/code&gt; package. The script above has no dependencies, which matters in small containers, CI jobs and serverless functions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How do I keep line breaks inside a cell?&lt;/strong&gt;&lt;br&gt;
Replace the newline with &lt;code&gt;&amp;lt;br&amp;gt;&lt;/code&gt; instead of a space in &lt;code&gt;escape_cell&lt;/code&gt;. Many Markdown renderers, including GitHub's, show it as a line break inside the cell. LLMs read it fine, but it adds characters.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What about very wide tables?&lt;/strong&gt;&lt;br&gt;
Split them. A table with 30 columns is hard for people and models to read. Pick the columns the prompt actually needs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does the script handle Excel files?&lt;/strong&gt;&lt;br&gt;
No. Export the sheet as CSV first, or read it with &lt;code&gt;openpyxl&lt;/code&gt; and pass the rows to &lt;code&gt;rows_to_markdown&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;If you want a website chatbot trained on files like these, you can try Appie's AI Chatbots on the Free plan (500 credits, no credit card) at &lt;a href="https://appie.run" rel="noopener noreferrer"&gt;appie.run&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;RG, Founder, Appie.run&lt;/p&gt;

</description>
      <category>python</category>
      <category>markdown</category>
      <category>llm</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>How to Compare Two XML Sitemaps Before and After a Site Migration With Python (Find Removed URLs and Missing Redirects)</title>
      <dc:creator>appierun</dc:creator>
      <pubDate>Sat, 31 Oct 2026 15:57:04 +0000</pubDate>
      <link>https://dev.to/appierun_66/how-to-compare-two-xml-sitemaps-before-and-after-a-site-migration-with-python-find-removed-urls-1eg2</link>
      <guid>https://dev.to/appierun_66/how-to-compare-two-xml-sitemaps-before-and-after-a-site-migration-with-python-find-removed-urls-1eg2</guid>
      <description>&lt;p&gt;A site migration (new domain, new CMS, new URL structure) is where search traffic quietly goes missing. Pages that existed before the move disappear from the new site, and nobody notices until rankings drop. Your old and new XML sitemaps are the quickest record of what changed. This post walks through a small Python script that diffs them, follows sitemap indexes, handles a domain change, and checks every removed URL against your redirect map.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Disclosure: Appie.run publishes this post. Appie has free sitemap tools, mentioned near the end. The script below has no dependency on them, and it uses only the Python standard library.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Earlier posts in this series cover &lt;a href="https://dev.to/appierun_66/how-to-generate-an-xml-sitemap-for-a-static-site-with-python-skip-noindex-pages-split-at-50000-2fak"&gt;generating a sitemap for a static site&lt;/a&gt; and &lt;a href="https://dev.to/appierun_66/how-to-validate-an-xml-sitemap-common-errors-what-google-ignores-and-a-small-python-checker-4feo"&gt;validating a sitemap&lt;/a&gt;. Run the validator on both files first if you are not sure they are well formed.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the script answers
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Which URLs are in the old sitemap but not the new one (&lt;strong&gt;removed&lt;/strong&gt;)?&lt;/li&gt;
&lt;li&gt;Which are new (&lt;strong&gt;added&lt;/strong&gt;)?&lt;/li&gt;
&lt;li&gt;Which survived (&lt;strong&gt;kept&lt;/strong&gt;)?&lt;/li&gt;
&lt;li&gt;For every removed URL, is there a redirect in your map, and does the target actually exist in the new sitemap?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Question 4 is the one that saves you. A redirect pointing at a page that was never built is as bad as no redirect.&lt;/p&gt;

&lt;h2&gt;
  
  
  Before you run it
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Save the old sitemap before launch day.&lt;/strong&gt; Once the new site is live, the old &lt;code&gt;sitemap.xml&lt;/code&gt; is usually gone. Download it (and every child sitemap) while you still can.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Export your redirect map&lt;/strong&gt; as a two-column CSV: &lt;code&gt;old_url,new_url&lt;/code&gt;. Most migration plans already have one in a spreadsheet.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The script
&lt;/h2&gt;

&lt;p&gt;Save this as &lt;code&gt;sitemap_diff.py&lt;/code&gt;:&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="c1"&gt;#!/usr/bin/env python3
&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Compare two XML sitemaps (before and after a site migration).&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;argparse&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;csv&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;gzip&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;urllib.request&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;xml.etree.ElementTree&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;ET&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;urllib.parse&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;urlsplit&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;urlunsplit&lt;/span&gt;

&lt;span class="n"&gt;NS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;{http://www.sitemaps.org/schemas/sitemap/0.9}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;UA&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sitemap-diff/1.0 (+https://example.com/contact)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;read_source&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;src&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Return raw bytes from a URL or a local file path.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;src&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startswith&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http://&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)):&lt;/span&gt;
        &lt;span class="n"&gt;req&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;src&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;User-Agent&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;UA&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
        &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;urlopen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;src&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rb&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;fh&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;fh&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&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;data&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sa"&gt;b&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\x1f\x8b&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="c1"&gt;# gzip magic number
&lt;/span&gt;        &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;gzip&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;decompress&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&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;data&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;collect_urls&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;src&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;seen&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;depth&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Return the set of page URLs in a sitemap, following sitemap indexes.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;seen&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;seen&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;seen&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="k"&gt;else&lt;/span&gt; &lt;span class="nf"&gt;set&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;src&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;seen&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;depth&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;seen&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;src&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;root&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ET&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fromstring&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;read_source&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;src&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="n"&gt;tag&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;root&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tag&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;NS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;locs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;el&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;el&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;root&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;iter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;NS&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;loc&lt;/span&gt;&lt;span class="sh"&gt;"&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;el&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&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;tag&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sitemapindex&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;urls&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;child&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;locs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;urls&lt;/span&gt; &lt;span class="o"&gt;|=&lt;/span&gt; &lt;span class="nf"&gt;collect_urls&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;child&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;seen&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;depth&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&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;urls&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;tag&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;urlset&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;locs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;src&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;: root element is &amp;lt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;tag&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;&amp;gt;, expected urlset or sitemapindex&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;normalise&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;old_host&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;new_host&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ignore_slash&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Make URLs comparable: lowercase host, optional host swap, trailing slash.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;parts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;urlsplit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;host&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;parts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;netloc&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;old_host&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;host&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;old_host&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="n"&gt;host&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;new_host&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="n"&gt;path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;parts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="ow"&gt;or&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="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;ignore_slash&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="nf"&gt;len&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="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;rstrip&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="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;urlunsplit&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;parts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;scheme&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="n"&gt;host&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="n"&gt;parts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;load_redirects&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="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Read a two-column CSV of old_url,new_url.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;open&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="n"&gt;newline&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;encoding&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;fh&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;csv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fh&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;ap&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;argparse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ArgumentParser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;__doc__&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;ap&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_argument&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;before&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;help&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;old sitemap: URL or file&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;ap&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_argument&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;after&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;help&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;new sitemap: URL or file&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;ap&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_argument&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--old-host&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;help&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;host used before the move, e.g. www.old.com&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;ap&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_argument&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--new-host&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;help&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;host used after the move, e.g. www.new.com&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;ap&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_argument&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--redirects&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;help&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CSV of old_url,new_url to check removed URLs against&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;ap&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_argument&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--out&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;default&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sitemap-diff.csv&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;help&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;where to write the report&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;args&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ap&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse_args&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;old_host&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="nf"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;new_host&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;ap&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--old-host and --new-host go together&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;before_raw&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;collect_urls&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;before&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;after_raw&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;collect_urls&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;after&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;before&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;normalise&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;old_host&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;new_host&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;before_raw&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;after&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;normalise&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;after_raw&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;kept&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;before&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;after&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;removed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;before&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;after&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;added&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;after&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;before&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="n"&gt;redirects&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;load_redirects&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;redirects&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;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;redirects&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
    &lt;span class="n"&gt;after_norm&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;after&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;

    &lt;span class="n"&gt;rows&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;removed&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;old&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;before&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="n"&gt;target&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;redirects&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;old&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;redirects&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;note&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;
        &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;note&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;no redirect mapped&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="nf"&gt;normalise&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;after_norm&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;note&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;redirect target not in new sitemap&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;note&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ok&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;removed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;old&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;note&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="n"&gt;rows&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="p"&gt;[(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;added&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;after&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;added&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
    &lt;span class="n"&gt;rows&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="p"&gt;[(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;kept&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;after&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;kept&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;

    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;w&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;newline&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;encoding&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;fh&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;writer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;csv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;writer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fh&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;writer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;writerow&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;url&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;redirect_to&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;note&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
        &lt;span class="n"&gt;writer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;writerows&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;before: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;before&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;  after: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;after&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;kept: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;kept&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;  added: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;added&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;  removed: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;removed&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&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;redirects&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;problems&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;removed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ok&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;removed URLs without a valid redirect: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;problems&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;report: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;redirects&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;problems&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;


&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What each part does
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;read_source&lt;/code&gt;&lt;/strong&gt; accepts a URL or a local file and decompresses &lt;code&gt;.gz&lt;/code&gt; sitemaps by checking the gzip magic bytes rather than trusting the file name.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;collect_urls&lt;/code&gt;&lt;/strong&gt; reads the root element. A &lt;code&gt;&amp;lt;urlset&amp;gt;&lt;/code&gt; returns its &lt;code&gt;&amp;lt;loc&amp;gt;&lt;/code&gt; values. A &lt;code&gt;&amp;lt;sitemapindex&amp;gt;&lt;/code&gt; makes the function fetch each child sitemap and merge the results. The &lt;code&gt;seen&lt;/code&gt; set and depth limit stop a loop if an index lists itself.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;normalise&lt;/code&gt;&lt;/strong&gt; makes URLs comparable. It lowercases the scheme and host, drops fragments, treats &lt;code&gt;/page&lt;/code&gt; and &lt;code&gt;/page/&lt;/code&gt; as the same, and, if you pass &lt;code&gt;--old-host&lt;/code&gt; and &lt;code&gt;--new-host&lt;/code&gt;, rewrites the old host so &lt;code&gt;https://www.old-shop.com/pricing/&lt;/code&gt; matches &lt;code&gt;https://www.new-shop.com/pricing&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The diff&lt;/strong&gt; is three set operations on the normalised keys. The original URLs are kept for the report, so you see exactly what each sitemap listed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Redirect checks&lt;/strong&gt; run only when you pass &lt;code&gt;--redirects&lt;/code&gt;. Each removed URL is marked &lt;code&gt;ok&lt;/code&gt;, &lt;code&gt;no redirect mapped&lt;/code&gt;, or &lt;code&gt;redirect target not in new sitemap&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Exit code&lt;/strong&gt; is 1 when any removed URL lacks a valid redirect, so you can fail a CI job on it.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Run it
&lt;/h2&gt;

&lt;p&gt;A small example with an old site at &lt;code&gt;www.old-shop.com&lt;/code&gt; and a new one at &lt;code&gt;www.new-shop.com&lt;/code&gt;, where the new sitemap is an index pointing to a child file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python3 sitemap_diff.py old-sitemap.xml https://www.new-shop.com/sitemap_index.xml &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--old-host&lt;/span&gt; www.old-shop.com &lt;span class="nt"&gt;--new-host&lt;/span&gt; www.new-shop.com &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--redirects&lt;/span&gt; redirects.csv
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;before: 4  after: 5
kept: 2  added: 3  removed: 2
removed URLs without a valid redirect: 1
report: sitemap-diff.csv
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And &lt;code&gt;sitemap-diff.csv&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;status,url,redirect_to,note
removed,https://www.old-shop.com/about-us,https://www.new-shop.com/about,redirect target not in new sitemap
removed,https://www.old-shop.com/blog/launch-post,https://www.new-shop.com/blog/launch,ok
added,https://www.new-shop.com/blog/launch,,
added,https://www.new-shop.com/company,,
added,https://www.new-shop.com/integrations,,
kept,https://www.new-shop.com/,,
kept,https://www.new-shop.com/pricing,,
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first row is the kind of bug this catches: the redirect map sends &lt;code&gt;/about-us&lt;/code&gt; to &lt;code&gt;/about&lt;/code&gt;, but the new site calls that page &lt;code&gt;/company&lt;/code&gt;. Fix the map before launch, not after.&lt;/p&gt;

&lt;p&gt;Notice that &lt;code&gt;/pricing/&lt;/code&gt; and &lt;code&gt;/pricing&lt;/code&gt; count as kept because of the trailing slash rule. If your new site really treats them as different pages, run without that rule (set &lt;code&gt;ignore_slash=False&lt;/code&gt; in &lt;code&gt;normalise&lt;/code&gt;).&lt;/p&gt;

&lt;h2&gt;
  
  
  Optional: check what the live URLs actually do
&lt;/h2&gt;

&lt;p&gt;The map says what &lt;em&gt;should&lt;/em&gt; happen. After launch, check what &lt;em&gt;does&lt;/em&gt; happen. This second script reads the report and requests each removed URL without following redirects, so you see the first hop:&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="c1"&gt;#!/usr/bin/env python3
&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;For each removed URL in sitemap-diff.csv, request it and report where it lands.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;csv&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;urllib.error&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;urllib.request&lt;/span&gt;


&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;NoFollow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HTTPRedirectHandler&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;redirect_request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;kwargs&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="c1"&gt;# stop at the first hop so we can see the status code
&lt;/span&gt;

&lt;span class="n"&gt;opener&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;build_opener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;NoFollow&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;first_hop&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;method&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;HEAD&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;req&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;method&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;method&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;User-Agent&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sitemap-diff/1.0&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;opener&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;resp&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;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HTTPError&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="c1"&gt;# 3xx lands here because we refuse to follow
&lt;/span&gt;        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;method&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;HEAD&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;code&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;405&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;501&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;first_hop&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;GET&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# some servers reject HEAD
&lt;/span&gt;        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;headers&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Location&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;URLError&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="n"&gt;report&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;argv&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;argv&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sitemap-diff.csv&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;report&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;newline&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;encoding&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;fh&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;csv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;DictReader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fh&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;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;removed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt;
        &lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;location&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;first_hop&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;url&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
        &lt;span class="n"&gt;flag&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;code&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;301&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;308&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;  &amp;lt;-- check&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;url&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; -&amp;gt; &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;location&lt;/span&gt;&lt;span class="si"&gt;}{&lt;/span&gt;&lt;span class="n"&gt;flag&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Example output (illustrative URLs):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;301 https://www.old-shop.com/blog/launch-post -&amp;gt; https://www.new-shop.com/blog/launch
404 https://www.old-shop.com/about-us -&amp;gt;   &amp;lt;-- check
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Anything that is not a 301 or 308 gets flagged. A 302 is a temporary redirect, which is usually not what you want for a permanent move, and a 200 means the old page still serves content. Some servers reject &lt;code&gt;HEAD&lt;/code&gt; requests, so the script retries those with &lt;code&gt;GET&lt;/code&gt;. Keep the request rate polite on large sites; for tens of thousands of URLs, add a short &lt;code&gt;time.sleep&lt;/code&gt; between requests or run it on a sample.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wire it into a pre-launch check
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# fails the job if any removed URL lacks a valid redirect&lt;/span&gt;
python3 sitemap_diff.py old-sitemap.xml staging-sitemap.xml &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--old-host&lt;/span&gt; www.old-shop.com &lt;span class="nt"&gt;--new-host&lt;/span&gt; staging.new-shop.com &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--redirects&lt;/span&gt; redirects.csv &lt;span class="nt"&gt;--out&lt;/span&gt; diff.csv
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Running it against the staging sitemap on every deploy means a page dropped by a template change shows up the same day.&lt;/p&gt;

&lt;h2&gt;
  
  
  Known limits
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A sitemap is not the whole site.&lt;/strong&gt; Pages that were never in the old sitemap won't be checked. Combine this with a crawl or your analytics' top landing pages.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Query strings are compared as-is.&lt;/strong&gt; If the old site used &lt;code&gt;?id=12&lt;/code&gt; and the new one uses clean paths, rely on the redirect map rather than the kept count.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Relative &lt;code&gt;&amp;lt;loc&amp;gt;&lt;/code&gt; values&lt;/strong&gt; inside an index are read as local paths. The sitemap protocol expects absolute URLs, so this only matters for hand-made test files.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Memory.&lt;/strong&gt; Everything is held in sets. That is fine for a few hundred thousand URLs; beyond that, stream to disk.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  When a script is more than you need
&lt;/h2&gt;

&lt;p&gt;For a one-off check, Appie's free tools do the core steps in the browser:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://appie.run/tools/sitemap-urls-comparison" rel="noopener noreferrer"&gt;Sitemap URLs Comparison Tool&lt;/a&gt;&lt;/strong&gt; takes two sitemaps, by URL or pasted XML, and reports how many URLs stayed, which were added and which were removed. The first sitemap is treated as the before state.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://appie.run/tools/sitemap-url-extractor" rel="noopener noreferrer"&gt;Sitemap URL Extractor&lt;/a&gt;&lt;/strong&gt; lists one URL per line from a sitemap. Given a sitemap index, it lists the child sitemaps, so you run it again on each child.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://appie.run/tools/sitemap-checker" rel="noopener noreferrer"&gt;Sitemap Finder and Checker&lt;/a&gt;&lt;/strong&gt; finds sitemaps through robots.txt and the usual paths, which helps when you don't know where the old site kept them.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each asks for a captcha before running. They don't check redirect maps or host changes, which is where the script above earns its keep.&lt;/p&gt;

&lt;p&gt;RG, Founder, Appie.run&lt;/p&gt;

</description>
      <category>python</category>
      <category>seo</category>
      <category>webdev</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Create and Track Short Links From Claude Code or Cursor With an MCP Server (Appie, Bitly, Dub, Short.io)</title>
      <dc:creator>appierun</dc:creator>
      <pubDate>Fri, 30 Oct 2026 16:02:40 +0000</pubDate>
      <link>https://dev.to/appierun_66/create-and-track-short-links-from-claude-code-or-cursor-with-an-mcp-server-appie-bitly-dub-4068</link>
      <guid>https://dev.to/appierun_66/create-and-track-short-links-from-claude-code-or-cursor-with-an-mcp-server-appie-bitly-dub-4068</guid>
      <description>&lt;p&gt;If you ship release notes, changelogs or launch posts from your editor, the short link is usually the last manual step: open a shortener in a browser tab, paste the URL, copy the result, come back. With an MCP server, your coding agent can create the link and later tell you how many clicks it got, in the same session where you wrote the post.&lt;/p&gt;

&lt;p&gt;This walkthrough shows the setup for four link shorteners that publish an MCP server: Appie.run, Bitly, Dub and Short.io. It covers the config for Claude Code and Cursor, the prompts that work, and the limits of each.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Plan details and prices are as of 29 Oct 2026, checked on each vendor's own docs and pricing pages that day.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Disclosure: Appie.run publishes this post, and one of the four servers is ours. We compared shorteners more broadly (not just MCP) in &lt;a href="https://medium.com/@appierun/bitly-alternatives-in-2026-short-io-rebrandly-dub-and-appie-compared-for-marketers-and-agencies-5e43b0233d43" rel="noopener noreferrer"&gt;Bitly alternatives in 2026&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The four servers at a glance
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Endpoint&lt;/strong&gt;: Appie.run: &lt;code&gt;https://appie.run/mcp&lt;/code&gt;; Bitly: &lt;code&gt;https://api-ssl.bitly.com/v4/mcp&lt;/code&gt;; Dub (Links): &lt;code&gt;https://mcp.dub.sh/mcp/dub-links&lt;/code&gt;; Short.io: &lt;code&gt;https://ai-assistant.short.io/mcp&lt;/code&gt; (via &lt;code&gt;npx mcp-remote&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Auth&lt;/strong&gt;: Appie.run: OAuth 2.1 (ChatGPT) or &lt;code&gt;esk_&lt;/code&gt; project API key; Bitly: OAuth 2.1 or Bitly API token; Dub (Links): Dub API key in &lt;code&gt;Mcp-Dub-Token&lt;/code&gt; header; Short.io: Short.io API key&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Custom domain&lt;/strong&gt;: Appie.run: No, links use &lt;code&gt;s.appie.run&lt;/code&gt;; Bitly: Yes, on Growth and up; Dub (Links): Yes; Short.io: Yes&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Edit or delete existing links via MCP&lt;/strong&gt;: Appie.run: Not in the documented tools; Bitly: See Bitly's MCP tools reference; Dub (Links): Dub says the server lets you "programmatically manage your links"; check its tool list; Short.io: No, per Short.io's docs&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Entry price&lt;/strong&gt;: Appie.run: Free plan, 500 credits, no card; links cost 1 credit, clicks 0.001 credit each; Bitly: Bitly support says MCP is "available with some paid plans"; Core is $10/month billed annually; Dub (Links): Free plan (25 new links/month); Pro $27/month; Short.io: Free plan (1,000 branded links total); Hobby $5/month&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Claude Code or Cursor with MCP support.&lt;/li&gt;
&lt;li&gt;An account with the shortener and an API key or OAuth access.&lt;/li&gt;
&lt;li&gt;Keep keys in environment variables. Do not commit them to a shared repo.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Option 1: Appie.run
&lt;/h2&gt;

&lt;p&gt;Appie's MCP server exposes two link tools, documented at &lt;a href="https://appie.run/docs/mcp" rel="noopener noreferrer"&gt;appie.run/docs/mcp&lt;/a&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;create_short_link&lt;/code&gt;&lt;/strong&gt;: Arguments: &lt;code&gt;long_url&lt;/code&gt;, optional &lt;code&gt;code&lt;/code&gt; (custom slug); Cost: 1 credit per link; each click on the link costs 0.001 credit&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;link_stats&lt;/code&gt;&lt;/strong&gt;: Arguments: &lt;code&gt;id&lt;/code&gt; or &lt;code&gt;code&lt;/code&gt;; Cost: 0 credits to read&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Reads are free. Writes spend the project's credits first, then the owner's.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Claude Code:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;APPIE_API_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;esk_your_project_key
claude mcp add &lt;span class="nt"&gt;--transport&lt;/span&gt; http appie https://appie.run/mcp &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--header&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$APPIE_API_KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Cursor&lt;/strong&gt; (&lt;code&gt;~/.cursor/mcp.json&lt;/code&gt; or &lt;code&gt;.cursor/mcp.json&lt;/code&gt; in the project):&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;"appie"&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;"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://appie.run/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;"headers"&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;"Authorization"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Bearer YOUR_API_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;p&gt;Appie's docs also accept the key in an &lt;code&gt;X-Api-Key&lt;/code&gt; header if your client prefers that.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Prompts that work:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Create a short link for https://example.com/blog/v2-release?utm_source=devto&amp;amp;utm_medium=post
with the code v2-devto.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Get link_stats for the code v2-devto and tell me the click count.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;What the links give you:&lt;/strong&gt; links live on the &lt;code&gt;s.appie.run&lt;/code&gt; domain, and the &lt;a href="https://appie.run/url-shortener" rel="noopener noreferrer"&gt;URL Shortener page&lt;/a&gt; lists clicks and unique visitors over the last 30 days, lifetime totals, and device and location breakdowns through the stats API.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Limits:&lt;/strong&gt; no custom domain, and the MCP tools cover create and read only. If you need branded domains or bulk edits, one of the other three is a better fit. The same MCP server also covers Appie's other apps (articles, chatbots, Reddit and LinkedIn research), which matters only if you want one server for several marketing jobs. We wrote about that in &lt;a href="https://medium.com/@appierun/one-mcp-server-for-your-marketing-agent-reddit-linkedin-articles-chatbots-and-short-links-6850e184b0d8" rel="noopener noreferrer"&gt;One MCP server for your marketing agent&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Option 2: Bitly
&lt;/h2&gt;

&lt;p&gt;Bitly hosts its server at &lt;code&gt;https://api-ssl.bitly.com/v4/mcp&lt;/code&gt;. Its developer docs list setup guides for ChatGPT, Claude (web, desktop and Claude Code), Cursor, VS Code and Windsurf, with OAuth 2.1 recommended and an API token as the fallback.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Claude Code or Cursor&lt;/strong&gt; (from Bitly's docs; Claude Code reads &lt;code&gt;~/.claude/mcp.json&lt;/code&gt;, Cursor reads &lt;code&gt;~/.cursor/mcp.json&lt;/code&gt;):&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;"Bitly"&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;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"http"&lt;/span&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://api-ssl.bitly.com/v4/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;"headers"&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;"Authorization"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Bearer YOUR_BITLY_AUTH_TOKEN"&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;p&gt;Drop the &lt;code&gt;headers&lt;/code&gt; block to use OAuth instead.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pricing note:&lt;/strong&gt; Bitly's support page says the MCP server is "available with some paid plans", so confirm yours before building a workflow around it. On Bitly's pricing page, Core is $10/month (billed annually, 100 links/month, 30 days of click data), Growth is $29/month (500 links/month, branded links, 4 months of data) and Premium is $199/month (3,000 links/month, city and device data). The Free plan allows 5 links a month.&lt;/p&gt;

&lt;h2&gt;
  
  
  Option 3: Dub
&lt;/h2&gt;

&lt;p&gt;Dub runs two MCP servers: one for its partner program product and one for links. For short links you want &lt;code&gt;https://mcp.dub.sh/mcp/dub-links&lt;/code&gt;. Dub's docs pass the API key in an &lt;code&gt;Mcp-Dub-Token&lt;/code&gt; header.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Claude Code:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;claude mcp add dub-links &lt;span class="nt"&gt;--transport&lt;/span&gt; http https://mcp.dub.sh/mcp/dub-links &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--header&lt;/span&gt; &lt;span class="s2"&gt;"Mcp-Dub-Token: &lt;/span&gt;&lt;span class="nv"&gt;$DUB_API_KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Cursor:&lt;/strong&gt;&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;"dub-links"&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;"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://mcp.dub.sh/mcp/dub-links"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"headers"&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;"Mcp-Dub-Token"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"dub_xxxxxx"&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;p&gt;&lt;strong&gt;Pricing:&lt;/strong&gt; Dub's Links pricing page lists a Free plan (25 new links/month, 1,000 tracked events/month, 30-day retention, API access, 3 custom domains), Pro at $27/month (12K new links/year, 1-year retention, 10 custom domains) and Business at $81/month. Dub's launch post says its MCP server is generally available on all Dub plans; confirm that the Links server is included on yours before you rely on it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Option 4: Short.io
&lt;/h2&gt;

&lt;p&gt;Short.io's docs connect through &lt;code&gt;mcp-remote&lt;/code&gt;:&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;"shortio"&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;"npx"&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="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"mcp-remote"&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://ai-assistant.short.io/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;"--header"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"Authorization: ${SHORTIO_API_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="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;"SHORTIO_API_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;"sk_secret_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;p&gt;Short.io's docs show creating links on your own domain with a custom slug, searching links, and pulling link and domain statistics (countries, browsers, operating systems, UTM campaigns). One limit they state plainly: with the current MCP implementation you cannot edit or delete an existing link; you remove it in the dashboard and create a new one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pricing:&lt;/strong&gt; the Free plan includes 1,000 branded links total, 5 custom domains and 50,000 tracked clicks a month. Its comparison table lists Hobby at $5/month, Pro at $18/month and Team at $48/month.&lt;/p&gt;

&lt;h2&gt;
  
  
  A small release workflow
&lt;/h2&gt;

&lt;p&gt;Here is a pattern that works with any of the four:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Write the release post in your repo.&lt;/li&gt;
&lt;li&gt;Ask the agent: "Create a short link for each external URL in &lt;code&gt;posts/v2-release.md&lt;/code&gt;, use slugs that start with &lt;code&gt;v2-&lt;/code&gt;, and replace the long URLs in the file."&lt;/li&gt;
&lt;li&gt;Review the diff before committing. The agent can pick the wrong URL, so check every replacement.&lt;/li&gt;
&lt;li&gt;A week later: "Get stats for every link with a &lt;code&gt;v2-&lt;/code&gt; slug and summarize clicks by link."&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Step 3 matters. An agent with write access to your shortener can create links you did not intend, and with Bitly, Dub or Short.io those links can sit on your branded domain. Review before you publish.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which one to pick
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;You already pay for Bitly, Dub or Short.io:&lt;/strong&gt; use that vendor's server. Your existing domains and analytics come with it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You need branded domains on a budget:&lt;/strong&gt; Short.io's or Dub's free plans.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You want link tracking plus other marketing tools in one MCP server and do not need a custom domain:&lt;/strong&gt; Appie.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Do I need a paid plan to use these MCP servers?&lt;/strong&gt;&lt;br&gt;
Appie, Dub and Short.io offer free plans with API access. Bitly says its MCP server is available with some paid plans.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can the agent post the link anywhere?&lt;/strong&gt;&lt;br&gt;
No. These servers create and read links. Publishing the post is still your step.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Are clicks from bots counted?&lt;/strong&gt;&lt;br&gt;
Each vendor filters differently, and none of the docs we read gives a full method. Treat small click counts with caution.&lt;/p&gt;

&lt;p&gt;RG, Founder, Appie.run&lt;/p&gt;

</description>
      <category>ai</category>
      <category>marketing</category>
      <category>cursor</category>
      <category>mcp</category>
    </item>
    <item>
      <title>Build and Train a Website Chatbot From Claude Code or Cursor (MCP Walkthrough)</title>
      <dc:creator>appierun</dc:creator>
      <pubDate>Thu, 29 Oct 2026 15:57:11 +0000</pubDate>
      <link>https://dev.to/appierun_66/build-and-train-a-website-chatbot-from-claude-code-or-cursor-mcp-walkthrough-eoi</link>
      <guid>https://dev.to/appierun_66/build-and-train-a-website-chatbot-from-claude-code-or-cursor-mcp-walkthrough-eoi</guid>
      <description>&lt;p&gt;You can create a support chatbot, train it on your sitemap, and test its answers without leaving your editor. The loop is five tool calls: &lt;code&gt;create_chatbot&lt;/code&gt;, &lt;code&gt;add_chatbot_content&lt;/code&gt;, &lt;code&gt;chatbot_status&lt;/code&gt;, &lt;code&gt;ask_chatbot&lt;/code&gt;, and &lt;code&gt;list_chatbots&lt;/code&gt; when you come back later. This walkthrough uses Appie.run's MCP server, with every tool name, argument and credit cost taken from &lt;a href="https://appie.run/docs/mcp" rel="noopener noreferrer"&gt;appie.run/docs/mcp&lt;/a&gt; on 28 Oct 2026.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Disclosure: Appie.run publishes this post, and the server used here is ours.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;This is the third walkthrough in the series, after &lt;a href="https://dev.to/appierun_66/find-reddit-threads-and-draft-replies-from-claude-code-or-cursor-mfo"&gt;Reddit threads and reply drafts&lt;/a&gt; and &lt;a href="https://dev.to/appierun_66/track-linkedin-profiles-and-pull-leads-into-claude-or-cursor-5e5e"&gt;LinkedIn profiles and leads&lt;/a&gt;. If you've done either, skip to Step 3.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you need
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;An Appie project. The Free plan has 500 credits and needs no credit card.&lt;/li&gt;
&lt;li&gt;A project API key that starts with &lt;code&gt;esk_&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Cursor, Claude Code, or another MCP client that supports remote servers with custom headers.&lt;/li&gt;
&lt;li&gt;A public sitemap, or a few pages of text you want the bot to answer from.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 1: Create the key
&lt;/h2&gt;

&lt;p&gt;In Appie, open the project and go to &lt;strong&gt;API&lt;/strong&gt; in the sidebar. A project admin generates one &lt;code&gt;esk_&lt;/code&gt; key. Two things worth knowing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The same key is used by Event Store. Regenerating it stops both immediately.&lt;/li&gt;
&lt;li&gt;The key has no balance of its own. Paid calls run on the project's credits, then the project owner's. Every tool result includes &lt;code&gt;credits_remaining&lt;/code&gt;.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;APPIE_API_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;esk_YOUR_KEY
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Keep it out of git.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Add the server
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Cursor.&lt;/strong&gt; From Appie's docs:&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;"appie"&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;"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://appie.run/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;"headers"&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;"Authorization"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Bearer YOUR_API_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;p&gt;&lt;strong&gt;Claude Code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;claude mcp add &lt;span class="nt"&gt;--transport&lt;/span&gt; http appie https://appie.run/mcp &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--header&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$APPIE_API_KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run &lt;code&gt;claude mcp list&lt;/code&gt; to check the connection. A request with a placeholder key returned HTTP 401 when we tried it on 28 Oct 2026, which is the expected response for a bad key.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Find the sitemap you'll train on
&lt;/h2&gt;

&lt;p&gt;The bot learns fastest from a sitemap, because one call pulls in every page it lists. If you don't know where your sitemap lives, Appie's free &lt;a href="https://appie.run/tools/sitemap-checker" rel="noopener noreferrer"&gt;Sitemap Finder and Checker&lt;/a&gt; reads your robots.txt &lt;code&gt;Sitemap:&lt;/code&gt; lines and the usual paths (including the WordPress path) and reports which files load and how many URLs they list.&lt;/p&gt;

&lt;p&gt;A tip before you train: a sitemap often includes pages you don't want a support bot quoting, such as old blog posts, legal archives or job ads. If yours is noisy, train on a smaller sitemap file or on specific URLs instead.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Create the bot
&lt;/h2&gt;

&lt;p&gt;Ask your agent: "Create an Appie chatbot called Support Bot for example.com with a short system prompt."&lt;/p&gt;

&lt;p&gt;The agent calls &lt;code&gt;create_chatbot&lt;/code&gt;. &lt;code&gt;name&lt;/code&gt; is required. Optional fields are &lt;code&gt;system_prompt&lt;/code&gt;, &lt;code&gt;openai_model&lt;/code&gt; (&lt;code&gt;gpt-4o-mini&lt;/code&gt;, &lt;code&gt;gpt-4o&lt;/code&gt; or &lt;code&gt;gpt-3.5-turbo&lt;/code&gt;) and &lt;code&gt;temperature&lt;/code&gt; from 0 to 1. Creation is free. Illustrative arguments:&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;"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;"Support Bot"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"system_prompt"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Answer questions about Example Co using only the provided content. If the answer is not in the content, say you don't know and suggest emailing support."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"openai_model"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"gpt-4o-mini"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"temperature"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.2&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;A low temperature keeps answers close to your content. Note the returned &lt;code&gt;chatbot_id&lt;/code&gt;; every later call needs it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5: Add knowledge
&lt;/h2&gt;

&lt;p&gt;Call &lt;code&gt;add_chatbot_content&lt;/code&gt; with one of these &lt;code&gt;content_type&lt;/code&gt; values:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;sitemap&lt;/code&gt;&lt;/strong&gt;: Fields: &lt;code&gt;url&lt;/code&gt; (http or https); Use it for: A whole site or section&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;url&lt;/code&gt;&lt;/strong&gt;: Fields: &lt;code&gt;url&lt;/code&gt; (http or https); Use it for: One page, such as pricing or a returns policy&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;text&lt;/code&gt;&lt;/strong&gt;: Fields: &lt;code&gt;title&lt;/code&gt;, &lt;code&gt;content&lt;/code&gt;; Use it for: Facts that aren't on the site yet&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;article&lt;/code&gt;&lt;/strong&gt;: Fields: &lt;code&gt;article_id&lt;/code&gt;; Use it for: An article from Appie's Content Writer in the same project&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Illustrative calls:&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;"chatbot_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;"YOUR_BOT_ID"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"content_type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"sitemap"&lt;/span&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.example.com/sitemap.xml"&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;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;"chatbot_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;"YOUR_BOT_ID"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"content_type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"text"&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;"Support hours"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"content"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Support replies Monday to Friday, 9am to 6pm CET. Email support@example.com."&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;The &lt;code&gt;text&lt;/code&gt; type is handy for the questions your site never answers clearly: support hours, refund windows, which plan includes what.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 6: Wait for training
&lt;/h2&gt;

&lt;p&gt;Processing runs in the background. Call &lt;code&gt;chatbot_status&lt;/code&gt; with the &lt;code&gt;chatbot_id&lt;/code&gt; to read the bot's training status and its recent sources. It's free, so poll it as often as you like. A prompt that works well:&lt;/p&gt;

&lt;p&gt;"Check chatbot_status for YOUR_BOT_ID every 30 seconds until training finishes, then list the sources it added."&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 7: Test it before customers do
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;ask_chatbot&lt;/code&gt; sends a &lt;code&gt;message&lt;/code&gt; to a &lt;code&gt;chatbot_id&lt;/code&gt; and returns a &lt;code&gt;reply&lt;/code&gt;. It costs 1 credit per call. Pass the &lt;code&gt;session_id&lt;/code&gt; from a previous reply to continue the same conversation, which lets you test follow-up questions.&lt;/p&gt;

&lt;p&gt;Write a short test set and have your agent run it:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Three questions the site answers clearly (pricing, a feature, a policy).&lt;/li&gt;
&lt;li&gt;Two questions it answers only in passing.&lt;/li&gt;
&lt;li&gt;Two questions it doesn't answer at all. The bot should say it doesn't know, not guess.&lt;/li&gt;
&lt;li&gt;One follow-up in the same session ("And for the annual plan?").&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Ten questions cost 10 credits. If an answer is wrong, the fix is usually the content, not the prompt: add a &lt;code&gt;text&lt;/code&gt; source with the correct fact, wait for &lt;code&gt;chatbot_status&lt;/code&gt;, and ask again.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 8: Put it on your site
&lt;/h2&gt;

&lt;p&gt;The MCP tools cover building, training and testing. To go live, open the chatbot in the Appie dashboard. Per &lt;a href="https://appie.run/ai-chatbots" rel="noopener noreferrer"&gt;appie.run/ai-chatbots&lt;/a&gt;, you embed it with a script tag (or the Shopify app embed), and the dashboard adds lead capture, CSV export and webhooks, and analytics.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prompts to paste
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;"List my Appie chatbots and their training status."&lt;/li&gt;
&lt;li&gt;"Create a chatbot for example.com, train it on &lt;a href="https://www.example.com/sitemap.xml" rel="noopener noreferrer"&gt;https://www.example.com/sitemap.xml&lt;/a&gt;, and tell me when training is done."&lt;/li&gt;
&lt;li&gt;"Add a text source titled Refund policy with this content: ..."&lt;/li&gt;
&lt;li&gt;"Ask the bot these eight questions, one by one, and give me a table of question, reply, and whether the reply is supported by our site."&lt;/li&gt;
&lt;li&gt;"Show credits_remaining after each paid call."&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What it costs
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;list_chatbots&lt;/code&gt;&lt;/strong&gt;: Credits (as of 28 Oct 2026): 0&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;create_chatbot&lt;/code&gt;&lt;/strong&gt;: Credits (as of 28 Oct 2026): 0&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;add_chatbot_content&lt;/code&gt;&lt;/strong&gt;: Credits (as of 28 Oct 2026): No cost stated in the docs&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;chatbot_status&lt;/code&gt;&lt;/strong&gt;: Credits (as of 28 Oct 2026): 0&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;ask_chatbot&lt;/code&gt;&lt;/strong&gt;: Credits (as of 28 Oct 2026): 1 per message&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Building a bot is free on the pricing page too; conversations use chatbot query credits. Paid plans are Starter at $19 a month (2,000 credits) and Standard at $49 a month (5,000 credits), per &lt;a href="https://appie.run/pricing" rel="noopener noreferrer"&gt;appie.run/pricing&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Troubleshooting
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;HTTP 401.&lt;/strong&gt; The key is missing, wrong, or was regenerated.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;app_not_allowed&lt;/code&gt;.&lt;/strong&gt; The person who generated the key can't use the chatbots app in that project. A project owner can use every app.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;insufficient_credits&lt;/code&gt;.&lt;/strong&gt; The response includes &lt;code&gt;required&lt;/code&gt; and &lt;code&gt;available&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;not_found&lt;/code&gt;.&lt;/strong&gt; The bot or article ID belongs to another project.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Answers ignore a page.&lt;/strong&gt; Check &lt;code&gt;chatbot_status&lt;/code&gt; to confirm the source was added, then ask about that page directly.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Errors come back as an MCP tool error whose text is JSON with &lt;code&gt;error&lt;/code&gt; and &lt;code&gt;message&lt;/code&gt;, so your agent can read and explain them.&lt;/p&gt;




&lt;p&gt;The same actions are also available as an HTTP API and CLI; see &lt;a href="https://appie.run/docs/mcp" rel="noopener noreferrer"&gt;appie.run/docs/mcp&lt;/a&gt;. The repo is on &lt;a href="https://github.com/appierun/appie-mcp" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;RG, Founder, Appie.run&lt;/p&gt;

</description>
      <category>mcp</category>
      <category>ai</category>
      <category>chatbot</category>
      <category>cursor</category>
    </item>
    <item>
      <title>How to Generate an XML Sitemap for a Static Site With Python (Skip noindex Pages, Split at 50,000 URLs)</title>
      <dc:creator>appierun</dc:creator>
      <pubDate>Wed, 28 Oct 2026 15:51:20 +0000</pubDate>
      <link>https://dev.to/appierun_66/how-to-generate-an-xml-sitemap-for-a-static-site-with-python-skip-noindex-pages-split-at-50000-2fak</link>
      <guid>https://dev.to/appierun_66/how-to-generate-an-xml-sitemap-for-a-static-site-with-python-skip-noindex-pages-split-at-50000-2fak</guid>
      <description>&lt;p&gt;Most static site generators can produce a sitemap, but the default output often includes pages you don't want indexed: drafts marked &lt;code&gt;noindex&lt;/code&gt;, duplicate pages that point a canonical elsewhere, and a &lt;code&gt;lastmod&lt;/code&gt; on every URL set to the moment the build ran. This post walks through a small Python script that builds &lt;code&gt;sitemap.xml&lt;/code&gt; from your build folder and avoids those three problems.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Disclosure: Appie.run publishes this post. Appie has a free XML Sitemap Generator, mentioned near the end. The script below has no dependency on it. Facts about the sitemap format come from sitemaps.org and Google Search Central, read on 28 Oct 2026.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;If you want to check an existing sitemap rather than build one, our earlier post &lt;a href="https://dev.to/appierun_66/how-to-validate-an-xml-sitemap-common-errors-what-google-ignores-and-a-small-python-checker-4feo"&gt;How to validate an XML sitemap&lt;/a&gt; covers the common errors.&lt;/p&gt;

&lt;h2&gt;
  
  
  The rules the script follows
&lt;/h2&gt;

&lt;p&gt;From the &lt;a href="https://www.sitemaps.org/protocol.html" rel="noopener noreferrer"&gt;sitemaps protocol&lt;/a&gt; and Google's &lt;a href="https://developers.google.com/search/docs/crawling-indexing/sitemaps/build-sitemap" rel="noopener noreferrer"&gt;Build and submit a sitemap&lt;/a&gt; page:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Size limits.&lt;/strong&gt; One sitemap holds at most 50,000 URLs or 50MB uncompressed. Past that, split it into several files and list them in a sitemap index.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;UTF-8 and escaping.&lt;/strong&gt; The file must be UTF-8. Tag values must be entity-escaped, and URLs must be URL-escaped.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Absolute URLs.&lt;/strong&gt; Use &lt;code&gt;https://www.example.com/page.html&lt;/code&gt;, not &lt;code&gt;/page.html&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Canonical URLs only.&lt;/strong&gt; List the URLs you want in search results.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;priority&lt;/code&gt; and &lt;code&gt;changefreq&lt;/code&gt;.&lt;/strong&gt; Google says it ignores both. The script leaves them out.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;lastmod&lt;/code&gt;.&lt;/strong&gt; Google uses it only if it's consistently and verifiably accurate, and it should reflect the last significant change to the page. A build timestamp on every page fails that test, so the script either uses a real date or omits the tag.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Location.&lt;/strong&gt; Unless you submit through Search Console, a sitemap only affects URLs under its own directory. Put it at the site root.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The script
&lt;/h2&gt;

&lt;p&gt;Save this as &lt;code&gt;gen_sitemap.py&lt;/code&gt;. It uses only the standard library (Python 3.8+).&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="c1"&gt;#!/usr/bin/env python3
&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Build sitemap.xml from a static site&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;s output folder (dist/, public/, _site/).&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;argparse&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;subprocess&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pathlib&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;urllib.parse&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;quote&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;xml.sax.saxutils&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;escape&lt;/span&gt;

&lt;span class="n"&gt;MAX_URLS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;50_000&lt;/span&gt;  &lt;span class="c1"&gt;# per sitemap file, per the sitemaps protocol
&lt;/span&gt;
&lt;span class="n"&gt;NOINDEX&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;meta[^&amp;gt;]+name=[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;\']robots[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;\'][^&amp;gt;]+content=[&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="s"&gt;\']*noindex&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;I&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;CANONICAL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;link[^&amp;gt;]+rel=[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;\']canonical[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;\'][^&amp;gt;]+href=[&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="s"&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="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;I&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;page_url&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;base&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;root&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="n"&gt;rel&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;relative_to&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;root&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;as_posix&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;rel&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;index.html&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;rel&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;
    &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;rel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;endswith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/index.html&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;rel&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;rel&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;index.html&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;  &lt;span class="c1"&gt;# keep trailing slash
&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;base&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;rstrip&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nf"&gt;quote&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rel&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# percent-encode spaces, non-ASCII
&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;last_modified&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="n"&gt;root&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;src_root&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Last git commit date of the matching source file, else None.

    Build output mtimes are all &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;now&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;, which makes lastmod useless, so we
    only trust git history for a file at the same relative path in --src.
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;src_root&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;src_file&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;src_root&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;relative_to&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;root&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;src_file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exists&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;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;out&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;subprocess&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;git&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;log&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--format=%cs&lt;/span&gt;&lt;span class="sh"&gt;"&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="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;relative_to&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;root&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;as_posix&lt;/span&gt;&lt;span class="p"&gt;()],&lt;/span&gt;
                &lt;span class="n"&gt;cwd&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;src_root&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;capture_output&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;check&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;stdout&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&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;out&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;out&lt;/span&gt;
        &lt;span class="nf"&gt;except &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;subprocess&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CalledProcessError&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;FileNotFoundError&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="k"&gt;pass&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;def&lt;/span&gt; &lt;span class="nf"&gt;collect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;root&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;base&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;src_root&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;entries&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;skipped&lt;/span&gt; &lt;span class="o"&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;for&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;root&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;rglob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;*.html&lt;/span&gt;&lt;span class="sh"&gt;"&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;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;404.html&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt;
        &lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;page_url&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;base&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;root&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="n"&gt;html&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read_text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;encoding&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ignore&lt;/span&gt;&lt;span class="sh"&gt;"&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;NOINDEX&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;html&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="n"&gt;skipped&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;noindex&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt;
        &lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;CANONICAL&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;html&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;m&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;group&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;rstrip&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;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;rstrip&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="n"&gt;skipped&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;canonical points to &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;group&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt;
        &lt;span class="n"&gt;entries&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;last_modified&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="n"&gt;root&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;src_root&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;entries&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;skipped&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;write_urlset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;entries&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;lines&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;?xml version=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;1.0&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; encoding=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;UTF-8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;?&amp;gt;&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
             &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;urlset xmlns=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http://www.sitemaps.org/schemas/sitemap/0.9&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;gt;&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lastmod&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;entries&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;tag&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;lastmod&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;lastmod&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;/lastmod&amp;gt;&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;lastmod&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;
        &lt;span class="n"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;  &amp;lt;url&amp;gt;&amp;lt;loc&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;escape&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;/loc&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;tag&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;/url&amp;gt;&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;/urlset&amp;gt;&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write_text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;encoding&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;ap&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;argparse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ArgumentParser&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;ap&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_argument&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;root&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;help&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;built site folder, e.g. dist&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;ap&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_argument&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--base&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;required&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;help&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://www.example.com&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;ap&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_argument&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--src&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;help&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;git checkout whose files mirror the output paths&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;args&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ap&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse_args&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="n"&gt;entries&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;skipped&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;collect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;root&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;base&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;src&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;why&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;skipped&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;skipped &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;why&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;chunks&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;entries&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;MAX_URLS&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;entries&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;MAX_URLS&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="p"&gt;[[]]&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;chunks&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;write_urlset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;chunks&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;root&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sitemap.xml&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;wrote sitemap.xml with &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;entries&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; URLs&lt;/span&gt;&lt;span class="sh"&gt;"&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;names&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;chunk&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;enumerate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;chunks&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sitemap-&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;.xml&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="nf"&gt;write_urlset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;root&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;names&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;today&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;today&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;isoformat&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;index&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;?xml version=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;1.0&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; encoding=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;UTF-8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;?&amp;gt;&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
             &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;sitemapindex xmlns=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http://www.sitemaps.org/schemas/sitemap/0.9&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;gt;&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;names&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;loc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;escape&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;base&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;rstrip&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;  &amp;lt;sitemap&amp;gt;&amp;lt;loc&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;loc&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;/loc&amp;gt;&amp;lt;lastmod&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;today&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;/lastmod&amp;gt;&amp;lt;/sitemap&amp;gt;&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;/sitemapindex&amp;gt;&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;root&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sitemap.xml&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;write_text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;encoding&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;wrote sitemap index with &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;names&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; files, &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;entries&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; URLs&lt;/span&gt;&lt;span class="sh"&gt;"&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;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What each part does
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;page_url&lt;/code&gt;&lt;/strong&gt; maps a file path to a URL. &lt;code&gt;index.html&lt;/code&gt; becomes the folder URL with a trailing slash, so &lt;code&gt;dist/blog/a-post/index.html&lt;/code&gt; becomes &lt;code&gt;https://www.example.com/blog/a-post/&lt;/code&gt;. Paths are percent-encoded, so &lt;code&gt;my page.html&lt;/code&gt; becomes &lt;code&gt;my%20page.html&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;collect&lt;/code&gt;&lt;/strong&gt; walks every &lt;code&gt;.html&lt;/code&gt; file and skips three kinds of page:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;404.html&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;pages with a robots meta tag containing &lt;code&gt;noindex&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;pages whose &lt;code&gt;rel="canonical"&lt;/code&gt; points to a different URL&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each skip is printed, so you can see why a page is missing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;last_modified&lt;/code&gt;&lt;/strong&gt; is the part most generators get wrong. Files in a build folder all have the build time as their modified date, which makes every &lt;code&gt;lastmod&lt;/code&gt; identical and meaningless. The script only sets &lt;code&gt;lastmod&lt;/code&gt; when you pass &lt;code&gt;--src&lt;/code&gt; with a git checkout whose files sit at the same relative paths as the output (true for hand-written HTML sites, often not true for generators that render Markdown). It then uses the date of the last commit touching that file. Otherwise it leaves &lt;code&gt;lastmod&lt;/code&gt; out, which is valid.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;write_urlset&lt;/code&gt;&lt;/strong&gt; escapes each URL with &lt;code&gt;xml.sax.saxutils.escape&lt;/code&gt;, so an &lt;code&gt;&amp;amp;&lt;/code&gt; in a query string becomes &lt;code&gt;&amp;amp;amp;&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;main&lt;/code&gt;&lt;/strong&gt; writes one &lt;code&gt;sitemap.xml&lt;/code&gt; when you have 50,000 URLs or fewer. Above that it writes &lt;code&gt;sitemap-1.xml&lt;/code&gt;, &lt;code&gt;sitemap-2.xml&lt;/code&gt; and so on, and makes &lt;code&gt;sitemap.xml&lt;/code&gt; a sitemap index pointing to them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Run it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# after your build step&lt;/span&gt;
python3 gen_sitemap.py dist &lt;span class="nt"&gt;--base&lt;/span&gt; https://www.example.com

&lt;span class="c"&gt;# with lastmod from git, for a site whose source paths match the output&lt;/span&gt;
python3 gen_sitemap.py dist &lt;span class="nt"&gt;--base&lt;/span&gt; https://www.example.com &lt;span class="nt"&gt;--src&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On a small test folder, the output looked like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;skipped https://www.example.com/about-copy.html: canonical points to https://www.example.com/about.html
skipped https://www.example.com/drafts/x.html: noindex
wrote sitemap.xml with 5 URLs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;?xml version="1.0" encoding="UTF-8"?&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;urlset&lt;/span&gt; &lt;span class="na"&gt;xmlns=&lt;/span&gt;&lt;span class="s"&gt;"http://www.sitemaps.org/schemas/sitemap/0.9"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;url&amp;gt;&amp;lt;loc&amp;gt;&lt;/span&gt;https://www.example.com/about.html&lt;span class="nt"&gt;&amp;lt;/loc&amp;gt;&amp;lt;/url&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;url&amp;gt;&amp;lt;loc&amp;gt;&lt;/span&gt;https://www.example.com/blog/a-post/&lt;span class="nt"&gt;&amp;lt;/loc&amp;gt;&amp;lt;/url&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;url&amp;gt;&amp;lt;loc&amp;gt;&lt;/span&gt;https://www.example.com/&lt;span class="nt"&gt;&amp;lt;/loc&amp;gt;&amp;lt;/url&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;url&amp;gt;&amp;lt;loc&amp;gt;&lt;/span&gt;https://www.example.com/my%20page.html&lt;span class="nt"&gt;&amp;lt;/loc&amp;gt;&amp;lt;/url&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;url&amp;gt;&amp;lt;loc&amp;gt;&lt;/span&gt;https://www.example.com/tom%26jerry.html&lt;span class="nt"&gt;&amp;lt;/loc&amp;gt;&amp;lt;/url&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/urlset&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To test the split, change &lt;code&gt;MAX_URLS&lt;/code&gt; to &lt;code&gt;2&lt;/code&gt; in a copy of the script. On the same folder it wrote three files plus an index.&lt;/p&gt;

&lt;h2&gt;
  
  
  Add it to your build
&lt;/h2&gt;

&lt;p&gt;In a &lt;code&gt;package.json&lt;/code&gt;:&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;"scripts"&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;"build"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"astro build &amp;amp;&amp;amp; python3 gen_sitemap.py dist --base https://www.example.com"&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;p&gt;Or as a CI step after the build, before deploy. Then add one line to &lt;code&gt;robots.txt&lt;/code&gt; so crawlers find it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Sitemap: https://www.example.com/sitemap.xml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Google's docs list three ways to submit: Search Console, the Search Console API, or the &lt;code&gt;Sitemap:&lt;/code&gt; line in robots.txt.&lt;/p&gt;

&lt;h2&gt;
  
  
  Known limits
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Regex HTML parsing.&lt;/strong&gt; The &lt;code&gt;noindex&lt;/code&gt; and canonical checks expect the attributes in the usual order (&lt;code&gt;name&lt;/code&gt; before &lt;code&gt;content&lt;/code&gt;, &lt;code&gt;rel&lt;/code&gt; before &lt;code&gt;href&lt;/code&gt;). If your templates write them the other way round, swap in &lt;code&gt;html.parser&lt;/code&gt; from the standard library.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No 50MB check.&lt;/strong&gt; At 50,000 short URLs you'll be well under 50MB, but if your URLs are very long, check the file size too.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Only HTML.&lt;/strong&gt; PDFs and other files you want indexed need adding by hand.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Trailing slashes.&lt;/strong&gt; The canonical comparison ignores a trailing slash. If your site treats &lt;code&gt;/page&lt;/code&gt; and &lt;code&gt;/page/&lt;/code&gt; as different pages, tighten that check.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  When a script is more than you need
&lt;/h2&gt;

&lt;p&gt;If you just need a one-off sitemap for a small site, a hosted tool is quicker:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Appie's free &lt;a href="https://appie.run/tools/sitemap-generator" rel="noopener noreferrer"&gt;XML Sitemap Generator&lt;/a&gt;&lt;/strong&gt; crawls a public site for up to 80 pages, or takes a pasted list of up to 50,000 URLs, and returns sitemap XML. A single free run stops at 80 HTML pages or after 12 seconds, so paste a URL list for larger sites. It asks you to choose &lt;code&gt;changefreq&lt;/code&gt; and &lt;code&gt;priority&lt;/code&gt;; since Google ignores both, any value is fine. There's a captcha before each run.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Screaming Frog SEO Spider&lt;/strong&gt; is a desktop crawler that can generate XML sitemaps. Per its site on 28 Oct 2026, it crawls 500 URLs for free, and a licence to remove the limit is £199 per year.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;After generating, run the file through a validator such as Appie's free &lt;a href="https://appie.run/tools/sitemap-validator" rel="noopener noreferrer"&gt;XML Sitemap Validator&lt;/a&gt;, which checks the root element, absolute URLs, &lt;code&gt;lastmod&lt;/code&gt;, duplicates and the 50,000 URL limit.&lt;/p&gt;

&lt;p&gt;RG, Founder, Appie.run&lt;/p&gt;

</description>
      <category>python</category>
      <category>seo</category>
      <category>webdev</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>How to Write a Website FAQ That AI Answers Can Quote (Now That Google's FAQ Rich Results Are Gone)</title>
      <dc:creator>appierun</dc:creator>
      <pubDate>Tue, 27 Oct 2026 15:50:42 +0000</pubDate>
      <link>https://dev.to/appierun_66/how-to-write-a-website-faq-that-ai-answers-can-quote-now-that-googles-faq-rich-results-are-gone-j28</link>
      <guid>https://dev.to/appierun_66/how-to-write-a-website-faq-that-ai-answers-can-quote-now-that-googles-faq-rich-results-are-gone-j28</guid>
      <description>&lt;p&gt;For years the main reason to add an FAQ section was the dropdown it could earn in Google results. That reason has gone. The page itself still matters, though, because AI assistants and AI search features pull short, direct answers from pages, and a well-built FAQ is one of the easiest things on a site for them to lift.&lt;/p&gt;

&lt;p&gt;This post covers what changed at Google, what still makes an FAQ worth writing, and the HTML pattern we use so each answer stands on its own.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Disclosure: Appie.run publishes this post. The AI FAQ Generator and Website FAQ Generator mentioned below are our free tools. Everything else here works with any CMS and no tool at all.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What Google changed (as read on 26 Oct 2026)
&lt;/h2&gt;

&lt;p&gt;From Google's Search Central updates and Search Console Help:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;FAQ rich results stopped appearing in Google Search on 7 May 2026, and Google added a deprecation notice on 8 May.&lt;/li&gt;
&lt;li&gt;On 15 June 2026 Google removed the FAQ structured data documentation, the FAQ report in Search Console and FAQ support in the Rich Results Test.&lt;/li&gt;
&lt;li&gt;Search Console API support for FAQ appearance data was removed in August 2026.&lt;/li&gt;
&lt;li&gt;Google says existing &lt;code&gt;FAQPage&lt;/code&gt; markup can stay on your pages and causes no harm.&lt;/li&gt;
&lt;li&gt;In the same June update Google noted that an &lt;code&gt;llms.txt&lt;/code&gt; file isn't needed for Google Search.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So: no more SERP dropdown, no penalty for leftover markup, and no special file required. What's left is the content.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why an FAQ is still worth writing
&lt;/h2&gt;

&lt;p&gt;Nobody outside the AI companies can tell you exactly how each assistant picks its sources, and we won't pretend to. What you can control is whether your answer is easy to find, easy to extract and correct when pulled out of context. A question-and-answer block does all three if you write it properly:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The question matches the way people actually ask it.&lt;/li&gt;
&lt;li&gt;The first sentence of the answer is the answer.&lt;/li&gt;
&lt;li&gt;The answer makes sense without the paragraphs around it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those same properties help a human who lands on the page from a support link, so the work isn't wasted whichever way the traffic arrives.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Collect real questions
&lt;/h2&gt;

&lt;p&gt;Don't invent questions in a meeting. Pull them from places where customers already asked:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Support tickets and live chat transcripts&lt;/li&gt;
&lt;li&gt;Sales call notes ("Does it work with...?", "What happens when...?")&lt;/li&gt;
&lt;li&gt;Reddit and community threads in your category&lt;/li&gt;
&lt;li&gt;Your site search logs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Group duplicates, then keep the 8 to 15 that come up most or block a purchase.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Write answers that survive being quoted alone
&lt;/h2&gt;

&lt;p&gt;A quick test for each answer: paste it into a blank doc with only the question above it. Does it still make sense?&lt;/p&gt;

&lt;p&gt;Weak:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Yes, as mentioned above, it does, depending on your plan.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Better:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Yes. Team plans include SSO via SAML. The Starter plan does not.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Rules we follow:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Answer in the first sentence.&lt;/strong&gt; Yes/no, a number, or a name.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Name the thing.&lt;/strong&gt; Say "the Starter plan", not "this plan".&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keep it to 40 to 80 words&lt;/strong&gt; for most answers. Link out for detail.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Date anything that changes.&lt;/strong&gt; "As of 26 Oct 2026, the Starter plan costs $19/month." Prices and limits go stale, and a stale quoted answer is worse than none.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Only state what the page can back up.&lt;/strong&gt; If the policy lives elsewhere, link to it rather than paraphrasing it loosely.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Step 3: Use plain, semantic HTML
&lt;/h2&gt;

&lt;p&gt;You don't need a plugin. Headings for questions and paragraphs for answers are enough, and they render the same for every crawler:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;section&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"faq"&lt;/span&gt; &lt;span class="na"&gt;aria-labelledby=&lt;/span&gt;&lt;span class="s"&gt;"faq-title"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;h2&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"faq-title"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Frequently asked questions&lt;span class="nt"&gt;&amp;lt;/h2&amp;gt;&lt;/span&gt;

  &lt;span class="nt"&gt;&amp;lt;h3&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"faq-sso"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Does the Starter plan include SSO?&lt;span class="nt"&gt;&amp;lt;/h3&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;p&amp;gt;&lt;/span&gt;No. SSO via SAML is included on Team plans only. The Starter plan
     uses email and password sign-in. &lt;span class="nt"&gt;&amp;lt;a&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"/pricing"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Compare plans&lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&lt;/span&gt;.&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;

  &lt;span class="nt"&gt;&amp;lt;h3&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"faq-cancel"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Can I cancel at any time?&lt;span class="nt"&gt;&amp;lt;/h3&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;p&amp;gt;&lt;/span&gt;Yes. You can cancel from the Billing page, and your plan stays active
     until the end of the current billing period.&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/section&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you prefer collapsible answers, &lt;code&gt;&amp;lt;details&amp;gt;&lt;/code&gt;/&lt;code&gt;&amp;lt;summary&amp;gt;&lt;/code&gt; keeps the text in the HTML:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;details&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;summary&amp;gt;&lt;/span&gt;Can I cancel at any time?&lt;span class="nt"&gt;&amp;lt;/summary&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;p&amp;gt;&lt;/span&gt;Yes. You can cancel from the Billing page, and your plan stays active
     until the end of the current billing period.&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/details&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Avoid loading answers only via JavaScript after a click. If the text isn't in the served HTML, some crawlers will never see it.&lt;/p&gt;

&lt;p&gt;Give each question an &lt;code&gt;id&lt;/code&gt; so you can link straight to it from support replies and docs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Keep or add FAQPage markup (optional)
&lt;/h2&gt;

&lt;p&gt;Google no longer shows FAQ rich results, and it says leftover markup does no harm. Other consumers of schema.org data may still read it, so if it's already there, keep it in sync with the visible text. If you add it, mirror the on-page wording exactly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"application/ld+json"&lt;/span&gt;&lt;span class="nt"&gt;&amp;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;@context&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;https://schema.org&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;@type&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;FAQPage&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;mainEntity&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@type&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;Question&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;name&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;Can I cancel at any time?&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;acceptedAnswer&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@type&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;Answer&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;text&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;Yes. You can cancel from the Billing page, and your plan stays active until the end of the current billing period.&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="p"&gt;}&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The risk with markup is drift: someone updates the visible answer and forgets the JSON. If your FAQ comes from a CMS field, generate both from the same source.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5: Put the FAQ where the question is asked
&lt;/h2&gt;

&lt;p&gt;One giant /faq page is fine as an index, but the most useful placement is next to the thing in question: pricing questions on the pricing page, setup questions in the docs. A short block of 4 to 6 questions per page tends to stay accurate because the owner of that page sees it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 6: Review it on a schedule
&lt;/h2&gt;

&lt;p&gt;Add a "last reviewed" date and put a recurring reminder on someone's calendar. Every price, limit and integration claim in the FAQ should be checked against the product at least each quarter.&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting a first draft faster
&lt;/h2&gt;

&lt;p&gt;If you have the source material but not the time, two of our free browser tools can produce a draft for you to edit (both run a captcha check first):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;AI FAQ Generator&lt;/strong&gt; (appie.run/tools/ai-faq-generator): paste up to 2,000 characters of text, pick 1 to 10 FAQs, a language and a tone. The answers are grounded in the text you pasted, and it doesn't add policies or prices that aren't there.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Website FAQ Generator&lt;/strong&gt; (appie.run/tools/website-faq-generator): give it up to 5 public URLs. It skips questions it can't answer from those pages instead of guessing.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Treat the output as a draft. Check every answer against Step 2's rules before it goes live.&lt;/p&gt;

&lt;h2&gt;
  
  
  Checklist
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Questions come from real customer sources&lt;/li&gt;
&lt;li&gt;[ ] Each answer starts with the answer&lt;/li&gt;
&lt;li&gt;[ ] Each answer makes sense on its own&lt;/li&gt;
&lt;li&gt;[ ] Prices and limits carry an "as of" date&lt;/li&gt;
&lt;li&gt;[ ] Questions and answers are in the served HTML&lt;/li&gt;
&lt;li&gt;[ ] Any FAQPage JSON-LD matches the visible text word for word&lt;/li&gt;
&lt;li&gt;[ ] A review date is set&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Google sources: Search Central blog and Search Console Help pages on FAQ rich results and structured data, read 26 Oct 2026.&lt;/p&gt;

&lt;p&gt;RG, Founder, Appie.run&lt;/p&gt;

</description>
      <category>seo</category>
      <category>html</category>
      <category>webdev</category>
      <category>ai</category>
    </item>
    <item>
      <title>How to Convert Notion to Markdown (Export, Public Pages and the API), With a Cleanup Script</title>
      <dc:creator>appierun</dc:creator>
      <pubDate>Mon, 26 Oct 2026 15:54:40 +0000</pubDate>
      <link>https://dev.to/appierun_66/how-to-convert-notion-to-markdown-export-public-pages-and-the-api-with-a-cleanup-script-3gmc</link>
      <guid>https://dev.to/appierun_66/how-to-convert-notion-to-markdown-export-public-pages-and-the-api-with-a-cleanup-script-3gmc</guid>
      <description>&lt;p&gt;You wrote the docs in Notion. Now you want them as &lt;code&gt;.md&lt;/code&gt; files: for a docs site, for a Git repo, or for an LLM pipeline that reads Markdown far more happily than it reads a Notion URL. There are three practical ways to get there, and each one fits a different situation:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Notion's built-in "Markdown &amp;amp; CSV" export&lt;/strong&gt;, for private pages and whole page trees.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A public-page converter in the browser&lt;/strong&gt;, for a page that's already published.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Notion API with &lt;code&gt;notion-to-md&lt;/code&gt;&lt;/strong&gt;, for repeatable, scripted conversion.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The built-in export is the most common route, and it leaves a few rough edges, so this post also includes a small Python script that tidies the export folder.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Disclosure: Appie.run publishes this post, and the public-page converter in Method 2 is our free tool. Methods 1 and 3 don't need anything from us.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Notion behaviour below is from Notion's Help Center and developer docs as read on 25 Oct 2026. Notion changes its menus from time to time, so check the linked pages if a label doesn't match.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which method should you use?
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Built-in export&lt;/strong&gt;: Needs an account or token: Notion account with access to the page; Private pages: Yes; Subpages and databases: Subpages as separate files; databases as CSV plus a &lt;code&gt;.md&lt;/code&gt; per row; Best for: One-off exports, backups, migrating a wiki&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Public-page converter&lt;/strong&gt;: Needs an account or token: The page must be published to the web; Private pages: No; Subpages and databases: The published page's text; Best for: Quick copy of a page that's already public&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;API + &lt;code&gt;notion-to-md&lt;/code&gt;&lt;/strong&gt;: Needs an account or token: Notion API token; Private pages: Yes, if the token can see the page; Subpages and databases: Child pages returned as separate strings; Best for: Docs pipelines, scheduled syncs, CI&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you only need to do this once, start with Method 1. If you'll do it every week, go straight to Method 3.&lt;/p&gt;

&lt;h2&gt;
  
  
  Method 1: Notion's built-in Markdown &amp;amp; CSV export
&lt;/h2&gt;

&lt;p&gt;According to &lt;a href="https://www.notion.com/help/export-your-content" rel="noopener noreferrer"&gt;Notion's export guide&lt;/a&gt;, any non-database page can be exported as a Markdown file. Full-page databases export as a CSV file, with a Markdown file for each subpage.&lt;/p&gt;

&lt;p&gt;On desktop:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Open the page and click the &lt;code&gt;•••&lt;/code&gt; menu at the top right.&lt;/li&gt;
&lt;li&gt;Click &lt;code&gt;Export&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Choose &lt;code&gt;Markdown &amp;amp; CSV&lt;/code&gt; from the format dropdown.&lt;/li&gt;
&lt;li&gt;Switch on &lt;code&gt;Include subpages&lt;/code&gt; if you want the pages nested under this one.&lt;/li&gt;
&lt;li&gt;Click &lt;code&gt;Export&lt;/code&gt;. You get a zip file with the Markdown and CSV files.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Mobile works the same way: &lt;code&gt;•••&lt;/code&gt;, then &lt;code&gt;Export&lt;/code&gt;, then pick &lt;code&gt;Markdown &amp;amp; CSV&lt;/code&gt; under &lt;code&gt;Export format&lt;/code&gt;. Notion also emails you a download link. For very large exports, Notion says it may email a link rather than start the download straight away.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Whole workspace.&lt;/strong&gt; Notion's guide lists &lt;code&gt;Settings&lt;/code&gt; &amp;gt; &lt;code&gt;Workspace&lt;/code&gt; &amp;gt; &lt;code&gt;General&lt;/code&gt; &amp;gt; &lt;code&gt;Export all workspace content&lt;/code&gt;. The download link arrives by email (or in your Notion inbox if email notifications are off), expires after 7 days, and Notion says the export can take up to 30 hours for large workspaces. Pages the exporter can't access, such as other people's private pages, are left out. There's also a beta admin API for starting workspace exports on the Enterprise Plan.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No Export option?&lt;/strong&gt; On Enterprise workspaces, an owner can disable exporting. Guests need &lt;code&gt;Full access&lt;/code&gt; to a page to export it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Clean up the export
&lt;/h2&gt;

&lt;p&gt;Unzip the export and open a file. The content is fine, but a few things get in the way of using it in a repo or docs site:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;File and folder names often carry a long ID&lt;/strong&gt; after the page title, for example &lt;code&gt;Team Wiki 1a2b3c4d5e6f708192a3b4c5d6e7f809.md&lt;/code&gt;. Links between pages point at those names, URL-encoded.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Callouts come out as HTML.&lt;/strong&gt; Notion's guide says callout blocks are exported as HTML because Markdown has no equivalent. Static site generators and Markdown linters may not render that the way you want.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here's a script that strips the ID suffix from file and folder names, rewrites relative links to match, and turns callouts into blockquotes. It only uses the Python standard library.&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="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Tidy a Notion &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Markdown &amp;amp; CSV&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; export: strip ID suffixes, fix links, convert callouts.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;pathlib&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;urllib.parse&lt;/span&gt;

&lt;span class="n"&gt;ID&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; [0-9a-f]{32}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# " &amp;lt;32 hex chars&amp;gt;" after the page title
&lt;/span&gt;&lt;span class="n"&gt;root&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pathlib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;argv&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

&lt;span class="c1"&gt;# 1. Rename files and folders, deepest paths first so parents rename last.
&lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;root&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;rglob&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="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;parts&lt;/span&gt;&lt;span class="p"&gt;)):&lt;/span&gt;
    &lt;span class="n"&gt;new&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;with_name&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ID&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&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;new&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;continue&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;new&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exists&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;skip (name taken): &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;continue&lt;/span&gt;
    &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;rename&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;new&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;fix_link&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;target&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;group&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&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="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;target&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startswith&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;mailto:&lt;/span&gt;&lt;span class="sh"&gt;"&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;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;group&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# leave external links and anchors alone
&lt;/span&gt;    &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;anchor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;partition&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="n"&gt;path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;quote&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ID&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;unquote&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;](&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="o"&gt;+&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="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;anchor&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;anchor&lt;/span&gt; &lt;span class="k"&gt;else&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;fix_callout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;lines&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;group&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;splitlines&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;gt; &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;gt;&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# 2. Rewrite relative links and turn &amp;lt;aside&amp;gt; callouts into blockquotes.
&lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;md&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;root&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;rglob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;*.md&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;md&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read_text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;encoding&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;\]\(([^)\s]+)\)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fix_link&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;aside&amp;gt;(.*?)&amp;lt;/aside&amp;gt;&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fix_callout&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;flags&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;S&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;md&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write_text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;encoding&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cleaned: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;md&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;relative_to&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;root&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run it on a &lt;strong&gt;copy&lt;/strong&gt; of the unzipped folder, since it renames files in place:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cp&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s2"&gt;"Export-folder"&lt;/span&gt; notion-md
python3 clean_notion_export.py notion-md
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We ran it with Python 3.13 on a small sample export we built by hand to mimic Notion's format, not a real Notion export: a parent page, a subpage, a database CSV and an image, all with ID suffixes, plus an external link and a two-line callout. Before:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;Start with &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;Onboarding&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;&lt;span class="sx"&gt;Team%20Wiki%201a2b3c4d5e6f708192a3b4c5d6e7f809/Onboarding%200f1e2d3c4b5a69788796a5b4c3d2e1f0.md&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;.

See our &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;site&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;&lt;span class="sx"&gt;https://example.com/docs?a=1&amp;amp;b=2#top&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;.

&lt;span class="nt"&gt;&amp;lt;aside&amp;gt;&lt;/span&gt;
💡 Callout text
Second line of the callout.

&lt;span class="nt"&gt;&amp;lt;/aside&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;Start with &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;Onboarding&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;&lt;span class="sx"&gt;Team%20Wiki/Onboarding.md&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;.

See our &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;site&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;&lt;span class="sx"&gt;https://example.com/docs?a=1&amp;amp;b=2#top&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;.
&lt;span class="gt"&gt;
&amp;gt; 💡 Callout text&lt;/span&gt;
&lt;span class="gt"&gt;&amp;gt; Second line of the callout.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every relative link in the sample still resolved to a real file after the rename, and the external URL was left untouched.&lt;/p&gt;

&lt;p&gt;Two things to check on your own export before you trust the script:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The ID pattern.&lt;/strong&gt; The script expects a space followed by 32 lowercase hex characters. Open your zip and compare. If your names look different, change the &lt;code&gt;ID&lt;/code&gt; regex.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The callout HTML.&lt;/strong&gt; The script assumes callouts are wrapped in &lt;code&gt;&amp;lt;aside&amp;gt;&lt;/code&gt; tags. Open one exported callout in a text editor; if Notion used different markup, change the second &lt;code&gt;re.sub&lt;/code&gt; pattern.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The script also skips a rename when the clean name is already taken (two sibling pages with the same title, for example), so nothing gets overwritten. Fix those by hand.&lt;/p&gt;

&lt;h2&gt;
  
  
  Method 2: Convert a public Notion page in the browser
&lt;/h2&gt;

&lt;p&gt;If the page is already published, you don't need an export at all. Our free &lt;a href="https://appie.run/tools/convert-notion-to-markdown" rel="noopener noreferrer"&gt;Convert Notion to Markdown&lt;/a&gt; tool takes a public &lt;code&gt;notion.so&lt;/code&gt; or &lt;code&gt;notion.site&lt;/code&gt; URL and converts the published text to Markdown.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Paste the public URL.&lt;/li&gt;
&lt;li&gt;Complete the captcha (Cloudflare Turnstile, one check per run).&lt;/li&gt;
&lt;li&gt;Copy the Markdown or download it.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The limit is in the name: it reads what the public page exposes. A private Notion URL doesn't expose any text to read, so for private pages you'd either publish the page first or use Method 1 or 3. It also works on one page at a time, so for a tree of subpages the built-in export is the better fit.&lt;/p&gt;

&lt;h2&gt;
  
  
  Method 3: Use the Notion API with notion-to-md
&lt;/h2&gt;

&lt;p&gt;For anything you'll repeat, such as syncing a docs space into a repo every night, use the API. &lt;a href="https://github.com/souvikinator/notion-to-md" rel="noopener noreferrer"&gt;&lt;code&gt;notion-to-md&lt;/code&gt;&lt;/a&gt; is an open-source package that turns Notion blocks into Markdown. As of 25 Oct 2026, the latest version on npm is 3.1.9 (a 4.0 alpha is also published; the code below is for 3.x).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Get a token.&lt;/strong&gt; Notion's developer docs describe two simple options:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A &lt;strong&gt;personal access token&lt;/strong&gt;, created in the Developer portal, which acts as you and uses your permissions. Notion notes that on Business and Enterprise plans, creating one may need a workspace owner to enable it.&lt;/li&gt;
&lt;li&gt;An &lt;strong&gt;internal connection&lt;/strong&gt; token, for team-owned automations. Notion says the page must be shared with the connection first: open the page, click &lt;code&gt;•••&lt;/code&gt;, choose &lt;code&gt;Add connections&lt;/code&gt;, and pick your connection. Without that, API requests return an error.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Keep the token in an environment variable, not in your code.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Install the packages.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install &lt;/span&gt;notion-to-md @notionhq/client
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;3. Convert a page.&lt;/strong&gt; This follows the package README:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// convert.js&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Client&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@notionhq/client&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="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;NotionToMarkdown&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;notion-to-md&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;fs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;fs&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;notion&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;Client&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;auth&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;NOTION_API_KEY&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;n2m&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;NotionToMarkdown&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;notionClient&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;notion&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;pageId&lt;/span&gt; &lt;span class="o"&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;argv&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt; &lt;span class="c1"&gt;// the page ID, taken from the page URL&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;mdblocks&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;n2m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pageToMarkdown&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pageId&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;mdString&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;n2m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toMarkdownString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;mdblocks&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;writeFileSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;page.md&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;mdString&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;parent&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;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Wrote page.md&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;NOTION_API_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;your_token node convert.js &amp;lt;page-id&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;toMarkdownString&lt;/code&gt; returns an object. The page itself is in &lt;code&gt;.parent&lt;/code&gt;. Since v2.7.0 the package doesn't write child pages to disk for you; it returns their Markdown in the same object, so loop over it and save them yourself if you need them.&lt;/p&gt;

&lt;p&gt;We checked the method names against version 3.1.9 by running the script above with a stubbed Notion client that returned a heading, paragraph, bullet, to-do and callout. Output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# Team Wiki&lt;/span&gt;

Hello from Notion.
&lt;span class="p"&gt;
-&lt;/span&gt; First point
&lt;span class="gt"&gt;
&amp;gt; 💡 Callout text&lt;/span&gt;
&lt;span class="p"&gt;
-&lt;/span&gt; [x] Ship docs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note that &lt;code&gt;notion-to-md&lt;/code&gt; rendered the callout as a blockquote, so API output doesn't need the callout fix from the cleanup script. Run it against one real page of yours before wiring it into CI, since a stub can't catch permission errors.&lt;/p&gt;

&lt;h2&gt;
  
  
  Gotchas
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Callouts are HTML in the export.&lt;/strong&gt; Covered above. The API route with &lt;code&gt;notion-to-md&lt;/code&gt; gave us blockquotes instead.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Databases come out as CSV.&lt;/strong&gt; A full-page database exports as a CSV plus one Markdown file per row (subpage). If you want the table itself in Markdown, convert the CSV. Our free &lt;a href="https://appie.run/tools/convert-csv-to-markdown" rel="noopener noreferrer"&gt;CSV to Markdown&lt;/a&gt; tool takes the first row as the header and returns a Markdown table. Notion also notes you can export only the current view or the default view of a database, not all views at once.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Windows and long paths.&lt;/strong&gt; Notion's guide says Windows may fail to open the zip when nested folder and file names pass the 260-character path limit. Its suggested fixes: uncheck &lt;code&gt;Create folders for subpages&lt;/code&gt; when exporting, or extract with 7-Zip. The cleanup script shortens names too, but only after you've extracted the files.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Images and attachments.&lt;/strong&gt; Check where the image files landed and whether the image links still resolve after you move the folder. The cleanup script rewrites relative image links along with page links.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Private pages.&lt;/strong&gt; The public-URL converter can't see them. Use the export or the API.&lt;/p&gt;

&lt;h2&gt;
  
  
  Using the Markdown for RAG or a chatbot
&lt;/h2&gt;

&lt;p&gt;Clean Markdown from Notion makes a decent knowledge source because headings survive, and headings are natural split points. For chunking and what to strip before indexing, see our guide on &lt;a href="https://dev.to/appierun_66/how-to-convert-pdfs-to-markdown-for-rag-and-chatbots-and-when-not-to-307d"&gt;converting PDFs to Markdown for RAG&lt;/a&gt;; the same advice applies here.&lt;/p&gt;

&lt;p&gt;To turn a published Notion FAQ or help page into question-and-answer pairs, our free &lt;a href="https://appie.run/tools/notion-to-faq-generator" rel="noopener noreferrer"&gt;Notion to FAQ Generator&lt;/a&gt; writes FAQs from a public Notion URL, and it's instructed to skip questions the page can't answer. For the full setup of a site chatbot on your docs, see &lt;a href="https://appierun.substack.com/p/how-to-set-up-an-ai-chatbot-for-your" rel="noopener noreferrer"&gt;How to set up an AI chatbot for your website FAQ&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Can Notion export to Markdown?&lt;/strong&gt;&lt;br&gt;
Yes. Use &lt;code&gt;•••&lt;/code&gt; &amp;gt; &lt;code&gt;Export&lt;/code&gt; and choose &lt;code&gt;Markdown &amp;amp; CSV&lt;/code&gt;. Non-database pages export as Markdown; full-page databases export as CSV with a Markdown file per subpage.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why are my Notion callouts HTML after export?&lt;/strong&gt;&lt;br&gt;
Notion exports callouts as HTML because Markdown has no equivalent. The cleanup script above turns &lt;code&gt;&amp;lt;aside&amp;gt;&lt;/code&gt; callouts into blockquotes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How do I convert a Notion database to Markdown?&lt;/strong&gt;&lt;br&gt;
It exports as a CSV plus a &lt;code&gt;.md&lt;/code&gt; file for each subpage. Convert the CSV into a Markdown table with a script or a CSV-to-Markdown converter.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can I convert a private Notion page without exporting?&lt;/strong&gt;&lt;br&gt;
Not with a public-URL converter, since a private page exposes no text. Use the built-in export or the API with a token that can see the page.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does notion-to-md need the page to be shared?&lt;/strong&gt;&lt;br&gt;
With an internal connection token, yes: add the connection to the page via &lt;code&gt;•••&lt;/code&gt; &amp;gt; &lt;code&gt;Add connections&lt;/code&gt;. A personal access token acts as you and uses your own permissions.&lt;/p&gt;




&lt;p&gt;Got a public Notion page? Paste the URL into our free converter and copy the Markdown: &lt;a href="https://appie.run/tools/convert-notion-to-markdown" rel="noopener noreferrer"&gt;appie.run/tools/convert-notion-to-markdown&lt;/a&gt;. To turn those docs into a site chatbot, &lt;a href="https://appie.run/ai-chatbots" rel="noopener noreferrer"&gt;Appie's AI Chatbots&lt;/a&gt; start on the Free plan (500 credits, no credit card needed).&lt;/p&gt;

&lt;p&gt;If you use the AI agent and approval patterns from our earlier post, &lt;a href="https://dev.to/appierun_66/how-to-add-human-approval-to-ai-agent-actions-draft-review-execute-ogn"&gt;how to add human approval to AI agent actions&lt;/a&gt;, a Notion-to-Markdown sync is a good low-risk job to automate first: it reads, it doesn't write back.&lt;/p&gt;

</description>
      <category>notion</category>
      <category>markdown</category>
      <category>python</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>How to Validate an XML Sitemap: Common Errors, What Google Ignores, and a Small Python Checker</title>
      <dc:creator>appierun</dc:creator>
      <pubDate>Sun, 25 Oct 2026 15:51:07 +0000</pubDate>
      <link>https://dev.to/appierun_66/how-to-validate-an-xml-sitemap-common-errors-what-google-ignores-and-a-small-python-checker-4feo</link>
      <guid>https://dev.to/appierun_66/how-to-validate-an-xml-sitemap-common-errors-what-google-ignores-and-a-small-python-checker-4feo</guid>
      <description>&lt;p&gt;Sitemaps are one of those files nobody looks at until something breaks. A deploy changes the URL structure, a plugin starts emitting relative paths, a query string sneaks in an unescaped &lt;code&gt;&amp;amp;&lt;/code&gt;, and the sitemap quietly stops parsing. Search Console tells you eventually. A check in CI tells you before you ship.&lt;/p&gt;

&lt;p&gt;This post covers what the sitemap protocol actually requires, which fields Google says it ignores, the errors that show up most often in real sitemaps, and a small Python script you can drop into a pipeline. Everything protocol-related below is from &lt;a href="https://www.sitemaps.org/protocol.html" rel="noopener noreferrer"&gt;sitemaps.org&lt;/a&gt; and Google's &lt;a href="https://developers.google.com/search/docs/crawling-indexing/sitemaps/build-sitemap" rel="noopener noreferrer"&gt;Build and submit a sitemap&lt;/a&gt; page, as read on 24 Oct 2026.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Disclosure: Appie.run publishes this post. We mention our free sitemap tools near the end as an option if you'd rather not run a script; the code here works without them.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What the protocol requires
&lt;/h2&gt;

&lt;p&gt;A minimal valid sitemap looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;?xml version="1.0" encoding="UTF-8"?&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;urlset&lt;/span&gt; &lt;span class="na"&gt;xmlns=&lt;/span&gt;&lt;span class="s"&gt;"http://www.sitemaps.org/schemas/sitemap/0.9"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;url&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;loc&amp;gt;&lt;/span&gt;https://www.example.com/foo.html&lt;span class="nt"&gt;&amp;lt;/loc&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;lastmod&amp;gt;&lt;/span&gt;2026-10-20&lt;span class="nt"&gt;&amp;lt;/lastmod&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/url&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/urlset&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The rules worth encoding in a checker:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Root element.&lt;/strong&gt; Either &lt;code&gt;&amp;lt;urlset&amp;gt;&lt;/code&gt; (a list of pages) or &lt;code&gt;&amp;lt;sitemapindex&amp;gt;&lt;/code&gt; (a list of sitemaps), in the &lt;code&gt;http://www.sitemaps.org/schemas/sitemap/0.9&lt;/code&gt; namespace.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Encoding.&lt;/strong&gt; The file must be UTF-8.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Size.&lt;/strong&gt; No more than 50,000 URLs and no larger than 50MB (52,428,800 bytes) uncompressed per file. A sitemap index has the same limits for the number of sitemaps it lists.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;&amp;lt;loc&amp;gt;&lt;/code&gt;.&lt;/strong&gt; Required. Google asks for fully qualified, absolute URLs, and sitemaps.org says the value must be less than 2,048 characters.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Escaping.&lt;/strong&gt; All values must be entity-escaped, so &lt;code&gt;&amp;amp;&lt;/code&gt; becomes &lt;code&gt;&amp;amp;amp;&lt;/code&gt;, and URLs must be URL-escaped.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;&amp;lt;lastmod&amp;gt;&lt;/code&gt;.&lt;/strong&gt; Optional, in W3C Datetime format: &lt;code&gt;2026-10-20&lt;/code&gt; or &lt;code&gt;2026-10-20T10:00:00+00:00&lt;/code&gt;, for example.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;&amp;lt;changefreq&amp;gt;&lt;/code&gt;.&lt;/strong&gt; Optional, one of &lt;code&gt;always&lt;/code&gt;, &lt;code&gt;hourly&lt;/code&gt;, &lt;code&gt;daily&lt;/code&gt;, &lt;code&gt;weekly&lt;/code&gt;, &lt;code&gt;monthly&lt;/code&gt;, &lt;code&gt;yearly&lt;/code&gt;, &lt;code&gt;never&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;&amp;lt;priority&amp;gt;&lt;/code&gt;.&lt;/strong&gt; Optional, from 0.0 to 1.0.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One host.&lt;/strong&gt; sitemaps.org says all URLs in a sitemap must be from a single host, and that a sitemap's location limits which URLs it can include: a file at &lt;code&gt;/catalog/sitemap.xml&lt;/code&gt; can list URLs under &lt;code&gt;/catalog/&lt;/code&gt;, not the rest of the site.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What Google says it ignores (and what it uses)
&lt;/h2&gt;

&lt;p&gt;This changes how much effort the optional fields deserve. Google's documentation says:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Google ignores &lt;code&gt;&amp;lt;priority&amp;gt;&lt;/code&gt; and &lt;code&gt;&amp;lt;changefreq&amp;gt;&lt;/code&gt;.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Google uses &lt;code&gt;&amp;lt;lastmod&amp;gt;&lt;/code&gt; if it's consistently and verifiably accurate,&lt;/strong&gt; for example when it matches the page's real last modification. Google says it should reflect the last significant update (main content, structured data, links), and that a copyright date change doesn't count.&lt;/li&gt;
&lt;li&gt;Sitemaps should list the URLs you want to see in search results, which in practice means canonical URLs.&lt;/li&gt;
&lt;li&gt;A sitemap at the site root can affect all files on the site, which is where Google recommends putting it. Placed elsewhere, it only affects descendants of its directory unless you submit it through Search Console.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The practical takeaway: don't spend time tuning priority values. Spend it making &lt;code&gt;lastmod&lt;/code&gt; honest. A generator that stamps every URL with today's date on every build is telling Google nothing useful.&lt;/p&gt;

&lt;p&gt;Other search engines may treat these fields differently, so keeping priority and changefreq valid costs nothing. Just don't expect them to move anything on Google.&lt;/p&gt;

&lt;h2&gt;
  
  
  The errors we see most
&lt;/h2&gt;

&lt;p&gt;These are the problems that come up again and again when people paste sitemaps into a validator.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Relative URLs
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;url&amp;gt;&amp;lt;loc&amp;gt;&lt;/span&gt;/pricing&lt;span class="nt"&gt;&amp;lt;/loc&amp;gt;&amp;lt;/url&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Usually a templating bug: the generator uses the path where it should use the full URL. Fix it with the absolute URL, including scheme and host.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Unescaped ampersands
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;loc&amp;gt;&lt;/span&gt;https://example.com/search?q=shoes&lt;span class="err"&gt;&amp;amp;&lt;/span&gt;page=2&lt;span class="nt"&gt;&amp;lt;/loc&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This breaks the XML parser, which means the entire file fails, not one entry. It must be &lt;code&gt;&amp;amp;amp;&lt;/code&gt;. If your sitemap is built with string concatenation instead of an XML library, this is the bug waiting to happen.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Invalid lastmod
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;lastmod&amp;gt;&lt;/span&gt;20/10/2026&lt;span class="nt"&gt;&amp;lt;/lastmod&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;lastmod&amp;gt;&lt;/span&gt;Oct 20, 2026&lt;span class="nt"&gt;&amp;lt;/lastmod&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both are invalid. Use &lt;code&gt;YYYY-MM-DD&lt;/code&gt; or a full timestamp with a timezone.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Out-of-range priority or made-up changefreq
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;priority&amp;gt;&lt;/span&gt;2&lt;span class="nt"&gt;&amp;lt;/priority&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;changefreq&amp;gt;&lt;/span&gt;often&lt;span class="nt"&gt;&amp;lt;/changefreq&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Google ignores both fields, but invalid values are still protocol errors and some tools will flag the file.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Duplicates and mixed hosts
&lt;/h3&gt;

&lt;p&gt;The same URL listed twice, or a mix of &lt;code&gt;example.com&lt;/code&gt; and &lt;code&gt;www.example.com&lt;/code&gt;. Duplicates are harmless but noisy. Mixed hosts usually mean your canonical setup and your sitemap disagree, and that's worth fixing at the source.&lt;/p&gt;

&lt;h3&gt;
  
  
  6. Too big for one file
&lt;/h3&gt;

&lt;p&gt;A growing catalogue crosses 50,000 URLs and nobody splits the file. Split it into several sitemaps and list them in a sitemap index.&lt;/p&gt;

&lt;h3&gt;
  
  
  7. Listing URLs you don't want indexed
&lt;/h3&gt;

&lt;p&gt;Redirects, 404s, &lt;code&gt;noindex&lt;/code&gt; pages and non-canonical variants. These aren't XML errors, so a schema check won't catch them. You need to request the URLs (see the optional step below).&lt;/p&gt;

&lt;h2&gt;
  
  
  A small Python checker
&lt;/h2&gt;

&lt;p&gt;This script uses only the standard library. It takes a file path or a URL, checks the rules above, prints errors and warnings, and exits non-zero on errors so it can fail a CI job.&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="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;urllib.request&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;urllib.parse&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;urlparse&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;xml.etree.ElementTree&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;ET&lt;/span&gt;

&lt;span class="n"&gt;NS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;{http://www.sitemaps.org/schemas/sitemap/0.9}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;MAX_URLS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;50_000&lt;/span&gt;
&lt;span class="n"&gt;MAX_BYTES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;52_428_800&lt;/span&gt;  &lt;span class="c1"&gt;# 50MB uncompressed
&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;source&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;source&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startswith&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http://&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)):&lt;/span&gt;
        &lt;span class="n"&gt;req&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;User-Agent&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sitemap-check/1.0&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
        &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;urlopen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;resp&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;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rb&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;f&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;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;valid_w3c_date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;fmt&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;%Y&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;%Y-%m&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;%Y-%m-%d&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;%Y-%m-%dT%H:%M:%S%z&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;%Y-%m-%dT%H:%M%z&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;%Y-%m-%dT%H:%M:%S.%f%z&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strptime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Z&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;+00:00&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;fmt&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;True&lt;/span&gt;
        &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;pass&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;check&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;raw&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;warnings&lt;/span&gt; &lt;span class="o"&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="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;MAX_BYTES&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;File is &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; bytes; the limit is &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;MAX_BYTES&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; uncompressed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;UnicodeDecodeError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;File is not valid UTF-8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;root&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ET&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fromstring&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;ET&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ParseError&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;XML does not parse: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;warnings&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;root&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tag&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;NS&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sitemapindex&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;child&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;kind&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sitemap&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;index&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;root&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tag&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;NS&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;urlset&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;child&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;kind&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;url&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;urlset&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Root element is &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;root&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tag&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;; expected urlset or sitemapindex in the sitemaps.org namespace&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;warnings&lt;/span&gt;

    &lt;span class="n"&gt;entries&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;root&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findall&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;NS&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;child&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;entries&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;MAX_URLS&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;entries&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; entries; the limit is &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;MAX_URLS&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; per file&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;seen&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;hosts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;entry&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;enumerate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;entries&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;loc_el&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;NS&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;loc&lt;/span&gt;&lt;span class="sh"&gt;"&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;loc_el&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;loc_el&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
            &lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Entry &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;: missing &amp;lt;loc&amp;gt;&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt;
        &lt;span class="n"&gt;loc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;loc_el&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;parsed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;urlparse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;loc&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;parsed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;scheme&lt;/span&gt; &lt;span class="ow"&gt;not&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;http&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;parsed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;netloc&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Entry &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;: &amp;lt;loc&amp;gt; is not an absolute URL: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;loc&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;loc&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;2048&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Entry &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;: &amp;lt;loc&amp;gt; is 2,048 characters or longer&lt;/span&gt;&lt;span class="sh"&gt;"&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;loc&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;seen&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;warnings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Entry &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;: duplicate URL &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;loc&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;seen&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;loc&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;parsed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;netloc&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;hosts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;parsed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;netloc&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="n"&gt;lastmod&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;NS&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;lastmod&lt;/span&gt;&lt;span class="sh"&gt;"&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;lastmod&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;valid_w3c_date&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;lastmod&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()):&lt;/span&gt;
            &lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Entry &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;: &amp;lt;lastmod&amp;gt; is not a W3C Datetime: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;lastmod&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&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;kind&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;urlset&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;cf&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;NS&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;changefreq&lt;/span&gt;&lt;span class="sh"&gt;"&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;cf&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="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="ow"&gt;not&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;always&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;hourly&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;daily&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;weekly&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;monthly&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;yearly&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;never&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
            &lt;span class="p"&gt;}:&lt;/span&gt;
                &lt;span class="n"&gt;warnings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Entry &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;: unexpected &amp;lt;changefreq&amp;gt; &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;cf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;pr&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;NS&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;priority&lt;/span&gt;&lt;span class="sh"&gt;"&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;pr&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;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="mf"&gt;0.0&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="nf"&gt;float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mf"&gt;1.0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nb"&gt;ValueError&lt;/span&gt;
                &lt;span class="nf"&gt;except &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;TypeError&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
                    &lt;span class="n"&gt;warnings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Entry &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;: &amp;lt;priority&amp;gt; must be 0.0 to 1.0, got &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;pr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hosts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;warnings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;URLs span several hosts: &lt;/span&gt;&lt;span class="si"&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="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hosts&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;kind&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;entries&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; entries&lt;/span&gt;&lt;span class="sh"&gt;"&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;errors&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;warnings&lt;/span&gt;


&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;errs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;warns&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;check&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;argv&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;errs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ERROR  &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;w&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;warns&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;WARNING&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;errs&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run it against a file or a live URL:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python3 check_sitemap.py public/sitemap.xml
python3 check_sitemap.py https://www.example.com/sitemap.xml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Against a deliberately broken test file, the output looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;urlset: 5 entries
ERROR   Entry 2: &amp;lt;loc&amp;gt; is not an absolute URL: /pricing
ERROR   Entry 4: &amp;lt;lastmod&amp;gt; is not a W3C Datetime: 20/10/2026
WARNING Entry 3: duplicate URL https://example.com/
WARNING Entry 3: &amp;lt;priority&amp;gt; must be 0.0 to 1.0, got 2
WARNING Entry 4: unexpected &amp;lt;changefreq&amp;gt; often
WARNING URLs span several hosts: example.com, www.example.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And a file with an unescaped &lt;code&gt;&amp;amp;&lt;/code&gt; fails at the parse step, before any entry checks:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ERROR   XML does not parse: not well-formed (invalid token): line 1, column 117
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A few notes on the choices:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Errors are things that break the protocol or the parse. Warnings are things that are valid XML but probably wrong.&lt;/li&gt;
&lt;li&gt;The host check is a warning, not an error, because cross-host sitemaps are allowed if you've proven ownership of the hosts (sitemaps.org describes how).&lt;/li&gt;
&lt;li&gt;It doesn't handle gzipped sitemaps. If you serve &lt;code&gt;.xml.gz&lt;/code&gt;, wrap &lt;code&gt;load()&lt;/code&gt; with &lt;code&gt;gzip.decompress()&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;For a sitemap index, it checks the index itself. To validate the child sitemaps, loop over the &lt;code&gt;&amp;lt;loc&amp;gt;&lt;/code&gt; values and call &lt;code&gt;check()&lt;/code&gt; on each.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Optional: check that listed URLs actually resolve
&lt;/h2&gt;

&lt;p&gt;Protocol-valid isn't the same as useful. Here's a quick status-code pass that flags anything other than a 200:&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="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;urllib.request&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;xml.etree.ElementTree&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;ET&lt;/span&gt;

&lt;span class="n"&gt;NS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;{http://www.sitemaps.org/schemas/sitemap/0.9}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;status_report&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sitemap_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;limit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;root&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ET&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sitemap_path&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;getroot&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;locs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;el&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;el&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;root&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;iter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;NS&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;loc&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)][:&lt;/span&gt;&lt;span class="n"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;locs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;req&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;method&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;HEAD&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                                     &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;User-Agent&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sitemap-check/1.0&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;urlopen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;final&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;geturl&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;final&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;REDIRECT&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;final&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HTTPError&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;FAIL&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;urlopen&lt;/code&gt; follows redirects, so the script compares the final URL with the listed one to catch them. Some servers reject HEAD requests; switch to GET if you see a lot of 405s. Keep the limit low and be polite to your own server.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wiring it into CI
&lt;/h2&gt;

&lt;p&gt;Generate the sitemap during the build, then validate it before deploying. A GitHub Actions step is enough:&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;Validate sitemap&lt;/span&gt;
  &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;python3 scripts/check_sitemap.py dist/sitemap.xml&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Because the script exits with 1 on errors, a broken sitemap fails the build. Warnings print but don't block.&lt;/p&gt;

&lt;p&gt;Two more habits that pay off:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Diff the URL list on each release.&lt;/strong&gt; A release that drops 3,000 URLs from the sitemap is worth a look before it ships.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reference the sitemap from robots.txt.&lt;/strong&gt; Google says it will find a &lt;code&gt;Sitemap:&lt;/code&gt; line in robots.txt the next time it crawls that file:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Sitemap: https://www.example.com/sitemap.xml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  If you'd rather not run code
&lt;/h2&gt;

&lt;p&gt;Appie.run has a set of free sitemap tools at &lt;a href="https://appie.run/tools" rel="noopener noreferrer"&gt;appie.run/tools&lt;/a&gt; that cover the same ground in a browser. Each asks you to complete a Cloudflare Turnstile check before it runs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;XML Sitemap Validator:&lt;/strong&gt; paste XML or a sitemap URL. It checks the root element, absolute URLs, lastmod, changefreq, priority, duplicates and the 50,000 URL limit, and gives a score that starts at 100 and drops for errors and warnings.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sitemap Finder and Checker:&lt;/strong&gt; enter a site URL. It reads &lt;code&gt;Sitemap:&lt;/code&gt; lines in robots.txt, then tries &lt;code&gt;/sitemap.xml&lt;/code&gt;, common sitemap index paths and the WordPress sitemap path, and reports which files load and how many URLs each lists.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sitemap URLs Comparison:&lt;/strong&gt; compare a "before" and "after" sitemap and get the added and removed URLs, which is the release diff above without writing code.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sitemap Split and Merger:&lt;/strong&gt; splits sets over 50,000 URLs into parts plus a sitemap index (you then replace the index locations with your real URLs).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;They're handy for a one-off audit or for a client site where you can't add a CI step.&lt;/p&gt;

&lt;h2&gt;
  
  
  Related reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://dev.to/appierun_66/how-to-get-ai-assistants-to-describe-your-product-correctly-51fa"&gt;How to get AI assistants to describe your product correctly&lt;/a&gt;: the site-side work, including llms.txt, that sits next to your sitemap.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/appierun_66/reddit-mcp-servers-for-marketing-2026-find-threads-and-draft-replies-from-claude-cursor-or-codex-1572"&gt;Reddit MCP servers for marketing (2026)&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  How do I check if my XML sitemap is valid?
&lt;/h3&gt;

&lt;p&gt;Parse it as XML, confirm the root is &lt;code&gt;urlset&lt;/code&gt; or &lt;code&gt;sitemapindex&lt;/code&gt; in the sitemaps.org namespace, and check each &lt;code&gt;&amp;lt;loc&amp;gt;&lt;/code&gt; is an absolute, escaped URL under 2,048 characters, with at most 50,000 URLs and 50MB uncompressed per file. The script above does this, or use a browser-based validator.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does Google use priority and changefreq?
&lt;/h3&gt;

&lt;p&gt;No. Google's documentation says it ignores both. It uses &lt;code&gt;lastmod&lt;/code&gt; when it's consistently and verifiably accurate.&lt;/p&gt;

&lt;h3&gt;
  
  
  What is the maximum size of a sitemap?
&lt;/h3&gt;

&lt;p&gt;50,000 URLs or 50MB uncompressed per file. For more, split into several sitemaps and list them in a sitemap index.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why does my sitemap say "XML parse error"?
&lt;/h3&gt;

&lt;p&gt;The most common cause is an unescaped &lt;code&gt;&amp;amp;&lt;/code&gt; in a URL. Replace it with &lt;code&gt;&amp;amp;amp;&lt;/code&gt;. Other causes include stray characters before the XML declaration and invalid UTF-8.&lt;/p&gt;

&lt;p&gt;RG, Founder, Appie.run&lt;/p&gt;

</description>
      <category>seo</category>
      <category>webdev</category>
      <category>python</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>How to Convert PDFs to Markdown for RAG and Chatbots (and When Not To)</title>
      <dc:creator>appierun</dc:creator>
      <pubDate>Sat, 24 Oct 2026 15:48:28 +0000</pubDate>
      <link>https://dev.to/appierun_66/how-to-convert-pdfs-to-markdown-for-rag-and-chatbots-and-when-not-to-307d</link>
      <guid>https://dev.to/appierun_66/how-to-convert-pdfs-to-markdown-for-rag-and-chatbots-and-when-not-to-307d</guid>
      <description>&lt;p&gt;Markdown keeps a document's headings, lists and tables in plain text that chunkers and LLMs handle well, so it's a sensible format to put between a PDF and your vector store. PDFs with a real text layer convert cleanly. Scanned PDFs need OCR first. Tables, multi-column layouts and repeated headers are where conversions usually go wrong, so check the output before you embed it.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Disclosure: this post is published by Appie.run. One of the conversion routes below is Appie's free converter and one section mentions Appie's chatbot product. The open-source routes work just as well, and for batch jobs they're the better choice.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Markdown before embedding
&lt;/h2&gt;

&lt;p&gt;Three practical reasons:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Headings give you chunk boundaries.&lt;/strong&gt; A &lt;code&gt;## Refunds&lt;/code&gt; heading is a natural place to split. Plain text extracted from a PDF loses that signal, and you end up splitting every N characters, often mid-sentence or mid-table.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cleaner, smaller context.&lt;/strong&gt; Markdown carries structure with very little markup. You avoid sending the model layout noise such as page furniture, odd line breaks and font artefacts.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Citations.&lt;/strong&gt; If you keep the heading and page number with each chunk, your bot or RAG app can say where an answer came from. That matters for policies, contracts and manuals, where people will want to check the source.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Markdown isn't magic, though. If the PDF has no text layer or the converter scrambles the reading order, the Markdown will be just as wrong as the plain text would have been.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: check what kind of PDF you have
&lt;/h2&gt;

&lt;p&gt;Before choosing a tool, find out whether the PDF has a text layer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The manual check:&lt;/strong&gt; open it in any PDF viewer and try to select a sentence. If the text highlights word by word, there's a text layer. If you can only draw a box around the whole page, it's an image, which usually means a scan or a photo.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The scripted check&lt;/strong&gt; (for a folder of files) uses PyMuPDF. Since PyMuPDF 2.x the Markdown helpers that used to live in the separate &lt;code&gt;pymupdf4llm&lt;/code&gt; package ship inside &lt;code&gt;pymupdf&lt;/code&gt; itself (per the &lt;a href="https://github.com/pymupdf/pymupdf4llm" rel="noopener noreferrer"&gt;pymupdf4llm repo&lt;/a&gt;, which is now an empty compatibility package).&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="c1"&gt;# pip install --upgrade pymupdf
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;pymupdf&lt;/span&gt;

&lt;span class="n"&gt;doc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pymupdf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;policy.pdf&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;enumerate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;chars&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_text&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="n"&gt;flag&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;OK&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;chars&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;NO TEXT LAYER? needs OCR&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;page &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;number&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;chars&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; characters  &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;flag&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The 50-character threshold is arbitrary. A page that is mostly a diagram will also come back low, so treat the result as a prompt to look, not a verdict. We ran this against a small test PDF with PyMuPDF 2.0; adapt it for your setup.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: convert
&lt;/h2&gt;

&lt;p&gt;Pick the route that matches the job.&lt;/p&gt;

&lt;h3&gt;
  
  
  Route A: a free web converter for one-off files
&lt;/h3&gt;

&lt;p&gt;If you have one policy document or one manual and want Markdown in a minute, a web converter is the fastest path. Appie's free &lt;a href="https://appie.run/tools/convert-pdf-to-markdown" rel="noopener noreferrer"&gt;Convert PDF to Markdown&lt;/a&gt; tool (ours) extracts each page to Markdown, and you copy or download the result. Its limits, as stated on the tool page on 22 Oct 2026:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;PDFs with real text only. A photo scan needs OCR first.&lt;/li&gt;
&lt;li&gt;The first 30 pages are included.&lt;/li&gt;
&lt;li&gt;You complete a Cloudflare Turnstile captcha before it runs.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For anything longer than 30 pages or for a recurring job, use a library.&lt;/p&gt;

&lt;h3&gt;
  
  
  Route B: a Python library for batches
&lt;/h3&gt;

&lt;p&gt;Four open-source options are worth a look. Descriptions below are taken from each project's own README, read on 22 Oct 2026; test them on your own documents, because results vary by layout.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/pymupdf/PyMuPDF" rel="noopener noreferrer"&gt;PyMuPDF&lt;/a&gt;&lt;/strong&gt;: its README lists "native Markdown output for RAG and AI pipelines" via &lt;code&gt;doc.to_markdown()&lt;/code&gt;. Lightweight and quick to install.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/datalab-to/marker" rel="noopener noreferrer"&gt;Marker&lt;/a&gt;&lt;/strong&gt;: converts PDF, images, PPTX, DOCX, XLSX, HTML and EPUB to Markdown, JSON and chunks. The README says it formats tables, equations and code blocks, removes headers and footers, and can optionally call an LLM to improve accuracy. Runs on GPU, CPU or MPS.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/docling-project/docling" rel="noopener noreferrer"&gt;Docling&lt;/a&gt;&lt;/strong&gt;: parses many formats with "advanced PDF understanding" including page layout, reading order and table structure, and exports Markdown, HTML and lossless JSON.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/microsoft/markitdown" rel="noopener noreferrer"&gt;MarkItDown&lt;/a&gt;&lt;/strong&gt; (Microsoft): a lightweight utility for converting PDF, Office files, HTML and more to Markdown for LLM pipelines. Its README is candid that output is meant for text analysis tools, not high-fidelity human-readable conversion. It also warns that it performs I/O with the privileges of the current process, so sanitise inputs in untrusted environments.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A minimal batch script with PyMuPDF:&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="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;pathlib&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;pymupdf&lt;/span&gt;

&lt;span class="n"&gt;src&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pathlib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pdfs&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;out&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pathlib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;markdown&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mkdir&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;exist_ok&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;pdf&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;src&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;glob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;*.pdf&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;md&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pymupdf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pdf&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;to_markdown&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;out&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;pdf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stem&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;.md&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;write_text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;md&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;encoding&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;converted&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pdf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Tested with PyMuPDF 2.0 on a two-page sample; adapt for your setup.&lt;/p&gt;

&lt;h3&gt;
  
  
  Route C: OCR first for scans
&lt;/h3&gt;

&lt;p&gt;If Step 1 flagged pages without a text layer, you need OCR before (or as part of) conversion. Marker and Docling both list scanned-document handling in their READMEs, and MarkItDown lists OCR for images. Another approach is to run an OCR tool that adds a text layer to the PDF, then use Route A or B. Whatever you pick, OCR output needs a closer review than a born-digital PDF: check numbers, names and anything in a table.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: clean up
&lt;/h2&gt;

&lt;p&gt;Run through this checklist on a sample of the output before converting hundreds of files:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Headers and footers.&lt;/strong&gt; Company name, document title and "Page 4 of 37" repeated on every page will pollute every chunk. Strip them with a regex or your converter's option.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tables.&lt;/strong&gt; Open a few tables in a Markdown preview. Merged cells and tables that span two pages are common failure points. If a table is critical (a price list, a spec sheet), consider fixing it by hand or storing it as CSV alongside the Markdown.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reading order in multi-column layouts.&lt;/strong&gt; Read a two-column page top to bottom. If paragraphs from the left and right columns interleave, try a layout-aware converter.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hyphenation.&lt;/strong&gt; Words split across lines ("con-\nversion") hurt search. Rejoin them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Page markers.&lt;/strong&gt; Keep a page reference somewhere (a comment such as &lt;code&gt;&amp;lt;!-- page 12 --&amp;gt;&lt;/code&gt; or chunk metadata) so you can cite it later.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Images and charts.&lt;/strong&gt; Most converters drop them or emit a placeholder. If a chart holds the answer to likely questions, describe it in text.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Step 4: chunk by heading and keep page numbers
&lt;/h2&gt;

&lt;p&gt;Chunk on headings first, then split long sections by size. Keep the heading and page number as metadata on every chunk. Here's a small example using PyMuPDF's per-page output:&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="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;pymupdf&lt;/span&gt;

&lt;span class="n"&gt;pages&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pymupdf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;policy.pdf&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;to_markdown&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;page_chunks&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# one dict per page
&lt;/span&gt;
&lt;span class="n"&gt;chunks&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;pages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;page_no&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;metadata&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;page_number&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="c1"&gt;# split before each H1 to H3 heading, keeping the heading with its text
&lt;/span&gt;    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;section&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;re&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="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;\n(?=#{1,3} )&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]):&lt;/span&gt;
        &lt;span class="n"&gt;section&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;section&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&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;section&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;heading&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;section&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;splitlines&lt;/span&gt;&lt;span class="p"&gt;()[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;lstrip&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="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="n"&gt;chunks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;section&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;heading&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;heading&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;page&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;page_no&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This splits per page, so a section that runs across a page break becomes two chunks. That's usually fine for retrieval, and the page numbers stay accurate. If you'd rather keep sections whole, convert the full document, split on headings, and record page numbers from the page markers you added in Step 3. Tested on a two-page sample with PyMuPDF 2.0; adapt for your setup.&lt;/p&gt;

&lt;p&gt;A few rules of thumb (general guidance, not benchmarks):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Prepend the heading path to each chunk's text ("Refund policy &amp;gt; EU orders") so the embedding knows the context.&lt;/li&gt;
&lt;li&gt;Keep tables whole inside one chunk where you can.&lt;/li&gt;
&lt;li&gt;Store the source file name with the page number, so answers can link back.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Skip the pipeline: upload the PDF straight to a hosted chatbot
&lt;/h2&gt;

&lt;p&gt;If your goal is a support bot or an internal Q&amp;amp;A assistant, rather than a custom RAG system, you may not need any of the above. Many hosted chatbot builders accept PDFs directly and handle extraction and chunking for you; check whether the one you use does.&lt;/p&gt;

&lt;p&gt;Appie AI Chatbots (ours) accepts PDFs, DOCX, CSV and text, plus URLs, sitemaps and product feeds, as knowledge sources (&lt;a href="https://appie.run/ai-chatbots" rel="noopener noreferrer"&gt;appie.run/ai-chatbots&lt;/a&gt;). Creating a bot is free, and each conversation with the bot costs 1 credit (&lt;a href="https://appie.run/pricing" rel="noopener noreferrer"&gt;pricing&lt;/a&gt;, as of 22 Oct 2026). The Free plan includes 500 credits and needs no credit card. It's a hosted bot, so you don't control the chunking; if you need that control, build the pipeline above.&lt;/p&gt;

&lt;p&gt;To test whether a PDF answers questions well before building anything, try Appie's free &lt;a href="https://appie.run/tools/ai-chat-with-your-pdf-document-data" rel="noopener noreferrer"&gt;AI Chat With Your PDF&lt;/a&gt; tool. You upload a text-based PDF and ask for a summary or a specific fact; per the tool page, answers must come from the PDF, and if the fact isn't there it says so. If it can't find answers you know are in the document, the PDF probably has a text-layer or layout problem to fix first.&lt;/p&gt;

&lt;p&gt;For hosted options, see &lt;a href="https://medium.com/@appierun/best-ai-chatbots-trained-on-your-own-website-data-2026-2b5715003f89" rel="noopener noreferrer"&gt;best AI chatbots trained on your own website data&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Before you choose a hosted option, compare &lt;a href="https://medium.com/@appierun/what-an-ai-support-chatbot-really-costs-in-2026-per-conversation-vs-per-credit-vs-per-resolution-3d9111a8d8b4" rel="noopener noreferrer"&gt;what an AI support chatbot really costs in 2026&lt;/a&gt;, since per-conversation, per-credit and per-resolution pricing add up very differently.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  How do I convert a PDF to Markdown for free?
&lt;/h3&gt;

&lt;p&gt;For a single text-based PDF, use a free web converter such as Appie's &lt;a href="https://appie.run/tools/convert-pdf-to-markdown" rel="noopener noreferrer"&gt;Convert PDF to Markdown&lt;/a&gt; (first 30 pages, text PDFs only). For batches, use an open-source library such as PyMuPDF (&lt;code&gt;doc.to_markdown()&lt;/code&gt;), Marker, Docling or MarkItDown.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I convert a scanned PDF to Markdown?
&lt;/h3&gt;

&lt;p&gt;Only after OCR. A scan is an image of text, so a converter has nothing to extract until OCR adds a text layer. Some libraries, such as Marker and Docling, include OCR; review OCR output carefully.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is Markdown better than plain text for RAG?
&lt;/h3&gt;

&lt;p&gt;Usually, because headings, lists and tables survive, which gives you better chunk boundaries and lets you keep section context for citations. It won't fix a bad extraction, so check the output either way.&lt;/p&gt;

&lt;h3&gt;
  
  
  How should I chunk Markdown from a PDF?
&lt;/h3&gt;

&lt;p&gt;Split on headings first, then split long sections by size. Keep the heading path, source file and page number as metadata on each chunk, and keep tables whole where you can.&lt;/p&gt;




&lt;p&gt;Convert a text PDF to Markdown free (first 30 pages): &lt;a href="https://appie.run/tools/convert-pdf-to-markdown" rel="noopener noreferrer"&gt;appie.run/tools/convert-pdf-to-markdown&lt;/a&gt;. Or upload the PDF straight into an Appie chatbot; the Free plan has 500 credits and needs no credit card. More free tools: &lt;a href="https://appie.run/tools" rel="noopener noreferrer"&gt;appie.run/tools&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>rag</category>
      <category>markdown</category>
      <category>python</category>
    </item>
    <item>
      <title>Track LinkedIn Profiles and Pull Leads Into Claude or Cursor</title>
      <dc:creator>appierun</dc:creator>
      <pubDate>Fri, 23 Oct 2026 15:53:52 +0000</pubDate>
      <link>https://dev.to/appierun_66/track-linkedin-profiles-and-pull-leads-into-claude-or-cursor-5e5e</link>
      <guid>https://dev.to/appierun_66/track-linkedin-profiles-and-pull-leads-into-claude-or-cursor-5e5e</guid>
      <description>&lt;p&gt;You can keep a LinkedIn watch list and work its leads from your AI client with three MCP tool calls: &lt;code&gt;set_linkedin_profile&lt;/code&gt; to add a profile, &lt;code&gt;get_linkedin_profiles&lt;/code&gt; to check the list, and &lt;code&gt;list_linkedin_leads&lt;/code&gt; to see the people who engaged, with email and phone when a lookup has found them.&lt;/p&gt;

&lt;p&gt;Disclosure: Appie.run publishes this post, and the MCP server below is ours. Other LinkedIn MCP servers exist, and our &lt;a href="https://medium.com/@appierun/linkedin-mcp-servers-in-2026-what-exists-how-they-get-data-and-which-one-fits-your-workflow-073e61c68f50" rel="noopener noreferrer"&gt;LinkedIn MCP servers explainer&lt;/a&gt; compares the kinds you'll find. This walkthrough covers Appie's only.&lt;/p&gt;

&lt;h2&gt;
  
  
  Who this is for
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;GTM engineers and RevOps people who already live in Claude Code, Cursor or Codex.&lt;/li&gt;
&lt;li&gt;Outbound agencies watching customers, partners, competitors' founders or industry voices.&lt;/li&gt;
&lt;li&gt;Teams moving off Trigify, whose access ended on 22 October 2026 for most customers. We have a separate &lt;a href="https://medium.com/@appierun/trigify-alternatives-2026-what-to-switch-to-before-the-22-october-shutdown-6f92c88044bc" rel="noopener noreferrer"&gt;Trigify alternatives&lt;/a&gt; piece.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 1: Key and server
&lt;/h2&gt;

&lt;p&gt;In Appie, open your project and choose &lt;strong&gt;API&lt;/strong&gt; in the sidebar. A project admin generates the &lt;code&gt;esk_&lt;/code&gt; key. Set it once in your shell:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;APPIE_API_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;esk_your_key_here
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The server is &lt;code&gt;https://appie.run/mcp&lt;/code&gt;, and it accepts &lt;code&gt;Authorization: Bearer esk_â€¦&lt;/code&gt; or &lt;code&gt;X-Api-Key: esk_â€¦&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Claude Code:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;claude mcp add &lt;span class="nt"&gt;--transport&lt;/span&gt; http appie https://appie.run/mcp &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--header&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$APPIE_API_KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Codex CLI:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;codex mcp add appie &lt;span class="nt"&gt;--url&lt;/span&gt; https://appie.run/mcp &lt;span class="nt"&gt;--bearer-token-env-var&lt;/span&gt; APPIE_API_KEY
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Cursor and other clients&lt;/strong&gt; (config from the Appie docs):&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;"appie"&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;"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://appie.run/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;"headers"&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;"Authorization"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Bearer YOUR_API_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;p&gt;With a valid key, &lt;code&gt;claude mcp list&lt;/code&gt; should show the server as connected. A 401 means the key is wrong or revoked. The &lt;a href="https://dev.to/appierun_66/find-reddit-threads-and-draft-replies-from-claude-code-or-cursor-mfo"&gt;Reddit MCP walkthrough&lt;/a&gt; covers scopes, &lt;code&gt;.mcp.json&lt;/code&gt; and troubleshooting in more detail.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Add the profiles you want to watch
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;set_linkedin_profile&lt;/code&gt; adds or updates one profile. &lt;code&gt;profile_url&lt;/code&gt; is required, and &lt;code&gt;tracking_enabled&lt;/code&gt; defaults to true.&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;"profile_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.linkedin.com/in/example"&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;To pause a profile without removing 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;"profile_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.linkedin.com/in/example"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"tracking_enabled"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&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;Saving a profile doesn't spend credits. In the docs' words, "the existing schedule runs the scan, which costs 30 credits." The docs don't say how often that schedule runs, so check the project settings in the app before you add a long list.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Check your watch list
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;get_linkedin_profiles&lt;/code&gt; lists the profiles the project tracks. It takes no required arguments and doesn't spend credits. Ask your agent to show it as a table with tracking status, so you can spot anything you paused by mistake.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: List leads by profile
&lt;/h2&gt;

&lt;p&gt;Leads are the people who engage with the profiles you track. &lt;code&gt;list_linkedin_leads&lt;/code&gt; takes an optional &lt;code&gt;limit&lt;/code&gt; (default 20, maximum 50) and an optional &lt;code&gt;tracked_profile_id&lt;/code&gt;, which you get from Step 3.&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;"tracked_profile_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;"PROFILE_ID_FROM_STEP_3"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"limit"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;50&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;Each lead includes &lt;code&gt;email&lt;/code&gt; and &lt;code&gt;phone&lt;/code&gt; when a lookup has found them. Listing is free. The lookup that fills email and phone costs 5 credits per lead.&lt;/p&gt;

&lt;p&gt;Illustrative shape of one lead (not real output; only &lt;code&gt;email&lt;/code&gt; and &lt;code&gt;phone&lt;/code&gt; are field names the docs confirm):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;lead: Example Person, engaged with https://www.linkedin.com/in/example
email: person@example.com
phone: (empty, no lookup result)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What the docs don't cover: whether the email lookup runs automatically or has to be triggered (no lookup tool is listed in the MCP), whether ICP scores come back over MCP, and which other fields are returned. Run the tool once and read the response before you build on it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5: Do something useful with them
&lt;/h2&gt;

&lt;p&gt;Two common routes:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Keep it in your own stack.&lt;/strong&gt; Ask your agent to turn the leads with an email into a CSV or rows for your sheet or CRM.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stay inside Appie.&lt;/strong&gt; The app groups leads by source profile with new, emailed and contacted counts, scores them against project-level ICP weights, shows posts from tracked profiles next to the people, and can hand a lead into a &lt;a href="https://appie.run/journeys" rel="noopener noreferrer"&gt;Journeys&lt;/a&gt; sequence once an email is found. Appie's page puts it as "no surprise sends from a profile sync": you choose who gets contacted.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The MCP has no messaging, connection-request or posting tools. Your agent can read and list; a person decides who to contact and what to say. For the outreach side, see &lt;a href="https://medium.com/@appierun/from-likes-to-replies-how-to-turn-linkedin-engagement-into-leads-with-warm-outreach-116e33209dde" rel="noopener noreferrer"&gt;From Likes to Replies: How to Turn LinkedIn Engagement Into Leads With Warm Outreach&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If you're writing first emails by hand, two free Appie tools help:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://appie.run/tools/ai-email-response-generator" rel="noopener noreferrer"&gt;AI Email Response Generator&lt;/a&gt;: paste an email, choose a language and tone, and get a draft with subject, greeting, body and sign-off. Review it before you send.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://appie.run/tools/email-signature-generator" rel="noopener noreferrer"&gt;Email Signature Generator&lt;/a&gt;: builds an HTML signature in your browser for Gmail, Outlook or Apple Mail.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you used Outfunnel to sync LinkedIn engagement into your CRM, here are &lt;a href="https://medium.com/@appierun/outfunnel-alternatives-in-2026-what-to-use-before-the-30-october-1-november-and-6-november-5ff9e48b9feb" rel="noopener noreferrer"&gt;Outfunnel alternatives to switch to before its shutdown dates&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prompts to paste
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;"Add these five LinkedIn profile URLs to Appie and confirm tracking is on for each: â€¦"&lt;/li&gt;
&lt;li&gt;"List my tracked LinkedIn profiles as a table with their IDs and tracking status."&lt;/li&gt;
&lt;li&gt;"Show leads from tracked profile PROFILE_ID that have an email, as a table. Leave out anyone without one."&lt;/li&gt;
&lt;li&gt;"Turn those leads into CSV rows with name, email and source profile so I can import them."&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What it costs
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Save or update a profile (&lt;code&gt;set_linkedin_profile&lt;/code&gt;)&lt;/strong&gt;: Credits: 0&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;List profiles (&lt;code&gt;get_linkedin_profiles&lt;/code&gt;)&lt;/strong&gt;: Credits: 0&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;List leads (&lt;code&gt;list_linkedin_leads&lt;/code&gt;)&lt;/strong&gt;: Credits: 0&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scheduled profile scan&lt;/strong&gt;: Credits: 30 per profile&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lead email lookup&lt;/strong&gt;: Credits: 5 per lead&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Illustrative example: 5 tracked profiles cost 150 credits per scheduled scan, and 40 email lookups cost 200 credits. That's 350 credits, inside the Free plan's 500 for a single run. How many scans happen in a month depends on the schedule, so work out your monthly number from that.&lt;/p&gt;

&lt;p&gt;Plans (appie.run/pricing, as of 20 Oct 2026): Free is $0 with 500 credits and no credit card needed. Starter is $19/month for 2,000 credits. Standard is $49/month for 5,000 credits.&lt;/p&gt;

&lt;p&gt;Appie gets LinkedIn data through third-party vendor APIs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Responsible use
&lt;/h2&gt;

&lt;p&gt;You're collecting data about real people. Read LinkedIn's terms yourself. For leads in the EU or UK, GDPR applies: have a lawful basis, keep only the data you need, and honour opt-outs. Check with your own advisers. Appie doesn't make that decision for you, and this post isn't legal advice.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;How do I track LinkedIn profile engagers from Claude?&lt;/strong&gt;&lt;br&gt;
Connect the Appie MCP server, add profiles with &lt;code&gt;set_linkedin_profile&lt;/code&gt;, then call &lt;code&gt;list_linkedin_leads&lt;/code&gt; to see the people who engaged with them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can an MCP server find emails for LinkedIn leads?&lt;/strong&gt;&lt;br&gt;
Appie's &lt;code&gt;list_linkedin_leads&lt;/code&gt; returns &lt;code&gt;email&lt;/code&gt; and &lt;code&gt;phone&lt;/code&gt; when a lookup has found them. Each lookup costs 5 credits per lead. The MCP doesn't list a separate lookup tool, so check the docs for how lookups run.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How much does a LinkedIn profile scan cost in Appie?&lt;/strong&gt;&lt;br&gt;
30 credits per profile scan. Saving and listing profiles is free.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can Claude send LinkedIn messages through Appie?&lt;/strong&gt;&lt;br&gt;
No. The MCP has no messaging, connection or posting tools.&lt;/p&gt;




&lt;p&gt;Add your watch list with &lt;code&gt;set_linkedin_profile&lt;/code&gt; and list your first leads from Cursor or Claude: &lt;a href="https://appie.run/docs/mcp" rel="noopener noreferrer"&gt;appie.run/docs/mcp&lt;/a&gt;&lt;/p&gt;

</description>
      <category>mcp</category>
      <category>linkedin</category>
      <category>claude</category>
      <category>sales</category>
    </item>
    <item>
      <title>Find Reddit Threads and Draft Replies From Claude Code or Cursor</title>
      <dc:creator>appierun</dc:creator>
      <pubDate>Thu, 22 Oct 2026 15:47:56 +0000</pubDate>
      <link>https://dev.to/appierun_66/find-reddit-threads-and-draft-replies-from-claude-code-or-cursor-mfo</link>
      <guid>https://dev.to/appierun_66/find-reddit-threads-and-draft-replies-from-claude-code-or-cursor-mfo</guid>
      <description>&lt;p&gt;The whole loop takes five steps: connect one MCP server, save your keywords and competitors, list today's threads, draft a reply, and paste it on Reddit yourself. This walkthrough uses Appie.run's MCP server, with every tool name and credit cost taken from &lt;a href="https://appie.run/docs/mcp" rel="noopener noreferrer"&gt;appie.run/docs/mcp&lt;/a&gt; on 22 Oct 2026.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Disclosure: this post is published by Appie.run, and the server used here is ours. Other Reddit MCP servers exist; see our &lt;a href="https://dev.to/appierun_66/reddit-mcp-servers-for-marketing-2026-find-threads-and-draft-replies-from-claude-cursor-or-codex-1572"&gt;roundup of Reddit MCP servers for marketing&lt;/a&gt;. Appie drafts replies but never posts them.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What you need
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;An Appie project (the Free plan has 500 credits, no credit card needed).&lt;/li&gt;
&lt;li&gt;A project API key that starts with &lt;code&gt;esk_&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Cursor, Claude Code or Codex CLI.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 1: Create the key
&lt;/h2&gt;

&lt;p&gt;In Appie, open the project and go to &lt;strong&gt;API&lt;/strong&gt; in the sidebar. A project admin generates one &lt;code&gt;esk_&lt;/code&gt; key.&lt;/p&gt;

&lt;p&gt;Two things to know before you use it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The same key is used by Event Store. Regenerating it stops both immediately, so update every client at once.&lt;/li&gt;
&lt;li&gt;The key has no balance of its own. A paid tool runs on the project's credits first, then the project owner's. Every tool result includes &lt;code&gt;credits_remaining&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Set it once in your shell:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;APPIE_API_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;esk_YOUR_KEY
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Never commit the key or paste it in public.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Add the server
&lt;/h2&gt;

&lt;p&gt;The server is &lt;code&gt;https://appie.run/mcp&lt;/code&gt; (streamable HTTP). It accepts &lt;code&gt;Authorization: Bearer esk_...&lt;/code&gt; or &lt;code&gt;X-Api-Key: esk_...&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cursor.&lt;/strong&gt; Add this to your MCP config (from Appie's docs):&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;"appie"&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;"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://appie.run/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;"headers"&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;"Authorization"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Bearer YOUR_API_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;p&gt;&lt;strong&gt;Claude Code.&lt;/strong&gt; One command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;claude mcp add &lt;span class="nt"&gt;--transport&lt;/span&gt; http appie https://appie.run/mcp &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--header&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$APPIE_API_KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;--transport http&lt;/code&gt; is required; without it, Claude Code treats the URL as a local command. The default scope is local (you, this project). Add &lt;code&gt;--scope user&lt;/code&gt; to use it everywhere. Your shell expands &lt;code&gt;$APPIE_API_KEY&lt;/code&gt;, so the key itself is saved in &lt;code&gt;~/.claude.json&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;To share the server with a repo without committing the key, use a project &lt;code&gt;.mcp.json&lt;/code&gt;:&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;"appie"&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;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"http"&lt;/span&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://appie.run/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;"headers"&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;"Authorization"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Bearer ${APPIE_API_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;p&gt;Check it with &lt;code&gt;claude mcp list&lt;/code&gt;. With a valid key, the status should read Connected. Note that &lt;code&gt;claude mcp get appie&lt;/code&gt; prints the header value in plain text, so don't share that output.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Codex CLI.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;codex mcp add appie &lt;span class="nt"&gt;--url&lt;/span&gt; https://appie.run/mcp &lt;span class="nt"&gt;--bearer-token-env-var&lt;/span&gt; APPIE_API_KEY
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This writes the following to &lt;code&gt;~/.codex/config.toml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="k"&gt;[&lt;/span&gt;&lt;span class="n"&gt;mcp_servers&lt;/span&gt;&lt;span class="k"&gt;.&lt;/span&gt;&lt;span class="n"&gt;appie&lt;/span&gt;&lt;span class="k"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"https://appie.run/mcp"&lt;/span&gt;
&lt;span class="n"&gt;bearer_token_env_var&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"APPIE_API_KEY"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Codex stores only the variable name and reads the key each time it starts. &lt;code&gt;codex mcp list&lt;/code&gt; reads the config but doesn't connect. Start &lt;code&gt;codex&lt;/code&gt; and run &lt;code&gt;/mcp&lt;/code&gt; to see the tools.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Smithery.&lt;/strong&gt; Paste the key into the &lt;code&gt;apiKey&lt;/code&gt; field. Smithery sends it as &lt;code&gt;X-Api-Key&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The commands above were run against appie.run with a placeholder key (which returned the expected 401). The success output described here is what you should expect with a real key, not something we captured.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Check and set your Reddit settings
&lt;/h2&gt;

&lt;p&gt;Ask your agent to call &lt;code&gt;get_reddit_settings&lt;/code&gt;. It's free. If you haven't saved anything yet and your project Brain has a website, about text, key messages or competitors, Appie saves those as your Reddit settings and returns them. That gives you a starting point.&lt;/p&gt;

&lt;p&gt;Then refine with &lt;code&gt;update_reddit_settings&lt;/code&gt; (also free). Send only the fields you want to change: &lt;code&gt;keywords&lt;/code&gt;, &lt;code&gt;competitors&lt;/code&gt; or &lt;code&gt;custom_prompt&lt;/code&gt;. Each list holds up to 25 items. Illustrative arguments:&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;"keywords"&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;"reddit monitoring"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"social listening for startups"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"find leads on reddit"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"brand mentions tool"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"reply to reddit threads"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"competitors"&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;"CompetitorA"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"CompetitorB"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"CompetitorC"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"custom_prompt"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Help first. Disclose that I work on the product. No links unless asked."&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;The docs don't say whether a list replaces or appends to what's saved. Send the full list each time, and check the response to see what was stored.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: List today's mentions
&lt;/h2&gt;

&lt;p&gt;Call &lt;code&gt;list_reddit_mentions&lt;/code&gt;. It lists Reddit threads for your saved keywords and competitors, with the latest reply draft for each. &lt;code&gt;limit&lt;/code&gt; defaults to 20, maximum 50.&lt;/p&gt;

&lt;p&gt;A scan runs at most once per day and costs 10 credits. Calling it again the same day lists results without a new scan. If your balance is too low, the result has &lt;code&gt;scan_skipped: true&lt;/code&gt; and a &lt;code&gt;message&lt;/code&gt; saying no scan ran.&lt;/p&gt;

&lt;p&gt;This is a monitored feed of your terms, not a search of all of Reddit, and it isn't real-time.&lt;/p&gt;

&lt;p&gt;To decide which of those threads are worth a reply at all, see &lt;a href="https://medium.com/@appierun/how-to-find-buyer-intent-reddit-threads-without-spamming-anyone-d333d1e2b5be" rel="noopener noreferrer"&gt;how to find buyer-intent Reddit threads without spamming anyone&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5: Draft a reply and post it yourself
&lt;/h2&gt;

&lt;p&gt;Pick a mention and call &lt;code&gt;draft_reddit_reply&lt;/code&gt; with its &lt;code&gt;mention_id&lt;/code&gt;. It costs 2 credits and returns &lt;code&gt;content&lt;/code&gt; (the draft) and &lt;code&gt;source_url&lt;/code&gt; (the thread). Illustrative shape:&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;"content"&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;"source_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.reddit.com/r/..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"credits_remaining"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;488&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;(The &lt;code&gt;credits_remaining&lt;/code&gt; number is only an example.)&lt;/p&gt;

&lt;p&gt;Then:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Open the thread and read the whole conversation.&lt;/li&gt;
&lt;li&gt;Check the subreddit's rules on self-promotion.&lt;/li&gt;
&lt;li&gt;Edit the draft so it answers the question first.&lt;/li&gt;
&lt;li&gt;Disclose that you work on the product if you mention it.&lt;/li&gt;
&lt;li&gt;Paste it from your own account.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Appie doesn't post to Reddit. If you only need a one-off draft for a thread you found yourself, Appie's free &lt;a href="https://appie.run/tools/ai-reply-generator" rel="noopener noreferrer"&gt;AI Reply Generator&lt;/a&gt; takes the message, a style and a length, and returns an editable reply without any setup.&lt;/p&gt;

&lt;p&gt;If the thread is a complaint about your product, read &lt;a href="https://medium.com/@appierun/how-to-respond-to-negative-brand-mentions-on-reddit-x-and-linkedin-c56009adb622" rel="noopener noreferrer"&gt;how to respond to negative brand mentions&lt;/a&gt; before you edit the draft.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prompts to paste
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;"Call get_reddit_settings and show me what's saved. Suggest five more buyer-intent keywords."&lt;/li&gt;
&lt;li&gt;"List today's Reddit mentions and group them by buying intent: asking for a tool, comparing tools, complaining, other."&lt;/li&gt;
&lt;li&gt;"Draft a reply to the top 'asking for a tool' mention. Answer the question first and mention us only at the end, with a disclosure."&lt;/li&gt;
&lt;li&gt;"Show credits_remaining after each call."&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What it costs
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;get_reddit_settings&lt;/code&gt;&lt;/strong&gt;: Credits (as of 22 Oct 2026): 0&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;update_reddit_settings&lt;/code&gt;&lt;/strong&gt;: Credits (as of 22 Oct 2026): 0&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;list_reddit_mentions&lt;/code&gt;&lt;/strong&gt;: Credits (as of 22 Oct 2026): 10 per scan, at most one scan per day&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;draft_reddit_reply&lt;/code&gt;&lt;/strong&gt;: Credits (as of 22 Oct 2026): 2 per draft&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Worked example (illustrative arithmetic from the docs, not a promise of usage): a daily scan for 30 days is 300 credits, and 50 drafts are 100 credits. Together that's 400 of the Free plan's 500 credits. Paid plans are Starter at $19/month (2,000 credits) and Standard at $49/month (5,000 credits), per &lt;a href="https://appie.run/pricing" rel="noopener noreferrer"&gt;appie.run/pricing&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Troubleshooting
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;HTTP 401.&lt;/strong&gt; The key is missing, wrong or was regenerated. Copy the current &lt;code&gt;esk_&lt;/code&gt; key from the API page. In Claude Code, a 401 with an Authorization header set means a bad key, not an OAuth prompt.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;app_not_allowed&lt;/code&gt;.&lt;/strong&gt; The person who generated the key can't use the Reddit app in that project. A project owner can use every app.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;insufficient_credits&lt;/code&gt;.&lt;/strong&gt; The response includes &lt;code&gt;required&lt;/code&gt; and &lt;code&gt;available&lt;/code&gt;. Top up or wait for your next cycle.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Codex shows no Appie tools.&lt;/strong&gt; Check &lt;code&gt;echo $APPIE_API_KEY&lt;/code&gt; in the same shell that starts Codex.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Empty results.&lt;/strong&gt; Widen your keywords: add problem phrases your buyers type, not only your brand name.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  How do I connect a Reddit MCP server to Cursor?
&lt;/h3&gt;

&lt;p&gt;Add the server URL and an Authorization header to your Cursor MCP config, as in Step 2.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do I add an MCP server with an API key in Claude Code?
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;claude mcp add --transport http &amp;lt;name&amp;gt; &amp;lt;url&amp;gt; --header "Authorization: Bearer &amp;lt;key&amp;gt;"&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can Claude post to Reddit for me?
&lt;/h3&gt;

&lt;p&gt;Not with Appie. It drafts the reply, and you paste it yourself.&lt;/p&gt;

&lt;h3&gt;
  
  
  How often does the Reddit scan run?
&lt;/h3&gt;

&lt;p&gt;At most once per day, at 10 credits per scan.&lt;/p&gt;




&lt;p&gt;Grab an API key, add &lt;code&gt;https://appie.run/mcp&lt;/code&gt; to Cursor, and run your first Reddit scan on the Free plan's 500 credits: &lt;a href="https://appie.run/docs/mcp" rel="noopener noreferrer"&gt;appie.run/docs/mcp&lt;/a&gt;. Skills and examples are also on &lt;a href="https://github.com/appierun/appie-mcp" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>mcp</category>
      <category>cursor</category>
      <category>claude</category>
      <category>reddit</category>
    </item>
    <item>
      <title>Reddit MCP Servers for Marketing (2026): Find Threads and Draft Replies From Claude, Cursor or Codex</title>
      <dc:creator>appierun</dc:creator>
      <pubDate>Wed, 21 Oct 2026 16:00:38 +0000</pubDate>
      <link>https://dev.to/appierun_66/reddit-mcp-servers-for-marketing-2026-find-threads-and-draft-replies-from-claude-cursor-or-codex-1572</link>
      <guid>https://dev.to/appierun_66/reddit-mcp-servers-for-marketing-2026-find-threads-and-draft-replies-from-claude-cursor-or-codex-1572</guid>
      <description>&lt;p&gt;Reddit doesn't publish an official MCP server, so every option below comes from a third party. The right one depends on the job: researching all of Reddit, working a monitored feed with reply drafts, or posting (which we suggest you keep doing by hand). Prices and features below were checked on each vendor's own site on 21 Oct 2026.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Disclosure: this article is published by Appie.run, and Appie is one of the servers compared here. We've placed it by job, not at the top.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What a Reddit MCP server does
&lt;/h2&gt;

&lt;p&gt;MCP (Model Context Protocol) is an open standard that lets an AI client such as Claude, Claude Code, Cursor or Codex call outside tools. A Reddit MCP server exposes tools like "search posts", "list my mentions" or "draft a reply", and your agent calls them in the middle of a conversation. Most hosted servers need only a URL and a key or sign-in. A typical client config looks like this:&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;"reddit-tool"&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;"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://example.com/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;"headers"&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;"Authorization"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Bearer YOUR_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;p&gt;If you rely on Reddit RSS feeds for keyword alerts today, check &lt;a href="https://medium.com/@appierun/will-your-reddit-keyword-alerts-keep-working-after-reddit-ends-rss-and-its-public-api-6a2196bdaab9" rel="noopener noreferrer"&gt;whether your Reddit keyword alerts will keep working after Reddit ends RSS and its public API&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Is there an official Reddit MCP server?
&lt;/h2&gt;

&lt;p&gt;No. Redreach's MCP page states that "Reddit does not publish an official MCP server," and Manifold's server page says Reddit "has no MCP server of its own" and that new API apps need Reddit's approval. Both are vendor pages, so treat them as vendor statements.&lt;/p&gt;

&lt;p&gt;One thing to watch: Reddit is closing its public Data API in stages. New public API requests stop on 31 October 2026, and remaining access ends in March 2027, per &lt;a href="https://support.reddithelp.com/hc/en-us/articles/54353370049684-Changelog-October-8-2026" rel="noopener noreferrer"&gt;Reddit's 8 October changelog&lt;/a&gt;. Every server here gets Reddit data in its own way, so ask each vendor how.&lt;/p&gt;

&lt;h2&gt;
  
  
  Comparison table
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Manifold&lt;/strong&gt;: Hosted or local: Hosted; Your own Reddit credentials?: No (reads "through a data provider"); All of Reddit or your monitored feed: Search all of Reddit, plus new posts in named subreddits; Drafts replies: Your agent drafts; Posts to Reddit: No; Price (as of 21 Oct 2026): Credits per call, "from 1 credit"; 250 free credits a month&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Redreach&lt;/strong&gt;: Hosted or local: Hosted; Your own Reddit credentials?: No; All of Reddit or your monitored feed: Your scored feed: tracked keywords, Google-ranking threads, mentions; Drafts replies: Yes; Posts to Reddit: No; Price (as of 21 Oct 2026): Included on Growth, Professional and Agency plans (price not shown in what we could read)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;RedShip&lt;/strong&gt;: Hosted or local: Hosted; Your own Reddit credentials?: No; All of Reddit or your monitored feed: Your ranked inbox; Drafts replies: Your agent drafts from the thread; Posts to Reddit: No; Price (as of 21 Oct 2026): Founder $29/month, Company $49/month, 7-day pass $12&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mentio&lt;/strong&gt;: Hosted or local: Hosted; Your own Reddit credentials?: No; All of Reddit or your monitored feed: Your keywords across Reddit and many other platforms; Drafts replies: Your agent drafts; Posts to Reddit: No; Price (as of 21 Oct 2026): $5 per keyword/month + $0.008 per mention&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Appie.run (ours)&lt;/strong&gt;: Hosted or local: Hosted; Your own Reddit credentials?: No; All of Reddit or your monitored feed: Your saved keywords and competitors; Drafts replies: Yes, 2 credits; Posts to Reddit: No; Price (as of 21 Oct 2026): Free plan (500 credits), Starter $19/mo, Standard $49/mo&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Syften PRO&lt;/strong&gt;: Hosted or local: Hosted; Your own Reddit credentials?: No; All of Reddit or your monitored feed: Your filters across communities; Drafts replies: No; Posts to Reddit: No; Price (as of 21 Oct 2026): MCP on PRO only, $119.95/month&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Octolens&lt;/strong&gt;: Hosted or local: Hosted; Your own Reddit credentials?: No; All of Reddit or your monitored feed: Your keywords across platforms; Drafts replies: No; Posts to Reddit: No; Price (as of 21 Oct 2026): Pro $159/mo billed annually&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The servers
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Manifold: hosted Reddit research
&lt;/h3&gt;

&lt;p&gt;Manifold's Reddit server lives at &lt;code&gt;https://mcp.manifoldmcp.com/mcp&lt;/code&gt; and connects with OAuth or an API key. Its tools search posts and comments across Reddit or one community, find subreddits for a topic, read a subreddit's rules, list every new post in named subreddits within a time window, and read a full thread with comments. It says it reads Reddit "through a data provider". Calls cost "from 1 credit", and every workspace gets 250 free credits a month. Manifold also says search is ranked and "never returns everything." Good for: research and one-off questions about all of Reddit.&lt;/p&gt;

&lt;h3&gt;
  
  
  Redreach: scored marketing feed with reply drafts
&lt;/h3&gt;

&lt;p&gt;Redreach (&lt;code&gt;https://connect.redreach.ai/mcp&lt;/code&gt;, browser OAuth or an &lt;code&gt;rr_&lt;/code&gt; token) gives your agent a filtered slice rather than raw Reddit: threads that rank on Google for your keywords, threads matching tracked keywords, and brand or competitor mentions with sentiment. Its &lt;code&gt;get_thread&lt;/code&gt; tool returns the post and comment tree for threads it has indexed for your project, and it drafts replies in several tones. It says it never posts to Reddit for you. MCP is included on its Growth, Professional and Agency plans. Good for: teams that want Google-ranking threads and mention reports.&lt;/p&gt;

&lt;h3&gt;
  
  
  RedShip: ranked reply inbox for founders
&lt;/h3&gt;

&lt;p&gt;RedShip (&lt;code&gt;https://mcp.redship.io&lt;/code&gt;) exposes seven tools per its docs: &lt;code&gt;get_project_context&lt;/code&gt;, &lt;code&gt;get_top_opportunities&lt;/code&gt;, &lt;code&gt;list_inbox&lt;/code&gt;, &lt;code&gt;get_inbox_item&lt;/code&gt;, &lt;code&gt;get_inbox_stats&lt;/code&gt;, &lt;code&gt;update_inbox_item&lt;/code&gt; and &lt;code&gt;log_reply&lt;/code&gt;. The loop it teaches is: read context, get the top opportunities, read the full post, you post the reply yourself, then mark it replied and log the text. Pricing on 21 Oct 2026: Founder $29/month, Company $49/month, and a 7-day full-access pass for $12. Good for: a solo founder who wants a daily shortlist and reply streaks.&lt;/p&gt;

&lt;h3&gt;
  
  
  Mentio: multi-platform monitor with pay-per-keyword pricing
&lt;/h3&gt;

&lt;p&gt;Mentio (&lt;code&gt;https://mcp.mentio.dev/mcp&lt;/code&gt;) monitors keywords on Reddit and many other platforms, including X, Hacker News, GitHub, LinkedIn and YouTube, and scores relevance, sentiment and intent. It lists Reddit as "real time." Pricing is usage-based: $5 per keyword per month, charged daily, plus $0.008 per mention found, with a one-time $5.80 welcome credit (as of 21 Oct 2026). MCP calls aren't billed. Good for: watching Reddit alongside several other sources on a small, variable budget.&lt;/p&gt;

&lt;h3&gt;
  
  
  Appie.run: Reddit settings, a daily scan and reply drafts on one key (ours)
&lt;/h3&gt;

&lt;p&gt;Appie's server is &lt;code&gt;https://appie.run/mcp&lt;/code&gt;, authenticated with a project API key (&lt;code&gt;Authorization: Bearer esk_...&lt;/code&gt;) that a project admin generates under API in the app. It has four Reddit tools, per &lt;a href="https://appie.run/docs/mcp" rel="noopener noreferrer"&gt;appie.run/docs/mcp&lt;/a&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;get_reddit_settings&lt;/code&gt;: reads your keywords, competitors and reply guidance. If none are saved, it seeds them from the project Brain. Free.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;update_reddit_settings&lt;/code&gt;: sets keywords, competitors or a custom prompt, up to 25 items per list. Free.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;list_reddit_mentions&lt;/code&gt;: lists threads for your saved terms with the latest draft (default 20, max 50). A scan runs at most once per day and costs 10 credits. If your balance is too low, it returns &lt;code&gt;scan_skipped&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;draft_reddit_reply&lt;/code&gt;: writes a reply for one mention and returns the text plus the thread URL. 2 credits. Appie doesn't post to Reddit; you paste the reply.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The same key reaches Appie's LinkedIn, articles, chatbot, short-link and Brain tools (21 tools in total). Honest limits: it's a monitored feed of your keywords and competitors, not a search of all of Reddit; it scans once a day, not in real time; and there's no documented tool for reading a full comment tree. Pricing on 21 Oct 2026: Free plan with 500 credits and no credit card needed, Starter $19/month (2,000 credits), Standard $49/month (5,000 credits). Good for: a once-a-day reply routine on the same server you use for other marketing jobs.&lt;/p&gt;

&lt;h3&gt;
  
  
  Syften PRO and Octolens: listening tools that added MCP
&lt;/h3&gt;

&lt;p&gt;Syften lists MCP on its PRO plan only. Its Entry ($29.95/month) and Standard ($49.95/month) plans don't include it, and PRO is $119.95/month as of 21 Oct 2026. Syften says it aims for at most a one-minute delay on Reddit.&lt;/p&gt;

&lt;p&gt;Octolens includes "API, webhooks and MCP on every plan." Pro is $159/month billed annually (10 keywords, 15,000 mentions, hourly search) as of 21 Oct 2026. Good for: broader listening where Reddit is one source among many.&lt;/p&gt;

&lt;h3&gt;
  
  
  Open-source servers
&lt;/h3&gt;

&lt;p&gt;Several open-source Reddit MCP servers run locally. Some browse without keys, others need your own Reddit API credentials, and at least one can post with them, according to vendor roundups. Check each README before installing, and remember that new public API access closes on 31 October 2026.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pick by job
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Research all of Reddit:&lt;/strong&gt; Manifold, or an open-source server if you already have API access.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A reply workflow:&lt;/strong&gt; Redreach, RedShip or Appie. All three draft or help draft, and none posts.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cross-platform listening with Reddit as one source:&lt;/strong&gt; Mentio, Octolens or Syften PRO.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Want to try reply drafting before wiring up a server? Appie's free &lt;a href="https://appie.run/tools/ai-reply-generator" rel="noopener noreferrer"&gt;AI Reply Generator&lt;/a&gt; drafts a reply to a message you paste in, with a choice of style and length.&lt;/p&gt;

&lt;h2&gt;
  
  
  Posting from an agent
&lt;/h2&gt;

&lt;p&gt;Most marketing-focused servers deliberately don't post. Reddit communities set their own rules, and many remove promotional replies. A few habits help whichever server you use:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Read each subreddit's rules before replying.&lt;/li&gt;
&lt;li&gt;Disclose your affiliation when you mention your product.&lt;/li&gt;
&lt;li&gt;Edit every draft and post it by hand from your own account.&lt;/li&gt;
&lt;li&gt;Reply only where you can actually help.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Is there an official Reddit MCP server?
&lt;/h3&gt;

&lt;p&gt;No. Vendors including Redreach and Manifold say Reddit doesn't publish one.&lt;/p&gt;

&lt;h3&gt;
  
  
  What is the best Reddit MCP server for marketing?
&lt;/h3&gt;

&lt;p&gt;It depends on the job. Research-first: Manifold. Reply workflow: Redreach, RedShip or Appie. Multi-platform listening: Mentio, Octolens or Syften PRO.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can a Reddit MCP server post replies for me?
&lt;/h3&gt;

&lt;p&gt;Some open-source servers can, using your own credentials. The marketing servers in this list don't; you post yourself.&lt;/p&gt;

&lt;h3&gt;
  
  
  Do I need Reddit API keys?
&lt;/h3&gt;

&lt;p&gt;Not for the hosted servers above. Local open-source servers often need them.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do I connect Reddit to Claude Code or Cursor?
&lt;/h3&gt;

&lt;p&gt;Add the server URL and your key to the client's MCP config. For Appie in Claude Code: &lt;code&gt;claude mcp add --transport http appie https://appie.run/mcp --header "Authorization: Bearer $APPIE_API_KEY"&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  How much does a Reddit MCP server cost?
&lt;/h3&gt;

&lt;p&gt;From pay-per-use (Manifold credits, Mentio at $5 per keyword) to flat plans (RedShip $29/month, Syften PRO $119.95/month, Octolens $159/month billed annually), as of 21 Oct 2026. Appie starts with a Free plan of 500 credits.&lt;/p&gt;




&lt;p&gt;To connect Appie's MCP server, save your keywords and competitors, and get reply drafts you post yourself, start with the Free plan's 500 credits: &lt;a href="https://appie.run/docs/mcp" rel="noopener noreferrer"&gt;appie.run/docs/mcp&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>mcp</category>
      <category>ai</category>
      <category>reddit</category>
      <category>marketing</category>
    </item>
  </channel>
</rss>
