Skip to content

Library: say what the place is now that anyone can open one, and put it where machines read - #238

Open
manager wants to merge 1 commit into
devfrom
feat/library-seo-geo
Open

manager wants to merge 1 commit into
devfrom
feat/library-seo-geo

Conversation

@manager

@manager manager commented Sep 12, 2026

Copy link
Copy Markdown
Contributor

The Library's public copy still described a shelf kept by the team and their friends. Creation has been open to every account since the release on 2026-09-12, so the page, the search result and the shared card all described something that is no longer true.

Copy

  • The hub's title, description and share card name what the place is and that a reader can open their own.
  • A library's own description says what stands on it instead of repeating "personal" twice.
  • The About dialog says anyone can open one, at an address of their own.

The surface for machines was dark. Both llms files and the per-page dump are written to public/keepsimple_/, a path nothing has served since public/ was flattened. The published /llms.txt had not moved since May, carried no Library at all, and the per-page dump named staging URLs.

  • The generators write where the site serves from, read the live CMS, and always name production addresses; LLMS_STRAPI_URL and LLMS_BASE_URL override both.
  • A run keeps whatever it cannot rebuild, so the cognitive-bias and UXCG lines an older generator wrote survive instead of being deleted by the next run.
  • llms.txt and llms-full.txt list the Library and every public library, expanded from the same anonymous read the sitemap makes. Objects stay out: they are a reader's own notes, they change daily, and this file is committed to a public repository.
  • The concierge widget read its page descriptions from the same missing path, so its map has been empty since the flatten. It reads the served file now, and knows a library, an object page and a share link when it stands on one.
  • library-seo-check could not load src/lib/library/seo.ts after the @utils import landed, so the gate has been failing to start rather than passing. It runs again, with its expectations matched to the server-rendered read.

Verified locally: yarn build, tsc --noEmit, yarn check:library, yarn check:library:seo.

Still outside this PR: production robots.txt is a host file that does not carry the library sitemap line, and /library-sitemap.xml has never been submitted to Google Search Console or Bing.

🤖 Generated with Claude Code

The Library's public copy still described a shelf kept by the team and
their friends. Creation has been open to every account since the
2026-09-12 release, so the page, the search result and the shared card
all described something that is no longer true.

- The hub's title, description and share card now name what the place
  is and that a reader can open their own. A library's own description
  says what stands on it rather than repeating "personal" twice.
- The About dialog says anyone can open one, at an address of their own.

The same surface for machines was dark. Both llms files and the
per-page dump are written to public/keepsimple_/, a path nothing has
served since public/ was flattened: the published /llms.txt had not
moved since May, carried no Library at all, and the per-page dump named
staging URLs. The generators now write where the site serves from, read
the live CMS, and always name production addresses; LLMS_STRAPI_URL and
LLMS_BASE_URL override both. A run keeps whatever it cannot rebuild,
so the cognitive-bias and UXCG lines an older generator wrote survive
instead of being deleted by the next run.

- llms.txt and llms-full.txt list the Library and every public library,
  expanded from the same anonymous read the sitemap makes. Objects stay
  out: they are a reader's own notes, they change daily, and this file
  is committed to a public repository.
- The concierge widget read its page descriptions from the same missing
  path, so its map has been empty since the flatten and every page fell
  through to the hand-coded blurbs. It reads the served file now, and
  knows a library, an object page and a share link when it stands on one.
- library-seo-check could not load src/lib/library/seo.ts at all after
  the @utils import landed, so the gate has been failing to start rather
  than passing. It runs again, with its expectations matched to the
  server-rendered read.

Verified: yarn build, tsc --noEmit, yarn check:library, yarn check:library:seo.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Sep 12, 2026 •

Copy link
Copy Markdown

Claude finished @manager's task in 3m 2s —— View job


Review complete

  • Gather context (PR diff, changed files)
  • Review copy changes (AboutLibraryModal, seo.config)
  • Review scripts/generate-llms.ts and generate-llms-pages.ts
  • Review src/lib/widget/pageIdentity.ts
  • Review src/lib/library/seo.ts and library-seo-check.cjs
  • Post inline comments for issues found
  • Post final summary

