diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7b4aa66..2d036c4 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,9 @@
 
 ## [Unreleased]
 
+- Remove blanket `Render` impl for `T: Display`
+  [#320](https://github.com/lambda-fairy/maud/pull/320)
+
 ## [0.23.0] - 2021-11-10
 
 - Update to support axum 0.2
diff --git a/docs/Cargo.lock b/docs/Cargo.lock
index ba73376..cb43050 100644
--- a/docs/Cargo.lock
+++ b/docs/Cargo.lock
@@ -326,6 +326,7 @@ checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d"
 name = "maud"
 version = "0.23.0"
 dependencies = [
+ "itoa",
  "maud_macros",
 ]
 
diff --git a/docs/content/render-trait.md b/docs/content/render-trait.md
index 0ee4ddf..e49b73a 100644
--- a/docs/content/render-trait.md
+++ b/docs/content/render-trait.md
@@ -1,15 +1,9 @@
 # The `Render` trait
 
-By default,
-a [`(splice)`](splices-toggles.md) is rendered using the [`std::fmt::Display`][Display] trait,
-with any HTML special characters escaped automatically.
+Maud uses the [`Render`][Render] trait to convert [`(spliced)`](splices-toggles.md) values to HTML.
+This is implemented for many Rust primitive types (`&str`, `i32`) by default, but you can implement it for your own types as well.
 
-To change this behavior,
-implement the [`Render`][Render] trait for your type.
-Then, when a value of this type is used in a template,
-Maud will call your custom code instead.
-
-Below are some examples of using `Render`.
+Below are some examples of implementing `Render`.
 Feel free to use these snippets in your own project!
 
 ## Example: a shorthand for including CSS stylesheets
diff --git a/docs/content/splices-toggles.md b/docs/content/splices-toggles.md
index 26b646d..a5fbabd 100644
--- a/docs/content/splices-toggles.md
+++ b/docs/content/splices-toggles.md
@@ -94,11 +94,11 @@ html! {
 
 ### What can be spliced?
 
-You can splice any value that implements [`std::fmt::Display`][Display].
+You can splice any value that implements [`Render`][Render].
 Most primitive types (such as `str` and `i32`) implement this trait,
 so they should work out of the box.
 
-To change this behavior for some type,
+To get this behavior for a custom type,
 you can implement the [`Render`][Render] trait by hand.
 The [`PreEscaped`][PreEscaped] wrapper type,
 which outputs its argument without escaping,
@@ -116,7 +116,6 @@ html! {
 # ;
 ```
 
-[Display]: http://doc.rust-lang.org/std/fmt/trait.Display.html
 [Render]: https://docs.rs/maud/*/maud/trait.Render.html
 [PreEscaped]: https://docs.rs/maud/*/maud/struct.PreEscaped.html
 
diff --git a/maud/Cargo.toml b/maud/Cargo.toml
index a6c1f02..33c54ba 100644
--- a/maud/Cargo.toml
+++ b/maud/Cargo.toml
@@ -19,6 +19,7 @@ actix-web = ["actix-web-dep", "futures-util"]
 
 [dependencies]
 maud_macros = { version = "0.23.0", path = "../maud_macros" }
+itoa = { version = "0.4.8", default-features = false, features = ["i128"] }
 rocket = { version = ">= 0.3, < 0.5", optional = true }
 futures-util = { version = "0.3.0", optional = true, default-features = false }
 actix-web-dep = { package = "actix-web", version = ">= 2, < 4", optional = true, default-features = false }
diff --git a/maud/src/lib.rs b/maud/src/lib.rs
index 5d4275a..364e21d 100644
--- a/maud/src/lib.rs
+++ b/maud/src/lib.rs
@@ -11,8 +11,8 @@
 
 extern crate alloc;
 
-use alloc::string::String;
-use core::fmt::{self, Write};
+use alloc::{borrow::Cow, boxed::Box, string::String};
+use core::fmt::{self, Arguments, Write};
 
 pub use maud_macros::{html, html_debug};
 
@@ -59,16 +59,9 @@ impl<'a> fmt::Write for Escaper<'a> {
 
 /// Represents a type that can be rendered as HTML.
 ///
-/// If your type implements [`Display`][1], then it will implement this
-/// trait automatically through a blanket impl.
-///
-/// [1]: https://doc.rust-lang.org/std/fmt/trait.Display.html
-///
-/// On the other hand, if your type has a custom HTML representation,
-/// then you can implement `Render` by hand. To do this, override
-/// either the `.render()` or `.render_to()` methods; since each is
-/// defined in terms of the other, you only need to implement one of
-/// them. See the example below.
+/// To implement this for your own type, override either the `.render()`
+/// or `.render_to()` methods; since each is defined in terms of the
+/// other, you only need to implement one of them. See the example below.
 ///
 /// # Minimal implementation
 ///
@@ -115,48 +108,81 @@ pub trait Render {
     }
 }
 
-impl<T: fmt::Display + ?Sized> Render for T {
+impl Render for str {
     fn render_to(&self, w: &mut String) {
-        let _ = write!(Escaper::new(w), "{}", self);
+        escape::escape_to_string(self, w);
     }
 }
 
-/// Spicy hack to specialize `Render` for `T: AsRef<str>`.
-///
-/// The `std::fmt` machinery is rather heavyweight, both in code size and speed.
-/// It would be nice to skip this overhead for the common cases of `&str` and
-/// `String`. But the obvious solution uses *specialization*, which (as of this
-/// writing) requires Nightly. The [*inherent method specialization*][1] trick
-/// is less clear but works on Stable.
-///
-/// This module is an implementation detail and should not be used directly.
-///
-/// [1]: https://github.com/dtolnay/case-studies/issues/14
-#[doc(hidden)]
-pub mod render {
-    use crate::{Escaper, Render};
-    use alloc::string::String;
-    use core::fmt::Write;
-
-    pub trait RenderInternal {
-        fn __maud_render_to(&self, w: &mut String);
+impl Render for String {
+    fn render_to(&self, w: &mut String) {
+        str::render_to(self, w);
     }
+}
 
-    pub struct RenderWrapper<'a, T: ?Sized>(pub &'a T);
-
-    impl<'a, T: AsRef<str> + ?Sized> RenderWrapper<'a, T> {
-        pub fn __maud_render_to(&self, w: &mut String) {
-            let _ = Escaper::new(w).write_str(self.0.as_ref());
-        }
+impl<'a> Render for Cow<'a, str> {
+    fn render_to(&self, w: &mut String) {
+        str::render_to(self, w);
     }
+}
 
-    impl<'a, T: Render + ?Sized> RenderInternal for RenderWrapper<'a, T> {
-        fn __maud_render_to(&self, w: &mut String) {
-            self.0.render_to(w);
-        }
+impl<'a> Render for Arguments<'a> {
+    fn render_to(&self, w: &mut String) {
+        let _ = Escaper::new(w).write_fmt(*self);
     }
 }
 
+impl<'a, T: Render + ?Sized> Render for &'a T {
+    fn render_to(&self, w: &mut String) {
+        T::render_to(self, w);
+    }
+}
+
+impl<'a, T: Render + ?Sized> Render for &'a mut T {
+    fn render_to(&self, w: &mut String) {
+        T::render_to(self, w);
+    }
+}
+
+impl<T: Render + ?Sized> Render for Box<T> {
+    fn render_to(&self, w: &mut String) {
+        T::render_to(self, w);
+    }
+}
+
+macro_rules! impl_render_with_display {
+    ($($ty:ty)*) => {
+        $(
+            impl Render for $ty {
+                fn render_to(&self, w: &mut String) {
+                    format_args!("{self}").render_to(w);
+                }
+            }
+        )*
+    };
+}
+
+impl_render_with_display! {
+    char f32 f64
+}
+
+macro_rules! impl_render_with_itoa {
+    ($($ty:ty)*) => {
+        $(
+            impl Render for $ty {
+                fn render_to(&self, w: &mut String) {
+                    let _ = itoa::fmt(w, *self);
+                }
+            }
+        )*
+    };
+}
+
+impl_render_with_itoa! {
+    i8 i16 i32 i64 i128 isize
+    u8 u16 u32 u64 u128 usize
+}
+
 /// A wrapper that renders the inner value without escaping.
 #[derive(Debug, Clone, Copy)]
 pub struct PreEscaped<T: AsRef<str>>(pub T);
diff --git a/maud_macros/src/generate.rs b/maud_macros/src/generate.rs
index e6dc9ff..ad8a088 100644
--- a/maud_macros/src/generate.rs
+++ b/maud_macros/src/generate.rs
@@ -103,11 +103,7 @@ impl Generator {
 
     fn splice(&self, expr: TokenStream, build: &mut Builder) {
         let output_ident = self.output_ident.clone();
-        let tokens = quote!({
-            use maud::render::{RenderInternal, RenderWrapper};
-            RenderWrapper(&#expr).__maud_render_to(&mut #output_ident);
-        });
-        build.push_tokens(tokens);
+        build.push_tokens(quote!(maud::Render::render_to(&#expr, &mut #output_ident);));
     }
 
     fn element(&self, name: TokenStream, attrs: Vec<Attr>, body: ElementBody, build: &mut Builder) {