sbuilder-mcp 0.17.1 → 0.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -6,6 +6,32 @@ All notable changes to this project are documented in this file.
6
6
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
7
7
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
8
8
 
9
+ ## [0.19.0] - 2026-09-10
10
+
11
+ ### Added
12
+ - sb_import and sb_import_site can now turn a source page's `<svg>` into an `icon` element, looking up the name the page itself uses (a sprite reference, an `aria-label`, a `<title>`, an icon-set class) against the platform's own 3,227-icon vocabulary and skipping it rather than guessing when no name matches.
13
+ - sb_import and sb_import_site can now turn one or more consecutive `<details>` elements into a single `accordion`, with each `<details>` becoming one `accordion-content` item labeled from its `<summary>`.
14
+
15
+ ### Fixed
16
+ - sb_import and sb_import_site no longer miss every `<svg>` icon on a page: `tagName` reports `"svg"` in lowercase, which never matched the capture's ignore list, and reading an SVG element's `class` attribute through `.className` returned the literal string `"[object SVGAnimatedString]"` instead of the class list.
17
+ - sb_import and sb_import_site now expand a collapsed `<details>` before measuring it, so an FAQ section no longer imports as a list of questions with no answers.
18
+
19
+ ### Internal
20
+ - The generated catalog gained an icon-name table (`icons.generated.ts`, 3,227 RemixIcon ids read from the platform's own manifest) that the importer's icon lookup is checked against.
21
+
22
+ ## [0.18.0] - 2026-09-10
23
+
24
+ ### Added
25
+ - sb_import and sb_import_site can now bring over a `<video>` element, an embedded YouTube, Vimeo, Google Map or SoundCloud player, and an `<hr>` divider, instead of silently dropping every iframe as unsupported.
26
+
27
+ ### Fixed
28
+ - sb_import and sb_import_site no longer capture a source site's own header or footer navigation when it sits inside a wrapper element rather than directly under `<body>`, and no longer mistake a footer marked only by a CSS class (with no `<footer>` tag) for page content.
29
+ - sb_import_site no longer imports the same page twice under two different slugs when the source declares a `<link rel="canonical">` pointing at a URL already in the plan, or when a sitemap or crawl lists per-language copies of the same page (`/about`, `/en/about`, `/vi/about`); the entry page's own language is kept.
30
+ - sb_import_site no longer imports pagination pages such as `/blog/page/2` as separate pages.
31
+ - sb_import_site now honors the site's robots.txt Disallow rules while crawling or reading a sitemap, except for the URL the caller explicitly named.
32
+ - sb_import and sb_import_site no longer duplicate a nested list's items: a `<ul>` inside an `<li>` was previously captured once inside its parent item's text and again as its own list item.
33
+ - sb_import and sb_import_site now skip any element marked `aria-hidden="true"`, so carousel clones and hidden mobile-menu copies no longer come through as duplicated content.
34
+
9
35
  ## [0.17.1] - 2026-09-10
10
36
 
11
37
  ### Fixed
package/CHANGELOG.vi.md CHANGED
@@ -6,6 +6,32 @@ Mọi thay đổi đáng chú ý của dự án được ghi lại trong file n
6
6
  Định dạng dựa trên [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
7
7
  và dự án tuân theo [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
8
8
 
9
+ ## [0.19.0] - 2026-09-10
10
+
11
+ ### Added
12
+ - sb_import và sb_import_site giờ có thể biến một `<svg>` trên trang nguồn thành element `icon`, bằng cách tra cứu tên mà trang tự sử dụng (một sprite reference, một `aria-label`, một `<title>`, một class của icon set) đối chiếu với vocabulary 3.227 icon của chính nền tảng, và bỏ qua thay vì đoán khi không có tên nào khớp.
13
+ - sb_import và sb_import_site giờ có thể biến một hoặc nhiều `<details>` liên tiếp thành một `accordion` duy nhất, mỗi `<details>` trở thành một item `accordion-content` được gắn nhãn từ `<summary>` của nó.
14
+
15
+ ### Fixed
16
+ - sb_import và sb_import_site giờ không còn bỏ lỡ mọi icon `<svg>` trên trang: `tagName` báo `"svg"` bằng chữ thường, nên trước đây không bao giờ khớp với ignore list của bước capture, và việc đọc thuộc tính `class` của một element SVG qua `.className` trả về đúng chuỗi ký tự `"[object SVGAnimatedString]"` thay vì danh sách class.
17
+ - sb_import và sb_import_site giờ mở một `<details>` đang thu gọn trước khi đo, để một mục FAQ không còn bị import thành danh sách câu hỏi mà không có câu trả lời.
18
+
19
+ ### Internal
20
+ - Catalog được sinh ra giờ có thêm bảng tên icon (`icons.generated.ts`, 3.227 id RemixIcon đọc từ manifest của chính nền tảng), dùng để đối chiếu với bước tra cứu icon của importer.
21
+
22
+ ## [0.18.0] - 2026-09-10
23
+
24
+ ### Added
25
+ - sb_import và sb_import_site giờ có thể mang theo một element `<video>`, một player nhúng YouTube, Vimeo, Google Map hoặc SoundCloud, và một divider `<hr>`, thay vì âm thầm bỏ qua mọi iframe vì không hỗ trợ.
26
+
27
+ ### Fixed
28
+ - sb_import và sb_import_site giờ không còn lấy nhầm header hoặc footer điều hướng của chính site nguồn khi nó nằm trong một element bọc ngoài thay vì là con trực tiếp của `<body>`, và không còn nhầm một footer chỉ được đánh dấu bằng class CSS (không có thẻ `<footer>`) là nội dung trang.
29
+ - sb_import_site giờ không còn import cùng một trang hai lần dưới hai slug khác nhau khi trang nguồn khai báo `<link rel="canonical">` trỏ tới một URL đã có trong kế hoạch, hoặc khi sitemap/crawl liệt kê các bản dịch theo ngôn ngữ của cùng một trang (`/about`, `/en/about`, `/vi/about`); ngôn ngữ của trang nhập vào được giữ lại.
30
+ - sb_import_site giờ không còn import các trang phân trang như `/blog/page/2` thành các trang riêng.
31
+ - sb_import_site giờ tuân theo các quy tắc Disallow trong robots.txt của site khi crawl hoặc đọc sitemap, trừ URL mà người gọi đã chỉ định rõ ràng.
32
+ - sb_import và sb_import_site giờ không còn nhân đôi các mục của một list lồng nhau: một `<ul>` bên trong `<li>` trước đây bị lấy một lần trong text của mục cha và lấy lại lần nữa như một mục list riêng.
33
+ - sb_import và sb_import_site giờ bỏ qua mọi element được đánh dấu `aria-hidden="true"`, để các bản sao carousel và bản sao ẩn của menu mobile không còn bị lấy thành nội dung trùng lặp.
34
+
9
35
  ## [0.17.1] - 2026-09-10
10
36
 
11
37
  ### Fixed
@@ -0,0 +1,17 @@
1
+ // GENERATED by scripts/gen-catalog.ts — do not edit by hand.
2
+ // Source: <WB_REPO>/schema/src/iconManifest.json (the RemixIcon catalog the
3
+ // editor's picker and the Go renderer share).
4
+ export const ICON_SOURCE = {
5
+ "names": 3227,
6
+ "default": "StarFill"
7
+ };
8
+ /**
9
+ * Every icon name this platform can render.
10
+ *
11
+ * A LOOKUP, not a suggestion list. `sb_import` reads a name off a source page's
12
+ * `<svg>` — an aria-label, a `<use href="#icon-search">`, a `ri-search-line`
13
+ * class — and may only emit an `icon` when the name it derived is IN here. A
14
+ * name that is not renders nothing, which is worse than the `<svg>` being
15
+ * skipped, because the page then carries an empty box nobody put there.
16
+ */
17
+ export const ICON_NAMES = new Set(["24HoursFill", "24HoursLine", "4kFill", "4kLine", "AB", "AccessibilityFill", "AccessibilityLine", "AccountBox2Fill", "AccountBox2Line", "AccountBoxFill", "AccountBoxLine", "AccountCircle2Fill", "AccountCircle2Line", "AccountCircleFill", "AccountCircleLine", "AccountPinBoxFill", "AccountPinBoxLine", "AccountPinCircleFill", "AccountPinCircleLine", "AddBoxFill", "AddBoxLine", "AddCircleFill", "AddCircleLine", "AddFill", "AddLargeFill", "AddLargeLine", "AddLine", "AdminFill", "AdminLine", "AdvertisementFill", "AdvertisementLine", "AedElectrodesFill", "AedElectrodesLine", "AedFill", "AedLine", "Ai", "AiAgentFill", "AiAgentLine", "AiGenerate", "AiGenerate2", "AiGenerate2Fill", "AiGenerate2Line", "AiGenerate3dFill", "AiGenerate3dLine", "AiGenerateText", "AirplayFill", "AirplayLine", "AlarmAddFill", "AlarmAddLine", "AlarmFill", "AlarmLine", "AlarmSnoozeFill", "AlarmSnoozeLine", "AlarmWarningFill", "AlarmWarningLine", "AlbumFill", "AlbumLine", "AlertFill", "AlertLine", "AlibabaCloudFill", "AlibabaCloudLine", "AliensFill", "AliensLine", "AlignBottom", "AlignCenter", "AlignItemBottomFill", "AlignItemBottomLine", "AlignItemHorizontalCenterFill", "AlignItemHorizontalCenterLine", "AlignItemLeftFill", "AlignItemLeftLine", "AlignItemRightFill", "AlignItemRightLine", "AlignItemTopFill", "AlignItemTopLine", "AlignItemVerticalCenterFill", "AlignItemVerticalCenterLine", "AlignJustify", "AlignLeft", "AlignRight", "AlignTop", "AlignVertically", "AlipayFill", "AlipayLine", "AmazonFill", "AmazonLine", "AnchorFill", "AnchorLine", "AncientGateFill", "AncientGateLine", "AncientPavilionFill", "AncientPavilionLine", "AndroidFill", "AndroidLine", "AngularjsFill", "AngularjsLine", "AnthropicFill", "AnthropicLine", "Anticlockwise2Fill", "Anticlockwise2Line", "AnticlockwiseFill", "AnticlockwiseLine", "AppStoreFill", "AppStoreLine", "AppleFill", "AppleLine", "Apps2AddFill", "Apps2AddLine", "Apps2AiFill", "Apps2AiLine", "Apps2Fill", "Apps2Line", "AppsAiFill", "AppsAiLine", "AppsFill", "AppsLine", "Archive2Fill", "Archive2Line", "ArchiveDrawerFill", "ArchiveDrawerLine", "ArchiveFill", "ArchiveLine", "ArchiveStackFill", "ArchiveStackLine", "AreaChartFill", "AreaChartLine", "ArmchairFill", "ArmchairLine", "ArrowDownBoxFill", "ArrowDownBoxLine", "ArrowDownCircleFill", "ArrowDownCircleLine", "ArrowDownDoubleFill", "ArrowDownDoubleLine", "ArrowDownFill", "ArrowDownLine", "ArrowDownLongFill", "ArrowDownLongLine", "ArrowDownSFill", "ArrowDownSLine", "ArrowDownWideFill", "ArrowDownWideLine", "ArrowDropDownFill", "ArrowDropDownLine", "ArrowDropLeftFill", "ArrowDropLeftLine", "ArrowDropRightFill", "ArrowDropRightLine", "ArrowDropUpFill", "ArrowDropUpLine", "ArrowGoBackFill", "ArrowGoBackLine", "ArrowGoForwardFill", "ArrowGoForwardLine", "ArrowLeftBoxFill", "ArrowLeftBoxLine", "ArrowLeftCircleFill", "ArrowLeftCircleLine", "ArrowLeftDoubleFill", "ArrowLeftDoubleLine", "ArrowLeftDownBoxFill", "ArrowLeftDownBoxLine", "ArrowLeftDownFill", "ArrowLeftDownLine", "ArrowLeftDownLongFill", "ArrowLeftDownLongLine", "ArrowLeftFill", "ArrowLeftLine", "ArrowLeftLongFill", "ArrowLeftLongLine", "ArrowLeftRightFill", "ArrowLeftRightLine", "ArrowLeftSFill", "ArrowLeftSLine", "ArrowLeftUpBoxFill", "ArrowLeftUpBoxLine", "ArrowLeftUpFill", "ArrowLeftUpLine", "ArrowLeftUpLongFill", "ArrowLeftUpLongLine", "ArrowLeftWideFill", "ArrowLeftWideLine", "ArrowRightBoxFill", "ArrowRightBoxLine", "ArrowRightCircleFill", "ArrowRightCircleLine", "ArrowRightDoubleFill", "ArrowRightDoubleLine", "ArrowRightDownBoxFill", "ArrowRightDownBoxLine", "ArrowRightDownFill", "ArrowRightDownLine", "ArrowRightDownLongFill", "ArrowRightDownLongLine", "ArrowRightFill", "ArrowRightLine", "ArrowRightLongFill", "ArrowRightLongLine", "ArrowRightSFill", "ArrowRightSLine", "ArrowRightUpBoxFill", "ArrowRightUpBoxLine", "ArrowRightUpFill", "ArrowRightUpLine", "ArrowRightUpLongFill", "ArrowRightUpLongLine", "ArrowRightWideFill", "ArrowRightWideLine", "ArrowTurnBackFill", "ArrowTurnBackLine", "ArrowTurnForwardFill", "ArrowTurnForwardLine", "ArrowUpBoxFill", "ArrowUpBoxLine", "ArrowUpCircleFill", "ArrowUpCircleLine", "ArrowUpDoubleFill", "ArrowUpDoubleLine", "ArrowUpDownFill", "ArrowUpDownLine", "ArrowUpFill", "ArrowUpLine", "ArrowUpLongFill", "ArrowUpLongLine", "ArrowUpSFill", "ArrowUpSLine", "ArrowUpWideFill", "ArrowUpWideLine", "Artboard2Fill", "Artboard2Line", "ArtboardFill", "ArtboardLine", "ArticleFill", "ArticleLine", "AspectRatioFill", "AspectRatioLine", "Asterisk", "AtFill", "AtLine", "AtomFill", "AtomLine", "Attachment2", "AttachmentFill", "AttachmentLine", "AuctionFill", "AuctionLine", "AwardFill", "AwardLine", "BaiduFill", "BaiduLine", "BallPenFill", "BallPenLine", "BankCard2Fill", "BankCard2Line", "BankCardFill", "BankCardLine", "BankFill", "BankLine", "BarChart2Fill", "BarChart2Line", "BarChartBoxAiFill", "BarChartBoxAiLine", "BarChartBoxFill", "BarChartBoxLine", "BarChartFill", "BarChartGroupedFill", "BarChartGroupedLine", "BarChartHorizontalFill", "BarChartHorizontalLine", "BarChartLine", "BarcodeBoxFill", "BarcodeBoxLine", "BarcodeFill", "BarcodeLine", "BardFill", "BardLine", "BarricadeFill", "BarricadeLine", "BaseStationFill", "BaseStationLine", "BaseballFill", "BaseballLine", "BasketballFill", "BasketballLine", "Battery2ChargeFill", "Battery2ChargeLine", "Battery2Fill", "Battery2Line", "BatteryChargeFill", "BatteryChargeLine", "BatteryFill", "BatteryLine", "BatteryLowFill", "BatteryLowLine", "BatterySaverFill", "BatterySaverLine", "BatteryShareFill", "BatteryShareLine", "BearSmileFill", "BearSmileLine", "BeerFill", "BeerLine", "BehanceFill", "BehanceLine", "BellFill", "BellLine", "BikeFill", "BikeLine", "BilibiliFill", "BilibiliLine", "BillFill", "BillLine", "BilliardsFill", "BilliardsLine", "BinocularsFill", "BinocularsLine", "BitCoinFill", "BitCoinLine", "BlazeFill", "BlazeLine", "BlenderFill", "BlenderLine", "BloggerFill", "BloggerLine", "BlueskyFill", "BlueskyLine", "BluetoothConnectFill", "BluetoothConnectLine", "BluetoothFill", "BluetoothLine", "BlurOffFill", "BlurOffLine", "BnbFill", "BnbLine", "BodyScanFill", "BodyScanLine", "Bold", "Book2Fill", "Book2Line", "Book3Fill", "Book3Line", "BookAiFill", "BookAiLine", "BookFill", "BookLine", "BookMarkedFill", "BookMarkedLine", "BookOpenFill", "BookOpenLine", "BookReadFill", "BookReadLine", "BookShelfFill", "BookShelfLine", "BookletFill", "BookletLine", "Bookmark2Fill", "Bookmark2Line", "Bookmark3Fill", "Bookmark3Line", "BookmarkFill", "BookmarkLine", "BootstrapFill", "BootstrapLine", "BowlFill", "BowlLine", "Box1Fill", "Box1Line", "Box2Fill", "Box2Line", "Box3Fill", "Box3Line", "BoxingFill", "BoxingLine", "BracesFill", "BracesLine", "BracketsFill", "BracketsLine", "Brain2Fill", "Brain2Line", "Brain3Fill", "Brain3Line", "Brain4Fill", "Brain4Line", "BrainAi3Fill", "BrainAi3Line", "BrainFill", "BrainLine", "BreadFill", "BreadLine", "Briefcase2Fill", "Briefcase2Line", "Briefcase3Fill", "Briefcase3Line", "Briefcase4Fill", "Briefcase4Line", "Briefcase5Fill", "Briefcase5Line", "BriefcaseFill", "BriefcaseLine", "BringForward", "BringToFront", "BroadcastFill", "BroadcastLine", "Brush2Fill", "Brush2Line", "Brush3Fill", "Brush3Line", "Brush4Fill", "Brush4Line", "BrushAi3Fill", "BrushAi3Line", "BrushAiFill", "BrushAiLine", "BrushFill", "BrushLine", "BtcFill", "BtcLine", "BubbleChartFill", "BubbleChartLine", "Bug2Fill", "Bug2Line", "BugFill", "BugLine", "Building2Fill", "Building2Line", "Building3Fill", "Building3Line", "Building4Fill", "Building4Line", "BuildingFill", "BuildingLine", "Bus2Fill", "Bus2Line", "BusFill", "BusLine", "BusWifiFill", "BusWifiLine", "CactusFill", "CactusLine", "Cake2Fill", "Cake2Line", "Cake3Fill", "Cake3Line", "CakeFill", "CakeLine", "CalculatorFill", "CalculatorLine", "Calendar2Fill", "Calendar2Line", "CalendarCheckFill", "CalendarCheckLine", "CalendarCloseFill", "CalendarCloseLine", "CalendarEventFill", "CalendarEventLine", "CalendarFill", "CalendarLine", "CalendarScheduleFill", "CalendarScheduleLine", "CalendarTodoFill", "CalendarTodoLine", "CalendarView", "Camera2Fill", "Camera2Line", "Camera3Fill", "Camera3Line", "Camera4Fill", "Camera4Line", "CameraAi2Fill", "CameraAi2Line", "CameraAiFill", "CameraAiLine", "CameraFill", "CameraLensAiFill", "CameraLensAiLine", "CameraLensFill", "CameraLensLine", "CameraLine", "CameraOffFill", "CameraOffLine", "CameraSwitchFill", "CameraSwitchLine", "CandleFill", "CandleLine", "CapsuleFill", "CapsuleLine", "CarFill", "CarLine", "CarWashingFill", "CarWashingLine", "CaravanFill", "CaravanLine", "CarouselView", "CashFill", "CashLine", "CastFill", "CastLine", "CellphoneFill", "CellphoneLine", "CelsiusFill", "CelsiusLine", "CentosFill", "CentosLine", "Certificate2Fill", "Certificate2Line", "CertificateFill", "CertificateLine", "CharacterRecognitionFill", "CharacterRecognitionLine", "ChargingPile2Fill", "ChargingPile2Line", "ChargingPileFill", "ChargingPileLine", "Chat1Fill", "Chat1Line", "Chat2Fill", "Chat2Line", "Chat3Fill", "Chat3Line", "Chat4Fill", "Chat4Line", "ChatAi2Fill", "ChatAi2Line", "ChatAi3Fill", "ChatAi3Line", "ChatAi4Fill", "ChatAi4Line", "ChatAiFill", "ChatAiLine", "ChatCheckFill", "ChatCheckLine", "ChatDeleteFill", "ChatDeleteLine", "ChatDownloadFill", "ChatDownloadLine", "ChatFollowUpFill", "ChatFollowUpLine", "ChatForwardFill", "ChatForwardLine", "ChatHeartFill", "ChatHeartLine", "ChatHistoryFill", "ChatHistoryLine", "ChatNewFill", "ChatNewLine", "ChatOffFill", "ChatOffLine", "ChatPollFill", "ChatPollLine", "ChatPrivateFill", "ChatPrivateLine", "ChatQuoteFill", "ChatQuoteLine", "ChatSearchFill", "ChatSearchLine", "ChatSettingsFill", "ChatSettingsLine", "ChatSmile2Fill", "ChatSmile2Line", "ChatSmile3Fill", "ChatSmile3Line", "ChatSmileAi3Fill", "ChatSmileAi3Line", "ChatSmileAiFill", "ChatSmileAiLine", "ChatSmileFill", "ChatSmileLine", "ChatThreadFill", "ChatThreadLine", "ChatUnreadFill", "ChatUnreadLine", "ChatUploadFill", "ChatUploadLine", "ChatVoiceAiFill", "ChatVoiceAiLine", "ChatVoiceFill", "ChatVoiceLine", "CheckDoubleFill", "CheckDoubleLine", "CheckFill", "CheckLine", "CheckboxBlankCircleFill", "CheckboxBlankCircleLine", "CheckboxBlankFill", "CheckboxBlankLine", "CheckboxCircleFill", "CheckboxCircleLine", "CheckboxFill", "CheckboxIndeterminateFill", "CheckboxIndeterminateLine", "CheckboxLine", "CheckboxMultipleBlankFill", "CheckboxMultipleBlankLine", "CheckboxMultipleFill", "CheckboxMultipleLine", "ChessFill", "ChessLine", "ChinaRailwayFill", "ChinaRailwayLine", "ChromeFill", "ChromeLine", "CircleFill", "CircleLine", "ClapperboardAiFill", "ClapperboardAiLine", "ClapperboardFill", "ClapperboardLine", "ClaudeFill", "ClaudeLine", "ClipboardFill", "ClipboardLine", "Clockwise2Fill", "Clockwise2Line", "ClockwiseFill", "ClockwiseLine", "CloseCircleFill", "CloseCircleLine", "CloseFill", "CloseLargeFill", "CloseLargeLine", "CloseLine", "ClosedCaptioningAiFill", "ClosedCaptioningAiLine", "ClosedCaptioningFill", "ClosedCaptioningLine", "CloudFill", "CloudLine", "CloudOffFill", "CloudOffLine", "CloudWindyFill", "CloudWindyLine", "Cloudy2Fill", "Cloudy2Line", "CloudyFill", "CloudyLine", "CodeAiFill", "CodeAiLine", "CodeBlock", "CodeBoxFill", "CodeBoxLine", "CodeFill", "CodeLine", "CodeSFill", "CodeSLine", "CodeSSlashFill", "CodeSSlashLine", "CodeView", "CodepenFill", "CodepenLine", "CoinFill", "CoinLine", "CoinsFill", "CoinsLine", "CollageFill", "CollageLine", "CollapseDiagonal2Fill", "CollapseDiagonal2Line", "CollapseDiagonalFill", "CollapseDiagonalLine", "CollapseHorizontalFill", "CollapseHorizontalLine", "CollapseVerticalFill", "CollapseVerticalLine", "ColorFilterAiFill", "ColorFilterAiLine", "ColorFilterFill", "ColorFilterLine", "CommandFill", "CommandLine", "CommunityFill", "CommunityLine", "Compass2Fill", "Compass2Line", "Compass3Fill", "Compass3Line", "Compass4Fill", "Compass4Line", "CompassDiscoverFill", "CompassDiscoverLine", "CompassFill", "CompassLine", "Compasses2Fill", "Compasses2Line", "CompassesFill", "CompassesLine", "ComputerFill", "ComputerLine", "ContactsBook2Fill", "ContactsBook2Line", "ContactsBook3Fill", "ContactsBook3Line", "ContactsBookFill", "ContactsBookLine", "ContactsBookUploadFill", "ContactsBookUploadLine", "ContactsFill", "ContactsLine", "ContractFill", "ContractLeftFill", "ContractLeftLine", "ContractLeftRightFill", "ContractLeftRightLine", "ContractLine", "ContractRightFill", "ContractRightLine", "ContractUpDownFill", "ContractUpDownLine", "Contrast2Fill", "Contrast2Line", "ContrastDrop2Fill", "ContrastDrop2Line", "ContrastDropFill", "ContrastDropLine", "ContrastFill", "ContrastLine", "CookieFill", "CookieLine", "CopilotFill", "CopilotLine", "CopperCoinFill", "CopperCoinLine", "CopperDiamondFill", "CopperDiamondLine", "CopyleftFill", "CopyleftLine", "CopyrightFill", "CopyrightLine", "CoreosFill", "CoreosLine", "CornerDownLeftFill", "CornerDownLeftLine", "CornerDownRightFill", "CornerDownRightLine", "CornerLeftDownFill", "CornerLeftDownLine", "CornerLeftUpFill", "CornerLeftUpLine", "CornerRightDownFill", "CornerRightDownLine", "CornerRightUpFill", "CornerRightUpLine", "CornerUpLeftDoubleFill", "CornerUpLeftDoubleLine", "CornerUpLeftFill", "CornerUpLeftLine", "CornerUpRightDoubleFill", "CornerUpRightDoubleLine", "CornerUpRightFill", "CornerUpRightLine", "Coupon2Fill", "Coupon2Line", "Coupon3Fill", "Coupon3Line", "Coupon4Fill", "Coupon4Line", "Coupon5Fill", "Coupon5Line", "CouponFill", "CouponLine", "CpuFill", "CpuLine", "CreativeCommonsByFill", "CreativeCommonsByLine", "CreativeCommonsFill", "CreativeCommonsLine", "CreativeCommonsNcFill", "CreativeCommonsNcLine", "CreativeCommonsNdFill", "CreativeCommonsNdLine", "CreativeCommonsSaFill", "CreativeCommonsSaLine", "CreativeCommonsZeroFill", "CreativeCommonsZeroLine", "CriminalFill", "CriminalLine", "Crop2Fill", "Crop2Line", "CropFill", "CropLine", "CrossFill", "CrossLine", "Crosshair2Fill", "Crosshair2Line", "CrosshairFill", "CrosshairLine", "Css3Fill", "Css3Line", "CupFill", "CupLine", "CurrencyFill", "CurrencyLine", "CursorAiFill", "CursorAiLine", "CursorFill", "CursorHand", "CursorLine", "CustomSize", "CustomerService2Fill", "CustomerService2Line", "CustomerServiceFill", "CustomerServiceLine", "Dashboard2Fill", "Dashboard2Line", "Dashboard3Fill", "Dashboard3Line", "DashboardFill", "DashboardHorizontalFill", "DashboardHorizontalLine", "DashboardLine", "Database2Fill", "Database2Line", "DatabaseFill", "DatabaseLine", "DeepseekFill", "DeepseekLine", "DeleteBack2Fill", "DeleteBack2Line", "DeleteBackFill", "DeleteBackLine", "DeleteBin2Fill", "DeleteBin2Line", "DeleteBin3Fill", "DeleteBin3Line", "DeleteBin4Fill", "DeleteBin4Line", "DeleteBin5Fill", "DeleteBin5Line", "DeleteBin6Fill", "DeleteBin6Line", "DeleteBin7Fill", "DeleteBin7Line", "DeleteBinFill", "DeleteBinLine", "DeleteColumn", "DeleteRow", "DeviceFill", "DeviceLine", "DeviceRecoverFill", "DeviceRecoverLine", "DiamondFill", "DiamondLine", "DiamondRingFill", "DiamondRingLine", "Dice1Fill", "Dice1Line", "Dice2Fill", "Dice2Line", "Dice3Fill", "Dice3Line", "Dice4Fill", "Dice4Line", "Dice5Fill", "Dice5Line", "Dice6Fill", "Dice6Line", "DiceFill", "DiceLine", "DingdingFill", "DingdingLine", "DirectionFill", "DirectionLine", "DiscFill", "DiscLine", "DiscordFill", "DiscordLine", "DiscountPercentFill", "DiscountPercentLine", "DiscussFill", "DiscussLine", "DislikeFill", "DislikeLine", "DisqusFill", "DisqusLine", "DivideFill", "DivideLine", "DnaFill", "DnaLine", "DonutChartFill", "DonutChartLine", "DoorClosedFill", "DoorClosedLine", "DoorFill", "DoorLine", "DoorLockBoxFill", "DoorLockBoxLine", "DoorLockFill", "DoorLockLine", "DoorOpenFill", "DoorOpenLine", "DossierFill", "DossierLine", "DoubanFill", "DoubanLine", "DoubleQuotesL", "DoubleQuotesR", "Download2Fill", "Download2Line", "DownloadCloud2Fill", "DownloadCloud2Line", "DownloadCloudFill", "DownloadCloudLine", "DownloadFill", "DownloadLine", "DraftFill", "DraftLine", "DragDropFill", "DragDropLine", "DragMove2Fill", "DragMove2Line", "DragMoveFill", "DragMoveLine", "Draggable", "DrawFill", "DrawLine", "DribbbleFill", "DribbbleLine", "Drinks2Fill", "Drinks2Line", "DrinksFill", "DrinksLine", "DriveFill", "DriveLine", "DrizzleFill", "DrizzleLine", "DropFill", "DropLine", "DropboxFill", "DropboxLine", "DropdownList", "DropperFill", "DropperLine", "DualSim1Fill", "DualSim1Line", "DualSim2Fill", "DualSim2Line", "DvFill", "DvLine", "DvdAiFill", "DvdAiLine", "DvdFill", "DvdLine", "EBike2Fill", "EBike2Line", "EBikeFill", "EBikeLine", "EarthFill", "EarthLine", "EarthquakeFill", "EarthquakeLine", "EdgeFill", "EdgeLine", "EdgeNewFill", "EdgeNewLine", "Edit2Fill", "Edit2Line", "EditBoxFill", "EditBoxLine", "EditCircleFill", "EditCircleLine", "EditFill", "EditLine", "EjectFill", "EjectLine", "EmojiStickerFill", "EmojiStickerLine", "Emotion2Fill", "Emotion2Line", "EmotionAddFill", "EmotionAddLine", "EmotionFill", "EmotionHappyFill", "EmotionHappyLine", "EmotionLaughFill", "EmotionLaughLine", "EmotionLine", "EmotionNormalFill", "EmotionNormalLine", "EmotionSadFill", "EmotionSadLine", "EmotionUnhappyFill", "EmotionUnhappyLine", "EmpathizeFill", "EmpathizeLine", "Emphasis", "EmphasisCn", "EnglishInput", "EqualFill", "EqualLine", "Equalizer2Fill", "Equalizer2Line", "Equalizer3Fill", "Equalizer3Line", "EqualizerFill", "EqualizerLine", "EraserFill", "EraserLine", "ErrorWarningFill", "ErrorWarningLine", "EthFill", "EthLine", "EvernoteFill", "EvernoteLine", "Exchange2Fill", "Exchange2Line", "ExchangeBoxFill", "ExchangeBoxLine", "ExchangeCnyFill", "ExchangeCnyLine", "ExchangeDollarFill", "ExchangeDollarLine", "ExchangeFill", "ExchangeFundsFill", "ExchangeFundsLine", "ExchangeLine", "ExpandDiagonal2Fill", "ExpandDiagonal2Line", "ExpandDiagonalFill", "ExpandDiagonalLine", "ExpandDiagonalS2Fill", "ExpandDiagonalS2Line", "ExpandDiagonalSFill", "ExpandDiagonalSLine", "ExpandHeightFill", "ExpandHeightLine", "ExpandHorizontalFill", "ExpandHorizontalLine", "ExpandHorizontalSFill", "ExpandHorizontalSLine", "ExpandLeftFill", "ExpandLeftLine", "ExpandLeftRightFill", "ExpandLeftRightLine", "ExpandRightFill", "ExpandRightLine", "ExpandUpDownFill", "ExpandUpDownLine", "ExpandVerticalFill", "ExpandVerticalLine", "ExpandVerticalSFill", "ExpandVerticalSLine", "ExpandWidthFill", "ExpandWidthLine", "ExportFill", "ExportLine", "ExternalLinkFill", "ExternalLinkLine", "Eye2Fill", "Eye2Line", "EyeCloseFill", "EyeCloseLine", "EyeFill", "EyeLine", "EyeOffFill", "EyeOffLine", "FacebookBoxFill", "FacebookBoxLine", "FacebookCircleFill", "FacebookCircleLine", "FacebookFill", "FacebookLine", "FahrenheitFill", "FahrenheitLine", "FediverseFill", "FediverseLine", "FeedbackFill", "FeedbackLine", "FigmaFill", "FigmaLine", "File2Fill", "File2Line", "File3Fill", "File3Line", "File4Fill", "File4Line", "FileAddFill", "FileAddLine", "FileAi2Fill", "FileAi2Line", "FileAiFill", "FileAiLine", "FileChart2Fill", "FileChart2Line", "FileChartFill", "FileChartLine", "FileCheckFill", "FileCheckLine", "FileCloseFill", "FileCloseLine", "FileCloudFill", "FileCloudLine", "FileCodeFill", "FileCodeLine", "FileCopy2Fill", "FileCopy2Line", "FileCopyFill", "FileCopyLine", "FileDamageFill", "FileDamageLine", "FileDownloadFill", "FileDownloadLine", "FileEditFill", "FileEditLine", "FileExcel2Fill", "FileExcel2Line", "FileExcelFill", "FileExcelLine", "FileFill", "FileForbidFill", "FileForbidLine", "FileGifFill", "FileGifLine", "FileHistoryFill", "FileHistoryLine", "FileHwpFill", "FileHwpLine", "FileImageFill", "FileImageLine", "FileInfoFill", "FileInfoLine", "FileLine", "FileList2Fill", "FileList2Line", "FileList3Fill", "FileList3Line", "FileListFill", "FileListLine", "FileLockFill", "FileLockLine", "FileMarkedFill", "FileMarkedLine", "FileMusicFill", "FileMusicLine", "FilePaper2Fill", "FilePaper2Line", "FilePaperFill", "FilePaperLine", "FilePdf2Fill", "FilePdf2Line", "FilePdfFill", "FilePdfLine", "FilePpt2Fill", "FilePpt2Line", "FilePptFill", "FilePptLine", "FileReduceFill", "FileReduceLine", "FileScanFill", "FileScanLine", "FileSearchFill", "FileSearchLine", "FileSettingsFill", "FileSettingsLine", "FileShield2Fill", "FileShield2Line", "FileShieldFill", "FileShieldLine", "FileShredFill", "FileShredLine", "FileTextFill", "FileTextLine", "FileTransferFill", "FileTransferLine", "FileUnknowFill", "FileUnknowLine", "FileUploadFill", "FileUploadLine", "FileUserFill", "FileUserLine", "FileVideoFill", "FileVideoLine", "FileWarningFill", "FileWarningLine", "FileWord2Fill", "FileWord2Line", "FileWordFill", "FileWordLine", "FileZipFill", "FileZipLine", "FilmAiFill", "FilmAiLine", "FilmFill", "FilmLine", "Filter2Fill", "Filter2Line", "Filter3Fill", "Filter3Line", "FilterFill", "FilterLine", "FilterOffFill", "FilterOffLine", "FindReplaceFill", "FindReplaceLine", "FinderFill", "FinderLine", "Fingerprint2Fill", "Fingerprint2Line", "FingerprintFill", "FingerprintLine", "FireFill", "FireLine", "FirebaseFill", "FirebaseLine", "FirefoxBrowserFill", "FirefoxBrowserLine", "FirefoxFill", "FirefoxLine", "FirstAidKitFill", "FirstAidKitLine", "FiverrFill", "FiverrLine", "Flag2Fill", "Flag2Line", "FlagFill", "FlagLine", "FlagOffFill", "FlagOffLine", "FlashlightFill", "FlashlightLine", "FlaskFill", "FlaskLine", "FlickrFill", "FlickrLine", "FlightLandFill", "FlightLandLine", "FlightTakeoffFill", "FlightTakeoffLine", "FlipHorizontal2Fill", "FlipHorizontal2Line", "FlipHorizontalFill", "FlipHorizontalLine", "FlipVertical2Fill", "FlipVertical2Line", "FlipVerticalFill", "FlipVerticalLine", "FloodFill", "FloodLine", "FlowChart", "FlowerFill", "FlowerLine", "FlutterFill", "FlutterLine", "Focus2Fill", "Focus2Line", "Focus3Fill", "Focus3Line", "FocusFill", "FocusLine", "FocusMode", "FoggyFill", "FoggyLine", "Folder2Fill", "Folder2Line", "Folder3Fill", "Folder3Line", "Folder4Fill", "Folder4Line", "Folder5Fill", "Folder5Line", "Folder6Fill", "Folder6Line", "FolderAddFill", "FolderAddLine", "FolderChart2Fill", "FolderChart2Line", "FolderChartFill", "FolderChartLine", "FolderCheckFill", "FolderCheckLine", "FolderCloseFill", "FolderCloseLine", "FolderCloudFill", "FolderCloudLine", "FolderDownloadFill", "FolderDownloadLine", "FolderFill", "FolderForbidFill", "FolderForbidLine", "FolderHistoryFill", "FolderHistoryLine", "FolderImageFill", "FolderImageLine", "FolderInfoFill", "FolderInfoLine", "FolderKeyholeFill", "FolderKeyholeLine", "FolderLine", "FolderLockFill", "FolderLockLine", "FolderMusicFill", "FolderMusicLine", "FolderOpenFill", "FolderOpenLine", "FolderReceivedFill", "FolderReceivedLine", "FolderReduceFill", "FolderReduceLine", "FolderSettingsFill", "FolderSettingsLine", "FolderSharedFill", "FolderSharedLine", "FolderShield2Fill", "FolderShield2Line", "FolderShieldFill", "FolderShieldLine", "FolderTransferFill", "FolderTransferLine", "FolderUnknowFill", "FolderUnknowLine", "FolderUploadFill", "FolderUploadLine", "FolderUserFill", "FolderUserLine", "FolderVideoFill", "FolderVideoLine", "FolderWarningFill", "FolderWarningLine", "FolderZipFill", "FolderZipLine", "FoldersFill", "FoldersLine", "FontColor", "FontFamily", "FontMono", "FontSansSerif", "FontSerif", "FontSize", "FontSize2", "FontSizeAi", "FootballFill", "FootballLine", "FootprintFill", "FootprintLine", "Forbid2Fill", "Forbid2Line", "ForbidFill", "ForbidLine", "FormatClear", "Formula", "Forward10Fill", "Forward10Line", "Forward15Fill", "Forward15Line", "Forward30Fill", "Forward30Line", "Forward5Fill", "Forward5Line", "ForwardEndFill", "ForwardEndLine", "ForwardEndMiniFill", "ForwardEndMiniLine", "FridgeFill", "FridgeLine", "FriendicaFill", "FriendicaLine", "FullscreenExitFill", "FullscreenExitLine", "FullscreenFill", "FullscreenLine", "FunctionAddFill", "FunctionAddLine", "FunctionAiFill", "FunctionAiLine", "FunctionFill", "FunctionLine", "Functions", "FundsBoxFill", "FundsBoxLine", "FundsFill", "FundsLine", "GalleryFill", "GalleryLine", "GalleryUploadFill", "GalleryUploadLine", "GalleryView", "GalleryView2", "Game2Fill", "Game2Line", "GameFill", "GameLine", "GamepadFill", "GamepadLine", "GasStationFill", "GasStationLine", "GatsbyFill", "GatsbyLine", "GeminiFill", "GeminiLine", "GenderlessFill", "GenderlessLine", "Ghost2Fill", "Ghost2Line", "Ghost3Fill", "Ghost3Line", "Ghost4Fill", "Ghost4Line", "GhostFill", "GhostLine", "GhostSmileFill", "GhostSmileLine", "Gift2Fill", "Gift2Line", "GiftFill", "GiftLine", "GitBranchFill", "GitBranchLine", "GitClosePullRequestFill", "GitClosePullRequestLine", "GitCommitFill", "GitCommitLine", "GitForkFill", "GitForkLine", "GitMergeFill", "GitMergeLine", "GitPrDraftFill", "GitPrDraftLine", "GitPullRequestFill", "GitPullRequestLine", "GitRepositoryCommitsFill", "GitRepositoryCommitsLine", "GitRepositoryFill", "GitRepositoryLine", "GitRepositoryPrivateFill", "GitRepositoryPrivateLine", "GitbookFill", "GitbookLine", "GiteeFill", "GiteeLine", "GithubFill", "GithubLine", "GitlabFill", "GitlabLine", "Glasses2Fill", "Glasses2Line", "GlassesFill", "GlassesLine", "GlobalFill", "GlobalLine", "GlobalOffFill", "GlobalOffLine", "GlobeFill", "GlobeLine", "Goblet2Fill", "Goblet2Line", "GobletBrokenFill", "GobletBrokenLine", "GobletFill", "GobletLine", "GogglesFill", "GogglesLine", "GolfBallFill", "GolfBallLine", "GoogleFill", "GoogleLine", "GooglePlayFill", "GooglePlayLine", "GovernmentFill", "GovernmentLine", "GpsFill", "GpsLine", "GradienterFill", "GradienterLine", "GraduationCapFill", "GraduationCapLine", "GridFill", "GridLine", "GrokAiFill", "GrokAiLine", "Group2Fill", "Group2Line", "Group3Fill", "Group3Line", "GroupFill", "GroupLine", "GuideFill", "GuideLine", "H1", "H2", "H3", "H4", "H5", "H6", "HailFill", "HailLine", "HammerFill", "HammerLine", "Hand", "Hand2", "HandCoinFill", "HandCoinLine", "HandHeartFill", "HandHeartLine", "HandSanitizerFill", "HandSanitizerLine", "HandbagFill", "HandbagLine", "HardDrive2Fill", "HardDrive2Line", "HardDrive3Fill", "HardDrive3Line", "HardDriveFill", "HardDriveLine", "Hashtag", "Haze2Fill", "Haze2Line", "HazeFill", "HazeLine", "HdFill", "HdLine", "Heading", "HeadphoneFill", "HeadphoneLine", "HealthBookFill", "HealthBookLine", "Heart2Fill", "Heart2Line", "Heart3Fill", "Heart3Line", "HeartAdd2Fill", "HeartAdd2Line", "HeartAddFill", "HeartAddLine", "HeartFill", "HeartLine", "HeartPulseFill", "HeartPulseLine", "HeartsFill", "HeartsLine", "HeavyShowersFill", "HeavyShowersLine", "HexagonFill", "HexagonLine", "HistoryFill", "HistoryLine", "Home2Fill", "Home2Line", "Home3Fill", "Home3Line", "Home4Fill", "Home4Line", "Home5Fill", "Home5Line", "Home6Fill", "Home6Line", "Home7Fill", "Home7Line", "Home8Fill", "Home8Line", "Home9Fill", "Home9Line", "HomeFill", "HomeGearFill", "HomeGearLine", "HomeHeartFill", "HomeHeartLine", "HomeLine", "HomeOfficeFill", "HomeOfficeLine", "HomeSmile2Fill", "HomeSmile2Line", "HomeSmileFill", "HomeSmileLine", "HomeWifiFill", "HomeWifiLine", "HonorOfKingsFill", "HonorOfKingsLine", "HonourFill", "HonourLine", "HospitalFill", "HospitalLine", "HotelBedFill", "HotelBedLine", "HotelFill", "HotelLine", "HotspotFill", "HotspotLine", "Hourglass2Fill", "Hourglass2Line", "HourglassFill", "HourglassLine", "HqFill", "HqLine", "Html5Fill", "Html5Line", "IdCardFill", "IdCardLine", "IeFill", "IeLine", "Image2Fill", "Image2Line", "ImageAddFill", "ImageAddLine", "ImageAiFill", "ImageAiLine", "ImageCircleAiFill", "ImageCircleAiLine", "ImageCircleFill", "ImageCircleLine", "ImageDownloadFill", "ImageDownloadLine", "ImageEditFill", "ImageEditLine", "ImageFill", "ImageLine", "ImageUploadFill", "ImageUploadLine", "ImportFill", "ImportLine", "Inbox2Fill", "Inbox2Line", "InboxArchiveFill", "InboxArchiveLine", "InboxFill", "InboxLine", "InboxUnarchiveFill", "InboxUnarchiveLine", "IncreaseDecreaseFill", "IncreaseDecreaseLine", "IndentDecrease", "IndentIncrease", "IndeterminateCircleFill", "IndeterminateCircleLine", "InfinityFill", "InfinityLine", "InfoCardFill", "InfoCardLine", "InfoI", "Information2Fill", "Information2Line", "InformationFill", "InformationLine", "InformationOffFill", "InformationOffLine", "InfraredThermometerFill", "InfraredThermometerLine", "InkBottleFill", "InkBottleLine", "InputCursorMove", "InputField", "InputMethodFill", "InputMethodLine", "InsertColumnLeft", "InsertColumnRight", "InsertRowBottom", "InsertRowTop", "InstagramFill", "InstagramLine", "InstallFill", "InstallLine", "InstanceFill", "InstanceLine", "InvisionFill", "InvisionLine", "IssuesFill", "IssuesLine", "IssuesReopenFill", "IssuesReopenLine", "Italic", "JavaFill", "JavaLine", "JavascriptFill", "JavascriptLine", "JewelryFill", "JewelryLine", "KakaoTalkFill", "KakaoTalkLine", "KanbanView", "KanbanView2", "Key2Fill", "Key2Line", "KeyFill", "KeyLine", "KeyboardBoxFill", "KeyboardBoxLine", "KeyboardFill", "KeyboardLine", "KeynoteFill", "KeynoteLine", "KickFill", "KickLine", "KnifeBloodFill", "KnifeBloodLine", "KnifeFill", "KnifeLine", "LandscapeAiFill", "LandscapeAiLine", "LandscapeFill", "LandscapeLine", "Layout2Fill", "Layout2Line", "Layout3Fill", "Layout3Line", "Layout4Fill", "Layout4Line", "Layout5Fill", "Layout5Line", "Layout6Fill", "Layout6Line", "LayoutBottom2Fill", "LayoutBottom2Line", "LayoutBottomFill", "LayoutBottomLine", "LayoutColumnFill", "LayoutColumnLine", "LayoutFill", "LayoutGrid2Fill", "LayoutGrid2Line", "LayoutGridFill", "LayoutGridLine", "LayoutHorizontalFill", "LayoutHorizontalLine", "LayoutLeft2Fill", "LayoutLeft2Line", "LayoutLeftFill", "LayoutLeftLine", "LayoutLine", "LayoutMasonryFill", "LayoutMasonryLine", "LayoutRight2Fill", "LayoutRight2Line", "LayoutRightFill", "LayoutRightLine", "LayoutRowFill", "LayoutRowLine", "LayoutTop2Fill", "LayoutTop2Line", "LayoutTopFill", "LayoutTopLine", "LayoutVerticalFill", "LayoutVerticalLine", "LeafFill", "LeafLine", "LetterSpacing2", "LifebuoyFill", "LifebuoyLine", "LightbulbAiFill", "LightbulbAiLine", "LightbulbFill", "LightbulbFlashFill", "LightbulbFlashLine", "LightbulbLine", "LineChartFill", "LineChartLine", "LineFill", "LineHeight", "LineHeight2", "LineLine", "Link", "LinkM", "LinkUnlink", "LinkUnlinkM", "LinkedinBoxFill", "LinkedinBoxLine", "LinkedinFill", "LinkedinLine", "LinksFill", "LinksLine", "ListCheck", "ListCheck2", "ListCheck3", "ListIndefinite", "ListOrdered", "ListOrdered2", "ListRadio", "ListSettingsFill", "ListSettingsLine", "ListUnordered", "ListView", "LiveFill", "LiveLine", "Loader2Fill", "Loader2Line", "Loader3Fill", "Loader3Line", "Loader4Fill", "Loader4Line", "Loader5Fill", "Loader5Line", "LoaderFill", "LoaderLine", "Lock2Fill", "Lock2Line", "LockFill", "LockLine", "LockPasswordFill", "LockPasswordLine", "LockStarFill", "LockStarLine", "LockUnlockFill", "LockUnlockLine", "LoginBoxFill", "LoginBoxLine", "LoginCircleFill", "LoginCircleLine", "LogoutBoxFill", "LogoutBoxLine", "LogoutBoxRFill", "LogoutBoxRLine", "LogoutCircleFill", "LogoutCircleLine", "LogoutCircleRFill", "LogoutCircleRLine", "LoopLeftAiFill", "LoopLeftAiLine", "LoopLeftFill", "LoopLeftLine", "LoopRightAiFill", "LoopRightAiLine", "LoopRightFill", "LoopRightLine", "LuggageCartFill", "LuggageCartLine", "LuggageDepositFill", "LuggageDepositLine", "LungsFill", "LungsLine", "MacFill", "MacLine", "MacbookFill", "MacbookLine", "MagicFill", "MagicLine", "MailAddFill", "MailAddLine", "MailAiFill", "MailAiLine", "MailCheckFill", "MailCheckLine", "MailCloseFill", "MailCloseLine", "MailDownloadFill", "MailDownloadLine", "MailFill", "MailForbidFill", "MailForbidLine", "MailLine", "MailLockFill", "MailLockLine", "MailOpenFill", "MailOpenLine", "MailSendFill", "MailSendLine", "MailSettingsFill", "MailSettingsLine", "MailStarFill", "MailStarLine", "MailUnreadFill", "MailUnreadLine", "MailVolumeFill", "MailVolumeLine", "Map2Fill", "Map2Line", "MapFill", "MapLine", "MapPin2Fill", "MapPin2Line", "MapPin3Fill", "MapPin3Line", "MapPin4Fill", "MapPin4Line", "MapPin5Fill", "MapPin5Line", "MapPinAddFill", "MapPinAddLine", "MapPinFill", "MapPinLine", "MapPinRangeFill", "MapPinRangeLine", "MapPinTimeFill", "MapPinTimeLine", "MapPinUserFill", "MapPinUserLine", "MarkPenFill", "MarkPenLine", "MarkdownFill", "MarkdownLine", "MarkupFill", "MarkupLine", "MastercardFill", "MastercardLine", "MastodonFill", "MastodonLine", "Medal2Fill", "Medal2Line", "MedalFill", "MedalLine", "MedicineBottleFill", "MedicineBottleLine", "MediumFill", "MediumLine", "Megaphone2Fill", "Megaphone2Line", "MegaphoneFill", "MegaphoneLine", "MemoriesFill", "MemoriesLine", "MenFill", "MenLine", "MentalHealthFill", "MentalHealthLine", "Menu2Fill", "Menu2Line", "Menu3Fill", "Menu3Line", "Menu4Fill", "Menu4Line", "Menu5Fill", "Menu5Line", "MenuAddFill", "MenuAddLine", "MenuFill", "MenuFold2Fill", "MenuFold2Line", "MenuFold3Fill", "MenuFold3Line", "MenuFold4Fill", "MenuFold4Line", "MenuFoldFill", "MenuFoldLine", "MenuLine", "MenuSearchFill", "MenuSearchLine", "MenuUnfold2Fill", "MenuUnfold2Line", "MenuUnfold3Fill", "MenuUnfold3Line", "MenuUnfold4Fill", "MenuUnfold4Line", "MenuUnfoldFill", "MenuUnfoldLine", "MergeCellsHorizontal", "MergeCellsVertical", "Message2Fill", "Message2Line", "Message3Fill", "Message3Line", "MessageAi3Fill", "MessageAi3Line", "MessageFill", "MessageLine", "MessengerFill", "MessengerLine", "MetaFill", "MetaLine", "MeteorFill", "MeteorLine", "Mic2AiFill", "Mic2AiLine", "Mic2Fill", "Mic2Line", "MicAiFill", "MicAiLine", "MicFill", "MicLine", "MicOffFill", "MicOffLine", "MickeyFill", "MickeyLine", "MicroscopeFill", "MicroscopeLine", "MicrosoftCopilotFill", "MicrosoftCopilotLine", "MicrosoftFill", "MicrosoftLine", "MicrosoftLoopFill", "MicrosoftLoopLine", "MindMap", "MiniProgramFill", "MiniProgramLine", "MistFill", "MistLine", "MixtralFill", "MixtralLine", "MobileDownloadFill", "MobileDownloadLine", "MoneyCnyBoxFill", "MoneyCnyBoxLine", "MoneyCnyCircleFill", "MoneyCnyCircleLine", "MoneyDollarBoxFill", "MoneyDollarBoxLine", "MoneyDollarCircleFill", "MoneyDollarCircleLine", "MoneyEuroBoxFill", "MoneyEuroBoxLine", "MoneyEuroCircleFill", "MoneyEuroCircleLine", "MoneyPoundBoxFill", "MoneyPoundBoxLine", "MoneyPoundCircleFill", "MoneyPoundCircleLine", "MoneyRupeeCircleFill", "MoneyRupeeCircleLine", "MoonClearFill", "MoonClearLine", "MoonCloudyFill", "MoonCloudyLine", "MoonFill", "MoonFoggyFill", "MoonFoggyLine", "MoonLine", "More2Fill", "More2Line", "MoreFill", "MoreLine", "MosaicFill", "MosaicLine", "MotorbikeFill", "MotorbikeLine", "MouseFill", "MouseLine", "Movie2AiFill", "Movie2AiLine", "Movie2Fill", "Movie2Line", "MovieAiFill", "MovieAiLine", "MovieFill", "MovieLine", "MultiImageFill", "MultiImageLine", "Music2Fill", "Music2Line", "MusicAiFill", "MusicAiLine", "MusicFill", "MusicLine", "MvAiFill", "MvAiLine", "MvFill", "MvLine", "NavigationFill", "NavigationLine", "NeteaseCloudMusicFill", "NeteaseCloudMusicLine", "NetflixFill", "NetflixLine", "NetworkErrorFill", "NetworkErrorLine", "NetworkFill", "NetworkLine", "NetworkOffFill", "NetworkOffLine", "NewsFill", "NewsLine", "NewspaperFill", "NewspaperLine", "NextjsFill", "NextjsLine", "NftFill", "NftLine", "NoCreditCardFill", "NoCreditCardLine", "NodeTree", "NodejsFill", "NodejsLine", "Notification2Fill", "Notification2Line", "Notification3Fill", "Notification3Line", "Notification4Fill", "Notification4Line", "NotificationBadgeFill", "NotificationBadgeLine", "NotificationFill", "NotificationLine", "NotificationOffFill", "NotificationOffLine", "NotificationSnoozeFill", "NotificationSnoozeLine", "NotionFill", "NotionLine", "NpmjsFill", "NpmjsLine", "Number0", "Number1", "Number2", "Number3", "Number4", "Number5", "Number6", "Number7", "Number8", "Number9", "NumbersFill", "NumbersLine", "NurseFill", "NurseLine", "OctagonFill", "OctagonLine", "OilFill", "OilLine", "Omega", "OpenArmFill", "OpenArmLine", "OpenSourceFill", "OpenSourceLine", "OpenaiFill", "OpenaiLine", "OpenbaseFill", "OpenbaseLine", "OperaFill", "OperaLine", "OrderPlayFill", "OrderPlayLine", "OrganizationChart", "Outlet2Fill", "Outlet2Line", "OutletFill", "OutletLine", "Overline", "P2pFill", "P2pLine", "PageSeparator", "PagesFill", "PagesLine", "PaintBrushFill", "PaintBrushLine", "PaintFill", "PaintLine", "PaintingAiFill", "PaintingAiLine", "PaintingFill", "PaintingLine", "PaletteFill", "PaletteLine", "PantoneFill", "PantoneLine", "Paragraph", "ParentFill", "ParentLine", "ParenthesesFill", "ParenthesesLine", "ParkingBoxFill", "ParkingBoxLine", "ParkingFill", "ParkingLine", "PassExpiredFill", "PassExpiredLine", "PassPendingFill", "PassPendingLine", "PassValidFill", "PassValidLine", "PassportFill", "PassportLine", "PatreonFill", "PatreonLine", "PauseCircleFill", "PauseCircleLine", "PauseFill", "PauseLargeFill", "PauseLargeLine", "PauseLine", "PauseMiniFill", "PauseMiniLine", "PaypalFill", "PaypalLine", "PenNibFill", "PenNibLine", "PencilAi2Fill", "PencilAi2Line", "PencilAiFill", "PencilAiLine", "PencilFill", "PencilLine", "PencilRuler2Fill", "PencilRuler2Line", "PencilRulerFill", "PencilRulerLine", "PentagonFill", "PentagonLine", "PercentFill", "PercentLine", "PerplexityFill", "PerplexityLine", "PhoneCameraFill", "PhoneCameraLine", "PhoneFill", "PhoneFindFill", "PhoneFindLine", "PhoneLine", "PhoneLockFill", "PhoneLockLine", "PhpFill", "PhpLine", "PianoFill", "PianoGrandFill", "PianoGrandLine", "PianoLine", "PictureInPicture2Fill", "PictureInPicture2Line", "PictureInPictureExitFill", "PictureInPictureExitLine", "PictureInPictureFill", "PictureInPictureLine", "PieChart2Fill", "PieChart2Line", "PieChartBoxFill", "PieChartBoxLine", "PieChartFill", "PieChartLine", "PinDistanceFill", "PinDistanceLine", "PingPongFill", "PingPongLine", "PinterestFill", "PinterestLine", "PinyinInput", "PixFill", "PixLine", "PixelfedFill", "PixelfedLine", "PlaneFill", "PlaneLine", "PlanetFill", "PlanetLine", "PlantFill", "PlantLine", "PlayCircleFill", "PlayCircleLine", "PlayFill", "PlayLargeFill", "PlayLargeLine", "PlayLine", "PlayList2Fill", "PlayList2Line", "PlayListAddFill", "PlayListAddLine", "PlayListFill", "PlayListLine", "PlayMiniFill", "PlayMiniLine", "PlayReverseFill", "PlayReverseLargeFill", "PlayReverseLargeLine", "PlayReverseLine", "PlayReverseMiniFill", "PlayReverseMiniLine", "PlaystationFill", "PlaystationLine", "Plug2Fill", "Plug2Line", "Plug3Fill", "Plug3Line", "PlugFill", "PlugLine", "PokerClubsFill", "PokerClubsLine", "PokerDiamondsFill", "PokerDiamondsLine", "PokerHeartsFill", "PokerHeartsLine", "PokerSpadesFill", "PokerSpadesLine", "Polaroid2Fill", "Polaroid2Line", "PolaroidFill", "PolaroidLine", "PoliceBadgeFill", "PoliceBadgeLine", "PoliceCarFill", "PoliceCarLine", "PresentationFill", "PresentationLine", "PriceTag2Fill", "PriceTag2Line", "PriceTag3Fill", "PriceTag3Line", "PriceTagFill", "PriceTagLine", "PrinterCloudFill", "PrinterCloudLine", "PrinterFill", "PrinterLine", "ProductHuntFill", "ProductHuntLine", "ProfileFill", "ProfileLine", "Progress1Fill", "Progress1Line", "Progress2Fill", "Progress2Line", "Progress3Fill", "Progress3Line", "Progress4Fill", "Progress4Line", "Progress5Fill", "Progress5Line", "Progress6Fill", "Progress6Line", "Progress7Fill", "Progress7Line", "Progress8Fill", "Progress8Line", "Prohibited2Fill", "Prohibited2Line", "ProhibitedFill", "ProhibitedLine", "Projector2Fill", "Projector2Line", "ProjectorFill", "ProjectorLine", "PsychotherapyFill", "PsychotherapyLine", "PulseAiFill", "PulseAiLine", "PulseFill", "PulseLine", "Pushpin2Fill", "Pushpin2Line", "PushpinFill", "PushpinLine", "Puzzle2Fill", "Puzzle2Line", "PuzzleFill", "PuzzleLine", "QqFill", "QqLine", "QrCodeFill", "QrCodeLine", "QrScan2Fill", "QrScan2Line", "QrScanAiFill", "QrScanAiLine", "QrScanFill", "QrScanLine", "QuestionAnswerFill", "QuestionAnswerLine", "QuestionFill", "QuestionLine", "QuestionMark", "QuestionnaireFill", "QuestionnaireLine", "QuillPenAiFill", "QuillPenAiLine", "QuillPenFill", "QuillPenLine", "QuoteText", "QwenAiFill", "QwenAiLine", "RadarFill", "RadarLine", "Radio2Fill", "Radio2Line", "RadioButtonFill", "RadioButtonLine", "RadioFill", "RadioLine", "RainbowFill", "RainbowLine", "RainyFill", "RainyLine", "Ram2Fill", "Ram2Line", "RamFill", "RamLine", "ReactjsFill", "ReactjsLine", "ReceiptFill", "ReceiptLine", "RecordCircleFill", "RecordCircleLine", "RecordMailFill", "RecordMailLine", "RectangleFill", "RectangleLine", "RecycleFill", "RecycleLine", "RedPacketFill", "RedPacketLine", "Reddit2Fill", "Reddit2Line", "RedditFill", "RedditLine", "RefreshFill", "RefreshLine", "Refund2Fill", "Refund2Line", "RefundFill", "RefundLine", "RegisteredFill", "RegisteredLine", "RemixFill", "RemixLine", "RemixRunFill", "RemixRunLine", "RemixiconFill", "RemixiconLine", "RemoteControl2Fill", "RemoteControl2Line", "RemoteControlFill", "RemoteControlLine", "Repeat2Fill", "Repeat2Line", "RepeatFill", "RepeatLine", "RepeatOneFill", "RepeatOneLine", "Replay10Fill", "Replay10Line", "Replay15Fill", "Replay15Line", "Replay30Fill", "Replay30Line", "Replay5Fill", "Replay5Line", "ReplyAllFill", "ReplyAllLine", "ReplyFill", "ReplyLine", "ReservedFill", "ReservedLine", "ResetLeftFill", "ResetLeftLine", "ResetRightFill", "ResetRightLine", "RestTimeFill", "RestTimeLine", "RestartFill", "RestartLine", "Restaurant2Fill", "Restaurant2Line", "RestaurantFill", "RestaurantLine", "RewindFill", "RewindLine", "RewindMiniFill", "RewindMiniLine", "RewindStartFill", "RewindStartLine", "RewindStartMiniFill", "RewindStartMiniLine", "RfidFill", "RfidLine", "RhythmFill", "RhythmLine", "RidingFill", "RidingLine", "RoadMapFill", "RoadMapLine", "RoadsterFill", "RoadsterLine", "Robot2Fill", "Robot2Line", "Robot3Fill", "Robot3Line", "RobotFill", "RobotLine", "Rocket2Fill", "Rocket2Line", "RocketFill", "RocketLine", "RotateLockFill", "RotateLockLine", "RoundedCorner", "RouteFill", "RouteLine", "RouterFill", "RouterLine", "RssFill", "RssLine", "Ruler2Fill", "Ruler2Line", "RulerFill", "RulerLine", "RunFill", "RunLine", "SafariFill", "SafariLine", "Safe2Fill", "Safe2Line", "Safe3Fill", "Safe3Line", "SafeFill", "SafeLine", "SailboatFill", "SailboatLine", "Save2Fill", "Save2Line", "Save3Fill", "Save3Line", "SaveFill", "SaveLine", "Scales2Fill", "Scales2Line", "Scales3Fill", "Scales3Line", "ScalesFill", "ScalesLine", "Scan2Fill", "Scan2Line", "ScanFill", "ScanLine", "SchoolFill", "SchoolLine", "Scissors2Fill", "Scissors2Line", "ScissorsCutFill", "ScissorsCutLine", "ScissorsFill", "ScissorsLine", "Screenshot2Fill", "Screenshot2Line", "ScreenshotFill", "ScreenshotLine", "ScrollToBottomFill", "ScrollToBottomLine", "SdCardFill", "SdCardLine", "SdCardMiniFill", "SdCardMiniLine", "Search2Fill", "Search2Line", "SearchAi2Fill", "SearchAi2Line", "SearchAi3Fill", "SearchAi3Line", "SearchAi4Fill", "SearchAi4Line", "SearchAiFill", "SearchAiLine", "SearchEyeFill", "SearchEyeLine", "SearchFill", "SearchLine", "SecurePaymentFill", "SecurePaymentLine", "SeedlingFill", "SeedlingLine", "SendBackward", "SendInsFill", "SendInsLine", "SendPlane2Fill", "SendPlane2Line", "SendPlaneFill", "SendPlaneLine", "SendToBack", "SensorFill", "SensorLine", "SeoFill", "SeoLine", "Separator", "ServerFill", "ServerLine", "ServiceBellFill", "ServiceBellLine", "ServiceFill", "ServiceLine", "Settings2Fill", "Settings2Line", "Settings3Fill", "Settings3Line", "Settings4Fill", "Settings4Line", "Settings5Fill", "Settings5Line", "Settings6Fill", "Settings6Line", "SettingsFill", "SettingsLine", "ShadowFill", "ShadowLine", "ShakeHandsFill", "ShakeHandsLine", "Shape2Fill", "Shape2Line", "ShapeFill", "ShapeLine", "ShapesFill", "ShapesLine", "Share2Fill", "Share2Line", "ShareBoxFill", "ShareBoxLine", "ShareCircleFill", "ShareCircleLine", "ShareFill", "ShareForward2Fill", "ShareForward2Line", "ShareForwardBoxFill", "ShareForwardBoxLine", "ShareForwardFill", "ShareForwardLine", "ShareLine", "ShieldCheckFill", "ShieldCheckLine", "ShieldCrossFill", "ShieldCrossLine", "ShieldFill", "ShieldFlashFill", "ShieldFlashLine", "ShieldKeyholeFill", "ShieldKeyholeLine", "ShieldLine", "ShieldStarFill", "ShieldStarLine", "ShieldUserFill", "ShieldUserLine", "Shining2Fill", "Shining2Line", "ShiningFill", "ShiningLine", "Ship2Fill", "Ship2Line", "ShipFill", "ShipLine", "ShirtFill", "ShirtLine", "ShoppingBag2Fill", "ShoppingBag2Line", "ShoppingBag3Fill", "ShoppingBag3Line", "ShoppingBag4Fill", "ShoppingBag4Line", "ShoppingBagFill", "ShoppingBagLine", "ShoppingBasket2Fill", "ShoppingBasket2Line", "ShoppingBasketFill", "ShoppingBasketLine", "ShoppingCart2Fill", "ShoppingCart2Line", "ShoppingCartFill", "ShoppingCartLine", "ShowersFill", "ShowersLine", "ShuffleFill", "ShuffleLine", "ShutDownFill", "ShutDownLine", "SideBarFill", "SideBarLine", "SidebarFoldFill", "SidebarFoldLine", "SidebarUnfoldFill", "SidebarUnfoldLine", "SignalCellular1Fill", "SignalCellular1Line", "SignalCellular2Fill", "SignalCellular2Line", "SignalCellular3Fill", "SignalCellular3Line", "SignalCellularOffFill", "SignalCellularOffLine", "SignalTowerFill", "SignalTowerLine", "SignalWifi1Fill", "SignalWifi1Line", "SignalWifi2Fill", "SignalWifi2Line", "SignalWifi3Fill", "SignalWifi3Line", "SignalWifiErrorFill", "SignalWifiErrorLine", "SignalWifiFill", "SignalWifiLine", "SignalWifiOffFill", "SignalWifiOffLine", "SignpostFill", "SignpostLine", "SimCard2Fill", "SimCard2Line", "SimCardFill", "SimCardLine", "SimCardWarningFill", "SimCardWarningLine", "SingleQuotesL", "SingleQuotesR", "SipFill", "SipLine", "Sketching", "SkipBackFill", "SkipBackLine", "SkipBackMiniFill", "SkipBackMiniLine", "SkipDownFill", "SkipDownLine", "SkipForwardFill", "SkipForwardLine", "SkipForwardMiniFill", "SkipForwardMiniLine", "SkipLeftFill", "SkipLeftLine", "SkipRightFill", "SkipRightLine", "SkipUpFill", "SkipUpLine", "Skull2Fill", "Skull2Line", "SkullFill", "SkullLine", "SkypeFill", "SkypeLine", "SlackFill", "SlackLine", "SlashCommands", "SlashCommands2", "SliceFill", "SliceLine", "Slideshow2Fill", "Slideshow2Line", "Slideshow3Fill", "Slideshow3Line", "Slideshow4Fill", "Slideshow4Line", "SlideshowFill", "SlideshowLine", "SlideshowView", "SlowDownFill", "SlowDownLine", "SmartphoneFill", "SmartphoneLine", "SnapchatFill", "SnapchatLine", "SnowflakeFill", "SnowflakeLine", "SnowyFill", "SnowyLine", "SofaFill", "SofaLine", "SortAlphabetAsc", "SortAlphabetDesc", "SortAsc", "SortDesc", "SortNumberAsc", "SortNumberDesc", "SoundModuleFill", "SoundModuleLine", "SoundcloudFill", "SoundcloudLine", "Space", "SpaceShip2Fill", "SpaceShip2Line", "SpaceShipFill", "SpaceShipLine", "Spam2Fill", "Spam2Line", "Spam3Fill", "Spam3Line", "SpamFill", "SpamLine", "Sparkling2Fill", "Sparkling2Line", "SparklingFill", "SparklingLine", "SpeakAiFill", "SpeakAiLine", "SpeakFill", "SpeakLine", "Speaker2Fill", "Speaker2Line", "Speaker3Fill", "Speaker3Line", "SpeakerFill", "SpeakerLine", "SpectrumFill", "SpectrumLine", "SpeechToTextFill", "SpeechToTextLine", "SpeedFill", "SpeedLine", "SpeedMiniFill", "SpeedMiniLine", "SpeedUpFill", "SpeedUpLine", "SplitCellsHorizontal", "SplitCellsVertical", "SpotifyFill", "SpotifyLine", "SpyFill", "SpyLine", "SquareFill", "SquareLine", "SquareRoot", "StackFill", "StackLine", "StackOverflowFill", "StackOverflowLine", "StackedChartFill", "StackedChartLine", "StackedView", "StackshareFill", "StackshareLine", "StairsFill", "StairsLine", "StarFill", "StarHalfFill", "StarHalfLine", "StarHalfSFill", "StarHalfSLine", "StarLine", "StarOffFill", "StarOffLine", "StarSFill", "StarSLine", "StarSmileFill", "StarSmileLine", "SteamFill", "SteamLine", "Steering2Fill", "Steering2Line", "SteeringFill", "SteeringLine", "StethoscopeFill", "StethoscopeLine", "StickyNote2Fill", "StickyNote2Line", "StickyNoteAddFill", "StickyNoteAddLine", "StickyNoteFill", "StickyNoteLine", "StockFill", "StockLine", "StopCircleFill", "StopCircleLine", "StopFill", "StopLargeFill", "StopLargeLine", "StopLine", "StopMiniFill", "StopMiniLine", "Store2Fill", "Store2Line", "Store3Fill", "Store3Line", "StoreFill", "StoreLine", "Strikethrough", "Strikethrough2", "SubredditFill", "SubredditLine", "Subscript", "Subscript2", "SubtractFill", "SubtractLine", "SubwayFill", "SubwayLine", "SubwayWifiFill", "SubwayWifiLine", "Suitcase2Fill", "Suitcase2Line", "Suitcase3Fill", "Suitcase3Line", "SuitcaseFill", "SuitcaseLine", "SunCloudyFill", "SunCloudyLine", "SunFill", "SunFoggyFill", "SunFoggyLine", "SunLine", "SupabaseFill", "SupabaseLine", "Superscript", "Superscript2", "SurgicalMaskFill", "SurgicalMaskLine", "SurroundSoundFill", "SurroundSoundLine", "SurveyFill", "SurveyLine", "SvelteFill", "SvelteLine", "Swap2Fill", "Swap2Line", "Swap3Fill", "Swap3Line", "SwapBoxFill", "SwapBoxLine", "SwapFill", "SwapLine", "SwitchFill", "SwitchLine", "SwordFill", "SwordLine", "SyringeFill", "SyringeLine", "TBoxFill", "TBoxLine", "TShirt2Fill", "TShirt2Line", "TShirtAirFill", "TShirtAirLine", "TShirtFill", "TShirtLine", "Table2", "Table3", "TableAltFill", "TableAltLine", "TableFill", "TableLine", "TableView", "TabletFill", "TabletLine", "TailwindCssFill", "TailwindCssLine", "TakeawayFill", "TakeawayLine", "TaobaoFill", "TaobaoLine", "TapeFill", "TapeLine", "TargetFill", "TargetLine", "TaskFill", "TaskLine", "TaxiFill", "TaxiLine", "TaxiWifiFill", "TaxiWifiLine", "TeamFill", "TeamLine", "Telegram2Fill", "Telegram2Line", "TelegramFill", "TelegramLine", "TempColdFill", "TempColdLine", "TempHotFill", "TempHotLine", "TentFill", "TentLine", "TerminalBoxFill", "TerminalBoxLine", "TerminalFill", "TerminalLine", "TerminalWindowFill", "TerminalWindowLine", "TestTubeFill", "TestTubeLine", "Text", "TextBlock", "TextDirectionL", "TextDirectionR", "TextSnippet", "TextSpacing", "TextToSpeechFill", "TextToSpeechLine", "TextWrap", "ThermometerFill", "ThermometerLine", "ThreadsFill", "ThreadsLine", "ThumbDownFill", "ThumbDownLine", "ThumbUpFill", "ThumbUpLine", "ThunderstormsFill", "ThunderstormsLine", "Ticket2Fill", "Ticket2Line", "TicketFill", "TicketLine", "TiktokFill", "TiktokLine", "TimeFill", "TimeLine", "TimeZoneFill", "TimeZoneLine", "TimelineView", "Timer2Fill", "Timer2Line", "TimerFill", "TimerFlashFill", "TimerFlashLine", "TimerLine", "TodoFill", "TodoLine", "ToggleFill", "ToggleLine", "TokenSwapFill", "TokenSwapLine", "ToolsFill", "ToolsLine", "ToothFill", "ToothLine", "TornadoFill", "TornadoLine", "TrademarkFill", "TrademarkLine", "TrafficLightFill", "TrafficLightLine", "TrainFill", "TrainLine", "TrainWifiFill", "TrainWifiLine", "Translate", "Translate2", "TranslateAi", "TranslateAi2", "TravestiFill", "TravestiLine", "TreasureMapFill", "TreasureMapLine", "TreeFill", "TreeLine", "TrelloFill", "TrelloLine", "TriangleFill", "TriangleLine", "TriangularFlagFill", "TriangularFlagLine", "TrophyFill", "TrophyLine", "TruckFill", "TruckLine", "TumblrFill", "TumblrLine", "Tv2Fill", "Tv2Line", "TvFill", "TvLine", "TwitchFill", "TwitchLine", "TwitterFill", "TwitterLine", "TwitterXFill", "TwitterXLine", "TyphoonFill", "TyphoonLine", "UDiskFill", "UDiskLine", "UbuntuFill", "UbuntuLine", "UmbrellaFill", "UmbrellaLine", "Underline", "UninstallFill", "UninstallLine", "UnpinFill", "UnpinLine", "UnsplashFill", "UnsplashLine", "Upload2Fill", "Upload2Line", "UploadCloud2Fill", "UploadCloud2Line", "UploadCloudFill", "UploadCloudLine", "UploadFill", "UploadLine", "UpworkFill", "UpworkLine", "UsbFill", "UsbLine", "User2Fill", "User2Line", "User3Fill", "User3Line", "User4Fill", "User4Line", "User5Fill", "User5Line", "User6Fill", "User6Line", "UserAddFill", "UserAddLine", "UserCommunityFill", "UserCommunityLine", "UserFill", "UserFollowFill", "UserFollowLine", "UserForbidFill", "UserForbidLine", "UserHeartFill", "UserHeartLine", "UserLine", "UserLocationFill", "UserLocationLine", "UserMinusFill", "UserMinusLine", "UserReceived2Fill", "UserReceived2Line", "UserReceivedFill", "UserReceivedLine", "UserSearchFill", "UserSearchLine", "UserSettingsFill", "UserSettingsLine", "UserShared2Fill", "UserShared2Line", "UserSharedFill", "UserSharedLine", "UserSmileFill", "UserSmileLine", "UserStarFill", "UserStarLine", "UserUnfollowFill", "UserUnfollowLine", "UserVoiceFill", "UserVoiceLine", "VercelFill", "VercelLine", "VerifiedBadgeFill", "VerifiedBadgeLine", "VideoAddFill", "VideoAddLine", "VideoAiFill", "VideoAiLine", "VideoChatFill", "VideoChatLine", "VideoDownloadFill", "VideoDownloadLine", "VideoFill", "VideoLine", "VideoOffFill", "VideoOffLine", "VideoOnAiFill", "VideoOnAiLine", "VideoOnFill", "VideoOnLine", "VideoUploadFill", "VideoUploadLine", "Vidicon2Fill", "Vidicon2Line", "VidiconFill", "VidiconLine", "VimeoFill", "VimeoLine", "VipCrown2Fill", "VipCrown2Line", "VipCrownFill", "VipCrownLine", "VipDiamondFill", "VipDiamondLine", "VipFill", "VipLine", "VirusFill", "VirusLine", "VisaFill", "VisaLine", "VkFill", "VkLine", "VoiceAiFill", "VoiceAiLine", "VoiceRecognitionFill", "VoiceRecognitionLine", "VoiceprintFill", "VoiceprintLine", "VolumeDownFill", "VolumeDownLine", "VolumeMuteFill", "VolumeMuteLine", "VolumeOffVibrateFill", "VolumeOffVibrateLine", "VolumeUpFill", "VolumeUpLine", "VolumeVibrateFill", "VolumeVibrateLine", "VuejsFill", "VuejsLine", "WalkFill", "WalkLine", "Wallet2Fill", "Wallet2Line", "Wallet3Fill", "Wallet3Line", "WalletFill", "WalletLine", "WaterFlashFill", "WaterFlashLine", "WaterPercentFill", "WaterPercentLine", "WebcamFill", "WebcamLine", "WebhookFill", "WebhookLine", "Wechat2Fill", "Wechat2Line", "WechatChannelsFill", "WechatChannelsLine", "WechatFill", "WechatLine", "WechatPayFill", "WechatPayLine", "WeiboFill", "WeiboLine", "WeightFill", "WeightLine", "WhatsappFill", "WhatsappLine", "WheelchairFill", "WheelchairLine", "WifiFill", "WifiLine", "WifiOffFill", "WifiOffLine", "Window2Fill", "Window2Line", "WindowFill", "WindowLine", "WindowsFill", "WindowsLine", "WindyFill", "WindyLine", "WirelessChargingFill", "WirelessChargingLine", "WomenFill", "WomenLine", "WordpressFill", "WordpressLine", "WrenchFill", "WrenchLine", "WubiInput", "XboxFill", "XboxLine", "XingFill", "XingLine", "XrpFill", "XrpLine", "XtzFill", "XtzLine", "YoutubeFill", "YoutubeLine", "YuqueFill", "YuqueLine", "ZcoolFill", "ZcoolLine", "ZhihuFill", "ZhihuLine", "ZhipuAiFill", "ZhipuAiLine", "ZoomInFill", "ZoomInLine", "ZoomOutFill", "ZoomOutLine", "ZzzFill", "ZzzLine"]);
@@ -4,7 +4,7 @@ export const SHAPE_SOURCE = {
4
4
  "fromHandlers": 166,
5
5
  "fromSwaggerOnly": 0,
6
6
  "withReadOnly": 26,
7
- "structsRead": 1641
7
+ "structsRead": 1647
8
8
  };
9
9
  export const REQUEST_SHAPES = {
10
10
  "post:/api/sites/{siteId}/api-keys": {
@@ -851,7 +851,6 @@ export const STORE_PAGE_SEEDS = {
851
851
  },
852
852
  "config": {
853
853
  "defaultNum": 1,
854
- "activeButton": "",
855
854
  "quantityButtonId": "sppro_16",
856
855
  "quantityInputId": "sppro_17"
857
856
  },
@@ -39,6 +39,79 @@ const NOT_CONTENT = [
39
39
  // as often /blog/tag/x as /tag/x.
40
40
  '/tag/', '/tags/', '/author/', '/authors/',
41
41
  ];
42
+ /**
43
+ * PAGE 2 OF A LIST IS NOT A PAGE.
44
+ *
45
+ * `/blog/page/2` is the same design as `/blog` holding the next twenty records,
46
+ * and this platform renders a list from its own catalogue rather than from
47
+ * somebody else's pagination. Importing them spends the page budget on repeats
48
+ * of a layout already taken. Narrow on purpose: `/blog/2024` is a year archive
49
+ * and a real page, so only an explicit `page` segment counts.
50
+ */
51
+ const PAGINATION = /\/(?:page|pages|p)\/\d+(?:\/|$)/i;
52
+ /** A leading path segment that is a language tag: `/en`, `/vi`, `/en-us`, `/zh-hans`. */
53
+ const LOCALE_SEG = /^[a-z]{2}(?:-[a-z]{2,4})?$/i;
54
+ /** A path split into the language it is written in and the page it names. */
55
+ function localeOf(path) {
56
+ const segs = path.split('/').filter(Boolean);
57
+ if (segs.length > 0 && LOCALE_SEG.test(segs[0])) {
58
+ return { lang: segs[0].toLowerCase(), key: `/${segs.slice(1).join('/')}` };
59
+ }
60
+ return { lang: '', key: path };
61
+ }
62
+ /**
63
+ * ONE PAGE PER PAGE, not one per language.
64
+ *
65
+ * A multilingual site lists every translation in its sitemap, so `/about`,
66
+ * `/en/about` and `/vi/about` all arrive and all describe the same page. Import
67
+ * them and the merchant gets the same content three times under three slugs,
68
+ * with the page budget spent on translations of a page already taken — and this
69
+ * platform has a translations surface for exactly that job.
70
+ *
71
+ * FOLDED ONLY ON A CONFLICT. A rule that simply dropped every `/xx/` prefix
72
+ * would empty the plan for a site that serves ALL its pages under one — which
73
+ * nodejs.org does, everything under `/en`. So a group forms only when two URLs
74
+ * name the same page in different languages, which means the list can never
75
+ * come back shorter than the number of distinct pages.
76
+ *
77
+ * The entry's own language wins, because that is the one the caller pointed at.
78
+ */
79
+ function foldLocales(kept, entry, skip) {
80
+ const entryLang = localeOf(pathOf(entry)).lang;
81
+ const groups = new Map();
82
+ for (const f of kept) {
83
+ const { key } = localeOf(pathOf(f.url));
84
+ const g = groups.get(key);
85
+ if (g)
86
+ g.push(f);
87
+ else
88
+ groups.set(key, [f]);
89
+ }
90
+ const out = [];
91
+ for (const group of groups.values()) {
92
+ if (group.length === 1) {
93
+ out.push(group[0]);
94
+ continue;
95
+ }
96
+ const langs = group.map((f) => localeOf(pathOf(f.url)).lang);
97
+ if (new Set(langs).size === 1) {
98
+ // Same language, same key: not translations at all, so nothing to choose
99
+ // between. `/en/a` and `/en/a` cannot both be here — `seen` dropped that —
100
+ // so this is a key collision the fold has no opinion about.
101
+ out.push(...group);
102
+ continue;
103
+ }
104
+ const pick = group.find((f) => f.url === entry) ??
105
+ group.find((f) => localeOf(pathOf(f.url)).lang === entryLang) ??
106
+ group.find((f) => localeOf(pathOf(f.url)).lang === '') ??
107
+ group[0];
108
+ for (const f of group)
109
+ if (f !== pick)
110
+ skip('other-locale');
111
+ out.push(pick);
112
+ }
113
+ return out;
114
+ }
42
115
  /**
43
116
  * Is this path the site's plumbing rather than one of its pages?
44
117
  *
@@ -167,6 +240,75 @@ export function robotsSitemaps(txt) {
167
240
  }
168
241
  return out;
169
242
  }
243
+ /**
244
+ * The `User-agent: *` group's rules.
245
+ *
246
+ * HONOURED, not read for interest. A merchant importing their own site is one
247
+ * caller; the other points this at somebody else's server, and a tool that
248
+ * fetches a dozen pages should obey the file that exists to say which. It is
249
+ * also an accuracy win on its own terms — what a site disallows is almost always
250
+ * its plumbing, and the plumbing list here is a guess where robots.txt is the
251
+ * site's own answer.
252
+ *
253
+ * `Allow` is collected too, because the standard resolves a conflict by the
254
+ * LONGEST match and a site that disallows `/blog/` and allows `/blog/public/`
255
+ * means the second.
256
+ */
257
+ export function robotsRules(txt) {
258
+ const allow = [];
259
+ const disallow = [];
260
+ let applies = false;
261
+ for (const line of txt.split(/\r?\n/)) {
262
+ const clean = line.replace(/#.*$/, '').trim();
263
+ if (!clean)
264
+ continue;
265
+ const m = /^([A-Za-z-]+)\s*:\s*(.*)$/.exec(clean);
266
+ if (!m)
267
+ continue;
268
+ const field = m[1].toLowerCase();
269
+ const value = m[2].trim();
270
+ if (field === 'user-agent') {
271
+ applies = value === '*';
272
+ continue;
273
+ }
274
+ if (!applies || !value)
275
+ continue;
276
+ // AN EMPTY `Disallow:` MEANS ALLOW EVERYTHING, and treating it as the empty
277
+ // prefix would block every path on the site — the one parsing mistake in
278
+ // this file that turns a polite crawler into a crawler that finds nothing.
279
+ if (field === 'disallow')
280
+ disallow.push(value);
281
+ else if (field === 'allow')
282
+ allow.push(value);
283
+ }
284
+ return { allow, disallow };
285
+ }
286
+ /** Does the site ask a general crawler to leave this path alone? */
287
+ export function blockedByRobots(path, rules) {
288
+ const match = (rule) => {
289
+ // `*` and `$` are the two wildcards every major crawler honours. Anything
290
+ // else in a rule is a literal prefix.
291
+ const pattern = rule
292
+ .replace(/[.+?^${}()|[\]\\]/g, '\\$&')
293
+ .replace(/\*/g, '.*')
294
+ .replace(/\\\$$/, '$');
295
+ let re;
296
+ try {
297
+ re = new RegExp(`^${pattern}`);
298
+ }
299
+ catch {
300
+ return path.startsWith(rule) ? rule.length : -1;
301
+ }
302
+ return re.test(path) ? rule.length : -1;
303
+ };
304
+ const deny = Math.max(-1, ...rules.disallow.map(match));
305
+ if (deny < 0)
306
+ return false;
307
+ const permit = Math.max(-1, ...rules.allow.map(match));
308
+ // LONGEST MATCH WINS, and a tie goes to the crawler — that is what the
309
+ // standard says and what every implementation does.
310
+ return deny > permit;
311
+ }
170
312
  /** A slug this platform will accept, from a path. The root is the home page. */
171
313
  export function slugFor(url, taken) {
172
314
  const path = pathOf(url);
@@ -271,6 +413,16 @@ export function choosePages(entry, urls, opts = {}) {
271
413
  skip('not-content');
272
414
  continue;
273
415
  }
416
+ if (PAGINATION.test(path)) {
417
+ skip('pagination');
418
+ continue;
419
+ }
420
+ // THE ENTRY IS EXEMPT. The caller typed that URL, and a merchant whose own
421
+ // robots.txt disallows their own home page has still asked for it.
422
+ if (opts.robots && norm !== entry && blockedByRobots(path, opts.robots)) {
423
+ skip('robots-disallow');
424
+ continue;
425
+ }
274
426
  }
275
427
  if (new URL(norm).search) {
276
428
  // A query string is nearly always a filter, a sort or a page number over
@@ -291,10 +443,11 @@ export function choosePages(entry, urls, opts = {}) {
291
443
  seen.add(norm);
292
444
  kept.push({ url: norm, from: norm === entry ? 'entry' : f.from });
293
445
  }
446
+ const folded = foldLocales(kept, entry, skip);
294
447
  // GROUPS ARE COUNTED BEFORE THE CAP, because the whole point of reporting them
295
448
  // is to say what the cap is about to hide.
296
449
  const groups = {};
297
- for (const f of kept) {
450
+ for (const f of folded) {
298
451
  const seg = pathOf(f.url).split('/').filter(Boolean)[0];
299
452
  if (seg)
300
453
  groups[seg] = (groups[seg] ?? 0) + 1;
@@ -303,7 +456,7 @@ export function choosePages(entry, urls, opts = {}) {
303
456
  if (groups[k] < 3)
304
457
  delete groups[k];
305
458
  const depthOf = (u) => pathOf(u).split('/').filter(Boolean).length;
306
- kept.sort((a, b) => {
459
+ folded.sort((a, b) => {
307
460
  if (a.url === entry)
308
461
  return -1;
309
462
  if (b.url === entry)
@@ -311,11 +464,11 @@ export function choosePages(entry, urls, opts = {}) {
311
464
  const d = depthOf(a.url) - depthOf(b.url);
312
465
  return d !== 0 ? d : a.url.localeCompare(b.url);
313
466
  });
314
- const over = Math.max(0, kept.length - maxPages);
467
+ const over = Math.max(0, folded.length - maxPages);
315
468
  if (over > 0)
316
469
  skipped['over-page-limit'] = over;
317
470
  const taken = new Set();
318
- const pages = kept.slice(0, maxPages).map((f) => {
471
+ const pages = folded.slice(0, maxPages).map((f) => {
319
472
  const slug = slugFor(f.url, taken);
320
473
  return { ...f, slug, name: nameFor(slug), depth: depthOf(f.url) };
321
474
  });
@@ -342,6 +495,8 @@ export function canonFor(entry) {
342
495
  return null;
343
496
  if (isPlumbing(path))
344
497
  return null;
498
+ if (PAGINATION.test(path))
499
+ return null;
345
500
  return norm;
346
501
  };
347
502
  }
@@ -1,5 +1,6 @@
1
1
  import { walk } from '../../core/tree.js';
2
2
  import { stickySeeds } from './sticky.js';
3
+ import { ICON_NAMES } from '../../catalog/icons.generated.js';
3
4
  /** Style keys read off a node, ignoring anything unset. */
4
5
  function styleOf(n) {
5
6
  return (n?.style ?? {});
@@ -56,6 +57,41 @@ function headingTag(level) {
56
57
  const n = Math.min(6, Math.max(1, Math.round(level ?? 2)));
57
58
  return `h${n}`;
58
59
  }
60
+ /**
61
+ * This platform's own spelling of an icon the source page named, or null.
62
+ *
63
+ * A LOOKUP, NOT A GUESS. `specials.name` is a PascalCase RemixIcon id and the
64
+ * catalog holds 3,227 of them; a name outside that set renders nothing. So the
65
+ * words the page used are normalised and TRIED — exactly, then as the `Line`
66
+ * and `Fill` variants every icon in the set comes in — and anything that does
67
+ * not land is skipped.
68
+ *
69
+ * Deliberately no last-word fallback. "Open main menu" resolving to `MenuLine`
70
+ * would be right, and "Acme Store" resolving to `StoreLine` would put a shop
71
+ * glyph where a wordmark was — a WRONG icon, which is worse than none, and
72
+ * indistinguishable from a right one to everything downstream.
73
+ */
74
+ function iconFor(raw) {
75
+ if (raw.length > 40)
76
+ return null;
77
+ const words = raw
78
+ .replace(/^(?:ri|fa[srlbd]?|lucide|bi|feather|icon|icons|material|mdi|ion|hero)[-_]/i, '')
79
+ .split(/[^A-Za-z0-9]+/)
80
+ .filter(Boolean);
81
+ if (!words.length)
82
+ return null;
83
+ const base = words
84
+ .map((w) => w.charAt(0).toUpperCase() + w.slice(1))
85
+ .join('');
86
+ for (const candidate of [base, `${base}Line`, `${base}Fill`]) {
87
+ if (ICON_NAMES.has(candidate))
88
+ return candidate;
89
+ }
90
+ // A source that already writes the id lowercase — `ri-shopping-cart-line`
91
+ // arrives as words, but `shoppingcartline` as one — cannot be split back, so
92
+ // the exact form is all there is to try.
93
+ return null;
94
+ }
59
95
  /**
60
96
  * One captured node as the element that can render it.
61
97
  *
@@ -94,6 +130,81 @@ function one(c, t) {
94
130
  },
95
131
  };
96
132
  }
133
+ case 'icon': {
134
+ const name = c.name ? iconFor(c.name) : null;
135
+ // NO NAME, NO NODE. An `icon` whose `specials.name` this platform does not
136
+ // hold renders nothing, so the page carries an empty box where the source
137
+ // had a glyph — worse than the `<svg>` being skipped, because nobody put
138
+ // it there on purpose. The COLOUR is deliberately not set either: it lives
139
+ // in the `icon-default` style preset, and a literal here would detach
140
+ // every imported icon from the theme permanently.
141
+ if (!name)
142
+ return null;
143
+ return { type: 'icon', specials: { name } };
144
+ }
145
+ case 'accordion': {
146
+ const items = (c.children ?? [])
147
+ .map((item) => one(item, t))
148
+ .filter((n) => n !== null);
149
+ if (!items.length)
150
+ return null;
151
+ return { type: 'accordion', children: items };
152
+ }
153
+ case 'accordion-item': {
154
+ const label = c.text?.trim();
155
+ const body = (c.children ?? [])
156
+ .map((k) => one(k, t))
157
+ .filter((n) => n !== null);
158
+ if (!label && !body.length)
159
+ return null;
160
+ // The label is omitted rather than defaulted when the source had no
161
+ // `<summary>`: `accordion-content` seeds its own, and inventing one here
162
+ // would ship English copy into a store that is not in English.
163
+ return {
164
+ type: 'accordion-content',
165
+ ...(label ? { specials: { label } } : {}),
166
+ children: body,
167
+ };
168
+ }
169
+ case 'divider': {
170
+ return { type: 'divider', style: { width: '100%' } };
171
+ }
172
+ case 'video': {
173
+ if (!c.src)
174
+ return null;
175
+ // NO STYLE OF OUR OWN. Every media element here already seeds
176
+ // `width: 100%` + `height: fit-content` and `google-map` seeds a height
177
+ // per breakpoint; writing a literal over that detaches the node from the
178
+ // element's own responsive answer to be less correct than it.
179
+ //
180
+ // `videoRatio` is seeded 16 / 9, which is right for almost every file a
181
+ // page embeds — and markup that does not state the real ratio is not
182
+ // something to guess a crop from.
183
+ return {
184
+ type: 'video',
185
+ specials: { videoSrc: c.src, ...(c.poster ? { poster: c.poster } : {}) },
186
+ };
187
+ }
188
+ case 'embed': {
189
+ // AN EMBED IS A PROVIDER, NOT A URL. `youtube` and `vimeo` store the ID
190
+ // alone — handing them a whole watch URL renders nothing — while
191
+ // `google-map` takes the embed URL its own hint asks for and `soundcloud`
192
+ // takes a track URL under a different key again.
193
+ if (c.provider === 'youtube' || c.provider === 'vimeo') {
194
+ if (!c.videoId)
195
+ return null;
196
+ return { type: c.provider, specials: { videoId: c.videoId } };
197
+ }
198
+ if (!c.src)
199
+ return null;
200
+ if (c.provider === 'map') {
201
+ return { type: 'google-map', specials: { src: c.src, mapType: 'location' } };
202
+ }
203
+ if (c.provider === 'soundcloud') {
204
+ return { type: 'soundcloud', specials: { trackUrl: c.src } };
205
+ }
206
+ return null;
207
+ }
97
208
  case 'image': {
98
209
  if (!c.src)
99
210
  return null;
@@ -5,7 +5,7 @@ import { uploadMedia } from '../transport/media.js';
5
5
  import { addSubtree } from '../domains/site/builder.js';
6
6
  import { middleEnd } from '../domains/site/traps.js';
7
7
  import { toSpecs, tokensFromPage, imageSources, rehostImages, } from '../domains/site/importmap.js';
8
- import { canonFor, choosePages, normalizeUrl, robotsSitemaps, sitemapUrls, } from '../domains/site/discover.js';
8
+ import { canonFor, choosePages, normalizeUrl, robotsRules, robotsSitemaps, sitemapUrls, } from '../domains/site/discover.js';
9
9
  import { loadSource } from '../transport/pages.js';
10
10
  import { PageDoc } from '../domains/site/document.js';
11
11
  import { request } from '../transport/http.js';
@@ -48,17 +48,9 @@ async function fetchForeign(ctx, url) {
48
48
  * One fetch and no browser, and it lists pages nothing links to — which is why
49
49
  * it is tried before the crawl rather than after it.
50
50
  */
51
- export async function fromSitemap(ctx, entry) {
51
+ export async function fromSitemap(ctx, entry, declaredIn) {
52
52
  const origin = new URL(entry).origin;
53
- const declared = new Set();
54
- // ROBOTS.TXT IS WHERE A SITE SAYS WHERE ITS SITEMAP REALLY IS, and plenty of
55
- // real ones are not at /sitemap.xml — a shop platform names
56
- // /sitemap_products_1.xml, a CMS a dated path. Guessing only the default is how
57
- // a site with a perfectly good sitemap gets crawled instead.
58
- const robots = await fetchForeign(ctx, `${origin}/robots.txt`);
59
- if (robots)
60
- for (const u of robotsSitemaps(robots))
61
- declared.add(u);
53
+ const declared = new Set(declaredIn);
62
54
  for (const guess of ['/sitemap.xml', '/sitemap_index.xml', '/sitemap-index.xml']) {
63
55
  declared.add(`${origin}${guess}`);
64
56
  }
@@ -132,20 +124,37 @@ async function existingPages(ctx, siteId) {
132
124
  * `include` or `max_pages`, not a slower way to find the same list.
133
125
  */
134
126
  async function discoverSite(ctx, entry, opts) {
135
- const listed = await fromSitemap(ctx, entry);
127
+ // ROBOTS.TXT ANSWERS TWO QUESTIONS AND IS FETCHED ONCE. Where the sitemap
128
+ // really is — plenty are not at /sitemap.xml, a shop platform names
129
+ // /sitemap_products_1.xml and a CMS a dated path — and which paths a general
130
+ // crawler is asked to leave alone.
131
+ const origin = new URL(entry).origin;
132
+ const txt = await fetchForeign(ctx, `${origin}/robots.txt`);
133
+ const robots = txt ? robotsRules(txt) : undefined;
134
+ const listed = await fromSitemap(ctx, entry, txt ? robotsSitemaps(txt) : []);
136
135
  if (listed)
137
- return { source: 'sitemap', urls: listed, titles: new Map(), visited: 0 };
136
+ return { source: 'sitemap', urls: listed, titles: new Map(), visited: 0, robots, aliases: 0 };
138
137
  const crawled = await crawlLinks(entry, {
139
138
  depth: opts.depth ?? 1,
140
139
  maxVisits: opts.maxVisits ?? 24,
141
140
  canon: canonFor(entry),
142
141
  });
143
- return {
144
- source: 'links',
145
- urls: crawled.urls.map((u) => ({ url: u, from: u === entry ? 'entry' : 'links' })),
146
- titles: crawled.titles,
147
- visited: crawled.visited,
148
- };
142
+ // A PAGE THAT NAMES ANOTHER ADDRESS AS ITS OWN IS THAT PAGE. Folded here, on
143
+ // the crawl path, where the answer is already in hand — the sitemap path has
144
+ // no canonical until the page is opened, and the import pass folds that one.
145
+ const seen = new Set();
146
+ const urls = [];
147
+ let aliases = 0;
148
+ for (const u of crawled.urls) {
149
+ const real = crawled.canonical.get(u) ?? u;
150
+ if (real !== u)
151
+ aliases += 1;
152
+ if (seen.has(real))
153
+ continue;
154
+ seen.add(real);
155
+ urls.push({ url: real, from: real === entry ? 'entry' : 'links' });
156
+ }
157
+ return { source: 'links', urls, titles: crawled.titles, visited: crawled.visited, robots, aliases };
149
158
  }
150
159
  export function registerImportTools(server, ctx, session) {
151
160
  server.registerTool('sb_import', {
@@ -321,6 +330,7 @@ export function registerImportTools(server, ctx, session) {
321
330
  maxPages: max_pages,
322
331
  include,
323
332
  exclude,
333
+ robots: found.robots,
324
334
  });
325
335
  // A CRAWL ALREADY READ THE TITLE. `nameFor` derives a name from the slug
326
336
  // because a sitemap offers nothing else, but the link crawl opened every
@@ -394,7 +404,9 @@ export function registerImportTools(server, ctx, session) {
394
404
  discovered_by: found.source,
395
405
  ...(found.visited ? { pages_read_to_find_them: found.visited } : {}),
396
406
  pages: plan.pages.map((p) => ({ url: p.url, slug: p.slug, name: p.name, ...lands(p) })),
397
- ...(Object.keys(plan.skipped).length ? { skipped: plan.skipped } : {}),
407
+ ...(Object.keys(plan.skipped).length || found.aliases
408
+ ? { skipped: { ...plan.skipped, ...(found.aliases ? { 'canonical-alias': found.aliases } : {}) } }
409
+ : {}),
398
410
  ...(unlistable
399
411
  ? {
400
412
  landing_unknown: `This site's own pages could not be read (${unlistable}), so which of the above ` +
@@ -467,6 +479,13 @@ export function registerImportTools(server, ctx, session) {
467
479
  // three pages built, nine not, and no report saying which.
468
480
  const built = [];
469
481
  const failed = [];
482
+ // WHAT EACH PAGE SAYS ITS OWN ADDRESS IS. A sitemap cannot tell you that
483
+ // two of its entries are one page — only the page can, and only once it is
484
+ // open. Measured: modelcontextprotocol.io's home page declares a dated
485
+ // docs path as its canonical, so `/` and that path are the same content
486
+ // under two slugs, and nothing in the plan looks wrong.
487
+ const identities = new Set();
488
+ const aliased = [];
470
489
  let lastOpened = '';
471
490
  for (const p of plan.pages) {
472
491
  const shot = byUrl.get(p.url);
@@ -474,6 +493,12 @@ export function registerImportTools(server, ctx, session) {
474
493
  failed.push({ url: p.url, why: shot ? shot.why : 'was not read' });
475
494
  continue;
476
495
  }
496
+ const identity = normalizeUrl(shot.result.canonical ?? p.url) ?? p.url;
497
+ if (identities.has(identity)) {
498
+ aliased.push({ url: p.url, same_as: identity });
499
+ continue;
500
+ }
501
+ identities.add(identity);
477
502
  try {
478
503
  const sections = rehosted.size > 0 ? rehostImages(shot.result.sections, rehosted) : shot.result.sections;
479
504
  const specs = toSpecs(sections, tokens);
@@ -557,7 +582,10 @@ export function registerImportTools(server, ctx, session) {
557
582
  discovered_by: found.source,
558
583
  built,
559
584
  ...(failed.length ? { failed } : {}),
560
- ...(Object.keys(plan.skipped).length ? { skipped: plan.skipped } : {}),
585
+ ...(aliased.length ? { same_page: aliased } : {}),
586
+ ...(Object.keys(plan.skipped).length || found.aliases
587
+ ? { skipped: { ...plan.skipped, ...(found.aliases ? { 'canonical-alias': found.aliases } : {}) } }
588
+ : {}),
561
589
  images: {
562
590
  copied: rehosted.size,
563
591
  ...(failedImages.length ? { failed: failedImages } : {}),
@@ -63,10 +63,31 @@ function capturePage(limits) {
63
63
  const bordered = cs.borderStyle !== '' && cs.borderStyle !== 'none' && parseFloat(cs.borderWidth || '0') > 0;
64
64
  return cs.display !== 'inline' && (filled || bordered);
65
65
  };
66
+ // IFRAME IS NOT HERE ANY MORE. It was, and it took every embedded video, every
67
+ // map and every audio player with it — silently, as a skip count. The platform
68
+ // has `video`, `youtube`, `vimeo`, `soundcloud` and `google-map`; a hero video
69
+ // and a contact page's map are ordinary things to import, and they were the
70
+ // one kind of content that could not survive the trip at all.
66
71
  const IGNORE = new Set([
67
- 'SCRIPT', 'STYLE', 'NOSCRIPT', 'TEMPLATE', 'SVG', 'PATH', 'IFRAME', 'CANVAS',
72
+ 'SCRIPT', 'STYLE', 'NOSCRIPT', 'TEMPLATE', 'SVG', 'PATH', 'CANVAS',
68
73
  'NAV', 'FORM', 'INPUT', 'SELECT', 'TEXTAREA', 'BUTTON',
69
74
  ]);
75
+ /** The provider and id behind an embed URL, or null if this platform has no element for it. */
76
+ const embedOf = (raw) => {
77
+ const u = raw.split('?')[0];
78
+ let m = /(?:youtube(?:-nocookie)?\.com\/(?:embed|v|shorts)\/|youtu\.be\/)([A-Za-z0-9_-]{6,})/.exec(u);
79
+ if (m)
80
+ return { provider: 'youtube', videoId: m[1] };
81
+ m = /player\.vimeo\.com\/video\/(\d+)/.exec(u);
82
+ if (m)
83
+ return { provider: 'vimeo', videoId: m[1] };
84
+ if (/(?:google\.[a-z.]+|maps\.google\.[a-z.]+)\/maps?\/embed/.test(u)) {
85
+ return { provider: 'map', src: raw };
86
+ }
87
+ if (/w\.soundcloud\.com\/player/.test(u))
88
+ return { provider: 'soundcloud', src: raw };
89
+ return null;
90
+ };
70
91
  /**
71
92
  * The renderable content under one section, AS A TREE.
72
93
  *
@@ -83,6 +104,121 @@ function capturePage(limits) {
83
104
  * merely wraps is not a design decision and reproducing it would nest the
84
105
  * result ten deep for nothing.
85
106
  */
107
+ // THE SOURCE'S OWN HEADER AND FOOTER ARE NEVER WANTED. The target site has its
108
+ // own, as shared globals, and importing somebody else's navigation onto a
109
+ // storefront is a second menu pointing at a different website.
110
+ //
111
+ // NOT NAMED `chrome`: that is a BROWSER GLOBAL (the extension API), so a
112
+ // `const chrome` declared in a nested scope left every OTHER scope resolving
113
+ // the name to `window.chrome` — `chrome.has is not a function`, thrown inside
114
+ // `evaluate`, which kills the whole capture. The same shape as the closure
115
+ // trap this file already carries, reached from the opposite direction.
116
+ //
117
+ // PAGE-LEVEL, BY THE SPEC'S OWN DEFINITION rather than by depth. This used to
118
+ // ask whether the element was a DIRECT child of `<body>`, which almost no real
119
+ // site satisfies — one wrapper div is enough — so blender.org's footer came
120
+ // through as eleven sections of link columns and the page a merchant asked for
121
+ // was its site map. `<header>` and `<footer>` belong to their nearest
122
+ // SECTIONING ancestor (article, aside, nav, section), so one with none of
123
+ // those above it is the page's, however deeply it is wrapped; one inside an
124
+ // `<article>` is that article's byline, and one inside a `<section>` is the
125
+ // hero the old comment was right to protect. `<main>` is not sectioning
126
+ // content, so it does not shield a footer.
127
+ // A COLLAPSED `<details>` MEASURES AS ZERO, so its body would read as hidden and
128
+ // an FAQ would import as a list of questions with no answers. The source's
129
+ // collapsed state is not content — this platform's accordion has its own
130
+ // `openItems` — so every one is opened before anything is measured. The page is
131
+ // a throwaway tab that is closed straight after.
132
+ for (const d of Array.from(document.querySelectorAll('details')))
133
+ d.setAttribute('open', '');
134
+ /**
135
+ * What the page CALLS this icon, in its own words. A candidate, never a verdict.
136
+ *
137
+ * Four places a real page says it, in falling order of how much it means:
138
+ * a sprite reference names the icon outright; an `aria-label` or `<title>` is
139
+ * what a screen reader is told; a class is the icon set's own id. The mapper
140
+ * decides whether this platform has one by that name — nothing in the page can
141
+ * answer that.
142
+ */
143
+ const iconName = (el) => {
144
+ const use = Array.from(el.querySelectorAll('use'))[0];
145
+ const ref = use ? use.getAttribute('href') ?? use.getAttribute('xlink:href') : null;
146
+ if (ref && ref.charAt(0) === '#' && ref.length > 1)
147
+ return ref.slice(1);
148
+ const label = clean(el.getAttribute('aria-label'));
149
+ if (label)
150
+ return label;
151
+ const title = Array.from(el.querySelectorAll('title'))[0];
152
+ const titled = title ? clean(title.textContent) : '';
153
+ if (titled)
154
+ return titled;
155
+ const dataIcon = clean(el.getAttribute('data-icon'));
156
+ if (dataIcon)
157
+ return dataIcon;
158
+ // A class list holds the icon set's id and a pile of layout classes with it.
159
+ // The longest hyphenated token is the id in every set seen here — `ri-…`,
160
+ // `fa-…`, `lucide-…`, `bi-…` — and a bare `w-4` cannot outrank it.
161
+ // `getAttribute('class')`, NOT `className`: on an SVG element the property is
162
+ // an SVGAnimatedString, so `String(el.className)` is the literal
163
+ // "[object SVGAnimatedString]" and every class-named icon went unrecognised.
164
+ let best = '';
165
+ for (const w of (el.getAttribute('class') ?? '').split(/\s+/)) {
166
+ if (w.indexOf('-') > 0 && w.length > best.length)
167
+ best = w;
168
+ }
169
+ return best;
170
+ };
171
+ const sectioning = Array.from(document.querySelectorAll('article, section, aside, nav'));
172
+ // A CLASS NAME IS EVIDENCE FOR A FOOTER AND NOT FOR A HEADER, and the
173
+ // asymmetry is the whole point. blender.org marks its site map
174
+ // `<div class="footer-navigation">` — no `<footer>` tag anywhere near it — so
175
+ // the spec rule alone let eleven sections of somebody else's links through as
176
+ // the page a merchant asked for. The same trick on the header side would eat
177
+ // HEROES: blender's own first band is `<div class="hero header-size-large">`,
178
+ // and losing the first thing on a landing page costs more than a stray footer.
179
+ // A header is caught by its tag or its ARIA role, and its links are `<nav>`,
180
+ // which is ignored already.
181
+ //
182
+ // The token must START with `footer` (footer, footer-note, footer__inner) so
183
+ // `card-footer` inside an ordinary div is not swept up with it.
184
+ const footerish = (el) => {
185
+ const words = `${el.getAttribute('id') ?? ''} ${el.getAttribute('class') ?? ''}`.toLowerCase();
186
+ for (const w of words.split(/[\s]+/)) {
187
+ if (!w)
188
+ continue;
189
+ if (w === 'colophon' || w === 'site-footer' || w === 'page-footer')
190
+ return true;
191
+ if (w === 'footer' || w.indexOf('footer-') === 0 || w.indexOf('footer_') === 0)
192
+ return true;
193
+ }
194
+ return false;
195
+ };
196
+ const pageChromeRoots = [];
197
+ const chromeCandidates = Array.from(document.querySelectorAll('header, footer, [role="banner"], [role="contentinfo"], [class*="footer"], [id*="footer"]'));
198
+ for (const el of chromeCandidates) {
199
+ const role = el.getAttribute('role');
200
+ const isChrome = el.tagName === 'HEADER' ||
201
+ el.tagName === 'FOOTER' ||
202
+ role === 'banner' ||
203
+ role === 'contentinfo' ||
204
+ footerish(el);
205
+ if (!isChrome)
206
+ continue;
207
+ if (sectioning.some((sec) => sec !== el && sec.contains(el)))
208
+ continue;
209
+ // A NESTED ONE ADDS NOTHING: the outer root already covers it, and keeping
210
+ // both makes the containment test scan the same subtree twice.
211
+ if (pageChromeRoots.some((c) => c.contains(el)))
212
+ continue;
213
+ pageChromeRoots.push(el);
214
+ }
215
+ // INSIDE the chrome, not equal to it. A real footer holds `<section>`s, and
216
+ // the candidate walk deliberately takes the INNERMOST sections — so on
217
+ // blender.org the candidates were the footer's own link columns, none of which
218
+ // IS the footer, and eleven sections of somebody else's site map came through
219
+ // as the page. Asking about containment is the same question the candidate
220
+ // list already answers for nesting.
221
+ const inPageChrome = (el) => pageChromeRoots.some((c) => c === el || c.contains(el));
86
222
  const leaves = (root) => {
87
223
  const walkChildren = (el) => {
88
224
  const kids = [];
@@ -90,7 +226,20 @@ function capturePage(limits) {
90
226
  for (const c of walk(child))
91
227
  kids.push(c);
92
228
  }
93
- return kids;
229
+ // CONSECUTIVE `<details>` ARE ONE ACCORDION. An FAQ is written as eight
230
+ // siblings, and eight separate accordions is eight containers where the
231
+ // author had one list — the same content, with seven wrappers nobody asked
232
+ // for and no shared open/close behaviour.
233
+ const merged = [];
234
+ for (const c of kids) {
235
+ const last = merged[merged.length - 1];
236
+ if (c.kind === 'accordion' && last && last.kind === 'accordion') {
237
+ last.children = (last.children ?? []).concat(c.children ?? []);
238
+ continue;
239
+ }
240
+ merged.push(c);
241
+ }
242
+ return merged;
94
243
  };
95
244
  const walk = (el) => {
96
245
  // ONE BOUND, on the whole import. There used to be a second, per section,
@@ -104,7 +253,48 @@ function capturePage(limits) {
104
253
  skip('over-node-limit');
105
254
  return [];
106
255
  }
107
- const tag = el.tagName;
256
+ if (inPageChrome(el)) {
257
+ skip('page-chrome');
258
+ return [];
259
+ }
260
+ // UPPERCASED, because an SVG element's `tagName` is not.
261
+ //
262
+ // `tagName` preserves case for XML-namespaced elements, so an inline
263
+ // `<svg>` reports "svg" while every HTML element reports "P", "DIV". The
264
+ // ignore list had held 'SVG' and 'PATH' since it was written and neither
265
+ // had ever matched — the icons were falling through to the text fallback,
266
+ // contributing nothing because an svg's textContent is empty, which is why
267
+ // it looked like the entry was working.
268
+ const tag = el.tagName.toUpperCase();
269
+ // AN ICON IS DECORATION, WHICH IS THE ONE THING `aria-hidden` MARKS AND
270
+ // THIS PLATFORM HAS AN ELEMENT FOR. Almost every real icon carries the
271
+ // attribute — that is correct authoring, the label beside it does the
272
+ // talking — so testing it before this branch would mean the `icon` element
273
+ // could never be reached from a real page. The attribute still governs
274
+ // everything else, and an aria-hidden CONTAINER is skipped before the walk
275
+ // ever descends to the svg inside it.
276
+ if (tag === 'SVG') {
277
+ const name = iconName(el);
278
+ if (!name) {
279
+ skip('svg');
280
+ return [];
281
+ }
282
+ taken.nodes++;
283
+ return [{ kind: 'icon', name }];
284
+ }
285
+ // WHAT THE PAGE ITSELF SAYS IS NOT CONTENT.
286
+ //
287
+ // `aria-hidden="true"` is the author's own mark for decoration and for
288
+ // duplicates — a carousel's cloned slides, the mobile copy of a menu that
289
+ // the desktop layout also carries, an icon that repeats the label beside
290
+ // it. Measured on real pages before this: 53 such elements on one, 15 on
291
+ // another, every one of them walked and some of them captured twice.
292
+ // Nothing here reads the accessibility tree, so this attribute is the only
293
+ // place that answer exists.
294
+ if (el.getAttribute('aria-hidden') === 'true') {
295
+ skip('aria-hidden');
296
+ return [];
297
+ }
108
298
  if (IGNORE.has(tag)) {
109
299
  skip(tag.toLowerCase());
110
300
  return [];
@@ -113,6 +303,58 @@ function capturePage(limits) {
113
303
  skip('hidden');
114
304
  return [];
115
305
  }
306
+ if (tag === 'DETAILS') {
307
+ const kids2 = Array.from(el.children);
308
+ const summary = kids2.filter((c) => c.tagName === 'SUMMARY')[0];
309
+ const label = summary ? clean(summary.textContent) : '';
310
+ const body = [];
311
+ for (const child of kids2) {
312
+ if (child.tagName === 'SUMMARY')
313
+ continue;
314
+ for (const c of walk(child))
315
+ body.push(c);
316
+ }
317
+ if (!label && body.length === 0) {
318
+ skip('empty-details');
319
+ return [];
320
+ }
321
+ taken.nodes++;
322
+ return [{ kind: 'accordion', children: [{ kind: 'accordion-item', text: label, children: body }] }];
323
+ }
324
+ // A RULE BETWEEN SECTIONS IS A DESIGN DECISION, and it is one node.
325
+ if (tag === 'HR') {
326
+ taken.nodes++;
327
+ return [{ kind: 'divider' }];
328
+ }
329
+ if (tag === 'VIDEO') {
330
+ const direct = el.getAttribute('src');
331
+ const source = Array.from(el.querySelectorAll('source'))[0];
332
+ const src = direct || (source ? source.getAttribute('src') : null);
333
+ if (!src) {
334
+ skip('video-without-src');
335
+ return [];
336
+ }
337
+ taken.nodes++;
338
+ const poster = el.getAttribute('poster');
339
+ return [{
340
+ kind: 'video',
341
+ src: abs(src),
342
+ ...(poster ? { poster: abs(poster) } : {}),
343
+ }];
344
+ }
345
+ if (tag === 'IFRAME') {
346
+ const src = el.getAttribute('src');
347
+ const embed = src ? embedOf(abs(src)) : null;
348
+ if (!embed) {
349
+ // An advert, a tracking pixel, a chat widget, a comment system: real
350
+ // pages carry several, and this platform has an element for none of
351
+ // them. Counted rather than guessed at.
352
+ skip('iframe');
353
+ return [];
354
+ }
355
+ taken.nodes++;
356
+ return [{ kind: 'embed', ...embed }];
357
+ }
116
358
  if (HEADINGS.has(tag)) {
117
359
  const text = clean(el.textContent);
118
360
  if (!text)
@@ -175,9 +417,39 @@ function capturePage(limits) {
175
417
  return [];
176
418
  }
177
419
  if (tag === 'UL' || tag === 'OL') {
178
- const items = Array.from(el.querySelectorAll('li'))
179
- .map((li) => clean(li.textContent))
180
- .filter(Boolean);
420
+ // A NESTED LIST WAS TAKEN TWICE, and the duplication reads as a page
421
+ // that stutters. `querySelectorAll('li')` returns the nested items as
422
+ // well as the outer ones, and an outer item's `textContent` ALREADY
423
+ // contains its sublist — so every nested entry arrived once inside its
424
+ // parent's line and once again on its own. Measured on a real import:
425
+ // one section of blender.org repeated five sublists that way.
426
+ //
427
+ // Walked by DIRECT children instead, with each item's own words
428
+ // separated from its sublist's and the sublist flattened after it. This
429
+ // platform's `list` is flat, so flattening is the honest translation —
430
+ // and the order a reader sees is preserved.
431
+ const items = [];
432
+ const collect = (list) => {
433
+ for (const li of Array.from(list.children)) {
434
+ if (li.tagName !== 'LI')
435
+ continue;
436
+ const sublists = Array.from(li.children).filter((c) => c.tagName === 'UL' || c.tagName === 'OL');
437
+ let text = clean(li.textContent);
438
+ for (const sub of sublists) {
439
+ const inner = clean(sub.textContent);
440
+ // textContent runs in document order, so a sublist's words are the
441
+ // tail of its parent's. Only strip what is actually there.
442
+ if (inner && text.length > inner.length && text.slice(-inner.length) === inner) {
443
+ text = clean(text.slice(0, text.length - inner.length));
444
+ }
445
+ }
446
+ if (text)
447
+ items.push(text);
448
+ for (const sub of sublists)
449
+ collect(sub);
450
+ }
451
+ };
452
+ collect(el);
181
453
  if (!items.length)
182
454
  return [];
183
455
  taken.nodes++;
@@ -286,20 +558,10 @@ function capturePage(limits) {
286
558
  // document is one candidate too, and keeping THAT would reduce every page to a
287
559
  // single band. The finest ones are the page's actual bands.
288
560
  candidates = candidates.filter((el) => !candidates.some((o) => o !== el && el.contains(o)));
289
- // THE SOURCE'S OWN HEADER AND FOOTER ARE NEVER WANTED. The target site has its
290
- // own, as shared globals, and importing somebody else's navigation onto a
291
- // storefront is a second menu pointing at a different website. Only the
292
- // PAGE-LEVEL ones are dropped — a `<header>` inside a section is a hero, and
293
- // excluding those would lose the first thing on most landing pages.
294
- const chrome = new Set();
295
- for (const el of Array.from(document.body.children)) {
296
- if (el.tagName === 'HEADER' || el.tagName === 'FOOTER')
297
- chrome.add(el);
298
- }
299
561
  const build = (from) => {
300
562
  const acc = [];
301
563
  for (const el of from) {
302
- if (chrome.has(el)) {
564
+ if (inPageChrome(el)) {
303
565
  skip('page-chrome');
304
566
  continue;
305
567
  }
@@ -340,7 +602,15 @@ function capturePage(limits) {
340
602
  const main = document.querySelectorAll('main')[0] ?? document.body;
341
603
  sections = build(Array.from(main.children));
342
604
  }
343
- return { url: here, title: clean(document.title), sections, skipped };
605
+ const link = Array.from(document.querySelectorAll('link[rel="canonical"]'))[0];
606
+ const canonical = link ? (link.getAttribute('href') ?? '') : '';
607
+ return {
608
+ url: here,
609
+ title: clean(document.title),
610
+ ...(canonical ? { canonical: abs(canonical) } : {}),
611
+ sections,
612
+ skipped,
613
+ };
344
614
  }
345
615
  /**
346
616
  * ONE BROWSER FOR THE WHOLE CALL.
@@ -462,7 +732,20 @@ function linksOnPage() {
462
732
  // One unresolvable href must not kill the crawl.
463
733
  }
464
734
  }
465
- return { title: document.title, links };
735
+ const link = Array.from(document.querySelectorAll('link[rel="canonical"]'))[0];
736
+ let canonical = '';
737
+ if (link) {
738
+ const href = link.getAttribute('href');
739
+ if (href) {
740
+ try {
741
+ canonical = new URL(href, here).href;
742
+ }
743
+ catch {
744
+ canonical = '';
745
+ }
746
+ }
747
+ }
748
+ return { title: document.title, canonical, links };
466
749
  }
467
750
  /**
468
751
  * Walk a site's own links from one entry page, breadth first.
@@ -479,6 +762,7 @@ export async function crawlLinks(entry, opts) {
479
762
  const maxVisits = opts.maxVisits ?? 24;
480
763
  const found = new Set([entry]);
481
764
  const titles = new Map();
765
+ const canonical = new Map();
482
766
  let visited = 0;
483
767
  await withBrowser(async (browser) => {
484
768
  let frontier = [entry];
@@ -496,6 +780,14 @@ export async function crawlLinks(entry, opts) {
496
780
  const got = await readPage(browser, url, 1440, (page) => page.evaluate(linksOnPage));
497
781
  if (got.title)
498
782
  titles.set(url, got.title);
783
+ if (got.canonical) {
784
+ const c = opts.canon(got.canonical);
785
+ // A PAGE THAT NAMES ANOTHER ADDRESS AS ITS OWN is that page. Recorded
786
+ // rather than acted on here: the crawl still walks this copy for its
787
+ // links, and the caller folds the two when it builds the plan.
788
+ if (c && c !== url)
789
+ canonical.set(url, c);
790
+ }
499
791
  // A LEVEL BELOW THE LAST IS WALKED FOR ITS LINKS AND NOT QUEUED: at
500
792
  // `depth` the crawl still wants what that page points at, it just
501
793
  // must not navigate any further.
@@ -514,5 +806,5 @@ export async function crawlLinks(entry, opts) {
514
806
  frontier = next;
515
807
  }
516
808
  });
517
- return { urls: [...found], titles, visited };
809
+ return { urls: [...found], titles, canonical, visited };
518
810
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sbuilder-mcp",
3
- "version": "0.17.1",
3
+ "version": "0.19.0",
4
4
  "description": "MCP server that designs and operates a Store Builder site — pages, data, theme and publish — through the platform's own API and live-edit protocol.",
5
5
  "mcpName": "io.github.vuluu2k/sbuilder-mcp",
6
6
  "type": "module",