UrsaProxy
A Bearblog-to-Gemini proxy built with Xitzin. It fetches content from a Bearblog RSS feed and HTML pages, converts them to Gemtext format, and serves them over the Gemini protocol.
Features
- Proxies Bearblog content to Gemini protocol
- Converts HTML to Gemtext via Markdown intermediate format
- Generates Atom feeds with Gemini URLs
- Configurable TTL caching for feed and post data
- Support for static pages not in RSS feed
Installation
Requires Python 3.13+.
# Using uv
uv add ursaproxy
# Using pip
pip install ursaproxy
Configuration
UrsaProxy can be configured via a TOML file or environment variables.
TOML Configuration (Recommended)
Create an ursaproxy.toml file in your working directory:
bearblog_url = "https://example.bearblog.dev"
blog_name = "My Gemini Blog"
cert_file = "/path/to/cert.pem"
key_file = "/path/to/key.pem"
# Optional settings
gemini_host = "gemini.example.com"
cache_ttl_feed = 300
cache_ttl_post = 1800
host = "localhost"
port = 1965
[pages]
about = "About Me"
now = "What I am doing now"
Environment Variables
Environment variables override TOML settings:
| Variable | Required | Default | Description |
|---|---|---|---|
BEARBLOG_URL |
Yes | - | The Bearblog URL to proxy |
BLOG_NAME |
Yes | - | Display name for the blog |
CERT_FILE |
No | None |
Path to TLS certificate file |
KEY_FILE |
No | None |
Path to TLS private key file |
PAGES |
No | {} |
JSON dict of static pages |
GEMINI_HOST |
No | None |
Hostname for Gemini URLs in feed |
CACHE_TTL_FEED |
No | 300 |
Feed cache TTL in seconds |
CACHE_TTL_POST |
No | 1800 |
Post cache TTL in seconds |
HOST |
No | localhost |
Server bind address |
PORT |
No | 1965 |
Server port |
Configuration Priority
Settings are loaded in this order (highest to lowest priority):
- Environment variables
ursaproxy.tomlfile- Default values
Usage
Standalone
ursaproxy
The server will start on gemini://localhost:1965/ by default.
As a Library
UrsaProxy can be embedded in other Xitzin applications for virtual hosting:
from ursaproxy import create_app, Settings
# Create settings programmatically
settings = Settings(
bearblog_url="https://myblog.bearblog.dev",
blog_name="My Blog",
)
# Create the app
app = create_app(settings)
# Mount in your main Xitzin app or run directly
app.run(certfile="cert.pem", keyfile="key.pem")
Or load settings from TOML/environment:
from ursaproxy import create_app, load_settings
app = create_app(load_settings())
Routes
| Route | Description |
|---|---|
/ |
Landing page with recent posts and page links |
/post/{slug} |
Individual blog post with date |
/page/{slug} |
Static page (without date) |
/about |
About page from feed metadata |
/feed |
Atom feed with Gemini URLs |
Development
For contributing, clone the repository and install with dev dependencies:
git clone https://github.com/alanbato/ursaproxy.git
cd ursaproxy
uv sync --group dev --group test
Commands
# Run linting
uv run ruff check .
# Run linting with auto-fix
uv run ruff check --fix .
# Format code
uv run ruff format .
# Type check
uv run ty check
# Run all pre-commit hooks
uv run pre-commit run --all-files
# Run tests
uv run pytest
# Run tests with verbose output
uv run pytest -v
Project Structure
src/ursaproxy/
├── __init__.py # Xitzin app, routes, and entry point
├── config.py # Pydantic settings for environment config
├── fetcher.py # HTTP client for fetching Bearblog content
├── converter.py # HTML -> Markdown -> Gemtext pipeline
├── cache.py # Simple TTL cache implementation
└── templates/ # Jinja2 templates
├── index.gmi # Landing page template
├── post.gmi # Post/page template
├── about.gmi # About page template
└── feed.xml # Atom feed template
Testing
The test suite uses pytest with fixtures for offline testing:
# Run all tests
uv run pytest
# Run specific test file
uv run pytest tests/test_converter.py
# Run with coverage (if installed)
uv run pytest --cov=ursaproxy
HTTP requests are mocked using respx, so tests run completely offline.
How It Works
- Feed Fetching: Fetches RSS feed from
{BEARBLOG_URL}/feed/?type=rss - HTML Fetching: Fetches individual pages from
{BEARBLOG_URL}/{slug}/ - Conversion Pipeline:
- Parse HTML with BeautifulSoup
- Extract content from
<main>element - Remove nav, footer, scripts, styles
- Convert to Markdown with markdownify
- Convert to Gemtext with md2gemini
- Caching: Feed and posts are cached with configurable TTLs
- Serving: Content served via Gemini protocol using Xitzin
License
MIT
Metadata
Release files for ursaproxy 0.4.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| ursaproxy-0.4.0.tar.gz | 8.2 kB | With provenance | Details for ursaproxy-0.4.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| ursaproxy-0.4.0-py3-none-any.whl | Python 3 | none | any | Details for ursaproxy-0.4.0-py3-none-any.whl |
Total release size: 19.5 kB
Release files / ursaproxy-0.4.0.tar.gz
| Download URL | ursaproxy-0.4.0.tar.gz |
|---|---|
| Size | 8.2 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
b0adb2a45f49e05dfe3b6d8a99fba956babf6672bd4924f9961091f2644a89c7
|
|
BLAKE2b-256 checksum How to use checksums |
8093663ef9f299241323b90c3590586d659a014c6ddda510dd42d68de926c61c
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.7
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Feb 5, 2026.
Transparency logRelease files / ursaproxy-0.4.0-py3-none-any.whl
| Download URL | ursaproxy-0.4.0-py3-none-any.whl |
|---|---|
| Size | 11.3 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
46d1d98c8bff3f48db77d06c21de636cf98773c0d9c31c5e64f2e7eca14336e3
|
|
BLAKE2b-256 checksum How to use checksums |
8519b4267fd7cf4f7bd1432318ed1053d871c3c549b1ec3b558be01aee533ca5
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.7
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Feb 5, 2026.
Transparency logSecurity for ursaproxy 0.4.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
All 2 files in this release have been uploaded with provenance metadata.
Attestation for ursaproxy-0.4.0.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Feb 5, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.