Rank Tracker

Keyword positions on Google, Google Maps, Bing, Yahoo and YouTube · US and Vietnam

Free mode
Checking DataForSEO connection...

Projects

Loading…

User guide: Rank Tracker

Rank Tracker

Tool theo dõi thứ hạng từ khóa của rowabi.com (hoặc một trang cụ thể) trên Google, Google Maps, Bing, Yahoo và YouTube, ở Mỹ và Việt Nam. Mỗi dự án tự kiểm tra hằng ngày và vẽ biểu đồ thay đổi.

1. Cài đặt một lần

Thứ hạng lấy từ DataForSEO SERP API, khoảng $0.0006 cho mỗi 10 kết quả.

  1. Đăng ký tại app.dataforseo.com (có $1 dùng thử, nạp từ $50).
  2. Trong DataForSEO, mở API Access, chép API login và password (không phải mật khẩu tài khoản).
  3. Cloudflare → Worker geo-tag → Settings → Variables and Secrets → thêm 2 secret DATAFORSEO_LOGIN và DATAFORSEO_PASSWORD → Deploy.
  4. Dùng lại binding D1 DB của UTM Builder (thêm nếu chưa có).
  5. Kiểm tra hằng ngày cần Cron trigger: Worker → Settings → Trigger events → Add → Cron trigger → */10 * * * *.

2. Dự án

Trang đầu liệt kê các dự án. Bấm một dự án để mở, ← All projects để quay lại, + New project để tạo mới.

Tạo dự án
  • Project name và Domain or URL to find (bắt buộc). Tick Exact URL khi chỉ muốn theo dõi đúng một trang.
  • Search engine: Google, Google Local (Maps), Bing, Yahoo, YouTube. Với Google Maps, điền Business name on Google Maps.
  • Search device: Desktop hoặc Phone. Nên tạo 2 dự án nếu muốn so sánh, vì thứ hạng trên điện thoại thường khác.
  • Results location: United States hoặc Vietnam. Google region và Results language đi kèm.
  • Search location (không bắt buộc): gõ thành phố rồi Find để xem thứ hạng như người tìm ở đó (ví dụ Houston, Los Angeles). Rất quan trọng với Google Maps.
  • Track positions down to: theo dõi đến Top 10, 20, 30, 50 hoặc 100. Càng sâu chi phí càng cao.
  • Check frequency: hằng ngày, hoặc chỉ khi bấm Check now. Daily check at: giờ kiểm tra (giờ của bạn).
  • Competitors: tối đa 5 domain đối thủ, mỗi dòng một domain.
  • Keywords: mỗi dòng một từ khóa.
  • Notes: ghi chú.
  • Bấm Save project.

3. Trong một dự án

  • Check all now: kiểm tra mọi từ khóa ngay. Kết quả về sau vài phút.
  • Add keywords: thêm từ khóa (mỗi dòng một), rồi Add and check.
  • Refresh search volumes: tải lại lượng tìm kiếm từ Google Ads (cần kết nối ở Keyword Research).
  • Export CSV: tải bảng thứ hạng.
  • Edit: sửa cài đặt dự án.
Bảng Keywords

Mỗi dòng là một từ khóa: vị trí hiện tại, thay đổi, URL đang xếp hạng, lượng tìm kiếm, vị trí của đối thủ. Ô chọn phía trên đổi cách so sánh: lần kiểm tra trước, 7, 30 hoặc 90 ngày.

Summary graph

Biểu đồ vị trí trung bình của các từ khóa có xếp hạng, và số từ khóa trong Top 10, theo từng ngày.

4. Cách đọc kết quả

  • Vị trí trống nghĩa là không nằm trong phạm vi theo dõi (ví dụ ngoài Top 100).
  • Thứ hạng dao động 1–3 bậc mỗi ngày là bình thường. Hãy xem xu hướng 7–30 ngày.
  • Nếu URL xếp hạng không phải trang bạn muốn, hai trang đang cạnh tranh nhau cùng từ khóa: gom nội dung hoặc thêm liên kết nội bộ về trang chính.

5. Mẹo

  • Tách dự án theo nhóm: từ khóa sản phẩm, từ khóa địa phương (Maps), từ khóa blog.
  • Đưa đối thủ trực tiếp vào để biết ai đang vượt lên.
  • Kết hợp với Keyword Research để chọn từ khóa có lượng tìm kiếm thật.

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

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 & and '.
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.