Builder Receipt: Reworking Bonumark Stream

Bonumark Stream gained a lot of capability this week, but the real work was deciding where each capability belonged. I tightened the boundaries between publishing, management, themes, and identity so the system could keep growing without becoming a collection of disconnected features. 

The Product Was Outgrowing Some of Its Earlier Decisions

Bonumark Stream has been gaining capability quickly, and that creates a different kind of development problem.

A feature can make sense when the system is smaller and become unnecessary later. A screen can remain functional while the workflow around it changes. CSS that once solved a real problem can become interference after the interface evolves. A feature can technically work in the bundled theme while exposing assumptions that break the idea of theme portability.

That was the pattern behind most of my work this week.

I added structured multi-photo galleries, consolidated the publishing workflow around the Stream itself, rebuilt large parts of the administration experience, removed proven CSS duplication, and restarted the Profile system after rejecting an earlier direction that did not fit the product.

None of those jobs were really about adding more stuff.

They were about deciding what belongs where.

Core needed to own behavior without dictating presentation. Themes needed to remain free to control appearance without breaking required functionality. The Stream needed to remain the publishing surface. Admin needed to become the place for deeper management instead of duplicating everyday actions. Profiles needed to represent identity without turning into another Stream.

Those boundaries guided the work.

Building Four-Photo Galleries Without Breaking Theme Independence

The first project expanded Bonumark Stream from one structured post image to galleries containing up to four photos.

The obvious implementation sounded simple: allow multiple uploads, store the images, and display them in a grid.

The real requirement was larger.

Existing posts with one photo still had to work. The first photo needed to remain the featured image so existing feeds, Open Graph behavior, imports, exports, and other integrations would continue behaving normally. Galleries needed to survive drafts, scheduling, revisions, trash restoration, Markdown storage, database storage, API publishing, and the rest of the existing publishing workflow.

Performance mattered too. I did not want a four-photo post forcing every visitor to download four original phone images just to scroll the Stream.

Bonumark already had responsive image variants, so I kept the gallery inside the existing media system rather than creating another pipeline. Core handled the gallery data, image order, responsive output, accessibility, loading behavior, viewer behavior, and a safe fallback layout. Themes remained responsible for visual treatment such as spacing, cropping, borders, colors, and corner radius.

That distinction became one of the most important decisions in the work.

Bonumark Stream supports independent themes. A gallery is not really a platform feature if it works only because Midnight Ledger knows how to display it.

The first build supported one to four photos in both the front composer and full editor. Users could preview selections, remove individual images, reorder them, and publish them together. One image displayed full width, two used a two-column layout, three used one larger image with two supporting images, and four used a two-by-two grid.

Testing exposed problems almost immediately.

The first problem was in the composer. Selected images appeared broken before publishing because their browser-generated blob: URLs were being blocked by Bonumark Stream’s Content Security Policy.

The uploads themselves were fine. The security policy was doing what it had been configured to do. The new feature simply introduced a legitimate image source that the policy had never needed before.

I adjusted the policy so local composer previews could work without broadly weakening the rest of the image restrictions.

The next problem appeared after publishing. Clicking a gallery image opened the original file directly in the browser. There was no overlay, no close control, and no useful way back to the post except the browser Back button.

Technically, the image opened.

That was not enough.

I replaced that behavior with a full-screen viewer inside Bonumark Stream. It included a visible close control, outside-click closing, Escape-key support, keyboard navigation, and previous and next controls when the post contained multiple photos.

The gallery then worked correctly on the development installation running Midnight Ledger.

That still was not enough testing.

I installed the same work on jimlunsford.net, which uses my custom Microblog Stream theme. The published gallery worked, but composer previews stretched into an uneven row instead of using the new gallery grid.

That test exposed an architectural mistake that the default theme never could have shown me.

The core behavior was correct. My custom theme still contained older single-image preview rules that loaded after core and overrode the gallery layout.

I could have responded by making the core stylesheet more aggressive until it overpowered the custom theme.

I decided against that.

