diff --git a/.changeset/funny-ravens-run.md b/.changeset/funny-ravens-run.md
new file mode 100644
index 000000000..d79104d79
--- /dev/null
+++ b/.changeset/funny-ravens-run.md
@@ -0,0 +1,47 @@
+---
+'@faustwp/core': minor
+---
+
+Feat: Added support `next/dynamic` imports for templates to reduce initial bundle size in a way that's backwards compatible with static imports.
+
+This solves a known issue in Faust where all defined templates are bundled together and loaded on every WordPress page. by enabling the use of dynamic importing of templates this issue is resolved. Now templates are only loaded as needed per route.
+
+It's recommended you migrate to dynamic imports by updating your template file. Here's an example:
+
+```js title=src/wp-templates/index.js
+// Old Static Templates
+import category from './category';
+import tag from './tag';
+import frontPage from './front-page';
+import page from './page';
+import single from './single';
+
+export default {
+ category,
+ tag,
+ 'front-page': frontPage,
+ page,
+ single,
+};
+
+// New Dynamic Templates
+import dynamic from 'next/dynamic';
+
+const category = dynamic(() => import('./category.js'));
+const tag = dynamic(() => import('./tag.js'));
+const frontPage = dynamic(() => import('./front-page.js'));
+const page = dynamic(() => import('./page.js'));
+
+// The above examples assume use of default exports. If you are using named exports you'll need to handle that:
+const single = dynamic(() => import('./single.js').then(mod => mod.Single));
+
+export default {
+ category,
+ tag,
+ 'front-page': frontPage,
+ page,
+ single,
+};
+```
+
+For further info see the Next.js docs on the use of [`next/dynamic`](https://nextjs.org/docs/pages/guides/lazy-loading#nextdynamic-1).
diff --git a/docs/how-to/basic-setup/index.md b/docs/how-to/basic-setup/index.md
index 611a174be..81449ce44 100644
--- a/docs/how-to/basic-setup/index.md
+++ b/docs/how-to/basic-setup/index.md
@@ -175,13 +175,13 @@ In the `SingleTemplate` component, we receive the props, destructure the `title`
Finally, we have to make Faust.js aware that this template exists. To do that, create an `index.js` file inside the `wp-templates` folder with this code inside:
```js title="wp-templates/index.js"
-import SingleTemplate from "./single";
+import dynamic from 'next/dynamic';
-const templates = {
- single: SingleTemplate,
-};
+const category = dynamic(() => import('./category.js'));
-export default templates;
+export default {
+ single,
+};
```
### C. Create a catch-all route
diff --git a/examples/next/faustwp-getting-started/components/FeaturedImage/FeaturedImage.js b/examples/next/faustwp-getting-started/components/FeaturedImage/FeaturedImage.js
index 40a4a78cb..55721c734 100644
--- a/examples/next/faustwp-getting-started/components/FeaturedImage/FeaturedImage.js
+++ b/examples/next/faustwp-getting-started/components/FeaturedImage/FeaturedImage.js
@@ -1,58 +1,61 @@
import { gql } from '@apollo/client';
-import Image from 'next/image';
+import Image from 'next/legacy/image';
export default function FeaturedImage({
- image,
- width,
- height,
- className,
- priority,
- layout,
- ...props
+ image,
+ width,
+ height,
+ className,
+ priority,
+ layout,
+ ...props
}) {
- const src = image?.sourceUrl;
-
- if (!src) return null;
-
- const { altText = '', mediaDetails = {} } = image ?? {};
-
- layout = layout ?? 'fill';
-
- const dimensions = {
- width: layout === 'fill' ? undefined : width ?? mediaDetails?.width,
- height: layout === 'fill' ? undefined : height ?? mediaDetails?.height
- };
-
- if (layout !== 'fill' && (!dimensions.width || !dimensions.height)) return null;
-
- return (
-
-
-
- );
+ const src = image?.sourceUrl;
+
+ if (!src) return null;
+
+ const { altText = '', mediaDetails = {} } = image ?? {};
+
+ layout = layout ?? 'fill';
+
+ const dimensions = {
+ width: layout === 'fill' ? undefined : width ?? mediaDetails?.width,
+ height: layout === 'fill' ? undefined : height ?? mediaDetails?.height,
+ };
+
+ if (layout !== 'fill' && (!dimensions.width || !dimensions.height))
+ return null;
+
+ console.log(layout, mediaDetails, dimensions);
+
+ return (
+
+
+
+ );
}
FeaturedImage.fragments = {
- entry: gql`
- fragment FeaturedImageFragment on NodeWithFeaturedImage {
- featuredImage {
- node {
- id
- sourceUrl
- altText
- mediaDetails {
- width
- height
- }
- }
- }
- }
- `,
+ entry: gql`
+ fragment FeaturedImageFragment on NodeWithFeaturedImage {
+ featuredImage {
+ node {
+ id
+ sourceUrl
+ altText
+ mediaDetails {
+ width
+ height
+ }
+ }
+ }
+ }
+ `,
};
diff --git a/examples/next/faustwp-getting-started/wp-templates/index.js b/examples/next/faustwp-getting-started/wp-templates/index.js
index 3cabd3e14..0e8e6b51e 100644
--- a/examples/next/faustwp-getting-started/wp-templates/index.js
+++ b/examples/next/faustwp-getting-started/wp-templates/index.js
@@ -1,13 +1,29 @@
-import category from './category';
-import tag from './tag';
-import frontPage from './front-page';
-import page from './page';
-import single from './single';
+import dynamic from 'next/dynamic';
+
+const category = dynamic(() => import('./category.js'), {
+ loading: () =>
Loading Category Template...
,
+});
+
+const tag = dynamic(() => import('./tag.js'), {
+ loading: () => Loading Tag Template...
,
+});
+
+const frontPage = dynamic(() => import('./front-page.js'), {
+ loading: () => Loading Front Page Template...
,
+});
+
+const page = dynamic(() => import('./page.js'), {
+ loading: () => Loading Page Template...
,
+});
+
+const single = dynamic(() => import('./single.js'), {
+ loading: () => Loading Single Post Template...
,
+});
export default {
- category,
- tag,
- 'front-page': frontPage,
- page,
- single,
+ category,
+ tag,
+ 'front-page': frontPage,
+ page,
+ single,
};
diff --git a/packages/faustwp-core/src/components/WordPressTemplate.tsx b/packages/faustwp-core/src/components/WordPressTemplate.tsx
index 28fc7421d..12b83f2c3 100644
--- a/packages/faustwp-core/src/components/WordPressTemplate.tsx
+++ b/packages/faustwp-core/src/components/WordPressTemplate.tsx
@@ -10,12 +10,17 @@ import React, {
} from 'react';
import { getApolloAuthClient, getApolloClient } from '../client.js';
import { getConfig } from '../config/index.js';
-import { getTemplate } from '../getTemplate.js';
+import {
+ getTemplate,
+ isDynamicComponent,
+ loadDynamicComponent,
+} from '../getTemplate.js';
import { useAuth } from '../hooks/useAuth.js';
import { SEED_QUERY, SeedNode } from '../queries/seedQuery.js';
import { FaustContext, FaustQueries } from '../store/FaustContext.js';
import { getQueryParam } from '../utils/convert.js';
import { isWordPressPreview } from '../utils/isWordPressPreview.js';
+import type { WordPressTemplate as WordPressTemplateType } from '../getWordPressProps.js';
export type FaustProps = {
__SEED_NODE__?: SeedNode | null;
@@ -38,6 +43,14 @@ export type FaustTemplateProps> = Props & {
__TEMPLATE_VARIABLES__?: { [key: string]: any };
};
+function checkDuplicateQueryQueries(template: WordPressTemplateType): void {
+ if (template.query && template.queries) {
+ throw new Error(
+ '`Only either `Component.query` or `Component.queries` can be provided, but not both.',
+ );
+ }
+}
+
export function WordPressTemplateInternal(
props: WordPressTemplateProps & {
seedNode: SeedNode;
@@ -62,27 +75,27 @@ export function WordPressTemplateInternal(
setLoading,
...wordpressTemplateProps
} = props;
- const template = getTemplate(seedNode, templates);
+ const unknownTemplate = getTemplate(seedNode, templates);
const [data, setData] = useState(templateQueryDataProp);
const { setQueries } = useContext(FaustContext) || {};
- if (template && template.queries && template.query) {
- throw new Error(
- '`Only either `Component.query` or `Component.queries` can be provided, but not both.',
- );
- }
-
/**
* Fetch the template's queries if defined.
*/
useEffect(() => {
void (async () => {
- const client = isPreview ? getApolloAuthClient() : getApolloClient();
-
- if (!template) {
+ if (!unknownTemplate) {
return;
}
+ const template = isDynamicComponent(unknownTemplate)
+ ? await loadDynamicComponent(unknownTemplate)
+ : unknownTemplate;
+
+ checkDuplicateQueryQueries(template);
+
+ const client = isPreview ? getApolloAuthClient() : getApolloClient();
+
if (template.query) {
return;
}
@@ -121,16 +134,33 @@ export function WordPressTemplateInternal(
setLoading(false);
})();
- }, [isAuthenticated, isPreview, seedNode, template, setQueries, setLoading]);
+ }, [
+ isAuthenticated,
+ isPreview,
+ seedNode,
+ unknownTemplate,
+ setQueries,
+ setLoading,
+ ]);
/**
* Fetch the template's query if defined.
*/
useEffect(() => {
void (async () => {
+ if (!unknownTemplate) {
+ return;
+ }
+
+ const template = isDynamicComponent(unknownTemplate)
+ ? await loadDynamicComponent(unknownTemplate)
+ : unknownTemplate;
+
+ checkDuplicateQueryQueries(template);
+
const client = isPreview ? getApolloAuthClient() : getApolloClient();
- if (!template || !template?.query || template?.queries || !seedNode) {
+ if (!template.query || template.queries || !seedNode) {
return;
}
@@ -153,14 +183,13 @@ export function WordPressTemplateInternal(
setLoading(false);
})();
- }, [data, template, seedNode, isPreview, isAuthenticated, setLoading]);
+ }, [data, unknownTemplate, seedNode, isPreview, isAuthenticated, setLoading]);
- if (!template) {
+ if (!unknownTemplate) {
return null;
}
- const Component = template as React.FC<{ [key: string]: any }>;
-
+ const Component = unknownTemplate as React.FC<{ [key: string]: any }>;
const newProps = {
...wordpressTemplateProps,
__TEMPLATE_QUERY_DATA__: templateQueryDataProp,
@@ -186,8 +215,8 @@ export function WordPressTemplate(props: WordPressTemplateProps) {
const [seedNode, setSeedNode] = useState(
seedNodeProp ?? null,
);
- const template = getTemplate(seedNode, templates);
- const [loading, setLoading] = useState(template === null);
+ const unknownTemplate = getTemplate(seedNode, templates);
+ const [loading, setLoading] = useState(unknownTemplate === null);
const [isPreview, setIsPreview] = useState(
templateQueryDataProp ? false : null,
);
diff --git a/packages/faustwp-core/src/getTemplate.ts b/packages/faustwp-core/src/getTemplate.ts
index d08ec3ca7..ab70cf733 100644
--- a/packages/faustwp-core/src/getTemplate.ts
+++ b/packages/faustwp-core/src/getTemplate.ts
@@ -146,10 +146,29 @@ export function getPossibleTemplates(node: SeedNode) {
return possibleTemplates;
}
+type DynamicComponent = {
+ render: {
+ preload: () => Promise<{ default: C }>;
+ displayName?: string;
+ };
+};
+
+export function isDynamicComponent(
+ component: C | DynamicComponent,
+): component is DynamicComponent {
+ return (component as DynamicComponent).render?.preload !== undefined;
+}
+
+export function loadDynamicComponent(
+ component: DynamicComponent,
+): Promise {
+ return component.render.preload().then((mod) => mod.default);
+}
+
export function getTemplate(
seedNode: SeedNode | null | undefined,
templates: { [key: string]: WordPressTemplate },
-): WordPressTemplate | null {
+): WordPressTemplate | DynamicComponent | null {
if (!seedNode) {
return null;
}
diff --git a/packages/faustwp-core/src/getWordPressProps.tsx b/packages/faustwp-core/src/getWordPressProps.tsx
index 6128ccaf6..153a2ff47 100644
--- a/packages/faustwp-core/src/getWordPressProps.tsx
+++ b/packages/faustwp-core/src/getWordPressProps.tsx
@@ -6,7 +6,12 @@ import { GetServerSidePropsContext, GetStaticPropsContext } from 'next';
import { addApolloState, getApolloClient } from './client.js';
import { FaustTemplateProps } from './components/WordPressTemplate.js';
import { getConfig } from './config/index.js';
-import { getPossibleTemplates, getTemplate } from './getTemplate.js';
+import {
+ getPossibleTemplates,
+ getTemplate,
+ isDynamicComponent,
+ loadDynamicComponent,
+} from './getTemplate.js';
import { SEED_QUERY, SeedNode } from './queries/seedQuery.js';
import { debugLog, infoLog } from './utils/log.js';
import { hooks } from './wpHooks/index.js';
@@ -142,12 +147,16 @@ export async function getWordPressProps(
getPossibleTemplates(seedNode),
);
- const template = getTemplate(seedNode, templates);
+ const unknownTemplate = getTemplate(seedNode, templates);
- if (!template) {
+ if (!unknownTemplate) {
return createNotFound(ctx, revalidate);
}
+ const template = isDynamicComponent(unknownTemplate)
+ ? await loadDynamicComponent(unknownTemplate)
+ : unknownTemplate;
+
if (template.query && template.queries) {
throw new Error(
'`Only either `Component.query` or `Component.queries` can be provided, but not both.',