SEO docs

Rowabi's SEO / GEO knowledge base: how-tos and the reasoning behind the tools

Free mode

Internal Link Strategy cho GEO / AI search

Tài liệu nội bộ Rowabi · chiến lược liên kết nội bộ cho Google và AI search, ví dụ áp dụng cho rowabi.com trên Shopify · Công cụ đi kèm: Internal LinksSocial Listening

1. Internal link là gì và vì sao quan trọng

Internal link là liên kết từ một trang của rowabi.com sang một trang khác cũng của rowabi.com: menu, breadcrumb, khối "sản phẩm liên quan", link trong mô tả sản phẩm hay trong bài blog. Khác với backlink (do site khác đặt), internal link nằm hoàn toàn trong tay mình, nên đây là đòn bẩy SEO rẻ và nhanh nhất.

Bot tìm thấy trangGooglebot và bot AI đi theo link. Trang không có link trỏ tới gần như không được đọc thường xuyên.
Bot hiểu quan hệAnchor text (chữ chứa link) cho biết trang đích nói về gì và liên quan trang nguồn thế nào.
Phân phối sức mạnhMỗi link chuyển một phần "uy tín" (PageRank) của trang nguồn sang trang đích.

Với AI search (ChatGPT, Perplexity, Gemini, Claude)

Các trợ lý AI đọc trang để trả lời rồi trích dẫn nguồn. Chúng dựa vào cấu trúc link và anchor để hiểu shop bán gì, trang nào là trang chính của một chủ đề. Ví dụ: nếu nhiều bài blog về chiếu sáng nhà bếp đều link tới /collections/rattan-pendant-lights với anchor kiểu "đèn thả mây cho đảo bếp", AI dễ coi đó là trang tiêu biểu của Rowabi cho chủ đề đó và dẫn link khi người dùng hỏi.

Ở Rowabi: Visitor Log → AI report cho thấy trang nào AI đang đọc cho người dùng; tab Internal link opportunities gợi ý trang nào nên nhận thêm link.

2. E-E-A-T và "AI-friendly": internal link góp phần thế nào

Google đánh giá chất lượng theo E-E-A-T (Experience, Expertise, Authoritativeness, Trust). Với AI search có thêm yếu tố thứ năm: nội dung có dễ để máy đọc, hiểu và trích dẫn không. Internal link hỗ trợ cả năm:

Yếu tốVai trò của internal linkVí dụ ở Rowabi
Experience (trải nghiệm)Dẫn tới bằng chứng thực tế: ảnh khách dùng, đánh giá, dự ánBài "bathroom sconce ideas" link tới sản phẩm có review và ảnh khách hàng
Expertise (chuyên môn)Dẫn tới nội dung chuyên sâu, hướng dẫn kỹ thuậtTrang sản phẩm link tới hướng dẫn chọn kích thước đèn thả, cách lắp đặt
Authority (thẩm quyền)Nhiều trang cùng chủ đề trỏ về một trang trung tâmMọi bài về đèn mây trỏ về collection Rattan Pendant Lights
Trust (tin cậy)Mọi trang đều dẫn tới chính sách, liên hệ, giới thiệuFooter: Shipping, Returns, Contact, About, Privacy
AI-friendlyAnchor mô tả rõ, cụm chủ đề rõ, tên riêng nhất quánAnchor "đèn ốp trần mây cho phòng ngủ" thay vì "xem thêm"

Một cách chấm điểm hay dùng: trọng số Expertise và Authority cao nhất (khoảng 25% mỗi phần), AI-friendly khoảng 20%, Experience và Trust khoảng 15% mỗi phần. Con số chỉ là quy ước để so sánh trước/sau; điều quan trọng là phần nào đang yếu.

3. Các loại anchor text và tỷ lệ nên có

Anchor text là chữ người dùng bấm vào. Google và AI dùng nó như mô tả ngắn của trang đích. Một hồ sơ anchor tự nhiên trộn nhiều loại; dùng một loại quá nhiều (nhất là khớp chính xác) trông giả tạo, quá ít anchor mô tả thì máy không hiểu.

LoạiVí dụ (trỏ tới /collections/rattan-pendant-lights)Tỷ lệ gợi ýGhi chú
Mô tả (contextual)"đèn thả mây đan tay cho đảo bếp"25–35%Quan trọng nhất cho AI: câu tự nhiên, mô tả đúng nội dung
Khớp chính xác (exact)"Rattan Pendant Lights"10–15%Trùng tiêu đề / từ khóa chính của trang đích
Khớp một phần (partial)"mẫu pendant mây mới"15–20%Biến thể tự nhiên của từ khóa
Thương hiệu (branded)"Rowabi", "đèn mây Rowabi"15–20%Giúp AI gắn sản phẩm với thực thể Rowabi
Chung chung (generic)"xem thêm", "shop now", "click here"≤ 10%Không nói gì về trang đích: hạn chế
URL trần (naked)"rowabi.com/collections/…"≤ 10%Ít ngữ cảnh
Ảnh (image)ảnh có alt "đèn thả mây Kloe treo trên đảo bếp"5–10%Alt text đóng vai trò anchor; alt rỗng = vô nghĩa
Rỗng (empty)icon/ảnh không alt, nút không chữ≈ 0%Lỗi cần sửa
Mô tả: ~30%30%Khớp chính xác: ~12%12%Khớp một phần: ~18%18%Thương hiệu: ~17%17%Chung chung: ~7%URL trần: ~5%Ảnh (alt): ~8%Rỗng: ~3%Mô tả ~30%Khớp chính xác ~12%Khớp một phần ~18%Thương hiệu ~17%Chung chung ~7%URL trần ~5%Ảnh (alt) ~8%Rỗng ~3%
Một hồ sơ anchor cân đối (giá trị giữa của mỗi khoảng gợi ý): phần lớn là anchor mô tả, chung chung và rỗng chỉ chiếm phần nhỏ.

Các tỷ lệ là mức tham khảo cho cả site. Với từng trang đích, xem thêm: số anchor khác nhau (đa dạng), anchor chung chung có chiếm nhiều không, và có trang nào chỉ nhận đúng một anchor khớp chính xác lặp đi lặp lại (dễ bị coi là tối ưu quá mức).

Trên Shopify: anchor của menu và footer lặp lại trên mọi trang, nên chúng chiếm phần lớn số link. Khi phân tích nên tách link "trong nội dung" (mô tả, blog) khỏi link "điều hướng" (menu, footer). Screaming Frog có cột Link Position cho việc này.

4. Trang mồ côi (orphan page)

Trang mồ côi là trang không có internal link nào trỏ tới. Bot chỉ tìm được nó qua sitemap, nên đọc ít, ít uy tín và gần như không được AI trích dẫn.

0 link vàoMồ côi: cần xử lý ngay
1–2 link vàoGần mồ côi: nên bổ sung
3+ link vàoỔn (tính link trong nội dung càng tốt)
Homemồ côi0 link vàogần mồ côi1 link vào
Bot đi theo link từ trang chủ: trang đỏ không có đường nào dẫn tới (chỉ thấy qua sitemap); trang cam chỉ có một đường.

Hay gặp ở Shopify: sản phẩm đã bỏ khỏi collection nhưng vẫn bán, bài blog cũ không còn được nhắc tới, landing page của chiến dịch đã hết, trang /pages/ chỉ dùng cho quảng cáo. Cách sửa: tìm 2–3 trang cùng chủ đề đang được đọc nhiều (bot và khách) rồi thêm link có anchor mô tả; nếu trang không còn giá trị thì chuyển hướng 301 về trang phù hợp.

Mục tiêu: dưới 5% số trang là mồ côi.

5. Lấy dữ liệu internal link

Google Search Console (miễn phí)

  1. Search Console → chọn property rowabi.com → Links.
  2. Khung Top linked pages (Internal links) → More → Export → CSV.

Cho biết mỗi trang nhận bao nhiêu link nội bộ theo Google, nhưng không có anchor text và trang nguồn. Đủ để tìm trang ít link, không đủ để phân tích anchor.

Screaming Frog (miễn phí tới 500 URL)

  1. Crawl https://www.rowabi.com (Configuration → Spider: bật crawl links ngoài nội dung nếu muốn tách menu/footer).
  2. Bulk Export → Links → All Inlinks → CSV: mỗi dòng một link (Source, Destination, Anchor, Alt Text, Link Position, Follow…).
  3. Internal → All → Export → CSV: danh sách trang (Address, Title, H1, Word Count, Crawl Depth, Inlinks…).

Đầy đủ nhất. Site Rowabi lớn hơn 500 URL nên bản miễn phí chỉ crawl được một phần: có thể crawl theo từng khu vực (chỉ /collections/, chỉ /blogs/) hoặc dùng bản trả phí.

Ahrefs Site Audit (trả phí)

Site Audit → Internal pages / Links → Export CSV. Cột thường gặp: Source URL, Target URL, Anchor. Hữu ích khi so sánh với đối thủ.

Tool Internal Links của Rowabi nhận trực tiếp các file CSV trên (tự nhận biết loại file), và có thể lấy danh sách toàn bộ trang từ Index Tool để tìm trang mồ côi.

6. Xây dựng internal link

6a. Site mới hoặc khu vực mới

  1. Kiến trúc: Trang chủ → nhóm sản phẩm (collection) → sản phẩm; blog → chủ đề → bài.
  2. Trang trung tâm (hub) cho mỗi chủ đề lớn: ở Rowabi thường là collection (Rattan Pendant Lights, Wall Sconces, Flush Mount) hoặc một bài hướng dẫn tổng.
  3. Trang vệ tinh (spoke): sản phẩm và bài blog cùng chủ đề, mỗi trang link về hub và link qua lại với vệ tinh liên quan.
  4. Anchor đa dạng theo tỷ lệ ở mục 3.
  5. Link tin cậy ở footer: Shipping, Returns, Contact, About, FAQ.
Trang chủ /
├── Collection: Pendant Lighting  (hub)
│   ├── Rattan Pendant Lights → các sản phẩm mây
│   └── Kitchen Concepts      → sản phẩm cho đảo bếp
├── Collection: Wall Sconces       (hub)
├── Collection: Flush Mount        (hub)
├── Blog: Lab of Wabi-Sabi
│   ├── "Bathroom sconce ideas"        → Wall Sconces + 3 sản phẩm
│   └── "How to change a pendant shade" → Pendant Lighting + phụ kiện
└── Footer: Shipping · Returns · Contact · About · FAQ

6b. Site đang chạy: kiểm tra và lập kế hoạch

Chỉ sốMục tiêuKhi không đạt
Tỷ lệ trang mồ côi< 5%Thêm link từ trang cùng chủ đề; 301 trang hết giá trị
Đa dạng anchorPhần lớn trang có nhiều anchor khác nhauĐổi bớt anchor lặp lại / chung chung
Anchor chung chung≤ 10%Thay "xem thêm" bằng anchor mô tả
Mô hình linkHub-spoke rõ ràngChọn hub cho từng chủ đề, cho spoke trỏ về hub
Thứ tự uy tínTrang chủ đứng đầu, rồi collection chínhThêm link về trang chủ / collection từ blog và sản phẩm

Kế hoạch nên chia giai đoạn: (1) sửa nhanh: trang mồ côi, anchor rỗng / chung chung; (2) cấu trúc: hub và link hub–spoke; (3) tinh chỉnh tỷ lệ anchor và link tin cậy; (4) so sánh đối thủ và bổ sung nội dung.

7. Mô hình liên kết