Core and themes fighting through CSS specificity would make theme development worse over time. The custom theme was the layer carrying an outdated assumption, so that was the layer I corrected.

I removed the old single-image flex rules and fixed thumbnail sizing in the Microblog Stream theme while preserving the theme’s own visual language.

After that update, the same gallery behaved correctly in both environments.

That test reinforced something I keep accounting for as Bonumark grows. The default theme can prove that core works. It cannot prove that the platform contract works.

For that, I have to test another theme.

Making the Stream the Place Where Publishing Happens

The gallery work also made another older design decision harder to justify.

Bonumark Stream still had a separate backend screen for creating new Stream Posts.

That screen had made sense earlier in the project when the front composer was simple. It made less sense after the Stream composer gained media, scheduling, Local Places, link previews, mobile sharing, and the other controls needed for normal publishing.

At that point, Bonumark had two places for creating the same kind of content. Every new composer feature had to be maintained, styled, and tested twice.

I reduced the workflow to one rule:

Create from the Stream. Manage from the Admin.

The full editor stayed because it still serves a different job. Drafts, revisions, metadata, media-library insertion, longer content, and deeper editing belong there.

The redundant backend creation screen did not.

I moved the missing creation controls into the front composer. Posting immediately, scheduling, saving a draft, continuing in the full editor, and opening advanced metadata all became available from the Stream.

The old backend creation route was converted into a compatibility redirect instead of simply being removed. Existing links could still work while the product established one clear publishing path.

The first implementation solved the workflow duplication.

It also made the composer look terrible.

Too many buttons, fields, descriptions, and controls were visible at once. The Stream composer began looking like an admin form dropped into the public interface.

That failed the real requirement.

The composer needs to feel fast.

A user should be able to write something, optionally attach media or a place, schedule it if necessary, and publish without confronting every advanced publishing decision each time.

I moved the secondary actions behind one More Options control. The default composer returned to the essentials: Media, Schedule, Location, More Options, and Post. Drafts, full-editor continuation, and advanced metadata remained available without competing with the main action.

Once the Stream became the primary creation surface, small edits exposed another unnecessary trip into Admin.

Correcting one typo still required opening the full editor.

I added Quick Edit to the existing post menu and kept its scope deliberately narrow. It edits the post body only. The URL, title, date, media, gallery order, location, SEO fields, link preview, comments, likes, author, status, and pin state remain untouched.

The save process creates a revision first, so even a quick correction preserves recoverability.

During testing, I also noticed that the front-end menu still lacked the removal option I had expected it to have.

I added Move to Trash rather than permanent deletion.

That distinction matters. Normal front-end management should be fast, but it should not casually bypass recovery. Posts can now be moved out of the Stream from the same interface where they are managed, while restoration and permanent deletion stay inside Admin.

By the end of that work, the division between the two surfaces was much clearer.

The Stream handles normal publishing and lightweight management. Admin handles full editing, metadata, revisions, media management, scheduled content, recovery, and permanent deletion.

The result was not fewer capabilities.

It was less confusion about where those capabilities belong.

Rebuilding the Admin Around Real Workflows

Once publishing had a cleaner front-end path, the backend itself became the next obvious problem.

Bonumark Stream’s administration area worked, but it showed the history of the project. Features had accumulated over time. Individual pages had received responsive fixes when they needed them. Forms, tables, notices, controls, and page-specific CSS had grown around each new capability.

The software was more mature than the interface supporting it.

I did not want another broad cosmetic layer placed on top of the existing admin stylesheet.

The requirement was to rebuild the interaction system workflow by workflow without changing the underlying content, permissions, routes, themes, comments, users, settings, places, or publishing behavior.

The first pass created a shared admin shell.

Navigation was reorganized around the jobs someone is trying to do rather than presenting every tool as an equal top-level option. Desktop navigation became clearer, while mobile received a real off-canvas drawer instead of placing the entire sidebar above the page content.

The Dashboard became the reference screen for the rest of the administration system.

From there, I worked through the major workflows.

