Confidential Portfolio Gateway, Memo Linking, and Memo Access Control
The confidential portfolio becomes its own SSR route behind middleware, the auth cookie widens to site-wide so it survives a hop between sections, and portfolio cards grow a memo icon that links straight through to the investment memo.
Changelog - 2025-12-12 (#1)
Hypernova Site: Confidential Portfolio Gateway + Memo Linking + Memo Access Control
Overview
This sprint tightened up the confidential portfolio experience end-to-end:
Confidential portfolio view is now a dedicated SSR page under
/portfolio/confidential/.Portfolio cards can link directly to investment memos (new memo icon on expanded cards).
Investment memo routes are now protected by the same passcode gate as confidential portfolio.
Memo slugs are more forgiving: case-insensitive and tolerant of dotless semantic versions (
v0.0.2→v002).Cleaned up an auth UI inconsistency by making the auth badge reflect the real server cookie.
Changes by Area
1. Route Protection: Gate /portfolio/confidential + /memos via Middleware
We now enforce passcode access centrally with Astro middleware, so direct navigation to memo URLs is also protected.
Key change: middleware guards both /portfolio/confidential and /memos.
// src/middleware.ts
if (pathname.startsWith('/portfolio/confidential') || pathname.startsWith('/memos')) {
const accessCookie = cookies.get('universal_portfolio_access');
if (!accessCookie?.value) {
const redirectPath = encodeURIComponent(pathname + (url.search || ''));
return redirect(`/portfolio-gate?redirect=${redirectPath}`);
}
}2. Passcode Cookie Scope: Make Auth Cookie Valid Site-Wide
Previously, the auth cookie was scoped to /portfolio, which prevented it from being sent to /memos/*.
Key change: cookie path is now /.
// src/pages/api/verify-portfolio-passcode.ts
cookies.set('universal_portfolio_access', sessionToken, {
httpOnly: true,
secure: import.meta.env.PROD,
sameSite: 'strict',
maxAge: 60 * 60 * 24,
path: '/',
});3. Memo Routing: Case-Insensitive + Dotless Slug Resolution
To support URLs like:
/memos/Example-Company-v002-draft/memos/example-company-v002-draft
…the memo lookup now normalizes:
casing (
toLowerCase())semantic version dots (
replace(/\./g, ''))
---
// src/pages/memos/[slug].astro
const normalize = (s: string) => s.toLowerCase().replace(/\./g, '');
const normalizedSlug = slug ? normalize(slug) : undefined;
const entry =
entries.find((e) => e.slug === slug) ??
(normalizedSlug ? entries.find((e) => normalize(e.slug) === normalizedSlug) : undefined);
---4. Confidential Portfolio Page: New SSR Page + Data Plumbing
Created a dedicated SSR-only confidential page:
src/pages/portfolio/confidential/index.astroexport const prerender = falseReads
universal_portfolio_accessto display auth statusRenders two confidential grids:
LP commits
Direct investments
The page maps portfolio JSON into the new grid props, including the memo slug field:
---
const accessCookie = Astro.cookies.get('universal_portfolio_access');
const authLevel = accessCookie?.value ? 'general-passcode' : 'unauthenticated';
const lpcommits: LogoOnlyItem[] = (lpcommitsData as any[]).map((p: any) => ({
brand: p.conventionalName ?? p.officialName,
lightMode: p.logoLightMode,
darkMode: p.logoDarkMode,
urlToPortfolioSite: p.urlToPortfolioSite,
extendedMemoMD: p.extendedMemoMD,
blurbShortTxt: p.blurbShortTxt,
team: p.listOfPeopleData,
slug: slugify(p.conventionalName ?? p.officialName ?? ''),
}));
---5. Memo Linking from Portfolio Cards
A) Data: extendedMemoMD on portfolio JSON entries
Portfolio JSON now includes extendedMemoMD to identify an associated memo slug.
Example additions:
// src/content/portfolio/lpcommits-portfolio.json
"extendedMemoMD": "Example-Company-v0.0.2-draft"// src/content/portfolio/directs-portfolio.json
"extendedMemoMD": "Second-Example-v0.0.2-draft"B) UI: Document icon on expanded card linking to /memos/{slug}
The expanded confidential card supports memoPath and generates a memo link, while also stripping semantic-version dots for dotless URLs.
---
// src/components/basics/grids/grid-cards/LogoCardExpanded--ConfidentialAccess.astro
const memoHref = memoPath
? `${memoBasePath.replace(/\/$/, '')}/${memoPath.replace(/^\//, '').replace(/\./g, '')}`
: undefined;
---And the grid passes extendedMemoMD into that memoPath prop:
<!-- src/components/basics/grids/grid-cards/LogoGrid--ConfidentialAccess.astro -->
<LogoCardExpanded
brand={p.brand}
lightMode={p.lightMode}
darkMode={p.darkMode}
urlToPortfolioSite={p.urlToPortfolioSite}
memoPath={p.extendedMemoMD}
blurbShortTxt={p.blurbShortTxt}
team={p.team}
/>6. Auth UI Consistency: Authentication Modal Reads Cookie
The auth badge inside the passcode UI previously displayed a hardcoded unauthenticated state.
It now reflects the real server cookie:
---
// src/components/ui/AuthenticationModal.astro
const accessCookie = Astro.cookies.get('universal_portfolio_access');
const level = accessCookie?.value ? 'general-passcode' : 'unauthenticated';
---
<AuthenticationStatus level={level} />Also replaced an unsupported Astro event attribute with standard onclick.
7. Memo UX: Table of Contents Spacing
Minor layout adjustment for memo TOC sidebar:
Added
padding-top: 3rem;to.memo-toc-sidebar.
Files Changed Summary (Staged)
New
src/pages/portfolio/confidential/index.astro— SSR confidential portfolio page.src/components/basics/grids/grid-cards/LogoGrid--ConfidentialAccess.astro— confidential grid that supports expanded cards + memo linking.src/components/basics/grids/grid-cards/LogoCardExpanded--ConfidentialAccess.astro— expanded card UI with memo icon linking to/memos/....src/components/basics/grids/grid-cards/LogoGrid--LogoOnly.astro— logo-only grid mode (supporting component).
Renamed
src/pages/portfolio/confidential.astro→src/pages/portfolio/confidential/no-access.astro— clarified route intent.
Modified
src/middleware.ts— gate/memosalong with confidential portfolio.src/pages/api/verify-portfolio-passcode.ts— cookie path widened to/.src/pages/memos/[slug].astro— case-insensitive + dotless slug resolution.src/components/ui/AuthenticationModal.astro— auth badge now reflects cookie state; fixed click handler.src/content/portfolio/lpcommits-portfolio.json— addedextendedMemoMDslugs.src/content/portfolio/directs-portfolio.json— addedextendedMemoMDslugs.src/components/markdown/MemoTableOfContents.astro— TOC spacing tweak.
Notes / Follow-Ups
If you previously authenticated before the cookie
pathchange, you may need to re-enter the passcode once to receive the new/-scoped cookie.Consider adding an explicit “Logout / Clear cookie” affordance if you expect frequent sharing/testing of this gated content.
