From 64c3164b7d81d09eca4bf70eea82eb4343e6831d Mon Sep 17 00:00:00 2001 From: Jason McIntosh Date: Sun, 21 Jun 2026 20:07:06 -0400 Subject: [PATCH] Give emoji-only post titles distinct filenames (#52) A post whose title is made entirely of characters that can't appear in a filename (e.g. emoji) slugged down to an empty string, so every such post on a given day produced the same "YYYY-MM-DD-.html" and silently overwrote its predecessor. When the title slug comes out empty, fall back to a short hash of the title. Distinct titles now get distinct, deterministic, stable filenames. md5_hex/encode_utf8 come from core Digest::MD5 (already a Plerd dependency) and Encode. Adds t/emoji_title.t covering two emoji-only titles on the same day. Co-Authored-By: Claude Opus 4.8 --- lib/Plerd/Post.pm | 25 +++++++++++------ t/emoji_title.t | 71 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 88 insertions(+), 8 deletions(-) create mode 100644 t/emoji_title.t diff --git a/lib/Plerd/Post.pm b/lib/Plerd/Post.pm index 00657a4..880670e 100644 --- a/lib/Plerd/Post.pm +++ b/lib/Plerd/Post.pm @@ -11,6 +11,8 @@ use HTML::SocialMeta; use Try::Tiny; use JSON; use Path::Class::File; +use Digest::MD5 qw( md5_hex ); +use Encode qw( encode_utf8 ); use Plerd::SmartyPants; use Web::Mention; @@ -194,15 +196,22 @@ sub _build_published_filename { $filename =~ s/\..*$/.html/; } else { - $filename = $self->title; my $stripper = HTML::Strip->new( emit_spaces => 0 ); - $filename = $stripper->parse( $filename ); - $filename =~ s/\s+/-/g; - $filename =~ s/--+/-/g; - $filename =~ s/[^\w\-]+//g; - $filename = lc $filename; - $filename = $self->date->ymd( q{-} ) . q{-} . $filename; - $filename .= '.html'; + my $slug = $stripper->parse( $self->title ); + $slug =~ s/\s+/-/g; + $slug =~ s/--+/-/g; + $slug =~ s/[^\w\-]+//g; + $slug = lc $slug; + + # A title made entirely of characters that can't appear in a filename + # (e.g. emoji) slugs down to nothing, which would make every such post + # on a given day collide on "YYYY-MM-DD-.html". Fall back to a short + # hash of the title, so distinct titles get distinct, stable filenames. + unless ( length $slug ) { + $slug = substr( md5_hex( encode_utf8( $self->title ) ), 0, 10 ); + } + + $filename = $self->date->ymd( q{-} ) . q{-} . $slug . '.html'; } return $filename; diff --git a/t/emoji_title.t b/t/emoji_title.t new file mode 100644 index 0000000..8c69f68 --- /dev/null +++ b/t/emoji_title.t @@ -0,0 +1,71 @@ +use warnings; +use strict; +use utf8; +use Test::More; +use Path::Class::Dir; +use Path::Class::File; +use URI; + +use FindBin; +use lib "$FindBin::Bin/../lib"; + +use_ok( 'Plerd' ); +use Plerd::Init; + +# Regression test for GitHub issue #52: a post titled entirely with +# characters that can't appear in a filename (e.g. emoji) used to slug down +# to nothing, so every such post on a given day collided on +# "YYYY-MM-DD-.html" and overwrote its predecessor. Distinct titles must +# produce distinct, non-empty published filenames. + +my $blog_dir = Path::Class::Dir->new( "$FindBin::Bin/emoji_title_blog" ); +$blog_dir->rmtree; +Plerd::Init::initialize( $blog_dir->stringify, 0 ); + +my $source_dir = Path::Class::Dir->new( $blog_dir, 'source' ); + +# Two emoji-only titles, same publication day, source filenames carry no date +# prefix so the title-based filename logic runs. +Path::Class::File->new( $source_dir, 'smile.md' )->spew( + iomode => '>:encoding(utf8)', + "title: \x{1F600}\ntime: 2021-01-19T10:00:00\n\nBody one.\n", +); +Path::Class::File->new( $source_dir, 'hats.md' )->spew( + iomode => '>:encoding(utf8)', + "title: \x{1F3A9}\x{1F3A9}\ntime: 2021-01-19T11:00:00\n\nBody two.\n", +); + +my $plerd = Plerd->new( + path => $blog_dir->stringify, + title => 'Test Blog', + author_name => 'Nobody', + author_email => 'nobody@example.com', + base_uri => URI->new( 'http://blog.example.com/' ), +); + +$plerd->publish_all; + +my %filename; +for my $post ( @{ $plerd->posts } ) { + $filename{ $post->title } = $post->published_filename; +} + +my @names = values %filename; +isnt( $names[0], $names[1], + 'Two emoji-only titles get distinct published filenames.' ); + +unlike( $_, qr/-\.html$/, 'Published filename is not empty-slugged.' ) + for @names; + +# Both posts' HTML files should actually exist on disk (neither overwrote the +# other). +for my $title ( keys %filename ) { + ok( + -e Path::Class::File->new( $blog_dir, 'docroot', $filename{ $title } ), + "HTML file exists for title '$title' ($filename{ $title })." + ); +} + +$blog_dir->rmtree; + +done_testing();