feat: UiProse prose component with link highjacking

This commit is contained in:
2026-01-13 22:19:23 -05:00
parent 764bc6aeea
commit 40becf1135
9 changed files with 909 additions and 3 deletions

View File

@@ -6,6 +6,6 @@ defineProps<SectionTextBlockFragment>();
<template>
<LayoutContained data-section-type="text-block" v-bind="layoutSettings!">
<div v-html="content" />
<UiProse :content="content" />
</LayoutContained>
</template>

View File

@@ -0,0 +1,9 @@
<script setup lang="ts">
defineProps<{ content: string }>();
const refContent = useTemplateRef("refContent");
useProseLinks(refContent);
</script>
<template>
<div ref="refContent" class="prose" v-html="content" />
</template>

View File

@@ -0,0 +1,65 @@
export function useProseLinks(refContent: Ref<HTMLElement | null>) {
const router = useRouter();
const { url } = useSiteConfig();
const siteUrl = new URL(url);
// Determine if the href is internal
const isInternal = (href: string) => {
if (!href) return false;
if (href.startsWith("/")) return true;
if (href.startsWith("#")) return false;
try {
const hrefUrl = new URL(href);
return hrefUrl.hostname === siteUrl.hostname;
}
catch {
return false;
}
};
// Convert href to relative path
const convertToRelative = (href: string) => {
if (href.startsWith("/")) return href;
try {
const hrefUrl = new URL(href);
if (hrefUrl.hostname === siteUrl.hostname) {
return hrefUrl.pathname + hrefUrl.search + hrefUrl.hash;
}
}
catch {
// Invalid URL
}
return href;
};
// Highjack click events to use router for internal links
const handleClick = (e: MouseEvent) => {
const target = e.target as HTMLElement;
const link = target.closest("a");
if (!link) return;
const href = link.getAttribute("href");
if (!href) return;
if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey || link.target === "_blank" || link.hasAttribute("download")) {
return;
}
if (isInternal(href)) {
e.preventDefault();
const path = convertToRelative(href);
router.push(path);
}
};
// Attach and detach event listeners
onMounted(() => {
const element = unref(refContent);
if (element) {
element.addEventListener("click", handleClick);
}
});
onBeforeUnmount(() => {
const element = unref(refContent);
if (element) {
element.removeEventListener("click", handleClick);
}
});
}

View File

@@ -2,3 +2,6 @@
// Core
require_once __DIR__ . '/includes/core/theme-setup.php';
// Vendors
require_once __DIR__ . '/includes/vendors/tinymce.php';

View File

@@ -0,0 +1,13 @@
<?php
// Convert absolute URLs to relative in link query
add_filter( 'wp_link_query', 'moonshine_tinymce_relative_urls' );
function moonshine_tinymce_relative_urls( $results ) {
foreach ( $results as &$result ) {
if ( empty( $result['permalink'] ) ) {
continue;
}
$result['permalink'] = str_replace( get_home_url(), '', $result['permalink'] );
}
return $results;
}

View File

@@ -1,3 +1,5 @@
import { description } from "../../../composer.json";
// https://nuxt.com/docs/api/configuration/nuxt-config
export default defineNuxtConfig({
@@ -5,6 +7,7 @@ export default defineNuxtConfig({
"@lewebsimple/nuxt-graphql",
"@nuxt/eslint",
"@nuxt/ui",
"@nuxtjs/seo",
"nuxt-auth-utils",
],
@@ -18,6 +21,12 @@ export default defineNuxtConfig({
css: ["~/assets/css/_main.css"],
site: {
name: description,
url: process.env.NUXT_SITE_URL || "https://wp-headless.ledevsimple.ca",
defaultLocale: "fr",
},
ui: {
colorMode: false,
},
@@ -47,4 +56,5 @@ export default defineNuxtConfig({
},
saveSdl: "server/graphql/schema.graphql",
},
});

View File

@@ -18,6 +18,7 @@
"@iconify-json/lucide": "^1.2.84",
"@lewebsimple/nuxt-graphql": "^0.4.0",
"@nuxt/ui": "4.3.0",
"@nuxtjs/seo": "^3.3.0",
"jwt-decode": "^4.0.0",
"nuxt": "^4.2.2",
"nuxt-auth-utils": "^0.5.27",

File diff suppressed because it is too large Load Diff

View File

@@ -1,2 +0,0 @@
User-Agent: *
Disallow: