-
Notifications
You must be signed in to change notification settings - Fork 78
feat: add Gravatar integration with privacy-preserving proxy #606
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,170 @@ | ||
| --- | ||
| title: Gravatar | ||
| description: Add Gravatar avatars and hovercards to your Nuxt app with privacy-preserving server-side proxying. | ||
| links: | ||
| - label: Source | ||
| icon: i-simple-icons-github | ||
| to: https://github.com/nuxt/scripts/blob/main/src/runtime/registry/gravatar.ts | ||
| size: xs | ||
| - label: Gravatar Developer Docs | ||
| icon: i-simple-icons-gravatar | ||
| to: https://docs.gravatar.com/ | ||
| size: xs | ||
| --- | ||
|
|
||
| [Gravatar](https://gravatar.com) provides globally recognized avatars linked to email addresses. Nuxt Scripts provides a privacy-preserving integration that proxies avatar requests through your own server, preventing Gravatar from tracking your users. | ||
|
|
||
| ## Privacy Benefits | ||
|
|
||
| When using the Gravatar proxy: | ||
|
|
||
| - **User IPs are hidden** from Gravatar's servers | ||
| - **Email hashes stay server-side** — the `?email=` parameter is SHA256-hashed on YOUR server, so hashes never appear in client HTML | ||
| - **Hovercards JS is bundled** through your domain via firstParty mode | ||
| - **Configurable caching** reduces requests to Gravatar | ||
|
|
||
| ## Nuxt Config Setup | ||
|
|
||
| Enable the Gravatar proxy in your `nuxt.config.ts`: | ||
|
|
||
| ```ts [nuxt.config.ts] | ||
| export default defineNuxtConfig({ | ||
| scripts: { | ||
| registry: { | ||
| gravatar: true | ||
| }, | ||
| gravatarProxy: { | ||
| enabled: true, | ||
| cacheMaxAge: 3600 // 1 hour (default) | ||
| } | ||
| } | ||
| }) | ||
| ``` | ||
|
|
||
| ## useScriptGravatar | ||
|
|
||
| The `useScriptGravatar` composable loads the Gravatar hovercards script and provides avatar URL helpers. | ||
|
|
||
| ```ts | ||
| const { proxy } = useScriptGravatar() | ||
|
|
||
| // Get avatar URL from a pre-computed SHA256 hash | ||
| const url = proxy.getAvatarUrl('sha256hash', { size: 200 }) | ||
|
|
||
| // Get avatar URL from email (hashed server-side) | ||
| const url = proxy.getAvatarUrlFromEmail('user@example.com', { size: 200 }) | ||
| ``` | ||
|
|
||
| Please follow the [Registry Scripts](/docs/guides/registry-scripts) guide to learn more about advanced usage. | ||
|
|
||
| ### GravatarApi | ||
|
|
||
| ```ts | ||
| export interface GravatarApi { | ||
| getAvatarUrl: (hash: string, options?: { | ||
| size?: number | ||
| default?: string | ||
| rating?: string | ||
| }) => string | ||
| getAvatarUrlFromEmail: (email: string, options?: { | ||
| size?: number | ||
| default?: string | ||
| rating?: string | ||
| }) => string | ||
| } | ||
| ``` | ||
|
|
||
| ### Config Schema | ||
|
|
||
| ```ts | ||
| export const GravatarOptions = object({ | ||
| cacheMaxAge: optional(number()), | ||
| default: optional(string()), // 'mp', '404', 'robohash', etc. | ||
| size: optional(number()), // 1-2048 | ||
| rating: optional(string()), // 'g', 'pg', 'r', 'x' | ||
| }) | ||
| ``` | ||
|
|
||
| ## ScriptGravatar Component | ||
|
|
||
| The `<ScriptGravatar>` component provides a simple way to render Gravatar avatars: | ||
|
|
||
| ```vue | ||
| <template> | ||
| <!-- By email (server-side hashed) --> | ||
| <ScriptGravatar email="user@example.com" :size="80" /> | ||
|
|
||
| <!-- By pre-computed hash --> | ||
| <ScriptGravatar hash="sha256hash" :size="80" /> | ||
|
|
||
| <!-- With hovercards enabled --> | ||
| <ScriptGravatar email="user@example.com" hovercards /> | ||
| </template> | ||
| ``` | ||
|
|
||
| ### Props | ||
|
|
||
| | Prop | Type | Default | Description | | ||
| |------|------|---------|-------------| | ||
| | `email` | `string` | — | Email address (hashed server-side, never exposed in HTML) | | ||
| | `hash` | `string` | — | Pre-computed SHA256 email hash | | ||
| | `size` | `number` | `80` | Avatar size in pixels | | ||
| | `default` | `string` | `'mp'` | Default image when no Gravatar exists | | ||
| | `rating` | `string` | `'g'` | Content rating filter | | ||
| | `hovercards` | `boolean` | `false` | Add hovercards class for profile pop-ups | | ||
|
|
||
| ## Example | ||
|
|
||
| ### Basic Avatar with Proxy | ||
|
|
||
| ```vue | ||
| <script setup lang="ts"> | ||
| const { proxy } = useScriptGravatar() | ||
|
|
||
| const avatarUrl = computed(() => | ||
| proxy.getAvatarUrlFromEmail('user@example.com', { size: 200 }) | ||
| ) | ||
| </script> | ||
|
|
||
| <template> | ||
| <img :src="avatarUrl" alt="User avatar" /> | ||
| </template> | ||
| ``` | ||
|
|
||
| ### With Hovercards | ||
|
|
||
| Load the Gravatar hovercards script to show profile pop-ups on hover: | ||
|
|
||
| ```vue | ||
| <script setup lang="ts"> | ||
| const { status } = useScriptGravatar() | ||
| </script> | ||
|
|
||
| <template> | ||
| <div> | ||
| <ScriptGravatar | ||
| email="user@example.com" | ||
| :size="80" | ||
| hovercards | ||
| /> | ||
| <p>Hovercards script: {{ status }}</p> | ||
| </div> | ||
| </template> | ||
| ``` | ||
|
|
||
| ## Gravatar Proxy Server Handler | ||
|
|
||
| The proxy handler at `/_scripts/gravatar-proxy` accepts: | ||
|
|
||
| | Parameter | Description | | ||
| |-----------|-------------| | ||
| | `hash` | SHA256 email hash | | ||
| | `email` | Raw email (hashed server-side) | | ||
| | `s` | Size in pixels (default: 80) | | ||
| | `d` | Default image (default: mp) | | ||
| | `r` | Rating filter (default: g) | | ||
|
|
||
| ``` | ||
| /_scripts/gravatar-proxy?email=user@example.com&s=200&d=mp&r=g | ||
| /_scripts/gravatar-proxy?hash=abc123...&s=80 | ||
| ``` | ||
|
Comment on lines
+167
to
+170
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add a language identifier to the fenced code block to fix the markdownlint MD040 error. 📝 Proposed fix-```
+```text
/_scripts/gravatar-proxy?email=user@example.com&s=200&d=mp&r=g
/_scripts/gravatar-proxy?hash=abc123...&s=80🧰 Tools🪛 markdownlint-cli2 (0.21.0)[warning] 167-167: Fenced code blocks should have a language specified (MD040, fenced-code-language) 🤖 Prompt for AI Agents |
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,63 @@ | ||
| <script setup lang="ts"> | ||
| import { computed, ref, onMounted, useAttrs } from 'vue' | ||
| import { useScriptGravatar } from '../registry/gravatar' | ||
|
|
||
| const props = withDefaults(defineProps<{ | ||
| /** Email address — hashed server-side, never exposed in client HTML */ | ||
| email?: string | ||
|
Comment on lines
+6
to
+7
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Email is exposed in client HTML despite JSDoc claiming otherwise. The The server-side hashing prevents the email from reaching Gravatar, but it does appear in client-rendered HTML. Either:
Option 2 would also require adjusting the proxy to accept Also applies to: 36-44 🤖 Prompt for AI Agents |
||
| /** Pre-computed SHA256 hash of the email */ | ||
| hash?: string | ||
| /** Avatar size in pixels */ | ||
| size?: number | ||
| /** Default avatar style when no Gravatar exists */ | ||
| default?: string | ||
| /** Content rating filter */ | ||
| rating?: string | ||
| /** Enable hovercards on hover */ | ||
| hovercards?: boolean | ||
| }>(), { | ||
| size: 80, | ||
| default: 'mp', | ||
| rating: 'g', | ||
| hovercards: false, | ||
| }) | ||
|
|
||
| const attrs = useAttrs() | ||
| const imgSrc = ref('') | ||
|
|
||
| const { $script } = useScriptGravatar() | ||
|
|
||
| const queryOverrides = computed(() => ({ | ||
| size: props.size, | ||
| default: props.default, | ||
| rating: props.rating, | ||
| })) | ||
|
|
||
| onMounted(() => { | ||
| $script.then((api) => { | ||
| if (props.email) { | ||
| imgSrc.value = api.getAvatarUrlFromEmail(props.email, queryOverrides.value) | ||
| } | ||
| else if (props.hash) { | ||
| imgSrc.value = api.getAvatarUrl(props.hash, queryOverrides.value) | ||
| } | ||
| }) | ||
| }) | ||
| </script> | ||
|
|
||
| <template> | ||
| <img | ||
| v-if="imgSrc" | ||
| :src="imgSrc" | ||
| :width="size" | ||
| :height="size" | ||
| :class="{ hovercard: hovercards }" | ||
| v-bind="attrs" | ||
| :alt="attrs.alt as string || 'Gravatar avatar'" | ||
| loading="lazy" | ||
| > | ||
| <span | ||
| v-else | ||
| :style="{ display: 'inline-block', width: `${size}px`, height: `${size}px`, borderRadius: '50%', background: '#e0e0e0' }" | ||
| /> | ||
| </template> | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Duplicate
const urldeclaration makes the example invalid JS/TS.Both
getAvatarUrlandgetAvatarUrlFromEmailassign to the sameconst urlinside the same code block. Readers who copy this snippet will hitCannot redeclare block-scoped variable 'url'.📝 Proposed fix
📝 Committable suggestion
🤖 Prompt for AI Agents