mirror of
https://github.com/Kaelio/ktx.git
synced 2026-07-22 11:51:01 +02:00
feat(docs): add Fumadocs site workspace
This commit is contained in:
parent
cb9ab25456
commit
572d515db0
28 changed files with 3979 additions and 84 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -29,6 +29,8 @@ htmlcov/
|
||||||
|
|
||||||
# Node
|
# Node
|
||||||
node_modules/
|
node_modules/
|
||||||
|
.next/
|
||||||
|
.source/
|
||||||
.npm/
|
.npm/
|
||||||
.pnpm-store/
|
.pnpm-store/
|
||||||
*.tsbuildinfo
|
*.tsbuildinfo
|
||||||
|
|
@ -49,6 +51,7 @@ yarn-error.log*
|
||||||
# Private local agent overlays
|
# Private local agent overlays
|
||||||
.agents/
|
.agents/
|
||||||
.claude/
|
.claude/
|
||||||
|
.superpowers/
|
||||||
|
|
||||||
# Editors and OS files
|
# Editors and OS files
|
||||||
.idea/
|
.idea/
|
||||||
|
|
|
||||||
7
docs/app/(home)/layout.tsx
Normal file
7
docs/app/(home)/layout.tsx
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
import { HomeLayout } from "fumadocs-ui/layouts/home";
|
||||||
|
import type { ReactNode } from "react";
|
||||||
|
import { baseOptions } from "@/app/layout.config";
|
||||||
|
|
||||||
|
export default function Layout({ children }: { children: ReactNode }) {
|
||||||
|
return <HomeLayout {...baseOptions}>{children}</HomeLayout>;
|
||||||
|
}
|
||||||
5
docs/app/(home)/page.tsx
Normal file
5
docs/app/(home)/page.tsx
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
import { redirect } from "next/navigation";
|
||||||
|
|
||||||
|
export default function HomePage() {
|
||||||
|
redirect("/docs/getting-started/introduction");
|
||||||
|
}
|
||||||
12
docs/app/docs/layout.tsx
Normal file
12
docs/app/docs/layout.tsx
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
import { source } from "@/lib/source";
|
||||||
|
import { DocsLayout } from "fumadocs-ui/layouts/docs";
|
||||||
|
import type { ReactNode } from "react";
|
||||||
|
import { baseOptions } from "@/app/layout.config";
|
||||||
|
|
||||||
|
export default function Layout({ children }: { children: ReactNode }) {
|
||||||
|
return (
|
||||||
|
<DocsLayout tree={source.pageTree} {...baseOptions}>
|
||||||
|
{children}
|
||||||
|
</DocsLayout>
|
||||||
|
);
|
||||||
|
}
|
||||||
10
docs/app/layout.config.tsx
Normal file
10
docs/app/layout.config.tsx
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
import type { BaseLayoutProps } from "fumadocs-ui/layouts/shared";
|
||||||
|
import { Logo } from "@/components/logo";
|
||||||
|
|
||||||
|
export const baseOptions: BaseLayoutProps = {
|
||||||
|
nav: {
|
||||||
|
title: <Logo />,
|
||||||
|
transparentMode: "top",
|
||||||
|
},
|
||||||
|
githubUrl: "https://github.com/kaelio/ktx",
|
||||||
|
};
|
||||||
44
docs/app/layout.tsx
Normal file
44
docs/app/layout.tsx
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
import "./global.css";
|
||||||
|
import { RootProvider } from "fumadocs-ui/provider";
|
||||||
|
import { Outfit, Inter, Geist_Mono } from "next/font/google";
|
||||||
|
import type { ReactNode } from "react";
|
||||||
|
import type { Metadata } from "next";
|
||||||
|
|
||||||
|
const outfit = Outfit({
|
||||||
|
variable: "--font-outfit",
|
||||||
|
subsets: ["latin"],
|
||||||
|
weight: ["400", "500", "600", "700", "800"],
|
||||||
|
});
|
||||||
|
|
||||||
|
const inter = Inter({
|
||||||
|
variable: "--font-inter",
|
||||||
|
subsets: ["latin"],
|
||||||
|
});
|
||||||
|
|
||||||
|
const geistMono = Geist_Mono({
|
||||||
|
variable: "--font-geist-mono",
|
||||||
|
subsets: ["latin"],
|
||||||
|
});
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: {
|
||||||
|
template: "%s | KTX Docs",
|
||||||
|
default: "KTX Docs",
|
||||||
|
},
|
||||||
|
description:
|
||||||
|
"Open-source context infrastructure that makes agentic analytics reliable.",
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function RootLayout({ children }: { children: ReactNode }) {
|
||||||
|
return (
|
||||||
|
<html
|
||||||
|
lang="en"
|
||||||
|
className={`${outfit.variable} ${inter.variable} ${geistMono.variable}`}
|
||||||
|
suppressHydrationWarning
|
||||||
|
>
|
||||||
|
<body>
|
||||||
|
<RootProvider>{children}</RootProvider>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
);
|
||||||
|
}
|
||||||
56
docs/components/logo.tsx
Normal file
56
docs/components/logo.tsx
Normal file
|
|
@ -0,0 +1,56 @@
|
||||||
|
export function Logo() {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center gap-2 group">
|
||||||
|
<div className="relative flex items-center justify-center transition-transform duration-300 ease-out group-hover:rotate-[-4deg]">
|
||||||
|
<svg
|
||||||
|
width="22"
|
||||||
|
height="22"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="ktx-grad-a" x1="0" y1="0" x2="24" y2="24" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop offset="0%" stopColor="var(--color-fd-primary)" />
|
||||||
|
<stop offset="100%" stopColor="var(--color-fd-primary)" stopOpacity="0.55" />
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="ktx-grad-b" x1="0" y1="0" x2="24" y2="24" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop offset="0%" stopColor="var(--color-fd-primary)" stopOpacity="0.85" />
|
||||||
|
<stop offset="100%" stopColor="var(--color-fd-primary)" stopOpacity="0.4" />
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
{/* Bottom layer */}
|
||||||
|
<path
|
||||||
|
d="M3 17 L12 21.5 L21 17 L12 12.5 Z"
|
||||||
|
fill="url(#ktx-grad-a)"
|
||||||
|
opacity="0.4"
|
||||||
|
/>
|
||||||
|
{/* Middle layer */}
|
||||||
|
<path
|
||||||
|
d="M3 12 L12 16.5 L21 12 L12 7.5 Z"
|
||||||
|
fill="url(#ktx-grad-b)"
|
||||||
|
opacity="0.7"
|
||||||
|
/>
|
||||||
|
{/* Top layer */}
|
||||||
|
<path
|
||||||
|
d="M3 7 L12 11.5 L21 7 L12 2.5 Z"
|
||||||
|
fill="var(--color-fd-primary)"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<span
|
||||||
|
className="text-[15px] font-semibold text-fd-foreground tracking-tight"
|
||||||
|
style={{ fontFamily: "var(--font-display), var(--font-sans), sans-serif" }}
|
||||||
|
>
|
||||||
|
KTX
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
className="text-[13px] font-medium text-fd-muted-foreground/80 tracking-tight border-l border-fd-border pl-2 ml-0.5"
|
||||||
|
style={{ fontFamily: "var(--font-display), var(--font-sans), sans-serif" }}
|
||||||
|
>
|
||||||
|
Docs
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
58
docs/components/scroll-reveal.tsx
Normal file
58
docs/components/scroll-reveal.tsx
Normal file
|
|
@ -0,0 +1,58 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useEffect, useRef, type ReactNode } from "react";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
children: ReactNode;
|
||||||
|
className?: string;
|
||||||
|
stagger?: boolean;
|
||||||
|
threshold?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function ScrollReveal({
|
||||||
|
children,
|
||||||
|
className = "",
|
||||||
|
stagger = false,
|
||||||
|
threshold = 0.1,
|
||||||
|
}: Props) {
|
||||||
|
const ref = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const node = ref.current;
|
||||||
|
if (!node) return;
|
||||||
|
|
||||||
|
const observer = new IntersectionObserver(
|
||||||
|
(entries) => {
|
||||||
|
for (const entry of entries) {
|
||||||
|
if (entry.isIntersecting) {
|
||||||
|
entry.target.classList.add("visible");
|
||||||
|
if (stagger) {
|
||||||
|
entry.target.querySelectorAll(".rv").forEach((el) => {
|
||||||
|
el.classList.add("visible");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
observer.unobserve(entry.target);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ threshold, rootMargin: "0px 0px -40px 0px" }
|
||||||
|
);
|
||||||
|
|
||||||
|
if (stagger) {
|
||||||
|
observer.observe(node);
|
||||||
|
} else {
|
||||||
|
node.querySelectorAll(".rv").forEach((el) => observer.observe(el));
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => observer.disconnect();
|
||||||
|
}, [stagger, threshold]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
className={`${stagger ? "rv rv-stagger" : ""} ${className}`}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
56
docs/components/terminal-preview.tsx
Normal file
56
docs/components/terminal-preview.tsx
Normal file
|
|
@ -0,0 +1,56 @@
|
||||||
|
export function TerminalPreview() {
|
||||||
|
return (
|
||||||
|
<div className="terminal-frame sheen w-full max-w-[560px]">
|
||||||
|
<div className="terminal-head">
|
||||||
|
<span className="terminal-dot" style={{ background: "#ff5f57" }} />
|
||||||
|
<span className="terminal-dot" style={{ background: "#febc2e" }} />
|
||||||
|
<span className="terminal-dot" style={{ background: "#28c840" }} />
|
||||||
|
<span className="ml-2 text-[11px] text-zinc-500 font-medium tracking-wide">
|
||||||
|
~/analytics
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="terminal-body">
|
||||||
|
<div>
|
||||||
|
<span className="term-prompt">$</span>{" "}
|
||||||
|
<span className="term-cmd">ktx setup</span>
|
||||||
|
</div>
|
||||||
|
<div className="h-2" />
|
||||||
|
<div className="term-dim">◆ Welcome to KTX setup</div>
|
||||||
|
<div className="term-dim">│</div>
|
||||||
|
<div>
|
||||||
|
<span className="term-dim">◇</span>{" "}
|
||||||
|
<span className="term-key">LLM</span>{" "}
|
||||||
|
<span className="term-ok">✓ claude-sonnet-4-6</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="term-dim">◇</span>{" "}
|
||||||
|
<span className="term-key">Embeddings</span>{" "}
|
||||||
|
<span className="term-ok">✓ openai · text-embedding-3-small</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="term-dim">◇</span>{" "}
|
||||||
|
<span className="term-key">Database</span>{" "}
|
||||||
|
<span className="term-ok">✓ postgres-warehouse · 42 tables</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="term-dim">◇</span>{" "}
|
||||||
|
<span className="term-key">Sources</span>{" "}
|
||||||
|
<span className="term-ok">✓ dbt-main · 218 models</span>
|
||||||
|
</div>
|
||||||
|
<div className="h-2" />
|
||||||
|
<div className="term-info">◐ Building context for agents…</div>
|
||||||
|
<div className="pl-3 text-[12px] term-dim">
|
||||||
|
enriching schema · detecting relationships · ingesting dbt
|
||||||
|
</div>
|
||||||
|
<div className="h-2" />
|
||||||
|
<div className="term-ok">✓ KTX context is ready for agents.</div>
|
||||||
|
<div className="h-2" />
|
||||||
|
<div>
|
||||||
|
<span className="term-prompt">$</span>{" "}
|
||||||
|
<span className="term-cmd">ktx serve</span>
|
||||||
|
<span className="term-cursor ml-1" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -88,16 +88,7 @@ The fixture runs in multiple modes to isolate the contribution of each pipeline
|
||||||
|
|
||||||
## Results
|
## Results
|
||||||
|
|
||||||
On the default configuration (`accept_threshold: 0.85`, `review_threshold: 0.55`, `llm_proposals: false`):
|
Results for the default configuration will be added after the benchmark run is finalized.
|
||||||
|
|
||||||
```text
|
|
||||||
Accepted: 9
|
|
||||||
Review: 0
|
|
||||||
Rejected: 0
|
|
||||||
Skipped: 0
|
|
||||||
```
|
|
||||||
|
|
||||||
All 9 ground-truth relationships are detected and accepted with no false positives and no relationships left in the review tier. Primary keys are correctly inferred for all 6 tables.
|
|
||||||
|
|
||||||
## Reproducing the benchmark
|
## Reproducing the benchmark
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"title": "Benchmarks",
|
"title": "Benchmarks",
|
||||||
"defaultOpen": false,
|
"defaultOpen": true,
|
||||||
"pages": ["link-detection"]
|
"pages": ["link-detection"]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"title": "CLI Reference",
|
"title": "CLI Reference",
|
||||||
"defaultOpen": false,
|
"defaultOpen": true,
|
||||||
"pages": [
|
"pages": [
|
||||||
"ktx-setup",
|
"ktx-setup",
|
||||||
"ktx-connection",
|
"ktx-connection",
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"title": "Community",
|
"title": "Community",
|
||||||
"defaultOpen": false,
|
"defaultOpen": true,
|
||||||
"pages": ["contributing"]
|
"pages": ["contributing"]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"title": "Concepts",
|
"title": "Concepts",
|
||||||
"defaultOpen": false,
|
"defaultOpen": true,
|
||||||
"pages": ["the-context-layer", "context-as-code"]
|
"pages": ["the-context-layer", "context-as-code"]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"title": "Getting Started",
|
"title": "Getting Started",
|
||||||
"defaultOpen": false,
|
"defaultOpen": true,
|
||||||
"pages": ["introduction", "quickstart"]
|
"pages": ["introduction", "quickstart"]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"title": "Guides",
|
"title": "Guides",
|
||||||
"defaultOpen": false,
|
"defaultOpen": true,
|
||||||
"pages": ["building-context", "writing-context", "serving-agents"]
|
"pages": ["building-context", "writing-context", "serving-agents"]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"title": "Integrations",
|
"title": "Integrations",
|
||||||
"defaultOpen": false,
|
"defaultOpen": true,
|
||||||
"pages": ["primary-sources", "context-sources", "agent-clients"]
|
"pages": ["primary-sources", "context-sources", "agent-clients"]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
7
docs/lib/source.ts
Normal file
7
docs/lib/source.ts
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
import { docs } from "@/.source";
|
||||||
|
import { loader } from "fumadocs-core/source";
|
||||||
|
|
||||||
|
export const source = loader({
|
||||||
|
source: docs.toFumadocsSource(),
|
||||||
|
baseUrl: "/docs",
|
||||||
|
});
|
||||||
6
docs/next-env.d.ts
vendored
Normal file
6
docs/next-env.d.ts
vendored
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
/// <reference types="next" />
|
||||||
|
/// <reference types="next/image-types/global" />
|
||||||
|
/// <reference path="./.next/types/routes.d.ts" />
|
||||||
|
|
||||||
|
// NOTE: This file should not be edited
|
||||||
|
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
||||||
8
docs/next.config.mjs
Normal file
8
docs/next.config.mjs
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
import { createMDX } from "fumadocs-mdx/next";
|
||||||
|
|
||||||
|
const withMDX = createMDX();
|
||||||
|
|
||||||
|
/** @type {import('next').NextConfig} */
|
||||||
|
const config = {};
|
||||||
|
|
||||||
|
export default withMDX(config);
|
||||||
26
docs/package.json
Normal file
26
docs/package.json
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
{
|
||||||
|
"name": "ktx-docs",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "next dev",
|
||||||
|
"build": "next build",
|
||||||
|
"start": "next start"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"fumadocs-core": "15.7.13",
|
||||||
|
"fumadocs-mdx": "11.10.1",
|
||||||
|
"fumadocs-ui": "15.7.13",
|
||||||
|
"next": "^15",
|
||||||
|
"react": "19.2.6",
|
||||||
|
"react-dom": "19.2.6"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/react": "^19",
|
||||||
|
"@types/react-dom": "^19",
|
||||||
|
"typescript": "^5.9",
|
||||||
|
"@tailwindcss/postcss": "^4",
|
||||||
|
"tailwindcss": "^4"
|
||||||
|
}
|
||||||
|
}
|
||||||
7
docs/postcss.config.mjs
Normal file
7
docs/postcss.config.mjs
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
const config = {
|
||||||
|
plugins: {
|
||||||
|
"@tailwindcss/postcss": {},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default config;
|
||||||
9
docs/source.config.ts
Normal file
9
docs/source.config.ts
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
import { defineDocs, defineConfig } from "fumadocs-mdx/config";
|
||||||
|
|
||||||
|
export const docs = defineDocs({
|
||||||
|
dir: "content/docs",
|
||||||
|
});
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
mdxOptions: {},
|
||||||
|
});
|
||||||
41
docs/tsconfig.json
Normal file
41
docs/tsconfig.json
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ESNext",
|
||||||
|
"lib": [
|
||||||
|
"dom",
|
||||||
|
"dom.iterable",
|
||||||
|
"esnext"
|
||||||
|
],
|
||||||
|
"allowJs": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"strict": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"jsx": "preserve",
|
||||||
|
"incremental": true,
|
||||||
|
"plugins": [
|
||||||
|
{
|
||||||
|
"name": "next"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"paths": {
|
||||||
|
"@/*": [
|
||||||
|
"./*"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"**/*.ts",
|
||||||
|
"**/*.tsx",
|
||||||
|
".source/**/*.ts",
|
||||||
|
"next-env.d.ts",
|
||||||
|
".next/types/**/*.ts"
|
||||||
|
],
|
||||||
|
"exclude": [
|
||||||
|
"node_modules"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -11,15 +11,6 @@ Run from the KTX workspace root:
|
||||||
pnpm run relationships:verify-orbit
|
pnpm run relationships:verify-orbit
|
||||||
```
|
```
|
||||||
|
|
||||||
Expected relationship summary:
|
|
||||||
|
|
||||||
```text
|
|
||||||
Accepted: 9
|
|
||||||
Review: 0
|
|
||||||
Rejected: 0
|
|
||||||
Skipped: 0
|
|
||||||
```
|
|
||||||
|
|
||||||
The command refreshes:
|
The command refreshes:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
|
|
|
||||||
3663
pnpm-lock.yaml
generated
3663
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load diff
|
|
@ -1,5 +1,6 @@
|
||||||
packages:
|
packages:
|
||||||
- "packages/*"
|
- "packages/*"
|
||||||
|
- "docs"
|
||||||
|
|
||||||
overrides:
|
overrides:
|
||||||
"@types/node": ^24.3.0
|
"@types/node": ^24.3.0
|
||||||
|
|
|
||||||
10
website/vercel.json
Normal file
10
website/vercel.json
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
{
|
||||||
|
"redirects": [
|
||||||
|
{
|
||||||
|
"source": "/:path*",
|
||||||
|
"has": [{ "type": "host", "value": "ktx.sh" }],
|
||||||
|
"destination": "https://docs.ktx.sh/:path*",
|
||||||
|
"permanent": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue