#!/usr/bin/env python3
"""Step 3 of the ai-industry-2026 post. Writes the article Markdown from results.json (every number is read from the
data, none typed in), then renders it into the locaihost.org blog page with two inline-SVG charts and copies the
scripts + results next to it.

  distribution/articles/13-ai-chatbot-share-by-industry.md   (dev.to draft, published: false, canonical = blog)
  site/public/blog/ai-chatbot-share-by-industry/index.html   (+ analysis/ with the scripts and results.json)

Same Markdown subset as the other *-blog.py generators: ## headings, paragraphs, "- " lists, pipe tables (---: = numeric
column), *note* lines, ---, **bold**, *italic*, `code`, [links](url), and <!-- chart:NAME --> for an inline SVG chart
(invisible on dev.to). The assert lines check that each sentence's wording still matches the data after a re-run.
Run: python3 distribution/analysis/ai-industry-2026-blog.py
"""
import html
import json
import re
import shutil
from pathlib import Path

ROOT = Path(__file__).resolve().parents[2]
SLUG = 'ai-chatbot-share-by-industry'
MD = ROOT / 'distribution' / 'articles' / f'13-{SLUG}.md'
OUT = ROOT / 'site' / 'public' / 'blog' / SLUG / 'index.html'
URL = f'https://locaihost.org/blog/{SLUG}/'
RES_DIR = ROOT / 'distribution' / 'analysis' / 'out' / 'ai-industry'
res = json.loads((RES_DIR / 'results.json').read_text())
DATE, DATE_LONG = '2026-10-19', '19 October 2026'
NAME = {'chatgpt': 'ChatGPT', 'gemini': 'Gemini', 'perplexity': 'Perplexity', 'claude': 'Claude', 'copilot': 'Copilot',
        'grok': 'Grok', 'deepseek': 'DeepSeek'}

# ------------------------------------------------------------------ figures used in the text
S, O = res['sample'], res['overall']
inds = res['industries']                       # named industries, sorted by ChatGPT average share (ascending)
by = {i['industry']: i for i in inds}
other = {i['industry']: i for i in res['otherGroups']}
desc_cg = sorted(inds, key=lambda i: -i['avgShare']['chatgpt'])
solid = [i for i in inds if not i['small']]
most_cg, least_cg = desc_cg[0], min(solid, key=lambda i: i['avgShare']['chatgpt'])
second_least_cg = sorted(solid, key=lambda i: i['avgShare']['chatgpt'])[1]
sub = {x['category']: x for x in res['subStats']}
uni = sub['Science and Education > Universities and Colleges']
weather = sub['Science and Education > Weather']
leaders = res['leaders']
lead_ind = {b: dict(v) for b, v in res['leadByIndustry'].items()}
top_claude = max(solid, key=lambda i: i['index']['claude'])
top_ppx = sorted(solid, key=lambda i: -i['index']['perplexity'])[:2]
low_ppx = min(solid, key=lambda i: i['avgShare']['perplexity'])
top_gem = max(solid, key=lambda i: i['avgShare']['gemini'])
shop = by['Shopping and e-commerce']
news = by['News and media']
soft = by['Software and developer tools']
ru = res['ru']
bottom5 = sorted(solid, key=lambda i: i['avgShare']['chatgpt'])[:5]
dep = res['topDependent']
gem_sub = res['subTop']['gemini'][0]
B2B = ('Software and developer tools', 'Other tech and web services', 'Business services and marketing')
claude_b2b = sum(lead_ind['claude'].get(k, 0) for k in B2B)
assert most_cg['industry'] == 'Universities and education' and uni['leads'] == {'chatgpt': uni['sites']}
assert {least_cg['industry'], second_least_cg['industry']} == {'Search engines and portals', 'News and media'}
assert res['notGeminiSecond'] == ['Jobs and careers'] and by['Jobs and careers']['small']
assert top_claude['industry'] == 'Software and developer tools'
assert max(lead_ind['perplexity'], key=lead_ind['perplexity'].get) == 'News and media'
assert low_ppx['industry'] == 'Shopping and e-commerce'
assert top_gem['industry'] == 'Search engines and portals'
assert gem_sub['category'] == 'Gambling > Sports Betting'

p1 = lambda x: f'{x:.1f}%'  # noqa: E731
n = lambda x: f'{x:,}'  # noqa: E731


def m(x):
    return f'{x / 1e6:.1f}M' if x >= 1e6 else f'{x / 1e3:.0f}k'


SHORTN = {'Search engines and portals': 'search portals', 'News and media': 'news', 'Adult': 'adult sites',
          'Entertainment and streaming': 'entertainment', 'Sports and betting': 'sports'}
