17 Commits

Author SHA1 Message Date
6a5d60e34c chore: Update theme translations 2026-01-28 21:55:55 -05:00
80e6555c88 chore: update seo-by-rank-math 2026-01-28 21:53:48 -05:00
ff866e2078 chore: update nuxt-graphql 0.6.7 2026-01-28 21:51:45 -05:00
5e39b53a44 feat: enhance refreshAuthToken to prevent duplicate requests 2026-01-28 21:50:58 -05:00
2d93d44a93 chore(release): v0.1.8
Some checks failed
Deployment / wordpress (push) Failing after 2s
Deployment / nuxt (push) Failing after 9s
2026-01-28 21:14:24 -05:00
ec64a42c2e chore: update pnpm-lock.yaml
Some checks failed
Deployment / wordpress (push) Failing after 2s
Deployment / nuxt (push) Has been cancelled
2026-01-28 21:14:12 -05:00
5f9c29c39a feat: Configure nuxt-svgo module 2026-01-28 21:13:49 -05:00
27f4f73148 fix: update nuxt-graphql and extractNodes typing (maybe => undefined instead of null) 2026-01-28 21:13:31 -05:00
21a7036ef5 feat: all of wrangler config in nuxt.config.ts 2026-01-28 21:12:43 -05:00
b1b1aa47c9 feat: graphql cache keyPrefix from package.json version 2026-01-28 21:11:48 -05:00
54fea5f64a minor: placeholder include for rankmath logic 2026-01-28 21:10:43 -05:00
a27e6af5db fix: term_order and default WPGraphQL settings 2026-01-28 21:10:26 -05:00
2c86905c91 fix: use public wpUrl runtime config for auth refresh token mutation 2026-01-28 21:09:37 -05:00
065b729a2f minor: better wp remote executor hooks 2026-01-28 21:08:58 -05:00
c82bf47e98 feat: useResponsive with device detection on SSR 2026-01-28 21:08:27 -05:00
51f594baa5 fix: headless_home_url needs to be in mu-plugins 2026-01-28 21:07:51 -05:00
3e56ba7eb3 minor: update schema.graphql 2026-01-28 08:34:44 -05:00
21 changed files with 585 additions and 310 deletions

6
composer.lock generated
View File