hub
Hub-spoke · các trang con trỏ về trang trung tâm
pendantsconce
Silo · chỉ link trong từng nhóm
Phẳng · không có trung tâm
AEuy tín giảm dần →
Chuỗi · trang nối tiếp, sâu dần
Mô hìnhMô tảĐánh giá
Hub-spokeMỗi chủ đề có một trang trung tâm; các trang con trỏ về đó và trỏ qua lạiTốt nhất cho AI: nhìn ra ngay trang nào đại diện cho chủ đề
SiloTrang gom theo nhóm, chỉ link trong nhómTốt cho shop; nhược điểm là thiếu link chéo giữa các nhóm liên quan (ví dụ pendant ↔ sconce cùng phòng)
Phẳng (flat)Trang nào cũng link ngang nhau, không có trung tâmTrung bình: khó biết trang nào quan trọng
Chuỗi (chain)A → B → C → D, trang sâu dầnYếu: bot phải đi nhiều bước, uy tín giảm dần

Shopify mặc định gần với silo (menu → collection → sản phẩm). Muốn thành hub-spoke cần thêm link trong nội dung: blog trỏ về collection, collection có đoạn giới thiệu trỏ về hướng dẫn, sản phẩm trỏ về collection chính.

8. Thứ tự ưu tiên: trang chủ › trang chủ đề › nội dung

Không phải trang nào cũng ngang nhau. Link nội bộ nên dồn uy tín theo thứ tự: trang chủ (nhiều link nhất) → collection / landing chính → sản phẩm chủ lực → blog → trang hỗ trợ.

Trang chủNhiều link nhấtCollection / landing chínhMenu + blog + sản phẩm trỏ vềSản phẩm chủ lựcTừ collection, bài viếtBài blogTừ blog hub, bài liên quanTrang hỗ trợFooter
Lượng link nội bộ (và uy tín) nên giảm dần theo tầm quan trọng của trang.

Lỗi hay gặp: một bài blog cũ nhận nhiều link hơn collection chính; landing page chạy quảng cáo không có link nội bộ nào; trang chủ không đứng đầu vì menu dùng link tuyệt đối khác host (rowabi.com và www.rowabi.com) làm phân tán.

9. Kỹ thuật nâng cao

9a. Cụm chủ đề (hub + spoke)

Hub: trang tổng quan của chủ đề (collection có đoạn mô tả hướng dẫn, hoặc bài "guide" dài), link ra tất cả spoke. Spoke: sản phẩm, bài chuyên sâu một khía cạnh, link về hub và link sang spoke liên quan. Với Rowabi, cụm có thể dựng theo dòng đèn × phòng giống Keyword Research: ví dụ cụm "đèn thả cho bếp" gồm collection Kitchen Concepts, các sản phẩm đèn thả cho đảo bếp và bài "pendant height over island".

HUBKitchen ConceptsLinear Island PendantKloe Rattan PendantBlog: treo đèn cao bao nhiêuBlog: chiếu sáng đảo bếpLumiere Pendant
Cụm "đèn thả cho bếp": mọi trang vệ tinh (sản phẩm, bài blog) trỏ về collection trung tâm; nét đứt = link ngang giữa các vệ tinh liên quan.

9b. Trang mạnh nâng trang yếu

Dùng trang đang có nhiều link / nhiều lượt bot và khách (trang chủ, collection chính, bài blog đông khách) để đặt link tới trang mới hoặc trang yếu. Áp dụng cho: sản phẩm mới, trang mồ côi, collection mới mở.

Trang chủCollectionSản phẩm mới / trang yếulink + anchor mô tả
Trang có nhiều uy tín chuyển bớt cho trang mới hoặc trang ít link.

9c. Nhiều trang nhỏ nâng một trang chính

Mỗi bài blog, mỗi câu trả lời FAQ đều thêm một link về trang bán hàng chính, mỗi bài dùng anchor khác nhau (mô tả, một phần, thương hiệu). Từng link nhỏ nhưng cộng lại giúp trang đích nổi bật cho cả Google lẫn AI.

Blog 1Blog 2FAQBlog 3GuideRattan PendantLights
Mỗi bài nhỏ một link, mỗi link một anchor khác nhau: cộng lại thành tín hiệu mạnh cho trang bán hàng chính.

10. Phân tích internal link của đối thủ

  1. Crawl site đối thủ (Screaming Frog), xuất All Inlinks + Internal All.
  2. Đưa vào tool Internal Links như một dự án riêng: xem mô hình link, các cụm, hồ sơ anchor, trang được dồn nhiều link nhất.
  3. So với Rowabi: họ có bao nhiêu hub, trang nào là trung tâm, anchor đa dạng tới đâu, blog trỏ về đâu.
  4. Làm trang tốt hơn ("skyscraper"): nội dung đầy đủ hơn và cấu trúc link chặt hơn (nhiều spoke hơn, anchor mô tả hơn, link tới bằng chứng: review, ảnh thực tế).

Kết hợp với Keyword Research → Content gap để biết đối thủ thắng ở từ khóa nào, rồi xem cấu trúc link của trang đó.

11. Dùng tool Internal Links của Rowabi

  1. Nạp dữ liệu: kéo thả CSV từ Screaming Frog (All Inlinks, Internal All), Search Console (Top linked pages) hoặc Ahrefs; có thể thêm danh sách trang từ Index Tool để tìm trang mồ côi.
  2. Thiết lập: tên miền, tên thương hiệu (để nhận anchor branded), trang mục tiêu (trang muốn AI ưu tiên).
  3. Phân tích: tool tính mọi chỉ số ngay trong trình duyệt (dữ liệu không gửi đi đâu).
  4. Đọc kết quả theo các tab: Audit, Trang mồ côi, Anchor, Cấu trúc & PageRank, Cụm chủ đề, Gợi ý link, Kế hoạch, Bot AI. Xuất CSV để giao việc.

Phụ lục: bảng tra nhanh

Chỉ sốMức tốt
Trang mồ côi< 5% số trang
Link vào mỗi trang (trong nội dung)≥ 3
Anchor mô tả25–35%
Anchor chung chung + rỗng≤ 10%
Độ sâu crawl của trang quan trọng≤ 3 lần bấm từ trang chủ
Điểm sức khỏeA+ ≥ 90 · A ≥ 80 · B ≥ 70 · C ≥ 60 · D ≥ 50 · F < 50

User guide: SEO docs

SEO docs

SEO docs là kho kiến thức SEO / GEO nội bộ của Rowabi: các bài hướng dẫn và lý do đằng sau từng tool. Mỗi bài có ví dụ áp dụng cho rowabi.com trên Shopify.

1. Cách đọc

  • Bài chia thành các mục đánh số. Đọc lần lượt, hoặc tìm trong trang (Ctrl+F) khi cần một phần cụ thể.
  • Đầu mỗi bài ghi các công cụ đi kèm. Bấm để mở tool tương ứng và áp dụng ngay.
  • Các bảng (tỉ lệ anchor, chỉ số mục tiêu, mô hình liên kết) là mức tham khảo để so sánh trước và sau, không phải luật cứng.
  • Hình minh họa tự đổi theo chế độ sáng hoặc tối.

2. Các bài hiện có

Internal Link Strategy cho GEO / AI search

Chiến lược liên kết nội bộ cho Google và các trợ lý AI:

  • Internal link là gì, vai trò với E-E-A-T và với AI search.
  • Các loại anchor text và tỉ lệ nên có.
  • Trang mồ côi và cách sửa.
  • Cách lấy dữ liệu link (Search Console, Screaming Frog, Ahrefs).
  • Xây dựng link cho site mới và kiểm tra site đang chạy.
  • Mô hình liên kết, thứ tự ưu tiên uy tín, cụm chủ đề hub và spoke, các kỹ thuật nâng cao.

Tool đi kèm: Internal Links (phân tích theo đúng các tiêu chí của bài) và Visitor Log (trang nào AI đang đọc).

3. Kết hợp với các tool

  • Keyword Research → chọn chủ đề và từ khóa có nhu cầu thật.
  • Internal Links → kiểm tra cấu trúc link theo SEO docs.
  • Index Tool → đảm bảo trang đã được Google index.
  • Visitor Log → xem AI có đọc trang không.
  • Rank Tracker → theo dõi kết quả sau khi sửa.
  • Social Listening → biết người mua đang hỏi gì để viết nội dung.

4. Đề xuất bài mới

Khi team cần tài liệu cho một chủ đề mới (ví dụ: schema sản phẩm, tối ưu ảnh, llms.txt), gửi yêu cầu cho người quản lý tool để thêm bài vào SEO docs.

Guides of the other tools: Geo Tag photos · UTM Builder · GeoCircle KML · Rank Tracker · Keyword Research · Internal Links · Social Listening · Index Tool · Traffic Sources · Attribution · Visitor Log

What's new

v2.01 latest 2026-10-29

Changed
  • Attribution: when a journey began before our pixel saw the visitor, Shopify's earlier visits are now added at the start of the journey. This covers a journey that began before the pixel was installed, or on another device. Shopify's visits come from its first and last visit data.
  • A gclid / gbraid / wbraid on Shopify's landing page now counts as Google Ads, not organic Google.

v2.00 2026-10-29

Changed
  • Attribution, Linear model: an order is now shared equally between the sources that touched it, each source once, as Triple Whale's "Linear All" does. Before, it was shared between the touches.

v1.99 2026-10-29

Added
  • Attribution: periods Today and Yesterday, calendar days in the shop's time zone, as Triple Whale shows them. The time zone is read from Shopify; set the variable SHOP_TZ to choose it by hand. With the same day and model ("Linear All" in Triple Whale = Linear here), the Sources table can be compared line by line.
Fixed
  • Attribution, Sources: CVR was above 100% (Direct 125.9%, AI 416.7%), because 30 days of orders were divided by the visits our pixel has recorded since it started (one day). CVR now uses only the orders placed since the pixel started, and a note under the table gives that time.
  • The same source written in two cases ("OrderlyEmails" and "orderlyemails") is one row.

v1.98 2026-10-29

Fixed
  • Attribution: a customer coming back from a payment page (PayPal, Shop Pay, Klarna, Afterpay, Affirm, Stripe, Amazon Pay, 3-D Secure…) started a new "Referral sites" visit right before the order and took the last click. Found on order #WABI15387: three Klaviyo checkout emails, then a referral one minute before the purchase.
Changed
  • Attribution, Sources: the rows without an online journey ("Other sales channels", "No journey") are at the bottom, as in Triple Whale.

v1.97 2026-10-29

Added
  • Attribution, Sources tab, like Triple Whale's attribution table:
  • Customer journey of an order, like Triple Whale's: every step of the visitor before the order, newest first and grouped by day.
  • Pixel v2 (Setup → paste over the pixel in Shopify → Customer events): it also records every page (title), product and collection viewed, the product and quantity added to the cart, the checkout steps, and a visit id that counts bounces and visit length.
  • Order sync also reads the order's products, discount codes, discount and tax (read_orders).
Changed
  • Page, product and collection views are kept 90 days (ATT_PAGE_DAYS). Visits, carts, checkouts and orders are kept 400 days.

v1.96 2026-10-29

Changed
  • Attribution: channels corrected from what the drill-down showed in Rowabi's real data (30 days, 451 orders):

v1.95 2026-10-29

Added
  • Attribution, Overview → By channel: click a channel to see its sources, mediums and campaigns, with the orders and revenue the model gives each.

v1.94 2026-10-29

Fixed
  • Attribution, order sync: Shopify refused the orders because the query also asked for the customer of each order, which needs the scope read_customers. The customer is no longer read, since attribution does not use it, so read_orders is enough.
  • The sync error now shows Shopify's own message (the field and the scope it needs) before the scopes the app has, instead of always saying "no access to orders".
  • Attribution, Cross-check GA4: under the table, the "Other / unassigned" purchases are listed as GA4 itself records them (source / medium · GA4 channel), with why GA4 leaves a purchase unassigned.

v1.93 2026-10-29

