mirror of
https://github.com/Patiun/com.gregkilmer.www.git
synced 2026-11-08 04:08:24 +00:00
Add a Before launch placeholder check to admin
Lists pages still showing built-in wording, notes holding placeholder text, unpublished posts and live notes missing the field their page is built around, each linked to its editor. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012aSbo2D1qP8cMFQCykf7kx
This commit is contained in:
@@ -155,6 +155,15 @@ export class ContentApi {
|
||||
notes = (p: { type?: string; q?: string; limit?: number; full?: boolean } = {}) =>
|
||||
this.get<Summary[]>("/api/notes", { type: p.type, q: p.q, limit: p.limit, full: p.full ? "true" : undefined });
|
||||
fullNotes = (type: string) => this.get<FullNote[]>("/api/notes", { type, full: "true", limit: 1000 });
|
||||
/** Every note, whole, a thousand at a time. */
|
||||
async allNotes(): Promise<FullNote[]> {
|
||||
const all: FullNote[] = [];
|
||||
for (let offset = 0; ; offset += 1000) {
|
||||
const page = await this.get<FullNote[]>("/api/notes", { full: "true", limit: 1000, offset });
|
||||
all.push(...page);
|
||||
if (page.length < 1000) return all;
|
||||
}
|
||||
}
|
||||
note = (path: string) => this.get<Note>("/api/note", { path });
|
||||
noteByName = (name: string) => this.get<Note>("/api/note", { name });
|
||||
history = (path: string) => this.get<Revision[]>("/api/note/history", { path });
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
/**
|
||||
* Before launch: what on the live site is still a placeholder, worked out
|
||||
* from the notes themselves so it stays true as they change. Pages using
|
||||
* their built-in wording, notes that still hold placeholder text, posts not
|
||||
* published yet, and the thin spots the page designs expect to be filled.
|
||||
*/
|
||||
import type { FullNote } from "./client.ts";
|
||||
import { editHref, newHref } from "./editor.ts";
|
||||
import { html, type Html } from "./html.ts";
|
||||
import { sitePages } from "./sitepages.ts";
|
||||
|
||||
export interface Finding {
|
||||
what: string;
|
||||
/** The note to open, or a page to add. */
|
||||
href: string;
|
||||
action: string;
|
||||
}
|
||||
|
||||
export interface Section {
|
||||
title: string;
|
||||
hint: string;
|
||||
findings: Finding[];
|
||||
}
|
||||
|
||||
/** Text that marks something as not written yet. */
|
||||
const PLACEHOLDER = /<!--|\bcoming soon\b|\bplaceholder\b|\blorem\b|\bTODO\b|\bTBD\b/i;
|
||||
/** Pages whose lede or portrait the site fills in when the note has none. */
|
||||
const BUILT_IN = new Set(["/", "/about", "/now", "/blog", "/library", "/library/shelf", "/inquiries", "/tabletop", "/workshop", "/work", "/store", "/404"]);
|
||||
|
||||
const fieldsText = (n: FullNote) => JSON.stringify(n.data);
|
||||
const edit = (n: FullNote) => (n.type === "post" ? `/posts/edit?path=${encodeURIComponent(n.path)}` : editHref(n.path));
|
||||
|
||||
export function launchCheck(notes: FullNote[]): Section[] {
|
||||
const live = notes.filter((n) => n.publish);
|
||||
const of = (type: string, list = live) => list.filter((n) => n.type === type);
|
||||
const pageNotes = of("page", notes);
|
||||
|
||||
const pages: Finding[] = [];
|
||||
for (const p of sitePages) {
|
||||
if (!BUILT_IN.has(p.page)) continue;
|
||||
const n = pageNotes.find((x) => x.data.page === p.page);
|
||||
if (!n) pages.push({ what: `${p.name} (${p.page}) has no Page text note, so it shows its built-in wording.`, href: newHref("page", { page: p.page, title: p.name }), action: "Add page text" });
|
||||
else if (!n.publish) pages.push({ what: `${p.name} (${p.page}): its Page text note is a draft, so the page shows its built-in wording.`, href: edit(n), action: "Edit" });
|
||||
else if (!n.data.lede && p.page !== "/") pages.push({ what: `${p.name} (${p.page}) has no lede, so it shows the built-in one.`, href: edit(n), action: "Edit" });
|
||||
}
|
||||
const about = pageNotes.find((x) => x.data.page === "/about");
|
||||
if (!about?.data.portrait) pages.push({ what: "About has no portrait, so star-chart art stands in.", href: about ? edit(about) : newHref("page", { page: "/about", title: "About" }), action: about ? "Edit" : "Add page text" });
|
||||
|
||||
const text = notes
|
||||
.filter((n) => !n.path.startsWith("_templates/") && (PLACEHOLDER.test(n.body) || PLACEHOLDER.test(fieldsText(n))))
|
||||
.map((n) => ({ what: `${n.title}${n.publish ? "" : " (draft)"}: ${PLACEHOLDER.exec(n.body)?.[0] ?? PLACEHOLDER.exec(fieldsText(n))?.[0]}`, href: edit(n), action: "Edit" }));
|
||||
|
||||
const drafts = of("post", notes)
|
||||
.filter((n) => !n.publish)
|
||||
.map((n) => ({ what: `${n.title}${n.data.postType ? ` (${String(n.data.postType)})` : ""}`, href: edit(n), action: "Finish" }));
|
||||
|
||||
const thin: Finding[] = [
|
||||
...of("craft").filter((n) => !n.data.blurb).map((n) => ({ what: `Craft ${n.title} has no blurb.`, href: edit(n), action: "Edit" })),
|
||||
...of("make").filter((n) => !n.data.date).map((n) => ({ what: `Make ${n.title} has no finish date, so Recently finished can't order it.`, href: edit(n), action: "Edit" })),
|
||||
...of("campaign").filter((n) => !n.data.blurb).map((n) => ({ what: `Campaign ${n.title} has no blurb.`, href: edit(n), action: "Edit" })),
|
||||
...of("deck").filter((n) => !n.data.take).map((n) => ({ what: `Deck ${n.title} has no take.`, href: edit(n), action: "Edit" })),
|
||||
...of("card-game").filter((n) => !n.data.blurb).map((n) => ({ what: `Card game ${n.title} has no blurb.`, href: edit(n), action: "Edit" })),
|
||||
...of("experience").filter((n) => !n.data.focus && !n.data.earlier).map((n) => ({ what: `${n.title} has no focus line.`, href: edit(n), action: "Edit" })),
|
||||
];
|
||||
|
||||
return [
|
||||
{ title: "Pages using built-in wording", hint: "Write the page's text so the site says it in your words.", findings: pages },
|
||||
{ title: "Placeholder text in notes", hint: "Notes holding a comment, Coming soon, TODO or similar. A draft note isn't on the site, but it still needs finishing before it goes live.", findings: text },
|
||||
{ title: "Articles not published", hint: "Drafts on the Blog and Work pages.", findings: drafts },
|
||||
{ title: "Thin spots", hint: "Live notes missing a field their page is designed around.", findings: thin },
|
||||
];
|
||||
}
|
||||
|
||||
export function launchPage(sections: Section[]): Html {
|
||||
const total = sections.reduce((n, s) => n + s.findings.length, 0);
|
||||
return html`<h1>Before launch</h1>
|
||||
<p class="lede">${total ? html`${total} things are still placeholders or unfinished. The list comes from the notes, so it shrinks as you fix them.` : "Nothing left: every page uses your words and nothing holds placeholder text."}</p>
|
||||
${sections.map(
|
||||
(s) => html`<section class="panel launch">
|
||||
<h2>${s.title} <span class="count-pill">${s.findings.length}</span></h2>
|
||||
<p class="hint">${s.hint}</p>
|
||||
${s.findings.length
|
||||
? html`<ul>${s.findings.map((f) => html`<li><span>${f.what}</span> <a href="${f.href}">${f.action}</a></li>`)}</ul>`
|
||||
: html`<p class="muted">All done.</p>`}
|
||||
</section>`,
|
||||
)}`;
|
||||
}
|
||||
@@ -9,6 +9,7 @@ export interface Flash {
|
||||
const nav: [string, string][] = [
|
||||
["/", "Overview"],
|
||||
["/site", "Site pages"],
|
||||
["/launch", "Before launch"],
|
||||
["/review", "Review"],
|
||||
["/inbox", "Inbox"],
|
||||
["/notes", "Notes"],
|
||||
|
||||
@@ -19,6 +19,7 @@ import * as inbox from "./inbox.ts";
|
||||
import * as posts from "./posts.ts";
|
||||
import * as editor from "./editor.ts";
|
||||
import { sitePagesView } from "./sitepages.ts";
|
||||
import { launchCheck, launchPage } from "./launch.ts";
|
||||
import { suggestionChange, suggestionsPanel } from "./suggestions.ts";
|
||||
|
||||
const css = readFileSync(new URL("../public/admin.css", import.meta.url), "utf8");
|
||||
@@ -134,6 +135,8 @@ export function createAdminServer(api: ContentApi, opts: { now?: () => Date } =
|
||||
const singles = new Map(all.slice(1).flatMap((l) => (l[0] ? [[l[0].type, l[0]] as const] : [])));
|
||||
return { title: "Site pages", body: sitePagesView(all[0]!, singles) };
|
||||
}
|
||||
case "/launch":
|
||||
return { title: "Before launch", body: launchPage(launchCheck(await api.allNotes())) };
|
||||
case "/edit": {
|
||||
const note = await api.note(required(q, "path"));
|
||||
if (note.type === "post") return new Redirect(posts.editHref(note.path));
|
||||
|
||||
Reference in New Issue
Block a user