diff --git a/.gitignore b/.gitignore index 5ce5797e..6c634480 100644 --- a/.gitignore +++ b/.gitignore @@ -24,3 +24,6 @@ debug.log .DS_Store .idea .vscode + +## Examples +examples/**/package-lock.json \ No newline at end of file diff --git a/README.md b/README.md index 9c5670e1..411832a9 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,30 @@ -# hwptoolkit +# HWP Toolkit Headless WordPress Toolkit > A modern toolkit for building headless WordPress applications. Provides CLI tools, plugins, and best practices for decoupling WordPress into a powerful headless CMS. + ## Project Organization -- `packages`: Dependency packages to be published on NPM -- `plugins`: PHP based WordPress Plugins -- `docs`: Documentation for the Toolkit? -- `examples`: Example code for HWP \ No newline at end of file +- [`packages`](./packages): Dependency packages to be published on NPM +- [`plugins`](./plugins): PHP based WordPress Plugins +- [`docs`](./docs): Documentation for the Toolkit +- [`examples`](./examples): Example code for HWP + + +## Community + +To chat with other headless WordPress users and the headless community as a whole, you can join the [WP Engine Developers Discord](https://discord.gg/J2khkF9XYK). + +> **Note**: We also have a fortnightly Headless WordPress Community Meeting [here](https://discord.gg/fdWjcTt8?event=1351712352155992075). + +Additionally, if you have questions or ideas, please share them on [GitHub Discussions](https://github.com/wpengine/hwptoolkit/discussions). + +## Contributing + +There are many ways to [contribute](/CONTRIBUTING.md) to this project. + +- [Discuss open issues](https://github.com/wpengine/hwptoolkit/issues) to help define the future of the project. +- [Submit bugs](https://github.com/wpengine/hwptoolkit/issues) and help us verify fixes as they are checked in. +- Review and discuss the [source code changes](https://github.com/wpengine/hwptoolkit/pulls). +- [Contribute bug fixes](/CONTRIBUTING.md) diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 00000000..f3d74282 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,23 @@ +# Security Policy + +WP Engine takes the security of our software and services seriously, including all +of the open-source code repositories managed through our +[WP Engine organization](https://github.com/wpengine). + +## Reporting Security Issues +If you believe you have found a security vulnerability in any WP Engine-owned repository, please report it to us via email at [opensource@wpengine.com](mailto:opensource@wpengine.com?subject=HWPToolkit%20Security%20Vulnerability) + +**Please do not report security vulnerabilities through public GitHub issues, +discussions, or pull requests.** + +Please include as much of the information listed below as you can to help us better understand and resolve the issue: + +- The type of issue (e.g., buffer overflow, SQL injection, or cross-site scripting). +- Full paths of the source file(s) related to the manifestation of the issue. +- The location of the affected source code (tag/branch/commit or direct URL). +- Any special configuration required to reproduce the issue. +- Step-by-step instructions to reproduce the issue. +- Proof-of-concept or exploit code (if possible). +- Impact of the issue, including how an attacker might exploit the issue. + +This information will help us triage your report more quickly. Thank you for helping us keep WP Engine and our users safe! diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 00000000..8b6491f0 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,25 @@ +# HWP Toolkit Documentation + + +## Explanation Documentation + +| Title | Description | +|------------------------------------|------------------------------| +| [GET vs POST](explanation/get-vs-post.md) | Explanation on choosing between GET and POST methods for WPGraphQL requests. | +| [GraphQL Endpoints](explanation/graphql-endpoints.md) | Documentation on differences between using /graphql or ?graphql for WPGraphQL endpoint. | +| [Headless Authentication](explanation/headless-authentication.md) | Overview of user authentication and access control in a decoupled WordPress architecture. | +| [Rendering Options](explanation/rendering-options.md) | A document that explores the various approaches to rendering content from a headless WordPress installation. | +| [Routing](explanation/routing.md) | A comprehensive explanation on routing in a headless WordPress application. | +| [Sitemaps](explanation/sitemaps.md) | A comprehensive explanation on sitemaps in a headless WordPress application. | + + +## How To Documentation + +| Title | Description | +|------------------------------------|------------------------------| +| [Automatic Persisted Queries in Next.js Pages Router](how-to/nextjs-pages-router/enable-apq/index.md) | Learn how to reduce latency and network strain in GraphQL queries using Automatic Persisted Queries (APQ) by hashing and reusing query hashes. | + + +## Contributing + +If you feel like something is missing or you want to add documentation, we encourage you to contribute! Please check out our [Contributing Guide](https://github.com/wpengine/hwptoolkit/blob/main/CONTRIBUTING.md) for more details. diff --git a/docs/explanation/rendering-options.md b/docs/explanation/rendering-options.md new file mode 100644 index 00000000..74d6d939 --- /dev/null +++ b/docs/explanation/rendering-options.md @@ -0,0 +1,139 @@ +# Headless WordPress Rendering Options +## Introduction + +This document explores the various approaches to rendering content from a headless WordPress installation. As a front-end developer working with headless WordPress, you'll need to make important decisions about how to handle and display WordPress content in your frontend application. This guide aims to help you understand the available options, their trade-offs, and best practices. + +### 1. Rendering Raw HTML Content from WordPress Classic Editor +When working with content created in the WordPress Classic Editor, you typically receive raw HTML content that needs to be rendered in your front-end application. Here's an overview of how to handle this content, focusing on React as an example framework. + +#### Overview +* **Raw HTML Content**: Content from the WordPress Classic Editor is often in raw HTML format, which includes tags, classes, and inline styles. + +* **Front-end Rendering**: To display this content in a React application, you need to render the raw HTML safely and efficiently. + +#### Implementation +To render raw HTML in React, you can use the `dangerouslySetInnerHTML` property. Here's a basic example for a single post: + +```javascript +const query = gql`query GetPost { + post(id: "hello-world", idType: SLUG) { + content + } +}`; + +function PostContent({ content }) { + return
; +} +``` +**Note**: Using `dangerouslySetInnerHTML` poses security risks if the content isn't sanitized. + +#### Considerations: +* Simplicity: Easy to implement initially with dangerouslySetInnerHTML (React) or v-html (Vue). +* Styling Challenges: Global CSS styles needed to match WordPress styling which is usually not available +* Limited Control: Limited ability to manipulate or enhance specific content elements. + +Considering those limitations, it's clear that rendering raw HTML Content is not suitable for most use cases. Hopefully there are better alternatives. + +### 2. Rendering Block Editor (Gutenberg) Content +#### Overview +WordPress Block Editor (Gutenberg) stores content as HTML with special comments and structured JSON properties in the comments representing blocks, offering more flexibility for headless implementations. However exposing them in a Headless enviroment is tricky. That's why with the help of the [wp-graphql-content-blocks](https://github.com/wpengine/wp-graphql-content-blocks) plugin, you can query (Gutenberg) Blocks as data using graphql. + +This gives you more controls and ability to manipulate or enhance specific content elements or blocks compared to the previous approach. Here is how to do it: + +1. Querying the block data + +```javascript +const query = gql`query GetPostBlocks { + posts { + nodes { + editorBlocks { + __typename + name + ... on CoreParagraph { + attributes { + content + } + } + } + } + } +}`; +``` +The response would include the block's `__typename`, `name`, and the `content` attribute: + +```json +{ + "__typename": "CoreParagraph", + "name": "core/paragraph", + "attributes": { + "content": "Hello World" + } +} +``` +This query fetches the content of a `CoreParagraph` block, allowing you to access and manipulate its text content in your application. + +To render this content in a React application, you can use the `dangerouslySetInnerHTML` property, but ensure you sanitize the content first to prevent XSS attacks: + +```javascript +import DOMPurify from 'dompurify'; + +function PostContent({ attributes }) { + const { content } = attributes; + const sanitizedContent = DOMPurify.sanitize(content); + return
; +} +``` +Similar to the Classic Editor approach, you can render the final HTML output of Gutenberg blocks. + +#### Considerations: +* Querying: The ability to query block data using GraphQL provides a structured approach to accessing and manipulating content. +* Complexity: Handling nested blocks and reconstructing their hierarchy can be complex. +* Styling: While able to query classNames or block attributes, developers still have to provide the actual styles for the components to display correctly. + +### 3. Rendering Blocks with WordPress Styles + +Rendering Gutenberg blocks with WordPress styles in a headless environment involves fetching and applying the styles defined in WordPress to your frontend application. This approach ensures that the content looks consistent with how it appears in the WordPress editor. + +#### Challenges +* CSS Sources: Gutenberg block styles come from multiple sources, including core blocks, themes, and user-defined styles. This complexity makes it challenging to replicate the exact styling in a headless setup. + +* Inline Styles and CSS Variables: Gutenberg blocks often include inline styles and CSS variables (e.g., `var(--wp--preset--color--cyan-bluish-gray))`. These styles are not automatically applied when rendering blocks in a headless environment. + +* Theme Styles: Themes provide additional styles that are crucial for maintaining consistency. However, fetching these styles dynamically can be difficult, especially if they are generated inline or through WordPress's Global Styles feature. + +#### Solutions +* Import Global Stylesheet: Use tools like faust-cli to generate and import a global stylesheet from your WordPress site. This stylesheet includes CSS variables and other theme-specific styles. + +```bash +"scripts": { + "generate": "faust generatePossibleTypes && faust generateGlobalStylesheet", +} +``` +Then import the generated stylesheet in your application: + +```javascript +import "../globalStylesheet.css"; +``` + +* Include Block Library Styles: Import CSS styles from `@wordpress/block-library` to apply basic block styling: + +```javascript +import "@wordpress/block-library/build-style/common.css"; +import "@wordpress/block-library/build-style/style.css"; +import "@wordpress/block-library/build-style/theme.css"; +``` + +**Note**: Changes to the `@wordpress/block-library` package may introduce new styles or css classnames potentially changing the look and feel of the application. + +* Define Custom CSS Variables: If using CSS variables, define them manually in your application's CSS to match WordPress's presets: + +```css +:root { + --wp--preset--color--black: #000000; + --wp--preset--color--cyan-bluish-gray: #abb8c3; +} +``` +Considerations +* Styling Parity: Achieving perfect styling parity can be challenging due to the dynamic nature of WordPress styles. + +* Maintenance: Styles may change with theme updates, customizations or major WordPress updates, requiring periodic updates in your headless application. diff --git a/docs/explanation/sitemaps-1.png b/docs/explanation/sitemaps-1.png new file mode 100644 index 00000000..e4967d41 Binary files /dev/null and b/docs/explanation/sitemaps-1.png differ diff --git a/docs/explanation/sitemaps.md b/docs/explanation/sitemaps.md new file mode 100644 index 00000000..c87ce994 --- /dev/null +++ b/docs/explanation/sitemaps.md @@ -0,0 +1,258 @@ +# Sitemaps in WordPress: A Comprehensive Overview + +## What is a Sitemap? +A sitemap is an XML file that provides a structured list of pages on a website by helping search engines discover and crawl content more efficiently. It acts as a roadmap of your website's structure, containing important metadata about each page. +Since WordPress 5.5, there's a built-in XML sitemap generator that: + +* Automatically creates sitemaps for posts, pages, and custom post types +* Dynamically updates as content is added, modified, or deleted +* Provides basic SEO and indexing support out of the box + +However, this default sitemap lacks customization options, which is why many users opt for plugins like Yoast SEO or Jetpack to generate more comprehensive sitemaps. + +You can view the current sitemap generated by WordPress by visiting yourdomain.com/sitemap.xml in your browser. +![Image showing sitemap for a specific website](./sitemaps-1.png) + +## Sitemaps in Headless WordPress +Headless WordPress environments introduce unique challenges for sitemap generation: + +* **Separation of content management and frontend rendering**: In traditional WordPress setups, content management and frontend rendering are integrated. However, in headless environments, WordPress acts as a backend CMS, while the frontend is handled by frameworks like Next.js. This separation requires custom solutions for sitemap generation. + +* **Dynamic routes**: Dynamic routes in frameworks like Next.js may not be directly accessible or easily discoverable by search engines. + +* **Need for custom sitemap generation and management**: Due to the decoupling of backend and frontend, traditional WordPress sitemap plugins might not work seamlessly. Therefore, custom approaches are needed to generate and manage sitemaps effectively. + +To address those, there are some proposed solutions for headless sitemap implementation: + +1. **Proxying Sitemaps from Backend to Frontend** + +Approach: This approach maintains WordPress's native sitemap generation capabilities while ensuring proper frontend URLs are used. It involves creating API routes in your Next.js application that proxy requests to the WordPress backend sitemap. + +Example Code: + +```javascript +// /pages/api/proxy-sitemap/[...slug].js +const wordpressUrl = ( + process.env.NEXT_PUBLIC_WORDPRESS_URL || "http://localhost:8888" +).trim(); + +export default async function handler(req, res) { + const slug = req.query.slug || []; + + // Reconstruct the original WordPress sitemap path + let wpUrl; + if (slug.length === 0 || slug[0] === "sitemap.xml") { + wpUrl = `${wordpressUrl}/sitemap.xml`; + } else { + const wpPath = slug.join("/"); + wpUrl = `${wordpressUrl}/${wpPath}.xml`; + } + + console.debug("Fetching sitemap", wpUrl); + try { + const wpRes = await fetch(wpUrl); + console.debug("Fetching sitemap", wpRes); + if (!wpRes.ok) { + return res.status(wpRes.status).send("Error fetching original sitemap"); + } + + const contentType = wpRes.headers.get("content-type") || "application/xml"; + let body = await wpRes.text(); + // Remove XML stylesheets if present + body = body.replace(/<\?xml-stylesheet[^>]*\?>\s*/g, ""); + + res.setHeader("Content-Type", contentType); + res.status(200).send(body); + } catch (err) { + res.status(500).send("Internal Proxy Error"); + } +} +``` +Then add the necessary rewrites in your `next.config.js`: +```javascript +module.exports = { + async rewrites() { + return [ + { + source: "/:path(wp-sitemap-.*).xml", + destination: "/api/proxy-sitemap/:path", + }, + { + source: "/sitemap.xml", + destination: "/api/proxy-sitemap/sitemap.xml", + }, + ]; + }, + // other Next.js configuration +}; +``` +To ensure that the sitemap URLs in your headless WordPress setup correctly point to your frontend application, it's essential to configure the WordPress Site Address (URL) setting to match your frontend's URL. This is done in the WordPress settings page. + +**Note**: The WordPress Address (URL) should remain set to the URL where your WordPress backend is hosted. Only the Site Address (URL) needs to be updated to reflect your frontend's URL. + +This implementation ensures that when visitors access `/sitemap.xml` on your headless frontend, they'll see the WordPress sitemap content. + +This route will serve the WordPress `sitemap.xml` in your Next.js application dynamically. + +- **Pros** + * Leverages WordPress's built-in sitemap generation capabilities + * Works seamlessly with SEO plugins like Yoast SEO or All in One SEO + * Simple implementation that requires minimal code + * Automatically updates when content changes in WordPress + +- **Cons** + * Limited flexibility for custom frontend routes not defined in WordPress + * Requires proper URL transformation to replace backend URLs with frontend URLs + * May require additional handling for caching and performance + * May propagate any errors experienced in WordPress when proxying the `sitemap.xml` + +2. **Generating a Sitemap from GraphQL Content** + +Approach: This approach involves fetching all available post and page info via GraphQL and generating a custom sitemap. This can be implemented using either server-side rendering (SSR) or static generation strategies. However since, WPGraphQL returns maximum 100 node per page trying to fetch all available post or pages on a large site might be problematic and slow. See [pagination limits in wp-graphql](https://www.wpgraphql.com/docs/known-limitations#pagination-limits). + +Example implementation using Next.js and WPGraphQL: + +```javascript +import { gql } from '@apollo/client'; +import { client } from '../lib/apolloClient'; + +// Function to fetch all posts from WordPress +async function fetchAllPosts() {} + +// Similar function for pages +async function fetchAllPages() {} + +export async function generateSitemap() { + const [posts, pages] = await Promise.all([ + fetchAllPosts(), + fetchAllPages(), + ]); + const allContent = [ + ...data.posts.nodes.map(post => ({ + slug: `posts/${post.slug}`, + modified: post.modified + })), + ...data.pages.nodes.map(page => ({ + slug: page.slug, + modified: page.modified + })), + // Add custom frontend routes here + { slug: '', modified: new Date().toISOString() }, // Homepage + { slug: 'about-us', modified: new Date().toISOString() }, + ]; + const sitemap = ` + + ${allContent.map(item => ` + + ${process.env.FRONTEND_URL}/${item.slug} + ${item.modified} + + `).join('')} + + `; + return sitemap; +} +export async function getServerSideProps({ res }) { + const sitemap = await generateSitemap(); + + res.setHeader('Content-Type', 'application/xml'); + res.write(sitemap); + res.end(); + + return { props: {} }; +} + +export async function GET() { + const sitemap = await generateSitemap(); + + return new Response(sitemap, { + headers: { + 'Content-Type': 'application/xml', + }, + }); +} +``` +- **Pros** + * Complete control over sitemap structure and content + * Ability to include custom frontend routes not defined in WordPress + * Easy integration with Next.js data fetching methods + +- **Cons**: + * More complex implementation than proxying + * Requires manual updates to include new content types or custom routes + * May require pagination handling for large sites + * Doesn't leverage WordPress SEO plugin sitemap enhancements + * Increasing the GraphQL limits may cause performance issues on resource-constrained WordPress instances. + +3. **Hybrid Approach: Fetching, Parsing, and Enhancing Existing Sitemaps** + +Approach: This approach (currently used by Faust) fetches the existing WordPress sitemaps, parses them, and enhances them with additional frontend routes. This provides the benefits of WordPress's sitemap generation while allowing for customization. + +```javascript +import { DOMParser } from 'xmldom'; + +export async function GET() { + const response = await fetch(`${process.env.WORDPRESS_URL}/wp-sitemap.xml`); + const sitemapIndex = await response.text(); + + const parser = new DOMParser(); + const xmlDoc = parser.parseFromString(sitemapIndex, 'text/xml'); + const sitemapUrls = Array.from(xmlDoc.getElementsByTagName('loc')).map( + node => node.textContent + ); + + const processedSitemaps = await Promise.all( + sitemapUrls.map(async (url) => { + const sitemapResponse = await fetch(url); + const sitemapContent = await sitemapResponse.text(); + + return sitemapContent.replace( + new RegExp(process.env.WORDPRESS_URL, 'g'), + process.env.FRONTEND_URL + ); + }) + ); + + const frontendRoutesSitemap = ` + + + + ${process.env.FRONTEND_URL}/custom-route + ${new Date().toISOString()} + + + + `; + + const combinedSitemap = ` + + + ${processedSitemaps.map((_, index) => ` + + ${process.env.FRONTEND_URL}/sitemaps/wp-sitemap-${index}.xml + + `).join('')} + + ${process.env.FRONTEND_URL}/sitemaps/frontend-routes.xml + + + `; + + return new Response(combinedSitemap, { + headers: { + 'Content-Type': 'application/xml', + }, + }); +} +``` + +- **Pros** + * Combines the best of both approaches + * Leverages WordPress SEO plugin enhancements + * Allows for custom frontend routes + * Provides flexibility for complex sitemap requirements + +- **Cons** + * Most complex implementation of the three approaches. + * Requires handling multiple sitemap files + * May have performance implications if not properly cached diff --git a/examples/README.md b/examples/README.md index 61ace089..e6333f65 100644 --- a/examples/README.md +++ b/examples/README.md @@ -1,7 +1,145 @@ -# HWP Examples - +# HWP Toolkit Examples This directory contains examples demonstrating how to use various features of the Headless WordPress Toolkit. -- **next-pages-apollo-authentication**: An example project showcasing authentication with Next.js, Apollo Client, and Headless WordPress. -- **next/client-app-router-fetch-data**: An example WordPress application using Next.js App Router and the fetch API to fetch data from WordPress using WPGraphQL -- **next/client-multisite-app-router-fetch-data**: An example WordPress multisite application using Next.js App Router and the fetch API to fetch data from WordPress using WPGraphQL +> **Feature Highlight:** Most examples include a `wp-env` setup, allowing you to fully configure your headless application with a single command. They also contain screenshots of the application. + +## Astro Example + +| Title | Description | +|-------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------| +| [template-hierarchy-data-fetching-urql](astro/template-hierarchy-data-fetching-urql) | Demonstrates template hierarchy and data fetching with Astro, URQL, and Headless WordPress. | + +## Next.js Examples + +| Title | Description | +|-------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------| +| [apollo-authentication](next/apollo-authentication) | Showcases authentication with Next.js, Apollo Client, and Headless WordPress. | +| [apollo-client-data-fetch](next/apollo-client-data-fetch) | Explores data fetching strategies and state management using Next.js, Apollo Client, and Headless WordPress. | +| [apollo-client-filesystem-routing](next/apollo-client-filesystem-routing) | Integrates WPGraphQL and WPGraphQL for ACF with Next.js for a headless WordPress site. | +| [client-app-router-fetch-data](next/client-app-router-fetch-data) | Uses Next.js App Router and fetch API to retrieve data from WordPress via WPGraphQL. | +| [client-multisite-app-router-fetch-data](next/client-multisite-app-router-fetch-data) | Implements a multisite headless WordPress app with Next.js App Router and fetch API. | +| [custom-sitemap-apollo](next/custom-sitemap-apollo) | Generates a custom sitemap using Next.js, Apollo Client, and WPGraphQL with an extended plugin.| +| [custom-sitemap-vanilla-wpgraphql](next/custom-sitemap-vanilla-wpgraphql) | Creates a custom sitemap using Next.js and WPGraphQL without extending its endpoints. | +| [hybrid-sitemap-apollo](next/hybrid-sitemap-apollo) | Fetches and transforms WordPress sitemaps for clean URL formatting with Next.js. | +| [proxied-sitemap-apollo](next/proxied-sitemap-apollo) | Provides a proxied sitemap by transforming WordPress XML sitemaps for SEO-friendly frontend URLs.| +| [render-blocks-pages-router](next/render-blocks-pages-router) | Renders WordPress Blocks with JSX in Next.js, including utilities for hierarchical block data. | +| [wp-theme-rendered-blocks](next/wp-theme-rendered-blocks) | Demonstrates how to fetch and apply WordPress Global Styles in a Next.js project using the `globalStylesheet` GraphQL field. | + +## Contributing + +If you feel like something is missing or you want to add an example for another framework, we encourage you to contribute! Please check out our [Contributing Guide](https://github.com/wpengine/hwptoolkit/blob/main/CONTRIBUTING.md) for more details. + +## Screenshots + +> **Feature Highlight:** Here are some sample screenshots of the examples listed above. + +### Apollo Authentication + +
+ View Screenshots + +![Enable Credentials Authentication](next/apollo-authentication/screenshots/enable-credentials-auth.png) + +![Logged In View](next/apollo-authentication/screenshots/logged.png) + +![Login Screen](next/apollo-authentication/screenshots/login.png) + +
+ +### Apollo Client Data Fetch + +
+ View Screenshots + +![Homepage View](next/apollo-client-data-fetch/screenshots/home.png) +![Live Search Feature](next/apollo-client-data-fetch/screenshots/live-search.png) +![Load More Button](next/apollo-client-data-fetch/screenshots/load-more.png) +![New Comment Form](next/apollo-client-data-fetch/screenshots/new-comment.png) +![Post with Comments](next/apollo-client-data-fetch/screenshots/post-with-comments.png) +![Static Page Example](next/apollo-client-data-fetch/screenshots/static-page.png) + +
+ +### Apollo Client Filesystem Routing + +
+ View Screenshots + +![Categories View](next/apollo-client-filesystem-routing/screenshots/categories.png) +![Category Page](next/apollo-client-filesystem-routing/screenshots/category.png) +![Homepage View](next/apollo-client-filesystem-routing/screenshots/home.png) +![Posts Overview](next/apollo-client-filesystem-routing/screenshots/posts.png) +![Single CPT Example](next/apollo-client-filesystem-routing/screenshots/single-cpt.png) +![Single Post View](next/apollo-client-filesystem-routing/screenshots/single-post.png) + +
+ +### Client App Router Fetch Data + +
+ View Screenshots + +![Blog Comment Form Submitted](next/client-app-router-fetch-data/screenshots/blog-comment-form-submitted.png) +![Blog Comment Form](next/client-app-router-fetch-data/screenshots/blog-comment-form.png) +![Blog Comments](next/client-app-router-fetch-data/screenshots/blog-comments.png) +![Blog Listing Pagination](next/client-app-router-fetch-data/screenshots/blog-listing-pagination.png) +![Blog Listing](next/client-app-router-fetch-data/screenshots/blog-listing.png) +![Blog Single](next/client-app-router-fetch-data/screenshots/blog-single.png) +![CPT Event Listing](next/client-app-router-fetch-data/screenshots/cpt-event-listing.png) +![CPT Event Single](next/client-app-router-fetch-data/screenshots/cpt-event-single.png) + +
+ +### Client Multisite App Router Fetch Data + +
+ View Screenshots + +![Blog Listing Pagination](next/client-multisite-app-router-fetch-data/screenshots/Blog_listing_pagination.png) +![Blog Listing](next/client-multisite-app-router-fetch-data/screenshots/Blog_listing.png) +![Catch All Second Site](next/client-multisite-app-router-fetch-data/screenshots/Catch_all_second_site.png) +![Catch All](next/client-multisite-app-router-fetch-data/screenshots/Catch_all.png) +![Comment Form](next/client-multisite-app-router-fetch-data/screenshots/Comment_form.png) +![Comments](next/client-multisite-app-router-fetch-data/screenshots/Comments.png) +![CPT Single](next/client-multisite-app-router-fetch-data/screenshots/Cpt_single.png) +![CPT](next/client-multisite-app-router-fetch-data/screenshots/cpt.png) +![Home](next/client-multisite-app-router-fetch-data/screenshots/Home.png) +![Single Blog](next/client-multisite-app-router-fetch-data/screenshots/Single_blog.png) + +
+ +### Custom Sitemap Apollo + +
+ View Screenshots + +![Sitemap Category](next/custom-sitemap-apollo/screenshots/sitemap-category.png) +![Sitemap CPT](next/custom-sitemap-apollo/screenshots/sitemap-cpt.png) +![Sitemap CTT](next/custom-sitemap-apollo/screenshots/sitemap-ctt.png) +![Sitemap Index](next/custom-sitemap-apollo/screenshots/sitemap-index.png) +![Sitemap Page](next/custom-sitemap-apollo/screenshots/sitemap-page.png) +![Sitemap Post](next/custom-sitemap-apollo/screenshots/sitemap-post.png) +![Sitemap Tag](next/custom-sitemap-apollo/screenshots/sitemap-tag.png) +![Sitemap User](next/custom-sitemap-apollo/screenshots/sitemap-user.png) + +
+ + +### Custom Sitemap Vanilla WPGraphQL + +
+ View Screenshots + +![Sitemap Category](next/custom-sitemap-vanilla-wpgraphql/screenshots/sitemap-category.png) + +![Sitemap CPT](next/custom-sitemap-vanilla-wpgraphql/screenshots/sitemap-cpt.png) + +![Sitemap Index](next/custom-sitemap-vanilla-wpgraphql/screenshots/sitemap-index.png) + +![Sitemap Page](next/custom-sitemap-vanilla-wpgraphql/screenshots/sitemap-page.png) + +![Sitemap Post](next/custom-sitemap-vanilla-wpgraphql/screenshots/sitemap-post.png) + +![Sitemap Tag](next/custom-sitemap-vanilla-wpgraphql/screenshots/sitemap-tag.png) + +
diff --git a/examples/astro/template-hierarchy-data-fetching-urql/.wp-env.json b/examples/astro/template-hierarchy-data-fetching-urql/.wp-env.json new file mode 100644 index 00000000..224937d4 --- /dev/null +++ b/examples/astro/template-hierarchy-data-fetching-urql/.wp-env.json @@ -0,0 +1,23 @@ +{ + "phpVersion": "8.3", + "plugins": [ + "https://github.com/wp-graphql/wp-graphql/releases/latest/download/wp-graphql.zip" + ], + "themes": ["https://downloads.wordpress.org/theme/nude.1.2.zip"], + "config": { + "WP_DEBUG": true, + "SCRIPT_DEBUG": false, + "GRAPHQL_DEBUG": true, + "WP_DEBUG_LOG": true, + "WP_DEBUG_DISPLAY": false, + "SAVEQUERIES": false + }, + "mappings": { + "db": "./wp-env/db", + "wp-content/uploads": "./wp-env/uploads", + ".htaccess": "./wp-env/setup/.htaccess" + }, + "lifecycleScripts": { + "afterStart": "wp-env run cli -- wp theme activate nude && wp-env run cli -- wp theme delete --all && wp-env run cli -- wp rewrite structure '/%postname%/' && wp-env run cli -- wp rewrite flush" + } +} diff --git a/examples/astro/template-hierarchy-data-fetching-urql/README.md b/examples/astro/template-hierarchy-data-fetching-urql/README.md new file mode 100644 index 00000000..ad71d533 --- /dev/null +++ b/examples/astro/template-hierarchy-data-fetching-urql/README.md @@ -0,0 +1,18 @@ +# Astro Template HIerarchy and Data fetching w/URQL Example + +In this example we show how to implement the WP Template Hierarchy in Astro for use with a Headless WordPress backend using WPGraphQL. We use URQL for all routing and fetching page content. + +## Getting Started + +> [!IMPORTANT] +> Docker Desktop needs to be installed to run WordPress locally. + +1. Run `npm run example:setup` to install dependencies and configure the local WP server. +2. Run `npm run example:start` to start the WP server and Astro development server. + +> [!NOTE] +> When you kill the long running process this will not shutdown the local WP instance, only Astro. You must run `npm run example:stop` to kill the local WP server. + +## Trouble Shooting + +To reset the WP server and re-run setup you can run `npm run example:prune` and confirm "Yes" at any prompts. diff --git a/examples/astro/template-hierarchy-data-fetching-urql/example-app/.gitignore b/examples/astro/template-hierarchy-data-fetching-urql/example-app/.gitignore new file mode 100644 index 00000000..016b59ea --- /dev/null +++ b/examples/astro/template-hierarchy-data-fetching-urql/example-app/.gitignore @@ -0,0 +1,24 @@ +# build output +dist/ + +# generated types +.astro/ + +# dependencies +node_modules/ + +# logs +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* + +# environment variables +.env +.env.production + +# macOS-specific files +.DS_Store + +# jetbrains setting folder +.idea/ diff --git a/examples/astro/template-hierarchy-data-fetching-urql/example-app/README.md b/examples/astro/template-hierarchy-data-fetching-urql/example-app/README.md new file mode 100644 index 00000000..ff19a3e7 --- /dev/null +++ b/examples/astro/template-hierarchy-data-fetching-urql/example-app/README.md @@ -0,0 +1,48 @@ +# Astro Starter Kit: Basics + +```sh +npm create astro@latest -- --template basics +``` + +[![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/withastro/astro/tree/latest/examples/basics) +[![Open with CodeSandbox](https://assets.codesandbox.io/github/button-edit-lime.svg)](https://codesandbox.io/p/sandbox/github/withastro/astro/tree/latest/examples/basics) +[![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/withastro/astro?devcontainer_path=.devcontainer/basics/devcontainer.json) + +> 🧑‍🚀 **Seasoned astronaut?** Delete this file. Have fun! + +![just-the-basics](https://github.com/withastro/astro/assets/2244813/a0a5533c-a856-4198-8470-2d67b1d7c554) + +## 🚀 Project Structure + +Inside of your Astro project, you'll see the following folders and files: + +```text +/ +├── public/ +│ └── favicon.svg +├── src/ +│ ├── layouts/ +│ │ └── Layout.astro +│ └── pages/ +│ └── index.astro +└── package.json +``` + +To learn more about the folder structure of an Astro project, refer to [our guide on project structure](https://docs.astro.build/en/basics/project-structure/). + +## 🧞 Commands + +All commands are run from the root of the project, from a terminal: + +| Command | Action | +| :------------------------ | :----------------------------------------------- | +| `npm install` | Installs dependencies | +| `npm run dev` | Starts local dev server at `localhost:4321` | +| `npm run build` | Build your production site to `./dist/` | +| `npm run preview` | Preview your build locally, before deploying | +| `npm run astro ...` | Run CLI commands like `astro add`, `astro check` | +| `npm run astro -- --help` | Get help using the Astro CLI | + +## 👀 Want to learn more? + +Feel free to check [our documentation](https://docs.astro.build) or jump into our [Discord server](https://astro.build/chat). diff --git a/examples/astro/template-hierarchy-data-fetching-urql/example-app/astro.config.mjs b/examples/astro/template-hierarchy-data-fetching-urql/example-app/astro.config.mjs new file mode 100644 index 00000000..17f6a62b --- /dev/null +++ b/examples/astro/template-hierarchy-data-fetching-urql/example-app/astro.config.mjs @@ -0,0 +1,5 @@ +// @ts-check +import { defineConfig } from "astro/config"; + +// https://astro.build/config +export default defineConfig({}); diff --git a/examples/astro/template-hierarchy-data-fetching-urql/example-app/package.json b/examples/astro/template-hierarchy-data-fetching-urql/example-app/package.json new file mode 100644 index 00000000..bc0469c1 --- /dev/null +++ b/examples/astro/template-hierarchy-data-fetching-urql/example-app/package.json @@ -0,0 +1,16 @@ +{ + "name": "template-hierarchy-data-fetching-urql-app", + "description": "A template for Astro with WordPress and URQL", + "type": "module", + "version": "0.0.1", + "scripts": { + "dev": "astro dev", + "build": "astro build", + "preview": "astro preview", + "astro": "astro" + }, + "dependencies": { + "@urql/core": "^5.1.1", + "astro": "^5.6.1" + } +} diff --git a/examples/astro/template-hierarchy-data-fetching-urql/example-app/public/favicon.svg b/examples/astro/template-hierarchy-data-fetching-urql/example-app/public/favicon.svg new file mode 100644 index 00000000..f157bd1c --- /dev/null +++ b/examples/astro/template-hierarchy-data-fetching-urql/example-app/public/favicon.svg @@ -0,0 +1,9 @@ + + + + diff --git a/examples/astro/template-hierarchy-data-fetching-urql/example-app/src/assets/astro.svg b/examples/astro/template-hierarchy-data-fetching-urql/example-app/src/assets/astro.svg new file mode 100644 index 00000000..8cf8fb0c --- /dev/null +++ b/examples/astro/template-hierarchy-data-fetching-urql/example-app/src/assets/astro.svg @@ -0,0 +1 @@ + diff --git a/examples/astro/template-hierarchy-data-fetching-urql/example-app/src/assets/background.svg b/examples/astro/template-hierarchy-data-fetching-urql/example-app/src/assets/background.svg new file mode 100644 index 00000000..4b2be0ac --- /dev/null +++ b/examples/astro/template-hierarchy-data-fetching-urql/example-app/src/assets/background.svg @@ -0,0 +1 @@ + diff --git a/examples/astro/template-hierarchy-data-fetching-urql/example-app/src/components/Nav.astro b/examples/astro/template-hierarchy-data-fetching-urql/example-app/src/components/Nav.astro new file mode 100644 index 00000000..d35cab7d --- /dev/null +++ b/examples/astro/template-hierarchy-data-fetching-urql/example-app/src/components/Nav.astro @@ -0,0 +1,50 @@ +--- +/** + * We've hard coded this but you can use the client to fetch this data from WP Menus! + * import { client, gql } from "../lib/client"; + */ +--- + + + diff --git a/examples/astro/template-hierarchy-data-fetching-urql/example-app/src/components/TemplateHierarchyInfo.astro b/examples/astro/template-hierarchy-data-fetching-urql/example-app/src/components/TemplateHierarchyInfo.astro new file mode 100644 index 00000000..8bba9a75 --- /dev/null +++ b/examples/astro/template-hierarchy-data-fetching-urql/example-app/src/components/TemplateHierarchyInfo.astro @@ -0,0 +1,48 @@ +--- +const template = Astro.locals.templateData; + +if (!template) { + return null; +} +--- + + diff --git a/examples/astro/template-hierarchy-data-fetching-urql/example-app/src/content.config.ts b/examples/astro/template-hierarchy-data-fetching-urql/example-app/src/content.config.ts new file mode 100644 index 00000000..fc82f890 --- /dev/null +++ b/examples/astro/template-hierarchy-data-fetching-urql/example-app/src/content.config.ts @@ -0,0 +1,32 @@ +import { defineCollection, z } from "astro:content"; +import { readdir } from "node:fs/promises"; +import { join } from "node:path"; + +const TEMPLATE_PATH = "wp-templates"; + +const templates = defineCollection({ + loader: async () => { + const files = await readdir(join("src", "pages", TEMPLATE_PATH)); + return files.map((file) => { + const slug = file.replace(".astro", ""); + + if (slug === "index") { + return { + id: slug, + path: join(TEMPLATE_PATH, "/"), + }; + } + + return { + id: slug, + path: join(TEMPLATE_PATH, slug), + }; + }); + }, + schema: z.object({ + id: z.string(), + path: z.string(), + }), +}); + +export const collections = { templates }; diff --git a/examples/astro/template-hierarchy-data-fetching-urql/example-app/src/env.d.ts b/examples/astro/template-hierarchy-data-fetching-urql/example-app/src/env.d.ts new file mode 100644 index 00000000..92aa9835 --- /dev/null +++ b/examples/astro/template-hierarchy-data-fetching-urql/example-app/src/env.d.ts @@ -0,0 +1,6 @@ +declare namespace App { + interface Locals { + uri?: string; + templateData?: import("./lib/templateHierarchy").TemplateData; + } +} diff --git a/examples/astro/template-hierarchy-data-fetching-urql/example-app/src/layouts/Layout.astro b/examples/astro/template-hierarchy-data-fetching-urql/example-app/src/layouts/Layout.astro new file mode 100644 index 00000000..7e3e8b01 --- /dev/null +++ b/examples/astro/template-hierarchy-data-fetching-urql/example-app/src/layouts/Layout.astro @@ -0,0 +1,36 @@ +--- +import Nav from "../components/Nav.astro"; +import TemplateHierarchyInfo from "../components/TemplateHierarchyInfo.astro"; +--- + + + + + + + + + Astro Basics + + + +
\n\n\n \n
\n\n
\n\n\n\n\n\n\n\n\n
\n\n\n\n\n \n
\n\n\n \n\n\n\n\n\n\n \n
\n
\n \n \n\n\n\n\n\n \n \n\n \n\n\n\n\n\n\n \n
\n\n
\n\n
\n \n
\n \n \n\n \n \n \n wpengine\n \n /\n \n hwptoolkit\n \n\n Public\n
\n\n\n
\n\n
\n \n\n
\n
\n\n
\n
\n

\n Headless WordPress Toolkit\n

\n\n \n\n \n\n
\n \n
\n \n \nNotifications\n You must be signed in to change notification settings\n\n
\n \n \n\n \n
\n
\n\n
\n\n\n