Stream Posts moved away from a wide desktop table that collapsed badly on phones. Desktop retained the information needed to scan content quickly, while mobile received actual content cards rather than a table dismantled into a vertical wall of labels.

The editor received a more substantial workflow change. JavaScript had been forcing the writing area to match the viewport and metadata rail, which meant even a very short post could produce a massive empty editing surface.

I removed that behavior.

The writing surface became content-driven, while Post URL, Stream Post metadata, Location, Media, and Revisions moved into collapsible sections. Publishing actions were reorganized around their real priority instead of appearing as equally important buttons.

Mobile received persistent Save Draft and Post Now controls.

Real testing then found the problems that source review could not.

The entire desktop metadata rail had its own scrollbar, creating a second scroll area inside the editor. Location used a disclosure pattern that did not match the other editor sections. On mobile, the fixed publishing controls covered formatting tools.

Each of those became a focused correction rather than another redesign.

Only the Publish card remained sticky. The rest returned to normal page flow. Location adopted the same disclosure system as the other sections. The mobile action bar became aware of the editor controls, viewport, safe-area spacing, and on-screen keyboard.

The Media Library required a different interaction model.

It already contained useful capabilities, but every image card carried too much information all the time. Filenames, file details, metadata state, Markdown, privacy explanation, edit links, view links, and copy actions were repeated on every item.

I changed the library to browsing first.

Cards focused on the thumbnail, filename, dimensions, size, privacy state, selection, and details access. The administrative controls moved into a shared details interface.

Desktop gained a denser grid. Mobile gained two thumbnails per row instead of turning each image into a full-width form.

Again, screenshots exposed what the first implementation missed. Details controls floated awkwardly. Long filenames created inconsistent cards. An IMAGE badge appeared where it did not belong. The mobile details sheet behaved too much like a desktop modal.

I rebuilt the details control, limited filename height, enlarged touch targets, separated the detail view into a fixed header, scrolling middle, and fixed action footer, and made the mobile version behave like a proper bottom sheet.

The same system-level work continued through Pages, comments, accounts, registration, Appearance, Settings, Local Places, and operational tools.

Pages adopted the responsive record system and editor hierarchy already proven on Stream Posts. Testing also exposed a real publishing defect: the Publish Page button submitted the current form, but the handler ignored the publishing instruction and saved the page as a draft. I corrected the behavior so publishing uses the current title, body, URL, and metadata.

Comments exposed another kind of failure. A package passed PHP syntax but displayed an undefined-variable warning because a bulk-selection check had been placed inside the wrong function. I stopped the acceptance pass immediately, fixed the workflow, and added a runtime regression with warnings promoted to exceptions.

Accounts moved away from crowded tables and permanently open forms. Registration was reorganized around actual access decisions instead of one long settings page. Testing caught misleading mail warnings and changed them so urgency reflects the real registration state.

Appearance became a connected design system rather than a collection of screens. Themes, Theme Settings, Site Identity, Navigation, installation, details, and deletion received clearer boundaries. Testing caught a metadata grid that wrapped values one character per line, which was corrected before the work was accepted.

Settings gained a shared component layer across Reading, Writing, Security, Mail, Remote Posting, Scheduled Tasks, and other system controls. Long technical URLs and API routes were given a shared wrapping treatment after desktop and mobile testing exposed overflow.

Local Places was brought into the same interface system, including its directory, editor, nearby search, privacy handling, and deletion workflow.

The operational side of Bonumark received its own design standard because upgrades, imports, exports, backups, system checks, and destructive actions should not feel like ordinary preference forms.

The important part of this admin work was not how many screens changed.

It was that I kept testing each workflow until the interface matched the job it was supposed to support.

Cleaning Up the CSS Only After the Interface Was Proven

The admin redesign created another question.

The new component styles were working, but the original admin.css file was still 7,601 lines long.

That file represented years of additions, responsive patches, page-specific rules, and previous cleanup work. During the redesign, leaving those rules in place had been the safer choice. New component files could take control of the rebuilt interfaces without risking screens that had not yet been reviewed.

Once the major workflows had been accepted, that same safety net became duplication.

I did not want to “clean up” the stylesheet by deleting whatever looked old.

The requirement was stricter.

A legacy declaration could only be removed when a newer component stylesheet already controlled the same selector, the same property, and the same responsive context.

Shared rules used by login, installation, public previews, or other secondary interfaces had to remain unless their replacements were also proven.

Most importantly, the final CSS cascade had to stay identical.

I worked through three cleanup passes.

The first removed 38 legacy rule blocks and 76 declarations that had been fully replaced by the new shell and editor components.

The second removed another 127 superseded declarations and 12 empty rule blocks.

The third was more selective. I deliberately excluded generic panels, shared controls, buttons, footer rules, field help, and public Local Places dialog styling because those could still support interfaces outside the main Admin shell. That pass removed another 104 declarations and 14 empty rule blocks.

The original stylesheet dropped from 7,601 lines to 7,117.

That number by itself does not mean much.

The important result was that 307 declarations and 484 lines were removed while the effective styling remained unchanged.

I compared final selector-property winners before and after each pass. The first comparison produced the same 6,507 effective results. The second produced the same 9,090. The third produced the same 8,719.

Every pass showed zero differences.

I still installed and reviewed the builds on desktop and phone. I also checked less frequently used states such as Quick Edit, revisions, media editing, autosave recovery, first-run screens, confirmations, errors, disclosures, empty states, login, and system results.

The cleanup changed the code beneath the interface without changing the interface itself.

That was exactly what I wanted.

The newer component files are no longer temporary overrides sitting on top of the old system. They have become the authoritative styling layer for the workflows they own.

Restarting Profiles After Rejecting the Wrong Product Direction

The Profile work was probably the clearest example this week of why functioning code is not enough.

I had already made an earlier attempt at expanding Profiles.

That implementation had routes, migrations, tests, and working behavior.

It was also moving Bonumark Stream in the wrong direction.

Profiles had started becoming separate publishing destinations. Ideas such as profile-specific Streams were creeping into the architecture. Core behavior was beginning to assume visual controls that only made sense in the bundled theme.

I stopped the work, returned to the last clean baseline, and started again.

The new requirement began with product invariants rather than features.

Bonumark Stream has one Stream.

The Stream represents what someone publishes.

The Profile represents who that person is.

There would be no /profile/username/stream route, no automatic recent-post feed inside the Profile, and no second publishing surface hidden inside identity.

I also reinforced the boundary between core and themes.

Core owns Profile data, behavior, privacy, portability, and stable semantic information. Themes decide how that information looks.

That meant no universal Profile layout selector in core. No assumption that every theme supports an accent color. No theme-specific visual feature presented as a platform guarantee.

With those boundaries in place, I rebuilt the identity foundation.

Profiles gained a cover image, profile picture, display name, headline, location, short bio, Markdown About section, Now section, interests, flexible links, Profile visibility, and optional activity details.

Post count, comment count, and membership date remained optional. Those numbers can provide context, but they should not define the person.

The editor also needed refinement. The first version displayed too many empty link fields and became unnecessarily long on mobile. I changed Links to an Add Link workflow so the page only displays links that actually exist.

Profile visibility and optional statistics moved into their own Profile Settings section so the Identity area could remain about the person rather than account controls.

Once the identity foundation worked, I added Featured Work.

The same boundary applied there.

The Profile does not automatically pull the newest Stream content into itself. Nothing appears unless the owner deliberately chooses it.

A Profile can feature up to four Stream Posts, Pages, or external links. Internal items remain references rather than copied content. If one stops being public, it stops appearing as Featured Work instead of leaving a broken public item behind.

The Stream remains publishing.

The Profile remains curation and identity.

Testing also exposed an unrelated theme bug. An empty Stream showed a huge vertical gap between the header and its empty state. I traced that to the default theme’s flex and grid behavior rather than hiding the symptom with an empty-state margin.

Fixing the shell behavior corrected the empty Stream without changing normal pages.

That was another case where browser testing uncovered a real issue outside the exact feature I was working on.