Summary

Solid, well-scoped PR: copy updates land cleanly, and the public/keepsimple_/ → public/ path fix for the llms surfaces is correct and consistent across the generator, the workflow, AGENTS.md, LIBRARY.md, and the skill doc. pageIdentity.ts's fs.readFileSync of public/llms.txt is safe — confirmed it's only reachable from src/pages/api/concierge*.ts, never from client code, so no SSR/hydration risk. library-seo-check.cjs's @utils alias fix matches the stated bug (the gate was failing to even load).

Two inline findings on scripts/generate-llms.ts, both in the new carry-forward/library-expansion logic:

  1. Duplicate /library entry — it's emitted once by the main routes loop (real scanned route + seoDescriptions['/library']) and again by the new See All Libraries custom entry, so the generated public/llms.txt/llms-full.txt list it twice with identical text. (/articles has the same pre-existing duplication with See All Articles, not introduced here — but this PR adds a second instance of the same pattern instead of fixing it.)
  2. Malformed trailing colon on carried entries — the new readPublishedEntries/carried logic doesn't skip entries with an empty description, so /user/username (which never had a description) now renders as - [User Profile](...): with a dangling colon in the committed file.

Neither is severe (they're cosmetic/redundancy issues in a machine-readable index file, not functional breakage), but both are visible in the regenerated public/llms.txt in this diff and worth a quick fix before merge.

One thing worth a sanity check but not blocking: fetchLibraryEntries reads only the first page (pagination[pageSize]=100, no pagination loop), unlike library-sitemap.xml.ts which loops through all pages. Fine today with a handful of libraries, but will silently truncate llms-full.txt's "full" library list once there are >100 public libraries.

Everything else — SEO copy, the About dialog wording, LIBRARY.md/AGENTS.md doc updates, and the library-seo-check.cjs assertions — looks correct and matches the PR description.

Comment thread scripts/generate-llms.ts
Comment on lines 580 to 595
@@ -394,6 +587,11 @@ const run = async () => {
'Browse all KeepSimple articles and categories.',
),
},
{
name: 'See All Libraries',
route: '/library',
description: stripHtml(DEFAULT_SEO.description),
},
],

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

/library ends up listed twice in the generated output. It's already emitted by the main routes.forEach loop (it's a real scanned route with a seoDescriptions['/library'] entry, see line 25 of public/llms.txt), and then emitted again here as the "See All Libraries" custom entry with the same URL and description.

- [Library](https://keepsimple.io/library): Libraries built by readers…
...
- [See All Libraries](https://keepsimple.io/library): Libraries built by readers…

(/articles has the same pre-existing duplication with "See All Articles" — not introduced here, but this PR adds a second instance of the same pattern rather than fixing it.) Since customEntries in buildContent doesn't dedupe against routes, either drop these two static custom entries (the routes are already covered) or filter routes to exclude /articles and /library before the main loop if you want the "See All …" wording to win.

Fix this →

Comment thread scripts/generate-llms.ts
Comment on lines +558 to +564
const carried = Object.entries(published)
.filter(([route]) => !withoutLibraryPlaceholder.includes(route))
.filter(
([route]) =>
!route.startsWith('/library/') && !route.startsWith('/articles/'),
)
.map(([route, entry]) => ({ route, ...entry }));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Carried entries with an empty description produce a malformed line: entry.description can be '' (e.g. the pre-existing /user/username line, which never had a colon-trailed description), and buildContent's customEntries loop unconditionally writes - [${entry.name}](${url}): ${entry.description} — giving - [User Profile](https://keepsimple.io/user/username): with a trailing colon and nothing after it (see line 56 of the regenerated public/llms.txt). Worth filtering carried to drop entries whose description is empty, same as routeToDescription already does for the non-carried path (if (published[route]?.description) ...).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant