Skip to content

blog: replace ad-hoc segment maps with a closed 5-category vocabulary - #1652

Open
popovayoana wants to merge 3 commits into
mainfrom
fix/blog-category-system
Open

popovayoana wants to merge 3 commits into
mainfrom
fix/blog-category-system

Conversation

@popovayoana

@popovayoana popovayoana commented Jul 18, 2026

Copy link
Copy Markdown
Member

Stacked on #1651

This PR is stacked on #1651 (content fixes) β€” the diff below includes that PR's commit until it merges. Only the commits after that are new.

Summary

  • Labels on /blog were driven by two hand-maintained maps (CATEGORY_MAP, LABEL_OVERRIDES) keyed by post slug, separate from each post's own frontmatter tags:. Every new post had to be manually added to both maps or it silently showed no label chips at all β€” this happened to 4 posts in a row this week.
  • Replaces both maps with a single closed vocabulary of 5 categories: PortalJS, PortalJS Cloud, CKAN, AI Integration, Open Data.
  • Posts now declare categories: in frontmatter as the source of truth. A POST_CATEGORIES fallback map covers existing posts that predate this so nothing regresses.
  • Filter buttons and label chips now read from the exact same list, so they can no longer drift apart.
  • getStaticProps throws a build error if a post declares a category outside the five β€” a typo now fails the build loudly instead of quietly rendering no labels.
  • Documents the new frontmatter contract in site/CLAUDE.md (not the repo root CLAUDE.md, which is the PortalJS framework guide for external developers building portals β€” unrelated to publishing on our own marketing site) so publishers, human or AI, see the rule before shipping a post.

Test plan

  • npm run dev locally, confirmed /blog renders with exactly the 5 filter buttons (All / PortalJS / PortalJS Cloud / CKAN / AI Integration / Open Data)
  • Confirmed label chips render correctly for posts using the POST_CATEGORIES fallback (no frontmatter changes needed)
  • Confirmed the build throws when a post's frontmatter declares a category outside the 5 (tested with a throwaway invalid value, reverted after confirming)
  • Rebased cleanly onto latest main, no conflicts

πŸ€– Generated with Claude Code

Yoana Popova added 2 commits July 18, 2026 20:22
portaljs-is-now-ai-native, the Kyle rebuild post, and the Giftless/R2
post went live without a hero image, and the Kyle post also shipped
without frontmatter tags. Adds Yoana's hero images following the
existing /static/img/blog/<slug>/hero.png convention, and backfills
tags on the Kyle post.
Labels on /blog were driven by two hand-maintained maps (CATEGORY_MAP,
LABEL_OVERRIDES) keyed by post slug. Every new post had to be added to
both or it silently showed no label chips, even with tags in its
frontmatter β€” this happened to 4 posts in a row.

Replaces both maps with a single closed vocabulary of 5 categories
(PortalJS, PortalJS Cloud, CKAN, AI Integration, Open Data), driven by
a `categories:` frontmatter field. A POST_CATEGORIES fallback map
covers existing posts that predate this so nothing regresses. Filter
buttons and label chips now read from the same list, so they can't
drift apart, and getStaticProps throws if a post declares a category
outside the five β€” a typo now fails the build instead of quietly
rendering nothing.

Documents the new frontmatter contract in CLAUDE.md so publishers
(human or AI) see it before shipping a post.
@changeset-bot

changeset-bot Bot commented Jul 18, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: d23b7d8

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@vercel

vercel Bot commented Jul 18, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
www.portaljs.com Ready Ready Preview, Comment Jul 18, 2026 5:59pm
7 Skipped Deployments
Project Deployment Actions Updated (UTC)
portaljs-alan-turing Ignored Ignored Preview Jul 18, 2026 5:59pm
portaljs-ckan Ignored Ignored Preview Jul 18, 2026 5:59pm
portaljs-ckan-ssg Ignored Ignored Preview Jul 18, 2026 5:59pm
portaljs-fivethirtyeight Ignored Ignored Preview Jul 18, 2026 5:59pm
portaljs-git-example Ignored Ignored Preview Jul 18, 2026 5:59pm
portaljs-learn Ignored Ignored Preview Jul 18, 2026 5:59pm
portaljs-openspending Ignored Ignored Preview Jul 18, 2026 5:59pm

Request Review

@coderabbitai

coderabbitai Bot commented Jul 18, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

πŸ“ Walkthrough

Walkthrough

The blog page replaces segment-based filtering with validated categories, fallback category labels, category icons, and category-aware cards. Three posts gain hero image metadata, one gains tags, and site guidance documents category and featured-post conventions.

Changes

Blog category and metadata flow

Layer / File(s) Summary
Category contract and build validation
site/CLAUDE.md, site/pages/blog.tsx, site/content/blog/*.md
Defines the closed category vocabulary, fallback handling, frontmatter metadata, and build-time validation for invalid categories.
Category filtering and presentation
site/pages/blog.tsx
Replaces segment state and labels with category-based filtering, icons, hero labels, and card labels.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Visitor
  participant BlogPage
  participant BlogCards
  Visitor->>BlogPage: select activeCategory
  BlogPage->>BlogPage: filter posts with getPostCategories
  BlogPage->>BlogCards: render category labels and filtered cards
Loading

Possibly related PRs

Suggested reviewers: anuveyatsu

πŸš₯ Pre-merge checks | βœ… 5
βœ… Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage βœ… Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check βœ… Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check βœ… Passed Check skipped because no linked issues were found for this pull request.
Description Check βœ… Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check βœ… Passed The title accurately summarizes the main change: replacing blog segment maps with a closed five-category vocabulary.
✨ Finishing Touches
πŸ“ Generate docstrings
  • Create stacked PR
  • Commit on current branch
πŸ§ͺ Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/blog-category-system

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❀️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

πŸ€– Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@site/content/blog/rebuilt-city-of-kyle-open-data-portal-in-30-minutes-with-claude-code.md`:
- Around line 6-11: Replace the frontmatter tags list with a categories list in
the blog post, mapping the existing labels to the allowed vocabulary only:
PortalJS, PortalJS Cloud, CKAN, AI Integration, or Open Data. Preserve the
existing image metadata and remove the obsolete tags property.

In `@site/pages/blog.tsx`:
- Around line 294-304: Update the category validation loop around
blog.categories to reject any defined categories value that is not an array,
adding an else-if branch that throws a clear error identifying the blog URL and
expected YAML list format. Preserve the existing invalid-category checks for
array values.
πŸͺ„ Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
βš™οΈ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: a88c7726-bbb8-4061-8781-86f15909d942

πŸ“₯ Commits

Reviewing files that changed from the base of the PR and between 2a56847 and da24527.

β›” Files ignored due to path filters (3)
  • site/public/static/img/blog/portaljs-is-now-ai-native/hero.png is excluded by !**/*.png
  • site/public/static/img/blog/rebuilt-city-of-kyle-open-data-portal-in-30-minutes-with-claude-code/hero.png is excluded by !**/*.png
  • site/public/static/img/blog/scaling-portaljs-data-with-giftless-and-r2/hero.png is excluded by !**/*.png
πŸ“’ Files selected for processing (5)
  • CLAUDE.md
  • site/content/blog/portaljs-is-now-ai-native.md
  • site/content/blog/rebuilt-city-of-kyle-open-data-portal-in-30-minutes-with-claude-code.md
  • site/content/blog/scaling-portaljs-data-with-giftless-and-r2.md
  • site/pages/blog.tsx

Comment on lines +6 to +11
tags:
- PortalJS
- AI
- civic data
- open data
image: "/static/img/blog/rebuilt-city-of-kyle-open-data-portal-in-30-minutes-with-claude-code/hero.png"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚑ Quick win

Replace tags with categories using the allowed vocabulary.

As per coding guidelines, every blog post frontmatter must declare categories using only PortalJS, PortalJS Cloud, CKAN, AI Integration, or Open Data.

Additionally, the tags property is no longer rendered on the /blog page. The updated components (BlogCard, FeaturedBlogPost) receive labels={getPostCategories(blog)}, which evaluates to an array (even if empty) and overrides blog.tags entirely. To ensure these labels appear, they must be converted to categories.

πŸ› οΈ Proposed fix
-tags:
-  - PortalJS
-  - AI
-  - civic data
-  - open data
+categories:
+  - PortalJS
+  - AI Integration
+  - Open Data
 image: "/static/img/blog/rebuilt-city-of-kyle-open-data-portal-in-30-minutes-with-claude-code/hero.png"
