@lexibase/rdx-next 3.2.0 → 4.0.0

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/README.md CHANGED
@@ -16,6 +16,10 @@ must still be trusted project-owned content, not untrusted uploads.
16
16
  It exposes the `rdx` command used by generated projects to create their
17
17
  internal `.rdx` application and run Next.js.
18
18
 
19
+ Run `pnpm rdx check` to validate static documentation links, heading fragments
20
+ and local files in public/ before deployment. It reports source locations and
21
+ exits nonzero for problems, without preparing the runtime or rendering documents.
22
+
19
23
  Run `pnpm rdx add versioning` (or `npm exec -- rdx add versioning`) from a
20
24
  documentation project to install and activate the official optional plugin.
21
25
  The command detects the package manager, preserves config and plugin options,
@@ -89,7 +93,6 @@ export default defineConfig({
89
93
  authors: [{ name: 'My team', url: 'https://example.com' }],
90
94
  keywords: ['documentation', 'my-project'],
91
95
  icons: { icon: '/favicon.ico' },
92
- robots: { index: true, follow: true },
93
96
  openGraph: {
94
97
  type: 'website',
95
98
  images: [{ url: '/og.png', width: 1200, height: 630, alt: 'My project' }],
@@ -111,7 +114,7 @@ production domain or force localhost. Image, favicon and manifest paths refer
111
114
  to files you create in `public/`. Nothing creates those assets automatically.
112
115
 
113
116
  Supported fields are `metadataBase` (an absolute http(s) string),
114
- `applicationName`, `authors`, `keywords`, `icons`, `manifest`, `robots`,
117
+ `applicationName`, `authors`, `keywords`, `icons`, `manifest`,
115
118
  `openGraph` and `twitter`. TypeScript describes the supported nested options,
116
119
  and invalid fields fail during configuration validation. This is a portable
117
120
  subset, not an unrestricted pass-through of all Next.js metadata options.
package/bin/cli.mjs CHANGED
@@ -50,7 +50,12 @@ export async function runRdxCli({ templateDir } = {}) {
50
50
 
51
51
  async function main() {
52
52
  if (!command || command === 'help' || command === '--help') {
53
- console.log('Usage: rdx <dev|build|start|prepare|add|plugin-command>')
53
+ console.log(
54
+ 'Usage: rdx <dev|build|start|prepare|check|add|plugin-command>'
55
+ )
56
+ console.log(
57
+ ' rdx check: validate documentation links, sections and local assets'
58
+ )
54
59
  console.log(' rdx add versioning [--package-manager npm|pnpm|yarn]')
55
60
  console.log(
56
61
  'Plugin commands are registered through plugins in rdx.config.ts.'
@@ -58,6 +63,19 @@ export async function runRdxCli({ templateDir } = {}) {
58
63
  return
59
64
  }
60
65
 
66
+ if (command === 'check') {
67
+ if (
68
+ commandArgs.length === 1 &&
69
+ ['--help', '-h'].includes(commandArgs[0])
70
+ ) {
71
+ console.log(
72
+ 'Usage: rdx check\nValidate static MDX links, section anchors and local public/ files without building the site.'
73
+ )
74
+ return
75
+ }
76
+ if (commandArgs.length) throw new Error('Usage: rdx check')
77
+ }
78
+
61
79
  if (command === 'add') {
62
80
  const { addVersioning, parseAddArguments } =
63
81
  await import('./add-plugin.mjs')
@@ -96,6 +114,17 @@ export async function runRdxCli({ templateDir } = {}) {
96
114
  rootDir: projectRoot,
97
115
  command,
98
116
  })
117
+ if (command === 'check') {
118
+ const { checkDocumentation, formatDocumentationCheck } =
119
+ await import('@lexibase/rdx-core/check')
120
+ const report = checkDocumentation(project)
121
+ const output = formatDocumentationCheck(report)
122
+ if (report.diagnostics.length) {
123
+ console.error(output)
124
+ process.exitCode = 1
125
+ } else console.log(output)
126
+ return
127
+ }
99
128
  if (command === 'prepare') {
100
129
  await prepareRuntime()
101
130
  console.log('RDX runtime prepared in .rdx/')
@@ -107,7 +136,7 @@ export async function runRdxCli({ templateDir } = {}) {
107
136
  )
108
137
  if (!pluginCommand)
109
138
  throw new Error(
110
- `Unknown command "${command}". Available: dev, build, start, prepare, add${project.commands.map((entry) => `, ${entry.name}`).join('')}. Install and configure the plugin that provides this command.`
139
+ `Unknown command "${command}". Available: dev, build, start, prepare, check, add${project.commands.map((entry) => `, ${entry.name}`).join('')}. Install and configure the plugin that provides this command.`
111
140
  )
112
141
  await pluginCommand.run({
113
142
  rootDir: projectRoot,
@@ -1,14 +1,26 @@
1
1
  import * as react from 'react';
2
2
  import { ReactNode } from 'react';
3
- import { RdxProject, RDXConfig, RdxDocPage } from '@lexibase/rdx-types';
3
+ import { RdxProject, RDXConfig, RdxContentSource, RdxDocPage } from '@lexibase/rdx-types';
4
4
  export { createRdxProject, getProjectDocsPaths } from '@lexibase/rdx-core';
5
- import { Metadata } from 'next';
5
+ import { MetadataRoute, Metadata } from 'next';
6
6
 
7
7
  declare function getSearchSources(project: RdxProject): {
8
8
  source: string;
9
9
  }[];
10
10
  declare function getSearchResponse(project: RdxProject, key: string): Response;
11
11
 
12
+ /** Published editions replace live development content in search results. */
13
+ declare function getSourceRobots(project: RdxProject, source: RdxContentSource): {
14
+ index: boolean;
15
+ follow: boolean;
16
+ };
17
+ /** Use the configured public URL only; development hosts are never inferred. */
18
+ declare function getRdxRobots(config: RDXConfig): MetadataRoute.Robots;
19
+ /** Share the indexing policy while allowing host applications to map their URLs. */
20
+ declare function getRdxSitemap(project: RdxProject, { documentPath, }?: {
21
+ documentPath?: (source: RdxContentSource, page: RdxDocPage) => string;
22
+ }): MetadataRoute.Sitemap;
23
+
12
24
  /** Read metadata from the same content source and index used by the page. */
13
25
  declare function getDocumentMetadata({ project, slug, }: {
14
26
  project: RdxProject;
@@ -35,4 +47,4 @@ declare function DocsPage({ slug, project, }: {
35
47
  project: RdxProject;
36
48
  }): Promise<react.JSX.Element>;
37
49
 
38
- export { DocsPage, HomePage, NotFoundPage, RdxLayout, getDocumentMetadata, getPageMetadata, getRdxMetadata, getSearchResponse, getSearchSources };
50
+ export { DocsPage, HomePage, NotFoundPage, RdxLayout, getDocumentMetadata, getPageMetadata, getRdxMetadata, getRdxRobots, getRdxSitemap, getSearchResponse, getSearchSources, getSourceRobots };
@@ -2,7 +2,7 @@
2
2
  import {
3
3
  getDocument as getDocument2,
4
4
  getDocsPaths,
5
- getSourceOptions as getSourceOptions2,
5
+ getSourceOptions as getSourceOptions3,
6
6
  resolveDocsRoute as resolveDocsRoute2
7
7
  } from "@lexibase/rdx-core";
8
8
  import { Button } from "@lexibase/rdx-ui/components/button";
@@ -63,11 +63,13 @@ function getSearchSources(project) {
63
63
  return project.config.search ? project.sources.filter((source) => source.redirectTo === void 0).map((source) => ({ source: source.routePrefix || "_" })) : [];
64
64
  }
65
65
  function getSearchResponse(project, key) {
66
- if (!project.config.search) return new Response(null, { status: 404 });
66
+ const headers = { "X-Robots-Tag": "noindex, follow" };
67
+ if (!project.config.search)
68
+ return new Response(null, { status: 404, headers });
67
69
  const source = project.sources.find(
68
70
  (entry) => entry.redirectTo === void 0 && (entry.routePrefix || "_") === key
69
71
  );
70
- if (!source) return new Response(null, { status: 404 });
72
+ if (!source) return new Response(null, { status: 404, headers });
71
73
  const options = getSourceOptions(project, source);
72
74
  const documents = getDocsIndex(options).flatMap(
73
75
  (category) => category.pages.map((page) => ({
@@ -82,7 +84,47 @@ function getSearchResponse(project, key) {
82
84
  content: getDocument(page.slug, options)?.content ?? ""
83
85
  }))
84
86
  );
85
- return Response.json(buildSearchIndex(documents));
87
+ return Response.json(buildSearchIndex(documents), { headers });
88
+ }
89
+
90
+ // src/runtime/indexing.ts
91
+ import { getDocsIndex as getDocsIndex2, getSourceOptions as getSourceOptions2 } from "@lexibase/rdx-core";
92
+ function getSourceRobots(project, source) {
93
+ const published = project.sources.some(
94
+ (entry) => entry.id !== "current" && entry.redirectTo === void 0 && entry.badge !== "Archived"
95
+ );
96
+ return {
97
+ index: source.redirectTo === void 0 && source.badge !== "Archived" && (source.id !== "current" || !published),
98
+ follow: true
99
+ };
100
+ }
101
+ function getRdxRobots(config) {
102
+ return {
103
+ // Do not disallow noindex pages: crawlers must read their HTML directive.
104
+ rules: { userAgent: "*", allow: "/" },
105
+ ...config.metadata.metadataBase ? { sitemap: new URL("/sitemap.xml", config.metadata.metadataBase).href } : {}
106
+ };
107
+ }
108
+ function getRdxSitemap(project, {
109
+ documentPath = (source, page) => [
110
+ "/docs",
111
+ source.routePrefix,
112
+ page.slug.split("/").map(encodeURIComponent).join("/")
113
+ ].filter(Boolean).join("/")
114
+ } = {}) {
115
+ const base = project.config.metadata.metadataBase;
116
+ if (!base) return [];
117
+ const urls = /* @__PURE__ */ new Set();
118
+ if (project.config.homepage?.enabled) urls.add(new URL("/", base).href);
119
+ for (const source of project.sources) {
120
+ if (!getSourceRobots(project, source).index) continue;
121
+ for (const category of getDocsIndex2(getSourceOptions2(project, source))) {
122
+ for (const page of category.pages) {
123
+ urls.add(new URL(documentPath(source, page), base).href);
124
+ }
125
+ }
126
+ }
127
+ return [...urls].map((url) => ({ url }));
86
128
  }
87
129
 
88
130
  // src/runtime/metadata.ts
@@ -93,11 +135,15 @@ function getDocumentMetadata({
93
135
  slug
94
136
  }) {
95
137
  const route = resolveDocsRoute(project, slug);
96
- if (route.source.redirectTo !== void 0) return {};
97
- if (!route.slug) return {};
138
+ if (route.source.redirectTo !== void 0)
139
+ return { robots: getSourceRobots(project, route.source) };
140
+ if (!route.slug) return { robots: { index: false, follow: true } };
98
141
  if (!route.document) notFound();
99
142
  const page = route.categories.flatMap((category) => category.pages).find((page2) => page2.slug === route.slug);
100
- return getPageMetadata(project.config, page);
143
+ return {
144
+ ...getPageMetadata(project.config, page),
145
+ robots: getSourceRobots(project, route.source)
146
+ };
101
147
  }
102
148
  function getPageMetadata(config, page) {
103
149
  const title = page.title;
@@ -117,6 +163,7 @@ function getRdxMetadata(config) {
117
163
  title: { default: name, template: `%s | ${name}` },
118
164
  description: config.description,
119
165
  ...metadata,
166
+ robots: { index: true, follow: true },
120
167
  ...metadataBase ? { metadataBase: new URL(metadataBase) } : {},
121
168
  ...metadata.openGraph ? {
122
169
  openGraph: {
@@ -164,7 +211,7 @@ function RdxHeader({
164
211
  const messages = getMessages(config.lang);
165
212
  const { source, slug: currentSlug } = resolveDocsRoute2(project, slug);
166
213
  const options = project.sources.filter((edition) => edition.redirectTo === void 0).map((edition) => {
167
- const docs = getSourceOptions2(project, edition);
214
+ const docs = getSourceOptions3(project, edition);
168
215
  const destination = currentSlug && getDocument2(currentSlug, docs) ? currentSlug : getDocsPaths(docs)[0];
169
216
  return {
170
217
  id: edition.id,
@@ -261,7 +308,7 @@ async function DocsPage({
261
308
  document.content,
262
309
  getMdxComponents(project.config.lang),
263
310
  {
264
- sourceRoot: getSourceOptions2(project, source).docsDir,
311
+ sourceRoot: getSourceOptions3(project, source).docsDir,
265
312
  sourcePath: document.sourcePath,
266
313
  routePrefix: source.routePrefix,
267
314
  documents: categories.flatMap((category2) => category2.pages)
@@ -381,6 +428,9 @@ export {
381
428
  getPageMetadata,
382
429
  getProjectDocsPaths,
383
430
  getRdxMetadata,
431
+ getRdxRobots,
432
+ getRdxSitemap,
384
433
  getSearchResponse,
385
- getSearchSources
434
+ getSearchSources,
435
+ getSourceRobots
386
436
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lexibase/rdx-next",
3
- "version": "3.2.0",
3
+ "version": "4.0.0",
4
4
  "description": "The Next.js runtime for RDX documentation projects.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -51,8 +51,8 @@
51
51
  "next-mdx-remote": "^6.0.0",
52
52
  "remark-gfm": "^4",
53
53
  "typescript": "6.0.3",
54
- "@lexibase/rdx-core": "1.4.0",
55
- "@lexibase/rdx-types": "1.2.0"
54
+ "@lexibase/rdx-types": "2.0.0",
55
+ "@lexibase/rdx-core": "2.0.0"
56
56
  },
57
57
  "peerDependencies": {
58
58
  "@lexibase/rdx-ui": "^3.2.0",
@@ -80,9 +80,9 @@
80
80
  "next": "^16.4.2",
81
81
  "react": "19.3.1",
82
82
  "react-dom": "19.3.1",
83
- "@lexibase/rdx-ui": "3.2.0",
84
- "@rdx/typescript-config": "1.0.0",
85
- "@rdx/eslint-config": "1.0.0"
83
+ "@rdx/eslint-config": "1.0.0",
84
+ "@lexibase/rdx-ui": "3.2.1",
85
+ "@rdx/typescript-config": "1.0.0"
86
86
  },
87
87
  "scripts": {
88
88
  "dev": "tsup src/index.ts src/runtime/index.tsx --format esm --dts --watch --external @lexibase/rdx-core --external @lexibase/rdx-types --external @lexibase/rdx-ui --external next --external react --external react-dom --external next-mdx-remote",
@@ -0,0 +1,6 @@
1
+ import { getRdxRobots } from '@lexibase/rdx-next/runtime'
2
+ import { getProject } from '../rdx-project'
3
+
4
+ export default async function robots() {
5
+ return getRdxRobots((await getProject()).config)
6
+ }
@@ -0,0 +1,6 @@
1
+ import { getRdxSitemap } from '@lexibase/rdx-next/runtime'
2
+ import { getProject } from '../rdx-project'
3
+
4
+ export default async function sitemap() {
5
+ return getRdxSitemap(await getProject())
6
+ }