WORD = dict(enumerate(['None', 'One', 'Two', 'Three', 'Four', 'Five', 'Six', 'Seven', 'Eight', 'Nine', 'Ten']))


def and_list(xs):
    return xs[0] if len(xs) == 1 else ', '.join(xs[:-1]) + ' and ' + xs[-1]


def low(s):
    return s[0].lower() + s[1:]


# ------------------------------------------------------------------ the article
title = 'Which AI chatbot does each industry depend on? Referral shares for 2,429 top websites (September 2026)'
title = title.replace('2,429', n(S['sites']))
description = (f"Similarweb's split of AI chatbot referrals for {n(S['sites'])} of the top 5,000 websites: ChatGPT sends "
               f"{p1(uni['chatgpt'])} of the AI referrals to the typical university site but {p1(news['avgShare']['chatgpt'])} at news sites. "
               f"Claude over-indexes on software, Perplexity on news and the Russian-language web, Gemini on search portals and betting.")
lede = ("Not every industry gets its AI referrals from the same chatbot. We split each top website's chatbot visits by "
        "chatbot and grouped the sites by industry.")

table_main = ['| Industry | Sites | ChatGPT | Gemini | Perplexity | Claude | Copilot, Grok, DeepSeek | ChatGPT leads at |',
              '|---|---:|---:|---:|---:|---:|---:|---:|']
for i in desc_cg:
    a = i['avgShare']
    rest = a['copilot'] + a['grok'] + a['deepseek']
    table_main.append(f"| {i['industry']}{' †' if i['small'] else ''} | {i['sites']} | {p1(a['chatgpt'])} | {p1(a['gemini'])} | "
                      f"{p1(a['perplexity'])} | {p1(a['claude'])} | {p1(rest)} | {p1(i['chatgptLeadsPct'])} |")
a = O['avgShare']
table_main.append(f"| **All {n(S['sites'])} sites** | **{n(O['sites'])}** | **{p1(a['chatgpt'])}** | **{p1(a['gemini'])}** | "
                  f"**{p1(a['perplexity'])}** | **{p1(a['claude'])}** | **{p1(a['copilot'] + a['grok'] + a['deepseek'])}** | **{p1(O['chatgptLeadsPct'])}** |")


def dep_table(b, k=10):
    rows = [f'| # | Site | Industry | AI referrals a month | {NAME[b]} | ChatGPT |', '|---:|---|---|---:|---:|---:|']
    for j, s in enumerate(dep[b][:k], 1):
        rows.append(f"| {j} | {s['domain']} | {s['industry']} | {m(s['ai'])} | {p1(s['shares'][b])} | {p1(s['shares']['chatgpt']) if 'chatgpt' in s['shares'] else '–'} |")
    return '\n'.join(rows)


ppx_ru10 = [s['domain'] for s in dep['perplexity'][:10] if s['domain'].endswith('.ru')]
ppx_news = [s['domain'] for s in leaders['perplexity'] if s['industry'] == 'News and media']
claude_news = [s['domain'] for s in leaders['claude'] if s['industry'] == 'News and media']
gem_led = leaders['gemini']
gem_big = [s for s in dep['gemini'] if s['ai'] >= 100_000 and s['shares']['gemini'] < 100]
grok, cop = dep['grok'], dep['copilot']
ld = S['listedCount']
an = f'https://locaihost.org/blog/{SLUG}/analysis/'