πŸ“ Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
tags:
- PortalJS
- AI
- civic data
- open data
image: "/static/img/blog/rebuilt-city-of-kyle-open-data-portal-in-30-minutes-with-claude-code/hero.png"
categories:
- PortalJS
- AI Integration
- Open Data
image: "/static/img/blog/rebuilt-city-of-kyle-open-data-portal-in-30-minutes-with-claude-code/hero.png"
πŸ€– Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@site/content/blog/rebuilt-city-of-kyle-open-data-portal-in-30-minutes-with-claude-code.md`
around lines 6 - 11, Replace the frontmatter tags list with a categories list in
the blog post, mapping the existing labels to the allowed vocabulary only:
PortalJS, PortalJS Cloud, CKAN, AI Integration, or Open Data. Preserve the
existing image metadata and remove the obsolete tags property.

Source: Coding guidelines

Comment thread site/pages/blog.tsx
Comment on lines +294 to +304
for (const blog of blogList) {
if (Array.isArray(blog?.categories)) {
const invalid = blog.categories.filter((c: string) => !CATEGORY_SET.has(c))
if (invalid.length) {
throw new Error(
`Blog post "${blog.urlPath}" has invalid categories: ${invalid.join(', ')}. ` +
`Allowed categories are: ${CATEGORIES.join(', ')}.`
)
}
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟑 Minor | ⚑ Quick win

Validate that blog.categories is an array.

If a post author incorrectly formats the frontmatter (e.g., using a plain string categories: PortalJS instead of a YAML list), Array.isArray(blog?.categories) evaluates to false, bypassing this validation. The build will then crash during the React render phase with a cryptic TypeError: declared.filter is not a function in getPostCategories.

Adding an else if branch prevents this by failing the build with a clear error message.

πŸ› οΈ Proposed fix
   for (const blog of blogList) {
     if (Array.isArray(blog?.categories)) {
       const invalid = blog.categories.filter((c: string) => !CATEGORY_SET.has(c))
       if (invalid.length) {
         throw new Error(
           `Blog post "${blog.urlPath}" has invalid categories: ${invalid.join(', ')}. ` +
             `Allowed categories are: ${CATEGORIES.join(', ')}.`
         )
       }
+    } else if (blog?.categories !== undefined) {
+      throw new Error(
+        `Blog post "${blog.urlPath}" has an invalid categories format. Expected an array, got ${typeof blog.categories}.`
+      )
     }
   }
πŸ“ Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
for (const blog of blogList) {
if (Array.isArray(blog?.categories)) {
const invalid = blog.categories.filter((c: string) => !CATEGORY_SET.has(c))
if (invalid.length) {
throw new Error(
`Blog post "${blog.urlPath}" has invalid categories: ${invalid.join(', ')}. ` +
`Allowed categories are: ${CATEGORIES.join(', ')}.`
)
}
}
}
for (const blog of blogList) {
if (Array.isArray(blog?.categories)) {
const invalid = blog.categories.filter((c: string) => !CATEGORY_SET.has(c))
if (invalid.length) {
throw new Error(
`Blog post "${blog.urlPath}" has invalid categories: ${invalid.join(', ')}. ` +
`Allowed categories are: ${CATEGORIES.join(', ')}.`
)
}
} else if (blog?.categories !== undefined) {
throw new Error(
`Blog post "${blog.urlPath}" has an invalid categories format. Expected an array, got ${typeof blog.categories}.`
)
}
}
πŸ€– Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@site/pages/blog.tsx` around lines 294 - 304, Update the category validation
loop around blog.categories to reject any defined categories value that is not
an array, adding an else-if branch that throws a clear error identifying the
blog URL and expected YAML list format. Preserve the existing invalid-category
checks for array values.

Root CLAUDE.md is the PortalJS framework guide for external developers
building portals with the template β€” it has nothing to do with
publishing on portaljs.com's own marketing blog. Moves the category
vocabulary rules into a new site/CLAUDE.md, scoped to this repo's
internal site/ directory, so it's picked up when working on the site
without polluting the framework-facing guide.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

πŸ€– Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@site/CLAUDE.md`:
- Line 10: Update the category requirement in CLAUDE.md to apply only to new or
updated posts, matching the fallback behavior in blog.tsx for older posts
without frontmatter categories; alternatively, remove that fallback before
requiring categories on every post.
- Around line 12-14: Add the text language identifier to the fenced Markdown
block containing the comma-separated category list in CLAUDE.md, changing the
opening fence to specify text while preserving the list content.
πŸͺ„ Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
βš™οΈ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: a6387678-e292-4779-9cc8-24d1937a1223

πŸ“₯ Commits

Reviewing files that changed from the base of the PR and between da24527 and d23b7d8.

πŸ“’ Files selected for processing (1)
  • site/CLAUDE.md

Comment thread site/CLAUDE.md

## Blog posts (site/content/blog)

Every post's frontmatter must declare `categories:`, using ONLY these five values:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

πŸ“ Maintainability & Code Quality | 🟑 Minor | ⚑ Quick win

Clarify the category requirement to match the fallback behavior.

site/pages/blog.tsx falls back to POST_CATEGORIES for older posts without frontmatter categories, so β€œEvery post” overstates the current contract. Specify that new or updated posts must declare categories, or remove the fallback before enforcing this requirement.

Also applies to: 22-25

πŸ€– Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@site/CLAUDE.md` at line 10, Update the category requirement in CLAUDE.md to
apply only to new or updated posts, matching the fallback behavior in blog.tsx
for older posts without frontmatter categories; alternatively, remove that
fallback before requiring categories on every post.

Comment thread site/CLAUDE.md
Comment on lines +12 to +14
```
PortalJS, PortalJS Cloud, CKAN, AI Integration, Open Data
```

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

πŸ“ Maintainability & Code Quality | 🟑 Minor | ⚑ Quick win

Add a language identifier to the fenced block.

This triggers Markdownlint MD040. Use text for the comma-separated category list.

Proposed fix
-```
+```text
 PortalJS, PortalJS Cloud, CKAN, AI Integration, Open Data

</details>

<!-- suggestion_start -->

<details>
<summary>πŸ“ Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

```suggestion

🧰 Tools
πŸͺ› markdownlint-cli2 (0.23.0)

[warning] 12-12: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

πŸ€– Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@site/CLAUDE.md` around lines 12 - 14, Add the text language identifier to the
fenced Markdown block containing the comma-separated category list in CLAUDE.md,
changing the opening fence to specify text while preserving the list content.

Source: Linters/SAST tools

This branch was successfully deployed

1 active deployment
Preview – www.portaljs.com β€” d23b7d8e Deployed Jul 18, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

1 participant