sahin.io

All skills and agents

page-news

The Portfolio News pages on sahin.io/angel-investments - the /news feed, the reviewed source-linked news registry (content/news/portfolio-news.yaml) and the recovered per-announcement pages with their search-history index allowlist (data/news-index-allowlist.json). Use when adding a news item, rebuilding the news pages, changing news rewording, or deciding whether a /news/{slug} page is indexed.

Source file
.claude/skills/page-news/SKILL.md
Routes covered
/news/news/*

News pages

Routes

Three route sources share the /news prefix:

URL Kind Count today Render Indexing
https://sahin.io/angel-investments/news legacyFeature (menu) 1 React news index
https://sahin.io/angel-investments/news/{slug} (reviewed registry) newsDetail 23 static renderPublicNewsDetailPage() always index
https://sahin.io/angel-investments/news/{slug} (recovered feed item) reactPage (/news family in FAMILIES) about 568 React newsItem index only if on the allowlist (about 50), else noindex, follow
  • createSeoRouteManifest() adds newsDetail routes first; the React family skips any slug already taken, and newsItemPageSlugs() also excludes reviewed slugs, so each slug has one owner.
  • reactPageForRoute() maps /news to buildNewsProps; the /news/* React family is in FAMILIES in server/reactPageRoutes.ts. newsDetail routes get no React page: the static body is kept inside the React layout.

The reviewed news registry

  • File: content/news/portfolio-news.yaml (schemaVersion: 1, items). Each item: slug, companySlug, headline, date, summary, publisher, sourceUrl (public HTTPS), reviewer, reviewedOn.
  • Loaded and validated by loadPublicNewsRegistry() / validatePublicNewsRegistry() in server/publicContentRegistries.ts: exact keys, unique slugs, company must be in the public portfolio. Only validated items reach the manifest.
  • Never add funding, revenue, company-value, transaction or private operating details.

The news index allowlist

  • File: data/news-index-allowlist.json (schemaVersion: 1, source, items of slug, impressions > 0, clicks), validated by validateNewsIndexAllowlist() in server/newsIndexAllowlist.ts; newsIndexAllowlistSlugs() returns an empty set when absent.
  • Meaning: a recovered /news/{slug} URL that already had Google Search Console impressions stays indexable (newsItemIndexing() in server/publicPages/newsItem.ts). Everything else is served but noindex and kept out of the sitemap, llms files and IndexNow.
  • Rebuild: npx tsx scripts/build-news-index-allowlist.ts --csv <GSC pages export> --window "<window>".
  • A new announcement gets indexed only by adding it to the reviewed registry, never by editing the allowlist by hand.

Files

  • Feed: server/publicPages/news.ts (loadNewsFeed, buildNewsProps, buildNewsPropsWithReport, newsItemPageSlugs, reviewedNewsSlugs, rewordForPolicy, headlineWithoutValue), reading client/public/news-items.json, kept only for current public companies.
  • Rewording table: content/news/rewordings.yaml (rules plus valueClauseRemoved).
  • Item pages: server/publicPages/newsItem.ts (listNewsItemSlugs, newsItemIndexing, buildNewsItemProps, newsItemPageMeta).
  • Components: client/src/publicPages/NewsPage.tsx, NewsItemPage.tsx, newsTypes.ts, newsItemTypes.ts; CSS styles/news.css, styles/newsItem.css.
  • Copy: content/pages/news.yaml, news-item.yaml, news-detail.yaml (static detail page via newsCopy()); title templates newsDetail in content/pages/site-titles.yaml.

Sections on the page

  • /news: eyebrow, title, subtitle; search; market and stage filters; company cards (logo, market, stage, sourced company value, highlight) with their announcements; "more" button; reviewed-news list linking each registry item.
  • Reviewed /news/{slug}: eyebrow, company logo link, H1 headline, summary, byline (date, publisher), source panel with outbound link, company context (one-liner, badges, link), up to 4 related items from the same company, related public pages, back link.
  • Recovered /news/{slug}: company header, headline, date, context (sourced value, market, stage, highlight), more from the company, more in the market, back link.

SEO

  • Reviewed: title from titles.newsDetail templates (firstFittingTitle, 60 chars), description headline. summary, ogType: article, breadcrumb hub > News > headline.
  • Recovered: newsItemPageMeta(): "{Headline} — {Company} | Sahin Boydas" fitted to 60 chars, description at most 155 chars; noindex unless allowlisted.

Rules

  • Company values never come from the feed; only via the sourced value component. A headline loses its value clause or is dropped; lines still naming a restricted term are dropped.
  • Slugs with restricted vocabulary get no page. No private deal data (private-data-boundary).
  • Words in YAML (page-content-yaml); URLs per public-url-conventions.

Tests

server/news-company-values.test.ts, server/route-indexing.test.ts, server/seo-route-manifest.test.ts, server/llms-text.test.ts, server/sitemap.test.ts, server/public-content-policy.test.ts.

Rebuild from scratch

  1. Create content/news/portfolio-news.yaml and the registry loader and validator.
  2. Add newsDetail routes in createSeoRouteManifest() and renderPublicNewsDetailPage() in renderRouteDocument(), with words in content/pages/news-detail.yaml.
  3. Build the feed (news.ts) with content/news/rewordings.yaml; add NewsPage and map /news in reactPageForRoute().
  4. Add newsItem.ts, NewsItemPage, and the /news entry in FAMILIES with indexing: newsItemIndexing; add server/newsIndexAllowlist.ts and generate the allowlist JSON.
  5. deploy-gate, then ui-before-after, then ship-to-production.