md = f'''---
title: "{title}"
published: false
description: "{description}"
lede: "{lede}"
tags: ai, seo, data, marketing
canonical_url: {URL}
---

Similarweb now splits a website's AI chatbot referrals by chatbot: what share came from ChatGPT, Gemini, Perplexity, Claude and the rest. Our [first look at AI chatbot traffic](https://locaihost.org/blog/ai-chatbot-traffic-top-1000/) asked how big that channel is. This one asks a different question: **which chatbot does each industry depend on?**

On {DATE_LONG} we looked up the top 5,000 domains of the Tranco list (`{res['source']['tranco']}`, infrastructure host names removed). {n(S['withChatbotSplit'])} of them had a per-chatbot split from Similarweb. We kept the {n(S['sites'])} that got at least {n(S['minAiReferrals'])} AI chatbot visits in the month, left out AI companies' own sites, and sorted them into {res['namedIndustries']} industries built from Similarweb's categories. All the figures are **Similarweb estimates for September 2026**. Similarweb names only the top three chatbots for each site, so the smaller chatbots' shares are floors (more on that in the method notes).

Five findings:

- **Universities depend on ChatGPT most.** ChatGPT sent {p1(uni['chatgpt'])} of the AI referrals to the average university or college website, and it was the top chatbot at all {uni['sites']} of them. Across all {n(S['sites'])} sites it sent {p1(O['avgShare']['chatgpt'])} on average.
- **Search portals and news depend on it least.** ChatGPT sent {p1(least_cg['avgShare']['chatgpt'])} of AI referrals at {low(least_cg['industry'])} and {p1(second_least_cg['avgShare']['chatgpt'])} at {low(second_least_cg['industry'])} sites. News is where the challengers win most often: another chatbot came first at {news['notChatgptLead']} of {news['sites']} news sites.
- **Gemini is the second chatbot almost everywhere.** It ranks second by average share in {res['secondBotCount']['gemini']} of the {res['namedIndustries']} industries, with {p1(O['avgShare']['gemini'])} of the average site's AI referrals. It is strongest at {low(top_gem['industry'])} ({p1(top_gem['avgShare']['gemini'])}) and sports-betting sites ({p1(gem_sub['gemini'])}, {gem_sub['sites']} sites), and comes first at {O['leads']['gemini']} sites, youtube.com among them.
- **Claude is the software chatbot.** It sends {p1(soft['avgShare']['claude'])} of AI referrals at software and developer tool sites, {soft['index']['claude']:.1f} times its average. {claude_b2b} of the {O['leads']['claude']} sites where Claude comes first are software, tech or business services, among them {and_list([f"{s['domain']} ({p1(s['shares']['claude'])})" for s in leaders['claude'] if s['domain'] in ('duosecurity.com', 'okta.com', 'figma.com', 'hubspot.com')])}.
- **Perplexity leans to news and the Russian-language web.** It comes first at {O['leads']['perplexity']} sites, {lead_ind['perplexity']['News and media']} of them news sites such as {and_list(ppx_news[:3])}. At the {ru['sites']} `.ru` sites it sent {p1(ru['perplexityAvg'])} of AI referrals on average, against {p1(ru['perplexityAvgRest'])} elsewhere. At shopping sites it sent {p1(shop['avgShare']['perplexity'])}.

## Which chatbot each industry depends on

The average share of a site's AI chatbot referrals that came from each chatbot, by industry. Each site counts once, however big it is.

<!-- chart:chatgpt -->

{chr(10).join(table_main)}

*Similarweb estimates, September 2026. Average of each site's share of its AI chatbot referrals; a chatbot outside a site's top three counts as 0, so the shares are floors and don't add up to 100. "ChatGPT leads at" is the share of sites where ChatGPT had the biggest share. † fewer than {S['smallFlag']} sites: read with care. {other['Everything else']['sites']} sites in smaller industries and {other['Uncategorised']['sites']} without a category are in the "All" row only.*

The spread is wide. At universities ChatGPT sent at least 80% of AI referrals at {p1(most_cg['chatgpt80Pct'])} of sites; at news sites only {p1(news['chatgpt80Pct'])} got that much from it. Government and law ({p1(by['Government and law']['avgShare']['chatgpt'])}) and science and research ({p1(by['Science and research']['avgShare']['chatgpt'])}) sit near the top too. Sites people use to study or to get official information get their chatbot visits mostly from ChatGPT.

At the bottom are {and_list([SHORTN.get(i['industry'], low(i['industry'])) for i in bottom5])}. One small sub-category stands out: weather sites ({weather['sites']} sites, so treat it as a hint): ChatGPT sent {p1(weather['chatgpt'])} on average there, and Perplexity came first at {weather['leads'].get('perplexity', 0)} of them.

## Where the smaller chatbots over-index

Claude and Perplexity each send a small slice on average, {p1(O['avgShare']['claude'])} and {p1(O['avgShare']['perplexity'])}. But they are not spread evenly:

<!-- chart:challengers -->

Claude's best industries are all business ones: software and developer tools ({p1(soft['avgShare']['claude'])}), other tech and web services ({p1(by['Other tech and web services']['avgShare']['claude'])}), business services and marketing ({p1(by['Business services and marketing']['avgShare']['claude'])}) and finance ({p1(by['Finance and crypto']['avgShare']['claude'])}). In games it sent {p1(by['Games']['avgShare']['claude'])}, in shopping {p1(shop['avgShare']['claude'])}.

Perplexity is highest at {low(top_ppx[0]['industry'])} ({p1(top_ppx[0]['avgShare']['perplexity'])}) and {low(top_ppx[1]['industry'])} ({p1(top_ppx[1]['avgShare']['perplexity'])}), and lowest at shopping sites ({p1(shop['avgShare']['perplexity'])}).

## The most Claude-dependent sites

Sites with at least {n(S['minAiForLists'])} AI chatbot visits a month, by Claude's share of them:

{dep_table('claude')}

*Similarweb estimates, September 2026. Share of each site's AI chatbot referrals; – means ChatGPT is not in Similarweb's top three for that site. AI companies' own sites left out.*

Cloud hosting, sign-in and security, design, automation and email marketing: tools people use at work. Other sites where Claude came first include {and_list([s['domain'] for s in leaders['claude'] if s['domain'] in ('visualstudio.com', 'docusign.com', 'asana.com', 'box.com', 'airtable.com')])}. Claude also came first at {len(claude_news)} news sites: {and_list(claude_news)}.

## The most Perplexity-dependent sites

{dep_table('perplexity')}

*Similarweb estimates, September 2026. Share of each site's AI chatbot referrals; – means ChatGPT is not in Similarweb's top three for that site. Sites with at least {n(S['minAiForLists'])} AI chatbot visits a month.*

{WORD[len(ppx_ru10)]} of the ten are Russian business, finance or government sites ({and_list(ppx_ru10)}). Among all the sites where Perplexity came first, the biggest are {and_list([s['domain'] for s in leaders['perplexity'][:5]])}. Of its {O['leads']['perplexity']} first places, {ru['perplexityLeads']} are `.ru` sites.

## Gemini, Grok and Copilot

**Gemini** comes first at {O['leads']['gemini']} sites. The biggest are {and_list([s['domain'] for s in gem_led[:6]])}. Among sites with at least 100,000 AI chatbot visits, its highest shares are at {and_list([f"{s['domain']} ({p1(s['shares']['gemini'])})" for s in gem_big[:5]])}. Across the {gem_sub['sites']} sports-betting sites it sent {p1(gem_sub['gemini'])} of AI referrals on average.

**Grok** is in the top three at only {p1(O['listedPct']['grok'])} of sites, but where it shows up it can be big: {and_list([f"{s['domain']} ({p1(s['shares']['grok'])})" for s in grok if s['domain'] in ('stripe.com', 'twitter.com', 'robinhood.com')])}. Leaving out adult sites, it is strongest at social networks ({p1(by['Social networks and forums']['avgShare']['grok'])} on average).

**Copilot** is in the top three at {p1(O['listedPct']['copilot'])} of sites. Its big numbers are on Microsoft's own sites: {', '.join(f"{s['domain']} ({p1(s['shares']['copilot'])})" for s in cop[:3] if s['domain'] in ('live.com', 'cloud.microsoft'))} and bing.com ({p1(next(s['shares']['copilot'] for s in cop if s['domain'] == 'bing.com'))}).

## Methodology and caveats

- **Domains.** The top 5,000 registrable domains of Tranco list [`{res['source']['tranco']}`](https://tranco-list.eu/list/{res['source']['tranco']}/1000000), down to rank {n(S['lastTrancoRank'])}, after removing {S['excludedInfra']} infrastructure host names (CDN, API and telemetry domains), the same list as our earlier studies. {n(S['withEstimate'])} had a Similarweb traffic estimate and {n(S['withChatbotSplit'])} had a per-chatbot split.
- **Sample.** We dropped {n(S['belowMinAi'])} sites with fewer than {n(S['minAiReferrals'])} AI chatbot visits in the month, where a split rests on a handful of clicks, and {len(S['aiOwnLeftOut'])} AI companies' own sites (chatgpt.com, claude.ai and the like), where a chatbot linking to its maker answers a different question. That leaves {n(S['sites'])} sites.
- **Shares.** Similarweb's AI-referral data for September 2026, read on {DATE_LONG} with our website-traffic tool. Similarweb gives each site's share for its top chatbots only: three at {n(ld['3'])} sites, two at {n(ld['2'])}, one at {n(ld['1'])}. The listed shares add up to {p1(S['sharesSumMedian'])} at the median site; the rest isn't broken down. A chatbot outside a site's list counts as 0, so the averages for the smaller chatbots are floors. These are modelled estimates, not measurements. Credit: Similarweb.
- **Averages.** Each site counts once (an unweighted mean), so the figures describe the typical site in an industry, not the industry's total visits. A visit-weighted version is in results.json; it is dominated by a few giants such as google.com.
- **Industries.** Our own groups of Similarweb's categories (the mapping is in the analysis script). Groups with fewer than {S['minSites']} sites are folded into "Everything else"; groups with fewer than {S['smallFlag']} are marked †. Adult sites count in the averages but are not named in the site lists.
- **Reproducible.** Two scripts [run the lookups]({an}ai-industry-2026-run.mjs) and [compute every figure]({an}ai-industry-2026-analyse.py) ([results.json]({an}results.json)); [a third]({an}ai-industry-2026-blog.py) writes this article from the results. The data covers websites only; no personal data was collected.

*Source: Tranco list {res['source']['tranco']} (tranco-list.eu); AI referral estimates by Similarweb for September 2026, collected by locaihost data on {DATE_LONG}.*

---

The data comes from our [website-traffic tool on Apify](https://apify.com/locaihost/website-traffic).
'''
MD.write_text(md)

