[{"data":1,"prerenderedAt":2858},["ShallowReactive",2],{"blog-post-\u002Fko\u002Fnuxt3-seo-setup":3,"blog-related-source":2484},{"id":4,"title":5,"body":6,"description":2473,"extension":2474,"meta":2475,"navigation":252,"path":2479,"seo":2480,"sitemap":2481,"stem":2482,"__hash__":2483},"ko\u002Fko\u002Fnuxt3-seo-setup.md","Nuxt 3 SEO 설정 정리: sitemap, canonical, hreflang, 구조화 데이터(JSON-LD)",{"type":7,"value":8,"toc":2455},"minimark",[9,14,18,35,43,94,97,101,111,128,207,525,534,539,546,661,683,699,703,717,719,723,728,735,880,884,905,920,1058,1081,1083,1087,1096,1099,1191,1198,1374,1422,1431,1433,1437,1442,1449,1613,1639,1873,1877,1892,2007,2059,2085,2087,2091,2096,2196,2209,2219,2221,2225,2240,2291,2299,2301,2305,2383,2395,2399,2451],[10,11,13],"h2",{"id":12},"이-글의-범위","이 글의 범위",[15,16,17],"p",{},"이 블로그(Nuxt 3 + Nuxt Content, Vercel 배포)의 구글 색인 문제를 조사하면서 SEO 설정을 하나씩 고쳤다. 원인 조사 과정은 앞의 두 글에 정리했다.",[19,20,21,29],"ul",{},[22,23,24],"li",{},[25,26,28],"a",{"href":27},"\u002Fko\u002Fsearch-console-discovered-crawled-not-indexed","서치 콘솔 \"발견됨 \u002F 크롤링됨 - 현재 색인이 생성되지 않음\" 차이",[22,30,31],{},[25,32,34],{"href":33},"\u002Fko\u002Fload-more-button-pagination-seo","\"더 보기\" 버튼 페이지네이션과 SEO",[15,36,37,38,42],{},"이 글은 그 결과로 ",[39,40,41],"strong",{},"실제로 적용한 설정을 코드로"," 정리한다. 버전은 다음과 같다.",[44,45,46,59],"table",{},[47,48,49],"thead",{},[50,51,52,56],"tr",{},[53,54,55],"th",{},"패키지",[53,57,58],{},"버전",[60,61,62,74,84],"tbody",{},[50,63,64,71],{},[65,66,67],"td",{},[68,69,70],"code",{},"nuxt",[65,72,73],{},"3.21",[50,75,76,81],{},[65,77,78],{},[68,79,80],{},"@nuxt\u002Fcontent",[65,82,83],{},"3.16",[50,85,86,91],{},[65,87,88],{},[68,89,90],{},"@nuxtjs\u002Fsitemap",[65,92,93],{},"8.6",[95,96],"hr",{},[10,98,100],{"id":99},"_1-사이트맵sitemap-nuxt-content-글을-자동으로-넣기","1. 사이트맵(sitemap): Nuxt Content 글을 자동으로 넣기",[102,103,104],"blockquote",{},[15,105,106,107,110],{},"사이트맵은 구글에게 \"이런 주소들이 있다\"고 알려주는 ",[39,108,109],{},"목록","이다. 글을 쓸 때마다 손으로 고치지 않도록, Nuxt Content 컬렉션에서 자동으로 만든다.",[15,112,113,115,116,119,120,123,124,127],{},[68,114,90],{}," 모듈을 넣고, ",[68,117,118],{},"content.config.ts","에서 컬렉션을 ",[68,121,122],{},"asSitemapCollection","으로 감싸면 그 컬렉션의 글이 ",[68,125,126],{},"\u002Fsitemap.xml","에 들어간다.",[129,130,135],"pre",{"className":131,"code":132,"language":133,"meta":134,"style":134},"language-ts shiki shiki-themes github-light github-dark","\u002F\u002F nuxt.config.ts\nexport default defineNuxtConfig({\n  site: { url: \"https:\u002F\u002Fiambilly-blog.vercel.app\" }, \u002F\u002F 사이트맵의 절대 주소에 쓰인다\n  modules: [\"@nuxtjs\u002Fsitemap\", \"@nuxt\u002Fcontent\"],      \u002F\u002F sitemap을 content보다 먼저\n});\n","ts","",[68,136,137,146,164,180,201],{"__ignoreMap":134},[138,139,142],"span",{"class":140,"line":141},"line",1,[138,143,145],{"class":144},"sJ8bj","\u002F\u002F nuxt.config.ts\n",[138,147,149,153,156,160],{"class":140,"line":148},2,[138,150,152],{"class":151},"szBVR","export",[138,154,155],{"class":151}," default",[138,157,159],{"class":158},"sScJk"," defineNuxtConfig",[138,161,163],{"class":162},"sVt8B","({\n",[138,165,167,170,174,177],{"class":140,"line":166},3,[138,168,169],{"class":162},"  site: { url: ",[138,171,173],{"class":172},"sZZnC","\"https:\u002F\u002Fiambilly-blog.vercel.app\"",[138,175,176],{"class":162}," }, ",[138,178,179],{"class":144},"\u002F\u002F 사이트맵의 절대 주소에 쓰인다\n",[138,181,183,186,189,192,195,198],{"class":140,"line":182},4,[138,184,185],{"class":162},"  modules: [",[138,187,188],{"class":172},"\"@nuxtjs\u002Fsitemap\"",[138,190,191],{"class":162},", ",[138,193,194],{"class":172},"\"@nuxt\u002Fcontent\"",[138,196,197],{"class":162},"],      ",[138,199,200],{"class":144},"\u002F\u002F sitemap을 content보다 먼저\n",[138,202,204],{"class":140,"line":203},5,[138,205,206],{"class":162},"});\n",[129,208,210],{"className":131,"code":209,"language":133,"meta":134,"style":134},"\u002F\u002F content.config.ts\nimport { defineContentConfig, defineCollection } from \"@nuxt\u002Fcontent\";\nimport { asSitemapCollection } from \"@nuxtjs\u002Fsitemap\u002Fcontent\";\n\nexport default defineContentConfig({\n  collections: {\n    ko: defineCollection(\n      asSitemapCollection(\n        { type: \"page\", source: \"ko\u002F*.md\" },\n        {\n          name: \"ko\",\n          \u002F\u002F frontmatter의 date(2026.10.14)를 lastmod(2026-10-14)로 넣는다\n          onUrl(url, entry) {\n            const raw = entry.date ?? entry.meta?.date;\n            const iso = raw ? String(raw).trim().replace(\u002F\\.\u002Fg, \"-\") : \"\";\n            if (\u002F^\\d{4}-\\d{2}-\\d{2}$\u002F.test(iso)) url.lastmod = iso;\n          },\n        }\n      )\n    ),\n  },\n});\n",[68,211,212,217,234,248,254,265,271,283,291,309,315,327,333,354,376,437,490,496,502,508,514,520],{"__ignoreMap":134},[138,213,214],{"class":140,"line":141},[138,215,216],{"class":144},"\u002F\u002F content.config.ts\n",[138,218,219,222,225,228,231],{"class":140,"line":148},[138,220,221],{"class":151},"import",[138,223,224],{"class":162}," { defineContentConfig, defineCollection } ",[138,226,227],{"class":151},"from",[138,229,230],{"class":172}," \"@nuxt\u002Fcontent\"",[138,232,233],{"class":162},";\n",[138,235,236,238,241,243,246],{"class":140,"line":166},[138,237,221],{"class":151},[138,239,240],{"class":162}," { asSitemapCollection } ",[138,242,227],{"class":151},[138,244,245],{"class":172}," \"@nuxtjs\u002Fsitemap\u002Fcontent\"",[138,247,233],{"class":162},[138,249,250],{"class":140,"line":182},[138,251,253],{"emptyLinePlaceholder":252},true,"\n",[138,255,256,258,260,263],{"class":140,"line":203},[138,257,152],{"class":151},[138,259,155],{"class":151},[138,261,262],{"class":158}," defineContentConfig",[138,264,163],{"class":162},[138,266,268],{"class":140,"line":267},6,[138,269,270],{"class":162},"  collections: {\n",[138,272,274,277,280],{"class":140,"line":273},7,[138,275,276],{"class":162},"    ko: ",[138,278,279],{"class":158},"defineCollection",[138,281,282],{"class":162},"(\n",[138,284,286,289],{"class":140,"line":285},8,[138,287,288],{"class":158},"      asSitemapCollection",[138,290,282],{"class":162},[138,292,294,297,300,303,306],{"class":140,"line":293},9,[138,295,296],{"class":162},"        { type: ",[138,298,299],{"class":172},"\"page\"",[138,301,302],{"class":162},", source: ",[138,304,305],{"class":172},"\"ko\u002F*.md\"",[138,307,308],{"class":162}," },\n",[138,310,312],{"class":140,"line":311},10,[138,313,314],{"class":162},"        {\n",[138,316,318,321,324],{"class":140,"line":317},11,[138,319,320],{"class":162},"          name: ",[138,322,323],{"class":172},"\"ko\"",[138,325,326],{"class":162},",\n",[138,328,330],{"class":140,"line":329},12,[138,331,332],{"class":144},"          \u002F\u002F frontmatter의 date(2026.10.14)를 lastmod(2026-10-14)로 넣는다\n",[138,334,336,339,342,346,348,351],{"class":140,"line":335},13,[138,337,338],{"class":158},"          onUrl",[138,340,341],{"class":162},"(",[138,343,345],{"class":344},"s4XuR","url",[138,347,191],{"class":162},[138,349,350],{"class":344},"entry",[138,352,353],{"class":162},") {\n",[138,355,357,360,364,367,370,373],{"class":140,"line":356},14,[138,358,359],{"class":151},"            const",[138,361,363],{"class":362},"sj4cs"," raw",[138,365,366],{"class":151}," =",[138,368,369],{"class":162}," entry.date ",[138,371,372],{"class":151},"??",[138,374,375],{"class":162}," entry.meta?.date;\n",[138,377,379,381,384,386,389,392,395,398,401,404,407,409,412,416,418,421,423,426,429,432,435],{"class":140,"line":378},15,[138,380,359],{"class":151},[138,382,383],{"class":362}," iso",[138,385,366],{"class":151},[138,387,388],{"class":162}," raw ",[138,390,391],{"class":151},"?",[138,393,394],{"class":158}," String",[138,396,397],{"class":162},"(raw).",[138,399,400],{"class":158},"trim",[138,402,403],{"class":162},"().",[138,405,406],{"class":158},"replace",[138,408,341],{"class":162},[138,410,411],{"class":172},"\u002F",[138,413,415],{"class":414},"snhLl","\\.",[138,417,411],{"class":172},[138,419,420],{"class":151},"g",[138,422,191],{"class":162},[138,424,425],{"class":172},"\"-\"",[138,427,428],{"class":162},") ",[138,430,431],{"class":151},":",[138,433,434],{"class":172}," \"\"",[138,436,233],{"class":162},[138,438,440,443,446,448,451,454,457,461,463,466,468,470,473,475,478,481,484,487],{"class":140,"line":439},16,[138,441,442],{"class":151},"            if",[138,444,445],{"class":162}," (",[138,447,411],{"class":172},[138,449,450],{"class":151},"^",[138,452,453],{"class":362},"\\d",[138,455,456],{"class":151},"{4}",[138,458,460],{"class":459},"sA_wV","-",[138,462,453],{"class":362},[138,464,465],{"class":151},"{2}",[138,467,460],{"class":459},[138,469,453],{"class":362},[138,471,472],{"class":151},"{2}$",[138,474,411],{"class":172},[138,476,477],{"class":162},".",[138,479,480],{"class":158},"test",[138,482,483],{"class":162},"(iso)) url.lastmod ",[138,485,486],{"class":151},"=",[138,488,489],{"class":162}," iso;\n",[138,491,493],{"class":140,"line":492},17,[138,494,495],{"class":162},"          },\n",[138,497,499],{"class":140,"line":498},18,[138,500,501],{"class":162},"        }\n",[138,503,505],{"class":140,"line":504},19,[138,506,507],{"class":162},"      )\n",[138,509,511],{"class":140,"line":510},20,[138,512,513],{"class":162},"    ),\n",[138,515,517],{"class":140,"line":516},21,[138,518,519],{"class":162},"  },\n",[138,521,523],{"class":140,"line":522},22,[138,524,206],{"class":162},[15,526,527,530,531,533],{},[68,528,529],{},"modules"," 순서도 중요하다. ",[68,532,80],{},"를 먼저 넣으면 sitemap 모듈이 \"연동에 문제가 생길 수 있다\"는 경고를 낸다.",[535,536,538],"h3",{"id":537},"함정-onurl-안에서-바깥-변수를-쓰면-안-된다","함정: onUrl 안에서 바깥 변수를 쓰면 안 된다",[15,540,541,542,545],{},"영어 컬렉션에도 똑같은 ",[68,543,544],{},"onUrl","을 쓰니, 함수를 하나 만들어서 두 곳에서 같이 쓰고 싶어진다. 그런데 이렇게 하면 빌드가 실패한다.",[129,547,549],{"className":131,"code":548,"language":133,"meta":134,"style":134},"\u002F\u002F ❌ 바깥에 정의한 함수를 onUrl 안에서 부른다\nconst toIsoDate = (raw) => String(raw).trim().replace(\u002F\\.\u002Fg, \"-\");\n\nasSitemapCollection(collection, {\n  name: \"ko\",\n  onUrl(url, entry) {\n    url.lastmod = toIsoDate(entry.date); \u002F\u002F 빌드 에러\n  },\n});\n",[68,550,551,556,603,607,614,623,638,653,657],{"__ignoreMap":134},[138,552,553],{"class":140,"line":141},[138,554,555],{"class":144},"\u002F\u002F ❌ 바깥에 정의한 함수를 onUrl 안에서 부른다\n",[138,557,558,561,564,566,568,571,573,576,578,580,582,584,586,588,590,592,594,596,598,600],{"class":140,"line":148},[138,559,560],{"class":151},"const",[138,562,563],{"class":158}," toIsoDate",[138,565,366],{"class":151},[138,567,445],{"class":162},[138,569,570],{"class":344},"raw",[138,572,428],{"class":162},[138,574,575],{"class":151},"=>",[138,577,394],{"class":158},[138,579,397],{"class":162},[138,581,400],{"class":158},[138,583,403],{"class":162},[138,585,406],{"class":158},[138,587,341],{"class":162},[138,589,411],{"class":172},[138,591,415],{"class":414},[138,593,411],{"class":172},[138,595,420],{"class":151},[138,597,191],{"class":162},[138,599,425],{"class":172},[138,601,602],{"class":162},");\n",[138,604,605],{"class":140,"line":166},[138,606,253],{"emptyLinePlaceholder":252},[138,608,609,611],{"class":140,"line":182},[138,610,122],{"class":158},[138,612,613],{"class":162},"(collection, {\n",[138,615,616,619,621],{"class":140,"line":203},[138,617,618],{"class":162},"  name: ",[138,620,323],{"class":172},[138,622,326],{"class":162},[138,624,625,628,630,632,634,636],{"class":140,"line":267},[138,626,627],{"class":158},"  onUrl",[138,629,341],{"class":162},[138,631,345],{"class":344},[138,633,191],{"class":162},[138,635,350],{"class":344},[138,637,353],{"class":162},[138,639,640,643,645,647,650],{"class":140,"line":273},[138,641,642],{"class":162},"    url.lastmod ",[138,644,486],{"class":151},[138,646,563],{"class":158},[138,648,649],{"class":162},"(entry.date); ",[138,651,652],{"class":144},"\u002F\u002F 빌드 에러\n",[138,654,655],{"class":140,"line":285},[138,656,519],{"class":162},[138,658,659],{"class":140,"line":293},[138,660,206],{"class":162},[15,662,663,664,666,667,670,671,674,675,678,679,682],{},"이유는 ",[68,665,544],{}," 함수가 ",[39,668,669],{},"소스 코드 그대로 서버 번들에 복사","되기 때문이다. 모듈 코드를 열어보면 함수를 ",[68,672,673],{},"fn.toString()","으로 문자열로 바꿔서 서버 쪽 가상 파일에 넣는다. 복사된 곳에는 ",[68,676,677],{},"toIsoDate","가 없으니, 모듈이 빌드할 때 미리 검사해서 \"바깥 변수나 import는 서버 번들에 없으니 콜백 안으로 옮기라\"는 에러를 낸다. 그래서 이 블로그는 두 컬렉션에 같은 코드를 ",[39,680,681],{},"일부러 중복해서"," 썼다.",[684,685,686],"note",{},[102,687,688],{},[15,689,690,691,694,695,698],{},"이 버전에서는 ",[68,692,693],{},"asSitemapCollection()","을 쓰면 \"deprecated(앞으로 없어질 예정)이니 컬렉션 schema 안에서 ",[68,696,697],{},"defineSitemapSchema()","를 쓰라\"는 경고가 나온다. 지금은 동작하지만, 모듈을 올릴 때 바꿔야 할 부분이다.",[535,700,702],{"id":701},"lastmod는-필수인가","lastmod는 필수인가?",[15,704,705,706,709,710,712,713,716],{},"아니다. ",[68,707,708],{},"lastmod","가 없어도 사이트맵은 정상이고, 구글은 사이트맵의 주소를 그대로 읽는다. 다만 구글은 ",[68,711,708],{},"가 ",[39,714,715],{},"정확하다면"," 다시 크롤링할 때 참고한다고 안내한다. 그래서 날짜가 정확한 frontmatter 값이 있으니 넣은 것이다. 글을 고치지 않았는데 빌드할 때마다 오늘 날짜를 넣는 식이면 오히려 신뢰를 잃는다.",[95,718],{},[10,720,722],{"id":721},"_2-canonical-이-페이지의-대표-주소","2. canonical: 이 페이지의 대표 주소",[102,724,725],{},[15,726,727],{},"canonical은 \"같은 내용이 여러 주소로 열릴 때, 이 주소가 대표\"라고 구글에 알려준다.",[15,729,730,731,734],{},"글 페이지에서는 ",[68,732,733],{},"useHead","로 현재 경로를 절대 주소로 넣는다.",[129,736,740],{"className":737,"code":738,"language":739,"meta":134,"style":134},"language-js shiki shiki-themes github-light github-dark","\u002F\u002F pages\u002Fko\u002F[slug]\u002Findex.vue\nuseHead(() => ({\n  title: `${post.value.title} | iambilly`,\n  link: [\n    { rel: \"canonical\", href: `https:\u002F\u002Fiambilly-blog.vercel.app${route.path}` },\n  ],\n  meta: [\n    { name: \"description\", content: post.value.description },\n    { property: \"og:title\", content: post.value.title },\n    { property: \"og:url\", content: `https:\u002F\u002Fiambilly-blog.vercel.app${route.path}` },\n  ],\n}));\n","js",[68,741,742,747,759,785,790,817,822,827,838,849,871,875],{"__ignoreMap":134},[138,743,744],{"class":140,"line":141},[138,745,746],{"class":144},"\u002F\u002F pages\u002Fko\u002F[slug]\u002Findex.vue\n",[138,748,749,751,754,756],{"class":140,"line":148},[138,750,733],{"class":158},[138,752,753],{"class":162},"(() ",[138,755,575],{"class":151},[138,757,758],{"class":162}," ({\n",[138,760,761,764,767,770,772,775,777,780,783],{"class":140,"line":166},[138,762,763],{"class":162},"  title: ",[138,765,766],{"class":172},"`${",[138,768,769],{"class":162},"post",[138,771,477],{"class":172},[138,773,774],{"class":162},"value",[138,776,477],{"class":172},[138,778,779],{"class":162},"title",[138,781,782],{"class":172},"} | iambilly`",[138,784,326],{"class":162},[138,786,787],{"class":140,"line":182},[138,788,789],{"class":162},"  link: [\n",[138,791,792,795,798,801,804,807,809,812,815],{"class":140,"line":203},[138,793,794],{"class":162},"    { rel: ",[138,796,797],{"class":172},"\"canonical\"",[138,799,800],{"class":162},", href: ",[138,802,803],{"class":172},"`https:\u002F\u002Fiambilly-blog.vercel.app${",[138,805,806],{"class":162},"route",[138,808,477],{"class":172},[138,810,811],{"class":162},"path",[138,813,814],{"class":172},"}`",[138,816,308],{"class":162},[138,818,819],{"class":140,"line":267},[138,820,821],{"class":162},"  ],\n",[138,823,824],{"class":140,"line":273},[138,825,826],{"class":162},"  meta: [\n",[138,828,829,832,835],{"class":140,"line":285},[138,830,831],{"class":162},"    { name: ",[138,833,834],{"class":172},"\"description\"",[138,836,837],{"class":162},", content: post.value.description },\n",[138,839,840,843,846],{"class":140,"line":293},[138,841,842],{"class":162},"    { property: ",[138,844,845],{"class":172},"\"og:title\"",[138,847,848],{"class":162},", content: post.value.title },\n",[138,850,851,853,856,859,861,863,865,867,869],{"class":140,"line":311},[138,852,842],{"class":162},[138,854,855],{"class":172},"\"og:url\"",[138,857,858],{"class":162},", content: ",[138,860,803],{"class":172},[138,862,806],{"class":162},[138,864,477],{"class":172},[138,866,811],{"class":162},[138,868,814],{"class":172},[138,870,308],{"class":162},[138,872,873],{"class":140,"line":317},[138,874,821],{"class":162},[138,876,877],{"class":140,"line":329},[138,878,879],{"class":162},"}));\n",[535,881,883],{"id":882},"필터-주소는-noindex-follow","필터 주소는 noindex, follow",[15,885,886,887,191,890,893,894,896,897,900,901,904],{},"목록 페이지는 ",[68,888,889],{},"\u002F?tag=nodejs",[68,891,892],{},"\u002F?field=backend","처럼 필터를 주소에 담는다. canonical은 ",[68,895,411],{},"로 되어 있었지만, 실제 ",[68,898,899],{},"site:"," 검색 결과를 보니 ",[39,902,903],{},"필터 주소들이 각각 색인되어 같은 목록 페이지가 여러 번"," 나오고 있었다. canonical은 구글에게 주는 \"힌트\"라서, 구글이 다르게 판단할 수 있다.",[15,906,907,908,911,912,915,916,919],{},"그래서 필터가 붙은 보기에는 ",[68,909,910],{},"robots","를 ",[68,913,914],{},"noindex, follow","로 바꿨다. ",[39,917,918],{},"색인은 하지 말되, 그 안의 글 링크는 계속 따라가라","는 뜻이다.",[129,921,923],{"className":737,"code":922,"language":739,"meta":134,"style":134},"\u002F\u002F pages\u002Findex.vue\nconst route = useRoute();\nconst isFilteredView = computed(() => [\"field\", \"tag\", \"q\"].some((k) => k in route.query));\n\nuseHead({\n  meta: [\n    { name: \"robots\", content: () => (isFilteredView.value ? \"noindex, follow\" : \"index, follow\") },\n  ],\n});\n",[68,924,925,930,945,1002,1006,1012,1016,1050,1054],{"__ignoreMap":134},[138,926,927],{"class":140,"line":141},[138,928,929],{"class":144},"\u002F\u002F pages\u002Findex.vue\n",[138,931,932,934,937,939,942],{"class":140,"line":148},[138,933,560],{"class":151},[138,935,936],{"class":362}," route",[138,938,366],{"class":151},[138,940,941],{"class":158}," useRoute",[138,943,944],{"class":162},"();\n",[138,946,947,949,952,954,957,959,961,964,967,969,972,974,977,980,983,986,989,991,993,996,999],{"class":140,"line":166},[138,948,560],{"class":151},[138,950,951],{"class":362}," isFilteredView",[138,953,366],{"class":151},[138,955,956],{"class":158}," computed",[138,958,753],{"class":162},[138,960,575],{"class":151},[138,962,963],{"class":162}," [",[138,965,966],{"class":172},"\"field\"",[138,968,191],{"class":162},[138,970,971],{"class":172},"\"tag\"",[138,973,191],{"class":162},[138,975,976],{"class":172},"\"q\"",[138,978,979],{"class":162},"].",[138,981,982],{"class":158},"some",[138,984,985],{"class":162},"((",[138,987,988],{"class":344},"k",[138,990,428],{"class":162},[138,992,575],{"class":151},[138,994,995],{"class":162}," k ",[138,997,998],{"class":151},"in",[138,1000,1001],{"class":162}," route.query));\n",[138,1003,1004],{"class":140,"line":182},[138,1005,253],{"emptyLinePlaceholder":252},[138,1007,1008,1010],{"class":140,"line":203},[138,1009,733],{"class":158},[138,1011,163],{"class":162},[138,1013,1014],{"class":140,"line":267},[138,1015,826],{"class":162},[138,1017,1018,1020,1023,1025,1028,1031,1033,1036,1038,1041,1044,1047],{"class":140,"line":273},[138,1019,831],{"class":162},[138,1021,1022],{"class":172},"\"robots\"",[138,1024,191],{"class":162},[138,1026,1027],{"class":158},"content",[138,1029,1030],{"class":162},": () ",[138,1032,575],{"class":151},[138,1034,1035],{"class":162}," (isFilteredView.value ",[138,1037,391],{"class":151},[138,1039,1040],{"class":172}," \"noindex, follow\"",[138,1042,1043],{"class":151}," :",[138,1045,1046],{"class":172}," \"index, follow\"",[138,1048,1049],{"class":162},") },\n",[138,1051,1052],{"class":140,"line":285},[138,1053,821],{"class":162},[138,1055,1056],{"class":140,"line":293},[138,1057,206],{"class":162},[1059,1060,1061],"warning",{},[102,1062,1063],{},[15,1064,1065,1066,1068,1069,1072,1073,1076,1077,1080],{},"한계가 있다. 이 블로그는 ",[68,1067,411],{},"를 정적 HTML로 미리 만들기(prerender) 때문에, 서버가 보내는 HTML은 필터가 있든 없든 똑같이 ",[68,1070,1071],{},"index, follow","다. ",[68,1074,1075],{},"noindex","는 ",[39,1078,1079],{},"브라우저에서 자바스크립트가 실행된 뒤","에 바뀐다. 구글은 페이지를 렌더링해서 보기 때문에 적용되는 것을 Chromium으로 확인했지만, 서버 HTML만 보는 크롤러에게는 적용되지 않는다.",[95,1082],{},[10,1084,1086],{"id":1085},"_3-hreflang-한국어-글과-영어-글을-짝으로-연결","3. hreflang: 한국어 글과 영어 글을 짝으로 연결",[102,1088,1089],{},[15,1090,1091,1092,1095],{},"hreflang은 \"이 글의 다른 언어 버전은 저 주소\"라고 알려준다. 같은 내용을 두 언어로 쓴 글이 ",[39,1093,1094],{},"중복이 아니라 번역","이라는 걸 구글이 알 수 있다.",[15,1097,1098],{},"번역한 글만 짝을 적어 두는 파일을 만들었다.",[129,1100,1102],{"className":737,"code":1101,"language":739,"meta":134,"style":134},"\u002F\u002F constants\u002FenPairs.js\nexport const EN_PAIRS = {\n  \"javascript-event-loop\": \"javascript-event-loop\",\n  \"cors-guide\": \"cors-guide\",\n  \u002F\u002F 한국어 슬러그: 영어 슬러그\n};\nexport const koToEnSlug = (koSlug) => EN_PAIRS[koSlug] ?? null;\n",[68,1103,1104,1109,1124,1137,1149,1154,1159],{"__ignoreMap":134},[138,1105,1106],{"class":140,"line":141},[138,1107,1108],{"class":144},"\u002F\u002F constants\u002FenPairs.js\n",[138,1110,1111,1113,1116,1119,1121],{"class":140,"line":148},[138,1112,152],{"class":151},[138,1114,1115],{"class":151}," const",[138,1117,1118],{"class":362}," EN_PAIRS",[138,1120,366],{"class":151},[138,1122,1123],{"class":162}," {\n",[138,1125,1126,1129,1132,1135],{"class":140,"line":166},[138,1127,1128],{"class":172},"  \"javascript-event-loop\"",[138,1130,1131],{"class":162},": ",[138,1133,1134],{"class":172},"\"javascript-event-loop\"",[138,1136,326],{"class":162},[138,1138,1139,1142,1144,1147],{"class":140,"line":182},[138,1140,1141],{"class":172},"  \"cors-guide\"",[138,1143,1131],{"class":162},[138,1145,1146],{"class":172},"\"cors-guide\"",[138,1148,326],{"class":162},[138,1150,1151],{"class":140,"line":203},[138,1152,1153],{"class":144},"  \u002F\u002F 한국어 슬러그: 영어 슬러그\n",[138,1155,1156],{"class":140,"line":267},[138,1157,1158],{"class":162},"};\n",[138,1160,1161,1163,1165,1168,1170,1172,1175,1177,1179,1181,1184,1186,1189],{"class":140,"line":273},[138,1162,152],{"class":151},[138,1164,1115],{"class":151},[138,1166,1167],{"class":158}," koToEnSlug",[138,1169,366],{"class":151},[138,1171,445],{"class":162},[138,1173,1174],{"class":344},"koSlug",[138,1176,428],{"class":162},[138,1178,575],{"class":151},[138,1180,1118],{"class":362},[138,1182,1183],{"class":162},"[koSlug] ",[138,1185,372],{"class":151},[138,1187,1188],{"class":362}," null",[138,1190,233],{"class":162},[15,1192,1193,1194,1197],{},"글 페이지에서는 짝이 있을 때만 ",[68,1195,1196],{},"alternate"," 링크 세 개를 넣는다.",[129,1199,1201],{"className":737,"code":1200,"language":739,"meta":134,"style":134},"const enSlug = computed(() => koToEnSlug(route.params.slug));\nconst base = \"https:\u002F\u002Fiambilly-blog.vercel.app\";\n\nuseHead(() => ({\n  link: enSlug.value\n    ? [\n        { rel: \"alternate\", hreflang: \"ko\", href: `${base}${route.path}` },\n        { rel: \"alternate\", hreflang: \"en\", href: `${base}\u002Fen\u002F${enSlug.value}` },\n        { rel: \"alternate\", hreflang: \"x-default\", href: `${base}${route.path}` },\n      ]\n    : [],\n}));\n",[68,1202,1203,1223,1237,1241,1251,1256,1264,1297,1328,1357,1362,1370],{"__ignoreMap":134},[138,1204,1205,1207,1210,1212,1214,1216,1218,1220],{"class":140,"line":141},[138,1206,560],{"class":151},[138,1208,1209],{"class":362}," enSlug",[138,1211,366],{"class":151},[138,1213,956],{"class":158},[138,1215,753],{"class":162},[138,1217,575],{"class":151},[138,1219,1167],{"class":158},[138,1221,1222],{"class":162},"(route.params.slug));\n",[138,1224,1225,1227,1230,1232,1235],{"class":140,"line":148},[138,1226,560],{"class":151},[138,1228,1229],{"class":362}," base",[138,1231,366],{"class":151},[138,1233,1234],{"class":172}," \"https:\u002F\u002Fiambilly-blog.vercel.app\"",[138,1236,233],{"class":162},[138,1238,1239],{"class":140,"line":166},[138,1240,253],{"emptyLinePlaceholder":252},[138,1242,1243,1245,1247,1249],{"class":140,"line":182},[138,1244,733],{"class":158},[138,1246,753],{"class":162},[138,1248,575],{"class":151},[138,1250,758],{"class":162},[138,1252,1253],{"class":140,"line":203},[138,1254,1255],{"class":162},"  link: enSlug.value\n",[138,1257,1258,1261],{"class":140,"line":267},[138,1259,1260],{"class":151},"    ?",[138,1262,1263],{"class":162}," [\n",[138,1265,1266,1269,1272,1275,1277,1279,1281,1284,1287,1289,1291,1293,1295],{"class":140,"line":273},[138,1267,1268],{"class":162},"        { rel: ",[138,1270,1271],{"class":172},"\"alternate\"",[138,1273,1274],{"class":162},", hreflang: ",[138,1276,323],{"class":172},[138,1278,800],{"class":162},[138,1280,766],{"class":172},[138,1282,1283],{"class":162},"base",[138,1285,1286],{"class":172},"}${",[138,1288,806],{"class":162},[138,1290,477],{"class":172},[138,1292,811],{"class":162},[138,1294,814],{"class":172},[138,1296,308],{"class":162},[138,1298,1299,1301,1303,1305,1308,1310,1312,1314,1317,1320,1322,1324,1326],{"class":140,"line":285},[138,1300,1268],{"class":162},[138,1302,1271],{"class":172},[138,1304,1274],{"class":162},[138,1306,1307],{"class":172},"\"en\"",[138,1309,800],{"class":162},[138,1311,766],{"class":172},[138,1313,1283],{"class":162},[138,1315,1316],{"class":172},"}\u002Fen\u002F${",[138,1318,1319],{"class":162},"enSlug",[138,1321,477],{"class":172},[138,1323,774],{"class":162},[138,1325,814],{"class":172},[138,1327,308],{"class":162},[138,1329,1330,1332,1334,1336,1339,1341,1343,1345,1347,1349,1351,1353,1355],{"class":140,"line":293},[138,1331,1268],{"class":162},[138,1333,1271],{"class":172},[138,1335,1274],{"class":162},[138,1337,1338],{"class":172},"\"x-default\"",[138,1340,800],{"class":162},[138,1342,766],{"class":172},[138,1344,1283],{"class":162},[138,1346,1286],{"class":172},[138,1348,806],{"class":162},[138,1350,477],{"class":172},[138,1352,811],{"class":162},[138,1354,814],{"class":172},[138,1356,308],{"class":162},[138,1358,1359],{"class":140,"line":311},[138,1360,1361],{"class":162},"      ]\n",[138,1363,1364,1367],{"class":140,"line":317},[138,1365,1366],{"class":151},"    :",[138,1368,1369],{"class":162}," [],\n",[138,1371,1372],{"class":140,"line":329},[138,1373,879],{"class":162},[44,1375,1376,1386],{},[47,1377,1378],{},[50,1379,1380,1383],{},[53,1381,1382],{},"규칙",[53,1384,1385],{},"이유",[60,1387,1388,1399,1411],{},[50,1389,1390,1396],{},[65,1391,1392,1395],{},[39,1393,1394],{},"양쪽 모두"," 서로를 가리켜야 한다",[65,1397,1398],{},"한쪽만 가리키면 구글이 무시할 수 있다. 영어 글 페이지에도 같은 세 줄을 넣는다",[50,1400,1401,1404],{},[65,1402,1403],{},"자기 자신도 포함한다",[65,1405,1406,1407,1410],{},"한국어 글에도 ",[68,1408,1409],{},"hreflang=\"ko\"","로 자기 주소를 넣는다",[50,1412,1413,1419],{},[65,1414,1415,1418],{},[68,1416,1417],{},"x-default","는 어느 언어에도 맞지 않을 때 보여줄 주소",[65,1420,1421],{},"이 블로그는 한국어 글을 기본으로 했다",[15,1423,1424,1426,1427,1430],{},[68,1425,90],{},"은 미리 만든 페이지의 이 태그를 읽어서 사이트맵에도 ",[68,1428,1429],{},"hreflang"," 항목을 넣어 준다.",[95,1432],{},[10,1434,1436],{"id":1435},"_4-구조화-데이터json-ld-날짜는-iso-8601로","4. 구조화 데이터(JSON-LD): 날짜는 ISO 8601로",[102,1438,1439],{},[15,1440,1441],{},"구조화 데이터는 \"이 페이지는 블로그 글이고, 제목은 이것, 작성일은 이것\"을 기계가 읽을 수 있게 적은 것이다.",[15,1443,1444,1445,1448],{},"글 페이지에 ",[68,1446,1447],{},"BlogPosting","을 넣는다.",[129,1450,1452],{"className":737,"code":1451,"language":739,"meta":134,"style":134},"useHead(() => ({\n  script: [\n    {\n      type: \"application\u002Fld+json\",\n      innerHTML: JSON.stringify({\n        \"@context\": \"https:\u002F\u002Fschema.org\",\n        \"@type\": \"BlogPosting\",\n        headline: post.value.title,\n        description: post.value.description,\n        datePublished: isoDateTime(post.value), \u002F\u002F \"2026-10-14T00:00:00+09:00\"\n        dateModified: isoDateTime(post.value),\n        mainEntityOfPage: `https:\u002F\u002Fiambilly-blog.vercel.app${route.path}`,\n        author: { \"@type\": \"Person\", name: \"iambilly\" },\n      }),\n    },\n  ],\n}));\n",[68,1453,1454,1464,1469,1474,1484,1499,1511,1523,1528,1533,1547,1557,1574,1595,1600,1605,1609],{"__ignoreMap":134},[138,1455,1456,1458,1460,1462],{"class":140,"line":141},[138,1457,733],{"class":158},[138,1459,753],{"class":162},[138,1461,575],{"class":151},[138,1463,758],{"class":162},[138,1465,1466],{"class":140,"line":148},[138,1467,1468],{"class":162},"  script: [\n",[138,1470,1471],{"class":140,"line":166},[138,1472,1473],{"class":162},"    {\n",[138,1475,1476,1479,1482],{"class":140,"line":182},[138,1477,1478],{"class":162},"      type: ",[138,1480,1481],{"class":172},"\"application\u002Fld+json\"",[138,1483,326],{"class":162},[138,1485,1486,1489,1492,1494,1497],{"class":140,"line":203},[138,1487,1488],{"class":162},"      innerHTML: ",[138,1490,1491],{"class":362},"JSON",[138,1493,477],{"class":162},[138,1495,1496],{"class":158},"stringify",[138,1498,163],{"class":162},[138,1500,1501,1504,1506,1509],{"class":140,"line":267},[138,1502,1503],{"class":172},"        \"@context\"",[138,1505,1131],{"class":162},[138,1507,1508],{"class":172},"\"https:\u002F\u002Fschema.org\"",[138,1510,326],{"class":162},[138,1512,1513,1516,1518,1521],{"class":140,"line":273},[138,1514,1515],{"class":172},"        \"@type\"",[138,1517,1131],{"class":162},[138,1519,1520],{"class":172},"\"BlogPosting\"",[138,1522,326],{"class":162},[138,1524,1525],{"class":140,"line":285},[138,1526,1527],{"class":162},"        headline: post.value.title,\n",[138,1529,1530],{"class":140,"line":293},[138,1531,1532],{"class":162},"        description: post.value.description,\n",[138,1534,1535,1538,1541,1544],{"class":140,"line":311},[138,1536,1537],{"class":162},"        datePublished: ",[138,1539,1540],{"class":158},"isoDateTime",[138,1542,1543],{"class":162},"(post.value), ",[138,1545,1546],{"class":144},"\u002F\u002F \"2026-10-14T00:00:00+09:00\"\n",[138,1548,1549,1552,1554],{"class":140,"line":317},[138,1550,1551],{"class":162},"        dateModified: ",[138,1553,1540],{"class":158},[138,1555,1556],{"class":162},"(post.value),\n",[138,1558,1559,1562,1564,1566,1568,1570,1572],{"class":140,"line":329},[138,1560,1561],{"class":162},"        mainEntityOfPage: ",[138,1563,803],{"class":172},[138,1565,806],{"class":162},[138,1567,477],{"class":172},[138,1569,811],{"class":162},[138,1571,814],{"class":172},[138,1573,326],{"class":162},[138,1575,1576,1579,1582,1584,1587,1590,1593],{"class":140,"line":335},[138,1577,1578],{"class":162},"        author: { ",[138,1580,1581],{"class":172},"\"@type\"",[138,1583,1131],{"class":162},[138,1585,1586],{"class":172},"\"Person\"",[138,1588,1589],{"class":162},", name: ",[138,1591,1592],{"class":172},"\"iambilly\"",[138,1594,308],{"class":162},[138,1596,1597],{"class":140,"line":356},[138,1598,1599],{"class":162},"      }),\n",[138,1601,1602],{"class":140,"line":378},[138,1603,1604],{"class":162},"    },\n",[138,1606,1607],{"class":140,"line":439},[138,1608,821],{"class":162},[138,1610,1611],{"class":140,"line":492},[138,1612,879],{"class":162},[15,1614,1615,1616,1619,1620,1623,1624,1627,1628,341,1631,1634,1635,1638],{},"여기서 실수가 있었다. 처음에는 frontmatter 날짜(",[68,1617,1618],{},"2026.10.14",")를 ",[39,1621,1622],{},"그대로"," ",[68,1625,1626],{},"datePublished","에 넣고 있었다. schema.org와 구글이 요구하는 날짜 형식은 ",[39,1629,1630],{},"ISO 8601",[68,1632,1633],{},"2026-10-14"," 또는 시간대까지 붙인 ",[68,1636,1637],{},"2026-10-14T00:00:00+09:00",")이라, 점으로 구분한 날짜는 날짜로 읽히지 않을 수 있다. 그래서 변환 함수를 만들었다.",[129,1640,1642],{"className":737,"code":1641,"language":739,"meta":134,"style":134},"\u002F\u002F utils\u002Fcontent.js\nexport const isoDate = (post) => {\n  const m = String(post?.meta?.date ?? \"\").trim().match(\u002F^(\\d{4})\\.(\\d{2})\\.(\\d{2})$\u002F);\n  return m ? `${m[1]}-${m[2]}-${m[3]}` : null;\n};\n\n\u002F\u002F 시간대가 없으면 해석이 애매해서 한국 시간(+09:00)을 붙인다\nexport const isoDateTime = (post) => {\n  const d = isoDate(post);\n  return d ? `${d}T00:00:00+09:00` : undefined;\n};\n",[68,1643,1644,1649,1670,1741,1797,1801,1805,1810,1831,1845,1869],{"__ignoreMap":134},[138,1645,1646],{"class":140,"line":141},[138,1647,1648],{"class":144},"\u002F\u002F utils\u002Fcontent.js\n",[138,1650,1651,1653,1655,1658,1660,1662,1664,1666,1668],{"class":140,"line":148},[138,1652,152],{"class":151},[138,1654,1115],{"class":151},[138,1656,1657],{"class":158}," isoDate",[138,1659,366],{"class":151},[138,1661,445],{"class":162},[138,1663,769],{"class":344},[138,1665,428],{"class":162},[138,1667,575],{"class":151},[138,1669,1123],{"class":162},[138,1671,1672,1675,1678,1680,1682,1685,1687,1689,1692,1694,1696,1699,1701,1703,1705,1707,1709,1711,1714,1716,1718,1720,1722,1724,1726,1728,1730,1732,1734,1737,1739],{"class":140,"line":166},[138,1673,1674],{"class":151},"  const",[138,1676,1677],{"class":362}," m",[138,1679,366],{"class":151},[138,1681,394],{"class":158},[138,1683,1684],{"class":162},"(post?.meta?.date ",[138,1686,372],{"class":151},[138,1688,434],{"class":172},[138,1690,1691],{"class":162},").",[138,1693,400],{"class":158},[138,1695,403],{"class":162},[138,1697,1698],{"class":158},"match",[138,1700,341],{"class":162},[138,1702,411],{"class":172},[138,1704,450],{"class":151},[138,1706,341],{"class":459},[138,1708,453],{"class":362},[138,1710,456],{"class":151},[138,1712,1713],{"class":459},")",[138,1715,415],{"class":414},[138,1717,341],{"class":459},[138,1719,453],{"class":362},[138,1721,465],{"class":151},[138,1723,1713],{"class":459},[138,1725,415],{"class":414},[138,1727,341],{"class":459},[138,1729,453],{"class":362},[138,1731,465],{"class":151},[138,1733,1713],{"class":459},[138,1735,1736],{"class":151},"$",[138,1738,411],{"class":172},[138,1740,602],{"class":162},[138,1742,1743,1746,1749,1751,1754,1757,1760,1763,1766,1769,1771,1773,1776,1778,1780,1782,1784,1787,1789,1791,1793,1795],{"class":140,"line":182},[138,1744,1745],{"class":151},"  return",[138,1747,1748],{"class":162}," m ",[138,1750,391],{"class":151},[138,1752,1753],{"class":172}," `${",[138,1755,1756],{"class":162},"m",[138,1758,1759],{"class":172},"[",[138,1761,1762],{"class":362},"1",[138,1764,1765],{"class":172},"]",[138,1767,1768],{"class":172},"}-${",[138,1770,1756],{"class":162},[138,1772,1759],{"class":172},[138,1774,1775],{"class":362},"2",[138,1777,1765],{"class":172},[138,1779,1768],{"class":172},[138,1781,1756],{"class":162},[138,1783,1759],{"class":172},[138,1785,1786],{"class":362},"3",[138,1788,1765],{"class":172},[138,1790,814],{"class":172},[138,1792,1043],{"class":151},[138,1794,1188],{"class":362},[138,1796,233],{"class":162},[138,1798,1799],{"class":140,"line":203},[138,1800,1158],{"class":162},[138,1802,1803],{"class":140,"line":267},[138,1804,253],{"emptyLinePlaceholder":252},[138,1806,1807],{"class":140,"line":273},[138,1808,1809],{"class":144},"\u002F\u002F 시간대가 없으면 해석이 애매해서 한국 시간(+09:00)을 붙인다\n",[138,1811,1812,1814,1816,1819,1821,1823,1825,1827,1829],{"class":140,"line":285},[138,1813,152],{"class":151},[138,1815,1115],{"class":151},[138,1817,1818],{"class":158}," isoDateTime",[138,1820,366],{"class":151},[138,1822,445],{"class":162},[138,1824,769],{"class":344},[138,1826,428],{"class":162},[138,1828,575],{"class":151},[138,1830,1123],{"class":162},[138,1832,1833,1835,1838,1840,1842],{"class":140,"line":293},[138,1834,1674],{"class":151},[138,1836,1837],{"class":362}," d",[138,1839,366],{"class":151},[138,1841,1657],{"class":158},[138,1843,1844],{"class":162},"(post);\n",[138,1846,1847,1849,1852,1854,1856,1859,1862,1864,1867],{"class":140,"line":311},[138,1848,1745],{"class":151},[138,1850,1851],{"class":162}," d ",[138,1853,391],{"class":151},[138,1855,1753],{"class":172},[138,1857,1858],{"class":162},"d",[138,1860,1861],{"class":172},"}T00:00:00+09:00`",[138,1863,1043],{"class":151},[138,1865,1866],{"class":362}," undefined",[138,1868,233],{"class":162},[138,1870,1871],{"class":140,"line":317},[138,1872,1158],{"class":162},[535,1874,1876],{"id":1875},"홈에는-website와-사이트-이름","홈에는 WebSite와 사이트 이름",[15,1878,1879,1880,1883,1884,1887,1888,1891],{},"홈에는 사이트 자체를 설명하는 ",[68,1881,1882],{},"WebSite","를 넣고, 모든 페이지에는 ",[68,1885,1886],{},"og:site_name","을 넣었다. 구글 검색 결과에 이 블로그 이름 대신 ",[39,1889,1890],{},"\"Vercel\""," 이 사이트 이름으로 나오고 있어서, 구글이 사이트 이름을 정할 때 참고하는 값을 채운 것이다.",[129,1893,1895],{"className":737,"code":1894,"language":739,"meta":134,"style":134},"\u002F\u002F pages\u002Findex.vue\nuseHead({\n  script: [\n    {\n      type: \"application\u002Fld+json\",\n      innerHTML: JSON.stringify({\n        \"@context\": \"https:\u002F\u002Fschema.org\",\n        \"@type\": \"WebSite\",\n        name: \"iambilly 기술 블로그\",\n        alternateName: [\"iambilly\", \"iambilly blog\"],\n        url: \"https:\u002F\u002Fiambilly-blog.vercel.app\u002F\",\n      }),\n    },\n  ],\n});\n",[68,1896,1897,1901,1907,1911,1915,1923,1935,1945,1956,1966,1981,1991,1995,1999,2003],{"__ignoreMap":134},[138,1898,1899],{"class":140,"line":141},[138,1900,929],{"class":144},[138,1902,1903,1905],{"class":140,"line":148},[138,1904,733],{"class":158},[138,1906,163],{"class":162},[138,1908,1909],{"class":140,"line":166},[138,1910,1468],{"class":162},[138,1912,1913],{"class":140,"line":182},[138,1914,1473],{"class":162},[138,1916,1917,1919,1921],{"class":140,"line":203},[138,1918,1478],{"class":162},[138,1920,1481],{"class":172},[138,1922,326],{"class":162},[138,1924,1925,1927,1929,1931,1933],{"class":140,"line":267},[138,1926,1488],{"class":162},[138,1928,1491],{"class":362},[138,1930,477],{"class":162},[138,1932,1496],{"class":158},[138,1934,163],{"class":162},[138,1936,1937,1939,1941,1943],{"class":140,"line":273},[138,1938,1503],{"class":172},[138,1940,1131],{"class":162},[138,1942,1508],{"class":172},[138,1944,326],{"class":162},[138,1946,1947,1949,1951,1954],{"class":140,"line":285},[138,1948,1515],{"class":172},[138,1950,1131],{"class":162},[138,1952,1953],{"class":172},"\"WebSite\"",[138,1955,326],{"class":162},[138,1957,1958,1961,1964],{"class":140,"line":293},[138,1959,1960],{"class":162},"        name: ",[138,1962,1963],{"class":172},"\"iambilly 기술 블로그\"",[138,1965,326],{"class":162},[138,1967,1968,1971,1973,1975,1978],{"class":140,"line":311},[138,1969,1970],{"class":162},"        alternateName: [",[138,1972,1592],{"class":172},[138,1974,191],{"class":162},[138,1976,1977],{"class":172},"\"iambilly blog\"",[138,1979,1980],{"class":162},"],\n",[138,1982,1983,1986,1989],{"class":140,"line":317},[138,1984,1985],{"class":162},"        url: ",[138,1987,1988],{"class":172},"\"https:\u002F\u002Fiambilly-blog.vercel.app\u002F\"",[138,1990,326],{"class":162},[138,1992,1993],{"class":140,"line":329},[138,1994,1599],{"class":162},[138,1996,1997],{"class":140,"line":335},[138,1998,1604],{"class":162},[138,2000,2001],{"class":140,"line":356},[138,2002,821],{"class":162},[138,2004,2005],{"class":140,"line":378},[138,2006,206],{"class":162},[129,2008,2010],{"className":131,"code":2009,"language":133,"meta":134,"style":134},"\u002F\u002F nuxt.config.ts: 모든 페이지 공통\napp: {\n  head: {\n    meta: [{ property: \"og:site_name\", content: \"iambilly 기술 블로그\" }],\n  },\n},\n",[68,2011,2012,2017,2025,2032,2050,2054],{"__ignoreMap":134},[138,2013,2014],{"class":140,"line":141},[138,2015,2016],{"class":144},"\u002F\u002F nuxt.config.ts: 모든 페이지 공통\n",[138,2018,2019,2022],{"class":140,"line":148},[138,2020,2021],{"class":158},"app",[138,2023,2024],{"class":162},": {\n",[138,2026,2027,2030],{"class":140,"line":166},[138,2028,2029],{"class":158},"  head",[138,2031,2024],{"class":162},[138,2033,2034,2037,2040,2043,2045,2047],{"class":140,"line":182},[138,2035,2036],{"class":158},"    meta",[138,2038,2039],{"class":162},": [{ property: ",[138,2041,2042],{"class":172},"\"og:site_name\"",[138,2044,858],{"class":162},[138,2046,1963],{"class":172},[138,2048,2049],{"class":162}," }],\n",[138,2051,2052],{"class":140,"line":203},[138,2053,519],{"class":162},[138,2055,2056],{"class":140,"line":267},[138,2057,2058],{"class":162},"},\n",[15,2060,2061,2062,2065,2066,2069,2070,2072,2073,2076,2077,2080,2081,2084],{},"구글은 2023년부터 ",[68,2063,2064],{},"iambilly-blog.vercel.app"," 같은 ",[39,2067,2068],{},"하위 도메인에도 사이트 이름을 지원","하고, 원하는 이름을 알리는 방법으로 ",[68,2071,1882],{}," 구조화 데이터의 ",[68,2074,2075],{},"name","과 ",[68,2078,2079],{},"alternateName","을 권장한다. 다만 이름은 구글이 여러 신호를 보고 정하기 때문에 ",[39,2082,2083],{},"반영이 보장되지는 않는다."," 결과는 배포 후 검색 결과를 보고 확인해야 한다.",[95,2086],{},[10,2088,2090],{"id":2089},"_5-prerender-모든-글을-정적-html로","5. prerender: 모든 글을 정적 HTML로",[102,2092,2093],{},[15,2094,2095],{},"prerender는 빌드할 때 페이지를 미리 HTML 파일로 만들어 두는 것이다. 요청마다 서버가 렌더링하지 않으니 빠르고, 서버 에러로 크롤러가 빈 페이지를 받을 일이 줄어든다.",[129,2097,2099],{"className":131,"code":2098,"language":133,"meta":134,"style":134},"\u002F\u002F nuxt.config.ts\nnitro: {\n  prerender: {\n    crawlLinks: true,                                \u002F\u002F \u002F에서 시작해 링크를 따라가며 만든다\n    ignore: [(route: string) => route.includes(\"?\")], \u002F\u002F 필터 주소는 만들지 않는다\n    routes: [\"\u002Fen\"],                                 \u002F\u002F 링크로 닿지 않는 시작점은 직접 추가\n  },\n},\n",[68,2100,2101,2105,2112,2119,2135,2171,2188,2192],{"__ignoreMap":134},[138,2102,2103],{"class":140,"line":141},[138,2104,145],{"class":144},[138,2106,2107,2110],{"class":140,"line":148},[138,2108,2109],{"class":158},"nitro",[138,2111,2024],{"class":162},[138,2113,2114,2117],{"class":140,"line":166},[138,2115,2116],{"class":158},"  prerender",[138,2118,2024],{"class":162},[138,2120,2121,2124,2126,2129,2132],{"class":140,"line":182},[138,2122,2123],{"class":158},"    crawlLinks",[138,2125,1131],{"class":162},[138,2127,2128],{"class":362},"true",[138,2130,2131],{"class":162},",                                ",[138,2133,2134],{"class":144},"\u002F\u002F \u002F에서 시작해 링크를 따라가며 만든다\n",[138,2136,2137,2140,2143,2145,2147,2150,2152,2154,2157,2160,2162,2165,2168],{"class":140,"line":203},[138,2138,2139],{"class":158},"    ignore",[138,2141,2142],{"class":162},": [(",[138,2144,806],{"class":344},[138,2146,431],{"class":151},[138,2148,2149],{"class":362}," string",[138,2151,428],{"class":162},[138,2153,575],{"class":151},[138,2155,2156],{"class":162}," route.",[138,2158,2159],{"class":158},"includes",[138,2161,341],{"class":162},[138,2163,2164],{"class":172},"\"?\"",[138,2166,2167],{"class":162},")], ",[138,2169,2170],{"class":144},"\u002F\u002F 필터 주소는 만들지 않는다\n",[138,2172,2173,2176,2179,2182,2185],{"class":140,"line":267},[138,2174,2175],{"class":158},"    routes",[138,2177,2178],{"class":162},": [",[138,2180,2181],{"class":172},"\"\u002Fen\"",[138,2183,2184],{"class":162},"],                                 ",[138,2186,2187],{"class":144},"\u002F\u002F 링크로 닿지 않는 시작점은 직접 추가\n",[138,2189,2190],{"class":140,"line":273},[138,2191,519],{"class":162},[138,2193,2194],{"class":140,"line":285},[138,2195,2058],{"class":162},[15,2197,2198,1076,2201,2204,2205,2208],{},[68,2199,2200],{},"crawlLinks",[39,2202,2203],{},"링크로 닿는 페이지만"," 만든다. 그래서 이 설정은 사이트의 링크 구조를 검사하는 도구 역할도 한다. \"더 보기\" 버튼 문제가 있던 때는 이 크롤러가 35개 페이지만 찾았고, 모든 글을 링크로 나열하는 ",[68,2206,2207],{},"\u002Farchive"," 페이지를 추가한 뒤에는 159개 경로를 모두 만들었다. 빌드 로그의 페이지 수가 글 수보다 적다면, 구글 크롤러도 링크로는 그 글들에 닿지 못한다는 신호다.",[15,2210,2211,2214,2215,2218],{},[68,2212,2213],{},"ignore","가 없으면 목록의 필터 링크(",[68,2216,2217],{},"\u002F?tag=...",")까지 따라가서 같은 목록을 수십 개 만든다.",[95,2220],{},[10,2222,2224],{"id":2223},"_6-주소를-옮겼다면-301-리다이렉트","6. 주소를 옮겼다면 301 리다이렉트",[15,2226,2227,2228,2231,2232,2235,2236,2239],{},"글 주소를 ",[68,2229,2230],{},"\u002Fblog\u002F...","에서 ",[68,2233,2234],{},"\u002Fko\u002F...","로 옮기면서, 예전 주소는 ",[68,2237,2238],{},"routeRules","로 301 리다이렉트를 걸었다.",[129,2241,2243],{"className":131,"code":2242,"language":133,"meta":134,"style":134},"routeRules: {\n  \"\u002Fblog\": { redirect: { to: \"\u002F\", statusCode: 301 } },\n  \"\u002Fblog\u002F**\": { redirect: { to: \"\u002Fko\u002F**\", statusCode: 301 } },\n},\n",[68,2244,2245,2251,2271,2287],{"__ignoreMap":134},[138,2246,2247,2249],{"class":140,"line":141},[138,2248,2238],{"class":158},[138,2250,2024],{"class":162},[138,2252,2253,2256,2259,2262,2265,2268],{"class":140,"line":148},[138,2254,2255],{"class":172},"  \"\u002Fblog\"",[138,2257,2258],{"class":162},": { redirect: { to: ",[138,2260,2261],{"class":172},"\"\u002F\"",[138,2263,2264],{"class":162},", statusCode: ",[138,2266,2267],{"class":362},"301",[138,2269,2270],{"class":162}," } },\n",[138,2272,2273,2276,2278,2281,2283,2285],{"class":140,"line":166},[138,2274,2275],{"class":172},"  \"\u002Fblog\u002F**\"",[138,2277,2258],{"class":162},[138,2279,2280],{"class":172},"\"\u002Fko\u002F**\"",[138,2282,2264],{"class":162},[138,2284,2267],{"class":362},[138,2286,2270],{"class":162},[138,2288,2289],{"class":140,"line":182},[138,2290,2058],{"class":162},[15,2292,2293,2294,2298],{},"301은 \"영구적으로 옮겼다\"는 뜻이라, 구글이 예전 주소의 색인을 새 주소로 넘긴다. 자세한 과정은 ",[25,2295,2297],{"href":2296},"\u002Fko\u002Fnitro-wildcard-redirect-url-migration","Nitro wildcard 리다이렉트로 안전하게 옮기기"," 글에 있다.",[95,2300],{},[10,2302,2304],{"id":2303},"정리-체크리스트","정리: 체크리스트",[44,2306,2307,2317],{},[47,2308,2309],{},[50,2310,2311,2314],{},[53,2312,2313],{},"항목",[53,2315,2316],{},"확인 방법",[60,2318,2319,2329,2337,2348,2355,2363,2371],{},[50,2320,2321,2324],{},[65,2322,2323],{},"사이트맵",[65,2325,2326,2328],{},[68,2327,126],{},"에 글 수만큼 주소가 있고, 주소가 실제 글 주소와 같은가",[50,2330,2331,2334],{},[65,2332,2333],{},"canonical",[65,2335,2336],{},"페이지 소스에 자기 주소를 가리키는 canonical이 있는가",[50,2338,2339,2342],{},[65,2340,2341],{},"필터 주소",[65,2343,2344,2345,2347],{},"필터 주소를 렌더링한 뒤 robots가 ",[68,2346,914],{},"인가",[50,2349,2350,2352],{},[65,2351,1429],{},[65,2353,2354],{},"한\u002F영 페이지가 서로를 가리키는가",[50,2356,2357,2360],{},[65,2358,2359],{},"구조화 데이터",[65,2361,2362],{},"날짜가 ISO 8601인가. 구글 리치 결과 테스트로 확인",[50,2364,2365,2368],{},[65,2366,2367],{},"prerender",[65,2369,2370],{},"빌드 로그의 페이지 수가 글 수 이상인가",[50,2372,2373,2376],{},[65,2374,2375],{},"링크 구조",[65,2377,2378,2379,2382],{},"모든 글로 가는 ",[68,2380,2381],{},"\u003Ca href>","가 있는가 (전체 글 페이지)",[15,2384,2385,2386,2389,2390,2394],{},"설정 하나하나보다 중요한 건 ",[39,2387,2388],{},"설정한 뒤 실제 결과물을 확인하는 것","이었다. 이 블로그의 문제들은 대부분 \"설정은 했는데 결과가 예상과 달랐던\" 경우였다. 사이트맵 주소가 실제 글 주소와 어긋났던 ",[25,2391,2393],{"href":2392},"\u002Fko\u002Fadsense-ads-txt-and-seo-indexing-bug","예전 버그","도, 날짜 형식도, 필터 주소 색인도 모두 결과물을 열어 보고서야 찾았다.",[10,2396,2398],{"id":2397},"참고-자료","참고 자료",[19,2400,2401,2409,2416,2423,2430,2437,2444],{},[22,2402,2403],{},[25,2404,2408],{"href":2405,"rel":2406},"https:\u002F\u002Fnuxtseo.com\u002Fsitemap\u002Fguides\u002Fcontent",[2407],"nofollow","Nuxt SEO: Sitemap - Nuxt Content 연동",[22,2410,2411],{},[25,2412,2415],{"href":2413,"rel":2414},"https:\u002F\u002Fdevelopers.google.com\u002Fsearch\u002Fdocs\u002Fcrawling-indexing\u002Fsitemaps\u002Fbuild-sitemap?hl=ko",[2407],"Google 검색 센터: 사이트맵 만들기 및 제출하기",[22,2417,2418],{},[25,2419,2422],{"href":2420,"rel":2421},"https:\u002F\u002Fdevelopers.google.com\u002Fsearch\u002Fdocs\u002Fcrawling-indexing\u002Fconsolidate-duplicate-urls?hl=ko",[2407],"Google 검색 센터: 중복 URL 통합(canonical)",[22,2424,2425],{},[25,2426,2429],{"href":2427,"rel":2428},"https:\u002F\u002Fdevelopers.google.com\u002Fsearch\u002Fdocs\u002Fspecialty\u002Finternational\u002Flocalized-versions?hl=ko",[2407],"Google 검색 센터: 다국어 버전 페이지 알리기(hreflang)",[22,2431,2432],{},[25,2433,2436],{"href":2434,"rel":2435},"https:\u002F\u002Fdevelopers.google.com\u002Fsearch\u002Fdocs\u002Fappearance\u002Fstructured-data\u002Farticle?hl=ko",[2407],"Google 검색 센터: Article 구조화 데이터",[22,2438,2439],{},[25,2440,2443],{"href":2441,"rel":2442},"https:\u002F\u002Fdevelopers.google.com\u002Fsearch\u002Fdocs\u002Fappearance\u002Fsite-names?hl=ko",[2407],"Google 검색 센터: 사이트 이름",[22,2445,2446],{},[25,2447,2450],{"href":2448,"rel":2449},"https:\u002F\u002Fdevelopers.google.com\u002Fsearch\u002Fblog\u002F2023\u002F07\u002Fsitenames",[2407],"Google 검색 센터 블로그: An update on site names (영문, 2023)",[2452,2453,2454],"style",{},"html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .snhLl, html code.shiki .snhLl{--shiki-default:#22863A;--shiki-default-font-weight:bold;--shiki-dark:#85E89D;--shiki-dark-font-weight:bold}html pre.shiki code .sA_wV, html code.shiki .sA_wV{--shiki-default:#032F62;--shiki-dark:#DBEDFF}",{"title":134,"searchDepth":148,"depth":148,"links":2456},[2457,2458,2462,2465,2466,2469,2470,2471,2472],{"id":12,"depth":148,"text":13},{"id":99,"depth":148,"text":100,"children":2459},[2460,2461],{"id":537,"depth":166,"text":538},{"id":701,"depth":166,"text":702},{"id":721,"depth":148,"text":722,"children":2463},[2464],{"id":882,"depth":166,"text":883},{"id":1085,"depth":148,"text":1086},{"id":1435,"depth":148,"text":1436,"children":2467},[2468],{"id":1875,"depth":166,"text":1876},{"id":2089,"depth":148,"text":2090},{"id":2223,"depth":148,"text":2224},{"id":2303,"depth":148,"text":2304},{"id":2397,"depth":148,"text":2398},"Nuxt 3와 Nuxt Content 블로그에 실제로 적용한 SEO 설정을 코드로 정리했다. 사이트맵 lastmod, canonical과 필터 주소 noindex, 한\u002F영 hreflang, JSON-LD 날짜 형식, prerender까지.","md",{"date":1618,"field":2476,"tags":2477},"blog",[2478,70],"seo","\u002Fko\u002Fnuxt3-seo-setup",{"title":5,"description":2473},{"loc":2479},"ko\u002Fnuxt3-seo-setup","WtVueoqS1X0exNVCFa2yYPwnppucLzlUCAe5cw2Fvok",[2485,2493,2503,2511,2519,2527,2535,2543,2550,2561,2569,2577,2588,2596,2605,2612,2618,2625,2631,2637,2645,2651,2657,2663,2669,2676,2682,2689,2694,2703,2711,2716,2723,2730,2736,2743,2750,2758,2765,2772,2775,2784,2790,2797,2804,2809,2815,2823,2830,2838,2844,2851],{"path":2392,"title":2486,"description":2487,"meta":2488},"애드센스 ads.txt 경고를 받고 파헤치다가 찾은 진짜 색인 버그","구글 애드센스가 ads.txt 문제를 알려왔는데, 확인해보니 파일은 멀쩡했다. 대신 서치콘솔 색인 수가 적었던 진짜 원인을 Nuxt Content 슬러그 불일치 버그에서 찾은 과정을 정리한다.",{"date":2489,"field":2476,"tags":2490},"2026.09.17",[2478,2491,2492],"adsense","nuxt-content",{"path":2494,"title":2495,"description":2496,"meta":2497},"\u002Fko\u002Fapi-error-response-format","API 에러 응답 형식 설계하기: 제각각인 에러 응답을 RFC 9457 Problem Details로 통일하는 법","API마다 제각각인 에러 응답을 RFC 9457 Problem Details로 통일하는 법. 필드별 검증 에러, traceId, 내부 정보 숨기기, Node.js 단일 에러 처리까지 정리했다.",{"date":2498,"field":2499,"tags":2500},"2026.10.08","backend",[2501,2502],"api","http",{"path":2504,"title":2505,"description":2506,"meta":2507},"\u002Fko\u002Fcors-guide","CORS 에러 완전 정리: 왜 막히는지부터 preflight, credentials까지 올바르게 해결하기","CORS 에러는 서버가 아니라 브라우저가 막은 것이다. 같은 출처 정책, preflight, credentials, Expose-Headers와 잘못된 해결법까지 실제 브라우저로 확인하며 정리했다.",{"date":2498,"field":2499,"tags":2508},[2502,2509,2510],"web-security","nodejs",{"path":2512,"title":2513,"description":2514,"meta":2515},"\u002Fko\u002Fcss-has-selector","CSS :has() 선택자 실전 가이드: 자바스크립트 없이 부모를 선택하는 법","체크하면 카드가 강조되고, 입력이 틀리면 폼 전체가 반응하고, 별점에 마우스를 올리면 앞의 별까지 칠해진다. 예전엔 자바스크립트가 필요했던 일을 CSS :has() 한 줄로 해결하는 실전 예제를 직접 눌러보며 정리했다.",{"date":2498,"field":2516,"tags":2517},"frontend",[2518],"css",{"path":2520,"title":2521,"description":2522,"meta":2523},"\u002Fko\u002Fcss-layouts","CSS Flexbox 완전 정복 가이드","Flexbox를 활용한 반응형 레이아웃 구현 방법과 실전 예제. 수직 정렬, 네비게이션 바, 카드 레이아웃까지 완벽 정리!",{"date":2524,"image":2525,"field":2516,"tags":2526},"2025.01.25","css.jpg",[2518],{"path":2528,"title":2529,"description":2530,"meta":2531},"\u002Fko\u002Fcss-login-form","로그인","HTML과 CSS로 로그인 카드 UI를 만들어보는 예제. 라벨, 아이콘이 들어간 입력칸, 버튼을 BEM 클래스로 구성하고 결과 화면을 코드와 함께 확인할 수 있다.",{"date":2532,"field":2516,"tags":2533},"2025.03.29",[2534,2518],"html",{"path":2536,"title":2537,"description":2538,"meta":2539},"\u002Fko\u002Fcsv-parser-cleaning","복잡한 CSV 데이터를 안전하게 파싱하는 방법","JavaScript에서 큰따옴표, 줄바꿈, 쉼표 등 복잡한 CSV 데이터를 안전하게 객체로 변환하는 전체 과정 설명",{"date":2540,"field":2516,"tags":2541},"2025.06.19",[2542],"javascript",{"path":2544,"title":2545,"description":2546,"meta":2547},"\u002Fko\u002Fdebounce-guide","debounce와 throttle이란??","반복되는 함수 호출을 제어하는 기술인 디바운스(debounce)와 쓰로틀(throttle)을 알아보자.",{"date":2548,"field":2516,"tags":2549},"2025.03.26",[2542],{"path":2551,"title":2552,"description":2553,"meta":2554},"\u002Fko\u002Fdeclarative-vs-procedural-programming","선언적 프로그래밍 vs 절차형 프로그래밍","선언적 프로그래밍과 절차형 프로그래밍은 서로 대비되는 대표적인 패러다임이다. 두 패러다임의 차이를 알아보고, 각 패러다임에 해당하는 언어와 Vue\u002FReact의 특징을 살펴보자.",{"date":2555,"field":2556,"tags":2557},"2025.05.24","concepts",[2558,2559,2560],"paradigm","vue","react",{"path":2562,"title":2563,"description":2564,"meta":2565},"\u002Fko\u002Fdesign-guide","Design-Guide","학습 서비스 EduTrack을 예로 든 웹 디자인 가이드. 색상 팔레트, 타이포그래피, 아이콘, 반응형 레이아웃과 UI 컴포넌트, 접근성까지 화면 설계 기준을 정리했다.",{"date":2566,"field":2516,"tags":2567},"2025.01.19",[2568],"design-system",{"path":2570,"title":2571,"description":2572,"meta":2573},"\u002Fko\u002Fdikw-pyramid","DIKW 피라미드 정리: 데이터가 지혜가 되기까지","Data, Information, Knowledge, Wisdom 4단계로 구성된 DIKW 피라미드 개념을 로그\u002F분석 데이터 같은 실제 개발 사례에 대입해서 정리한다.",{"date":2574,"field":2556,"tags":2575},"2026.09.18",[2576],"data",{"path":2578,"title":2579,"description":2580,"meta":2581},"\u002Fko\u002Fdisplaying-commits-with-github-actions","깃허브 액션으로 커밋 로그를 블로그에 띄우기: Nuxt와 Vercel 활용법","깃허브 액션을 통해 커밋 데이터를 자동으로 수집하고, Nuxt와 Vercel로 UI에 노출하는 방법을 소개합니다.",{"date":2582,"image":2583,"field":2584,"tags":2585},"2025.02.28","github-commits.jpg","infra",[2586,2587,70],"github-actions","vercel",{"path":2589,"title":2590,"description":2591,"meta":2592},"\u002Fko\u002Fdocker-auto-build-run","로컬에서 커밋 시 도커 빌드 후 컨테이너 시작하기","로컬에서 커밋할 때마다 Docker 이미지를 자동으로 빌드하고 컨테이너를 다시 띄우는 방법. 셸 스크립트를 작성하고 Git 훅으로 연결하는 과정을 정리했다.",{"date":2593,"field":2584,"tags":2594},"2025.03.10",[2595],"docker",{"path":2597,"title":2598,"description":2599,"meta":2600},"\u002Fko\u002Fhow-to-mount-storage","라즈베리파이4에 저장장치 마운트 시키기","라즈베리파이에 저장장치를 연결하고 파티션 생성부터 포맷, 마운트까지의 전체 과정을 정리한 기록입니다.",{"date":2601,"field":2584,"tags":2602},"2025.07.11",[2603,2604],"raspberry-pi","linux",{"path":2606,"title":2607,"description":2608,"meta":2609},"\u002Fko\u002Fhtml","HTML이란 무엇인가","HTML의 역할과 기본 문서 구조를 정리하고, 단일 페이지, 내비게이션 포함, 그리드 레이아웃처럼 자주 쓰는 페이지 뼈대를 예제 코드로 살펴본다.",{"date":2524,"image":2610,"field":2516,"tags":2611},"html.jpg",[2534],{"path":2613,"title":2614,"description":2615,"meta":2616},"\u002Fko\u002Fhtml-features-without-javascript","자바스크립트 없이 HTML만으로 되는 것들: 아직도 div로 다 만들고 있다면","아코디언, 팝오버, 모달, 자동완성, 폼 검증, 이미지 지연 로딩까지. 라이브러리 없이 HTML 태그와 속성만으로 해결되는 기능을 예제와 함께 정리했다. 코드는 줄고 접근성은 덤으로 따라온다.",{"date":2498,"field":2516,"tags":2617},[2534],{"path":2619,"title":2620,"description":2621,"meta":2622},"\u002Fko\u002Fhtml2","미치광이 카드 컴포넌트","글리치 효과와 움직이는 도형으로 꾸민 '미치광이' 카드 컴포넌트를 HTML과 CSS 애니메이션만으로 만들어보는 예제. 결과 화면을 바로 확인할 수 있다.",{"date":2623,"field":2516,"tags":2624},"2025.03.28",[2534,2518],{"path":2626,"title":2627,"description":2628,"meta":2629},"\u002Fko\u002Fhttp-cache-control-guide","HTTP 캐시 제대로 쓰기: Cache-Control, ETag, no-cache와 no-store의 차이","no-cache는 '캐시하지 마라'가 아니다. max-age, no-cache, no-store, ETag와 304, 배포 후 옛날 화면이 보이는 원인, Vercel CDN 캐시까지 직접 확인하며 정리했다.",{"date":2498,"field":2499,"tags":2630},[2502,2587],{"path":2632,"title":2633,"description":2634,"meta":2635},"\u002Fko\u002Fhttp-status-codes-guide","HTTP 상태 코드 올바르게 쓰기: 400 vs 422, 401 vs 403, 200 vs 201 vs 204 헷갈릴 때 보는 정리","400 vs 422, 401 vs 403, 200 vs 201 vs 204처럼 헷갈리는 HTTP 상태 코드를 상황별로 정리하고, 301이 POST를 GET으로 바꾸는 리다이렉트 함정까지 다뤘다.",{"date":2498,"field":2499,"tags":2636},[2502,2501],{"path":2638,"title":2639,"description":2640,"meta":2641},"\u002Fko\u002Fhttps-guide","Let's Encrypt를 이용해서 HTTPS 적용하기","무료 인증서인 Let's Encrypt와 Certbot으로 VM에 올린 웹 서비스에 HTTPS를 적용하는 방법. 인증서 발급부터 nginx 설정, 브라우저 확인까지 단계별로 정리했다.",{"date":2642,"field":2584,"tags":2643},"2025.03.11",[2644],"https",{"path":2646,"title":2647,"description":2648,"meta":2649},"\u002Fko\u002Fjavascript-async-error-handling","자바스크립트 비동기 에러 올바르게 처리하기: try\u002Fcatch가 못 잡는 경우부터 Promise.all vs allSettled까지","try\u002Fcatch가 비동기 에러를 못 잡는 이유, fetch의 res.ok, Promise.all vs allSettled, 반복문 속 await, 타임아웃과 요청 취소까지 비동기 에러 처리 함정을 정리했다.",{"date":2498,"field":2516,"tags":2650},[2542],{"path":2652,"title":2653,"description":2654,"meta":2655},"\u002Fko\u002Fjavascript-event-loop","자바스크립트 이벤트 루프 완전 정복: 마이크로태스크와 매크로태스크의 실행 순서","setTimeout(0)보다 Promise.then이 먼저 실행되는 이유는? 콜 스택, 태스크 큐, 마이크로태스크 큐가 어떤 순서로 돌아가는지 예제로 정리하고, 브라우저와 Node.js의 차이, 실무에서 UI 멈춤을 피하는 방법까지 알아보자.",{"date":2498,"field":2556,"tags":2656},[2542,2510],{"path":2658,"title":2659,"description":2660,"meta":2661},"\u002Fko\u002Fjavascript-higher-order-functions","자바스크립트 고차 함수 완전 정복: map, filter, reduce부터 직접 만드는 once, memoize까지","map, filter, reduce를 제대로 쓰는 법부터 once, memoize, pipe, 커링을 직접 만들어보고 map(parseInt) 같은 함정까지 자바스크립트 고차 함수를 정리했다.",{"date":2498,"field":2516,"tags":2662},[2542],{"path":2664,"title":2665,"description":2666,"meta":2667},"\u002Fko\u002Fjavascript-nullish-coalescing-optional-chaining","자바스크립트 ?? vs || 완벽 정리: null, undefined, 0, 빈 문자열을 올바르게 처리하는 법","볼륨 0이 50으로 바뀌고, 빈 닉네임이 '익명'이 되는 버그는 대부분 || 때문이다. ??와 ||의 차이, ?. 옵셔널 체이닝, ??=, 기본 매개변수가 적용되는 조건, null 검사하는 올바른 방법까지 직접 값을 넣어보며 정리했다.",{"date":2498,"field":2516,"tags":2668},[2542],{"path":2670,"title":2671,"description":2672,"meta":2673},"\u002Fko\u002Fjs-data-01","JavaScript 데이터01","배열 필터링과 변환, 합계 계산, 그룹화, 정렬, 중첩 데이터 처리, 중복 제거까지 자바스크립트로 데이터를 다루는 연습 문제를 난이도별로 풀어본다.",{"date":2674,"field":2516,"tags":2675},"2025.03.08",[2542],{"path":2677,"title":2678,"description":2679,"meta":2680},"\u002Fko\u002Fjs-data-02","JavaScript 데이터02","주문 데이터 필터링과 총액 계산, 고객별 주문 요약처럼 실무에 가까운 자바스크립트 데이터 처리 문제를 reduce 등 배열 메서드로 풀어본다.",{"date":2674,"field":2516,"tags":2681},[2542],{"path":2683,"title":2684,"description":2685,"meta":2686},"\u002Fko\u002Fjwt-vs-session-auth","JWT vs 세션 인증 비교: 무엇을 써야 할까? 저장 위치, 로그아웃, 보안까지 정리","JWT는 암호화가 아니라 서명이다. 세션과 JWT의 동작 방식, 강제 로그아웃 문제, localStorage와 httpOnly 쿠키 중 저장 위치, 리프레시 토큰까지 비교했다.",{"date":2498,"field":2499,"tags":2687},[2688,2509],"auth",{"path":33,"title":2690,"description":2691,"meta":2692},"\"더 보기\" 버튼 페이지네이션과 SEO: 구글 크롤러는 버튼을 누르지 않는다","목록을 '더 보기' 버튼으로 바꿨더니 글 49개 중 10개만 링크로 연결돼 있었다. 구글 크롤러가 버튼을 누르지 않는 이유를 구글 문서로 확인하고, 모든 글을 \u003Ca href> 링크로 잇는 방법을 정리했다.",{"date":1618,"field":2476,"tags":2693},[2478,2534],{"path":2695,"title":2696,"description":2697,"meta":2698},"\u002Fko\u002Fmarkdown-guid","마크다운 문법","제목, 강조, 목록, 링크, 이미지, 코드 블록까지 블로그 글과 README 작성에 필요한 마크다운 문법을 예제와 함께 한 번에 정리했다.",{"date":2699,"image":2700,"field":2476,"tags":2701},"2025.03.02","markdown.webp",[2702],"markdown",{"path":2704,"title":2705,"description":2706,"meta":2707},"\u002Fko\u002Fminio-guide","MinIO 완벽 가이드: 설치부터 Node.js 연동까지","MinIO는 오픈소스 기반의 고성능 객체 스토리지 서버입니다. AWS S3와 호환되는 API를 제공하여 클라우드 네이티브 애플리케이션을 위한 완벽한 스토리지 솔루션으로 자리잡고 있습니다.",{"date":2708,"field":2499,"tags":2709},"2025.11.23",[2710,2510],"minio",{"path":2296,"title":2712,"description":2713,"meta":2714},"글 주소 프리픽스를 통째로 바꿔야 할 때: Nitro wildcard 리다이렉트로 안전하게 옮기기","\u002Fblog\u002F[슬러그]로 쓰던 글 주소를 \u002Fko\u002F[슬러그]로 옮기면서, 기존 링크\u002F북마크를 하나도 안 깨뜨리고 처리한 방법. Nitro routeRules의 와일드카드 리다이렉트 패턴과 실제로 같이 바꿔야 했던 범위를 정리한다.",{"date":2498,"field":2476,"tags":2715},[70,2478],{"path":2717,"title":2718,"description":2719,"meta":2720},"\u002Fko\u002Fnode-simple-server","Node.js 간단한 서버 만들기","Node.js 기본 http 모듈로 간단한 웹 서버를 만들고 실행하는 방법. 서버 코드 작성부터 Express로 라우팅을 확장하는 과정까지 정리했다.",{"date":2721,"field":2499,"tags":2722},"2025.01.30",[2510],{"path":2724,"title":2725,"description":2726,"meta":2727},"\u002Fko\u002Fnodejs-graceful-shutdown","Node.js 서버 안전하게 종료하기 (Graceful Shutdown): 배포할 때마다 요청이 끊기는 이유","배포할 때마다 요청이 끊긴다면 SIGTERM을 처리하지 않았기 때문이다. server.close(), keep-alive 함정, Docker와 PM2의 신호 전달 문제까지 실행하며 정리했다.",{"date":2498,"field":2499,"tags":2728},[2510,2595,2729],"pm2",{"path":2731,"title":2732,"description":2733,"meta":2734},"\u002Fko\u002Fnuxt-colormode","Nuxt3 다크모드 적용하기","@nuxtjs\u002Fcolor-mode로 Nuxt 3에 다크 모드를 적용하는 방법. 설정, useColorMode()와 $colorMode의 차이, 토글 버튼과 초기 테마 설정까지 정리했다.",{"date":2532,"field":2516,"tags":2735},[70],{"path":2737,"title":2738,"description":2739,"meta":2740},"\u002Fko\u002Fnuxt-content-vercel-empty-query-sqlite","배포만 하면 블로그 글이 하나도 안 보이던 버그: Nuxt Content와 Vercel의 \u002Ftmp 문제","로컬에서는 멀쩡한데 Vercel에 올리면 queryCollection()이 빈 배열만 반환하던 버그를 추적한 과정. 원인은 @nuxt\u002Fcontent가 Vercel의 읽기 전용 파일시스템을 지원하기 전 버전에 고정돼 있었던 것.",{"date":2741,"field":2476,"tags":2742},"2026.10.07",[2492,2587],{"path":2744,"title":2745,"description":2746,"meta":2747},"\u002Fko\u002Fnuxt-runtimeconfig-appconfig","Nuxt 설정 runtimeConfig vs app.config 완전 정복","Nuxt3에서 전역 설정을 어떻게 관리하고, 어떤 기준으로 나누어야 할까? 다크모드 예시부터 실무 기준 설정 전략까지 총정리",{"date":2748,"field":2516,"tags":2749},"2025.06.16",[70],{"path":2751,"title":2752,"description":2753,"meta":2754},"\u002Fko\u002Fnuxt-useasyncdata-cache-error","useAsyncData 캐싱 문제와 Key 동적 설정으로 해결하기","다른 글로 이동해도 이전 글 데이터가 계속 보이는 Nuxt 3 버그. useAsyncData의 key 기반 캐싱이 원인이었고, key를 동적으로 바꿔 해결한 과정을 정리했다.",{"date":2755,"field":2516,"tags":2756,"image":2757},"2025.01.24",[70],"cache.jpg",{"path":2759,"title":2760,"description":2761,"meta":2762},"\u002Fko\u002Fnuxt-vercel-sql-error","Nuxt 3에서 Vercel 배포 시 발생한 md 파일 로딩 문제 해결기","Nuxt 3 웹 어플리케이션에서 Vercel 배포 환경에서 발생한 md 파일 로딩 문제를 분석하고 해결한 방법에 대해 공유합니다.",{"date":2755,"image":2763,"field":2516,"tags":2764},"ssg.jpg",[70,2492,2587],{"path":2766,"title":2767,"description":2768,"meta":2769},"\u002Fko\u002Fnuxt3-pinia-guide","nuxt3 Pinia 적용하기","Nuxt 3 프로젝트에 @pinia\u002Fnuxt로 Pinia를 설치하고 스토어를 만드는 방법. 상태 값 읽기, 액션으로 값 바꾸기까지 예제로 정리했다.",{"date":2532,"field":2516,"tags":2770},[70,2771],"pinia",{"path":2479,"title":5,"description":2473,"meta":2773},{"date":1618,"field":2476,"tags":2774},[2478,70],{"path":2776,"title":2777,"description":2778,"meta":2779},"\u002Fko\u002Fnuxt3-unit-test","Nuxt 프로젝트에 유닛 테스트 코드 추가하기","Nuxt 3 프로젝트에서 Vitest를 활용한 테스트 환경 설정과 유틸리티 함수 테스트 방법",{"date":2780,"image":2781,"field":2516,"tags":2782},"2025.03.07","unit-test.jpg",[70,2783],"vitest",{"path":2785,"title":2786,"description":2787,"meta":2788},"\u002Fko\u002Fpm2-guide","프로세스 매니저 PM2","Node.js 배포 환경에서 쓰는 프로세스 매니저 PM2. 설치와 실행부터 프로세스 목록, 재시작, 로그 확인, 모니터링까지 자주 쓰는 명령어를 정리했다.",{"date":2593,"field":2584,"tags":2789},[2729,2510],{"path":2791,"title":2792,"description":2793,"meta":2794},"\u002Fko\u002Freact-createportal","React createPortal 완벽 가이드","React의 `createPortal`을 활용한 UI 구현법, 활용 사례, 장단점 및 코드 예제를 소개합니다.",{"date":2795,"field":2516,"tags":2796},"2025.01.27",[2560],{"path":2798,"title":2799,"description":2800,"meta":2801},"\u002Fko\u002Frouting","Routing","Nuxt file-system routing creates a route for every file in the pages\u002F directory.",{"date":2802,"field":2516,"tags":2803},"2025.01.22",[70],{"path":27,"title":2805,"description":2806,"meta":2807},"구글 서치 콘솔 \"발견됨 \u002F 크롤링됨 - 현재 색인이 생성되지 않음\" 차이와 확인할 것","서치 콘솔의 '발견됨 - 현재 색인이 생성되지 않음'과 '크롤링됨 - 현재 색인이 생성되지 않음'은 멈춘 단계가 다르다. 두 상태의 뜻과 각각 확인할 것, 보고서와 site: 숫자가 다른 이유까지 정리했다.",{"date":1618,"field":2476,"tags":2808},[2478],{"path":2810,"title":2811,"description":2812,"meta":2813},"\u002Fko\u002Fsemantic-html-common-mistakes","의미 있는 HTML 쓰기: 대부분이 하는 시맨틱 HTML 실수 7가지","화면에서는 똑같아 보여도 div로 만든 버튼, label 없는 입력칸, 크기로 고른 제목 태그는 키보드 사용자와 스크린 리더, 검색 엔진에게 전혀 다르게 보인다. 직접 눌러볼 수 있는 예제로 흔한 시맨틱 HTML 실수와 고치는 법을 정리했다.",{"date":2498,"field":2516,"tags":2814},[2534],{"path":2816,"title":2817,"description":2818,"meta":2819},"\u002Fko\u002Ftanstack-query-guide","TanStack Query란 무엇인가?","TanStack Query가 웹 애플리케이션에서 데이터 가져오기, 캐싱, 상태 관리를 어떻게 개선하는지 알아본다.",{"date":2820,"field":2516,"tags":2821},"2025.04.15",[2560,2822],"tanstack-query",{"path":2824,"title":2825,"description":2826,"meta":2827},"\u002Fko\u002Fuse-context","리엑트 상태관리 useContext()","React에서 `useContext`를 사용하면 컴포넌트 트리에 깊이 중첩된 컴포넌트에서도 prop drilling 없이 상태를 공유할 수 있습니다. 이 글에서는 `useContext`의 개념, 사용 방법, 장단점, 그리고 실전 예제를 함께 알아보겠습니다.",{"date":2828,"field":2516,"tags":2829},"2025.02.03",[2560],{"path":2831,"title":2832,"description":2833,"meta":2834},"\u002Fko\u002Fvite-plugin-style-inject","리엑티 컴포넌트를 패키지 빌드 시 CSS 같이 적용시키기","React 컴포넌트를 Vite 라이브러리 모드로 빌드하면 CSS가 적용되지 않는 문제. 원인을 분석하고 vite-plugin-lib-inject-css로 자동 주입해 해결했다.",{"date":2835,"field":2516,"tags":2836},"2025.04.06",[2837,2560],"vite",{"path":2839,"title":2840,"description":2841,"meta":2842},"\u002Fko\u002Fvue-component-guide","Vue의 \u003Ccomponent :is \u002F> 완벽 정리: 동적 렌더링의 모든 것","Vue에서 \u003Ccomponent :is \u002F>를 활용한 동적 컴포넌트 렌더링 방법과 VNode, 컴포넌트 객체의 차이를 실전 예제로 이해하기",{"date":2524,"field":2516,"tags":2843},[2559],{"path":2845,"title":2846,"description":2847,"meta":2848},"\u002Fko\u002Fvue-query-ref-value-trap-nuxt3","Vue Query에서 데이터가 안 보이는 이유? Nuxt 3와 Ref 구조의 함정","Nuxt 3 + vue-query를 사용할 때 API 데이터가 `v-if` 조건문에서 제대로 표시되지 않는 현상을 해결한 경험을 바탕으로, Ref 구조와 template 디렉티브에서의 차이를 자세히 설명합니다.",{"date":2849,"field":2516,"tags":2850},"2025.06.08",[70,2559,2822],{"path":2852,"title":2853,"description":2854,"meta":2855},"\u002Fko\u002Fweb-security-sql-injection-xss-csrf","웹 보안 기본 3대 공격: SQL Injection, XSS, CSRF 원리와 방어법","로그인 폼 한 줄로 관리자가 되고 댓글 하나로 스크립트가 실행된다. SQL Injection, XSS, CSRF의 원리를 직접 확인하고 각각을 막는 방법을 정리했다.",{"date":2856,"field":2499,"tags":2857},"2026.10.09",[2509,2510],1791974052197]