Devy

글 검색

제목, 설명, 태그, 본문에서 글을 검색합니다.

목록으로

블로그 만들기 #4 - 고급 검색 & SEO 최적화

3분 읽기0 조회
reactseoshadcn-uiblog

고급 검색 페이지

Cmd+K Command Palette 외에 별도 검색 페이지를 추가했습니다. 키워드뿐 아니라 날짜 범위와 태그 필터를 조합해서 검색할 수 있습니다.

검색 필터 구성

  • 키워드 - 제목, 설명, 본문에서 검색
  • 날짜 범위 - 시작일 ~ 종료일 (DatePicker)
  • 태그 필터 - 전체 태그 목록에서 토글 선택

DatePicker 구현

shadcn/ui의 Calendar + Popover 컴포넌트를 조합해 DatePicker를 만들었습니다. 처음에는 기본 <input type="date">를 사용했지만 디자인 일관성을 위해 shadcn 컴포넌트로 교체했습니다.

export function DatePicker({ value, onChange, placeholder }) {
  const [open, setOpen] = useState(false)
  const date = value ? new Date(value + "T00:00:00") : undefined

  return (
    <Popover open={open} onOpenChange={setOpen}>
      <PopoverTrigger asChild>
        <Button variant="outline">
          <CalendarIcon className="mr-2 size-4" />
          {date ? format(date, "yyyy-MM-dd") : placeholder}
        </Button>
      </PopoverTrigger>
      <PopoverContent>
        <Calendar
          mode="single"
          selected={date}
          onSelect={(d) => {
            onChange(d ? format(d, "yyyy-MM-dd") : "")
            setOpen(false)
          }}
        />
      </PopoverContent>
    </Popover>
  )
}

advancedSearch 함수

모든 필터를 조합하는 검색 함수입니다. 각 필터는 선택사항이며 입력된 필터만 적용됩니다.

export function advancedSearch({ query, dateFrom, dateTo, tag }) {
  return getAllPosts().filter((post) => {
    if (query) {
      const q = query.toLowerCase()
      const match = [post.title, post.description, post.content]
        .some((f) => f.toLowerCase().includes(q))
      if (!match) return false
    }
    if (dateFrom && post.date < dateFrom) return false
    if (dateTo && post.date > dateTo) return false
    if (tag && !post.tags.includes(tag)) return false
    return true
  })
}

SEO 최적화

정적 사이트지만 검색 엔진 최적화를 위해 여러 가지를 적용했습니다.

동적 메타 태그

useMetaTags 커스텀 훅으로 페이지별 <title>, <meta description>, OG 태그를 동적으로 설정합니다.

export function useMetaTags({ title, description, url, type = "website" }) {
  useEffect(() => {
    document.title = title
      ? `${title} | Devy's Blog`
      : "Devy's Blog"

    setMeta("description", description || "개발하며 배운 것들을 정리하고 공유합니다.")
    setMeta("og:title", title || "Devy's Blog")
    setMeta("og:url", `https://devy1540.dev${url || "/"}`)
    // ...
  }, [title, description, url, type])
}

모든 6개 페이지(Home, Posts, Post, Search, Tags, About)에 적용했습니다.

sitemap.xml

Vite 플러그인으로 빌드 시 자동 생성합니다. content/posts/*.md 파일과 정적 페이지들의 URL을 포함합니다.

// vite.config.ts
function sitemapPlugin() {
  return {
    name: "sitemap",
    closeBundle() {
      const posts = glob.sync("content/posts/*.md")
      const urls = [
        { loc: "/", priority: "1.0" },
        { loc: "/posts", priority: "0.8" },
        ...posts.map((f) => ({
          loc: `/posts/${path.basename(f, ".md")}`,
          priority: "0.6",
        })),
      ]
      // XML 생성 후 dist/sitemap.xml에 저장
    },
  }
}

RSS 피드

마찬가지로 Vite 플러그인으로 빌드 시 rss.xml을 생성합니다. 각 포스트의 frontmatter를 파싱해서 제목, 설명, 날짜, 링크를 포함합니다. index.html에 RSS alternate 링크도 추가했습니다.

<link rel="alternate" type="application/rss+xml"
  title="Devy's Blog RSS"
  href="https://devy1540.dev/rss.xml" />

다음 글에서는 Google Analytics 연동과 방문자 수 표시를 다룹니다.