Mighil

configuring bear blog's media proxy

Update: Test instance is now running on deadauthor.org (only my own domains whitelisted for signups and testing). Here's the Dockerfile.

I've been a Bear user for a while, and today I finally tried setting up Bear locally. Since I already have my Cloudflare Tunnel configured, I figured -- why not containerize Bear and experiment a bit? This blog post walks through how I configured Bear's media proxy to serve images from DigitalOcean Spaces with clean, branded URLs.

The Setup

I connected my own DigitalOcean Spaces bucket to Bear Blog and configured it to serve media through branded proxy URLs instead of direct object storage links. This approach allows media to be served through blog's subdomain while maintaining a single backend storage bucket.

The goal:

Configuration Steps

1. URL Pattern Configuration (blogs/urls.py)

Define the media proxy route to accept file paths with multiple segments:

path('media/<path:img>', media.image_proxy_no_slash, name="image-proxy"),

Using <path:img> is crucial here -- it allows the URL pattern to capture paths containing slashes (like mgx/chonker.jpg), whereas <str:img> would only capture the first segment.

2. Media Proxy Function (blogs/views/media.py)

Implement the proxy handler to fetch media from DigitalOcean Spaces and stream it back to the client:

def image_proxy_no_slash(request, img):
    """Handle media proxy URLs without trailing slash"""
    return image_proxy(request, img)

def image_proxy(request, img):
    # Remove any trailing slashes from the path
    if img.endswith('/'):
        img = img.rstrip('/')

    # Construct the DigitalOcean Spaces URL
    remote_url = f'https://{bucket_name}.sfo3.digitaloceanspaces.com/{img}'

    # Stream the content from the remote URL
    response = requests.get(remote_url, stream=True, timeout=10)

    # Define a generator to yield chunks of the response content
    def generate():
        for chunk in response.iter_content(chunk_size=8192):
            yield chunk

    # Return a StreamingHttpResponse
    return StreamingHttpResponse(
        generate(),
        status=response.status_code,
        content_type=response.headers['Content-Type']
    )

3. Enhanced Copy URL Functionality (blogs/models.py & templates/dashboard/media.html)

Add a property to Media model that generates clean proxy URLs:

@property
def proxy_url(self):
    """Generate proxy URL for this media file"""
    # Extract the file path from the full URL
    # URL format: https://bearblog.sfo3.digitaloceanspaces.com/notes/filename.webp
    # I want: https://notes.deadauthor.org/media/notes/chonker.jpg
    file_path = self.url.split('/')[-2] + '/' + self.url.split('/')[-1]
    return f"https://{self.blog.subdomain}.deadauthor.org/media/{file_path}"

Update the copy URL button in media dashboard to use the proxy URL:

<!-- Before: Copied raw Digital Ocean Spaces URLs -->
<button type="button" onclick="navigator.clipboard.writeText('{{ image.url }}').then(() => alert('URL copied to clipboard'));">Copy URL</button>

<!-- After: Copies clean proxy URLs -->
<button type="button" onclick="navigator.clipboard.writeText('{{ image.proxy_url }}').then(() => alert('URL copied to clipboard'));">Copy URL</button>

Tagged in bear blog, tech