# ------------------------------------------------------------------ Markdown -> HTML (same subset as the other posts)
text = md
_, front, body = text.split('---', 2)
fm = {}
for line in front.strip().splitlines():
    k, v = line.split(':', 1)
    fm[k.strip()] = v.strip().strip('"')


def inline(s):
    s = html.escape(s, quote=False)
    s = re.sub(r'`([^`]+)`', r'<code>\1</code>', s)
    s = re.sub(r'\*\*(.+?)\*\*', r'<strong>\1</strong>', s)
    s = re.sub(r'(?<![\w*])\*(?!\s)(.+?)(?<!\s)\*(?![\w*])', r'<em>\1</em>', s)

    def link(mm):
        href = mm.group(2).replace('https://locaihost.org/', '/')
        return f'<a href="{html.escape(href)}">{mm.group(1)}</a>'
    s = re.sub(r'\[([^\]]+)\]\(([^)]+)\)', link, s)
    return s


def table(lines):
    rows = [[c.strip() for c in l.strip().strip('|').split('|')] for l in lines]
    head, align, data = rows[0], rows[1], rows[2:]
    num = [a.endswith(':') for a in align]
    rank = head[0] == '#'
    out = [f'<div class="table-scroll"><table class="data-table{" fit" if len(head) <= 4 else ""}">']
    out.append('<thead><tr>' + ''.join(f'<th scope="col"{" class=\"num\"" if num[i] else ""}>{inline(h)}</th>' for i, h in enumerate(head)) + '</tr></thead>')
    out.append('<tbody>')
    for r in data:
        cells = []
        for i, c in enumerate(r):
            is_head = (i == 1) if rank else (i == 0)
            cls = ' class="num rank"' if (rank and i == 0) else (' class="num"' if num[i] else '')
            tag = 'th scope="row"' if is_head else 'td'
            cells.append(f'<{tag}{cls}>{inline(c)}</{tag.split()[0]}>')
        out.append('<tr>' + ''.join(cells) + '</tr>')
    out.append('</tbody></table></div>')
    return '\n'.join(out)