Fixed
  • Attribution, Cross-check GA4: 164 of the 235 GA4 purchases were "Other / unassigned". The query read only the session's manual (UTM) campaign, which is empty for organic and direct visits. It now reads the session's cross-channel last click with GA4's default channel group, the same attribution as GA4's own reports. Older exports fall back to the manual campaign, then to the user's first source.
  • Attribution, Cross-check Triple Whale: when its pixel table has no order at all, the tool also counts Triple Whale's own orders of the shop. If Triple Whale has orders, its pixel is not attributing them. If it has none, the shop id is wrong, and the message says to set TRIPLEWHALE_SHOP to the shop domain shown in Triple Whale.

v1.92 2026-10-29

Fixed
  • Attribution, order sync: a Shopify token made before read_orders was added keeps the old scopes for up to 24 hours. On "Access denied for orders field" the sync now gets a new token and tries once more.
  • If it is still denied, the error lists the scopes the app really has and says what to do: add read_orders, release the new app version and approve it on the store.

v1.91 2026-10-29

Added
  • Attribution, Search keywords tab: the Search Console queries behind the orders credited to Google organic.
  • Attribution, Cross-check: GA4 BigQuery export, read only.
  • The Google service account token also asks for the scope bigquery.readonly. It reads only: nothing is written to GA4 or BigQuery.

v1.90 2026-10-29

Fixed
  • Attribution, Cross-check: Triple Whale answered with no row. The query no longer filters on attribution_window, since the table already uses the lifetime window by default. When the answer is still empty, the tab shows the shop id that was asked and the attribution models Triple Whale has for the period, so a wrong model name or shop id can be seen at once.

v1.89 2026-10-29

Added
  • New tool Attribution (menu after Traffic Sources): the customer journey to each Shopify order and the channels that led to it, like Triple Whale, through our own gate:
  • Cross-check with Triple Whale (secret TRIPLEWHALE_API_KEY, read only): orders and revenue per channel of its pixel_orders_table for the closest model, next to ours.
  • User guide of the Attribution tool.

v1.88 2026-10-29

Added
  • Social Listening, Facebook groups: Run every (days), 7 by default (was every 2 days), and Max cost per run (USD), $3 by default. Apify stops the run at the cap.
Changed
  • User guide: each tool again has its own guide, opened from the footer of its page, since one guide for all tools was too long to read. Links at the end open the guides of the other tools.
Fixed
  • A comment whose text is "None" (empty in the Apify data) is skipped.

v1.87 2026-10-29

Added
  • One user guide for every tool in the footer: a table of contents of the 11 tools, then each tool with its own list of sections. It opens at the part of the page you are on.
