From 9bb27a66f36676257457839e609cbabe3bf4fdf7 Mon Sep 17 00:00:00 2001 From: plx Date: Sun, 15 Mar 2026 13:52:46 -0500 Subject: [PATCH] Fix heading hierarchy across all pages - Add sr-only h1 on home page - Convert div page titles to h1 on blog, briefs, projects index pages - Change h5 section headings to h2 on home page and briefs index - Add configurable headingLevel prop to ContentCard component - Refactor contentCardHelpers to use options object pattern - Fix heading levels in content markdown (h3 -> h2 where h2 was skipped) - Fix list numbering in hdxl-xctest-retrofit - Fix list indentation in agentic-navigation-guide - Remove excessive blank lines in decision-execution-pattern Co-Authored-By: Claude Opus 4.6 --- src/components/ContentCard.astro | 10 +++-- ...st-trailing-closure-cannot-have-a-label.md | 8 ++-- .../testing/decision-execution-pattern.md | 3 -- .../agentic-navigation-guide/index.md | 26 ++++++------- .../projects/hdxl-xctest-retrofit/index.md | 2 +- src/lib/contentCardHelpers.ts | 38 ++++++++++++------- src/pages/blog/index.astro | 10 ++--- src/pages/briefs/[category].astro | 2 +- src/pages/briefs/index.astro | 10 ++--- src/pages/index.astro | 23 +++++------ src/pages/projects/index.astro | 6 +-- 11 files changed, 75 insertions(+), 63 deletions(-) diff --git a/src/components/ContentCard.astro b/src/components/ContentCard.astro index 9820efa..37b3bf9 100644 --- a/src/components/ContentCard.astro +++ b/src/components/ContentCard.astro @@ -1,6 +1,8 @@ --- import { renderInlineMarkdown } from "@lib/markdown"; +type HeadingLevel = 2 | 3 | 4 | 5 | 6; + type Props = { titlePrefix?: string; title: string; @@ -8,9 +10,11 @@ type Props = { link: string; ariaLabel?: string; maxLines?: number | "none"; + headingLevel?: HeadingLevel; } -const { titlePrefix, title, subtitle, link, ariaLabel, maxLines = 2 } = Astro.props; +const { titlePrefix, title, subtitle, link, ariaLabel, maxLines = 2, headingLevel = 3 } = Astro.props; +const HeadingTag = `h${headingLevel}` as keyof HTMLElementTagNameMap; const renderedTitlePrefix = titlePrefix ? renderInlineMarkdown(titlePrefix) : undefined; const renderedTitle = renderInlineMarkdown(title); const renderedSubtitle = renderInlineMarkdown(subtitle); @@ -38,12 +42,12 @@ const accessibleLabel = ariaLabel || `${titlePrefix ? titlePrefix + ': ' : ''}${ aria-label={accessibleLabel} class="relative group flex flex-nowrap py-3 px-4 pr-10 rounded-lg border border-black/15 dark:border-white/20 hover:bg-black/10 dark:hover:bg-white/10 hover:text-black dark:hover:text-white focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-blue-500 dark:focus-visible:ring-blue-400 motion-safe:transition-colors motion-safe:duration-300 motion-safe:ease-in-out">
-

+ {renderedTitlePrefix && ( )} -

+

{ Given the need to use labels to disambiguate between the two methods, we wind up with `@autoclosure` being the best fit for this API (instead of just using closures). -### Fused Functional Chains +## Fused Functional Chains As another example, for performance reason I often create "fused" versions of common functional chains: a fused "map, filter", a fused "filter, map", and so on. @@ -97,10 +97,10 @@ let premiumContactInfo = orders.mapFilterMap // feels clunky no matter how you finesse the formatting let premiumContactInfo = orders.mapFilterMap { $0.customer } - filter: { $0.isPremium } + filter: { $0.isPremium } map: { $0.contactInfo } ``` -### Is There Hope? +## Is There Hope? Sadly, no: this capability has already been discussed-and-decided against, [as discussed a bit here](https://forums.swift.org/t/can-first-trailing-closure-be-named/69793/8). diff --git a/src/content/briefs/testing/decision-execution-pattern.md b/src/content/briefs/testing/decision-execution-pattern.md index bdbe6a2..3887670 100644 --- a/src/content/briefs/testing/decision-execution-pattern.md +++ b/src/content/briefs/testing/decision-execution-pattern.md @@ -22,7 +22,4 @@ TODO: provide a *motivated*, *concrete* example. - - - The code in the "decision" phase should *generally* be structured as a pure function that receives all relevant information via parameters and returns a a function that returns a data item, e.g.: diff --git a/src/content/projects/agentic-navigation-guide/index.md b/src/content/projects/agentic-navigation-guide/index.md index 7f00498..8d6e3dd 100644 --- a/src/content/projects/agentic-navigation-guide/index.md +++ b/src/content/projects/agentic-navigation-guide/index.md @@ -66,19 +66,19 @@ For the *initial* implementation, I used a specification-driven workflow: 2. In *plan mode*, I had Opus generate a high-level roadmap with distinct *phases* (and iterated a bit until it was satisfactory) 3. I asked Claude to implement "phase 1" (and just "phase 1") 4. I had Claude write a `ContinuingMission.md` file that: - - described the work done so far - - described the work remaining - - described the immediate "next steps" for the next session -4. I then entered a loop like this: - - start a fresh session - - have Claude copy the `ContinuingMission.md` file into a `missions/` folder in the repo (and rename with a timestamp, to make it unique) - - have Claude read the `ContinuingMission.md` file and take on the next task - - review the results, offer feedback, and keep Claude iterating until he finished the task - - have Claude *rewrite* `ContinuingMission.md` to once again: - - describe the work done so far - - describe the work remaining - - describe the immediate "next steps" for the next session -5. I kept repeating that loop until the initial pass on the project was complete + - described the work done so far + - described the work remaining + - described the immediate "next steps" for the next session +5. I then entered a loop like this: + - start a fresh session + - have Claude copy the `ContinuingMission.md` file into a `missions/` folder in the repo (and rename with a timestamp, to make it unique) + - have Claude read the `ContinuingMission.md` file and take on the next task + - review the results, offer feedback, and keep Claude iterating until he finished the task + - have Claude *rewrite* `ContinuingMission.md` to once again: + - describe the work done so far + - describe the work remaining + - describe the immediate "next steps" for the next session +6. I kept repeating that loop until the initial pass on the project was complete Since this was my first pure vibe-coding experiment, I iteratively improved my workflow as I went: diff --git a/src/content/projects/hdxl-xctest-retrofit/index.md b/src/content/projects/hdxl-xctest-retrofit/index.md index f91c648..ae62e8d 100644 --- a/src/content/projects/hdxl-xctest-retrofit/index.md +++ b/src/content/projects/hdxl-xctest-retrofit/index.md @@ -11,7 +11,7 @@ repoURL: "https://github.com/plx/hdxl-xctest-retrofit/" 1. migrate from `XCTestCase` subclasses to `@Suite` structs 2. apply `@Test` annotation to test functions[^2] -2. prepend `#` to `XCTAssert*` calls +3. prepend `#` to `XCTAssert*` calls [^1]: The primary gaps are around expectations, expected failures, and attachments—IMHO those don't map cleanly to Swift Testing's APIs, so they're currently unsupported. diff --git a/src/lib/contentCardHelpers.ts b/src/lib/contentCardHelpers.ts index 9456d19..a474d8d 100644 --- a/src/lib/contentCardHelpers.ts +++ b/src/lib/contentCardHelpers.ts @@ -1,56 +1,66 @@ import type { CollectionEntry } from "astro:content"; import { extractCategoryFromSlug, getCategory } from "./category"; +type HeadingLevel = 2 | 3 | 4 | 5 | 6; + +type CardOptions = { + maxLines?: number | "none"; + headingLevel?: HeadingLevel; +}; + /** - * Transform a blog or project entry into ContentCard props + * Transform a blog entry into ContentCard props */ -export function getBlogCardProps(entry: CollectionEntry<"blog">, maxLines?: number | "none") { +export function getBlogCardProps(entry: CollectionEntry<"blog">, options?: CardOptions) { const displayTitle = entry.data.cardTitle || entry.data.title; - + return { title: displayTitle, subtitle: entry.data.description, link: `/${entry.collection}/${entry.slug}`, - ...(maxLines !== undefined && { maxLines }), + ...(options?.maxLines !== undefined && { maxLines: options.maxLines }), + ...(options?.headingLevel !== undefined && { headingLevel: options.headingLevel }), }; } /** - * Transform a blog or project entry into ContentCard props + * Transform a project entry into ContentCard props */ -export function getProjectCardProps(entry: CollectionEntry<"blog"> | CollectionEntry<"projects">, maxLines?: number | "none") { +export function getProjectCardProps(entry: CollectionEntry<"blog"> | CollectionEntry<"projects">, options?: CardOptions) { const displayTitle = entry.data.cardTitle || entry.data.title; - + return { title: displayTitle, subtitle: entry.data.description, link: `/${entry.collection}/${entry.slug}`, - ...(maxLines !== undefined && { maxLines }), + ...(options?.maxLines !== undefined && { maxLines: options.maxLines }), + ...(options?.headingLevel !== undefined && { headingLevel: options.headingLevel }), }; } /** * Transform a brief entry into ContentCard props * @param includeCategory - Whether to include the category as a title prefix - * @param maxLines - Maximum number of lines to display for the description, or "none" for unlimited + * @param options - Card display options (maxLines, headingLevel) */ -export function getBriefCardProps(entry: CollectionEntry<"briefs">, includeCategory = true, maxLines?: number | "none") { +export function getBriefCardProps(entry: CollectionEntry<"briefs">, includeCategory = true, options?: CardOptions) { const displayTitle = entry.data.cardTitle || entry.data.title; - + // Extract category from slug path const categorySlug = extractCategoryFromSlug(entry.slug); let categoryPrefix: string | undefined; - + if (includeCategory && categorySlug) { const category = getCategory(categorySlug, `src/content/briefs/${categorySlug}`); categoryPrefix = category.titlePrefix || category.displayName; } - + return { titlePrefix: categoryPrefix, title: displayTitle, subtitle: entry.data.description, link: `/${entry.collection}/${entry.slug}`, - ...(maxLines !== undefined && { maxLines }), + ...(options?.maxLines !== undefined && { maxLines: options.maxLines }), + ...(options?.headingLevel !== undefined && { headingLevel: options.headingLevel }), }; } diff --git a/src/pages/blog/index.astro b/src/pages/blog/index.astro index 56337d0..c928dae 100644 --- a/src/pages/blog/index.astro +++ b/src/pages/blog/index.astro @@ -39,21 +39,21 @@ const ogData = getListOGData(
-
+

Blog -

+
{years.map(year => (
-
+

{year} -

+
    { posts[year].map((post) => (
  • - +
  • )) } diff --git a/src/pages/briefs/[category].astro b/src/pages/briefs/[category].astro index fc6a197..544e52a 100644 --- a/src/pages/briefs/[category].astro +++ b/src/pages/briefs/[category].astro @@ -80,7 +80,7 @@ const ogData = getListOGData(
      {renderedBriefs.map((brief) => (
    • - +
    • ))}
    diff --git a/src/pages/briefs/index.astro b/src/pages/briefs/index.astro index 320d1b5..5fbfdf7 100644 --- a/src/pages/briefs/index.astro +++ b/src/pages/briefs/index.astro @@ -79,9 +79,9 @@ const ogData = getListOGData(
    -
    +

    Briefs -

    +
      { brief_categories.map(categoryKey => { @@ -91,9 +91,9 @@ const ogData = getListOGData( return (
    • -
      +

      { metadata.displayName } -

      + {hasCategory && ( @@ -111,7 +111,7 @@ const ogData = getListOGData( { categoryBriefs.map((brief: Brief) => (
    • - +
    • )) } diff --git a/src/pages/index.astro b/src/pages/index.astro index e4b3e5d..f65cf5a 100644 --- a/src/pages/index.astro +++ b/src/pages/index.astro @@ -39,6 +39,7 @@ const ogData = getHomeOGData( +

      Dispatches

      @@ -52,9 +53,9 @@ const ogData = getHomeOGData(
      -
      +

      Latest posts -

      + See all posts @@ -62,7 +63,7 @@ const ogData = getHomeOGData(
        {blog.map(post => (
      • - +
      • ))}
      @@ -70,9 +71,9 @@ const ogData = getHomeOGData(
      -
      +

      Recent briefs -

      + See all briefs @@ -80,7 +81,7 @@ const ogData = getHomeOGData(
        {briefs.map(brief => (
      • - +
      • ))}
      @@ -88,9 +89,9 @@ const ogData = getHomeOGData(
      -
      +

      Recent projects -

      + See all projects @@ -98,16 +99,16 @@ const ogData = getHomeOGData(
        {projects.map(project => (
      • - +
      • ))}
      -
      +

      Let's Connect -

      +

      Here's how to get in touch: diff --git a/src/pages/projects/index.astro b/src/pages/projects/index.astro index 4291e14..419becc 100644 --- a/src/pages/projects/index.astro +++ b/src/pages/projects/index.astro @@ -24,14 +24,14 @@ const ogData = getListOGData(

      -
      +

      Projects -

      +
        { projects.map((project) => (
      • - +
      • )) }