e = lambda s: html.escape(str(s), quote=True)  # noqa: E731


def fig(name, title_, svg, note):
    return (f'<figure class="chart" id="chart-{name}"><figcaption class="chart-title">{e(title_)}</figcaption>'
            f'<div class="chart-scroll">{svg}</div><p class="table-note">{note}</p></figure>')


def chart_chatgpt():
    rows = desc_cg
    overall = O['avgShare']['chatgpt']
    W, row, L, R, T = 500, 24, 244, 46, 30
    H = T + row * len(rows) + 30
    x = lambda p: L + (W - L - R) * p / 100  # noqa: E731  axis 0-100%
    g = []
    for v in (0, 25, 50, 75, 100):
        g.append(f'<line class="grid" x1="{x(v):.1f}" x2="{x(v):.1f}" y1="{T - 6}" y2="{T + row * len(rows)}"/>'
                 f'<text class="tick" x="{x(v):.1f}" y="{T + row * len(rows) + 16}" text-anchor="middle">{v}%</text>')
    xo = x(overall)
    g.append(f'<line class="ref" x1="{xo:.1f}" x2="{xo:.1f}" y1="{T - 12}" y2="{T + row * len(rows)}"/>'
             f'<text class="refl" x="{xo - 5:.1f}" y="{T - 14}" text-anchor="end">All sites {overall:.1f}%</text>')
    for i, c in enumerate(rows):
        y = T + i * row
        v = c['avgShare']['chatgpt']
        w = max(x(v) - L, 1.5)
        lab = c['industry'] + (' †' if c['small'] else '')
        g.append(f'<g class="mark" tabindex="0"><title>{e(c["industry"])}: ChatGPT sent {v:.1f}% of AI chatbot referrals on average ({c["sites"]} sites)</title>'
                 f'<text class="lab" x="{L - 10}" y="{y + 15}" text-anchor="end">{e(lab)} <tspan class="tick">({c["sites"]})</tspan></text>'
                 f'<rect class="bar" x="{L}" y="{y + 5}" width="{w:.1f}" height="{row - 9}" rx="2"/>'
                 f'<text class="val" x="{L + w + 6:.1f}" y="{y + 15}">{v:.1f}%</text></g>')
    svg = (f'<svg viewBox="0 0 {W} {H}" role="img" aria-labelledby="t-cg"><title id="t-cg">Bar chart: ChatGPT\'s average share of a site\'s AI chatbot referrals by industry, September 2026. '
           + '; '.join(f'{c["industry"]} {c["avgShare"]["chatgpt"]:.1f}%' for c in rows) + f'. All sites {overall:.1f}%.</title>' + ''.join(g) + '</svg>')
    return fig('chatgpt', "ChatGPT's share of each site's AI chatbot referrals, by industry", svg,
               f'Similarweb estimates, September 2026, average over sites; number of sites in brackets; † fewer than {S["smallFlag"]} sites. Table below.')


