Repository: rtCamp/snapwp
Pull request #119: feat: load favicon from WordPress server
Author: login: SH4LIN
Created: 2025-03-05T05:34:07Z | Merged: 2025-03-19T20:17:58Z

<!--
Thanks for taking the time to submit a Pull Request.
Please make sure to review the [Contribution Guidelines](../DEVELOPMENT.md) before submitting your PR.
-->

## What
<!-- In a few words, what does this PR actually change -->
- This PR will load the favicon icon, apple touch icon and msapplication-tileImage from the WordPress server

### Related Issue(s):
<!-- E.g.
- Fixes | Closes | Part of: #123
-->
- Part of: https://github.com/rtCamp/headless/issues/405

## How
<!-- How does your PR address the issue at hand? What are the implementation details? Please be specific. -->
- This PR will use the app router metadata API and using that metadata API and the icon data fetched from server it will add the necessary tags into the head


## Checklist
<!--
We encourage you to complete this checklist to the best of your abilities.
If you can't do everything, that's okay too.
Contributing Guidelines: https://github.com/rtCamp/snapwp/blob/develop/.github/CONTRIBUTING.md
-->

-   [x] I have read the [Contribution Guidelines](https://github.com/rtCamp/snapwp/blob/develop/.github/CONTRIBUTING.md).
-   [x] My code is tested to the best of my abilities.
-   [x] My code passes all lints (ESLint, tsc, prettier etc.).
-   [x] My code has detailed inline documentation.
-   [ ] I have added unit tests to verify the code works as intended.
-   [x] I have updated the project documentation as needed.
-   [x] I have added a changeset for this PR using `npm run changeset`.



Diff:
diff --git a/.changeset/late-breads-listen.md b/.changeset/late-breads-listen.md
new file mode 100644
index 00000000..57e8e6d6
--- /dev/null
+++ b/.changeset/late-breads-listen.md
@@ -0,0 +1,7 @@
+---
+"@snapwp/query": patch
+"@snapwp/next": patch
+"snapwp": patch
+---
+
+feat: load default favicon metadata from WordPress
diff --git a/examples/nextjs/starter/src/app/layout.tsx b/examples/nextjs/starter/src/app/layout.tsx
index 3ce8c01d..3e1b98e5 100644
--- a/examples/nextjs/starter/src/app/layout.tsx
+++ b/examples/nextjs/starter/src/app/layout.tsx
@@ -1,9 +1,24 @@
-import { RootLayout } from '@snapwp/next';
+import { RootLayout, generateRootMetaData } from '@snapwp/next';
+import type { Metadata } from 'next';
+import type { PropsWithChildren } from 'react';
 
-export default function Layout( { children }: { children: React.ReactNode } ) {
+export default function Layout( { children }: PropsWithChildren ) {
 	return (
 		<RootLayout>
 			<>{ children }</>
 		</RootLayout>
 	);
 }
+
+/**
+ * Generate custom metadata with rootMetadata generated using generateRootMeraData.
+ *
+ * @return dynamic metadata generated from generateRootMetaData() and custom logic.
+ */
+export async function generateMetadata(): Promise< Metadata > {
+	const rootMetaData = await generateRootMetaData();
+
+	return {
+		...rootMetaData,
+	};
+}
diff --git a/packages/next/src/root-layout/icons-metadata.ts b/packages/next/src/root-layout/icons-metadata.ts
new file mode 100644
index 00000000..62b82f29
--- /dev/null
+++ b/packages/next/src/root-layout/icons-metadata.ts
@@ -0,0 +1,151 @@
+import { QueryEngine } from '@snapwp/query';
+
+interface GeneralSettingsProps {
+	generalSettings: {
+		siteIcon: {
+			mediaItemUrl: string | undefined;
+			mediaDetails: {
+				sizes: IconData[];
+			};
+		};
+	};
+}
+
+interface IconMetaData {
+	faviconIcons: FormattedIconData[];
+	appleIcons: FormattedIconData[] | undefined;
+	msApplicationTileIcon: IconData | undefined;
+}
+
+interface IconData {
+	sourceUrl: string;
+	height: string;
+	width: string;
+}
+
+interface FormattedIconData {
+	url: string;
+	sizes: string;
+}
+
+/**
+ * Fetch site icon data and filter them into categorized formats.
+ * @see - https://developer.wordpress.org/reference/functions/wp_site_icon/#:~:text=Displays%20site%20icon%20meta%20tags. Reason why we are different resolution icons into individual category.
+ *
+ * @todo Refactor for composability alongside SEO metadata patterns
+ *
+ * @return Categorized icons.
+ */
+export const getIcons = async (): Promise< IconMetaData > => {
+	const settings: GeneralSettingsProps | undefined =
+		await QueryEngine.getGeneralSettings();
+
+	if ( ! settings ) {
+		return {
+			faviconIcons: [],
+			appleIcons: undefined,
+			msApplicationTileIcon: undefined,
+		};
+	}
+
+	let fallbackIcons: IconMetaData = {
+		faviconIcons: [],
+		appleIcons: undefined,
+		msApplicationTileIcon: undefined,
+	};
+
+	// Creating fallback icons if siteIcon is present but mediaDetails is not.
+	if ( settings.generalSettings.siteIcon.mediaItemUrl ) {
+		fallbackIcons = {
+			...fallbackIcons,
+			faviconIcons: [
+				{
+					sizes: '512x512',
+					url: settings.generalSettings.siteIcon.mediaItemUrl,
+				},
+			],
+		};
+	}
+
+	// Return fallback icons if sizes are not present.
+	if ( ! settings.generalSettings.siteIcon.mediaDetails.sizes ) {
+		return fallbackIcons;
+	}
+
+	const sizes = settings.generalSettings.siteIcon.mediaDetails.sizes;
+
+	// Filter out valid icons
+	const validIcons: IconData[] = sizes.filter(
+		( icon ) => !! ( icon.sourceUrl && icon.height && icon.width )
+	) as IconData[];
+
+	// Return fallback icons if no valid icons are found.
+	if ( ! validIcons.length ) {
+		return fallbackIcons;
+	}
+
+	// Filter icons by sizes.
+	const filteredFaviconIcons = filterIconsBySize( validIcons, [
+		'32x32',
+		'192x192',
+	] );
+
+	// Format icons into required metadata structure. If filteredFaviconIcons is empty, return # as url of icon.
+	const formattedFaviconIcons: FormattedIconData[] = filteredFaviconIcons
+		? formatIcons( filteredFaviconIcons )
+		: [ { url: '#', sizes: '' } ];
+
+	const filteredAppleIcons = filterIconsBySize( validIcons, [ '180x180' ] );
+
+	// Format icons into required metadata structure.
+	const formattedAppleIcons: FormattedIconData[] = filteredAppleIcons
+		? formatIcons( filteredAppleIcons )
+		: [];
+
+	return {
+		faviconIcons: formattedFaviconIcons,
+		appleIcons: formattedAppleIcons,
+		msApplicationTileIcon: findIconBySize( validIcons, '270x270' ),
+	};
+};
+
+/**
+ * Filter icons by multiple size formats.
+ *
+ * @param icons - List of all available icons.
+ * @param sizes - Array of sizes in "WxH" format (e.g., "32x32").
+ * @return Filtered list of icons.
+ */
+export const filterIconsBySize = (
+	icons: IconData[],
+	sizes: string[]
+): IconData[] =>
+	icons.filter( ( icon ) =>
+		sizes.includes( `${ icon.width }x${ icon.height }` )
+	);
+
+/**
+ * Find a single icon matching the specified size.
+ *
+ * @param icons - List of icons.
+ * @param size - Size in "WxH" format (e.g., "270x270").
+ * @return The first matching icon or undefined.
+ */
+export const findIconBySize = (
+	icons: IconData[],
+	size: string
+): IconData | undefined =>
+	icons.find( ( icon ) => `${ icon.width }x${ icon.height }` === size );
+
+/**
+ * Format icons into the required metadata structure.
+ *
+ * @param icons - List of icons.
+ *
+ * @return Formatted list of icons.
+ */
+const formatIcons = ( icons: IconData[] ): FormattedIconData[] =>
+	icons.map( ( { sourceUrl, width, height } ) => ( {
+		url: sourceUrl,
+		sizes: `${ width }x${ height }`,
+	} ) );
diff --git a/packages/next/src/root-layout/index.tsx b/packages/next/src/root-layout/index.tsx
index d2642b5d..cae3cc28 100644
--- a/packages/next/src/root-layout/index.tsx
+++ b/packages/next/src/root-layout/index.tsx
@@ -1,6 +1,8 @@
-import type { PropsWithChildren } from 'react';
 import { QueryEngine } from '@snapwp/query';
 import { GlobalHead } from './global-head';
+import { getIcons } from './icons-metadata';
+import type { Metadata } from 'next';
+import type { PropsWithChildren } from 'react';
 
 export type RootLayoutProps = {
 	getGlobalStyles?: ( typeof QueryEngine )[ 'getGlobalStyles' ];
@@ -30,3 +32,30 @@ export async function RootLayout( {
 		</html>
 	);
 }
+
+/**
+ * Generate and return root metadata, including icons and other metadata.
+ *
+ * @return Merged metadata.
+ */
+export async function generateRootMetaData(): Promise< Metadata > {
+	/**
+	 * Fetch icons in required format, apply faviconIcons and apple touch icons in icons metadata property while apply msapplication-TileImage in other metadata property.
+	 *
+	 * @todo Review composability when implementing SEO metadata
+	 */
+	const { faviconIcons, appleIcons, msApplicationTileIcon } =
+		await getIcons();
+
+	return {
+		icons: {
+			icon: faviconIcons,
+			apple: appleIcons,
+		},
+		other: {
+			...( msApplicationTileIcon && {
+				'msapplication-TileImage': msApplicationTileIcon.sourceUrl,
+			} ),
+		},
+	};
+}
diff --git a/packages/query/src/queries/get-general-settings.graphql b/packages/query/src/queries/get-general-settings.graphql
new file mode 100644
index 00000000..1249d35c
--- /dev/null
+++ b/packages/query/src/queries/get-general-settings.graphql
@@ -0,0 +1,15 @@
+query GetGeneralSettings {
+	generalSettings {
+		siteIcon {
+			id
+			mediaDetails {
+				sizes {
+					width
+					height
+					sourceUrl
+				}
+			}
+			mediaItemUrl
+		}
+	}
+}
diff --git a/packages/query/src/query-engine/index.ts b/packages/query/src/query-engine/index.ts
index ce4c5e1e..4b43667a 100644
--- a/packages/query/src/query-engine/index.ts
+++ b/packages/query/src/query-engine/index.ts
@@ -1,6 +1,7 @@
 import { getGraphqlUrl, getConfig } from '@snapwp/core/config';
 import {
 	GetCurrentTemplateDocument,
+	GetGeneralSettingsDocument,
 	GetGlobalStylesDocument,
 } from '@graphqlTypes/graphql';
 import {
@@ -14,6 +15,7 @@ import {
 import parseTemplate from '@/utils/parse-template';
 import parseGlobalStyles from '@/utils/parse-global-styles';
 import { Logger, type GlobalHeadProps } from '@snapwp/core';
+import parseGeneralSettings from '@/utils/parse-general-settings';
 
 /**
  * Singleton class to handle GraphQL queries using Apollo.
@@ -88,6 +90,42 @@ export class QueryEngine {
 		}
 	};
 
+	/**
+	 * Fetches the general settings, like favicon icon.
+	 *
+	 * @return General settings data.
+	 */
+	static getGeneralSettings = async () => {
+		if ( ! QueryEngine.isClientInitialized ) {
+			QueryEngine.initialize();
+		}
+
+		try {
+			const data = await QueryEngine.apolloClient.query( {
+				query: GetGeneralSettingsDocument,
+				fetchPolicy: 'network-only', // @todo figure out a caching strategy, instead of always fetching from network
+				errorPolicy: 'all',
+			} );
+
+			return parseGeneralSettings( data );
+		} catch ( error ) {
+			if ( error instanceof ApolloError ) {
+				logApolloErrors( error );
+
+				// If there are networkError throw the error with proper message.
+				if ( error.networkError ) {
+					// Throw the error with proper message.
+					throw new Error(
+						getNetworkErrorMessage( error.networkError )
+					);
+				}
+			}
+
+			// If error is not an instance of ApolloError, throw the error again.
+			throw error;
+		}
+	};
+
 	/**
 	 * Fetches blocks, scripts and styles for the given uri.
 	 * @param uri - The URL of the seed node.
diff --git a/packages/query/src/utils/parse-general-settings.ts b/packages/query/src/utils/parse-general-settings.ts
new file mode 100644
index 00000000..73c41bc1
--- /dev/null
+++ b/packages/query/src/utils/parse-general-settings.ts
@@ -0,0 +1,67 @@
+import { Logger } from '@snapwp/core';
+import type { GetGeneralSettingsQuery } from '@graphqlTypes/graphql';
+import type { ApolloQueryResult } from '@apollo/client';
+
+/**
+ * @param queryData - The data fetched from the general settings query.
+ *
+ * @throws Throws an error if the query data is missing or invalid.
+ *
+ * @return An object containing parsed general settings data.
+ */
+export default function parseGeneralSettings(
+	queryData: ApolloQueryResult< GetGeneralSettingsQuery >
+) {
+	if ( queryData.errors?.length ) {
+		queryData.errors?.forEach( ( error ) => {
+			Logger.error( `Error fetching global styles: ${ error }` );
+		} );
+	}
+
+	// If queryData does not have generalSettings or siteIcon, return undefined because without siteIcon, generalSettings is not valid.
+	if ( ! queryData.data.generalSettings?.siteIcon ) {
+		return undefined;
+	}
+
+	const sizes:
+		| undefined
+		| { sourceUrl: string; height: string; width: string }[] = [];
+
+	// If mediaDetails and sizes are present, parse the sizes.
+	if ( queryData.data.generalSettings.siteIcon.mediaDetails?.sizes ) {
+		queryData.data.generalSettings.siteIcon.mediaDetails.sizes.forEach(
+			( size ) => {
+				// If size is null or undefined skip the iteration.
+				if ( ! size ) {
+					return;
+				}
+
+				// If sourceUrl, height and width are not present, skip the iteration.
+				if ( ! ( size.sourceUrl && size.height && size.width ) ) {
+					return;
+				}
+
+				sizes.push( {
+					sourceUrl: size.sourceUrl,
+					height: size.height,
+					width: size.width,
+				} );
+			}
+		);
+	}
+
+	// MediaItemUrl and mediaDetails are optional fields. Meaning if either of them is there, it should be returned.
+	return {
+		generalSettings: {
+			siteIcon: {
+				// If mediaItemUrl is not present, return undefined because it is an optional field.
+				mediaItemUrl:
+					queryData.data.generalSettings.siteIcon.mediaItemUrl ||
+					undefined,
+				mediaDetails: {
+					sizes,
+				},
+			},
+		},
+	};
+}


Reviews:
user: login: ayushnirwal
state: CHANGES_REQUESTED
author_association: COLLABORATOR
submitted_at: 2025-03-05T07:08:58Z
user: login: SH4LIN
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-05T07:55:12Z
user: login: ayushnirwal
body: LGTM
state: DISMISSED
author_association: COLLABORATOR
submitted_at: 2025-03-05T09:31:22Z
user: login: ayushnirwal
state: CHANGES_REQUESTED
author_association: COLLABORATOR
submitted_at: 2025-03-06T08:20:03Z
user: login: justlevine
body: Functionally working 🚀

Feedback on composability/code quality patterns below:
state: CHANGES_REQUESTED
author_association: COLLABORATOR
submitted_at: 2025-03-09T22:04:39Z
user: login: SH4LIN
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-11T13:17:56Z
user: login: SH4LIN
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-11T13:22:16Z
user: login: SH4LIN
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-11T13:37:50Z
user: login: justlevine
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-11T19:54:42Z
user: login: justlevine
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-11T21:23:00Z
user: login: justlevine
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-11T21:28:08Z
user: login: justlevine
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-11T21:30:18Z
user: login: justlevine
body: Functionally, things are passing, but I would like @ayushnirwal 's feedback on the composability (generally, and per the open conversations on this diff).

Once that's done (since the package diff might change) `npm run changeset` and we should be able to merge this. 
state: CHANGES_REQUESTED
author_association: COLLABORATOR
submitted_at: 2025-03-18T17:27:45Z
user: login: ayushnirwal
body: Although the patterns around queries, documents, client management are not ideal in the current state. I'm willing to merge this PR (after the last few feedbacks are addressed) for the following reasons. 

- It works.
- This adds parsers, some of the types around favicon and meta data management. When we implement the metadata management and data fetching management interfaces these will be used.
state: CHANGES_REQUESTED
author_association: COLLABORATOR
submitted_at: 2025-03-19T08:37:30Z
user: login: justlevine
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-19T10:32:21Z
user: login: justlevine
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-19T10:37:28Z
user: login: justlevine
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-19T10:39:45Z
user: login: ayushnirwal
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-03-19T11:23:02Z
user: login: justlevine
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-19T17:44:28Z
user: login: justlevine
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-19T20:14:49Z
user: login: justlevine
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-03-19T20:15:11Z

Review comments:
diff_hunk: @@ -30,3 +31,19 @@ export async function RootLayout( {
 		</html>
 	);
 }
+
+/**
+ * Export our custom metadata like icons, manifest, SEO metadata etc.
+ * This function can be async (https://nextjs.org/docs/app/api-reference/functions/generate-metadata#async-function) and we can also use fetch API or make a GraphQL request to fetch data from server.
+ *
+ * @return Merged metadata.
+ */
+export function generateRootMetaData(): Metadata {
+	return {
+		icons: {
+			// This is an interim solution for the favicon.ico making a separate GraphQL request, we are setting # as icon so Browser won't make any default request to /favicon.ico path.
+			// Todo: Once the site icon field is exposed make graphQL request to get that URL and replace it with # and remove these both comments.
path: packages/next/src/root-layout/index.tsx
user: login: ayushnirwal
body: ```suggestion
			// @todo: Once the site icon field is exposed make graphQL request to get that URL and replace it with # and remove these both comments.
```
created_at: 2025-03-05T06:38:52Z
author_association: COLLABORATOR
diff_hunk: @@ -30,3 +31,19 @@ export async function RootLayout( {
 		</html>
 	);
 }
+
+/**
+ * Export our custom metadata like icons, manifest, SEO metadata etc.
+ * This function can be async (https://nextjs.org/docs/app/api-reference/functions/generate-metadata#async-function) and we can also use fetch API or make a GraphQL request to fetch data from server.
+ *
+ * @return Merged metadata.
+ */
+export function generateRootMetaData(): Metadata {
path: packages/next/src/root-layout/index.tsx
user: login: ayushnirwal
body: Since SEO is our goal for the next release, this seems to be a good enough place to establish a pattern for metadata overloading (Please feel free to disagree)

The pattern that I'm proposing is - we have functions for generating any metadata for this case (and the scope of this PR) there will be only one function `generateIcons`. Later we can add other functions ( `generateOpenGraph`, `generateRobots` etc). `generateRootMetaData` will use all these functions to generate the full metadata.

Users will have the following options in their next app.
- Use `generateRootMetaData` directly.
- Use `generateRootMetaData` to get default meta-data and add/update the object.
- Use compartmentalized functions like `generateIcons` and compose them to make a custom `generateRootMeta` function.
- Use compartmentalized functions in their own `sitemap.ts`, `robots.ts` files in the next app.
created_at: 2025-03-05T06:53:41Z
author_association: COLLABORATOR
diff_hunk: @@ -1,4 +1,5 @@
-import { RootLayout } from '@snapwp/next';
+import { RootLayout, generateRootMetaData } from '@snapwp/next';
path: examples/nextjs/starter/src/app/layout.tsx
user: login: ayushnirwal
body: Nit: I disagree with the notion of adding `root` to meta-data semantics. Just say metadata.

Why - I don't see [Nextjs's documentation](https://nextjs.org/docs/app/building-your-application/optimizing/metadata) using "root". Although they have used parent though in [this](https://arc.net/l/quote/thsbzuey) example. Let's omit using "root" like them.
created_at: 2025-03-05T07:08:38Z
author_association: COLLABORATOR
diff_hunk: @@ -1,4 +1,5 @@
-import { RootLayout } from '@snapwp/next';
+import { RootLayout, generateRootMetaData } from '@snapwp/next';
path: examples/nextjs/starter/src/app/layout.tsx
user: login: SH4LIN
body: used `root` in the context of RootLayout that this metadata is generated from the RootLayout component (Metadata related to the RootLayout). When this function is imported in app/layout.tsx, there will be a generateMetaData function so we will need something to differentiate between that generateMetaData and our meta data from the root layout
created_at: 2025-03-05T07:55:11Z
author_association: CONTRIBUTOR
diff_hunk: @@ -30,3 +32,132 @@ export async function RootLayout( {
 		</html>
 	);
 }
+
+type IconTypes = {
+	faviconIcons: Icon[];
+	appleIcons: Icon[];
+	msApplicationTileIcon: Icon;
+};
+
+type Icon = {
+	sourceUrl: string;
+	height: string;
+	width: string;
+};
+
+type FormattedIcon = {
+	url: string;
+	sizes: string;
+};
path: packages/next/src/root-layout/index.tsx
user: login: ayushnirwal
body: Move types and interfaces to `@snapwp/types`


created_at: 2025-03-06T08:14:48Z
author_association: COLLABORATOR
path: packages/next/src/root-layout/index.tsx
user: login: ayushnirwal
body: I think it is time to update the file structure. Leave `generateRootMetaData` in this file. Move `generateIconsMetaData` to its file with its utilities.
created_at: 2025-03-06T08:16:08Z
author_association: COLLABORATOR
diff_hunk: @@ -1,4 +1,5 @@
-import { RootLayout } from '@snapwp/next';
+import { RootLayout, generateRootMetaData } from '@snapwp/next';
+import type { Metadata } from 'next';
 
 export default function Layout( { children }: { children: React.ReactNode } ) {
path: examples/nextjs/starter/src/app/layout.tsx
user: login: justlevine
body: Not sure how this slipped through. but this should be a `PropsWithChildren` typing situation. We should *avoid* directly typing `children` as a prop whenever possible.
created_at: 2025-03-09T21:46:13Z
author_association: COLLABORATOR
path: packages/next/src/root-layout/icons-metadata.ts
user: login: justlevine
body: Nothing here is next-js specific...
created_at: 2025-03-09T21:47:16Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,14 @@
+query GetGeneralSettings {
+	generalSettings {
+		siteIcon {
path: packages/query/src/queries/get-general-settings.graphql
user: login: justlevine
body: Add the global `id` here for caching. (General rule for `Node` objects to help with caching, but extra important when using Apollo).
created_at: 2025-03-09T21:49:14Z
author_association: COLLABORATOR
diff_hunk: @@ -88,6 +89,42 @@ export class QueryEngine {
 		}
 	};
 
+	/**
+	 * Fetches the general settings, like favicon icon.
+	 *
+	 * @return General settings data.
+	 */
+	static getGeneralSettings = async () => {
path: packages/query/src/query-engine/index.ts
user: login: justlevine
body: I'm not spending time reviewing this function internals, since the entire pattern needs to be ripped out of this class as part of making our Query Engine ~less-shitty~ composable. 

If it works, it's good enough for this PR -  🙈
created_at: 2025-03-09T21:52:00Z
author_association: COLLABORATOR
path: packages/types/src/queries/index.ts
user: login: justlevine
body: This is either counterintuitive _or_ indicative of a horrible antipattern hiding in this PR.

As a reminder, separation of concerns requires GraphQL Query (codegen-types) => our parsers/adapers => agnostic/composable frontend components.

IOW **only** types we should be distributing are the agnosting ones that our components expect. Further, the only reason for defining the types in the global types packages (versus colocated/directly `typeof ComponentProps`d) is to avoid circular dependency issues.

Tbe entire concept of a `types/queries` namespace hints to a coupling that's the exact opposite of the above behavior.
created_at: 2025-03-09T22:01:05Z
author_association: COLLABORATOR
diff_hunk: @@ -30,3 +32,132 @@ export async function RootLayout( {
 		</html>
 	);
 }
+
+type IconTypes = {
+	faviconIcons: Icon[];
+	appleIcons: Icon[];
+	msApplicationTileIcon: Icon;
+};
+
+type Icon = {
+	sourceUrl: string;
+	height: string;
+	width: string;
+};
+
+type FormattedIcon = {
+	url: string;
+	sizes: string;
+};
path: packages/next/src/root-layout/index.tsx
user: login: justlevine
body: Related (possible disagreement - didnt look into how the types are used, naming convention there was enough for me to 🚩 ): https://github.com/rtCamp/snapwp/pull/119#discussion_r1986430854
created_at: 2025-03-09T22:03:04Z
author_association: COLLABORATOR
path: packages/next/src/root-layout/icons-metadata.ts
user: login: SH4LIN
body: I’m sorry, I’m a bit confused here. What should I do here? If nothing is Next.js specific, should I move it to another package? However, it wouldn’t be specific to that package either. The function that consumes this is specific to the Next.js Metadata API.
created_at: 2025-03-11T13:17:56Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,4 +1,5 @@
-import { RootLayout } from '@snapwp/next';
+import { RootLayout, generateRootMetaData } from '@snapwp/next';
+import type { Metadata } from 'next';
 
 export default function Layout( { children }: { children: React.ReactNode } ) {
path: examples/nextjs/starter/src/app/layout.tsx
user: login: SH4LIN
body: That's not slipped in. The Next.js documentation uses this approach in their docs – [[Next.js Layouts](https://nextjs.org/docs/app/api-reference/file-conventions/layout#root-layouts)]  

Whoever implemented this followed the same approach.
created_at: 2025-03-11T13:22:15Z
author_association: CONTRIBUTOR
path: packages/types/src/queries/index.ts
user: login: SH4LIN
body: Please correct me if I misunderstood anything.

We want our types to be as close to the component as possible, defining the exact type that the component expects. For the parser, we have decided not to modify anything; instead, we will pass the data down to the component and let it handle any nullish values. Correct?
created_at: 2025-03-11T13:37:49Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,4 +1,5 @@
-import { RootLayout } from '@snapwp/next';
+import { RootLayout, generateRootMetaData } from '@snapwp/next';
+import type { Metadata } from 'next';
 
 export default function Layout( { children }: { children: React.ReactNode } ) {
path: examples/nextjs/starter/src/app/layout.tsx
user: login: justlevine
body: "Slipped through" means got passed code review; I assume that developers have a reason for _all_ their code decisions ;-)

If for some reason we cannot use `PropsWithChildren`, then:
1. `import type { ReactNode } from 'react';
2. Add a comment explaining why we cant use `PropsWithChildren`.
created_at: 2025-03-11T19:54:42Z
author_association: COLLABORATOR
path: packages/types/src/queries/index.ts
user: login: justlevine
body: > We want our types to be as close to the component as possible, defining the exact type that the component expects. 

Correct.

> For the parser, we have decided not to modify anything; instead, we will pass the data down to the component and let it handle any nullish values.

Ummmm it depends?
We're creating an _agnostic_ component, the purpose of the parser is to coerce the GraphQL data into the shape expected by the component.
In general the reason to allow nullish values on the component props is so users can use them directly with minimal headaches so that's the theoretical "ideal shape", _not_ because any constraints on the parser.

created_at: 2025-03-11T21:22:59Z
author_association: COLLABORATOR
path: packages/next/src/root-layout/icons-metadata.ts
user: login: justlevine
body: If there is value in a _user_ accessing these functions to do something outside of nextjs (favicons in cra?), then it make's sense to split it up. (cc @ayushnirwal for final decision)
created_at: 2025-03-11T21:28:07Z
author_association: COLLABORATOR
diff_hunk: @@ -1,9 +1,23 @@
-import { RootLayout } from '@snapwp/next';
+import { RootLayout, generateRootMetaData } from '@snapwp/next';
+import type { Metadata } from 'next';
+import { PropsWithChildren } from 'react';
path: examples/nextjs/starter/src/app/layout.tsx
user: login: justlevine
body: 
```suggestion
import type { PropsWithChildren } from 'react';
```
created_at: 2025-03-11T21:30:17Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,140 @@
+import { QueryEngine } from '@snapwp/query';
+
+interface GeneralSettingsProps {
+	generalSettings: {
+		siteIcon: {
+			mediaItemUrl?: string;
+			mediaDetails: {
+				sizes?: IconData[];
+			};
+		};
+	};
+}
+
+type IconMetaData = {
path: packages/next/src/root-layout/icons-metadata.ts
user: login: ayushnirwal
body: ```suggestion
interface IconMetaData = {
```
created_at: 2025-03-19T08:35:26Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,140 @@
+import { QueryEngine } from '@snapwp/query';
+
+interface GeneralSettingsProps {
+	generalSettings: {
+		siteIcon: {
+			mediaItemUrl?: string;
+			mediaDetails: {
+				sizes?: IconData[];
+			};
+		};
+	};
+}
+
+type IconMetaData = {
+	faviconIcons: FormattedIconData[];
+	appleIcons: FormattedIconData[];
+	msApplicationTileIcon: IconData;
+};
+
+type IconData = {
path: packages/next/src/root-layout/icons-metadata.ts
user: login: ayushnirwal
body: ```suggestion
interface IconData = {
```
created_at: 2025-03-19T08:35:38Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,140 @@
+import { QueryEngine } from '@snapwp/query';
+
+interface GeneralSettingsProps {
+	generalSettings: {
+		siteIcon: {
+			mediaItemUrl?: string;
+			mediaDetails: {
+				sizes?: IconData[];
+			};
+		};
+	};
+}
+
+type IconMetaData = {
+	faviconIcons: FormattedIconData[];
+	appleIcons: FormattedIconData[];
+	msApplicationTileIcon: IconData;
+};
+
+type IconData = {
+	sourceUrl: string;
+	height: string;
+	width: string;
+};
+
+type FormattedIconData = {
path: packages/next/src/root-layout/icons-metadata.ts
user: login: ayushnirwal
body: ```suggestion
interface FormattedIconData = {
```
created_at: 2025-03-19T08:35:57Z
author_association: COLLABORATOR
path: packages/types/src/queries/index.ts
user: login: justlevine
body: Making as resolved since the file is gone.
created_at: 2025-03-19T10:32:21Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,140 @@
+import { QueryEngine } from '@snapwp/query';
+
+interface GeneralSettingsProps {
+	generalSettings: {
+		siteIcon: {
+			mediaItemUrl?: string;
+			mediaDetails: {
+				sizes?: IconData[];
+			};
+		};
+	};
+}
+
+interface IconMetaData {
+	faviconIcons: FormattedIconData[];
+	appleIcons: FormattedIconData[];
+	msApplicationTileIcon: IconData;
+}
+
+interface IconData {
+	sourceUrl: string;
+	height: string;
+	width: string;
+}
+
+interface FormattedIconData {
+	url: string;
+	sizes: string;
+}
+
+/**
+ * Fetch site icon data and filter them into categorized formats.
+ * @see - https://developer.wordpress.org/reference/functions/wp_site_icon/#:~:text=Displays%20site%20icon%20meta%20tags. Reason why we are different resolution icons into individual category.
path: packages/next/src/root-layout/icons-metadata.ts
user: login: justlevine
body: Documenting https://github.com/rtCamp/snapwp/pull/119#issuecomment-2736076355

```suggestion
 *
 * @see - https://developer.wordpress.org/reference/functions/wp_site_icon/#:~:text=Displays%20site%20icon%20meta%20tags. Reason why we are different resolution icons into individual category.
 *
 * @todo Refactor for composability alongside SEO metadata patterns 
```
created_at: 2025-03-19T10:37:28Z
author_association: COLLABORATOR
diff_hunk: @@ -30,3 +32,26 @@ export async function RootLayout( {
 		</html>
 	);
 }
+
+/**
+ * Generate and return root metadata, including icons and other metadata.
+ *
+ * @return Merged metadata.
+ */
+export async function generateRootMetaData(): Promise< Metadata > {
+	// Fetch icons in required format, apply faviconIcons and apple touch icons in icons metadata property while apply msapplication-TileImage in other metadata property
path: packages/next/src/root-layout/index.tsx
user: login: justlevine
body: Documenting https://github.com/rtCamp/snapwp/pull/119#issuecomment-2736076355

```suggestion
	/**
	 * Fetch icons in required format, apply faviconIcons and apple touch icons in icons metadata property while apply msapplication-TileImage in other metadata property.
	 *
	 * @todo Review composability when implementing SEO metadata
	 */
```
created_at: 2025-03-19T10:39:45Z
author_association: COLLABORATOR
diff_hunk: @@ -36,19 +36,28 @@ interface FormattedIconData {
  *
  * @return Categorized icons.
  */
-export const getIcons = async (): Promise< Partial< IconMetaData > > => {
+export const getIcons = async (): Promise< IconMetaData > => {
 	const settings: GeneralSettingsProps | undefined =
 		await QueryEngine.getGeneralSettings();
 
 	if ( ! settings ) {
-		return {};
+		return {
+			faviconIcons: [],
+			appleIcons: undefined,
+			msApplicationTileIcon: undefined,
+		};
 	}
 
-	let fallBackIcons = {};
+	let fallBackIcons: IconMetaData = {
path: packages/next/src/root-layout/icons-metadata.ts
user: login: justlevine
body: ```suggestion
	let fallbackIcons: IconMetaData = {
```
created_at: 2025-03-19T17:44:28Z
author_association: COLLABORATOR
path: .changeset/late-breads-listen.md
user: login: justlevine
body: Reminder, changesets is "dumb" and `major|minor|patch` is mapped blindly to `x.y.z`, not real semver
created_at: 2025-03-19T20:14:48Z
author_association: COLLABORATOR

PR comments:
user: login: changeset-bot[bot]
created_at: 2025-03-05T05:34:11Z
body: ###  🦋  Changeset detected

Latest commit: 7fea521c42befa88f6abd430ae0b754d3930d002

**The changes in this PR will be included in the next version bump.**

<details><summary>This PR includes changesets to release 3 packages</summary>

  | Name          | Type  |
| ------------- | ----- |
| @snapwp/query | Patch |
| @snapwp/next  | Patch |
| snapwp        | Patch |

</details>

Not sure what this means? [Click here  to learn what changesets are](https://github.com/changesets/changesets/blob/main/docs/adding-a-changeset.md).

[Click here if you're a maintainer who wants to add another changeset to this PR](https://github.com/SH4LIN/snapwp/new/fix/favicon-loading?filename=.changeset/tough-waves-stare.md&value=---%0A%22%40snapwp%2Fnext%22%3A%20patch%0A%22%40snapwp%2Fquery%22%3A%20patch%0A---%0A%0Afeat%3A%20load%20favicon%20from%20WordPress%20server%0A)


author_association: NONE
user: login: justlevine
created_at: 2025-03-19T09:36:23Z
body: > Although the patterns around queries, documents, client management are not ideal in the current state. I'm willing to merge this PR 

@ayushnirwal can you flag some parts of the diff that deserve a `// @todo` and/or generally list what the next steps should theoretically be for improving things?

I don't want to merge this PR without making sure we have a (vague) plan to follow it up.

author_association: COLLABORATOR
user: login: ayushnirwal
created_at: 2025-03-19T09:55:02Z
body: ```typescript
import type { DocumentNode, TypedQueryDocumentNode } from 'graphql';

export type Variables = Record< string, unknown >;

export type FetchQuery< T = unknown, V extends Variables = Variables > = (
	queryKey: string[],
	document: DocumentNode | TypedQueryDocumentNode< T, V >
) => Promise< T >;

export type getServerClient = (
	url: string,
	options: Record< string, unknown >
) => ServerClient;

export type getBrowserClient = (
	url: string,
	options: Record< string, unknown >
) => BrowserClient;

export interface ServerClient {
	fetchQuery: FetchQuery;
}

export interface BrowserClient {}

```

These types/interfaces are the general broad strokes for the composability in our data-fetching mechanisms. This allows users to 
- Implement their own ServerClient with the custom `getServerClient` and `fetchQuery`
- Compose their own ServerClient with the `getServerClient` and `fetchQuery` with implementations exported from `@snapwp/query`
- Use a pre-implemented ServerClient exported from `@snapwp/query`
- Any implementation of ServerClient will have the `fetchQuery` function to which a doc can be passed for querying.
- Wrappers will be built around `FetchQuery` to get functions like `FetchGlobalStyles`. These will be exported for internal or external use. 

author_association: COLLABORATOR
user: login: justlevine
created_at: 2025-03-19T10:04:39Z
body: @ayushnirwal anything specific to favicons / site metadata (see the remaining open conversations on this PR) or just the underlying Query API?
author_association: COLLABORATOR
user: login: ayushnirwal
created_at: 2025-03-19T10:28:08Z
body: > Wrappers will be built around FetchQuery to get functions like FetchGlobalStyles. These will be exported for internal or external use.

- Similar to this we will create a wrapper `FetchFaviconDetails`
- `getIcons` (without the fetch call) and its internal utilities become data parsing utilities. Which allows us to move it to `@snapwp/core` along with the related types.  
- `FetchFaviconDetails` and renamed `getIcons` will be then composed to make `getMetaData` in `@snapwp/next`.
- The same pattern will be followed for the SEO-related meta data too.  
author_association: COLLABORATOR
user: login: justlevine
created_at: 2025-03-19T14:55:25Z
body: @SH4LIN what's with the failing tests?

Also can you add a changeset ?

(Once those are resolved this should be good to merge)
author_association: COLLABORATOR
user: login: SH4LIN
created_at: 2025-03-19T17:22:59Z
body: @justlevine Checks are passing now
author_association: CONTRIBUTOR