diff --git a/content/upload-content.md b/content/upload-content.md
new file mode 100644
index 00000000..01d3cfcd
--- /dev/null
+++ b/content/upload-content.md
@@ -0,0 +1,44 @@
+# How to Upload Content
+
+bitvid is a decentralized platform, but for larger video files, we currently support **Cloudflare R2** as a storage backend. This allows you to upload files directly from your browser to a high-performance, affordable storage bucket.
+
+## Prerequisites
+
+- A **Cloudflare Account** (free to create).
+- An activated **R2 Plan** (there is a generous free tier, but you may need to add a payment method).
+
+## Step-by-Step Guide
+
+### 1. Create an API Token
+
+To let bitvid upload files to your bucket, you need an API Token with permission to edit R2 storage.
+
+1. Log in to your [Cloudflare Dashboard](https://dash.cloudflare.com/).
+2. Navigate to **R2** in the sidebar.
+3. On the right side of the R2 Overview page, look for **"Manage R2 API Tokens"** and click it.
+4. Click the **"Create API Token"** button.
+5. **Configure the token:**
+ - **Token name**: Enter something recognizable, e.g., "bitvid-upload".
+ - **Permissions**: Select **"Admin Read & Write"** (easiest) or select the specific **"Workers R2 Storage"** permission with **"Edit"** access.
+ - **Specific Bucket(s)**: You can grant access to "All buckets" or a specific one if you already created it.
+ - **TTL**: Set this to "Forever" or a duration of your choice.
+6. Click **"Create API Token"**.
+
+### 2. Copy Your Credentials
+
+Once created, you will see your credentials. **Do not close this page yet!** You cannot see the Secret Access Key again.
+
+Copy the following values:
+
+- **Access Key ID**: Usually starts with a random string.
+- **Secret Access Key**: A long string of characters (this is your password).
+- **Account ID**: This is found in the URL of your dashboard or on the R2 Overview page (e.g., `https://dash.cloudflare.com/
/r2`).
+
+### 3. Configure bitvid
+
+1. Return to the **Upload Video** modal in bitvid.
+2. Click **"Configure R2 Storage"**.
+3. Enter your **Account ID**, **Access Key ID**, and **Secret Access Key**.
+4. Click **"Save Credentials"**.
+
+bitvid will automatically attempt to create a private bucket for your videos if one doesn't exist, or you can use the "Advanced Options" to specify a custom bucket or domain.
diff --git a/verification/upload_content_page.png b/verification/upload_content_page.png
new file mode 100644
index 00000000..6d4227d9
Binary files /dev/null and b/verification/upload_content_page.png differ
diff --git a/verification/upload_modal_link.png b/verification/upload_modal_link.png
new file mode 100644
index 00000000..4970503a
Binary files /dev/null and b/verification/upload_modal_link.png differ
diff --git a/verification/verify_upload.py b/verification/verify_upload.py
new file mode 100644
index 00000000..305414bd
--- /dev/null
+++ b/verification/verify_upload.py
@@ -0,0 +1,69 @@
+
+from playwright.sync_api import sync_playwright, expect
+
+def run(playwright):
+ browser = playwright.chromium.launch(headless=True)
+ page = browser.new_page()
+
+ # Navigate to the app
+ page.goto("http://localhost:8080")
+
+ # Wait for app
+ page.wait_for_selector("#app")
+
+ # Dismiss disclaimer if present
+ try:
+ if page.locator("#disclaimerModal").is_visible():
+ page.locator("#disclaimerModal").get_by_role("button", name="I understand").click(timeout=2000)
+ except:
+ pass
+
+ # 1. Verify the "Upload Content" Page
+ print("Navigating to Upload Content view...")
+ page.goto("http://localhost:8080/#view=upload-content")
+ page.wait_for_timeout(2000)
+
+ expect(page.locator("h2#uploadContentHeading")).to_have_text("Upload Content")
+ page_icon = page.locator("#viewContainer img[src='assets/svg/upload-content-icon.svg']")
+ expect(page_icon).to_be_visible()
+ page.screenshot(path="verification/upload_content_page.png")
+ print("Screenshot of page saved.")
+
+ # 2. Verify the Link in Upload Modal
+ # Force remove 'hidden' class from uploadButton
+ page.evaluate("document.getElementById('uploadButton').classList.remove('hidden')")
+
+ # Wait for it to be visible? Playwright waits for visibility.
+ # Sometimes removing class isn't enough if parent is hidden, but parent is visible header.
+ # The previous error said "element is not visible".
+ # Let's force click via JS if UI click fails, or force visible style.
+
+ page.evaluate("document.getElementById('uploadButton').style.display = 'block'")
+ page.wait_for_selector("#uploadButton", state="visible")
+ page.locator("#uploadButton").click()
+
+ # Verify modal visible
+ modal = page.locator("#uploadModal")
+ # Modal also has 'hidden' class toggled by JS.
+ # Wait for it.
+ expect(modal).to_be_visible()
+
+ # Open storage settings
+ page.locator("#btn-storage-settings").click()
+
+ # Verify link
+ link = page.locator("#section-storage-settings").get_by_role("link", name="Learn How")
+ expect(link).to_be_visible()
+ expect(link).to_have_attribute("href", "#view=upload-content")
+
+ # Verify link icon
+ link_icon = link.locator("img")
+ expect(link_icon).to_have_attribute("src", "assets/svg/upload-content-icon.svg")
+
+ page.screenshot(path="verification/upload_modal_link.png")
+ print("Screenshot of modal saved.")
+
+ browser.close()
+
+with sync_playwright() as playwright:
+ run(playwright)
diff --git a/views/upload-content.html b/views/upload-content.html
new file mode 100644
index 00000000..7b5fce87
--- /dev/null
+++ b/views/upload-content.html
@@ -0,0 +1,77 @@
+
+
+
+
+
+

+
+ Upload Content
+
+
+
+
+
+
+
+