From 29a4c1aace76e62ba6de0c02abac4cac7a2fe86e Mon Sep 17 00:00:00 2001 From: AminDhouib Date: Sun, 27 Sep 2026 03:21:31 +0000 Subject: [PATCH 1/5] docs(landing): add DevExtreme, ng2-file-upload and PrimeReact to the roundups The Angular roundup gains DevExtreme FileUploader and ng2-file-upload, and the React roundup gains PrimeReact FileUpload. Each gets a table row, a library section, a "How to choose" bullet and FAQ updates. The Angular page also notes the npm status of angular-file-uploader and of ng-file-upload, the AngularJS directive. PrimeNG 22 (2026-07-15) moved from MIT to PrimeTek's PrimeUI License, so the Angular page no longer lists PrimeNG as MIT or says every library on it is MIT-licensed. The comparisons hub lists the new entries under each roundup. --- .../best-angular-file-upload-libraries.mdx | 95 +++++++++++++++---- .../best-react-file-upload-libraries.mdx | 50 +++++++--- .../content/docs/comparisons/index.mdx | 6 +- 3 files changed, 113 insertions(+), 38 deletions(-) diff --git a/apps/landing/content/docs/comparisons/best-angular-file-upload-libraries.mdx b/apps/landing/content/docs/comparisons/best-angular-file-upload-libraries.mdx index 67b1d2e8..85c5c272 100644 --- a/apps/landing/content/docs/comparisons/best-angular-file-upload-libraries.mdx +++ b/apps/landing/content/docs/comparisons/best-angular-file-upload-libraries.mdx @@ -1,9 +1,9 @@ --- title: Best Angular File Upload Libraries Compared -description: Angular file upload libraries compared - upup, ngx-file-drop, PrimeNG FileUpload, FilePond and Uppy on drag and drop, S3 and resumable uploads. +description: Angular file upload libraries compared - upup, ngx-file-drop, ng2-file-upload, PrimeNG, DevExtreme, FilePond and Uppy on S3, resumable uploads and licensing. faq: - q: What is the best file upload library for Angular? - a: For a complete uploader against your own storage, upup or Uppy. Inside a PrimeNG app, PrimeNG FileUpload. For just a drop zone, ngx-file-drop. + a: For a complete uploader against your own storage, upup or Uppy. Inside a PrimeNG or DevExtreme app, that suite's upload component. For just a drop zone, ngx-file-drop, and for an upload queue behind your own markup, ng2-file-upload. - q: Which Angular upload libraries upload directly to S3? a: upup, with presigned URLs from your endpoint or @useupup/server, and Uppy, with its AWS S3 plugin. With the others you write the S3 signing and upload code yourself. - q: Does upup's Angular package include the image editor? @@ -11,27 +11,30 @@ faq: - q: Is upup's Angular component standalone? a: Yes. UpupUploaderComponent is a standalone component with the selector upup-uploader. Add it to a standalone component's imports, or to an NgModule's imports. - q: Are these libraries free? - a: Yes. All five are MIT-licensed. FilePond's full image editor, Pintura, is a separate commercial product. + a: Most are. upup, ngx-file-drop, ng2-file-upload, FilePond and Uppy are MIT-licensed. PrimeNG is MIT through version 21; version 22 moved to the PrimeUI License, which is free for individuals and eligible small organizations and paid otherwise. DevExtreme needs a paid per-developer DevExpress license. FilePond's full image editor, Pintura, is a separate commercial product. --- **The short answer:** for an Angular uploader that sends files straight to your own S3-compatible bucket, with cloud drives and resumable uploads, use upup or -Uppy. If your app already uses PrimeNG, its FileUpload component is the -smallest step. ngx-file-drop only handles the drop zone, and FilePond gives you -one polished component that posts to your server. +Uppy. If your app already uses PrimeNG or DevExtreme, that suite's upload +component is the smallest step. ngx-file-drop only handles the drop zone, +ng2-file-upload adds an upload queue behind your own markup, and FilePond gives +you one polished component that posts to your server. -This roundup compares five Angular file upload libraries on the features that +This roundup compares seven Angular file upload libraries on the features that usually decide the choice, then explains how to pick. ## Comparison table -| Library | Drag and drop | Cloud drives | Resumable | Direct-to-S3 | Image editing | License | Bundle approach | -| ------------------ | ------------- | ------------------------------------ | ------------------------------ | ------------------------------------ | -------------------------------------------------------- | ------- | ------------------------------------------- | -| upup | Yes | Google Drive, OneDrive, Dropbox, Box | Yes (tus or S3 multipart) | Yes (presigned URLs) | No (the editor is React/Preact only) | MIT | Headless core + native standalone component | -| ngx-file-drop | Yes | No | No | You build it | No | MIT | One drop-zone component; no upload code | -| PrimeNG FileUpload | Yes | No | No | Not built in (custom upload handler) | No | MIT | Part of the PrimeNG component suite | -| FilePond | Yes | No | Chunked uploads to your server | Not built in | Crop/resize plugins; full editor is Pintura (commercial) | MIT | Vanilla JS core + Angular adapter + plugins | -| Uppy | Yes | Many, via Companion | Yes (tus or S3 multipart) | Yes (AWS S3 plugin) | Yes (Image Editor plugin) | MIT | Modular plugins; install only what you use | +| Library | Drag and drop | Cloud drives | Resumable | Direct-to-S3 | Image editing | License | Bundle approach | +| ----------------------- | ------------- | ------------------------------------ | ------------------------------ | -------------------------------------- | -------------------------------------------------------- | ------------------------------------------------------------------------------ | -------------------------------------------------- | +| upup | Yes | Google Drive, OneDrive, Dropbox, Box | Yes (tus or S3 multipart) | Yes (presigned URLs) | No (the editor is React/Preact only) | MIT | Headless core + native standalone component | +| ngx-file-drop | Yes | No | No | You build it | No | MIT | One drop-zone component; no upload code | +| ng2-file-upload | Yes | No | No | Not built in | No | MIT | Two directives + an upload queue; you build the UI | +| PrimeNG FileUpload | Yes | No | No | Not built in (custom upload handler) | No | v22+: PrimeUI License (paid, with a free community tier); v21 and earlier: MIT | Part of the PrimeNG component suite | +| DevExtreme FileUploader | Yes | No | Chunked uploads to your server | Not built in (custom upload functions) | No | Commercial, per developer (30-day trial) | Part of the DevExtreme component suite | +| FilePond | Yes | No | Chunked uploads to your server | Not built in | Crop/resize plugins; full editor is Pintura (commercial) | MIT | Vanilla JS core + Angular adapter + plugins | +| Uppy | Yes | Many, via Companion | Yes (tus or S3 multipart) | Yes (AWS S3 plugin) | Yes (Image Editor plugin) | MIT | Modular plugins; install only what you use | ## The libraries @@ -53,12 +56,45 @@ and hands them to your code. It does not upload anything, so you add the HTTP requests, progress and storage yourself. It is a good fit when you only need the drop target. +### ng2-file-upload + +ng2-file-upload, from Valor Software, pairs an upload queue with two +directives. Its `FileUploader` class holds the queue, per-file progress and +filters such as a maximum size or allowed MIME types, and sends each file with +`XMLHttpRequest` to a URL you set. The `ng2FileSelect` and `ng2FileDrop` +directives connect it to a file input and a drop area; the file list, progress +bars and buttons are your own markup. It can send the raw file instead of a +multipart form (`disableMultipart`), which is what an S3 presigned PUT expects, +but it does not sign URLs, split files into chunks or resume uploads. Since +3.0.0, each major version declares a single Angular major as its peer +dependency (9.x for Angular 20; 10.x, on npm's `next` tag, for Angular 21), so +match the version to your Angular release. + ### PrimeNG FileUpload FileUpload is one component in the PrimeNG suite. It offers a basic button mode and an advanced mode with a drop zone, file list and progress bar, and it posts -to a URL you give it or hands the files to your own upload handler. It's the -natural choice if PrimeNG is already your component library. +to a URL you give it or hands the files to your own upload handler. PrimeNG 21 +and earlier are MIT-licensed. Version 22 moved to PrimeTek's PrimeUI License, +which is free for individuals and eligible small organizations, paid +otherwise, and requires a license key. It's the natural choice if PrimeNG is +already your component library. + +### DevExtreme FileUploader + +FileUploader (`dx-file-uploader`) is one component in DevExpress's DevExtreme +suite. It shows its own drop area, or you can point `dropZone` at another +element. Its `instantly` and `useButtons` upload modes send files with Ajax +requests to `uploadUrl`, and `useForm` submits them with an HTML form. Setting +`chunkSize` splits files into chunks that your server merges (DevExpress +documents ASP.NET and PHP handlers), and the `uploadFile` and `uploadChunk` +functions replace the built-in requests with your own code. S3, cloud drives +and resuming an interrupted upload are not built in; DevExpress publishes +example projects that implement S3 and Azure Blob uploads with `uploadChunk`. +DevExtreme is commercial: the `devextreme-angular` wrapper is MIT, but the +`devextreme` package it wraps needs a paid per-developer license, with a +30-day trial for evaluation. It's the natural choice if DevExtreme is already +your component library. ### FilePond @@ -74,18 +110,31 @@ UI, sources (webcam, screen capture, remote drives through Companion, a server you host) and destinations such as tus and S3. See [upup vs Uppy](/docs/comparisons/upup-vs-uppy/). +### Older packages with similar names + +Two packages that come up in searches are not compared above. The +`angular-file-uploader` package on npm has had no release since 7.1.1 in +November 2021, and that version declares Angular 10 as its peer dependency. +`ng-file-upload`, whose module is named `ngFileUpload`, is a directive for +AngularJS 1.x, not for current Angular. Its last release, 12.2.13, was in +November 2016, and AngularJS support has officially ended. + ## How to choose - **You want a finished uploader on your own bucket:** upup or Uppy. - **Your users pick files from Google Drive, OneDrive, Dropbox or Box:** upup or Uppy. Uppy routes remote files through Companion; upup does it in the browser (client mode) or in `@useupup/server` (server mode). -- **You already use PrimeNG:** start with PrimeNG FileUpload and switch only if - you need S3, cloud drives or resumable uploads. +- **You already use PrimeNG or DevExtreme:** start with that suite's upload + component and switch only if you need S3, cloud drives or resumable uploads. - **You only need a drop target:** ngx-file-drop, then write the upload code. +- **You want an upload queue but your own markup:** ng2-file-upload. - **You need image editing in Angular:** Uppy's Image Editor plugin, or FilePond's plugins. upup's editor is not available in Angular. - **You upload very large files:** use resumable uploads, with upup or Uppy. +- **You need permissive licenses throughout:** upup, ngx-file-drop, + ng2-file-upload, FilePond and Uppy are MIT. DevExtreme needs a paid license, + and PrimeNG 22 and later are under the PrimeUI License. ## Minimal upup example @@ -124,7 +173,8 @@ ready-to-copy presign handler. ### What is the best file upload library for Angular? For a complete uploader against your own storage, upup or Uppy. Inside a -PrimeNG app, PrimeNG FileUpload. For just a drop zone, ngx-file-drop. +PrimeNG or DevExtreme app, that suite's upload component. For just a drop zone, +ngx-file-drop, and for an upload queue behind your own markup, ng2-file-upload. ### Which Angular upload libraries upload directly to S3? @@ -146,8 +196,11 @@ Yes. `UpupUploaderComponent` is a standalone component with the selector ### Are these libraries free? -Yes. All five are MIT-licensed. FilePond's full image editor, Pintura, is a -separate commercial product. +Most are. upup, ngx-file-drop, ng2-file-upload, FilePond and Uppy are +MIT-licensed. PrimeNG is MIT through version 21; version 22 moved to the +PrimeUI License, which is free for individuals and eligible small organizations +and paid otherwise. DevExtreme needs a paid per-developer DevExpress license. +FilePond's full image editor, Pintura, is a separate commercial product. ## Related comparisons diff --git a/apps/landing/content/docs/comparisons/best-react-file-upload-libraries.mdx b/apps/landing/content/docs/comparisons/best-react-file-upload-libraries.mdx index 9678a40e..c2e27b88 100644 --- a/apps/landing/content/docs/comparisons/best-react-file-upload-libraries.mdx +++ b/apps/landing/content/docs/comparisons/best-react-file-upload-libraries.mdx @@ -1,9 +1,9 @@ --- title: Best React File Upload Libraries Compared -description: React file upload libraries compared - upup, react-dropzone, Uppy, FilePond, UploadThing and react-uploady on drag and drop, S3 and resumable uploads. +description: React file upload libraries compared - upup, react-dropzone, Uppy, FilePond, UploadThing, react-uploady and PrimeReact on S3, resumable uploads and licensing. faq: - q: What is the best file upload library for React? - a: For a complete uploader against your own storage, upup or Uppy. For the smallest possible building block, react-dropzone. For a hosted service, UploadThing. + a: For a complete uploader against your own storage, upup or Uppy. For the smallest possible building block, react-dropzone. For a hosted service, UploadThing. Inside a PrimeReact app, PrimeReact FileUpload. - q: Is react-dropzone enough on its own? a: Only if you write the rest. react-dropzone handles selecting files; you still need upload requests, progress, retries, error states and a storage backend. - q: Which React upload libraries upload directly to S3? @@ -11,28 +11,30 @@ faq: - q: Which React upload libraries support resumable uploads? a: upup (tus or S3 multipart), Uppy (tus or S3 multipart), react-uploady (tus) and UploadThing. FilePond supports chunked uploads to your server. - q: Are these libraries free? - a: upup, react-dropzone, Uppy, FilePond and react-uploady are MIT-licensed. UploadThing's SDK is MIT, but the service it uploads to is paid with a free tier. + a: upup, react-dropzone, Uppy, FilePond and react-uploady are MIT-licensed. UploadThing's SDK is MIT, but the service it uploads to is paid with a free tier. PrimeReact is MIT through version 10; version 11 moved to the PrimeUI License, which is free for individuals and eligible small organizations and paid otherwise. --- **The short answer:** for a complete React uploader that sends files straight to your own S3-compatible bucket, with cloud drives and resumable uploads, use upup or Uppy. For a bare drop target you style yourself, use react-dropzone. For modular hooks, react-uploady. For one polished component, FilePond. For no -storage setup at all, UploadThing's hosted service. +storage setup at all, UploadThing's hosted service. If your app already uses +PrimeReact, its FileUpload component is the smallest step. -This roundup compares six React file upload libraries on the features that +This roundup compares seven React file upload libraries on the features that decide most projects, then shows how to pick one. ## Comparison table -| Library | Drag and drop | Cloud drives | Resumable | Direct-to-S3 | Image editing | License | Bundle approach | -| -------------- | ------------- | ------------------------------------ | ------------------------------ | --------------------------- | -------------------------------------------------------- | ------------------------------- | ------------------------------------------------------- | -| upup | Yes | Google Drive, OneDrive, Dropbox, Box | Yes (tus or S3 multipart) | Yes (presigned URLs) | Yes | MIT | Headless core + React UI; heavy features load on demand | -| react-dropzone | Yes | No | No | You build it | No | MIT | One small hook; no upload code | -| Uppy | Yes | Many, via Companion | Yes (tus or S3 multipart) | Yes (AWS S3 plugin) | Yes (Image Editor plugin) | MIT | Modular plugins; install only what you use | -| FilePond | Yes | No | Chunked uploads to your server | Not built in | Crop/resize plugins; full editor is Pintura (commercial) | MIT | Vanilla JS core + React adapter + plugins | -| UploadThing | Yes | Not built in | Yes (since v7) | No, files go to UploadThing | Not built in | MIT SDK; hosted service is paid | SDK packages talking to a hosted service | -| react-uploady | Yes | No | Yes, with its tus package | Not built in | No | MIT | Many small `@rpldy/*` packages; hooks-first | +| Library | Drag and drop | Cloud drives | Resumable | Direct-to-S3 | Image editing | License | Bundle approach | +| --------------------- | ------------- | ------------------------------------ | ------------------------------ | ------------------------------------ | -------------------------------------------------------- | ------------------------------------------------------------------------------ | ------------------------------------------------------- | +| upup | Yes | Google Drive, OneDrive, Dropbox, Box | Yes (tus or S3 multipart) | Yes (presigned URLs) | Yes | MIT | Headless core + React UI; heavy features load on demand | +| react-dropzone | Yes | No | No | You build it | No | MIT | One small hook; no upload code | +| Uppy | Yes | Many, via Companion | Yes (tus or S3 multipart) | Yes (AWS S3 plugin) | Yes (Image Editor plugin) | MIT | Modular plugins; install only what you use | +| FilePond | Yes | No | Chunked uploads to your server | Not built in | Crop/resize plugins; full editor is Pintura (commercial) | MIT | Vanilla JS core + React adapter + plugins | +| UploadThing | Yes | Not built in | Yes (since v7) | No, files go to UploadThing | Not built in | MIT SDK; hosted service is paid | SDK packages talking to a hosted service | +| react-uploady | Yes | No | Yes, with its tus package | Not built in | No | MIT | Many small `@rpldy/*` packages; hooks-first | +| PrimeReact FileUpload | Yes | No | No | Not built in (custom upload handler) | No | v11+: PrimeUI License (paid, with a free community tier); v10 and earlier: MIT | Part of the PrimeReact component suite | ## The libraries @@ -86,6 +88,19 @@ react-uploady is a hooks-first React upload library split into many small tus-based resumable uploads and more. You install the pieces you need and build the UI yourself from its hooks and components. +### PrimeReact FileUpload + +FileUpload is one component in the PrimeReact suite. It offers drag and drop, +multiple files, automatic upload, progress, and size and type validation, and +it posts to a URL you give it or, with `customUpload`, hands the files to your +own `uploadHandler`. Version 11 splits it into composable parts such as +`FileUpload.Root` and `FileUpload.Trigger`, plus a headless `useFileUpload` +hook; version 10 is a single component with basic and advanced modes. +PrimeReact 10 and earlier are MIT-licensed. Version 11 moved to PrimeTek's +PrimeUI License, which is free for individuals and eligible small +organizations, paid otherwise, and requires a license key. It's the natural +choice if PrimeReact is already your component library. + ## How to choose - **You want it working today, on your own bucket:** upup or Uppy. Both ship a @@ -96,6 +111,8 @@ the UI yourself from its hooks and components. - **You have a design system and want to build the UI:** react-dropzone for selection only, react-uploady for selection plus upload hooks, or upup's headless core. +- **You already use PrimeReact:** start with PrimeReact FileUpload and switch + only if you need S3, cloud drives or resumable uploads. - **You don't want to run storage at all:** UploadThing. Check their pricing page against your expected volume and egress. - **You upload very large files:** pick a library with resumable uploads: upup, @@ -134,7 +151,8 @@ server handlers; see the [Next.js quickstart](/docs/quickstarts/next/). For a complete uploader against your own storage, upup or Uppy. For the smallest possible building block, react-dropzone. For a hosted service, -UploadThing. The table above shows the trade-offs. +UploadThing. Inside a PrimeReact app, PrimeReact FileUpload. The table above +shows the trade-offs. ### Is react-dropzone enough on its own? @@ -156,7 +174,9 @@ and UploadThing. FilePond supports chunked uploads to your server. upup, react-dropzone, Uppy, FilePond and react-uploady are MIT-licensed. UploadThing's SDK is MIT, but the service it uploads to is paid with a free -tier; see their pricing page. +tier; see their pricing page. PrimeReact is MIT through version 10; version 11 +moved to the PrimeUI License, which is free for individuals and eligible small +organizations and paid otherwise. ## Related comparisons diff --git a/apps/landing/content/docs/comparisons/index.mdx b/apps/landing/content/docs/comparisons/index.mdx index a0a984be..ff3bc907 100644 --- a/apps/landing/content/docs/comparisons/index.mdx +++ b/apps/landing/content/docs/comparisons/index.mdx @@ -88,11 +88,13 @@ focuses on a type-safe upload flow for TypeScript full-stack apps. ## Roundups by framework - [Best React file upload libraries](/docs/comparisons/best-react-file-upload-libraries/): - upup, react-dropzone, Uppy, FilePond, UploadThing and react-uploady. + upup, react-dropzone, Uppy, FilePond, UploadThing, react-uploady and + PrimeReact FileUpload. - [Best Vue file upload libraries](/docs/comparisons/best-vue-file-upload-libraries/): upup, vue-upload-component, PrimeVue FileUpload, FilePond and Uppy. - [Best Angular file upload libraries](/docs/comparisons/best-angular-file-upload-libraries/): - upup, ngx-file-drop, PrimeNG FileUpload, FilePond and Uppy. + upup, ngx-file-drop, ng2-file-upload, PrimeNG FileUpload, DevExtreme + FileUploader, FilePond and Uppy. ## Head-to-head comparisons From 306cfbb9ae32914ed50316d7738b464bdec77c9b Mon Sep 17 00:00:00 2001 From: AminDhouib Date: Sun, 27 Sep 2026 03:22:07 +0000 Subject: [PATCH 2/5] docs(landing): correct PrimeVue and vue-upload-component licenses in the Vue roundup The Vue roundup called all five libraries MIT-licensed. Two are not: vue-upload-component is Apache-2.0 (its npm license field and GitHub repo), and PrimeVue 5 (2026-07-15) moved from MIT to PrimeTek's PrimeUI License, the same change PrimeNG 22 and PrimeReact 11 made. The table, the PrimeVue section and the "Are these libraries free?" answer now say so. --- .../best-vue-file-upload-libraries.mdx | 26 ++++++++++++------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/apps/landing/content/docs/comparisons/best-vue-file-upload-libraries.mdx b/apps/landing/content/docs/comparisons/best-vue-file-upload-libraries.mdx index 18e8d0e9..964963b4 100644 --- a/apps/landing/content/docs/comparisons/best-vue-file-upload-libraries.mdx +++ b/apps/landing/content/docs/comparisons/best-vue-file-upload-libraries.mdx @@ -11,7 +11,7 @@ faq: - q: Which Vue upload libraries support resumable uploads? a: upup and Uppy support tus and S3 multipart. vue-upload-component and FilePond support chunked uploads to your own server. - q: Are these libraries free? - a: Yes. All five are MIT-licensed. FilePond's full image editor, Pintura, is a separate commercial product. + a: Most are. upup, FilePond and Uppy are MIT-licensed, and vue-upload-component is Apache-2.0-licensed. PrimeVue is MIT through version 4; version 5 moved to the PrimeUI License, which is free for individuals and eligible small organizations and paid otherwise. FilePond's full image editor, Pintura, is a separate commercial product. --- **The short answer:** for a Vue 3 uploader that sends files straight to your own @@ -25,13 +25,13 @@ usually decide the choice, then explains how to pick. ## Comparison table -| Library | Drag and drop | Cloud drives | Resumable | Direct-to-S3 | Image editing | License | Bundle approach | -| -------------------- | ------------- | ------------------------------------ | ------------------------------ | ------------------------------------ | -------------------------------------------------------- | ------- | ------------------------------------------------------- | -| upup | Yes | Google Drive, OneDrive, Dropbox, Box | Yes (tus or S3 multipart) | Yes (presigned URLs) | No (the editor is React/Preact only) | MIT | Headless core + native Vue UI; heavy features on demand | -| vue-upload-component | Yes | No | Chunked uploads | Not built in | No | MIT | One component; you build the UI around it | -| PrimeVue FileUpload | Yes | No | No | Not built in (custom upload handler) | No | MIT | Part of the PrimeVue component suite | -| FilePond | Yes | No | Chunked uploads to your server | Not built in | Crop/resize plugins; full editor is Pintura (commercial) | MIT | Vanilla JS core + Vue adapter + plugins | -| Uppy | Yes | Many, via Companion | Yes (tus or S3 multipart) | Yes (AWS S3 plugin) | Yes (Image Editor plugin) | MIT | Modular plugins; install only what you use | +| Library | Drag and drop | Cloud drives | Resumable | Direct-to-S3 | Image editing | License | Bundle approach | +| -------------------- | ------------- | ------------------------------------ | ------------------------------ | ------------------------------------ | -------------------------------------------------------- | ---------------------------------------------------------------------------- | ------------------------------------------------------- | +| upup | Yes | Google Drive, OneDrive, Dropbox, Box | Yes (tus or S3 multipart) | Yes (presigned URLs) | No (the editor is React/Preact only) | MIT | Headless core + native Vue UI; heavy features on demand | +| vue-upload-component | Yes | No | Chunked uploads | Not built in | No | Apache-2.0 | One component; you build the UI around it | +| PrimeVue FileUpload | Yes | No | No | Not built in (custom upload handler) | No | v5+: PrimeUI License (paid, with a free community tier); v4 and earlier: MIT | Part of the PrimeVue component suite | +| FilePond | Yes | No | Chunked uploads to your server | Not built in | Crop/resize plugins; full editor is Pintura (commercial) | MIT | Vanilla JS core + Vue adapter + plugins | +| Uppy | Yes | Many, via Companion | Yes (tus or S3 multipart) | Yes (AWS S3 plugin) | Yes (Image Editor plugin) | MIT | Modular plugins; install only what you use | ## The libraries @@ -58,7 +58,10 @@ Check that you install the major version that matches Vue 3. FileUpload is one component in the PrimeVue suite. It offers a basic button mode and an advanced mode with a drop zone, file list and progress bar, and it posts to a URL you give it or hands the files to your own upload function. -It's the natural choice if PrimeVue is already your component library. +PrimeVue 4 and earlier are MIT-licensed. Version 5 moved to PrimeTek's PrimeUI +License, which is free for individuals and eligible small organizations, paid +otherwise, and requires a license key. It's the natural choice if PrimeVue is +already your component library. ### FilePond @@ -140,7 +143,10 @@ support chunked uploads to your own server. ### Are these libraries free? -Yes. All five are MIT-licensed. FilePond's full image editor, Pintura, is a +Most are. upup, FilePond and Uppy are MIT-licensed, and vue-upload-component +is Apache-2.0-licensed. PrimeVue is MIT through version 4; version 5 moved to +the PrimeUI License, which is free for individuals and eligible small +organizations and paid otherwise. FilePond's full image editor, Pintura, is a separate commercial product. ## Related comparisons From 34e645f0f5c662c26eeb4afab7aa5007a9600a25 Mon Sep 17 00:00:00 2001 From: AminDhouib Date: Sun, 27 Sep 2026 03:55:30 +0000 Subject: [PATCH 3/5] fix(landing): breadcrumb JSON-LD MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Every docs page nested in a sidebar folder that has no URL of its own emitted a BreadcrumbList whose middle ListItems carried only a name. (comparisons/ has a hub page, but its meta.json lists `index` as a child page, so fumadocs leaves the folder node itself without a URL.) schema.org allows that, but Google's breadcrumb rich result requires `item` on every ListItem except the last (https://developers.google.com/search/docs/appearance/structured-data/breadcrumb), so those trails were invalid for the rich result. DocsStructuredData now drops name-only folder crumbs from the JSON-LD trail and numbers positions 1..n over what remains. The page's own crumb is always last and keeps its URL. The visual is unchanged. The new test walks every docs page, not a sample. In each BreadcrumbList, every ListItem except the last must have an `item`, positions must run 1..n, every `item` must be an existing docs page or /docs/, and the last crumb must be the page itself. RED against the unfixed component (vitest run src/__tests__/docs-structured-data.test.ts, exit 1; 85 entries, elided): FAIL src/__tests__/docs-structured-data.test.ts > docs BreadcrumbList JSON-LD meets the Google breadcrumb rules > numbers every trail 1..n and links every crumb but the last to a docs page AssertionError: expected [ …(85) ] to deeply equal [] - Expected + Received - [] + [ + "/docs/api-reference/azure-generate-sas-url: \"API reference\" has no item", + "/docs/api-reference/error-codes: \"API reference\" has no item", + "/docs/api-reference/events: \"API reference\" has no item", ... + "/docs/comparisons/upup-vs-uppy: \"Comparisons\" has no item", ... + "/docs/guides/storage/azure-blob: \"Guides\" has no item", + "/docs/guides/storage/azure-blob: \"Storage\" has no item", ... + "/docs/migration/v1-to-v3: \"Migration\" has no item", + "/docs/quickstarts/angular: \"Quickstarts\" has no item", ... + "/docs/quickstarts/vue: \"Quickstarts\" has no item", + ] Test Files 1 failed (1) Tests 1 failed | 27 passed (28) GREEN after the fix: the same file passes 28/28 (exit 0), and the full landing suite passes 246/246 across 20 files (exit 0). --- .../__tests__/docs-structured-data.test.ts | 51 +++++++++++++++++++ .../components/docs/DocsStructuredData.tsx | 15 ++++-- 2 files changed, 62 insertions(+), 4 deletions(-) diff --git a/apps/landing/src/__tests__/docs-structured-data.test.ts b/apps/landing/src/__tests__/docs-structured-data.test.ts index e8c7d40d..dfffcfc4 100644 --- a/apps/landing/src/__tests__/docs-structured-data.test.ts +++ b/apps/landing/src/__tests__/docs-structured-data.test.ts @@ -204,6 +204,57 @@ describe('docs FAQPage JSON-LD is opt-in per page', () => { }) }) +describe('docs BreadcrumbList JSON-LD meets the Google breadcrumb rules', () => { + // Google requires `item` on every ListItem except the last + // (https://developers.google.com/search/docs/appearance/structured-data/breadcrumb). + // schema.org alone accepts a name-only crumb, so this walks every docs page + // rather than a sample. The docs root renders , which emits no + // per-page JSON-LD. + const pages = source.getPages().filter(page => page.slugs.length > 0) + const docsUrls = new Set([ + `${PRODUCTION_ORIGIN}/docs/`, + ...pages.map(page => `${PRODUCTION_ORIGIN}${normalizeUrl(page.url)}/`), + ]) + + it('numbers every trail 1..n and links every crumb but the last to a docs page', () => { + expect(pages.length).toBeGreaterThan(0) + const violations: string[] = [] + for (const page of pages) { + const where = normalizeUrl(page.url) + const graph = renderGraph(propsFor(page.slugs)) + const list = graph.find(node => node['@type'] === 'BreadcrumbList') + const crumbs = (list?.itemListElement ?? []) as { + '@type': string + position: number + name: string + item?: string + }[] + if (crumbs.length === 0) violations.push(`${where}: no ListItem`) + crumbs.forEach((crumb, i) => { + const label = `${where}: "${crumb.name}"` + if (crumb['@type'] !== 'ListItem') + violations.push(`${label} is a ${crumb['@type']}`) + if (crumb.position !== i + 1) + violations.push( + `${label} at position ${crumb.position}, expected ${i + 1}`, + ) + if (crumb.item === undefined) { + if (i < crumbs.length - 1) + violations.push(`${label} has no item`) + } else if (!docsUrls.has(crumb.item)) { + violations.push( + `${label} item ${crumb.item} is not a docs page`, + ) + } + }) + const last = crumbs[crumbs.length - 1] + if (last && last.item !== `${PRODUCTION_ORIGIN}${where}/`) + violations.push(`${where}: last crumb is not the page itself`) + } + expect(violations).toEqual([]) + }) +}) + describe('docs folder hub pages', () => { it.each(HUBS)( 'serves /docs/$dir/ as a page listed in the sitemap', diff --git a/apps/landing/src/components/docs/DocsStructuredData.tsx b/apps/landing/src/components/docs/DocsStructuredData.tsx index 66e5b2e1..4d7d1b76 100644 --- a/apps/landing/src/components/docs/DocsStructuredData.tsx +++ b/apps/landing/src/components/docs/DocsStructuredData.tsx @@ -45,15 +45,22 @@ export function DocsStructuredData({ ...trail.map(node => ({ name: node.name, url: node.url })), ] + // Google requires `item` on every ListItem except the last + // (https://developers.google.com/search/docs/appearance/structured-data/breadcrumb), + // and a folder with no index page has no URL to point at. So the trail + // drops those name-only folder crumbs rather than fabricating a target, + // and positions are numbered over what remains. The page's own crumb is + // always last and always keeps its URL. + const linkedCrumbs = crumbs.filter( + (crumb, i) => crumb.url || i === crumbs.length - 1, + ) + const breadcrumbList = { '@type': 'BreadcrumbList', - itemListElement: crumbs.map((crumb, i) => ({ + itemListElement: linkedCrumbs.map((crumb, i) => ({ '@type': 'ListItem', position: i + 1, name: crumb.name, - // A folder with no index page has no URL to point at; schema.org - // allows a ListItem to carry only a name, so omit `item` instead - // of fabricating a target. ...(crumb.url ? { item: canonicalUrl(crumb.url) } : {}), })), } From bb83b07f890a2f813a1d6e02b98538ec8c9bdbbd Mon Sep 17 00:00:00 2001 From: AminDhouib Date: Sun, 27 Sep 2026 04:05:27 +0000 Subject: [PATCH 4/5] feat(landing): docs hub pages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add index pages for guides/, guides/storage/ and quickstarts/, carved from #445. Each folder now has a real URL, so its sidebar label is a link and its breadcrumb crumb is a link. The Azure trail is now Docs (/docs/) → Guides (/docs/guides/) → Storage (/docs/guides/storage/) → page. The Guides, Storage and Quickstarts crumbs that the previous commit dropped from the JSON-LD for having no URL are back, now with one. The hubs list their child pages. I re-checked every claim against the current dev docs: - The storage hub says what the modes and storage-providers guides say: in both modes the browser uploads bytes directly to the bucket over short-lived signed URLs, and server mode signs those URLs and writes cloud-drive files to storage itself. - Azure Blob is client mode only: createUpupHandler rejects it. - The provider list matches guides/storage/, the framework list matches quickstarts/, and the peer ranges match packages/*/package.json: React 19, Vue 3.4+, Svelte 5, Angular 19 (^19.0.0), Next.js 15+. Changes from #445's versions: - No MDX link points at a nested hub URL. dev's scripts/docs/check-links.mjs still resolves guides/storage/index.mdx to /docs/guides/storage/index, and a probe link to /docs/guides/storage/ failed it with "page /docs/guides/storage does not exist". So the guides hub links each storage page directly, and its "New to upup?" line points at Getting Started instead of /docs/quickstarts/. - The quickstarts description no longer says "the same features everywhere". The image editor ships for React and Preact only, and the hub now says so, in the FAQ's words. - The storage hub no longer says every guide covers a CORS rule. s3-compatible.mdx has no CORS step. guides/meta.json gains "title": "Guides" so the folder keeps its sidebar name now that it has an index page. The DocsPageNav comment's example of a URL-less folder moves from Quickstarts to Auth. Pins: the docs page count goes from 69 to 72 in docs-source, docs-llms and the seo-surfaces sitemap total. docs-structured-data now covers all four hubs: - the sitemap, llms.txt and markdown-twin checks run for every hub; - the served-twin check uses guides/storage, the two-level case; - "links every child" leaves out only the nested hub page itself; - a new test pins the full Azure BreadcrumbList. --- apps/landing/content/docs/guides/index.mdx | 90 +++++++++++++++++++ apps/landing/content/docs/guides/meta.json | 1 + .../content/docs/guides/storage/index.mdx | 46 ++++++++++ .../content/docs/quickstarts/index.mdx | 35 ++++++++ apps/landing/src/__tests__/docs-llms.test.ts | 11 +-- .../landing/src/__tests__/docs-source.test.ts | 15 ++-- .../__tests__/docs-structured-data.test.ts | 57 ++++++++++-- .../src/__tests__/seo-surfaces.test.ts | 6 +- .../src/components/docs/DocsPageNav.tsx | 2 +- 9 files changed, 241 insertions(+), 22 deletions(-) create mode 100644 apps/landing/content/docs/guides/index.mdx create mode 100644 apps/landing/content/docs/guides/storage/index.mdx create mode 100644 apps/landing/content/docs/quickstarts/index.mdx diff --git a/apps/landing/content/docs/guides/index.mdx b/apps/landing/content/docs/guides/index.mdx new file mode 100644 index 00000000..797044d5 --- /dev/null +++ b/apps/landing/content/docs/guides/index.mdx @@ -0,0 +1,90 @@ +--- +title: Guides +description: Every upup guide in one place — client vs server mode, server adapters, auth, storage providers, file processing, sources, reliability, theming, and plugins. +--- + +Task-focused guides for running upup in production: picking a mode, mounting +the server package on your framework, scoping uploads to your users, connecting +a storage provider, processing files before they upload, and making the +uploader look and behave the way your app needs. New to upup? Start with +[Getting Started](/docs/getting-started/) and the quickstart for your framework. + +## Modes and server setup + +- [Client Mode vs Server Mode](/docs/guides/modes/) — what runs where in each + mode, and when to pick each. +- [Server Mode Setup](/docs/guides/server-mode-setup/) — mount + `createUpupHandler`, configure S3 storage, secrets, limits and hooks. +- [Express](/docs/guides/server-adapters/express/) — mount `@useupup/server` on + an Express route with `createUpupMiddleware`. +- [Fastify](/docs/guides/server-adapters/fastify/) — register `@useupup/server` + as a Fastify plugin with `createUpupPlugin`. +- [Hono](/docs/guides/server-adapters/hono/) — the web-native mount with + `createUpupRoutes`, including edge runtimes. +- [Next.js](/docs/guides/server-adapters/nextjs/) — App Router and Pages Router + handlers from `@useupup/next`. + +## Auth + +- [Server Auth & Trust Model](/docs/guides/server-auth/) — the HMAC upload-token + secret, the secure-by-default 403 on anonymous uploads, and per-user scoping. +- [Auth Recipes](/docs/guides/auth-recipes/) — one `getUserId` hook, plus Redis + and SQL token stores. +- [Better Auth](/docs/guides/auth/better-auth/) — scope every upload to the + signed-in Better Auth session. +- [NextAuth (Auth.js v5)](/docs/guides/auth/next-auth/) — the `auth()` helper + in the App Router and `getToken` outside a request. +- [Clerk](/docs/guides/auth/clerk/) — `authenticateRequest` inside `getUserId`, + and why `authorizedParties` is mandatory. +- [Custom JWT](/docs/guides/auth/custom-jwt/) — verify your own bearer or cookie + JWT with `jose`, including remote JWKS. + +## Storage + +- [Storage Providers](/docs/guides/storage-providers/) — how upup connects to + S3-compatible storage, the full `storage.type` table, and why Azure Blob + Storage is the exception. +- [Amazon S3](/docs/guides/storage/aws-s3/) — bucket setup, the IAM policy, + CORS, and a `createUpupHandler` config. +- [Cloudflare R2](/docs/guides/storage/cloudflare-r2/) — the account-scoped + endpoint, region `auto`, API tokens, and CORS. +- [Backblaze B2](/docs/guides/storage/backblaze-b2/) — application keys, the + region-scoped S3 endpoint, and CORS. +- [DigitalOcean Spaces](/docs/guides/storage/digitalocean-spaces/) — Spaces + access keys, the regional endpoint, CORS, and the CDN. +- [MinIO](/docs/guides/storage/minio/) — a self-hosted MinIO server with Docker, + path-style addressing, and CORS. +- [Azure Blob Storage](/docs/guides/storage/azure-blob/) — client mode only: a + SAS URL from your endpoint and the mandatory `x-ms-blob-type` header. +- [Any S3-compatible storage](/docs/guides/storage/s3-compatible/) — Wasabi, + Google Cloud Storage, Supabase, Hetzner, Scaleway, Storj, and more. + +## Files and sources + +- [File Processing](/docs/guides/file-processing/) — the client-side pipeline: + compression, HEIC conversion, EXIF stripping, thumbnails, and checksums. +- [Image compression](/docs/guides/processing/compression/) — compress and + resize images in the browser before they upload. +- [HEIC to JPEG](/docs/guides/processing/heic-conversion/) — convert iPhone + HEIC/HEIF photos with the optional libheif decoder. +- [Custom pipeline steps](/docs/guides/processing/custom-steps/) — write your + own `PipelineStep`, with Web Worker offload. +- [Upload Sources](/docs/guides/sources/) — local files, drag and drop, paste, + folders, camera, microphone, screen capture, URL import, and cloud drives. +- [Reliability](/docs/guides/reliability/) — per-file retries and backoff, + upload concurrency, and multipart resume. +- [Error Monitoring](/docs/guides/error-monitoring/) — send `upload-error` + events and `UpupError` codes to your error tracker. + +## UI and extensibility + +- [Theming](/docs/guides/theming/) — light, dark and system modes, design + tokens, and slot class overrides. +- [Accessibility](/docs/guides/accessibility/) — keyboard interaction, ARIA + live regions, focus management, and reduced motion. +- [Headless Usage](/docs/guides/headless/) — build your own UI on the upup + engine with `useUpupUpload` or `UpupCore`. +- [Plugins & Extensions](/docs/guides/plugins/) — enable the built-in + cloud-drive plugins and register third-party ones. +- [Write a custom plugin](/docs/guides/writing-plugins/) — the `UpupPlugin` + contract, namespaced events, and `PopupOAuthPlugin`. diff --git a/apps/landing/content/docs/guides/meta.json b/apps/landing/content/docs/guides/meta.json index 13508df5..905a1ca3 100644 --- a/apps/landing/content/docs/guides/meta.json +++ b/apps/landing/content/docs/guides/meta.json @@ -1,4 +1,5 @@ { + "title": "Guides", "pages": [ "modes", "server-mode-setup", diff --git a/apps/landing/content/docs/guides/storage/index.mdx b/apps/landing/content/docs/guides/storage/index.mdx new file mode 100644 index 00000000..f77c3b5c --- /dev/null +++ b/apps/landing/content/docs/guides/storage/index.mdx @@ -0,0 +1,46 @@ +--- +title: Storage provider guides +description: Upload files from the browser to AWS S3, Cloudflare R2, Backblaze B2, DigitalOcean Spaces, MinIO, Azure Blob, or any S3-compatible store with upup. +--- + +upup uploads straight to storage you own. In both modes the browser uploads file +bytes directly to your bucket over short-lived signed URLs, and the storage +credentials never leave your server. In client mode your own endpoint signs +those URLs. In server mode [`@useupup/server`](/docs/guides/server-mode-setup/) +holds the credentials, signs the URLs for you, and writes cloud-drive files to +storage itself. + +Each guide below walks through the setup for its provider and includes a +config you can copy. For the full `storage.type` table and the shared `storage` +config reference, read [Storage Providers](/docs/guides/storage-providers/). + +## S3-compatible providers + +These work in client mode and in server mode. In server mode you pick one with +the `storage.type` value in your `createUpupHandler` config. + +- [Amazon S3](/docs/guides/storage/aws-s3/) — bucket setup, the IAM policy, + CORS, and a `createUpupHandler` config. +- [Cloudflare R2](/docs/guides/storage/cloudflare-r2/) — the account-scoped + endpoint, region `auto`, API tokens, and CORS. +- [Backblaze B2](/docs/guides/storage/backblaze-b2/) — application keys, the + region-scoped S3 endpoint, and CORS. +- [DigitalOcean Spaces](/docs/guides/storage/digitalocean-spaces/) — Spaces + access keys, the regional endpoint, CORS, and the CDN. +- [MinIO](/docs/guides/storage/minio/) — a self-hosted MinIO server with Docker, + path-style addressing, and CORS. +- [Any S3-compatible storage](/docs/guides/storage/s3-compatible/) — Wasabi, + Google Cloud Storage, Supabase, Hetzner, Scaleway, Storj, and any other store + that speaks the S3 API. + +## Azure Blob Storage + +Azure Blob Storage has no S3-compatible API, so `createUpupHandler` rejects it +and server mode cannot serve it. + +- [Azure Blob Storage](/docs/guides/storage/azure-blob/) — client mode only: + your endpoint returns a SAS URL, the browser uploads straight to the blob, + and the guide covers the mandatory `x-ms-blob-type` header. + +Not sure which mode you need? [Client Mode vs Server Mode](/docs/guides/modes/) +covers what runs where in each. diff --git a/apps/landing/content/docs/quickstarts/index.mdx b/apps/landing/content/docs/quickstarts/index.mdx new file mode 100644 index 00000000..36ffcfd8 --- /dev/null +++ b/apps/landing/content/docs/quickstarts/index.mdx @@ -0,0 +1,35 @@ +--- +title: Framework quickstarts +description: Add the upup file uploader to React, Vue, Svelte, Angular, Vanilla JS, Preact, or Next.js — one install per framework and the same drag-and-drop UI. +--- + +Pick your framework and get a working uploader. Every quickstart starts in +client mode: the browser uploads straight to your storage, your app only issues +short-lived upload credentials at `uploadEndpoint`, and there is no upup server +package to run. Each one then shows how to add server mode, where +[`@useupup/server`](/docs/guides/server-mode-setup/) signs uploads and runs +cloud drives on your own server. For the concepts behind them, see +[Getting Started](/docs/getting-started/). + +- [React](/docs/quickstarts/react/) — `@useupup/react`, the canonical upup UI, + for React 19 apps. +- [Vue](/docs/quickstarts/vue/) — `@useupup/vue` for Vue 3.4+ apps. +- [Svelte](/docs/quickstarts/svelte/) — `@useupup/svelte` for Svelte 5 apps. +- [Angular](/docs/quickstarts/angular/) — `@useupup/angular`, a standalone + component for Angular 19 apps. +- [Vanilla JS](/docs/quickstarts/vanilla/) — `@useupup/vanilla`, framework-free, + for any page. +- [Preact](/docs/quickstarts/preact/) — `@useupup/preact`, a `preact/compat` + re-export of the React UI. +- [Next.js](/docs/quickstarts/next/) — `@useupup/next`, client UI and server + handlers in one install. + +## The same uploader in every framework + +The Vue, Svelte, Angular and Vanilla JS packages are native ports that render +the same DOM as the React UI. Cloud drives, camera, screen capture, link import, +compression, HEIC conversion, resumable uploads, i18n, and theming work the +same in every framework. The one difference is the image editor (crop, rotate, +annotate), which ships for React and Preact only; the other frameworks +intentionally stub it. Next.js re-exports the React UI, so it includes the +editor too. diff --git a/apps/landing/src/__tests__/docs-llms.test.ts b/apps/landing/src/__tests__/docs-llms.test.ts index f89250cd..4e86a2b5 100644 --- a/apps/landing/src/__tests__/docs-llms.test.ts +++ b/apps/landing/src/__tests__/docs-llms.test.ts @@ -17,20 +17,21 @@ describe('llms corpus', () => { expect(full.length).toBeGreaterThan(20_000) }) - it('index lists 69 pages and full contains all 69 page bodies', () => { + it('index lists 72 pages and full contains all 72 page bodies', () => { // Pinned to the docs page inventory (same count as docs-source.test.ts) — // bump deliberately when pages are added/removed. 36 + 9 (2026-08 // coverage sprint) + 19 (2026-08 SEO split: 7 storage + 4 auth + // 4 server-adapters + 3 processing + writing-plugins) = 64, + 1 // (2026-09 FAQ) = 65, + 4 (2026-09 comparisons hub + React/Vue/Angular - // roundups) = 69. The "## Start here" links above the page list are - // plain lines, not `- [` bullets, so they do not count here. + // roundups) = 69, + 3 (2026-09 folder hubs: guides/, guides/storage/, + // quickstarts/) = 72. The "## Start here" links above the page list + // are plain lines, not `- [` bullets, so they do not count here. const index = buildLlmsIndex() const linkCount = (index.match(/^- \[/gm) ?? []).length - expect(linkCount).toBe(69) + expect(linkCount).toBe(72) const full = buildLlmsFull() const pageCount = (full.match(/\n---\n/g)?.length ?? 0) + 1 - expect(pageCount).toBe(69) + expect(pageCount).toBe(72) }) }) diff --git a/apps/landing/src/__tests__/docs-source.test.ts b/apps/landing/src/__tests__/docs-source.test.ts index 8452ac46..a58eacbe 100644 --- a/apps/landing/src/__tests__/docs-source.test.ts +++ b/apps/landing/src/__tests__/docs-source.test.ts @@ -27,8 +27,9 @@ describe('docs source', () => { // plugins) = 45, + 19 (2026-08 SEO split: 7 guides/storage + // 4 guides/auth + 4 guides/server-adapters + 3 guides/processing + // writing-plugins) = 64, + 1 (2026-09 FAQ) = 65, + 4 (2026-09 - // comparisons hub + React/Vue/Angular roundups) = 69. - expect(pages.length).toBe(69) + // comparisons hub + React/Vue/Angular roundups) = 69, + 3 (2026-09 + // folder hubs: guides/, guides/storage/, quickstarts/) = 72. + expect(pages.length).toBe(72) const indexPage = source.getPage([]) // index.mdx expect(indexPage).toBeDefined() expect(indexPage?.data.body).toBeDefined() @@ -41,10 +42,12 @@ describe('docs source', () => { }) it('every internal /docs link in the corpus resolves to a real page', () => { - // Section folders (quickstarts/, guides/, api-reference/, ...) have NO - // index pages — a link to a bare section URL 404s in production (and - // next/link prefetch surfaces it as a console error on every page that - // renders the link). This walked twice before this pin existed. + // Only some section folders have a hub (index) page — guides/, + // guides/storage/, quickstarts/ and comparisons/ do; api-reference/, + // guides/auth/ and the rest do NOT, and a link to a hub-less section + // URL 404s in production (next/link prefetch surfaces it as a console + // error on every page that renders the link). This walked twice + // before this pin existed. const validUrls = new Set( source .getPages() diff --git a/apps/landing/src/__tests__/docs-structured-data.test.ts b/apps/landing/src/__tests__/docs-structured-data.test.ts index dfffcfc4..7bedf56e 100644 --- a/apps/landing/src/__tests__/docs-structured-data.test.ts +++ b/apps/landing/src/__tests__/docs-structured-data.test.ts @@ -22,7 +22,12 @@ import { source } from '@/lib/docs/source' const PRODUCTION_ORIGIN = 'https://useupup.com' const CONTENT_DIR = new URL('../../content/docs/', import.meta.url) -const HUBS = [{ slug: ['comparisons'], dir: 'comparisons' }] as const +const HUBS = [ + { slug: ['guides'], dir: 'guides' }, + { slug: ['guides', 'storage'], dir: 'guides/storage' }, + { slug: ['quickstarts'], dir: 'quickstarts' }, + { slug: ['comparisons'], dir: 'comparisons' }, +] as const // Every docs page that carries `faq:` frontmatter, with its visible question // count. The FAQ page asks each question as a `## ` heading; every other page @@ -272,14 +277,53 @@ describe('docs folder hub pages', () => { it.each(HUBS)('links every child of $dir from its hub', ({ dir }) => { const hub = readContent(`${dir}/index.mdx`) const prefix = `/docs/${dir}/` + // A nested hub page itself is not linked: the docs link check + // (scripts/docs/check-links.mjs) still resolves guides/storage/index.mdx + // to /docs/guides/storage/index, so an MDX link to /docs/guides/storage/ + // fails it. The nested hub's own children are linked from here too. + const nestedHubs = HUBS.map(other => `/docs/${other.dir}`).filter( + other => other.startsWith(prefix), + ) const children = source .getPages() .map(page => normalizeUrl(page.url)) .filter(url => url.startsWith(prefix)) + .filter(url => !nestedHubs.includes(url)) expect(children.length).toBeGreaterThan(0) const missing = children.filter(url => !hub.includes(`](${url}/)`)) expect(missing).toEqual([]) }) + + it('links every breadcrumb middle crumb to its hub on a nested page', () => { + const graph = renderGraph(propsFor(['guides', 'storage', 'azure-blob'])) + const list = graph.find(node => node['@type'] === 'BreadcrumbList') + expect(list?.itemListElement).toEqual([ + { + '@type': 'ListItem', + position: 1, + name: 'Docs', + item: `${PRODUCTION_ORIGIN}/docs/`, + }, + { + '@type': 'ListItem', + position: 2, + name: 'Guides', + item: `${PRODUCTION_ORIGIN}/docs/guides/`, + }, + { + '@type': 'ListItem', + position: 3, + name: 'Storage', + item: `${PRODUCTION_ORIGIN}/docs/guides/storage/`, + }, + { + '@type': 'ListItem', + position: 4, + name: 'Upload Files to Azure Blob Storage from the Browser', + item: `${PRODUCTION_ORIGIN}/docs/guides/storage/azure-blob/`, + }, + ]) + }) }) describe('nested index pages map to their folder slug in the agent surfaces', () => { @@ -311,16 +355,15 @@ describe('nested index pages map to their folder slug in the agent surfaces', () expect(params).toContain(slug.join('/')) } + // The two-level hub: guides/storage/index.mdx. const res = await markdownTwin( - new Request(`${PRODUCTION_ORIGIN}/docs-md/comparisons/`), - { params: Promise.resolve({ slug: ['comparisons'] }) }, + new Request(`${PRODUCTION_ORIGIN}/docs-md/guides/storage/`), + { params: Promise.resolve({ slug: ['guides', 'storage'] }) }, ) expect(res.status).toBe(200) expect(res.headers.get('link')).toBe( - `<${PRODUCTION_ORIGIN}/docs/comparisons/>; rel="canonical"`, - ) - expect(await res.text()).toMatch( - /^# JavaScript File Upload Libraries Compared\n/, + `<${PRODUCTION_ORIGIN}/docs/guides/storage/>; rel="canonical"`, ) + expect(await res.text()).toMatch(/^# Storage provider guides\n/) }) }) diff --git a/apps/landing/src/__tests__/seo-surfaces.test.ts b/apps/landing/src/__tests__/seo-surfaces.test.ts index 977138b2..cedd941b 100644 --- a/apps/landing/src/__tests__/seo-surfaces.test.ts +++ b/apps/landing/src/__tests__/seo-surfaces.test.ts @@ -64,12 +64,12 @@ function nodeOfType( describe('sitemap enumerates only canonical, indexable page URLs', () => { const entries = sitemap() - it('lists the homepage, six framework pages, support and privacy, the agent-setup pages, and all 69 docs pages', () => { + it('lists the homepage, six framework pages, support and privacy, the agent-setup pages, and all 72 docs pages', () => { // 1 home + 6 frameworks + support + privacy + agent-setup index + 4 - // per-agent guides + 69 fumadocs pages. The docs count is + // per-agent guides + 72 fumadocs pages. The docs count is // independently pinned by docs-source.test.ts, so a page added to // content/docs updates both or neither. - expect(entries).toHaveLength(1 + 6 + 2 + 1 + 4 + 69) + expect(entries).toHaveLength(1 + 6 + 2 + 1 + 4 + 72) }) it('points every entry at the production origin with the trailing slash the site actually serves', () => { diff --git a/apps/landing/src/components/docs/DocsPageNav.tsx b/apps/landing/src/components/docs/DocsPageNav.tsx index adc0f3a2..79d36c76 100644 --- a/apps/landing/src/components/docs/DocsPageNav.tsx +++ b/apps/landing/src/components/docs/DocsPageNav.tsx @@ -12,7 +12,7 @@ interface FlatPage { // order is byte-identical to the visible sidebar order. Separators set the // section label for their following siblings; a folder with an index url is a // navigable page in its own right, and also sets the section for its children. -// Folders without a url (e.g. Quickstarts) are section labels only, not pages. +// Folders without a url (e.g. Auth) are section labels only, not pages. function flatten( nodes: SidebarNode[], section: string | undefined, From d0a8f49cb5c28d078d7fb839db559b639705b6fd Mon Sep 17 00:00:00 2001 From: AminDhouib Date: Sun, 27 Sep 2026 04:10:24 +0000 Subject: [PATCH 5/5] fix(landing): comparisons folder index MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit comparisons/meta.json listed "index" in `pages`. fumadocs then treats comparisons/index.mdx as an ordinary child page and gives the folder itself no index. So the sidebar's "Comparisons" label was plain text, with the hub repeated below it as a child entry, and every comparison page's breadcrumb had a Comparisons crumb with no URL. The breadcrumb JSON-LD fix earlier on this branch drops such crumbs, so those pages emitted only Docs → page. With "index" removed, the hub is the folder index: - The sidebar "Comparisons" label links to /docs/comparisons/. The seven comparison pages keep their order below it. - Comparison pages emit Docs → Comparisons (/docs/comparisons/) → page. - /docs/comparisons/ still returns 200 with an unchanged MDX source, title, H1, canonical URL and markdown twin. - Prev/next is unchanged: faq → comparisons → best-react → best-vue ... The hub-trail test becomes a table and pins the full trail for best-angular-file-upload-libraries next to the Azure one. Against the old meta.json it fails (exit 1): - "item": "https://useupup.com/docs/comparisons/", - "name": "Comparisons", - "position": 2, ... - "position": 3, + "position": 2, apps/e2e-test/landing/docs.spec.ts has no sidebar, prev/next or overview assertion for comparisons. It has only the /documentation/comparisons redirect, which next.config.mjs owns. --- .../content/docs/comparisons/meta.json | 1 - .../__tests__/docs-structured-data.test.ts | 69 +++++++++++-------- 2 files changed, 39 insertions(+), 31 deletions(-) diff --git a/apps/landing/content/docs/comparisons/meta.json b/apps/landing/content/docs/comparisons/meta.json index c61cef80..1a5f9c51 100644 --- a/apps/landing/content/docs/comparisons/meta.json +++ b/apps/landing/content/docs/comparisons/meta.json @@ -1,7 +1,6 @@ { "title": "Comparisons", "pages": [ - "index", "best-react-file-upload-libraries", "best-vue-file-upload-libraries", "best-angular-file-upload-libraries", diff --git a/apps/landing/src/__tests__/docs-structured-data.test.ts b/apps/landing/src/__tests__/docs-structured-data.test.ts index 7bedf56e..a57cb09c 100644 --- a/apps/landing/src/__tests__/docs-structured-data.test.ts +++ b/apps/landing/src/__tests__/docs-structured-data.test.ts @@ -294,36 +294,45 @@ describe('docs folder hub pages', () => { expect(missing).toEqual([]) }) - it('links every breadcrumb middle crumb to its hub on a nested page', () => { - const graph = renderGraph(propsFor(['guides', 'storage', 'azure-blob'])) - const list = graph.find(node => node['@type'] === 'BreadcrumbList') - expect(list?.itemListElement).toEqual([ - { - '@type': 'ListItem', - position: 1, - name: 'Docs', - item: `${PRODUCTION_ORIGIN}/docs/`, - }, - { - '@type': 'ListItem', - position: 2, - name: 'Guides', - item: `${PRODUCTION_ORIGIN}/docs/guides/`, - }, - { - '@type': 'ListItem', - position: 3, - name: 'Storage', - item: `${PRODUCTION_ORIGIN}/docs/guides/storage/`, - }, - { - '@type': 'ListItem', - position: 4, - name: 'Upload Files to Azure Blob Storage from the Browser', - item: `${PRODUCTION_ORIGIN}/docs/guides/storage/azure-blob/`, - }, - ]) - }) + it.each([ + { + page: 'guides/storage/azure-blob', + trail: [ + ['Docs', '/docs/'], + ['Guides', '/docs/guides/'], + ['Storage', '/docs/guides/storage/'], + [ + 'Upload Files to Azure Blob Storage from the Browser', + '/docs/guides/storage/azure-blob/', + ], + ], + }, + { + page: 'comparisons/best-angular-file-upload-libraries', + trail: [ + ['Docs', '/docs/'], + ['Comparisons', '/docs/comparisons/'], + [ + 'Best Angular File Upload Libraries Compared', + '/docs/comparisons/best-angular-file-upload-libraries/', + ], + ], + }, + ])( + 'links every breadcrumb middle crumb to its hub on $page', + ({ page, trail }) => { + const graph = renderGraph(propsFor(page.split('/'))) + const list = graph.find(node => node['@type'] === 'BreadcrumbList') + expect(list?.itemListElement).toEqual( + trail.map(([name, path], i) => ({ + '@type': 'ListItem', + position: i + 1, + name, + item: `${PRODUCTION_ORIGIN}${path}`, + })), + ) + }, + ) }) describe('nested index pages map to their folder slug in the agent surfaces', () => {