def chart_challengers():
    rows = sorted(inds, key=lambda i: -i['avgShare']['claude'])
    W, row, L, R, T = 500, 24, 244, 24, 44
    H = T + row * len(rows) + 30
    top = 5
    x = lambda p: L + (W - L - R) * min(p, top) / top  # noqa: E731  axis 0-5%
    g = [f'<g class="legend"><circle class="dot hl" cx="{L + 6}" cy="12" r="5"/><text class="lab" x="{L + 16}" y="16">Claude</text>'
         f'<circle class="dot" cx="{L + 86}" cy="12" r="5"/><text class="lab" x="{L + 96}" y="16">Perplexity</text></g>']
    for v in (0, 1, 2, 3, 4, 5):
        g.append(f'<line class="grid" x1="{x(v):.1f}" x2="{x(v):.1f}" y1="{T - 6}" y2="{T + row * len(rows)}"/>'
                 f'<text class="tick" x="{x(v):.1f}" y="{T + row * len(rows) + 16}" text-anchor="middle">{v}%</text>')
    for i, c in enumerate(rows):
        y = T + i * row + 12
        cl, px = c['avgShare']['claude'], c['avgShare']['perplexity']
        lab = c['industry'] + (' †' if c['small'] else '')
        g.append(f'<g class="mark" tabindex="0"><title>{e(c["industry"])}: Claude {cl:.1f}%, Perplexity {px:.1f}% of AI chatbot referrals on average ({c["sites"]} sites)</title>'
                 f'<text class="lab" x="{L - 10}" y="{y + 4}" text-anchor="end">{e(lab)}</text>'
                 f'<line class="stem" x1="{x(min(cl, px)):.1f}" x2="{x(max(cl, px)):.1f}" y1="{y}" y2="{y}"/>'
                 f'<circle class="dot" cx="{x(px):.1f}" cy="{y}" r="5"/>'
                 f'<circle class="dot hl" cx="{x(cl):.1f}" cy="{y}" r="5"/></g>')
    svg = (f'<svg viewBox="0 0 {W} {H}" role="img" aria-labelledby="t-ch"><title id="t-ch">Dot chart: Claude\'s and Perplexity\'s average share of a site\'s AI chatbot referrals by industry, September 2026. '
           + '; '.join(f'{c["industry"]}: Claude {c["avgShare"]["claude"]:.1f}%, Perplexity {c["avgShare"]["perplexity"]:.1f}%' for c in rows) + '.</title>' + ''.join(g) + '</svg>')
    return fig('challengers', 'Claude and Perplexity: average share of AI chatbot referrals, by industry', svg,
               f'Similarweb estimates, September 2026, average over sites, sorted by Claude. All sites: Claude {O["avgShare"]["claude"]:.1f}%, Perplexity {O["avgShare"]["perplexity"]:.1f}%. Exact figures in the table above.')


CHARTS = {'chatgpt': chart_chatgpt, 'challengers': chart_challengers}

blocks = re.split(r'\n\s*\n', body.strip())
parts = []
first_list = True
after_table = False
for b in blocks:
    b = b.strip()
    lines = b.splitlines()
    m_chart = re.fullmatch(r'<!-- chart:(\w+) -->', b)
    if m_chart:
        parts.append(CHARTS[m_chart.group(1)]())
    elif b.startswith('## '):
        t = b[3:].strip()
        slug = re.sub(r'[^a-z0-9]+', '-', t.lower()).strip('-')
        parts.append(f'<h2 id="{slug}">{inline(t)}</h2>')
    elif b == '---':
        parts.append('<hr>')
    elif all(l.startswith('|') for l in lines):
        parts.append(table(lines)); after_table = True; continue
    elif all(l.startswith('- ') for l in lines):
        cls = ' class="findings"' if first_list else ''
        first_list = False
        parts.append(f'<ul{cls}>' + ''.join(f'<li>{inline(l[2:])}</li>' for l in lines) + '</ul>')
    elif b.startswith('*') and b.endswith('*') and not b.startswith('**'):
        cls = 'table-note' if after_table else 'source'
        parts.append(f'<p class="{cls}">{inline(b[1:-1])}</p>')
    else:
        parts.append(f'<p>{inline(" ".join(lines))}</p>')
    after_table = False

