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()addsnewsDetailroutes first; the React family skips any slug already taken, andnewsItemPageSlugs()also excludes reviewed slugs, so each slug has one owner.reactPageForRoute()maps/newstobuildNewsProps; the/news/*React family is inFAMILIESinserver/reactPageRoutes.ts.newsDetailroutes 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()inserver/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,itemsofslug,impressions> 0,clicks), validated byvalidateNewsIndexAllowlist()inserver/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()inserver/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), readingclient/public/news-items.json, kept only for current public companies. - Rewording table:
content/news/rewordings.yaml(rules plusvalueClauseRemoved). - Item pages:
server/publicPages/newsItem.ts(listNewsItemSlugs,newsItemIndexing,buildNewsItemProps,newsItemPageMeta). - Components:
client/src/publicPages/NewsPage.tsx,NewsItemPage.tsx,newsTypes.ts,newsItemTypes.ts; CSSstyles/news.css,styles/newsItem.css. - Copy:
content/pages/news.yaml,news-item.yaml,news-detail.yaml(static detail page vianewsCopy()); title templatesnewsDetailincontent/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.newsDetailtemplates (firstFittingTitle, 60 chars), descriptionheadline. 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 perpublic-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
- Create
content/news/portfolio-news.yamland the registry loader and validator. - Add
newsDetailroutes increateSeoRouteManifest()andrenderPublicNewsDetailPage()inrenderRouteDocument(), with words incontent/pages/news-detail.yaml. - Build the feed (
news.ts) withcontent/news/rewordings.yaml; addNewsPageand map/newsinreactPageForRoute(). - Add
newsItem.ts,NewsItemPage, and the/newsentry inFAMILIESwithindexing: newsItemIndexing; addserver/newsIndexAllowlist.tsand generate the allowlist JSON. deploy-gate, thenui-before-after, thenship-to-production.