{
    "version": "https://jsonfeed.org/version/1",
    "title": "Notes soltes — Joan Sanfeliu",
    "home_page_url": "https://formacio.joansanfeliu.com/blog",
    "description": "Joan Sanfeliu Blog",
    "items": [
        {
            "id": "https://formacio.joansanfeliu.com/blog/trobat-per-chatgpt-registres-web",
            "content_html": "<p>Dilluns em va escriure un propietari d'un pis turístic. Tenia un problema amb el programa de la seva gestora i em demanava ajuda. Dimecres, per telèfon, li vaig preguntar com m'havia trobat. «Per ChatGPT.» Li havia explicat el problema i el meu nom li havia sortit el primer, amb el telèfon inclòs.</p>\n<p>Qui busca algú per resoldre un problema ja no només obre Google. Cada vegada més gent li pregunta a un assistent com ChatGPT, i l'assistent no torna deu enllaços: en recomana un o dos. Vaig voler veure-ho amb dades: si la visita havia quedat apuntada a la web, quins robots d'OpenAI llegeixen les meves webs de debò i quins només en porten el nom.</p>\n<h2 class=\"anchor anchorWithStickyNavbar_LWe7\" id=\"la-visita-porta-una-etiqueta\">La visita porta una etiqueta<a href=\"https://formacio.joansanfeliu.com/blog/trobat-per-chatgpt-registres-web#la-visita-porta-una-etiqueta\" class=\"hash-link\" aria-label=\"Direct link to La visita porta una etiqueta\" title=\"Direct link to La visita porta una etiqueta\">​</a></h2>\n<p>Quan ChatGPT enllaça una web en una resposta, hi afegeix <code>?utm_source=chatgpt.com</code>. La pàgina no en fa res, però queda escrit al registre d'accés, el fitxer on el servidor apunta cada petició que rep. Al de joansanfeliu.com hi és:</p>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">2026-09-14 09:36:17 UTC   GET /es/?utm_source=chatgpt.com   200</span><br></span></code></pre></div></div>\n<p>Eren les 11:36 a Barcelona. El correu va arribar a les 12:11.</p>\n<h2 class=\"anchor anchorWithStickyNavbar_LWe7\" id=\"com-treure-el-registre-damplify\">Com treure el registre d'Amplify<a href=\"https://formacio.joansanfeliu.com/blog/trobat-per-chatgpt-registres-web#com-treure-el-registre-damplify\" class=\"hash-link\" aria-label=\"Direct link to Com treure el registre d'Amplify\" title=\"Direct link to Com treure el registre d'Amplify\">​</a></h2>\n<p>Les meves webs són a AWS Amplify, que guarda cada petició en el format de CloudFront i la dona amb una ordre (les dates, en la sintaxi del <code>date</code> de macOS):</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">aws amplify generate-access-logs \\</span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  --region eu-west-1 \\</span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  --app-id &lt;id-de-l-app&gt; \\</span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  --domain-name joansanfeliu.com \\</span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  --start-time $(date -u -v-7d +%s) \\</span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  --end-time $(date -u +%s) \\</span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  --query logUrl --output text</span><br></span></code></pre></div></div>\n<p>Retorna una adreça temporal a un CSV amb una fila per petició. Dues coses em van fer perdre temps:</p>\n<ul>\n<li><strong>El rang de dates té un límit, i no és el mateix per a totes les apps.</strong> Amb catorze dies, agencia.joansanfeliu.com i formacio.joansanfeliu.com van tornar el fitxer. joansanfeliu.com va respondre <code>Unable to complete request for the given time range, reduce time range and try again</code> i només va acceptar set dies.</li>\n<li><strong>Els noms de les columnes porten els parèntesis escapats</strong>, <code>cs\\(User-Agent)</code> i <code>cs\\(Referer)</code>, i dins les cel·les també hi ha barres invertides (<code>utm_source\\=chatgpt.com</code>). El primer recompte, buscant la columna <code>cs(User-Agent)</code>, em va donar zero robots.</li>\n</ul>\n<p>Llegint-ho bé:</p>\n<div class=\"language-python codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-python codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token keyword\" style=\"color:#00009f\">import</span><span class=\"token plain\"> csv</span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">from</span><span class=\"token plain\"> urllib</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">parse </span><span class=\"token keyword\" style=\"color:#00009f\">import</span><span class=\"token plain\"> unquote</span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">def</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">neteja</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">text</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">return</span><span class=\"token plain\"> unquote</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">unquote</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">text</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">replace</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">'\\\\'</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">''</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">files </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> csv</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">reader</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token builtin\">open</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">'joansanfeliu.csv'</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> newline</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token string\" style=\"color:#e3116c\">''</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">I </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token plain\">neteja</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">nom</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> i </span><span class=\"token keyword\" style=\"color:#00009f\">for</span><span class=\"token plain\"> i</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> nom </span><span class=\"token keyword\" style=\"color:#00009f\">in</span><span class=\"token plain\"> </span><span class=\"token builtin\">enumerate</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token builtin\">next</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">files</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token plain\"></span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">for</span><span class=\"token plain\"> f </span><span class=\"token keyword\" style=\"color:#00009f\">in</span><span class=\"token plain\"> files</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">if</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'utm_source=chatgpt.com'</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">in</span><span class=\"token plain\"> neteja</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">f</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token plain\">I</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token string\" style=\"color:#e3116c\">'cs-uri-query'</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        </span><span class=\"token keyword\" style=\"color:#00009f\">print</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">f</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token plain\">I</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token string\" style=\"color:#e3116c\">'date'</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> f</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token plain\">I</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token string\" style=\"color:#e3116c\">'time'</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> f</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token plain\">I</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token string\" style=\"color:#e3116c\">'cs-uri-stem'</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><br></span></code></pre></div></div>\n<h2 class=\"anchor anchorWithStickyNavbar_LWe7\" id=\"els-tres-robots-dopenai\">Els tres robots d'OpenAI<a href=\"https://formacio.joansanfeliu.com/blog/trobat-per-chatgpt-registres-web#els-tres-robots-dopenai\" class=\"hash-link\" aria-label=\"Direct link to Els tres robots d'OpenAI\" title=\"Direct link to Els tres robots d'OpenAI\">​</a></h2>\n<p>OpenAI té tres agents, i la seva documentació diu per a què serveix cadascun:</p>\n<ul>\n<li><strong>GPTBot</strong> recorre webs buscant contingut que pot servir per entrenar els seus models.</li>\n<li><strong>OAI-SearchBot</strong> serveix perquè les webs surtin als resultats de cerca de ChatGPT.</li>\n<li><strong>ChatGPT-User</strong> visita una pàgina quan algú fa una pregunta a ChatGPT. Com que l'acció la inicia una persona, OpenAI avisa que les regles del <code>robots.txt</code> poden no aplicar-s'hi.</li>\n</ul>\n<p>En catorze dies, als registres de dues de les webs:</p>\n<ul>\n<li><strong>agencia.joansanfeliu.com:</strong> 93 peticions de GPTBot i 5 d'OAI-SearchBot, totes cinc a <code>/robots.txt</code>.</li>\n<li><strong>formacio.joansanfeliu.com:</strong> 151 de GPTBot i 6 d'OAI-SearchBot, també totes a <code>/robots.txt</code>.</li>\n</ul>\n<p>El que més demana GPTBot és el mapa del lloc (<code>sitemap-index.xml</code> a l'una, <code>sitemap.xml</code> a l'altra). Totes aquestes peticions surten d'adreces que OpenAI publica com a seves.</p>\n<h2 class=\"anchor anchorWithStickyNavbar_LWe7\" id=\"els-que-sen-fan-passar\">Els que se'n fan passar<a href=\"https://formacio.joansanfeliu.com/blog/trobat-per-chatgpt-registres-web#els-que-sen-fan-passar\" class=\"hash-link\" aria-label=\"Direct link to Els que se'n fan passar\" title=\"Direct link to Els que se'n fan passar\">​</a></h2>\n<p>A joansanfeliu.com, en set dies, surt una altra cosa:</p>\n<ul>\n<li><strong>GPTBot:</strong> 116 peticions, 30 des d'adreces d'OpenAI.</li>\n<li><strong>OAI-SearchBot:</strong> 115, 35 des d'adreces d'OpenAI.</li>\n<li><strong>ChatGPT-User:</strong> 91, 2 des d'adreces d'OpenAI.</li>\n</ul>\n<p>Més de la meitat de la resta no demana pàgines: demana <code>/.env</code>, <code>/.aws/credentials</code>, <code>/@fs/etc/passwd</code> o <code>/id_ed25519</code>, els fitxers on un projecte mal desplegat deixa contrasenyes i claus. <code>/@fs/</code> és la ruta del servidor de desenvolupament de Vite per servir fitxers del disc, i joansanfeliu.com està fet amb Vite. Escriure el nom d'un robot d'OpenAI a l'agent d'usuari, la capçalera amb què cada programa diu qui és, no costa res.</p>\n<p>Comptant tots els agents, aquella setmana hi va haver 2.059 peticions buscant fitxers d'aquest tipus. Totes van rebre HTML o un error: 1.765 van rebre un 200 amb la portada, perquè la web té una regla que envia qualsevol ruta desconeguda cap a l'<code>index.html</code> (és una web d'una sola pàgina, que dibuixa el contingut amb JavaScript), i 216 un 404. A agencia. i formacio., en catorze dies, no n'hi va haver cap.</p>\n<h2 class=\"anchor anchorWithStickyNavbar_LWe7\" id=\"com-saber-si-és-openai-de-debò\">Com saber si és OpenAI de debò<a href=\"https://formacio.joansanfeliu.com/blog/trobat-per-chatgpt-registres-web#com-saber-si-%C3%A9s-openai-de-deb%C3%B2\" class=\"hash-link\" aria-label=\"Direct link to Com saber si és OpenAI de debò\" title=\"Direct link to Com saber si és OpenAI de debò\">​</a></h2>\n<p>L'agent no prova res. L'adreça d'origen, en canvi, no es pot inventar en una connexió HTTP, i OpenAI publica les seves en tres fitxers: <code>openai.com/gptbot.json</code>, <code>openai.com/searchbot.json</code> i <code>openai.com/chatgpt-user.json</code>.</p>\n<div class=\"language-python codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-python codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token keyword\" style=\"color:#00009f\">import</span><span class=\"token plain\"> ipaddress</span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">import</span><span class=\"token plain\"> json</span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">import</span><span class=\"token plain\"> urllib</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">request</span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">xarxes </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token plain\"></span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">for</span><span class=\"token plain\"> nom </span><span class=\"token keyword\" style=\"color:#00009f\">in</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token string\" style=\"color:#e3116c\">'gptbot'</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'searchbot'</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'chatgpt-user'</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">with</span><span class=\"token plain\"> urllib</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">request</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">urlopen</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">f'https://openai.com/</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">{</span><span class=\"token string-interpolation interpolation\">nom</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">}</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">.json'</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">as</span><span class=\"token plain\"> r</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        prefixos </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> json</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">load</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">r</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token string\" style=\"color:#e3116c\">'prefixes'</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token plain\"></span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    xarxes </span><span class=\"token operator\" style=\"color:#393A34\">+=</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token plain\">ipaddress</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">ip_network</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">p</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">get</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">'ipv4Prefix'</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">or</span><span class=\"token plain\"> p</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">get</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">'ipv6Prefix'</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">for</span><span class=\"token plain\"> p </span><span class=\"token keyword\" style=\"color:#00009f\">in</span><span class=\"token plain\"> prefixos</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token plain\"></span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">def</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">es_openai</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">ip</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    adreca </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> ipaddress</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">ip_address</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">ip</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">return</span><span class=\"token plain\"> </span><span class=\"token builtin\">any</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">adreca </span><span class=\"token keyword\" style=\"color:#00009f\">in</span><span class=\"token plain\"> xarxa </span><span class=\"token keyword\" style=\"color:#00009f\">for</span><span class=\"token plain\"> xarxa </span><span class=\"token keyword\" style=\"color:#00009f\">in</span><span class=\"token plain\"> xarxes</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><br></span></code></pre></div></div>\n<p>Una fila és d'OpenAI si l'agent ho diu i <code>es_openai()</code> ho confirma amb la columna <code>c-ip</code>. Els fitxers es refresquen: el de ChatGPT-User que vaig baixar era de l'11 de setembre, i una adreça que OpenAI ja no fa servir pot no ser-hi. Per això els números d'abans diuen «des d'adreces d'OpenAI» i no «falsos».</p>\n<h2 class=\"anchor anchorWithStickyNavbar_LWe7\" id=\"què-hi-ha-a-les-webs-perquè-una-ia-les-llegeixi\">Què hi ha a les webs perquè una IA les llegeixi<a href=\"https://formacio.joansanfeliu.com/blog/trobat-per-chatgpt-registres-web#qu%C3%A8-hi-ha-a-les-webs-perqu%C3%A8-una-ia-les-llegeixi\" class=\"hash-link\" aria-label=\"Direct link to Què hi ha a les webs perquè una IA les llegeixi\" title=\"Direct link to Què hi ha a les webs perquè una IA les llegeixi\">​</a></h2>\n<p>No sé què va fer que ChatGPT em recomanés. Sé què hi ha a cada web:</p>\n<ul>\n<li><strong>agencia.joansanfeliu.com</strong> té un <code>robots.txt</code> que dona permís pel seu nom a GPTBot, OAI-SearchBot, ChatGPT-User, ClaudeBot, PerplexityBot i la resta de robots d'IA coneguts, amb un comentari que explica per què, i un <code>/llms.txt</code>: un resum en text pla, pensat per a models, de qui som, on treballem i com.</li>\n<li><strong>formacio.joansanfeliu.com</strong> i <strong>joansanfeliu.com</strong> tenen un <code>robots.txt</code> que ho permet tot sense anomenar ningú, i cap <code>llms.txt</code>.</li>\n<li><strong>joansanfeliu.com</strong> porta a l'<code>index.html</code> les dades estructurades de la persona i de l'organització (JSON-LD, un format que els cercadors llegeixen sense haver d'interpretar el text).</li>\n</ul>\n<p>La visita de ChatGPT va entrar per joansanfeliu.com, una web sense <code>llms.txt</code>.</p>\n<p><strong>Actualització del 16 de setembre al vespre.</strong> El mateix dia de publicar això ho vaig canviar:</p>\n<ul>\n<li>joansanfeliu.com, formacio.joansanfeliu.com i getguestproof.com ja tenen un <code>robots.txt</code> que anomena els robots d'IA, amb <code>Content-Signal: search=yes, ai-input=yes, ai-train=yes</code>, i el seu <code>/llms.txt</code>.</li>\n<li>formacio.joansanfeliu.com i getguestproof.com porten dades estructurades. Les de joansanfeliu.com fan servir els mateixos identificadors que les de l'agència: una sola persona i un sol estudi.</li>\n<li>joansanfeliu.com genera cada pàgina en HTML quan es publica, en els cinc idiomes. Un robot que no executa JavaScript ja no rep la portada a totes les adreces: la pàgina de productes en castellà en porta més de quatre-centes paraules pròpies.</li>\n<li>La regla que enviava qualsevol ruta desconeguda a la portada ja no hi és: <code>/.env</code> respon un 404.</li>\n</ul>\n<h2 class=\"anchor anchorWithStickyNavbar_LWe7\" id=\"si-voleu-fer-la-mateixa-comprovació\">Si voleu fer la mateixa comprovació<a href=\"https://formacio.joansanfeliu.com/blog/trobat-per-chatgpt-registres-web#si-voleu-fer-la-mateixa-comprovaci%C3%B3\" class=\"hash-link\" aria-label=\"Direct link to Si voleu fer la mateixa comprovació\" title=\"Direct link to Si voleu fer la mateixa comprovació\">​</a></h2>\n<ol>\n<li>Traieu el registre d'accés de la vostra web.</li>\n<li>Busqueu <code>utm_source=chatgpt.com</code>: són les visites que us arriben des de ChatGPT.</li>\n<li>Contrasteu cada GPTBot, OAI-SearchBot i ChatGPT-User amb les adreces que publica OpenAI.</li>\n</ol>\n<p>El segon pas us diu si ja us recomanen. El tercer, qui entra a la web dient que és un altre.</p>",
            "url": "https://formacio.joansanfeliu.com/blog/trobat-per-chatgpt-registres-web",
            "title": "M'han trobat per ChatGPT: què diuen els registres de la web",
            "summary": "Un propietari d'un pis turístic em va trobar preguntant a ChatGPT. Als registres d'accés es veu la visita, quins robots d'OpenAI llegeixen la web de debò i quins se'n fan passar per buscar contrasenyes.",
            "date_modified": "2026-09-16T00:00:00.000Z",
            "author": {
                "name": "Joan Sanfeliu",
                "url": "https://joansanfeliu.com"
            },
            "tags": [
                "chatgpt",
                "aeo",
                "amplify",
                "logs",
                "seguretat"
            ]
        },
        {
            "id": "https://formacio.joansanfeliu.com/blog/donar-alta-client-escrivint-nomes-url",
            "content_html": "<p>Quan algú vol provar un producte de programari, cada camp extra al formulari de registre és una oportunitat perquè marxi. Si per donar-se d'alta li demanes nom del negoci, logotip, colors i dades de cada propietat, la majoria tancarà la pestanya abans d'acabar. Aquesta setmana he tancat un flux de registre que demana tres coses: correu, contrasenya i l'adreça web del negoci. El sistema entra a aquella pàgina, en treu la identitat visual, crea el compte i, pocs minuts després, la pàgina pública del client ja és en línia sense que ningú més hi hagi tocat res.</p>\n<p>La decisió de producte és simple: si la informació ja existeix en algun lloc, no la demanis. La web d'un negoci ja porta el nom, el logo i els colors. Només cal anar-los a buscar.</p>\n<h2 class=\"anchor anchorWithStickyNavbar_LWe7\" id=\"tres-passos-fins-a-tenir-la-web-en-línia\">Tres passos fins a tenir la web en línia<a href=\"https://formacio.joansanfeliu.com/blog/donar-alta-client-escrivint-nomes-url#tres-passos-fins-a-tenir-la-web-en-l%C3%ADnia\" class=\"hash-link\" aria-label=\"Direct link to Tres passos fins a tenir la web en línia\" title=\"Direct link to Tres passos fins a tenir la web en línia\">​</a></h2>\n<p>L'experiència d'un usuari nou té tres passos:</p>\n<ol>\n<li>Escriu l'adreça d'un dels seus anuncis (la propietat que lloga, el servei que ofereix).</li>\n<li>El producte li fa una recollida gratuïta: contacta els seus clients, els demana una valoració i les recull.</li>\n<li>Amb les valoracions recollides, la pàgina pública es genera i es publica sola.</li>\n</ol>\n<p>Cap d'aquests passos requereix un humà a l'altre costat. La web neix sola. Això és el que internament en dic <em>auto-debut</em>: la primera publicació de la web es dispara quan la primera recollida de valoracions acaba, sense que l'usuari hagi de prémer cap botó.</p>\n<h2 class=\"anchor anchorWithStickyNavbar_LWe7\" id=\"post-publicsignup-alta-atòmica-amb-rollback\"><code>POST /public/signup</code>: alta atòmica amb rollback<a href=\"https://formacio.joansanfeliu.com/blog/donar-alta-client-escrivint-nomes-url#post-publicsignup-alta-at%C3%B2mica-amb-rollback\" class=\"hash-link\" aria-label=\"Direct link to post-publicsignup-alta-atòmica-amb-rollback\" title=\"Direct link to post-publicsignup-alta-atòmica-amb-rollback\">​</a></h2>\n<p>Tot comença amb un sol endpoint públic:</p>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">POST /public/signup</span><br></span><span class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">Body: { email, password, websiteUrl }</span><br></span></code></pre></div></div>\n<p>L'endpoint fa dues coses en seqüència: crea el tenant (el compte del negoci dins el sistema, amb la seva identitat visual extreta de la URL) i crea l'usuari a Cognito, el servei d'autenticació d'AWS que gestiona correus i contrasenyes. Si la segona operació falla —per exemple perquè el correu ja existeix a Cognito—, el sistema esborra el tenant que acaba de crear. Sense aquest rollback quedaries amb comptes orfes: tenants sense cap usuari que hi pugui entrar.</p>\n<p>L'extracció de marca passa dins la creació del tenant. El sistema fa un GET a la <code>websiteUrl</code> que l'usuari ha proporcionat, analitza l'HTML i en treu:</p>\n<ul>\n<li>El nom del negoci (normalment de l'element <code>&lt;title&gt;</code> o de les metadades Open Graph).</li>\n<li>El favicon o logo.</li>\n<li>Els colors principals del CSS.</li>\n</ul>\n<p>No és perfecte. Hi ha webs que porten tot el contingut carregat amb JavaScript i el GET simple no veu res. Hi ha logos que són SVG incrustats que costen de parsejar. Però en la majoria de casos n'hi ha prou per tenir un punt de partida raonable que el client pot corregir després.</p>\n<h2 class=\"anchor anchorWithStickyNavbar_LWe7\" id=\"generació-de-disseny-amb-bedrock\">Generació de disseny amb Bedrock<a href=\"https://formacio.joansanfeliu.com/blog/donar-alta-client-escrivint-nomes-url#generaci%C3%B3-de-disseny-amb-bedrock\" class=\"hash-link\" aria-label=\"Direct link to Generació de disseny amb Bedrock\" title=\"Direct link to Generació de disseny amb Bedrock\">​</a></h2>\n<p>Un cop el tenant té la identitat visual bàsica, el sistema la passa a Amazon Bedrock (el servei d'AWS que dóna accés a models de llenguatge) per prendre decisions de disseny: quina tipografia complementa aquells colors, quina disposició d'elements funciona millor, quin to de text encaixa amb la marca.</p>\n<p>Tot passa dins d'un sol endpoint intern que executa la cadena sencera:</p>\n<ol>\n<li>Llegeix el branding del tenant.</li>\n<li>Envia un prompt a Bedrock amb la identitat visual i rep les decisions de disseny.</li>\n<li>Renderitza la pàgina en tres llengües.</li>\n<li>Crea una aplicació d'Amplify (el servei de desplegament web d'AWS) i la desplega.</li>\n</ol>\n<!-- -->\n<p>El resultat és una URL pública funcional. El client no ha pujat cap foto, no ha triat cap plantilla, no ha escrit cap text.</p>\n<h2 class=\"anchor anchorWithStickyNavbar_LWe7\" id=\"la-web-que-es-regenera-sola\">La web que es regenera sola<a href=\"https://formacio.joansanfeliu.com/blog/donar-alta-client-escrivint-nomes-url#la-web-que-es-regenera-sola\" class=\"hash-link\" aria-label=\"Direct link to La web que es regenera sola\" title=\"Direct link to La web que es regenera sola\">​</a></h2>\n<p>La pàgina pública no és estàtica. Cada vegada que acaba una recollida de valoracions, la web es torna a generar per incorporar les noves dades.</p>\n<p>El mecanisme és un comptador <code>pendingJobs</code> al registre del tenant. Cada tasca en curs l'incrementa. Quan una tasca acaba, el decrementa. Quan arriba a zero, el sistema crida <code>publishTenantSite</code>, que torna a executar tota la cadena de generació i desplegament.</p>\n<p>Això vol dir que la primera recollida —la gratuïta que forma part de l'onboarding— és la que dispara la primera publicació de la web. L'auto-debut no és un pas separat: és una conseqüència natural de que <code>pendingJobs</code> arribi a zero per primera vegada.</p>\n<h2 class=\"anchor anchorWithStickyNavbar_LWe7\" id=\"freemium-amb-límits-durs\">Freemium amb límits durs<a href=\"https://formacio.joansanfeliu.com/blog/donar-alta-client-escrivint-nomes-url#freemium-amb-l%C3%ADmits-durs\" class=\"hash-link\" aria-label=\"Direct link to Freemium amb límits durs\" title=\"Direct link to Freemium amb límits durs\">​</a></h2>\n<p>El pla gratuït és deliberadament curt: una propietat, un canal de recollida i una recollida amb un sostre d'unes trenta valoracions. Aquests límits s'apliquen al servidor, no a la interfície. No és un avís que diu \"has arribat al límit\": és un rebuig net de l'operació.</p>\n<p>La raó és pràctica. Si el límit és només visual, qualsevol crida directa a l'API se'l salta. Si és al servidor, no hi ha escletxa. El codi comprova el pla del tenant abans d'executar qualsevol operació que consumeixi quota.</p>\n<p>El pla gratuït té prou corda perquè l'usuari vegi el producte funcionar de veritat —rep valoracions, la web es publica— però no prou perquè s'hi quedi indefinidament sense pagar.</p>\n<h2 class=\"anchor anchorWithStickyNavbar_LWe7\" id=\"el-test-de-punta-a-punta\">El test de punta a punta<a href=\"https://formacio.joansanfeliu.com/blog/donar-alta-client-escrivint-nomes-url#el-test-de-punta-a-punta\" class=\"hash-link\" aria-label=\"Direct link to El test de punta a punta\" title=\"Direct link to El test de punta a punta\">​</a></h2>\n<p>He provat el funnel sencer amb un test E2E que reprodueix tot el camí:</p>\n<ol>\n<li>Alta amb correu, contrasenya i URL.</li>\n<li>Creació d'una propietat.</li>\n<li>Recollida de 29 valoracions.</li>\n<li>Verificació que la web s'ha publicat automàticament.</li>\n</ol>\n<p>El test confirma que no hi ha cap pas manual entremig. Des del <code>POST /public/signup</code> fins a la web desplegada a Amplify, tot passa sense intervenció. Si algun dia algú trenca la cadena —un canvi a Bedrock, un límit nou a Amplify, un error al rollback—, el test ho detectarà abans que cap usuari real s'hi trobi.</p>",
            "url": "https://formacio.joansanfeliu.com/blog/donar-alta-client-escrivint-nomes-url",
            "title": "Donar d'alta un client escrivint només la seva URL",
            "summary": "Com reduir el registre d'un SaaS a tres camps —correu, contrasenya i URL— fent que el sistema extregui la identitat visual de la web del client, generi el disseny amb IA i publiqui la pàgina automàticament.",
            "date_modified": "2026-09-07T00:00:00.000Z",
            "author": {
                "name": "Joan Sanfeliu",
                "url": "https://joansanfeliu.com"
            },
            "tags": [
                "aws",
                "amplify",
                "bedrock",
                "cognito",
                "onboarding",
                "saas"
            ]
        },
        {
            "id": "https://formacio.joansanfeliu.com/blog/newsletter-sense-proveidor-dynamodb-ses",
            "content_html": "<h2 class=\"anchor anchorWithStickyNavbar_LWe7\" id=\"per-què-no-mailchimp\">Per què no Mailchimp<a href=\"https://formacio.joansanfeliu.com/blog/newsletter-sense-proveidor-dynamodb-ses#per-qu%C3%A8-no-mailchimp\" class=\"hash-link\" aria-label=\"Direct link to Per què no Mailchimp\" title=\"Direct link to Per què no Mailchimp\">​</a></h2>\n<p>Mailchimp és una bona eina. Però té un preu: a partir d'uns quants centenars de contactes, pagues cada mes tant si envies com si no. I cedeixes les dades dels teus subscriptors a un tercer.</p>\n<p>Per a una llista petita, on l'únic que necessites és guardar adreces, enviar un correu i permetre la baixa, la plataforma és excés. Pots construir el que necessites amb el que ja tens a AWS.</p>\n<p>Això és el que he fet a <code>joan-utils</code>, el meu stack de CloudFormation a <code>eu-west-3</code>.</p>\n<h2 class=\"anchor anchorWithStickyNavbar_LWe7\" id=\"què-he-construït-de-debò\">Què he construït de debò<a href=\"https://formacio.joansanfeliu.com/blog/newsletter-sense-proveidor-dynamodb-ses#qu%C3%A8-he-constru%C3%AFt-de-deb%C3%B2\" class=\"hash-link\" aria-label=\"Direct link to Què he construït de debò\" title=\"Direct link to Què he construït de debò\">​</a></h2>\n<p>Una taula DynamoDB en mode <code>PAY_PER_REQUEST</code>. La clau de partició és <code>email</code>. Cada ítem té un camp <code>token</code> (UUID v4), un camp <code>status</code> i un <code>createdAt</code>.</p>\n<p>Els estats possibles són tres: <code>pending</code>, <code>active</code> i <code>unsubscribed</code>. La transició és sempre cap endavant: mai es torna a <code>pending</code> des d'<code>active</code>.</p>\n<p>Hi ha un índex secundari global (<code>token-index</code>) sobre el camp <code>token</code>. Sense aquest índex, confirmar o donar de baixa una adreça requeriria un <code>scan</code> de tota la taula. Amb l'índex, és una consulta puntual.</p>\n<p>Una sola Lambda en Node 24, arquitectura <code>arm64</code>, memòria 512 MB, darrere d'un HTTP API de API Gateway. Tres rutes:</p>\n<ul>\n<li><code>POST /newsletter/subscribe</code></li>\n<li><code>GET /newsletter/confirm</code></li>\n<li><code>GET /newsletter/unsubscribe</code></li>\n</ul>\n<!-- -->\n<h2 class=\"anchor anchorWithStickyNavbar_LWe7\" id=\"la-doble-confirmació-no-és-opcional\">La doble confirmació no és opcional<a href=\"https://formacio.joansanfeliu.com/blog/newsletter-sense-proveidor-dynamodb-ses#la-doble-confirmaci%C3%B3-no-%C3%A9s-opcional\" class=\"hash-link\" aria-label=\"Direct link to La doble confirmació no és opcional\" title=\"Direct link to La doble confirmació no és opcional\">​</a></h2>\n<p>Si acceptes una adreça sense confirmar, qualsevol pot subscriure qualsevol altra persona. Rebràs rebots. SES et penalitza pels rebots: si superes un cert percentatge, et suspenen l'enviament.</p>\n<p>Amb la doble confirmació, l'adreça no entra a la llista fins que el propietari fa clic. Punt.</p>\n<p>El flux és: reps el <code>POST</code>, guardes l'ítem en estat <code>pending</code>, envies el correu de confirmació amb l'enllaç <code>GET /newsletter/confirm?token=UUID</code>. Quan l'usuari fa clic, passes l'ítem a <code>active</code>.</p>\n<h2 class=\"anchor anchorWithStickyNavbar_LWe7\" id=\"el-token-de-baixa-a-cada-correu\">El token de baixa a cada correu<a href=\"https://formacio.joansanfeliu.com/blog/newsletter-sense-proveidor-dynamodb-ses#el-token-de-baixa-a-cada-correu\" class=\"hash-link\" aria-label=\"Direct link to El token de baixa a cada correu\" title=\"Direct link to El token de baixa a cada correu\">​</a></h2>\n<p>Cada correu d'enviament ha de portar un enllaç de baixa. No a la pàgina principal, no a un formulari: un enllaç directe que faci la baixa en un sol clic.</p>\n<p>El token que guardes a DynamoDB serveix per a les dues coses: confirmació i baixa. No cal generar-ne un de nou per a cada enviament.</p>\n<p>El que no pots fer és respondre 404 si l'adreça ja és <code>unsubscribed</code>. Respondries informació sobre l'estat de la llista. Retorna sempre 200.</p>\n<h2 class=\"anchor anchorWithStickyNavbar_LWe7\" id=\"el-cas-de-ladreça-ja-activa\">El cas de l'adreça ja activa<a href=\"https://formacio.joansanfeliu.com/blog/newsletter-sense-proveidor-dynamodb-ses#el-cas-de-ladre%C3%A7a-ja-activa\" class=\"hash-link\" aria-label=\"Direct link to El cas de l'adreça ja activa\" title=\"Direct link to El cas de l'adreça ja activa\">​</a></h2>\n<p>Si algú envia el formulari dues vegades amb la mateixa adreça i l'estat ja és <code>active</code>, no reenvies el correu de confirmació. Retorna 200 i prou. Si reenviessis, estaríes confirmant implícitament que aquella adreça és a la llista. No filtris informació.</p>\n<h2 class=\"anchor anchorWithStickyNavbar_LWe7\" id=\"els-números-de-la-primera-prova-real\">Els números de la primera prova real<a href=\"https://formacio.joansanfeliu.com/blog/newsletter-sense-proveidor-dynamodb-ses#els-n%C3%BAmeros-de-la-primera-prova-real\" class=\"hash-link\" aria-label=\"Direct link to Els números de la primera prova real\" title=\"Direct link to Els números de la primera prova real\">​</a></h2>\n<p>Aquestes són les mesures que he obtingut:</p>\n<ul>\n<li>Alta (cold start): <strong>518 ms</strong></li>\n<li>Confirmació (segona invocació): <strong>149 ms</strong></li>\n<li>Tercera invocació (warm): <strong>6,5 ms</strong></li>\n</ul>\n<p>El cold start de 518 ms és acceptable per a un formulari web. No és un camí crític.</p>\n<p>El cost del tram:</p>\n<ul>\n<li>SES: <strong>0,10 $ per cada 1.000 correus</strong></li>\n<li>DynamoDB: sota el nivell gratuït amb qualsevol llista petita</li>\n<li>Lambda: dècimes de cèntim al mes</li>\n</ul>\n<p>Per a una llista de menys de deu mil subscriptors, el cost mensual és essencialment zero si no envies cada dia.</p>\n<h2 class=\"anchor anchorWithStickyNavbar_LWe7\" id=\"quan-no-compensa-fer-tho-tu\">Quan NO compensa fer-t'ho tu<a href=\"https://formacio.joansanfeliu.com/blog/newsletter-sense-proveidor-dynamodb-ses#quan-no-compensa-fer-tho-tu\" class=\"hash-link\" aria-label=\"Direct link to Quan NO compensa fer-t'ho tu\" title=\"Direct link to Quan NO compensa fer-t'ho tu\">​</a></h2>\n<p>Aquesta solució cobreix: guardar adreces, confirmar, donar de baixa, enviar un correu simple.</p>\n<p>No cobreix:</p>\n<ul>\n<li><strong>Plantilles visuals</strong>: SES accepta HTML, però el disseny i el manteniment de les plantilles van a càrrec teu.</li>\n<li><strong>Segmentació</strong>: si vols enviar coses diferents a grups diferents, necessites més camps, més índexs i més lògica.</li>\n<li><strong>Informes d'obertura i clics</strong>: SES pot publicar events a SNS, però processar-los i mostrar-los és un projecte a part.</li>\n<li><strong>Gestió de rebots i queixes automàtica</strong>: SES envia notificacions, però has d'implementar el handler que actualitzi els estats.</li>\n</ul>\n<p>Si necessites alguna d'aquestes coses, una plataforma dedicada et sortirà més barata en hores de feina. La decisió no és ideològica: és quant val el teu temps.</p>\n<h2 class=\"anchor anchorWithStickyNavbar_LWe7\" id=\"el-que-ha-costat-de-trobar\">El que ha costat de trobar<a href=\"https://formacio.joansanfeliu.com/blog/newsletter-sense-proveidor-dynamodb-ses#el-que-ha-costat-de-trobar\" class=\"hash-link\" aria-label=\"Direct link to El que ha costat de trobar\" title=\"Direct link to El que ha costat de trobar\">​</a></h2>\n<p>Dos problemes que no eren evidents:</p>\n<p>Primer, l'índex secundari global triga uns segons a estar disponible després de crear la taula. En un desplegament de CloudFormation en un entorn nou, la primera invocació pot fallar si la Lambda s'executa just després de la creació. Ho vaig resoldre afegint un <code>DependsOn</code> explícit al recurs de la Lambda cap a la taula, i acceptant que el primer cold start pot trobar l'índex encara en estat <code>CREATING</code>. En producció no és un problema perquè la taula ja existeix.</p>\n<p>Segon, SES a <code>eu-west-3</code> requereix que el domini estigui verificat. Si estàs en sandbox, a més, les adreces de destinació també han d'estar verificades. Vaig perdre una estona fins que vaig entendre que el problema no era el codi sinó que SES encara no havia sortit del sandbox.</p>",
            "url": "https://formacio.joansanfeliu.com/blog/newsletter-sense-proveidor-dynamodb-ses",
            "title": "Una newsletter sense proveïdor: DynamoDB, SES i 150 línies",
            "summary": "Com he construït la gestió de subscriptors d'una newsletter petita amb DynamoDB, SES i una sola Lambda, sense contractar cap plataforma externa. Doble confirmació, token de baixa i els números reals.",
            "date_modified": "2026-08-31T00:00:00.000Z",
            "author": {
                "name": "Joan Sanfeliu",
                "url": "https://joansanfeliu.com"
            },
            "tags": [
                "aws",
                "dynamodb",
                "ses",
                "lambda",
                "cloudformation"
            ]
        },
        {
            "id": "https://formacio.joansanfeliu.com/blog/que-hi-trobaras",
            "content_html": "<p>Fa temps que tinc aquí els apunts dels cursos que faig: HTML, CSS, JavaScript i, des\nd'aquesta setmana, AWS serverless. Són material de classe, ordenat i pensat per\nseguir-lo de principi a fi.</p>\n<p>Aquest blog és una altra cosa.</p>\n<h2 class=\"anchor anchorWithStickyNavbar_LWe7\" id=\"la-diferència\">La diferència<a href=\"https://formacio.joansanfeliu.com/blog/que-hi-trobaras#la-difer%C3%A8ncia\" class=\"hash-link\" aria-label=\"Direct link to La diferència\" title=\"Direct link to La diferència\">​</a></h2>\n<p>Els apunts expliquen <strong>com funciona una cosa</strong>. Estan pensats perquè algú que comença\nels pugui seguir, i per això són ordenats i previsibles.</p>\n<p>El blog explica <strong>què m'he trobat construint</strong>. Tinc software propi en producció —un\nSaaS de reserves per a restaurants, un parell de productes més i un grapat de coses\npetites que em fan la feina— i cada mes hi ha alguna cosa que no surt com esperava.\nUna factura que no quadra, una decisió d'arquitectura que va sortir malament, un\nservei d'AWS que fa una cosa diferent de la que diu la documentació.</p>\n<p>Això és el que hi posaré.</p>\n<h2 class=\"anchor anchorWithStickyNavbar_LWe7\" id=\"tres-regles-que-em-poso\">Tres regles que em poso<a href=\"https://formacio.joansanfeliu.com/blog/que-hi-trobaras#tres-regles-que-em-poso\" class=\"hash-link\" aria-label=\"Direct link to Tres regles que em poso\" title=\"Direct link to Tres regles que em poso\">​</a></h2>\n<p><strong>Números reals.</strong> Si escric sobre costos, seran els meus, tal com surten a la factura.\nNo estimacions ni exemples inventats. Si dic que una cosa em costa 8,40 $, és perquè\nme n'ha costat 8,40.</p>\n<p><strong>També el que no ha funcionat.</strong> Els posts on tot surt bé a la primera no serveixen a\nningú, perquè no és així com va. Si vaig trigar tres dies a trobar per què petava,\naixò és el post.</p>\n<p><strong>Res copiat.</strong> Hi ha blogs de serverless molt millors que aquest, i els llegeixo. Quan\nparteixi d'una idea d'algú altre, ho diré i hi posaré l'enllaç.</p>\n<h2 class=\"anchor anchorWithStickyNavbar_LWe7\" id=\"cada-quan\">Cada quan<a href=\"https://formacio.joansanfeliu.com/blog/que-hi-trobaras#cada-quan\" class=\"hash-link\" aria-label=\"Direct link to Cada quan\" title=\"Direct link to Cada quan\">​</a></h2>\n<p>Un post a la setmana, els dilluns. Si alguna setmana no tinc res que valgui la pena,\nno publicaré per publicar.</p>\n<p>Si vols que t'avisi quan n'hi hagi un de nou, tens el formulari aquí sota. Un correu\nper setmana com a molt, i te'n pots donar de baixa quan vulguis.</p>",
            "url": "https://formacio.joansanfeliu.com/blog/que-hi-trobaras",
            "title": "Què hi trobaràs aquí",
            "summary": "Per què obro un blog al costat dels apunts, què hi publicaré i cada quan.",
            "date_modified": "2026-08-30T00:00:00.000Z",
            "author": {
                "name": "Joan Sanfeliu",
                "url": "https://joansanfeliu.com"
            },
            "tags": [
                "meta"
            ]
        }
    ]
}