Skip to content

Commit 0517859

Browse files
committed
-
1 parent 6e42561 commit 0517859

File tree

9 files changed

+531
-35
lines changed

9 files changed

+531
-35
lines changed

website-v3/astro.config.mjs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,16 @@
11
import { defineConfig } from 'astro/config';
22
import tailwind from '@astrojs/tailwind';
3+
import node from '@astrojs/node';
4+
5+
// https://astro.build/config
36

47
// https://astro.build/config
58

69
// https://astro.build/config
710
export default defineConfig({
11+
output: 'server',
12+
adapter: node({
13+
mode: 'standalone',
14+
}),
815
integrations: [tailwind()],
916
});

website-v3/package-lock.json

Lines changed: 237 additions & 33 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

website-v3/package.json

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,15 @@
1111
"astro": "astro"
1212
},
1313
"dependencies": {
14+
"@astrojs/node": "^3.1.0",
1415
"@astrojs/tailwind": "^2.1.3",
15-
"astro": "^1.6.13",
16-
"tailwindcss": "^3.2.4"
16+
"astro": "^1.6.14",
17+
"lodash.get": "^4.4.2",
18+
"querystring": "^0.2.1",
19+
"tailwindcss": "^3.2.4",
20+
"zod": "^3.19.1"
21+
},
22+
"devDependencies": {
23+
"typescript": "^4.9.4"
1724
}
1825
}

website-v3/src/bundle.ts

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
import { z } from 'zod';
2+
import querystring from 'querystring';
3+
4+
const $SidebarItem = z.tuple([z.string(), z.string()]);
5+
6+
const $BundleConfig = z.object({
7+
name: z.string(),
8+
logo: z.string(),
9+
logoDark: z.string(),
10+
favicon: z.string(),
11+
socialPreview: z.string(),
12+
twitter: z.string(),
13+
noindex: z.boolean(),
14+
theme: z.string(),
15+
docsearch: z
16+
.object({
17+
appId: z.string(),
18+
apiKey: z.string(),
19+
indexName: z.string(),
20+
})
21+
.optional(),
22+
sidebar: z.array(z.tuple([z.string(), z.union([z.string(), z.array($SidebarItem)])])).optional(),
23+
headerDepth: z.number(),
24+
variables: z.record(z.any()).optional(),
25+
googleTagManager: z.string(),
26+
googleAnalytics: z.string(),
27+
zoomImages: z.boolean(),
28+
experimentalCodehike: z.boolean(),
29+
experimentalMath: z.boolean(),
30+
automaticallyInferNextPrevious: z.boolean(),
31+
plausibleAnalytics: z.boolean().optional(),
32+
});
33+
34+
const $GetBundleRequest = z.object({
35+
owner: z.string(),
36+
repository: z.string(),
37+
ref: z.string().optional(),
38+
path: z.string().optional(),
39+
});
40+
41+
const $GetBundleResponseError = z.object({
42+
statusCode: z.number(),
43+
message: z.string(),
44+
reason: z.enum([
45+
'REPO_NOT_FOUND',
46+
'BUNDLE_ERROR',
47+
'REF_NOT_FOUND',
48+
'BAD_CONFIG',
49+
'MISSING_CONFIG',
50+
'FILE_NOT_FOUND',
51+
]),
52+
});
53+
54+
const $GetBundleResponseSuccess = z.object({
55+
code: z.string(),
56+
config: $BundleConfig,
57+
frontmatter: z.record(z.string()),
58+
headings: z
59+
.array(
60+
z.object({
61+
id: z.string(),
62+
title: z.string(),
63+
rank: z.number().nullable(),
64+
}),
65+
)
66+
.nullable(),
67+
baseBranch: z.string().nullable(),
68+
path: z.string().nullable(),
69+
repositoryFound: z.boolean(),
70+
source: z.object({
71+
type: z.enum(['PR', 'branch', 'commit']),
72+
owner: z.string(),
73+
repository: z.string(),
74+
ref: z.string(),
75+
}),
76+
});
77+
78+
const $GetBundleResponse = z.union([$GetBundleResponseError, $GetBundleResponseSuccess]);
79+
80+
export type GetBundleRequest = z.infer<typeof $GetBundleRequest>;
81+
export type GetBundleResponse = z.infer<typeof $GetBundleResponse>;
82+
export type GetBundleResponseError = z.infer<typeof $GetBundleResponseError>;
83+
export type GetBundleResponseSuccess = z.infer<typeof $GetBundleResponseSuccess>;
84+
85+
export type BundleConfig = z.infer<typeof $BundleConfig>;
86+
87+
export async function getBundle(options: GetBundleRequest): Promise<GetBundleResponse> {
88+
// Validate the input
89+
$GetBundleRequest.parse(options);
90+
91+
if (import.meta.env.NODE_ENV == 'production' && !import.meta.env.API_PASSWORD) {
92+
throw new Error('Please provide API_PASSWORD env variable');
93+
}
94+
95+
const endpoint = getEndpoint(options);
96+
97+
const response = await fetch(endpoint, {
98+
headers: new Headers({
99+
Authorization:
100+
'Bearer ' + Buffer.from(`admin:${import.meta.env.API_PASSWORD}`).toString('base64'),
101+
}),
102+
});
103+
104+
// These are valid JSON responses from the server.
105+
if ([200, 404, 400].includes(response.status)) {
106+
return $GetBundleResponse.parse(await response.json());
107+
}
108+
109+
throw new Error(`Failed to fetch bundle for "${endpoint}". HTTP Status: "${response.status}".`);
110+
}
111+
112+
function getEndpoint(options: GetBundleRequest): string {
113+
const params: Record<string, string> = {
114+
owner: options.owner,
115+
repository: options.repository,
116+
};
117+
118+
if (options.path) params['path'] = options.path;
119+
if (options.ref) params['ref'] = options.ref;
120+
121+
const base =
122+
import.meta.env.BUNDLER_URL || import.meta.env.PROD
123+
? `https://api.docs.page`
124+
: 'http://localhost:8000';
125+
126+
if (import.meta.env.K_REVISION) params['_k_revision'] = import.meta.env.K_REVISION;
127+
128+
// TODO: querystring is deprecated
129+
return `${base}/bundle?${querystring.stringify(params)}`;
130+
}

