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 generate and 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 };
});