diff --git a/examples/sveltekit/template-hierarchy/.gitignore b/examples/sveltekit/template-hierarchy/.gitignore new file mode 100644 index 000000000..c7e35228d --- /dev/null +++ b/examples/sveltekit/template-hierarchy/.gitignore @@ -0,0 +1,25 @@ +node_modules + +# Output +.output +.vercel +.netlify +.wrangler +/.svelte-kit +/build + +# OS +.DS_Store +Thumbs.db + +# Env +.env +.env.* +!.env.example +!.env.test + +# Vite +vite.config.js.timestamp-* +vite.config.ts.timestamp-* + +package-lock.json \ No newline at end of file diff --git a/examples/sveltekit/template-hierarchy/README.md b/examples/sveltekit/template-hierarchy/README.md new file mode 100644 index 000000000..2f6cc332c --- /dev/null +++ b/examples/sveltekit/template-hierarchy/README.md @@ -0,0 +1,88 @@ +# FaustJS SvelteKit Template Hierarchy Example + +This example demonstrates how to use FaustJS with SvelteKit to create a headless WordPress application with automatic template hierarchy support. + +## What is Template Hierarchy? + +WordPress template hierarchy determines which template file is used to display different types of content (posts, pages, archives, etc.). This example shows how to implement similar functionality in a SvelteKit application using FaustJS. + +## Features + +- ✅ Automatic template selection based on WordPress content type +- ✅ Support for custom post types and archives +- ✅ WordPress-style template hierarchy (single.svelte, archive.svelte, index.svelte) +- ✅ GraphQL data fetching with URQL +- ✅ Server-side rendering (SSR) + +## Getting Started + +### Prerequisites + +- Node.js v16.0.0 or newer +- A WordPress site with the [FaustJS plugin](https://wordpress.org/plugins/faustwp/) installed +- WPGraphQL plugin installed on your WordPress site + +### Installation + +1. Clone this repository or copy this example +2. Install dependencies: + +```bash +npm install +``` + +3. Configure your WordPress URL in the `.env` file: + +```bash +WORDPRESS_URL=https://your-wordpress-site.com +``` + +### Development + +Start the development server: + +```bash +npm run dev + +# or start the server and open the app in a new browser tab +npm run dev -- --open +``` + +The application will automatically: + +- Fetch content from your WordPress site +- Determine the appropriate template based on the URL +- Render the content using the matching Svelte template + +### Template Structure + +Templates are located in `src/wp-templates/`: + +- `index.svelte` - Default template (homepage, fallback) +- `single.svelte` - Single post/page template +- `archive.svelte` - Archive pages (categories, tags, custom post types) + +### Building + +To create a production version of your app: + +```bash +npm run build +``` + +You can preview the production build with `npm run preview`. + +## How It Works + +1. The `[...uri]/+page.server.js` route catches all URLs +2. Uses `uriToTemplate()` from `@faustjs/sveltekit` to: + - Query WordPress for content at the given URI + - Determine the appropriate template type + - Fetch the necessary data +3. Renders the content using the matching Svelte template + +## Learn More + +- [FaustJS Documentation](https://faustjs.org/docs/) +- [SvelteKit Documentation](https://kit.svelte.dev/docs) +- [WordPress Template Hierarchy](https://developer.wordpress.org/themes/basics/template-hierarchy/) diff --git a/examples/sveltekit/template-hierarchy/package.json b/examples/sveltekit/template-hierarchy/package.json new file mode 100644 index 000000000..45d741df3 --- /dev/null +++ b/examples/sveltekit/template-hierarchy/package.json @@ -0,0 +1,31 @@ +{ + "name": "@faustjs/sveltekit-template-hierarchy-example", + "private": true, + "version": "0.1.0", + "license": "0BSD", + "type": "module", + "scripts": { + "dev": "vite dev", + "build": "vite build", + "preview": "vite preview", + "prepare": "svelte-kit sync || echo ''", + "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", + "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch" + }, + "devDependencies": { + "@faustjs/sveltekit": "workspace:*", + "@faustjs/template-hierarchy": "workspace:*", + "@sveltejs/adapter-auto": "^6.0.0", + "@sveltejs/kit": "^2.16.0", + "@sveltejs/vite-plugin-svelte": "^5.0.0", + "svelte": "^5.0.0", + "svelte-check": "^4.0.0", + "vite": "^6.2.6" + }, + "dependencies": { + "@urql/core": "^5.1.1", + "@urql/exchange-persisted": "^4.3.1", + "deepmerge": "^4.3.1", + "graphql": "^16.11.0" + } +} diff --git a/examples/sveltekit/template-hierarchy/src/app.html b/examples/sveltekit/template-hierarchy/src/app.html new file mode 100644 index 000000000..77a5ff52c --- /dev/null +++ b/examples/sveltekit/template-hierarchy/src/app.html @@ -0,0 +1,12 @@ + + + + + + + %sveltekit.head% + + +
%sveltekit.body%
+ + diff --git a/examples/sveltekit/template-hierarchy/src/hooks.server.js b/examples/sveltekit/template-hierarchy/src/hooks.server.js new file mode 100644 index 000000000..fd6a32612 --- /dev/null +++ b/examples/sveltekit/template-hierarchy/src/hooks.server.js @@ -0,0 +1,14 @@ +import { dev } from '$app/environment'; + +export const handle = async ({ event, resolve }) => { + if ( + dev && + event.url.pathname === '/.well-known/appspecific/com.chrome.devtools.json' + ) { + return new Response(undefined, { status: 404 }); + } + + return resolve(event, { + filterSerializedResponseHeaders: () => true, // basically get all headers + }); +}; diff --git a/examples/sveltekit/template-hierarchy/src/routes/[...uri]/+layout.svelte b/examples/sveltekit/template-hierarchy/src/routes/[...uri]/+layout.svelte new file mode 100644 index 000000000..7cabee222 --- /dev/null +++ b/examples/sveltekit/template-hierarchy/src/routes/[...uri]/+layout.svelte @@ -0,0 +1,18 @@ + + +
+ {@render children()} +
+ + diff --git a/examples/sveltekit/template-hierarchy/src/routes/[...uri]/+page.js b/examples/sveltekit/template-hierarchy/src/routes/[...uri]/+page.js new file mode 100644 index 000000000..ab81a48d0 --- /dev/null +++ b/examples/sveltekit/template-hierarchy/src/routes/[...uri]/+page.js @@ -0,0 +1,10 @@ +export const load = async (event) => { + const { data } = event; + + const template = await import(`$wp/${data.templateData.template.id}.svelte`); + + return { + ...data, + template: template.default, + }; +}; diff --git a/examples/sveltekit/template-hierarchy/src/routes/[...uri]/+page.server.js b/examples/sveltekit/template-hierarchy/src/routes/[...uri]/+page.server.js new file mode 100644 index 000000000..f05215e35 --- /dev/null +++ b/examples/sveltekit/template-hierarchy/src/routes/[...uri]/+page.server.js @@ -0,0 +1,29 @@ +import { + createDefaultClient, + setGraphQLClient, + uriToTemplate, +} from '@faustjs/sveltekit'; +import { WORDPRESS_URL } from '$env/static/private'; + +export const load = async (event) => { + const { + params: { uri }, + fetch, + } = event; + + const workingUri = uri || '/'; + + const client = createDefaultClient(WORDPRESS_URL); + setGraphQLClient(client); + + const templateData = await uriToTemplate({ + fetch, + uri: workingUri, + graphqlClient: client, + }); + + return { + uri: workingUri, + templateData, + }; +}; diff --git a/examples/sveltekit/template-hierarchy/src/routes/[...uri]/+page.svelte b/examples/sveltekit/template-hierarchy/src/routes/[...uri]/+page.svelte new file mode 100644 index 000000000..9da53e09e --- /dev/null +++ b/examples/sveltekit/template-hierarchy/src/routes/[...uri]/+page.svelte @@ -0,0 +1,5 @@ + + + diff --git a/examples/sveltekit/template-hierarchy/src/routes/api/templates/+server.js b/examples/sveltekit/template-hierarchy/src/routes/api/templates/+server.js new file mode 100644 index 000000000..0bab26bae --- /dev/null +++ b/examples/sveltekit/template-hierarchy/src/routes/api/templates/+server.js @@ -0,0 +1,31 @@ +import { readdir } from 'node:fs/promises'; +import { join } from 'node:path'; +import { json } from '@sveltejs/kit'; +const TEMPLATE_PATH = 'wp-templates'; + +export const GET = async ({ url }) => { + const uri = url.searchParams.get('uri'); + + if (!uri) { + return new Response('Missing URI', { status: 400 }); + } + + const files = await readdir(join('src', TEMPLATE_PATH)); + + const templates = []; + + for (const file of files) { + if (file.startsWith('+')) { + continue; + } + + const slug = file.replace('.svelte', ''); + + templates.push({ + id: slug, + path: join('/', TEMPLATE_PATH, slug), + }); + } + + return json(templates); +}; diff --git a/examples/sveltekit/template-hierarchy/src/wp-templates/archive.svelte b/examples/sveltekit/template-hierarchy/src/wp-templates/archive.svelte new file mode 100644 index 000000000..be014dc4c --- /dev/null +++ b/examples/sveltekit/template-hierarchy/src/wp-templates/archive.svelte @@ -0,0 +1,90 @@ + + + + Archive Template - WordPress Archive + + +

WordPress Archive Template

+

+ This template would render WordPress archive pages (category, tag, date, + etc.). +

+ +
+

Archive: {archiveType} - {archiveName}

+

Showing posts from this {archiveType.toLowerCase()}...

+ + {#each posts as post (post.id)} +
+

{post.title}

+

{post.excerpt}

+ Posted on {post.date} +
+ {/each} +
+ + diff --git a/examples/sveltekit/template-hierarchy/src/wp-templates/index.svelte b/examples/sveltekit/template-hierarchy/src/wp-templates/index.svelte new file mode 100644 index 000000000..26ea752f8 --- /dev/null +++ b/examples/sveltekit/template-hierarchy/src/wp-templates/index.svelte @@ -0,0 +1,112 @@ + + + + Home Template - WordPress Blog + + +

WordPress Home/Index Template

+

This template would render the WordPress homepage or blog index.

+ +
+ + {#each posts as post (post.id)} +
+

+ {post.title} +

+

{post.excerpt}

+ +
+ {/each} + + {#if posts.length === 0} +

No posts found.

+ {/if} +
+ + diff --git a/examples/sveltekit/template-hierarchy/src/wp-templates/page.svelte b/examples/sveltekit/template-hierarchy/src/wp-templates/page.svelte new file mode 100644 index 000000000..bbc3d87af --- /dev/null +++ b/examples/sveltekit/template-hierarchy/src/wp-templates/page.svelte @@ -0,0 +1,40 @@ + + + + Page Template + + +

WordPress Page Template

+

This template would render individual WordPress posts.

+
+ +

+ Page title, content, and metadata would be displayed here using data from + the WordPress GraphQL API. +

+
+ + diff --git a/examples/sveltekit/template-hierarchy/src/wp-templates/single.svelte b/examples/sveltekit/template-hierarchy/src/wp-templates/single.svelte new file mode 100644 index 000000000..b1cc61beb --- /dev/null +++ b/examples/sveltekit/template-hierarchy/src/wp-templates/single.svelte @@ -0,0 +1,40 @@ + + + + Single Post Template + + +

WordPress Single Post Template

+

This template would render individual WordPress posts.

+
+ +

+ Post title, content, and metadata would be displayed here using data from + the WordPress GraphQL API. +

+
+ + diff --git a/examples/sveltekit/template-hierarchy/static/favicon.png b/examples/sveltekit/template-hierarchy/static/favicon.png new file mode 100644 index 000000000..825b9e65a Binary files /dev/null and b/examples/sveltekit/template-hierarchy/static/favicon.png differ diff --git a/examples/sveltekit/template-hierarchy/svelte.config.js b/examples/sveltekit/template-hierarchy/svelte.config.js new file mode 100644 index 000000000..3722c7ed4 --- /dev/null +++ b/examples/sveltekit/template-hierarchy/svelte.config.js @@ -0,0 +1,20 @@ +import adapter from '@sveltejs/adapter-auto'; +import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; + +const config = { + // Consult https://svelte.dev/docs/kit/integrations + // for more information about preprocessors + preprocess: vitePreprocess(), + kit: { + // adapter-auto only supports some environments, see https://svelte.dev/docs/kit/adapter-auto for a list. + // If your environment is not supported, or you settled on a specific environment, switch out the adapter. + // See https://svelte.dev/docs/kit/adapters for more information about adapters. + adapter: adapter(), + alias: { + $wp: 'src/wp-templates', + $components: 'src/components', + }, + }, +}; + +export default config; diff --git a/examples/sveltekit/template-hierarchy/vite.config.ts b/examples/sveltekit/template-hierarchy/vite.config.ts new file mode 100644 index 000000000..bbf8c7da4 --- /dev/null +++ b/examples/sveltekit/template-hierarchy/vite.config.ts @@ -0,0 +1,6 @@ +import { sveltekit } from '@sveltejs/kit/vite'; +import { defineConfig } from 'vite'; + +export default defineConfig({ + plugins: [sveltekit()] +}); diff --git a/packages/sveltekit/index.js b/packages/sveltekit/index.js new file mode 100644 index 000000000..f805b9b70 --- /dev/null +++ b/packages/sveltekit/index.js @@ -0,0 +1,13 @@ +/** + * @file Sveltekit integration for FaustJS template hierarchy + */ + +// Export template utilities +export { uriToTemplate } from './templateHierarchy.js'; + +// Export GraphQL configuration utilities +export { + setGraphQLClient, + getGraphQLClient, + createDefaultGraphQLClient as createDefaultClient, +} from '@faustjs/graphql'; diff --git a/packages/sveltekit/package.json b/packages/sveltekit/package.json new file mode 100644 index 000000000..e443f712d --- /dev/null +++ b/packages/sveltekit/package.json @@ -0,0 +1,35 @@ +{ + "name": "@faustjs/sveltekit", + "version": "4.0.0-alpha.0", + "description": "SvelteKit integration for FaustJS template hierarchy", + "type": "module", + "exports": { + ".": "./index.js" + }, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [ + "sveltekit", + "svelte", + "wordpress", + "template-hierarchy", + "faustjs" + ], + "author": "", + "license": "MIT", + "packageManager": "pnpm@10.14.0", + "engines": { + "node": ">=24" + }, + "dependencies": { + "@faustjs/template-hierarchy": "workspace:*", + "@faustjs/graphql": "workspace:*", + "graphql": "^16.8.1" + }, + "peerDependencies": { + "@sveltejs/kit": "^2.0.0", + "svelte": "^4.0.0 || ^5.0.0", + "graphql": "^16.0.0" + } +} diff --git a/packages/sveltekit/templateHierarchy.js b/packages/sveltekit/templateHierarchy.js index c2b1def9d..e9497fbe9 100644 --- a/packages/sveltekit/templateHierarchy.js +++ b/packages/sveltekit/templateHierarchy.js @@ -5,30 +5,41 @@ import { getTemplate, getPossibleTemplates, -} from '../template-hierarchy/templates.js'; -import { SEED_QUERY } from '../template-hierarchy/seedQuery.js'; -import { client } from './client.js'; + getSeedQuery, +} from '@faustjs/template-hierarchy'; +import { getGraphQLClient } from '@faustjs/graphql'; import { error } from '@sveltejs/kit'; -import '../template-hierarchy/types.js'; // Import central type definitions /** * Resolve a URI to template data using WordPress template hierarchy * @param {import('../template-hierarchy/types.js').UriToTemplateOptions} options - The options object * @returns {Promise} The resolved template data */ -export async function uriToTemplate({ fetch, uri }) { - const { data: seedQueryData, error: errorMessage } = await client.query( - SEED_QUERY, - { uri }, - { fetch }, - ); +export async function uriToTemplate({ fetch, uri, graphqlClient }) { + /** @type {import('../template-hierarchy/types.js').TemplateData} */ + const returnData = { + uri, + seedQuery: undefined, + availableTemplates: undefined, + possibleTemplates: undefined, + template: undefined, + }; + + // Get the GraphQL client - use provided one or get configured one + const client = getGraphQLClient(graphqlClient); + const { data, error: errorMessage } = await getSeedQuery({ + uri, + graphqlClient: client, + }); + + returnData.seedQuery = { data, error: errorMessage }; if (errorMessage) { - console.error('Error fetching seedQuery:', error); + console.error('Error fetching seedQuery:', errorMessage); throw error(500, 'Error fetching seedQuery'); } - if (!seedQueryData?.nodeByUri) { + if (!data?.nodeByUri) { console.error('HTTP/404 - Not Found in WordPress:', uri); throw error(404, 'Not Found'); } @@ -37,36 +48,40 @@ export async function uriToTemplate({ fetch, uri }) { if (!resp.ok) { console.error('Error fetching available templates:', resp.statusText); - throw error(500, 'Error fetching available templates'); } const availableTemplates = await resp.json(); + returnData.availableTemplates = availableTemplates; + if (!availableTemplates || availableTemplates.length === 0) { console.error('No templates found'); - throw error(500, 'No available templates'); } - const possibleTemplates = getPossibleTemplates(seedQueryData.nodeByUri); + const possibleTemplates = getPossibleTemplates(data.nodeByUri); + + returnData.possibleTemplates = possibleTemplates; if (!possibleTemplates || possibleTemplates.length === 0) { console.error('No possible templates found'); throw error(500, 'No possible templates for this URI'); } - const template = getTemplate(availableTemplates, possibleTemplates); + + const templateId = getTemplate( + availableTemplates.map((template) => template.id || template), + possibleTemplates, + ); + + const template = availableTemplates.find((t) => (t.id || t) === templateId); + + returnData.template = template; if (!template) { - console.error('No template not found for route'); + console.error('No template found for route'); throw error(500, 'No template found for this URI'); } - return { - uri, - seedQuery: seedQueryData, - availableTemplates, - possibleTemplates, - template, - }; + return returnData; }