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
13 changes: 13 additions & 0 deletions apps/www/.vscode/tasks.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"version": "2.0.0",
"tasks": [
{
"type": "npm",
"script": "dev",
"problemMatcher": ["$tsc-watch"],
"label": "dev",
"detail": "conc -n Next.js,next-video 'next dev' 'pnpm exec next-video sync -w'",
"isBackground": true
}
]
}
2 changes: 0 additions & 2 deletions apps/www/content/docs/arkenv/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,8 @@ import { SiGithub as GitHub } from "@icons-pack/react-simple-icons";

## The ArkEnv icon


<img src="/assets/icon.svg" alt="ArkEnv icon" width="128" height="128"/>


The ArkEnv icon tells the story of what we're building. It weaves together three distinct symbols:

- **A gear**: At first glance, it's a settings gear, representing the core mechanical purpose of the library: precise environment configuration.
Expand Down
52 changes: 52 additions & 0 deletions apps/www/lib/plugins/rehype-optimize-internal-links.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import type { Root } from "hast";
import type { MdxJsxFlowElement, MdxJsxTextElement } from "mdast-util-mdx-jsx";
import { visit } from "unist-util-visit";
import { optimizeInternalLink } from "../utils/url";

type MdxJsxElement = MdxJsxFlowElement | MdxJsxTextElement;

export function rehypeOptimizeInternalLinks() {
return (tree: Root) => {
// handle standard element
visit(tree, "element", (node) => {
if (node.properties && typeof node.properties.href === "string") {
const href = node.properties.href;
const optimized = optimizeInternalLink(href);

if (optimized !== href) {
node.properties.href = optimized;
}
}
});

// handle mdx jsx element
// biome-ignore lint/suspicious/noExplicitAny: generic tree traversal
visit(tree as any, (node: any) => {
if (
node.type === "mdxJsxFlowElement" ||
node.type === "mdxJsxTextElement"
) {
const mdxNode = node as MdxJsxElement;
const hrefAttr = mdxNode.attributes.find(
(attr) =>
attr.type === "mdxJsxAttribute" &&
(attr.name === "href" || attr.name === "url") &&
typeof attr.value === "string",
);

if (
hrefAttr &&
hrefAttr.type === "mdxJsxAttribute" &&
typeof hrefAttr.value === "string"
) {
const href = hrefAttr.value;
const optimized = optimizeInternalLink(href);

if (optimized !== href) {
hrefAttr.value = optimized;
}
}
}
});
};
}
68 changes: 68 additions & 0 deletions apps/www/lib/utils/url.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { describe, expect, it } from "vitest";
import { isExternalUrl, optimizeInternalLink } from "./url";

describe("URL Utilities", () => {
describe("isExternalUrl", () => {
it("identifies internal relative paths", () => {
expect(isExternalUrl("/docs/foo")).toBe(false);
expect(isExternalUrl("#anchor")).toBe(false);
});

it("identifies explicitly internal domains", () => {
expect(isExternalUrl("https://arkenv.js.org/docs")).toBe(false);
expect(isExternalUrl("https://www.arkenv.js.org/docs")).toBe(false);
});

it("identifies external domains", () => {
expect(isExternalUrl("https://google.com")).toBe(true);
expect(isExternalUrl("https://arktype.io")).toBe(true);
});

it("identifies localhost as internal when running in client (jsdom)", () => {
// In JSDOM/Client, if we are on localhost, pointing to localhost is internal.
expect(isExternalUrl("http://localhost:3000")).toBe(false);
});
});

describe("optimizeInternalLink", () => {
it("optimizes production URLs to relative paths", () => {
expect(
optimizeInternalLink("https://arkenv.js.org/docs/quickstart"),
).toBe("/docs/quickstart");
expect(
optimizeInternalLink("https://www.arkenv.js.org/login?foo=bar"),
).toBe("/login?foo=bar");
});

it("preserves hash fragments", () => {
expect(
optimizeInternalLink(
"https://arkenv.js.org/docs/quickstart#installation",
),
).toBe("/docs/quickstart#installation");
});

it("preserves external links", () => {
expect(optimizeInternalLink("https://google.com")).toBe(
"https://google.com",
);
});

it("preserves localhost/IP links (tutorial mode)", () => {
expect(optimizeInternalLink("http://localhost:3000/docs")).toBe(
"http://localhost:3000/docs",
);
expect(optimizeInternalLink("http://127.0.0.1:8080/api")).toBe(
"http://127.0.0.1:8080/api",
);
});

it("preserves already relative paths", () => {
expect(optimizeInternalLink("/docs/foo")).toBe("/docs/foo");
});

it("handles undefined", () => {
expect(optimizeInternalLink(undefined)).toBeUndefined();
});
});
});
75 changes: 59 additions & 16 deletions apps/www/lib/utils/url.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,23 @@
/**
* List of domains that should be treated as internal (same site)
* List of domains that should be treated as "our" application.
* Note: Localhost is intentionally excluded here to prevent "Open localhost:3000" tutorial links
* from being treated as internal navigation on the production site.
*/
export const INTERNAL_DOMAINS = [
"arkenv.js.org",
"www.arkenv.js.org",
"localhost",
"127.0.0.1",
];
export const INTERNAL_DOMAINS = ["arkenv.js.org", "www.arkenv.js.org"];

/**
* Checks if a URL is external (not same domain or not a relative path)
* Checks if a hostname belongs to our internal domains list.
*/
function isInternalDomain(hostname: string): boolean {
return INTERNAL_DOMAINS.some(
// hostname matches exactly or is a subdomain (leading dot prevents matches like "evilarkenv.js.org")
(domain) => hostname === domain || hostname.endsWith(`.${domain}`),
);
}

