Lossless Changelog
product

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.

Michael StatonMusic Editor

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.

TS
// 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}`);
  }
}

Previously, the auth cookie was scoped to /portfolio, which prevented it from being sent to /memos/*.

Key change: cookie path is now /.

TS
// 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, ''))

ASTRO
---
// 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.astro

  • export const prerender = false

  • Reads universal_portfolio_access to display auth status

  • Renders two confidential grids:

    • LP commits

    • Direct investments

The page maps portfolio JSON into the new grid props, including the memo slug field:

ASTRO
---
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:

JSON
// src/content/portfolio/lpcommits-portfolio.json
"extendedMemoMD": "Example-Company-v0.0.2-draft"
JSON
// 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.

ASTRO
---
// 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:

ASTRO
<!-- 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}
/>

The auth badge inside the passcode UI previously displayed a hardcoded unauthenticated state.

It now reflects the real server cookie:

ASTRO
---
// 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 /memos along 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 — added extendedMemoMD slugs.

  • src/content/portfolio/directs-portfolio.json — added extendedMemoMD slugs.

  • src/components/markdown/MemoTableOfContents.astro — TOC spacing tweak.


Notes / Follow-Ups

  • If you previously authenticated before the cookie path change, 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.