let update = PieceUpdateOp::Modify(());
 
     let logent = LogEntry {
-      html : format!("{} grasped {}",
+      html : Html(format!("{} grasped {}",
                      &htmlescape::encode_minimal(&pl.nick),
-                     pc.describe_html(&lens.log_pri(piece, pc))),
+                     pc.describe_html(&lens.log_pri(piece, pc)).0)),
     };
 
     (update, vec![logent])
     let update = PieceUpdateOp::Modify(());
 
     let logent = LogEntry {
-      html : format!("{} released {}",
+      html : Html(format!("{} released {}",
                      &htmlescape::encode_minimal(&pl.nick),
-                     pc.describe_html(&lens.log_pri(piece, pc))),
+                     pc.describe_html(&lens.log_pri(piece, pc)).0)),
     };
 
     (update, vec![logent])
 
       ig.gs.table_size = size;
       (U{ pcs: vec![],
           log: vec![ LogEntry {
-            html: format!("The table was resized to {}x{}", size[0], size[1]),
+            html: Html(format!("The table was resized to {}x{}",
+                               size[0], size[1])),
           }],
           raw: Some(vec![ PreparedUpdateEntry::SetTableSize(size) ]) },
        Fine)
         Err(ME::AlreadyExists)?;
       }
       let logentry = LogEntry {
-        html: format!("The facilitator added a player: {}",
-                      htmlescape::encode_minimal(&pl.nick)),
+        html: Html(format!("The facilitator added a player: {}",
+                      htmlescape::encode_minimal(&pl.nick))),
       };
       let (player, logentry) = ig.player_new(pl, logentry)?;
       (U{ pcs: vec![],
       let old_state = ig.player_remove(player)?;
       (U{ pcs: vec![],
           log: old_state.iter().map(|pl| LogEntry {
-            html: format!("The facilitator removed a player: {}",
-                          htmlescape::encode_minimal(&pl.nick)),
+            html: Html(format!("The facilitator removed a player: {}",
+                          htmlescape::encode_minimal(&pl.nick))),
           }).collect(),
           raw: None},
        Fine)
       p.p.delete_hook(&p, gs);
       (U{ pcs: vec![(piece, PieceUpdateOp::Delete())],
           log: vec![ LogEntry {
-            html: format!("A piece {} was removed from the game",
-                          desc_html),
+            html: Html(format!("A piece {} was removed from the game",
+                          desc_html.0)),
           }],
           raw: None },
        Fine)
 
       (U{ pcs: updates,
           log: vec![ LogEntry {
-            html: format!("The facilitaror added {} pieces", count),
+            html: Html(format!("The facilitaror added {} pieces",
+                               count)),
           }],
           raw: None },
        Fine)
 
         if bulk.logs {
           buf.log_updates(vec![LogEntry {
-            html: "The facilitator (re)configured the game".to_owned(),
+            html: Html::lit("The facilitator (re)configured the game")
           }]);
         }
 
 
   pub piece: PieceId,
   pub pos: Pos,
   pub face: FaceId,
-  pub desc_html: String,
+  pub desc_html: Html,
 }
 
 #[derive(Debug,Copy,Clone,Serialize,Deserialize)]
 
 #[serde(try_from="f64")]
 pub struct ZCoord(pub f64);
 
+#[derive(Clone,Debug,Serialize,Deserialize)]
+#[serde(transparent)]
+pub struct Html (pub String);
+
 pub const DEFAULT_TABLE_SIZE : Pos = [ 400, 200 ];
 
 // ---------- general data types ----------
 
 #[derive(Debug,Serialize,Deserialize)]
 pub struct LogEntry {
-  pub html : String,
+  pub html : Html,
 }
 
 // ---------- piece trait, and rendering ----------
 #[typetag::serde]
 pub trait Piece : Send + Debug {
   // #[throws] doesn't work here for some reason
-  fn svg_piece(&self, f: &mut String, pri: &PieceRenderInstructions) -> IR;
+  fn svg_piece(&self, f: &mut Html, pri: &PieceRenderInstructions) -> IR;
 
   #[throws(IE)]
-  fn surround_path(&self, pri : &PieceRenderInstructions) -> String;
+  fn surround_path(&self, pri : &PieceRenderInstructions) -> Html;
 
-  fn svg_x_defs(&self, f: &mut String, pri : &PieceRenderInstructions) -> IR;
+  fn svg_x_defs(&self, f: &mut Html, pri : &PieceRenderInstructions) -> IR;
 
   #[throws(IE)]
   fn thresh_dragraise(&self, pri : &PieceRenderInstructions)
                       -> Option<Coord>;
 
-  fn describe_html(&self, face : Option<FaceId>) -> String;
+  fn describe_html(&self, face : Option<FaceId>) -> Html;
 
   fn delete_hook(&self, _p: &PieceState, _gs: &mut GameState)
                  -> ExecuteGameChangeUpdates { 
   }
 }
 
+impl Html {
+  pub fn lit(s: &str) -> Self { Html(s.to_owned()) }
+}
+
 // ---------- game state - rendering etc. ----------
 
 impl PieceState {
   #[throws(IE)]
-  pub fn make_defs(&self, pri : &PieceRenderInstructions) -> String {
+  pub fn make_defs(&self, pri : &PieceRenderInstructions) -> Html {
     let pr = self;
-    let mut defs = String::new();
+    let mut defs = Html(String::new());
     let dragraise = match pr.p.thresh_dragraise(pri)? {
       Some(n) if n < 0 => throw!(SE::NegativeDragraise),
       Some(n) => n,
       None => -1,
     };
-    write!(defs,
+    write!(&mut defs.0,
            r##"<g id="piece{}" data-dragraise="{}">"##,
            pri.id, dragraise)?;
     pr.p.svg_piece(&mut defs, &pri)?;
-    write!(defs, r##"</g>"##)?;
-    write!(defs,
+    write!(&mut defs.0, r##"</g>"##)?;
+    write!(&mut defs.0,
            r##"<path id="surround{}" d="{}"/>"##,
-           pri.id, pr.p.surround_path(&pri)?)?;
+           pri.id, pr.p.surround_path(&pri)?.0)?;
     pr.p.svg_x_defs(&mut defs, &pri)?;
     defs
   }
     }
   }
 
-  pub fn describe_html(&self, pri : &PieceRenderInstructions) -> String {
+  pub fn describe_html(&self, pri : &PieceRenderInstructions) -> Html {
     self.p.describe_html(Some(pri.face))
   }
 }
 
 pub type OE = OnlineError;
 
 pub type SvgData = Vec<u8>;
-pub type Colour = String;
+pub type Colour = Html;
 
 #[derive(Debug,Serialize,Deserialize)]
 // todo: this serialisation is rather large
 struct SimpleShape {
-  desc : String,
-  path : String,
-  scaled_path : String,
+  desc : Html,
+  path : Html,
+  scaled_path : Html,
   approx_dia : Coord,
   colours : ColourMap,
 }
 type SE = SVGProcessingError;
 
 #[throws(SE)]
-pub fn svg_rescale_path(input: &str, scale: f64) -> String {
+pub fn svg_rescale_path(input: &Html, scale: f64) -> Html {
   type BM = u64;
   type BI = u32;
   #[derive(Debug,Copy,Clone)]
   let mut map = ALWAYS_MAP;
   let mut first = iter::once(());
 
-  for w in input.split_ascii_whitespace() {
+  for w in input.0.split_ascii_whitespace() {
     if first.next().is_none() { write!(&mut out, " ")?; }
     match w {
       "L" | "l" | "M" | "m" |
     write!(&mut out, "{}", w)?;
   }
 
-  trace!("rescaled by {}: {} as {}",scale,&input,&out);
-  out
+  trace!("rescaled by {}: {:?} as {:?}",scale,input,&out);
+  Html(out)
 }
 
 #[typetag::serde]
 impl Piece for SimpleShape {
   #[throws(IE)]
-  fn svg_piece(&self, f: &mut String, pri: &PieceRenderInstructions) {
-    write!(f, r##"<path fill="{}" d="{}"/>"##,
-           self.colours[pri.face],
-           &self.path)?;
+  fn svg_piece(&self, f: &mut Html, pri: &PieceRenderInstructions) {
+    write!(&mut f.0, r##"<path fill="{}" d="{}"/>"##,
+           self.colours[pri.face].0,
+           &self.path.0)?;
   }
   #[throws(IE)]
-  fn surround_path(&self, _pri : &PieceRenderInstructions) -> String {
+  fn surround_path(&self, _pri : &PieceRenderInstructions) -> Html {
     self.scaled_path.clone()
   }
   #[throws(IE)]
     Some(self.approx_dia / 2)
   }
   #[throws(IE)]
-  fn svg_x_defs(&self, _f: &mut String, _pri : &PieceRenderInstructions) {
+  fn svg_x_defs(&self, _f: &mut Html, _pri : &PieceRenderInstructions) {
   }
-  fn describe_html(&self, face : Option<FaceId>) -> String {
-    if let Some(face) = face {
-      format!("a {} {}", self.colours[face], self.desc)
+  fn describe_html(&self, face : Option<FaceId>) -> Html {
+    Html(if let Some(face) = face {
+      format!("a {} {}", self.colours[face].0, self.desc.0)
     } else {
-      format!("a {}", self.desc)
-    }
+      format!("a {}", self.desc.0)
+    })
   }
 }
 
 impl SimpleShape {
-  fn new_from_path(desc: String, path: String, approx_dia: Coord,
+  fn new_from_path(desc: Html, path: Html, approx_dia: Coord,
                    faces: &IndexVec<FaceId,ColourSpec>)
                    -> Result<Box<dyn Piece>,SpecError> {
     let scaled_path = svg_rescale_path(&path, SELECT_SCALE)?;
 impl PieceSpec for piece_specs::Disc {
   #[throws(SpecError)]
   fn load(&self) -> Box<dyn Piece> {
-    let unit_path =
+    let unit_path = Html::lit(
       "M 0 1  a 1 1 0 1 0 0 -2 \
-              a 1 1 0 1 0 0  2  z";
+              a 1 1 0 1 0 0  2  z"
+    );
     let scale = (self.diam as f64) * 0.5;
     let path = svg_rescale_path(&unit_path, scale)?;
-    SimpleShape::new_from_path("circle".to_owned(), path, self.diam,
+    SimpleShape::new_from_path(Html::lit("circle"), path, self.diam,
                                &self.faces)?
   }
   #[throws(SpecError)]
       [x, y] => (x,y),
       _ => throw!(SpecError::ImproperSizeSpec),
     };
-    let path = format!("M {} {} h {} v {} h {} z",
-                       -(x as f64)*0.5, -(y as f64)*0.5, x, y, -x);
-    SimpleShape::new_from_path("square".to_owned(), path, (x+y+1)/2,
+    let path = Html(format!("M {} {} h {} v {} h {} z",
+                            -(x as f64)*0.5, -(y as f64)*0.5, x, y, -x));
+    SimpleShape::new_from_path(Html::lit("square"), path, (x+y+1)/2,
                                &self.faces)?
   }
   #[throws(SpecError)]
 
   gen : Generation,
   table_size : Pos,
   uses : Vec<SessionPieceContext>,
-  defs : Vec<(VisiblePieceId,String)>,
+  defs : Vec<(VisiblePieceId,Html)>,
   nick : String,
   load : String,
   log : Vec<(Generation,Arc<LogEntry>)>,
 
       if !RE.is_match(s) {
         throw!(SpecError::UnsupportedColourSpec);
       }
-      spec.0.clone()
+      Html(spec.0.clone())
     }
   }
 }
 
 #[derive(Debug,Clone,Serialize)]
 pub struct PreparedPieceState {
   pub pos : Pos,
-  pub svg : String,
+  pub svg : Html,
   pub held : Option<PlayerId>,
   pub z : ZCoord,
   pub zg : Generation,
     match self {
       Piece { ref op, .. } => {
         50 +
-        op.new_state().map(|x| x.svg.len()).unwrap_or(0)
+        op.new_state().map(|x| x.svg.0.len()).unwrap_or(0)
       },
       Log(logent) => {
-        logent.html.as_bytes().len() * 3
+        logent.html.0.as_bytes().len() * 3
       },
       SetTableSize(_) |
       Error(_,_) => {
 
       data-gen="{{gen}}"
       data-load="{{ load | escape }}"
       >
-<h1>Hi {{nick}}!</h1>
+<h1>Hi {{nick | escape}}!</h1>
 <pre id="error"></pre>
 <p>
 <div id="status">nothing</div>