diff --git a/NOTES.md b/NOTES.md
index d72eda9..974e1ad 100644
--- a/NOTES.md
+++ b/NOTES.md
@@ -1,5 +1,13 @@
 # Notes
 
+## Temp
+- Recalculating `about:feeds` adds pages multiple times to the view
+- Only options for feed files is the download modal - there should be a feed modal before that one
+- Auto feed detection fails on `ebc.li/atom.xml`
+
+- TODO: remove all logger lines
+  
+
 ## Issues
 - URL for each tab should not be stored as a string - in the current code there's lots of reparsing the URL
 
diff --git a/amfora.go b/amfora.go
index c63eeca..d99a94a 100644
--- a/amfora.go
+++ b/amfora.go
@@ -7,6 +7,7 @@ import (
 	"github.com/makeworld-the-better-one/amfora/config"
 	"github.com/makeworld-the-better-one/amfora/display"
 	"github.com/makeworld-the-better-one/amfora/feeds"
+	"github.com/makeworld-the-better-one/amfora/logger"
 )
 
 var (
@@ -16,10 +17,10 @@ var (
 )
 
 func main() {
-	// err := logger.Init()
-	// if err != nil {
-	// 	panic(err)
-	// }
+	err := logger.Init()
+	if err != nil {
+		panic(err)
+	}
 
 	if len(os.Args) > 1 {
 		if os.Args[1] == "--version" || os.Args[1] == "-v" {
@@ -38,7 +39,7 @@ func main() {
 		}
 	}
 
-	err := config.Init()
+	err = config.Init()
 	if err != nil {
 		fmt.Fprintf(os.Stderr, "Config error: %v\n", err)
 		os.Exit(1)
diff --git a/config/config.go b/config/config.go
index c68a62b..8f783f2 100644
--- a/config/config.go
+++ b/config/config.go
@@ -6,7 +6,6 @@ package config
 
 import (
 	"fmt"
-	"io"
 	"os"
 	"path/filepath"
 	"runtime"
@@ -41,7 +40,6 @@ var bkmkPath string
 var DownloadsDir string
 
 // Feeds
-var FeedJSON io.ReadCloser
 var feedDir string
 var FeedPath string
 
@@ -158,8 +156,6 @@ func Init() error {
 	if err != nil {
 		return err
 	}
-	f, _ = os.OpenFile(FeedPath, os.O_RDWR|os.O_CREATE|os.O_EXCL, 0666)
-	FeedJSON = f
 
 	// *** Downloads paths, setup, and creation ***
 
@@ -234,6 +230,7 @@ func Init() error {
 	viper.SetDefault("a-general.page_max_size", 2097152)
 	viper.SetDefault("a-general.page_max_time", 10)
 	viper.SetDefault("a-general.emoji_favicons", false)
+	viper.SetDefault("a-general.feeds_popup", true)
 	viper.SetDefault("keybindings.shift_numbers", "!@#$%^&*()")
 	viper.SetDefault("url-handlers.other", "off")
 	viper.SetDefault("cache.max_size", 0)
diff --git a/config/default.go b/config/default.go
index 6329539..556da67 100644
--- a/config/default.go
+++ b/config/default.go
@@ -68,6 +68,9 @@ page_max_time = 10
 # Whether to replace tab numbers with emoji favicons, which are cached.
 emoji_favicons = false
 
+# Whether a pop-up appears when viewing a potential feed
+feed_popup = true
+
 
 [auth]
 # Authentication settings
@@ -189,6 +192,8 @@ max_pages = 30 # The maximum number of pages the cache will store
 # yesno_modal_text
 # tofu_modal_bg
 # tofu_modal_text
+# feed_modal_bg
+# feed_modal_text
 
 # input_modal_bg
 # input_modal_text
diff --git a/config/theme.go b/config/theme.go
index 6559864..8a34655 100644
--- a/config/theme.go
+++ b/config/theme.go
@@ -38,6 +38,8 @@ var theme = map[string]tcell.Color{
 	"yesno_modal_text":     tcell.ColorWhite,
 	"tofu_modal_bg":        tcell.ColorMaroon,
 	"tofu_modal_text":      tcell.ColorWhite,
+	"feed_modal_bg":        tcell.Color61, // xterm:SlateBlue3, #5f5faf
+	"feed_modal_text":      tcell.ColorWhite,
 
 	"input_modal_bg":         tcell.ColorGreen,
 	"input_modal_text":       tcell.ColorWhite,
diff --git a/default-config.toml b/default-config.toml
index 9ac0d21..5855c02 100644
--- a/default-config.toml
+++ b/default-config.toml
@@ -65,6 +65,9 @@ page_max_time = 10
 # Whether to replace tab numbers with emoji favicons, which are cached.
 emoji_favicons = false
 
+# Whether a pop-up appears when viewing a potential feed
+feed_popup = true
+
 
 [auth]
 # Authentication settings
@@ -186,6 +189,8 @@ max_pages = 30 # The maximum number of pages the cache will store
 # yesno_modal_text
 # tofu_modal_bg
 # tofu_modal_text
+# feed_modal_bg
+# feed_modal_text
 
 # input_modal_bg
 # input_modal_text
diff --git a/display/display.go b/display/display.go
index d2b6921..eaf48d3 100644
--- a/display/display.go
+++ b/display/display.go
@@ -207,6 +207,7 @@ func Init() {
 	})
 
 	// Render the default new tab content ONCE and store it for later
+	// This code is repeated in Reload()
 	newTabContent := getNewTabContent()
 	renderedNewTabContent, newTabLinks := renderer.RenderGemini(newTabContent, textWidth(), leftMargin(), false)
 	newTabPage = structs.Page{
@@ -292,6 +293,13 @@ func Init() {
 					Info("The current page has no content, so it couldn't be downloaded.")
 				}
 				return nil
+			case tcell.KeyCtrlA:
+				Feeds(tabs[curTab])
+				tabs[curTab].addToHistory("about:feeds")
+				return nil
+			case tcell.KeyCtrlX:
+				go addFeed()
+				return nil
 			case tcell.KeyRune:
 				// Regular key was sent
 				switch string(event.Rune()) {
@@ -575,6 +583,11 @@ func URL(u string) {
 		tabs[curTab].addToHistory("about:bookmarks")
 		return
 	}
+	if u == "about:feeds" { //nolint:goconst
+		Feeds(tabs[curTab])
+		tabs[curTab].addToHistory("about:feeds")
+		return
+	}
 	if u == "about:newtab" {
 		temp := newTabPage // Copy
 		setPage(tabs[curTab], &temp)
diff --git a/display/feeds.go b/display/feeds.go
index e326c7a..0203746 100644
--- a/display/feeds.go
+++ b/display/feeds.go
@@ -2,17 +2,25 @@ package display
 
 import (
 	"fmt"
+	"net/url"
+	"path"
+	"strconv"
 	"strings"
 	"time"
 
+	"github.com/gdamore/tcell"
 	"github.com/makeworld-the-better-one/amfora/cache"
+	"github.com/makeworld-the-better-one/amfora/config"
 	"github.com/makeworld-the-better-one/amfora/feeds"
+	"github.com/makeworld-the-better-one/amfora/logger"
 	"github.com/makeworld-the-better-one/amfora/renderer"
 	"github.com/makeworld-the-better-one/amfora/structs"
+	"github.com/mmcdole/gofeed"
+	"github.com/spf13/viper"
 )
 
 var feedPageRaw = "# Feeds & Pages\n\nUpdates" + strings.Repeat(" ", 80-25) + "[Newest -> Oldest]\n" +
-	strings.Repeat("-", 80) + "\n\n"
+	strings.Repeat("-", 80) + "\nSee the help (by pressing ?) for details on how to use this page.\n\n"
 
 var feedPageUpdated time.Time
 
@@ -25,6 +33,8 @@ func toLocalDay(t time.Time) time.Time {
 
 // Feeds displays the feeds page on the current tab.
 func Feeds(t *tab) {
+	logger.Log.Println("display.Feeds called")
+
 	// Retrieve cached version if there hasn't been any updates
 	p, ok := cache.GetPage("about:feeds")
 	if feedPageUpdated.After(feeds.LastUpdated) && ok {
@@ -74,6 +84,108 @@ func Feeds(t *tab) {
 	feedPageUpdated = time.Now()
 }
 
-func feedInit() {
-	// TODO
+// openFeedModal displays the "Add feed/page" modal
+// It returns whether the user wanted to add the feed/page.
+// The tracked arg specifies whether this feed/page is already
+// being tracked.
+func openFeedModal(validFeed, tracked bool) bool {
+	logger.Log.Println("display.openFeedModal called")
+	// Reuses yesNoModal
+
+	if viper.GetBool("a-general.color") {
+		yesNoModal.
+			SetBackgroundColor(config.GetColor("feed_modal_bg")).
+			SetTextColor(config.GetColor("feed_modal_text"))
+		yesNoModal.GetFrame().
+			SetBorderColor(config.GetColor("feed_modal_text")).
+			SetTitleColor(config.GetColor("feed_modal_text"))
+	} else {
+		yesNoModal.
+			SetBackgroundColor(tcell.ColorBlack).
+			SetTextColor(tcell.ColorWhite)
+		yesNoModal.GetFrame().
+			SetBorderColor(tcell.ColorWhite).
+			SetTitleColor(tcell.ColorWhite)
+	}
+	if validFeed {
+		yesNoModal.GetFrame().SetTitle("Feed Tracking")
+		if tracked {
+			yesNoModal.SetText("This is already being tracked. Would you like to manually update it?")
+		} else {
+			yesNoModal.SetText("Would you like to start tracking this feed?")
+		}
+	} else {
+		yesNoModal.GetFrame().SetTitle("Page Tracking")
+		if tracked {
+			yesNoModal.SetText("This is already being tracked. Would you like to manually update it?")
+		} else {
+			yesNoModal.SetText("Would you like to start tracking this page?")
+		}
+	}
+
+	tabPages.ShowPage("yesno")
+	tabPages.SendToFront("yesno")
+	App.SetFocus(yesNoModal)
+	App.Draw()
+
+	resp := <-yesNoCh
+	tabPages.SwitchToPage(strconv.Itoa(curTab))
+	App.SetFocus(tabs[curTab].view)
+	App.Draw()
+	return resp
+}
+
+// getFeedFromPage is like feeds.GetFeed but takes a structs.Page as input.
+func getFeedFromPage(p *structs.Page) (*gofeed.Feed, bool) {
+	parsed, _ := url.Parse(p.URL)
+	filename := path.Base(parsed.Path)
+	r := strings.NewReader(p.Raw)
+	return feeds.GetFeed(p.RawMediatype, filename, r)
+}
+
+// addFeedDirect is only for adding feeds, not pages.
+// It's for when you already have a feed and know if it's tracked.
+// Use mainly by handleURL because it already did a lot of the work.
+//
+// Like addFeed, it should be called in a goroutine.
+func addFeedDirect(u string, feed *gofeed.Feed, tracked bool) {
+	logger.Log.Println("display.addFeedDirect called")
+
+	if openFeedModal(true, tracked) {
+		err := feeds.AddFeed(u, feed)
+		if err != nil {
+			Error("Feed Error", err.Error())
+		}
+	}
+}
+
+// addFeed goes through the process of adding a bookmark for the current page.
+// It is the high-level way of doing it. It should be called in a goroutine.
+func addFeed() {
+	logger.Log.Println("display.addFeed called")
+
+	t := tabs[curTab]
+	p := t.page
+
+	if !t.hasContent() {
+		// It's an about: page, or a malformed one
+		return
+	}
+
+	feed, isFeed := getFeedFromPage(p)
+	tracked := feeds.IsTracked(p.URL)
+
+	if openFeedModal(isFeed, tracked) {
+		var err error
+
+		if isFeed {
+			err = feeds.AddFeed(p.URL, feed)
+		} else {
+			err = feeds.AddPage(p.URL, strings.NewReader(p.Raw))
+		}
+
+		if err != nil {
+			Error("Feed/Page Error", err.Error())
+		}
+	}
 }
diff --git a/display/help.go b/display/help.go
index 1391a07..9a4c8de 100644
--- a/display/help.go
+++ b/display/help.go
@@ -42,6 +42,8 @@ Ctrl-R, R|Reload a page, discarding the cached version.
 Ctrl-B|View bookmarks
 Ctrl-D|Add, change, or remove a bookmark for the current page.
 Ctrl-S|Save the current page to your downloads.
+Ctrl-A|View tracked feeds and pages.
+Ctrl-X|Track or update the current feed/page.
 q, Ctrl-Q|Quit
 Ctrl-C|Hard quit. This can be used when in the middle of downloading,
 |for example.
diff --git a/display/modals.go b/display/modals.go
index fa8ddc3..5a472d9 100644
--- a/display/modals.go
+++ b/display/modals.go
@@ -150,7 +150,6 @@ func modalInit() {
 
 	bkmkInit()
 	dlInit()
-	feedInit()
 }
 
 // Error displays an error on the screen in a modal.
diff --git a/display/newtab.go b/display/newtab.go
index e1755c5..ff212d0 100644
--- a/display/newtab.go
+++ b/display/newtab.go
@@ -18,6 +18,7 @@ You can customize this page by creating a gemtext file called newtab.gmi, in Amf
 Happy browsing!
 
 => about:bookmarks Bookmarks
+=> about:feeds Feed and Page Tracking
 
 => //gemini.circumlunar.space Project Gemini
 => https://github.com/makeworld-the-better-one/amfora Amfora homepage [HTTPS]
diff --git a/display/private.go b/display/private.go
index 8686fd7..ff41f91 100644
--- a/display/private.go
+++ b/display/private.go
@@ -14,6 +14,7 @@ import (
 	"github.com/makeworld-the-better-one/amfora/cache"
 	"github.com/makeworld-the-better-one/amfora/client"
 	"github.com/makeworld-the-better-one/amfora/config"
+	"github.com/makeworld-the-better-one/amfora/feeds"
 	"github.com/makeworld-the-better-one/amfora/renderer"
 	"github.com/makeworld-the-better-one/amfora/structs"
 	"github.com/makeworld-the-better-one/amfora/webbrowser"
@@ -36,6 +37,11 @@ func followLink(t *tab, prev, next string) {
 		t.addToHistory("about:bookmarks")
 		return
 	}
+	if next == "about:feeds" {
+		Feeds(t)
+		t.addToHistory("about:feeds")
+		return
+	}
 	if strings.HasPrefix(next, "about:") {
 		Error("Error", "Not a valid 'about:' URL for linking")
 		return
@@ -328,6 +334,20 @@ func handleURL(t *tab, u string, numRedirects int) (string, bool) {
 			t.barText = oldText
 		}
 		t.mode = tabModeDone
+
+		go func(p *structs.Page) {
+			if b && t.hasContent() && !feeds.IsTracked(s) && viper.GetBool("a-general.feed_popup") {
+				// The current page might be an untracked feed, and the user wants
+				// to be notified in such cases.
+
+				feed, isFeed := getFeedFromPage(p)
+				if isFeed && isValidTab(t) && t.page == p {
+					// After parsing and track-checking time, the page is still being displayed
+					addFeedDirect(p.URL, feed, false)
+				}
+			}
+		}(t.page)
+
 		return s, b
 	}
 
@@ -341,6 +361,10 @@ func handleURL(t *tab, u string, numRedirects int) (string, bool) {
 		Bookmarks(t)
 		return ret("about:bookmarks", true)
 	}
+	if u == "about:feeds" {
+		Feeds(t)
+		return ret("about:feeds", true)
+	}
 
 	u = normalizeURL(u)
 	u = cache.Redirect(u)
diff --git a/feeds/feeds.go b/feeds/feeds.go
index 5ac320a..224a797 100644
--- a/feeds/feeds.go
+++ b/feeds/feeds.go
@@ -10,6 +10,7 @@ import (
 	urlPkg "net/url"
 	"os"
 	"path"
+	"reflect"
 	"sort"
 	"strings"
 	"sync"
@@ -17,6 +18,7 @@ import (
 
 	"github.com/makeworld-the-better-one/amfora/client"
 	"github.com/makeworld-the-better-one/amfora/config"
+	"github.com/makeworld-the-better-one/amfora/logger"
 	"github.com/makeworld-the-better-one/go-gemini"
 	"github.com/mmcdole/gofeed"
 )
@@ -38,22 +40,32 @@ var LastUpdated time.Time
 
 // Init should be called after config.Init.
 func Init() error {
-	defer config.FeedJSON.Close()
+	f, err := os.Open(config.FeedPath)
+	if err == nil {
+		defer f.Close()
 
-	dec := json.NewDecoder(config.FeedJSON)
-	err := dec.Decode(&data)
-	if err != nil && err != io.EOF {
-		return fmt.Errorf("feeds json is corrupted: %v", err) //nolint:goerr113
+		fi, err := f.Stat()
+		if err == nil && fi.Size() > 0 {
+			dec := json.NewDecoder(f)
+			err = dec.Decode(&data)
+			if err != nil && err != io.EOF {
+				return fmt.Errorf("feeds.json is corrupted: %w", err) //nolint:goerr113
+			}
+		}
+	} else if !os.IsNotExist(err) {
+		// There's an error opening the file, but it's not bc is doesn't exist
+		return fmt.Errorf("open feeds.json error: %w", err) //nolint:goerr113
 	}
 
 	LastUpdated = time.Now()
-
 	go updateAll()
 	return nil
 }
 
 // IsTracked returns true if the feed/page URL is already being tracked.
 func IsTracked(url string) bool {
+	logger.Log.Println("feeds.IsTracked called")
+
 	data.feedMu.RLock()
 	for u := range data.Feeds {
 		if url == u {
@@ -76,6 +88,8 @@ func IsTracked(url string) bool {
 // GetFeed returns a Feed object and a bool indicating whether the passed
 // content was actually recognized as a feed.
 func GetFeed(mediatype, filename string, r io.Reader) (*gofeed.Feed, bool) {
+	logger.Log.Println("feeds.GetFeed called")
+
 	if r == nil {
 		return nil, false
 	}
@@ -95,11 +109,14 @@ func GetFeed(mediatype, filename string, r io.Reader) (*gofeed.Feed, bool) {
 }
 
 func writeJSON() error {
+	logger.Log.Println("feeds.writeJSON called")
+
 	writeMu.Lock()
 	defer writeMu.Unlock()
 
 	f, err := os.OpenFile(config.FeedPath, os.O_WRONLY|os.O_CREATE, 0666)
 	if err != nil {
+		logger.Log.Println("feeds.writeJSON error", err)
 		return err
 	}
 	defer f.Close()
@@ -108,9 +125,14 @@ func writeJSON() error {
 	enc.SetIndent("", "  ")
 
 	data.Lock()
+	logger.Log.Println("feeds.writeJSON acquired data lock")
 	err = enc.Encode(&data)
 	data.Unlock()
 
+	if err != nil {
+		logger.Log.Println("feeds.writeJSON error", err)
+	}
+
 	return err
 }
 
@@ -118,6 +140,8 @@ func writeJSON() error {
 // It can be used to update a feed for a URL, although the package
 // will handle that on its own.
 func AddFeed(url string, feed *gofeed.Feed) error {
+	logger.Log.Println("feeds.AddFeed called")
+
 	if feed == nil {
 		panic("feed is nil")
 	}
@@ -128,17 +152,20 @@ func AddFeed(url string, feed *gofeed.Feed) error {
 	}
 
 	data.feedMu.Lock()
-	data.Feeds[url] = feed
-	err := writeJSON()
-	if err != nil {
-		// Don't use in-memory if it couldn't be saved
-		delete(data.Feeds, url)
-		data.feedMu.Unlock()
-		return ErrSaving
-	}
-	data.feedMu.Unlock()
+	oldFeed, ok := data.Feeds[url]
+	if !ok || !reflect.DeepEqual(feed, oldFeed) {
+		// Feeds are different, or there was never an old one
 
-	LastUpdated = time.Now()
+		data.Feeds[url] = feed
+		data.feedMu.Unlock()
+		err := writeJSON()
+		if err != nil {
+			return ErrSaving
+		}
+		LastUpdated = time.Now()
+	} else {
+		data.feedMu.Unlock()
+	}
 	return nil
 }
 
@@ -146,6 +173,8 @@ func AddFeed(url string, feed *gofeed.Feed) error {
 // It can be used to update the page as well, although the package
 // will handle that on its own.
 func AddPage(url string, r io.Reader) error {
+	logger.Log.Println("feeds.AddPage called")
+
 	if r == nil {
 		return nil
 	}
@@ -164,22 +193,23 @@ func AddPage(url string, r io.Reader) error {
 			Hash:    newHash,
 			Changed: time.Now().UTC(),
 		}
-	}
 
-	err := writeJSON()
-	if err != nil {
-		// Don't use in-memory if it couldn't be saved
-		delete(data.Pages, url)
 		data.pageMu.Unlock()
-		return err
+		err := writeJSON()
+		if err != nil {
+			return ErrSaving
+		}
+		LastUpdated = time.Now()
+	} else {
+		data.pageMu.Unlock()
 	}
-	data.pageMu.Unlock()
 
-	LastUpdated = time.Now()
 	return nil
 }
 
 func updateFeed(url string) error {
+	logger.Log.Println("feeds.updateFeed called")
+
 	res, err := client.Fetch(url)
 	if err != nil {
 		if res != nil {
@@ -205,6 +235,8 @@ func updateFeed(url string) error {
 }
 
 func updatePage(url string) error {
+	logger.Log.Println("feeds.updatePage called")
+
 	res, err := client.Fetch(url)
 	if err != nil {
 		if res != nil {
@@ -224,6 +256,8 @@ func updatePage(url string) error {
 // updateAll updates all feeds and pages using workers.
 // It only returns once all the workers are done.
 func updateAll() {
+	logger.Log.Println("feeds.updateAll called")
+
 	// TODO: Is two goroutines the right amount?
 
 	worker := func(jobs <-chan [2]string, wg *sync.WaitGroup) {
@@ -246,10 +280,19 @@ func updateAll() {
 	numJobs := len(data.Feeds) + len(data.Pages)
 	jobs := make(chan [2]string, numJobs)
 
+	if numJobs == 0 {
+		data.RUnlock()
+		return
+	}
+
 	// Start 2 workers, waiting for jobs
 	for w := 0; w < 2; w++ {
 		wg.Add(1)
-		go worker(jobs, &wg)
+		go func(i int) {
+			logger.Log.Println("started worker", i)
+			worker(jobs, &wg)
+			logger.Log.Println("ended worker", i)
+		}(w)
 	}
 
 	// Get map keys in a slice
@@ -277,6 +320,7 @@ func updateAll() {
 			jobs <- [2]string{"page", pageKeys[j-len(feedKeys)]}
 		}
 	}
+	close(jobs)
 
 	wg.Wait()
 }
@@ -287,6 +331,8 @@ func updateAll() {
 // so this function needs to be called again to get updates.
 // It always returns sorted entries - by post time, from newest to oldest.
 func GetPageEntries() *PageEntries {
+	logger.Log.Println("feeds.GetPageEntries called")
+
 	var pe PageEntries
 
 	data.RLock()
diff --git a/feeds/structs.go b/feeds/structs.go
index a5263a8..b21ae07 100644
--- a/feeds/structs.go
+++ b/feeds/structs.go
@@ -34,8 +34,8 @@ The time is in RFC 3339 format, preferably in the UTC timezone.
 
 // Decoded JSON
 type jsonData struct {
-	feedMu sync.RWMutex
-	pageMu sync.RWMutex
+	feedMu *sync.RWMutex
+	pageMu *sync.RWMutex
 	Feeds  map[string]*gofeed.Feed `json:"feeds,omitempty"`
 	Pages  map[string]*pageJSON    `json:"pages,omitempty"`
 }
@@ -69,7 +69,13 @@ type pageJSON struct {
 	Changed time.Time `json:"changed"` // When the latest change happened
 }
 
-var data jsonData // Global instance of jsonData - loaded from JSON and used
+// Global instance of jsonData - loaded from JSON and used
+var data = jsonData{
+	feedMu: &sync.RWMutex{},
+	pageMu: &sync.RWMutex{},
+	Feeds:  make(map[string]*gofeed.Feed),
+	Pages:  make(map[string]*pageJSON),
+}
 
 // PageEntry is a single item on a feed page.
 // It is used both for tracked feeds and pages.
diff --git a/renderer/page.go b/renderer/page.go
index d7afb80..1319d16 100644
--- a/renderer/page.go
+++ b/renderer/page.go
@@ -104,31 +104,34 @@ func MakePage(url string, res *gemini.Response, width, leftMargin int, proxied b
 	if mediatype == "text/gemini" {
 		rendered, links := RenderGemini(utfText, width, leftMargin, proxied)
 		return &structs.Page{
-			Mediatype: structs.TextGemini,
-			URL:       url,
-			Raw:       utfText,
-			Content:   rendered,
-			Links:     links,
+			Mediatype:    structs.TextGemini,
+			RawMediatype: mediatype,
+			URL:          url,
+			Raw:          utfText,
+			Content:      rendered,
+			Links:        links,
 		}, nil
 	} else if strings.HasPrefix(mediatype, "text/") {
 		if mediatype == "text/x-ansi" || strings.HasSuffix(url, ".ans") || strings.HasSuffix(url, ".ansi") {
 			// ANSI
 			return &structs.Page{
-				Mediatype: structs.TextAnsi,
-				URL:       url,
-				Raw:       utfText,
-				Content:   RenderANSI(utfText, leftMargin),
-				Links:     []string{},
+				Mediatype:    structs.TextAnsi,
+				RawMediatype: mediatype,
+				URL:          url,
+				Raw:          utfText,
+				Content:      RenderANSI(utfText, leftMargin),
+				Links:        []string{},
 			}, nil
 		}
 
 		// Treated as plaintext
 		return &structs.Page{
-			Mediatype: structs.TextPlain,
-			URL:       url,
-			Raw:       utfText,
-			Content:   RenderPlainText(utfText, leftMargin),
-			Links:     []string{},
+			Mediatype:    structs.TextPlain,
+			RawMediatype: mediatype,
+			URL:          url,
+			Raw:          utfText,
+			Content:      RenderPlainText(utfText, leftMargin),
+			Links:        []string{},
 		}, nil
 	}
 
diff --git a/structs/structs.go b/structs/structs.go
index 4dd194c..ce78fe0 100644
--- a/structs/structs.go
+++ b/structs/structs.go
@@ -18,18 +18,19 @@ const (
 
 // Page is for storing UTF-8 text/gemini pages, as well as text/plain pages.
 type Page struct {
-	URL        string
-	Mediatype  Mediatype
-	Raw        string   // The raw response, as received over the network
-	Content    string   // The processed content, NOT raw. Uses cview color tags. It will also have a left margin.
-	Links      []string // URLs, for each region in the content.
-	Row        int      // Scroll position
-	Column     int      // ditto
-	Width      int      // The terminal width when the Content was set, to know when reformatting should happen.
-	Selected   string   // The current text or link selected
-	SelectedID string   // The cview region ID for the selected text/link
-	Mode       PageMode
-	Favicon    string
+	URL          string
+	Mediatype    Mediatype // Used for rendering purposes, generalized
+	RawMediatype string    // The actual mediatype sent by the server
+	Raw          string    // The raw response, as received over the network
+	Content      string    // The processed content, NOT raw. Uses cview color tags. It will also have a left margin.
+	Links        []string  // URLs, for each region in the content.
+	Row          int       // Scroll position
+	Column       int       // ditto
+	Width        int       // The terminal width when the Content was set, to know when reformatting should happen.
+	Selected     string    // The current text or link selected
+	SelectedID   string    // The cview region ID for the selected text/link
+	Mode         PageMode
+	Favicon      string
 }
 
 // Size returns an approx. size of a Page in bytes.