Skip to content

Commit 1f68e5b

Browse files
committed
Crawl4AI Cloud is live: the README with the two ways to use it and the launch banner, the docs home, the cloud notice as a daily yellow banner while the offer runs
1 parent e2006d5 commit 1f68e5b

9 files changed

Lines changed: 451 additions & 1891 deletions

File tree

‎README-first.md‎

Lines changed: 0 additions & 809 deletions
This file was deleted.

‎README.md‎

Lines changed: 225 additions & 1021 deletions
Large diffs are not rendered by default.

‎crawl4ai/cloud_notice.py‎

Lines changed: 54 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,24 @@
1-
"""The Crawl4AI Cloud notice: short lines that tell a user the hosted version exists.
1+
"""The Crawl4AI Cloud notice: a short yellow banner that tells a user the hosted version exists.
22
33
It prints in four places:
44
55
- at the end of ``crawl4ai-setup``, every time (``show_at_install``);
6-
- once per minor release per machine, when the first ``AsyncWebCrawler`` starts or the ``crwl`` CLI runs
7-
(``show_once``; a marker file in the crawl4ai home folder holds the last minor version shown);
6+
- when the first ``AsyncWebCrawler`` starts or the ``crwl`` CLI runs (``show_once``): once a day per
7+
machine while the launch offer runs (before ``DAILY_UNTIL``), then once per version per machine, a
8+
patch release included. A marker file in the crawl4ai home folder holds the day, or the version, last shown;
89
- when a crawl is blocked by anti-bot protection, at most once a day (``show_blocked``);
910
- when ``crawl4ai-setup`` could not install the browser (``show_setup_failed``).
1011
12+
The banner goes to the logger's console (stderr by default), never to stdout, so a piped ``crwl`` output
13+
or an MCP stdio transport stays clean. Rich drops the color when the output is not a terminal and honours
14+
``NO_COLOR``. Only ASCII characters, so every console can print it.
15+
1116
Each link carries its own ``ref`` so the cloud can tell which moment brought a user.
1217
``CRAWL4AI_NO_CLOUD_NOTICE=1`` turns every line off, for operators who run the library in production logs.
1318
"""
1419

1520
import os
21+
import sys
1622
import time
1723
from pathlib import Path
1824

@@ -23,19 +29,25 @@
2329
_MARKER = ".cloud-notice-shown"
2430
_BLOCKED_MARKER = ".cloud-notice-blocked"
2531

32+
# The first day the banner stops returning daily: the launch offer ends on 31 December 2026.
33+
DAILY_UNTIL = "2027-01-01"
34+
_RULE = "=" * 72
35+
2636
LINES = (
27-
"Crawl4AI Cloud is live: the same crawler, hosted. Your first $10 pack is on us until 31 Dec 2026 (then $5 to start). No card.",
28-
f"Get a key in 10 seconds: {CLOUD_URL} (the library stays open source, forever)",
29-
f"Hide this notice: {_ENV_OFF}=1",
37+
"Crawl4AI Cloud is live: the same crawler, hosted.",
38+
"Your first $10 pack is on us until 31 Dec 2026 (then $5 to start). No card.",
39+
f"Get a key in 10 seconds: {CLOUD_URL}",
40+
f"The library stays open source, forever. Hide this banner: {_ENV_OFF}=1",
3041
)
3142

3243
BLOCKED_LINES = (
33-
f"Blocked? Crawl4AI Cloud gets through the hard sites, free credit to start: {BLOCKED_URL}",
34-
f"Hide this notice: {_ENV_OFF}=1",
44+
"Blocked? Crawl4AI Cloud gets through the hard sites, free credit to start.",
45+
f"{BLOCKED_URL} Hide this banner: {_ENV_OFF}=1",
3546
)
3647

3748
SETUP_LINES = (
38-
f"Browser setup failed? Skip it: the same crawler, hosted, nothing to install, free credit to start: {SETUP_URL}",
49+
"Browser setup failed? Skip it: the same crawler, hosted, nothing to install, free credit to start.",
50+
SETUP_URL,
3951
)
4052

4153

@@ -48,32 +60,43 @@ def _home() -> Path:
4860
return Path.home() / ".crawl4ai"
4961

5062

51-
def _minor_version() -> str:
52-
"""This release's minor version, "0.9" for 0.9.3: the notice returns once per minor release."""
63+
def _version() -> str:
64+
"""This release's full version, "0.9.4": after the offer the banner returns once per version."""
5365
try:
5466
from .__version__ import __version__
5567

56-
return ".".join(__version__.split(".")[:2])
68+
return __version__
5769
except Exception:
5870
return ""
5971

6072

