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.4.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.4.0-py3-none-any.whl (11.3 kB view details)

Uploaded Python 3

File details

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

File metadata

  • Download URL: ursaproxy-0.4.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.4.0.tar.gz
Algorithm Hash digest
SHA256 b0adb2a45f49e05dfe3b6d8a99fba956babf6672bd4924f9961091f2644a89c7
MD5 674f6e44443a4e6f6894885acfaafe9a
BLAKE2b-256 8093663ef9f299241323b90c3590586d659a014c6ddda510dd42d68de926c61c

See more details on using hashes here.

Provenance

The following attestation bundles were made for ursaproxy-0.4.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.4.0-py3-none-any.whl.

File metadata

  • Download URL: ursaproxy-0.4.0-py3-none-any.whl
  • Upload date:
  • Size: 11.3 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.4.0-py3-none-any.whl
Algorithm Hash digest
SHA256 46d1d98c8bff3f48db77d06c21de636cf98773c0d9c31c5e64f2e7eca14336e3
MD5 afe7961be6ff52b937d0ea016bc6404e
BLAKE2b-256 8519b4267fd7cf4f7bd1432318ed1053d871c3c549b1ec3b558be01aee533ca5

See more details on using hashes here.

Provenance

The following attestation bundles were made for ursaproxy-0.4.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