@ai-growth/nextjs
Version:
Seamlessly integrate Sanity CMS with Next.js applications for automated blog routing and rendering
375 lines (374 loc) • 14.4 kB
JavaScript
;
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.withCmsRouting = withCmsRouting;
exports.createCmsRoutingFactory = createCmsRoutingFactory;
exports.useCmsRoutingContext = useCmsRoutingContext;
const jsx_runtime_1 = require("react/jsx-runtime");
const react_1 = __importStar(require("react"));
const router_1 = require("next/router");
const route_config_1 = require("../utils/route-config");
const cms_content_1 = require("../utils/cms-content");
const CmsRouteHandler_1 = require("./CmsRouteHandler");
// ============================================================================
// HOC IMPLEMENTATION
// ============================================================================
/**
* Higher-Order Component that wraps page components with CMS routing capabilities
*
* This HOC provides automatic CMS content handling for pages by injecting CMS-related
* props into the wrapped component. The wrapped component maintains full control over
* its rendering while receiving CMS state as props.
*
* @param WrappedComponent - The page component to wrap with CMS functionality
* @param options - Configuration options for CMS behavior
* @returns A new component with CMS routing capabilities
*
* @example
* ```tsx
* // Basic usage - component receives CMS props
* const BlogPage = ({ title, cmsContent, isCmsRoute, cmsLoading }) => {
* if (isCmsRoute && cmsLoading) {
* return <div>Loading CMS content...</div>;
* }
*
* if (isCmsRoute && cmsContent) {
* return (
* <div>
* <h1>{cmsContent.title}</h1>
* <div>{cmsContent.content}</div>
* </div>
* );
* }
*
* return <div>Regular page: {title}</div>;
* };
*
* export default withCmsRouting(BlogPage);
*
* // With options
* export default withCmsRouting(BlogPage, {
* contentOptions: { includeSEO: true, includeAuthor: true },
* onContentLoaded: (content) => console.log('Loaded:', content.title)
* });
*
* // TypeScript usage
* interface BlogPageProps {
* title: string;
* }
*
* const BlogPage: React.FC<BlogPageProps & CmsInjectedProps> = ({
* title,
* cmsContent,
* isCmsRoute,
* cmsLoading,
* cmsError
* }) => {
* // Component implementation with full control over rendering
* };
*
* export default withCmsRouting<BlogPageProps>(BlogPage, options);
* ```
*/
function withCmsRouting(WrappedComponent, options = {}) {
const { contentOptions = {}, displayName, autoFetch = true, customTemplate, onContentLoaded, onContentError, onRouteChange, } = options;
// Create the wrapped component
const WithCmsRoutingComponent = (0, react_1.forwardRef)((props, ref) => {
const router = (0, router_1.useRouter)();
const [cmsState, setCmsState] = (0, react_1.useState)({
content: null,
isLoading: false,
error: null,
isCmsRoute: false,
currentPath: '',
});
// ============================================================================
// CONTENT FETCHING
// ============================================================================
const fetchContent = async (path) => {
try {
setCmsState(prev => ({
...prev,
isLoading: true,
error: null,
}));
// Validate the route and fetch content
const validationResult = await (0, cms_content_1.validateContentRoute)(path, contentOptions);
if (validationResult.isValid && validationResult.content) {
setCmsState(prev => ({
...prev,
isLoading: false,
content: validationResult.content,
error: null,
}));
// Notify about successful content load
onContentLoaded?.(validationResult.content);
}
else {
const error = validationResult.error || 'Content not found';
setCmsState(prev => ({
...prev,
isLoading: false,
content: null,
error,
}));
// Notify about content error
onContentError?.(error);
}
}
catch (err) {
const errorMessage = err instanceof Error
? err.message
: 'Failed to fetch content';
setCmsState(prev => ({
...prev,
isLoading: false,
content: null,
error: errorMessage,
}));
// Notify about content error
onContentError?.(errorMessage);
}
};
const handleFetchCmsContent = async () => {
if (cmsState.currentPath && cmsState.isCmsRoute) {
await fetchContent(cmsState.currentPath);
}
};
const handleClearCmsContent = () => {
setCmsState(prev => ({
...prev,
content: null,
error: null,
isLoading: false,
}));
};
// ============================================================================
// ROUTER INTEGRATION
// ============================================================================
(0, react_1.useEffect)(() => {
// Skip if router is not ready
if (!router.isReady) {
return;
}
const currentPath = router.asPath;
// Skip if path hasn't changed
if (currentPath === cmsState.currentPath) {
return;
}
try {
// Check if this is a valid CMS route
const isCmsRoute = (0, route_config_1.isValidCmsRoute)(currentPath);
// Update state with new path and CMS route status
setCmsState(prev => ({
...prev,
currentPath,
isCmsRoute,
content: null,
error: null,
isLoading: false,
}));
// Notify about route change
onRouteChange?.(currentPath, isCmsRoute);
// Fetch content if this is a CMS route and autoFetch is enabled
if (isCmsRoute && autoFetch) {
fetchContent(currentPath);
}
}
catch {
// If route validation fails, treat as non-CMS route
setCmsState(prev => ({
...prev,
currentPath,
isCmsRoute: false,
content: null,
error: null,
isLoading: false,
}));
onRouteChange?.(currentPath, false);
}
}, [router.isReady, router.asPath, cmsState.currentPath, contentOptions, onRouteChange, autoFetch]);
// ============================================================================
// RENDERING
// ============================================================================
// Create enhanced props with CMS data
const enhancedProps = react_1.default.useMemo(() => ({
...props,
cmsContent: cmsState.content,
isCmsRoute: cmsState.isCmsRoute,
cmsLoading: cmsState.isLoading,
cmsError: cmsState.error,
fetchCmsContent: handleFetchCmsContent,
clearCmsContent: handleClearCmsContent,
}), [props, cmsState]);
// If we have a custom template and this is a CMS route, use CmsRouteHandler
if (customTemplate && cmsState.isCmsRoute) {
const cmsHandlerProps = {
customTemplate,
contentOptions,
};
if (onContentLoaded)
cmsHandlerProps.onContentLoaded = onContentLoaded;
if (onContentError)
cmsHandlerProps.onContentError = onContentError;
if (onRouteChange)
cmsHandlerProps.onRouteChange = onRouteChange;
return ((0, jsx_runtime_1.jsx)(CmsRouteHandler_1.CmsRouteHandler, { ...cmsHandlerProps, children: (0, jsx_runtime_1.jsx)(WrappedComponent, { ...props }) }));
}
// Always render the wrapped component with enhanced props
return (0, jsx_runtime_1.jsx)(WrappedComponent, { ...enhancedProps, ref: ref });
});
// Set display name for debugging
const componentName = displayName || WrappedComponent.displayName || WrappedComponent.name || 'Component';
WithCmsRoutingComponent.displayName = `withCmsRouting(${componentName})`;
// Copy static properties from the wrapped component (improved version)
const hoistStatics = (target, source) => {
// Get all property names from the source component
const sourceProperties = Object.getOwnPropertyNames(source);
// List of properties that should not be copied
const skipProperties = [
'prototype',
'length',
'name',
'caller',
'callee',
'arguments',
'constructor'
];
sourceProperties.forEach(propName => {
if (!skipProperties.includes(propName)) {
try {
const descriptor = Object.getOwnPropertyDescriptor(source, propName);
if (descriptor && (descriptor.value !== undefined || descriptor.get)) {
Object.defineProperty(target, propName, descriptor);
}
}
catch {
// Ignore copy failures for read-only properties
}
}
});
return target;
};
return hoistStatics(WithCmsRoutingComponent, WrappedComponent);
}
// ============================================================================
// CONVENIENCE FUNCTIONS
// ============================================================================
/**
* Creates a pre-configured version of withCmsRouting with default options
*
* @param defaultOptions - Default options to apply to all wrapped components
* @returns A withCmsRouting function with pre-configured options
*
* @example
* ```tsx
* // Create a factory with common options
* const withBlogCms = createCmsRoutingFactory({
* contentOptions: { includeSEO: true, includeAuthor: true },
* autoFetch: true,
* });
*
* // Use the factory
* const BlogPage = ({ title, cmsContent, isCmsRoute }) => {
* if (isCmsRoute && cmsContent) {
* return <div>CMS: {cmsContent.title}</div>;
* }
* return <div>Regular: {title}</div>;
* };
* export default withBlogCms(BlogPage);
*
* // Override specific options when needed
* const SpecialBlogPage = ({ title }) => <div>{title}</div>;
* export default withBlogCms(SpecialBlogPage, {
* autoFetch: false
* });
* ```
*/
function createCmsRoutingFactory(defaultOptions) {
return function (WrappedComponent, overrideOptions = {}) {
const mergedOptions = {
...defaultOptions,
...overrideOptions,
// Merge nested objects
contentOptions: {
...defaultOptions.contentOptions,
...overrideOptions.contentOptions,
},
};
return withCmsRouting(WrappedComponent, mergedOptions);
};
}
/**
* Hook to access CMS routing state from within a component wrapped by withCmsRouting
*
* Note: When using withCmsRouting, CMS state is available directly as props.
* This hook is provided for consistency but will warn and return default values.
*
* @returns CMS routing state including content, loading, and error information
*
* @example
* ```tsx
* function MyPageComponent({ cmsContent, isCmsRoute, cmsLoading, cmsError }) {
* // Access CMS state directly from props when using withCmsRouting
*
* if (cmsLoading) {
* return <div>Loading CMS content...</div>;
* }
*
* if (cmsError) {
* return <div>Error: {cmsError}</div>;
* }
*
* if (isCmsRoute && cmsContent) {
* return <div>CMS: {cmsContent.title}</div>;
* }
*
* return <div>Regular page content</div>;
* }
* ```
*/
function useCmsRoutingContext() {
// This hook would typically use a context provider, but since we're injecting
// props directly into components, we'll provide a warning for now
console.warn('useCmsRoutingContext: CMS state is available as props when using withCmsRouting. ' +
'Access cmsContent, isCmsRoute, cmsLoading, and cmsError from component props instead.');
return {
isCmsRoute: false,
cmsLoading: false,
cmsError: null,
};
}
exports.default = withCmsRouting;