73+
def _today() -> str:
74+
"""Today's date in UTC, "2026-09-25": the daily markers compare this string."""
75+
return time.strftime("%Y-%m-%d", time.gmtime())
76+
77+
6178
def is_off() -> bool:
6279
"""True when the operator turned the notice off with the environment variable."""
6380
return os.environ.get(_ENV_OFF, "").strip().lower() in ("1", "true", "yes")
6481

6582

66-
def text() -> str:
67-
"""The notice as one block of text."""
68-
return "\n".join(LINES)
83+
def text(lines=LINES) -> str:
84+
"""The banner as plain text: a rule, the lines indented by two spaces, a rule."""
85+
return "\n".join([_RULE, *(f" {line}" for line in lines), _RULE])
6986

7087

7188
def _print(lines, logger=None) -> None:
72-
if logger is not None:
73-
for line in lines:
74-
logger.info(line, tag="CLOUD")
75-
else:
76-
print("\n".join(lines))
89+
"""Print the banner in yellow, the first line bold, through the logger's console; without a logger,
90+
through a console on stderr. Rich keeps the text plain when the output is not a terminal."""
91+
from rich.console import Console
92+
from rich.text import Text
93+
94+
console = getattr(logger, "console", None) or Console(stderr=True, width=200)
95+
banner = Text(_RULE + "\n", style="yellow")
96+
for i, line in enumerate(lines):
97+
banner.append(f" {line}\n", style="bold yellow" if i == 0 else "yellow")
98+
banner.append(_RULE, style="yellow")
99+
console.print(banner, highlight=False, soft_wrap=True)
77100

78101

79102
def _claim(name: str, value: str) -> bool:
@@ -91,32 +114,36 @@ def _claim(name: str, value: str) -> bool:
91114

92115

93116
def show_at_install(logger=None) -> None:
94-
"""Print the notice at the end of ``crawl4ai-setup``. Every run; nothing is remembered."""
117+
"""Print the banner at the end of ``crawl4ai-setup``. Every run; nothing is remembered."""
95118
if is_off():
96119
return
97120
_print(LINES, logger)
98121

99122

100123
def show_once(logger=None) -> bool:
101-
"""Print the notice the first time the library or the CLI runs on this machine after a new minor
102-
release. Returns True when it printed."""
103-
if is_off() or not _claim(_MARKER, _minor_version() or "shown"):
124+
"""Print the banner when the library or the CLI runs: once a day per machine while the offer runs,
125+
then once per version per machine. Returns True when it printed."""
126+
if is_off():
127+
return False
128+
today = _today()
129+
value = today if today < DAILY_UNTIL else (_version() or "shown")
130+
if not _claim(_MARKER, value):
104131
return False
105132
_print(LINES, logger)
106133
return True
107134

108135

