about summary refs log tree commit homepage
diff options
context:
space:
mode:
authorEric Wong <e@80x24.org>2015-09-01 08:55:26 +0000
committerEric Wong <e@80x24.org>2015-09-01 08:56:07 +0000
commit1b4b2c7b8b2f2df8f114617d2e875eaf5c839ce0 (patch)
tree692f3f27b9b763d0fdc7b594246182f0957df64b
parent97bef984363d1279a6ac130d35f063a834e9c241 (diff)
downloadpublic-inbox-1b4b2c7b8b2f2df8f114617d2e875eaf5c839ce0.tar.gz
This allows common /m/ links to be used without a prefix,
saving 2 precious bytes for permalinks and raw messages.

Old URLs continue to redirect.
-rw-r--r--Documentation/design_www.txt37
-rw-r--r--lib/PublicInbox/Feed.pm19
-rw-r--r--lib/PublicInbox/View.pm48
-rw-r--r--lib/PublicInbox/WWW.pm177
-rw-r--r--t/cgi.t20
-rw-r--r--t/feed.t2
-rw-r--r--t/plack.t32
-rw-r--r--t/view.t6
8 files changed, 179 insertions, 162 deletions
diff --git a/Documentation/design_www.txt b/Documentation/design_www.txt
index a11c3896..b73a7987 100644
--- a/Documentation/design_www.txt
+++ b/Documentation/design_www.txt
@@ -2,29 +2,28 @@ URL naming
 ----------
 
 ### Unstable endpoints
-/$LISTNAME/?r=$GIT_COMMIT                       -> HTML only
-/$LISTNAME/new.atom                        -> Atom feed
+/$LISTNAME/?r=$GIT_COMMIT                 -> HTML only
+/$LISTNAME/new.atom                       -> Atom feed
 
 #### Optional, relies on Search::Xapian
-/$LISTNAME/t/$MESSAGE_ID/                  -> HTML content of thread
-/$LISTNAME/t/$MESSAGE_ID/atom              -> Atom feed for thread
-/$LISTNAME/t/$MESSAGE_ID/mbox.gz           -> gzipped mbox of thread
+/$LISTNAME/$MESSAGE_ID/t/                 -> HTML content of thread
+/$LISTNAME/$MESSAGE_ID/t.atom             -> Atom feed for thread
+/$LISTNAME/$MESSAGE_ID/t.mbox.gz          -> gzipped mbox of thread
 
 ### Stable endpoints
-/$LISTNAME/m/$MESSAGE_ID/                  -> HTML content (short quotes)
-/$LISTNAME/m/$MESSAGE_ID                   -> 301 to above
-/$LISTNAME/m/$MESSAGE_ID/raw               -> raw mbox
-/$LISTNAME/f/$MESSAGE_ID/                  -> HTML content (full quotes)
-/$LISTNAME/f/$MESSAGE_ID                   -> 301 to above
-/$LISTNAME/f/$MESSAGE_ID/raw [1]           -> 301 to ../m/$MESSAGE_ID/raw
-
-### Legacy endpoints (may be ambiguous given Message-IDs with similar suffixes)
-/$LISTNAME/m/$MESSAGE_ID.html              -> 301 to $MESSAGE_ID/
-/$LISTNAME/m/$MESSAGE_ID.txt               -> 301 to $MESSAGE_ID/raw
-/$LISTNAME/f/$MESSAGE_ID.html              -> 301 to $MESSAGE_ID/
-/$LISTNAME/f/$MESSAGE_ID.txt [1]           -> 301 to ../m/$MESSAGE_ID/raw
-
-/$LISTNAME/atom.xml [2]                    -> identical to /$LISTNAME/new.atom
+/$LISTNAME/$MESSAGE_ID/                   -> HTML content (short quotes)
+/$LISTNAME/$MESSAGE_ID                    -> 301 to /$LISTNAME/$MESSAGE_ID
+/$LISTNAME/$MESSAGE_ID/raw                -> raw mbox
+/$LISTNAME/$MESSAGE_ID/f/                 -> HTML content (full quotes)
+
+### Legacy endpoints (may be ambiguous given Message-IDs with similar suffies)
+/$LISTNAME/m/$MESSAGE_ID/                 -> 301 to /$LISTNAME/$MESSAGE_ID/
+/$LISTNAME/m/$MESSAGE_ID.html             -> 301 to /$LISTNAME/$MESSAGE_ID/
+/$LISTNAME/m/$MESSAGE_ID.txt              -> 301 to /$LISTNAME/$MESSAGE_ID/raw
+/$LISTNAME/f/$MESSAGE_ID.html             -> 301 to /$LISTNAME/$MESSAGE_ID/f/
+/$LISTNAME/f/$MESSAGE_ID.txt [1]          -> 301 to /$LISTNAME/$MESSAGE_ID/raw
+
+/$LISTNAME/atom.xml [2]                   -> identical to /$LISTNAME/new.atom
 
 FIXME: we must refactor/cleanup/add tests for most of our CGI before
 adding more endpoints and features.
