zudoku 0.93.0 → 0.93.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/cli.js +1249 -767
- package/dist/declarations/app/env.d.ts +2 -26
- package/dist/declarations/app/wrapProtectedRoutes.d.ts +3 -4
- package/dist/declarations/lib/components/Heading.d.ts +1 -1
- package/dist/declarations/lib/plugins/blog/BlogComponents.d.ts +18 -0
- package/dist/declarations/lib/plugins/blog/BlogListPage.d.ts +8 -0
- package/dist/declarations/lib/plugins/blog/BlogPostPage.d.ts +9 -0
- package/dist/declarations/lib/plugins/blog/BlogTagsPage.d.ts +5 -0
- package/dist/declarations/lib/plugins/blog/index.d.ts +4 -0
- package/dist/declarations/lib/plugins/blog/options.d.ts +61 -0
- package/dist/declarations/lib/plugins/openapi/ParamInfos.d.ts +2 -1
- package/dist/declarations/lib/plugins/openapi/components/EnumValues.d.ts +2 -1
- package/dist/declarations/lib/plugins/openapi/schema/SchemaPropertyItem.d.ts +1 -1
- package/dist/declarations/lib/plugins/openapi/schema/SchemaView.d.ts +2 -2
- package/dist/declarations/lib/plugins/openapi/schema/utils.d.ts +7 -0
- package/dist/declarations/lib/ui/Form.d.ts +1 -1
- package/dist/declarations/vite/zuplo.d.ts +13 -0
- package/docs/configuration/api-catalog.md +16 -0
- package/docs/configuration/blog.md +214 -0
- package/package.json +48 -44
- package/src/app/entry.server.tsx +1 -5
- package/src/app/env.ts +30 -24
- package/src/app/main.tsx +2 -4
- package/src/app/wrapProtectedRoutes.ts +10 -14
- package/src/lib/oas/graphql/index.ts +3 -1
- package/src/lib/plugins/blog/BlogComponents.tsx +138 -0
- package/src/lib/plugins/blog/BlogListPage.tsx +150 -0
- package/src/lib/plugins/blog/BlogPostPage.tsx +162 -0
- package/src/lib/plugins/blog/BlogTagsPage.tsx +69 -0
- package/src/lib/plugins/blog/index.tsx +163 -0
- package/src/lib/plugins/blog/options.ts +129 -0
- package/src/lib/plugins/blog/virtual.d.ts +7 -0
- package/src/lib/plugins/openapi/OperationListItem.tsx +7 -1
- package/src/lib/plugins/openapi/ParamInfos.tsx +26 -5
- package/src/lib/plugins/openapi/ParameterListItem.tsx +19 -4
- package/src/lib/plugins/openapi/Sidecar.tsx +13 -2
- package/src/lib/plugins/openapi/components/EnumValues.tsx +27 -8
- package/src/lib/plugins/openapi/components/ResponseContent.tsx +5 -3
- package/src/lib/plugins/openapi/playground/createUrl.ts +30 -13
- package/src/lib/plugins/openapi/schema/SchemaPropertyItem.tsx +15 -3
- package/src/lib/plugins/openapi/schema/SchemaView.tsx +45 -8
- package/src/lib/plugins/openapi/schema/utils.ts +90 -1
- package/src/lib/plugins/openapi/util/generateSchemaExample.ts +5 -1
- package/src/vite/api/SchemaManager.ts +2 -9
- package/src/vite/api/upgradeOpenApi.ts +104 -0
- package/src/vite/config.ts +5 -6
- package/src/vite/plugin-api-keys.ts +1 -5
- package/src/vite/plugin-blog.ts +648 -0
- package/src/vite/plugin-markdown-export.ts +2 -2
- package/src/vite/plugin-mdx.ts +5 -0
- package/src/vite/plugin.ts +2 -0
- package/src/vite-env.d.ts +1 -0
- package/dist/declarations/app/ZuploBuildConfig.d.ts +0 -47
- package/src/app/ZuploBuildConfig.ts +0 -36
|
@@ -1,30 +1,6 @@
|
|
|
1
1
|
export declare const ZuploEnv: {
|
|
2
2
|
readonly isZuplo: boolean;
|
|
3
3
|
readonly serverUrl: string | undefined;
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
devPortalZuploBranding: boolean;
|
|
7
|
-
numberOfProjects: number;
|
|
8
|
-
numberOfUsers: number;
|
|
9
|
-
egressGbPerMonth: number;
|
|
10
|
-
requestsPerMonth: number;
|
|
11
|
-
numberOfApiKeys: number;
|
|
12
|
-
customDomains: number;
|
|
13
|
-
advancedAnalyticsEnabled: boolean;
|
|
14
|
-
devPortalAnalyticsEnabled: boolean;
|
|
15
|
-
premiumSupport: boolean;
|
|
16
|
-
emergencyPhoneSupport: boolean;
|
|
17
|
-
rbacEnabled: boolean;
|
|
18
|
-
onPremEnabled: boolean;
|
|
19
|
-
largeBuildRunners: boolean;
|
|
20
|
-
};
|
|
21
|
-
deploymentName: string;
|
|
22
|
-
environmentType?: string | undefined;
|
|
23
|
-
deploymentUrl?: string | undefined;
|
|
24
|
-
projectId?: string | undefined;
|
|
25
|
-
projectType?: string | undefined;
|
|
26
|
-
sourceType?: string | undefined;
|
|
27
|
-
accountName?: string | undefined;
|
|
28
|
-
projectName?: string | undefined;
|
|
29
|
-
} | undefined;
|
|
4
|
+
readonly deploymentName: string | undefined;
|
|
5
|
+
readonly devPortalZuploBranding: boolean | undefined;
|
|
30
6
|
};
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import type { RouteObject } from "react-router";
|
|
2
2
|
import type { ProtectedRoutesInput } from "../config/validators/ProtectedRoutesSchema.js";
|
|
3
|
-
export declare const wrapProtectedRoutes: (routes: RouteObject[], protectedRoutes: ProtectedRoutesInput, isAuthenticated: boolean
|
|
4
|
-
export declare const wrapProtectedRoutesForRender: (routes: RouteObject[], protectedRoutes: ProtectedRoutesInput, { isAuthenticated, bypassProtection,
|
|
3
|
+
export declare const wrapProtectedRoutes: (routes: RouteObject[], protectedRoutes: ProtectedRoutesInput, isAuthenticated: boolean) => RouteObject[];
|
|
4
|
+
export declare const wrapProtectedRoutesForRender: (routes: RouteObject[], protectedRoutes: ProtectedRoutesInput, { isAuthenticated, bypassProtection, }: {
|
|
5
5
|
isAuthenticated: boolean;
|
|
6
6
|
bypassProtection?: boolean;
|
|
7
|
-
basePath?: string;
|
|
8
7
|
}) => RouteObject[];
|
|
9
|
-
export declare const warnInlineProtectedRoutes: (routes: RouteObject[], protectedRoutes: ProtectedRoutesInput
|
|
8
|
+
export declare const warnInlineProtectedRoutes: (routes: RouteObject[], protectedRoutes: ProtectedRoutesInput) => void;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { type VariantProps } from "class-variance-authority";
|
|
2
2
|
import type { HTMLAttributes, ReactNode } from "react";
|
|
3
3
|
declare const heading: (props?: ({
|
|
4
|
-
level?: 1 | 2 | 3 | 4 |
|
|
4
|
+
level?: 1 | 2 | 3 | 4 | 6 | 5 | null | undefined;
|
|
5
5
|
} & import("class-variance-authority/types").ClassProp) | undefined) => string;
|
|
6
6
|
export type HeadingProps = HTMLAttributes<HTMLHeadingElement> & VariantProps<typeof heading> & {
|
|
7
7
|
children: ReactNode;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { type BlogAuthor, type BlogPost, type BlogTag } from "./options.js";
|
|
2
|
+
export declare const BlogFeedLink: ({ feedUrl }: {
|
|
3
|
+
feedUrl?: string;
|
|
4
|
+
}) => import("react").JSX.Element | null;
|
|
5
|
+
export declare const formatPostDate: (date: string) => string;
|
|
6
|
+
export declare const PostMeta: ({ post, showReadingTime, className, }: {
|
|
7
|
+
post: BlogPost;
|
|
8
|
+
showReadingTime: boolean;
|
|
9
|
+
className?: string;
|
|
10
|
+
}) => import("react").JSX.Element;
|
|
11
|
+
export declare const PostAuthors: ({ authors, compact, }: {
|
|
12
|
+
authors: BlogAuthor[];
|
|
13
|
+
compact?: boolean;
|
|
14
|
+
}) => import("react").JSX.Element | null;
|
|
15
|
+
export declare const PostTags: ({ tags, blogPath, }: {
|
|
16
|
+
tags: BlogTag[];
|
|
17
|
+
blogPath: string;
|
|
18
|
+
}) => import("react").JSX.Element | null;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type BlogPageOptions, type BlogPost, type BlogTag } from "./options.js";
|
|
2
|
+
export declare const BlogListPage: ({ options, posts, page, pageCount, tag, }: {
|
|
3
|
+
options: BlogPageOptions;
|
|
4
|
+
posts: BlogPost[];
|
|
5
|
+
page?: number;
|
|
6
|
+
pageCount?: number;
|
|
7
|
+
tag?: BlogTag;
|
|
8
|
+
}) => import("react").JSX.Element;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { MDXImport } from "../markdown/index.js";
|
|
2
|
+
import type { BlogPageOptions, BlogPost } from "./options.js";
|
|
3
|
+
export declare const BlogPostPage: ({ options, post, newer, older, mdxComponent: MdxComponent, frontmatter, tableOfContents, __filepath, }: Omit<MDXImport, "default" | "excerpt"> & {
|
|
4
|
+
options: BlogPageOptions;
|
|
5
|
+
post: BlogPost;
|
|
6
|
+
newer?: BlogPost;
|
|
7
|
+
older?: BlogPost;
|
|
8
|
+
mdxComponent: MDXImport["default"];
|
|
9
|
+
}) => import("react").JSX.Element;
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { ZudokuPlugin } from "../../core/plugins.js";
|
|
2
|
+
import { type BlogPluginOptions } from "./options.js";
|
|
3
|
+
export type { BlogAuthor, BlogPageOptions, BlogPluginOptions, BlogPost, BlogTag, } from "./options.js";
|
|
4
|
+
export declare const blogPlugin: (options?: BlogPluginOptions) => ZudokuPlugin;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { MDXImport } from "../markdown/index.js";
|
|
2
|
+
export declare const BLOG_PLUGIN_NAME = "zudoku-blog";
|
|
3
|
+
export type BlogAuthor = {
|
|
4
|
+
name: string;
|
|
5
|
+
title?: string;
|
|
6
|
+
url?: string;
|
|
7
|
+
imageUrl?: string;
|
|
8
|
+
};
|
|
9
|
+
export type BlogTag = {
|
|
10
|
+
label: string;
|
|
11
|
+
slug: string;
|
|
12
|
+
};
|
|
13
|
+
export type BlogPost = {
|
|
14
|
+
path: string;
|
|
15
|
+
title: string;
|
|
16
|
+
description?: string;
|
|
17
|
+
date: string;
|
|
18
|
+
authors: BlogAuthor[];
|
|
19
|
+
tags: BlogTag[];
|
|
20
|
+
readingTime: number;
|
|
21
|
+
image?: string;
|
|
22
|
+
draft?: boolean;
|
|
23
|
+
};
|
|
24
|
+
export type BlogPluginOptions = {
|
|
25
|
+
files?: string | string[];
|
|
26
|
+
path?: string;
|
|
27
|
+
title?: string;
|
|
28
|
+
description?: string;
|
|
29
|
+
postsPerPage?: number;
|
|
30
|
+
showReadingTime?: boolean;
|
|
31
|
+
authors?: Record<string, BlogAuthor>;
|
|
32
|
+
sidebar?: {
|
|
33
|
+
title?: string;
|
|
34
|
+
count?: number;
|
|
35
|
+
} | false;
|
|
36
|
+
feed?: boolean;
|
|
37
|
+
};
|
|
38
|
+
export type ResolvedBlogOptions = {
|
|
39
|
+
files: string[];
|
|
40
|
+
path: string;
|
|
41
|
+
title: string;
|
|
42
|
+
description?: string;
|
|
43
|
+
postsPerPage: number;
|
|
44
|
+
showReadingTime: boolean;
|
|
45
|
+
authors: Record<string, BlogAuthor>;
|
|
46
|
+
sidebar: {
|
|
47
|
+
title: string;
|
|
48
|
+
count: number;
|
|
49
|
+
} | false;
|
|
50
|
+
feed?: boolean;
|
|
51
|
+
};
|
|
52
|
+
export declare const resolveBlogOptions: (options?: BlogPluginOptions) => ResolvedBlogOptions;
|
|
53
|
+
export type BlogManifest = {
|
|
54
|
+
tags: BlogTag[];
|
|
55
|
+
feedUrl?: string;
|
|
56
|
+
postImports: Record<string, () => Promise<MDXImport>>;
|
|
57
|
+
loadPosts: () => Promise<BlogPost[]>;
|
|
58
|
+
};
|
|
59
|
+
export type BlogPageOptions = Pick<ResolvedBlogOptions, "path" | "title" | "description" | "showReadingTime"> & Pick<BlogManifest, "feedUrl" | "tags">;
|
|
60
|
+
export declare const getBlogPagePath: (blogPath: string, page: number) => string;
|
|
61
|
+
export declare const getBlogTagPath: (blogPath: string, tag: Pick<BlogTag, "slug">) => string;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { SchemaObject } from "../../oas/parser/index.js";
|
|
2
|
-
export declare const ParamInfos: ({ schema, extraItems, className, }: {
|
|
2
|
+
export declare const ParamInfos: ({ schema, nullable, extraItems, className, }: {
|
|
3
3
|
schema?: SchemaObject;
|
|
4
|
+
nullable?: boolean;
|
|
4
5
|
extraItems?: unknown[];
|
|
5
6
|
className?: string;
|
|
6
7
|
}) => import("react").JSX.Element;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
export declare const EnumValues: ({ values, className, maxVisibleValues, }: {
|
|
1
|
+
export declare const EnumValues: ({ values, className, descriptions, maxVisibleValues, }: {
|
|
2
2
|
values: Array<string | number>;
|
|
3
|
+
descriptions?: Record<string, string>;
|
|
3
4
|
className?: string;
|
|
4
5
|
maxVisibleValues?: number;
|
|
5
6
|
}) => import("react").JSX.Element | null;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { SchemaObject } from "../../../oas/parser/index.js";
|
|
2
|
-
export declare const SchemaPropertyItem: ({ name, schema, group, defaultOpen, showCollapseButton, }: {
|
|
2
|
+
export declare const SchemaPropertyItem: ({ name, schema: propertySchema, group, defaultOpen, showCollapseButton, }: {
|
|
3
3
|
name: string;
|
|
4
4
|
schema: SchemaObject;
|
|
5
5
|
group: "required" | "optional" | "deprecated";
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import type { SchemaObject } from "../../../oas/parser/index.js";
|
|
2
|
-
export declare const SchemaView: ({ schema, defaultOpen, cardHeader, embedded, hideRootRef, }: {
|
|
2
|
+
export declare const SchemaView: ({ schema: rootSchema, defaultOpen, cardHeader, embedded, hideRootRef, }: {
|
|
3
3
|
schema?: SchemaObject | null;
|
|
4
4
|
defaultOpen?: boolean;
|
|
5
5
|
cardHeader?: React.ReactNode;
|
|
6
6
|
embedded?: boolean;
|
|
7
7
|
hideRootRef?: boolean;
|
|
8
|
-
}) => import("react").JSX.Element |
|
|
8
|
+
}) => import("react").JSX.Element | null;
|
|
@@ -1,10 +1,17 @@
|
|
|
1
1
|
import type { ArraySchemaObject, SchemaObject } from "../../../oas/parser/index.js";
|
|
2
2
|
export declare const isBasicType: (type: unknown) => type is "string" | "number" | "boolean" | "integer" | "null";
|
|
3
3
|
export declare const isArrayType: (value: SchemaObject) => value is ArraySchemaObject;
|
|
4
|
+
export declare const isBinarySchema: (schema?: SchemaObject) => boolean;
|
|
5
|
+
export declare const withBinaryMediaType: (schema: SchemaObject | null | undefined, mediaType: string | undefined) => SchemaObject | undefined;
|
|
6
|
+
export declare const unwrapNullableUnion: (schema: SchemaObject) => {
|
|
7
|
+
schema: SchemaObject;
|
|
8
|
+
nullable: boolean;
|
|
9
|
+
};
|
|
4
10
|
export declare const isComplexType: (value?: SchemaObject) => boolean | undefined;
|
|
5
11
|
export declare const isCircularRef: (schema: unknown) => schema is string;
|
|
6
12
|
export declare const isArrayCircularRef: (schema: SchemaObject) => schema is SchemaObject & {
|
|
7
13
|
items: SchemaObject;
|
|
8
14
|
};
|
|
15
|
+
export declare const getEnumDescriptions: (schema?: SchemaObject) => Record<string, string> | undefined;
|
|
9
16
|
export declare const getSchemaRefName: (schema?: SchemaObject | null) => string | undefined;
|
|
10
17
|
export declare const extractCircularRefInfo: (ref?: string | SchemaObject) => string | undefined;
|
|
@@ -18,7 +18,7 @@ declare const useFormField: () => {
|
|
|
18
18
|
declare const FormItem: React.ForwardRefExoticComponent<React.HTMLAttributes<HTMLDivElement> & React.RefAttributes<HTMLDivElement>>;
|
|
19
19
|
declare const FormLabel: React.ForwardRefExoticComponent<Omit<LabelPrimitive.LabelProps & React.RefAttributes<HTMLLabelElement>, "ref"> & React.RefAttributes<HTMLLabelElement>>;
|
|
20
20
|
declare const FormControl: React.ForwardRefExoticComponent<Omit<React.HTMLAttributes<HTMLElement> & {
|
|
21
|
-
children?: React.ReactNode;
|
|
21
|
+
children?: React.ReactNode | undefined;
|
|
22
22
|
} & React.RefAttributes<HTMLElement>, "ref"> & React.RefAttributes<HTMLElement>>;
|
|
23
23
|
declare const FormDescription: React.ForwardRefExoticComponent<React.HTMLAttributes<HTMLParagraphElement> & React.RefAttributes<HTMLParagraphElement>>;
|
|
24
24
|
declare const FormMessage: React.ForwardRefExoticComponent<React.HTMLAttributes<HTMLParagraphElement> & React.RefAttributes<HTMLParagraphElement>>;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export type ZuploEnvironmentVariables = {
|
|
2
|
+
__ZUPLO_DEPLOYMENT_NAME?: string;
|
|
3
|
+
__ZUPLO_LOGGING_ID?: string;
|
|
4
|
+
__ZUPLO_REMOTE_LOG_TOKEN?: string;
|
|
5
|
+
__ZUPLO_AUTH_API_JWT?: string;
|
|
6
|
+
__ZUPLO_EXTERNAL_SERVICE_TOKEN?: string;
|
|
7
|
+
ZUPLO_ENVIRONMENT_TYPE?: "PRODUCTION" | "DEVELOPMENT" | "STAGING";
|
|
8
|
+
ZUPLO_PROJECT_ID?: string;
|
|
9
|
+
ZUPLO_API_KEY_SERVICE_BUCKET_NAME?: string;
|
|
10
|
+
ZUPLO_SERVICE_BUCKET_ID?: string;
|
|
11
|
+
__ZUPLO_CONFIG?: string;
|
|
12
|
+
};
|
|
13
|
+
export declare const getZuploSystemConfigurations: (string?: string) => ZuploEnvironmentVariables | undefined;
|
|
@@ -6,6 +6,22 @@ sidebar_icon: book-open
|
|
|
6
6
|
If you're dealing with multiple APIs and multiple OpenAPI files, the API Catalog comes in handy. It
|
|
7
7
|
creates an overview of all your APIs and lets you organize them into categories and tags.
|
|
8
8
|
|
|
9
|
+
## Categories and Tags
|
|
10
|
+
|
|
11
|
+
Each entry in an API's `categories` array has two fields, `label` and `tags`, which serve different
|
|
12
|
+
purposes:
|
|
13
|
+
|
|
14
|
+
- **`label`** is the primary grouping. The catalog page renders one filter chip per unique label, so
|
|
15
|
+
this is what users click to narrow the list. An API can belong to multiple categories by adding
|
|
16
|
+
multiple entries to the array.
|
|
17
|
+
- **`tags`** are secondary descriptors attached to that category. They do _not_ get their own filter
|
|
18
|
+
chip. Instead, they are matched against the search input and rendered as additional badges on the
|
|
19
|
+
API card (alongside the label). Use them for finer-grained keywords that should help users
|
|
20
|
+
discover an API without cluttering the top-level filters.
|
|
21
|
+
|
|
22
|
+
For example, `{ label: "Payments", tags: ["billing", "subscriptions"] }` puts the API under the
|
|
23
|
+
"Payments" filter chip and adds "billing" and "subscriptions" as searchable badges on its card.
|
|
24
|
+
|
|
9
25
|
## Enable API Catalog
|
|
10
26
|
|
|
11
27
|
To enable the API Catalog, add a `catalogs` object to your Zudoku configuration file.
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Blog
|
|
3
|
+
navigation_icon: newspaper
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
Zudoku can publish a blog next to your documentation and API reference. Posts are Markdown or MDX
|
|
7
|
+
files in a folder of your project: add a file and it shows up in the blog, with a paginated index,
|
|
8
|
+
tag pages, a "Recent posts" sidebar and an RSS feed.
|
|
9
|
+
|
|
10
|
+
:::caution{title="Beta"}
|
|
11
|
+
|
|
12
|
+
The blog plugin is in beta. Its options, frontmatter fields and generated URLs may still change in a
|
|
13
|
+
minor release. Feedback is welcome in [GitHub issues](https://github.com/zuplo/zudoku/issues).
|
|
14
|
+
|
|
15
|
+
:::
|
|
16
|
+
|
|
17
|
+
## Enable the blog
|
|
18
|
+
|
|
19
|
+
Add the blog plugin to the `plugins` of your config. Every option has a default:
|
|
20
|
+
|
|
21
|
+
```tsx title="zudoku.config.tsx"
|
|
22
|
+
import { blogPlugin } from "zudoku/plugins/blog";
|
|
23
|
+
|
|
24
|
+
export default {
|
|
25
|
+
plugins: [blogPlugin()],
|
|
26
|
+
};
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Then put your posts in the `blog` folder at the root of your project:
|
|
30
|
+
|
|
31
|
+
```text title="File tree"
|
|
32
|
+
blog/
|
|
33
|
+
├── 2025-03-14-hello-world.md → /blog/hello-world
|
|
34
|
+
├── 2025-04-02-launch-week/
|
|
35
|
+
│ ├── index.mdx → /blog/launch-week
|
|
36
|
+
│ └── screenshot.png
|
|
37
|
+
└── release-notes/
|
|
38
|
+
└── 2025-05-20-v2.md → /blog/release-notes/v2
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
To link to the blog from the top navigation, add a link to your [navigation](./navigation.mdx):
|
|
42
|
+
|
|
43
|
+
```tsx title="zudoku.config.tsx"
|
|
44
|
+
export default {
|
|
45
|
+
navigation: [
|
|
46
|
+
// ...
|
|
47
|
+
{ type: "link", to: "/blog", label: "Blog", icon: "newspaper" },
|
|
48
|
+
],
|
|
49
|
+
};
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Writing posts
|
|
53
|
+
|
|
54
|
+
The file name sets the URL and the date of a post: a `YYYY-MM-DD-` prefix is the publish date and
|
|
55
|
+
the rest is the slug. A post can also be a folder with an `index.md` or `index.mdx` file, which
|
|
56
|
+
keeps its images next to it. Folders that aren't dated stay part of the URL.
|
|
57
|
+
|
|
58
|
+
Some files are not posts:
|
|
59
|
+
|
|
60
|
+
- Other Markdown files in a post's folder, such as partials imported by its `index.mdx` or a
|
|
61
|
+
`README.md`
|
|
62
|
+
- Files and folders whose name starts with `_`, e.g. `_drafts/` or `_snippet.mdx`
|
|
63
|
+
- Posts whose URL is taken by the blog's own pages: `<path>/tags`, `<path>/tags/*`, `<path>/page/*`
|
|
64
|
+
and `<path>/rss.xml`. These are skipped with a warning.
|
|
65
|
+
|
|
66
|
+
Link to other posts by their file, e.g. `[Launch](./2025-04-02-launch-week/index.mdx)`, and the link
|
|
67
|
+
points to the post's URL.
|
|
68
|
+
|
|
69
|
+
Posts support everything [Markdown and MDX pages](../markdown/overview.md) do, including
|
|
70
|
+
[admonitions](../markdown/admonitions.md), [code blocks](../markdown/code-blocks.md) and React
|
|
71
|
+
components.
|
|
72
|
+
|
|
73
|
+
```md title="blog/2025-03-14-hello-world.md"
|
|
74
|
+
---
|
|
75
|
+
title: Hello World
|
|
76
|
+
description: The first post on our new developer blog.
|
|
77
|
+
authors: [jane]
|
|
78
|
+
tags: [Announcements, API]
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
This paragraph is the excerpt shown on the blog index page.
|
|
82
|
+
|
|
83
|
+
## A section
|
|
84
|
+
|
|
85
|
+
The post continues here.
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### Frontmatter
|
|
89
|
+
|
|
90
|
+
| Field | Description |
|
|
91
|
+
| ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
92
|
+
| `title` | Title of the post. Defaults to the first `# Heading`, which is then not repeated in the post body. |
|
|
93
|
+
| `description` | Shown on the index page, in the feed and as the meta description. Defaults to the first paragraph. |
|
|
94
|
+
| `date` | Publish date, e.g. `2025-03-14` or `2025-03-14T09:00:00Z`. Defaults to the date in the file name, then the date of the commit that added the file (needs the full git history), then the file's modification time. |
|
|
95
|
+
| `slug` | Overrides the URL: `slug: launch` serves the post at `/blog/launch`. |
|
|
96
|
+
| `authors` | Author keys from [`authors`](#authors), or inline objects such as `{ name: "Jane Doe", title: "Engineer" }`. |
|
|
97
|
+
| `tags` | List of tags. Each tag gets a page listing its posts. |
|
|
98
|
+
| `image` | Image used as `og:image` when the post is shared. An absolute URL or a path in the `public` folder, e.g. `/cover.png`. |
|
|
99
|
+
| `draft` | When `true`, the post is only shown in development. |
|
|
100
|
+
| `toc` | Set to `false` to hide the table of contents. |
|
|
101
|
+
| `seo` | `title` and `description` that override the browser title and meta description only. |
|
|
102
|
+
|
|
103
|
+
Posts are sorted by date, newest first.
|
|
104
|
+
|
|
105
|
+
## Plugin options
|
|
106
|
+
|
|
107
|
+
```tsx title="zudoku.config.tsx"
|
|
108
|
+
import { blogPlugin } from "zudoku/plugins/blog";
|
|
109
|
+
|
|
110
|
+
export default {
|
|
111
|
+
plugins: [
|
|
112
|
+
blogPlugin({
|
|
113
|
+
files: "/blog/**/*.{md,mdx}",
|
|
114
|
+
path: "/blog",
|
|
115
|
+
title: "Blog",
|
|
116
|
+
description: "News and updates from the team.",
|
|
117
|
+
postsPerPage: 10,
|
|
118
|
+
showReadingTime: true,
|
|
119
|
+
authors: {
|
|
120
|
+
jane: {
|
|
121
|
+
name: "Jane Doe",
|
|
122
|
+
title: "Developer Advocate",
|
|
123
|
+
url: "https://example.com/jane",
|
|
124
|
+
imageUrl: "https://example.com/jane.png",
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
sidebar: { title: "Recent posts", count: 5 },
|
|
128
|
+
feed: true,
|
|
129
|
+
}),
|
|
130
|
+
],
|
|
131
|
+
};
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
### `files`
|
|
135
|
+
|
|
136
|
+
**Type:** `string | string[]`
|
|
137
|
+
**Default:** `"/blog/**/*.{md,mdx}"`
|
|
138
|
+
|
|
139
|
+
Glob patterns for the post files. Keep them outside of [`docs.files`](./docs.md#files) so a post
|
|
140
|
+
isn't also published as a documentation page.
|
|
141
|
+
|
|
142
|
+
### `path`
|
|
143
|
+
|
|
144
|
+
**Type:** `string`
|
|
145
|
+
**Default:** `"/blog"`
|
|
146
|
+
|
|
147
|
+
The route the blog is served under. The index is at `path`, further index pages at `path/page/2`,
|
|
148
|
+
tags at `path/tags` and posts at `path/<slug>`.
|
|
149
|
+
|
|
150
|
+
### `title` and `description`
|
|
151
|
+
|
|
152
|
+
**Type:** `string`
|
|
153
|
+
**Default:** `"Blog"` and none
|
|
154
|
+
|
|
155
|
+
Heading and introduction of the index page. Both are also used for the RSS feed.
|
|
156
|
+
|
|
157
|
+
### `postsPerPage`
|
|
158
|
+
|
|
159
|
+
**Type:** `number`
|
|
160
|
+
**Default:** `10`
|
|
161
|
+
|
|
162
|
+
Number of posts on each page of the index.
|
|
163
|
+
|
|
164
|
+
### `showReadingTime`
|
|
165
|
+
|
|
166
|
+
**Type:** `boolean`
|
|
167
|
+
**Default:** `true`
|
|
168
|
+
|
|
169
|
+
Shows an estimated reading time next to the date of each post.
|
|
170
|
+
|
|
171
|
+
### `authors`
|
|
172
|
+
|
|
173
|
+
**Type:** `Record<string, { name: string; title?: string; url?: string; imageUrl?: string }>`
|
|
174
|
+
**Default:** `{}`
|
|
175
|
+
|
|
176
|
+
Authors that posts reference by key in their `authors` frontmatter, so their details are kept in one
|
|
177
|
+
place. Authors without an `imageUrl` show their initial instead.
|
|
178
|
+
|
|
179
|
+
### `sidebar`
|
|
180
|
+
|
|
181
|
+
**Type:** `{ title?: string; count?: number } | false`
|
|
182
|
+
**Default:** `{ title: "Recent posts", count: 5 }`
|
|
183
|
+
|
|
184
|
+
Lists the most recent posts in the sidebar of all blog pages. Set to `false` to hide it.
|
|
185
|
+
|
|
186
|
+
### `feed`
|
|
187
|
+
|
|
188
|
+
**Type:** `boolean`
|
|
189
|
+
**Default:** on when a site URL is configured
|
|
190
|
+
|
|
191
|
+
Generates an RSS feed at `<path>/rss.xml` and links it from every blog page. Feeds need absolute
|
|
192
|
+
links, so the site URL is taken from [`canonicalUrlOrigin`](./overview.md#canonicalurlorigin) or
|
|
193
|
+
[`sitemap.siteUrl`](./overview.md#sitemap). Without either, no feed is generated.
|
|
194
|
+
|
|
195
|
+
## Multiple blogs
|
|
196
|
+
|
|
197
|
+
Add the plugin once per blog, each with its own `files` and `path`, for example a blog and a
|
|
198
|
+
changelog:
|
|
199
|
+
|
|
200
|
+
```tsx title="zudoku.config.tsx"
|
|
201
|
+
import { blogPlugin } from "zudoku/plugins/blog";
|
|
202
|
+
|
|
203
|
+
export default {
|
|
204
|
+
plugins: [
|
|
205
|
+
blogPlugin({ files: "/blog/**/*.{md,mdx}", path: "/blog" }),
|
|
206
|
+
blogPlugin({
|
|
207
|
+
files: "/changelog/**/*.{md,mdx}",
|
|
208
|
+
path: "/changelog",
|
|
209
|
+
title: "Changelog",
|
|
210
|
+
sidebar: false,
|
|
211
|
+
}),
|
|
212
|
+
],
|
|
213
|
+
};
|
|
214
|
+
```
|