<?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: Lucas</title>
    <description>The latest articles on DEV Community by Lucas (@_9848c5582063b42abecb7).</description>
    <link>https://dev.to/_9848c5582063b42abecb7</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%2F3915788%2Fb07257c2-824f-43cb-b42e-635e733556bf.png</url>
      <title>DEV Community: Lucas</title>
      <link>https://dev.to/_9848c5582063b42abecb7</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/_9848c5582063b42abecb7"/>
    <language>en</language>
    <item>
      <title>I tried "GEO" on my indie tool — it was just SEO (here's the GSC data)</title>
      <dc:creator>Lucas</dc:creator>
      <pubDate>Sun, 14 Jun 2026 23:34:48 +0000</pubDate>
      <link>https://dev.to/_9848c5582063b42abecb7/i-tried-geo-on-my-indie-tool-it-was-just-seo-heres-the-gsc-data-59gn</link>
      <guid>https://dev.to/_9848c5582063b42abecb7/i-tried-geo-on-my-indie-tool-it-was-just-seo-heres-the-gsc-data-59gn</guid>
      <description>&lt;p&gt;Every other newsletter this year told me my SaaS needed &lt;strong&gt;GEO&lt;/strong&gt; — Generative Engine Optimization. Write an &lt;code&gt;llms.txt&lt;/code&gt;. Add "AI-only" schema. Chunk your content for the models. So before sinking a weekend into it, I read one contrarian source, opened Google Search Console for my tool, and looked at the actual numbers. The verdict was boring and freeing: for Google, optimizing for AI answers is &lt;em&gt;still just SEO&lt;/em&gt;. The real problem wasn't my markup — it was that almost nothing about my site was discoverable in the first place. Here's the data, the three fixes that mattered, and the two places I face-planted.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I didn't buy the GEO hype
&lt;/h2&gt;

&lt;p&gt;I'm building &lt;a href="https://mimi-seed.pryzm.gg" rel="noopener noreferrer"&gt;Mimi Seed&lt;/a&gt;, an open-source MCP server that lets indie devs drive Play Store / App Store / Firebase releases from inside Claude Code or Codex. Classic indie-hacker situation: tiny audience, and a constant temptation to chase the shiny new growth lever.&lt;/p&gt;

&lt;p&gt;The single most useful thing I read was a curated list (&lt;a href="https://github.com/aldegad/awesome-geo" rel="noopener noreferrer"&gt;&lt;code&gt;awesome-geo&lt;/code&gt;&lt;/a&gt;) whose thesis is blunt: &lt;strong&gt;most "GEO" advice is repackaged SEO with a markup tax.&lt;/strong&gt; Their reading of the primary docs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Google has publicly said optimizing for AI Overviews is the same SEO you already know. No special schema required.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;llms.txt&lt;/code&gt; is a community proposal. Google doesn't support it and has no plans to. Treating it as mandatory is hype.&lt;/li&gt;
&lt;li&gt;The one engine that &lt;em&gt;does&lt;/em&gt; ask for special treatment is Bing/Copilot (FAQ markup, tables, IndexNow).&lt;/li&gt;
&lt;li&gt;The actual common denominator across every engine: clear authorship, a consistent entity identity, canonical URLs, and &lt;strong&gt;being cited by trustworthy external sources.&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last bullet turned out to be the whole game. But I didn't take their word for it — I went to the data.&lt;/p&gt;

&lt;h2&gt;
  
  
  What GSC actually told me
&lt;/h2&gt;

&lt;p&gt;My landing page lives on a subdomain. I didn't even have a dedicated Search Console property for it — it was rolled up under the parent domain property. So I queried the parent property and filtered to just my pages. Ninety days:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;One&lt;/strong&gt; URL had any impressions: the homepage. &lt;strong&gt;19 impressions, 0 clicks, average position 6.2.&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;The query breakdown came back &lt;strong&gt;empty.&lt;/strong&gt; Every one of those 19 impressions was below GSC's anonymization threshold — i.e., low-volume brand-ish searches. &lt;strong&gt;Zero non-brand discovery.&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;URL inspection: the homepage &lt;em&gt;was&lt;/em&gt; indexed, canonical correct, crawled fine. So indexing wasn't broken.&lt;/li&gt;
&lt;li&gt;Referring URLs: &lt;strong&gt;exactly one&lt;/strong&gt; — &lt;code&gt;libraries.io&lt;/code&gt;, an npm mirror. My entire backlink profile was an auto-generated package page.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This reframed everything. My problem was never snippet wording or AI-readability. You can't optimize the click-through rate of a result nobody sees. The bottleneck was &lt;strong&gt;impressions&lt;/strong&gt; — being found at all — and the lever for that, per the data and the contrarian list, is &lt;em&gt;external citations&lt;/em&gt;, not markup.&lt;/p&gt;

&lt;p&gt;And one more thing I'd half-ignored: my sitemap had &lt;strong&gt;never been submitted&lt;/strong&gt; to Search Console. It existed at &lt;code&gt;/sitemap.xml&lt;/code&gt;, generated by the framework, registered nowhere.&lt;/p&gt;

&lt;h2&gt;
  
  
  The three fixes that were actually SEO
&lt;/h2&gt;

&lt;p&gt;No &lt;code&gt;llms.txt&lt;/code&gt;. No secret AI schema. Just hygiene:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. A real entity graph.&lt;/strong&gt; My JSON-LD was a lone &lt;code&gt;SoftwareApplication&lt;/code&gt; object. I replaced it with a proper &lt;code&gt;@graph&lt;/code&gt; tying together &lt;code&gt;Organization&lt;/code&gt; + &lt;code&gt;WebSite&lt;/code&gt; + &lt;code&gt;SoftwareApplication&lt;/code&gt;, and — the part that matters for both SEO and "GEO" — a &lt;code&gt;sameAs&lt;/code&gt; array linking the GitHub repo and both npm packages.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json-doc"&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;"@context"&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://schema.org"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"@graph"&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="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;"Organization"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"@id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://example.com/#organization"&lt;/span&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;"Mimi Seed"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"sameAs"&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;"https://github.com/jeonghwanko/mimi-seed-sdk"&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.npmjs.com/package/mimi-seed"&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.npmjs.com/package/@yoonion/mimi-seed-mcp"&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="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;"WebSite"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"publisher"&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;"@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;"https://example.com/#organization"&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="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;"SoftwareApplication"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"publisher"&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;"@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;"https://example.com/#organization"&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;code&gt;sameAs&lt;/code&gt; is the literal mechanism for telling a search/answer engine "the website, the GitHub org, and the npm packages are all the same entity." For a project that recently went through a rename, consolidating that identity is the highest-leverage markup change you can make.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. A sitemap that isn't lying.&lt;/strong&gt; My generated sitemap listed anchor URLs — &lt;code&gt;/#install&lt;/code&gt;, &lt;code&gt;/#features&lt;/code&gt;, &lt;code&gt;/#cli&lt;/code&gt;. Here's the detail no tutorial mentions: &lt;strong&gt;Google strips the fragment.&lt;/strong&gt; Every one of those collapses to the same canonical &lt;code&gt;/&lt;/code&gt;. They add zero index coverage and just noise. Worse, the sitemap also listed &lt;code&gt;/tool&lt;/code&gt; — which my &lt;code&gt;robots.txt&lt;/code&gt; &lt;em&gt;disallowed&lt;/em&gt;. That's a self-inflicted "Submitted URL blocked by robots.txt" warning. My landing is a single-page app, so the honest sitemap is exactly one URL. I trimmed it to that, then submitted it. Google fetched it within seconds: 0 errors, &lt;strong&gt;1 URL&lt;/strong&gt; — which is the fragment-dedup behavior proving itself.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. A dedicated property.&lt;/strong&gt; I added a URL-prefix property for the subdomain so its impressions/positions stop being averaged in with unrelated sites. Pure reporting hygiene, but you can't improve what you can't see cleanly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where I face-planted
&lt;/h2&gt;

&lt;p&gt;Two pitfalls ate more time than the actual SEO work.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Next.js caches your &lt;code&gt;public/&lt;/code&gt; file list.&lt;/strong&gt; Google's verification needed &lt;code&gt;google&amp;lt;token&amp;gt;.html&lt;/code&gt; served at the site root. I dropped it into &lt;code&gt;public/&lt;/code&gt; on the live box and curled it — &lt;strong&gt;404.&lt;/strong&gt; But an existing file in the same directory (&lt;code&gt;/icons/...png&lt;/code&gt;) returned 200. The running server had snapshotted the set of public files at build/boot and never rescanned. A graceful reload fixed it (the file was on disk the whole time). If you ever hot-patch a static file into a running Node server and it 404s while its neighbors don't, that's your clue — restart, don't debug the path.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Your API's Google identity ≠ your browser's.&lt;/strong&gt; I tried to submit the sitemap programmatically and got a flat &lt;code&gt;403: insufficient permission&lt;/code&gt; for the new property — even though I'd just verified it in the browser. The catch: the automation authenticates as one Google account; my browser session was a &lt;em&gt;different&lt;/em&gt; account. The property was owned by the browser identity, invisible to the API identity. Once I re-verified it under the same account that owned the parent domain property (the one the automation actually uses), the submit went through instantly. Verification is per-identity, and "I'm logged in as me" in two places doesn't mean it's the &lt;em&gt;same&lt;/em&gt; me.&lt;/p&gt;

&lt;h2&gt;
  
  
  The honest limitation
&lt;/h2&gt;

&lt;p&gt;I fixed the hygiene. I did &lt;strong&gt;not&lt;/strong&gt; fix the actual bottleneck. My backlink profile is still one auto-generated npm mirror. The entity graph helps an engine &lt;em&gt;understand&lt;/em&gt; my project once it finds it — but the data is unambiguous that the gating factor is being &lt;em&gt;found&lt;/em&gt;, and that's a function of external citations I don't have yet. Markup was the easy 20%. The hard 80% — getting real, trustworthy sources to mention the project — is exactly the work that doesn't fit in a JSON-LD block.&lt;/p&gt;

&lt;p&gt;So this post is also me doing something about that. Which is very on-brand for the conclusion.&lt;/p&gt;

&lt;h2&gt;
  
  
  What surprised me
&lt;/h2&gt;

&lt;p&gt;The contrarian source was &lt;em&gt;more&lt;/em&gt; actionable than any "47 GEO tactics" listicle, precisely because it told me what &lt;strong&gt;not&lt;/strong&gt; to do. Skipping &lt;code&gt;llms.txt&lt;/code&gt; and AI-schema cargo-culting saved the weekend I'd budgeted for them, and the data said that weekend would've moved nothing.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd ask you
&lt;/h2&gt;

&lt;p&gt;If you've shipped a small tool or SaaS: did "GEO" ever do anything measurable for you that plain SEO + getting cited didn't? Or is it, as far as the data goes, just SEO with a markup tax and better marketing? I'd genuinely like to be wrong here — tell me what you've measured.&lt;/p&gt;

</description>
      <category>seo</category>
      <category>nextjs</category>
      <category>webdev</category>
      <category>opensource</category>
    </item>
    <item>
      <title>"커피콩 이용 방법 — 매일 마시는 커피로 기프티콘 모으는 법 (완전 가이드)"</title>
      <dc:creator>Lucas</dc:creator>
      <pubDate>Tue, 09 Jun 2026 06:04:22 +0000</pubDate>
      <link>https://dev.to/_9848c5582063b42abecb7/keopikong-iyong-bangbeob-maeil-masineun-keopiro-gipeutikon-moeuneun-beob-wanjeon-gaideu-4mec</link>
      <guid>https://dev.to/_9848c5582063b42abecb7/keopikong-iyong-bangbeob-maeil-masineun-keopiro-gipeutikon-moeuneun-beob-wanjeon-gaideu-4mec</guid>
      <description>&lt;p&gt;매일 커피 한 잔은 마시는데, 그 돈이 그냥 사라지는 게 아깝다면 — 리워드 앱 &lt;strong&gt;&lt;a href="https://coffee.pryzm.gg/" rel="noopener noreferrer"&gt;커피콩 (CoffeeCong)&lt;/a&gt;&lt;/strong&gt; 이용 방법을 처음부터 끝까지 정리했습니다. 카페에 가고, 영수증을 찍고, 짧은 광고를 보는 일상 행동을 &lt;strong&gt;원두(포인트)&lt;/strong&gt; 로 바꿔 모으면, 실제 &lt;strong&gt;커피 기프티콘&lt;/strong&gt;으로 교환할 수 있습니다.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;한 줄 요약: 일상 커피 습관 → 원두 적립 → 기프티콘 교환. &lt;strong&gt;10원두 = 1원&lt;/strong&gt; 가치로 환산됩니다.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  1. 설치하고 시작하기
&lt;/h2&gt;

&lt;p&gt;먼저 앱을 받습니다.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Google Play / App Store에서 &lt;strong&gt;"커피콩"&lt;/strong&gt; 검색&lt;/li&gt;
&lt;li&gt;설치 후 닉네임만 정하면 끝 — 회원가입 절차가 따로 없고, 첫 진입에서 원두를 바로 줍니다.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;웹에서 서비스 소개와 커피 혜택 정보를 먼저 보고 싶다면 &lt;a href="https://coffee.pryzm.gg/" rel="noopener noreferrer"&gt;coffee.pryzm.gg&lt;/a&gt; 에서 확인할 수 있습니다.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. 원두 모으는 6가지 방법
&lt;/h2&gt;

&lt;p&gt;커피콩의 핵심은 "한 가지만 하는 앱"이 아니라는 점입니다. 본인 생활 패턴에 맞는 경로 몇 개만 챙겨도 충분합니다.&lt;/p&gt;

&lt;h3&gt;
  
  
  ① 카페 체크인
&lt;/h3&gt;

&lt;p&gt;커피숍에 갔을 때 앱에서 체크인하면 원두가 적립됩니다. 가장 가볍고, 매일 하는 카페 방문이 그대로 적립으로 이어집니다.&lt;/p&gt;

&lt;h3&gt;
  
  
  ② 커피 영수증 인증
&lt;/h3&gt;

&lt;p&gt;오늘 마신 커피 영수증을 사진으로 인증하면 추가 원두를 받습니다. 그냥 버릴 영수증 한 장이 기프티콘에 한 걸음 더 가까워지는 셈입니다.&lt;/p&gt;

&lt;h3&gt;
  
  
  ③ 통신사·카드사 커피 혜택 챙기기
&lt;/h3&gt;

&lt;p&gt;매일 바뀌는 커피 할인·쿠폰·멤버십 혜택을 앱에서 모아 보여줍니다. 놓치기 쉬운 통신사·카드사 커피 혜택을 한곳에서 확인할 수 있어, 적립과 별개로 &lt;strong&gt;당장 커피값을 아끼는&lt;/strong&gt; 용도로도 좋습니다. (커피값 아끼는 법은 &lt;a href="https://coffee.pryzm.gg/guide/" rel="noopener noreferrer"&gt;이 가이드&lt;/a&gt;에 정리돼 있습니다.)&lt;/p&gt;

&lt;h3&gt;
  
  
  ④ 방치형 원두농장
&lt;/h3&gt;

&lt;p&gt;앱을 계속 붙잡고 있지 않아도 됩니다. 방치형 원두농장은 시간이 지날수록 원두가 쌓이고, 가끔 들어와 수확만 하면 됩니다. 잠잘 때·일할 때도 적립이 굴러갑니다.&lt;/p&gt;

&lt;h3&gt;
  
  
  ⑤ 미니게임
&lt;/h3&gt;

&lt;p&gt;쉬는 시간에 짧은 미니게임을 플레이해 원두를 모을 수 있습니다. 가볍게 즐기면서 적립하는 재미 요소입니다.&lt;/p&gt;

&lt;h3&gt;
  
  
  ⑥ 광고 보기 · 행운 뽑기
&lt;/h3&gt;

&lt;p&gt;짧은 광고를 보면 원두를 받고, 모은 티켓으로 &lt;strong&gt;행운 뽑기&lt;/strong&gt;를 돌려 한 번에 큰 원두를 노릴 수도 있습니다.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. 모은 원두를 기프티콘으로 교환
&lt;/h2&gt;

&lt;p&gt;원두가 충분히 쌓이면 &lt;strong&gt;상품권 교환소&lt;/strong&gt;에서 교환합니다.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;교환비율: &lt;strong&gt;10원두 = 1원&lt;/strong&gt; 가치&lt;/li&gt;
&lt;li&gt;교환 가능 품목: 커피 기프티콘, 네이버페이, 편의점 상품권 등 모바일 상품권&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;즉 "오늘 마신 커피값"을 시간을 들여 다시 커피 기프티콘으로 돌려받는 구조입니다.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. 더 빨리 모으는 꿀팁
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;매일 출석(스트릭) 유지&lt;/strong&gt;: 연속 출석일이 쌓일수록 보너스가 붙습니다. 하루도 빠지지 않는 게 누적 적립에서 가장 큽니다.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;주간 챌린지&lt;/strong&gt;: 한 주 동안 체크인 횟수를 채우면 추가 보상을 받습니다.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;경로 분산&lt;/strong&gt;: 체크인 + 영수증 + 방치 농장 세 가지만 매일 챙겨도 적립 속도가 확 올라갑니다.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  보너스 — 공부하기 좋은 카페 찾기
&lt;/h2&gt;

&lt;p&gt;커피콩 생태계에는 &lt;strong&gt;&lt;a href="https://coffee.pryzm.gg/coffeemap/" rel="noopener noreferrer"&gt;서울 카공 카페 지도&lt;/a&gt;&lt;/strong&gt; 도 있습니다. 자치구·지하철역·목적(조용한 곳, 노트북 작업 등)별로 카페를 찾을 수 있어, 카페에서 공부하거나 작업하는 분이라면 체크인 적립과 함께 활용하기 좋습니다.&lt;/p&gt;




&lt;p&gt;정리하면 — 커피콩은 &lt;strong&gt;이미 하고 있는 커피 소비를 리워드로 전환&lt;/strong&gt;하는 앱입니다. 설치하고, 본인에게 맞는 적립 경로 2~3개만 매일 챙기면 됩니다. 자세한 서비스 정보는 &lt;a href="https://coffee.pryzm.gg/" rel="noopener noreferrer"&gt;coffee.pryzm.gg&lt;/a&gt; 에서 볼 수 있습니다.&lt;/p&gt;

&lt;p&gt;여러분은 커피값을 어떻게 아끼고 계신가요? 쓰고 있는 커피 적립·할인 꿀팁이 있다면 댓글로 공유해 주세요.&lt;/p&gt;

</description>
      <category>korean</category>
      <category>tutorial</category>
      <category>productivity</category>
      <category>mobile</category>
    </item>
    <item>
      <title>"새 도메인에 프로그래매틱 SEO 페이지 8,000개를 띄웠다. 구글은 거의 색인하지 않았다 — 부검 기록"</title>
      <dc:creator>Lucas</dc:creator>
      <pubDate>Tue, 09 Jun 2026 06:02:28 +0000</pubDate>
      <link>https://dev.to/_9848c5582063b42abecb7/sae-domeine-peurogeuraemaetig-seo-peiji-8000gaereul-ddyiweossda-gugeuleun-geoyi-saeginhaji-anhassda-bugeom-girog-2o7p</link>
      <guid>https://dev.to/_9848c5582063b42abecb7/sae-domeine-peurogeuraemaetig-seo-peiji-8000gaereul-ddyiweossda-gugeuleun-geoyi-saeginhaji-anhassda-bugeom-girog-2o7p</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;3줄 요약&lt;/strong&gt; — 갓 만든 도메인에 서울 "카공 카페" 지도용 페이지 약 8,000개를 SSR로 띄웠다. 노출이 6일 만에 3 → 209로 올랐다가 ~0으로 붕괴했다. 버그가 아니었다. robots·canonical·fetch 전부 정상. 신규 도메인의 &lt;em&gt;권위(authority)&lt;/em&gt; 벽이었고, 깨끗한 코드나 &lt;code&gt;sitemap.xml&lt;/code&gt; 수정으로는 못 뚫는다. 내가 점검한 전부, 바꾼 것, 그리고 불편한 교훈을 적는다.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  배경
&lt;/h2&gt;

&lt;p&gt;작은 한국 앱 &lt;a href="https://coffee.pryzm.gg/" rel="noopener noreferrer"&gt;커피콩 (CoffeeCong)&lt;/a&gt; 을 운영한다. 매일 마시는 커피 습관을 리워드로 바꿔주는 앱이다. 콘텐츠/SEO 전략으로 프로그래매틱 사이트를 하나 만들었다 — &lt;strong&gt;서울의 공부하기 좋은 카페 지도&lt;/strong&gt;. 자치구별·지하철역별·목적별(조용한 곳, 노트북 작업, 심야 등)로 탐색할 수 있다.&lt;/p&gt;

&lt;p&gt;페이지 수는 조합에서 나온다:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;자치구(25) × 목적(7)        → 카테고리 허브 약 175개
+ 지하철역(약 300)          → 역세권 허브
+ 개별 카페(약 6,000)       → 카페당 1페이지
≈ 8,000 URL
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;아키텍처는 &lt;strong&gt;셸 주입(shell-injection) SSR&lt;/strong&gt; 패턴이다 — 사용자에게는 인터랙티브 React 지도를 그대로 주고, 봇에게는 같은 셸에 완전히 렌더된 HTML 본문을 주입한다:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;renderSsr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;PageData&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;string&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;template&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;readTemplate&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;              &lt;span class="c1"&gt;// SPA 셸 (&amp;lt;div id="root"&amp;gt;&amp;lt;/div&amp;gt; 보유)&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ssrBody&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;buildSsrBody&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;           &lt;span class="c1"&gt;// h1 + 본문 + 디렉토리 링크&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;headInject&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`
    &amp;lt;link rel="canonical" href="&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;canonical&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;" /&amp;gt;
    &amp;lt;meta name="robots" content="index, follow, max-image-preview:large" /&amp;gt;
    &amp;lt;script type="application/ld+json"&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;jsonLd&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;lt;/script&amp;gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;html&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;template&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;&amp;lt;div id="root"&amp;gt;&amp;lt;/div&amp;gt;&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;`&amp;lt;div id="root"&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;ssrBody&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;lt;/div&amp;gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;html&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;html&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;&amp;lt;/head&amp;gt;&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;headInject&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;lt;/head&amp;gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;html&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;p&gt;그래서 모든 URL이 Googlebot에게 진짜 HTML이다: &lt;code&gt;&amp;lt;h1&amp;gt;&lt;/code&gt;, 브레드크럼, &lt;code&gt;JSON-LD&lt;/code&gt;, 내부 링크. 본문을 읽는 데 JS가 필요 없다. 사이트맵 제출 완료, &lt;code&gt;robots.txt&lt;/code&gt; 깨끗함. 다들 "이건 꼭 제대로 해라"라고 말하는 그 부분 — 나는 제대로 했다.&lt;/p&gt;

&lt;h2&gt;
  
  
  상승, 그리고 절벽
&lt;/h2&gt;

&lt;p&gt;다음은 Search Console의 실제 일별 노출 곡선이다:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;날짜&lt;/th&gt;
&lt;th&gt;노출&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1일차&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2일차&lt;/td&gt;
&lt;td&gt;31&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3일차&lt;/td&gt;
&lt;td&gt;64&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4일차&lt;/td&gt;
&lt;td&gt;126&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5일차&lt;/td&gt;
&lt;td&gt;189&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6일차&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;209&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7일차&lt;/td&gt;
&lt;td&gt;14&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8일차~&lt;/td&gt;
&lt;td&gt;2 → 0 → 0&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;"되고 있어!" 6일 — 그리고 절벽에서 떨어져 0에 수렴했다. 모든 사이트맵: &lt;strong&gt;제출 N개, 색인 0개.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;내 첫 직감은 당신과 같았다: &lt;em&gt;뭔가 망가졌다.&lt;/em&gt; 아니었다.&lt;/p&gt;

&lt;h2&gt;
  
  
  부검 (실제로 점검한 것)
&lt;/h2&gt;

&lt;p&gt;루트·허브·카테고리 샘플에 URL Inspection API를 돌렸다. 모든 신호가 멀쩡했다:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;verdict&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;           &lt;span class="s"&gt;NEUTRAL&lt;/span&gt;
&lt;span class="na"&gt;coverageState&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;     &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Crawled&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;-&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;currently&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;not&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;indexed"&lt;/span&gt;
&lt;span class="na"&gt;robotsTxtState&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;    &lt;span class="s"&gt;ALLOWED&lt;/span&gt;
&lt;span class="na"&gt;indexingState&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;     &lt;span class="s"&gt;INDEXING_ALLOWED&lt;/span&gt;
&lt;span class="na"&gt;pageFetchState&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;    &lt;span class="s"&gt;SUCCESSFUL&lt;/span&gt;
&lt;span class="na"&gt;googleCanonical&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;   &lt;span class="s"&gt;(허브에서 userCanonical과 일치)&lt;/span&gt;
&lt;span class="na"&gt;crawledAs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;         &lt;span class="s"&gt;MOBILE&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;다시 읽어보자: &lt;strong&gt;&lt;code&gt;SUCCESSFUL&lt;/code&gt; fetch, &lt;code&gt;ALLOWED&lt;/code&gt; robots, &lt;code&gt;INDEXING_ALLOWED&lt;/code&gt;&lt;/strong&gt; — 그런데도 색인 안 됨. 구글이 페이지를 &lt;em&gt;크롤했다.&lt;/em&gt; 단지 색인에 둘 가치가 없다고 판단했을 뿐이다.&lt;/p&gt;

&lt;p&gt;이 "상승 후 붕괴" 모양은 알려진 패턴이다: 신규 도메인은 &lt;strong&gt;탐색 크롤 버스트&lt;/strong&gt;(구글이 새 사이트맵을 일단 한번 훑음)를 받고, 도메인이 권위를 못 쌓았으면 구글이 &lt;strong&gt;색인 쿼터를 회수&lt;/strong&gt;한다. 페이지들이 &lt;em&gt;"Crawled - currently not indexed"&lt;/em&gt; 연옥으로 미끄러진다.&lt;/p&gt;

&lt;p&gt;이게 "프로그래매틱 SEO로 페이지 1만 개 찍어라" 글에서 아무도 경고 안 해주는 부분이다: &lt;strong&gt;크롤됨 ≠ 색인됨, 색인됨 ≠ 랭킹.&lt;/strong&gt; 권위 있는 도메인에서는 대량 페이지가 후한 신뢰를 받는다. 생긴 지 4주 된 도메인에서는, 그냥 솎아내진다.&lt;/p&gt;

&lt;h2&gt;
  
  
  시도한 것 (그리고 그 값어치)
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. 사이트맵 가지치기 — 크롤 예산 집중.&lt;/strong&gt;&lt;br&gt;
사이트맵 인덱스에 URL이 약 8,000개였다. 대부분은 노출 0인 얇은 개별 카페 페이지였고, 카테고리/역세권 허브가 사실상 모든 노출을 가져갔다. 그래서 사이트맵을 허브 위주(약 560개, 90%+ 감축)로 줄였다. 카페 라우트는 여전히 존재하고 내부 링크로 크롤 가능하다 — 이건 &lt;strong&gt;de-submit이지 de-index가 아니다&lt;/strong&gt;. 목표: 쥐꼬리만 한 크롤 예산을 8천 개 얇은 페이지에 흩뿌리지 말고, 노출을 버는 소수에 집중시키기.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;값어치?&lt;/em&gt; 있다. 단 이건 초점 맞추기지 해결책은 아니다.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. 내부 링크 — 내가 놓친 갭.&lt;/strong&gt;&lt;br&gt;
가장 권위 높은 페이지(홈/랜딩)가 &lt;em&gt;모든&lt;/em&gt; 콘텐츠 기둥에 링크하고 있었다... 그 8,000페이지 지도만 빼고. 전형적인 실수. 정작 핵심 섹션은 사이트 상단에서 들어오는 내부 링크가 &lt;strong&gt;0개&lt;/strong&gt;였다. 고쳤고, 허브 간 크로스링크도 추가했다.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;값어치?&lt;/em&gt; 진짜로 필요하다(고립된 섹션은 크롤 우선순위를 못 받는다) — 하지만 내부 링크는 &lt;em&gt;외부&lt;/em&gt; 권위를 만들지 못한다.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. canonical 정리.&lt;/strong&gt;&lt;br&gt;
한 stale 스냅샷에서 루트의 &lt;code&gt;userCanonical&lt;/code&gt;(&lt;code&gt;/landing/&lt;/code&gt;)과 &lt;code&gt;googleCanonical&lt;/code&gt;(&lt;code&gt;/&lt;/code&gt;)이 어긋나 있었다. 구글이 추측하지 않게 해소할 가치는 있지만, 본 게임이 아니라 곁다리였다.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. 내가 계속 바랐던 것: "이거 색인해줘" API.&lt;/strong&gt;&lt;br&gt;
일반 페이지용은 없다. 구글의 &lt;strong&gt;Indexing API는 &lt;code&gt;JobPosting&lt;/code&gt;과 &lt;code&gt;BroadcastEvent&lt;/code&gt; 구조화 데이터에만 허용&lt;/strong&gt;된다. 카페 디렉토리에 쓰는 건 정책 위반이고 동작도 안 한다. 진짜 버튼은 GSC UI의 &lt;strong&gt;수동 "색인 요청"&lt;/strong&gt;, 하루 약 10건뿐이고, 그것도 &lt;em&gt;크롤&lt;/em&gt;을 살짝 유도할 뿐 색인을 보장하지 않는다.&lt;/p&gt;

&lt;h2&gt;
  
  
  불편한 교훈
&lt;/h2&gt;

&lt;p&gt;기술적 정리를 다 하고 나면, 진단은 지루하고 겸허해진다:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;갓 만든 도메인은 물량으로 색인을 뚫을 수 없다. &lt;strong&gt;권위 + 콘텐츠 고유성 + 시간&lt;/strong&gt; — 대략 이 순서로 들어간다. 그리고 프로그래매틱 페이지는 &lt;em&gt;고유성&lt;/em&gt; 허들을 &lt;strong&gt;더 높인다&lt;/strong&gt;, 낮추는 게 아니라 — 템플릿 콘텐츠는 도메인이 신뢰를 얻기 전까지는 저가치로 읽히기 때문이다.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;구체적으로, 실제로 바늘을 움직이는 레버:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;외부 editorial 백링크.&lt;/strong&gt; 이게 진짜 병목이다. 스토어 리스팅 링크 아님(그건 &lt;code&gt;nofollow&lt;/code&gt;라 거의 안 넘어감), 같은-회사 크로스 프로모션 아님(구글은 같은 주체가 소유한 사이트 간 링크를 강하게 할인한다). 진짜 사이트에서 오는, 주제 관련성 있는 독립 링크.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;템플릿 모양이 아닌 콘텐츠.&lt;/strong&gt; 고유 데이터, 실제 사진, N번째 페이지를 N+1번째와 구별되게 만드는 무엇이든.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;시간.&lt;/strong&gt; 신규 도메인 "샌드박스"는 실재한다. 다 잘해도 2~6개월 잡아라.&lt;/li&gt;
&lt;li&gt;우선순위 페이지 몇 개에 대한 &lt;strong&gt;수동 색인 요청&lt;/strong&gt; — 치료가 아니라 넛지.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;생산적으로 &lt;em&gt;느껴지지만&lt;/em&gt; 색인엔 거의 영향 없는 것들: 가짜 &lt;code&gt;lastmod&lt;/code&gt; 신선도로 사이트맵 재생성, 핑, 같은 사이트맵 재제출, &lt;code&gt;JSON-LD&lt;/code&gt; 더 추가하기. 한 번 제대로 해두고, 그다음엔 그만 만져라.&lt;/p&gt;

&lt;h2&gt;
  
  
  그래서, 성공했나?
&lt;/h2&gt;

&lt;p&gt;솔직히 — 이 글을 쓰는 지금도 연옥에 있다. 기술 토대는 탄탄하고(그 부분은 &lt;em&gt;내 통제 안&lt;/em&gt;에 있다), 권위 작업은 결과를 가르는 느린 노가다다. 위 아키텍처의 라이브 결과가 궁금하면 허브는 여기다: &lt;strong&gt;&lt;a href="https://coffee.pryzm.gg/coffeemap/" rel="noopener noreferrer"&gt;서울 카공 카페 지도&lt;/a&gt;&lt;/strong&gt;. 몇 달 뒤 다시 와서, 이 부검이 회복으로 바뀌었는지 보자.&lt;/p&gt;

&lt;p&gt;프로그래매틱 사이트를 신규 도메인의 벽 너머로 밀어 올려본 적 있다면, 무엇이 결정적이었는지 댓글로 정말 듣고 싶다 — 백링크였나, 콘텐츠 깊이였나, 아니면 그냥 인내였나?&lt;/p&gt;

</description>
      <category>seo</category>
      <category>webdev</category>
      <category>googlesearchconsole</category>
      <category>korean</category>
    </item>
    <item>
      <title>"I built 8,000 programmatic SEO pages. Google indexed zero. A GSC postmortem."</title>
      <dc:creator>Lucas</dc:creator>
      <pubDate>Sat, 06 Jun 2026 22:10:21 +0000</pubDate>
      <link>https://dev.to/_9848c5582063b42abecb7/i-built-8000-programmatic-seo-pages-google-indexed-zero-a-gsc-postmortem-569i</link>
      <guid>https://dev.to/_9848c5582063b42abecb7/i-built-8000-programmatic-seo-pages-google-indexed-zero-a-gsc-postmortem-569i</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Status: this is a live postmortem, not a victory lap. As I write this the site is still at &lt;strong&gt;0 indexed pages&lt;/strong&gt;. The interesting part isn't the win — it's the diagnosis.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The setup
&lt;/h2&gt;

&lt;p&gt;I run &lt;a href="https://coffee.pryzm.gg/landing/" rel="noopener noreferrer"&gt;커피콩 (CoffeeCong)&lt;/a&gt;, a Korean coffee-rewards app. Alongside it I shipped a programmatic-SEO site: &lt;strong&gt;&lt;a href="https://coffee.pryzm.gg/coffeemap/" rel="noopener noreferrer"&gt;a "workability" map of Seoul cafes&lt;/a&gt;&lt;/strong&gt; — every cafe scored on outlets, Wi‑Fi, seat room, noise, and price, then sliced into ~8,000 server-rendered landing pages:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;/coffeemap/area/{district}&lt;/code&gt; — 25 districts&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/coffeemap/area/{district}/{purpose}&lt;/code&gt; — district × 7 purposes&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/coffeemap/station/{station}&lt;/code&gt; and &lt;code&gt;/station/{station}/{purpose}&lt;/code&gt; — ~313 subway stations × purposes&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/coffeemap/cafe/{id}&lt;/code&gt; — ~6,000 individual cafes&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All SSR (Express), each page with a unique &lt;code&gt;&amp;lt;h1&amp;gt;&lt;/code&gt;, canonical, JSON‑LD, FAQ, and cross-links. Textbook programmatic SEO.&lt;/p&gt;

&lt;h2&gt;
  
  
  The crash
&lt;/h2&gt;

&lt;p&gt;Search Console told a brutally clean story:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Sitemaps:   8,653 submitted  /  0 indexed
Impressions: 05-24 → 05-29  ramp to 209/day
             05-30           collapse to ~0   ← and it stayed there
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;URL Inspection on every sample URL came back the same way:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Hub + category pages → &lt;strong&gt;"Crawled — currently not indexed"&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Long-tail pages → &lt;strong&gt;"Discovered — currently not indexed"&lt;/strong&gt; (never even crawled)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The reflex is to hunt for a technical bug. I checked all the usual suspects:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Check&lt;/th&gt;
&lt;th&gt;Result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;robots.txt&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;Allow&lt;/code&gt; on all SEO paths&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;pageFetchState&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;SUCCESSFUL&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rendering&lt;/td&gt;
&lt;td&gt;Full SSR — &lt;code&gt;curl&lt;/code&gt; shows the content, not an empty &lt;code&gt;#root&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Canonical&lt;/td&gt;
&lt;td&gt;Self-canonical, consistent&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;noindex&lt;/td&gt;
&lt;td&gt;None — &lt;code&gt;INDEXING_ALLOWED&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Everything was green. &lt;strong&gt;It wasn't a technical problem.&lt;/strong&gt; That's the trap with this failure mode: nothing is broken, Google just decided the site wasn't worth the index quota.&lt;/p&gt;

&lt;h2&gt;
  
  
  The real diagnosis
&lt;/h2&gt;

&lt;p&gt;Two things, neither of which a code fix can directly force:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. New-domain authority (the "sandbox").&lt;/strong&gt; A brand-new domain gets a burst of exploratory crawling, then Google reclaims index quota until the domain earns trust. The 05-30 cliff is exactly that reclamation. This is normal and mostly a function of time + backlinks.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Thin content at scale.&lt;/strong&gt; 8,000 templated pages where ~89% are near-duplicates with thin data (a station × purpose with 3 matching cafes) sends a "low average quality" signal. Google samples a few, decides the &lt;em&gt;site&lt;/em&gt; isn't worth it, and the whole domain's quota suffers.&lt;/p&gt;

&lt;h2&gt;
  
  
  The four levers that actually move this
&lt;/h2&gt;

&lt;p&gt;There is &lt;strong&gt;no API to force-index general pages&lt;/strong&gt; — Google's Indexing API is officially JobPosting/BroadcastEvent only. So you're left with levers, not buttons:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Concentrate crawl budget (cut thin pages)
&lt;/h3&gt;

&lt;p&gt;Counter-intuitively, the fix for "too many unindexed pages" is &lt;em&gt;fewer&lt;/em&gt; pages in the sitemap. I removed the 6,000 individual cafe pages and the 2,191 &lt;code&gt;station × purpose&lt;/code&gt; combos from the sitemap index — dropping it from ~8,600 to ~560 high-value URLs:&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;// Before: every station × every purpose (2,191 thin URLs)&lt;/span&gt;
&lt;span class="k"&gt;for &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;st&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;stations&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;for &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;purpose&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;PURPOSES&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nx"&gt;urls&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;stationPurposeUrl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;st&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;purpose&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

&lt;span class="c1"&gt;// After: station hubs only (313 substantive URLs)&lt;/span&gt;
&lt;span class="k"&gt;for &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;st&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;stations&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nx"&gt;urls&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;stationUrl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;st&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Key nuance: &lt;strong&gt;de-submit ≠ de-index.&lt;/strong&gt; The routes stay live and crawlable via internal links — I'm just not &lt;em&gt;asking&lt;/em&gt; Google to spend budget on them. When authority recovers, the loop comes back.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Fix internal linking from your highest-authority page
&lt;/h3&gt;

&lt;p&gt;I found the embarrassing one last: my &lt;strong&gt;homepage linked to the cafe map zero times.&lt;/strong&gt; The footer had links to every other content pillar but not the 8,000-page section. The single most-linked page on the domain was passing no equity to the thing I most wanted indexed. One &lt;code&gt;&amp;lt;a&amp;gt;&lt;/code&gt; tag fixed that.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Build real backlinks
&lt;/h3&gt;

&lt;p&gt;New domains need trust signals from established ones:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;App Store / Play Store listing → site URL (store pages are high-authority and constantly crawled)&lt;/li&gt;
&lt;li&gt;Cross-links from sister domains&lt;/li&gt;
&lt;li&gt;Content like… this post.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  4. Manual "Request Indexing" + time
&lt;/h3&gt;

&lt;p&gt;Search Console's URL Inspection → &lt;em&gt;Request Indexing&lt;/em&gt; (~10/day) forces a re-crawl of your strongest pages. It's a nudge, not a guarantee — most effective &lt;em&gt;after&lt;/em&gt; the above land. Then: 2–6 months of patience.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaways if you're doing programmatic SEO
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Don't submit your whole long-tail on a new domain.&lt;/strong&gt; Earn quota with a tight, high-value sitemap first, then expand.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"Crawled — not indexed" is a quality/authority verdict, not a bug.&lt;/strong&gt; Stop grepping your renderer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Your homepage must link to your money section.&lt;/strong&gt; Check it. Mine didn't.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;De-submit, don't de-index&lt;/strong&gt; when pruning — keep routes crawlable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;There's no magic button.&lt;/strong&gt; Plan for months, not days.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The map is live and the pruning just shipped — &lt;a href="https://coffee.pryzm.gg/coffeemap/" rel="noopener noreferrer"&gt;see the Seoul cafe workability map here&lt;/a&gt;. I'll post a follow-up when (if!) the index count moves off zero.&lt;/p&gt;

</description>
      <category>seo</category>
      <category>webdev</category>
      <category>googlesearch</category>
      <category>node</category>
    </item>
    <item>
      <title>I built my own IAP backend instead of using RevenueCat — what 3 weeks of pain taught me</title>
      <dc:creator>Lucas</dc:creator>
      <pubDate>Wed, 06 May 2026 11:36:34 +0000</pubDate>
      <link>https://dev.to/_9848c5582063b42abecb7/i-built-my-own-iap-backend-instead-of-using-revenuecat-what-3-weeks-of-pain-taught-me-1l06</link>
      <guid>https://dev.to/_9848c5582063b42abecb7/i-built-my-own-iap-backend-instead-of-using-revenuecat-what-3-weeks-of-pain-taught-me-1l06</guid>
      <description>&lt;p&gt;I'm shipping a subscription-based React Native app and went through the&lt;br&gt;
"do I use RevenueCat or roll my own?" question that probably every solo&lt;br&gt;
RN dev hits. I ended up rolling my own, ran into more edge cases than I&lt;br&gt;
expected, and eventually pulled the working backend into an MIT package.&lt;br&gt;
Sharing the post-mortem in case it saves someone else the same weeks.&lt;/p&gt;
&lt;h2&gt;
  
  
  Why not RevenueCat
&lt;/h2&gt;

&lt;p&gt;To be clear — RevenueCat is good. For a lot of apps it's the right call.&lt;br&gt;
Two things pushed me off it:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Revenue share scales with you.&lt;/strong&gt; 1% after $2.5K MRR is fair pricing,
but it's a surface I want to own for the lifetime of the product, not
rent.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;My subscription state lives in their DB.&lt;/strong&gt; I still need to mirror
"user X is subscribed" into my own Postgres to join with the rest of
my data, which means I'm running a webhook handler from them either
way. Felt like I was paying to add a hop.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;So I started writing it myself. Here's where the time actually went.&lt;/p&gt;
&lt;h2&gt;
  
  
  Where the time went
&lt;/h2&gt;
&lt;h3&gt;
  
  
  Apple StoreKit 2 JWS verification (~2 days)
&lt;/h3&gt;

&lt;p&gt;You don't just trust the JWT. You walk the &lt;code&gt;x5c&lt;/code&gt; chain in the JWT&lt;br&gt;
header, verify each certificate against Apple Root CA G3, then verify&lt;br&gt;
the JWT signature against the leaf cert's public key. None of the&lt;br&gt;
tutorials I found did the full chain — most just decoded the payload&lt;br&gt;
and hoped.&lt;/p&gt;
&lt;h3&gt;
  
  
  Google Play Developer API v3 (~1 day)
&lt;/h3&gt;

&lt;p&gt;OAuth2 service account is fine. The non-obvious bit: use&lt;br&gt;
&lt;code&gt;purchases.subscriptionsv2.get&lt;/code&gt; — it returns a &lt;code&gt;subscriptionState&lt;/code&gt;&lt;br&gt;
enum that maps cleanly to lifecycle states. The v1 API doesn't, and&lt;br&gt;
most Stack Overflow answers still reference v1. Don't infer state from&lt;br&gt;
&lt;code&gt;expiryTimeMillis&lt;/code&gt; + &lt;code&gt;cancelReason&lt;/code&gt;, just read the enum.&lt;/p&gt;
&lt;h3&gt;
  
  
  Lifecycle state classification (~3 days)
&lt;/h3&gt;

&lt;p&gt;This is where it got nasty. Apple's &lt;code&gt;DID_FAIL_TO_RENEW&lt;/code&gt; with subtype&lt;br&gt;
&lt;code&gt;GRACE_PERIOD&lt;/code&gt; vs &lt;code&gt;GRACE_PERIOD_EXPIRED&lt;/code&gt;. Google's &lt;code&gt;IN_GRACE_PERIOD&lt;/code&gt;,&lt;br&gt;
&lt;code&gt;ON_HOLD&lt;/code&gt;, &lt;code&gt;SUBSCRIPTION_PAUSED&lt;/code&gt;. I needed an &lt;code&gt;active: boolean&lt;/code&gt; for&lt;br&gt;
gating but also the raw state for UX (showing "your card failed but&lt;br&gt;
you still have access" is a legitimately different message than "your&lt;br&gt;
subscription is on hold"). Collapsing both vendor's events into one&lt;br&gt;
state machine took a few rewrites.&lt;/p&gt;
&lt;h3&gt;
  
  
  The 3-day refund trap
&lt;/h3&gt;

&lt;p&gt;Google auto-refunds any purchase you don't &lt;code&gt;acknowledgePurchase&lt;/code&gt; within&lt;br&gt;
3 days. My first version didn't call it. None of the RN tutorials I&lt;br&gt;
followed mentioned it. Lost a handful of test purchases before I&lt;br&gt;
noticed pattern in the dashboard. Subscriptions need acknowledgement&lt;br&gt;
too, not just one-time IAP.&lt;/p&gt;
&lt;h3&gt;
  
  
  Webhook miss recovery
&lt;/h3&gt;

&lt;p&gt;Apple's App Store Server Notifications V2 are reliable but not&lt;br&gt;
guaranteed. If you miss one, the user's status drifts. Solution:&lt;br&gt;
direct fetch via App Store Server API on &lt;code&gt;/status&lt;/code&gt; checks, treat&lt;br&gt;
webhooks as "fast path" not "only path." Same for Google — RTDN can&lt;br&gt;
drop, fall back to &lt;code&gt;subscriptionsv2.get&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;
  
  
  What I extracted
&lt;/h2&gt;

&lt;p&gt;Once it was working in production, none of the above was app-specific.&lt;br&gt;
So I pulled it out: &lt;a href="https://github.com/jeonghwanko/onesub" rel="noopener noreferrer"&gt;github.com/jeonghwanko/onesub&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;One line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;createOneSubMiddleware&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;MIT licensed. Pluggable subscription store (PostgreSQL built-in,&lt;br&gt;
implement the interface for Redis / whatever). Optional RN SDK&lt;br&gt;
(&lt;code&gt;useOneSub()&lt;/code&gt; hook + paywall component) but the server works with any&lt;br&gt;
client — Flutter, native, plain fetch.&lt;/p&gt;

&lt;h2&gt;
  
  
  Honest limitations
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No analytics dashboard yet.&lt;/strong&gt; RevenueCat's actual moat is cohort
retention / LTV / experiments, not the receipt validation. There's a
self-hosted Docker dashboard but it's operational (active counts,
failed webhooks) — not cohort analysis.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No hosted version.&lt;/strong&gt; You run your own server. If "I want to ship
an MVP without running infra" is the goal, RevenueCat still wins.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Apple Family Sharing and Promotional Offers&lt;/strong&gt; aren't implemented
yet.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Things I think turned out interesting
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;An MCP server is bundled — point Claude Code or Cursor at it and you
can say "add a monthly subscription to this Expo app" and it
generates the App Store Connect product, the Play Console product,
and the client integration. Not the main feature but it's the part
that surprised me with how much friction it removed.&lt;/li&gt;
&lt;li&gt;296+ tests, including multi-notification e2e scenarios for the
lifecycle stuff above. That's where most of the bugs live.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What I'm asking
&lt;/h2&gt;

&lt;p&gt;If you've shipped IAP yourself in RN — what edge case tripped you up&lt;br&gt;
that I haven't listed? Curious if there's a class of bug I haven't&lt;br&gt;
hit yet. Especially interested in hearing from anyone who's dealt with&lt;br&gt;
Family Sharing or upgrade/downgrade chains in production.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Repo: &lt;a href="https://github.com/jeonghwanko/onesub" rel="noopener noreferrer"&gt;github.com/jeonghwanko/onesub&lt;/a&gt; — MIT licensed. Issues and PRs welcome.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>reactnative</category>
      <category>expo</category>
      <category>opensource</category>
      <category>javascript</category>
    </item>
  </channel>
</rss>
