@chiselandco/nexus 3.1.0 → 3.1.1
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 +16 -2
- package/dist/GalleryCarousel.d.ts.map +1 -1
- package/dist/GalleryCarousel.js +5 -4
- package/dist/ProjectMenuClient.d.ts.map +1 -1
- package/dist/ProjectMenuClient.js +5 -4
- package/dist/types.d.ts +6 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +21 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Self-contained project portfolio components for Next.js App Router. Pass a `clientSlug`, `apiBase`, and `apiKey` — each component fetches, caches, and renders everything it needs with no client-side waterfall requests.
|
|
4
4
|
|
|
5
|
-
**Version:** 3.1.
|
|
5
|
+
**Version:** 3.1.1
|
|
6
6
|
|
|
7
7
|
---
|
|
8
8
|
|
|
@@ -474,7 +474,7 @@ export function Nav() {
|
|
|
474
474
|
| `dataUrl` | `string` | No* | — | URL of a local API route created with `createMenuHandler()` — recommended for production |
|
|
475
475
|
| `clientSlug` | `string` | No* | — | Client slug for direct fetch mode |
|
|
476
476
|
| `apiBase` | `string` | No* | — | API base URL for direct fetch mode |
|
|
477
|
-
| `apiKey` | `string` | No* |
|
|
477
|
+
| `apiKey` | `string` | No* | ��� | Client API key for direct fetch mode |
|
|
478
478
|
| `menuId` | `string` | No | — | Slug of a curated menu |
|
|
479
479
|
| `basePath` | `string` | Yes | — | Base path for project detail links |
|
|
480
480
|
| `viewAllPath` | `string` | Yes | — | Path for the "View All Projects" link |
|
|
@@ -670,6 +670,20 @@ revalidateTag("chisel-menu-your-client-slug-main-nav")
|
|
|
670
670
|
|
|
671
671
|
---
|
|
672
672
|
|
|
673
|
+
## Image optimisation
|
|
674
|
+
|
|
675
|
+
All components that render images use the built-in `thumbUrl()` helper to request appropriately-sized variants from Vercel Blob instead of loading full-resolution originals. The helper appends `?w=<width>&q=<quality>` query params which Vercel Blob handles on-the-fly. For non-Blob image sources the URL is returned unchanged.
|
|
676
|
+
|
|
677
|
+
| Location | Display size | Requested size | Quality |
|
|
678
|
+
|---|---|---|---|
|
|
679
|
+
| `ProjectMenuClient` card thumbnails | 72×54px | 144px wide (2× retina) | 60% |
|
|
680
|
+
| `GalleryCarousel` thumbnail strip | 72px wide | 160px wide (2× retina) | 60% |
|
|
681
|
+
| `GalleryCarousel` main image | Full width | 1600px wide | 80% |
|
|
682
|
+
|
|
683
|
+
If you are hosting images outside Vercel Blob (e.g. Cloudinary, imgix, your own CDN), you can override image rendering by passing pre-transformed URLs in your media objects — `thumbUrl()` will pass them through untouched.
|
|
684
|
+
|
|
685
|
+
---
|
|
686
|
+
|
|
673
687
|
## Publishing
|
|
674
688
|
|
|
675
689
|
```bash
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"GalleryCarousel.d.ts","sourceRoot":"","sources":["../src/GalleryCarousel.tsx"],"names":[],"mappings":"AAEA,OAAO,KAA4D,MAAM,OAAO,CAAA;AAEhF,OAAO,KAAK,EAAE,KAAK,EAAE,iBAAiB,EAAe,MAAM,SAAS,CAAA;
|
|
1
|
+
{"version":3,"file":"GalleryCarousel.d.ts","sourceRoot":"","sources":["../src/GalleryCarousel.tsx"],"names":[],"mappings":"AAEA,OAAO,KAA4D,MAAM,OAAO,CAAA;AAEhF,OAAO,KAAK,EAAE,KAAK,EAAE,iBAAiB,EAAe,MAAM,SAAS,CAAA;AAUpE,MAAM,WAAW,oBAAoB;IACnC,MAAM,EAAE,KAAK,EAAE,CAAA;IACf,YAAY,EAAE,MAAM,CAAA;IACpB;;;;;OAKG;IACH,MAAM,CAAC,EAAE,iBAAiB,EAAE,CAAA;CAC7B;AAgOD,wBAAgB,eAAe,CAAC,EAAE,MAAM,EAAE,YAAY,EAAE,MAAW,EAAE,EAAE,oBAAoB,4BA2V1F"}
|
package/dist/GalleryCarousel.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { useState, useMemo, useEffect, useCallback, useRef } from "react";
|
|
4
4
|
import { useRouter, useSearchParams, usePathname } from "next/navigation";
|
|
5
|
+
import { thumbUrl } from "./types";
|
|
5
6
|
const FONT = "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif";
|
|
6
7
|
const ACCENT = "#C8872A";
|
|
7
8
|
const INK = "#0f0f0f";
|
|
@@ -168,7 +169,7 @@ function FilterDropdown({ field, values, activeSlug, onSelect, }) {
|
|
|
168
169
|
}
|
|
169
170
|
// ─── Main component ────────────────────────────────────────────────────────
|
|
170
171
|
export function GalleryCarousel({ images, projectTitle, schema = [] }) {
|
|
171
|
-
var _a;
|
|
172
|
+
var _a, _b;
|
|
172
173
|
const router = useRouter();
|
|
173
174
|
const pathname = usePathname();
|
|
174
175
|
const searchParams = useSearchParams();
|
|
@@ -286,7 +287,7 @@ export function GalleryCarousel({ images, projectTitle, schema = [] }) {
|
|
|
286
287
|
cursor: "pointer",
|
|
287
288
|
letterSpacing: "0.04em",
|
|
288
289
|
textDecoration: "underline",
|
|
289
|
-
}, children: "Clear filters" })] })), active && (_jsxs("div", { style: { position: "relative", width: "100%", aspectRatio: "16/9", backgroundColor: "#1a1916", overflow: "hidden" }, children: [_jsx("img", { src: active.url, alt: active.alt || projectTitle, style: { position: "absolute", inset: 0, width: "100%", height: "100%", objectFit: "cover", display: "block" } }), _jsx("div", { style: {
|
|
290
|
+
}, children: "Clear filters" })] })), active && (_jsxs("div", { style: { position: "relative", width: "100%", aspectRatio: "16/9", backgroundColor: "#1a1916", overflow: "hidden" }, children: [_jsx("img", { src: (_b = thumbUrl(active.url, 1600, 80)) !== null && _b !== void 0 ? _b : active.url, alt: active.alt || projectTitle, style: { position: "absolute", inset: 0, width: "100%", height: "100%", objectFit: "cover", display: "block" } }), _jsx("div", { style: {
|
|
290
291
|
position: "absolute",
|
|
291
292
|
inset: "50% 0 0 0",
|
|
292
293
|
background: "linear-gradient(to bottom, transparent, rgba(0,0,0,0.52))",
|
|
@@ -344,7 +345,7 @@ export function GalleryCarousel({ images, projectTitle, schema = [] }) {
|
|
|
344
345
|
marginTop: "6px",
|
|
345
346
|
paddingBottom: "2px",
|
|
346
347
|
}, children: filteredImages.map((img, i) => {
|
|
347
|
-
var _a;
|
|
348
|
+
var _a, _b;
|
|
348
349
|
const isActive = i === safeIndex;
|
|
349
350
|
return (_jsx("button", { onClick: () => setCurrent(i), "aria-label": `View image ${i + 1}`, style: {
|
|
350
351
|
position: "relative",
|
|
@@ -361,7 +362,7 @@ export function GalleryCarousel({ images, projectTitle, schema = [] }) {
|
|
|
361
362
|
outlineOffset: isActive ? "1px" : "0",
|
|
362
363
|
transition: "outline 0.12s, opacity 0.12s",
|
|
363
364
|
opacity: isActive ? 1 : 0.55,
|
|
364
|
-
}, children: _jsx("img", { src: img.url, alt: img.alt || `Image ${i + 1}`, style: { width: "100%", height: "100%", objectFit: "cover", display: "block" } }) }, (
|
|
365
|
+
}, children: _jsx("img", { src: (_a = thumbUrl(img.url, 160, 60)) !== null && _a !== void 0 ? _a : img.url, alt: img.alt || `Image ${i + 1}`, style: { width: "100%", height: "100%", objectFit: "cover", display: "block" } }) }, (_b = img.id) !== null && _b !== void 0 ? _b : i));
|
|
365
366
|
}) }))] }));
|
|
366
367
|
}
|
|
367
368
|
function arrowBtn(side) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ProjectMenuClient.d.ts","sourceRoot":"","sources":["../src/ProjectMenuClient.tsx"],"names":[],"mappings":"AAEA,OAAO,KAA8B,MAAM,OAAO,CAAA;AAClD,OAAO,KAAK,EAAE,OAAO,EAAE,iBAAiB,EAAoB,MAAM,SAAS,CAAA;
|
|
1
|
+
{"version":3,"file":"ProjectMenuClient.d.ts","sourceRoot":"","sources":["../src/ProjectMenuClient.tsx"],"names":[],"mappings":"AAEA,OAAO,KAA8B,MAAM,OAAO,CAAA;AAClD,OAAO,KAAK,EAAE,OAAO,EAAE,iBAAiB,EAAoB,MAAM,SAAS,CAAA;AAc3E,MAAM,WAAW,sBAAsB;IACrC;;;;;OAKG;IACH,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,gFAAgF;IAChF,MAAM,CAAC,EAAE,MAAM,CAAA;IACf;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,CAAA;IACf;;;;OAIG;IACH,OAAO,CAAC,EAAE,OAAO,CAAA;IAEjB,QAAQ,CAAC,EAAE,OAAO,EAAE,CAAA;IACpB,MAAM,CAAC,EAAE,iBAAiB,EAAE,CAAA;IAC5B,aAAa,CAAC,EAAE;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,EAAE,CAAA;IAC/C,cAAc,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC9B,eAAe,CAAC,EAAE,MAAM,CAAA;IACxB,eAAe,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAA;IACxD,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,QAAQ,EAAE,MAAM,CAAA;IAChB,WAAW,EAAE,MAAM,CAAA;IACnB,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,WAAW,CAAC,EAAE,MAAM,CAAA;CACrB;AAkBD,wBAAgB,iBAAiB,CAAC,EAChC,OAAO,EACP,UAAU,EACV,OAAO,EACP,MAAM,EACN,MAAM,EACN,OAAe,EACf,QAAQ,EAAE,YAAY,EACtB,MAAM,EAAE,UAAU,EAClB,aAAa,EAAE,iBAAiB,EAChC,cAAc,EAAE,kBAAkB,EAClC,eAAe,EAAE,mBAAoC,EACrD,eAAe,EAAE,mBAAwB,EACzC,QAAQ,EACR,QAAQ,EACR,WAAW,EACX,IAAmB,EACnB,WAAe,GAChB,EAAE,sBAAsB,qBAqfxB"}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
3
|
import { useState, useEffect } from "react";
|
|
4
|
+
import { thumbUrl } from "./types";
|
|
4
5
|
function parseSingleValue(raw) {
|
|
5
6
|
if (typeof raw === "string")
|
|
6
7
|
return raw.replace(/`/g, "").trim();
|
|
@@ -318,11 +319,11 @@ export function ProjectMenuClient({ dataUrl, clientSlug, apiBase, apiKey, menuId
|
|
|
318
319
|
margin: "0 0 16px 0",
|
|
319
320
|
display: "none",
|
|
320
321
|
}, className: "chisel-menu-subtitle", children: subtitle })), displayed.length === 0 ? (_jsx("p", { style: { fontSize: "14px", color: "#a1a1aa", margin: 0 }, children: "No projects found." })) : (_jsx("div", { className: "chisel-menu-card-grid", children: displayed.map((project) => {
|
|
321
|
-
var _a, _b, _c, _d, _e
|
|
322
|
-
const imageUrl = (
|
|
322
|
+
var _a, _b, _c, _d, _e;
|
|
323
|
+
const imageUrl = thumbUrl((_a = project.image_url) !== null && _a !== void 0 ? _a : (_c = (_b = project.media) === null || _b === void 0 ? void 0 : _b[0]) === null || _c === void 0 ? void 0 : _c.url, 144, 60);
|
|
323
324
|
const badgeRaw = badgeField ? parseSingleValue(project.custom_field_values[badgeField.key]) : null;
|
|
324
|
-
const badgeOptMap = badgeField ? ((
|
|
325
|
-
const badge = badgeRaw ? ((
|
|
325
|
+
const badgeOptMap = badgeField ? ((_d = fieldOptionsMap[badgeField.key]) !== null && _d !== void 0 ? _d : {}) : {};
|
|
326
|
+
const badge = badgeRaw ? ((_e = badgeOptMap[badgeRaw]) !== null && _e !== void 0 ? _e : badgeRaw) : null;
|
|
326
327
|
const tags = tagsField ? parseMultiValue(project.custom_field_values[tagsField.key]) : [];
|
|
327
328
|
const href = `${basePath}/${project.slug}`;
|
|
328
329
|
const isHovered = hoveredCard === project.id;
|
package/dist/types.d.ts
CHANGED
|
@@ -30,6 +30,12 @@ export interface LocationValue {
|
|
|
30
30
|
state?: string;
|
|
31
31
|
}
|
|
32
32
|
export type CustomFieldValue = string | number | string[] | LocationValue | null;
|
|
33
|
+
/**
|
|
34
|
+
* Returns a resized image URL for Vercel Blob-hosted images.
|
|
35
|
+
* Appends ?w=<width>&q=<quality> for on-the-fly resizing.
|
|
36
|
+
* Falls back to the original URL unchanged for non-Blob sources.
|
|
37
|
+
*/
|
|
38
|
+
export declare function thumbUrl(url: string | null | undefined, width: number, quality?: number): string | null;
|
|
33
39
|
export interface Project {
|
|
34
40
|
id: string;
|
|
35
41
|
title: string;
|
package/dist/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,WAAW;IAC1B,EAAE,EAAE,MAAM,CAAA;IACV,KAAK,EAAE,MAAM,CAAA;IACb,GAAG,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CACpB;AAED,MAAM,WAAW,iBAAiB;IAChC,GAAG,EAAE,MAAM,CAAA;IACX,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,MAAM,GAAG,QAAQ,GAAG,QAAQ,GAAG,cAAc,GAAG,UAAU,CAAA;IAChE,OAAO,EAAE,MAAM,EAAE,GAAG,WAAW,EAAE,CAAA;IACjC,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IACpC,aAAa,EAAE,OAAO,CAAA;IACtB,UAAU,EAAE,MAAM,CAAA;IAClB,gBAAgB,CAAC,EAAE,eAAe,GAAG,MAAM,GAAG,UAAU,GAAG,QAAQ,CAAA;CACpE;AAED,MAAM,WAAW,KAAK;IACpB,EAAE,EAAE,MAAM,CAAA;IACV,UAAU,EAAE,MAAM,CAAA;IAClB,GAAG,EAAE,MAAM,CAAA;IACX,GAAG,EAAE,MAAM,CAAA;IACX,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACvB,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC3B,UAAU,EAAE,OAAO,CAAA;IACnB,UAAU,EAAE,MAAM,CAAA;IAClB,+FAA+F;IAC/F,mBAAmB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,IAAI,CAAC,CAAA;CAC/D;AAED,MAAM,WAAW,aAAa;IAC5B,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,KAAK,CAAC,EAAE,MAAM,CAAA;CACf;AAED,MAAM,MAAM,gBAAgB,GAAG,MAAM,GAAG,MAAM,GAAG,MAAM,EAAE,GAAG,aAAa,GAAG,IAAI,CAAA;AAEhF,MAAM,WAAW,OAAO;IACtB,EAAE,EAAE,MAAM,CAAA;IACV,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,EAAE,MAAM,CAAA;IACb,WAAW,EAAE,MAAM,CAAA;IACnB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,WAAW,EAAE,OAAO,CAAA;IACpB,YAAY,CAAC,EAAE,OAAO,CAAA;IACtB,mBAAmB,EAAE,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAAA;IACrD,UAAU,EAAE,MAAM,CAAA;IAClB,UAAU,EAAE,MAAM,CAAA;IAClB,KAAK,EAAE,KAAK,EAAE,CAAA;CACf"}
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,WAAW;IAC1B,EAAE,EAAE,MAAM,CAAA;IACV,KAAK,EAAE,MAAM,CAAA;IACb,GAAG,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CACpB;AAED,MAAM,WAAW,iBAAiB;IAChC,GAAG,EAAE,MAAM,CAAA;IACX,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,MAAM,GAAG,QAAQ,GAAG,QAAQ,GAAG,cAAc,GAAG,UAAU,CAAA;IAChE,OAAO,EAAE,MAAM,EAAE,GAAG,WAAW,EAAE,CAAA;IACjC,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IACpC,aAAa,EAAE,OAAO,CAAA;IACtB,UAAU,EAAE,MAAM,CAAA;IAClB,gBAAgB,CAAC,EAAE,eAAe,GAAG,MAAM,GAAG,UAAU,GAAG,QAAQ,CAAA;CACpE;AAED,MAAM,WAAW,KAAK;IACpB,EAAE,EAAE,MAAM,CAAA;IACV,UAAU,EAAE,MAAM,CAAA;IAClB,GAAG,EAAE,MAAM,CAAA;IACX,GAAG,EAAE,MAAM,CAAA;IACX,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACvB,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC3B,UAAU,EAAE,OAAO,CAAA;IACnB,UAAU,EAAE,MAAM,CAAA;IAClB,+FAA+F;IAC/F,mBAAmB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,IAAI,CAAC,CAAA;CAC/D;AAED,MAAM,WAAW,aAAa;IAC5B,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,KAAK,CAAC,EAAE,MAAM,CAAA;CACf;AAED,MAAM,MAAM,gBAAgB,GAAG,MAAM,GAAG,MAAM,GAAG,MAAM,EAAE,GAAG,aAAa,GAAG,IAAI,CAAA;AAEhF;;;;GAIG;AACH,wBAAgB,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,SAAK,GAAG,MAAM,GAAG,IAAI,CAanG;AAED,MAAM,WAAW,OAAO;IACtB,EAAE,EAAE,MAAM,CAAA;IACV,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,EAAE,MAAM,CAAA;IACb,WAAW,EAAE,MAAM,CAAA;IACnB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,WAAW,EAAE,OAAO,CAAA;IACpB,YAAY,CAAC,EAAE,OAAO,CAAA;IACtB,mBAAmB,EAAE,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAAA;IACrD,UAAU,EAAE,MAAM,CAAA;IAClB,UAAU,EAAE,MAAM,CAAA;IAClB,KAAK,EAAE,KAAK,EAAE,CAAA;CACf"}
|
package/dist/types.js
CHANGED
|
@@ -1 +1,21 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Returns a resized image URL for Vercel Blob-hosted images.
|
|
3
|
+
* Appends ?w=<width>&q=<quality> for on-the-fly resizing.
|
|
4
|
+
* Falls back to the original URL unchanged for non-Blob sources.
|
|
5
|
+
*/
|
|
6
|
+
export function thumbUrl(url, width, quality = 75) {
|
|
7
|
+
if (!url)
|
|
8
|
+
return null;
|
|
9
|
+
try {
|
|
10
|
+
const u = new URL(url);
|
|
11
|
+
if (u.hostname.endsWith(".public.blob.vercel-storage.com")) {
|
|
12
|
+
u.searchParams.set("w", String(width));
|
|
13
|
+
u.searchParams.set("q", String(quality));
|
|
14
|
+
return u.toString();
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
catch (_a) {
|
|
18
|
+
// not a valid absolute URL — return as-is
|
|
19
|
+
}
|
|
20
|
+
return url;
|
|
21
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chiselandco/nexus",
|
|
3
|
-
"version": "3.1.
|
|
3
|
+
"version": "3.1.1",
|
|
4
4
|
"description": "Self-contained project portfolio components for Next.js App Router. Includes ProjectPortfolio, ProjectPortfolioClient, ProjectDetail, SimilarProjects, ProjectMenu, ProjectMenuClient, GalleryCarousel, and FilterSidebar. Pass a clientSlug and apiBase — done.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"nextjs",
|