TRANCO = res['source']['tranco']
ld_json = {
    '@context': 'https://schema.org', '@type': 'Article', 'headline': fm['title'], 'description': fm['description'],
    'datePublished': DATE, 'dateModified': DATE, 'inLanguage': 'en',
    'mainEntityOfPage': {'@type': 'WebPage', '@id': URL}, 'url': URL,
    'author': {'@type': 'Organization', 'name': 'locaihost data', 'url': 'https://locaihost.org/'},
    'publisher': {'@type': 'Organization', 'name': 'locaihost data', 'url': 'https://locaihost.org/'},
    'about': ['Web traffic', 'AI chatbots', 'Referral traffic', 'ChatGPT', 'Claude', 'Perplexity', 'Gemini', 'Generative engine optimization'],
    'isBasedOn': [{'@type': 'CreativeWork', 'name': f'Tranco list {TRANCO}', 'url': f'https://tranco-list.eu/list/{TRANCO}/1000000'},
                  {'@type': 'CreativeWork', 'name': 'Similarweb AI referral estimates, September 2026', 'url': 'https://www.similarweb.com/'}],
}
CHART_CSS = '''<style>
.chart { margin: 1.75rem 0 1.5rem; }
.chart-title { font: 600 0.9375rem/1.35 var(--sans); margin: 0 0 0.625rem; }
.chart-scroll { overflow-x: auto; border-top: 1px solid var(--ink); padding-top: 0.75rem; }
.chart svg { display: block; width: 100%; max-width: 40rem; min-width: 20rem; height: auto; font-family: var(--sans); }
.chart .bar { fill: var(--ink-2); }
.chart .dot { fill: var(--ink); stroke: var(--bg); stroke-width: 2; }
.chart .dot.hl { fill: var(--vermilion); }
.chart .stem { stroke: var(--rule); stroke-width: 2; }
.chart .grid { stroke: var(--rule); stroke-width: 1; }
.chart .ref { stroke: var(--vermilion); stroke-width: 2; stroke-dasharray: 4 3; }
.chart .refl { fill: var(--vermilion-ink); font-size: 12px; font-weight: 500; }
.chart .tick { fill: var(--ink-2); font-size: 12px; }
.chart .lab { fill: var(--ink); font-size: 12.5px; }
.chart .val { fill: var(--ink); font: 500 12px var(--mono); paint-order: stroke; stroke: var(--bg); stroke-width: 4px; stroke-linejoin: round; }
.chart .mark:hover .bar, .chart .mark:focus .bar { opacity: 0.8; }
.chart .mark:hover .dot, .chart .mark:focus .dot { stroke: var(--ink); }
.chart .mark:focus { outline: none; }
.chart .mark:focus-visible .lab { text-decoration: underline; }
@media (forced-colors: active) { .chart .bar, .chart .dot { fill: CanvasText; } .chart .dot.hl { fill: Highlight; } }
</style>'''
page = f'''<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="color-scheme" content="light dark">
<script>try{{var t=localStorage.getItem('theme');if(t==='dark'||t==='light')document.documentElement.dataset.theme=t}}catch(e){{}}</script>
<title>Which AI chatbot each industry depends on (Sept 2026) | locaihost data</title>
<meta name="description" content="{e(fm['description'])}">
<link rel="preload" href="/fonts/Newsreader-400-normal.woff2" as="font" type="font/woff2" crossorigin>
<link rel="preload" href="/fonts/IBMPlexSans-400-normal.woff2" as="font" type="font/woff2" crossorigin>
<link rel="stylesheet" href="/fonts/fonts.css">
<link rel="stylesheet" href="/styles.css">
<link rel="stylesheet" href="/visa.css">
<link rel="stylesheet" href="/blog.css">
{CHART_CSS}
<link rel="canonical" href="{URL}">
<meta property="og:title" content="{e(fm['title'])}">
<meta property="og:description" content="{e(fm['description'])}">
<meta property="og:type" content="article">
<meta property="og:url" content="{URL}">
<meta property="og:site_name" content="locaihost data">
<meta property="og:image" content="https://locaihost.org/og.png">
<meta property="og:image:width" content="1200">
<meta property="og:image:height" content="630">
<meta property="og:image:alt" content="locaihost data: clean public-record data feeds on Apify">
<meta name="twitter:card" content="summary_large_image">
<meta property="article:published_time" content="{DATE}">
<script type="application/ld+json">{json.dumps(ld_json, ensure_ascii=False)}</script>
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' fill='%23111'/%3E%3Crect x='6' y='20' width='20' height='4' fill='%23d9482b'/%3E%3C/svg%3E">
</head>
<body>
<a class="skip" href="#main">Skip to content</a>

<header class="masthead">
  <div class="wrap masthead-row">
    <a class="wordmark" href="/">locaihost <span>data</span></a>
    <nav aria-label="Primary">
      <a href="/#register">Feeds</a>
      <a href="/#method">Method</a>
      <a href="/blog/" aria-current="page">Blog</a>
      <a href="/#contact">Contact</a>
    </nav>
    <button type="button" class="theme-toggle" aria-label="Dark theme" aria-pressed="false" title="Toggle dark theme" hidden><svg viewBox="0 0 16 16" aria-hidden="true" focusable="false"><circle cx="8" cy="8" r="6.25" fill="none" stroke="currentColor" stroke-width="1.5"/><path d="M8 1.75a6.25 6.25 0 0 1 0 12.5z" fill="currentColor"/></svg></button>
  </div>
</header>

<main id="main">
  <article>
    <header class="post-head wrap">
      <nav class="crumbs" aria-label="Breadcrumb"><ol><li><a href="/">locaihost data</a></li><li><a href="/blog/">Blog</a></li><li aria-current="page">AI chatbot share by industry</li></ol></nav>
      <p class="kicker">Data analysis · Web traffic</p>
      <h1>{inline(fm['title'])}</h1>
      <p class="lede">{e(fm['lede'])}</p>
      <p class="byline">By locaihost data · <time datetime="{DATE}">{DATE_LONG}</time> · <span class="mono">Tranco {TRANCO} · Similarweb estimates, September 2026</span></p>
    </header>
    <div class="wrap"><div class="prose">
{chr(10).join(parts)}
    </div></div>
  </article>
</main>

<footer class="wrap footer">
  <p class="footer-nav">locaihost data · data feeds and web tools on <a href="https://apify.com/locaihost">Apify</a> · <a href="/#register">All feeds</a> · <a href="/blog/">Blog</a> · <a href="/tools/">Free tools</a> · <a href="/visa-sponsors/employers/">H-1B sponsor lookup</a> · <a href="mailto:apify@locaihost.org">apify@locaihost.org</a></p>
  <p class="footer-note">Traffic figures are Similarweb estimates; domain ranking from the <a href="https://tranco-list.eu/">Tranco list</a>.</p>
</footer>
<script src="/apify-links.js" defer></script>
<script src="/theme.js" defer></script>
</body>
</html>
'''
OUT.parent.mkdir(parents=True, exist_ok=True)
OUT.write_text(page)
AN = OUT.parent / 'analysis'
AN.mkdir(exist_ok=True)
for name in ['ai-industry-2026-run.mjs', 'ai-industry-2026-analyse.py', 'ai-industry-2026-blog.py']:
    shutil.copy(ROOT / 'distribution' / 'analysis' / name, AN / name)
