프로젝트 소개

esen_seo는 Flutter Web의 구조적 문제를 해결하는 Flutter 패키지다. Flutter 앱은 문서가 아니라 위젯 트리를 렌더링하므로 크롤러가 읽을 제목, 문단, 링크를 찾지 못한다. 이 패키지는 그 위젯 트리를 DOM에 직접 시맨틱 HTML로 미러링하고, 메타 태그와 OpenGraph, Schema.org JSON-LD를 관리하며, 봇에게 페이지 소스에 완전한 HTML 문서를 전달하는 shelf 기반 서버 사이드 렌더링 컴포넌트를 포함한다. 순수 Dart로 작성되었으며 Puppeteer나 헤드리스 Chrome을 사용하지 않는다. 사용 방식은 재작성이 아니라 점진적이다. 대부분의 위젯은 자동으로 미러링되고, `.seo()` 확장 호출이 시맨틱 의미를 추가하며, 일련의 라이브러리 위젯이 미러가 볼 수 없는 경우(닫힌 드롭다운, 가상화된 목록, 비활성 탭, 그려진 차트)를 처리한다. iOS, Android, 데스크톱에서는 모든 호출이 no-op이고 위젯은 이전과 같이 렌더링된다. HTML은 웹에서만 존재한다. README에 설명된 주요 기능: - Text, Image, Column, Row, GestureDetector용 `.seo()` 확장과 타입이 지정된 태그 상수(SeoTextTag.h1, SeoContainerTag.section), 그리고 .h1–.h6, .p, .li, .ul, .section, .article, .nav, .tr 같은 축약형. - 스마트 기본값: `.seo()` 호출이 전혀 없는 페이지도 렌더링되며, 첫 번째 텍스트는 h1, 이후 텍스트는 p, 이미지는 semanticLabel을 alt로 사용하는 img가 된다. 차단되거나 유효하지 않은 태그는 안전한 요소로 대체된다. - `.seoNodes()`를 통한 사용자 정의 변환으로 어떤 위젯이든 자체 HTML을 선언할 수 있다. SEO 위젯 라이브러리는 그려진 콘텐츠를 변환한다(예: SeoBarChart는 CSS 막대와 실제 데이터 테이블로 미러링). - SeoRichText는 하나의 선언적 span 트리에서 네이티브 Flutter TextSpan과 중첩된 strong, em, code, 안전한 a 요소를 만든다. - 페이지당 단일 EsenSeo.setMeta() 호출을 통한 메타 태그, OpenGraph, Twitter Cards와 title에서 og:title을 가져오는 등의 폴백. - Article, Product(AggregateRating 포함), Review, Event, LocalBusiness, Organization, WebSite, BreadcrumbList, FAQPage용 Schema.org JSON-LD 빌더와 범용 이스케이프 해치. - 봇 인식 SSR 서버: shelf 미들웨어가 User-Agent로 크롤러를 감지하고 실제 HTML 문서를 제공하며 `dart run`으로 실행 가능. - 단일 진실 공급원으로서의 URL 라우팅: 순수 Dart 라우트 테이블이 탐색 시 앱 메타 태그와 봇용 서버 렌더링 라우트 본문을 구동하고, sitemap.xml(lastmod와 hreflang 대체 포함), robots.txt, canonical URL, 실제 HTTP 404를 생성한다. - 서버 없이 CDN 호스팅을 위해 라우트 테이블을 웹 빌드에 정적 HTML 파일로 정적 사전 렌더링. - 선택적 가시 셸: Flutter 엔진이 로드되기 전 첫 프레임이 사전 렌더링된 HTML인 경우. - Flutter Web을 로드하지 않고 순수 라우트 본문을 영구 페이지로 유지할 수 있는 옵트인 DOM 우선 라우트와, 동일한 순수 Dart 소스에서 컴파일되는 SeoTabs, SeoCarousel, 제한된 SeoCollection 상호작용. - 라우트 테이블에서 생성되는 llms.txt와 llms-full.txt, 그리고 더 빠른 인덱싱을 위한 IndexNow 핑. 안전 정책은 명시적이다. 태그는 허용 목록이므로 script, style, iframe, form, plaintext, svg, head 전용 태그, 사용자 정의 요소, 유효하지 않은 이름은 렌더링 시 거부되고 span/div로 대체된다(SeoMode.strict에서는 디버그 경고). 속성 처리는 이벤트 핸들러와 유효하지 않은 이름을 제거하면서 data-*, aria-*, id, lang, cite를 허용한다. URL 속성은 상대 URL과 http, https, mailto, tel, sms, ftp로 제한되어 javascript: 및 유사 스킴을 차단한다. README는 또한 SEO 위젯 라이브러리를 문서화한다: SeoBarChart, SeoPieChart(순수 CSS conic-gradient 파이와 테이블), SeoRating, SeoDataTable, SeoFaq(details/summary 아코디언), SeoBreadcrumbs, SeoFigure, SeoResponsiveImage(AVIF/WebP 소스가 있는 picture/srcset), SeoTestimonial, SeoRichText. 다섯 개 위젯은 Flutter가 절대 만들지 않는 콘텐츠를 처리한다: SeoNavMenu(드롭다운 항목을 데이터로), SeoListView(지연 렌더링에도 모든 항목 미러링), SeoCarousel, SeoTabs, SeoStepper. 비웹 플랫폼에서는 모두 일반 Flutter 위젯을 렌더링하는 no-op이다. 라우팅은 Flutter 임포트 없이 순수 Dart 파일에 한 번 정의되어 main.dart와 서버가 공유한다. SeoRoute.dynamic은 단일 데이터베이스 읽기에서 메타데이터와 본문을 해석하며, sitemap, llms.txt, 사전 렌더링을 위한 enumeratePaths가 있다. SeoRouteObserver는 미러와 메타 태그가 탐색을 따라가도록 유지하며 go_router, beamer, auto_route 또는 기타 Router 기반 패키지와 작동한다. README는 라이브 DOM 미러가 위젯 트리에서 파생되며 두 번째로 작성된 콘텐츠 트리가 필요하지 않다고 언급하는 한편, 서버 렌더링 라우트 본문은 둘 다 공유 순수 데이터 모델에서 파생되지 않는 한 별개이며, 드리프트를 잡기 위해 auditSeoParity가 제공된다고 설명한다. README는 미러가 기본적으로 숨겨져 있고(aria-hidden, 크기 0) Flutter가 자체 시맨틱 트리를 게시하므로 접근성 기능이 아니라고 명시한다.