/**
* Check if a URL is external (not same domain or not a relative path)
*
* This utility is safe to call from both Client and Server components.
*/
export function isExternalUrl(href: string | undefined): boolean {
Expand All @@ -26,15 +34,10 @@ export function isExternalUrl(href: string | undefined): boolean {
? window.location.origin
: "https://arkenv.js.org";
const url = new URL(href, base);

// Check against internal domains list
const hostname = url.hostname.toLowerCase();
if (
INTERNAL_DOMAINS.some(
// hostname matches exactly or is a subdomain (leading dot prevents matches like "evilarkenv.js.org")
(domain) => hostname === domain || hostname.endsWith(`.${domain}`),
)
) {

// check explicit internal domains
if (isInternalDomain(hostname)) {
return false;
}

Expand All @@ -44,9 +47,49 @@ export function isExternalUrl(href: string | undefined): boolean {
}

// During SSR, check if it's an absolute URL with http/https
// Note: We treat localhost as external during SSR if not in the optional allowed list
// to avoid hydration mismatches if possible, but mostly to err on side of caution.
if (hostname === "localhost" || hostname === "127.0.0.1") {
// If we are strictly checking, localhost is external to arkenv.js.org
return true;
}

return url.protocol === "http:" || url.protocol === "https:";
} catch {
// If URL parsing fails, treat as internal
return false;
}
}

/**
* Convert an absolute internal URL to a relative path
* e.g. https://arkenv.js.org/docs/quickstart -> /docs/quickstart
*
* This enables links to the production site (e.g. in READMEs) to:
* 1. Work instantly via client-side routing
* 2. Work correctly on Localhost (keeping you on localhost)
* 3. Work correctly on Deploy Previews (keeping you on the preview)
*/
export function optimizeInternalLink(
href: string | undefined,
): string | undefined {
if (!href) return href;

// Already relative
if (href.startsWith("/") || href.startsWith("#")) return href;

try {
const url = new URL(href);
const hostname = url.hostname.toLowerCase();

// Only optimize if it matches our production domains.
// We purposefully do NOT optimize localhost here, preserving "tutorial style" links.
if (isInternalDomain(hostname)) {
return `${url.pathname}${url.search}${url.hash}`;
}

return href;
} catch {
return href;
}
}
5 changes: 5 additions & 0 deletions apps/www/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,12 +42,14 @@
"react": "19.2.3",
"react-dom": "19.2.3",
"rehype-github-alerts": "4.2.0",
"remark-directive": "^4.0.0",
"remark-gemoji": "8.0.0",
"require-in-the-middle": "8.0.1",
"shiki": "3.20.0",
"tailwind-merge": "3.4.0",
"tailwindcss-animate": "1.0.7",
"twoslash": "0.3.6",
"unist-util-visit": "^5.0.0",
"valibot": "catalog:",
"vite": "7.3.0",
"zod": "catalog:"
Expand All @@ -58,13 +60,16 @@
"@testing-library/jest-dom": "6.9.1",
"@testing-library/react": "16.3.1",
"@testing-library/user-event": "14.6.1",
"@types/hast": "^3.0.4",
"@types/mdast": "^4.0.4",
"@types/mdx": "2.0.13",
"@types/node": "catalog:",
"@types/react": "catalog:",
"@types/react-dom": "catalog:",
"@vitejs/plugin-react": "catalog:",
"concurrently": "catalog:",
"jsdom": "27.3.0",
"mdast-util-mdx-jsx": "^3.2.0",
"next-video": "2.6.0",
"postcss": "8.5.6",
"rimraf": "catalog:",
Expand Down
6 changes: 4 additions & 2 deletions apps/www/source.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ import {
transformerTwoslash,
} from "fumadocs-twoslash";
import { rehypeGithubAlerts } from "rehype-github-alerts";
import remarkDirective from "remark-directive";
import remarkGemoji from "remark-gemoji";
import { rehypeOptimizeInternalLinks } from "./lib/plugins/rehype-optimize-internal-links";

export const docs = defineDocs({
dir: "content/docs",
Expand Down Expand Up @@ -191,8 +193,8 @@ declare global {

export default defineConfig({
mdxOptions: {
rehypePlugins: [rehypeGithubAlerts],
remarkPlugins: [remarkGemoji, remarkNpm],
rehypePlugins: [rehypeGithubAlerts, rehypeOptimizeInternalLinks],
remarkPlugins: [remarkGemoji, remarkNpm, remarkDirective],
rehypeCodeOptions: {
langs: ["ts", "js", "json", "bash", "dotenv"],
themes: {
Expand Down
9 changes: 5 additions & 4 deletions packages/arkenv/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,15 +25,15 @@

### [Read the docs →](https://arkenv.js.org/docs/arkenv)

<sup>The best way to learn ArkEnv, with interactive code blocks and type hints.</sup>

<br/>
<br/>

## Introduction

> [!TIP]
> 📖 **Reading this on GitHub?** Check out [this page in our docs](https://arkenv.js.org/docs/arkenv) to hover over code blocks and get type hints!
## Introduction

ArkEnv is an environment variable validator for modern JavaScript runtimes.
ArkEnv is an environment variable validator for modern JavaScript runtimes.
It lets you create a ready-to-use, typesafe environment variable object:

```ts twoslash
Expand Down Expand Up @@ -77,6 +77,7 @@ ArkEnvError: Errors found while validating environment variables
* Compatible with any Standard Schema validator (Zod, Valibot, etc.)
* Native support for ArkType, TypeScript’s 1:1 validator


> See how ArkEnv compares to alternatives like T3 Env, znv, and envalid in the [comparison cheatsheet](https://arkenv.js.org/docs/arkenv/comparison#comparison-cheatsheet).

## Installation
Expand Down
Loading
Loading