shutil.copy(RES_DIR / 'results.json', AN / 'results.json')
# Bluesky thread draft (3 posts, each <= 300 characters, link in post 1). Not posted by this script.
cl = {s['domain']: s['shares']['claude'] for s in leaders['claude']}
posts = [
    f"Which AI chatbot does each industry depend on? We split Similarweb's AI referral estimates for {n(S['sites'])} top websites by chatbot (Sept 2026).\n\n"
    f"ChatGPT sends {p1(uni['chatgpt'])} of a typical university site's AI referrals, but only {p1(news['avgShare']['chatgpt'])} at news sites.\n\n{URL}",
    f"Claude is the software chatbot: {p1(soft['avgShare']['claude'])} of AI referrals at software and developer tool sites, {soft['index']['claude']:.1f}x its average. "
    f"It comes first at duosecurity.com ({p1(cl['duosecurity.com'])}), okta.com ({p1(cl['okta.com'])}), figma.com ({p1(cl['figma.com'])}) and hubspot.com ({p1(cl['hubspot.com'])}).",
    f"Perplexity leans to news and the Russian-language web: first at {lead_ind['perplexity']['News and media']} news sites incl. {ppx_news[0]} and {ppx_news[2]}, "
    f"and {p1(ru['perplexityAvg'])} of AI referrals at .ru sites vs {p1(ru['perplexityAvgRest'])} elsewhere. Gemini is No. 2 in {res['secondBotCount']['gemini']} of {res['namedIndustries']} industries.\n\n"
    "The data comes from our website-traffic tool on Apify.",
]
for t in posts:
    assert len(t) <= 300, (len(t), t)
THREAD = ROOT / 'distribution' / 'social' / 'drafts' / f'{SLUG}-thread.txt'
THREAD.write_text('\n---\n'.join(posts) + '\n')
print(THREAD, [len(t) for t in posts])
print(MD, len(md))
print(OUT, len(page))
