Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# for build
css/xterm.css
js/xterm*.js*
js/aalib.js*
# Everything `npm run build` produces — vendor assets, the app bundle, and the
# static mirror — lands here. It is the Netlify publish directory and is never
# committed, so generated files cannot drift from config/*.js.
dist/

images/*Zone.Identifier

Expand Down
79 changes: 76 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ Who needs a website when you have a terminal.
- instagram: instagram account
- git: this repo
- github: all repos
- locate: physical address
- www: plain-text version of this site
- test: do not use
- other: try your favorite linux commands

Expand Down Expand Up @@ -83,12 +85,62 @@ Future project: get the Hello Worlds working for every portfolio company with a
- zed

## Build Notes
Run `npm run build` before serving the site locally or deploying.
`npm run build` produces `dist/`, which is what Netlify publishes. It is wiped
and rebuilt from scratch each time, and it is gitignored — no build output is
ever committed.

That build now:
That build:
- copies the static assets (`images/`, `videos/`, `css/`, `welcome.htm`, and the
`config/*.js` files `welcome.htm` loads directly)
- copies and minifies the xterm vendor assets
- bundles the app boot/runtime code into `js/app.bundle.js`
- bundles the app boot/runtime code into `dist/js/app.bundle.js`
- emits a minified lazy-load asset for the RickRoll animation
- generates the static mirror and `dist/index.html` (see below)

Anything not on the copy list in `scripts/build-assets.js` stays out of `dist/`,
so `scripts/`, `tests/`, `package.json`, and the unbundled `js/` sources are not
served.

`npm start` builds and then runs `netlify dev` against `dist/`.

## The static mirror
The terminal renders its content only when someone types a command, so search
engines and LLM crawlers see an empty page. `scripts/build-pages.js` renders the
same `config/*.js` data as plain HTML at real URLs:

```
/about/ /jobs/ /portfolio/ /portfolio/<slug>/ /team/ /team/<slug>/
robots.txt sitemap.xml llms.txt llms-full.txt
```

**`config/*.js` is the only source of truth.** Changing a portfolio description
regenerates that company's page, both indexes, the sitemap, the llms files, and
the `<noscript>` block in `index.html`.

There is nothing to keep in sync. The mirror is never committed — it exists only
in `dist/`, and every deploy runs `npm run build` and regenerates it from the
current `config/*.js`. No cron, no CI drift check, no "rebuild and commit" step:
a config edit reaches the static pages the moment it is deployed, because that
is the only way the pages come into existence.

`index.html` in the repo root is a **template**. The regions between its
`<!-- BEGIN generated-* -->` sentinels are empty; the build injects the JSON-LD
and `<noscript>` blocks when it writes `dist/index.html`. Do not paste generated
content back into the template.

Use `npm run build:pages` to regenerate just the mirror while iterating —
`netlify dev` does not watch `config/*.js`.

Firm-level facts (blurb, thesis, fund size, office address, email) live in
`config/firm.js` so the terminal and the static pages cannot disagree. It has no
dependencies and must load before `config/commands.js` in both
`scripts/build-assets.js` and `welcome.htm`.

`config/firm.js`, `portfolio.js`, `team.js`, and `jobs.js` end with a guarded
`module.exports` so they work unchanged as classic browser scripts *and* as
CommonJS modules for `scripts/build-pages.js`. Keep the `typeof module` guard —
an unguarded `module` reference is a ReferenceError in the browser — and do not
convert them to ESM, which would break the bundle and `welcome.htm`.

## Performance Notes
The terminal now initializes on `DOMContentLoaded` instead of waiting for `window.onload`, and optional work such as ASCII art preloading happens after the terminal is already usable.
Expand All @@ -98,6 +150,27 @@ In local repeated Chromium benchmarks against the previous `HEAD`, median startu
- homepage first command rendered: `2076.5ms` -> `223.5ms`
- `#whois-lee` deep link rendered: `1159.9ms` -> `151.9ms`

## Hosting
root.vc is served by **Netlify**, which publishes `dist/` — `server: Netlify` on
the live response, the apex `A` record points at Netlify's load balancer, and
`www` 301s to the apex.

The repo is also connected to a **Vercel** project. That project belongs to the
`ai-incarnations` branch (the parked AI-reinvention experiment, #110), which
carries its own `vercel.json` and builds there successfully. `main` has no
Vercel deployment, so Vercel had nothing to build on this line: every attempt
failed and posted a red check on PRs that had nothing to do with Vercel.

`vercel.json` here sets [`git.deploymentEnabled: false`][1], which turns off
automatic Vercel deployments for branches carrying this file.

Do not "fix" this by making it match the `ai-incarnations` copy — that branch
deliberately omits the key so it keeps deploying. If it is ever merged down,
expect a conflict on `vercel.json` and resolve it toward whichever host is
actually serving the domain at that point.

[1]: https://vercel.com/docs/project-configuration/git-configuration#git.deploymentenabled

Live at: [https://root.vc](https://root.vc).

Special thanks to [Jerry Neumann](https://www.linkedin.com/in/jerryneumann/) at [Neu Venture Capital](https://neuvc.com/) for the inspiration for this website concept.
Expand Down
50 changes: 31 additions & 19 deletions config/commands.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,12 @@
// To add a new command, add an entry to `commands` below.
// To add a simple redirect (alias), add an entry to `_aliases` at the bottom.

// Blurb displayed by `whois root`.
const whoisRoot =
"Root Ventures is a San Francisco-based deep tech seed fund. As engineers ourselves, we specialize in leading initial funding for founders tackling new technical opportunities. Our initial investments typically range from $3-5M. With a selective few new deals a year and 2/3 of our funds in reserve, we are committed to being a long-term partner. Try %whois% and one of avidan, kane, chrissy, lee, ben, zodi, or laelah to learn more about our team.";
// Blurb displayed by `whois root`. The firm description itself lives in
// config/firm.js so the static pages read the same copy; only the terminal
// nudge at the end is added here.
const whoisRoot = `${firm.blurb} Try %whois% and one of ${Object.keys(team)
.slice(0, -1)
.join(", ")}, or ${Object.keys(team).slice(-1)} to learn more about our team.`;

// Set to 10 during development to speed up animated sequences (upgrade, etc.).
const timeUnit = 1000;
Expand Down Expand Up @@ -117,7 +120,7 @@ const commands = {
}
} else if (!portfolio[name]) {
term.stylePrint(
`Portfolio company ${name} not found. Should we talk to them? Email us: hello@root.vc`
`Portfolio company ${name} not found. Should we talk to them? Email us: ${firm.email}`
);
} else {
const company = portfolio[name];
Expand Down Expand Up @@ -159,7 +162,17 @@ const commands = {
},

pine: function () {
term.openURL("mailto:hello@root.vc");
term.openURL(`mailto:${firm.email}`);
},

// Points at the static, crawlable mirror of this terminal generated by
// scripts/build-pages.js. Same data, plain HTML, real URLs.
www: function () {
term.stylePrint("Plain-text version of this site:");
term.displayURL("https://root.vc/about/");
term.displayURL("https://root.vc/portfolio/");
term.displayURL("https://root.vc/team/");
term.displayURL("https://root.vc/jobs/");
},

// ── File System ─────────────────────────────────────────────────────────────
Expand Down Expand Up @@ -505,9 +518,11 @@ const commands = {
},

locate: function () {
term.stylePrint("Root Ventures");
term.stylePrint("2670 Harrison St");
term.stylePrint("San Francisco, CA 94110");
const { streetAddress, addressLocality, addressRegion, postalCode } =
firm.address;
term.stylePrint(firm.name);
term.stylePrint(streetAddress);
term.stylePrint(`${addressLocality}, ${addressRegion} ${postalCode}`);
},

history: function () {
Expand Down Expand Up @@ -671,25 +686,19 @@ const commands = {
await term.delayStylePrint("\r\n", 1 * timeUnit);
await term.dottedPrint("Calculating new fund size", 3);
await term.delayPrint(
`Updated fund size: ${colorText("$190M", "prompt")}\r\n`,
`Updated fund size: ${colorText(firm.fundSize, "prompt")}\r\n`,
1 * timeUnit
);
await term.delayPrint(
`Updated typical check size: ${colorText("up to $5M", "prompt")}\r\n`,
`Updated typical check size: ${colorText(firm.checkSize, "prompt")}\r\n`,
1 * timeUnit
);
await term.delayPrint(
`Found mission: ${colorText(
"Seeding bold engineers.",
"user"
)}\r\n`,
`Found mission: ${colorText(`${firm.tagline}.`, "user")}\r\n`,
1 * timeUnit
);
await term.delayPrint(
`Thesis (no update): ${colorText(
"Investing at the earliest stages of technical founders taking engineering risk.",
"user"
)}\r\n`,
`Thesis (no update): ${colorText(firm.thesis, "user")}\r\n`,
1 * timeUnit
);

Expand Down Expand Up @@ -855,7 +864,7 @@ const commands = {
`\r\n${colorText("✗", "user")} Error submitting application: ${error.message}`
);
term.stylePrint(
"\r\nPlease try again or email us directly at hello@root.vc"
`\r\nPlease try again or email us directly at ${firm.email}`
);
}

Expand Down Expand Up @@ -912,6 +921,9 @@ const _aliases = {
// Contact shortcuts
email: "pine",
insta: "instagram",
// Static mirror. Note `exit`/`quit`/`stop` open welcome.htm instead.
web: "www",
site: "www",
// Portfolio easter egg chains
ge: "great_expectations",
great_expectations: "superconductive",
Expand Down
61 changes: 61 additions & 0 deletions config/firm.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
// firm.js — Facts about Root Ventures itself.
//
// This is pure data with no dependencies, so it can be read both by the
// terminal (via js/app.bundle.js) and by scripts/build-pages.js, which loads
// it in a Node vm sandbox to generate the static pages crawlers read.
//
// Anything here that describes the firm — the blurb, the address, the fund
// size — should live in this file only, so the terminal and the static pages
// can never disagree. Keep it free of `term`, `document`, and other globals.

const firm = {
name: "Root Ventures",
tagline: "Seeding bold engineers",
thesis:
"Investing at the earliest stages of technical founders taking engineering risk.",

// Displayed by `whois root` and used as the description on the static pages.
blurb:
"Root Ventures is a San Francisco-based deep tech seed fund. As engineers ourselves, we specialize in leading initial funding for founders tackling new technical opportunities. Our initial investments typically range from $3-5M. With a selective few new deals a year and 2/3 of our funds in reserve, we are committed to being a long-term partner.",

fundSize: "$190M",
checkSize: "up to $5M",
email: "hello@root.vc",

// Split into schema.org PostalAddress fields so the static pages can emit
// structured data without re-parsing a formatted string.
address: {
streetAddress: "2670 Harrison St",
addressLocality: "San Francisco",
addressRegion: "CA",
postalCode: "94110",
addressCountry: "US",
},

// Emitted as schema.org `sameAs`, which is how search engines and LLMs
// reconcile this site with the firm's other profiles.
social: [
"https://twitter.com/rootvc",
"https://github.com/rootvc",
"https://instagram.com/machinepix/",
],

// Emitted as schema.org `knowsAbout` on the Organization node.
focusAreas: [
"deep tech",
"robotics",
"hardware",
"manufacturing",
"developer tools",
"machine learning",
"semiconductors",
"automation",
],
};

// Dual-mode. This file stays a classic browser script — it is concatenated into
// js/app.bundle.js and loaded by a raw <script src> tag in welcome.htm — while
// also being requireable by scripts/build-pages.js. `module` is undefined in a
// browser, so the line is inert there. Do not switch this to `export`: that
// would make the file a module and break both of the browser paths above.
if (typeof module !== "undefined") module.exports = { firm };
1 change: 1 addition & 0 deletions config/help.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ const help = {
"%git%": "this repo",
"%github%": "all repos",
"%locate%": "physical address",
"%www%": "plain-text version of this site",
"%jobs%": "check out our job openings",
"%test%": "do not use",
"%upgrade%": "upgrade to the latest version of Root Ventures",
Expand Down
3 changes: 3 additions & 0 deletions config/jobs.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,6 @@ const jobs = {
"and work with some of the most innovative technical founders in the world.",
],
};

// Dual-mode; see the note in config/firm.js. Inert in a browser.
if (typeof module !== "undefined") module.exports = { jobs };
3 changes: 3 additions & 0 deletions config/portfolio.js
Original file line number Diff line number Diff line change
Expand Up @@ -435,3 +435,6 @@ const portfolio = {
"Vibe Robotics, founded by Kai Backman and Luke Church, builds novel programming interfaces for robotics.",
}
};

// Dual-mode; see the note in config/firm.js. Inert in a browser.
if (typeof module !== "undefined") module.exports = { portfolio };
3 changes: 3 additions & 0 deletions config/team.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,3 +56,6 @@ const team = {
groups: "wheel admin operations miracleworkers gamers",
},
};

// Dual-mode; see the note in config/firm.js. Inert in a browser.
if (typeof module !== "undefined") module.exports = { team };
Loading
Loading