@@ -420,15 +420,15 @@
},
{
"name": "wpackagist-plugin/seo-by-rank-math",
"version": "1.0.262",
"version": "1.0.263",
"source": {
"type": "svn",
"url": "https://plugins.svn.wordpress.org/seo-by-rank-math/",
"reference": "tags/1.0.262"
"reference": "tags/1.0.263"
},
"dist": {
"type": "zip",
"url": "https://downloads.wordpress.org/plugin/seo-by-rank-math.1.0.262.zip"
"url": "https://downloads.wordpress.org/plugin/seo-by-rank-math.1.0.263.zip"
},
"require": {
"composer/installers": "^1.0 || ^2.0"

View File

@@ -0,0 +1,20 @@
<?php
// Customize home URL for headless WordPress
add_filter( 'home_url', 'headless_home_url', 10, 4 );
function headless_home_url( $url, $path, $orig_scheme, $blog_id ) {
// Exclude specific patterns from rewriting
$excluded_patterns = array(
'#/wp-json(/|$)#i', // WP REST API
'#\.(xsl|xml)$#i', // Sitemap and XSLT files
);
foreach ( $excluded_patterns as $pattern ) {
if ( preg_match( $pattern, $url ) ) {
return get_site_url( $blog_id, $path, $orig_scheme );
}
}
// Rewrite URL protocol to match original home scheme
$scheme = wp_parse_url( get_option( 'home' ) )['scheme'] ?? 'https';
return preg_replace( '#^https:#i', "$scheme:", $url );
}

View File

@@ -1,5 +1,23 @@
# Changelog
## v0.1.8
[compare changes](https://gitea.websimple.com/templates/wp-headless/compare/v0.1.7...v0.1.8)
### 🚀 Enhancements
- UseResponsive with device detection on SSR (c82bf47)
- Graphql cache keyPrefix from package.json version (b1b1aa4)
- All of wrangler config in nuxt.config.ts (21a7036)
- Configure nuxt-svgo module (5f9c29c)
### 🩹 Fixes
- Headless_home_url needs to be in mu-plugins (51f594b)
- Use public wpUrl runtime config for auth refresh token mutation (2c86905)
- Term_order and default WPGraphQL settings (a27e6af)
- Update nuxt-graphql and extractNodes typing (maybe => undefined instead of null) (27f4f73)
## v0.1.7
[compare changes](https://gitea.websimple.com/templates/wp-headless/compare/v0.1.6...v0.1.7)

View File

@@ -0,0 +1,9 @@
import { breakpointsTailwind, useBreakpoints } from "@vueuse/core";
export function useResponsive() {
const { isMobileOrTablet } = useDevice();
const breakpoints = useBreakpoints(breakpointsTailwind, { ssrWidth: isMobileOrTablet ? 375 : 1024 });
const isDesktop = breakpoints.greaterOrEqual("lg");
return { breakpoints, isDesktop };
}

View File

@@ -5,4 +5,9 @@ require_once __DIR__ . '/includes/core/theme-setup.php';
// Vendors
require_once __DIR__ . '/includes/vendors/acf.php';
require_once __DIR__ . '/includes/vendors/rankmath.php';
require_once __DIR__ . '/includes/vendors/tinymce.php';
require_once __DIR__ . '/includes/vendors/wpgraphql.php';
// WPGraphQL
require_once __DIR__ . '/includes/wpgraphql/term-connection.php';

View File

@@ -17,21 +17,6 @@ function moonshine_after_setup_theme() {
// Register sidebars
}
// Bypass headless home URL for specific cases
add_filter( 'home_url', 'moonshine_bypass_home_url', 10, 4 );
function moonshine_bypass_home_url( $url, $path, $orig_scheme, $blog_id ) {
$excluded_patterns = array(
'#/wp-json(/|$)#i', // WP REST API
'#\.(xsl|xml)$#i', // Sitemap and XSLT files
);
foreach ( $excluded_patterns as $pattern ) {
if ( preg_match( $pattern, $url ) ) {
return get_site_url( $blog_id, $path, $orig_scheme );
}
}
return $url;
}
// Display theme version in admin footer
add_filter( 'update_footer', 'moonshine_update_footer', 100 );
function moonshine_update_footer() {

View File

@@ -0,0 +1 @@
<?php

View File

@@ -0,0 +1,17 @@
<?php
// Default WPGraphQL settings
add_filter( 'graphql_get_setting_section_field_value', 'moonshine_wpgraphql_settings', 10, 5 );
function moonshine_wpgraphql_settings( $value, $default_value, $option_name, $section_fields, $section_name ) {
if ( $section_name === 'graphql_general_settings' ) {
switch ( $option_name ) {
case 'graphql_endpoint':
$value = 'graphql';
break;
case "public_introspection_enabled":
$value = "on";
break;
}
}
return $value;
}

View File

@@ -0,0 +1,13 @@
<?php
// Override TermConnection query args
add_filter( 'graphql_term_object_connection_query_args', 'moonshine_graphql_term_object_connection_query_args', 10, 3 );
function moonshine_graphql_term_object_connection_query_args( $query_args, $source, $args ) {
// Sort by 'order' meta value instead of legacy 'term_order' field
if ( 'term_order' === $args['where']['orderby'] ?? false ) {
$query_args['meta_key'] = 'order';
$query_args['orderby'] = 'meta_value_num';
}
return $query_args;
}

View File

@@ -1,24 +1,2 @@
<?php
return array(
'project-id-version' => 'Moonshine',
'report-msgid-bugs-to' => '',
'pot-creation-date' => '2026-01-13 15:52+0000',
'po-revision-date' => '2026-01-13 15:53+0000',
'last-translator' => '',
'language-team' => 'Français du Canada',
'language' => 'fr_CA',
'plural-forms' => 'nplurals=2; plural=n > 1;',
'mime-version' => '1.0',
'content-type' => 'text/plain; charset=UTF-8',
'content-transfer-encoding' => '8bit',
'x-generator' => 'Loco https://localise.biz/',
'x-loco-version' => '2.8.1; wp-6.9; php-8.3.27',
'x-domain' => 'moonshine',
'messages' => array(
'Headless WordPress theme based on Nuxt.' => 'Thème Wordpress headless basé sur Nuxt.',
'https://websimple.com/' => 'https://websimple.com/',
'Main menu' => 'Menu principal',
'Moonshine' => 'Moonshine',
'Pascal Martineau ' => 'Pascal Martineau ',
),
);
return ['project-id-version'=>'Moonshine','report-msgid-bugs-to'=>'','pot-creation-date'=>'2026-01-13 15:52+0000','po-revision-date'=>'2026-01-29 02:55+0000','last-translator'=>'','language-team'=>'Français du Canada','language'=>'fr_CA','plural-forms'=>'nplurals=2; plural=n > 1;','mime-version'=>'1.0','content-type'=>'text/plain; charset=UTF-8','content-transfer-encoding'=>'8bit','x-generator'=>'Loco https://localise.biz/','x-loco-version'=>'2.8.1; wp-6.9; php-8.3.27','x-domain'=>'moonshine','messages'=>['Heading styles'=>'Styles de titres','Headless WordPress theme based on Nuxt.'=>'Thème Wordpress headless basé sur Nuxt.','https://websimple.com/'=>'https://websimple.com/','Inline styles'=>'Styles de caractères','Link styles'=>'Styles de liens','Main menu'=>'Menu principal','Moonshine'=>'Moonshine','Paragraph styles'=>'Styles de paragraphes','Pascal Martineau '=>'Pascal Martineau ','Semi-bold'=>'Semi-gras']];

View File

@@ -3,7 +3,7 @@ msgstr ""
"Project-Id-Version: Moonshine\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-01-13 15:52+0000\n"
"PO-Revision-Date: 2026-01-13 15:53+0000\n"
"PO-Revision-Date: 2026-01-29 02:55+0000\n"
"Last-Translator: \n"
"Language-Team: Français du Canada\n"
"Language: fr_CA\n"
@@ -15,6 +15,10 @@ msgstr ""
"X-Loco-Version: 2.8.1; wp-6.9; php-8.3.27\n"
"X-Domain: moonshine"
#: includes/vendors/tinymce.php:54
msgid "Heading styles"
msgstr "Styles de titres"
#. Description of the theme
msgid "Headless WordPress theme based on Nuxt."
msgstr "Thème Wordpress headless basé sur Nuxt."
@@ -23,6 +27,14 @@ msgstr "Thème Wordpress headless basé sur Nuxt."
msgid "https://websimple.com/"
msgstr "https://websimple.com/"
#: includes/vendors/tinymce.php:34
msgid "Inline styles"
msgstr "Styles de caractères"
#: includes/vendors/tinymce.php:24
msgid "Link styles"
msgstr "Styles de liens"
#: includes/core/theme-setup.php:15
msgid "Main menu"
msgstr "Menu principal"
@@ -31,6 +43,14 @@ msgstr "Menu principal"
msgid "Moonshine"
msgstr "Moonshine"
#: includes/vendors/tinymce.php:44
msgid "Paragraph styles"
msgstr "Styles de paragraphes"
#. Author of the theme
msgid "Pascal Martineau "
msgstr "Pascal Martineau "
#: includes/vendors/tinymce.php:37
msgid "Semi-bold"
msgstr "Semi-gras"

View File

@@ -3,7 +3,7 @@ msgid ""
msgstr ""
"Project-Id-Version: Moonshine\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-01-13 15:52+0000\n"
"POT-Creation-Date: 2026-01-29 02:55+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: \n"
@@ -16,6 +16,10 @@ msgstr ""
"X-Loco-Version: 2.8.1; wp-6.9; php-8.3.27\n"
"X-Domain: moonshine"
#: includes/vendors/tinymce.php:54
msgid "Heading styles"
msgstr ""
#. Description of the theme
msgid "Headless WordPress theme based on Nuxt."
msgstr ""
@@ -24,6 +28,14 @@ msgstr ""
msgid "https://websimple.com/"
msgstr ""
#: includes/vendors/tinymce.php:34
msgid "Inline styles"
msgstr ""
#: includes/vendors/tinymce.php:24
msgid "Link styles"
msgstr ""
#: includes/core/theme-setup.php:15
msgid "Main menu"
msgstr ""
@@ -32,6 +44,14 @@ msgstr ""
msgid "Moonshine"
msgstr ""
#: includes/vendors/tinymce.php:44
msgid "Paragraph styles"
msgstr ""
#. Author of the theme
msgid "Pascal Martineau "
msgstr ""
#: includes/vendors/tinymce.php:37
msgid "Semi-bold"
msgstr ""

View File

@@ -1,3 +1,5 @@
import { version } from "./package.json";
const siteUrl = process.env.NUXT_SITE_URL;
if (!siteUrl) {
throw new Error(`NUXT_SITE_URL is not defined. Make sure to set it in your build environment variables.`);
@@ -15,8 +17,10 @@ export default defineNuxtConfig({
"@lewebsimple/nuxt-graphql",
"@nuxt/eslint",
"@nuxt/ui",
"@nuxtjs/device",
"@nuxtjs/seo",
"nuxt-auth-utils",
"nuxt-svgo",
],
components: {
@@ -39,6 +43,10 @@ export default defineNuxtConfig({
colorMode: false,
},
runtimeConfig: {
wpUrl,
},
compatibilityDate: "2026-01-01",
nitro: {
@@ -47,13 +55,24 @@ export default defineNuxtConfig({
deployConfig: true,
nodeCompat: true,
wrangler: {
name: "foobar",
// Project name
name: "moonshine",
// Cloudflare Workers settings
compatibility_date: "2026-01-27",
main: "./.output/server/index.mjs",
observability: { enabled: true },
preview_urls: false,
// Environment variables
vars: {
NODE_ENV: "production",
NODE_ENV: "staging",
NUXT_SITE_URL: siteUrl,
NUXT_WP_URL: wpUrl,
},
// Bindings
assets: {
binding: "ASSETS",
directory: "./.output/public/",
},
},
},
},
@@ -71,6 +90,11 @@ export default defineNuxtConfig({
},
graphql: {
client: {
cache: {
keyVersion: version,
},
},
server: {
context: ["server/graphql/context"],
schema: {
@@ -85,4 +109,10 @@ export default defineNuxtConfig({
sitemap: false,
svgo: {
autoImportPath: "~/assets/svg/",
componentPrefix: "Svg",
defaultImport: "component",
},
});

View File

@@ -1,7 +1,7 @@
{
"name": "@lewebsimple/moonshine",
"description": "Headless WordPress theme based on Nuxt.",
"version": "0.1.7",
"version": "0.1.8",
"type": "module",
"private": true,
"scripts": {
@@ -16,12 +16,14 @@
},
"dependencies": {
"@iconify-json/lucide": "^1.2.87",
"@lewebsimple/nuxt-graphql": "^0.6.1",
"@lewebsimple/nuxt-graphql": "^0.6.7",
"@nuxt/ui": "4.3.0",
"@nuxtjs/seo": "^3.3.0",
"@nuxtjs/device": "4.0.0",
"@nuxtjs/seo": "^3.4.0",
"jwt-decode": "^4.0.0",
"nuxt": "^4.3.0",
"nuxt-auth-utils": "^0.5.28",
"nuxt-svgo": "^4.2.6",
"tailwindcss": "^4.1.18",
"vue": "^3.5.27",
"vue-router": "^4.6.4",

File diff suppressed because it is too large Load Diff

View File

@@ -3250,6 +3250,19 @@ type GeneralSettings {
"""Code local de linstallation WordPress."""
language: String
"""
The media item representing the site icon configured in site settings, used as the site&#039;s favicon and app icon.
"""
siteIcon: GeneralSettingsToMediaItemConnectionEdge
"""
Site icon URL configured in site settings, used as the site&#039;s favicon and app icon.
"""
siteIconUrl(
"""Size of the site icon in pixels. Defaults to 512. Max 512."""
size: Int
): String
"""
Le numéro du jour de la semaine à laquelle la semaine devrait commencer.
"""
@@ -3268,6 +3281,17 @@ type GeneralSettings {
url: String
}
"""Connection between the GeneralSettings type and the MediaItem type"""
type GeneralSettingsToMediaItemConnectionEdge implements Edge & MediaItemConnectionEdge & OneToOneConnection {
"""
Opaque reference to the nodes position in the connection. Value can be used with pagination args.
"""
cursor: String
"""The node of the connection, without the edges"""
node: MediaItem!
}
"""The Login client options for the github provider."""
type GithubClientOptions implements LoginClientOptions {
"""The client ID."""
@@ -9478,6 +9502,18 @@ type RankMathAuthorArchiveMetaSettings implements RankMathMetaSettingWithArchive
robotsMeta: [RankMathRobotsMetaValueEnum]
}
"""The Breadcrumb trail."""
type RankMathBreadcrumbs {
"""Whether the given breadcrumb is hidden from the schema"""
isHidden: Boolean
"""The text for the given breadcrumb"""
text: String
"""The url for the given breadcrumb"""
url: String
}
"""The RankMath SEO breadcrumbs settings."""
type RankMathBreadcrumbsConfig {
"""Format the label used for archive pages."""
@@ -9533,6 +9569,9 @@ type RankMathCategoryTermSeo implements RankMathSeo {
"""The title to use in the breadcrumbs for this post"""
breadcrumbTitle: String
"""The breadcrumbs trail for the given object"""
breadcrumbs: [RankMathBreadcrumbs]
"""The canonical url."""
canonicalUrl: String
@@ -9563,6 +9602,9 @@ interface RankMathContentNodeSeo implements RankMathSeo {
"""The title to use in the breadcrumbs for this post"""
breadcrumbTitle: String
"""The breadcrumbs trail for the given object"""
breadcrumbs: [RankMathBreadcrumbs]
"""The canonical url."""
canonicalUrl: String
@@ -9825,6 +9867,9 @@ type RankMathMediaItemObjectSeo implements RankMathContentNodeSeo & RankMathSeo
"""The title to use in the breadcrumbs for this post"""
breadcrumbTitle: String
"""The breadcrumbs trail for the given object"""
breadcrumbs: [RankMathBreadcrumbs]
"""The canonical url."""
canonicalUrl: String
@@ -9861,6 +9906,9 @@ type RankMathMediaItemTypeSeo implements RankMathSeo {
"""The title to use in the breadcrumbs for this post"""
breadcrumbTitle: String
"""The breadcrumbs trail for the given object"""
breadcrumbs: [RankMathBreadcrumbs]
"""The canonical url."""
canonicalUrl: String
@@ -10602,6 +10650,9 @@ type RankMathPageObjectSeo implements RankMathContentNodeSeo & RankMathSeo {
"""The title to use in the breadcrumbs for this post"""
breadcrumbTitle: String
"""The breadcrumbs trail for the given object"""
breadcrumbs: [RankMathBreadcrumbs]
"""The canonical url."""
canonicalUrl: String
@@ -10638,6 +10689,9 @@ type RankMathPageTypeSeo implements RankMathSeo {
"""The title to use in the breadcrumbs for this post"""
breadcrumbTitle: String
"""The breadcrumbs trail for the given object"""
breadcrumbs: [RankMathBreadcrumbs]
"""The canonical url."""
canonicalUrl: String
@@ -10668,6 +10722,9 @@ type RankMathPostFormatTermSeo implements RankMathSeo {
"""The title to use in the breadcrumbs for this post"""
breadcrumbTitle: String
"""The breadcrumbs trail for the given object"""
breadcrumbs: [RankMathBreadcrumbs]
"""The canonical url."""
canonicalUrl: String
@@ -10698,6 +10755,9 @@ type RankMathPostObjectSeo implements RankMathContentNodeSeo & RankMathSeo {
"""The title to use in the breadcrumbs for this post"""
breadcrumbTitle: String
"""The breadcrumbs trail for the given object"""
breadcrumbs: [RankMathBreadcrumbs]
"""The canonical url."""
canonicalUrl: String
@@ -10734,6 +10794,9 @@ type RankMathPostTypeSeo implements RankMathSeo {
"""The title to use in the breadcrumbs for this post"""
breadcrumbTitle: String
"""The breadcrumbs trail for the given object"""
breadcrumbs: [RankMathBreadcrumbs]
"""The canonical url."""
canonicalUrl: String
@@ -10806,6 +10869,9 @@ interface RankMathSeo {
"""The title to use in the breadcrumbs for this post"""
breadcrumbTitle: String
"""The breadcrumbs trail for the given object"""
breadcrumbs: [RankMathBreadcrumbs]
"""The canonical url."""
canonicalUrl: String
@@ -11253,6 +11319,9 @@ type RankMathTagTermSeo implements RankMathSeo {
"""The title to use in the breadcrumbs for this post"""
breadcrumbTitle: String
"""The breadcrumbs trail for the given object"""
breadcrumbs: [RankMathBreadcrumbs]
"""The canonical url."""
canonicalUrl: String
@@ -11313,6 +11382,9 @@ type RankMathUserSeo implements RankMathSeo {
"""The title to use in the breadcrumbs for this post"""
breadcrumbTitle: String
"""The breadcrumbs trail for the given object"""
breadcrumbs: [RankMathBreadcrumbs]
"""The canonical url."""
canonicalUrl: String
@@ -15551,9 +15623,6 @@ enum UserRoleEnum {
"""User role with specific capabilities"""
SUBSCRIBER
"""User role with specific capabilities"""
TRANSLATOR
}
"""Connection between the User type and the Comment type"""

View File

@@ -1,11 +1,10 @@
import { defu } from "defu";
export default defineRemoteExecutorHooks({
onRequest(request) {
if (request.context.authToken) {
request.extensions ??= {};
request.extensions.headers = {
...request.extensions.headers,
Authorization: `Bearer ${request.context.authToken}`,
};
// Attach the Authorization header if an authToken is present in the context
if (request.context?.authToken) {
request.extensions = defu(request.extensions, { headers: { Authorization: `Bearer ${request.context.authToken}` } });
}
},
});

View File

@@ -40,15 +40,35 @@ function getAuthUser(user: AuthUserFragment): User {
};
}
// Track in-flight refreshAuthToken calls to prevent duplicate requests
const refreshTokenPromises = new Map<string, Promise<string | undefined>>();
// Refresh auth token by calling remote GraphQL endpoint directly
export async function refreshAuthToken(refreshToken: string): Promise<string | undefined> {
// TODO: const { public: { graphql: { endpoint } } } = useRuntimeConfig();
const endpoint = `${process.env.NUXT_WP_URL || "https://cultureat.ledevsimple.ca"}/graphql`;
const { data } = await executeGraphQLHTTP<ResultOf<"AuthRefreshToken">>({
query: AuthRefreshTokenDocument,
variables: { refreshToken },
}, { endpoint });
return data?.refreshToken?.authToken || undefined;
// Return existing in-flight promise if available
const inFlight = refreshTokenPromises.get(refreshToken);
if (inFlight) {
return inFlight;
}
const refreshPromise = (async () => {
const { wpUrl } = useRuntimeConfig();
const endpoint = `${wpUrl}/graphql`;
const { data } = await executeGraphQLHTTP<ResultOf<"AuthRefreshToken">>({
query: AuthRefreshTokenDocument,
variables: { refreshToken },
}, { endpoint });
return data?.refreshToken?.authToken || undefined;
})();
refreshTokenPromises.set(refreshToken, refreshPromise);
return refreshPromise.finally(() => {
const current = refreshTokenPromises.get(refreshToken);
if (current === refreshPromise) {
refreshTokenPromises.delete(refreshToken);
}
});
}
// Get auth token from user session (refresh if needed)

View File

@@ -1,4 +1,4 @@
// Helper: Extracts nodes from a GraphQL connection object, returning an empty array if nodes are absent.
export function extractNodes<T>(connection: { nodes?: readonly T[] } | null | undefined): readonly T[] {
export function extractNodes<T>(connection: { nodes?: T[] } | undefined): T[] {
return connection?.nodes ?? [];
}

View File

@@ -1,21 +0,0 @@
/**
* For more details on how to configure Wrangler, refer to:
* https://developers.cloudflare.com/workers/wrangler/configuration/
*/
{
"$schema": "node_modules/wrangler/config-schema.json",
"main": "./.output/server/index.mjs",
/**
* Static Assets Binding
*/
"assets": {
"binding": "ASSETS",
"directory": "./.output/public/"
},
/**
* Observability & Analytics
*/
"observability": {
"enabled": true
},
}