diff --git a/lib/PublicInbox/Feed.pm b/lib/PublicInbox/Feed.pm
index 9d581935..4420fde1 100644
--- a/lib/PublicInbox/Feed.pm
+++ b/lib/PublicInbox/Feed.pm
@@ -101,7 +101,7 @@ sub emit_atom_thread {
         my $feed_opts = get_feedopts($ctx);
 
         my $html_url = $feed_opts->{atomurl} = $ctx->{self_url};
-        $html_url =~ s!/atom\z!/!;
+        $html_url =~ s!/t\.atom\z!/!;
         $feed_opts->{url} = $html_url;
         $feed_opts->{emit_header} = 1;
 
@@ -285,7 +285,7 @@ sub get_feedopts {
                 }
                 $url_base = "$base/$listname";
                 if (my $mid = $ctx->{mid}) { # per-thread feed:
-                        $rv{atomurl} = "$url_base/t/$mid/atom";
+                        $rv{atomurl} = "$url_base/$mid/t.atom";
                 } else {
                         $rv{atomurl} = "$url_base/new.atom";
                 }
@@ -294,8 +294,7 @@ sub get_feedopts {
                 $rv{atomurl} = "$url_base/new.atom";
         }
         $rv{url} ||= "$url_base/";
-        $rv{midurl} = "$url_base/m/";
-        $rv{fullurl} = "$url_base/f/";
+        $rv{midurl} = "$url_base/";
 
         \%rv;
 }
