A Poster's Title Shows Up as IMG_1234 on the Classroom TV
This article may contain affiliate links. Its content is not affected by advertising.
In short
The title built from a filename only fell back to a fixed placeholder when the string was empty, so a machine-generated name like IMG_1234 was displayed as-is on the classroom TV.
The short version
The title built from a filename only checked whether the string was empty. If it wasn’t empty, the filename was used as-is; if it was, it fell back to a fixed placeholder string. That meant a camera- or scanner-generated name like IMG_1234 was passed straight through and displayed as-is on the classroom TV.
The fix classifies filenames against a set of regex patterns for “machine-generated names” and returns an empty string on a match. An empty string is treated as “no name,” and nothing is shown on the board.
What it looks like
When a poster is uploaded, the title field is auto-filled with the filename, extension stripped. Photos taken with a camera and documents scanned in are frequently saved with names like IMG_1234, scan_20261005, or poster, and that title was displayed as-is on the classroom TV’s board.
If the name was left blank when saving, it was filled in with a fixed placeholder string, which was likewise displayed as-is on the board. Neither case produces a string a person can actually read as meaningful.
Why
titleFromFileName() was a function that only stripped the extension and trimmed the result.
// before
export function titleFromFileName(name: string): string {
const base = name.replace(/\.[^.]+$/, "").trim();
return base || "掲示物";
}
It never looked at the filename’s content — only whether base was truthy. A string like IMG_1234 is truthy as base, so it was returned as the title unchanged.
The storage side, parsePosterRecord(), worked the same way: it fell back to the placeholder string only when the saved title was an empty or whitespace-only string.
// before
const title =
typeof r.title === "string" && r.title.trim()
? r.title.trim().slice(0, MAX_POSTER_TITLE_LENGTH)
: "掲示物";
And the function that converts records into board data, recordsToSignagePosters(), passed title: r.title through unchanged. Whether r.title held a raw filename or the placeholder string, it went to the board with no distinction.
Fixing it
We added an array of regex patterns that classify a filename as machine-generated.
const MACHINE_NAME_PATTERNS: readonly RegExp[] = [
// digits, symbols, and date separators only (e.g. 20261005_123456 / 2026-10-05 / 001).
/^[\d\s_.\-()()::年月日時分秒]+$/,
// camera/phone/scanner/generic English names + a number (e.g. IMG_1234 / DSC01234 / PXL_20261005_… / image (1) / poster).
/^(img|image|images|dsc|dscn|dscf|dcim|pxl|mvimg|photo|pic|picture|scan|scanned|scanner|document|doc|file|download|downloads|untitled|new|poster|pdf|jpeg|jpg|png|heic|cimg|sam|wp|p)[\s_\-.]*[a-z]?[\d\s_\-.()]*(copy|コピー)?[\s_\-.]*[\d()]*$/i,
// generic Japanese names + a number (e.g. スキャン_20261005 / 画像 2 / 写真(3) / 名称未設定 / 無題のドキュメント).
/^(スキャン|画像|写真|書類|文書|ドキュメント|名称未設定|無題|新規|新しい|掲示物)[^\p{L}]*$/u,
/^(名称未設定|無題)/,
// screenshots (e.g. Screenshot 2026-10-05 at 9.12.33 / スクリーンショット 2026-10-05 9.12.33).
/^(screenshot|screen shot|スクリーンショット|スクショ)/i,
// alphanumeric-only long random strings / UUIDs (e.g. 3f9a…-…).
/^[0-9a-f]{8}-?[0-9a-f]{4}-?[0-9a-f]{4}-?[0-9a-f]{4}-?[0-9a-f]{12}$/i,
/^(?=.*\d)(?=.*[A-Za-z])[0-9A-Za-z]{20,}$/,
];
titleFromFileName() now checks the result against these patterns after normalizing (NFC) and trimming, returning an empty string on a match.
// after
export function titleFromFileName(name: string): string {
const base = name
.replace(/\.[^.]+$/, "")
.normalize("NFC")
.trim();
if (!base) return "";
return MACHINE_NAME_PATTERNS.some((re) => re.test(base)) ? "" : base;
}
The storage default changed from the placeholder string to an empty string too.
// after
const title = typeof r.title === "string" ? r.title.trim().slice(0, MAX_POSTER_TITLE_LENGTH) : "";
On the board-conversion side, an empty string is now converted to null before being passed on.
// after
title: r.title || null,
The point here is distinguishing an empty string from null. The stored record keeps the type guarantee “title is always a string,” using an empty string to represent “no name,” and converts to null only right before handing data to the board. The board’s rendering decides whether there’s a title to show by checking for null, so “whether to display” can change without the stored type changing.
Why it went unnoticed
Looking back at titleFromFileName() before the fix, the only axis it checked was whether base was an empty string. Anything non-empty was used as-is, whatever it was.
That design can’t distinguish “the name was left blank” from “a string with no human meaning was uploaded as the name.” The former was anticipated and had a fallback; the latter made base truthy and slipped through untouched. Because the filename existing at all is itself valid data, this wasn’t something a type check or a build would catch — it was the kind of inconsistency that only surfaced once it actually appeared on the classroom TV.
Another inconsistency in the poster editor is covered in Hand-Built Preview Payload Drifts From the Device.
Frequently asked questions
Q1What counts as a "machine-generated name"?
Sequential or date-only names cameras and scanners assign automatically (IMG_1234, scan_20261005), default screenshot names, OS placeholders like "Untitled", and UUIDs or long random strings. None of these mean anything on the classroom TV.
Q2What happens to the title once it's classified as machine-generated?
The title becomes an empty string. The editor treats this as "no name" and places the cursor in the name field to prompt input. On the signage side, the empty string is converted to null, so no title is shown on the TV screen at all.
Q3Are human-given names used as-is?
Yes. A human-given name doesn't match any of the machine-name patterns, so it passes through unchanged. The check only asks whether a name matches a machine-generated pattern — it doesn't constrain what a human-given name can look like.
Q4What was displayed before this fix?
An empty title was filled in with a fixed placeholder string and shown as-is on the board. There was no check at all for machine-generated filenames, so a string like IMG_1234 was also passed straight through as the title and displayed on the TV.
Environment verified
- Next.js 16.3 (apps/web/package.json, as of 2026-10-05)
- Merged to main 2026-10-05 (UX finding recorded 2026-10-04)
What this article is based on
- TypeScript file lines 113-145commit 879a898
- TypeScript file lines 41-54commit 879a898
- TypeScript file lines 133-150commit 879a898
- TypeScript file lines 165-186commit 879a898
Every claim in this article comes from the records above. The repositories we operate are private so we cannot link to them, but which file, which lines, and at which commit we read them is recorded for every article. Nothing here is written from guesswork.