Privacy and Portability Had to Be Part of the Profile Design

Once the visible Profile was stable, I worked on what other systems could understand about it.

Public Profiles now generate identity-specific titles, descriptions, canonical information, Open Graph data, social sharing images, and structured ProfilePage and Person metadata.

I did not stop at checking whether the tags existed.

I tested the privacy boundary.

When a Profile is Private, Bonumark generates noindex,nofollow, stops exposing the public identity structured data, removes the Profile from the sitemap, and stops exposing it through public Stream author metadata.

Public Stream posts can reference a public Profile through authorship metadata.

Private identity stays private.

That requirement needed explicit verification because machine-readable information is still public information even when it is not visible in the page design.

Portability followed the same ownership principle.

A Profile export produces a ZIP containing structured Profile data, readable Markdown, supporting documentation, and the original Profile media.

I tested the actual downloaded package.

It preserved identity information, links, Featured Work references, the avatar, cover image, and later the dedicated Profile Photos.

It intentionally excluded account credentials, roles, email addresses, security information, activity counts, Stream post bodies, comment content, generated image variants, and bundled-theme presentation settings.

The export represents the identity.

It is not a hidden system backup.

The final visible Profile addition was a small photo section limited to four images.

I chose a hard limit because the Profile should not become another feed or another Media Library.

Profile Photos belong to the Profile itself. Adding one does not create a Stream post. Each image supports alt text, an optional caption, ordering, and removal. Bonumark reuses the existing image validation, privacy processing, responsive variants, and full-screen viewer instead of creating another media subsystem.

The theme decides how one, two, three, or four photos are arranged.

Core preserves what those photos mean.

I tested populated Profiles on desktop and mobile, Featured Work, Profile Photos, metadata, sitemap behavior, public authorship, private Profiles, and real downloadable portability packages.

By the end of the cycle, the Profile finally felt like what I had intended from the beginning.

Identity beside publishing, not another version of publishing.

What This Week Proved

A lot changed in Bonumark Stream this week, but the most useful result was not the number of features or development versions.

It was the clarity that came from defining responsibilities.

Core owns platform behavior.

Themes own presentation.

The Stream owns publishing.

Admin owns deeper management.

Profiles own identity.

Exports preserve portable data rather than visual assumptions.

Security policies should support legitimate product behavior without being weakened casually.

Mobile design has to be tested as an interaction model, not treated as a desktop layout squeezed into a smaller width.

Automated tests and real use also continued to expose different classes of problems.

Syntax checks found structural errors. Runtime tests caught defects that valid PHP syntax could not. Package verification protected the upgrade files. CSS cascade comparisons proved that cleanup did not change styling. Desktop screenshots exposed hierarchy and spacing problems. Phone testing exposed clipped controls, bad touch targets, overflowing URLs, oversized sheets, and fixed controls covering the interface.

The default theme proved whether core behavior worked.

The custom theme proved whether the architecture really supported independent themes.

The development environment proved whether a feature could work.

The real installation showed whether it actually belonged.

That distinction is becoming more important as Bonumark Stream grows.

The work is no longer only about whether I can add another capability. I have to decide how that capability fits the product that already exists, what layer should own it, what old assumption it invalidates, and how I am going to prove that I did not break something else while adding it.

This week, that meant rejecting a Profile direction even though it technically worked. It meant changing a custom theme instead of making core CSS more aggressive. It meant removing a redundant publishing surface rather than maintaining two composers forever. It meant rebuilding admin workflows before deleting their old CSS. It meant correcting runtime behavior that passed syntax tests and interface problems that no automated test could see.

That is the work I want these Builder Receipts to document.

Not whether a ZIP built successfully.

Whether I made the right product decision, tested it honestly, found what the first pass missed, and kept working until the system behaved the way I intended.

You can see Bonumark Stream in action at demo.bonumark.org or jimlunsford.net, and download the project from GitHub at github.com/jimlunsford/bonumarkstream.


New Here?

Read Next:


Get the Work
Articles on discipline, recovery, identity, and ownership. Delivered when published.