website-v3/src/layouts/Root.astro

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ const {
3333
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Fira+Code:wght@500&display=block">
3434
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;700&display=block">
3535

36+
<slot name="head" />
37+
3638
<link rel="shortcut icon" href="/favicons/favicon.ico" />
3739
<meta name="msapplication-TileColor" content="#333333" />
3840
<meta name="msapplication-config" content="/favicons/browserconfig.xml" />

website-v3/src/pages/404.astro

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
<div>404 Page</div>
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
---
2+
import Root from "@layouts/Root.astro";
3+
import { getBundle } from "src/bundle";
4+
import type { Context } from "src/types";
5+
import type { GetBundleResponse } from "src/bundle";
6+
import { isExternalLink, replaceMoustacheVariables } from "src/utils";
7+
import domains from '../../../../../domains.json';
8+
9+
let { owner, repository } = Astro.params;
10+
let ref: string | undefined;
11+
12+
if (!owner || !repository) {
13+
return Astro.redirect('/404');
14+
}
15+
16+
// Check if the repo includes a ref (invertase/foo~bar)
17+
if (repository.includes('~')) {
18+
[repository, ref] = repository.split('~');
19+
}
20+
21+
let bundle: GetBundleResponse;
22+
23+
try {
24+
bundle = await getBundle({
25+
owner,
26+
repository,
27+
ref,
28+
path: Astro.params.slug!,
29+
});
30+
} catch (e) {
31+
console.error(e);
32+
}
33+
34+
if ('statusCode' in bundle!) {
35+
// TODO handle 404
36+
return Astro.redirect('/404');
37+
}
38+
39+
// Handle a frontmatter redirect
40+
const redirect = bundle!.frontmatter.redirect;
41+
if (redirect && isExternalLink(redirect)) {
42+
return Astro.redirect(redirect);
43+
} else if (redirect) {
44+
return Astro.redirect(`/${owner}/${repository}${redirect}`)
45+
}
46+
47+
const context: Context = {
48+
owner,
49+
repository,
50+
config: bundle!.config,
51+
frontmatter: bundle!.frontmatter,
52+
code: replaceMoustacheVariables(bundle!.config.variables ?? {}, bundle!.code),
53+
headings: bundle!.headings,
54+
domain: domains.find(([, repository]) => repository === `${owner}/${repository}`)?.at(0),
55+
};
56+
---
57+
58+
<Root>
59+
{context.config.googleAnalytics && (
60+
<script
61+
slot="head"
62+
async
63+
src={`https://www.googletagmanager.com/gtag/js?id=${context.config.googleAnalytics}`}
64+
/>
65+
)}
66+
{context.config.googleAnalytics && (
67+
<script slot="head" type="text/javascript">
68+
{`
69+
window.dataLayer = window.dataLayer || [];
70+
function gtag(){dataLayer.push(arguments);}
71+
gtag('js', new Date());
72+
73+
gtag('config', '${config.googleAnalytics}');
74+
`}
75+
</script>
76+
)}
77+
{context.config.googleTagManager && (
78+
<script slot="head" type="text/javascript">
79+
{`
80+
(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
81+
new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
82+
j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
83+
'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
84+
})(window,document,'script','dataLayer','${config.googleTagManager}');
85+
`}
86+
</script>
87+
)}
88+
{context.config.plausibleAnalytics && context.domain && (
89+
<script slot="head" defer data-domain={domain} src="https://plausible.io/js/plausible.js"></script>
90+
)}
91+
<!-- TODO: <link rel="icon" href={favicon} /> -->
92+
{context.config.experimentalMath && (
93+
<link
94+
slot="head"
95+
rel="stylesheet"
96+
href="https://cdn.jsdelivr.net/npm/[email protected]/dist/katex.min.css"
97+
/>
98+
)}
99+
<!-- TODO: {config.experimentalCodehike && (
100+
<link slot="head" data-testid="codehike-styles" rel="stylesheet" href={codeHikeStyles} />
101+
)} -->
102+
</Root>

website-v3/src/types.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import type { BundleConfig, GetBundleResponseSuccess } from './bundle';
2+
3+
export type Context = {
4+
owner: string;
5+
repository: string;
6+
config: BundleConfig;
7+
frontmatter: GetBundleResponseSuccess['frontmatter'];
8+
code: string;
9+
headings: GetBundleResponseSuccess['headings'];
10+
domain: string | undefined;
11+
};

website-v3/src/utils.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import get from 'lodash.get';
2+
3+
const VARIABLE_REGEX = /{{\s([a-zA-Z0-9_.]*)\s}}/gm;
4+
5+
// Removes any trailing slash from string
6+
export function removeTrailingSlash(value: string) {
7+
return value.replace(/\/$/, '');
8+
}
9+
10+
export function isExternalLink(href: string) {
11+
return href.startsWith('http');
12+
}
13+
14+
export function isHashLink(href: string): boolean {
15+
return href.startsWith('#');
16+
}
17+
18+
// Replaces an object of variables with their moustache values in a string
19+
export function replaceMoustacheVariables(variables: Record<string, string>, value: string) {
20+
let output = value;
21+
let m: RegExpExecArray | null;
22+
23+
while ((m = VARIABLE_REGEX.exec(value)) !== null) {
24+
// This is necessary to avoid infinite loops with zero-width matches
25+
if (m.index === VARIABLE_REGEX.lastIndex) {
26+
VARIABLE_REGEX.lastIndex++;
27+
}
28+
output = output.replace(m[0], get(variables, m[1], m[0]));
29+
}
30+
31+
return output;
32+
}

0 commit comments

Comments
 (0)