QUICKSTARTS BY STACK
The fast path: open the Connect wizard. Pick Dynamic site (SSR / on-request rendering) for the default SSR Nuxt setup, or Static site (Vercel / Netlify / Cloudflare Pages) if you use
nuxi generateand your blog is pre-rendered. The wizard provides the right env vars and (for static) the receiver.
Server route
// server/api/blog/[...].get.ts
export default defineEventHandler(async (event) => {
const config = useRuntimeConfig();
const path = getRouterParam(event, "_") ?? "";
const target = `${config.mentionwellApiUrl}/api/public/${config.mentionwellSiteSlug}/${path}${event.node.req.url?.includes("?") ? "?" + event.node.req.url.split("?")[1] : ""}`;
return await $fetch(target, {
headers: { Authorization: `Bearer ${config.mentionwellApiKey}` }
});
});
Composable
// composables/useBlog.ts
export const useBlogList = () => useFetch("/api/blog/posts");
export const useBlogPost = (slug: string) => useFetch(`/api/blog/posts/${slug}`);
Pages
<!-- pages/blog/index.vue -->
<script setup lang="ts">
const { data } = await useBlogList();
</script>
<template>
<ul><li v-for="post in data?.posts" :key="post.slug">
<NuxtLink :to="`/blog/${post.slug}`">{{ post.title }}</NuxtLink>
</li></ul>
</template>
<!-- pages/blog/[slug].vue -->
<script setup lang="ts">
const route = useRoute();
const { data } = await useBlogPost(route.params.slug as string);
</script>
<template>
<article>
<h1>{{ data?.post.title }}</h1>
<div v-html="data?.post.html" />
</article>
</template>
nuxt.config.ts
export default defineNuxtConfig({
runtimeConfig: {
mentionwellApiUrl: process.env.MENTIONWELL_API_URL,
mentionwellSiteSlug: process.env.MENTIONWELL_SITE_SLUG,
mentionwellApiKey: process.env.MENTIONWELL_API_KEY,
mentionwellWebhookSecret: process.env.MENTIONWELL_WEBHOOK_SECRET
}
});
Webhook receiver
// server/api/mentionwell.post.ts
import { Buffer } from "node:buffer";
import { createHmac, timingSafeEqual } from "node:crypto";
export default defineEventHandler(async (event) => {
const config = useRuntimeConfig(event);
const raw = (await readRawBody(event, "utf8")) ?? "";
const signature = getHeader(event, "x-mentionwell-signature") ?? "";
const expected = createHmac("sha256", config.mentionwellWebhookSecret).update(raw).digest("hex");
const verified =
signature.length === expected.length &&
timingSafeEqual(Buffer.from(signature, "utf8"), Buffer.from(expected, "utf8"));
if (!verified) throw createError({ statusCode: 401, statusMessage: "Invalid signature" });
const { post } = JSON.parse(raw) as { post?: { slug?: string } };
// Invalidate Nitro/CDN cache entries for /blog and post?.slug here.
return { ok: true, slug: post?.slug ?? null };
});