Skip to main content

A Bearblog-to-Gemini proxy showcasing Xitzin

Project description

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):

  1. Environment variables
  2. ursaproxy.toml file
  3. 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

  1. Feed Fetching: Fetches RSS feed from {BEARBLOG_URL}/feed/?type=rss
  2. HTML Fetching: Fetches individual pages from {BEARBLOG_URL}/{slug}/
  3. 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
  4. Caching: Feed and posts are cached with configurable TTLs
  5. Serving: Content served via Gemini protocol using Xitzin

License

MIT

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

ursaproxy-0.3.0.tar.gz (8.2 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

ursaproxy-0.3.0-py3-none-any.whl (11.2 kB view details)

Uploaded Python 3

File details

Details for the file ursaproxy-0.3.0.tar.gz.

File metadata

  • Download URL: ursaproxy-0.3.0.tar.gz
  • Upload date:
  • Size: 8.2 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for ursaproxy-0.3.0.tar.gz
Algorithm Hash digest
SHA256 72d0231c8c9a3d74c9ae285028b2877ebf711cf203d9fed67e908d33ae7be54f
MD5 2013096eb3475abdab9ff255f86334eb
BLAKE2b-256 899f0f7d6ffaf7b77b518296b2fef0d96d1c15702ded0c2f9f339d4701f5c97d

See more details on using hashes here.

Provenance

The following attestation bundles were made for ursaproxy-0.3.0.tar.gz:

Publisher: release-pypi.yml on alanbato/ursaproxy

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file ursaproxy-0.3.0-py3-none-any.whl.

File metadata

  • Download URL: ursaproxy-0.3.0-py3-none-any.whl
  • Upload date:
  • Size: 11.2 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for ursaproxy-0.3.0-py3-none-any.whl
Algorithm Hash digest
SHA256 b1379592989d78511411320ec17e6a60fe56e1609e8efb1bbc034526768ab950
MD5 b6f529895fb20d2989262b7a61e04ca4
BLAKE2b-256 a0a9b8377d7c2fa2da92a526a7ecede4e46a5b0b5c7ca2b5d22d19fb7d70a809

See more details on using hashes here.

Provenance

The following attestation bundles were made for ursaproxy-0.3.0-py3-none-any.whl:

Publisher: release-pypi.yml on alanbato/ursaproxy

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page