109136
def show_blocked(logger=None) -> bool:
110-
"""Print the blocked line when a crawl hit anti-bot protection: the moment the cloud helps most.
137+
"""Print the blocked banner when a crawl hit anti-bot protection: the moment the cloud helps most.
111138
At most once a day per machine. Returns True when it printed."""
112-
if is_off() or not _claim(_BLOCKED_MARKER, time.strftime("%Y-%m-%d", time.gmtime())):
139+
if is_off() or not _claim(_BLOCKED_MARKER, _today()):
113140
return False
114141
_print(BLOCKED_LINES, logger)
115142
return True
116143

117144

118145
def show_setup_failed(logger=None) -> None:
119-
"""Print the setup line when ``crawl4ai-setup`` could not install the browser."""
146+
"""Print the setup banner when ``crawl4ai-setup`` could not install the browser."""
120147
if is_off():
121148
return
122149
_print(SETUP_LINES, logger)
Lines changed: 20 additions & 0 deletions
Loading
Lines changed: 20 additions & 0 deletions
Loading
59.5 KB
Loading

‎docs/md_v2/core/installation.md‎

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,4 +126,14 @@ Some older docs mention running Crawl4AI as a local server. This approach has be
126126
5. **Docker** is **experimental**—use at your own risk until the stable version is released.
127127
6. **Local server** references in older docs are largely deprecated; a new solution is in progress.
128128

129-
**Got questions?** Check [GitHub issues](https://github.com/unclecode/crawl4ai/issues) for updates or ask the community!
129+
**Got questions?** Check [GitHub issues](https://github.com/unclecode/crawl4ai/issues) for updates or ask the community!
130+
131+
## Version numbering
132+
133+
Crawl4AI follows PEP 440: `MAJOR.MINOR.PATCH`, for example `0.9.4`. A pre-release carries a suffix: `dev` (unstable), `a` (alpha), `b` (beta), `rc` (release candidate).
134+
135+
- The stable version: `pip install -U crawl4ai`
136+
- The latest pre-release: `pip install crawl4ai --pre`
137+
- One exact version: `pip install crawl4ai==0.9.4`
138+
139+
Use the stable version in production. Pre-releases exist to test new features with real users before the final release.

‎docs/md_v2/index.md‎

Lines changed: 51 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# 🚀🤖 Crawl4AI: Open-Source LLM-Friendly Web Crawler & Scraper
1+
# 🚀🤖 Crawl4AI: the open-source web crawler for LLMs and AI agents
22

33
<div class = "badges" align="center">
44

@@ -55,19 +55,60 @@
5555

5656
</div>
5757

58-
---
59-
#### 🚀 Crawl4AI Cloud API — Closed Beta (Launching Soon)
60-
Reliable, large-scale web extraction, now built to be _**drastically more cost-effective**_ than any of the existing solutions.
58+
Crawl4AI turns any website into clean, LLM-ready Markdown for RAG, AI agents and data pipelines. Run the open-source web crawler and scraper yourself, free forever, or use it hosted with one key: scrape, search and extract through one API, with MCP for your agent.
6159

62-
👉 **Apply [here](https://forms.gle/E9MyPaNXACnAMaqG7) for early access**
63-
_We’ll be onboarding in phases and working closely with early users.
64-
Limited slots._
60+
## Two ways to use Crawl4AI
6561

66-
---
62+
### 🐍 Run it yourself: open source, forever
63+
64+
```bash
65+
pip install -U crawl4ai
66+
crawl4ai-setup # installs the browser, once
67+
```
68+
69+
```python
70+
import asyncio
71+
from crawl4ai import AsyncWebCrawler
72+
73+
async def main():
74+
async with AsyncWebCrawler() as crawler:
75+
result = await crawler.arun(url="https://news.ycombinator.com")
76+
print(result.markdown)
77+
78+
asyncio.run(main())
79+
```
80+
81+
Docker server, CLI and every option: [Installation](core/installation.md) · [Self-hosting](core/self-hosting.md)
82+
83+
### ☁️ Or use the cloud: no browsers, no proxies
84+
85+
1. [![Get a key in 10 seconds](https://img.shields.io/badge/Get_a_key_in_10_seconds-%241_pass%2C_no_signup-f5a300?style=for-the-badge&labelColor=0d0d10)](https://crawl4ai.com/?ref=docs)
86+
Verify your email and your first $10 pack is on us (until 31 December 2026, then $5 to start). No card.
87+
2. Get any page as Markdown:
88+
89+
```bash
90+
curl -s https://api.crawl4ai.com/scrape \
91+
-H "Authorization: Bearer $CRAWL4AI_KEY" \
92+
-H "Content-Type: application/json" \
93+
-d '{"url": "https://news.ycombinator.com"}' | jq -r .markdown
94+
```
6795

68-
Crawl4AI is the #1 trending GitHub repository, actively maintained by a vibrant community. It delivers blazing-fast, AI-ready web crawling tailored for large language models, AI agents, and data pipelines. Fully open source, flexible, and built for real-time performance, **Crawl4AI** empowers developers with unmatched speed, precision, and deployment ease.
96+
The same key works for `/search`, `/answer`, `/extract` and many URLs at once (`/scrape/batch`, `/scrape/jobs`). Pay as you go: [live prices](https://crawl4ai.com/docs?ref=docs#pricing).
97+
3. Give it to your AI agent. Claude Code shown; [Codex, Cursor and OpenCode →](https://crawl4ai.com/docs?ref=docs#mcp)
6998

70-
> Enjoy using Crawl4AI? Consider **[becoming a sponsor](https://github.com/sponsors/unclecode)** to support ongoing development and community growth!
99+
```bash
100+
claude mcp add --transport http crawl4ai https://api.crawl4ai.com/mcp \
101+
--header "Authorization: Bearer $CRAWL4AI_KEY"
102+
```
103+
104+
### Which one?
105+
106+
| | 🐍 Library | 🐳 Your own server | ☁️ Crawl4AI Cloud |
107+
|---|---|---|---|
108+
| **Runs the browsers** | you, in your Python process | you, in Docker on your machine | we do |
109+
| **JS-heavy pages and bot walls** | your settings, your proxies | your settings, your proxies | handled for you, automatically |
110+
| **Web search** | – | – | `/search` and `/answer` |
111+
| **Price** | free, forever | free (your hosting) | pay as you go; your first $10 is on us |
71112

72113
## 🆕 AI Assistant Skill Now Available!
73114

@@ -97,29 +138,6 @@ Crawl4AI now features intelligent adaptive crawling that knows when to stop! Usi
97138
[Learn more about Adaptive Crawling →](core/adaptive-crawling.md)
98139

99140

100-
## Quick Start
101-
102-
Here's a quick example to show you how easy it is to use Crawl4AI with its asynchronous capabilities:
103-
104-
```python
105-
import asyncio
106-
from crawl4ai import AsyncWebCrawler
107-
108-
async def main():
109-
# Create an instance of AsyncWebCrawler
110-
async with AsyncWebCrawler() as crawler:
111-
# Run the crawler on a URL
112-
result = await crawler.arun(url="https://crawl4ai.com")
113-
114-
# Print the extracted content
115-
print(result.markdown)
116-
117-
# Run the async main function
118-
asyncio.run(main())
119-
```
120-
121-
---
122-
123141
## Video Tutorial
124142

125143
<div align="center">

‎tests/unit/test_cloud_notice.py‎

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
"""The cloud notice: when it prints, and what it prints."""
2+
3+
import io
4+
5+
import pytest
6+
from rich.console import Console
7+
8+
from crawl4ai import cloud_notice
9+
10+
11+
@pytest.fixture
12+
def home(tmp_path, monkeypatch):
13+
"""The marker files go to a temporary home folder; the notice is on."""
14+
monkeypatch.setattr(cloud_notice, "_home", lambda: tmp_path)
15+
monkeypatch.delenv(cloud_notice._ENV_OFF, raising=False)
16+
return tmp_path
17+
18+
19+
def _render(lines) -> str:
20+
"""The banner as a terminal without colour would show it."""
21+
out = io.StringIO()
22+
console = Console(file=out, width=200, force_terminal=False, color_system=None)
23+
cloud_notice._print(lines, logger=type("L", (), {"console": console})())
24+
return out.getvalue()
25+
26+
27+
def test_the_environment_variable_turns_every_banner_off(home, monkeypatch):
28+
monkeypatch.setenv(cloud_notice._ENV_OFF, "1")
29+
assert cloud_notice.show_once() is False
30+
assert cloud_notice.show_blocked() is False
31+
assert not (home / cloud_notice._MARKER).exists()
32+
33+
34+
def test_daily_while_the_offer_runs_then_once_per_version(home, monkeypatch):
35+
printed = []
36+
monkeypatch.setattr(cloud_notice, "_print", lambda lines, logger=None: printed.append(lines))
37+
monkeypatch.setattr(cloud_notice, "_version", lambda: "0.9.4")
38+
39+
monkeypatch.setattr(cloud_notice, "_today", lambda: "2026-10-01")
40+
assert cloud_notice.show_once() is True
41+
assert cloud_notice.show_once() is False, "the same day prints once"
42+
monkeypatch.setattr(cloud_notice, "_today", lambda: "2026-10-02")
43+
assert cloud_notice.show_once() is True, "the next day prints again"
44+
45+
monkeypatch.setattr(cloud_notice, "_today", lambda: "2027-01-01")
46+
assert cloud_notice.show_once() is True, "the first run after the offer prints once per version"
47+
monkeypatch.setattr(cloud_notice, "_today", lambda: "2027-01-02")
48+
assert cloud_notice.show_once() is False, "the next day, same version: quiet"
49+
monkeypatch.setattr(cloud_notice, "_version", lambda: "0.9.5")
50+
assert cloud_notice.show_once() is True, "a new version, a patch included, prints again"
51+
assert len(printed) == 4
52+
53+
54+
def test_the_blocked_banner_prints_once_a_day(home, monkeypatch):
55+
monkeypatch.setattr(cloud_notice, "_print", lambda lines, logger=None: None)
56+
monkeypatch.setattr(cloud_notice, "_today", lambda: "2026-10-01")
57+
assert cloud_notice.show_blocked() is True
58+
assert cloud_notice.show_blocked() is False
59+
monkeypatch.setattr(cloud_notice, "_today", lambda: "2026-10-02")
60+
assert cloud_notice.show_blocked() is True
61+
62+
63+
def test_the_banner_is_ascii_with_the_link_and_the_off_switch():
64+
for lines in (cloud_notice.LINES, cloud_notice.BLOCKED_LINES, cloud_notice.SETUP_LINES):
65+
shown = _render(lines)
66+
assert shown.isascii(), "every console can print it"
67+
assert shown.startswith(cloud_notice._RULE) and shown.rstrip().endswith(cloud_notice._RULE)
68+
assert "https://crawl4ai.com/?ref=" in shown
69+
assert cloud_notice._ENV_OFF in _render(cloud_notice.LINES)
70+
assert cloud_notice.text() == _render(cloud_notice.LINES).rstrip("\n")

0 commit comments

Comments
 (0)