Changed
  • Social Listening keeps lighting fixtures only (Rowabi's product lines):
  • Mentions: a comment shows its own words as the title, and the video or post it is under on a small line. Two comments under one video no longer look like the same item twice.
  • AI digest:
  • Google results: from the last year instead of the last month, since a month found almost no Quora or Houzz thread. Only results of the sites of Settings are kept. A result without a date is dated a year back, so it never counts as this week's thread in Trends.
  • Mentions: every site of Settings (Quora, Houzz, Reddit) is in the source filter, with 0 until Google finds a lighting thread there.
  • Trends: "confirmed" no longer needs Rowabi's own reviews, because they are not collected now.
  • Settings: "Review keywords" is now Lighting keywords.
Removed
  • Social Listening: the reviews of Google Maps (competitor stores), Trustpilot and Judge.me: the sources, their schedule and cost, the Reviews tab and their settings.
  • The separate guide of each tool (replaced by the one guide).

v1.86 2026-10-29

Added
  • User guide in the footer of every tool, in Vietnamese: Geo Tag photos, GeoCircle KML, UTM Builder, Rank Tracker, Keyword Research, Index Tool, Traffic Sources, Visitor Log, Internal Links and SEO docs, as for Social Listening. Each guide covers the one-time setup, every step or tab, how to read the results, and the usual problems. The texts are in guides/*.md.
  • Social Listening, AI digest: choose the model next to the button. ChatGPT uses the deep OpenAI model (OPENAI_MODEL, needs the secret OPENAI_API_KEY) and is the default when that key is set. Claude is the other choice. The line above the report shows when it was written, by which model, and how many sources it cites.
Changed
  • The tab that reviews insights is now called Insights. It was "Review", next to "Reviews" (the customer reviews), and the two names were confusing.
  • Sources: the Reddit API row moves to the end of the table while it has no key.
  • Mentions: the source filter lists every source with its count, not only the sources of the first 50 items. Google results (Quora, Houzz and other sites) are grouped: "Google results: all sites", then one line per site.
Fixed
  • A digest written before v1.79 has no numbered sources [n]. It now says so above the report and asks to write it again.

v1.85 2026-10-29

Added
  • Social Listening, User guide in the footer: a detailed guide in Vietnamese for each tab and setting. It covers Overview, Mentions, Questions, Pain points, Trends, Review, Brands, Reviews, Actions and Settings, with the weekly routine of the team, the costs and frequent questions, and has a table of contents. The page address with ?guide opens it at once. The text lives in guides/social.md and build.py puts it into the page.
Changed
  • Sources: the Reddit row says it waits for the Reddit API. Until then it points to the Apify template "Reddit posts and comments", or says that Reddit comes through Apify when that source exists.
Fixed
  • An Apify source that is off (for example just added from a template) had no row on Overview, so it could not be tested with "Collect now" as Settings says. It now has a row with "off (test with Collect now)", and the cron still skips it.

v1.84 2026-10-29

Changed
  • Social Listening: each automatic run analyses up to 4 batches of 25 items (100 items, about 300 an hour) instead of one batch of 25. It stops after 90 seconds or when nothing is left. Before, the YouTube channel runs (about 400 new comments every 3 hours) added more than the AI could read, so items waited for analysis and Trends counted too few. Claude Haiku costs well under $1 per 1,000 items.

v1.83 2026-10-29

Added
  • Social Listening, Review tab: where the team decides what to act on (the "team reviews and acts" step of the flow, inside the tool instead of Google Sheets):
  • Action board under the queue, with 4 columns: Approved → In progress → Done (measure in 14 days) → With a result. Due dates in the past show in red. "Ready to measure" shows 14 days after done.
  • Actions have a due date, and the date they were done is kept.
  • The Trends CSV (and its Google Sheets link) has the team's decision, reviewer, date and reason. The Actions CSV has the due and done dates.
Changed
  • Trends evidence: up to 6 items, at most 2 from one community, one per thread, with the AI quote when there is one.
  • The Google Sheets and Looker Studio box moved from Overview to Settings. It is optional now that the team reviews in the tool.

v1.82 2026-10-29

Added
  • Social Listening, Reddit through Apify while the Reddit API request waits: a template "Reddit posts and comments: chosen subreddits" (actor trudax/reddit-scraper-lite, 5 subreddits, newest posts of the week, 20 comments each, weekly, at most 300 items and $2 per run). It starts off; "Collect now" tests it.
Changed
  • Reddit records from Apify are read as Reddit mentions:

v1.81 2026-10-29

Added
  • Social Listening, more sources through Apify (Settings, secret APIFY_TOKEN):
  • Google Sheets / Looker Studio live link: /feed/social.csv?tab=mentions|trends|actions|runlog&key=…, used with =IMPORTDATA in Google Sheets (refreshed about every hour). Looker Studio reads the sheet. Overview shows the formulas.
  • Hook for n8n and Apify: POST /hook/social?key=… with {"source": "youtube"} starts a source. An Apify "run succeeded" webhook collects the finished run at once.
  • The feed and the hook need the secret SOCIAL_FEED_KEY (16+ characters) and are off without it.
  • Export CSV of the Trends.
Changed
  • build.py also writes the date of the newest changelog entry in the first line of src/worker.js. The version notes at the top of src/worker.js and src/social.js now list v1.74–v1.81.

v1.80 2026-10-29

Fixed
  • Social Listening: when Claude answers without text, the AI digest now shows the real reason (stop reason, model). Before, it said "No AI key", although the key is set. The answer limit is raised to 8,000 tokens.
  • "Find channels": channel names and video titles show "&" and "'" instead of &amp; and &#39;.
Changed
  • AI analysis: a comment (YouTube, Facebook) is relevant only when it talks about lighting or names a brand. Comments about paint, furniture or the creator alone no longer count in the pain points and trends.

v1.79 2026-10-29

Added
  • YouTube channels followed: creators and lighting / decor brands (Renee Renovates, Reserve Home, Lily Walters, Serena & Lily, Shades of Light, Foundry, Landscape Lighting Designers by default). 3 channels per run, in turn: their newest uploads (a creator's lighting videos only, a brand's all) and the newest comments under them. Settings: a table of the channels followed (subscribers, videos and comments kept) and "Find channels", which lists channels with 5,000+ subscribers that publish lighting / home decor videos watched in the US, each with an "Add" button.
  • Trends tab with a fixed evidence gate instead of raw counts:
  • Actions tab: the team's actions, each with its reason, evidence links, owner, status, metric and result.
  • Run log: every run with records found and new, status, error and estimated cost. Overview shows the cost of the last 30 days.
  • Export CSV for Google Sheets (mentions of the period, actions, run log).
  • Facebook groups (optional, secret APIFY_TOKEN): public groups read through Apify's Facebook Groups Scraper every 2 days. It takes the posts of the last 14 days, 30 per group, and their top comments, without author names.
  • AI analysis returns 3 more fields for each item:
Changed
  • The AI digest rests on evidence:
  • Google Maps: only the 10 most reviewed competitor stores of each state (editable), all competitors together.

v1.78 2026-10-29

Fixed
  • Social Listening, Google Maps stores: only listings whose website is the brand's own domain (or one of its subdomains) count. Before, other businesses slipped in, for example manhattanschoolhouse.com for Schoolhouse, or "Rejuvenation Nails & Spa" (no website) for Rejuvenation.
Changed
  • YouTube: only medium-length videos (4–20 minutes); each topic is also searched as "… ideas" and "… review". Shorts and product clips have almost no comments.

v1.77 2026-10-29

Fixed
  • Social Listening: the Google Maps store search called a wrong DataForSEO path (error 40400). It now uses business_data/business_listings/search/live.
Changed
  • YouTube: takes the most watched videos of the last 2 years for each topic (US) and their newest comments. Before, it took videos of the last month, which have almost no comments.

v1.76 2026-10-29

Fixed
  • Social Listening: a source that fails now shows why in the Sources table, also after "Collect now". Before, a failed Google Maps store search (DataForSEO Business Listings) showed only "0 found", and failed YouTube comment requests showed nothing. A partial failure shows as a red note under "ready".

v1.75 2026-10-29

Added
  • Social Listening: Judge.me product reviews (daily) of Shopify shops: Rowabi (wabi-element) and Lulu and Georgia by default, editable in Settings. They are read from the public "all reviews" widget the shop loads and need no key.
  • Settings: review keywords (Rowabi's product groups: pendant, chandelier, sconce, flush mount, lamp, lighting…) and store states.
Changed
  • Competitor reviews on every site (Trustpilot, Google Maps, Judge.me) are kept only when they name a review keyword. Rowabi's own reviews are all kept. The AI also marks a competitor review relevant only when it is about lighting.
  • Competitor stores on Google Maps only in the chosen states. The default is CA, NY, TX, FL and GA: the coastal states of the top 10 revenue cities in GA4 (Chicago left out).
  • More reviews read per competitor so enough lighting reviews remain: Trustpilot 100 (was 20), Google stores 50 (was 20).
  • Reddit stays off until Reddit approves API access.

v1.74 2026-10-29

Added
  • Social Listening: Google reviews of every competitor location. A new monthly source finds every Google Maps location of each competitor (DataForSEO Business Listings; a location counts when its website is the competitor's domain). The weekly review task then fetches the 20 newest Google reviews of each location and keeps only those that talk about lighting.
  • Reviews tab: each brand's number of locations, Google reviews, average rating and locations under 4 ★. Click a brand to list its locations, lowest rated first, with links to Google Maps. Store reviews are grouped per brand in the ratings table.
  • Settings: on / off switch for the competitor locations.

v1.73 2026-10-29

Added
  • Social Listening page (/social, menu after Internal Links): what people ask, share and struggle with about home decor lighting, Rowabi and its competitors.

v1.72 2026-10-29

Added
  • SEO docs → Internal Link Strategy: illustrations (drawn in the page, light and dark mode) for the four link models, a balanced anchor mix, orphan and near-orphan pages, the authority order of page types, a topic cluster (Kitchen Concepts), strong page → weak page, and many small pages → one key page.
Changed
  • SEO docs: the introduction line of the document no longer names an outside source. Worker v1.72 (pages only).

v1.71 2026-10-29

Added
  • SEO docs (/docs, link "SEO docs" in the footer of every page): a knowledge base. First document Internal Link Strategy (GEO), written for Rowabi on Shopify: internal links and AI search, E-E-A-T + AI-friendliness, 8 anchor types with target shares, orphan pages, exporting from Search Console / Screaming Frog / Ahrefs, building and auditing, link models, authority order, topic hubs, strong → weak and many → one linking, competitor analysis, using the tool, quick reference. Side bar with the documents, table of contents (follows the reading) and search; new documents are new <article class="doc"> blocks in docs-template.html.
  • Internal Links page (/links, menu after Keyword Research): internal link strategy from link exports, analysed in the browser (files are not uploaded).

v1.70 2026-10-29

Added
  • AI answers → Names and domains to look for: a list edited on the page (brand, product lines, best sellers, e.g. Rowabi, Kloe, Naia) and the own domains. An answer is "Named" when it contains one of the names as a whole word (any case); the Named column shows which names and how many times; "Linked" counts cited sources on the domains or their subdomains (not look-alikes such as rowabi.com.example.net). Saving checks every saved answer again with the new list, without asking the assistants again (answers are now saved in full; older ones are checked on their saved excerpt). Defaults: variables GEO_BRAND (comma-separated) and GEO_DOMAIN. Worker v1.70.

v1.69 2026-10-28

Fixed
  • GEO checklist → "Who read robots.txt / llms.txt" listed the tool's own checker (RowabiGEOChecker); it is left out now. Worker v1.69.

v1.68 2026-10-28

Fixed
  • After v1.67 Shopify still refused every check from the Worker (robots.txt and pages, 429 for more than 10 minutes), while llms.txt through the new binding LLMS worked (19 KB file, 81 broken links, 1 placeholder). The retry now uses a normal browser user agent, which Shopify's bot protection treats differently from an unknown bot; the audit says when it was needed. Both user agents can be changed with the variables GEO_UA and GEO_BROWSER_UA. These requests go straight to Shopify and are not logged as visitors. Worker v1.68.

v1.67 2026-10-28

Fixed
  • Page audit and the robots.txt / llms.txt check got 429 "Verifying your connection" from Shopify after a few checks: requests of the geo-tag Worker to www.rowabi.com go straight to Shopify (not through the Workers of the zone, so they are not logged either), and Shopify's bot protection limits quick requests from servers. A page refused this way was scored 20 as if it were broken. Now: one retry after 2.5 s; pages are checked one after the other with a pause; a refused page is reported ("try again in a few minutes") and not saved or scored; robots.txt and llms.txt are kept 10 minutes, and no checklist task is made from a refused check. Worker v1.67.

v1.66 2026-10-28

Fixed
  • Visitor Log GEO tools after the first deploy: ticking checklist tasks, saving notes, running page audits, asking AI assistants and marking alerts as seen were refused ("method not allowed": the Worker only accepted reads under /api/logs/). POST is now allowed for /api/logs/geo/*.
  • llms.txt check: a request of the geo-tag Worker to www.rowabi.com did not reach rowabi-llms-page like a visitor's request, so the tool saw Shopify's own llms.txt (5.8 KB "Agent Instructions") instead of the file bots get (19 KB, with 81 broken links). With a Service binding LLMS → rowabi-llms-page on the geo-tag Worker it reads the same file as bots; without it the page says the result may differ.
  • GEO checklist: errors of bots on probe paths (/.dev.vars, /api/system/fileView) and broken asset links became tasks; now shop pages always count, other paths only from 10 errors. Worker v1.66.

v1.65 2026-10-28

Added
  • Visitor Log → AI report, new sections (each with an explanation of how to read it):
  • GEO checklist tab: tasks with priority, built from the log, robots.txt, llms.txt and the page audits (robots.txt blocking AI search / real-user bots, training crawlers to decide on, llms.txt missing / broken links / placeholders / pages not listed, 404 / 5xx / 403 on pages read by AI, unverified bot names, pages AI search has not reached, pages to audit or improve, cart agents). Tick done and add a note; saved in D1 (geo_tasks).
  • Page audit tab: pages fetched like an AI crawler (no JavaScript) and scored: reachable, not noindex, title, description, one H1, canonical, JSON-LD, Product with price and availability, rating, enough text, questions answered (FAQ), listed in llms.txt; each failed check with its fix. "Audit the 8 pages AI reads most" or any paths; saved (geo_audit).
  • AI answers tab: ask Claude (web search), OpenAI (web search) and / or Perplexity the questions buyers ask (or questions suggested from Keyword Research) and see whether the answer names Rowabi, links rowabi.com, at which rank, and which sites it cites instead; history and mention rate per assistant (geo_answers). Uses ANTHROPIC_API_KEY / OPENAI_API_KEY / PERPLEXITY_API_KEY (models: ANTHROPIC_MODEL_FAST, OPENAI_MODEL_FAST, PERPLEXITY_MODEL; search tools ANTHROPIC_SEARCH_TOOL, OPENAI_SEARCH_TOOL).
  • GEO history tab: one snapshot per day saved by the cron (geo_daily, Vietnam time; GEO_TZ_MINUTES), period vs previous period, daily chart and table, checklist progress, Print / save as PDF.
  • Alerts: the cron checks once an hour for AI search / real-user requests falling below half of the 7-day average, 5xx on shop pages, product 404s for visitors, unverified requests using the names of ChatGPT / OpenAI / Google / Claude / Perplexity / Bing bots, and any bot above 1,000 requests an hour (GEO_HEAVY_PER_HOUR); shown on top of the Visitor Log and sent to Slack when the secret SLACK_WEBHOOK_URL is set.
Changed
  • The live site checked by these tools is https://www.rowabi.com (variable LOG_SITE); its requests are logged as the bot "RowabiGEOChecker". Worker v1.65; edge-logger.js unchanged.

v1.64 2026-10-28

Changed
  • Content gap laid out like Ahrefs Content Gap:

v1.63 2026-10-28

Added
  • Content gap → Opportunities: Show 20 / 50 / 100 / 500 / all rows (remembered).
Fixed
  • Content gap showed "No keyword matches these filters" without saying why (e.g. 215 keywords fetched, 0 shown). The page filters (competitors in the top N, how many competitors, include / exclude words, KD, line, brand names) now run one after the other and the list says how many keywords each one hides, with a hint to loosen the first one. Worker v1.63 (no functional change).

v1.62 2026-10-28

Added
  • Content gap: Only keywords containing (default: light, lamp, pendant, sconce, chandelier, lantern, flush mount, fixture). Only those keywords are fetched and paid for. The first real run against serenaandlily.com, potterybarn.com and westelm.com returned mostly furniture keywords (beds, sofas, rugs) because those sites sell much more than lighting. Worker v1.62.

v1.61 2026-10-28

Added
  • Keyword Research → Content gap (like Ahrefs Content Gap): keywords up to 10 competitors rank for on Google where your site does not rank in the top 100, or where it ranks lower (positions 11–20, 4–10, 21–50, 11–100 or any).
  • Visitor Log: Today in the period menu (since local midnight).
Changed
  • Visitor Log → Raw log: one line per request; long page addresses, referrers and user agents are cut and shown in full on hover. Worker v1.61.

v1.60 2026-10-28

Added
  • Visitor Log → AI report: a Data gap finding when the log has no request for /products/, /collections/, /blogs/ or /pages/ (their requests go to another Worker, e.g. rowabi-md, and never pass through rowabi-edge-log), with the Cloudflare steps to fix it.
Fixed
  • AI report with real data: Visits from AI chats counted theme and app requests that carry the page's utm_source=chatgpt.com (e.g. /sf_private_access_tokens, 401) as landings, and added visitors up per page; now only page views count, and each source shows its distinct visitors. "Most asked" pages and "pages fetched for people" leave out robots.txt and other files. Worker v1.60.

v1.59 2026-10-28

Added
  • Keyword Research → Product lines: add, rename, delete and order your own product lines (e.g. Chandelier, Floor lamp), each with the words that recognise it in a keyword ("chandelier, chandeliers"). Words also match plurals and hyphen / no-space forms. Lines are checked from the top, the first match wins; "Try a keyword" shows which line a keyword gets. Every line gets its own row of room cards in By room, and appears in the filters, the line menus and the CSV. Kept in D1 (kw_lines), the same for everyone.
  • Visitor Log → AI report: what AI bots do on the shop, split by purpose:
Changed
  • Keywords pasted or added from ideas without a line chosen stay on auto, so they follow later changes of the line words; a pasted second column can name any line. Worker v1.59; edge-logger.js unchanged.

v1.58 2026-10-28

Added
  • Keyword Research → Keywords: Delete all removes every keyword of the chosen country (asks first), to start a new list.
Fixed
  • Keywords added from Find ideas were saved with the idea's search volume, which Google gives for a whole group of close variants (e.g. "rattan pendant light" 5,400 while the exact keyword has 880), and changed to the exact-keyword volume at the next refresh. They now get the exact-keyword volume right away, the same as pasted keywords and the Rank Tracker; the Find ideas list says that its numbers cover close variants. Worker v1.58.

v1.57 2026-10-28

Added
  • Search volumes refresh by themselves: the cron loads volumes of new keywords a few minutes after they are added and reloads every keyword once its volume is 30 days old, on the Keyword Research page and in the Rank Tracker (free, needs Google Ads connected; the Rank Tracker no longer needs DataForSEO for this).
Changed
  • Refresh volumes no longer asks for confirmation (it is free).
Removed
  • DataForSEO as a source of search volumes: volumes come from Google Ads only. Rank positions and backlink index checks still use DataForSEO. Worker v1.57.

v1.56 2026-10-28

Added
  • New page Keyword Research (/keywords, menu after Rank Tracker): search volumes from Google Ads Keyword Planner through the Google Ads API, free with the shop's Ads account.
  • Google Ads connection: developer token (ADS_DEVELOPER_TOKEN, Basic access), account ID (ADS_CUSTOMER_ID), optional manager ID (ADS_LOGIN_CUSTOMER_ID) and API version (ADS_API_VERSION, default v26); sign-in with the Business Profiles OAuth client (or ADS_CLIENT_ID / ADS_CLIENT_SECRET) at /ads/oauth/callback, refresh token kept encrypted in D1. Setup steps on the page.
  • Worker: /api/kw/status, oauth-start, disconnect, keywords (GET / POST / DELETE), refresh, line, ideas (behind the authenticator lock); D1 table kw_keywords (created automatically).
Changed
  • Rank Tracker search volumes come from Google Ads when it is connected (free), otherwise from DataForSEO as before; the 12 monthly volumes are saved too, and the volume tooltip shows the peak month and the source.
  • Menu: Keyword Research added after Rank Tracker on every page. Worker v1.56. geotag.py unchanged (still 1.14).

v1.55 2026-10-27

Added
  • edge-logger.js (Worker rowabi-edge-log) can log paths that another Worker answers: /llms.txt is passed on, unchanged, to the Worker rowabi-llms-page through a Service binding named LLMS, so AI bots reading llms.txt now appear in the Visitor Log. The route www.rowabi.com/llms.txt points to rowabi-edge-log; the code of rowabi-llms-page is not changed. If that Worker fails, the request goes to Shopify.
  • Ready for later: a Service binding MD (Worker rowabi-md) would do the same for /products/, /collections/, /blogs/, /pages/ once their routes point to rowabi-edge-log.
Changed
  • Only edge-logger.js changed (update rowabi-edge-log); the geo-tag Worker is functionally unchanged (version number and What's new only).

v1.54 2026-10-27

Added
  • Language switch English | Tiếng Việt in the footer of every page (remembered in this browser). Vietnamese mode translates headings, buttons, labels, placeholders, tooltips, table headers, notes, status messages and confirm dialogs, also the texts the pages draw later (tables, reports, charts), and switching back to English restores the original texts. Names of other products' menus and buttons (Google, Shopify, Cloudflare…), metric codes, data (URLs, keywords, bot names) and the changelog stay in English.
  • src/i18n-vi.json: 1,752 English → Vietnamese texts (editable); src/i18n.js: the translator. build.py gives each page only the texts it uses.
Changed
  • Worker v1.54 (pages only; the API and edge-logger.js are unchanged, rowabi-edge-log does not need an update). geotag.py unchanged (still 1.14).

v1.53 2026-10-27

Removed
  • Shared mode of v1.52: the geo-tag Worker no longer contains the shop logger and never handles www.rowabi.com (variable EDGE_HOSTS not used any more). Only the separate Worker rowabi-edge-log (edge-logger.js) records the shop; geo-tag keeps the Visitor Log page, its API and the 60-day clean-up.
Changed
  • Visitor Log setup steps: separate Worker only, routes added in rowabi.com → Workers Routes, and "Always Use HTTPS" off with a Redirect Rule that keeps Shopify's certificate path on http.
  • build.py checks that worker-single.js never contains the logger; edge-logger.js stays a single file with src/edge-core.js inlined. Worker v1.53. geotag.py unchanged (still 1.14).

v1.52 2026-10-27

Added
  • Visitor Log shared mode: the route www.rowabi.com/* can point to the existing geo-tag Worker instead of a separate one. For the shop hostnames (Text variable EDGE_HOSTS, default www.rowabi.com) the geo-tag Worker runs only the logger, first and before any tool code, and passes the request to Shopify; if anything fails it still fetches the page from Shopify.
  • Setup steps on the Visitor Log page explain both options: A shared with geo-tag (nothing new to create) or B separate rowabi-edge-log Worker (safest).
Changed
  • The logger code lives in one file, src/edge-core.js, used by both Workers; build.py inlines it into worker-single.js and edge-logger.js, so both stay single files to paste. Worker v1.52. geotag.py unchanged (still 1.14).

v1.51 2026-10-27

Added
  • New page Visitor Log (/logs, last item of the menu): every request to www.rowabi.com like a server log, recorded by Cloudflare in front of Shopify ("Orange-to-Orange").
  • New separate Worker file edge-logger.js (Worker rowabi-edge-log on the route www.rowabi.com/*): passes each request to Shopify unchanged, then writes one row to the D1 table edge_log (time, IP, method, page, query, status, redirect target, referrer, user agent, country / region / city, ASN / network, Cloudflare data center, response time, device, bot name and type, verified flag). People: pages and errors only (no images, theme files or background cart requests); bots: everything except theme files. Logging can never break the shop.
  • Setup steps on the page (DNS proxy, no "Always Use HTTPS", new Worker, D1 binding, routes with www.rowabi.com/cdn/* excluded, plan limits).
  • Worker: /api/logs/status, summary, visitors, rows, export (behind the authenticator lock); the cron deletes log lines older than 60 days (Text variable LOG_DAYS).
Changed
  • Menu: Visitor Log added after Traffic Sources on every page. build.py also writes edge-logger.js with the current version. Worker v1.51. geotag.py unchanged (still 1.14).

v1.50 2026-10-27

Changed
  • Traffic Sources users now match GA4:
  • If GA4 refuses the extra user reports, the page falls back to the old sums and says so under the cards.
  • Worker: up to 3 extra GA4 batches per report (users by new / returning, property and group totals); report cache key v3. Worker v1.50. geotag.py unchanged (still 1.14).

v1.49 2026-10-27

Added
  • Favicon (Rowabi "Rw" logo) and theme color on every page.
  • Index Tool → Scan the whole website: collects every URL from the Shopify Admin API (published products, collections, pages, blog posts, blogs), the sitemap and the pages Search Console has seen in the last 16 months; labels each URL as page / blog post / collection / product / blog / home / other and adds it to the list.
  • Check Google index of all unchecked and Re-check pages older than 7 days: URL Inspection in priority order (pages → blog posts → collections → products), progress bar, Stop, pause under Google's 600-per-minute limit, stops cleanly at the 2,000-per-day limit.
  • Optional automatic checks in the Cron Trigger: about 20 pages every 20 minutes (new pages first, then every page again after 7 days).
  • Why a page is not indexed: every row shows the reason and what to do (noindex, blocked by robots.txt, redirect, 404, soft 404, alternate canonical, duplicate / Google chose another canonical, crawled or discovered but not indexed, unknown to Google, server error, 401/403…). New Index coverage card: indexed / not indexed / not checked per page type and the list of reasons with counts; click to filter.
  • Pages list: Type column, type and reason filters, Show 50 / 100 / 500 / All; CSV with type, reason, what to do, robots, fetch, canonicals and where the URL was found.
  • Worker: POST /api/idx/scan, POST /api/idx/auto; idx_urls gets page_type and found_in (added automatically).
Changed
  • "What's new" moved from the header to the footer, next to "Rowabi Lighting - Mini Tools - vX".
  • Sitemap reading uses browser-like headers and reports when the site's bot protection blocks it. Worker v1.49. geotag.py unchanged (still 1.14).

v1.48 2026-10-27

Added
  • Business Profiles (uploaded reports): Searches that showed the profiles. Paste each profile's "Searches breakdown" from its Performance page (the bulk report has no search terms): "1. lighting stores 16", "bathroom < 15", lines copied straight from Google or "term, number" all work, with a preview of what was read. Saved with the report per profile; view all profiles merged or one profile; "< 15" kept as Google shows it; total = sum of the numbers like in Google; CSV; remove a profile's terms. Uploading the same period again keeps the pasted terms.
  • Traffic sources → Landing pages: Add to carts and Checkouts per landing page (also when drilling down to one source, e.g. openai / paid), in the CSV too. Worker: the landing page report reads the second GA4 metric set (add to carts, checkouts, new users, views, transactions).
  • Rows per table: AI assistants, Channel groups, Sources / Campaigns and Landing pages show 10 rows by default with a Show 10 / 20 / 50 / 100 / 500 / All box (remembered in this browser); sorting always uses all rows.
  • Landing pages: click a column title to sort (sessions, change, engagement, add to carts, checkouts, purchases, revenue, key events, Search Console clicks, impressions, CTR, position).
Changed
  • Landing pages: "Show all" button replaced by the Show box; column order Sessions · Change · Eng. rate · Add to carts · Checkouts · Purchases · Revenue · Key events. Sources no longer stop at 200 rows. Worker v1.48. geotag.py unchanged (still 1.14).

v1.47 2026-10-27

Changed
  • Wider pages on big screens: the content width now grows with the window from 1200px up to 1440px (was fixed at 1240px); phones and tablets unchanged.
  • Menu order: UTM Builder moved next to Geo Tag photos (Geo Tag photos · UTM Builder · GeoCircle KML · Rank Tracker · Index Tool · Traffic Sources) on every page.
  • Business Profiles: the default utm_content is the profile name + city (e.g. rowabi-houston, cecilia-lighting-boston), because several profiles share the name "Rowabi …"; the UTM link list shows the city under each profile. GA4 rows match a profile by name + city; a value that fits several profiles (e.g. just rowabi) is shown as not matched instead of guessing.
Fixed
  • Business Profiles upload: the period in the real Google file name ("GMB insights Performance Report - 2026-7-27 - 2026-10-24 - ….csv", months and days without a leading zero) is now read automatically. Tested with the real report: the 6 profiles, views by Search / Maps × mobile / desktop, calls, directions and website clicks are read correctly.
  • Worker v1.47 (pages only, API unchanged). geotag.py unchanged (still 1.14).

v1.46 2026-10-27

Added
  • UTM Builder → Analytics → Top links: a checkbox on every link and "select all" in the header; a bar shows how many links are selected (and their clicks) with Delete selected and Clear selection. Deleting several links asks once and removes the links and their clicks together.
  • Worker: DELETE /api/utm/links?ids=a,b,c (up to 200 links per request; ?id= for one link still works).
Changed
  • Worker v1.46. geotag.py unchanged (still 1.14).

v1.45 2026-10-27

Added
  • UTM Builder page info: when the store's bot protection answers instead of the page (Shopify "Verifying your connection…", HTTP 429), the title and meta description are read from the Shopify Admin API (product, collection, page, blog post, blog, home page SEO settings). Shows whether an SEO title / meta description is set or Shopify falls back to the name / content. Optional Worker secrets: SHOPIFY_SHOP, SHOPIFY_CLIENT_ID, SHOPIFY_CLIENT_SECRET (Dev Dashboard app, client credentials, token renewed every 24 h) or SHOPIFY_ADMIN_TOKEN; SHOPIFY_DOMAINS (default rowabi.com).
Fixed
  • UTM Builder showed the bot-protection page ("Verifying your connection…", HTTP 429) as the page title: these pages are now recognised and never shown as the title; without the Shopify connection a clear message says how to fix it.
Changed
  • The page is requested like a browser (Chrome user agent and headers), retried once with the link-checker user agent, and error pages are no longer cached. Worker v1.45. geotag.py unchanged (still 1.14).

v1.44 2026-10-26

Added
  • Business Profiles works without the Business Profile API: new switch Uploaded reports | Google API (uploaded reports by default).
  • Upload a report: drop the file from Business Profile Manager → Actions → Insights → Download Report (.csv or .xlsx, read in the browser). The columns (views by Search / Maps × mobile / desktop, calls, directions, website clicks, bookings, messages) are found by name; preview before saving; the period is read from the file when it says it, else picked (Last month / This month / Last 28 days). Saved in D1 (table gbp_uploads); the same period uploaded again replaces the old one.
  • Saved reports view: pick a period and what to compare it with (auto = the previous report of the same length); KPIs (profile views, interactions, calls, directions, website clicks, GA4 website visits), how people found the profiles, interactions, profiles table with a calls / directions / website bar, change against the compared report, GA4 visits, visits ÷ clicks and GA4 revenue per profile; history chart across all saved reports; CSV; delete a report.
  • Website visits from the profiles (GA4): sessions, users, engagement, views, add to carts, checkouts, purchases, revenue and key events of visits from Business Profile UTM links, per profile (matched by utm_content), with the comparison period and the top landing pages; CSV. Worker: GET /api/ga/gbp-visits (campaign or source contains gbp / gmb / business_profile).
  • UTM links for the Website button: one ready link per profile (utm_source=google, utm_medium=organic, utm_campaign=gbp_website, utm_content = profile, editable), copy one or all.
Changed
  • The Google API part (Connect, Overview, Calls, Directions, Website clicks, Searches) moved under the Google API switch, unchanged. The date picker is hidden while viewing saved reports (each report has its own period). Worker v1.44. geotag.py unchanged (still 1.14).

v1.43 2026-10-26

Added
  • Traffic Sources: the GA4 exploration metrics Add to carts, Checkouts, Purchases, Purchase revenue, Sessions, Active users, New users, Returning users, Views and Transactions (plus engagement rate and key events) in the AI assistants, Channel groups and Sources tables.
  • Second KPI row: Active users, New users, Returning users, Views, Add to carts, Checkouts, each compared with the comparison period.
  • Step 2 switch Source / medium | Campaign: GA4 Session campaign + source + medium rows with the same metrics (filtered by the channel group chosen in step 1, searchable, CSV). Clicking a campaign row opens the landing pages of its source / medium.
  • Click a column title to sort any of these tables (click again to reverse); hover a number to see the comparison-period value.
  • Tooltips for the new metrics; returning users = active users − new users.
  • Worker: two extra GA4 requests per report (add to carts, checkouts, new users, views, transactions) and a campaign / source / medium report (up to 1,000 rows).
Changed
  • The metric tables scroll sideways with the first column fixed. The sources CSV has all the new metrics; the campaign CSV is new.
  • "Users" is now GA4 Active users (same as the GA4 exploration). The report cache key changed, so the first load after the update fetches fresh data. Worker v1.43. geotag.py unchanged (still 1.14).

v1.42 2026-10-26

Added
  • Traffic Sources: third tab Business Profiles (Google Business Profile), for all profiles of the connected Google account (e.g. the 6 Rowabi / Cecilia Lighting locations) or one at a time.
  • Overview: KPIs (profile views, searches, interactions, calls, directions, website clicks) compared with the comparison period of the date picker; "how people found the profiles" (Google Search / Maps × mobile / desktop); interactions breakdown; interactions per day with switchable lines; profiles table (click a row to see one profile, click a column to sort); top search terms.
  • Calls, Directions, Website clicks tabs: that metric per day, average per weekday, profiles ranked by it.
  • Searches tab: the search terms that showed the profiles (by whole month; "< 15" when Google hides the number), with CSV.
  • Connect button with Google sign-in (OAuth, scope business.manage, read-only use); the connection is kept encrypted in D1; Disconnect revokes it. Setup steps on the page (API access request, the 3 APIs to enable, OAuth client with the redirect URI shown, secrets GBP_CLIENT_ID / GBP_CLIENT_SECRET). Clear messages when Google has not approved the API access yet (quota 0) or an API is not enabled.
  • Worker: /api/gbp/status, oauth-start, disconnect, locations, report, keywords, and /gbp/oauth/callback; reports cached 6 hours, search terms 12 hours.
Changed
  • Worker: GA4 and Business Profiles share one date-range parser. Worker v1.42. geotag.py unchanged (still 1.14).

v1.41 2026-10-26

Fixed
  • AI insights region-block hint and setup steps now point to the right place: Worker → Settings → Runtime → Placement → Region → AWS us-east-1 (the Worker then always runs in the US, so Claude / OpenAI are not blocked). wrangler.jsonc uses "placement": { "region": "aws:us-east-1" }.
Changed
  • Worker v1.41 (hint text only). geotag.py unchanged (still 1.14).

v1.40 2026-10-26

Changed
  • AI insights: when Claude or OpenAI is region-blocked (403 "Request not allowed", e.g. from Cloudflare's Hong Kong data center) and the Workers AI binding AI exists, the request is answered by Cloudflare Workers AI automatically; the status line says so.
  • Fixed the hint: Placement is under Worker → Settings → General → Placement → Smart (Cloudflare needs up to 15 minutes to analyse). wrangler.jsonc explains the sure fix for Wrangler deploys: "placement": { "region": "aws:us-east-1" }.
  • Worker v1.40. geotag.py unchanged (still 1.14).

v1.39 2026-10-26

Added
  • AI insights: Cloudflare Workers AI as a free option (Llama 3.3 70B, about 10 runs a day free, no API key, never region-blocked). Turn it on with the Workers AI binding AI (Settings → Bindings → Add → Workers AI); it then shows in the model list. Optional variable WORKERS_AI_MODEL.
  • When Claude or OpenAI answers 403 "Request not allowed", the message now says which Cloudflare data center the request came from (e.g. HKG Hong Kong) and how to fix it: Worker → Settings → Runtime → Placement → Smart, or use Workers AI.
  • wrangler.jsonc: Smart Placement and the Workers AI binding for deploys with Wrangler.
Changed
  • /api/ga/status returns the Cloudflare data center (colo). Worker v1.39. geotag.py unchanged (still 1.14).

v1.38 2026-10-26

Fixed
  • AI insights with Claude Sonnet 5.5: "Claude API 400: tool_choice: type "tool" and "any" are not supported for this model". The request now lets Claude choose the report tool by itself (tool_choice auto); if a model refuses tools, it asks again for plain JSON. Cut-off answers are still salvaged, and the output limit is higher (16,000 tokens per batch of 10 pages).
Changed
  • Worker v1.38. geotag.py unchanged (still 1.14).

v1.37 2026-10-26

Fixed
  • AI insights: "The AI answer was not valid JSON". The answer for 25 pages in Vietnamese was longer than the AI's output limit and got cut off. Now the pages go to the AI in batches of 10 (in parallel, so it is also faster) plus one short request for the summary; Claude answers through structured output (always valid JSON); if an answer is still cut off, every complete page is kept and the status line says so; a failed batch no longer loses the others.
  • UTM Builder: "Clean values (lowercase, spaces → _)" stays on one line next to the campaign name.
  • Top pages: CTR change of less than 0.005 points shows "0 pt" instead of "−0.00 pt".
Changed
  • Traffic Sources: the date button is one line: preset · dates · "vs" comparison dates.
  • Top pages: Download CSV moved next to the table title.
  • Top pages and Landing pages: under each number the comparison value comes first, then the change (e.g. "vs 7" then "▲ 143%", position "vs 10.0" then "▲ 5.9", CTR "vs 0.03%" then "0 pt"); the filters sit on one row under the page-type tabs.
  • Worker v1.37. geotag.py unchanged (still 1.14).

v1.36 2026-10-26

Changed
  • All pages use the same wider layout: content up to 1240px (was 880px; Traffic Sources 1180px), with side margins that grow with the screen, so text is less squeezed and pages look the same from one tool to the next.
  • Wide screens (1080px and up) use two balanced columns on the form pages, in the same step order: Geo Tag photos (photos + location | details + download), GeoCircle KML (settings | map preview + Generate, which stays in view while scrolling), UTM Builder (website + custom link | generated links + history). Rank Tracker, Index Tool and Traffic Sources get the extra width for their tables.
  • Tablets: one column, up to 920px wide.
  • Phones: the page menu is one swipeable row that opens on the current page; tabs scroll sideways instead of wrapping; smaller card padding, hints no longer indented; compact KPI cards and tables; dialogs use almost the full screen; text fields use 16px text so iPhone Safari does not zoom in; Traffic Sources date picker opens full width, toolbars and AI buttons stack neatly.
  • GeoCircle KML map preview height adapts to the screen (300–460px).
  • UTM Builder: "Tags for generated links" and "Manage tags" wrap cleanly on small screens.
  • Worker v1.36 (embedded pages only; API code unchanged). geotag.py unchanged (still 1.14).

v1.35 2026-10-26

Added
  • Traffic Sources → Top pages: AI insights with Claude or ChatGPT (OpenAI). Pick a model (Claude Sonnet 5.5 / Claude Haiku 5.5 / GPT-6 Sol / GPT-6 Luna; only the ones with a key are listed) and click AI insights: the AI reads the pages shown (up to 30) with their Search Console and GA4 numbers for both periods, plus the top search queries of the 10 biggest pages, and writes for each page an insight, an action and a priority (high / medium / low), in Vietnamese, plus an AI summary of the page type (biggest wins, risks, what to do first).
  • AI results are saved in D1 (table ai_insights): the same numbers open the saved result for free; Regenerate asks again. The status line shows the model, time, tokens and cost of the run.
  • AI / Rules switch to compare with the rule-based insights; the CSV has both (insight_rules, action_rules, ai_priority, ai_insight, ai_action).
  • Setup steps on the page for the secrets ANTHROPIC_API_KEY and OPENAI_API_KEY (optional variables ANTHROPIC_MODEL, ANTHROPIC_MODEL_FAST, OPENAI_MODEL, OPENAI_MODEL_FAST to change models).
  • Worker: POST /api/ga/ai (behind the authenticator lock).
Changed
  • Worker v1.35. geotag.py unchanged (still 1.14).

v1.34 2026-10-26

Added
  • Traffic Sources: new Top pages view (tab next to "Traffic sources"), a summary dashboard of Products, Collections, Blog posts and Other pages.
  • For each page: Search Console clicks, impressions, CTR and average position, plus GA4 sessions, engagement, key events and revenue, all for the same dates, with the change against the comparison period under each number (▲ / ▼, CTR in percentage points, position improved = ▲).
  • KPIs per page type: search clicks (and share of the site), impressions, CTR, average position, GA4 sessions and revenue, compared with the comparison period.
  • Insight and Action for every page, rule-based: clicks falling or growing (and why: position, impressions or CTR), position falling 2+ places, top-3 or page-1 positions with a CTR far below the typical rate (quick wins), page-2 pages close to page 1, pages beyond page 2, high CTR with few impressions, visits but no search impressions, product visits without purchases, top earners, blog posts without key events.
  • Controls: GA4 filter (all channels / Google organic only / AI assistants only), rank by clicks, impressions, CTR, best position, click change, sessions or revenue; Top 10 / 25 / 50 / 100; search; Queries per page; CSV with current and comparison values plus insight and action.
  • The chosen view, page type, filter, ranking and row count are remembered in this browser.
Changed
  • CTR below 10% is shown with 2 decimals (e.g. 0.03%) on the Traffic Sources page.
  • Worker v1.34 (embedded pages only; API code unchanged). geotag.py unchanged (still 1.14).

v1.33 2026-10-26

Added
  • Traffic Sources, step 3 Landing pages: Google Search Console columns next to the GA4 numbers: clicks, impressions, CTR and average position of each page for the same dates (Google organic search, all queries; www / non-www and ?variant= URLs merged by path). Uses the same service account as the Index Tool.
  • Queries button on each page with search impressions: opens the search terms of that page (clicks, impressions, CTR, position with color: top 3, page 1, page 2), top 20 with "Show all" (up to 500) and a CSV.
  • Landing pages CSV has the Search Console columns too.
  • Worker: /api/ga/gsc-pages and /api/ga/gsc-queries (cached 1 hour).
Fixed
  • The (?) tooltip icon no longer drops to a line of its own (e.g. under "AI assistant sessions"): it stays with the last word of the title.
Changed
  • Worker v1.33. geotag.py unchanged (still 1.14).

v1.32 2026-10-26

Changed
  • Traffic Sources works as a drill-down: 1 Channel groups → click a group (e.g. Direct) → 2 Sources of that group → click a source → 3 Landing pages of that source. Without a source picked, step 3 shows the landing pages of the whole group (with chips for the sources that sent them); with no group picked, of all traffic.
  • AI assistants works the same way: click an AI source → its landing pages; the button "Landing pages from all AI assistants" opens step 3 for the whole AI group.
  • A sticky drill-down bar (All channel groups › group › source) shows the current selection; click a step to go back, × to remove it. Clicking a selected row again also goes back one step.
  • Landing pages now have a Change column (against the comparison period), show 50 rows with "Show all" (up to 500), and the CSV is named after the selection.
  • Page order: KPIs → Sessions per day → AI assistants → steps 1, 2, 3.
  • Worker v1.32: new POST /api/ga/pages (landing pages of a channel group or one source / medium, cached 15 minutes); the report no longer loads AI landing pages up front. geotag.py unchanged (still 1.14).

v1.31 2026-10-26

Added
  • Traffic Sources: date picker like GA4. Presets: Custom, Today, Yesterday, This week (Sun – Today / Mon – Today), Last 7 days, Last week (Sun – Sat / Mon – Sun), Last 14 / 28 / 30 days, This month, Last month, Last 90 days, Quarter to date, Last 12 months, This year (Jan – Today), Last calendar year. Calendar to click a start and an end date, or type them; Cancel / Apply.
  • Compare switch: previous period, same period last year, or a custom period (shown in the calendar and on the button). With Compare off, the change columns are hidden.
  • The chosen range is remembered in this browser; presets move with today (e.g. "Last 28 days" stays the last 28 days).
  • Small (?) tooltips on every section title (Channel groups, Sessions per day, AI assistants, Landing pages, All sources, AI source list), on the KPI cards and on the column headers Share, Change, Engagement, Key events and GA4 channel: hover (or tap on a phone) to read what it means.
  • Worker: /api/ga/report accepts cmp=none|prev|year|custom with cmpStart / cmpEnd; end dates after today are cut to today.
Changed
  • Worker v1.31. geotag.py unchanged (still 1.14).

v1.30 2026-10-26

Added
  • New page Traffic Sources (/traffic, link at the top of every page), behind the authenticator lock. It reads Google Analytics 4 (Data API, free, read-only) with the same service account as the Index Tool.
  • Sessions grouped into AI assistants, Google organic, Bing organic, Yahoo organic, DuckDuckGo organic, other search engines, paid ads, email, organic social, referral, direct and other. KPIs (sessions, AI sessions and share, engagement rate, key events, purchases, revenue) compared with the previous period; channel group table; sessions-per-day chart with groups to switch on and off; AI assistants table (source / medium, GA4 channel, engagement, key events, purchases, revenue) with a comparison against GA4's own "AI Assistant" channel; landing pages from AI assistants; all sources with group filter, search and CSV.
  • Editable AI source list (chatgpt, perplexity, gemini, copilot, claude, deepseek, grok…) saved in D1, and a ready-to-copy regex with steps to create the same "AI Assistants" custom channel group inside GA4.
  • Worker: /api/ga/status, /api/ga/report (results cached 15 minutes, fresh=1 to skip the cache), /api/ga/rules; variable GA4_PROPERTY; the service account token now also asks for GA4 read access.
Changed
  • Geo Tag photos: the Location search now starts on Business name (Address is still in the list).
  • Worker v1.30. geotag.py unchanged (still 1.14).

v1.29 2026-10-26

Changed
  • Geo Tag photos and Index Tool: the How this tool works box now shows only its title and the short intro; click the + button (or the title) to open the details, − to close them again. Closed by default.
  • Worker v1.29 (embedded pages only; API code unchanged). geotag.py unchanged (still 1.14).

v1.28 2026-10-26

Added
  • Geo Tag photos and Index Tool: a How this tool works box at the bottom of the page (what the tool does step by step, what it writes or checks, limits and tips).
  • UTM Builder tags: pick tags for the generated links (step 1) or the custom link (step 2); tag chips on every link; tag button on each link to change its tags; filter the list by tag (or "No tag"); Manage tags to add, rename, recolor (7 colors) and delete tags. Renaming or deleting updates every link that has the tag; links and clicks are never deleted. Tags column in the CSV.
  • With click tracking on, tags are saved in D1 with the tracked links, and Analytics gets a tag filter (KPIs, trend, top links, sources, countries, devices, recent clicks). Without tracking, tags are kept in this browser (and in History).
  • UTM Builder page info: after you type the website URL, the page title and meta description appear under it, with their length (warning above ~60 / ~160 characters), the share image (og:image), redirects, a canonical pointing elsewhere and noindex.
  • Worker: /api/utm/meta?url= (works without the D1 database), /api/utm/tags (GET/POST/DELETE), /api/utm/link-tags, tag filter on /api/utm/stats; table utm_tags and column utm_links.tags are created automatically.
Changed
  • Worker v1.28. geotag.py unchanged (still 1.14).

v1.27 2026-10-26

Added
  • Index Tool: when Search Console answers "User does not have sufficient permission", the Connections box now shows which properties the service account can open (and with which permission) and says exactly what to change: add the service account as a user of the rowabi.com property, set GSC_SITE to the right property (e.g. a URL-prefix property https://www.rowabi.com/), or upgrade Restricted to Full for sitemaps.
  • Worker: /api/idx/sites.
Changed
  • Worker v1.27. geotag.py unchanged (still 1.14).

v1.26 2026-10-26

Added
  • New page Index Tool (/indexing, link at the top of every page), behind the authenticator lock.
  • My pages (rowabi.com): add URLs or import them from the sitemap (products, collections, pages, blog posts); check the Google index status in bulk with the Search Console URL Inspection API (indexed or not, coverage, noindex / robots.txt, last crawl, canonical Google picked; 2,000 checks a day); "Request indexing" opens the URL in Search Console; submit selected URLs to Bing (Bing URL Submission API, up to 10,000 a day) with the remaining quota shown; list, submit and resubmit sitemaps in Search Console; KPIs, filters, CSV.
  • Backlinks: paste the pages that link to you; live check (HTTP status, redirects, link to your domain found or missing, dofollow / nofollow / ugc / sponsored, anchor text, noindex on the page); Google index check per backlink (DataForSEO "site:" search, about $0.003 each); weekly automatic re-check of links and of not-yet-indexed backlinks; KPIs, filters, CSV.
  • Worker: /api/idx/* (urls, import, inspect, bing, bing-quota, sitemaps, backlinks, check, index, collect) with a Google service account (secret GSC_SERVICE_ACCOUNT, variable GSC_SITE) and Bing (secret BING_API_KEY, variable BING_SITE); the cron alternates between the Rank Tracker and the Index Tool.
Changed
  • Worker v1.26. geotag.py unchanged (still 1.14).

v1.25 2026-10-26

Fixed
  • Rank Tracker: the Volume column stayed at "…" for keywords added before v1.23 (volumes were only loaded for newly added keywords or by the monthly cron). Opening a project now loads missing volumes automatically, and the result or the DataForSEO error is shown under the project info instead of being hidden.
  • Rank Tracker: "URL found" no longer shows the long tracking parameters Google adds (srsltid, utm_*, gclid); the full link is still opened on click.
Changed
  • Worker v1.25: only the embedded pages changed, API code unchanged. geotag.py unchanged (still 1.14).

v1.24 2026-10-26

Changed
  • Authenticator lock setup is owner-only: the "Set up lock" button appears only when the page is opened with ?setup (e.g. /rank?setup) and only while TOTP_SECRET is not set. Visitors never see a setup QR code; once the secret is saved no page can show it again.
  • Setup box: warning to scan only with the owner's phone and not to screenshot or share the QR code / key; each opening makes a new key that is thrown away when the box closes.
  • Worker v1.24: only the embedded pages changed, API code unchanged. geotag.py unchanged (still 1.14).

v1.23 2026-10-26

Added
  • Rank Tracker: Volume column (average monthly Google searches in the project's country, Google Ads data from DataForSEO), with CPC and competition on hover; included in the CSV. New keywords get their volume right away, all volumes refresh by themselves about once a month (cron), and "Refresh search volumes" loads them on demand. Cost about $0.09 per request of up to 1,000 keywords.
  • Authenticator lock for GeoCircle KML and Rank Tracker: after the Worker secret TOTP_SECRET is set, these pages ask for a 6-digit code from Google / Microsoft Authenticator; one unlock lasts 12 hours on that browser. The Worker also refuses the paid APIs (/api/nearby, /api/rank/*) without it. At most 5 wrong codes per IP in 15 minutes.
  • "Set up lock" button: creates the key in the browser, shows the QR code for the app, tests a code, and explains where to add TOTP_SECRET. "Lock" button locks the tools again.
Changed
  • Worker v1.23. geotag.py unchanged (still 1.14).

v1.22 2026-10-26

Changed
  • Footer on every page: "Rowabi Lighting - Mini Tools - v1.22" (the version comes from this changelog); the Worker version is no longer shown.
  • UTM Builder: history kept for 90 days (was 10 days), up to 200 lists.
  • UTM Builder: short guide under "Generated links" on how to get a tracked short link and a custom short path.
  • Worker v1.22: only the embedded pages changed, API code unchanged. geotag.py unchanged (still 1.14).
Removed
  • "Changelog" link in the footer (the changelog stays under the "What's new" button at the top).

v1.21 2026-10-26

Added
  • New page Rank Tracker (/rank, link at the top of every page), similar to SERPROBOT: projects with keywords, daily or on-demand position checks, history.
  • Project settings: domain or exact URL; search engine Google, Google Local (Maps), Bing, Yahoo or YouTube; device desktop or phone; results location United States or Vietnam; Google region google.com or google.com.vn; results language English or Vietnamese; optional search location (city, address or ZIP, Google and Google Maps only); depth top 10 / 20 / 30 / 50 / 100 (default top 50); daily check hour or manual; up to 5 competitors; notes.
  • Project page: average position, improved, top 3 / 10 / 30 / depth cards compared with the last check or 7 / 30 / 90 days; keyword table with change, latest, best, first, competitor positions, last update, URL found, check now, link to the real search, history chart, delete; summary graph; add keywords; export CSV.
  • Dashboard: projects with keywords in top 10 / ranked / total, frequency, last update, cost in the last 30 days; DataForSEO balance.
  • Worker: /api/rank/* (projects, project, keywords, check, collect, balance) using the DataForSEO SERP API Standard queue; Cron Trigger handler scheduled() collects results and starts each project's daily check. Needs secrets DATAFORSEO_LOGIN, DATAFORSEO_PASSWORD and the D1 binding DB.
Changed
  • Shared styles for tabs, card headers, icon buttons, KPI cards and tables moved to src/base.css; the page links wrap on small screens. geotag.py unchanged (still 1.14).

v1.20 2026-10-26

Changed
  • UTM Builder: the tracked short link under each link is now clickable (opens in a new tab) and has its own copy button; short links in Analytics → Top links are clickable too.
  • Worker v1.20: only the embedded pages changed, API code unchanged. geotag.py unchanged (still 1.14).

v1.19 2026-10-26

Added
  • Short link domain: set the Worker variable SHORT_DOMAIN (e.g. go.rowabi.com) after adding it as a Custom Domain of the Worker. Short links become https://go.rowabi.com/<path>. That domain only serves redirects: its home page goes to rowabi.com and the tool pages / API are not reachable there, so it needs no Cloudflare Access exception.
  • UTM Builder: "Custom short path" box, e.g. go.rowabi.com/bfcm-email instead of a random code; lowercase letters, numbers, - and _; checked for duplicates; works in any letter case.
Changed
  • Old short links <worker>/r/<code> keep working, and go.rowabi.com/r/<code> works too.
  • wrangler.jsonc: assets served through the Worker (run_worker_first, binding ASSETS); comments translated to English; SHORT_DOMAIN variable and D1 example added.
  • Worker v1.19. geotag.py unchanged (still 1.14).

v1.18 2026-10-26

Added
  • New page UTM Builder (/utm, link at the top of every page) with two tabs: Link builder and Analytics.
  • Generate all links: one UTM link per channel (Google Ads, Google Business Profile, Google My Maps, Facebook, Instagram, Pinterest, YouTube, TikTok, LinkedIn, Twitter/X, Email, QR code, AI search, ChatGPT, Perplexity); optional campaign name for all links.
  • Custom UTM link: boxes for utm_source, utm_medium, utm_campaign, utm_content, utm_term with a short explanation under each, live preview, optional clean-up (lowercase, spaces → _).
  • Link list: copy, QR code (PNG download), edit, remove, copy all, CSV download.
  • History: every list is saved in this browser for 10 days, then removed automatically.
  • Click tracking (needs a Cloudflare D1 database bound as DB): "Save for tracking" creates short links /r/<code>; each click is logged (time, country, city, device, browser, referrer; no IP address) and redirects to the UTM link. Bots and link previews are flagged and not counted.
  • Analytics: total clicks, tracked links, average clicks per link, bot visits; click trends per day, top links, clicks by source / country / device, recent clicks; 7 / 30 / 90 days or 12 months.
  • Worker: /api/utm/links (list, save, delete), /api/utm/stats, /r/<code> redirect; /api/health also returns utm; worker-single.js serves /utm.
Changed
  • Shared styles: segmented tabs; the page links wrap on small screens. geotag.py unchanged (still 1.14).

v1.17 2026-10-26

Added
  • GeoCircle KML: new step "Photos and videos": paste image links, Google Drive file links or YouTube links (one per line); they show in the marker popup in My Maps (gx_media_links). Keyword markers get one link each in turn, all links, or none; option to add them to points of interest too. Google Drive file links are converted to direct image links.
  • GeoCircle KML: "Social profiles and more links": Facebook, Instagram, Pinterest, YouTube, X (Twitter), TikTok, LinkedIn, Google Drive folder, Google Site, plus other links (one per line).
  • GeoCircle KML: "Marker spacing" option: same number on every circle, or even spacing (outer circle gets the full number, inner circles fewer, at least 8).
Changed
  • Marker popups: each item on its own line (name, address, pinpoint message, website, phone, Google Business Profile, social links), with a blank line between sections.
  • The KML file is named after the map title as typed (e.g. Rattan Pendant Light Outdoor.kml); after generating, the page reminds you to paste the title into "Untitled map" because My Maps does not take the map title from the file.
  • Worker v1.17: only the embedded pages changed, API code unchanged. geotag.py unchanged (still 1.14).
Fixed
  • Marker popups in My Maps showed everything as one paragraph: My Maps ignores plain line breaks, so descriptions now use <br>.

v1.16 2026-10-25

Added
  • New page GeoCircle KML (/kml, link at the top of both pages): builds a KML file for Google My Maps with concentric circles around the business, keyword markers on every circle and points of interest.
  • Business location: search by address or business name, paste a Google Maps link or coordinates; Service Area Business switch (no street address in the KML, optional service area description instead).
  • Business details shown on every marker: name, website, phone, Google Business Profile link, pinpoint message.
  • Keywords: one per line, markers take them in turn; map title (first keyword if empty).
  • Coverage radius 5 / 10 / 15 / 30 / 50 / 100 miles (2 / 3 / 4 / 6 / 10 / 20 circles), 25 / 50 / 75 keyword markers per circle.
  • Points of interest: airports, museums, parks, restaurants, hospitals, schools, shopping malls, tourist attractions, stadiums, universities; up to 20 places per type within the radius (max 31 miles). Google mode uses Places API (New) Nearby Search; free mode uses OpenStreetMap (Overpass).
  • Live preview: circles, sample markers and found places (Google map in Google mode, OpenStreetMap in free mode).
  • History: the 15 most recently generated maps are saved on this browser and can be loaded again.
  • Worker: endpoint /api/nearby (Places Nearby Search, 10 allowed types, radius up to 50 km); worker-single.js serves /kml.
Changed
  • Shared code moved to src/common.js (source detection, Google Maps link reading, place search) and src/base.css (styles); build.py builds both pages.
  • Geo Tag photos page: link bar to switch pages; behavior unchanged. geotag.py unchanged (still 1.14).

v1.15 2026-10-25

Changed
  • Font changed to Bricolage Grotesque (Google Fonts), replacing Be Vietnam Pro: font-family: "Bricolage Grotesque", sans-serif.
  • Worker v1.15: only the embedded page changed, API code unchanged. geotag.py unchanged (still 1.14).

v1.14 2026-10-25

Changed
  • Everything is now in English: page interface, messages, search results, changelog, Worker error messages, geotag.py help and output, and code comments.
  • Search results always come back in English (previously Vietnamese when searching in Vietnam).
  • Country list: "Vietnam" instead of "Việt Nam"; page language set to en.
  • Changelog section names: Added / Changed / Fixed / Removed.
  • Worker v1.14: only error messages and comments translated, API behavior unchanged. geotag.py v1.14: only text translated, behavior unchanged.

v1.13 2026-10-25

Added
  • New interface: 4 steps (Choose photos, Location, Photo details, Tag and download), warm brown rattan palette, Be Vietnam Pro font, automatic dark mode.
  • Drag and drop photos onto the photo box; non-JPEG files are skipped and counted.
  • "What's new" button (next to the version number) and "Changelog" link at the bottom of the page: open the change list for every version.
  • Mode indicator (free / Google) at the top of the page.
  • build.py: builds the page and worker-single.js from page-template.html, src/app.js and CHANGELOG.md; the version number is taken from the CHANGELOG automatically.
Changed
  • Subject, Comments, Rating, Date taken, Program name, Authors, Credit and Copyright moved into the collapsible section "More fields for Windows and copyright".
  • The page is no longer edited by hand in index.html; edit page-template.html or src/app.js, then run python build.py.
  • Worker v1.13: only the embedded page changed, API code unchanged. geotag.py unchanged (still 1.10).

v1.12 2026-10-25

Changed
  • The Google map only appears once there are coordinates (previously an empty frame with a red ring showed on page load).
  • The "Not found" message in business-name mode includes an example with the state: "Aurelighting Tennessee".
  • Worker v1.12: only the embedded page changed, API code unchanged. geotag.py unchanged (still 1.10).
Fixed
  • Note on v1.11: a live test on the Worker showed "Aurelighting" was not found because a rare name needs a location ("Aurelighting Tennessee" returns 2158 Schneider Ln suite 956, Sevierville, TN), not because the business hides its address as guessed in v1.11. The v1.11 changes are kept since they are harmless.

v1.11 2026-10-25

Changed
  • Business-name search (Places) now also finds service-area businesses that hide their address (includePureServiceAreaBusinesses) and newly created businesses not yet open (includeFutureOpeningBusinesses). Google skips both groups by default.
  • Results without a public address are marked ⚠: the coordinates are only the approximate location Google shows.
  • The "Not found" message in business-name mode suggests pasting a Google Maps link.
Fixed
  • Searching "Aurelighting" (listed on Google Maps) returned not found.
  • Places results without coordinates: clear error instead of filling empty boxes.
  • geotag.py unchanged (still 1.10).

v1.10 2026-10-25

Added
  • Paste a Google Maps link into the search box to get the exact pin coordinates (reads the !3d…!4d… part of the link). Uses no Google quota.
  • Short links maps.app.goo.gl/...: the Worker expands them to the full link (endpoint /api/expand, accepts only maps.app.goo.gl / goo.gl).
  • Type coordinates like 29.8002425, -95.5672618 straight into the search box.
  • Google mode: "Search by" box with Address (Geocoding API) and Business name (Places API (New) Text Search, endpoint /api/places), giving coordinates that match the Google Maps pin.
  • Warning when a link only has @lat,lng (center of the map view, not the pin).
  • geotag.py: the place column also accepts Google Maps links (including short links) and lat, lng coordinates.
Changed
  • Worker: Google key check moved into each endpoint (so /api/expand works without a key); warnings stored per result (warn).
  • geotag.py bumped to 1.10 to match the web page and Worker version numbers.
Fixed
  • Address search gave coordinates offset from the Google Maps pin (e.g. 1500 Brittmoore Rd was about 220 m off): the Geocoding API returns the address point, not the business location. Three ways to get the exact pin added: paste a link, search by business name, type coordinates.

v1.9 2026-10-25

Added
  • Google mode: Google map (Maps Embed API, free) to check coordinates, with a red ring in the center marking the exact GPS point. Needs the MAPS_EMBED_KEY variable on the Worker; without it only the link is shown.
  • "Open these coordinates in Google Maps" link below the map, in both modes, no key needed.
  • ⚠ warning when a search result is too broad (whole country/state/county, or Google only partially matched); such results are not selected automatically.
  • Worker: /api/geocode also returns types, partial; /api/health also returns embedKey.
Changed
  • Editing the latitude/longitude boxes by hand updates the map and link.
  • Altitude rounded to 0.1 m (previously shown as 3236.24951171875).
Fixed
  • Searching "Rowabi" limited to the US: Google returned the center of the US (Colorado) and the page filled in those coordinates as if correct.

v1.8 2026-10-25

Added
  • Web: "Download" option: Automatic (1 photo downloads alone, several are zipped), Always zip, Download each file.
  • Web: the .zip contains one folder; "Folder name inside the .zip" box (empty = automatic geotag-YYYY-MM-DD-HHhMM from the computer's clock).
  • Web: JSZip 3.10.1 (jsdelivr) to build the .zip in the browser.
Changed
  • Web: all photos are tagged first, then downloaded, instead of downloading each as soon as it is done.
  • Duplicate file names: -2, -3 added based on the actual name (even with rename off), instead of photo order.
  • Worker v1.8: only the embedded page in worker-single.js changed; API code unchanged. geotag.py unchanged (still 1.7).

v1.7 2026-10-25

Added
  • Web: preview of selected photos (thumbnail, file name, size, dimensions); after tagging, the new file name is shown.
  • Web: × button to remove one photo, "Clear all" button; choosing more photos several times keeps the ones already chosen.
  • Web: green border for photos tagged successfully, red for errors.
  • Version number at the bottom of the page (plus the Worker version on Cloudflare); /api/health also returns version.
  • geotag.py --version.
Changed
  • Web: the photos processed come from the preview list instead of the file input directly.

v1.6 2026-10-25

Added
  • Country selector for place search (United States / Worldwide / Vietnam); geotag.py --country.
  • Worker: country, lang parameters for /api/geocode.
Changed
  • Searches are limited to the US by default, results in English; the map opens on the US.
Fixed
  • Short names (e.g. "Rowabi") matched a place in another country (Oman).

v1.5 2026-10-25

Added
  • One Worker serves both the web page and the API (/api/health, /api/geocode, /api/elevation).
  • worker-single.js: single-file version to paste straight into the Cloudflare dashboard.
  • The page detects its mode: Worker + Google key = Google, otherwise free services.
Changed
  • API paths moved to /api/...; requests from the page itself (same-origin) are accepted.
  • Rate limit namespace_id changed to its own number (58217) so it doesn't share a counter with other Workers.

v1.4 2026-10-25

Added
  • start.bat: opens the web version at http://localhost:8000.
Fixed
  • Confusing "Failed to fetch" message when opening the file directly: now explains the cause and the fix.

v1.3 2026-10-25

Added
  • Cloudflare Worker as a proxy that hides the Google API key (origin check, per-IP request limit, 7-day cache).
  • Web: Google mode through the Worker; the OpenStreetMap map is hidden when using Google results (Google terms).
Fixed
  • geotag.py: cached Google results expire after 30 days (Google Maps Platform terms).

v1.2 2026-10-25

Added
  • Google Geocoding API + Elevation API when GOOGLE_MAPS_API_KEY is set; Nominatim + Open-Meteo as fallback.
  • Altitude: --auto-alt (Python), filled automatically when picking a point (web).
  • Windows fields: Subject, Comments, Rating, Date taken, Date acquired, Program name.
  • Web writes XMP (Credit, Copyright, Date acquired).
Fixed
  • Wrong URI for the Microsoft XMP namespace (MicrosoftPhoto).

v1.1 2026-10-25

Added
  • Find coordinates by place name (Nominatim/OpenStreetMap): place column, --place, search box on the web version.

v1.0 2026-10-25

Added
  • geotag.py: writes GPS, Title/Description/Keywords, Author/Copyright (EXIF + XMP), Credit, Web Statement; renames files by keyword; batch run from CSV.
  • Web index.html: pick a location on the map, write EXIF, rename files.