@@ -317,14 +316,15 @@ sub add_to_feed {
         my ($feed_opts, $fh, $add, $git) = @_;
 
         my $mime = do_cat_mail($git, $add) or return 0;
-        my $fullurl = $feed_opts->{fullurl} || 'http://example.com/f/';
+        my $url = $feed_opts->{url};
+        my $midurl = $feed_opts->{midurl};
 
         my $header_obj = $mime->header_obj;
         my $mid = $header_obj->header('Message-ID');
         defined $mid or return 0;
         $mid = PublicInbox::Hval->new_msgid($mid);
-        my $href = $mid->as_href . '/';
-        my $content = PublicInbox::View->feed_entry($mime, $fullurl . $href);
+        my $href = $mid->as_href;
+        my $content = PublicInbox::View->feed_entry($mime, "$midurl$href/f/");
         defined($content) or return 0;
         $mime = undef;
 
@@ -355,8 +355,7 @@ sub add_to_feed {
         my $h = '[a-f0-9]';
         my (@uuid5) = ($add =~ m!\A($h{8})($h{4})($h{4})($h{4})($h{12})!o);
         my $id = 'urn:uuid:' . join('-', @uuid5);
-        my $midurl = $feed_opts->{midurl};
-        $fh->write(qq{</div></content><link\nhref="$midurl$href"/>}.
+        $fh->write(qq!</div></content><link\nhref="$midurl$href/"/>!.
                    "<id>$id</id></entry>");
         1;
 }
@@ -414,7 +413,7 @@ sub dump_topics {
                 $mid = PublicInbox::Hval->new($mid)->as_href;
                 $subj = PublicInbox::Hval->new($subj)->as_html;
                 $u = PublicInbox::Hval->new($u)->as_html;
-                $dst .= "\n<a\nhref=\"t/$mid/#u\"><b>$subj</b></a>\n- ";
+                $dst .= "\n<a\nhref=\"$mid/t/#u\"><b>$subj</b></a>\n- ";
                 $ts = strftime('%Y-%m-%d %H:%M', gmtime($ts));
                 if ($n == 1) {
                         $dst .= "created by $u @ $ts UTC\n"
diff --git a/lib/PublicInbox/View.pm b/lib/PublicInbox/View.pm
index a30bf70a..2be16b43 100644
--- a/lib/PublicInbox/View.pm
+++ b/lib/PublicInbox/View.pm
@@ -80,7 +80,7 @@ sub index_entry {
                 $anchor = $seen->{$anchor_idx};
         }
         if ($srch) {
-                $subj = "<a\nhref=\"${path}t/$href/#u\">$subj</a>";
+                $subj = "<a\nhref=\"${path}$href/t/#u\">$subj</a>";
         }
         if ($root_anchor && $root_anchor eq $id) {
                 $subj = "<u\nid=\"u\">$subj</u>";
@@ -101,9 +101,9 @@ sub index_entry {
         $fh->write($rv .= "\n\n");
 
         my ($fhref, $more_ref);
-        my $mhref = "${path}m/$href/";
+        my $mhref = "${path}$href/";
         if ($level > 0) {
-                $fhref = "${path}f/$href/";
+                $fhref = "${path}$href/f/";
                 $more_ref = \$more;
         }
         # scan through all parts, looking for displayable text
@@ -112,7 +112,7 @@ sub index_entry {
         });
         $mime->body_set('');
 
-        my $txt = "${path}m/$href/raw";
+        my $txt = "${path}$href/raw";
         $rv = "\n<a\nhref=\"$mhref\">$more</a> <a\nhref=\"$txt\">raw</a> ";
         $rv .= html_footer($mime, 0, undef, $ctx);
 
@@ -120,7 +120,7 @@ sub index_entry {
                 unless (defined $anchor) {
                         my $v = PublicInbox::Hval->new_msgid($irt);
                         $v = $v->as_href;
-                        $anchor = "${path}m/$v/";
+                        $anchor = "${path}$v/";
                         $seen->{$anchor_idx} = $anchor;
                 }
                 $rv .= " <a\nhref=\"$anchor\">parent</a>";
@@ -160,8 +160,8 @@ sub emit_thread_html {
         my $next = "<a\nid=\"s$final_anchor\">";
         $next .= $final_anchor == 1 ? 'only message in' : 'end of';
         $next .= " thread</a>, back to <a\nhref=\"../../\">index</a>\n";
-        $next .= "download: <a\nhref=\"mbox.gz\">mbox.gz</a>";
-        $next .= " / <a\nhref=\"atom\">Atom feed</a>\n\n";
+        $next .= "download: <a\nhref=\"../t.mbox.gz\">mbox.gz</a>";
+        $next .= " / <a\nhref=\"../t.atom\">Atom feed</a>\n\n";
         $fh->write("<hr />" . PRE_WRAP . $next . $foot .
                    "</pre></body></html>");
         $fh->close;
@@ -349,8 +349,8 @@ sub headers_to_html_header {
                 } elsif ($h eq 'Subject') {
                         $title[0] = $v->as_html;
                         if ($srch) {
-                                $rv .= "$h: <a\nid=\"t\"\n" .
-                                        "href=\"../../t/$mid_href/\">";
+                                my $p = $full_pfx ? '' : '../';
+                                $rv .= "$h: <a\nid=\"t\"\nhref=\"${p}t/#u\">";
                                 $rv .= $v->as_html . "</a>\n";
                                 next;
                         }
@@ -359,7 +359,7 @@ sub headers_to_html_header {
 
         }
         $rv .= 'Message-ID: &lt;' . $mid->as_html . '&gt; ';
-        my $raw_ref = $full_pfx ? 'raw' : "../../m/$mid_href/raw";
+        my $raw_ref = $full_pfx ? 'raw' : '../raw';
         $rv .= "(<a\nhref=\"$raw_ref\">raw</a>)\n";
         if ($srch) {
                 $rv .= "<a\nhref=\"#r\">References: [see below]</a>\n";
@@ -373,7 +373,7 @@ sub headers_to_html_header {
 }
 
 sub thread_inline {
-        my ($dst, $ctx, $cur) = @_;
+        my ($dst, $ctx, $cur, $full_pfx) = @_;
         my $srch = $ctx->{srch};
         my $mid = mid_compress(mid_clean($cur->header('Message-ID')));
         my $res = $srch->get_thread($mid);
@@ -383,9 +383,10 @@ sub thread_inline {
                 $$dst .= "\n[no followups, yet]</a>\n";
                 return;
         }
+        my $upfx = $full_pfx ? '' : '../';
 
         $$dst .= "\n\n~$nr messages in thread: ".
-                 "(<a\nhref=\"../../t/$mid/#u\">expand</a>)\n";
+                 "(<a\nhref=\"${upfx}t/#u\">expand</a>)\n";
         my $subj = $srch->subject_path($cur->header('Subject'));
         my $state = {
                 seen => { $subj => 1 },
@@ -393,7 +394,7 @@ sub thread_inline {
                 cur => $mid,
         };
         for (thread_results(load_results($res))->rootset) {
-                inline_dump($dst, $state, $_, 0);
+                inline_dump($dst, $state, $upfx, $_, 0);
         }
         $state->{next_msg};
 }
@@ -461,19 +462,20 @@ sub html_footer {
         my $href = "mailto:$to?In-Reply-To=$irt&Cc=${cc}&Subject=$subj";
 
         my $srch = $ctx->{srch} if $ctx;
-        my $idx = $standalone ? " <a\nhref=\"../../\">index</a>" : '';
+        my $upfx = $full_pfx ? '../' : '../../';
+        my $idx = $standalone ? "<a\nhref=\"$upfx\">index</a>" : '';
         if ($idx && $srch) {
-                my $next = thread_inline(\$idx, $ctx, $mime);
+                my $next = thread_inline(\$idx, $ctx, $mime, $full_pfx);
                 $irt = $mime->header('In-Reply-To');
                 if (defined $irt) {
                         $irt = PublicInbox::Hval->new_msgid($irt);
                         $irt = $irt->as_href;
-                        $irt = "<a\nhref=\"../$irt/\">parent</a> ";
+                        $irt = "<a\nhref=\"$upfx$irt/\">parent</a> ";
                 } else {
                         $irt = ' ' x length('parent ');
                 }
                 if ($next) {
-                        $irt .= "<a\nhref=\"../$next/\">next</a> ";
+                        $irt .= "<a\nhref=\"$upfx$next/\">next</a> ";
                 } else {
                         $irt .= '     ';
                 }
@@ -564,7 +566,7 @@ sub _msg_date {
 }
 
 sub _inline_header {
-        my ($dst, $state, $mime, $level) = @_;
+        my ($dst, $state, $upfx, $mime, $level) = @_;
         my $pfx = '  ' x $level;
 
         my $cur = $state->{cur};
@@ -601,7 +603,7 @@ sub _inline_header {
                 $s = $s->as_html;
         }
         my $m = PublicInbox::Hval->new_msgid($mid);
-        $m = '../' . $m->as_href . '/';
+        $m = $upfx . '../' . $m->as_href . '/';
         if (defined $s) {
                 $$dst .= "$pfx` <a\nhref=\"$m\">$s</a>\n" .
                          "$pfx  $f @ $d\n";
@@ -611,14 +613,14 @@ sub _inline_header {
 }
 
 sub inline_dump {
-        my ($dst, $state, $node, $level) = @_;
+        my ($dst, $state, $upfx, $node, $level) = @_;
         return unless $node;
         return if $state->{stopped};
         if (my $mime = $node->message) {
-                _inline_header($dst, $state, $mime, $level);
+                _inline_header($dst, $state, $upfx, $mime, $level);
         }
-        inline_dump($dst, $state, $node->child, $level+1);
-        inline_dump($dst, $state, $node->next, $level);
+        inline_dump($dst, $state, $upfx, $node->child, $level+1);
+        inline_dump($dst, $state, $upfx, $node->next, $level);
 }
 
 1;
diff --git a/lib/PublicInbox/WWW.pm b/lib/PublicInbox/WWW.pm
index a9cb6d71..d666a1b7 100644
--- a/lib/PublicInbox/WWW.pm
+++ b/lib/PublicInbox/WWW.pm
@@ -16,6 +16,7 @@ use URI::Escape qw(uri_escape_utf8 uri_unescape);
 use constant SSOMA_URL => 'http://ssoma.public-inbox.org/';
 use constant PI_URL => 'http://public-inbox.org/';
 our $LISTNAME_RE = qr!\A/([\w\.\-]+)!;
+our $MID_RE = qr!([^/]+)!;
 our $pi_config;
 
 sub run {
@@ -31,56 +32,37 @@ sub run {
         if ($path_info eq '/') {
                 r404();
         } elsif ($path_info =~ m!$LISTNAME_RE\z!o) {
-                invalid_list(\%ctx, $1) || redirect_list_index($cgi);
+                invalid_list(\%ctx, $1) || r301(\%ctx, $1);
         } elsif ($path_info =~ m!$LISTNAME_RE(?:/|/index\.html)?\z!o) {
                 invalid_list(\%ctx, $1) || get_index(\%ctx);
         } elsif ($path_info =~ m!$LISTNAME_RE/(?:atom\.xml|new\.atom)\z!o) {
                 invalid_list(\%ctx, $1) || get_atom(\%ctx);
 
+        # thread display
+        } elsif ($path_info =~ m!$LISTNAME_RE/$MID_RE/t/\z!o) {
+                invalid_list_mid(\%ctx, $1, $2) || get_thread(\%ctx);
+        } elsif ($path_info =~ m!$LISTNAME_RE/$MID_RE/t\.mbox(\.gz)?\z!o) {
+                my $sfx = $3;
+                invalid_list_mid(\%ctx, $1, $2) || get_thread_mbox(\%ctx, $sfx);
+        } elsif ($path_info =~ m!$LISTNAME_RE/$MID_RE/t\.atom\z!o) {
+                invalid_list_mid(\%ctx, $1, $2) || get_thread_atom(\%ctx);
+
         # single-message pages
-        } elsif ($path_info =~ m!$LISTNAME_RE/m/(\S+)/\z!o) {
+        } elsif ($path_info =~ m!$LISTNAME_RE/$MID_RE/\z!o) {
                 invalid_list_mid(\%ctx, $1, $2) || get_mid_html(\%ctx);
-        } elsif ($path_info =~ m!$LISTNAME_RE/m/(\S+)/raw\z!o) {
+        } elsif ($path_info =~ m!$LISTNAME_RE/$MID_RE/raw\z!o) {
                 invalid_list_mid(\%ctx, $1, $2) || get_mid_txt(\%ctx);
 
         # full-message page
-        } elsif ($path_info =~ m!$LISTNAME_RE/f/(\S+)/\z!o) {
+        } elsif ($path_info =~ m!$LISTNAME_RE/$MID_RE/f/\z!o) {
                 invalid_list_mid(\%ctx, $1, $2) || get_full_html(\%ctx);
 
-        # thread display
-        } elsif ($path_info =~ m!$LISTNAME_RE/t/(\S+)/\z!o) {
-                invalid_list_mid(\%ctx, $1, $2) || get_thread(\%ctx);
-
-        } elsif ($path_info =~ m!$LISTNAME_RE/t/(\S+)/mbox(\.gz)?\z!o) {
-                my $sfx = $3;
-                invalid_list_mid(\%ctx, $1, $2) ||
-                        get_thread_mbox(\%ctx, $sfx);
-
-        } elsif ($path_info =~ m!$LISTNAME_RE/t/(\S+)/atom\z!o) {
-                invalid_list_mid(\%ctx, $1, $2) || get_thread_atom(\%ctx);
-
-        # legacy redirects
-        } elsif ($path_info =~ m!$LISTNAME_RE/(t|m|f)/(\S+)\.html\z!o) {
-                my $pfx = $2;
-                invalid_list_mid(\%ctx, $1, $3) ||
-                        redirect_mid(\%ctx, $pfx, qr/\.html\z/, '/');
-        } elsif ($path_info =~ m!$LISTNAME_RE/(m|f)/(\S+)\.txt\z!o) {
-                my $pfx = $2;
-                invalid_list_mid(\%ctx, $1, $3) ||
-                        redirect_mid(\%ctx, $pfx, qr/\.txt\z/, '/raw');
-        } elsif ($path_info =~ m!$LISTNAME_RE/t/(\S+)(\.mbox(?:\.gz)?)\z!o) {
-                my $end = $3;
-                invalid_list_mid(\%ctx, $1, $2) ||
-                        redirect_mid(\%ctx, 't', $end, '/mbox.gz');
-
-        # convenience redirects, order matters
-        } elsif ($path_info =~ m!$LISTNAME_RE/(m|f|t|s)/(\S+)\z!o) {
-                my $pfx = $2;
-                invalid_list_mid(\%ctx, $1, $3) ||
-                        redirect_mid(\%ctx, $pfx, qr/\z/, '/');
+        # convenience redirects order matters
+        } elsif ($path_info =~ m!$LISTNAME_RE/([^/]{2,})\z!o) {
+                r301(\%ctx, $1, $2);
 
         } else {
-                r404();
+                legacy_redirects(\%ctx, $path_info);
         }
 }
 
@@ -163,7 +145,7 @@ sub mid2blob {
         }
 }
 
-# /$LISTNAME/m/$MESSAGE_ID.txt                    -> raw mbox
+# /$LISTNAME/$MESSAGE_ID/raw                    -> raw mbox
 sub get_mid_txt {
         my ($ctx) = @_;
         my $x = mid2blob($ctx) or return r404();
@@ -171,22 +153,21 @@ sub get_mid_txt {
         PublicInbox::Mbox::emit1($x);
 }
 
-# /$LISTNAME/m/$MESSAGE_ID.html                   -> HTML content (short quotes)
+# /$LISTNAME/$MESSAGE_ID/                   -> HTML content (short quotes)
 sub get_mid_html {
         my ($ctx) = @_;
         my $x = mid2blob($ctx) or return r404();
 
         require PublicInbox::View;
-        my $pfx = msg_pfx($ctx);
         my $foot = footer($ctx);
         require Email::MIME;
         my $mime = Email::MIME->new($x);
         searcher($ctx);
         [ 200, [ 'Content-Type' => 'text/html; charset=UTF-8' ],
-          [ PublicInbox::View::msg_html($ctx, $mime, $pfx, $foot) ] ];
+          [ PublicInbox::View::msg_html($ctx, $mime, 'f/', $foot) ] ];
 }
 
-# /$LISTNAME/f/$MESSAGE_ID.html                   -> HTML content (fullquotes)
+# /$LISTNAME/$MESSAGE_ID/f/                   -> HTML content (fullquotes)
 sub get_full_html {
         my ($ctx) = @_;
         my $x = mid2blob($ctx) or return r404();
@@ -200,7 +181,7 @@ sub get_full_html {
           [ PublicInbox::View::msg_html($ctx, $mime, undef, $foot)] ];
 }
 
-# /$LISTNAME/t/$MESSAGE_ID.html
+# /$LISTNAME/$MESSAGE_ID/t/
 sub get_thread {
         my ($ctx) = @_;
         my $srch = searcher($ctx) or return need_search($ctx);
@@ -214,39 +195,6 @@ sub self_url {
         ref($cgi) eq 'CGI' ? $cgi->self_url : $cgi->uri->as_string;
 }
 
-sub redirect_list_index {
-        my ($cgi) = @_;
-        do_redirect(self_url($cgi) . "/");
-}
-
-sub redirect_mid {
-        my ($ctx, $pfx, $old, $sfx) = @_;
-        my $url = self_url($ctx->{cgi});
-        my $anchor = '';
-        if (lc($pfx) eq 't' && $sfx eq '/') {
-                $anchor = '#u'; # <u id='#u'> is used to highlight in View.pm
-        }
-        $url =~ s/$old/$sfx/;
-        do_redirect($url . $anchor);
-}
-
-# only hit when somebody tries to guess URLs manually:
-sub redirect_mid_txt {
-        my ($ctx, $pfx) = @_;
-        my $listname = $ctx->{listname};
-        my $url = self_url($ctx->{cgi});
-        $url =~ s!/$listname/f/(\S+\.txt)\z!/$listname/m/$1!;
-        do_redirect($url);
-}
-
-sub do_redirect {
-        my ($url) = @_;
-        [ 301,
-          [ Location => $url, 'Content-Type' => 'text/plain' ],
-          [ "Redirecting to $url\n" ]
-        ]
-}
-
 sub ctx_get {
         my ($ctx, $key) = @_;
         my $val = $ctx->{$key};
@@ -333,14 +281,8 @@ EOF
         [ 501, [ 'Content-Type' => 'text/html; charset=UTF-8' ], [ $msg ] ];
 }
 
-sub msg_pfx {
-        my ($ctx) = @_;
-        my $href = PublicInbox::Hval::ascii_html(uri_escape_utf8($ctx->{mid}));
-        "../../f/$href/";
-}
-
-# /$LISTNAME/t/$MESSAGE_ID/mbox           -> thread as mbox
-# /$LISTNAME/t/$MESSAGE_ID/mbox.gz        -> thread as gzipped mbox
+# /$LISTNAME/$MESSAGE_ID/t.mbox           -> thread as mbox
+# /$LISTNAME/$MESSAGE_ID/t.mbox.gz        -> thread as gzipped mbox
 # note: I'm not a big fan of other compression formats since they're
 # significantly more expensive on CPU than gzip and less-widely available,
 # especially on older systems.  Stick to zlib since that's what git uses.
@@ -352,7 +294,7 @@ sub get_thread_mbox {
 }
 
 
-# /$LISTNAME/t/$MESSAGE_ID/atom                  -> thread as Atom feed
+# /$LISTNAME/$MESSAGE_ID/t.atom                  -> thread as Atom feed
 sub get_thread_atom {
         my ($ctx) = @_;
         searcher($ctx) or return need_search($ctx);
@@ -361,4 +303,71 @@ sub get_thread_atom {
         PublicInbox::Feed::generate_thread_atom($ctx);
 }
 
+sub legacy_redirects {
+        my ($ctx, $path_info) = @_;
+
+        # single-message pages
+        if ($path_info =~ m!$LISTNAME_RE/m/(\S+)/\z!o) {
+                r301($ctx, $1, $2);
+        } elsif ($path_info =~ m!$LISTNAME_RE/m/(\S+)/raw\z!o) {
+                r301($ctx, $1, $2, 'raw');
+
+        } elsif ($path_info =~ m!$LISTNAME_RE/f/(\S+)/\z!o) {
+                r301($ctx, $1, $2, 'f/');
+
+        # thread display
+        } elsif ($path_info =~ m!$LISTNAME_RE/t/(\S+)/\z!o) {
+                r301($ctx, $1, $2, 't/#u');
+
+        } elsif ($path_info =~ m!$LISTNAME_RE/t/(\S+)/mbox(\.gz)?\z!o) {
+                r301($ctx, $1, $2, "t.mbox$3");
+
+        # even older legacy redirects
+        } elsif ($path_info =~ m!$LISTNAME_RE/m/(\S+)\.html\z!o) {
+                r301($ctx, $1, $2);
+
+        } elsif ($path_info =~ m!$LISTNAME_RE/t/(\S+)\.html\z!o) {
+                r301($ctx, $1, $2, 't/#u');
+
+        } elsif ($path_info =~ m!$LISTNAME_RE/f/(\S+)\.html\z!o) {
+                r301($ctx, $1, $2, 'f/');
+
+        } elsif ($path_info =~ m!$LISTNAME_RE/(?:m|f)/(\S+)\.txt\z!o) {
+                r301($ctx, $1, $2, 'raw');
+
+        } elsif ($path_info =~ m!$LISTNAME_RE/t/(\S+)(\.mbox(?:\.gz)?)\z!o) {
+                r301($ctx, $1, $2, "t$3");
+
+        # legacy convenience redirects, order still matters
+        } elsif ($path_info =~ m!$LISTNAME_RE/m/(\S+)\z!o) {
+                r301($ctx, $1, $2);
+        } elsif ($path_info =~ m!$LISTNAME_RE/t/(\S+)\z!o) {
+                r301($ctx, $1, $2, 't/#u');
+        } elsif ($path_info =~ m!$LISTNAME_RE/f/(\S+)\z!o) {
+                r301($ctx, $1, $2, 'f/');
+
+        } else {
+                r404();
+        }
+}
+
+sub r301 {
+        my ($ctx, $listname, $mid, $suffix) = @_;
+        my $cgi = $ctx->{cgi};
+        my $url;
+        if (ref($cgi) eq 'CGI') {
+                $url = $cgi->url(-base) . '/';
+        } else {
+                $url = $cgi->base->as_string;
+        }
+
+        $url .= $listname . '/';
+        $url .= (uri_escape_utf8($mid) . '/') if (defined $mid);
+        $url .= $suffix if (defined $suffix);
+
+        [ 301,
+          [ Location => $url, 'Content-Type' => 'text/plain' ],
+          [ "Redirecting to $url\n" ] ]
+}
+
 1;
diff --git a/t/cgi.t b/t/cgi.t
index d84e6348..a6600c20 100644
--- a/t/cgi.t
+++ b/t/cgi.t
@@ -109,7 +109,7 @@ EOF
         like($res->{body}, qr/<title>test for public-inbox/,
                 "set title in XML feed");
         like($res->{body},
-                qr!http://test\.example\.com/test/m/blah%40example\.com!,
+                qr!http://test\.example\.com/test/blah%40example\.com/!,
                 "link id set");
         like($res->{body}, qr/what\?/, "reply included");
 }
@@ -152,26 +152,26 @@ EOF
         }
         local $ENV{GIT_DIR} = $maindir;
 
-        my $res = cgi_run("/test/m/slashy%2fasdf%40example.com/raw");
+        my $res = cgi_run("/test/slashy%2fasdf%40example.com/raw");
         like($res->{body}, qr/Message-Id: <\Q$slashy_mid\E>/,
                 "slashy mid raw hit");
 
-        $res = cgi_run("/test/m/blahblah\@example.com/raw");
+        $res = cgi_run("/test/blahblah\@example.com/raw");
         like($res->{body}, qr/Message-Id: <blahblah\@example\.com>/,
                 "mid raw hit");
-        $res = cgi_run("/test/m/blahblah\@example.con/raw");
+        $res = cgi_run("/test/blahblah\@example.con/raw");
         like($res->{head}, qr/Status: 404 Not Found/, "mid raw miss");
 
-        $res = cgi_run("/test/m/blahblah\@example.com/");
+        $res = cgi_run("/test/blahblah\@example.com/");
         like($res->{body}, qr/\A<html>/, "mid html hit");
         like($res->{head}, qr/Status: 200 OK/, "200 response");
-        $res = cgi_run("/test/m/blahblah\@example.con/");
+        $res = cgi_run("/test/blahblah\@example.con/");
         like($res->{head}, qr/Status: 404 Not Found/, "mid html miss");
 
-        $res = cgi_run("/test/f/blahblah\@example.com/");
+        $res = cgi_run("/test/blahblah\@example.com/f/");
         like($res->{body}, qr/\A<html>/, "mid html");
         like($res->{head}, qr/Status: 200 OK/, "200 response");
-        $res = cgi_run("/test/f/blahblah\@example.con/");
+        $res = cgi_run("/test/blahblah\@example.con/f/");
         like($res->{head}, qr/Status: 404 Not Found/, "mid html miss");
 
         $res = cgi_run("/test/");
@@ -183,7 +183,7 @@ EOF
 {
         local $ENV{HOME} = $home;
         local $ENV{PATH} = $main_path;
-        my $path = "/test/t/blahblah%40example.com/mbox.gz";
+        my $path = "/test/blahblah%40example.com/t.mbox.gz";
         my $res = cgi_run($path);
         like($res->{head}, qr/^Status: 501 /, "search not-yet-enabled");
         my $indexed = system($index, $maindir) == 0;
@@ -203,7 +203,7 @@ EOF
 
         my $have_xml_feed = eval { require XML::Feed; 1 } if $indexed;
         if ($have_xml_feed) {
-                $path = "/test/t/blahblah%40example.com/atom";
+                $path = "/test/blahblah%40example.com/t.atom";
                 $res = cgi_run($path);
                 like($res->{head}, qr/^Status: 200 /, "atom returned 200");
                 like($res->{head}, qr!^Content-Type: application/xml!m,
diff --git a/t/feed.t b/t/feed.t
index a9955f08..e4ec7522 100644
--- a/t/feed.t
+++ b/t/feed.t
@@ -77,7 +77,7 @@ EOF
                 }
 
                 unlike($feed, qr/drop me/, "long quoted text dropped");
-                like($feed, qr!/f/\d%40example\.com/#q!,
+                like($feed, qr!/\d%40example\.com/f/#q!,
                         "/f/ url generated for long quoted text");
                 like($feed, qr/inline me here/, "short quoted text kept");
                 like($feed, qr/keep me/, "unquoted text saved");
diff --git a/t/plack.t b/t/plack.t
index 50c9e605..067a5933 100644
--- a/t/plack.t
+++ b/t/plack.t
@@ -88,7 +88,7 @@ EOF
                 is(200, $res->code, 'success response received');
                 like($res->content, qr!href="\Q$atomurl\E"!,
                         'atom URL generated');
-                like($res->content, qr!href="m/blah%40example\.com/"!,
+                like($res->content, qr!href="blah%40example\.com/"!,
                         'index generated');
         });
 
@@ -98,14 +98,14 @@ EOF
                 my $res = $cb->(GET($pfx . '/atom.xml'));
                 is(200, $res->code, 'success response received for atom');
                 like($res->content,
-                        qr!link\s+href="\Q$pfx\E/m/blah%40example\.com/"!s,
+                        qr!link\s+href="\Q$pfx\E/blah%40example\.com/"!s,
                         'atom feed generated correct URL');
         });
 
-        foreach my $t (qw(f m)) {
+        foreach my $t (('', 'f/')) {
                 test_psgi($app, sub {
                         my ($cb) = @_;
-                        my $path = "/$t/blah%40example.com/";
+                        my $path = "/blah%40example.com/$t";
                         my $res = $cb->(GET($pfx . $path));
                         is(200, $res->code, "success for $path");
                         like($res->content, qr!<title>hihi - Me</title>!,
@@ -114,8 +114,8 @@ EOF
         }
         test_psgi($app, sub {
                 my ($cb) = @_;
-                my $res = $cb->(GET($pfx . '/m/blah%40example.com/raw'));
-                is(200, $res->code, 'success response received for /m/*/raw');
+                my $res = $cb->(GET($pfx . '/blah%40example.com/raw'));
+                is(200, $res->code, 'success response received for /*/raw');
                 like($res->content, qr!\AFrom !, "mbox returned");
         });
 
@@ -126,18 +126,25 @@ EOF
                         my $res = $cb->(GET($pfx . "/$t/blah%40example.com.txt"));
                         is(301, $res->code, "redirect for old $t .txt link");
                         my $location = $res->header('Location');
-                        like($location, qr!/$t/blah%40example\.com/raw\z!,
+                        like($location, qr!/blah%40example\.com/raw\z!,
                                 ".txt redirected to /raw");
                 });
         }
-        foreach my $t (qw(m f t)) {
+
+        my %umap = (
+                'm' => '',
+                'f' => 'f/',
+                't' => 't/',
+        );
+        while (my ($t, $e) = each %umap) {
                 test_psgi($app, sub {
                         my ($cb) = @_;
                         my $res = $cb->(GET($pfx . "/$t/blah%40example.com.html"));
                         is(301, $res->code, "redirect for old $t .html link");
                         my $location = $res->header('Location');
-                        like($location, qr!/$t/blah%40example\.com/(?:#u)?\z!,
-                                ".html redirected to /raw");
+                        like($location,
+                                qr!/blah%40example\.com/$e(?:#u)?\z!,
+                                ".html redirected to new location");
                 });
         }
         foreach my $sfx (qw(mbox mbox.gz)) {
@@ -146,8 +153,9 @@ EOF
                         my $res = $cb->(GET($pfx . "/t/blah%40example.com.$sfx"));
                         is(301, $res->code, 'redirect for old thread link');
                         my $location = $res->header('Location');
-                        like($location, qr!/t/blah%40example\.com/mbox\.gz\z!,
-                                "$sfx redirected to /mbox.gz");
+                        like($location,
+                             qr!/blah%40example\.com/t\.mbox(?:\.gz)?\z!,
+                             "$sfx redirected to /mbox.gz");
                 });
         }
 }
diff --git a/t/view.t b/t/view.t
index 77cf3a32..83823d89 100644
--- a/t/view.t
+++ b/t/view.t
@@ -44,17 +44,17 @@ EOF
         my $html = PublicInbox::View::msg_html(undef, $mime);
 
         # ghetto tests
-        like($html, qr!<a\nhref="\.\./\.\./m/hello%40!s, "MID link present");
+        like($html, qr!<a\nhref="\.\./raw"!s, "raw link present");
         like($html, qr/hello world\b/, "body present");
         like($html, qr/&gt; keep this inline/, "short quoted text is inline");
         like($html, qr/<a\nid=[^>]+><\/a>&gt; Long and wordy/,
                 "long quoted text is anchored");
 
         # short page
-        my $pfx = "../../f/hello%40example.com/";
+        my $pfx = "../hello%40example.com/f/";
         $mime = Email::MIME->new($s);
         my $short = PublicInbox::View::msg_html(undef, $mime, $pfx);
-        like($short, qr!<a\nhref="\.\./\.\./f/hello%40example\.com/!s,
+        like($short, qr!<a\nhref="\.\./hello%40example\.com/f/!s,
                 "MID link present");
         like($short, qr/\n&gt; keep this inline/,
                 "short quoted text is inline");