[
  {
    "path": ".editorconfig",
    "content": "# editorconfig.org\nroot = true\n\n[*]\nindent_size = 2\nindent_style = space\nend_of_line = lf\ncharset = utf-8\ntrim_trailing_whitespace = true\ninsert_final_newline = true\n\n[*.md]\ntrim_trailing_whitespace = false\n"
  },
  {
    "path": ".eslintignore",
    "content": "dist\nnode_modules\ntest/fixtures\nplayground/*\n\nsaas\n.unlighthouse\n"
  },
  {
    "path": ".github/FUNDING.yml",
    "content": "github: [harlan-zw]\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.yml",
    "content": "name: 🐞 Bug report\ndescription: Report an issue\nlabels: [pending triage]\nbody:\n  - type: markdown\n    attributes:\n      value: |\n        Thanks for taking the time to fill out this bug report!\n  - type: textarea\n    id: bug-description\n    attributes:\n      label: Describe the bug\n      description: A clear and concise description of what the bug is. If you intend to submit a PR for this issue, tell us in the description. Thanks!\n      placeholder: Bug description\n    validations:\n      required: true\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request.yml",
    "content": "name: 🚀 New feature proposal\ndescription: Propose a new feature\nlabels: [enhancement]\nbody:\n  - type: markdown\n    attributes:\n      value: |\n        Thanks for your interest in the project and taking the time to fill out this feature report!\n  - type: textarea\n    id: feature-description\n    attributes:\n      label: Clear and concise description of the problem\n      description: 'As a developer using VueUse I want [goal / wish] so that [benefit]. If you intend to submit a PR for this issue, tell us in the description. Thanks!'\n    validations:\n      required: true\n"
  },
  {
    "path": ".github/workflows/release.yml",
    "content": "name: Release\n\npermissions:\n  contents: write\n\non:\n  push:\n    tags:\n      - 'v*'\njobs:\n  release:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v3\n        with:\n          fetch-depth: 0\n\n      - name: Install pnpm\n        uses: pnpm/action-setup@v2\n\n      - name: Set node\n        uses: actions/setup-node@v3\n        with:\n          node-version: 18.x\n\n      - run: npx changelogithub\n        env:\n          GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}\n"
  },
  {
    "path": ".gitignore",
    "content": "# Nuxt dev/build outputs\n.output\n.data\n../.nuxt\n.nuxt\n.nitro\n.cache\ndist\n\n.saas\n\n# Node dependencies\nnode_modules\n\n# Logs\nlogs\n*.log\n\n# Misc\n.DS_Store\n.fleet\n.idea\n\n# Local env files\n.env\n.env.*\n!.env.example\n\n.tokens.js\n.db\n"
  },
  {
    "path": ".npmrc",
    "content": "shamefully-hoist=true\nignore-workspace-root-check=true\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2024 Harlan Wilton\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "<h1 align='center'>Request Indexing</h1>\n\n<p align=\"center\">\nGet your pages indexed on Google within 48 hours. (on average)\n</p>\n\n<p align=\"center\">\n<table>\n<tbody>\n<td align=\"center\">\n<img width=\"800\" height=\"0\" /><br>\n<i></i> <a href=\"https://requestindexing.com/\">requestindexing.com 🥳</a></b> <br>\n<sup> Please report any issues 🐛</sup><br>\n<sub>Made possible by my <a href=\"https://github.com/sponsors/harlan-zw\">Sponsor Program 💖</a><br> Follow me <a href=\"https://twitter.com/harlan_zw\">@harlan_zw</a> 🐦 • Join <a href=\"https://discord.gg/275MBUBvgP\">Discord</a> for help</sub><br>\n<img width=\"800\" height=\"0\" />\n</td>\n</tbody>\n</table>\n</p>\n\n> [!NOTE]\n> These docs are a work in progress. Please check back soon for updates.\n\n## Features\n\n- ⚡ Request indexing on new sites and pages, have them appear on Google in 48 hours.\n- 📊 Dashboard to see the search performance of all your Google Search Console sites.\n- 🗓️ Keep all your site data. Google Search Console data deletes site data longer than 16 months, start keeping it. (soon)\n\n## Background\n\nBuilding a SaaS is quick and easy with [Nuxt](https://nuxt.com).\n\nThis project is an effort to prove that and was a success. I shipped the first version in 64 hours total.\n\nBuilt With:\n\n- [Nuxt](https://nuxt.com)\n- [Nuxt UI Pro](https://ui.nuxt.com/pro?aff=5zj9e)\n- [Nuxt SEO](https://nuxtseo.com)\n- [Google APIs](https://developers.google.com/apis-explorer)\n\nCredits to [google-indexing-script](https://github.com/goenning/google-indexing-script) is the inspiration for this project.\nLearn more about how it works by reading the [Google Indexing Script](https://seogets.com/blog/google-indexing-script).\n\n## Run Locally\n\n1. Git clone the project:\n\n```bash\ngit clone git@github.com:harlan-zw/request-indexing.git\n```\n\n2. Install deps:\n\n```bash\npnpm i\n```\n\n3. Configure the keys:\n\nYou will need to create a Google OAuth Client ID and Secret. You can do this by visiting the [Google Developer Console](https://console.developers.google.com/).\n\nThe following scopes are required:\n- `userinfo.email`\n- `webmasters.readonly`\n- `indexing`\n\nYou will need to add the redirect URL to your OAuth client. This will be `http://localhost:3000/auth/google` and `http://localhost:3000/auth/google-indexing`.\n\n```bash\nNUXT_OAUTH_GOOGLE_CLIENT_ID=<clientId>\nNUXT_OAUTH_GOOGLE_CLIENT_SECRET=<clientSecret>\n```\n\nYou should also set a unique 32 character string for the security keys:\n\n```bash\nNUXT_KEY=<mustbe32chars>\nNUXT_SESSION_PASSWORD=<secret>\n```\n\n4. Start the server:\n\n```bash\npnpm dev\n```\n\n5. Building your site:\n\nTo build and deploy site you will need to purchase a [Nuxt UI Pro](https://ui.nuxt.com/pro?aff=5zj9e) license.\n\nOnce you have your license update your `.env` file with your license key:\n\n```bash\nNUXT_UI_PRO_LICENSE_KEY=<license>\n```\n\nThen run the build command:\n\n```bash\npnpm build\n```\n\nThat's it!\n\n## Sponsors\n\n<p align=\"center\">\n  <a href=\"https://raw.githubusercontent.com/harlan-zw/static/main/sponsors.svg\">\n    <img src='https://raw.githubusercontent.com/harlan-zw/static/main/sponsors.svg'/>\n  </a>\n</p>\n\n## License\n\nMIT License © 2022-PRESENT [Harlan Wilton](https://github.com/harlan-zw)\n"
  },
  {
    "path": "app/router.options.ts",
    "content": "import type { RouterConfig } from '@nuxt/schema'\n\nfunction findHashPosition(hash): { el: any, behavior: ScrollBehavior, top: number } {\n  const el = document.querySelector(hash)\n  // vue-router does not incorporate scroll-margin-top on its own.\n  if (el) {\n    const top = Number.parseFloat(getComputedStyle(el).scrollMarginTop)\n\n    return {\n      el: hash,\n      behavior: 'smooth',\n      top,\n    }\n  }\n}\n\n// https://router.vuejs.org/api/#routeroptions\nexport default <RouterConfig>{\n  scrollBehavior(to, from, savedPosition) {\n    const nuxtApp = useNuxtApp()\n\n    // If history back\n    if (savedPosition) {\n      // Handle Suspense resolution\n      return new Promise((resolve) => {\n        nuxtApp.hooks.hookOnce('page:finish', () => {\n          setTimeout(() => resolve(savedPosition), 50)\n        })\n      })\n    }\n\n    // Scroll to heading on click\n    if (to.hash) {\n      return new Promise((resolve) => {\n        if (to.path === from.path) {\n          setTimeout(() => resolve(findHashPosition(to.hash)), 50)\n        }\n        else {\n          nuxtApp.hooks.hookOnce('page:finish', () => {\n            setTimeout(() => resolve(findHashPosition(to.hash)), 50)\n          })\n        }\n      })\n    }\n\n    // Scroll to top of window\n    return { top: 0 }\n  },\n}\n"
  },
  {
    "path": "app.d.ts",
    "content": "module '#auth-utils' {\n  export interface UserSession {\n    user?: {\n      userId: string\n      picture: string\n      indexingTokenId?: string\n    }\n  }\n}\n\nexport {}\n"
  },
  {
    "path": "app.vue",
    "content": "<script setup>\nconst colorMode = useColorMode()\n\nconst color = computed(() => colorMode.value === 'dark' ? '#111827' : 'white')\n\nuseHead({\n  meta: [\n    { key: 'theme-color', name: 'theme-color', content: color },\n  ],\n})\n\nconst entry = useHead({\n  link: [\n    {\n      rel: 'icon',\n      type: 'image/svg+xml',\n      href: `/icons/icon-${colorMode.value === 'system' ? 'dark' : colorMode.value}.svg`,\n    },\n  ],\n})\n// switch logos on colorMode\nwatch(colorMode, () => {\n  entry.patch({\n    link: [\n      {\n        rel: 'icon',\n        type: 'image/svg+xml',\n        href: `/icons/icon-${colorMode.value}.svg`,\n      },\n    ],\n  })\n})\n\nuseSeoMeta({\n  titleTemplate: '%s %separator Request Indexing',\n  ogSiteName: 'Request Indexing',\n  ogTitle: 'Get your pages indexed within 48 hours.',\n  twitterTitle: 'Get your pages indexed within 48 hours.',\n})\n\ndefineOgImageComponent('Home')\n</script>\n\n<template>\n  <div>\n    <NuxtLoadingIndicator />\n\n    <NuxtLayout>\n      <NuxtPage />\n    </NuxtLayout>\n\n    <UNotifications />\n  </div>\n</template>\n\n<style>\npre {\n  --scrollbar-thumb: #3b5178;\n}\n\n.dark pre {\n  --scrollbar-thumb: #acbad2;\n}\n\n* {\n  --scrollbar-track: initial;\n  --scrollbar-thumb: initial;\n  scrollbar-color: var(--scrollbar-thumb) var(--scrollbar-track);\n  scrollbar-width: thin;\n  --scrollbar-thumb: #acbad2;\n}\n\n::-webkit-scrollbar-track {\n  background-color: var(--scrollbar-track)\n}\n\n::-webkit-scrollbar-thumb {\n  background-color: var(--scrollbar-thumb);\n  border-radius: .25rem\n}\n\n::-webkit-scrollbar {\n  width: 8px;\n  height: 8px\n}\n\n.dark * {\n  --scrollbar-thumb: #3b5178;\n}\n\n.page-enter-active,\n.page-leave-active {\n  transition: all 0.2s;\n}\n.page-enter-from,\n.page-leave-to {\n  opacity: 0;\n  transform: translateY(1rem);\n  filter: blur(0.2rem);\n}\n</style>\n"
  },
  {
    "path": "components/Footer.vue",
    "content": "<script setup lang=\"ts\">\nconst toast = useToast()\nconst links = [\n  {\n    label: 'Request Indexing',\n    children: [\n      {\n        label: 'Get Started',\n        to: '/get-started',\n      },\n    ],\n  },\n  {\n    label: 'Resources',\n    children: [\n      {\n        label: 'Docs',\n        to: 'https://github.com/harlan-zw/request-indexing',\n        target: '_blank',\n      },\n      {\n        label: 'Privacy',\n        to: '/privacy',\n      },\n      {\n        label: 'Terms',\n        to: '/terms',\n      },\n    ],\n  },\n  {\n    label: 'Project',\n    children: [\n      {\n        label: 'Report a bug',\n        to: 'https://github.com/harlan-zw/request-indexing/issues/new?assignees=&labels=pending+triage&projects=&template=bug_report.yml',\n        target: '_blank',\n      },\n      {\n        label: 'Roadmap',\n        to: 'https://github.com/harlan-zw/request-indexing/issues?q=is%3Aopen+label%3Aenhancement+sort%3Aupdated-desc',\n        target: '_blank',\n      },\n      {\n        label: 'Changelog',\n        to: 'https://github.com/harlan-zw/request-indexing/releases',\n        target: '_blank',\n      },\n    ],\n  },\n]\n\ninterface JSConfettiApi {\n  addConfetti: (options?: { emojis: string[] }) => void\n}\ndeclare global {\n  interface Window {\n    JSConfetti: { new (): JSConfettiApi }\n  }\n}\nfunction toaster() {\n  const $script = useScript<JSConfettiApi>({\n    key: 'confetti',\n    src: 'https://cdn.jsdelivr.net/npm/js-confetti@latest/dist/js-confetti.browser.js',\n  }, {\n    skipEarlyConnections: true,\n    use() {\n      return new window.JSConfetti()\n    },\n  })\n  $script.addConfetti({ emojis: ['🍞'] })\n  toast.add({\n    title: 'So you like easters eggs? 🥚',\n    description: 'How about some bread? 🍞',\n  })\n}\n</script>\n\n<template>\n  <UFooter>\n    <template #top>\n      <UFooterColumns :links=\"links\">\n        <template #right>\n          <UCard class=\" p-5\">\n            <div>\n              <div class=\"mb-2\">\n                Hey <Icon name=\"noto:waving-hand\" /> My name is <a href=\"https://harlanzw.com\" target=\"_blank\" class=\"underline\">Harlan</a> <img alt=\"Harlan Wilton\" loading=\"lazy\" src=\"https://avatars.githubusercontent.com/u/5326365?v=4\" class=\"inline rounded-full w-5 h-5\">, I'm the creator of Request Indexing.\n              </div>\n              <div>\n                Do you like this tool? Need a hand? Get in <a href=\"https://twitter.com/harlan_zw\" class=\"underline\">touch</a> with me.\n              </div>\n            </div>\n          </UCard>\n        </template>\n      </UFooterColumns>\n    </template>\n\n    <template #left>\n      <p class=\"text-gray-500 dark:text-gray-400 text-sm mr-5\">\n        Copyright © {{ new Date().getFullYear() }}. All rights reserved.\n      </p>\n      <p class=\"text-gray-500 dark:text-gray-400 text-sm\">\n        Credits for the idea to <a href=\"https://seogets.com/\" target=\"_blank\" class=\"underline\">SEO Gets</a>.\n      </p>\n    </template>\n\n    <template #right>\n      <UButton color=\"gray\" title=\"Bread\" variant=\"link\" target=\"_blank\" class=\"group\" @click=\"toaster\">\n        <div class=\"hidden group-hover:block h-0\">\n          <Icon name=\"noto:bread\" class=\"text-xl  \" />\n        </div>\n      </UButton>\n\n      <UColorModeButton size=\"sm\" />\n\n      <UButton color=\"gray\" title=\"Twitter\" variant=\"link\" to=\"https://twitter.com/harlan_zw\" target=\"_blank\">\n        <UIcon name=\"i-simple-icons-twitter\" class=\"text-xl\" />\n      </UButton>\n      <UButton color=\"gray\" title=\"GitHub\" aria-label=\"GitHub\" variant=\"link\" to=\"https://github.com/harlan-zw/request-indexing\" target=\"_blank\">\n        <UIcon name=\"i-simple-icons-github\" class=\"text-xl\" />\n      </UButton>\n      <UButton color=\"gray\" title=\"Discord\" aria-label=\"Discord\" variant=\"link\" to=\"https://discord.gg/275MBUBvgP\" target=\"_blank\">\n        <UIcon name=\"i-simple-icons-discord\" class=\"text-xl\" />\n      </UButton>\n    </template>\n  </UFooter>\n</template>\n"
  },
  {
    "path": "components/GithubStar.vue",
    "content": "<script lang=\"ts\" setup>\nimport { withBase, withoutBase } from 'ufo'\nimport { useFetch } from '#imports'\n\nconst props = defineProps<{\n  repo: string\n  raw?: boolean\n  to?: string\n}>()\n\nconst repo = computed(() => {\n  // support users providing full github url\n  return withoutBase(props.repo, 'https://github.com/')\n})\nconst link = computed(() => {\n  return props.to || withBase(repo.value, 'https://github.com/')\n})\n// pull the stars from the server\nconst { data } = await useFetch('/api/github/repo', {\n  query: {\n    repo: repo.value,\n  },\n  watch: [\n    repo,\n  ],\n  key: `github-stars-${repo.value}`,\n}).catch(() => {\n  return {\n    data: ref({ stars: 0 }),\n  }\n})\n\nconst stars = computed(() => {\n  if (props.raw)\n    return data.value\n  return new Intl.NumberFormat('en', { notation: 'compact' }).format(data.value?.stars || 0)\n})\n</script>\n\n<template>\n  <NuxtLink :to=\"link\" target=\"_blank\" :aria-label=\"`Star ${repo} on GitHub`\">\n    <slot :stars=\"stars\">\n      <div>{{ stars }}</div>\n    </slot>\n  </NuxtLink>\n</template>\n"
  },
  {
    "path": "components/GoogleSvg.vue",
    "content": "<template>\n  <svg width=\"800\" height=\"800\" viewBox=\"-0.5 0 48 48\" xmlns=\"http://www.w3.org/2000/svg\"><g fill=\"none\" fill-rule=\"evenodd\"><path d=\"M9.827 24c0-1.524.253-2.986.705-4.356l-7.909-6.04A23.456 23.456 0 0 0 .213 24c0 3.737.868 7.26 2.407 10.388l7.905-6.05A13.885 13.885 0 0 1 9.827 24\" fill=\"#FBBC05\" /><path d=\"M23.714 10.133c3.311 0 6.302 1.174 8.652 3.094L39.202 6.4C35.036 2.773 29.695.533 23.714.533a23.43 23.43 0 0 0-21.09 13.071l7.908 6.04a13.849 13.849 0 0 1 13.182-9.51\" fill=\"#EB4335\" /><path d=\"M23.714 37.867a13.849 13.849 0 0 1-13.182-9.51l-7.909 6.038a23.43 23.43 0 0 0 21.09 13.072c5.732 0 11.205-2.036 15.312-5.849l-7.507-5.804c-2.118 1.335-4.786 2.053-7.804 2.053\" fill=\"#34A853\" /><path d=\"M46.145 24c0-1.387-.213-2.88-.534-4.267H23.714V28.8h12.604c-.63 3.091-2.346 5.468-4.8 7.014l7.507 5.804c4.314-4.004 7.12-9.969 7.12-17.618\" fill=\"#4285F4\" /></g></svg>\n</template>\n"
  },
  {
    "path": "components/Gradient.vue",
    "content": "<script lang=\"ts\" setup>\nconst colorMode = useColorMode()\nconst dark = computed(() => colorMode.value === 'dark')\n</script>\n\n<template>\n  <div class=\"gradient\" :class=\"{ dark }\">\n    <div class=\"overlay\" :class=\"{ dark }\">\n      <slot />\n    </div>\n  </div>\n</template>\n\n<style lang=\"postcss\" scoped>\n.gradient {\n  position: absolute;\n  inset: 0;\n  pointer-events: none;\n  background: radial-gradient(50% 50% at 50% 50%, rgb(var(--color-primary-500) / 0.25) 0, #FFF 100%);\n}\n\n.dark {\n  .gradient {\n    background: radial-gradient(50% 50% at 50% 50%, rgb(var(--color-primary-400) / 0.1) 0, rgb(var(--color-gray-950)) 100%);\n  }\n}\n\n.overlay {\n  background-size: 100px 100px;\n  background-image:\n    linear-gradient(to right, rgb(var(--color-gray-200)) 0.5px, transparent 0.5px),\n    linear-gradient(to bottom, rgb(var(--color-gray-200)) 0.5px, transparent 0.5px);\n}\n.dark {\n  .overlay {\n    background-image:\n      linear-gradient(to right, rgb(var(--color-gray-900)) 0.5px, transparent 0.5px),\n      linear-gradient(to bottom, rgb(var(--color-gray-900)) 0.5px, transparent 0.5px);\n  }\n}\n</style>\n"
  },
  {
    "path": "components/GraphClicks.vue",
    "content": "<script lang=\"ts\" setup>\nimport { createChart } from 'lightweight-charts'\n\nconst props = defineProps<{\n  value: { time: string, value: number }[]\n  value2: { time: string, value: number }[]\n  height?: number | string\n}>()\n\nconst colorMode = useColorMode()\n\nconst chart = ref(null)\nconst container = ref(null)\n\nconst tooltipData = ref({\n  clicks: 0,\n  impressions: 0,\n  time: '',\n})\n\nconst darkTheme = {\n  chart: {\n    layout: {\n      background: {\n        type: 'solid',\n        color: 'transparent',\n      },\n      lineColor: '#2B2B43',\n      textColor: '#D9D9D9',\n    },\n    watermark: {\n      color: 'rgba(0, 0, 0, 0)',\n    },\n    crosshair: {\n      color: '#758696',\n    },\n    grid: {\n      vertLines: {\n        visible: false,\n      },\n      horzLines: {\n        visible: false,\n      },\n    },\n  },\n  series: {\n    topColor: 'rgba(32, 226, 47, 0.56)',\n    bottomColor: 'rgba(32, 226, 47, 0.04)',\n    lineColor: 'rgba(32, 226, 47, 1)',\n  },\n  series2: {\n    topColor: 'rgba(156, 39, 176, 0.4)',\n    bottomColor: 'rgba(156, 39, 176, 0.04)',\n    lineColor: 'rgba(156, 39, 176, 0.5)',\n  },\n}\n\nconst lightTheme = {\n  chart: {\n    layout: {\n      background: {\n        type: 'solid',\n        color: 'transparent',\n      },\n      lineColor: '#2B2B43',\n      textColor: '#191919',\n    },\n    watermark: {\n      color: 'rgba(0, 0, 0, 0)',\n    },\n    grid: {\n      vertLines: {\n        visible: false,\n      },\n      horzLines: {\n        visible: false,\n      },\n    },\n  },\n  series: {\n    topColor: 'rgba(33, 150, 243, 0.9)',\n    bottomColor: 'rgba(33, 150, 243, 0.04)',\n    lineColor: 'rgba(33, 150, 243, 0.5)',\n  },\n  // this is the impressions from google search console, we want to use a similar purple\n  series2: {\n    topColor: 'rgba(156, 39, 176, 0.3)',\n    bottomColor: 'rgba(156, 39, 176, 0.04)',\n    lineColor: 'rgba(156, 39, 176, 0.4)',\n  },\n}\n\nconst themesData = {\n  Dark: darkTheme,\n  Light: lightTheme,\n}\n\nonMounted(() => {\n  const _chart = createChart(chart.value!, {\n    height: Number(props.height) || 100,\n    autoSize: true,\n    rightPriceScale: {\n      visible: false,\n    },\n    timeScale: {\n      visible: false,\n    },\n    crosshair: {\n      horzLine: {\n        visible: false,\n      },\n      vertLine: {\n        visible: false,\n      },\n    },\n  })\n  _chart.timeScale().fitContent()\n\n  const areaSeries = _chart.addAreaSeries({\n    topColor: 'rgba(33, 150, 243, 0.56)',\n    bottomColor: 'rgba(33, 150, 243, 0.04)',\n    lineColor: 'rgba(33, 150, 243, 1)',\n    lineWidth: 2,\n    priceLineVisible: false,\n    lastValueVisible: false,\n    priceFormat: {\n      type: 'volume',\n    },\n    lineType: 2,\n  })\n  areaSeries.setData(props.value)\n\n  const areaSeries2 = _chart.addAreaSeries({\n    topColor: 'rgba(33, 150, 243, 0.56)',\n    bottomColor: 'rgba(33, 150, 243, 0.04)',\n    lineColor: 'rgba(33, 150, 243, 1)',\n    lineWidth: 2,\n    priceLineVisible: false,\n    lastValueVisible: false,\n    priceFormat: {\n      type: 'volume',\n    },\n    lineType: 2,\n  })\n  areaSeries2.setData(props.value2)\n\n  _chart.subscribeCrosshairMove((param) => {\n    const _container = container.value!\n    if (\n      param.point === undefined\n      || !param.time\n      || param.point.x < 0\n      || param.point.x > _container.clientWidth\n      || param.point.y < 0\n      || param.point.y > _container.clientHeight\n    ) {\n      tooltipData.value = {\n        clicks: 0,\n        impressions: 0,\n        time: '',\n      }\n    }\n    else {\n      // time will be in the same format that we supplied to setData.\n      // thus it will be YYYY-MM-DD\n      const dateStr = param.time\n      // toolTip.style.display = 'block'\n      const _clicks = param.seriesData.get(areaSeries)!\n      const clicks = _clicks.value !== undefined ? _clicks.value : _clicks.time\n      const _impressions = param.seriesData.get(areaSeries2)\n      const impressions = _impressions.value !== undefined ? _impressions.value : _impressions.time\n\n      tooltipData.value = {\n        clicks,\n        impressions,\n        time: dateStr,\n      }\n    }\n  })\n\n  function syncToTheme(theme) {\n    _chart.applyOptions(themesData[theme].chart)\n    areaSeries.applyOptions(themesData[theme].series)\n    areaSeries2.applyOptions(themesData[theme].series2)\n  }\n\n  syncToTheme(colorMode.value === 'dark' ? 'Dark' : 'Light')\n  watch(colorMode, (newVal) => {\n    syncToTheme(newVal.value === 'dark' ? 'Dark' : 'Light')\n  })\n})\n</script>\n\n<template>\n  <div ref=\"container\" class=\"w-full h-full\">\n    <div ref=\"chart\" />\n    <div class=\"tooltip\">\n      <div v-if=\"tooltipData.time\" class=\"dark:text-gray-200 text-gray-600 text-xs\">\n        <div class=\"dark:text-gray-400 text-gray-500 text-xs \">\n          {{ tooltipData.time }}\n        </div>\n        <div>Clicks {{ useHumanFriendlyNumber(tooltipData.clicks) }}</div>\n        <div>Impressions {{ useHumanFriendlyNumber(tooltipData.impressions) }}</div>\n      </div>\n    </div>\n  </div>\n</template>\n\n<style scoped>\n.tooltip {\n  text-align: right;\n  position: absolute;\n  padding: 4px;\n  z-index: 20;\n  top: 4px;\n  right: 8px;\n  pointer-events: none;\n}\n</style>\n"
  },
  {
    "path": "components/Header.vue",
    "content": "<script setup lang=\"ts\">\nimport { createLogoutHandler } from '~/composables/auth'\nimport type { User } from '~/types'\n\nconst { loggedIn, user, session } = useUserSession()\n\nconst logout = createLogoutHandler()\nconst router = useRouter()\n\nconst links = computed(\n  () => loggedIn.value\n    ? [{\n        label: 'Dashboard',\n        to: '/dashboard',\n      }]\n    : [],\n)\n\nconst authDropdownItems = computed(() => {\n  return [\n    [\n      { label: 'Account', slot: 'account', to: '/account', icon: 'i-heroicons-user-circle' },\n    ],\n    user.value.access === 'pro'\n      ? false\n      : [\n          // upgrade to pro item\n          { label: 'Upgrade', slot: 'pro', to: '/account/upgrade', icon: 'i-heroicons-star' },\n        ],\n    [\n      {\n        label: 'Logout',\n        click: () => logout(),\n        icon: 'i-heroicons-arrow-left-end-on-rectangle',\n      },\n    ],\n  ].filter(Boolean)\n})\n\nasync function updateAnalyticsPeriod(newPeriod: User['analyticsPeriod']) {\n  session.value = await $fetch('/api/user/me', {\n    method: 'POST',\n    body: JSON.stringify({ analyticsPeriod: newPeriod }),\n  })\n  const currentRoute = router.currentRoute.value\n  const sites = await fetchSites()\n  // refresh all sites\n  if (currentRoute.path === '/dashboard') {\n    for (const site of sites.data.value!) {\n      const res = await fetchSite(site)\n      await res.forceRefresh()\n    }\n  }\n  else if (currentRoute.name === 'dashboard-site-slug') {\n    const res = await fetchSite(sites.data.value!.find(site => site.siteUrl === currentRoute.params.slug)!)\n    await res.forceRefresh()\n  }\n}\n\nconst periodItems = [\n  [{ slot: 'info', disabled: true }],\n  // days\n  [\n    { label: '7 days', value: '7d', click: () => updateAnalyticsPeriod('7d') },\n    { label: '28 days', value: '28d', click: () => updateAnalyticsPeriod('28d') },\n  ],\n  // months\n  [\n    { label: '3 months', value: '3mo', click: () => updateAnalyticsPeriod('3mo') },\n    { label: '6 months', value: '6mo', click: () => updateAnalyticsPeriod('6mo') },\n    { label: '12 months', value: '12mo', click: () => updateAnalyticsPeriod('12mo') },\n    { label: '16 months', value: '16mo', click: () => updateAnalyticsPeriod('16mo') },\n  ],\n]\n\nconst isOnDashboard = computed(() => router.currentRoute.value.path.startsWith('/dashboard'))\n</script>\n\n<template>\n  <UHeader :links=\"links\">\n    <template #logo>\n      <div class=\"flex items-center gap-2 group\">\n        <svg height=\"25\" width=\"25\" class=\"text-green-500 group-hover:text-green-300 transition\" xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 267 262\" xml:space=\"preserve\">\n          <!-- magnifying glass  -->\n          <path class=\"text-gray-900 dark:text-gray-200\" fill=\"currentColor\" d=\"M263.158 210.588a356.834 356.834 0 0 1-4.262 20.272c-6.823 10.235-16.074 16.193-27.508 15.709-9.868-.419-18.375-5.864-25.2-13.736-7.18-7.119-14.246-13.954-21.458-21.087-4.04-3.958-7.935-7.62-11.912-11.405-.083-.122-.328-.288-.451-.52-.814-.79-1.505-1.35-2.333-2.224-5.983-5.888-11.828-11.46-16.3-15.723-9.814 1.528-18.278 2.847-27.163 4.085-3.796-.411-7.172-.742-10.549-1.072-19.389-5.02-33.04-16.713-40.374-35.368-10.582-26.915.226-58.306 24.396-71.556 25.178-13.803 57.44-7.278 74.137 15.112 6.41 8.596 10.107 18.333 10.876 29.054.275 3.834 1.534 6.756 4.436 9.489 21.078 19.857 42.005 39.875 62.945 59.878 5.396 5.154 9.691 10.99 10.72 19.092m-69.483-19.776c.804.663 1.608 1.325 2.566 2.512.287.213.573.427 1.057 1.278 2.907 2.725 5.815 5.45 9.127 8.72 2.455 2.256 4.911 4.512 7.592 7.414 3.66 3.52 7.319 7.042 11.37 11.21 3.465 3.398 7.165 5.256 11.897 2.24 4.23-2.697 5.523-6.184 4.113-12.584-2.101-2.542-3.977-5.32-6.341-7.588-13.16-12.62-26.433-25.122-39.679-37.653-4.967-4.698-9.964-9.365-15.173-14.258-2.053 3.451-3.647 6.132-5.838 9.14l-5.036 5.914c6.051 5.716 11.883 11.225 17.91 17.228.29.192.579.384 1.088 1.179 1.682 1.562 3.364 3.124 5.347 5.248m-43.732-90.58c-10.045-7.61-21.03-9.549-33.713-5.474-1.433.688-2.866 1.375-4.906 2.127-.823.554-1.645 1.107-3.11 1.95-1.039.934-2.077 1.87-3.61 2.887-.303.377-.606.755-1.512 1.472-5.304 5.515-8.8 11.892-9.804 20.37-1.858 24.173 14.898 41.888 38.296 40.312 2.839-.19 5.615-1.305 9.083-1.922.778-.395 1.557-.79 2.999-1.304 4.836-4.424 9.673-8.849 15.047-13.656.408-.997.816-1.995 1.76-3.519 3.31-7.65 3.88-15.5 1.815-24.308-.937-2.443-1.875-4.887-2.895-7.816 0 0-.342-.355-.329-.988-.84-1.146-1.681-2.293-2.834-4.067-1.981-1.82-3.963-3.64-6.287-6.063z\" />\n          <path fill=\"currentColor\" d=\"M142.168 224.923c-.526 1.84-1.433 3.664-1.505 5.52-.188 4.822.056 9.658.009 14.487-.057 5.809-2.836 8.954-8.622 9.652a36.509 36.509 0 0 1-6.98.147c-5.381-.391-8.263-3.162-8.426-8.61-.247-8.319-.117-16.65-.165-24.975-.025-4.483 1.265-8.377 5.64-10.267 4.862-2.1 9.787-1.754 14.345 1.032 1.224.748 2.085 2.088 3.38 3.442.266.285.322.316.323.626.354 2.784.706 5.26 1.107 7.933.332.47.613.742.894 1.013z\" />\n          <path fill=\"currentColor\" d=\"M229.032 137.723c-2.992-.023-5.493.017-7.988-.077-7.696-.289-11.938-4.695-11.957-12.346-.018-7.269 4.268-11.49 11.917-11.63 7.66-.14 15.322-.278 22.983-.27 7.137.006 10.745 4.34 10.699 12.578-.046 8.296-3.206 11.818-10.67 11.827-4.83.006-9.66-.05-14.984-.082z\" />\n          <path fill=\"currentColor\" d=\"M25.01 137.827c-4.32-.054-8.17.15-11.964-.22-6.226-.61-8.5-4.17-8.358-12.357.126-7.24 3.074-11.277 8.997-11.502 8.31-.314 16.64-.423 24.953-.257 6.21.125 10.54 5.497 10.444 12.268-.101 7.245-4.43 11.996-11.095 12.082-4.16.054-8.32-.005-12.976-.014z\" />\n          <path fill=\"currentColor\" d=\"M116.648 11.1c3.823-6.647 16.339-8.611 21.333-3.45 1.458 1.506 2.47 4.046 2.56 6.16.316 7.3.195 14.62.127 21.933-.068 7.247-3.83 11.077-10.982 11.382-8.093.345-12.946-3.286-13.235-10.728-.32-8.287.032-16.599.197-25.298z\" />\n          <path fill=\"currentColor\" d=\"M72.732 206.65c-4.6 4.605-8.826 9.08-13.316 13.273-4.275 3.993-7.4 4.17-11.957.613-2.451-1.914-4.745-4.227-6.512-6.775-3.156-4.553-2.753-8.061 1.071-12.05 4.254-4.435 8.672-8.716 13.059-13.022 6.077-5.966 11.537-6.346 16.865-1.235 6.481 6.217 6.842 12.726.79 19.196z\" />\n          <path fill=\"currentColor\" d=\"M196.987 72.077c-5.68.823-10.224-.669-13.739-4.82-4.215-4.979-4.175-10.527.33-15.182 4.389-4.536 8.932-8.93 13.54-13.243 4.206-3.936 8.573-4.372 12.893-.621 2.793 2.425 5.056 5.692 6.809 8.98.834 1.564.758 4.848-.333 6-6.143 6.484-12.72 12.557-19.5 18.886z\" />\n          <path fill=\"currentColor\" d=\"M77.148 59.041c-.723 5.547-3.386 9.498-7.936 11.834-4.095 2.103-8.397 2.318-12.106-1.054-5.422-4.927-10.722-9.99-16.12-14.944-2.673-2.454-3.005-5.423-.94-8.037 2.857-3.616 5.937-7.273 9.634-9.915 1.78-1.271 6.2-1.4 7.693-.084 6.712 5.92 12.833 12.514 19.07 18.96.584.605.481 1.875.705 3.24z\" />\n        </svg>\n        <div class=\"text-black-800 text-mono\" style=\"letter-spacing: -1px;\">\n          Request Indexing\n        </div>\n      </div>\n    </template>\n\n    <template #right>\n      <div v-if=\"!loggedIn\" class=\"flex gap-3 items-center gap-2\">\n        <UButton to=\"/get-started\" external color=\"gray\" variant=\"link\" class=\"hidden md:block\">\n          <span>Login</span>\n        </UButton>\n        <UButton to=\"/get-started\" external color=\"green\" variant=\"outline\">\n          <span>Get Started</span>\n        </UButton>\n      </div>\n      <template v-else>\n        <div class=\"items-center gap-2 hidden md:flex\">\n          <UDropdown v-if=\"isOnDashboard\" mode=\"click\" :items=\"periodItems\" class=\"mr-5\">\n            <template #info>\n              <p class=\"text-xs\">\n                Change the period used to display your site data.\n              </p>\n            </template>\n            <template #item=\"{ item }\">\n              <template v-if=\"item.value === user.analyticsPeriod\">\n                <span class=\"truncate font-bold\">{{ item.label }}</span>\n                <UIcon v name=\"i-heroicons-check-circle\" class=\"flex-shrink-0 h-5 w-5 text-gray-400 dark:text-gray-500\" />\n              </template>\n              <template v-else>\n                <span class=\"truncate\">{{ item.label }}</span>\n              </template>\n            </template>\n            <UButton\n              icon=\"i-heroicons-calendar\"\n              color=\"gray\"\n              size=\"xs\"\n            >\n              <template v-if=\"user.analyticsPeriod.endsWith('d')\">\n                {{ user.analyticsPeriod.replace('d', '') }} days\n              </template>\n              <template v-else>\n                {{ user.analyticsPeriod.replace('mo', '') }} months\n              </template>\n            </UButton>\n          </UDropdown>\n        </div>\n        <UDropdown mode=\"click\" :items=\"authDropdownItems\" class=\"flex items-center\">\n          <template #account=\"{ item }\">\n            <div class=\"flex flex-col w-full\">\n              <div class=\"flex items-center gap-2\">\n                <UIcon :name=\"item.icon\" class=\"flex-shrink-0 h-4 w-4 text-gray-400 dark:text-gray-500\" />\n                <span class=\"truncate\">{{ item.label }}</span>\n              </div>\n              <div class=\"text-gray-400 text-xs\">\n                {{ user.email }}\n              </div>\n            </div>\n          </template>\n          <template #pro=\"{ item }\">\n            <UIcon :name=\"item.icon\" class=\"flex-shrink-0 h-4 w-4 text-gray-400 dark:text-gray-500\" />\n            <span class=\"truncate\">{{ item.label }}</span>\n            <UBadge label=\"0 left\" color=\"purple\" variant=\"subtle\" class=\"ml-0.5\" />\n          </template>\n          <UAvatar :src=\"user.picture\" />\n          <div class=\"ml-2 flex items-center\">\n            <UBadge v-if=\"user.access === 'pro'\" label=\"Pro\" color=\"purple\" variant=\"subtle\" class=\"ml-0.5\" />\n          </div>\n          <UButton\n            icon=\"i-heroicons-chevron-down\"\n            color=\"gray\"\n            size=\"xs\"\n            variant=\"ghost\"\n          />\n        </UDropdown>\n      </template>\n    </template>\n\n    <template #panel>\n      <UNavigationTree\n        v-if=\"loggedIn\"\n        :links=\"[links[0], ...authDropdownItems.flat()]\" default-open\n      />\n      <UButton v-else to=\"/get-started\" external color=\"green\" variant=\"outline\">\n        <span>Get Started</span>\n      </UButton>\n    </template>\n  </UHeader>\n</template>\n"
  },
  {
    "path": "components/Icon/IconClicks.vue",
    "content": "<template>\n  <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"25\" height=\"25\" viewBox=\"0 0 24 24\" class=\"w-4 h-4 text-blue-500\"><path fill=\"currentColor\" d=\"m11.5 11l6.38 5.37l-.88.18l-.64.12c-.63.13-.99.83-.71 1.4l.27.58l1.36 2.94l-1.42.66l-1.36-2.93l-.26-.58a.985.985 0 0 0-1.52-.36l-.51.4l-.71.57zm-.74-2.31a.76.76 0 0 0-.76.76V20.9c0 .42.34.76.76.76c.19 0 .35-.06.48-.16l1.91-1.55l1.66 3.62c.13.27.4.43.69.43c.11 0 .22 0 .33-.08l2.76-1.28c.38-.18.56-.64.36-1.01L17.28 18l2.41-.45a.88.88 0 0 0 .43-.26c.27-.32.23-.79-.12-1.08l-8.74-7.35l-.01.01a.756.756 0 0 0-.49-.18M15 10V8h5v2zm-1.17-5.24l2.83-2.83l1.41 1.41l-2.83 2.83zM10 0h2v5h-2zM3.93 14.66l2.83-2.83l1.41 1.41l-2.83 2.83zm0-11.32l1.41-1.41l2.83 2.83l-1.41 1.41zM7 10H2V8h5z\" /></svg>\n</template>\n"
  },
  {
    "path": "components/Icon/IconImpressions.vue",
    "content": "<template>\n  <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"0\" height=\"20\" viewBox=\"0 0 32 32\" class=\"w-4 h-4 text-purple-500\"><path fill=\"currentColor\" d=\"M30.94 15.66A16.69 16.69 0 0 0 16 5A16.69 16.69 0 0 0 1.06 15.66a1 1 0 0 0 0 .68A16.69 16.69 0 0 0 16 27a16.69 16.69 0 0 0 14.94-10.66a1 1 0 0 0 0-.68ZM16 25c-5.3 0-10.9-3.93-12.93-9C5.1 10.93 10.7 7 16 7s10.9 3.93 12.93 9C26.9 21.07 21.3 25 16 25Z\" /><path fill=\"currentColor\" d=\"M16 10a6 6 0 1 0 6 6a6 6 0 0 0-6-6Zm0 10a4 4 0 1 1 4-4a4 4 0 0 1-4 4Z\" /></svg>\n</template>\n"
  },
  {
    "path": "components/InspectionResult.vue",
    "content": "<script setup lang=\"ts\">\nimport { useTimeAgo } from '~/composables/formatting'\nimport type { SitePage } from '~/types'\n\nconst props = defineProps<{\n  value: Required<SitePage>\n}>()\n\nconst root = computed(() => {\n  return props.value\n})\n\nconst value = computed(() => {\n  return props.value?.inspectionResult\n})\n</script>\n\n<template>\n  <div v-if=\"value?.indexStatusResult\" class=\"flex items-center gap-2\">\n    <UPopover mode=\"hover\">\n      <template v-if=\"value.indexStatusResult.verdict === 'PASS'\">\n        <UButton :to=\"value.inspectionResultLink\" icon=\"i-heroicons-check-circle\" color=\"green\" variant=\"link\">\n          <UChip color=\"green\" />\n        </UButton>\n      </template>\n      <template v-else-if=\"value.indexStatusResult.verdict === 'NEUTRAL'\">\n        <UButton :to=\"value.inspectionResultLink\" icon=\"i-heroicons-clock\" color=\"gray\" variant=\"link\">\n          <UChip color=\"gray\" />\n        </UButton>\n      </template>\n      <template v-else-if=\"value.indexStatusResult.verdict === 'FAIL'\">\n        <UButton :to=\"value.inspectionResultLink\" icon=\"i-heroicons-x-circle\" color=\"red\" variant=\"link\">\n          <UChip color=\"red\" />\n        </UButton>\n      </template>\n      <template #panel>\n        <div class=\"p-4\">\n          <div>\n            <div class=\"text-gray-800 dark:text-gray-100 font-semibold mb-2\">\n              {{ value.indexStatusResult.coverageState }}\n            </div>\n            <div class=\"flex gap-3 justify-between mb-1\">\n              <div class=\"text-gray-700 dark:text-gray-200\">\n                Verdict\n              </div>\n              <div>{{ value.indexStatusResult.verdict }}</div>\n            </div>\n            <div class=\"flex gap-3 justify-between mb-1\">\n              <div class=\"text-gray-700 dark:text-gray-200\">\n                Robots.txt\n              </div>\n              <div>{{ value.indexStatusResult.robotsTxtState }}</div>\n            </div>\n            <div class=\"flex gap-3 justify-between mb-1\">\n              <div class=\"text-gray-700 dark:text-gray-200\">\n                Indexing\n              </div>\n              <div>{{ value.indexStatusResult.indexingState }}</div>\n            </div>\n            <div class=\"flex gap-3 justify-between mb-1\">\n              <div class=\"text-gray-700 dark:text-gray-200\">\n                Last Crawled\n              </div>\n              <div>{{ useTimeAgo(value.indexStatusResult.lastCrawlTime, true) }}</div>\n            </div>\n          </div>\n          <slot />\n        </div>\n      </template>\n    </UPopover>\n    <div>\n      <div v-if=\"root.lastInspected\" class=\"text-xs mb-1\">\n        Inspected {{ useTimeAgo(root.lastInspected, true) }}\n      </div>\n      <UPopover v-if=\"value.indexStatusResult.verdict === 'NEUTRAL' && !root.urlNotificationMetadata?.latestUpdate\" mode=\"hover\">\n        <div class=\"flex items-center gap-1\">\n          <UIcon name=\"i-heroicons-question-mark-circle\" size=\"w-5 h-5\" />\n          <div class=\"text-xs\">\n            Why am I seeing this?\n          </div>\n        </div>\n        <template #panel>\n          <div class=\"p-4 text-sm space-y-2\">\n            <div>Google knows about your URL but has not indexed it yet.<br>Click the Request Indexing button to move it along.</div>\n          </div>\n        </template>\n      </UPopover>\n      <UPopover v-else-if=\"value.indexStatusResult.verdict === 'NEUTRAL'\" mode=\"hover\">\n        <div class=\"flex items-center gap-1\">\n          <UIcon name=\"i-heroicons-question-mark-circle\" size=\"w-5 h-5\" />\n          <div class=\"text-xs\">\n            Why am I seeing this?\n          </div>\n        </div>\n        <template #panel>\n          <div class=\"p-4 text-sm space-y-2\">\n            <div>You have submitted a Request Index request.<br>We're waiting for Google to process it.</div>\n          </div>\n        </template>\n      </UPopover>\n      <UPopover v-if=\"value.indexStatusResult.verdict === 'PASS'\" mode=\"hover\">\n        <div class=\"flex items-center gap-1\">\n          <UIcon name=\"i-heroicons-question-mark-circle\" size=\"w-5 h-5\" />\n          <div class=\"text-xs\">\n            Why am I seeing this?\n          </div>\n        </div>\n        <template #panel>\n          <div class=\"p-4 text-sm space-y-2\">\n            <div>Google has reported that this URL has been indexed. Congrats! <br>But it doesn't mean people can find it just yet.</div>\n            <div>We'll track this URL here until it has appeared on a<br> search page at least once. Use the \"Hide Actioned\" filter to hide this.</div>\n          </div>\n        </template>\n      </UPopover>\n    </div>\n  </div>\n</template>\n"
  },
  {
    "path": "components/MetricGuage.vue",
    "content": "<script lang=\"ts\" setup>\nconst props = defineProps<{\n  score?: number\n}>()\n\nconst { score } = toRefs(props)\n\nconst arc = ref(null)\n\nconst guageModifiers = computed(() => {\n  let result = 'fail'\n  if (score.value >= 0.9)\n    result = 'pass'\n  else if (score.value >= 0.5)\n    result = 'average'\n\n  return [\n    `guage__wrapper--${result}`,\n  ]\n})\n\nconst guageArcStyle = computed(() => {\n  // r = 56\n  const r = 56\n  // stroke-width = 8\n  const n = 2 * Math.PI * r\n  const rotationOffset = 0.25 * 8 / n\n\n  let o = score.value * n - r / 2\n  if (score.value === 1)\n    o = n\n\n  return {\n    opacity: score.value === 0 ? '0' : 1,\n    transform: `rotate(${360 * rotationOffset - 90}deg)`,\n    strokeDasharray: `${Math.max(o, 0)}, ${n}`,\n  }\n})\n</script>\n\n<template>\n  <div class=\"guage__wrapper guage__wrapper--huge\" :class=\"guageModifiers\">\n    <div\n      class=\"guage__svg-wrapper relative\"\n    >\n      <svg class=\"guage\" viewBox=\"0 0 120 120\">\n        <circle\n          class=\"guage-base\"\n          r=\"56\"\n          cx=\"60\"\n          cy=\"60\"\n          stroke-width=\"8\"\n        />\n        <circle\n          v-if=\"score !== null\"\n          ref=\"arc\"\n          class=\"guage-arc\"\n          r=\"56\"\n          cx=\"60\"\n          cy=\"60\"\n          stroke-width=\"8\"\n          :style=\"guageArcStyle\"\n        />\n      </svg>\n      <div\n        class=\"text-sm mt-[1px] font-bold left-[50%] top-[50%] transform -translate-y-[50%] -translate-x-[50%] absolute text-mono font-mono\"\n      >\n        <slot />\n      </div>\n    </div>\n  </div>\n</template>\n\n<style scoped>\n* {\n  --color-amber-50: #fff8e1;\n  --color-blue-200: #90caf9;\n  --color-blue-900: #0d47a1;\n  --color-blue-A700: #2962ff;\n  --color-cyan-500: #00bcd4;\n  --color-gray-100: #f5f5f5;\n  --color-gray-300: #cfcfcf;\n  --color-gray-200: #e0e0e0;\n  --color-gray-400: #bdbdbd;\n  --color-gray-50: #fafafa;\n  --color-gray-500: #9e9e9e;\n  --color-gray-600: #757575;\n  --color-gray-700: #616161;\n  --color-gray-800: #424242;\n  --color-gray-900: #212121;\n  --color-gray: #000000;\n  --color-green-700: #018642;\n  --color-green: #0cce6b;\n  --color-lime-400: #d3e156;\n  --color-orange-50: #fff3e0;\n  --color-orange-700: #d04900;\n  --color-orange: #ffa400;\n  --color-red-700: #eb0f00;\n  --color-red: #ff4e42;\n  --color-teal-600: #00897b;\n  --color-white: #ffffff;\n  --color-average-secondary: var(--color-orange-700);\n  --color-average: var(--color-orange);\n  --color-fail-secondary: var(--color-red-700);\n  --color-fail: var(--color-red);\n  --color-hover: var(--color-gray-50);\n  --color-informative: var(--color-blue-900);\n  --color-pass-secondary: var(--color-green-700);\n  --color-pass: var(--color-green);\n  --color-not-applicable: var(--color-gray-600);\n}\n.guage__wrapper--pass {\n  @apply text-green-500 fill-current stroke-current;\n}\n.guage__wrapper--average {\n  @apply  text-yellow-500 fill-current stroke-current;\n}\n\n.guage__wrapper--fail {\n  @apply text-red-500 fill-current stroke-current;\n}\n\n.guage__wrapper--not-applicable {\n  color: var(--color-not-applicable);\n  fill: var(--color-not-applicable);\n  stroke: var(--color-not-applicable);\n}\n.guage__wrapper--huge {\n  --gauge-circle-size: 50px;\n}\n.guage__wrapper {\n  position: relative;\n  display: flex;\n  align-items: center;\n  flex-direction: column;\n  text-decoration: none;\n  padding: var(--score-container-padding);\n  --transition-length: 1s;\n  contain: content;\n  will-change: opacity;\n}\n.guage__svg-wrapper {\n  position: relative;\n  height: var(--gauge-circle-size);\n}\n.guage {\n  stroke-linecap: round;\n  width: var(--gauge-circle-size);\n  height: var(--gauge-circle-size);\n}\n.guage-base {\n  opacity: 0.1;\n}\n.guage-arc {\n  fill: none;\n  transform-origin: 50% 50%;\n  animation: load-gauge var(--transition-length) ease forwards;\n  animation-delay: 250ms;\n}\n</style>\n"
  },
  {
    "path": "components/OgImage/Home.vue",
    "content": "<script setup lang=\"ts\">\nimport { computed } from 'vue'\n\n// convert to typescript props\nconst props = withDefaults(defineProps<{\n  colorMode?: 'dark' | 'light'\n  title?: string\n  description?: string\n  icon?: string | boolean\n  version?: string\n  siteName?: string\n  siteLogo?: string\n  theme?: string\n}>(), {\n  colorMode: 'light',\n  theme: '#00dc82',\n  title: 'title',\n})\n\nconst HexRegex = /^#([0-9a-f]{3}){1,2}$/i\n\nconst themeHex = computed(() => {\n  // regex test if valid hex\n  if (HexRegex.test(props.theme))\n    return props.theme\n\n  // if it's hex without the hash, just add the hash\n  if (HexRegex.test(`#${props.theme}`))\n    return `#${props.theme}`\n\n  // if it's rgb or rgba, we convert it to hex\n  if (props.theme.startsWith('rgb')) {\n    const rgb = props.theme\n      .replace('rgb(', '')\n      .replace('rgba(', '')\n      .replace(')', '')\n      .split(',')\n      .map(v => Number.parseInt(v.trim(), 10))\n    const hex = rgb\n      .map((v) => {\n        const hex = v.toString(16)\n        return hex.length === 1 ? `0${hex}` : hex\n      })\n      .join('')\n    return `#${hex}`\n  }\n  return '#FFFFFF'\n})\n\nconst themeRgb = computed(() => {\n  // we want to convert it so it's just `<red>, <green>, <blue>` (255, 255, 255)\n  return themeHex.value\n    .replace('#', '')\n    .match(/.{1,2}/g)\n    ?.map(v => Number.parseInt(v, 16))\n    .join(', ')\n})\n</script>\n\n<template>\n  <div\n    class=\"w-full h-full flex flex-col justify-center items-center relative p-[60px]\"\n    :class=\"[\n      colorMode === 'light' ? ['bg-white', 'text-gray-900'] : ['bg-gray-900', 'text-gray-50'],\n    ]\"\n  >\n    <div\n      class=\"flex absolute bottom-[-200%] right-[-50%]\" :style=\"{\n        width: '200%',\n        height: '250%',\n        backgroundImage: `radial-gradient(circle, rgba(${themeRgb}, 0.5) 0%,  ${colorMode === 'dark' ? 'rgba(5, 5, 5,0.3)' : 'rgba(255, 255, 255, 0.7)'} 50%, ${props.colorMode === 'dark' ? 'rgba(5, 5, 5,0)' : 'rgba(255, 255, 255, 0)'} 70%)`,\n      }\"\n    />\n    <div class=\"flex flex-row justify-center items-center text-left w-full mb-12\">\n      <svg height=\"100\" width=\"100\" class=\"text-green-500\" xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 267 262\" xml:space=\"preserve\">\n        <path class=\"text-gray-900 dark:text-gray-200\" fill=\"currentColor\" d=\"M263.158 210.588a356.834 356.834 0 0 1-4.262 20.272c-6.823 10.235-16.074 16.193-27.508 15.709-9.868-.419-18.375-5.864-25.2-13.736-7.18-7.119-14.246-13.954-21.458-21.087-4.04-3.958-7.935-7.62-11.912-11.405-.083-.122-.328-.288-.451-.52-.814-.79-1.505-1.35-2.333-2.224-5.983-5.888-11.828-11.46-16.3-15.723-9.814 1.528-18.278 2.847-27.163 4.085-3.796-.411-7.172-.742-10.549-1.072-19.389-5.02-33.04-16.713-40.374-35.368-10.582-26.915.226-58.306 24.396-71.556 25.178-13.803 57.44-7.278 74.137 15.112 6.41 8.596 10.107 18.333 10.876 29.054.275 3.834 1.534 6.756 4.436 9.489 21.078 19.857 42.005 39.875 62.945 59.878 5.396 5.154 9.691 10.99 10.72 19.092m-69.483-19.776c.804.663 1.608 1.325 2.566 2.512.287.213.573.427 1.057 1.278 2.907 2.725 5.815 5.45 9.127 8.72 2.455 2.256 4.911 4.512 7.592 7.414 3.66 3.52 7.319 7.042 11.37 11.21 3.465 3.398 7.165 5.256 11.897 2.24 4.23-2.697 5.523-6.184 4.113-12.584-2.101-2.542-3.977-5.32-6.341-7.588-13.16-12.62-26.433-25.122-39.679-37.653-4.967-4.698-9.964-9.365-15.173-14.258-2.053 3.451-3.647 6.132-5.838 9.14l-5.036 5.914c6.051 5.716 11.883 11.225 17.91 17.228.29.192.579.384 1.088 1.179 1.682 1.562 3.364 3.124 5.347 5.248m-43.732-90.58c-10.045-7.61-21.03-9.549-33.713-5.474-1.433.688-2.866 1.375-4.906 2.127-.823.554-1.645 1.107-3.11 1.95-1.039.934-2.077 1.87-3.61 2.887-.303.377-.606.755-1.512 1.472-5.304 5.515-8.8 11.892-9.804 20.37-1.858 24.173 14.898 41.888 38.296 40.312 2.839-.19 5.615-1.305 9.083-1.922.778-.395 1.557-.79 2.999-1.304 4.836-4.424 9.673-8.849 15.047-13.656.408-.997.816-1.995 1.76-3.519 3.31-7.65 3.88-15.5 1.815-24.308-.937-2.443-1.875-4.887-2.895-7.816 0 0-.342-.355-.329-.988-.84-1.146-1.681-2.293-2.834-4.067-1.981-1.82-3.963-3.64-6.287-6.063z\" />\n        <path fill=\"currentColor\" d=\"M142.168 224.923c-.526 1.84-1.433 3.664-1.505 5.52-.188 4.822.056 9.658.009 14.487-.057 5.809-2.836 8.954-8.622 9.652a36.509 36.509 0 0 1-6.98.147c-5.381-.391-8.263-3.162-8.426-8.61-.247-8.319-.117-16.65-.165-24.975-.025-4.483 1.265-8.377 5.64-10.267 4.862-2.1 9.787-1.754 14.345 1.032 1.224.748 2.085 2.088 3.38 3.442.266.285.322.316.323.626.354 2.784.706 5.26 1.107 7.933.332.47.613.742.894 1.013z\" />\n        <path fill=\"currentColor\" d=\"M229.032 137.723c-2.992-.023-5.493.017-7.988-.077-7.696-.289-11.938-4.695-11.957-12.346-.018-7.269 4.268-11.49 11.917-11.63 7.66-.14 15.322-.278 22.983-.27 7.137.006 10.745 4.34 10.699 12.578-.046 8.296-3.206 11.818-10.67 11.827-4.83.006-9.66-.05-14.984-.082z\" />\n        <path fill=\"currentColor\" d=\"M25.01 137.827c-4.32-.054-8.17.15-11.964-.22-6.226-.61-8.5-4.17-8.358-12.357.126-7.24 3.074-11.277 8.997-11.502 8.31-.314 16.64-.423 24.953-.257 6.21.125 10.54 5.497 10.444 12.268-.101 7.245-4.43 11.996-11.095 12.082-4.16.054-8.32-.005-12.976-.014z\" />\n        <path fill=\"currentColor\" d=\"M116.648 11.1c3.823-6.647 16.339-8.611 21.333-3.45 1.458 1.506 2.47 4.046 2.56 6.16.316 7.3.195 14.62.127 21.933-.068 7.247-3.83 11.077-10.982 11.382-8.093.345-12.946-3.286-13.235-10.728-.32-8.287.032-16.599.197-25.298z\" />\n        <path fill=\"currentColor\" d=\"M72.732 206.65c-4.6 4.605-8.826 9.08-13.316 13.273-4.275 3.993-7.4 4.17-11.957.613-2.451-1.914-4.745-4.227-6.512-6.775-3.156-4.553-2.753-8.061 1.071-12.05 4.254-4.435 8.672-8.716 13.059-13.022 6.077-5.966 11.537-6.346 16.865-1.235 6.481 6.217 6.842 12.726.79 19.196z\" />\n        <path fill=\"currentColor\" d=\"M196.987 72.077c-5.68.823-10.224-.669-13.739-4.82-4.215-4.979-4.175-10.527.33-15.182 4.389-4.536 8.932-8.93 13.54-13.243 4.206-3.936 8.573-4.372 12.893-.621 2.793 2.425 5.056 5.692 6.809 8.98.834 1.564.758 4.848-.333 6-6.143 6.484-12.72 12.557-19.5 18.886z\" />\n        <path fill=\"currentColor\" d=\"M77.148 59.041c-.723 5.547-3.386 9.498-7.936 11.834-4.095 2.103-8.397 2.318-12.106-1.054-5.422-4.927-10.722-9.99-16.12-14.944-2.673-2.454-3.005-5.423-.94-8.037 2.857-3.616 5.937-7.273 9.634-9.915 1.78-1.271 6.2-1.4 7.693-.084 6.712 5.92 12.833 12.514 19.07 18.96.584.605.481 1.875.705 3.24z\" />\n      </svg>\n      <div class=\"text-black-800 text-5xl text-mono font-bold ml-10\" style=\"letter-spacing: -2px;\">\n        Request Indexing\n      </div>\n    </div>\n    <div class=\"flex flex-row w-full justify-around items-center relative\">\n      <div class=\"w-1/2\">\n        <div class=\"text-4xl\" style=\"line-height: 1.6\">\n          <p>A free, open-source tool to get your pages indexed on Google within 48 hours.</p>\n        </div>\n      </div>\n      <div>\n        <img src=\"/card.png\" style=\"width: 400px; height: 298px;\" class=\"rounded-xl shadow-xl border-1 border-gray-50\">\n      </div>\n    </div>\n  </div>\n</template>\n"
  },
  {
    "path": "components/PositionMetric.vue",
    "content": "<script lang=\"ts\" setup>\nconst props = defineProps<{ value: number }>()\n// we're showing the Position metric from Google Search Console, we want to\n// alter the colour depending on the position of the keyword\nconst color = computed(() => {\n  if (props.value < 10)\n    return 'green'\n  if (props.value < 20)\n    return 'amber'\n  if (props.value < 30)\n    return 'orange'\n  return 'red'\n})\n// we should show the value without decimals\nconst formattedValue = computed(() => Math.round(props.value))\n\n// it should be shown in a badge with minimal padding\n</script>\n\n<template>\n  <UTooltip :text=\"`Average position of ${useHumanFriendlyNumber(value)}.`\">\n    <UBadge :color=\"color\" variant=\"soft\" class=\"px-2 py-1\">\n      {{ formattedValue }}\n    </UBadge>\n  </UTooltip>\n</template>\n"
  },
  {
    "path": "components/SiteCard.vue",
    "content": "<script lang=\"ts\" setup>\nimport type { Ref } from 'vue'\nimport { toRef } from 'vue'\nimport { useFriendlySiteUrl } from '~/composables/formatting'\nimport type { GoogleSearchConsoleSite, SiteExpanded } from '~/types'\nimport { fetchSite } from '~/composables/fetch'\n\nconst props = defineProps<{\n  site: GoogleSearchConsoleSite\n  mockData?: SiteExpanded\n}>()\n\nconst { site } = toRefs(props)\n\nconst { session, user } = useUserSession()\n\nconst { data: _data, pending: _pending, forceRefresh, error } = await fetchSite(site.value, !!props.mockData)\n\nconst isForceRefreshing = ref(false)\nconst pending = computed(() => {\n  if (props.mockData)\n    return false\n  return toValue(_pending) || toValue(isForceRefreshing)\n})\nconst data = toRef(props.mockData || _data) as Ref<SiteExpanded | null>\n\nconst siteUrlFriendly = computed(() => {\n  return useFriendlySiteUrl(site.value.siteUrl)\n})\nconst link = computed(() => `/dashboard/site/${encodeURIComponent(site.value.siteUrl)}`)\n\nfunction refresh() {\n  callFnSyncToggleRef(forceRefresh, isForceRefreshing)\n}\n\nasync function hide() {\n  const sites = new Set([...(session.value.user.hiddenSites || []), props.site.siteUrl])\n  // save it upstream\n  session.value = await $fetch('/api/user/me', {\n    method: 'POST',\n    body: JSON.stringify({ hiddenSites: [...sites] }),\n  })\n}\n</script>\n\n<template>\n  <UCard class=\"min-h-[270px] flex flex-col\" :ui=\"{ body: { base: 'flex-grow flex items-center', padding: ' px-0 py-0 sm:p-0' } }\">\n    <template #header>\n      <div class=\"flex justify-between\">\n        <NuxtLink :to=\"link\" class=\"flex items-center gap-2\">\n          <img :src=\"`https://www.google.com/s2/favicons?domain=${site.siteUrl.replace('sc-domain:', 'https://')}`\" alt=\"favicon\" class=\"w-4 h-4\">\n          <h3 class=\"font-bold\">\n            {{ siteUrlFriendly }}\n          </h3>\n        </NuxtLink>\n        <UDropdown :class=\"mockData ? 'pointer-events-none' : ''\" :items=\"[[{ label: 'View', icon: 'i-heroicons-eye', click: () => link && navigateTo(link) }, { label: 'Reload', icon: 'i-heroicons-arrow-path', click: refresh }, { label: 'Hide', icon: 'i-heroicons-trash', click: hide }]]\" :popper=\"{ offsetDistance: 0, placement: 'right-start' }\">\n          <UButton color=\"white\" label=\"\" variant=\"ghost\" trailing-icon=\"i-heroicons-chevron-down\" />\n        </UDropdown>\n      </div>\n    </template>\n    <div class=\"flex-grow w-full h-full\">\n      <div v-if=\"pending\" class=\"w-full h-full flex items-center justify-center\">\n        <UIcon name=\"i-heroicons-arrow-path\" class=\"animate-spin w-12 h-12\" />\n      </div>\n      <div v-else-if=\"error\" class=\"w-full h-full flex items-center justify-center\">\n        <div>\n          <div class=\"mb-3\">\n            <div class=\"text-lg font-semibold\">\n              <UIcon name=\"i-heroicons-exclamation-triangle\" class=\"w-4 h-4\" /> Failed to refresh\n            </div>\n            <div class=\"opacity-80 text-sm\">\n              {{ error.statusMessage }}\n            </div>\n          </div>\n          <UButton size=\"xs\" color=\"gray\" @click=\"refresh\">\n            Refresh\n          </UButton>\n        </div>\n      </div>\n      <div v-else-if=\"data\" class=\"relative w-full h-full\">\n        <div class=\"flex px-5 pt-2 items-start justify-start gap-4\">\n          <div class=\"flex items-center justify-center gap-2\">\n            <div class=\"flex items-center flex-col justify-center\">\n              <div class=\"text-2xl font-semibold\">\n                <template v-if=\"data.nonIndexedPercent && data.nonIndexedPercent !== -1\">\n                  {{ Math.round(data.nonIndexedPercent * 100) }}<span class=\"text-xl\">%</span>\n                </template>\n                <template v-else>\n                  ?\n                </template>\n              </div>\n              <div class=\"opacity-80 text-sm\">\n                Indexed\n              </div>\n            </div>\n          </div>\n          <div class=\"flex flex-col items-center justify-center gap-1\">\n            <div class=\"flex items-center justify-center gap-2\">\n              <TrendPercentage :value=\"data.analytics.period.totalClicks\" :prev-value=\"data.analytics.prevPeriod.totalClicks\" />\n              <UTooltip :text=\"`Clicks last ${user?.analyticsPeriod || '28d'}`\" class=\"flex items-end gap-2\">\n                <span>{{ useHumanFriendlyNumber(data!.analytics.period.totalClicks) }}</span>\n                <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"32\" height=\"32\" viewBox=\"0 0 24 24\" class=\"w-5 h-5 text-blue-300\"><path fill=\"currentColor\" d=\"m11.5 11l6.38 5.37l-.88.18l-.64.12c-.63.13-.99.83-.71 1.4l.27.58l1.36 2.94l-1.42.66l-1.36-2.93l-.26-.58a.985.985 0 0 0-1.52-.36l-.51.4l-.71.57zm-.74-2.31a.76.76 0 0 0-.76.76V20.9c0 .42.34.76.76.76c.19 0 .35-.06.48-.16l1.91-1.55l1.66 3.62c.13.27.4.43.69.43c.11 0 .22 0 .33-.08l2.76-1.28c.38-.18.56-.64.36-1.01L17.28 18l2.41-.45a.88.88 0 0 0 .43-.26c.27-.32.23-.79-.12-1.08l-8.74-7.35l-.01.01a.756.756 0 0 0-.49-.18M15 10V8h5v2zm-1.17-5.24l2.83-2.83l1.41 1.41l-2.83 2.83zM10 0h2v5h-2zM3.93 14.66l2.83-2.83l1.41 1.41l-2.83 2.83zm0-11.32l1.41-1.41l2.83 2.83l-1.41 1.41zM7 10H2V8h5z\" /></svg>\n              </UTooltip>\n            </div>\n            <div class=\"flex items-center justify-center gap-2\">\n              <TrendPercentage :value=\"data.analytics.period.totalImpressions\" :prev-value=\"data.analytics.prevPeriod.totalImpressions\" />\n              <UTooltip :text=\"`Impressions last ${user?.analyticsPeriod || '28d'}`\" class=\"flex items-end gap-2\">\n                <span>{{ useHumanFriendlyNumber(data!.analytics.period.totalImpressions) }}</span>\n                <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"0\" height=\"24\" viewBox=\"0 0 32 32\" class=\"w-5 h-5 text-purple-300\"><path fill=\"currentColor\" d=\"M30.94 15.66A16.69 16.69 0 0 0 16 5A16.69 16.69 0 0 0 1.06 15.66a1 1 0 0 0 0 .68A16.69 16.69 0 0 0 16 27a16.69 16.69 0 0 0 14.94-10.66a1 1 0 0 0 0-.68ZM16 25c-5.3 0-10.9-3.93-12.93-9C5.1 10.93 10.7 7 16 7s10.9 3.93 12.93 9C26.9 21.07 21.3 25 16 25Z\" /><path fill=\"currentColor\" d=\"M16 10a6 6 0 1 0 6 6a6 6 0 0 0-6-6Zm0 10a4 4 0 1 1 4-4a4 4 0 0 1-4 4Z\" /></svg>\n              </UTooltip>\n            </div>\n          </div>\n        </div>\n        <div class=\"h-[100px] max-w-full overflow-hidden\">\n          <GraphClicks v-if=\"!pending && data.graph\" :value=\"data.graph.map(g => ({ time: g.time, value: g.clicks }))\" :value2=\"data.graph.map(g => ({ time: g.time, value: g.impressions }))\" />\n        </div>\n      </div>\n    </div>\n    <template #footer>\n      <div class=\"flex items-center justify-between\">\n        <div class=\"flex gap-2 items-center \">\n          <UButton :class=\"mockData ? 'pointer-events-none' : ''\" :disabled=\"pending\" variant=\"ghost\" color=\"gray\" :to=\"link\">\n            View\n          </UButton>\n        </div>\n\n        <div class=\"flex items-center gap-2\">\n          <div class=\"opacity-60 text-xs\">\n            <template v-if=\"site.siteUrl.includes('sc-domain:')\">\n              Domain Property\n            </template>\n            <template v-else>\n              Site Property\n            </template>\n          </div>\n          <UTooltip v-if=\"!mockData && site.permissionLevel !== 'siteOwner'\" :text=\"`'${site.permissionLevel}' is unable to submit URLs for indexing`\">\n            <UIcon name=\"i-heroicons-exclamation-triangle\" class=\"w-5 h-5 text-yellow-500\" />\n          </UTooltip>\n          <UTooltip v-else text=\"You can submit URLs for indexing for this site.\">\n            <UIcon name=\"i-heroicons-check-circle\" class=\"w-5 h-5 text-green-500\" />\n          </UTooltip>\n        </div>\n      </div>\n    </template>\n  </UCard>\n</template>\n"
  },
  {
    "path": "components/Table/TableData.vue",
    "content": "<script lang=\"ts\" setup generic=\"T extends Record<string, any>\">\nimport { useUrlSearchParams } from '@vueuse/core'\nimport { get } from '#ui/utils'\n\nconst props = defineProps<{\n  value: T[]\n  columns: any[]\n  filters?: { key: string, label: string, filter: (rows: T[]) => T[] }[]\n  expandable?: boolean\n  pageCount?: number\n}>()\n\nconst emit = defineEmits<{\n  'update:expanded': [id: number]\n  'update:rows': [rows: T[]]\n}>()\n\nconst params = useUrlSearchParams('history', {\n  removeNullishValues: true,\n  removeFalsyValues: false,\n})\n\nconst sort = ref()\nconst q = ref(params.q || '')\nconst page = ref(params.page || 1)\nconst filter = ref(params.filter || 'default')\nconst rows = computed<T>(() => props.value || [])\nconst expandedRow = ref(null)\n\nfunction toggleExpandedRow(index: number) {\n  if (expandedRow.value === index)\n    expandedRow.value = null\n  else\n    expandedRow.value = index\n}\n\nwatch([q, filter, page, sort], () => {\n  expandedRow.value = null\n})\n\n// order: sort, query, paginate\n\nfunction defaultSort(a, b, direction) {\n  if (a === b)\n    return 0\n\n  if (direction === 'asc')\n    return a < b ? -1 : 1\n  else\n    return a > b ? -1 : 1\n}\n\nconst sortedRows = computed(() => {\n  if (!sort.value)\n    return rows.value\n  const { column, direction } = sort.value\n  return rows.value.slice().sort((a, b) => {\n    const aValue = get(a, column)\n    const bValue = get(b, column)\n    return defaultSort(aValue, bValue, direction)\n  })\n})\n\nconst filters = computed(() => {\n  return [\n    {\n      key: 'default',\n      label: 'Show all',\n      description: 'Show all results',\n      filter: (rows: T[]) => {\n        return rows\n      },\n    },\n    ...props.filters || [],\n  ].filter(Boolean)\n})\n\nconst queriedRows = computed<T[]>(() => {\n  const queried = q.value\n    ? sortedRows.value.filter((row) => {\n      return Object.values(row).some((value) => {\n        return String(value).toLowerCase().includes(q.value.toLowerCase())\n      })\n    })\n    : sortedRows.value\n  const applyFilter = filters.value.find(f => f.key === filter.value)\n  if (applyFilter)\n    return applyFilter.filter(queried)\n  return queried\n})\n\nfunction toggleFilter(_filter: string) {\n  if (filter.value === _filter)\n    filter.value = null\n  else\n    filter.value = _filter\n}\nconst pageCount = props.pageCount || 8\nconst paginatedRows = computed<T[]>(() => {\n  return queriedRows.value.slice((page.value - 1) * pageCount, (page.value) * pageCount)\n})\n\nfunction updateSort(_sort: any) {\n  if (_sort.column === null) {\n    sort.value = null\n    return\n  }\n  sort.value = {\n    column: _sort.column,\n    direction: _sort.direction,\n  }\n}\n\nconst columns = computed(() => {\n  return [props.expandable ? { key: 'expand' } : null, ...props.columns].filter(Boolean).map(c => ({\n    ...c,\n    slotName: `${c.key}-data`,\n  }))\n})\n\nwatch(expandedRow, () => {\n  emit('update:expanded', expandedRow.value ? paginatedRows.value[expandedRow.value] : null)\n})\n\nwatch(paginatedRows, () => {\n  emit('update:rows', paginatedRows.value)\n})\n</script>\n\n<template>\n  <div>\n    <div class=\"flex justify-between\">\n      <div class=\"flex items-center gap-5 mb-5\">\n        <div class=\"flex w-[300px] dark:border-gray-700\">\n          <UInput\n            v-model=\"q\"\n            class=\"w-full\"\n            placeholder=\"Search...\"\n            icon=\"i-heroicons-magnifying-glass\"\n            autocomplete=\"off\"\n            :ui=\"{ icon: { trailing: { pointer: '' } } }\"\n          >\n            <template #trailing>\n              <UButton\n                v-show=\"q !== ''\"\n                color=\"gray\"\n                variant=\"link\"\n                icon=\"i-heroicons-x-mark\"\n                :padded=\"false\"\n                @click=\"q = ''\"\n              />\n            </template>\n          </UInput>\n        </div>\n      </div>\n      <div>\n        <div class=\"flex items-center gap-3 mb-3\">\n          <UBadge v-for=\"_filter in filters.filter(f => !f.special)\" :key=\"_filter.key\" class=\"cursor-pointer\" :ui=\"{ rounded: 'rounded-full' }\" :color=\"filter === _filter.key ? 'green' : 'gray'\" :variant=\"filter === _filter.key ? 'subtle' : 'soft'\" @click=\"toggleFilter(_filter.key)\">\n            <UTooltip :text=\"_filter.description || ''\" class=\"flex gap-1 items-center\">\n              {{ _filter.label }} <span v-if=\"_filter.key === filter\"> - {{ queriedRows.length }}</span>\n            </UTooltip>\n          </UBadge>\n        </div>\n        <div>\n          <UBadge v-for=\"_filter in filters.filter(f => f.special)\" :key=\"_filter.key\" class=\"cursor-pointer\" :ui=\"{ rounded: 'rounded-full' }\" :color=\"filter === _filter.key ? 'green' : 'gray'\" :variant=\"filter === _filter.key ? 'subtle' : 'soft'\" @click=\"toggleFilter(_filter.key)\">\n            <UTooltip :text=\"_filter.description || ''\" :ui=\"{ width: 'max-w-lg' }\" class=\"flex gap-1 items-center\">\n              <UIcon name=\"i-heroicons-sparkles\" class=\"w-4 h-4\" />\n              {{ _filter.label }} <span v-if=\"_filter.key === filter\"> - {{ queriedRows.length }}</span>\n            </UTooltip>\n          </UBadge>\n        </div>\n      </div>\n    </div>\n    <UDivider />\n    <UTable :loading=\"!value\" :rows=\"paginatedRows\" :columns=\"columns\" @update:sort=\"updateSort\">\n      <template #expand-data=\"{ index }\">\n        <UButton :icon=\"expandedRow === index ? 'i-heroicons-chevron-up' : 'i-heroicons-chevron-down'\" color=\"gray\" size=\"xs\" variant=\"ghost\" @click=\"toggleExpandedRow(index)\" />\n      </template>\n      <!-- we need to re-implement the slots i think  -->\n      <template v-for=\"column in columns.filter(c => c.key !== 'expand')\" #[column.slotName]=\"data\">\n        <slot :name=\"column.slotName\" v-bind=\"data\" :rows=\"paginatedRows\" :expanded=\"expandedRow === data.index\" />\n      </template>\n    </UTable>\n    <div v-if=\"queriedRows.length > pageCount\" class=\"flex items-center justify-between mt-7 px-3 py-5 border-t border-gray-200 dark:border-gray-700\">\n      <UPagination v-model=\"page\" :page-count=\"pageCount\" :total=\"queriedRows.length\" />\n      <div class=\"text-base dark:text-gray-300 text-gray-600 mb-2\">\n        {{ queriedRows.length }} total\n      </div>\n    </div>\n  </div>\n</template>\n"
  },
  {
    "path": "components/Table/TableKeywords.vue",
    "content": "<script setup lang=\"ts\">\nimport Fuse from 'fuse.js'\nimport type { GoogleSearchConsoleSite } from '~/types/data'\n\nconst props = withDefaults(\n  defineProps<{ mock?: boolean, value?: GscDataRow[], site: GoogleSearchConsoleSite, pending?: boolean, pageCount?: number }>(),\n  {\n    pageCount: 8,\n  },\n)\n\nconst { user } = useUserSession()\n\nconst columns = computed(() => {\n  return [{\n    key: 'keyword',\n    label: 'Keyword',\n    sortable: true,\n  }, {\n    key: 'position',\n    label: 'Position',\n    sortable: true,\n  }, user.value?.analyticsPeriod === 'all'\n    ? null\n    : {\n        key: 'positionPercent',\n        label: '%',\n        sortable: true,\n      }, {\n    key: 'ctr',\n    label: 'CTR',\n    sortable: true,\n  }, user.value?.analyticsPeriod === 'all'\n    ? null\n    : {\n        key: 'ctrPercent',\n        label: '%',\n        sortable: true,\n      }].filter(Boolean)\n})\n\nfunction highestRowClickCount(rows) {\n  return rows.reduce((acc, row) => acc + row.clicks, 0)\n}\n//\n// const selected = ref([])\n// function select(row) {\n//   const index = selected.value.findIndex(item => item.url === row.url)\n//   if (index === -1)\n//     selected.value.push(row)\n//   else\n//     selected.value.splice(index, 1)\n// }\n\nconst filters = computed(() => {\n  return [\n    {\n      key: 'new',\n      label: 'New',\n      description: 'Keywords that are new verse the previous period.',\n      filter: (rows: T[]) => {\n        return rows.filter(row => !row.prevPosition)\n      },\n    },\n    {\n      key: 'improving',\n      description: 'Keywords that are improving verse the previous period.',\n      label: 'Improving',\n      filter: (rows: T[]) => {\n        return rows.filter(row => row.position > row.prevPosition)\n      },\n    },\n    {\n      key: 'declining',\n      description: 'Keywords that are declining verse the previous period.',\n      label: 'Declining',\n      filter: (rows: T[]) => {\n        return rows.filter(row => row.position < row.prevPosition)\n      },\n    },\n    {\n      key: 'first-page',\n      label: 'First Page',\n      description: 'Keywords that are on the first page of Google search',\n      special: true,\n      filter: (rows: GscDataRow[]) => rows.filter(row => row.impressions > 5 && row.position <= 12 && row.position >= 0),\n    },\n    {\n      key: 'content-gap',\n      label: 'Content Gap',\n      description: 'Keywords that are not on the first page but have high impressions',\n      special: true,\n      filter: (rows: GscDataRow[]) => {\n        // compute avg. impressions and avg ctr\n        const avgImpressions = rows.filter(row => row.impressions >= 0).reduce((acc, row) => acc + (row.impressions || 0), 0) / rows.length\n        const avgCtr = rows.filter(row => row.ctr >= 0).reduce((acc, row) => acc + (row.ctr || 0), 0) / rows.length\n        const thresholdImpressions = Math.max(avgImpressions * 0.5, 50)\n        const thresholdCTR = avgCtr * 0.5\n        return rows.filter(row => row.impressions > thresholdImpressions && row.ctr < thresholdCTR)\n          .sort((a, b) => b.impressions - a.impressions)\n      },\n    },\n    {\n      key: 'indirect',\n      label: 'Indirect',\n      description: 'Keywords with impressions that don\\'t include the site name',\n      special: true,\n      filter: (rows: GscDataRow[]) => {\n        // only with 1 clicks\n        const trafficRows = rows.filter(row => row.impressions >= 10)\n        // use fuse.js, add all data, search for the siteurl and see what' not returned\n        const fuse = new Fuse(trafficRows, {\n          keys: ['keyword'],\n          includeScore: true,\n          threshold: 0.5,\n        })\n        const siteName = props.site.siteUrl.replace('www.', '').replace('https://', '').replace('sc-domain:', '').split('.')[0]\n        const results = fuse.search(siteName)\n        // do a diff of results on rows, we only want the ones that are not returned\n        return trafficRows.filter(row => !results.find(result => result.item.keyword === row.keyword))\n      },\n    },\n  ]\n})\n</script>\n\n<template>\n  <div>\n    <TableData :value=\"value\" :columns=\"columns\" :filters=\"filters\" :page-count=\"pageCount\">\n      <template #keyword-data=\"{ row, rows }\">\n        <div class=\"flex items-center\">\n          <div class=\"relative group w-[225px] truncate text-ellipsis\">\n            <div class=\"flex items-center\">\n              <UButton class=\"max-w-[185px] block\" variant=\"link\" size=\"xs\" :class=\"mock ? ['pointer-events-none'] : []\" color=\"gray\" @click=\"q = row.keyword\">\n                <div class=\"text-black dark:text-white max-w-[185px] truncate text-ellipsis\">\n                  {{ row.keyword }}\n                </div>\n              </UButton>\n              <UBadge v-if=\"!row.prevPosition\" size=\"xs\" variant=\"subtle\">\n                New\n              </UBadge>\n              <UBadge v-else-if=\"row.lost\" size=\"xs\" color=\"red\" variant=\"subtle\">\n                Lost\n              </UBadge>\n            </div>\n            <UProgress :value=\"Math.round((row.clicks / highestRowClickCount(rows)) * 100)\" color=\"blue\" size=\"xs\" class=\"ml-2 opacity-75 group-hover:opacity-100 transition\" />\n          </div>\n        </div>\n      </template>\n\n      <template #position-data=\"{ row }\">\n        <div class=\"text-center\">\n          <UDivider v-if=\"row.lostKeyword\" />\n          <template v-else>\n            <div>\n              <PositionMetric :value=\"row.position\" />\n            </div>\n            <UTooltip :text=\"`${row.impressions} impressions`\">\n              <div class=\"text-xs flex items-center gap-1\">\n                {{ useHumanFriendlyNumber(row.impressions) }}\n                <IconImpressions />\n              </div>\n            </UTooltip>\n          </template>\n        </div>\n      </template>\n      <template #prevPosition-data=\"{ row }\">\n        <div class=\"text-center\">\n          <div>\n            {{ useHumanFriendlyNumber(row.prevPosition, 1) }}\n          </div>\n          <UTooltip :text=\"`${row.prevImpressions} impressions`\">\n            <div class=\"text-xs\">\n              {{ useHumanFriendlyNumber(row.prevImpressions) }} impressions\n            </div>\n          </UTooltip>\n        </div>\n      </template>\n      <template #positionPercent-data=\"{ row }\">\n        <UDivider v-if=\"!row.prevPosition\" />\n        <TrendPercentage v-else :value=\"row.position\" :prev-value=\"row.prevPosition\" symbol=\"%\" />\n      </template>\n      <template #ctr-data=\"{ row }\">\n        <div class=\"text-center\">\n          <div>{{ useHumanFriendlyNumber(row.ctr * 100, 1) }}%</div>\n          <UTooltip :text=\"`${row.clicks} impressions`\">\n            <div class=\"text-xs flex items-center gap-1\">\n              {{ useHumanFriendlyNumber(row.clicks) }}\n              <IconClicks />\n            </div>\n          </UTooltip>\n        </div>\n      </template>\n      <template #prevCtr-data=\"{ row }\">\n        <div class=\"text-center\">\n          <div>\n            {{ useHumanFriendlyNumber(row.prevCtr * 100, 1) }}%\n          </div>\n          <UTooltip :text=\"`${row.prevClicks} clicks`\">\n            <div class=\"text-xs\">\n              {{ useHumanFriendlyNumber(row.prevClicks) }} clicks\n            </div>\n          </UTooltip>\n        </div>\n      </template>\n      <template #ctrPercent-data=\"{ row }\">\n        <TrendPercentage v-if=\"row.prevCtr\" :value=\"row.ctr * 100\" :prev-value=\"row.prevCtr * 100\" symbol=\"%\" />\n        <UDivider v-else />\n      </template>\n      <template #actions-data>\n        <UDropdown :items=\"[]\">\n          <UButton variant=\"link\" icon=\"i-heroicons-ellipsis-vertical\" color=\"gray\" />\n        </UDropdown>\n      </template>\n    </TableData>\n  </div>\n</template>\n"
  },
  {
    "path": "components/Table/TableNonIndexedUrls.vue",
    "content": "<script setup lang=\"ts\">\nimport { joinURL, withBase, withHttps } from 'ufo'\nimport { defu } from 'defu'\nimport { useTimeAgo } from '~/composables/formatting'\nimport { createLogoutHandler, createSessionReloader, useAuthenticatedUser } from '~/composables/auth'\nimport type { GoogleSearchConsoleSite, SitePage } from '~/types'\n\nconst props = defineProps<{ mock?: boolean, value: SitePage[], site: GoogleSearchConsoleSite }>()\n\nconst logout = createLogoutHandler()\nconst user = useAuthenticatedUser()\nconst reloadSession = createSessionReloader()\n\nconst toast = useToast()\nconst params = useUrlSearchParams('history')\nconst autoRequestIndex = ref(false)\n\nconst q = ref(params.q || '')\nconst page = ref(params.page || 1)\n\nwatch(q, (q) => {\n  params.q = q\n})\nwatch(page, (page) => {\n  params.page = page\n})\n\nconst columns = [{\n  key: 'url',\n  label: 'URL',\n  sortable: true,\n}, {\n  key: 'inspection',\n  label: 'URL Inspection',\n  icon: 'i-heroicons-magnifying-glass',\n}, {\n  key: 'requestIndexing',\n  label: 'Request Indexing',\n}, props.mock\n  ? false\n  : {\n      key: 'actions',\n    }].filter(Boolean)\n\nconst siteUrlFriendly = useFriendlySiteUrl(props.site.siteUrl)\nconst inspectionsLoading = ref([])\nconst submitIndexingLoading = ref([])\nconst updatedUrls = ref([])\nasync function inspectUrl(row: SitePage) {\n  if (props.mock)\n    return\n\n  const siteUrl = withHttps(siteUrlFriendly)\n  inspectionsLoading.value = [...inspectionsLoading.value, row.url]\n  await $fetch<SitePage>(`/api/sites/${encodeURIComponent(props.site.siteUrl)}/${encodeURIComponent(withBase(row.url, siteUrl))}`, {\n    timeout: 90000, // 90 seconds\n  })\n    .finally(() => {\n      inspectionsLoading.value = inspectionsLoading.value.filter(url => url !== row.url)\n    }).then((data) => {\n      toast.add({\n        color: 'green',\n        title: `Inspected URL Successfully`,\n        description: `Received a verdict of ${data.inspectionResult?.indexStatusResult.verdict}.`,\n      })\n      pushUpdatedUrls(data, row)\n    })\n    .catch(async (err) => {\n      if (err.status === 401) {\n        // make sure we have context\n        await logout(true)\n        toast.add({\n          id: 'unauthorized-error',\n          title: 'Oops, looks like session has expired.',\n          description: 'Please login again to continue.',\n          color: 'red',\n        })\n      }\n      else if (err.status === 429) {\n        toast.add({\n          color: 'red',\n          title: 'Rate Limited',\n          description: err.statusText,\n        })\n      }\n      else {\n        toast.add({\n          color: 'red',\n          title: 'Failed to inspect the URL.',\n          description: err.statusText || err.message,\n        })\n      }\n    })\n}\n\nfunction getUpdatedRow(row: SitePage) {\n  return updatedUrls.value.find(result => result.url === row.url) || row\n}\n\nfunction getUrlNotificationLatestUpdate(row: SitePage) {\n  return updatedUrls.value.find(result => result.url === row.url)?.urlNotificationMetadata?.latestUpdate || row.urlNotificationMetadata?.latestUpdate\n}\n\nfunction pushUpdatedUrls(data: SitePage, row: SitePage) {\n  let updatedUrl\n  updatedUrls.value = updatedUrls.value.map((result) => {\n    if (result.url === row.url) {\n      updatedUrl = true\n      return { ...defu(data, result, row), url: row.url }\n    }\n    return result\n  })\n  if (!updatedUrl)\n    updatedUrls.value = [...updatedUrls.value, { ...defu(data, row), url: row.url }]\n}\nconst { pause: pauseAutoRequestIndex, resume: resumeAutoRequestIndex } = useTimeoutPoll(pollForRequestIndex, 2000, { immediate: false })\n\nasync function submitForIndexing(row: SitePage) {\n  if (props.mock)\n    return\n\n  const siteUrl = withHttps(siteUrlFriendly)\n  submitIndexingLoading.value = [...submitIndexingLoading.value, row.url]\n  const { url: data, status } = await $fetch<{ status: 'already-submitted' | 'submitted', url: SitePage }>(`/api/indexing/${encodeURIComponent(withBase(row.url, siteUrl))}`, {\n    method: 'POST',\n    timeout: 90000, // 90 seconds\n    query: { siteUrl },\n    onResponseError({ response }) {\n      // handle 429\n      if (response.status === 429) {\n        toast.add({\n          color: 'red',\n          title: 'Rate Limited',\n          description: response.statusText,\n        })\n      }\n      else {\n        toast.add({\n          color: 'red',\n          title: 'Failed to submit the URL for indexing.',\n          description: response.statusText,\n        })\n      }\n    },\n  }).catch((e) => {\n    pauseAutoRequestIndex()\n    submitIndexingLoading.value = submitIndexingLoading.value.filter(url => url !== row.url)\n    throw e\n  })\n  if (status === 'already-submitted') {\n    const hoursAgo = useDayjs()(data.urlNotificationMetadata?.latestUpdate?.notifyTime).fromNow()\n    toast.add({\n      color: 'blue',\n      title: 'Already Submitted',\n      description: `This URL was submitted for indexing ${hoursAgo}.`,\n    })\n  }\n  else {\n    toast.add({\n      color: 'green',\n      title: 'Submitted',\n      description: 'Your URL has been submitted for indexing.',\n    })\n    await reloadSession()\n  }\n  pushUpdatedUrls(data, row)\n  submitIndexingLoading.value = submitIndexingLoading.value.filter(url => url !== row.url)\n}\n\nfunction hasOneHourPassed(date?: string | number) {\n  if (!date)\n    return true\n  return useDayjs()().diff(useDayjs()(date), 'hour') >= 1\n}\n\nfunction hasOneDayPassed(date?: string | number) {\n  if (!date)\n    return true\n  return useDayjs()().diff(useDayjs()(date), 'day') >= 1\n}\n\nfunction openUrl(url: string, target?: string) {\n  window.open(url, target)\n}\n\nconst visibleRows = ref([])\n\nconst { pause, resume } = useTimeoutPoll(pollForInspectUrl, 2000, { immediate: false })\n\nasync function pollForInspectUrl() {\n  const row = [...visibleRows.value]\n    .map(row => getUpdatedRow(row))\n    .filter((row) => {\n      if (row.inspectionResult?.indexStatusResult?.verdict && row.inspectionResult?.indexStatusResult?.verdict !== 'NEUTRAL')\n        return false\n\n      if (row.lastInspected) {\n        // if url has been submitted for indexing, we will trigger it after one day\n        if (row.urlNotificationMetadata?.latestUpdate?.type === 'URL_UPDATED')\n          return hasOneDayPassed(row.lastInspected)\n        return false\n      }\n\n      return true\n    })\n    .shift()\n  if (row)\n    await inspectUrl(row)\n  else\n    pause()\n}\n\nwatch(visibleRows, () => {\n  resume()\n}, {\n  immediate: true,\n})\n\nasync function pollForRequestIndex() {\n  const row = [...visibleRows.value]\n    .map(row => getUpdatedRow(row))\n    .filter(row => row.inspectionResult?.indexStatusResult?.verdict === 'NEUTRAL' && getUrlNotificationLatestUpdate(row)?.type !== 'URL_UPDATED')\n    .shift()\n  if (row)\n    await submitForIndexing(row)\n  else\n    pauseAutoRequestIndex()\n}\n\nwatch([visibleRows, autoRequestIndex], () => {\n  if (autoRequestIndex.value && user.value.indexingOAuthIdNext)\n    resumeAutoRequestIndex()\n}, {\n  immediate: true,\n})\n\nconst filters = [\n  {\n    key: 'new',\n    label: 'Hide Actioned',\n    description: 'Hide pages indexed or requested indexing pages.',\n    filter: (rows: any) => {\n      return rows.filter((row) => {\n        row = getUpdatedRow(row)\n        if (row.inspectionResult?.indexStatusResult?.verdict && row.inspectionResult?.indexStatusResult?.verdict !== 'NEUTRAL')\n          return false\n\n        // if url has been submitted for indexing, we will trigger it after one day\n        if (row.urlNotificationMetadata?.latestUpdate?.type === 'URL_UPDATED')\n          return hasOneDayPassed(row.lastInspected)\n\n        return true\n      })\n    },\n  },\n]\n</script>\n\n<template>\n  <div v-if=\"!mock\" class=\"flex justify-between mb-3\">\n    <div v-if=\"value?.length\">\n      <div>\n        <UTooltip :ui=\"{ width: 'max-w-md' }\" text=\"Non-Indexed pages are initially guessed from your page impressions.\">\n          <div><strong>{{ value.filter(row => getUpdatedRow(row).inspectionResult?.indexStatusResult?.verdict === 'NEUTRAL').length }}</strong> Confirmed non-indexed pages.</div>\n        </UTooltip>\n      </div>\n      <div>\n        <UTooltip :ui=\"{ width: 'max-w-md' }\" text=\"Total pages that you've 'Requested Indexing' on\">\n          <div><strong>{{ value.filter(row => !!getUpdatedRow(row)?.urlNotificationMetadata?.latestUpdate).length }}</strong> Indexing requests.</div>\n        </UTooltip>\n      </div>\n    </div>\n    <div v-if=\"site.permissionLevel === 'siteOwner'\">\n      <UTooltip text=\"Automatically trigger the Request Indexing button.\">\n        <label class=\"flex items-center gap-2 text-sm font-semibold\">\n          Auto Request Indexing\n          <UToggle\n            v-model=\"autoRequestIndex\"\n            on-icon=\"i-heroicons-check-20-solid\"\n            off-icon=\"i-heroicons-x-mark-20-solid\"\n          />\n        </label>\n      </UTooltip>\n    </div>\n  </div>\n  <TableData :columns=\"columns\" :value=\"value\" :filters=\"mock ? [] : filters\" @update:rows=\"rows => visibleRows = rows\">\n    <template #url-data=\"{ row }\">\n      <div style=\"max-width: 400px;\" class=\"flex flex-col\">\n        <UButton :title=\"row.url\" variant=\"link\" size=\"xs\" :class=\"mock ? ['pointer-events-none'] : []\" :to=\"joinURL(`https://${siteUrlFriendly}`, row.url)\" target=\"_blank\" color=\"gray\" class=\"w-full\">\n          <div class=\"max-w-[300px] truncate text-ellipsis\">\n            {{ row.url }}\n          </div>\n        </UButton>\n        <UTooltip v-if=\"getUpdatedRow(row)?.inspectionResult?.inspectionResultLink\" mode=\"hover\" text=\"View Inspection Result\">\n          <UButton size=\"xs\" target=\"_blank\" :to=\"mock ? undefined : getUpdatedRow(row)?.inspectionResult?.inspectionResultLink\" icon=\"i-heroicons-document-magnifying-glass\" color=\"gray\" variant=\"link\">\n            View Inspection Report\n          </UButton>\n        </UTooltip>\n      </div>\n    </template>\n    <template #inspection-data=\"{ row }\">\n      <div>\n        <InspectionResult :value=\"getUpdatedRow(row)\">\n          <template v-if=\"getUrlNotificationLatestUpdate(row)?.type === 'URL_UPDATED' || getUpdatedRow(row)?.inspectionResult?.indexStatusResult?.verdict === 'NEUTRAL'\">\n            <UDivider class=\"my-3\" />\n            <div class=\"flex items-center justify-between\">\n              <div class=\"text-gray-600\">\n                <span class=\"text-sm\">Inspected</span><br>\n                {{ $dayjs(getUpdatedRow(row)?.lastInspected).fromNow() }}\n              </div>\n              <div v-if=\"getUpdatedRow(row)?.lastInspected && !hasOneHourPassed(row?.lastInspected)\">\n                <UButton :disabled=\"mock\" color=\"gray\" size=\"xs\" class=\"mt-2\" icon=\"i-heroicons-arrow-path\" :loading=\"inspectionsLoading.includes(row.url)\" @click=\"inspectUrl(row)\">\n                  Inspect Again\n                </UButton>\n              </div>\n              <div v-else>\n                <UButton :disabled=\"mock\" color=\"gray\" size=\"xs\" class=\"mt-2\" icon=\"i-heroicons-arrow-path\" :loading=\"inspectionsLoading.includes(row.url)\" @click=\"inspectUrl(row)\">\n                  Inspect Again\n                </UButton>\n              </div>\n            </div>\n          </template>\n        </InspectionResult>\n        <div v-if=\"!getUpdatedRow(row)?.lastInspected\">\n          <UButton size=\"xs\" color=\"gray\" :loading=\"inspectionsLoading.includes(row.url)\" @click=\"inspectUrl(row)\">\n            Inspect\n          </UButton>\n        </div>\n        <div v-else-if=\"hasOneHourPassed(getUpdatedRow(row)?.lastInspected) && getUpdatedRow(row)?.inspectionResult?.indexStatusResult?.verdict === 'NEUTRAL'\">\n          <UButton size=\"xs\" color=\"gray\" :loading=\"inspectionsLoading.includes(row.url)\" @click=\"inspectUrl(row)\">\n            Inspect Again\n          </UButton>\n        </div>\n      </div>\n    </template>\n\n    <template #requestIndexing-header>\n      <div class=\"flex justify-end\">\n        <div class=\"flex items-center gap-2\">\n          Request Indexing\n        </div>\n      </div>\n    </template>\n    <template #requestIndexing-data=\"{ row }\">\n      <div class=\"flex justify-end\">\n        <div v-if=\"getUrlNotificationLatestUpdate(row)?.type !== 'URL_UPDATED' && getUpdatedRow(row)?.inspectionResult?.indexStatusResult.verdict === 'NEUTRAL'\" class=\"flex items-center gap-2\">\n          <UButton :disabled=\"!mock && (!user?.indexingOAuthIdNext || site.permissionLevel !== 'siteOwner')\" size=\"xs\" :loading=\"submitIndexingLoading.includes(row.url)\" icon=\"i-heroicons-arrow-up-circle\" variant=\"outline\" @click=\"submitForIndexing(row)\">\n            Request Indexing\n          </UButton>\n        </div>\n        <div v-else-if=\"getUrlNotificationLatestUpdate(row)?.type === 'URL_UPDATED'\" class=\"flex items-center text-right\">\n          <div><span class=\"text-xs\">Submitted</span><br>{{ useTimeAgo(getUrlNotificationLatestUpdate(row).notifyTime) }}</div>\n        </div>\n        <div v-else>\n          <div class=\"w-5 h-[2px] dark:bg-gray-800 bg-gray-200\" />\n        </div>\n      </div>\n    </template>\n    <template #actions-data=\"{ row }\">\n      <UDropdown :items=\"[[{ label: 'Open URL', click: () => openUrl(row.url, '_blank'), icon: 'i-heroicons-arrow-up-right' }]]\">\n        <UButton variant=\"link\" icon=\"i-heroicons-ellipsis-vertical\" color=\"gray\" />\n      </UDropdown>\n    </template>\n  </TableData>\n</template>\n"
  },
  {
    "path": "components/Table/TablePages.vue",
    "content": "<script setup lang=\"ts\">\nimport { withLeadingSlash, withoutLeadingSlash } from 'ufo'\nimport type { GoogleSearchConsoleSite, GscDataRow } from '~/types/data'\n\nwithDefaults(\n  defineProps<{ mock?: boolean, value?: GscDataRow[], site: GoogleSearchConsoleSite, pending?: boolean, pageCount?: number }>(),\n  {\n    pageCount: 8,\n  },\n)\n\nconst { user } = useUserSession()\n\nconst columns = computed(() => [\n  {\n    key: 'url',\n    label: 'URL',\n    sortable: true,\n  },\n  {\n    key: 'clicks',\n    label: 'Clicks',\n    sortable: true,\n  },\n  (user.value?.analyticsPeriod === 'all')\n    ? null\n    : {\n        key: 'clicksPercent',\n        label: '%',\n        sortable: true,\n      },\n  {\n    key: 'impressions',\n    label: 'Impressions',\n    sortable: true,\n  },\n  (user.value?.analyticsPeriod === 'all')\n    ? null\n    : {\n        key: 'impressionsPercent',\n        label: '%',\n        sortable: true,\n      },\n].filter(Boolean))\n\nconst filters = [\n  {\n    key: 'new',\n    label: 'New',\n    filter: (rows: T[]) => {\n      return rows.filter(row => !row.prevImpressions)\n    },\n  },\n  {\n    key: 'improving',\n    label: 'Improving',\n    filter: (rows: T[]) => {\n      return rows.filter(row => row.clicks > row.prevClicks)\n    },\n  },\n  {\n    key: 'declining',\n    label: 'Declining',\n    filter: (rows: T[]) => {\n      return rows.filter(row => row.clicks < row.prevClicks)\n    },\n  },\n  {\n    key: 'top-level',\n    special: true,\n    label: 'Top Level',\n    filter: (_rows: GscDataRow[]) => {\n      const topLevelPaths = _rows.map(row => row.url.split('/').slice(1, 2)?.[0] || false)\n      const uniqueTopLevelPaths = Array.from(new Set(topLevelPaths)).filter(Boolean)\n      return uniqueTopLevelPaths.map((topLevelPath) => {\n        const rows = _rows.filter(row => withoutLeadingSlash(row.url).startsWith(topLevelPath))\n        const clicks = rows.reduce((acc, row) => acc + row.clicks, 0)\n        const prevClicks = rows.reduce((acc, row) => acc + row.prevClicks, 0)\n        const impressions = rows.reduce((acc, row) => acc + row.impressions, 0)\n        const prevImpressions = rows.reduce((acc, row) => acc + row.prevImpressions, 0)\n        // compute the avg keyword position\n        const avgKeywordPosition = rows.reduce((acc, row) => acc + (row.keywordPosition || 0), 0) / rows.length\n        return {\n          url: withLeadingSlash(topLevelPath),\n          keyword: rows[0].keyword,\n          keywordPosition: avgKeywordPosition,\n          clicks,\n          prevClicks,\n          impressions,\n          prevImpressions,\n        }\n      })\n    },\n  },\n]\n// const siteUrlFriendly = useFriendlySiteUrl(props.siteUrl)\n\nfunction highestRowClickCount(rows) {\n  // do a sum of all clicks\n  return rows.reduce((acc, row) => acc + row.clicks, 0)\n}\n//\n// const selected = ref([])\n// function select(row) {\n//   const index = selected.value.findIndex(item => item.url === row.url)\n//   if (index === -1)\n//     selected.value.push(row)\n//   else\n//     selected.value.splice(index, 1)\n// }\n\nfunction openUrl(url: string, target?: string) {\n  window.open(url, target)\n}\n</script>\n\n<template>\n  <div>\n    <TableData :value=\"value\" :columns=\"columns\" :filters=\"filters\" :page-count=\"pageCount\">\n      <template #url-data=\"{ row, rows }\">\n        <div class=\"flex items-center\">\n          <div class=\"relative group w-[260px] max-w-full\">\n            <div class=\"flex items-center\">\n              <UButton :title=\"`Open ${row.url}`\" class=\"max-w-[260px]\" variant=\"link\" size=\"xs\" :class=\"mock ? ['pointer-events-none'] : []\" target=\"_blank\" color=\"gray\" @click=\"q = row.url\">\n                <div class=\"max-w-[220px] truncate text-ellipsis\">\n                  {{ row.url }}\n                </div>\n              </UButton>\n              <UBadge v-if=\"!row.prevImpressions\" size=\"xs\" variant=\"subtle\">\n                New\n              </UBadge>\n              <UBadge v-else-if=\"row.lost\" size=\"xs\" color=\"red\" variant=\"subtle\">\n                Lost\n              </UBadge>\n            </div>\n            <UTooltip :text=\"`${Math.round((row.clicks / highestRowClickCount(rows)) * 100)}% of clicks`\" class=\"w-full block\">\n              <UProgress :value=\"Math.round((row.clicks / highestRowClickCount(rows)) * 100)\" color=\"blue\" size=\"xs\" class=\"ml-2 opacity-75 group-hover:opacity-100 transition py-1\" />\n            </UTooltip>\n          </div>\n        </div>\n      </template>\n      <template #keywordPosition-data=\"{ row }\">\n        <div class=\"flex items-center\">\n          <UButton :title=\"row.keyword\" variant=\"link\" size=\"xs\" :class=\"mock ? ['pointer-events-none'] : []\" :to=\"`/dashboard/site/${site.domain}/keywords?q=${encodeURIComponent(row.keyword)}`\" color=\"gray\">\n            <div class=\"max-w-[150px] truncate text-ellipsis\">\n              <PositionMetric :value=\"row.keywordPosition\" />\n              {{ row.keyword }}\n            </div>\n          </UButton>\n        </div>\n      </template>\n      <template #clicks-data=\"{ row }\">\n        <div class=\"text-center\">\n          <UDivider v-if=\"row.lostPage\" />\n          <UTooltip v-else :text=\"row.clicks\" class=\"flex items-center justify-center gap-1\">\n            <IconClicks />\n            {{ useHumanFriendlyNumber(row.clicks) }}\n          </UTooltip>\n        </div>\n      </template>\n      <template #clicksPercent-data=\"{ row }\">\n        <UDivider v-if=\"!row.prevImpressions\" />\n        <TrendPercentage v-else :value=\"row.clicks\" :prev-value=\"row.prevClicks\" />\n      </template>\n      <template #impressions-data=\"{ row }\">\n        <UTooltip :text=\"row.impressions\" class=\"flex items-center justify-center gap-1\">\n          <IconImpressions />\n          {{ useHumanFriendlyNumber(row.impressions) }}\n        </UTooltip>\n      </template>\n      <template #impressionsPercent-data=\"{ row }\">\n        <UDivider v-if=\"!row.prevImpressions\" />\n        <TrendPercentage v-else :value=\"row.impressions\" :prev-value=\"row.prevImpressions\" />\n      </template>\n      <template #actions-data=\"{ row }\">\n        <UDropdown :items=\"[[{ label: 'Open URL', click: () => openUrl(row.url, '_blank') }], [{ label: 'URL Inspections', icon: 'i-heroicons-document-magnifying-glass', disabled: true }, (row.inspectionResult?.inspectionResultLink ? { label: 'View Inspection Result' } : undefined), { label: 'Inspect Index Status' }].filter(Boolean)]\">\n          <UButton variant=\"link\" icon=\"i-heroicons-ellipsis-vertical\" color=\"gray\" />\n        </UDropdown>\n      </template>\n    </TableData>\n  </div>\n</template>\n"
  },
  {
    "path": "components/TrendPercentage.vue",
    "content": "<script lang=\"ts\" setup>\nconst props = defineProps<{\n  prevValue?: string | number | null\n  value: string | number\n  symbol?: string\n  negative?: boolean\n}>()\n\nconst percentage = computed(() => {\n  const prev = Number(props.prevValue)\n  const current = Number(props.value)\n  if (prev === 0)\n    return 1\n  return (current - prev) / ((prev + current) / 2)\n})\n</script>\n\n<template>\n  <UTooltip v-if=\"prevValue\" :text=\"`${useHumanFriendlyNumber(Number(prevValue))}${symbol || ''} previous period`\">\n    <div v-if=\"percentage > 0 && !negative\" class=\"text-sm items-center flex gap-1 text-green-500\">\n      <UIcon name=\"i-heroicons-arrow-trending-up\" class=\"w-4 h-4 opacity-70\" />\n      <div>{{ useHumanFriendlyNumber(Math.round(percentage * 100)) }}%</div>\n    </div>\n    <div v-else class=\"text-sm  items-center flex gap-1 text-red-500\">\n      <UIcon name=\"i-heroicons-arrow-trending-down\" class=\"w-4 h-4 opacity-70\" />\n      <div>{{ useHumanFriendlyNumber(Math.round(percentage * 100)) }}%</div>\n    </div>\n  </UTooltip>\n</template>\n"
  },
  {
    "path": "composables/auth.ts",
    "content": "import type { ComputedRef } from 'vue'\nimport type { User } from '~/types'\n\nexport function useAuthenticatedUser() {\n  const { loggedIn, user } = useUserSession()\n  if (!loggedIn) {\n    throw createError({\n      statusCode: 401,\n      message: 'Unauthorized',\n    })\n  }\n  return user as ComputedRef<User>\n}\n\nexport function createSessionReloader() {\n  const { session } = useUserSession()\n  return async () => {\n    session.value = await $fetch('/api/_auth/session')\n  }\n}\n\n// work around nuxt-auth-utils async context bug\nexport function createLogoutHandler() {\n  const { session } = useUserSession()\n  const toast = useToast()\n\n  const nextTickFn = nextTick\n  return async (force?: boolean) => {\n    if (!force) {\n      toast.add({ id: 'logout', title: 'See you next time!', description: 'You have logged out of the site.', color: 'green' })\n      await navigateTo('/')\n    }\n    else {\n      await navigateTo('/get-started')\n    }\n    await nextTickFn(() => {\n      // can't access clear API here\n      $fetch('/api/_auth/session', { method: 'DELETE' })\n        .then(() => {\n          session.value = {}\n        })\n        .catch(() => {})\n    })\n  }\n}\n"
  },
  {
    "path": "composables/fetch.ts",
    "content": "import { createLogoutHandler } from '~/composables/auth'\nimport type { GoogleSearchConsoleSite } from '~/types'\nimport { useAsyncData } from '#imports'\n\nexport async function fetchSite(site: GoogleSearchConsoleSite, isMock?: boolean) {\n  const toast = useToast()\n  const logout = createLogoutHandler()\n  const force = ref()\n  const fetchFn = useRequestFetch()\n  const res = await useAsyncData(\n    `sites:${site.siteUrl}`,\n    async () => await fetchFn(`/api/sites/${encodeURIComponent(site.siteUrl)}`, {\n      query: { force: force.value },\n      onResponseError: async ({ response }) => {\n        if (response.status === 401) {\n          // make sure we have context\n          await logout(true)\n          toast.add({\n            id: 'unauthorized-error',\n            title: 'Oops, looks like session has expired.',\n            description: 'Please login again to continue.',\n            color: 'red',\n          })\n        }\n        else {\n          toast.add({\n            id: `sites:${site.siteUrl}:error`,\n            title: `Failed to fetch ${site.siteUrl}`,\n            description: response.statusText,\n            color: 'red',\n          })\n        }\n      },\n    }),\n    {\n      server: false,\n      deep: false,\n      immediate: !isMock,\n    },\n  )\n  return {\n    ...res,\n    async forceRefresh() {\n      return callFnSyncToggleRef(res.refresh, force)\n    },\n  }\n}\n\nexport async function fetchSites() {\n  const force = ref()\n  const toast = useToast()\n  const logout = createLogoutHandler()\n  const fetchFn = useRequestFetch()\n  const res = await useAsyncData(\n    `sites`,\n    async () => await fetchFn('/api/sites/list', {\n      query: { force: force.value },\n      async onResponseError(res) {\n        if ([401, 400].includes(res.response.status)) {\n          // make sure we have context\n          await logout(true)\n          toast.add({\n            id: 'unauthorized-error',\n            title: 'Oops, looks like session has expired.',\n            description: 'Please login again to continue.',\n            color: 'red',\n          })\n        }\n        else { toast.add({ id: 'unauthorized-error', title: 'Error fetching sites', description: res.error?.message, color: 'red' }) }\n      },\n    }),\n    {\n      server: true,\n      deep: false,\n    },\n  )\n  return {\n    ...res,\n    forceRefresh() {\n      force.value = true\n      res.refresh().then(() => {\n        force.value = false\n      })\n    },\n  }\n}\n"
  },
  {
    "path": "composables/formatting.ts",
    "content": "import type { Ref } from '@vue/reactivity'\nimport type { type ComputedRef, MaybeRef } from 'vue'\nimport { withoutTrailingSlash } from 'ufo'\n\nfunction useHumanFriendlyNumber(number: Ref<number>): ComputedRef<string>\nfunction useHumanFriendlyNumber(number: number): string\nexport function useHumanFriendlyNumber(number: MaybeRef<number>) {\n  const format = (number: number) => new Intl.NumberFormat('en', { notation: 'compact' }).format(number)\n  if (isRef(number)) {\n    return computed(() => {\n      return format(number.value)\n    })\n  }\n  // use intl to format the number, should have `k` or `m` suffix if needed\n  return format(number)\n}\n\nexport function useFriendlySiteUrl(url: string): string\nexport function useFriendlySiteUrl(url: MaybeRef<string>) {\n  const format = (s: string) => withoutTrailingSlash(s.replace('https://', '').replace('sc-domain:', ''))\n  if (isRef(url)) {\n    return computed(() => {\n      return format(url.value)\n    })\n  }\n  // use intl to format the number, should have `k` or `m` suffix if needed\n  return format(url)\n}\n\nexport function useTimeAgo(date: string, absAgo?: boolean): string\nexport function useTimeAgo(date: MaybeRef<string>, absAgo?: boolean): string {\n  const format = (_d: string) => {\n    const d = useDayjs()(_d)\n    const hourDiff = useDayjs()().diff(d, 'hour')\n    if (hourDiff < 1 || absAgo)\n      return d.fromNow()\n    return `${hourDiff} hours ago`\n  }\n  if (isRef(date)) {\n    return computed(() => {\n      return format(date.value)\n    })\n  }\n  return format(date)\n}\n\nexport function useTimeHoursAgo(date: string): string\nexport function useTimeHoursAgo(date: MaybeRef<string>) {\n  const format = (_d: string) => {\n    const d = useDayjs()(_d)\n    return useDayjs()().diff(d, 'hour')\n  }\n  if (isRef(date)) {\n    return computed(() => {\n      return format(date.value)\n    })\n  }\n  return format(date)\n}\n"
  },
  {
    "path": "composables/loader.ts",
    "content": "import type { Ref } from 'vue'\n\nexport async function callFnSyncToggleRef<T extends (() => (Promise<any> | any))>(\n  fn: T,\n  toggleRef: Ref<boolean>,\n  thresholdMs: number = 550,\n): Promise<ReturnType<T>> {\n  toggleRef.value = true\n  return callFnDelayedResolve(fn, thresholdMs).finally(() => {\n    toggleRef.value = false\n  })\n}\n\nexport async function callFnDelayedResolve<T extends (() => (Promise<any> | any))>(\n  fn: T,\n  thresholdMs: number = 550,\n): Promise<ReturnType<T>> {\n  const res = await Promise.all([\n    fn(),\n    new Promise(resolve => setTimeout(resolve, thresholdMs)),\n  ])\n  return Array.isArray(res) ? res[0] : res\n}\n"
  },
  {
    "path": "data/home.ts",
    "content": "export const NuxtSeoPages = [\n  {\n    url: '/',\n    clicks: 654,\n    prevClicks: 665,\n    clicksPercent: -1.6679302501895377,\n    impressions: 2414,\n    impressionsPercent: -2.5761602944183193,\n    prevImpressions: 2477,\n  },\n  {\n    url: '/sitemap/getting-started/installation',\n    clicks: 206,\n    prevClicks: 242,\n    clicksPercent: -16.071428571428573,\n    impressions: 2807,\n    impressionsPercent: 5.9427732942039615,\n    prevImpressions: 2645,\n  },\n  {\n    url: '/robots/getting-started/installation',\n    clicks: 62,\n    prevClicks: 58,\n    clicksPercent: 6.666666666666667,\n    impressions: 2216,\n    impressionsPercent: 56.11095059231436,\n    prevImpressions: 1245,\n  },\n  {\n    url: '/og-image/guides/cache',\n    clicks: 46,\n    prevClicks: 49,\n    clicksPercent: -6.315789473684211,\n    impressions: 550,\n    impressionsPercent: -6.848112379280071,\n    prevImpressions: 589,\n  },\n  {\n    url: '/experiments/guides/open-graph-images',\n    clicks: 40,\n    prevClicks: 22,\n    clicksPercent: 58.06451612903226,\n    impressions: 2332,\n    impressionsPercent: 18.14780168381665,\n    prevImpressions: 1944,\n  },\n  {\n    url: '/sitemap/guides/prerendering',\n    clicks: 39,\n    prevClicks: 37,\n    clicksPercent: 5.263157894736842,\n    impressions: 821,\n    impressionsPercent: 19.812583668005352,\n    prevImpressions: 673,\n  },\n  {\n    url: '/og-image/api/define-og-image',\n    clicks: 32,\n    prevClicks: 10,\n    clicksPercent: 104.76190476190477,\n    impressions: 207,\n    impressionsPercent: 118.46153846153847,\n    prevImpressions: 53,\n  },\n  {\n    url: '/site-config/getting-started/installation',\n    clicks: 27,\n    prevClicks: 28,\n    clicksPercent: -3.6363636363636362,\n    impressions: 349,\n    impressionsPercent: -124.98656636217088,\n    prevImpressions: 1512,\n  },\n  {\n    url: '/og-image/getting-started/installation',\n    clicks: 26,\n    prevClicks: 33,\n    clicksPercent: -23.728813559322035,\n    impressions: 1182,\n    impressionsPercent: 21.867667761614264,\n    prevImpressions: 949,\n  },\n  {\n    url: '/sitemap/guides/dynamic-urls',\n    clicks: 24,\n    prevClicks: 17,\n    clicksPercent: 34.146341463414636,\n    impressions: 757,\n    impressionsPercent: 24.296296296296298,\n    prevImpressions: 593,\n  },\n  {\n    url: '/nuxt-seo/guides/redirect-canonical',\n    clicks: 22,\n    prevClicks: 2,\n    clicksPercent: 166.66666666666669,\n    impressions: 339,\n    impressionsPercent: 94.14316702819957,\n    prevImpressions: 122,\n  },\n  {\n    url: '/robots/guides/disable-indexing',\n    clicks: 21,\n    prevClicks: 3,\n    clicksPercent: 150,\n    impressions: 132,\n    impressionsPercent: 123.92638036809815,\n    prevImpressions: 31,\n  },\n  {\n    url: '/sitemap/releases/v4',\n    clicks: 20,\n    prevClicks: 16,\n    clicksPercent: 22.22222222222222,\n    impressions: 405,\n    impressionsPercent: 8.494208494208493,\n    prevImpressions: 372,\n  },\n  {\n    url: '/nuxt-seo/guides/title-templates',\n    clicks: 19,\n    prevClicks: 12,\n    clicksPercent: 45.16129032258064,\n    impressions: 641,\n    impressionsPercent: -2.1604938271604937,\n    prevImpressions: 655,\n  },\n  {\n    url: '/schema-org/getting-started/installation',\n    clicks: 19,\n    prevClicks: 18,\n    clicksPercent: 5.405405405405405,\n    impressions: 654,\n    impressionsPercent: 10.120481927710843,\n    prevImpressions: 591,\n  },\n  {\n    url: '/sitemap/api/config',\n    clicks: 19,\n    prevClicks: 10,\n    clicksPercent: 62.06896551724138,\n    impressions: 263,\n    impressionsPercent: 41.284403669724774,\n    prevImpressions: 173,\n  },\n  {\n    url: '/robots/guides/robots-txt',\n    clicks: 13,\n    prevClicks: 6,\n    clicksPercent: 73.68421052631578,\n    impressions: 386,\n    impressionsPercent: 129.21108742004265,\n    prevImpressions: 83,\n  },\n  {\n    url: '/robots/guides/route-rules',\n    clicks: 13,\n    prevClicks: 10,\n    clicksPercent: 26.08695652173913,\n    impressions: 350,\n    impressionsPercent: -1.9801980198019802,\n    prevImpressions: 357,\n  },\n  {\n    url: '/nuxt-seo/api/breadcrumbs',\n    clicks: 12,\n    prevClicks: 4,\n    clicksPercent: 100,\n    impressions: 42,\n    impressionsPercent: 50.74626865671642,\n    prevImpressions: 25,\n  },\n  {\n    url: '/site-config/getting-started/how-it-works',\n    clicks: 12,\n    prevClicks: 6,\n    clicksPercent: 66.66666666666666,\n    impressions: 1102,\n    impressionsPercent: 6.077606358111267,\n    prevImpressions: 1037,\n  },\n  {\n    url: '/site-config/guides/setting-site-config',\n    clicks: 12,\n    prevClicks: 6,\n    clicksPercent: 66.66666666666666,\n    impressions: 80,\n    impressionsPercent: -7.228915662650602,\n    prevImpressions: 86,\n  },\n  {\n    url: '/sitemap/guides/best-practices',\n    clicks: 12,\n    prevClicks: 2,\n    clicksPercent: 142.85714285714286,\n    impressions: 701,\n    impressionsPercent: -20.601407549584135,\n    prevImpressions: 862,\n  },\n  {\n    url: '/sitemap/guides/multi-sitemaps',\n    clicks: 12,\n    prevClicks: 6,\n    clicksPercent: 66.66666666666666,\n    impressions: 324,\n    impressionsPercent: -9.131075110456553,\n    prevImpressions: 355,\n  },\n  {\n    url: '/sitemap/integrations/i18n',\n    clicks: 12,\n    prevClicks: 4,\n    clicksPercent: 100,\n    impressions: 635,\n    impressionsPercent: 0.949367088607595,\n    prevImpressions: 629,\n  },\n  {\n    url: '/robots/api/config',\n    clicks: 11,\n    prevClicks: 8,\n    clicksPercent: 31.57894736842105,\n    impressions: 371,\n    impressionsPercent: 6.397774687065369,\n    prevImpressions: 348,\n  },\n  {\n    url: '/og-image/api/define-og-image-component',\n    clicks: 10,\n    prevClicks: 6,\n    clicksPercent: 50,\n    impressions: 45,\n    impressionsPercent: 43.24324324324324,\n    prevImpressions: 29,\n  },\n  {\n    url: '/link-checker/getting-started/installation',\n    clicks: 9,\n    prevClicks: 4,\n    clicksPercent: 76.92307692307693,\n    impressions: 419,\n    impressionsPercent: 24.966442953020135,\n    prevImpressions: 326,\n  },\n  {\n    url: '/nuxt-seo/guides/configuring-modules',\n    clicks: 9,\n    prevClicks: 5,\n    clicksPercent: 57.14285714285714,\n    impressions: 107,\n    impressionsPercent: -7.207207207207207,\n    prevImpressions: 115,\n  },\n  {\n    url: '/og-image/guides/custom-fonts',\n    clicks: 9,\n    prevClicks: 6,\n    clicksPercent: 40,\n    impressions: 401,\n    impressionsPercent: 71.40439932318104,\n    prevImpressions: 190,\n  },\n  {\n    url: '/nuxt-seo/migration-guide/nuxt-seo-kit',\n    clicks: 8,\n    prevClicks: 0,\n    clicksPercent: 0,\n    impressions: 343,\n    impressionsPercent: 158.22454308093995,\n    prevImpressions: 40,\n  },\n  {\n    url: '/sitemap/guides/cache',\n    clicks: 8,\n    prevClicks: 4,\n    clicksPercent: 66.66666666666666,\n    impressions: 226,\n    impressionsPercent: 41.06666666666667,\n    prevImpressions: 149,\n  },\n  {\n    url: '/sitemap/releases/v3',\n    clicks: 8,\n    prevClicks: 4,\n    clicksPercent: 66.66666666666666,\n    impressions: 181,\n    impressionsPercent: 16.766467065868262,\n    prevImpressions: 153,\n  },\n  {\n    url: '/og-image/guides/satori',\n    clicks: 7,\n    prevClicks: 1,\n    clicksPercent: 150,\n    impressions: 54,\n    impressionsPercent: 154.0983606557377,\n    prevImpressions: 7,\n  },\n  {\n    url: '/og-image/releases/v3',\n    clicks: 7,\n    prevClicks: 7,\n    clicksPercent: 0,\n    impressions: 273,\n    impressionsPercent: -21.859706362153343,\n    prevImpressions: 340,\n  },\n  {\n    url: '/experiments/getting-started/installation',\n    clicks: 6,\n    prevClicks: 2,\n    clicksPercent: 100,\n    impressions: 1862,\n    impressionsPercent: -5.8900182434193376,\n    prevImpressions: 1975,\n  },\n  {\n    url: '/robots/guides/disable-page-indexing',\n    clicks: 6,\n    prevClicks: 2,\n    clicksPercent: 100,\n    impressions: 108,\n    impressionsPercent: 132.3076923076923,\n    prevImpressions: 22,\n  },\n  {\n    url: '/nuxt-seo/guides/fallback-title',\n    clicks: 5,\n    prevClicks: 0,\n    clicksPercent: 0,\n    impressions: 253,\n    impressionsPercent: 80.33240997229917,\n    prevImpressions: 108,\n  },\n  {\n    url: '/nuxt-seo/guides/trailing-slashes',\n    clicks: 5,\n    prevClicks: 2,\n    clicksPercent: 85.71428571428571,\n    impressions: 18,\n    impressionsPercent: -36.36363636363637,\n    prevImpressions: 26,\n  },\n  {\n    url: '/site-config/api/site-link',\n    clicks: 5,\n    prevClicks: 2,\n    clicksPercent: 85.71428571428571,\n    impressions: 30,\n    impressionsPercent: 124.32432432432432,\n    prevImpressions: 7,\n  },\n  {\n    url: '/site-config/api/use-site-config',\n    clicks: 5,\n    prevClicks: 3,\n    clicksPercent: 50,\n    impressions: 27,\n    impressionsPercent: 34.78260869565217,\n    prevImpressions: 19,\n  },\n  {\n    url: '/experiments/guides/nuxt-config-seo-meta',\n    clicks: 4,\n    prevClicks: 0,\n    clicksPercent: 0,\n    impressions: 343,\n    impressionsPercent: 187.57062146892656,\n    prevImpressions: 11,\n  },\n  {\n    url: '/nuxt-seo/api/config',\n    clicks: 4,\n    prevClicks: 1,\n    clicksPercent: 120,\n    impressions: 20,\n    impressionsPercent: 66.66666666666666,\n    prevImpressions: 10,\n  },\n  {\n    url: '/nuxt-seo/getting-started/installation',\n    clicks: 4,\n    prevClicks: 6,\n    clicksPercent: -40,\n    impressions: 1788,\n    impressionsPercent: -5.281786005989654,\n    prevImpressions: 1885,\n  },\n  {\n    url: '/schema-org/guides/nodes',\n    clicks: 4,\n    prevClicks: 3,\n    clicksPercent: 28.57142857142857,\n    impressions: 84,\n    impressionsPercent: 30.136986301369863,\n    prevImpressions: 62,\n  },\n  {\n    url: '/site-config/integrations/i18n',\n    clicks: 4,\n    prevClicks: 0,\n    clicksPercent: 0,\n    impressions: 505,\n    impressionsPercent: 175.4646840148699,\n    prevImpressions: 33,\n  },\n  {\n    url: '/sitemap/guides/route-rules',\n    clicks: 4,\n    prevClicks: 5,\n    clicksPercent: -22.22222222222222,\n    impressions: 287,\n    impressionsPercent: -13.333333333333334,\n    prevImpressions: 328,\n  },\n  {\n    url: '/ui',\n    clicks: 4,\n    prevClicks: 6,\n    clicksPercent: -40,\n    impressions: 47,\n    impressionsPercent: -88.09523809523809,\n    prevImpressions: 121,\n  },\n  {\n    url: '/link-checker/releases/v2',\n    clicks: 3,\n    prevClicks: 6,\n    clicksPercent: -66.66666666666666,\n    impressions: 177,\n    impressionsPercent: 42.465753424657535,\n    prevImpressions: 115,\n  },\n  {\n    url: '/robots/getting-started/how-it-works',\n    clicks: 3,\n    prevClicks: 4,\n    clicksPercent: -28.57142857142857,\n    impressions: 283,\n    impressionsPercent: 161.66134185303514,\n    prevImpressions: 30,\n  },\n  {\n    url: '/schema-org/guides/default-schema-org',\n    clicks: 3,\n    prevClicks: 2,\n    clicksPercent: 40,\n    impressions: 23,\n    impressionsPercent: 78.78787878787878,\n    prevImpressions: 10,\n  },\n  {\n    url: '/site-config/api/config',\n    clicks: 3,\n    prevClicks: 3,\n    clicksPercent: 0,\n    impressions: 8,\n    impressionsPercent: -47.61904761904761,\n    prevImpressions: 13,\n  },\n  {\n    url: '/site-config/api/nuxt-hooks',\n    clicks: 3,\n    prevClicks: 0,\n    clicksPercent: 0,\n    impressions: 117,\n    impressionsPercent: 174.4,\n    prevImpressions: 8,\n  },\n  {\n    url: '/site-config/guides/runtime-site-config',\n    clicks: 3,\n    prevClicks: 0,\n    clicksPercent: 0,\n    impressions: 110,\n    impressionsPercent: 10.526315789473683,\n    prevImpressions: 99,\n  },\n  {\n    url: '/sitemap/api/schema',\n    clicks: 3,\n    prevClicks: 0,\n    clicksPercent: 0,\n    impressions: 104,\n    impressionsPercent: 188.78504672897196,\n    prevImpressions: 3,\n  },\n  {\n    url: '/sitemap/guides/debugging',\n    clicks: 3,\n    prevClicks: 2,\n    clicksPercent: 40,\n    impressions: 72,\n    impressionsPercent: -5.405405405405405,\n    prevImpressions: 76,\n  },\n  {\n    url: '/sitemap/guides/images-videos',\n    clicks: 3,\n    prevClicks: 0,\n    clicksPercent: 0,\n    impressions: 35,\n    impressionsPercent: 50,\n    prevImpressions: 21,\n  },\n  {\n    url: '/sitemap/integrations/content',\n    clicks: 3,\n    prevClicks: 1,\n    clicksPercent: 100,\n    impressions: 205,\n    impressionsPercent: 131.9838056680162,\n    prevImpressions: 42,\n  },\n  {\n    url: '/sitemap/releases/v5',\n    clicks: 3,\n    prevClicks: 1,\n    clicksPercent: 100,\n    impressions: 67,\n    impressionsPercent: 12.698412698412698,\n    prevImpressions: 59,\n  },\n  {\n    url: '/link-checker/guides/live-inspections',\n    clicks: 2,\n    prevClicks: 4,\n    clicksPercent: -66.66666666666666,\n    impressions: 43,\n    impressionsPercent: -6.741573033707865,\n    prevImpressions: 46,\n  },\n  {\n    url: '/og-image/api/components',\n    clicks: 2,\n    prevClicks: 1,\n    clicksPercent: 66.66666666666666,\n    impressions: 25,\n    impressionsPercent: 170.37037037037038,\n    prevImpressions: 2,\n  },\n  {\n    url: '/og-image/api/define-og-image-screenshot',\n    clicks: 2,\n    prevClicks: 0,\n    clicksPercent: 0,\n    impressions: 5,\n    impressionsPercent: 0,\n    prevImpressions: 0,\n  },\n  {\n    url: '/og-image/getting-started/getting-familar-with-nuxt-og-image',\n    clicks: 2,\n    prevClicks: 1,\n    clicksPercent: 66.66666666666666,\n    impressions: 95,\n    impressionsPercent: 130.43478260869566,\n    prevImpressions: 20,\n  },\n  {\n    url: '/og-image/guides/chromium',\n    clicks: 2,\n    prevClicks: 0,\n    clicksPercent: 0,\n    impressions: 24,\n    impressionsPercent: 82.35294117647058,\n    prevImpressions: 10,\n  },\n  {\n    url: '/robots/guides/nuxt-config',\n    clicks: 2,\n    prevClicks: 2,\n    clicksPercent: 0,\n    impressions: 101,\n    impressionsPercent: -12.093023255813954,\n    prevImpressions: 114,\n  },\n  {\n    url: '/sitemap/getting-started/data-sources',\n    clicks: 2,\n    prevClicks: 0,\n    clicksPercent: 0,\n    impressions: 8,\n    impressionsPercent: 0,\n    prevImpressions: 8,\n  },\n  {\n    url: '/sitemap/guides/customising-ui',\n    clicks: 2,\n    prevClicks: 1,\n    clicksPercent: 66.66666666666666,\n    impressions: 8,\n    impressionsPercent: 0,\n    prevImpressions: 8,\n  },\n  {\n    url: '/sitemap/guides/filtering-urls',\n    clicks: 2,\n    prevClicks: 1,\n    clicksPercent: 66.66666666666666,\n    impressions: 9,\n    impressionsPercent: -20,\n    prevImpressions: 11,\n  },\n  {\n    url: '/sitemap/guides/submitting-sitemap',\n    clicks: 2,\n    prevClicks: 0,\n    clicksPercent: 0,\n    impressions: 17,\n    impressionsPercent: 0,\n    prevImpressions: 0,\n  },\n  {\n    url: '/sitemap/nitro-api/nitro-hooks',\n    clicks: 2,\n    prevClicks: 2,\n    clicksPercent: 0,\n    impressions: 44,\n    impressionsPercent: -6.593406593406594,\n    prevImpressions: 47,\n  },\n  {\n    url: '/link-checker/api/config',\n    clicks: 1,\n    prevClicks: 0,\n    clicksPercent: 0,\n    impressions: 3,\n    impressionsPercent: 0,\n    prevImpressions: 0,\n  },\n  {\n    url: '/link-checker/guides/exclude-links',\n    clicks: 1,\n    prevClicks: 0,\n    clicksPercent: 0,\n    impressions: 1,\n    impressionsPercent: 0,\n    prevImpressions: 0,\n  },\n  {\n    url: '/nuxt-seo/getting-started',\n    clicks: 1,\n    prevClicks: 0,\n    clicksPercent: 0,\n    impressions: 1,\n    impressionsPercent: 0,\n    prevImpressions: 0,\n  },\n  {\n    url: '/nuxt-seo/getting-started/what-is-nuxt-seo',\n    clicks: 1,\n    prevClicks: 0,\n    clicksPercent: 0,\n    impressions: 99,\n    impressionsPercent: -7.766990291262135,\n    prevImpressions: 107,\n  },\n  {\n    url: '/nuxt-seo/guides/default-meta',\n    clicks: 1,\n    prevClicks: 1,\n    clicksPercent: 0,\n    impressions: 35,\n    impressionsPercent: 69.23076923076923,\n    prevImpressions: 17,\n  },\n  {\n    url: '/nuxt-seo/guides/disabling-modules',\n    clicks: 1,\n    prevClicks: 2,\n    clicksPercent: -66.66666666666666,\n    impressions: 4,\n    impressionsPercent: -85.71428571428571,\n    prevImpressions: 10,\n  },\n  {\n    url: '/og-image/api/config',\n    clicks: 1,\n    prevClicks: 0,\n    clicksPercent: 0,\n    impressions: 20,\n    impressionsPercent: 75.86206896551724,\n    prevImpressions: 9,\n  },\n  {\n    url: '/og-image/guides/emojis',\n    clicks: 1,\n    prevClicks: 0,\n    clicksPercent: 0,\n    impressions: 11,\n    impressionsPercent: 0,\n    prevImpressions: 0,\n  },\n  {\n    url: '/og-image/guides/icons-and-images',\n    clicks: 1,\n    prevClicks: 0,\n    clicksPercent: 0,\n    impressions: 5,\n    impressionsPercent: 0,\n    prevImpressions: 0,\n  },\n  {\n    url: '/og-image/integrations/content',\n    clicks: 1,\n    prevClicks: 2,\n    clicksPercent: -66.66666666666666,\n    impressions: 54,\n    impressionsPercent: 22.68041237113402,\n    prevImpressions: 43,\n  },\n  {\n    url: '/og-image/nitro-api/nitro-hooks',\n    clicks: 1,\n    prevClicks: 0,\n    clicksPercent: 0,\n    impressions: 53,\n    impressionsPercent: 0,\n    prevImpressions: 0,\n  },\n  {\n    url: '/robots/api/nuxt-hooks',\n    clicks: 1,\n    prevClicks: 5,\n    clicksPercent: -133.33333333333331,\n    impressions: 124,\n    impressionsPercent: 65.24064171122996,\n    prevImpressions: 63,\n  },\n  {\n    url: '/robots/getting-started/features',\n    clicks: 1,\n    prevClicks: 3,\n    clicksPercent: -100,\n    impressions: 26,\n    impressionsPercent: -105.45454545454544,\n    prevImpressions: 84,\n  },\n  {\n    url: '/robots/integrations/i18n',\n    clicks: 1,\n    prevClicks: 1,\n    clicksPercent: 0,\n    impressions: 703,\n    impressionsPercent: 175.43391188251002,\n    prevImpressions: 46,\n  },\n  {\n    url: '/robots/nitro-api/get-site-indexable',\n    clicks: 1,\n    prevClicks: 0,\n    clicksPercent: 0,\n    impressions: 1,\n    impressionsPercent: 0,\n    prevImpressions: 0,\n  },\n  {\n    url: '/robots/releases/v4',\n    clicks: 1,\n    prevClicks: 0,\n    clicksPercent: 0,\n    impressions: 67,\n    impressionsPercent: 0,\n    prevImpressions: 0,\n  },\n  {\n    url: '/schema-org/api/nuxt-hooks',\n    clicks: 1,\n    prevClicks: 2,\n    clicksPercent: -66.66666666666666,\n    impressions: 42,\n    impressionsPercent: 111.11111111111111,\n    prevImpressions: 12,\n  },\n  {\n    url: '/site-config/getting-started/background',\n    clicks: 1,\n    prevClicks: 2,\n    clicksPercent: -66.66666666666666,\n    impressions: 611,\n    impressionsPercent: 27.137546468401485,\n    prevImpressions: 465,\n  },\n  {\n    url: '/site-config/guides/debugging',\n    clicks: 1,\n    prevClicks: 0,\n    clicksPercent: 0,\n    impressions: 15,\n    impressionsPercent: -46.15384615384615,\n    prevImpressions: 24,\n  },\n  {\n    url: '/experiments/api/config',\n    clicks: 0,\n    prevClicks: 0,\n    clicksPercent: 0,\n    impressions: 1,\n    impressionsPercent: 0,\n    prevImpressions: 0,\n  },\n  {\n    url: '/experiments/getting-started/features',\n    clicks: 0,\n    prevClicks: 0,\n    clicksPercent: 0,\n    impressions: 9,\n    impressionsPercent: 0,\n    prevImpressions: 0,\n  },\n  {\n    url: '/experiments/guides/app-icons',\n    clicks: 0,\n    prevClicks: 1,\n    clicksPercent: 0,\n    impressions: 2,\n    impressionsPercent: -85.71428571428571,\n    prevImpressions: 5,\n  },\n  {\n    url: '/experiments/guides/open-graph-images',\n    clicks: 0,\n    prevClicks: 0,\n    clicksPercent: 0,\n    impressions: 56,\n    impressionsPercent: 83.54430379746836,\n    prevImpressions: 23,\n  },\n  {\n    url: '/experiments/guides/open-graph-images',\n    clicks: 0,\n    prevClicks: 0,\n    clicksPercent: 0,\n    impressions: 56,\n    impressionsPercent: 83.54430379746836,\n    prevImpressions: 23,\n  },\n  {\n    url: '/experiments/guides/open-graph-images',\n    clicks: 0,\n    prevClicks: 0,\n    clicksPercent: 0,\n    impressions: 56,\n    impressionsPercent: 83.54430379746836,\n    prevImpressions: 23,\n  },\n  {\n    url: '/experiments/guides/route-rules',\n    clicks: 0,\n    prevClicks: 0,\n    clicksPercent: 0,\n    impressions: 2,\n    impressionsPercent: -40,\n    prevImpressions: 3,\n  },\n  {\n    url: '/experiments/releases/v3',\n    clicks: 0,\n    prevClicks: 0,\n    clicksPercent: 0,\n    impressions: 63,\n    impressionsPercent: 0,\n    prevImpressions: 0,\n  },\n  {\n    url: '/learn',\n    clicks: 0,\n    prevClicks: 0,\n    clicksPercent: 0,\n    impressions: 1,\n    impressionsPercent: -150,\n    prevImpressions: 7,\n  },\n  {\n    url: '/link-checker/guides/build-scans',\n    clicks: 0,\n    prevClicks: 0,\n    clicksPercent: 0,\n    impressions: 2,\n    impressionsPercent: 66.66666666666666,\n    prevImpressions: 1,\n  },\n  {\n    url: '/link-checker/guides/skip-inspections',\n    clicks: 0,\n    prevClicks: 0,\n    clicksPercent: 0,\n    impressions: 2,\n    impressionsPercent: 0,\n    prevImpressions: 0,\n  },\n  {\n    url: '/link-checker/integrations/sitemap',\n    clicks: 0,\n    prevClicks: 0,\n    clicksPercent: 0,\n    impressions: 7,\n    impressionsPercent: 0,\n    prevImpressions: 0,\n  },\n  {\n    url: '/nuxt-seo',\n    clicks: 0,\n    prevClicks: 0,\n    clicksPercent: 0,\n    impressions: 1,\n    impressionsPercent: 0,\n    prevImpressions: 0,\n  },\n  {\n    url: '/nuxt-seo/getting-started/faq',\n    clicks: 0,\n    prevClicks: 1,\n    clicksPercent: 0,\n    impressions: 12,\n    impressionsPercent: -147.25274725274727,\n    prevImpressions: 79,\n  },\n  {\n    url: '/nuxt-seo/guides/going-live',\n    clicks: 0,\n    prevClicks: 0,\n    clicksPercent: 0,\n    impressions: 17,\n    impressionsPercent: 0,\n    prevImpressions: 0,\n  },\n  {\n    url: '/nuxt-seo/guides/i18n',\n    clicks: 0,\n    prevClicks: 0,\n    clicksPercent: 0,\n    impressions: 17,\n    impressionsPercent: 0,\n    prevImpressions: 0,\n  },\n  {\n    url: '/nuxt-seo/guides/using-the-modules',\n    clicks: 0,\n    prevClicks: 0,\n    clicksPercent: 0,\n    impressions: 12,\n    impressionsPercent: 142.85714285714286,\n    prevImpressions: 2,\n  },\n  {\n    url: '/nuxt-seo/migration-guide/nuxt-seo-kit',\n    clicks: 0,\n    prevClicks: 0,\n    clicksPercent: 0,\n    impressions: 5,\n    impressionsPercent: 0,\n    prevImpressions: 0,\n  },\n  {\n    url: '/nuxt-seo/migration-guide/nuxt-seo-kit',\n    clicks: 0,\n    prevClicks: 0,\n    clicksPercent: 0,\n    impressions: 5,\n    impressionsPercent: 0,\n    prevImpressions: 0,\n  },\n  {\n    url: '/nuxt-seo/migration-guide/nuxt-seo-kit',\n    clicks: 0,\n    prevClicks: 0,\n    clicksPercent: 0,\n    impressions: 5,\n    impressionsPercent: 0,\n    prevImpressions: 0,\n  },\n  {\n    url: '/nuxt-seo/migration-guide/nuxt-seo-kit',\n    clicks: 0,\n    prevClicks: 0,\n    clicksPercent: 0,\n    impressions: 5,\n    impressionsPercent: 0,\n    prevImpressions: 0,\n  },\n  {\n    url: '/og-image/api/define-og-image',\n    clicks: 0,\n    prevClicks: 0,\n    clicksPercent: 0,\n    impressions: 36,\n    impressionsPercent: 160,\n    prevImpressions: 4,\n  },\n  {\n    url: '/og-image/api/define-og-image',\n    clicks: 0,\n    prevClicks: 0,\n    clicksPercent: 0,\n    impressions: 36,\n    impressionsPercent: 160,\n    prevImpressions: 4,\n  },\n  {\n    url: '/og-image/api/define-og-image',\n    clicks: 0,\n    prevClicks: 0,\n    clicksPercent: 0,\n    impressions: 36,\n    impressionsPercent: 160,\n    prevImpressions: 4,\n  },\n  {\n    url: '/og-image/api/nuxt-hooks',\n    clicks: 0,\n    prevClicks: 0,\n    clicksPercent: 0,\n    impressions: 6,\n    impressionsPercent: 0,\n    prevImpressions: 0,\n  },\n  {\n    url: '/og-image/api/nuxt-seo-template',\n    clicks: 0,\n    prevClicks: 1,\n    clicksPercent: 0,\n    impressions: 2,\n    impressionsPercent: -66.66666666666666,\n    prevImpressions: 4,\n  },\n  {\n    url: '/og-image/getting-started/examples',\n    clicks: 0,\n    prevClicks: 0,\n    clicksPercent: 0,\n    impressions: 2,\n    impressionsPercent: -66.66666666666666,\n    prevImpressions: 4,\n  },\n  {\n    url: '/og-image/getting-started/stackblitz',\n    clicks: 0,\n    prevClicks: 2,\n    clicksPercent: 0,\n    impressions: 20,\n    impressionsPercent: -36.734693877551024,\n    prevImpressions: 29,\n  },\n  {\n    url: '/og-image/guides/compatibility',\n    clicks: 0,\n    prevClicks: 0,\n    clicksPercent: 0,\n    impressions: 5,\n    impressionsPercent: 0,\n    prevImpressions: 0,\n  },\n  {\n    url: '/og-image/guides/jpegs',\n    clicks: 0,\n    prevClicks: 0,\n    clicksPercent: 0,\n    impressions: 2,\n    impressionsPercent: 0,\n    prevImpressions: 0,\n  },\n  {\n    url: '/og-image/guides/styling',\n    clicks: 0,\n    prevClicks: 0,\n    clicksPercent: 0,\n    impressions: 5,\n    impressionsPercent: 0,\n    prevImpressions: 0,\n  },\n  {\n    url: '/og-image/integrations/color-mode',\n    clicks: 0,\n    prevClicks: 0,\n    clicksPercent: 0,\n    impressions: 59,\n    impressionsPercent: 147.05882352941177,\n    prevImpressions: 9,\n  },\n  {\n    url: '/og-image/migration-guide/v3',\n    clicks: 0,\n    prevClicks: 0,\n    clicksPercent: 0,\n    impressions: 2,\n    impressionsPercent: 0,\n    prevImpressions: 0,\n  },\n  {\n    url: '/og-image/releases/v3',\n    clicks: 0,\n    prevClicks: 0,\n    clicksPercent: 0,\n    impressions: 1,\n    impressionsPercent: -160,\n    prevImpressions: 9,\n  },\n  {\n    url: '/og-image/releases/v3',\n    clicks: 0,\n    prevClicks: 0,\n    clicksPercent: 0,\n    impressions: 1,\n    impressionsPercent: -160,\n    prevImpressions: 9,\n  },\n  {\n    url: '/og-image/releases/v3',\n    clicks: 0,\n    prevClicks: 0,\n    clicksPercent: 0,\n    impressions: 1,\n    impressionsPercent: -160,\n    prevImpressions: 9,\n  },\n  {\n    url: '/robots/api/config',\n    clicks: 0,\n    prevClicks: 0,\n    clicksPercent: 0,\n    impressions: 4,\n    impressionsPercent: 0,\n    prevImpressions: 0,\n  },\n  {\n    url: '/robots/api/config',\n    clicks: 0,\n    prevClicks: 0,\n    clicksPercent: 0,\n    impressions: 4,\n    impressionsPercent: 0,\n    prevImpressions: 0,\n  },\n  {\n    url: '/robots/getting-started/stackblitz',\n    clicks: 0,\n    prevClicks: 0,\n    clicksPercent: 0,\n    impressions: 5,\n    impressionsPercent: -18.181818181818183,\n    prevImpressions: 6,\n  },\n  {\n    url: '/robots/integrations',\n    clicks: 0,\n    prevClicks: 0,\n    clicksPercent: 0,\n    impressions: 4,\n    impressionsPercent: 0,\n    prevImpressions: 0,\n  },\n  {\n    url: '/robots/integrations/content',\n    clicks: 0,\n    prevClicks: 1,\n    clicksPercent: 0,\n    impressions: 118,\n    impressionsPercent: -9.67741935483871,\n    prevImpressions: 130,\n  },\n  {\n    url: '/robots/nitro-api/nitro-hooks',\n    clicks: 0,\n    prevClicks: 0,\n    clicksPercent: 0,\n    impressions: 9,\n    impressionsPercent: 0,\n    prevImpressions: 0,\n  },\n  {\n    url: '/robots/releases',\n    clicks: 0,\n    prevClicks: 0,\n    clicksPercent: 0,\n    impressions: 1,\n    impressionsPercent: 0,\n    prevImpressions: 0,\n  },\n  {\n    url: '/robots/releases/v3',\n    clicks: 0,\n    prevClicks: 0,\n    clicksPercent: 0,\n    impressions: 3,\n    impressionsPercent: 0,\n    prevImpressions: 0,\n  },\n  {\n    url: '/schema-org/api/config',\n    clicks: 0,\n    prevClicks: 0,\n    clicksPercent: 0,\n    impressions: 68,\n    impressionsPercent: 21.138211382113823,\n    prevImpressions: 55,\n  },\n  {\n    url: '/schema-org/getting-started',\n    clicks: 0,\n    prevClicks: 0,\n    clicksPercent: 0,\n    impressions: 1,\n    impressionsPercent: 0,\n    prevImpressions: 0,\n  },\n  {\n    url: '/schema-org/getting-started/stackblitz',\n    clicks: 0,\n    prevClicks: 1,\n    clicksPercent: 0,\n    impressions: 9,\n    impressionsPercent: 127.27272727272727,\n    prevImpressions: 2,\n  },\n  {\n    url: '/schema-org/guides/debugging',\n    clicks: 0,\n    prevClicks: 0,\n    clicksPercent: 0,\n    impressions: 11,\n    impressionsPercent: 0,\n    prevImpressions: 0,\n  },\n  {\n    url: '/schema-org/guides/full-documentation',\n    clicks: 0,\n    prevClicks: 0,\n    clicksPercent: 0,\n    impressions: 5,\n    impressionsPercent: 133.33333333333331,\n    prevImpressions: 1,\n  },\n  {\n    url: '/schema-org/guides/quick-setup',\n    clicks: 0,\n    prevClicks: 0,\n    clicksPercent: 0,\n    impressions: 47,\n    impressionsPercent: 32.098765432098766,\n    prevImpressions: 34,\n  },\n  {\n    url: '/site-config/api/create-site-path-resolver',\n    clicks: 0,\n    prevClicks: 0,\n    clicksPercent: 0,\n    impressions: 5,\n    impressionsPercent: 0,\n    prevImpressions: 0,\n  },\n  {\n    url: '/sitemap/getting-started/stackblitz',\n    clicks: 0,\n    prevClicks: 0,\n    clicksPercent: 0,\n    impressions: 4,\n    impressionsPercent: 120,\n    prevImpressions: 1,\n  },\n  {\n    url: '/sitemap/guides/multi-sitemaps',\n    clicks: 0,\n    prevClicks: 0,\n    clicksPercent: 0,\n    impressions: 6,\n    impressionsPercent: 142.85714285714286,\n    prevImpressions: 1,\n  },\n  {\n    url: '/sitemap/guides/multi-sitemaps',\n    clicks: 0,\n    prevClicks: 0,\n    clicksPercent: 0,\n    impressions: 6,\n    impressionsPercent: 142.85714285714286,\n    prevImpressions: 1,\n  },\n  {\n    url: '/sitemap/guides/multi-sitemaps',\n    clicks: 0,\n    prevClicks: 0,\n    clicksPercent: 0,\n    impressions: 6,\n    impressionsPercent: 142.85714285714286,\n    prevImpressions: 1,\n  },\n  {\n    url: '/sitemap/releases/v3',\n    clicks: 0,\n    prevClicks: 0,\n    clicksPercent: 0,\n    impressions: 3,\n    impressionsPercent: 100,\n    prevImpressions: 1,\n  },\n  {\n    url: '/sitemap/releases/v3',\n    clicks: 0,\n    prevClicks: 0,\n    clicksPercent: 0,\n    impressions: 3,\n    impressionsPercent: 100,\n    prevImpressions: 1,\n  },\n  {\n    url: '/sitemap/releases/v4',\n    clicks: 0,\n    prevClicks: 0,\n    clicksPercent: 0,\n    impressions: 47,\n    impressionsPercent: 23.809523809523807,\n    prevImpressions: 37,\n  },\n  {\n    url: '/sitemap/releases/v4',\n    clicks: 0,\n    prevClicks: 0,\n    clicksPercent: 0,\n    impressions: 47,\n    impressionsPercent: 23.809523809523807,\n    prevImpressions: 37,\n  },\n]\n\nexport const NuxtSeoKeywords = [\n  {\n    keyword: 'nuxt seo',\n    position: 2.369410861638841,\n    positionPercent: 2.9748164400652812,\n    prevPosition: 2.2999582811848143,\n    ctr: 0.07944732297063903,\n    ctrPercent: 2.895430519328157,\n    prevCtr: 0.07717980809345015,\n    clicks: 414,\n    impressions: 5211,\n  },\n  {\n    keyword: 'nuxtseo',\n    position: 2.087403598971722,\n    positionPercent: 2.2153622662066197,\n    prevPosition: 2.041666666666667,\n    ctr: 0.14395886889460155,\n    ctrPercent: 15.9566158584213,\n    prevCtr: 0.12268518518518519,\n    clicks: 56,\n    impressions: 389,\n  },\n  {\n    keyword: 'nuxt-simple-sitemap',\n    position: 2.8443579766536966,\n    positionPercent: -4.286851327435932,\n    prevPosition: 2.968962172647915,\n    ctr: 0.03501945525291829,\n    ctrPercent: -87.2642225799593,\n    prevCtr: 0.0892337536372454,\n    clicks: 45,\n    impressions: 1285,\n  },\n  {\n    keyword: 'nuxt simple sitemap',\n    position: 2.896030245746692,\n    positionPercent: -2.823958737766958,\n    prevPosition: 2.978984238178634,\n    ctr: 0.06427221172022685,\n    ctrPercent: -39.91424271784616,\n    prevCtr: 0.09632224168126094,\n    clicks: 34,\n    impressions: 529,\n  },\n  {\n    keyword: 'nuxt seo kit',\n    position: 2.7309941520467835,\n    positionPercent: -9.916090188189237,\n    prevPosition: 3.015929203539823,\n    ctr: 0.08771929824561403,\n    ctrPercent: 31.699815460323965,\n    prevCtr: 0.06371681415929203,\n    clicks: 30,\n    impressions: 342,\n  },\n  {\n    keyword: 'nuxt sitemap',\n    position: 8.979700854700855,\n    positionPercent: 2.8856875209472865,\n    prevPosition: 8.724260355029585,\n    ctr: 0.029914529914529916,\n    ctrPercent: 39.15900131406045,\n    prevCtr: 0.020118343195266272,\n    clicks: 28,\n    impressions: 936,\n  },\n  {\n    keyword: 'nuxt-simple-robots',\n    position: 3.494071146245059,\n    positionPercent: 1.986636855913256,\n    prevPosition: 3.425339366515837,\n    ctr: 0.10276679841897234,\n    ctrPercent: 3.1824611032531926,\n    prevCtr: 0.09954751131221719,\n    clicks: 26,\n    impressions: 253,\n  },\n  {\n    keyword: 'definesitemapeventhandler',\n    position: 1.9903846153846154,\n    positionPercent: -1.0014019627478599,\n    prevPosition: 2.010416666666667,\n    ctr: 0.20192307692307693,\n    ctrPercent: 2.0040080160320706,\n    prevCtr: 0.19791666666666666,\n    clicks: 21,\n    impressions: 104,\n  },\n  {\n    keyword: 'nuxt seo module',\n    position: 1.6939890710382515,\n    positionPercent: 0.16617210682493275,\n    prevPosition: 1.6911764705882353,\n    ctr: 0.11475409836065574,\n    ctrPercent: 18.22349570200574,\n    prevCtr: 0.09558823529411764,\n    clicks: 21,\n    impressions: 183,\n  },\n  {\n    keyword: 'nuxt-seo',\n    position: 2.1271676300578033,\n    positionPercent: -1.6917755871342168,\n    prevPosition: 2.1634615384615383,\n    ctr: 0.12138728323699421,\n    ctrPercent: -29.541463414634155,\n    prevCtr: 0.16346153846153846,\n    clicks: 21,\n    impressions: 173,\n  },\n  {\n    keyword: 'nuxt seo sitemap',\n    position: 2.166666666666667,\n    positionPercent: -15.647921760391178,\n    prevPosition: 2.5344827586206895,\n    ctr: 0.16666666666666666,\n    ctrPercent: -21.538461538461544,\n    prevCtr: 0.20689655172413793,\n    clicks: 20,\n    impressions: 120,\n  },\n  {\n    keyword: 'defineogimage',\n    position: 1.2972972972972974,\n    positionPercent: -46.41428704306576,\n    prevPosition: 2.0813953488372094,\n    ctr: 0.12162162162162163,\n    ctrPercent: -21.658986175115196,\n    prevCtr: 0.1511627906976744,\n    clicks: 18,\n    impressions: 148,\n  },\n  {\n    keyword: '@nuxtseo/module',\n    position: 1.353448275862069,\n    positionPercent: -23.17009093971035,\n    prevPosition: 1.7081339712918662,\n    ctr: 0.10344827586206896,\n    ctrPercent: 12.903225806451607,\n    prevCtr: 0.09090909090909091,\n    clicks: 12,\n    impressions: 116,\n  },\n  {\n    keyword: 'nuxt simple robots',\n    position: 3.4324324324324325,\n    positionPercent: 8.887606406002021,\n    prevPosition: 3.1403508771929824,\n    ctr: 0.16216216216216217,\n    ctrPercent: 20.823244552058124,\n    prevCtr: 0.13157894736842105,\n    clicks: 12,\n    impressions: 74,\n  },\n  {\n    keyword: 'nuxt image cache',\n    position: 4.5,\n    positionPercent: 40,\n    prevPosition: 3,\n    ctr: 0.22727272727272727,\n    ctrPercent: -0.56980056980057,\n    prevCtr: 0.22857142857142856,\n    clicks: 10,\n    impressions: 44,\n  },\n  {\n    keyword: '@nuxtjs/sitemap',\n    position: 7.691029900332226,\n    positionPercent: -15.370164931425029,\n    prevPosition: 8.971563981042653,\n    ctr: 0.026578073089700997,\n    ctrPercent: 139.46706887883357,\n    prevCtr: 0.004739336492890996,\n    clicks: 8,\n    impressions: 301,\n  },\n  {\n    keyword: 'nuxt site config',\n    position: 5.916666666666667,\n    positionPercent: -5.200191418089011,\n    prevPosition: 6.232558139534884,\n    ctr: 0.14583333333333334,\n    ctrPercent: 4.4142614601018755,\n    prevCtr: 0.13953488372093023,\n    clicks: 7,\n    impressions: 48,\n  },\n  {\n    keyword: 'nuxt 3 seo',\n    position: 9.937007874015748,\n    positionPercent: 46.33621386143734,\n    prevPosition: 6.198675496688741,\n    ctr: 0.047244094488188976,\n    ctrPercent: -42.64003473729917,\n    prevCtr: 0.0728476821192053,\n    clicks: 6,\n    impressions: 127,\n  },\n  {\n    keyword: 'nuxt-seo-kit',\n    position: 2.7515151515151515,\n    positionPercent: -5.361482570819672,\n    prevPosition: 2.9031007751937983,\n    ctr: 0.030303030303030304,\n    ctrPercent: -56.666666666666664,\n    prevCtr: 0.05426356589147287,\n    clicks: 5,\n    impressions: 165,\n  },\n  {\n    keyword: 'nuxt_public_site_url',\n    position: 5.16,\n    positionPercent: 8.115375213884134,\n    prevPosition: 4.757575757575758,\n    ctr: 0.1,\n    ctrPercent: 49.056603773584904,\n    prevCtr: 0.06060606060606061,\n    clicks: 5,\n    impressions: 50,\n  },\n  {\n    keyword: 'nuxtjs sitemap',\n    position: 7.231343283582089,\n    positionPercent: -23.622118196194688,\n    prevPosition: 9.168316831683168,\n    ctr: 0.03731343283582089,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 5,\n    impressions: 134,\n  },\n  {\n    keyword: '@nuxtjs/seo',\n    position: 6.404761904761905,\n    positionPercent: -67.18571971135586,\n    prevPosition: 12.884615384615385,\n    ctr: 0.09523809523809523,\n    ctrPercent: 84.93150684931507,\n    prevCtr: 0.038461538461538464,\n    clicks: 4,\n    impressions: 42,\n  },\n  {\n    keyword: 'nuxt robots',\n    position: 6.512195121951219,\n    positionPercent: 8.747926350541194,\n    prevPosition: 5.966386554621849,\n    ctr: 0.032520325203252036,\n    ctrPercent: 25.325443786982255,\n    prevCtr: 0.025210084033613446,\n    clicks: 4,\n    impressions: 123,\n  },\n  {\n    keyword: 'nuxtjs/sitemap',\n    position: 7.146067415730337,\n    positionPercent: -11.2759643916914,\n    prevPosition: 8,\n    ctr: 0.0449438202247191,\n    ctrPercent: 99.15014164305948,\n    prevCtr: 0.015151515151515152,\n    clicks: 4,\n    impressions: 89,\n  },\n  {\n    keyword: 'seo nuxt',\n    position: 3.633663366336634,\n    positionPercent: 40.62553919031262,\n    prevPosition: 2.4066985645933014,\n    ctr: 0.039603960396039604,\n    ctrPercent: -18.851570964247017,\n    prevCtr: 0.04784688995215311,\n    clicks: 4,\n    impressions: 101,\n  },\n  {\n    keyword: 'seo nuxt 3',\n    position: 7.666666666666667,\n    positionPercent: 15.006150061500618,\n    prevPosition: 6.5964912280701755,\n    ctr: 0.12121212121212122,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 4,\n    impressions: 33,\n  },\n  {\n    keyword: 'nuxt og image',\n    position: 6.7615384615384615,\n    positionPercent: -3.6255090403055106,\n    prevPosition: 7.011204481792717,\n    ctr: 0.007692307692307693,\n    ctrPercent: -74.40633245382585,\n    prevCtr: 0.01680672268907563,\n    clicks: 3,\n    impressions: 390,\n  },\n  {\n    keyword: 'nuxt prerender routes',\n    position: 5.65625,\n    positionPercent: 64.42658875091307,\n    prevPosition: 2.9,\n    ctr: 0.09375,\n    ctrPercent: -95.95375722543352,\n    prevCtr: 0.26666666666666666,\n    clicks: 3,\n    impressions: 32,\n  },\n  {\n    keyword: 'nuxt schema org',\n    position: 4.87719298245614,\n    positionPercent: -1.2289129706178068,\n    prevPosition: 4.9375,\n    ctr: 0.05263157894736842,\n    ctrPercent: 23.255813953488374,\n    prevCtr: 0.041666666666666664,\n    clicks: 3,\n    impressions: 57,\n  },\n  {\n    keyword: 'nuxt titletemplate',\n    position: 8.147058823529413,\n    positionPercent: -12.253634174618739,\n    prevPosition: 9.210526315789474,\n    ctr: 0.08823529411764706,\n    ctrPercent: 108.10810810810811,\n    prevCtr: 0.02631578947368421,\n    clicks: 3,\n    impressions: 34,\n  },\n  {\n    keyword: 'nuxt v4',\n    position: 5.735849056603773,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0.05660377358490566,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 3,\n    impressions: 53,\n  },\n  {\n    keyword: 'nuxt3 seo',\n    position: 13.166666666666666,\n    positionPercent: 43.07692307692307,\n    prevPosition: 8.5,\n    ctr: 0.5,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 3,\n    impressions: 6,\n  },\n  {\n    keyword: 'useseometa',\n    position: 8.297777777777778,\n    positionPercent: -67.9955402061295,\n    prevPosition: 16.846153846153847,\n    ctr: 0.013333333333333334,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 3,\n    impressions: 225,\n  },\n  {\n    keyword: 'nuxt hooks',\n    position: 13.938461538461539,\n    positionPercent: -59.12688557514767,\n    prevPosition: 25.63888888888889,\n    ctr: 0.015384615384615385,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 2,\n    impressions: 130,\n  },\n  {\n    keyword: 'nuxt robots.txt',\n    position: 10.084615384615384,\n    positionPercent: -20.275550071972035,\n    prevPosition: 12.36,\n    ctr: 0.015384615384615385,\n    ctrPercent: 14.285714285714285,\n    prevCtr: 0.013333333333333334,\n    clicks: 2,\n    impressions: 130,\n  },\n  {\n    keyword: 'nuxt routerules',\n    position: 10.96938775510204,\n    positionPercent: -0.5565862708719879,\n    prevPosition: 11.03061224489796,\n    ctr: 0.02040816326530612,\n    ctrPercent: 66.66666666666666,\n    prevCtr: 0.01020408163265306,\n    clicks: 2,\n    impressions: 98,\n  },\n  {\n    keyword: 'nuxt-site-config',\n    position: 5.477611940298507,\n    positionPercent: 5.474795544980093,\n    prevPosition: 5.185714285714286,\n    ctr: 0.029850746268656716,\n    ctrPercent: -17.88617886178861,\n    prevCtr: 0.03571428571428571,\n    clicks: 2,\n    impressions: 67,\n  },\n  {\n    keyword: 'nuxtseo/module',\n    position: 1,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0.16666666666666666,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 2,\n    impressions: 12,\n  },\n  {\n    keyword: 'sitemap nuxt',\n    position: 7.5058823529411764,\n    positionPercent: -22.541087649974063,\n    prevPosition: 9.412698412698413,\n    ctr: 0.023529411764705882,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 2,\n    impressions: 85,\n  },\n  {\n    keyword: 'defineogimagecomponent',\n    position: 1.4,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0.2,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 1,\n    impressions: 5,\n  },\n  {\n    keyword: 'google fonts nuxt',\n    position: 26,\n    positionPercent: -6.511627906976744,\n    prevPosition: 27.75,\n    ctr: 0.3333333333333333,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 1,\n    impressions: 3,\n  },\n  {\n    keyword: 'i18n nuxt',\n    position: 24.033898305084747,\n    positionPercent: -17.237805780769662,\n    prevPosition: 28.56756756756757,\n    ctr: 0.00847457627118644,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 1,\n    impressions: 118,\n  },\n  {\n    keyword: 'nitro prerender',\n    position: 10.108695652173912,\n    positionPercent: 20.2039121446115,\n    prevPosition: 8.253731343283583,\n    ctr: 0.010869565217391304,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 1,\n    impressions: 92,\n  },\n  {\n    keyword: 'nitro route rules',\n    position: 10.484848484848484,\n    positionPercent: 4.022322838791196,\n    prevPosition: 10.071428571428571,\n    ctr: 0.030303030303030304,\n    ctrPercent: -16.39344262295081,\n    prevCtr: 0.03571428571428571,\n    clicks: 1,\n    impressions: 33,\n  },\n  {\n    keyword: 'nuxt canonical',\n    position: 12.181818181818182,\n    positionPercent: -14.675374567806374,\n    prevPosition: 14.11111111111111,\n    ctr: 0.030303030303030304,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 1,\n    impressions: 33,\n  },\n  {\n    keyword: 'nuxt canonical url',\n    position: 8.26,\n    positionPercent: -23.544670786602573,\n    prevPosition: 10.464285714285714,\n    ctr: 0.02,\n    ctrPercent: -56.4102564102564,\n    prevCtr: 0.03571428571428571,\n    clicks: 1,\n    impressions: 50,\n  },\n  {\n    keyword: 'nuxt config hooks',\n    position: 9.947368421052632,\n    positionPercent: 12.807881773399018,\n    prevPosition: 8.75,\n    ctr: 0.05263157894736842,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 1,\n    impressions: 19,\n  },\n  {\n    keyword: 'nuxt config routerules',\n    position: 11.9,\n    positionPercent: 32.76283618581906,\n    prevPosition: 8.55,\n    ctr: 0.05,\n    ctrPercent: 0,\n    prevCtr: 0.05,\n    clicks: 1,\n    impressions: 20,\n  },\n  {\n    keyword: 'nuxt content i18n',\n    position: 24.627906976744185,\n    positionPercent: -8.753386094438842,\n    prevPosition: 26.88235294117647,\n    ctr: 0.011627906976744186,\n    ctrPercent: -23.376623376623375,\n    prevCtr: 0.014705882352941176,\n    clicks: 1,\n    impressions: 86,\n  },\n  {\n    keyword: 'nuxt js seo',\n    position: 12.421052631578947,\n    positionPercent: 60.429877295561226,\n    prevPosition: 6.656716417910448,\n    ctr: 0.02631578947368421,\n    ctrPercent: -95.71984435797664,\n    prevCtr: 0.07462686567164178,\n    clicks: 1,\n    impressions: 38,\n  },\n  {\n    keyword: 'nuxt js sitemap',\n    position: 8,\n    positionPercent: -6.0606060606060606,\n    prevPosition: 8.5,\n    ctr: 0.02040816326530612,\n    ctrPercent: -84.05797101449278,\n    prevCtr: 0.05,\n    clicks: 1,\n    impressions: 49,\n  },\n  {\n    keyword: 'nuxt publicruntimeconfig',\n    position: 20.125,\n    positionPercent: 15.673141326188883,\n    prevPosition: 17.2,\n    ctr: 0.125,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 1,\n    impressions: 8,\n  },\n  {\n    keyword: 'nuxt schema',\n    position: 8.765625,\n    positionPercent: 6.935233258801679,\n    prevPosition: 8.178082191780822,\n    ctr: 0.015625,\n    ctrPercent: 13.138686131386867,\n    prevCtr: 0.0136986301369863,\n    clicks: 1,\n    impressions: 64,\n  },\n  {\n    keyword: 'nuxt title template',\n    position: 7.409090909090909,\n    positionPercent: 4.69849518241854,\n    prevPosition: 7.068965517241379,\n    ctr: 0.045454545454545456,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 1,\n    impressions: 22,\n  },\n  {\n    keyword: 'nuxt-og-image',\n    position: 6.4021739130434785,\n    positionPercent: -9.060324413221634,\n    prevPosition: 7.009756097560976,\n    ctr: 0.0036231884057971015,\n    ctrPercent: -120.61955469506292,\n    prevCtr: 0.014634146341463415,\n    clicks: 1,\n    impressions: 276,\n  },\n  {\n    keyword: 'nuxt-schema-org',\n    position: 5.565656565656566,\n    positionPercent: 15.088139738854355,\n    prevPosition: 4.784810126582279,\n    ctr: 0.010101010101010102,\n    ctrPercent: -115.95744680851064,\n    prevCtr: 0.0379746835443038,\n    clicks: 1,\n    impressions: 99,\n  },\n  {\n    keyword: 'nuxt.config.ts',\n    position: 40,\n    positionPercent: 19.17808219178082,\n    prevPosition: 33,\n    ctr: 0.5,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 1,\n    impressions: 2,\n  },\n  {\n    keyword: 'nuxt_site_env',\n    position: 1.8333333333333335,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0.16666666666666666,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 1,\n    impressions: 6,\n  },\n  {\n    keyword: 'nuxtjs seo',\n    position: 8.971428571428572,\n    positionPercent: -10.408873137756784,\n    prevPosition: 9.956521739130435,\n    ctr: 0.02857142857142857,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 1,\n    impressions: 35,\n  },\n  {\n    keyword: 'nuxtjs/seo',\n    position: 8.333333333333332,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0.3333333333333333,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 1,\n    impressions: 3,\n  },\n  {\n    keyword: 'opengraph image url',\n    position: 17,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 1,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 1,\n    impressions: 1,\n  },\n  {\n    keyword: '\"seo\"',\n    position: 92,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 1,\n  },\n  {\n    keyword: '<meta name=\"robots\" content=\"max-image-preview:large\">',\n    position: 87,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 3,\n  },\n  {\n    keyword: '<meta property=\"og:image\" content=',\n    position: 58,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 1,\n  },\n  {\n    keyword: '<meta property=\"og:image\" content=\"',\n    position: 89,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 1,\n  },\n  {\n    keyword: '@nuxt/content',\n    position: 25.5,\n    positionPercent: -9.345794392523365,\n    prevPosition: 28,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 4,\n  },\n  {\n    keyword: '@nuxt/devtools',\n    position: 75,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 2,\n  },\n  {\n    keyword: '@nuxt/i18n',\n    position: 20,\n    positionPercent: 5.128205128205128,\n    prevPosition: 19,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 1,\n  },\n  {\n    keyword: '@nuxt/schema',\n    position: 8.25531914893617,\n    positionPercent: 19.11733946873039,\n    prevPosition: 6.814814814814815,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 47,\n  },\n  {\n    keyword: '@nuxtjs/color-mode',\n    position: 60,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 2,\n  },\n  {\n    keyword: '@nuxtjs/i18n',\n    position: 24.225806451612904,\n    positionPercent: -41.72813487881981,\n    prevPosition: 37,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 31,\n  },\n  {\n    keyword: '@nuxtjs/robots',\n    position: 6.3125,\n    positionPercent: -35.10204081632653,\n    prevPosition: 9,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 16,\n  },\n  {\n    keyword: '@nuxtjs/sitmeap',\n    position: 7,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 1,\n  },\n  {\n    keyword: '@unhead/vue',\n    position: 47,\n    positionPercent: 16.091954022988507,\n    prevPosition: 40,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 1,\n  },\n  {\n    keyword: 'adsbot login',\n    position: 21.583333333333332,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 12,\n  },\n  {\n    keyword: 'app config nuxt',\n    position: 21,\n    positionPercent: -47.27272727272727,\n    prevPosition: 34,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 1,\n  },\n  {\n    keyword: 'article:published_time',\n    position: 94,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 2,\n  },\n  {\n    keyword: 'best sitemap',\n    position: 150,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 1,\n  },\n  {\n    keyword: 'best sitemaps',\n    position: 101,\n    positionPercent: 5.912334352701323,\n    prevPosition: 95.2,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 2,\n  },\n  {\n    keyword: 'cache engine',\n    position: 78,\n    positionPercent: -1.2738853503184715,\n    prevPosition: 79,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 1,\n  },\n  {\n    keyword: 'cache images',\n    position: 73,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 1,\n  },\n  {\n    keyword: 'cache photography',\n    position: 50,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 1,\n  },\n  {\n    keyword: 'cache seo',\n    position: 88.8,\n    positionPercent: -5.26315789473684,\n    prevPosition: 93.6,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 5,\n  },\n  {\n    keyword: 'canonical redirect',\n    position: 70.4,\n    positionPercent: -5.297308796936482,\n    prevPosition: 74.23076923076923,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 20,\n  },\n  {\n    keyword: 'cf_pages_url',\n    position: 17.333333333333332,\n    positionPercent: -0.9569377990430691,\n    prevPosition: 17.5,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 3,\n  },\n  {\n    keyword: 'cfg simple',\n    position: 65,\n    positionPercent: -16.901408450704224,\n    prevPosition: 77,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 1,\n  },\n  {\n    keyword: 'check open graph image',\n    position: 88,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 1,\n  },\n  {\n    keyword: 'chromium binary',\n    position: 46.5,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 2,\n  },\n  {\n    keyword: 'chromium rendering',\n    position: 50,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 1,\n  },\n  {\n    keyword: 'clear cache nuxt',\n    position: 42.5,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 2,\n  },\n  {\n    keyword: 'cloudflare sitemap',\n    position: 64,\n    positionPercent: 72.3404255319149,\n    prevPosition: 30,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 1,\n  },\n  {\n    keyword: 'colormode nuxt',\n    position: 26.2,\n    positionPercent: -76.99530516431923,\n    prevPosition: 59,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 5,\n  },\n  {\n    keyword: 'com sitemap',\n    position: 97.5,\n    positionPercent: 13.698630136986301,\n    prevPosition: 85,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 2,\n  },\n  {\n    keyword: 'config simple',\n    position: 79,\n    positionPercent: 47.84313725490196,\n    prevPosition: 48.5,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 2,\n  },\n  {\n    keyword: 'config site',\n    position: 10,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 1,\n  },\n  {\n    keyword: 'create og image',\n    position: 20.5,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 2,\n  },\n  {\n    keyword: 'custom robot txt',\n    position: 114,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 1,\n  },\n  {\n    keyword: 'debug nuxt',\n    position: 21,\n    positionPercent: 4.878048780487805,\n    prevPosition: 20,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 1,\n  },\n  {\n    keyword: 'defineeventhandler nuxt',\n    position: 32,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 2,\n  },\n  {\n    keyword: 'definei18nroute',\n    position: 6.833333333333333,\n    positionPercent: -1.1776931070315295,\n    prevPosition: 6.914285714285715,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 66,\n  },\n  {\n    keyword: 'definenitroplugin',\n    position: 13.696428571428571,\n    positionPercent: 5.482805607519809,\n    prevPosition: 12.96551724137931,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 56,\n  },\n  {\n    keyword: 'definenitroplugin is not defined',\n    position: 14.5,\n    positionPercent: 41.66666666666667,\n    prevPosition: 9.5,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 4,\n  },\n  {\n    keyword: 'definenuxtmodule',\n    position: 28,\n    positionPercent: 11.320754716981133,\n    prevPosition: 25,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 2,\n  },\n  {\n    keyword: 'definepagemeta',\n    position: 51.25,\n    positionPercent: 40.00000000000001,\n    prevPosition: 34.166666666666664,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 4,\n  },\n  {\n    keyword: 'definepagemeta title',\n    position: 10.375,\n    positionPercent: 3.6809815950920246,\n    prevPosition: 10,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 8,\n  },\n  {\n    keyword: 'disable nuxt link',\n    position: 38,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 2,\n  },\n  {\n    keyword: 'discord image cache',\n    position: 66,\n    positionPercent: 35.714285714285715,\n    prevPosition: 46,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 1,\n  },\n  {\n    keyword: 'dynamic endpoint',\n    position: 49,\n    positionPercent: -5.9405940594059405,\n    prevPosition: 52,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 1,\n  },\n  {\n    keyword: 'dynamic url',\n    position: 13.1,\n    positionPercent: 27.418943068675006,\n    prevPosition: 9.941176470588236,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 20,\n  },\n  {\n    keyword: 'dynamic url seo',\n    position: 101,\n    positionPercent: 80.55555555555556,\n    prevPosition: 43,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 1,\n  },\n  {\n    keyword: 'dynamic urls',\n    position: 56.4,\n    positionPercent: 193.03135888501743,\n    prevPosition: 1,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 5,\n  },\n  {\n    keyword: 'dynamic urls seo',\n    position: 67.66666666666667,\n    positionPercent: -18.79044856058915,\n    prevPosition: 81.7,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 6,\n  },\n  {\n    keyword: 'easy robots',\n    position: 60,\n    positionPercent: -23.52941176470588,\n    prevPosition: 76,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 1,\n  },\n  {\n    keyword: 'endpoint urls',\n    position: 1,\n    positionPercent: -163.63636363636365,\n    prevPosition: 10,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 2,\n  },\n  {\n    keyword: 'experiments seo',\n    position: 83.375,\n    positionPercent: 3.3020066040132017,\n    prevPosition: 80.66666666666667,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 8,\n  },\n  {\n    keyword: 'generate open graph image',\n    position: 59,\n    positionPercent: -34.96503496503497,\n    prevPosition: 84,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 1,\n  },\n  {\n    keyword: 'get og image from url',\n    position: 56,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 1,\n  },\n  {\n    keyword: 'get site map',\n    position: 94,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 1,\n  },\n  {\n    keyword: 'github seo',\n    position: 63.5,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 2,\n  },\n  {\n    keyword: 'google font api',\n    position: 82,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 2,\n  },\n  {\n    keyword: 'google font inter',\n    position: 79.2,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 5,\n  },\n  {\n    keyword: 'google fonts inter',\n    position: 80.86363636363636,\n    positionPercent: 5.763855421686748,\n    prevPosition: 76.33333333333333,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 22,\n  },\n  {\n    keyword: 'google sitemap best practices',\n    position: 80,\n    positionPercent: 5.178791615289759,\n    prevPosition: 75.96153846153847,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 13,\n  },\n  {\n    keyword: 'google sitemap lastmod',\n    position: 82.6,\n    positionPercent: -5.951448707909175,\n    prevPosition: 87.66666666666667,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 5,\n  },\n  {\n    keyword: 'google-indexing-script',\n    position: 39,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 1,\n  },\n  {\n    keyword: 'googlefonts inter',\n    position: 95,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 1,\n  },\n  {\n    keyword: 'graph image png',\n    position: 29,\n    positionPercent: -65.11627906976744,\n    prevPosition: 57,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 1,\n  },\n  {\n    keyword: 'graph images',\n    position: 87.25,\n    positionPercent: 1.0079193664506838,\n    prevPosition: 86.375,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 8,\n  },\n  {\n    keyword: 'graph photo',\n    position: 91,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 1,\n  },\n  {\n    keyword: 'graph pics',\n    position: 92.5,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 2,\n  },\n  {\n    keyword: 'how many urls in sitemap',\n    position: 53,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 1,\n  },\n  {\n    keyword: 'how to disable indexing',\n    position: 1,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 1,\n  },\n  {\n    keyword: 'how to split large sitemap',\n    position: 36.25,\n    positionPercent: 4.946996466431095,\n    prevPosition: 34.5,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 4,\n  },\n  {\n    keyword: 'https://developers.google.com/search/blog/2023/06/sitemaps-lastmod-ping',\n    position: 13,\n    positionPercent: 8,\n    prevPosition: 12,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 4,\n  },\n  {\n    keyword: 'hyperlink checker',\n    position: 68,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 1,\n  },\n  {\n    keyword: 'i18 nuxt',\n    position: 21.5,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 4,\n  },\n  {\n    keyword: 'i18n alternatives',\n    position: 56,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 1,\n  },\n  {\n    keyword: 'i18n module',\n    position: 55,\n    positionPercent: 4.911092294665531,\n    prevPosition: 52.36363636363637,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 10,\n  },\n  {\n    keyword: 'i18n nuxt config',\n    position: 17.25,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 4,\n  },\n  {\n    keyword: 'i18n nuxt example',\n    position: 58,\n    positionPercent: 26.34146341463415,\n    prevPosition: 44.5,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 8,\n  },\n  {\n    keyword: 'i18n nuxt js',\n    position: 33.38095238095238,\n    positionPercent: -20.083413538658977,\n    prevPosition: 40.833333333333336,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 21,\n  },\n  {\n    keyword: 'i18n nuxt3',\n    position: 28,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 1,\n  },\n  {\n    keyword: 'i18n nuxtjs',\n    position: 26.88235294117647,\n    positionPercent: -0.436681222707422,\n    prevPosition: 27,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 17,\n  },\n  {\n    keyword: 'i18n seo',\n    position: 38.333333333333336,\n    positionPercent: -12.539184952978047,\n    prevPosition: 43.46153846153846,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 6,\n  },\n  {\n    keyword: 'image cache server',\n    position: 34,\n    positionPercent: 10.30927835051546,\n    prevPosition: 30.666666666666668,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 1,\n  },\n  {\n    keyword: 'image caching',\n    position: 93,\n    positionPercent: 7.82122905027933,\n    prevPosition: 86,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 1,\n  },\n  {\n    keyword: 'image open graph',\n    position: 80.54838709677419,\n    positionPercent: -9.695798053776727,\n    prevPosition: 88.7560975609756,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 31,\n  },\n  {\n    keyword: 'index, follow, max-image-preview:large, max-snippet:-1, max-video-preview:-1',\n    position: 42.333333333333336,\n    positionPercent: 34.10138248847927,\n    prevPosition: 30,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 3,\n  },\n  {\n    keyword: 'install nuxt',\n    position: 59,\n    positionPercent: 15.244299674267095,\n    prevPosition: 50.642857142857146,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 27,\n  },\n  {\n    keyword: 'inter google font',\n    position: 81,\n    positionPercent: -9.411764705882353,\n    prevPosition: 89,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 8,\n  },\n  {\n    keyword: 'inter google fonts',\n    position: 83.57142857142857,\n    positionPercent: -11.745776347546261,\n    prevPosition: 94,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 14,\n  },\n  {\n    keyword: 'kv cache explained',\n    position: 69,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 1,\n  },\n  {\n    keyword: 'kv caching',\n    position: 71,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 2,\n  },\n  {\n    keyword: 'lastmod',\n    position: 76.875,\n    positionPercent: 4.585956991802301,\n    prevPosition: 73.42857142857143,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 8,\n  },\n  {\n    keyword: 'lastmod sitemap format',\n    position: 89,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 1,\n  },\n  {\n    keyword: 'link checker',\n    position: 109,\n    positionPercent: -6.222222222222222,\n    prevPosition: 116,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 1,\n  },\n  {\n    keyword: 'link checker seo',\n    position: 136.66666666666666,\n    positionPercent: 41.06739032112166,\n    prevPosition: 90.1025641025641,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 87,\n  },\n  {\n    keyword: 'linkchecker',\n    position: 128,\n    positionPercent: 18.803418803418804,\n    prevPosition: 106,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 2,\n  },\n  {\n    keyword: 'linkcheker',\n    position: 73,\n    positionPercent: -11.612903225806452,\n    prevPosition: 82,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 1,\n  },\n  {\n    keyword: 'max-image-preview:large, max-snippet:-1, max-video-preview:-1',\n    position: 46,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 2,\n  },\n  {\n    keyword: 'max-snippet:-1',\n    position: 51,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 1,\n  },\n  {\n    keyword: 'max-video-preview:-1',\n    position: 27,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 1,\n  },\n  {\n    keyword: 'mehrere sitemaps',\n    position: 49.3125,\n    positionPercent: 10.821643286573146,\n    prevPosition: 44.25,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 16,\n  },\n  {\n    keyword: 'meta image dimensions',\n    position: 78,\n    positionPercent: 1.2903225806451613,\n    prevPosition: 77,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 1,\n  },\n  {\n    keyword: 'meta name image',\n    position: 71.5,\n    positionPercent: 2.1201413427561837,\n    prevPosition: 70,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 2,\n  },\n  {\n    keyword: 'meta og image',\n    position: 86.3125,\n    positionPercent: -7.637840975043534,\n    prevPosition: 93.16666666666667,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 16,\n  },\n  {\n    keyword: 'meta og:image',\n    position: 89.5,\n    positionPercent: 16.314199395770395,\n    prevPosition: 76,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 2,\n  },\n  {\n    keyword: 'meta property og image',\n    position: 85,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 3,\n  },\n  {\n    keyword: 'meta property og image content',\n    position: 87.4,\n    positionPercent: 10.729355033152508,\n    prevPosition: 78.5,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 10,\n  },\n  {\n    keyword: 'meta property=\"og:image',\n    position: 93,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 1,\n  },\n  {\n    keyword: 'meta redirect seo',\n    position: 98,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 1,\n  },\n  {\n    keyword: 'meta tags for images',\n    position: 93,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 1,\n  },\n  {\n    keyword: 'multiple sitemap',\n    position: 26.666666666666668,\n    positionPercent: -20.476858345021032,\n    prevPosition: 32.75,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 3,\n  },\n  {\n    keyword: 'multiple sitemaps',\n    position: 34.4054054054054,\n    positionPercent: -1.33173802216096,\n    prevPosition: 34.86666666666667,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 37,\n  },\n  {\n    keyword: 'next js metadata open graph',\n    position: 71.5,\n    positionPercent: 17.490494296577946,\n    prevPosition: 60,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 2,\n  },\n  {\n    keyword: 'next open graph',\n    position: 86,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 2,\n  },\n  {\n    keyword: 'next.js og image',\n    position: 89,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 1,\n  },\n  {\n    keyword: 'nextjs og image',\n    position: 92,\n    positionPercent: 9.885931558935368,\n    prevPosition: 83.33333333333333,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 1,\n  },\n  {\n    keyword: 'nitro cache',\n    position: 25.666666666666668,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 3,\n  },\n  {\n    keyword: 'nitro cloudflare',\n    position: 41,\n    positionPercent: -19.78021978021978,\n    prevPosition: 50,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 1,\n  },\n  {\n    keyword: 'nitro cloudflare pages',\n    position: 68,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 1,\n  },\n  {\n    keyword: 'nitro hooks',\n    position: 9.333333333333334,\n    positionPercent: 15.384615384615389,\n    prevPosition: 8,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 6,\n  },\n  {\n    keyword: 'nitro middleware',\n    position: 22,\n    positionPercent: -7.299270072992706,\n    prevPosition: 23.666666666666668,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 3,\n  },\n  {\n    keyword: 'nitro nuxt',\n    position: 84,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 2,\n  },\n  {\n    keyword: 'nitro prerender routes',\n    position: 7.666666666666667,\n    positionPercent: 16.069221260815826,\n    prevPosition: 6.526315789473684,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0.05263157894736842,\n    clicks: 0,\n    impressions: 21,\n  },\n  {\n    keyword: 'nitro routerules',\n    position: 10.708333333333334,\n    positionPercent: -9.597575349385407,\n    prevPosition: 11.787878787878787,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 48,\n  },\n  {\n    keyword: 'nitro routes',\n    position: 27,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 2,\n  },\n  {\n    keyword: 'nitro.prerender.routes',\n    position: 5.857142857142857,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 14,\n  },\n  {\n    keyword: 'npm satori',\n    position: 61,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 1,\n  },\n  {\n    keyword: 'nu html checker',\n    position: 91,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 2,\n  },\n  {\n    keyword: 'nuxi generate',\n    position: 25.25,\n    positionPercent: -14.9618320610687,\n    prevPosition: 29.333333333333332,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 4,\n  },\n  {\n    keyword: 'nuxot',\n    position: 4,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 2,\n  },\n  {\n    keyword: 'nuxt',\n    position: 67.63636363636364,\n    positionPercent: -10.251142361234097,\n    prevPosition: 74.94444444444444,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 11,\n  },\n  {\n    keyword: 'nuxt 18n',\n    position: 21.333333333333332,\n    positionPercent: -11.764705882352947,\n    prevPosition: 24,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 3,\n  },\n  {\n    keyword: 'nuxt 2 cache',\n    position: 18,\n    positionPercent: -5.405405405405405,\n    prevPosition: 19,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 1,\n  },\n  {\n    keyword: 'nuxt 2 config',\n    position: 34,\n    positionPercent: 102.22222222222221,\n    prevPosition: 11,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 1,\n  },\n  {\n    keyword: 'nuxt 2 redirect',\n    position: 48,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 2,\n  },\n  {\n    keyword: 'nuxt 2 robots.txt',\n    position: 15.666666666666666,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 6,\n  },\n  {\n    keyword: 'nuxt 2 runtime config',\n    position: 20,\n    positionPercent: 43.65482233502537,\n    prevPosition: 12.833333333333334,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 1,\n  },\n  {\n    keyword: 'nuxt 2 seo',\n    position: 8.357142857142858,\n    positionPercent: -1.302009623549387,\n    prevPosition: 8.466666666666667,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 14,\n  },\n  {\n    keyword: 'nuxt 2 sitemap',\n    position: 17.2,\n    positionPercent: -51.88374596340151,\n    prevPosition: 29.25,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 5,\n  },\n  {\n    keyword: 'nuxt 3 app config',\n    position: 39,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 1,\n  },\n  {\n    keyword: 'nuxt 3 config',\n    position: 41,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 1,\n  },\n  {\n    keyword: 'nuxt 3 env',\n    position: 38,\n    positionPercent: 11.11111111111111,\n    prevPosition: 34,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 3,\n  },\n  {\n    keyword: 'nuxt 3 env production',\n    position: 28,\n    positionPercent: -22.22222222222222,\n    prevPosition: 35,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 1,\n  },\n  {\n    keyword: 'nuxt 3 env variables',\n    position: 48,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 1,\n  },\n  {\n    keyword: 'nuxt 3 environment variables',\n    position: 38.2,\n    positionPercent: 12.475168851807714,\n    prevPosition: 33.714285714285715,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 5,\n  },\n  {\n    keyword: 'nuxt 3 font',\n    position: 39,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 1,\n  },\n  {\n    keyword: 'nuxt 3 og image',\n    position: 10,\n    positionPercent: -26.08695652173913,\n    prevPosition: 13,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 1,\n  },\n  {\n    keyword: 'nuxt 3 process.env',\n    position: 31,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 1,\n  },\n  {\n    keyword: 'nuxt 3 production environment variables',\n    position: 32.5,\n    positionPercent: -1.5267175572519083,\n    prevPosition: 33,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 2,\n  },\n  {\n    keyword: 'nuxt 3 robots',\n    position: 9.833333333333334,\n    positionPercent: -1.6806722689075566,\n    prevPosition: 10,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 6,\n  },\n  {\n    keyword: 'nuxt 3 robots.txt',\n    position: 26.357142857142858,\n    positionPercent: 61.238938053097336,\n    prevPosition: 14,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 14,\n  },\n  {\n    keyword: 'nuxt 3 routerules',\n    position: 40,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 2,\n  },\n  {\n    keyword: 'nuxt 3 runtime config',\n    position: 18,\n    positionPercent: -5.405405405405405,\n    prevPosition: 19,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 1,\n  },\n  {\n    keyword: 'nuxt 3 runtimeconfig',\n    position: 18,\n    positionPercent: -20,\n    prevPosition: 22,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 2,\n  },\n  {\n    keyword: 'nuxt 3 sitemap',\n    position: 95.5,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 2,\n  },\n  {\n    keyword: 'nuxt 4.0',\n    position: 6.428571428571429,\n    positionPercent: -4.878048780487801,\n    prevPosition: 6.75,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 7,\n  },\n  {\n    keyword: 'nuxt access environment variables',\n    position: 49,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 1,\n  },\n  {\n    keyword: 'nuxt add font',\n    position: 25,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 1,\n  },\n  {\n    keyword: 'nuxt add fonts',\n    position: 25,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 1,\n  },\n  {\n    keyword: 'nuxt alternatives',\n    position: 68,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 1,\n  },\n  {\n    keyword: 'nuxt api party',\n    position: 45,\n    positionPercent: 33.00970873786408,\n    prevPosition: 32.25,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 1,\n  },\n  {\n    keyword: 'nuxt api url',\n    position: 39,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 1,\n  },\n  {\n    keyword: 'nuxt app config',\n    position: 36.2,\n    positionPercent: -13.88174807197943,\n    prevPosition: 41.6,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 5,\n  },\n  {\n    keyword: 'nuxt app head',\n    position: 43,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 1,\n  },\n  {\n    keyword: 'nuxt app hooks',\n    position: 55.5,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 2,\n  },\n  {\n    keyword: 'nuxt appconfig',\n    position: 54.5,\n    positionPercent: 84.9673202614379,\n    prevPosition: 22,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 2,\n  },\n  {\n    keyword: 'nuxt assets images',\n    position: 73,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 2,\n  },\n  {\n    keyword: 'nuxt breadcrumb',\n    position: 14,\n    positionPercent: 24,\n    prevPosition: 11,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 2,\n  },\n  {\n    keyword: 'nuxt build environment variables',\n    position: 44,\n    positionPercent: -16.666666666666664,\n    prevPosition: 52,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 1,\n  },\n  {\n    keyword: 'nuxt build hooks',\n    position: 21.5,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 2,\n  },\n  {\n    keyword: 'nuxt cache',\n    position: 25.884615384615383,\n    positionPercent: 7.555898226676941,\n    prevPosition: 24,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 26,\n  },\n  {\n    keyword: 'nuxt cache data',\n    position: 31,\n    positionPercent: 10.16949152542373,\n    prevPosition: 28,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 1,\n  },\n  {\n    keyword: 'nuxt caching',\n    position: 26.4,\n    positionPercent: 28.57142857142856,\n    prevPosition: 19.8,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 10,\n  },\n  {\n    keyword: 'nuxt carousel',\n    position: 57.111111111111114,\n    positionPercent: -3.1688286822076486,\n    prevPosition: 58.95,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 9,\n  },\n  {\n    keyword: 'nuxt change page title',\n    position: 17,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 2,\n  },\n  {\n    keyword: 'nuxt change title',\n    position: 8.75,\n    positionPercent: 8.264462809917356,\n    prevPosition: 8.055555555555555,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 16,\n  },\n  {\n    keyword: 'nuxt chart',\n    position: 44,\n    positionPercent: -18.556701030927837,\n    prevPosition: 53,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 1,\n  },\n  {\n    keyword: 'nuxt clean cache',\n    position: 20,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 1,\n  },\n  {\n    keyword: 'nuxt cloudfare',\n    position: 79.5,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 2,\n  },\n  {\n    keyword: 'nuxt color mode',\n    position: 30.25,\n    positionPercent: -69.1891891891892,\n    prevPosition: 62.25,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 16,\n  },\n  {\n    keyword: 'nuxt color-mode',\n    position: 48,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 3,\n  },\n  {\n    keyword: 'nuxt colormode',\n    position: 23.666666666666668,\n    positionPercent: -32.94117647058823,\n    prevPosition: 33,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 3,\n  },\n  {\n    keyword: 'nuxt config',\n    position: 25.071428571428573,\n    positionPercent: -32.49821045096635,\n    prevPosition: 34.8,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 14,\n  },\n  {\n    keyword: 'nuxt config components',\n    position: 39,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 1,\n  },\n  {\n    keyword: 'nuxt config file',\n    position: 45,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 1,\n  },\n  {\n    keyword: 'nuxt config meta',\n    position: 10.833333333333334,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 6,\n  },\n  {\n    keyword: 'nuxt content',\n    position: 45.375,\n    positionPercent: 16.37296875369587,\n    prevPosition: 38.507936507936506,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 136,\n  },\n  {\n    keyword: 'nuxt content api',\n    position: 70,\n    positionPercent: 45.614035087719294,\n    prevPosition: 44,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 2,\n  },\n  {\n    keyword: 'nuxt content blog',\n    position: 67.5,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 2,\n  },\n  {\n    keyword: 'nuxt content cache',\n    position: 24.6,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 5,\n  },\n  {\n    keyword: 'nuxt content document driven',\n    position: 16,\n    positionPercent: -34.48275862068966,\n    prevPosition: 22.666666666666668,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0.16666666666666666,\n    clicks: 0,\n    impressions: 1,\n  },\n  {\n    keyword: 'nuxt content image',\n    position: 67.5,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 2,\n  },\n  {\n    keyword: 'nuxt content images',\n    position: 80,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 2,\n  },\n  {\n    keyword: 'nuxt content module',\n    position: 51.5,\n    positionPercent: 25.95978062157222,\n    prevPosition: 39.666666666666664,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 6,\n  },\n  {\n    keyword: 'nuxt content pagination',\n    position: 92,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 2,\n  },\n  {\n    keyword: 'nuxt content prerender',\n    position: 27.25,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 8,\n  },\n  {\n    keyword: 'nuxt content search',\n    position: 40,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 1,\n  },\n  {\n    keyword: 'nuxt content sitemap',\n    position: 16.583333333333336,\n    positionPercent: -0.5012531328320518,\n    prevPosition: 16.666666666666664,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 12,\n  },\n  {\n    keyword: 'nuxt content table of contents',\n    position: 59,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 3,\n  },\n  {\n    keyword: 'nuxt context',\n    position: 33,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 1,\n  },\n  {\n    keyword: 'nuxt csr',\n    position: 40.5,\n    positionPercent: 72.26890756302521,\n    prevPosition: 19,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 4,\n  },\n  {\n    keyword: 'nuxt custom font',\n    position: 12,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 3,\n  },\n  {\n    keyword: 'nuxt custom fonts',\n    position: 10.428571428571429,\n    positionPercent: -80.57259713701431,\n    prevPosition: 24.5,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 7,\n  },\n  {\n    keyword: 'nuxt debug',\n    position: 21.166666666666668,\n    positionPercent: -10.750399148483236,\n    prevPosition: 23.571428571428573,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 6,\n  },\n  {\n    keyword: 'nuxt debugger',\n    position: 30.166666666666668,\n    positionPercent: 21.406727828746185,\n    prevPosition: 24.333333333333332,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 6,\n  },\n  {\n    keyword: 'nuxt debugging',\n    position: 18.5,\n    positionPercent: 20.8955223880597,\n    prevPosition: 15,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 2,\n  },\n  {\n    keyword: 'nuxt define props',\n    position: 55.5,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 2,\n  },\n  {\n    keyword: 'nuxt defineeventhandler',\n    position: 40,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 2,\n  },\n  {\n    keyword: 'nuxt definenitroplugin',\n    position: 20.25,\n    positionPercent: 26.573426573426573,\n    prevPosition: 15.5,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 4,\n  },\n  {\n    keyword: 'nuxt definenuxtconfig',\n    position: 46,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 1,\n  },\n  {\n    keyword: 'nuxt dev tools',\n    position: 65.11111111111111,\n    positionPercent: -1.3559322033898256,\n    prevPosition: 66,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 9,\n  },\n  {\n    keyword: 'nuxt devtools',\n    position: 81,\n    positionPercent: 25.39130434782609,\n    prevPosition: 62.75,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 4,\n  },\n  {\n    keyword: 'nuxt disable cache',\n    position: 9.05,\n    positionPercent: -17.632241813602015,\n    prevPosition: 10.8,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 20,\n  },\n  {\n    keyword: 'nuxt disable nitro',\n    position: 29.6,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 5,\n  },\n  {\n    keyword: 'nuxt download',\n    position: 75,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 1,\n  },\n  {\n    keyword: 'nuxt dynamic route',\n    position: 87,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 1,\n  },\n  {\n    keyword: 'nuxt dynamic routes',\n    position: 53,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 2,\n  },\n  {\n    keyword: 'nuxt env',\n    position: 43,\n    positionPercent: -18.309859154929576,\n    prevPosition: 51.666666666666664,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 1,\n  },\n  {\n    keyword: 'nuxt env config',\n    position: 39.5,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 2,\n  },\n  {\n    keyword: 'nuxt env variables',\n    position: 54,\n    positionPercent: -5.988023952095812,\n    prevPosition: 57.333333333333336,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 1,\n  },\n  {\n    keyword: 'nuxt env vars',\n    position: 56.5,\n    positionPercent: -17.00404858299595,\n    prevPosition: 67,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 2,\n  },\n  {\n    keyword: 'nuxt environment variables',\n    position: 51.142857142857146,\n    positionPercent: -7.569988801791704,\n    prevPosition: 55.166666666666664,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 7,\n  },\n  {\n    keyword: 'nuxt experimental',\n    position: 9.294117647058824,\n    positionPercent: -5.976258698321732,\n    prevPosition: 9.866666666666667,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 17,\n  },\n  {\n    keyword: 'nuxt fetch',\n    position: 74,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 1,\n  },\n  {\n    keyword: 'nuxt font',\n    position: 23.285714285714285,\n    positionPercent: -11.014492753623193,\n    prevPosition: 26,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 7,\n  },\n  {\n    keyword: 'nuxt font loader',\n    position: 15.5,\n    positionPercent: -30.136986301369863,\n    prevPosition: 21,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 2,\n  },\n  {\n    keyword: 'nuxt fonts',\n    position: 19.857142857142858,\n    positionPercent: -1.959038290293853,\n    prevPosition: 20.25,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 14,\n  },\n  {\n    keyword: 'nuxt gallery',\n    position: 53,\n    positionPercent: 3.8461538461538463,\n    prevPosition: 51,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 3,\n  },\n  {\n    keyword: 'nuxt generate dynamic routes',\n    position: 57,\n    positionPercent: 28.000000000000004,\n    prevPosition: 43,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 1,\n  },\n  {\n    keyword: 'nuxt generate sitemap',\n    position: 25.5,\n    positionPercent: 6.756756756756762,\n    prevPosition: 23.833333333333332,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 2,\n  },\n  {\n    keyword: 'nuxt get page title',\n    position: 10.875,\n    positionPercent: -25.125628140703515,\n    prevPosition: 14,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 8,\n  },\n  {\n    keyword: 'nuxt get started',\n    position: 84,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 3,\n  },\n  {\n    keyword: 'nuxt google font',\n    position: 23,\n    positionPercent: 6.741573033707865,\n    prevPosition: 21.5,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 6,\n  },\n  {\n    keyword: 'nuxt google fonts',\n    position: 21.5,\n    positionPercent: -22.273249138920782,\n    prevPosition: 26.88888888888889,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 26,\n  },\n  {\n    keyword: 'nuxt head title',\n    position: 29,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 2,\n  },\n  {\n    keyword: 'nuxt hook',\n    position: 27,\n    positionPercent: -12.173913043478262,\n    prevPosition: 30.5,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0.25,\n    clicks: 0,\n    impressions: 2,\n  },\n  {\n    keyword: 'nuxt i18',\n    position: 23.6,\n    positionPercent: 24.22802850356295,\n    prevPosition: 18.5,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 5,\n  },\n  {\n    keyword: 'nuxt i18n',\n    position: 22.370967741935484,\n    positionPercent: -5.511519660350168,\n    prevPosition: 23.63888888888889,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 434,\n  },\n  {\n    keyword: 'nuxt i18n change language',\n    position: 70,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 2,\n  },\n  {\n    keyword: 'nuxt i18n config',\n    position: 22.4,\n    positionPercent: -35.29411764705883,\n    prevPosition: 32,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 5,\n  },\n  {\n    keyword: 'nuxt i18n current locale',\n    position: 38.44444444444444,\n    positionPercent: -17.894736842105267,\n    prevPosition: 46,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 9,\n  },\n  {\n    keyword: 'nuxt i18n example',\n    position: 44.02197802197802,\n    positionPercent: -19.99146240086274,\n    prevPosition: 53.8,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 91,\n  },\n  {\n    keyword: 'nuxt i18n get current language',\n    position: 59,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 1,\n  },\n  {\n    keyword: 'nuxt i18n get current locale',\n    position: 30.75,\n    positionPercent: -13.871374527112238,\n    prevPosition: 35.333333333333336,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 48,\n  },\n  {\n    keyword: 'nuxt i18n locale',\n    position: 52.083333333333336,\n    positionPercent: 13.492741246797612,\n    prevPosition: 45.5,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 12,\n  },\n  {\n    keyword: 'nuxt i18n middleware',\n    position: 60,\n    positionPercent: 10.526315789473683,\n    prevPosition: 54,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 1,\n  },\n  {\n    keyword: 'nuxt i18n module',\n    position: 26.6,\n    positionPercent: 26.382978723404264,\n    prevPosition: 20.4,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 5,\n  },\n  {\n    keyword: 'nuxt i18n seo',\n    position: 10.258064516129032,\n    positionPercent: -91.06353229048327,\n    prevPosition: 27.408163265306122,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 62,\n  },\n  {\n    keyword: 'nuxt i18n set locale',\n    position: 40.5,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 2,\n  },\n  {\n    keyword: 'nuxt i18n strategy',\n    position: 29.25,\n    positionPercent: 22.503961965134714,\n    prevPosition: 23.333333333333332,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 4,\n  },\n  {\n    keyword: 'nuxt iamge',\n    position: 31,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 1,\n  },\n  {\n    keyword: 'nuxt image',\n    position: 44.5,\n    positionPercent: -35.85570924871169,\n    prevPosition: 63.94117647058823,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 22,\n  },\n  {\n    keyword: 'nuxt image background image',\n    position: 57,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 1,\n  },\n  {\n    keyword: 'nuxt image config',\n    position: 15,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 1,\n  },\n  {\n    keyword: 'nuxt image gallery',\n    position: 77.5,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 2,\n  },\n  {\n    keyword: 'nuxt image install',\n    position: 7,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 2,\n  },\n  {\n    keyword: 'nuxt image module',\n    position: 20.666666666666668,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 3,\n  },\n  {\n    keyword: 'nuxt image npm',\n    position: 9,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 1,\n  },\n  {\n    keyword: 'nuxt image sharp',\n    position: 12.285714285714286,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 7,\n  },\n  {\n    keyword: 'nuxt images',\n    position: 60.31818181818182,\n    positionPercent: -9.100266633595831,\n    prevPosition: 66.06896551724138,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 44,\n  },\n  {\n    keyword: 'nuxt img',\n    position: 43.888888888888886,\n    positionPercent: -53.26213141397725,\n    prevPosition: 75.75,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 9,\n  },\n  {\n    keyword: 'nuxt import font',\n    position: 23,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 1,\n  },\n  {\n    keyword: 'nuxt install',\n    position: 56.97872340425532,\n    positionPercent: -9.15353551350067,\n    prevPosition: 62.44444444444444,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 47,\n  },\n  {\n    keyword: 'nuxt installation',\n    position: 42,\n    positionPercent: -25,\n    prevPosition: 54,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 1,\n  },\n  {\n    keyword: 'nuxt internationalization',\n    position: 38.48148148148148,\n    positionPercent: -0.048111618955979235,\n    prevPosition: 38.5,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 27,\n  },\n  {\n    keyword: 'nuxt island',\n    position: 30.5,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 2,\n  },\n  {\n    keyword: 'nuxt islands',\n    position: 29.5,\n    positionPercent: 16.956077630234933,\n    prevPosition: 24.88888888888889,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 6,\n  },\n  {\n    keyword: 'nuxt js i18n',\n    position: 30.058823529411764,\n    positionPercent: -11.685981670949914,\n    prevPosition: 33.78947368421053,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 68,\n  },\n  {\n    keyword: 'nuxt js i18n example',\n    position: 45.625,\n    positionPercent: -30.95329988421459,\n    prevPosition: 62.333333333333336,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 24,\n  },\n  {\n    keyword: 'nuxt js internationalization',\n    position: 36,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 2,\n  },\n  {\n    keyword: 'nuxt kit',\n    position: 39.333333333333336,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 9,\n  },\n  {\n    keyword: 'nuxt layer',\n    position: 62.666666666666664,\n    positionPercent: 24.477611940298505,\n    prevPosition: 49,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 3,\n  },\n  {\n    keyword: 'nuxt layers',\n    position: 93,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 3,\n  },\n  {\n    keyword: 'nuxt link',\n    position: 28.5,\n    positionPercent: 1.3506493506493522,\n    prevPosition: 28.11764705882353,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 44,\n  },\n  {\n    keyword: 'nuxt link to',\n    position: 55.666666666666664,\n    positionPercent: 48.3271375464684,\n    prevPosition: 34,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 3,\n  },\n  {\n    keyword: 'nuxt links',\n    position: 55.5,\n    positionPercent: 23.865546218487403,\n    prevPosition: 43.666666666666664,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 2,\n  },\n  {\n    keyword: 'nuxt localization',\n    position: 57.25,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 4,\n  },\n  {\n    keyword: 'nuxt markdown',\n    position: 98,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 1,\n  },\n  {\n    keyword: 'nuxt meta',\n    position: 63.25,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 4,\n  },\n  {\n    keyword: 'nuxt meta description',\n    position: 75,\n    positionPercent: 3.160270880361167,\n    prevPosition: 72.66666666666667,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 3,\n  },\n  {\n    keyword: 'nuxt meta tags',\n    position: 64.5,\n    positionPercent: 5.577689243027888,\n    prevPosition: 61,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 4,\n  },\n  {\n    keyword: 'nuxt metadata',\n    position: 48,\n    positionPercent: -4.081632653061225,\n    prevPosition: 50,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 1,\n  },\n  {\n    keyword: 'nuxt migration',\n    position: 42.5,\n    positionPercent: 49.38875305623471,\n    prevPosition: 25.666666666666668,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 2,\n  },\n  {\n    keyword: 'nuxt migration guide',\n    position: 21,\n    positionPercent: 24,\n    prevPosition: 16.5,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 1,\n  },\n  {\n    keyword: 'nuxt minify',\n    position: 35,\n    positionPercent: 12.121212121212121,\n    prevPosition: 31,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 1,\n  },\n  {\n    keyword: 'nuxt module',\n    position: 79,\n    positionPercent: 16.93363844393592,\n    prevPosition: 66.66666666666667,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 2,\n  },\n  {\n    keyword: 'nuxt modules',\n    position: 51.77777777777778,\n    positionPercent: 12.389805097451276,\n    prevPosition: 45.73684210526316,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 9,\n  },\n  {\n    keyword: 'nuxt multi cache',\n    position: 34.75,\n    positionPercent: -0.7168458781362007,\n    prevPosition: 35,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 8,\n  },\n  {\n    keyword: 'nuxt multi tenant',\n    position: 24.5,\n    positionPercent: -0.40733197556008727,\n    prevPosition: 24.6,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 2,\n  },\n  {\n    keyword: 'nuxt nitro',\n    position: 78.16666666666667,\n    positionPercent: 34.35352904434728,\n    prevPosition: 55.25,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 6,\n  },\n  {\n    keyword: 'nuxt nitro cache',\n    position: 8.90909090909091,\n    positionPercent: -17.3345759552656,\n    prevPosition: 10.6,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 11,\n  },\n  {\n    keyword: 'nuxt nitro hooks',\n    position: 10.466666666666667,\n    positionPercent: 2.9079159935379715,\n    prevPosition: 10.166666666666666,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 15,\n  },\n  {\n    keyword: 'nuxt nitro plugin',\n    position: 23,\n    positionPercent: 9.090909090909092,\n    prevPosition: 21,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 1,\n  },\n  {\n    keyword: 'nuxt nitro prerender',\n    position: 7,\n    positionPercent: -23.67758186397984,\n    prevPosition: 8.879999999999999,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0.08,\n    clicks: 0,\n    impressions: 16,\n  },\n  {\n    keyword: 'nuxt node_env',\n    position: 21.5,\n    positionPercent: -56.26740947075209,\n    prevPosition: 38.333333333333336,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 4,\n  },\n  {\n    keyword: 'nuxt noindex',\n    position: 22.5,\n    positionPercent: 33.76623376623377,\n    prevPosition: 16,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 2,\n  },\n  {\n    keyword: 'nuxt nuxtlink',\n    position: 22,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 1,\n  },\n  {\n    keyword: 'nuxt og',\n    position: 7.428571428571429,\n    positionPercent: -1.6949152542372823,\n    prevPosition: 7.555555555555555,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 21,\n  },\n  {\n    keyword: 'nuxt ogimage',\n    position: 5.142857142857143,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 14,\n  },\n  {\n    keyword: 'nuxt open graph',\n    position: 8,\n    positionPercent: 16.541353383458645,\n    prevPosition: 6.777777777777778,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 1,\n  },\n  {\n    keyword: 'nuxt page meta',\n    position: 72.5,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 2,\n  },\n  {\n    keyword: 'nuxt page template',\n    position: 51.6,\n    positionPercent: -0.7722007722007695,\n    prevPosition: 52,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 5,\n  },\n  {\n    keyword: 'nuxt page title',\n    position: 9.488636363636363,\n    positionPercent: -4.071342095081192,\n    prevPosition: 9.882978723404255,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 88,\n  },\n  {\n    keyword: 'nuxt picture',\n    position: 46,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 2,\n  },\n  {\n    keyword: 'nuxt playground',\n    position: 45,\n    positionPercent: -28.57142857142857,\n    prevPosition: 60,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 1,\n  },\n  {\n    keyword: 'nuxt png',\n    position: 48,\n    positionPercent: -29.585798816568033,\n    prevPosition: 64.66666666666666,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 1,\n  },\n  {\n    keyword: 'nuxt prerender',\n    position: 16.05263157894737,\n    positionPercent: 48.72118022092076,\n    prevPosition: 9.763636363636364,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 38,\n  },\n  {\n    keyword: 'nuxt prerendering',\n    position: 9,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 19,\n  },\n  {\n    keyword: 'nuxt process.env',\n    position: 27,\n    positionPercent: -65,\n    prevPosition: 53,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 1,\n  },\n  {\n    keyword: 'nuxt production env',\n    position: 39,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 1,\n  },\n  {\n    keyword: 'nuxt provide',\n    position: 76,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 1,\n  },\n  {\n    keyword: 'nuxt public folder',\n    position: 42,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 1,\n  },\n  {\n    keyword: 'nuxt redirect',\n    position: 36.63157894736842,\n    positionPercent: 12.611826314641059,\n    prevPosition: 32.285714285714285,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 19,\n  },\n  {\n    keyword: 'nuxt redis',\n    position: 40.333333333333336,\n    positionPercent: 0.8298755186722049,\n    prevPosition: 40,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 3,\n  },\n  {\n    keyword: 'nuxt redis cache',\n    position: 30,\n    positionPercent: 0,\n    prevPosition: 30,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 1,\n  },\n  {\n    keyword: 'nuxt release',\n    position: 79.6,\n    positionPercent: 55.53772070626003,\n    prevPosition: 45,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 5,\n  },\n  {\n    keyword: 'nuxt release note',\n    position: 25.333333333333332,\n    positionPercent: 0.6600660066006507,\n    prevPosition: 25.166666666666668,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 3,\n  },\n  {\n    keyword: 'nuxt release notes',\n    position: 26.75,\n    positionPercent: 45.27220630372493,\n    prevPosition: 16.875,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0.125,\n    clicks: 0,\n    impressions: 4,\n  },\n  {\n    keyword: 'nuxt releases',\n    position: 44,\n    positionPercent: 6.788511749347251,\n    prevPosition: 41.111111111111114,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 2,\n  },\n  {\n    keyword: 'nuxt remove trailing slash',\n    position: 41,\n    positionPercent: 23.12925170068027,\n    prevPosition: 32.5,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 1,\n  },\n  {\n    keyword: 'nuxt robots txt',\n    position: 9.6,\n    positionPercent: -22.222222222222225,\n    prevPosition: 12,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 15,\n  },\n  {\n    keyword: 'nuxt rootdir',\n    position: 20,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 1,\n  },\n  {\n    keyword: 'nuxt route rules',\n    position: 10.096296296296297,\n    positionPercent: 5.932169695041945,\n    prevPosition: 9.514619883040936,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0.005847953216374269,\n    clicks: 0,\n    impressions: 135,\n  },\n  {\n    keyword: 'nuxt runtime',\n    position: 29.8,\n    positionPercent: 2.7210884353741522,\n    prevPosition: 29,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 5,\n  },\n  {\n    keyword: 'nuxt runtime config',\n    position: 24.833333333333332,\n    positionPercent: 16.512549537648603,\n    prevPosition: 21.045454545454547,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 24,\n  },\n  {\n    keyword: 'nuxt runtime environment variables',\n    position: 28,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 1,\n  },\n  {\n    keyword: 'nuxt runtimeconfig',\n    position: 31.25,\n    positionPercent: 56.88930406352172,\n    prevPosition: 17.40909090909091,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 8,\n  },\n  {\n    keyword: 'nuxt seo meta',\n    position: 10,\n    positionPercent: -127.92792792792793,\n    prevPosition: 45.5,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 2,\n  },\n  {\n    keyword: 'nuxt server cache',\n    position: 25,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 2,\n  },\n  {\n    keyword: 'nuxt set page title',\n    position: 9.48,\n    positionPercent: -0.8664828672705744,\n    prevPosition: 9.5625,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 25,\n  },\n  {\n    keyword: 'nuxt set title',\n    position: 9.466666666666667,\n    positionPercent: -15.627173661024804,\n    prevPosition: 11.071428571428571,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 15,\n  },\n  {\n    keyword: 'nuxt site map',\n    position: 7.294117647058823,\n    positionPercent: -17.890520694259006,\n    prevPosition: 8.727272727272727,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 17,\n  },\n  {\n    keyword: 'nuxt sitemap dynamic routes',\n    position: 36,\n    positionPercent: 11.76470588235294,\n    prevPosition: 32,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 2,\n  },\n  {\n    keyword: 'nuxt sitemap generator',\n    position: 34,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 8,\n  },\n  {\n    keyword: 'nuxt sitemap xml',\n    position: 17,\n    positionPercent: 51.85185185185185,\n    prevPosition: 10,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 12,\n  },\n  {\n    keyword: 'nuxt sitemap.xml',\n    position: 15.142857142857142,\n    positionPercent: 40.90909090909091,\n    prevPosition: 10,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 7,\n  },\n  {\n    keyword: 'nuxt social share',\n    position: 55,\n    positionPercent: 21.105527638190953,\n    prevPosition: 44.5,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 1,\n  },\n  {\n    keyword: 'nuxt storage',\n    position: 51,\n    positionPercent: 14.736842105263156,\n    prevPosition: 44,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 1,\n  },\n  {\n    keyword: 'nuxt swr',\n    position: 51.4,\n    positionPercent: 27.937915742793802,\n    prevPosition: 38.8,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 5,\n  },\n  {\n    keyword: 'nuxt template',\n    position: 58.666666666666664,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 3,\n  },\n  {\n    keyword: 'nuxt templates',\n    position: 49.75,\n    positionPercent: 6.039689387402928,\n    prevPosition: 46.833333333333336,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 8,\n  },\n  {\n    keyword: 'nuxt testing',\n    position: 79,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 2,\n  },\n  {\n    keyword: 'nuxt title',\n    position: 12.423076923076923,\n    positionPercent: 2.975695407078832,\n    prevPosition: 12.058823529411764,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 26,\n  },\n  {\n    keyword: 'nuxt title page',\n    position: 10.5,\n    positionPercent: -5.347593582887698,\n    prevPosition: 11.076923076923077,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 2,\n  },\n  {\n    keyword: 'nuxt trailing slash',\n    position: 48.6,\n    positionPercent: 3.347280334728037,\n    prevPosition: 47,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 5,\n  },\n  {\n    keyword: 'nuxt translation',\n    position: 55.5,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 2,\n  },\n  {\n    keyword: 'nuxt translations',\n    position: 55.833333333333336,\n    positionPercent: -43.091334894613574,\n    prevPosition: 86.5,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 6,\n  },\n  {\n    keyword: 'nuxt ui',\n    position: 42,\n    positionPercent: 11.320754716981133,\n    prevPosition: 37.5,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 1,\n  },\n  {\n    keyword: 'nuxt use env variables',\n    position: 43,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 1,\n  },\n  {\n    keyword: 'nuxt use head',\n    position: 71.5,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 4,\n  },\n  {\n    keyword: 'nuxt usecolormode',\n    position: 26.5,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 2,\n  },\n  {\n    keyword: 'nuxt usehead',\n    position: 42.39473684210526,\n    positionPercent: 32.99051904226257,\n    prevPosition: 30.38888888888889,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 38,\n  },\n  {\n    keyword: 'nuxt usehead example',\n    position: 21,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 1,\n  },\n  {\n    keyword: 'nuxt usehead script',\n    position: 26,\n    positionPercent: -23.728813559322035,\n    prevPosition: 33,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 1,\n  },\n  {\n    keyword: 'nuxt user agent',\n    position: 23.166666666666668,\n    positionPercent: 40.276577355229044,\n    prevPosition: 15.4,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 6,\n  },\n  {\n    keyword: 'nuxt useroute',\n    position: 83,\n    positionPercent: 16.99346405228758,\n    prevPosition: 70,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 2,\n  },\n  {\n    keyword: 'nuxt useseo',\n    position: 5.5,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 2,\n  },\n  {\n    keyword: 'nuxt useseometa',\n    position: 10.861538461538462,\n    positionPercent: -42.93659621802002,\n    prevPosition: 16.8,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 65,\n  },\n  {\n    keyword: 'nuxt vue-i18n',\n    position: 64,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 1,\n  },\n  {\n    keyword: 'nuxt wasm',\n    position: 10.5,\n    positionPercent: -37.41935483870968,\n    prevPosition: 15.333333333333334,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 2,\n  },\n  {\n    keyword: 'nuxt webshop',\n    position: 17.545454545454547,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 11,\n  },\n  {\n    keyword: 'nuxt website',\n    position: 30.77777777777778,\n    positionPercent: -101.33570792520037,\n    prevPosition: 94,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 9,\n  },\n  {\n    keyword: 'nuxt-color-mode',\n    position: 31,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 5,\n  },\n  {\n    keyword: 'nuxt-content',\n    position: 49.25,\n    positionPercent: 55.016181229773466,\n    prevPosition: 28,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 4,\n  },\n  {\n    keyword: 'nuxt-i18n',\n    position: 23,\n    positionPercent: -7.6045627376425795,\n    prevPosition: 24.818181818181817,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 79,\n  },\n  {\n    keyword: 'nuxt-link',\n    position: 36.4,\n    positionPercent: -4.3010752688172085,\n    prevPosition: 38,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 15,\n  },\n  {\n    keyword: 'nuxt-multi-cache',\n    position: 36.5,\n    positionPercent: 25.192802056555273,\n    prevPosition: 28.333333333333332,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 2,\n  },\n  {\n    keyword: 'nuxt-sitemap',\n    position: 7,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 5,\n  },\n  {\n    keyword: 'nuxt.config',\n    position: 16,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 2,\n  },\n  {\n    keyword: 'nuxt.js i18n',\n    position: 28.23076923076923,\n    positionPercent: -2.6881720430107556,\n    prevPosition: 29,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 26,\n  },\n  {\n    keyword: 'nuxt.js seo',\n    position: 11.3,\n    positionPercent: 23.6617183985605,\n    prevPosition: 8.90909090909091,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 10,\n  },\n  {\n    keyword: 'nuxt/content',\n    position: 50.1875,\n    positionPercent: 21.53906788846191,\n    prevPosition: 40.42857142857143,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 16,\n  },\n  {\n    keyword: 'nuxt/i18n',\n    position: 22.4,\n    positionPercent: -18.623481781376526,\n    prevPosition: 27,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 5,\n  },\n  {\n    keyword: 'nuxt/image',\n    position: 68,\n    positionPercent: 36.52173913043478,\n    prevPosition: 47,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 2,\n  },\n  {\n    keyword: 'nuxt/schema',\n    position: 6.642857142857143,\n    positionPercent: 2.76729559748428,\n    prevPosition: 6.461538461538462,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 14,\n  },\n  {\n    keyword: 'nuxt3 canonical',\n    position: 9,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 1,\n  },\n  {\n    keyword: 'nuxt3 i18n',\n    position: 43.5,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 2,\n  },\n  {\n    keyword: 'nuxt3 robots.txt',\n    position: 21,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 4,\n  },\n  {\n    keyword: 'nuxtconfig',\n    position: 20,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 2,\n  },\n  {\n    keyword: 'nuxtcontent',\n    position: 53.25,\n    positionPercent: 70.47619047619048,\n    prevPosition: 25.5,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 4,\n  },\n  {\n    keyword: 'nuxti18n',\n    position: 25.3,\n    positionPercent: -46.83544303797468,\n    prevPosition: 40.77272727272727,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 60,\n  },\n  {\n    keyword: 'nuxtjs i18n',\n    position: 27.688524590163933,\n    positionPercent: -24.167816151784084,\n    prevPosition: 35.3,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 61,\n  },\n  {\n    keyword: 'nuxtjs image',\n    position: 53.5,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 2,\n  },\n  {\n    keyword: 'nuxtjs robots',\n    position: 10.333333333333334,\n    positionPercent: -51.49700598802395,\n    prevPosition: 17.5,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 6,\n  },\n  {\n    keyword: 'nuxtjs/i18n',\n    position: 22.3125,\n    positionPercent: -37.66578249336872,\n    prevPosition: 32.66666666666667,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 16,\n  },\n  {\n    keyword: 'nuxtjs/robots',\n    position: 36.625,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 8,\n  },\n  {\n    keyword: 'nuxtkit',\n    position: 37,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 1,\n  },\n  {\n    keyword: 'nuxtlink',\n    position: 34.2,\n    positionPercent: 5.641524449553448,\n    prevPosition: 32.32352941176471,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 35,\n  },\n  {\n    keyword: 'nuxtlink button',\n    position: 55,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 1,\n  },\n  {\n    keyword: 'nuxtlink disabled',\n    position: 45.5,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 2,\n  },\n  {\n    keyword: 'nuxtlink download',\n    position: 6.5,\n    positionPercent: 0,\n    prevPosition: 6.5,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 2,\n  },\n  {\n    keyword: 'nuxtlink new tab',\n    position: 96,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 1,\n  },\n  {\n    keyword: 'og fonts',\n    position: 38.5,\n    positionPercent: 1.3071895424836601,\n    prevPosition: 38,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 2,\n  },\n  {\n    keyword: 'og imag',\n    position: 65.33333333333333,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 3,\n  },\n  {\n    keyword: 'og image',\n    position: 62.79365079365079,\n    positionPercent: -20.645490351647737,\n    prevPosition: 77.25,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 63,\n  },\n  {\n    keyword: 'og image alt',\n    position: 16,\n    positionPercent: -17.88617886178862,\n    prevPosition: 19.142857142857142,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 1,\n  },\n  {\n    keyword: 'og image cache',\n    position: 31.55,\n    positionPercent: -9.195244313105635,\n    prevPosition: 34.59090909090909,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 20,\n  },\n  {\n    keyword: 'og image checker',\n    position: 74,\n    positionPercent: -9.032258064516128,\n    prevPosition: 81,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 1,\n  },\n  {\n    keyword: 'og image dimensions',\n    position: 54.483333333333334,\n    positionPercent: 1.0764262648008656,\n    prevPosition: 53.9,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 60,\n  },\n  {\n    keyword: 'og image download',\n    position: 8,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 1,\n  },\n  {\n    keyword: 'og image example',\n    position: 70.17647058823529,\n    positionPercent: -4.529229328808014,\n    prevPosition: 73.42857142857143,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 17,\n  },\n  {\n    keyword: 'og image examples',\n    position: 31,\n    positionPercent: 17.543859649122805,\n    prevPosition: 26,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 2,\n  },\n  {\n    keyword: 'og image gallery',\n    position: 60,\n    positionPercent: -10.276679841897234,\n    prevPosition: 66.5,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 1,\n  },\n  {\n    keyword: 'og image gif',\n    position: 15.5,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 2,\n  },\n  {\n    keyword: 'og image meaning',\n    position: 99,\n    positionPercent: 5.714285714285714,\n    prevPosition: 93.5,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 1,\n  },\n  {\n    keyword: 'og image nuxt',\n    position: 6.458333333333333,\n    positionPercent: -11.237199003598127,\n    prevPosition: 7.2272727272727275,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 48,\n  },\n  {\n    keyword: 'og image resolution',\n    position: 53,\n    positionPercent: -48.028673835125446,\n    prevPosition: 86.5,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 1,\n  },\n  {\n    keyword: 'og image seo',\n    position: 38.714285714285715,\n    positionPercent: -8.536944362672937,\n    prevPosition: 42.166666666666664,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 7,\n  },\n  {\n    keyword: 'og image size',\n    position: 65.41379310344827,\n    positionPercent: 151.6218721037998,\n    prevPosition: 9,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 29,\n  },\n  {\n    keyword: 'og image size 2023',\n    position: 67.5,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 2,\n  },\n  {\n    keyword: 'og image template',\n    position: 91.5,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 2,\n  },\n  {\n    keyword: 'og image twitter',\n    position: 17,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 1,\n  },\n  {\n    keyword: 'og image type',\n    position: 93,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 1,\n  },\n  {\n    keyword: 'og image url',\n    position: 85,\n    positionPercent: -6.8181818181818175,\n    prevPosition: 91,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 3,\n  },\n  {\n    keyword: 'og image width',\n    position: 48,\n    positionPercent: 14.925373134328352,\n    prevPosition: 41.333333333333336,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 1,\n  },\n  {\n    keyword: 'og image width and height',\n    position: 36,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 2,\n  },\n  {\n    keyword: 'og images',\n    position: 27.1875,\n    positionPercent: 30.4635761589404,\n    prevPosition: 20,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 16,\n  },\n  {\n    keyword: 'og images for seo',\n    position: 27.5625,\n    positionPercent: -3.279501053240261,\n    prevPosition: 28.48148148148148,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 16,\n  },\n  {\n    keyword: 'og in seo',\n    position: 1,\n    positionPercent: 0,\n    prevPosition: 1,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 3,\n  },\n  {\n    keyword: 'og photos download',\n    position: 2,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 1,\n  },\n  {\n    keyword: 'og picture',\n    position: 10,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 1,\n  },\n  {\n    keyword: 'og seo',\n    position: 65.0909090909091,\n    positionPercent: 6.243473877506937,\n    prevPosition: 61.15,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 11,\n  },\n  {\n    keyword: 'og twitter image',\n    position: 26,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 1,\n  },\n  {\n    keyword: 'og twitter image size',\n    position: 42.75,\n    positionPercent: -45.598194130925506,\n    prevPosition: 68,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 4,\n  },\n  {\n    keyword: 'og: image',\n    position: 67,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 1,\n  },\n  {\n    keyword: 'og:image',\n    position: 48.9,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 20,\n  },\n  {\n    keyword: 'og:image dimensions',\n    position: 41,\n    positionPercent: -30.927835051546392,\n    prevPosition: 56,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 5,\n  },\n  {\n    keyword: 'og:image example',\n    position: 55.75,\n    positionPercent: 26.97201017811705,\n    prevPosition: 42.5,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 4,\n  },\n  {\n    keyword: 'og:image resolution',\n    position: 80,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 1,\n  },\n  {\n    keyword: 'og:image size',\n    position: 63.166666666666664,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 6,\n  },\n  {\n    keyword: 'og:image sizes',\n    position: 43,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 2,\n  },\n  {\n    keyword: 'og:image:alt',\n    position: 17.6875,\n    positionPercent: 8.369125046017922,\n    prevPosition: 16.266666666666666,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 16,\n  },\n  {\n    keyword: 'og:image:height',\n    position: 37.5,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 2,\n  },\n  {\n    keyword: 'og:image:type',\n    position: 19.066666666666666,\n    positionPercent: -18.70047543581617,\n    prevPosition: 23,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 15,\n  },\n  {\n    keyword: 'og:image:url',\n    position: 87.5,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 2,\n  },\n  {\n    keyword: 'og:image:width',\n    position: 36.666666666666664,\n    positionPercent: 14.634146341463406,\n    prevPosition: 31.666666666666668,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 12,\n  },\n  {\n    keyword: 'ogimage',\n    position: 79,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 1,\n  },\n  {\n    keyword: 'ogimage size',\n    position: 53,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 1,\n  },\n  {\n    keyword: 'ogimages',\n    position: 71.33333333333333,\n    positionPercent: -25.3061224489796,\n    prevPosition: 92,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 3,\n  },\n  {\n    keyword: 'open graph dimensions',\n    position: 38,\n    positionPercent: 14.084507042253522,\n    prevPosition: 33,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 1,\n  },\n  {\n    keyword: 'open graph image',\n    position: 75.6923076923077,\n    positionPercent: -8.462204307326589,\n    prevPosition: 82.38053097345133,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 117,\n  },\n  {\n    keyword: 'open graph image checker',\n    position: 99,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 1,\n  },\n  {\n    keyword: 'open graph image dimensions',\n    position: 36.48076923076923,\n    positionPercent: -15.342469563282643,\n    prevPosition: 42.542857142857144,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 52,\n  },\n  {\n    keyword: 'open graph image example',\n    position: 24,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 1,\n  },\n  {\n    keyword: 'open graph image examples',\n    position: 49.25,\n    positionPercent: -2.8356964136780607,\n    prevPosition: 50.666666666666664,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 4,\n  },\n  {\n    keyword: 'open graph image format',\n    position: 22.833333333333332,\n    positionPercent: -17.1301446051168,\n    prevPosition: 27.11111111111111,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 6,\n  },\n  {\n    keyword: 'open graph image nextjs',\n    position: 80,\n    positionPercent: 7.792207792207792,\n    prevPosition: 74,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 1,\n  },\n  {\n    keyword: 'open graph image size',\n    position: 63.407407407407405,\n    positionPercent: 12.406947890818856,\n    prevPosition: 56,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 27,\n  },\n  {\n    keyword: 'open graph image size recommendation',\n    position: 91,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 2,\n  },\n  {\n    keyword: 'open graph image sizes',\n    position: 52.4,\n    positionPercent: -13.523131672597867,\n    prevPosition: 60,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 5,\n  },\n  {\n    keyword: 'open graph image tag',\n    position: 96,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 2,\n  },\n  {\n    keyword: 'open graph image tester',\n    position: 80,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 1,\n  },\n  {\n    keyword: 'open graph image url',\n    position: 59.84615384615385,\n    positionPercent: -18.580166885642438,\n    prevPosition: 72.1044776119403,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 26,\n  },\n  {\n    keyword: 'open graph images',\n    position: 60.8,\n    positionPercent: -10.597466717606622,\n    prevPosition: 67.60377358490567,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 50,\n  },\n  {\n    keyword: 'open graph imageurl',\n    position: 62.42857142857143,\n    positionPercent: -27.10187932739861,\n    prevPosition: 82,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 7,\n  },\n  {\n    keyword: 'open graph in seo',\n    position: 1,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 1,\n  },\n  {\n    keyword: 'open graph seo',\n    position: 84.33333333333333,\n    positionPercent: -5.197305101058711,\n    prevPosition: 88.83333333333333,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 3,\n  },\n  {\n    keyword: 'open graph size',\n    position: 64,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 1,\n  },\n  {\n    keyword: 'open graph type list',\n    position: 98,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 2,\n  },\n  {\n    keyword: 'open graph types',\n    position: 89.375,\n    positionPercent: 26.524685382381413,\n    prevPosition: 68.44444444444444,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 8,\n  },\n  {\n    keyword: 'open meta file',\n    position: 90.33333333333333,\n    positionPercent: 8.461538461538456,\n    prevPosition: 83,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 3,\n  },\n  {\n    keyword: 'opengraph image',\n    position: 42.04761904761905,\n    positionPercent: -62.19274287943816,\n    prevPosition: 80,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 21,\n  },\n  {\n    keyword: 'opengraph image format',\n    position: 16,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 1,\n  },\n  {\n    keyword: 'opengraph image size',\n    position: 41.833333333333336,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 6,\n  },\n  {\n    keyword: 'opengraph images',\n    position: 39.588235294117645,\n    positionPercent: -3.966988001941886,\n    prevPosition: 41.19047619047619,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 17,\n  },\n  {\n    keyword: 'opengraph seo',\n    position: 90.25,\n    positionPercent: -6.951871657754011,\n    prevPosition: 96.75,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 16,\n  },\n  {\n    keyword: 'opengraph size',\n    position: 60,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 1,\n  },\n  {\n    keyword: 'opengraph twitter',\n    position: 27,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 1,\n  },\n  {\n    keyword: 'opengraph-image',\n    position: 58.5,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 2,\n  },\n  {\n    keyword: 'opengraph-image.jpg',\n    position: 7,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 1,\n  },\n  {\n    keyword: 'perender',\n    position: 15,\n    positionPercent: 107.6923076923077,\n    prevPosition: 4.5,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 4,\n  },\n  {\n    keyword: 'prerender nuxt',\n    position: 7.4,\n    positionPercent: -15.767634854771792,\n    prevPosition: 8.666666666666668,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 5,\n  },\n  {\n    keyword: 'prerender seo',\n    position: 93,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 1,\n  },\n  {\n    keyword: 'prerendering',\n    position: 91.70370370370371,\n    positionPercent: -0.9846277504270039,\n    prevPosition: 92.61111111111111,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 27,\n  },\n  {\n    keyword: 'process.env nuxt 3',\n    position: 39,\n    positionPercent: 41.23711340206185,\n    prevPosition: 25.666666666666668,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 1,\n  },\n  {\n    keyword: 'public runtime config',\n    position: 18,\n    positionPercent: -36.36363636363637,\n    prevPosition: 26,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 2,\n  },\n  {\n    keyword: 'publicruntimeconfig',\n    position: 59.4,\n    positionPercent: 0.11229646266141978,\n    prevPosition: 59.333333333333336,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 5,\n  },\n  {\n    keyword: 'publicruntimeconfig nuxt',\n    position: 15,\n    positionPercent: -3.278688524590164,\n    prevPosition: 15.5,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 2,\n  },\n  {\n    keyword: 'publicruntimeconfig nuxt 3',\n    position: 16.5,\n    positionPercent: 9.523809523809524,\n    prevPosition: 15,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 2,\n  },\n  {\n    keyword: 'robotique simple',\n    position: 83.10526315789474,\n    positionPercent: -7.89196204071701,\n    prevPosition: 89.93333333333334,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 19,\n  },\n  {\n    keyword: 'robots file configuration',\n    position: 80.86666666666666,\n    positionPercent: 2.3352793994995746,\n    prevPosition: 79,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 15,\n  },\n  {\n    keyword: 'robots.txt disable indexing',\n    position: 100,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 1,\n  },\n  {\n    keyword: 'robots.txt nuxt',\n    position: 7.833333333333333,\n    positionPercent: -31.39013452914799,\n    prevPosition: 10.75,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 6,\n  },\n  {\n    keyword: 'route rules nuxt',\n    position: 9.6,\n    positionPercent: 9.005442850074209,\n    prevPosition: 8.772727272727273,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 10,\n  },\n  {\n    keyword: 'routerules',\n    position: 9.642857142857142,\n    positionPercent: -15.58522198075133,\n    prevPosition: 11.272727272727273,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0.045454545454545456,\n    clicks: 0,\n    impressions: 42,\n  },\n  {\n    keyword: 'routerules nitro',\n    position: 14.285714285714286,\n    positionPercent: 41.935483870967744,\n    prevPosition: 9.333333333333334,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 7,\n  },\n  {\n    keyword: 'routerules nuxt',\n    position: 9.53125,\n    positionPercent: -0.6336501584125441,\n    prevPosition: 9.591836734693878,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 32,\n  },\n  {\n    keyword: 'rules of the route',\n    position: 5,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 1,\n  },\n  {\n    keyword: 'runtime config nuxt',\n    position: 30,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 2,\n  },\n  {\n    keyword: 'runtime config nuxt 3',\n    position: 34.5,\n    positionPercent: 67.96116504854369,\n    prevPosition: 17,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 2,\n  },\n  {\n    keyword: 'runtimeconfig nuxt',\n    position: 17,\n    positionPercent: 0,\n    prevPosition: 17,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 1,\n  },\n  {\n    keyword: 'runtimeconfig nuxt 3',\n    position: 25,\n    positionPercent: 29.88505747126437,\n    prevPosition: 18.5,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 2,\n  },\n  {\n    keyword: 'satori og',\n    position: 11.5,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 6,\n  },\n  {\n    keyword: 'satori tailwind',\n    position: 19,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 1,\n  },\n  {\n    keyword: 'schema-dts',\n    position: 30.875,\n    positionPercent: 6.263048016701461,\n    prevPosition: 29,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 8,\n  },\n  {\n    keyword: 'see https://developers.google.com/search/blog/2023/06/sitemaps-lastmod-ping',\n    position: 21,\n    positionPercent: 3.2258064516129092,\n    prevPosition: 20.333333333333332,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 4,\n  },\n  {\n    keyword: 'see https://developers.google.com/search/blog/2023/06/sitemaps-lastmod-ping.',\n    position: 26,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 1,\n  },\n  {\n    keyword: 'seo experimentation',\n    position: 78.5,\n    positionPercent: 16.551724137931036,\n    prevPosition: 66.5,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 2,\n  },\n  {\n    keyword: 'seo experiments',\n    position: 91.6268656716418,\n    positionPercent: 5.335095039723977,\n    prevPosition: 86.86549707602339,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 134,\n  },\n  {\n    keyword: 'seo kit',\n    position: 75.33333333333333,\n    positionPercent: 18.225709112854553,\n    prevPosition: 62.75,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 3,\n  },\n  {\n    keyword: 'seo link check',\n    position: 89.37931034482759,\n    positionPercent: -10.924100629825992,\n    prevPosition: 99.70731707317073,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 29,\n  },\n  {\n    keyword: 'seo link checker',\n    position: 102.72,\n    positionPercent: 4.635101932399232,\n    prevPosition: 98.06666666666666,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 50,\n  },\n  {\n    keyword: 'seo links checker',\n    position: 124.5,\n    positionPercent: 31.3588850174216,\n    prevPosition: 90.75,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 2,\n  },\n  {\n    keyword: 'seo meta image',\n    position: 76,\n    positionPercent: 12.587412587412588,\n    prevPosition: 67,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 2,\n  },\n  {\n    keyword: 'seo module',\n    position: 95.33333333333333,\n    positionPercent: 39.33054393305439,\n    prevPosition: 64,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 3,\n  },\n  {\n    keyword: 'seo modules',\n    position: 86,\n    positionPercent: 15,\n    prevPosition: 74,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 1,\n  },\n  {\n    keyword: 'seo nichero',\n    position: 36.5,\n    positionPercent: 45.378151260504204,\n    prevPosition: 23,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 2,\n  },\n  {\n    keyword: 'seo nitro',\n    position: 90,\n    positionPercent: 85.71428571428571,\n    prevPosition: 36,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 1,\n  },\n  {\n    keyword: 'seo og',\n    position: 52.333333333333336,\n    positionPercent: -0.9114325341787154,\n    prevPosition: 52.8125,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 3,\n  },\n  {\n    keyword: 'seo og image',\n    position: 52.75,\n    positionPercent: 28.10810810810811,\n    prevPosition: 39.75,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0.25,\n    clicks: 0,\n    impressions: 4,\n  },\n  {\n    keyword: 'seo og tags',\n    position: 92,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 1,\n  },\n  {\n    keyword: 'seo open graph',\n    position: 95.5,\n    positionPercent: -46.27766599597585,\n    prevPosition: 153,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 2,\n  },\n  {\n    keyword: 'seo setup',\n    position: 100,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 1,\n  },\n  {\n    keyword: 'seo setx',\n    position: 51.785714285714285,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 14,\n  },\n  {\n    keyword: 'seo sitemap best practices',\n    position: 88.6923076923077,\n    positionPercent: -4.249229948339842,\n    prevPosition: 92.54285714285714,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 26,\n  },\n  {\n    keyword: 'seo title separator',\n    position: 82.86666666666666,\n    positionPercent: 4.146992404023806,\n    prevPosition: 79.5,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 15,\n  },\n  {\n    keyword: 'seo.com',\n    position: 86,\n    positionPercent: -6.275140788415124,\n    prevPosition: 91.57142857142857,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 1,\n  },\n  {\n    keyword: 'seometa',\n    position: 31.5,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 2,\n  },\n  {\n    keyword: 'serperator seo',\n    position: 67.5,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 2,\n  },\n  {\n    keyword: 'serverquerycontent',\n    position: 9.857142857142858,\n    positionPercent: -38.123167155425215,\n    prevPosition: 14.5,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 7,\n  },\n  {\n    keyword: 'simple config',\n    position: 61.5,\n    positionPercent: -19.11764705882353,\n    prevPosition: 74.5,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 2,\n  },\n  {\n    keyword: 'simple robots',\n    position: 58.4,\n    positionPercent: 18.726591760299627,\n    prevPosition: 48.4,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 10,\n  },\n  {\n    keyword: 'simple robots txt',\n    position: 65,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 1,\n  },\n  {\n    keyword: 'simple robots.txt',\n    position: 62,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 3,\n  },\n  {\n    keyword: 'simple site map',\n    position: 79.83333333333333,\n    positionPercent: 4.657124546037162,\n    prevPosition: 76.2,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 6,\n  },\n  {\n    keyword: 'simple sitemap',\n    position: 36.23529411764706,\n    positionPercent: 21.824212271973465,\n    prevPosition: 29.105263157894736,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 17,\n  },\n  {\n    keyword: 'simple-sitemap',\n    position: 26.666666666666668,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 3,\n  },\n  {\n    keyword: 'site map best practices',\n    position: 55.9375,\n    positionPercent: 4.922724670864339,\n    prevPosition: 53.25,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 16,\n  },\n  {\n    keyword: 'siteconfig',\n    position: 58,\n    positionPercent: -7.202216066481998,\n    prevPosition: 62.333333333333336,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 2,\n  },\n  {\n    keyword: 'sitelink seo',\n    position: 58,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 1,\n  },\n  {\n    keyword: 'sitemap api',\n    position: 49,\n    positionPercent: -39.34426229508197,\n    prevPosition: 73,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 3,\n  },\n  {\n    keyword: 'sitemap best practice',\n    position: 46,\n    positionPercent: 16.470588235294116,\n    prevPosition: 39,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 1,\n  },\n  {\n    keyword: 'sitemap best practices',\n    position: 60.472727272727276,\n    positionPercent: -0.6443395519592413,\n    prevPosition: 60.86363636363637,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 220,\n  },\n  {\n    keyword: 'sitemap best practices 2023',\n    position: 44,\n    positionPercent: -15.384615384615389,\n    prevPosition: 51.333333333333336,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 1,\n  },\n  {\n    keyword: 'sitemap dynamic url',\n    position: 94,\n    positionPercent: -4.166666666666666,\n    prevPosition: 98,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 5,\n  },\n  {\n    keyword: 'sitemap example',\n    position: 100,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 1,\n  },\n  {\n    keyword: 'sitemap guidelines',\n    position: 101,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 1,\n  },\n  {\n    keyword: 'sitemap i18n',\n    position: 6.5,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 4,\n  },\n  {\n    keyword: 'sitemap link checker',\n    position: 93.78571428571429,\n    positionPercent: 13.75661375661377,\n    prevPosition: 81.71428571428571,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 14,\n  },\n  {\n    keyword: 'sitemap nuxt 3',\n    position: 94,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 1,\n  },\n  {\n    keyword: 'sitemap rules',\n    position: 42.666666666666664,\n    positionPercent: -27.899159663865554,\n    prevPosition: 56.5,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 3,\n  },\n  {\n    keyword: 'sitemap schema',\n    position: 79.49367088607595,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 79,\n  },\n  {\n    keyword: 'sitemap seo',\n    position: 117,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 1,\n  },\n  {\n    keyword: 'sitemap seo best practices',\n    position: 90.51773049645391,\n    positionPercent: 1.997777050383261,\n    prevPosition: 88.72727272727273,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 141,\n  },\n  {\n    keyword: 'sitemap xml best practices',\n    position: 52.77777777777778,\n    positionPercent: 14.932126696832585,\n    prevPosition: 45.44444444444444,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 18,\n  },\n  {\n    keyword: 'sitemaps best practices',\n    position: 54.629629629629626,\n    positionPercent: -6.862215184573341,\n    prevPosition: 58.51162790697674,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 27,\n  },\n  {\n    keyword: 'sitemaps ping is deprecated. see https://developers.google.com/search/blog/2023/06/sitemaps-lastmod-ping.',\n    position: 22,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 1,\n  },\n  {\n    keyword: 'summary_large_image',\n    position: 17,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 1,\n  },\n  {\n    keyword: 'the hook robot',\n    position: 99,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 1,\n  },\n  {\n    keyword: 'the og meaning',\n    position: 7,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 1,\n  },\n  {\n    keyword: 'this module cannot be imported in server runtime. [importing @nuxt/kit from node_modules/nuxt-simple-sitemap/dist/runtime/util/pageutils.mjs]',\n    position: 9,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 1,\n  },\n  {\n    keyword: 'title templates',\n    position: 92.5,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 2,\n  },\n  {\n    keyword: 'titletemplate',\n    position: 17,\n    positionPercent: 26.666666666666668,\n    prevPosition: 13,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 4,\n  },\n  {\n    keyword: 'ttps://developers.google.com/search/blog/2023/06/sitemaps-lastmod-ping',\n    position: 11.714285714285714,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 7,\n  },\n  {\n    keyword: 'twitter og',\n    position: 34,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 1,\n  },\n  {\n    keyword: 'twitter og image size',\n    position: 21,\n    positionPercent: -55.172413793103445,\n    prevPosition: 37,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 1,\n  },\n  {\n    keyword: 'twitter og:image',\n    position: 46,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 1,\n  },\n  {\n    keyword: 'twitter open graph image',\n    position: 23,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 1,\n  },\n  {\n    keyword: 'twitter seo meta tags',\n    position: 97,\n    positionPercent: 21.714285714285715,\n    prevPosition: 78,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 1,\n  },\n  {\n    keyword: 'twitter seo tags',\n    position: 91,\n    positionPercent: -6.382978723404255,\n    prevPosition: 97,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 1,\n  },\n  {\n    keyword: 'ui seo',\n    position: 83,\n    positionPercent: 42.33576642335766,\n    prevPosition: 54,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 1,\n  },\n  {\n    keyword: 'unhead vue',\n    position: 43,\n    positionPercent: 7.228915662650602,\n    prevPosition: 40,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 5,\n  },\n  {\n    keyword: 'unlighthouse',\n    position: 74.95238095238095,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 21,\n  },\n  {\n    keyword: 'unlighthouse github',\n    position: 7,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 1,\n  },\n  {\n    keyword: 'unocss tutorial',\n    position: 88,\n    positionPercent: 75,\n    prevPosition: 40,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 1,\n  },\n  {\n    keyword: 'url endpoint example',\n    position: 49.5,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 2,\n  },\n  {\n    keyword: 'use runtime config nuxt',\n    position: 18,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 1,\n  },\n  {\n    keyword: 'usehead',\n    position: 40,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 1,\n  },\n  {\n    keyword: 'usehead nuxt',\n    position: 31.25,\n    positionPercent: 53.80710659898477,\n    prevPosition: 18,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 8,\n  },\n  {\n    keyword: 'usehead titletemplate',\n    position: 6.666666666666667,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 3,\n  },\n  {\n    keyword: 'usehead vue',\n    position: 29,\n    positionPercent: -6.666666666666667,\n    prevPosition: 31,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 1,\n  },\n  {\n    keyword: 'usehead vue3',\n    position: 45,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 1,\n  },\n  {\n    keyword: 'useschemaorg',\n    position: 28.2,\n    positionPercent: 31.622176591375766,\n    prevPosition: 20.5,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 5,\n  },\n  {\n    keyword: 'useseometa nuxt',\n    position: 11.08695652173913,\n    positionPercent: -23.223570190641244,\n    prevPosition: 14,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 23,\n  },\n  {\n    keyword: 'v3.0.0',\n    position: 19,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 1,\n  },\n  {\n    keyword: 'vue open graph',\n    position: 10.5,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 2,\n  },\n  {\n    keyword: 'vue unhead',\n    position: 40.5,\n    positionPercent: 3.7735849056603774,\n    prevPosition: 39,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 2,\n  },\n  {\n    keyword: 'websites with multiple sitemaps',\n    position: 41.38095238095238,\n    positionPercent: 8.014362326512757,\n    prevPosition: 38.19230769230769,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 21,\n  },\n  {\n    keyword: 'what are the best practices for using xml sitemaps?',\n    position: 76,\n    positionPercent: -2.780352177942534,\n    prevPosition: 78.14285714285714,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 5,\n  },\n  {\n    keyword: 'what is a dynamic url',\n    position: 1,\n    positionPercent: 0,\n    prevPosition: 1,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 1,\n  },\n  {\n    keyword: 'what is an open graph image',\n    position: 78.54545454545455,\n    positionPercent: -15.736696401107356,\n    prevPosition: 91.96153846153847,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 11,\n  },\n  {\n    keyword: 'what is dynamic url',\n    position: 19.75,\n    positionPercent: 180.72289156626508,\n    prevPosition: 1,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 4,\n  },\n  {\n    keyword: 'what is nuxt',\n    position: 95,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 1,\n  },\n  {\n    keyword: 'what is og image',\n    position: 79,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 1,\n  },\n  {\n    keyword: 'what is og type',\n    position: 161,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 1,\n  },\n  {\n    keyword: 'what is og:image',\n    position: 33,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 1,\n  },\n  {\n    keyword: 'what is open graph in seo',\n    position: 1,\n    positionPercent: -191.83673469387753,\n    prevPosition: 48,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 3,\n  },\n  {\n    keyword: 'whats the meaning of og',\n    position: 1,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 1,\n  },\n  {\n    keyword: 'whatsapp og image not showing',\n    position: 80,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 1,\n  },\n  {\n    keyword: 'x robots tag',\n    position: 100,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 1,\n  },\n  {\n    keyword: 'xml best practices',\n    position: 59.4,\n    positionPercent: -0.1682085786375129,\n    prevPosition: 59.5,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 5,\n  },\n  {\n    keyword: 'xml sitemap best practices',\n    position: 80.25,\n    positionPercent: 2.5828317653246,\n    prevPosition: 78.20370370370371,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 48,\n  },\n  {\n    keyword: 'xt twitter',\n    position: 31,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 1,\n  },\n  {\n    keyword: 'you should use slots with <contentrenderer>',\n    position: 16,\n    positionPercent: 0,\n    prevPosition: 0,\n    ctr: 0,\n    ctrPercent: 0,\n    prevCtr: 0,\n    clicks: 0,\n    impressions: 1,\n  },\n]\n"
  },
  {
    "path": "error.vue",
    "content": "<script setup lang=\"ts\">\nimport type { NuxtError } from '#app'\n\ndefineProps({\n  error: {\n    type: Object as PropType<NuxtError>,\n    required: true,\n  },\n})\n\nuseSeoMeta({\n  title: 'Page not found',\n  description: 'We are sorry but this page could not be found.',\n})\n\nuseHead({\n  htmlAttrs: {\n    lang: 'en',\n  },\n})\n</script>\n\n<template>\n  <div>\n    <Header />\n\n    <UMain>\n      <UContainer>\n        <UPage>\n          <UPageError :error=\"error\" />\n        </UPage>\n      </UContainer>\n    </UMain>\n\n    <Footer />\n\n    <UNotifications />\n  </div>\n</template>\n"
  },
  {
    "path": "eslint.config.js",
    "content": "import antfu from '@antfu/eslint-config'\n\nexport default antfu({\n  rules: {\n    'unicorn/prefer-node-protocol': 'off',\n    'node/prefer-global': 'off',\n    'node/prefer-global/buffer': 'off',\n  },\n})\n"
  },
  {
    "path": "layouts/account.vue",
    "content": "<script setup lang=\"ts\">\nimport DefaultLayout from './default.vue'\n\nconst user = useAuthenticatedUser()\n\nconst links = [\n  { label: 'Profile', to: '/account', icon: 'i-heroicons-user-circle' },\n  { label: 'Pro', to: '/account/upgrade', icon: 'i-heroicons-star' },\n]\n\nconst supportLinks = computed(() => [\n  {\n    icon: 'i-ph-envelope-open-duotone',\n    label: 'Email',\n    to: 'mailto:harlan@harlanzw.com',\n    target: '_blank',\n  },\n  {\n    icon: 'i-ph-chat-centered-text-duotone',\n    label: 'Discord',\n    to: 'https://discord.gg/275MBUBvgP',\n    target: '_blank',\n  },\n  {\n    icon: 'i-ph-github-logo',\n    label: 'Submit a bug',\n    to: `https://github.com/harlan-zw/request-indexing/issues/new/choose`,\n    target: '_blank',\n  },\n])\n</script>\n\n<template>\n  <DefaultLayout>\n    <UContainer>\n      <UPage>\n        <template #left>\n          <UAside>\n            <UNavigationTree title=\"test\" :links=\"links\" />\n          </UAside>\n        </template>\n        <slot />\n        <template #right>\n          <UAside>\n            <UPageLinks title=\"Get Help\" :links=\"supportLinks\" class=\"mb-7\" />\n            <div>\n              <div class=\"mb-1\">\n                User ID\n              </div>\n              <div class=\"text-gray-400 text-xs mb-1\">\n                When reporting issues please provide your ID.\n              </div>\n              <div class=\"font-mono text-gray-500 text-sm bg-gray-50 px-2 py-1 rounded\">\n                <code>{{ user.userId }}</code>\n              </div>\n            </div>\n          </UAside>\n        </template>\n      </UPage>\n    </UContainer>\n  </DefaultLayout>\n</template>\n"
  },
  {
    "path": "layouts/auth.vue",
    "content": "<script setup lang=\"ts\">\nuseHead({\n  bodyAttrs: {\n    class: 'dark:bg-gray-950',\n  },\n})\n</script>\n\n<template>\n  <div class=\"min-h-screen py-14 flex items-center justify-center overlay\">\n    <Gradient />\n\n    <UButton icon=\"i-heroicons-home\" label=\"Home\" to=\"/\" color=\"white\" class=\"absolute top-4\" />\n\n    <slot />\n  </div>\n</template>\n"
  },
  {
    "path": "layouts/default.vue",
    "content": "<template>\n  <div>\n    <Header />\n\n    <UMain>\n      <slot />\n    </UMain>\n\n    <Footer />\n  </div>\n</template>\n"
  },
  {
    "path": "middleware/auth.global.ts",
    "content": "const AuthenticatedRoutePrefixes = ['/dashboard', '/account']\nconst AdminRoutePrefixes = ['/admin']\n\nexport default defineNuxtRouteMiddleware((to) => {\n  const { loggedIn, user } = useUserSession()\n  if (AuthenticatedRoutePrefixes.some(p => to.path.startsWith(p))) {\n    if (!loggedIn.value)\n      return navigateTo('/get-started')\n  }\n  if (AdminRoutePrefixes.some(p => to.path.startsWith(p))) {\n    // TODO refactor this out to use env\n    if (!loggedIn.value || user.value?.email !== 'harlan@harlanzw.com')\n      return navigateTo('/dashboard')\n  }\n})\n"
  },
  {
    "path": "nuxt.config.ts",
    "content": "import { env } from 'std-env'\nimport { hash } from 'ohash'\nimport type { OAuthPoolToken } from '~/types'\n\nlet tokens: Partial<OAuthPoolToken>[] = env.NUXT_OAUTH_POOL ? JSON.parse(env.NUXT_OAUTH_POOL) : []\nconst privateTokens: Partial<OAuthPoolToken>[] = env.NUXT_OAUTH_PRIVATE_POOL ? JSON.parse(env.NUXT_OAUTH_PRIVATE_POOL) : []\n\nexport default defineNuxtConfig({\n  extends: ['@nuxt/ui-pro'],\n  modules: [\n    'nuxt-auth-utils',\n    'dayjs-nuxt',\n    '@nuxt/image',\n    '@nuxt/ui',\n    '@nuxtjs/fontaine',\n    '@nuxtjs/google-fonts',\n    '@vueuse/nuxt',\n    '@nuxtjs/seo',\n    (_, nuxt) => {\n      // seed the main tokens if there isn't a pool available\n      if (tokens.length === 0) {\n        tokens = [{\n          label: 'primary',\n          client_id: env.NUXT_OAUTH_GOOGLE_CLIENT_ID!,\n          client_secret: env.NUXT_OAUTH_GOOGLE_CLIENT_SECRET!,\n        }]\n      }\n      nuxt.options.nitro!.virtual = nuxt.options.nitro!.virtual || {}\n      nuxt.options.nitro.virtual['#app/token-pool.mjs']\n        = [\n          `export const tokens = ${JSON.stringify(tokens.map((t) => {\n            t.id = t.id || hash(t)\n            return t\n          }))}`,\n          `export const privateTokens = ${JSON.stringify(privateTokens.map((t) => {\n            t.id = t.id || hash(t)\n            return t\n          }))}`,\n        ].join('\\n')\n    },\n  ],\n  runtimeConfig: {\n    key: '', // .env NUXT_KEY\n    session: {\n      cookie: {\n        maxAge: 60 * 60 * 24 * 90, // 3mo\n      },\n    },\n    postmark: {\n      apiKey: '', // .env NUXT_POSTMARK_API_KEY\n    },\n    public: {\n      features: {\n        keyLogin: false,\n        crawler: false,\n      },\n      indexing: {\n        usageLimitPerUser: 15,\n      },\n    },\n    indexing: {\n      maxUsersPerOAuth: 15, // we over provision slightly (25 over),\n    },\n  },\n  nitro: {\n    vercel: {\n      functions: {\n        maxDuration: 90, // second timeout for API calls\n      },\n    },\n    devStorage: {\n      app: {\n        base: '.db',\n        driver: 'fs',\n      },\n    },\n    storage: {\n      app: {\n        driver: 'vercelKV',\n      },\n    },\n  },\n  ui: {\n    icons: ['heroicons', 'simple-icons', 'ph'],\n  },\n  app: {\n    pageTransition: {\n      name: 'page',\n      mode: 'out-in',\n    },\n    seoMeta: {\n      themeColor: [\n        { content: '#18181b', media: '(prefers-color-scheme: dark)' },\n        { content: 'white', media: '(prefers-color-scheme: light)' },\n      ],\n    },\n    head: {\n      templateParams: {\n        separator: '·',\n      },\n      script: [\n        {\n          'src': 'https://cdn.usefathom.com/script.js',\n          'data-spa': 'auto',\n          'data-site': 'UHBNWPCP',\n          'defer': true,\n        },\n      ],\n    },\n  },\n  site: {\n    name: 'Request Indexing',\n    url: 'requestindexing.com',\n  },\n  // Fonts\n  fontMetrics: {\n    fonts: ['DM Sans'],\n  },\n  googleFonts: {\n    display: 'swap',\n    download: true,\n    families: {\n      'DM+Sans': [300, 400, 500, 600, 700],\n    },\n  },\n  dayjs: {\n    locales: ['en'],\n    plugins: ['relativeTime', 'utc'],\n    defaultLocale: 'en',\n  },\n  devtools: { enabled: true },\n})\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"requestindexing.com\",\n  \"type\": \"module\",\n  \"version\": \"0.0.1\",\n  \"private\": true,\n  \"packageManager\": \"pnpm@8.15.2\",\n  \"scripts\": {\n    \"build\": \"nuxt build\",\n    \"dev\": \"nuxt dev\",\n    \"lint\": \"eslint . --fix\",\n    \"generate\": \"nuxt generate\",\n    \"preview\": \"nuxt preview\",\n    \"postinstall\": \"nuxt prepare\",\n    \"release\": \"bumpp\"\n  },\n  \"engines\": {\n    \"node\": \"^20\"\n  },\n  \"dependencies\": {\n    \"@googleapis/indexing\": \"^2.0.0\",\n    \"@googleapis/searchconsole\": \"^1.0.3\",\n    \"@iconify-json/heroicons\": \"^1.1.19\",\n    \"@iconify-json/ph\": \"^1.1.11\",\n    \"@iconify-json/simple-icons\": \"^1.1.90\",\n    \"@nuxt/content\": \"^2.12.0\",\n    \"@nuxt/image\": \"^1.3.0\",\n    \"@nuxt/ui-pro\": \"^0.7.5\",\n    \"@nuxtjs/fontaine\": \"^0.4.1\",\n    \"@nuxtjs/google-fonts\": \"^3.1.3\",\n    \"@nuxtjs/seo\": \"^2.0.0-rc.8\",\n    \"@vercel/kv\": \"^1.0.1\",\n    \"@vueuse/nuxt\": \"^10.7.2\",\n    \"dayjs-nuxt\": \"^2.1.9\",\n    \"fuse.js\": \"^7.0.0\",\n    \"lightweight-charts\": \"^4.1.3\",\n    \"nuxt\": \"^3.10.1\",\n    \"nuxt-auth-utils\": \"^0.0.15\",\n    \"postmark\": \"^4.0.2\",\n    \"sitemapper\": \"^3.2.8\",\n    \"vue\": \"^3.4.18\",\n    \"vue-router\": \"^4.2.5\"\n  },\n  \"devDependencies\": {\n    \"@antfu/eslint-config\": \"^2.6.4\",\n    \"@nuxt/test-utils\": \"^3.11.0\",\n    \"bumpp\": \"^9.3.0\",\n    \"eslint\": \"^8.56.0\",\n    \"typescript\": \"^5.3.3\",\n    \"vitest\": \"^1.2.2\"\n  }\n}\n"
  },
  {
    "path": "pages/account/index.vue",
    "content": "<script setup lang=\"ts\">\nimport { withHttps } from 'ufo'\nimport { useFriendlySiteUrl } from '~/composables/formatting'\n\ndefinePageMeta({\n  layout: 'account',\n})\n\nconst { fetch } = useUserSession()\nconst user = useAuthenticatedUser()\nconst { session } = useUserSession()\nconst logout = createLogoutHandler()\n\nconst toast = useToast()\nconst isRevokingIndexingAuth = ref(false)\nconst isDeletingAccount = ref(false)\nasync function revokeIndexingAuth() {\n  isRevokingIndexingAuth.value = true\n  await $fetch('/api/indexing/auth', {\n    method: 'DELETE',\n    headers: {\n      Accept: 'text/json',\n    },\n  })\n    .finally(() => {\n      isRevokingIndexingAuth.value = false\n    })\n    .then(() => {\n      setTimeout(() => {\n        toast.add({ title: 'Google Token Revoked', description: 'You have revoked access to the Web Indexing API.', color: 'green' })\n        fetch()\n      }, 500) // make sure user knows we actually did something (it's too quick)\n    }).catch(() => {\n      toast.add({ title: 'Failed to revoke Google Token', description: 'Sorry about that, maybe try again later.', color: 'red' })\n    })\n}\n\nfunction deleteAccount() {\n  // /api/user/me DELETE\n  isDeletingAccount.value = true\n  $fetch('/api/user/me', {\n    method: 'DELETE',\n    headers: {\n      Accept: 'text/json',\n    },\n  }).finally(() => {\n    setTimeout(() => {\n      toast.add({ id: 'logout', title: 'Account Deleted', description: 'Your account has been deleted.', color: 'green' })\n      isDeletingAccount.value = false\n      logout()\n    }, 500) // make sure user knows we actually did something (it's too quick)\n  }).catch(() => {\n    toast.add({ title: 'Failed to delete account', description: 'Sorry about that, maybe try again later.', color: 'red' })\n  })\n}\n\nasync function showSite(site: string) {\n  // save it upstream\n  session.value = await $fetch('/api/user/me', {\n    method: 'POST',\n    body: JSON.stringify({\n      hiddenSites: (user.value.hiddenSites || []).filter(s => s !== site),\n    }),\n  })\n}\n</script>\n\n<template>\n  <div>\n    <UPageHeader title=\"Account\" description=\"Update your details.\" :links=\"[]\" headline=\"Your Account\" />\n    <UPageBody>\n      <div v-if=\"user.hiddenSites?.length\">\n        <div class=\"mb-5\">\n          <h2 class=\"text-lg font-bold flex items-center gap-1 mb-1\">\n            <UIcon name=\"i-heroicons-eye-slash\" class=\"mr-1.5\" />\n            Hidden Sites\n          </h2>\n        </div>\n        <p class=\"text-gray-600 dark:text-gray-300 mb-3\">\n          Sites you are hiding from your dashboard. You can toggle them at any time.\n        </p>\n        <ul class=\"ml-5 space-y-2\">\n          <li v-for=\"(site, key) in user.hiddenSites\" :key=\"key\">\n            <img :src=\"`https://www.google.com/s2/favicons?domain=${withHttps(useFriendlySiteUrl(site))}`\" alt=\"favicon\" class=\"w-4 h-4 mr-1.5 inline-block\">\n            {{ useFriendlySiteUrl(site) }}\n            <UButton variant=\"link\" color=\"gray\" @click=\"showSite(site)\">\n              Unhide\n            </UButton>\n          </li>\n        </ul>\n      </div>\n      <div>\n        <div class=\"mt-10 mb-5\">\n          <h2 class=\"text-lg flex items-center font-bold mb-1\">\n            <UIcon name=\"i-heroicons-lock-closed\" class=\"mr-1.5\" />\n            Web Indexing API\n          </h2>\n        </div>\n        <template v-if=\"user.indexingOAuthIdNext\">\n          <p class=\"text-gray-600 dark:text-gray-300 mb-3\">\n            You have provided authenticated access to the Web Indexing API. You\n            can safely revoke access at any time.\n          </p>\n          <UButton color=\"red\" variant=\"soft\" :loading=\"isRevokingIndexingAuth\" @click=\"revokeIndexingAuth\">\n            Revoke Tokens\n          </UButton>\n        </template>\n        <template v-else>\n          <p class=\"text-gray-600 dark:text-gray-300 mb-3\">\n            You have not provided authenticated access to the Web Indexing API. You\n            can provide access when requesting indexing.\n          </p>\n        </template>\n      </div>\n      <div>\n        <div class=\"mt-10 mb-5\">\n          <h2 class=\"text-lg flex items-center font-bold mb-1\">\n            <UIcon name=\"i-heroicons-trash\" class=\"mr-1.5\" />\n            Delete Account\n          </h2>\n        </div>\n        <p class=\" text-gray-600 dark:text-gray-300 mb-2\">\n          Delete all data associated with your account.\n        </p>\n        <ul class=\"mb-3 text-sm text-gray-600 dark:text-gray-300 list-disc ml-5\">\n          <li>Any cached / permanently stored data related to your account will be deleted.</li>\n          <li>Your Google Account Tokens will be revoked.</li>\n        </ul>\n        <UButton color=\"red\" variant=\"outline\" :loading=\"isDeletingAccount\" @click=\"deleteAccount\">\n          Delete Account\n        </UButton>\n      </div>\n    </UPageBody>\n  </div>\n</template>\n"
  },
  {
    "path": "pages/account/upgrade.vue",
    "content": "<script setup lang=\"ts\">\ndefinePageMeta({\n  layout: 'account',\n})\n\nconst user = useAuthenticatedUser()\n</script>\n\n<template>\n  <div>\n    <div v-if=\"user.access === 'pro'\">\n      <UPageHeader title=\"Request Indexing Pro\" icon=\"i-heroicons-star\" description=\"You are currently a Pro User.\" headline=\"Your Account\" />\n      <UPageBody>\n        <UButton to=\"https://billing.stripe.com/p/login/00g16Gas1gID8jCcMM\">\n          Manage my subscription\n        </UButton>\n      </UPageBody>\n    </div>\n    <template v-else>\n      <UPageHeader title=\"Upgrade To Pro\" icon=\"i-heroicons-star\" description=\"Take your account to the next level with Request Indexing Pro.\" headline=\"Your Account\" />\n      <UPageBody>\n        <ULandingSection :ui=\"{ wrapper: ['py-12 sm:py-16'], container: ['px-0 lg:px-0 sm:px-0'] }\" title=\"Features\" description=\"More API credits, less manual work with Request Indexing Pro.\">\n          <div class=\"grid grid-cols-3 gap-5 mt-10\">\n            <ULandingCard icon=\"i-heroicons-sparkles\" color=\"purple\" title=\"200 API Quota\" description=\"Quota of 200 Request Indexing credits per day. Use sites with more than 2500 pages.\" />\n            <ULandingCard class=\"relative\" icon=\"i-heroicons-bug-ant\" color=\"purple\" title=\"Automatic Indexing\" description=\"Set and forget, always have your latest links indexed straight away.\">\n              <UBadge class=\"absolute top-5 right-5\" color=\"blue\" variant=\"soft\">\n                Coming Soon\n              </UBadge>\n            </ULandingCard>\n            <ULandingCard class=\"relative\" icon=\"i-heroicons-bolt\" color=\"purple\" title=\"AI Backlink Finder\" description=\"Backlinks are improving your web presence, automatically find opportunities.\">\n              <UBadge class=\"absolute top-5 right-5\" color=\"blue\" variant=\"soft\">\n                Coming Soon\n              </UBadge>\n            </ULandingCard>\n          </div>\n        </ULandingSection>\n        <div class=\"mt-10\">\n          <UPricingCard class=\"mt-10\" title=\"Pro Plan\" price=\"$30 USD\" cycle=\"month\" :button=\"{ label: 'Purchase Pro Plan', color: 'purple', variant: 'solid' }\" description=\"Unlock more API credits and new features by upgrading to pro.\">\n            <template #footer>\n              <UBadge variant=\"soft\" color=\"orange\">\n                0 left\n              </UBadge>\n              <div class=\"text-gray-500 dark:text-gray-400 text-sm mt-1 mx-2 max-w-sm\">\n                The Pro Plan is currently sold out.\n              </div>\n            </template>\n          </UPricingCard>\n        </div>\n      </UPageBody>\n    </template>\n  </div>\n</template>\n"
  },
  {
    "path": "pages/admin/index.vue",
    "content": "<script setup lang=\"ts\">\nconst links = [{\n  label: 'Users',\n  to: '/admin/users',\n  icon: 'i-heroicons-user-circle',\n}]\n\nconst usage = ref()\n\nonMounted(async () => {\n  usage.value = await $fetch('/api/admin/usage')\n})\n</script>\n\n<template>\n  <UContainer>\n    <UPage>\n      <template #left>\n        <UAside>\n          <UNavigationTree title=\"test\" :links=\"links\" />\n        </UAside>\n      </template>\n      <div>\n        <UPageHeader title=\"Admin\" description=\"Request Indexing Admin Portal\" :links=\"[]\" headline=\"Restricted\" />\n        <UPageBody>\n          <h2 class=\"font-bold text-2xl mb-3\">\n            Usage\n          </h2>\n          <div v-if=\"usage\">\n            <div>Sign Ups: {{ usage.signups }}</div>\n            <h3 class=\"mb-2 text-lg font-semibold\">\n              Web Indexing API\n            </h3>\n            <div>Used: {{ usage.webIndexingApi.used }}</div>\n            <div>Free: {{ usage.webIndexingApi.free }}</div>\n            <div>Slots left {{ 100 - Math.round(usage.webIndexingApi.used / usage.webIndexingApi.free * 100) }}%</div>\n          </div>\n        </UPageBody>\n      </div>\n    </UPage>\n  </UContainer>\n</template>\n"
  },
  {
    "path": "pages/dashboard/index.vue",
    "content": "<script lang=\"ts\" setup>\nimport { fetchSites } from '~/composables/fetch'\n\nconst { user } = useUserSession()\n\nconst { data: sites, forceRefresh } = await fetchSites()\n\nconst isSyncLoading = ref(false)\nfunction sync() {\n  callFnSyncToggleRef(forceRefresh, isSyncLoading)\n}\n\nconst hiddenSites = computed(() => user.value.hiddenSites || [])\n\nconst visibleSites = computed(() => {\n  return (sites.value || []).filter(site => !hiddenSites.value.includes(site.siteUrl))\n})\n\nconst allSitesTotal = ref(null)\nonMounted(async () => {\n  let clicks = 0\n  let impressions = 0\n  let prevClicks = 0\n  let prevImpressions = 0\n  for (const _site of visibleSites.value!) {\n    const { data: site } = await fetchSite(_site)\n    if (site.value) {\n      clicks += site.value.analytics.period.totalClicks\n      impressions += site.value.analytics.period.totalImpressions\n      prevClicks += site.value.analytics.prevPeriod.totalClicks\n      prevImpressions += site.value.analytics.prevPeriod.totalImpressions\n    }\n  }\n  allSitesTotal.value = {\n    period: {\n      totalClicks: clicks,\n      totalImpressions: impressions,\n    },\n    prevPeriod: {\n      totalClicks: prevClicks,\n      totalImpressions: prevImpressions,\n    },\n  }\n})\n</script>\n\n<template>\n  <UContainer>\n    <div class=\"py-10\">\n      <div class=\"flex flex-col md:flex-row md:items-center md:justify-between mb-3 md:mb-8\">\n        <div class=\"flex flex-col md:flex-row md:items-center md:gap-10\">\n          <div class=\"mb-3 md:mb-0\">\n            <h2 class=\"text-2xl font-bold mb-1 flex items-center gap-2\">\n              <span>Your Sites</span>\n              <UBadge color=\"gray\" variant=\"soft\">\n                {{ visibleSites.length }}\n              </UBadge>\n            </h2>\n            <p class=\"text-gray-400 text-sm\">\n              Data from <NuxtLink to=\"https://search.google.com/search-console\" target=\"_blank\" class=\"underline\">\n                Google Search Console\n              </NuxtLink>\n            </p>\n          </div>\n          <div v-if=\"allSitesTotal\" class=\"mb-3 md:mb-0 flex items-center md:justify-center gap-2 md:gap-5\">\n            <div class=\"flex flex-col justify-center\">\n              <span class=\"text-sm opacity-70\">Clicks</span>\n              <div class=\"text-xl flex items-end gap-2\">\n                <span>{{ useHumanFriendlyNumber(allSitesTotal.period.totalClicks) }}</span>\n                <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"32\" height=\"32\" viewBox=\"0 0 24 24\" class=\"w-5 h-5 opacity-80\"><path fill=\"#888888\" d=\"m11.5 11l6.38 5.37l-.88.18l-.64.12c-.63.13-.99.83-.71 1.4l.27.58l1.36 2.94l-1.42.66l-1.36-2.93l-.26-.58a.985.985 0 0 0-1.52-.36l-.51.4l-.71.57zm-.74-2.31a.76.76 0 0 0-.76.76V20.9c0 .42.34.76.76.76c.19 0 .35-.06.48-.16l1.91-1.55l1.66 3.62c.13.27.4.43.69.43c.11 0 .22 0 .33-.08l2.76-1.28c.38-.18.56-.64.36-1.01L17.28 18l2.41-.45a.88.88 0 0 0 .43-.26c.27-.32.23-.79-.12-1.08l-8.74-7.35l-.01.01a.756.756 0 0 0-.49-.18M15 10V8h5v2zm-1.17-5.24l2.83-2.83l1.41 1.41l-2.83 2.83zM10 0h2v5h-2zM3.93 14.66l2.83-2.83l1.41 1.41l-2.83 2.83zm0-11.32l1.41-1.41l2.83 2.83l-1.41 1.41zM7 10H2V8h5z\" /></svg>\n              </div>\n              <TrendPercentage :value=\"allSitesTotal.period.totalClicks\" :prev-value=\"allSitesTotal.prevPeriod.totalClicks\" />\n            </div>\n            <div class=\"flex flex-col justify-center\">\n              <span class=\"text-sm opacity-70\">Impressions</span>\n              <div class=\"text-xl flex items-end gap-2\">\n                <span>{{ useHumanFriendlyNumber(allSitesTotal.period.totalImpressions) }}</span>\n                <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"0\" height=\"24\" viewBox=\"0 0 32 32\" class=\"w-5 h-5 opacity-80\"><path fill=\"#888888\" d=\"M30.94 15.66A16.69 16.69 0 0 0 16 5A16.69 16.69 0 0 0 1.06 15.66a1 1 0 0 0 0 .68A16.69 16.69 0 0 0 16 27a16.69 16.69 0 0 0 14.94-10.66a1 1 0 0 0 0-.68ZM16 25c-5.3 0-10.9-3.93-12.93-9C5.1 10.93 10.7 7 16 7s10.9 3.93 12.93 9C26.9 21.07 21.3 25 16 25Z\" /><path fill=\"#888888\" d=\"M16 10a6 6 0 1 0 6 6a6 6 0 0 0-6-6Zm0 10a4 4 0 1 1 4-4a4 4 0 0 1-4 4Z\" /></svg>\n              </div>\n              <TrendPercentage :value=\"allSitesTotal.period.totalImpressions\" :prev-value=\"allSitesTotal.prevPeriod.totalImpressions\" />\n            </div>\n          </div>\n        </div>\n        <UButton class=\"hidden md:flex\" variant=\"ghost\" color=\"gray\" icon=\"i-heroicons-arrow-path\" :loading=\"isSyncLoading\" @click=\"sync\">\n          Refresh\n        </UButton>\n      </div>\n      <div class=\"grid 2xl:grid-cols-3 lg:grid-cols-2 gap-5\">\n        <div v-for=\"(site) in visibleSites\" :key=\"site.siteUrl\">\n          <SiteCard :site=\"site\" class=\"max-w-full\" />\n        </div>\n        <UCard :ui=\"{ body: { base: ['w-full h-full min-h-[275px]'] } }\">\n          <div class=\"flex text-center items-center flex-col justify-center h-full\">\n            <UButton to=\"https://search.google.com/search-console\" variant=\"link\" target=\"_blank\" size=\"xl\" icon=\"i-heroicons-plus\" color=\"gray\" class=\"mb-2\">\n              <span>Add New Site</span>\n            </UButton>\n            <p class=\"text-gray-500 text-xs\">\n              You will need to create a new Property in Google Search Console and then refresh.\n            </p>\n          </div>\n        </UCard>\n      </div>\n    </div>\n  </UContainer>\n</template>\n"
  },
  {
    "path": "pages/dashboard/site/[slug].vue",
    "content": "<script lang=\"ts\" setup>\nimport { useAuthenticatedUser } from '~/composables/auth'\nimport type { GoogleSearchConsoleSite } from '~/types'\n\nconst user = useAuthenticatedUser()\n\nconst slug = useRoute().params.slug as string\n\nconst { data: sites } = await fetchSites()\n\nconst site: GoogleSearchConsoleSite = (sites.value || []).find(site => site.siteUrl === slug)\nif (!sites.value || !site) {\n  throw createError({\n    statusCode: 404,\n    statusMessage: 'Page Not Found',\n  })\n}\n\nconst { data, pending, refresh } = await fetchSite(site)\n\nconst crawl = ref<undefined | true>()\n\nconst nonIndexedUrlsCount = computed(() => (data.value?.nonIndexedUrls || []).length)\nconst period = computed(() => data.value?.period || [])\nconst analytics = computed(() => (data.value?.analytics || { period: { totalClicks: 0 }, prevPeriod: { totalClicks: 0 } }))\n\nconst siteUrlFriendly = useFriendlySiteUrl(slug)\n\nconst params = useUrlSearchParams('history', {\n  removeNullishValues: true,\n  removeFalsyValues: false,\n})\n\nconst tab = computed({\n  get() {\n    return 0\n  },\n  set(value) {\n    if (value === 0 && !pending.value && data.value)\n      refresh()\n\n    if (value)\n      params.tab = String(value)\n    else\n      params.tab = null\n  },\n})\n\nuseHead({\n  title: siteUrlFriendly,\n})\n\nconst crawlerEnabled = useRuntimeConfig().public.features.crawler\n\nconst apiCallLimit = user.value.access === 'pro' ? 200 : useRuntimeConfig().public.indexing.usageLimitPerUser\n</script>\n\n<template>\n  <UContainer>\n    <div class=\"py-10\">\n      <div>\n        <div class=\"relative xl:grid grid-cols-3 gap-10\">\n          <div class=\"col-span-2\">\n            <div class=\"flex items-center justify-between gap-10 mb-5 md:mb-12\">\n              <div class=\"flex items-center gap-7\">\n                <div>\n                  <div class=\"text-lg md:text-3xl flex items-center gap-2 dark:text-gray-300 text-gray-900 \">\n                    <img :src=\"`https://www.google.com/s2/favicons?domain=${siteUrlFriendly}`\" alt=\"favicon\" class=\"w-4 h-4 rounded-sm\">\n                    {{ siteUrlFriendly }}\n                  </div>\n                  <div>\n                    <div class=\"opacity-60 text-sm md:text-lg\">\n                      <template v-if=\"slug.includes('sc-domain:')\">\n                        Domain Property\n                      </template>\n                      <template v-else>\n                        Site Property\n                      </template>\n                    </div>\n                  </div>\n                </div>\n                <div class=\"lg:block hidden\">\n                  <MetricGuage v-if=\"data?.nonIndexedPercent\" :score=\"data.nonIndexedPercent\">\n                    <div class=\"text-xl\">\n                      {{ data.nonIndexedPercent === -1 ? '?' : Math.round(data.nonIndexedPercent * 100) }}\n                    </div>\n                  </MetricGuage>\n                </div>\n              </div>\n              <div v-if=\"!pending\" class=\"lg:flex items-center justify-center gap-10 hidden \">\n                <div class=\"flex flex-col justify-center\">\n                  <span class=\"text-sm opacity-70\">Clicks</span>\n                  <div class=\"text-xl flex items-end gap-2\">\n                    <span>{{ useHumanFriendlyNumber(analytics.period.totalClicks) }}</span>\n                    <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"32\" height=\"32\" viewBox=\"0 0 24 24\" class=\"w-5 h-5 opacity-80\"><path fill=\"#888888\" d=\"m11.5 11l6.38 5.37l-.88.18l-.64.12c-.63.13-.99.83-.71 1.4l.27.58l1.36 2.94l-1.42.66l-1.36-2.93l-.26-.58a.985.985 0 0 0-1.52-.36l-.51.4l-.71.57zm-.74-2.31a.76.76 0 0 0-.76.76V20.9c0 .42.34.76.76.76c.19 0 .35-.06.48-.16l1.91-1.55l1.66 3.62c.13.27.4.43.69.43c.11 0 .22 0 .33-.08l2.76-1.28c.38-.18.56-.64.36-1.01L17.28 18l2.41-.45a.88.88 0 0 0 .43-.26c.27-.32.23-.79-.12-1.08l-8.74-7.35l-.01.01a.756.756 0 0 0-.49-.18M15 10V8h5v2zm-1.17-5.24l2.83-2.83l1.41 1.41l-2.83 2.83zM10 0h2v5h-2zM3.93 14.66l2.83-2.83l1.41 1.41l-2.83 2.83zm0-11.32l1.41-1.41l2.83 2.83l-1.41 1.41zM7 10H2V8h5z\" /></svg>\n                  </div>\n                  <TrendPercentage :value=\"analytics.period.totalClicks\" :prev-value=\"analytics.prevPeriod.totalClicks\" />\n                </div>\n                <div class=\"flex flex-col justify-center\">\n                  <span class=\"text-sm opacity-70\">Impressions</span>\n                  <div class=\"text-xl flex items-end gap-2\">\n                    <span>{{ useHumanFriendlyNumber(analytics.period.totalImpressions) }}</span>\n                    <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"0\" height=\"24\" viewBox=\"0 0 32 32\" class=\"w-5 h-5 opacity-80\"><path fill=\"#888888\" d=\"M30.94 15.66A16.69 16.69 0 0 0 16 5A16.69 16.69 0 0 0 1.06 15.66a1 1 0 0 0 0 .68A16.69 16.69 0 0 0 16 27a16.69 16.69 0 0 0 14.94-10.66a1 1 0 0 0 0-.68ZM16 25c-5.3 0-10.9-3.93-12.93-9C5.1 10.93 10.7 7 16 7s10.9 3.93 12.93 9C26.9 21.07 21.3 25 16 25Z\" /><path fill=\"#888888\" d=\"M16 10a6 6 0 1 0 6 6a6 6 0 0 0-6-6Zm0 10a4 4 0 1 1 4-4a4 4 0 0 1-4 4Z\" /></svg>\n                  </div>\n                  <TrendPercentage :value=\"analytics.period.totalImpressions\" :prev-value=\"analytics.prevPeriod.totalImpressions\" />\n                </div>\n              </div>\n            </div>\n            <UTabs\n              v-model=\"tab\"\n              class=\"col-span-2\" :items=\"[\n                { label: 'Non-Indexed Pages', count: nonIndexedUrlsCount },\n                { label: 'Indexed Pages', count: period.length },\n                { label: 'Keywords', count: data?.keywords?.length || 0 },\n              ]\"\n            >\n              <template #default=\"{ item, index, selected }\">\n                <div class=\"flex items-center gap-2 relative truncate\">\n                  <UBadge v-if=\"item.count\" :color=\"selected ? 'green' : 'gray'\" class=\"hidden md:flex\">\n                    {{ item.count }}\n                  </UBadge>\n                  <UIcon v-else-if=\"index === 0\" name=\"i-heroicons-check-circle\" class=\"w-5 h-5\" :class=\"selected ? 'text-green-400' : 'text-gray-400'\" />\n                  <span class=\"truncate\">{{ item.label }}</span>\n                </div>\n              </template>\n              <template #item=\"{ item }\">\n                <div class=\"mt-4\">\n                  <UCard v-if=\"item.label === 'Non-Indexed Pages'\">\n                    <div class=\"text-sm text-gray-500 \">\n                      <div v-if=\"data?.nonIndexedPercent === -1\" class=\"text-lg text-gray-600 dark:text-gray-300 mb-2\">\n                        <UAlert color=\"yellow\" variant=\"outline\" icon=\"i-heroicons-exclamation-circle\" title=\"Too many URLs\">\n                          <template #description>\n                            <div>\n                              We are unable to calculate the percentage of non-indexed pages due to the large number of\n                              URLs. You can still inspect URLs to see if they are indexed.\n                            </div>\n                          </template>\n                        </UAlert>\n                        <div>Upgrade to Pro to start indexing large sites.</div>\n                        <UButton />\n                        <TableNonIndexedUrls :value=\"data?.period\" :site=\"site\" />\n                      </div>\n                      <div v-else-if=\"!pending && data?.needsCrawl\">\n                        <div>Missing sitemap, you will need our bots to crawl your site.</div>\n                        <UButton @click=\"crawl = true\">\n                          Crawl\n                        </UButton>\n                      </div>\n                      <div v-else-if=\"!pending && !nonIndexedUrlsCount\">\n                        <div class=\"text-lg text-gray-600 dark:text-gray-300 mb-2\">\n                          All of your pages are indexed. Congrats!\n                        </div>\n                      </div>\n                      <div v-else>\n                        <TableNonIndexedUrls v-if=\"data?.nonIndexedPercent !== -1\" :value=\"data?.nonIndexedUrls\" :site=\"site\" />\n                      </div>\n                    </div>\n                  </UCard>\n                  <UCard v-else-if=\"item.label === 'Indexed Pages'\">\n                    <TablePages :value=\"data?.period\" :site=\"site\" />\n                  </UCard>\n                  <UCard v-else>\n                    <TableKeywords :value=\"data?.keywords\" :site=\"site\" />\n                  </UCard>\n                </div>\n              </template>\n            </UTabs>\n          </div>\n          <div class=\"gap-7 relative\">\n            <div class=\"h-[115px] w-full mb-5\">\n              <GraphClicks v-if=\"!pending && data?.graph\" height=\"115\" class=\"mt-9\" :value=\"data.graph.map(g => ({ time: g.time, value: g.clicks }))\" :value2=\"data.graph.map(g => ({ time: g.time, value: g.impressions }))\" />\n            </div>\n            <div class=\"sticky flex flex-col gap-7\" style=\"top: 100px;\">\n              <UCard v-if=\"site.permissionLevel !== 'siteOwner'\">\n                <template #header>\n                  <h2 class=\"text-lg font-bold flex items-center gap-2\">\n                    <UIcon name=\"i-heroicons-lock-closed\" class=\"w-5 h-5\" />\n                    <span>Invalid Permission</span>\n                  </h2>\n                </template>\n                <div class=\"text-sm mb-4\">\n                  <div class=\"mb-2\">\n                    You are not the owner of <NuxtLink target=\"_blank\" :to=\"`https://search.google.com/search-console/ownership?resource_id=${encodeURIComponent(slug)}`\" class=\"font-mono whitespace-nowrap underline\">\n                      {{ slug }}\n                    </NuxtLink> property. You will only be able to view data\n                    and inspect URLs.\n                  </div>\n                  <div>\n                    Please update the permissions in <NuxtLink target=\"_blank\" :to=\"`https://search.google.com/search-console/ownership?resource_id=${encodeURIComponent(slug)}`\" class=\"whitespace-nowrap underline\">\n                      Google Search Console\n                    </NuxtLink> to fix this.\n                  </div>\n                </div>\n              </UCard>\n              <UCard v-else-if=\"user.indexingOAuthIdNext\">\n                <template #header>\n                  <h2 class=\"text-xl font-bold\">\n                    Quota\n                  </h2>\n                  <p class=\"text-sm text-gray-500 dark:text-gray-400\">\n                    You are limited to the number of indexing requests you can make each day.\n                  </p>\n                </template>\n                <div class=\"text-gray-600 dark:text-gray-300\">\n                  <div>\n                    <div class=\"flex items-center justify-between\">\n                      <div>\n                        <div class=\"text-sm\">\n                          Request Indexing\n                        </div>\n                        <div class=\"text-lg font-bold\">\n                          {{ user.quota.indexingApi }}/{{ apiCallLimit }}\n                        </div>\n                        <UProgress :value=\"user.quota.indexingApi / apiCallLimit * 100\" :color=\"user.quota.indexingApi < apiCallLimit ? 'purple' : 'red'\" class=\"mt-1\" />\n                      </div>\n                      <UButton v-if=\"user.access !== 'pro'\" to=\"/account/upgrade\" color=\"purple\" size=\"xs\" :variant=\"user.quota.indexingApi < apiCallLimit ? 'soft' : 'solid'\">\n                        Upgrade\n                      </UButton>\n                    </div>\n                    <div v-if=\"user.quota.indexingApi >= apiCallLimit\" class=\"mt-5 text-gray-600 text-sm\">\n                      <p class=\"mb-2\">\n                        You've used up all of your API calls for the day. They will reset at midnight PTD.\n                      </p>\n                      <template v-if=\"user.access !== 'pro'\">\n                        <p class=\"mb-2\">\n                          Don't feel like waiting? You can upgrade to Pro and get the following:\n                        </p>\n                        <ul class=\"list-disc ml-5\">\n                          <li class=\"mb-2\">\n                            200 API calls / per day\n                          </li>\n                          <li>\n                            <UBadge color=\"blue\" variant=\"soft\">\n                              Soon\n                            </UBadge> Automatic index requests for new pages\n                          </li>\n                        </ul>\n                      </template>\n                    </div>\n                  </div>\n                </div>\n              </UCard>\n              <UCard v-else>\n                <template #header>\n                  <h2 class=\"text-lg font-bold\">\n                    Permissions\n                  </h2>\n                </template>\n                <div class=\"text-sm mb-4\">\n                  <div>\n                    Authorization is required for the <NuxtLink to=\"/\" class=\"underline\">\n                      Web Indexing API\n                    </NuxtLink>.\n                    This is used to submit URLs to be indexed. You can safely revoke it at any\n                    time on the <NuxtLink class=\"underline\">\n                      account page\n                    </NuxtLink>.\n                  </div>\n                </div>\n                <UButton external to=\"/auth/google-indexing\" color=\"gray\" icon=\"i-heroicons-lock-closed\">\n                  Authorize Web Indexing API\n                </UButton>\n              </UCard>\n              <UCard v-if=\"crawlerEnabled\">\n                <template #header>\n                  <h3 class=\"font-semibold text-lg text-gray-700 dark:text-gray-200 mb-1\">\n                    Crawler\n                  </h3>\n                  <p class=\"text-sm text-gray-500\">\n                    Discover pages that may be missing from your sitemap or last crawl. Show meta data with the URLs.\n                  </p>\n                </template>\n                <div>\n                  <div v-if=\"!pending && data?.lastCrawl\" class=\"mb-2\">\n                    Last crawl was {{ useDayjs()(data?.lastCrawl).fromNow() }}.\n                  </div>\n                  <UBadge color=\"sky\" variant=\"subtle\">\n                    Coming Soon\n                  </UBadge>\n                  <UButton color=\"gray\" icon=\"i-heroicons-sparkles\" disabled @click=\"crawl = true\">\n                    <UIcon v-if=\"pending && crawl\" name=\"i-heroicons-arrow-path\" class=\"animate-spin w-5 h-5\" />\n                    Crawl\n                  </UButton>\n                </div>\n              </UCard>\n            </div>\n          </div>\n        </div>\n      </div>\n    </div>\n  </UContainer>\n</template>\n"
  },
  {
    "path": "pages/get-started.vue",
    "content": "<script setup lang=\"ts\">\ndefinePageMeta({\n  layout: 'auth',\n})\n\nuseSeoMeta({\n  title: 'Login',\n})\n\nconst keyLogin = useRuntimeConfig().public.features.keyLogin\n</script>\n\n<template>\n  <div class=\"px-5 py-10 flex xl:flex-row flex-col gap-10\">\n    <UCard class=\"max-w-sm w-full bg-white/75 dark:bg-white/5 backdrop-blur\">\n      <p class=\"text-2xl text-gray-900 dark:text-white font-bold flex items-center gap-2\">\n        Google Account <UBadge color=\"blue\" variant=\"subtle\">\n          Easy\n        </UBadge>\n      </p>\n      <p class=\"text-gray-500 dark:text-gray-400 mt-1 mb-3\">\n        Get up and running in less than a minute using Google.\n      </p>\n      <UButton to=\"/auth/google\" external color=\"gray\" size=\"lg\" class=\"flex justify-center\">\n        <GoogleSvg class=\"w-4 h-4\" />\n        <span>Sign in with Google</span>\n      </UButton>\n      <UAlert title=\"Required Scopes\" class=\"mt-5\">\n        <template #description>\n          <div class=\"space-y-2\">\n            <div>userinfo.email - You may be emailed in the future.</div>\n            <div>\n              <NuxtLink to=\"/\" class=\"underline\">\n                Google Search Console API\n              </NuxtLink> (read only) - Used to query pages that are indexed.\n            </div>\n          </div>\n        </template>\n      </UAlert>\n      <p class=\"mt-5 \">\n        <UIcon name=\"i-heroicons-shield-check\" class=\"text-blue-500 -mb-[3px] mr-1\" />\n        <span>You can delete your data and revoke tokens at any time.</span>\n      </p>\n    </UCard>\n    <div class=\"gap-5 flex flex-col\">\n      <UCard class=\"max-w-sm w-full bg-white/75 dark:bg-white/5 backdrop-blur\">\n        <p class=\"text-2xl text-gray-900 dark:text-white font-bold flex items-center gap-2\">\n          <UIcon name=\"i-heroicons-key\" />Use Your Keys <UBadge variant=\"soft\" color=\"gray\">\n            Hard\n          </UBadge>\n        </p>\n        <p class=\"text-gray-500 dark:text-gray-400 mt-1 mb-3\">\n          Get a full API quota by using your own Google API keys.\n        </p>\n        <div class=\"mb-5\">\n          <ul class=\"ml-7 mb-5 list-disc\" />\n        </div>\n        <UButton :disabled=\"!keyLogin\" color=\"gray\" size=\"lg\" icon=\"i-heroicons-arrow-up-tray\" class=\"flex w-full justify-center\">\n          <span>Upload Key</span>\n          <UBadge color=\"yellow\" variant=\"subtle\">\n            Coming Soon\n          </UBadge>\n        </UButton>\n      </UCard>\n      <UCard class=\"max-w-sm w-full bg-white/75 dark:bg-white/5 backdrop-blur\">\n        <p class=\"text-2xl text-gray-900 dark:text-white font-bold flex items-center gap-2\">\n          <UIcon name=\"i-heroicons-command-line\" /> Run Locally <UBadge variant=\"soft\" color=\"gray\">\n            Hard\n          </UBadge>\n        </p>\n        <p class=\"text-gray-500 dark:text-gray-400 mt-1 mb-3\">\n          Run this site on your own server for maximum privacy and control.\n        </p>\n        <UButton to=\"https://github.com/harlan-zw/request-indexing\" icon=\"i-simple-icons-github\" external color=\"gray\" size=\"lg\" class=\"flex justify-center\">\n          <span>Visit Documentation</span>\n        </UButton>\n      </UCard>\n    </div>\n  </div>\n</template>\n"
  },
  {
    "path": "pages/index.vue",
    "content": "<script lang=\"ts\" setup>\nimport { NuxtSeoKeywords, NuxtSeoPages } from '~/data/home'\n\nconst { loggedIn, user } = useUserSession()\n\nconst lastInspectedBase = Date.now()\n\nconst SiteCards = [\n  {\n    analytics: {\n      period: {\n        totalClicks: 3471,\n        totalImpressions: 64141,\n      },\n      prevPeriod: {\n        totalClicks: 902,\n        totalImpressions: 14100,\n      },\n    },\n    nonIndexedPercent: 0.91,\n    siteUrl: 'https://nuxtseo.com',\n    indexedUrls: Array.from({ length: 109 }),\n    nonIndexedUrls: [\n      {\n        inspectionResult: {\n          inspectionResultLink: 'https://search.google.com/search-console/inspect?resource_id=sc-domain:nuxtseo.com&id=kh5uM9YfPMye-g6EmaWJ0w&utm_medium=link&utm_source=api',\n          indexStatusResult: {\n            verdict: 'NEUTRAL',\n            coverageState: 'Discovered - currently not indexed',\n            robotsTxtState: 'ROBOTS_TXT_STATE_UNSPECIFIED',\n            indexingState: 'INDEXING_STATE_UNSPECIFIED',\n            pageFetchState: 'PAGE_FETCH_STATE_UNSPECIFIED',\n            sitemap: [\n              'https://nuxtseo.com/sitemap.xml',\n            ],\n            crawledAs: 'CRAWLING_USER_AGENT_UNSPECIFIED',\n          },\n        },\n        url: '/blog/hello-world',\n        lastInspected: lastInspectedBase - 1000 * 60,\n      },\n      {\n        inspectionResult: {\n          inspectionResultLink: 'https://search.google.com/search-console/inspect?resource_id=sc-domain:nuxtseo.com&id=SuEcdGMwVNxKwRNrtABaUg&utm_medium=link&utm_source=api',\n          indexStatusResult: {\n            verdict: 'NEUTRAL',\n            coverageState: 'Discovered - currently not indexed',\n            robotsTxtState: 'ROBOTS_TXT_STATE_UNSPECIFIED',\n            indexingState: 'INDEXING_STATE_UNSPECIFIED',\n            pageFetchState: 'PAGE_FETCH_STATE_UNSPECIFIED',\n            sitemap: [\n              'https://nuxtseo.com/sitemap.xml',\n              'https://nuxtseo.com/sitemap.xml',\n            ],\n            referringUrls: [\n              'https://nuxtseo.com/sitemap.xml',\n              'https://nuxtseo.com/sitemap/getting-started/installation',\n              'https://nuxtseo.com/sitemap.xml',\n              'https://nuxtseo.com/sitemap/getting-started/installation',\n            ],\n            crawledAs: 'CRAWLING_USER_AGENT_UNSPECIFIED',\n          },\n        },\n        url: '/is-this-thing-on',\n        lastInspected: lastInspectedBase - 1000 * 94,\n        urlNotificationMetadata: {\n          url: 'https://nuxtseo.com/nuxt-seo/migration-guide/beta-to-rc',\n          latestUpdate: {\n            url: 'https://nuxtseo.com/nuxt-seo/migration-guide/beta-to-rc',\n            type: 'URL_UPDATED',\n            notifyTime: lastInspectedBase - 1000 * 94,\n          },\n        },\n      },\n      {\n        inspectionResult: {\n          inspectionResultLink: 'https://search.google.com/search-console/inspect?resource_id=sc-domain:nuxtseo.com&id=jBbvkezl_lDwTpVNvganYw&utm_medium=link&utm_source=api',\n          indexStatusResult: {\n            verdict: 'PASS',\n            coverageState: 'Submitted and indexed',\n            robotsTxtState: 'ALLOWED',\n            indexingState: 'INDEXING_ALLOWED',\n            lastCrawlTime: '2024-02-08T09:12:23Z',\n            pageFetchState: 'SUCCESSFUL',\n            googleCanonical: 'https://nuxtseo.com/og-image/guides/jpegs',\n            userCanonical: 'https://nuxtseo.com/og-image/guides/jpegs',\n            sitemap: [\n              'https://nuxtseo.com/sitemap.xml',\n            ],\n            crawledAs: 'MOBILE',\n          },\n        },\n        url: '/maybe-it-is',\n        lastInspected: lastInspectedBase - 1000 * 457,\n      },\n      {\n        url: '/one-way-to-find-out',\n      },\n    ],\n    graph: [\n      {\n        clicks: 3,\n        impressions: 59,\n        time: '2023-08-14',\n      },\n      {\n        clicks: 7,\n        impressions: 62,\n        time: '2023-08-15',\n      },\n      {\n        clicks: 2,\n        impressions: 60,\n        time: '2023-08-16',\n      },\n      {\n        clicks: 1,\n        impressions: 52,\n        time: '2023-08-17',\n      },\n      {\n        clicks: 2,\n        impressions: 30,\n        time: '2023-08-18',\n      },\n      {\n        clicks: 0,\n        impressions: 28,\n        time: '2023-08-19',\n      },\n      {\n        clicks: 1,\n        impressions: 44,\n        time: '2023-08-20',\n      },\n      {\n        clicks: 0,\n        impressions: 41,\n        time: '2023-08-21',\n      },\n      {\n        clicks: 0,\n        impressions: 55,\n        time: '2023-08-22',\n      },\n      {\n        clicks: 2,\n        impressions: 70,\n        time: '2023-08-23',\n      },\n      {\n        clicks: 1,\n        impressions: 55,\n        time: '2023-08-24',\n      },\n      {\n        clicks: 5,\n        impressions: 51,\n        time: '2023-08-25',\n      },\n      {\n        clicks: 3,\n        impressions: 34,\n        time: '2023-08-26',\n      },\n      {\n        clicks: 1,\n        impressions: 52,\n        time: '2023-08-27',\n      },\n      {\n        clicks: 3,\n        impressions: 58,\n        time: '2023-08-28',\n      },\n      {\n        clicks: 6,\n        impressions: 89,\n        time: '2023-08-29',\n      },\n      {\n        clicks: 4,\n        impressions: 77,\n        time: '2023-08-30',\n      },\n      {\n        clicks: 0,\n        impressions: 69,\n        time: '2023-08-31',\n      },\n      {\n        clicks: 1,\n        impressions: 40,\n        time: '2023-09-01',\n      },\n      {\n        clicks: 1,\n        impressions: 42,\n        time: '2023-09-02',\n      },\n      {\n        clicks: 2,\n        impressions: 38,\n        time: '2023-09-03',\n      },\n      {\n        clicks: 5,\n        impressions: 72,\n        time: '2023-09-04',\n      },\n      {\n        clicks: 1,\n        impressions: 99,\n        time: '2023-09-05',\n      },\n      {\n        clicks: 11,\n        impressions: 97,\n        time: '2023-09-06',\n      },\n      {\n        clicks: 3,\n        impressions: 89,\n        time: '2023-09-07',\n      },\n      {\n        clicks: 3,\n        impressions: 70,\n        time: '2023-09-08',\n      },\n      {\n        clicks: 2,\n        impressions: 31,\n        time: '2023-09-09',\n      },\n      {\n        clicks: 0,\n        impressions: 38,\n        time: '2023-09-10',\n      },\n      {\n        clicks: 4,\n        impressions: 102,\n        time: '2023-09-11',\n      },\n      {\n        clicks: 4,\n        impressions: 100,\n        time: '2023-09-12',\n      },\n      {\n        clicks: 1,\n        impressions: 114,\n        time: '2023-09-13',\n      },\n      {\n        clicks: 6,\n        impressions: 93,\n        time: '2023-09-14',\n      },\n      {\n        clicks: 6,\n        impressions: 85,\n        time: '2023-09-15',\n      },\n      {\n        clicks: 2,\n        impressions: 49,\n        time: '2023-09-16',\n      },\n      {\n        clicks: 3,\n        impressions: 63,\n        time: '2023-09-17',\n      },\n      {\n        clicks: 10,\n        impressions: 146,\n        time: '2023-09-18',\n      },\n      {\n        clicks: 4,\n        impressions: 127,\n        time: '2023-09-19',\n      },\n      {\n        clicks: 10,\n        impressions: 154,\n        time: '2023-09-20',\n      },\n      {\n        clicks: 5,\n        impressions: 161,\n        time: '2023-09-21',\n      },\n      {\n        clicks: 10,\n        impressions: 99,\n        time: '2023-09-22',\n      },\n      {\n        clicks: 5,\n        impressions: 75,\n        time: '2023-09-23',\n      },\n      {\n        clicks: 7,\n        impressions: 83,\n        time: '2023-09-24',\n      },\n      {\n        clicks: 7,\n        impressions: 132,\n        time: '2023-09-25',\n      },\n      {\n        clicks: 6,\n        impressions: 175,\n        time: '2023-09-26',\n      },\n      {\n        clicks: 5,\n        impressions: 172,\n        time: '2023-09-27',\n      },\n      {\n        clicks: 10,\n        impressions: 134,\n        time: '2023-09-28',\n      },\n      {\n        clicks: 6,\n        impressions: 158,\n        time: '2023-09-29',\n      },\n      {\n        clicks: 10,\n        impressions: 88,\n        time: '2023-09-30',\n      },\n      {\n        clicks: 2,\n        impressions: 102,\n        time: '2023-10-01',\n      },\n      {\n        clicks: 15,\n        impressions: 184,\n        time: '2023-10-02',\n      },\n      {\n        clicks: 13,\n        impressions: 176,\n        time: '2023-10-03',\n      },\n      {\n        clicks: 11,\n        impressions: 209,\n        time: '2023-10-04',\n      },\n      {\n        clicks: 11,\n        impressions: 130,\n        time: '2023-10-05',\n      },\n      {\n        clicks: 11,\n        impressions: 138,\n        time: '2023-10-06',\n      },\n      {\n        clicks: 6,\n        impressions: 112,\n        time: '2023-10-07',\n      },\n      {\n        clicks: 9,\n        impressions: 124,\n        time: '2023-10-08',\n      },\n      {\n        clicks: 9,\n        impressions: 176,\n        time: '2023-10-09',\n      },\n      {\n        clicks: 13,\n        impressions: 166,\n        time: '2023-10-10',\n      },\n      {\n        clicks: 22,\n        impressions: 201,\n        time: '2023-10-11',\n      },\n      {\n        clicks: 14,\n        impressions: 146,\n        time: '2023-10-12',\n      },\n      {\n        clicks: 17,\n        impressions: 155,\n        time: '2023-10-13',\n      },\n      {\n        clicks: 5,\n        impressions: 109,\n        time: '2023-10-14',\n      },\n      {\n        clicks: 8,\n        impressions: 139,\n        time: '2023-10-15',\n      },\n      {\n        clicks: 32,\n        impressions: 278,\n        time: '2023-10-16',\n      },\n      {\n        clicks: 26,\n        impressions: 339,\n        time: '2023-10-17',\n      },\n      {\n        clicks: 20,\n        impressions: 263,\n        time: '2023-10-18',\n      },\n      {\n        clicks: 25,\n        impressions: 269,\n        time: '2023-10-19',\n      },\n      {\n        clicks: 19,\n        impressions: 215,\n        time: '2023-10-20',\n      },\n      {\n        clicks: 13,\n        impressions: 162,\n        time: '2023-10-21',\n      },\n      {\n        clicks: 10,\n        impressions: 162,\n        time: '2023-10-22',\n      },\n      {\n        clicks: 15,\n        impressions: 289,\n        time: '2023-10-23',\n      },\n      {\n        clicks: 20,\n        impressions: 300,\n        time: '2023-10-24',\n      },\n      {\n        clicks: 28,\n        impressions: 372,\n        time: '2023-10-25',\n      },\n      {\n        clicks: 29,\n        impressions: 353,\n        time: '2023-10-26',\n      },\n      {\n        clicks: 18,\n        impressions: 215,\n        time: '2023-10-27',\n      },\n      {\n        clicks: 8,\n        impressions: 150,\n        time: '2023-10-28',\n      },\n      {\n        clicks: 9,\n        impressions: 157,\n        time: '2023-10-29',\n      },\n      {\n        clicks: 26,\n        impressions: 349,\n        time: '2023-10-30',\n      },\n      {\n        clicks: 32,\n        impressions: 329,\n        time: '2023-10-31',\n      },\n      {\n        clicks: 27,\n        impressions: 282,\n        time: '2023-11-01',\n      },\n      {\n        clicks: 24,\n        impressions: 308,\n        time: '2023-11-02',\n      },\n      {\n        clicks: 14,\n        impressions: 216,\n        time: '2023-11-03',\n      },\n      {\n        clicks: 6,\n        impressions: 179,\n        time: '2023-11-04',\n      },\n      {\n        clicks: 6,\n        impressions: 253,\n        time: '2023-11-05',\n      },\n      {\n        clicks: 21,\n        impressions: 405,\n        time: '2023-11-06',\n      },\n      {\n        clicks: 27,\n        impressions: 502,\n        time: '2023-11-07',\n      },\n      {\n        clicks: 27,\n        impressions: 508,\n        time: '2023-11-08',\n      },\n      {\n        clicks: 31,\n        impressions: 468,\n        time: '2023-11-09',\n      },\n      {\n        clicks: 23,\n        impressions: 344,\n        time: '2023-11-10',\n      },\n      {\n        clicks: 9,\n        impressions: 254,\n        time: '2023-11-11',\n      },\n      {\n        clicks: 19,\n        impressions: 305,\n        time: '2023-11-12',\n      },\n      {\n        clicks: 29,\n        impressions: 491,\n        time: '2023-11-13',\n      },\n      {\n        clicks: 32,\n        impressions: 640,\n        time: '2023-11-14',\n      },\n      {\n        clicks: 27,\n        impressions: 551,\n        time: '2023-11-15',\n      },\n      {\n        clicks: 18,\n        impressions: 455,\n        time: '2023-11-16',\n      },\n      {\n        clicks: 18,\n        impressions: 388,\n        time: '2023-11-17',\n      },\n      {\n        clicks: 13,\n        impressions: 284,\n        time: '2023-11-18',\n      },\n      {\n        clicks: 17,\n        impressions: 262,\n        time: '2023-11-19',\n      },\n      {\n        clicks: 24,\n        impressions: 561,\n        time: '2023-11-20',\n      },\n      {\n        clicks: 40,\n        impressions: 570,\n        time: '2023-11-21',\n      },\n      {\n        clicks: 29,\n        impressions: 432,\n        time: '2023-11-22',\n      },\n      {\n        clicks: 20,\n        impressions: 419,\n        time: '2023-11-23',\n      },\n      {\n        clicks: 31,\n        impressions: 536,\n        time: '2023-11-24',\n      },\n      {\n        clicks: 9,\n        impressions: 292,\n        time: '2023-11-25',\n      },\n      {\n        clicks: 19,\n        impressions: 296,\n        time: '2023-11-26',\n      },\n      {\n        clicks: 28,\n        impressions: 566,\n        time: '2023-11-27',\n      },\n      {\n        clicks: 25,\n        impressions: 544,\n        time: '2023-11-28',\n      },\n      {\n        clicks: 37,\n        impressions: 643,\n        time: '2023-11-29',\n      },\n      {\n        clicks: 29,\n        impressions: 491,\n        time: '2023-11-30',\n      },\n      {\n        clicks: 42,\n        impressions: 552,\n        time: '2023-12-01',\n      },\n      {\n        clicks: 20,\n        impressions: 265,\n        time: '2023-12-02',\n      },\n      {\n        clicks: 22,\n        impressions: 369,\n        time: '2023-12-03',\n      },\n      {\n        clicks: 51,\n        impressions: 715,\n        time: '2023-12-04',\n      },\n      {\n        clicks: 34,\n        impressions: 599,\n        time: '2023-12-05',\n      },\n      {\n        clicks: 55,\n        impressions: 701,\n        time: '2023-12-06',\n      },\n      {\n        clicks: 48,\n        impressions: 632,\n        time: '2023-12-07',\n      },\n      {\n        clicks: 30,\n        impressions: 430,\n        time: '2023-12-08',\n      },\n      {\n        clicks: 15,\n        impressions: 361,\n        time: '2023-12-09',\n      },\n      {\n        clicks: 20,\n        impressions: 325,\n        time: '2023-12-10',\n      },\n      {\n        clicks: 43,\n        impressions: 717,\n        time: '2023-12-11',\n      },\n      {\n        clicks: 32,\n        impressions: 655,\n        time: '2023-12-12',\n      },\n      {\n        clicks: 29,\n        impressions: 600,\n        time: '2023-12-13',\n      },\n      {\n        clicks: 41,\n        impressions: 600,\n        time: '2023-12-14',\n      },\n      {\n        clicks: 24,\n        impressions: 439,\n        time: '2023-12-15',\n      },\n      {\n        clicks: 17,\n        impressions: 289,\n        time: '2023-12-16',\n      },\n      {\n        clicks: 26,\n        impressions: 408,\n        time: '2023-12-17',\n      },\n      {\n        clicks: 39,\n        impressions: 722,\n        time: '2023-12-18',\n      },\n      {\n        clicks: 35,\n        impressions: 660,\n        time: '2023-12-19',\n      },\n      {\n        clicks: 35,\n        impressions: 595,\n        time: '2023-12-20',\n      },\n      {\n        clicks: 47,\n        impressions: 690,\n        time: '2023-12-21',\n      },\n      {\n        clicks: 24,\n        impressions: 451,\n        time: '2023-12-22',\n      },\n      {\n        clicks: 23,\n        impressions: 361,\n        time: '2023-12-23',\n      },\n      {\n        clicks: 16,\n        impressions: 453,\n        time: '2023-12-24',\n      },\n      {\n        clicks: 26,\n        impressions: 548,\n        time: '2023-12-25',\n      },\n      {\n        clicks: 36,\n        impressions: 664,\n        time: '2023-12-26',\n      },\n      {\n        clicks: 32,\n        impressions: 614,\n        time: '2023-12-27',\n      },\n      {\n        clicks: 31,\n        impressions: 691,\n        time: '2023-12-28',\n      },\n      {\n        clicks: 39,\n        impressions: 506,\n        time: '2023-12-29',\n      },\n      {\n        clicks: 26,\n        impressions: 458,\n        time: '2023-12-30',\n      },\n      {\n        clicks: 19,\n        impressions: 312,\n        time: '2023-12-31',\n      },\n      {\n        clicks: 36,\n        impressions: 543,\n        time: '2024-01-01',\n      },\n      {\n        clicks: 41,\n        impressions: 731,\n        time: '2024-01-02',\n      },\n      {\n        clicks: 47,\n        impressions: 777,\n        time: '2024-01-03',\n      },\n      {\n        clicks: 53,\n        impressions: 766,\n        time: '2024-01-04',\n      },\n      {\n        clicks: 55,\n        impressions: 720,\n        time: '2024-01-05',\n      },\n      {\n        clicks: 32,\n        impressions: 516,\n        time: '2024-01-06',\n      },\n      {\n        clicks: 37,\n        impressions: 598,\n        time: '2024-01-07',\n      },\n      {\n        clicks: 54,\n        impressions: 969,\n        time: '2024-01-08',\n      },\n      {\n        clicks: 65,\n        impressions: 1124,\n        time: '2024-01-09',\n      },\n      {\n        clicks: 55,\n        impressions: 1103,\n        time: '2024-01-10',\n      },\n      {\n        clicks: 64,\n        impressions: 1099,\n        time: '2024-01-11',\n      },\n      {\n        clicks: 52,\n        impressions: 834,\n        time: '2024-01-12',\n      },\n      {\n        clicks: 24,\n        impressions: 597,\n        time: '2024-01-13',\n      },\n      {\n        clicks: 39,\n        impressions: 858,\n        time: '2024-01-14',\n      },\n      {\n        clicks: 72,\n        impressions: 1314,\n        time: '2024-01-15',\n      },\n      {\n        clicks: 66,\n        impressions: 1297,\n        time: '2024-01-16',\n      },\n      {\n        clicks: 70,\n        impressions: 1343,\n        time: '2024-01-17',\n      },\n      {\n        clicks: 61,\n        impressions: 1090,\n        time: '2024-01-18',\n      },\n      {\n        clicks: 53,\n        impressions: 1075,\n        time: '2024-01-19',\n      },\n      {\n        clicks: 34,\n        impressions: 664,\n        time: '2024-01-20',\n      },\n      {\n        clicks: 47,\n        impressions: 944,\n        time: '2024-01-21',\n      },\n      {\n        clicks: 58,\n        impressions: 1486,\n        time: '2024-01-22',\n      },\n      {\n        clicks: 57,\n        impressions: 1123,\n        time: '2024-01-23',\n      },\n      {\n        clicks: 70,\n        impressions: 1348,\n        time: '2024-01-24',\n      },\n      {\n        clicks: 65,\n        impressions: 1381,\n        time: '2024-01-25',\n      },\n      {\n        clicks: 48,\n        impressions: 1172,\n        time: '2024-01-26',\n      },\n      {\n        clicks: 25,\n        impressions: 728,\n        time: '2024-01-27',\n      },\n      {\n        clicks: 35,\n        impressions: 861,\n        time: '2024-01-28',\n      },\n      {\n        clicks: 93,\n        impressions: 1597,\n        time: '2024-01-29',\n      },\n      {\n        clicks: 82,\n        impressions: 1594,\n        time: '2024-01-30',\n      },\n      {\n        clicks: 88,\n        impressions: 1520,\n        time: '2024-01-31',\n      },\n      {\n        clicks: 70,\n        impressions: 1474,\n        time: '2024-02-01',\n      },\n      {\n        clicks: 49,\n        impressions: 1128,\n        time: '2024-02-02',\n      },\n      {\n        clicks: 28,\n        impressions: 692,\n        time: '2024-02-03',\n      },\n      {\n        clicks: 41,\n        impressions: 952,\n        time: '2024-02-04',\n      },\n      {\n        clicks: 74,\n        impressions: 1571,\n        time: '2024-02-05',\n      },\n      {\n        clicks: 90,\n        impressions: 1444,\n        time: '2024-02-06',\n      },\n    ],\n  },\n  {\n    siteUrl: 'https://unlighthouse.dev',\n    analytics: {\n      period: { totalClicks: 436 },\n      prevPeriod: { totalClicks: 0 },\n    },\n    indexedUrls: Array.from({ length: 45 }),\n    nonIndexedUrls: Array.from({ length: 26 }),\n  },\n  {\n    siteUrl: 'https://harlanzw.com/',\n    analytics: { period: { totalClicks: 308, totalImpressions: 16593 }, prevPeriod: { totalClicks: 185, totalImpressions: 12407 } },\n    indexedUrls: Array.from({ length: 19 }),\n    nonIndexedUrls: Array.from({ length: 3 }),\n    sitemaps: Array.from({ length: 1 }),\n    nonIndexedPercent: 19 / 22,\n  },\n]\n</script>\n\n<template>\n  <div>\n    <Gradient class=\"z-0\" />\n    <UContainer class=\"z-1 relative\">\n      <section class=\"py-5 sm:py-12 xl:py-20\">\n        <div class=\"xl:grid gap-8 xl:grid-cols-12 mx-auto w-full sm:px-6 lg:px-0 px-0\">\n          <div class=\"mx-auto max-w-[50rem] xl:col-span-6 xl:mr-10 xl:ml-0 mb-10 xl:mb-0 flex flex-col justify-center\">\n            <div class=\"text-gray-900 mb-3 dark:text-gray-100 text-center text-4xl font-bold sm:text-5xl lg:text-left lg:text-6xl\">\n              <div class=\"text-2xl opacity-70 font-normal mb-2\">\n                Crawled, but not indexed?\n              </div>\n              <span class=\"max-w-5xl\">Get your pages indexed within 48 hours</span>\n            </div>\n            <p class=\"text-gray-700 dark:text-gray-300 max-w-3xl text-center text-xl lg:text-left mb-5\">\n              A free, open-source tool to request pages to be\n              indexed using the <NuxtLink to=\"/\" class=\"font-semibold\">\n                Web Search Indexing API\n              </NuxtLink> and view your <NuxtLink to=\"https://search.google.com/search-console/about\" class=\"font-semibold\">\n                Google Search Console\n              </NuxtLink> data.\n            </p>\n            <div class=\"mb-10 flex items-center justify-center gap-3 flex-row sm:gap-6 lg:justify-start\">\n              <UButton v-if=\"!loggedIn\" icon=\"i-heroicons-arrow-long-right\" to=\"/get-started\" size=\"xl\" external color=\"green\">\n                <span>Get Started<span class=\"hidden sm:inline\"> For Free</span></span>\n              </UButton>\n              <template v-else>\n                <UButton to=\"/dashboard\" size=\"lg\" color=\"gray\">\n                  <UAvatar :src=\"user.picture\" size=\"xs\" />\n                  Dashboard\n                </UButton>\n              </template>\n              <UButton size=\"xl\" variant=\"link\" color=\"black\" icon=\"i-simple-icons-github\" target=\"_blank\" to=\"https://github.com/harlan-zw/request-indexing\">\n                View code\n              </UButton>\n            </div>\n            <div class=\"text-base ml-5 space-y-5\">\n              <p>\n                ⚡ <a href=\"#\" class=\"underline\">Request indexing</a> on new sites and pages, have them appear on Google in 48 hours.\n              </p>\n              <p>\n                📊 <a href=\"#dashboard\" class=\"underline\">Dashboard</a> to see the search performance of all your Google Search Console sites.\n              </p>\n              <p>\n                🗓️ <a href=\"#dashboard\" class=\"underline\">Keep your site data</a>. Google Search Console data deletes site data longer than 16 months, start keeping it.\n              </p>\n            </div>\n          </div>\n          <div class=\"xl:col-span-6 relative max-w-full flex items-center justify-center\">\n            <UCard class=\"shadow-lg max-w-full\">\n              <template #header>\n                <div class=\"flex items-center justify-between gap-3\">\n                  <div class=\"flex items-center justify-between gap-7 w-full\">\n                    <div>\n                      <div class=\"text-xl sm:text-3xl flex items-center gap-2 dark:text-gray-300 text-gray-900 \">\n                        <img src=\"https://www.google.com/s2/favicons?domain=https://nuxtseo.com\" alt=\"favicon\" class=\"w-4 h-4 rounded-sm\">\n                        nuxtseo.com\n                      </div>\n                      <div>\n                        <div class=\"opacity-60 text-sm sm:text-lg\">\n                          Domain Property\n                        </div>\n                      </div>\n                    </div>\n                    <div class=\"hidden sm:flex items-center text-right gap-3 text-gray-500 text-sm justify-end\">\n                      <span>% of your pages<br> indexed</span>\n                      <MetricGuage :score=\"0.91\">\n                        <div class=\"text-xl\">\n                          91\n                        </div>\n                      </MetricGuage>\n                    </div>\n                  </div>\n                </div>\n              </template>\n              <TableNonIndexedUrls mock :value=\"SiteCards[0].nonIndexedUrls\" :site=\"{ siteUrl: 'test', permissionLevel: 'siteOwner' }\" />\n            </UCard>\n          </div>\n        </div>\n      </section>\n    </UContainer>\n\n    <div class=\"dark:bg-gray-950\">\n      <UContainer class=\"z-1 relative\">\n        <ULandingSection\n          id=\"dashboard\"\n          class=\"relative\"\n          align=\"left\"\n          title=\"Insights for all your sites in one dashboard\"\n          description=\"Google Search Console has no dashboard. Why is that? I'm not sure. It would be pretty useful, just check out these site cards.\"\n          :features=\"[\n            { name: 'All of your sites. One page.', description: 'See the search performance of all your sites in one a simple dashboard.', icon: 'i-heroicons-eye' },\n            { name: 'Growth insights as clear as day.', description: 'See how your pages clicks and impressions are trending between periods, as well as your keywords.', icon: 'i-heroicons-chart-bar-square' },\n            { name: 'See Percent Indexed', description: 'See the search performance of all your sites in one a simple dashboard.', icon: 'i-heroicons-arrow-path' },\n          ]\"\n        >\n          <div class=\"relative\">\n            <SiteCard\n              v-for=\"(card, i) in SiteCards\" :key=\"i\"\n              :site=\"{ siteUrl: card.siteUrl, permissionLevel: 'owner' }\"\n              :mock-data=\"card\"\n              :style=\"{ zIndex: 5 - i, translate: `${(i === 0 ? -1 : i) * 25}px ${i * 50}px` }\"\n              class=\"lg:absolute -bottom-20  right-0 w-full mx-auto max-w-[400px] bg-white shadow-xl\"\n              :class=\"i > 0 ? 'hidden lg:block' : ''\"\n            />\n          </div>\n          <template #bottom>\n            <div class=\"mt-12 max-w-3xl mx-auto\">\n              <UTabs\n                class=\"\" :items=\"[\n                  { label: 'Pages', count: NuxtSeoPages.length },\n                  { label: 'Keywords', count: NuxtSeoKeywords.length },\n                ]\"\n              >\n                <template #default=\"{ item, index, selected }\">\n                  <div class=\"flex items-center gap-2 relative truncate \">\n                    <UBadge v-if=\"item.count\" :color=\"selected ? 'green' : 'gray'\">\n                      {{ item.count }}\n                    </UBadge>\n                    <UIcon v-else-if=\"index === 0\" name=\"i-heroicons-check-circle\" class=\"w-5 h-5\" :class=\"selected ? 'text-green-400' : 'text-gray-400'\" />\n                    <span class=\"truncate\">{{ item.label }}</span>\n                  </div>\n                </template>\n                <template #item=\"{ item }\">\n                  <div class=\"my-5 shadow-lg\">\n                    <UCard v-if=\"item.label === 'Pages'\">\n                      <TablePages :page-count=\"4\" :value=\"NuxtSeoPages\" :site=\"{ siteUrl: 'sc-domain:nuxtseo.com' }\" />\n                    </UCard>\n                    <UCard v-else>\n                      <TableKeywords :page-count=\"4\" :value=\"NuxtSeoKeywords\" :site=\"{ siteUrl: 'sc-domain:nuxtseo.com' }\" />\n                    </UCard>\n                  </div>\n                </template>\n              </UTabs>\n            </div>\n          </template>\n        </ULandingSection>\n\n        <ULandingSection\n          title=\"Google is deleting your valuable data\"\n          description=\"Your data is only stored for 16 months before its permanently deleted from Google Search Console. Without this data you won't be able to see larger trends on your sites.\"\n          align=\"left\"\n          :features=\"[\n            { name: 'Retain for as long as you want', description: 'You have your data retained for as long as you like, you will always have access to it and can export it.', icon: 'i-heroicons-circle-stack' },\n            { name: 'Own your data', description: 'It\\'s your data, do what you want with it. Export it, access it via an API or delete it.', icon: 'i-heroicons-eye-slash' },\n          ]\"\n        >\n          <template #top>\n            <UBadge color=\"blue\" class=\"ml-7 mb-3\" variant=\"outline\">\n              Coming Soon\n            </UBadge>\n          </template>\n          <ULandingTestimonial\n            icon=\"i-simple-icons-google\"\n            quote=\"Search Console keeps data for the last 16 months. As a result, SEO reports in Analytics also include a maximum of 16 months of data.\"\n            :author=\"{ name: 'Google Analytics', to: 'https://support.google.com/analytics/answer/1308621?hl=en#:~:text=Search%20Console%20keeps%20data%20for,is%20collected%20by%20Search%20Console.', target: '_blank' }\"\n            card\n          />\n        </ULandingSection>\n\n        <ULandingSection title=\"You have questions\">\n          <ULandingFAQ\n            :items=\"[\n              {\n                label: 'Does it actually work?',\n                content: 'It does not guarantee to have all of your URLs indexed quicker, but it does, on average, speed up the process.',\n              },\n              {\n                label: 'Will I be penalized for using this?',\n                content: 'Nope! These are public APIs managed by Google. We just make it easier to use them.',\n              }, {\n                label: 'Can I see the code?',\n                content: 'Sure! The entire project is open-source and available on GitHub under the MIT license. Build your own version if you like.',\n              }]\" multiple default-open class=\"max-w-3xl mx-auto\"\n          />\n        </ULandingSection>\n\n        <ULandingSection title=\"Built by developers, for developers\" description=\"Request Indexing is a project build for everyone. It does not paywall you or expect you to know how to code.\">\n          <UPageGrid>\n            <ULandingCard\n              v-for=\"(item, index) in [\n                { title: 'Open Source', description: 'Released under MIT license on GitHub. Built with Nuxt by a Nuxt core team as an idealized version of an open-source SaaS.', icon: 'i-heroicons-check-badge' },\n                { title: 'Private', description: 'It\\'s simple, we want as little of your data as possible to make a great tool. Delete your data at any time.', icon: 'i-heroicons-eye-slash' },\n                { title: 'Secure', description: 'Encryption at rest and transit for sensitive data. Secure hashes to avoid leaking your Google sub, you name.', icon: 'i-heroicons-lock-closed' },\n              ]\" :key=\"index\" v-bind=\"item\"\n            />\n          </UPageGrid>\n        </ULandingSection>\n\n        <ULandingSection>\n          <ULandingCTA title=\"Request Indexing on your pages today\" description=\"Get started for free now and get your pages, find valuable insights through your GSC data and storing your GSC data older than 16 months.\" :links=\"[{ label: 'Get Started', to: '/get-started', size: 'xl' }]\" class=\"bg-gray-100/50 dark:bg-gray-800/50\">\n            <template #description>\n              <div class=\"text-lg max-w-xl mx-auto text-left space-y-5\">\n                <p>\n                  ⚡ <a href=\"#\" class=\"underline\">Request indexing</a> on new sites and pages, have them appear on Google in 48 hours.\n                </p>\n                <p>\n                  📊 <a href=\"#dashboard\" class=\"underline\">Dashboard</a> to see the search performance of all your Google Search Console sites.\n                </p>\n                <p>\n                  🌲 <a href=\"#dashboard\" class=\"underline\">Keep your site data</a>. Google Search Console data deletes site data longer than 16 months, start keeping it.\n                </p>\n              </div>\n            </template>\n          </ULandingCTA>\n        </ULandingSection>\n      </UContainer>\n    </div>\n  </div>\n</template>\n"
  },
  {
    "path": "pages/privacy.vue",
    "content": "<template>\n  <UContainer>\n    <UPage>\n      <UPageHeader title=\"Privacy Policy\" description=\"We value your privacy and want to be transparent about how we handle your information.\" />\n      <UPageBody class=\"prose \">\n        <h2>What We Collect</h2>\n        <p>We gather some basic info to make our service work better for you. This includes stuff like your email address and how you use our site. We only collect what’s necessary to improve your experience.</p>\n\n        <h2>How We Use Your Data</h2>\n        <p>Your data helps us personalize your experience and keep things running smoothly. We use it to fix bugs, improve functionality, and keep you logged in so you don’t have to bother entering your details every time.</p>\n\n        <h2>Cookies</h2>\n        <p>Yes, we use cookies. You can manage them in your browser settings if you like.</p>\n\n        <h2>Analytics</h2>\n        <p>We use the anonymous analytics provided by <a href=\"https://usefathom.com/\" rel=\"nofollow\" target=\"_blank\">Fathom Analytics</a> to see how our tool is doing. This helps us scale and improve without getting nosy.</p>\n\n        <h2>Your Rights</h2>\n        <p>You’ve got control over your info. Want to delete your account or see what data we have on you? Just hit the button on your account page or reach out to us.</p>\n\n        <h2>Keeping Your Data Safe</h2>\n        <p>We take your privacy seriously. We don’t collect sensitive info, and we encrypt authentication tokens to keep them secure. Your trust is important to us.</p>\n\n        <h2>Changes to This Policy</h2>\n        <p>We’ll let you know about any major changes to this policy via email or a notice on our site. It’s a good idea to review this page periodically to stay informed.</p>\n\n        <h2>Got Questions?</h2>\n        <p>If you have any questions about this privacy policy or how we handle your data, feel free to reach out at <a href=\"mailto:harlan@harlanzw.com\">harlan@harlanzw.com</a>.</p>\n      </UPageBody>\n    </UPage>\n  </UContainer>\n</template>\n"
  },
  {
    "path": "pages/terms.vue",
    "content": "<script setup lang=\"ts\">\n</script>\n\n<template>\n  <UContainer>\n    <UPage>\n      <UPageHeader title=\"Terms of Service\" description=\"By accessing or using our service, you're agreeing to these terms, so please read them carefully.\" />\n      <UPageBody class=\"prose \">\n        <h2>Agreement to Terms</h2>\n        <p>By using our service, you agree to be bound by these terms and to follow Google's API Terms where necessary. You can review Google's terms <a href=\"https://developers.google.com/terms\">here</a>.</p>\n\n        <h2>Your Content</h2>\n        <p>Anything you create on our site belongs to you. You have the freedom to delete it whenever you choose.</p>\n\n        <h2>Usage Rules</h2>\n        <p>We expect our users to behave responsibly on our platform:</p>\n        <ul>\n          <li>Avoid any actions that could harm the service or other users, such as hacking or spamming the API.</li>\n        </ul>\n\n        <h2>Service Changes</h2>\n        <p>We may update our service from time to time. Any significant changes will be communicated through email or a notice on our site.</p>\n\n        <h2>Account Termination</h2>\n        <p>If we find that you're not following these terms, particularly regarding misuse of the service, we may terminate your access.</p>\n\n        <h2>Liability and Warranty</h2>\n        <p>Our service is provided on an \"as is\" basis. We don't offer warranties of any kind, and we limit our liability to the fullest extent permitted by law.</p>\n\n        <h2>Dispute Resolution</h2>\n        <p>If you have any issues, we encourage you to contact us directly at <a href=\"mailto:harlan@harlanzw.com\">harlan@harlanzw.com</a> so we can resolve them.</p>\n\n        <h2>Terms Updates</h2>\n        <p>We may update these terms occasionally. We'll inform you about any changes via email. It's your responsibility to stay informed about these changes.</p>\n\n        <h2>Contact Us</h2>\n        <p>If you have any questions or feedback, please don't hesitate to reach out to us at <a href=\"mailto:harlan@harlanzw.com\">harlan@harlanzw.com</a>.</p>\n      </UPageBody>\n    </UPage>\n  </UContainer>\n</template>\n"
  },
  {
    "path": "robots.txt",
    "content": "User-agent: *\nDisallow: /account\nDisallow: /admin\nDisallow: /dashboard\n"
  },
  {
    "path": "server/api/admin/usage.get.ts",
    "content": "import { getMetric } from '~/server/utils/storage'\n\nexport default defineEventHandler(async () => {\n  const pool = createOAuthPool()\n  return {\n    signups: await getMetric('signups'),\n    webIndexingApi: await pool.usage(),\n  }\n})\n"
  },
  {
    "path": "server/api/github/repo.get.ts",
    "content": "import { getQuery } from 'h3'\nimport { $fetch } from 'ofetch'\n\nexport interface GitHubRepo {\n  id: number\n  name: string\n  repo: string\n  description: string\n  createdAt: string\n  updatedAt: string\n  pushedAt: string\n  stars: number\n  watchers: number\n  forks: number\n}\n\nconst cachedGitHubRepo = cachedFunction<GitHubRepo, [string]>(\n  repo => $fetch(`https://ungh.cc/repos/${repo}`).then(r => r.repo as GitHubRepo),\n  {\n    base: 'app',\n    maxAge: 60 * 60,\n    group: 'app/cache',\n    name: 'github-repo',\n    getKey: (repo: string) => repo,\n  },\n)\n\nexport default defineEventHandler(async (event) => {\n  const repoWithOwner = getQuery(event).repo as string\n\n  if (!repoWithOwner)\n    return sendError(event, new Error('Missing repo name.'))\n\n  return cachedGitHubRepo(repoWithOwner)\n})\n"
  },
  {
    "path": "server/api/indexing/[url].post.ts",
    "content": "import { indexing } from '@googleapis/indexing'\nimport type { GaxiosError } from 'googleapis-common'\nimport { OAuth2Client } from 'googleapis-common'\nimport type { indexing_v3 } from '@googleapis/indexing/v3'\nimport { getUserToken, updateUserSite } from '~/server/utils/storage'\nimport { getUserQuotaUsage, incrementUserQuota } from '~/server/utils/quota'\nimport type { SitePage, UserSession } from '~/types'\n\nexport default defineEventHandler(async (event) => {\n  const { user } = event.context.authenticatedData\n\n  const { url } = getRouterParams(event, { decode: true })\n\n  const { siteUrl: _siteUrl } = getQuery(event)\n\n  const siteUrl = decodeURIComponent(_siteUrl as string)\n\n  const tokens = await getUserToken(user.userId, 'indexing')\n\n  const { indexing: indexingConfig } = useRuntimeConfig().public\n  const quotaLimit = user.access === 'pro' ? 200 : indexingConfig.usageLimitPerUser\n  // increment users usage\n  const quota = await getUserQuotaUsage(user.userId, 'indexingApi')\n  if (quota >= quotaLimit) {\n    return sendError(event, createError({\n      statusCode: 429,\n      statusText: 'Daily API Quota exceeded.',\n    }))\n  }\n  const pool = createOAuthPool().get(user.indexingOAuthIdNext || '')\n  if (!user.indexingOAuthIdNext || !pool) {\n    return sendError(event, createError({\n      statusCode: 401,\n      statusText: 'Invalid Google account. Please reconnect your account.',\n    }))\n  }\n  const oauth2Client = new OAuth2Client({\n    clientId: pool.client_id,\n    clientSecret: pool.client_secret,\n  })\n  oauth2Client.setCredentials(tokens!)\n  const api = indexing({\n    version: 'v3',\n    auth: oauth2Client,\n  })\n  const metadata = await api.urlNotifications.getMetadata({\n    url: decodeURIComponent(url),\n  })\n    .then(res => res.data)\n    .catch((e: GaxiosError) => {\n      if (e.status === 404)\n        return { latestUpdate: { type: 'URL_MISSING' } } as indexing_v3.Schema$UrlNotificationMetadata\n      else\n        return { latestUpdate: { type: 'URL_ERROR' } } as indexing_v3.Schema$UrlNotificationMetadata\n    })\n\n  // notify time looks like \"2024-02-05T05:11:09.069175248Z\"\n  // check if the notify time was within the last 48 hours, if so we can skip\n  const submittedLast48Hours = metadata.latestUpdate?.notifyTime\n    ? new Date(metadata.latestUpdate.notifyTime) > new Date(Date.now() - 1000 * 60 * 60 * 48)\n    : false\n  if (metadata.latestUpdate?.type === 'URL_UPDATED' && submittedLast48Hours) {\n    const page: SitePage = { urlNotificationMetadata: metadata, url }\n    // already published, update link and return it\n    await updateUserSite(user.userId, siteUrl, { urls: [page] })\n    return {\n      status: 'already-submitted',\n      url: page,\n    }\n  }\n\n  const res = await api.urlNotifications.publish({\n    requestBody: {\n      type: 'URL_UPDATED',\n      url: decodeURIComponent(url),\n    },\n  })\n    .then(res => res.data)\n\n  await setUserSession(event, <UserSession> {\n    // public data only!\n    user: {\n      quota: {\n        indexingApi: await incrementUserQuota(user.userId, 'indexingApi'),\n      },\n    },\n  })\n\n  const page: SitePage = { ...res, url }\n  await updateUserSite(user.userId, siteUrl, { urls: [page] })\n\n  return {\n    status: 'submitted',\n    url: page,\n  }\n})\n"
  },
  {
    "path": "server/api/indexing/auth.delete.ts",
    "content": "import { OAuth2Client } from 'googleapis-common'\nimport { setUserSession } from '#imports'\nimport { deleteUserToken, getUserToken } from '~/server/utils/storage'\n\nexport default defineEventHandler(async (event) => {\n  const { user } = event.context.authenticatedData\n\n  if (!user.indexingOAuthIdNext!) {\n    return createError({\n      statusCode: 400,\n      message: 'No indexing OAuth found.',\n    })\n  }\n  // need to claim back the token from the pool\n  const pool = createOAuthPool()\n  const oAuth = pool.get(user.indexingOAuthIdNext)\n  if (oAuth)\n    await pool.release(oAuth.id, user.userId)\n\n  // keep a reference of the last indexingOAuthId\n  await setUserSession(event, {\n    user: {\n      indexingOAuthIdNext: '',\n      lastIndexingOAuthIdNext: user.indexingOAuthIdNext,\n    },\n  })\n\n  // delete tokens\n  const tokens = await getUserToken(user.userId, 'indexing')\n  await deleteUserToken(user.userId, 'indexing')\n\n  if (!tokens) {\n    // already deleted\n    return { status: 'ok' }\n  }\n\n  // revoke the token with google\n  const oauth2Client = new OAuth2Client()\n  oauth2Client.setCredentials(tokens!)\n  return oauth2Client.revokeToken(tokens!.refresh_token || tokens.access_token!)\n    .then(res => res.data)\n})\n"
  },
  {
    "path": "server/api/sites/[siteUrl]/[url].get.ts",
    "content": "import { searchconsole } from '@googleapis/searchconsole'\nimport type { GaxiosError } from 'googleapis-common'\nimport { parseURL } from 'ufo'\nimport { createGoogleOAuthClient } from '~/server/utils/api/googleSearchConsole'\nimport { getUserSite, updateUserSite } from '~/server/utils/storage'\nimport type { SitePage } from '~/types'\n\nexport default defineEventHandler(async (event) => {\n  const { tokens, user } = event.context.authenticatedData\n\n  const { siteUrl, url } = getRouterParams(event, { decode: true })\n\n  const { urls } = await getUserSite(user.userId, siteUrl)\n  // find for url\n  const lastInspected = urls.filter(Boolean).find(u => parseURL(u.url).pathname === url)?.lastInspected\n  if (lastInspected) {\n    // compare with current time, if it's within 1 hour then we block them\n    if (Date.now() - lastInspected < 1000 * 60 * 60) {\n      return sendError(event, createError({\n        statusCode: 429,\n        statusText: `You can only check the URL index status once per hour. Try again in ${Math.round((1000 * 60 * 60 - (Date.now() - lastInspected)) / 1000 / 60)} minutes.`,\n      }))\n    }\n  }\n\n  const gscApi = searchconsole({\n    version: 'v1',\n    auth: createGoogleOAuthClient(tokens),\n  })\n  return gscApi.urlInspection.index.inspect({\n    requestBody: {\n      inspectionUrl: url,\n      siteUrl,\n    },\n  })\n    .then(async (res) => {\n      const page: SitePage = { ...res.data, url, lastInspected: Date.now() }\n      await updateUserSite(user.userId, siteUrl, { urls: [page] })\n      // only save lastInspected if it was successful\n      return page\n    })\n    .catch((e: GaxiosError) => {\n      // if we have a 400 error, it means we've been unauthorized, send proper error\n      if (e.response?.status === 400) {\n        throw createError({\n          statusCode: 401,\n          statusText: 'Unauthorized',\n        })\n      }\n      else if (e.response?.status === 500) {\n        throw createError({\n          statusCode: 500,\n          statusText: 'Google Server is rate limiting us. Please try again in a minute.',\n        })\n      }\n      throw e\n    })\n})\n"
  },
  {
    "path": "server/api/sites/[siteUrl]/crawl.post.ts",
    "content": "export default defineEventHandler(async () => {\n  // TODO re-implement\n  // const { tokens, user } = event.context.authenticatedData\n  //\n  // const { siteUrl } = getRouterParams(event, { decode: true })\n\n  // const googleSearchConsoleAnalytics = await fetchGoogleSearchConsoleAnalytics(\n  //   tokens,\n  //   user.analyticsPeriod,\n  //   siteUrl,\n  // )\n  //\n  // const { indexedUrls } = googleSearchConsoleAnalytics\n  //\n  // const indexedPaths = indexedUrls.map(url => withoutTrailingSlash(parseURL(url).pathname))\n  //\n  // const siteCacheKey = `user:${user.userId}:sites:${normalizeSiteUrlForKey(siteUrl)}`\n  // const robots = await fetchRobots({\n  //   cacheKey: siteCacheKey,\n  //   siteUrl,\n  // })\n  //\n  // const nonIndexedUrls = new Set<string>()\n  //\n  // const { crawl } = await getUserSite(user.userId, siteUrl)\n  // let crawledUrls: string[] = []\n  // // cache\n  // crawledUrls = (await crawlSite({\n  //   robots,\n  //   siteUrl: normalizeSiteUrl(siteUrl),\n  // }))\n  // await crawlStorage.setItem('', { updatedAt: Date.now(), urls: crawledUrls })\n  // crawledUrls\n  //   .filter(url => !indexedPaths.includes(url))\n  //   .forEach(url => nonIndexedUrls.add(url))\n\n  // interface CrawlCache { updatedAt: number, urls: string[] }\n  // const crawlStorage = userSiteAppStorage<CrawlCache>(user.userId, siteUrl, 'crawledUrls')\n  // let crawledUrls: string[] = []\n  // if (crawl) {\n  //   // cache\n  //   crawledUrls = (await crawlSite({\n  //     robots,\n  //     siteUrl: normalizedSiteUrl(siteUrl),\n  //   }))\n  //   await crawlStorage.setItem('', { updatedAt: Date.now(), urls: crawledUrls })\n  // }\n  // else {\n  //   const hasCrawlCached = await crawlStorage.hasItem(``)\n  //   if (hasCrawlCached) {\n  //     const cachedCrawl = await crawlStorage.getItem(``)\n  //     crawledUrls = cachedCrawl?.urls || []\n  //     res.lastCrawl = cachedCrawl?.updatedAt\n  //   }\n  //   else if (!sitemapsUrls.length && !crawl) {\n  //     res.needsCrawl = true\n  //   }\n  // }\n  // crawledUrls\n  //   .filter(url => !indexedPaths.includes(url))\n  //   .forEach(url => nonIndexedUrls.add(url))\n  return []\n})\n"
  },
  {
    "path": "server/api/sites/[siteUrl].get.ts",
    "content": "import { parseURL, withoutTrailingSlash } from 'ufo'\nimport { getUserSite, normalizeSiteUrlForKey } from '~/server/utils/storage'\nimport { fetchGoogleSearchConsoleAnalytics } from '~/server/utils/api/googleSearchConsole'\nimport type { NitroAuthData, SiteAnalytics, SiteExpanded } from '~/types'\nimport { fetchRobots, fetchSitemapUrls } from '~/server/utils/crawler/crawl'\n\nconst fetchSite = cachedFunction<SiteAnalytics, [NitroAuthData, string, boolean]>(\n  async ({ tokens, user }: NitroAuthData, siteUrl: string) => {\n    const periodRange = user.analyticsPeriod || { days: 28 }\n    return await fetchGoogleSearchConsoleAnalytics(tokens, periodRange, siteUrl, user.access === 'pro' ? 100001 : 2500)\n  },\n  {\n    maxAge: 60 * 60 * 6, // cache for 6 hours\n    group: 'app',\n    name: 'user',\n    shouldInvalidateCache(_, force?: boolean) {\n      return !!force || import.meta.dev\n    },\n    getKey({ user }: NitroAuthData, siteUrl: string) {\n      return `${user.userId}:sites:${normalizeSiteUrlForKey(siteUrl)}:analytics:${user.analyticsPeriod}`\n    },\n  },\n)\n\nexport default defineEventHandler(async (event) => {\n  const { user } = event.context.authenticatedData\n  const { siteUrl } = getRouterParams(event, { decode: true })\n\n  // TODO throttle force?\n  const force = String(getQuery(event).force) === 'true'\n\n  const sites = await fetchSitesCached(event.context.authenticatedData, force)\n  const site = sites.find(s => s.siteUrl === siteUrl)\n  if (!site) {\n    return sendError(event, createError({\n      statusCode: 404,\n      message: 'Site not found',\n    }))\n  }\n\n  const googleSearchConsoleAnalytics = await fetchSite(event.context.authenticatedData, siteUrl, force)\n\n  // compute the non-indexed urls\n  const { indexedUrls, period, sitemaps } = googleSearchConsoleAnalytics\n\n  if (user.access !== 'pro' && period.length >= 2500) {\n    return {\n      ...site,\n      ...googleSearchConsoleAnalytics,\n      nonIndexedPercent: -1,\n      nonIndexedUrls: [],\n    } satisfies SiteExpanded\n  }\n\n  const indexedPaths = indexedUrls.map(url => withoutTrailingSlash(parseURL(url).pathname))\n  const siteCacheKey = `user:${user.userId}:sites:${normalizeSiteUrlForKey(siteUrl)}`\n  const robots = await fetchRobots({\n    cacheKey: siteCacheKey,\n    siteUrl,\n  })\n  const sitemapPaths = sitemaps!.map(s => s.path).filter(Boolean) as string[]\n  const nonIndexedUrls = new Set<string>()\n  // easy case\n  const sitemapsUrls = sitemapPaths.length\n    ? await fetchSitemapUrls({\n      cacheKey: siteCacheKey,\n      robots,\n      siteUrl,\n      sitemapPaths,\n    })\n    : []\n  sitemapsUrls\n    .map(url => withoutTrailingSlash(parseURL(url).pathname))\n    .filter(url => !indexedPaths.includes(url))\n    // .filter(u => u !== '/') // some trailing slash issues, should fix properly\n    .forEach(url => nonIndexedUrls.add(url))\n\n  const { urls } = await getUserSite(user.userId, siteUrl)\n  return {\n    ...site,\n    ...googleSearchConsoleAnalytics,\n    nonIndexedPercent: indexedUrls.length / (indexedUrls.length + [...nonIndexedUrls].length),\n    nonIndexedUrls: [...nonIndexedUrls].map((url) => {\n      const entry = urls.filter(Boolean).find(u => parseURL(u.url).pathname === url)\n      return {\n        ...entry,\n        url,\n      }\n    }),\n  } satisfies SiteExpanded\n})\n"
  },
  {
    "path": "server/api/sites/list.get.ts",
    "content": "export default defineEventHandler((event) => {\n  return fetchSitesCached(event.context.authenticatedData, String(getQuery(event).force) === 'true')\n})\n"
  },
  {
    "path": "server/api/user/me.delete.ts",
    "content": "import { OAuth2Client } from 'googleapis-common'\nimport { clearUserStorage } from '~/server/utils/storage'\n\nexport default defineEventHandler(async (event) => {\n  const { user, tokens } = event.context.authenticatedData\n\n  if (user.indexingOAuthIdNext) {\n    // need to claim back the token from the pool\n    const pool = createOAuthPool()\n    const oAuth = pool.get(user.indexingOAuthIdNext!)\n    if (oAuth)\n      await pool.release(oAuth.id, user.userId)\n  }\n\n  await incrementMetric('deletedUsers')\n\n  await clearUserStorage(user.userId)\n\n  // // clear user session\n  await clearUserSession(event)\n\n  // revoke the token with google\n  const oauth2Client = new OAuth2Client()\n  oauth2Client.setCredentials(tokens!)\n  return oauth2Client.revokeToken(tokens!.refresh_token || tokens.access_token!)\n    .then(res => res.data)\n})\n"
  },
  {
    "path": "server/api/user/me.post.ts",
    "content": "import type { User } from '~/types'\nimport { updateUser, userMerger } from '~/server/utils/storage'\nimport { mergeUserSessionData } from '~/server/utils/session'\n\nexport default defineEventHandler(async (event) => {\n  const { user: _user } = event.context.authenticatedData\n\n  const { analyticsPeriod, hiddenSites } = await readBody<Partial<User>>(event)\n\n  await incrementMetric('updatedDetails')\n\n  const user = await updateUser(_user.userId, { analyticsPeriod, hiddenSites })\n  return await mergeUserSessionData(event, { user }, userMerger)\n})\n"
  },
  {
    "path": "server/composables/auth.ts",
    "content": "import type { H3Error, H3Event } from 'h3'\nimport { hash } from 'ohash'\nimport type { TokenPayload, UserSession } from '~/types'\nimport { getUserToken } from '~/server/utils/storage'\n\nexport function getHashSecure(input: any) {\n  const appKey = useRuntimeConfig().key\n  // make an object\n  return hash({ input, appKey })\n}\n\nexport async function getAuthenticatedData(event: H3Event): Promise<H3Error | ({ session: UserSession, user: UserSession['user'], sub: string, tokens: TokenPayload['tokens'] })> {\n  const session = (await getUserSession(event)) as UserSession\n  if (!session?.user) {\n    // unauthorized\n    return createError({\n      statusCode: 401,\n      message: 'Unauthorized',\n    })\n  }\n  const token = await getUserToken(session.user.userId, 'login')\n  if (!token) {\n    // unauthorized\n    return createError({\n      statusCode: 401,\n      message: 'Unauthorized',\n    })\n  }\n  return {\n    sub: token.sub,\n    tokens: token.tokens,\n    user: session.user,\n    session,\n  }\n}\n"
  },
  {
    "path": "server/email/welcome.ts",
    "content": "import type { H3Event } from 'h3'\nimport { ServerClient } from 'postmark'\n\nexport const WelcomeEmail = `Thanks for trying out Request Indexing.\n\nHere's the deal: You're one of the first to try it out, and I'm excited to have you on board. However, it's early days, and I'm still actively working on the project to make it the best it can be.\n\nI'd love to hear your thoughts on how I can make Request Indexing better for you. If you need any help, have any feedback or feature requests hit reply and let me know.\n\nP.s. You can find the source code for Request Indexing on GitHub: https://github.com/harlan-zw/request-indexing. Feel free to open an issue or a PR.\n\nCheers\nHarlan`\n\nexport function sendWelcomeEmail(event: H3Event, email: string) {\n  const client = new ServerClient(useRuntimeConfig(event).postmark.apiKey)\n  return client.sendEmail({\n    From: 'harlan@harlanzw.com',\n    Bcc: 'harlan@harlanzw.com',\n    To: email,\n    Subject: 'Welcome to Request Indexing',\n    TextBody: WelcomeEmail,\n  })\n}\n"
  },
  {
    "path": "server/middleware/auth.ts",
    "content": "import { isError } from 'h3'\nimport { getAuthenticatedData } from '~/server/composables/auth'\n\n// TODO move to route rules\n// authenticated by default\nconst NonAuthenticatedPaths = [\n  '/api/_auth/session',\n  '/api/github',\n]\n\nconst AdminPaths = [\n  '/api/admin',\n]\n\nexport default defineEventHandler(async (event) => {\n  if (NonAuthenticatedPaths.some(p => event.path.startsWith(p)))\n    return\n  // only need to auth the context once\n  if (!event.path.startsWith('/api') || event.context.authenticatedData)\n    return\n  const data = await getAuthenticatedData(event)\n  if (isError(data))\n    return sendError(event, data)\n  if (!data.user?.userId) {\n    throw createError({\n      statusCode: 401,\n      message: 'Invalid user data.',\n    })\n  }\n  if (AdminPaths.some(p => event.path.startsWith(p)) && data.user.email !== 'harlan@harlanzw.com') {\n    return sendError(event, createError({\n      statusCode: 401,\n      message: 'Unauthorized',\n    }))\n  }\n  event.context.authenticatedData = data\n})\n"
  },
  {
    "path": "server/routes/auth/google-indexing.get.ts",
    "content": "import {\n  createError,\n  defineEventHandler,\n  getQuery,\n  getRequestURL,\n  sendRedirect,\n} from 'h3'\nimport { parsePath, withQuery } from 'ufo'\nimport { ofetch } from 'ofetch'\nimport { hash } from 'ohash'\nimport { createOAuthPool } from '~/server/utils/oauthPool'\nimport { updateUser, updateUserToken } from '~/server/utils/storage'\nimport { setUserSession } from '#imports'\nimport { getAuthenticatedData } from '~/server/composables/auth'\nimport type { OAuthPoolToken } from '~/types'\n\n// this is a copy of the googleEventHandler from nuxt-auth-utils\n// we need to provide runtime config for the client id and client secret\nexport default defineEventHandler(async (event) => {\n  const authData = await getAuthenticatedData(event)\n  if (isError(authData))\n    return sendError(event, authData)\n\n  const { sub, user, session } = authData\n  const pool = createOAuthPool()\n  let tokenId = session.googleIndexingAuth?.indexingOAuthIdNext || user.indexingOAuthIdNext || user.lastIndexingOAuthIdNext\n  let token: OAuthPoolToken | undefined\n  if (!tokenId) {\n    // generate one\n    token = await pool.free()\n    tokenId = token?.id\n  }\n  else {\n    token = pool.get(tokenId)\n    if (!token) {\n      // claim a free one (pro user fallback)\n      token = await pool.free()\n      tokenId = token?.id\n    }\n  }\n  if (!token || !tokenId) {\n    // sent rate limted, too many users\n    return sendError(event, createError({\n      statusCode: 429,\n      statusMessage: 'Oops, looks like we have too many users right now. Please try again later.',\n    }))\n  }\n  const config = {\n    clientId: token!.client_id,\n    clientSecret: token!.client_secret,\n    authorizationURL: 'https://accounts.google.com/o/oauth2/v2/auth',\n    tokenURL: 'https://oauth2.googleapis.com/token',\n    scope: [\n      'https://www.googleapis.com/auth/indexing',\n    ],\n  }\n\n  const query = getQuery(event)\n  const { code, state } = query\n\n  const redirectUrl = getRequestURL(event).href\n  if (!code) {\n    // get the referrer\n    const referrer = getHeader(event, 'referer')\n    const data = {\n      googleIndexingAuth: { indexingOAuthIdNext: tokenId, referrer, state: hash(new Date()) },\n    }\n    await setUserSession(event, data)\n\n    config.scope = config.scope || ['email', 'profile']\n    return sendRedirect(\n      event,\n      withQuery(config.authorizationURL, {\n        response_type: 'code',\n        client_id: config.clientId,\n        redirect_uri: redirectUrl,\n        scope: config.scope.join(' '),\n        state: data.googleIndexingAuth.state,\n        login_hint: sub,\n        access_type: 'offline',\n        prompt: 'consent',\n      }),\n    )\n  }\n  const authPayload = session.googleIndexingAuth! || {}\n  // cross-site request forgery protection\n  if (authPayload.state !== state) {\n    return sendError(event, createError({\n      statusCode: 401,\n      message: 'Invalid state',\n    }))\n  }\n  const body = {\n    grant_type: 'authorization_code',\n    redirect_uri: parsePath(redirectUrl).pathname,\n    client_id: config.clientId,\n    client_secret: config.clientSecret,\n    code,\n  }\n  const tokens = await ofetch(config.tokenURL, {\n    method: 'POST',\n    body,\n  }).catch((error) => {\n    return { error }\n  })\n  if (tokens.error) {\n    return sendError(event, createError({\n      statusCode: 401,\n      message: `Google login failed: ${tokens.error?.data?.error_description || 'Unknown error'}`,\n      data: tokens,\n    }))\n  }\n\n  // user has claimed spot in pool\n  await pool.claim(tokenId, user.userId)\n  // save the accessToken to the user (server only)\n\n  // delete tokens\n  await updateUserToken(user.userId, 'indexing', tokens)\n  await updateUser(user.userId, { indexingOAuthIdNext: tokenId })\n  await setUserSession(event, { user: { indexingOAuthIdNext: tokenId } })\n\n  await incrementMetric('googleIndexingAuth')\n\n  return sendRedirect(event, authPayload.referrer || '/dashboard')\n})\n"
  },
  {
    "path": "server/routes/auth/google.get.ts",
    "content": "import { defu } from 'defu'\nimport { getUser, getUserToken, incrementMetric, updateUserToken } from '~/server/utils/storage'\nimport { googleAuthEventHandler } from '~/server/utils/auth/googleAuthEventHandler'\nimport { getHashSecure } from '~/server/composables/auth'\nimport type { UserSession } from '~/types'\nimport { getUserQuota } from '~/server/utils/quota'\nimport { sendWelcomeEmail } from '~/server/email/welcome'\n\nexport default googleAuthEventHandler({\n  config: {\n    redirectUrl: '/auth/google',\n    scope: [\n      'https://www.googleapis.com/auth/userinfo.email',\n      'https://www.googleapis.com/auth/webmasters.readonly',\n    ],\n    authorizationParams: {\n      prompt: 'consent',\n      access_type: 'offline',\n    },\n  },\n  async onSuccess(event, { user: _user, tokens }) {\n    const user = _user as { sub: string, picture: string, email: string }\n    // sub is an openid claim that is unique to the user, we don't want to expose this to the client\n    const userId = `user-${getHashSecure(user.sub)}`\n    if (!(await getUser(userId))) {\n      // TODO handle sign up login (emails, etc)\n      await incrementMetric('signups')\n      await sendWelcomeEmail(event, user.email)\n    }\n    const quota = await getUserQuota(userId)\n    await updateUser(userId, {\n      email: user.email,\n    })\n    const userPublicPersistentData = await getUser(userId)\n\n    await setUserSession(event, {\n      // public data only!\n      user: {\n        email: user.email,\n        quota,\n        userId,\n        picture: user.picture,\n        analyticsPeriod: '28d',\n        ...userPublicPersistentData, // contains analyticsPeriod if changed\n      } satisfies UserSession['user'],\n    })\n    const { tokens: currentTokens } = await getUserToken(userId, 'login') || {}\n    await updateUserToken(userId, 'login', {\n      updatedAt: Date.now(),\n      sub: user.sub,\n      tokens: defu(tokens, currentTokens), // avoid refresh token getting deleted\n    })\n    return sendRedirect(event, '/dashboard')\n  },\n  // Optional, will return a json error and 401 status code by default\n  onError(event, error) {\n    console.error('Google OAuth error:', error)\n    return sendRedirect(event, '/')\n  },\n})\n"
  },
  {
    "path": "server/tsconfig.json",
    "content": "{\n  \"extends\": \"../.nuxt/tsconfig.server.json\"\n}\n"
  },
  {
    "path": "server/utils/api/googleSearchConsole.ts",
    "content": "import { parseURL } from 'ufo'\nimport type { Credentials } from 'google-auth-library'\nimport { OAuth2Client } from 'googleapis-common'\nimport type { searchconsole_v1 } from '@googleapis/searchconsole'\nimport { searchconsole } from '@googleapis/searchconsole'\nimport type { GoogleSearchConsoleSite, SiteAnalytics, User } from '~/types'\nimport { normalizeSiteUrl, percentDifference } from '~/server/utils/formatting'\n\n// @ts-expect-error untyped\nimport { tokens } from '#app/token-pool.mjs'\n\nfunction formatDate(date: Date = new Date()) {\n  return `${date.getFullYear()}-${(date.getMonth() + 1).toString().padStart(2, '0')}-${date.getDate().toString().padStart(2, '0')}`\n}\n\nexport function createGoogleOAuthClient(credentials: Credentials, token?: { client_id: string, client_secret: string }) {\n  token = token || tokens[0]\n  return new OAuth2Client({\n    // tells client to use the refresh_token...\n    forceRefreshOnFailure: true,\n    credentials,\n    clientId: token.client_id,\n    clientSecret: token.client_secret,\n  })\n}\n\nexport async function fetchGoogleSearchConsoleSites(credentials: Credentials): Promise<GoogleSearchConsoleSite[]> {\n  const api = searchconsole({\n    version: 'v1',\n    auth: createGoogleOAuthClient(credentials),\n  })\n  return api.sites.list().then(res => res.data.siteEntry! as GoogleSearchConsoleSite[])\n}\n\nasync function recursiveQuery(api: searchconsole_v1.Searchconsole, query: searchconsole_v1.Params$Resource$Searchanalytics$Query, maxRows: number, page: number = 1, rows: searchconsole_v1.Schema$ApiDataRow[] = []) {\n  const rowLimit = query.requestBody?.rowLimit || maxRows\n  const res = await api.searchanalytics.query({\n    ...query,\n    requestBody: {\n      ...query.requestBody,\n      startRow: (page - 1) * rowLimit,\n    },\n  })\n  // add res rows\n  rows.push(...res.data.rows!)\n  if (res.data.rows!.length === rowLimit && res.data.rows!.length < maxRows && page <= 4)\n    await recursiveQuery(api, query, maxRows, page + 1, rows)\n\n  return { data: { rows } }\n}\n\nexport async function fetchGoogleSearchConsoleAnalytics(credentials: Credentials, periodRange: User['analyticsPeriod'], siteUrl: string, maxRows = 1000): Promise<SiteAnalytics> {\n  const api = searchconsole({\n    version: 'v1',\n    auth: createGoogleOAuthClient(credentials),\n  })\n\n  const periodDays = periodRange.includes('d')\n    ? Number.parseInt(periodRange.replace('d', ''))\n    : (Number.parseInt(periodRange.replace('mo', '')) * 30)\n  const startPeriod = new Date()\n  startPeriod.setDate(new Date().getDate() - periodDays)\n  const startPrevPeriod = new Date()\n  startPrevPeriod.setDate(new Date().getDate() - periodDays * 2)\n  const endPrevPeriod = new Date()\n  endPrevPeriod.setDate(new Date().getDate() - periodDays - 1)\n\n  const requestBody = {\n    dimensions: ['page'],\n    type: 'web',\n    aggregationType: 'byPage',\n  }\n  const rowLimit = maxRows > 25000 ? 25000 : maxRows\n  const [keywordsPeriod, keywordsPrevPeriod, period, prevPeriod, graph] = (await Promise.all([\n    // do a query based on keywords instead of dates\n    // period\n    await recursiveQuery(api, {\n      siteUrl,\n      requestBody: {\n        ...requestBody,\n        // 1 month\n        startDate: formatDate(startPeriod),\n        endDate: formatDate(),\n        // keywords\n        dimensions: ['query'],\n        rowLimit,\n      },\n    }, maxRows),\n    // prev period\n    api.searchanalytics.query({\n      siteUrl,\n      requestBody: {\n        ...requestBody,\n        // 1 month\n        startDate: formatDate(startPrevPeriod),\n        endDate: formatDate(endPrevPeriod),\n        // keywords\n        dimensions: ['query'],\n        rowLimit,\n      },\n    }),\n    await recursiveQuery(api, {\n      siteUrl,\n      requestBody: {\n        ...requestBody,\n        // 1 month\n        startDate: formatDate(startPeriod),\n        endDate: formatDate(),\n        rowLimit,\n      },\n    }, maxRows),\n    api.searchanalytics.query({\n      siteUrl,\n      requestBody: {\n        ...requestBody,\n        startDate: formatDate(startPrevPeriod),\n        endDate: formatDate(endPrevPeriod),\n        rowLimit,\n      },\n    }),\n    // do another query but do it based on clicks / impressions for the day\n    api.searchanalytics.query({\n      siteUrl,\n      requestBody: {\n        ...requestBody,\n        startDate: formatDate(startPrevPeriod),\n        endDate: formatDate(),\n        dimensions: ['date'],\n        rowLimit,\n      },\n    }),\n  ]))\n    .map(res => res.data.rows || [])\n  const analytics = {\n    // compute analytics from calcualting each url stats togethor\n    period: {\n      totalClicks: period!.reduce((acc, row) => acc + row.clicks!, 0),\n      totalImpressions: period!.reduce((acc, row) => acc + row.impressions!, 0),\n    },\n    prevPeriod: {\n      totalClicks: prevPeriod!.reduce((acc, row) => acc + row.clicks!, 0),\n      totalImpressions: prevPeriod!.reduce((acc, row) => acc + row.impressions!, 0),\n    },\n  }\n  const normalizedSiteUrl = normalizeSiteUrl(siteUrl)\n  const indexedUrls = period!\n    .map(r => r.keys![0]) // doman property using www.\n    // strip out subdomains, hash and query\n    .filter(r => !r.includes('#') && !r.includes('?')\n    // fix www.\n    && r.startsWith(normalizedSiteUrl),\n    )\n\n  const sitemaps = await api.sitemaps.list({\n    siteUrl,\n  })\n    .then(res => res.data.sitemap || [])\n  return {\n    analytics,\n    sitemaps,\n    indexedUrls,\n    period: period.map((row) => {\n      const prevPeriodRow = prevPeriod.find(r => r.keys![0] === row.keys![0])\n      return {\n        url: parseURL(row.keys![0]).pathname,\n        clicks: row.clicks!,\n        prevClicks: prevPeriodRow ? prevPeriodRow.clicks! : 0,\n        clicksPercent: percentDifference(row.clicks!, prevPeriodRow?.clicks || 0),\n        impressions: row.impressions!,\n        impressionsPercent: percentDifference(row.impressions!, prevPeriodRow?.impressions || 0),\n        prevImpressions: prevPeriodRow ? prevPeriodRow.impressions! : 0,\n      } satisfies SiteAnalytics['period'][0]\n    }),\n    keywords: keywordsPeriod.map((row) => {\n      const prevPeriodRow = keywordsPrevPeriod.find(r => r.keys![0] === row.keys![0])\n      return {\n        keyword: row.keys![0],\n        // position and ctr\n        position: row.position!,\n        positionPercent: percentDifference(row.position!, prevPeriodRow?.position || 0),\n        prevPosition: prevPeriodRow ? prevPeriodRow.position! : 0,\n        ctr: row.ctr!,\n        ctrPercent: percentDifference(row.ctr!, prevPeriodRow?.ctr || 0),\n        prevCtr: prevPeriodRow ? prevPeriodRow.ctr! : 0,\n        clicks: row.clicks!,\n        impressions: row.impressions,\n      } satisfies SiteAnalytics['keywords'][0]\n    }),\n    graph: graph.map((row) => {\n      // fix key\n      return {\n        clicks: row.clicks!,\n        impressions: row.impressions!,\n        time: row.keys![0],\n        keys: undefined,\n      } satisfies SiteAnalytics['graph'][0]\n    }),\n  }\n}\n"
  },
  {
    "path": "server/utils/auth/googleAuthEventHandler.ts",
    "content": "import {\n  createError,\n  eventHandler,\n  getQuery,\n  getRequestURL,\n  sendRedirect,\n} from 'h3'\nimport { parsePath, withQuery } from 'ufo'\nimport { ofetch } from 'ofetch'\nimport { defu } from 'defu'\nimport type { OAuthGoogleConfig } from 'nuxt-auth-utils/dist/runtime/server/lib/oauth/google'\nimport { useRuntimeConfig } from '#imports'\nimport type { OAuthConfig } from '#auth-utils'\n\n// clone of oauth.googleEventHandler from nuxt-auth-utils until\n// https://github.com/Atinux/nuxt-auth-utils/issues/48 is fixed\nexport function googleAuthEventHandler({\n  config,\n  onSuccess,\n  onError,\n}: OAuthConfig<OAuthGoogleConfig & { authorizationParams: any }>) {\n  return eventHandler(async (event) => {\n    config = defu(config, useRuntimeConfig(event).oauth?.google, {\n      authorizationURL: 'https://accounts.google.com/o/oauth2/v2/auth',\n      tokenURL: 'https://oauth2.googleapis.com/token',\n    })\n    const { code } = getQuery(event)\n    if (!config.clientId) {\n      const error = createError({\n        statusCode: 500,\n        message: 'Missing NUXT_OAUTH_GOOGLE_CLIENT_ID env variables.',\n      })\n      if (!onError)\n        throw error\n      return onError(event, error)\n    }\n    const redirectUrl = getRequestURL(event).href\n    if (!code) {\n      config.scope = config.scope || ['email', 'profile']\n      return sendRedirect(\n        event,\n        withQuery(config.authorizationURL, {\n          response_type: 'code',\n          client_id: config.clientId,\n          redirect_uri: redirectUrl,\n          scope: config.scope.join(' '),\n          ...(config.authorizationParams || {}),\n        }),\n      )\n    }\n    const body = {\n      grant_type: 'authorization_code',\n      redirect_uri: parsePath(redirectUrl).pathname,\n      client_id: config.clientId,\n      client_secret: config.clientSecret,\n      code,\n    }\n\n    const tokens = await ofetch(config.tokenURL, {\n      method: 'POST',\n      body,\n    }).catch((error) => {\n      return { error }\n    })\n    if (tokens.error) {\n      const error = createError({\n        statusCode: 401,\n        message: `Google login failed: ${tokens.error?.data?.error_description || 'Unknown error'}`,\n        data: tokens,\n      })\n      if (!onError)\n        throw error\n      return onError(event, error)\n    }\n    const user = await ofetch(\n      'https://www.googleapis.com/oauth2/v3/userinfo',\n      {\n        headers: {\n          Authorization: `Bearer ${tokens.access_token}`,\n        },\n      },\n    )\n    return onSuccess(event, {\n      tokens,\n      user,\n    })\n  })\n}\n"
  },
  {
    "path": "server/utils/crawler/crawl.ts",
    "content": "import { $fetch } from 'ofetch'\nimport { withBase } from 'ufo'\nimport Sitemapper from 'sitemapper'\nimport type { ParsedRobotsTxt } from './robotsTxt'\nimport { parseRobotsTxt } from './robotsTxt'\n\nexport interface CrawlOptions {\n  robots: ParsedRobotsTxt\n  siteUrl: string\n}\n\nexport async function crawlSite(options: CrawlOptions) {\n  // hard path, we need to manually crawl the site, some rules:\n  // 1. we respect canonical URLs, don't include pages that have a different canonical URL\n  // 2. we respect robots meta tag and x-robots-tag headers\n  // 3. we respect robots.txt\n\n  // first we need to make a queue worker where we can batch pages in chunks of 10\n  // we'll use a set to keep track of visited pages\n  const visited = new Set<string>()\n  const queue = new Set<string>()\n\n  // we'll add home page to the queue first and add any URLs it discovered to be processed by the queue\n  queue.add('/')\n\n  // now work the queue, 200 hard cap\n  while (queue.size || visited.size > 200) {\n    // need to work queue in 5 so take 5 from the queue (or however many are left)\n    const chunk = Array.from(queue).slice(0, 5)\n    // remove the chunk from the queue\n    chunk.forEach((url) => {\n      visited.add(url)\n      queue.delete(url)\n    })\n    // process the chunk\n    const chunkResults = (await Promise.all(chunk.map(\n      url => processPage({\n        siteUrl: options.siteUrl,\n        url,\n        // we want to scan at least some pages, so we'll allow the first 5 to be scanned\n        // isValidPath: path => (visited.size < 5) || !options.excludePaths.includes(path),\n        robots: options.robots,\n      }),\n    ))) as { indexable: boolean, reason: string, links?: string[] }[]\n    // add the results to the queue\n    chunkResults\n      .filter(({ indexable }) => indexable)\n      .forEach((results) => {\n        results.links!.forEach(url => !visited.has(url) && queue.add(url))\n      })\n  }\n  return [...visited]\n}\n\nexport async function processPage(options: { robots: ParsedRobotsTxt, url: string, siteUrl: string, isValidPath?: (path: string) => boolean }) {\n  const url = options.url === '/' ? options.siteUrl : withBase(options.url, options.siteUrl)\n  options.siteUrl = options.siteUrl.replace('sc-domain:', 'https://')\n  if (!url.startsWith(options.siteUrl))\n    return { indexable: false, reason: 'wrong domain' }\n\n  // avoid making a request if the URL is not allowed by robots.txt\n  if (!options.robots.test(url))\n    return { indexable: false, reason: 'robots.txt' }\n\n  const res = await $fetch.raw(url)\n    .catch(() => {\n      return { indexable: false, reason: 'error response' }\n    })\n  if (res.indexable === false)\n    return res\n\n  // check meta tag\n  const tag = res.headers['x-robots-tag'] || res.headers['x-robots-tag'] || ''\n  if (tag.includes('noindex'))\n    return { indexable: false, reason: 'x-robots-tag header' }\n\n  // see if we were redirected\n  if (res.headers.location && res.headers.location !== url)\n    return { indexable: false, reason: 'redirect' }\n\n  const html = res._data\n\n  // check for robots meta tag blocking page, need to use regex here\n  // tag may have noindex, but also \"noindex, nofollow\" or \"nofollow, noindex\"\n  if (/<meta[^>]+name=\"robots\"[^>]+content=\"[^\"]*noindex[^\"]*\"[^>]*>/.test(html))\n    return { indexable: false, reason: 'robots meta tag' }\n\n  // check canonical tag\n  const canonical = html.match(/<link[^>]+rel=\"canonical\"[^>]+href=\"([^\"]+)\"/)\n  if (canonical && canonical[1] !== url)\n    return { indexable: false, reason: 'canonical URL different' }\n\n  // parse the page and extract all a tag hrefs that start with a / OR start with the site url\n  const links = res._data.match(/<a[^>]+href=\"([^\"]+)\"/g)\n    .map(a => a.match(/href=\"([^\"]+)\"/)[1])\n    .filter(href => href.startsWith('/') || href.startsWith(options.siteUrl))\n    .map(href => href.startsWith('/') ? href : new URL(href).pathname)\n    // exclude files\n    .filter(href => !href.split('/').pop().includes('.'))\n    // exclude hash or query strings\n    .filter(href => !href.includes('#') && !href.includes('?'))\n    .filter(href => options.isValidPath ? options.isValidPath(href) : true)\n  return { indexable: true, links }\n}\n\nexport async function fetchRobots(options: { cacheKey: string, siteUrl: string }) {\n  const fetchRobotsCached = cachedFunction<string>(async () => {\n    return await $fetch('/robots.txt', {\n      baseURL: options.siteUrl,\n    }).catch(() => `User-agent: *\\nDisallow:`) // allow everything by default\n  }, {\n    group: 'app',\n    maxAge: 60 * 60, // 1 hour\n    name: options.cacheKey,\n    getKey: () => 'robots',\n  })\n\n  return parseRobotsTxt(await fetchRobotsCached())\n}\n\n/**\n * Fetches routes from a sitemap file.\n */\nexport async function fetchSitemapUrls(options: { cacheKey: string, siteUrl: string, sitemapPaths: string[], robots?: ParsedRobotsTxt }) {\n  const sitemaps = options.sitemapPaths || options.robots?.sitemaps || []\n  if (!sitemaps.length)\n    sitemaps.push('/sitemap.xml')\n  // make sure we're working from the host name\n  const sitemap = new Sitemapper({\n    timeout: 15000, // 15 seconds\n  })\n\n  return cachedFunction<string[]>(async () => {\n    return (await Promise.all([...(new Set(sitemaps))]\n      .map(async (url) => {\n        const abs = withBase(url, options.siteUrl)\n        const isTxt = url.endsWith('.txt')\n        if (isTxt) {\n          return await $fetch<string>(abs)\n            .then(text => text.trim().split('\\n'))\n            .catch(() => [])\n        }\n        return await sitemap.fetch(abs).then(r => r.sites).catch(() => [])\n      })))\n      .filter(Boolean)\n      .flat()\n  }, {\n    name: options.cacheKey,\n    group: 'app',\n    getKey: () => 'sitemap',\n    maxAge: 60 * 60, // 1 hour\n  })()\n}\n"
  },
  {
    "path": "server/utils/crawler/robotsTxt.ts",
    "content": "export interface ParsedRobotsTxt {\n  groups: RobotsGroupResolved[]\n  sitemaps: string[]\n  test: (path: string) => boolean\n}\n\nexport type Arrayable<T> = T | T[]\n\nexport type RobotsGroupInput = GoogleInput | YandexInput\n\nexport interface GoogleInput {\n  comment?: Arrayable<string>\n  disallow?: Arrayable<string>\n  allow?: Arrayable<string>\n  userAgent?: Arrayable<string>\n}\n\nexport interface YandexInput extends GoogleInput {\n  cleanParam?: Arrayable<string>\n}\n\nexport interface RobotsGroupResolved {\n  comment: string[]\n  disallow: string[]\n  allow: string[]\n  userAgent: string[]\n  host?: string\n  // yandex only\n  cleanParam?: string[]\n}\n\n/**\n * We're going to read the robots.txt and extract any disallow or sitemaps rules from it.\n *\n * We're going to use the Google specification, the keys should be checked:\n *\n * - user-agent: identifies which crawler the rules apply to.\n * - allow: a URL path that may be crawled.\n * - disallow: a URL path that may not be crawled.\n * - sitemap: the complete URL of a sitemap.\n * - host: the host name of the site, this is optional non-standard directive.\n *\n * @see https://developers.google.com/search/docs/crawling-indexing/robots/robots_txt\n */\nexport function parseRobotsTxt(s: string): ParsedRobotsTxt {\n  // then we'll extract the disallow and sitemap rules\n  const groups: RobotsGroupResolved[] = []\n  const sitemaps: string[] = []\n  let createNewGroup = false\n  let currentGroup: RobotsGroupResolved = {\n    comment: [], // comments are too hard to parse in a logical order, we'll just omit them\n    disallow: [],\n    allow: [],\n    userAgent: [],\n  }\n  // read the contents\n  for (const line of s.split('\\n')) {\n    const sepIndex = line.indexOf(':')\n    // may not exist for comments\n    if (sepIndex === -1)\n      continue\n    // get the rule, pop before the first :\n    const rule = line.substring(0, sepIndex).trim()\n    const val = line.substring(sepIndex + 1).trim()\n\n    switch (rule) {\n      case 'User-agent':\n        if (createNewGroup) {\n          groups.push({\n            ...currentGroup,\n          })\n          currentGroup = <RobotsGroupResolved> {\n            comment: [],\n            disallow: [],\n            allow: [],\n            userAgent: [],\n          }\n          createNewGroup = false\n        }\n        currentGroup.userAgent.push(val)\n        break\n      case 'Allow':\n        currentGroup.allow.push(val)\n        createNewGroup = true\n        break\n      case 'Disallow':\n        currentGroup.disallow.push(val)\n        createNewGroup = true\n        break\n      case 'Sitemap':\n        sitemaps.push(val)\n        break\n      case 'Host':\n        currentGroup.host = val\n        break\n      case 'Clean-param':\n        if (currentGroup.userAgent.includes('Yandex')) {\n          currentGroup.cleanParam = currentGroup.cleanParam || []\n          currentGroup.cleanParam.push(val)\n        }\n        break\n    }\n  }\n  // push final stack\n  groups.push({\n    ...currentGroup,\n  })\n  return {\n    groups,\n    sitemaps,\n    test(path: string) {\n      return indexableFromGroup(groups, path)\n    },\n  }\n}\n\nexport function asArray(v: any) {\n  return typeof v === 'undefined' ? [] : (Array.isArray(v) ? v : [v])\n}\n\nexport function indexableFromGroup(groups: RobotsGroupInput[], path: string) {\n  let indexable = true\n  const wildCardGroups = groups.filter((group: any) => asArray(group.userAgent).includes('*'))\n  for (const group of wildCardGroups) {\n    if (asArray(group.disallow).includes((rule: string) => rule === '/'))\n      return false\n    const hasDisallowRule = asArray(group.disallow)\n      // filter out empty rule\n      .filter(rule => Boolean(rule))\n      .some((rule: string) => path.startsWith(rule))\n    const hasAllowRule = asArray(group.allow).some((rule: string) => path.startsWith(rule))\n    if (hasDisallowRule && !hasAllowRule) {\n      indexable = false\n      break\n    }\n  }\n  return indexable\n}\n\nexport function generateRobotsTxt({ groups, sitemaps }: { groups: RobotsGroupResolved[], sitemaps: string[] }): string {\n  // iterate over the groups\n  const lines: string[] = []\n  for (const group of groups) {\n    // add the comments\n    for (const comment of group.comment || [])\n      lines.push(`# ${comment}`)\n    // add the user agent\n    for (const userAgent of group.userAgent || ['*'])\n      lines.push(`User-agent: ${userAgent}`)\n\n    // add the allow rules\n    for (const allow of group.allow || [])\n      lines.push(`Allow: ${allow}`)\n\n    // add the disallow rules\n    for (const disallow of group.disallow || [])\n      lines.push(`Disallow: ${disallow}`)\n\n    // yandex only (see https://yandex.com/support/webmaster/robot-workings/clean-param.html)\n    for (const cleanParam of group.cleanParam || [])\n      lines.push(`Clean-param: ${cleanParam}`)\n\n    lines.push('') // seperator\n  }\n  // add sitemaps\n  for (const sitemap of sitemaps)\n    lines.push(`Sitemap: ${sitemap}`)\n\n  return lines.join('\\n')\n}\n"
  },
  {
    "path": "server/utils/crypto.ts",
    "content": "import { createCipheriv, createDecipheriv, randomBytes } from 'crypto'\n\n// Function to encrypt a token\nexport function encryptToken(token: string, secretKey: string): string {\n  if (secretKey.length !== 32)\n    throw new Error('Secret key must be 32 characters long for aes-256-gcm.')\n\n  const iv = randomBytes(16) // Initialization vector\n  const cipher = createCipheriv('aes-256-gcm', secretKey, iv)\n\n  let encrypted = cipher.update(token, 'utf8', 'hex')\n  encrypted += cipher.final('hex')\n\n  const authTag = cipher.getAuthTag().toString('hex')\n\n  // Return the IV, encrypted token, and authTag in a single string\n  return `${iv.toString('hex')}:${encrypted}:${authTag}`\n}\n\n// Function to decrypt a token\nexport function decryptToken(encryptedToken: string, secretKey: string): string {\n  if (secretKey.length !== 32)\n    throw new Error('Secret key must be 32 characters long for aes-256-gcm.')\n\n  const parts = encryptedToken.split(':')\n  const iv = Buffer.from(parts[0], 'hex')\n  const encrypted = parts[1]\n  const authTag = Buffer.from(parts[2], 'hex')\n\n  const decipher = createDecipheriv('aes-256-gcm', secretKey, iv)\n  decipher.setAuthTag(authTag)\n\n  let decrypted = decipher.update(encrypted, 'hex', 'utf8')\n  decrypted += decipher.final('utf8')\n\n  return decrypted\n}\n"
  },
  {
    "path": "server/utils/date.ts",
    "content": "// pacific timezone is 7 hours behind UTC, it has 3 letter abbreviation PST\nexport function datePST() {\n  const d = new Date()\n  d.setHours(d.getHours() - 7)\n  // format it as yyyy:mm:dd\n  return d.toISOString().split('T')[0].replace(/-/g, ':')\n}\n"
  },
  {
    "path": "server/utils/formatting.ts",
    "content": "import { withHttps } from 'ufo'\n\nexport function normalizeSiteUrl(siteUrl: string) {\n  return siteUrl.startsWith('sc-domain:') ? withHttps(`${siteUrl.split(':')[1]}/`) : siteUrl\n}\n\nexport function percentDifference(a?: number, b?: number) {\n  if (!b || !a)\n    return 0\n  return ((a - b) / ((a + b) / 2)) * 100\n}\n"
  },
  {
    "path": "server/utils/oauthPool.ts",
    "content": "import type { Storage } from 'unstorage'\nimport { prefixStorage } from 'unstorage'\nimport type { OAuthPoolPayload, OAuthPoolToken } from '~/types'\nimport { appStorage } from '~/server/utils/storage'\n\n// @ts-expect-error runtime\nimport { tokens as _tokens, privateTokens } from '#app/token-pool.mjs'\n\nexport const oAuthPoolStorage = prefixStorage(appStorage as Storage<OAuthPoolPayload>, 'auth:pool')\n\nexport function createOAuthPool() {\n  const tokens = _tokens as OAuthPoolToken[]\n  const { maxUsersPerOAuth } = useRuntimeConfig().indexing\n  return {\n    get(id: string) {\n      const token = tokens.find(t => t.id === id)\n      if (token)\n        return token\n      const privateToken = privateTokens.find(t => t.id === id)\n      if (privateToken)\n        return privateToken\n    },\n    async free() {\n      const available = (await Promise.all(\n        tokens.map(async (k) => {\n          const payload = await oAuthPoolStorage.getItem(k.id) || { id: k.id, users: [] } satisfies OAuthPoolPayload\n          return payload!.users.length < maxUsersPerOAuth ? k : null\n        }),\n      )).filter(Boolean) as OAuthPoolToken[]\n      // get random available token\n      if (available.length)\n        return available[Math.floor(Math.random() * available.length)]\n    },\n    async claim(id: string, userId: string) {\n      const token = tokens.find(t => t.id === id)\n      if (token) {\n        const payload = await oAuthPoolStorage.getItem(token.id) || { id: token.id, users: [] } satisfies OAuthPoolPayload\n        payload.users = [...new Set<string>([...payload.users, userId])]\n        await oAuthPoolStorage.setItem(token.id, payload)\n      }\n    },\n    async release(id: string, userId: string) {\n      const token = tokens.find(t => t.id === id)\n      if (token) {\n        const payload = await oAuthPoolStorage.getItem(token.id) || { id: token.id, users: [] } satisfies OAuthPoolPayload\n        payload.users = payload.users.filter(u => u !== userId)\n        await oAuthPoolStorage.setItem(token.id, payload)\n      }\n    },\n    async usage() {\n      // iterate tokens, fetch payload, count users\n      const usage = await Promise.all(tokens.map(async (t) => {\n        const payload = await oAuthPoolStorage.getItem(t.id) || { id: t.id, users: [] } satisfies OAuthPoolPayload\n        return payload.users.length\n      }))\n      // want to return how many have been used and how many are free\n      return {\n        used: usage.reduce((a, b) => a + b, 0),\n        free: usage.length * maxUsersPerOAuth - usage.reduce((a, b) => a + b, 0),\n      }\n    },\n  }\n}\n"
  },
  {
    "path": "server/utils/quota.ts",
    "content": "import { datePST } from '~/server/utils/date'\nimport type { UserQuota } from '~/types'\n\nexport async function getUserQuotaUsage(userId: string, key: keyof UserQuota) {\n  const quota = await getUserQuota(userId)\n  return quota[key] || 0\n}\n\nexport async function incrementUserQuota(userId: string, key: keyof UserQuota) {\n  const quota = await getUserQuota(userId)\n  quota[key] = (quota[key] || 0) + 1\n  await userAppStorage(userId, 'quota').setItem(`${datePST()}.json`, quota)\n  return quota[key]\n}\n\nexport async function getUserQuota(userId: string) {\n  return (await userAppStorage<UserQuota>(userId, 'quota').getItem(`${datePST()}.json`)) || { indexingApi: 0 } satisfies UserQuota\n}\n"
  },
  {
    "path": "server/utils/session.ts",
    "content": "import type { H3Event, SessionConfig } from 'h3'\nimport type { defuFn } from 'defu'\nimport { defu } from 'defu'\nimport { useRuntimeConfig } from '#imports'\nimport type { UserSession } from '~/types'\n\nexport async function mergeUserSessionData(event: H3Event, data: Partial<UserSession>, merger: typeof defuFn) {\n  const session = await _useSession(event)\n  const fn = merger || defu\n  await session.update(fn(data, session.data))\n  return session.data\n}\n\nlet sessionConfig: SessionConfig\nfunction _useSession(event: H3Event) {\n  sessionConfig = sessionConfig || defu<SessionConfig, SessionConfig[]>({ password: import.meta.env.NUXT_SESSION_PASSWORD }, useRuntimeConfig(event).session as SessionConfig)\n  return useSession(event, sessionConfig)\n}\n"
  },
  {
    "path": "server/utils/sharedCache.ts",
    "content": "import type { GoogleSearchConsoleSite, NitroAuthData } from '~/types'\nimport { fetchGoogleSearchConsoleSites } from '~/server/utils/api/googleSearchConsole'\n\nexport const fetchSitesCached = cachedFunction<GoogleSearchConsoleSite[], [NitroAuthData, boolean]>(\n  async ({ tokens }: NitroAuthData) => {\n    return await fetchGoogleSearchConsoleSites(tokens)\n  },\n  {\n    maxAge: 60 * 10,\n    group: 'app',\n    name: 'user',\n    validate(item) {\n      return Array.isArray(item) && Boolean(item.length)\n    },\n    shouldInvalidateCache(_, force?: boolean) {\n      return !!force\n    },\n    transform(entry) {\n      if (entry.value)\n        return entry.value.sort((a, b) => normalizeSiteUrlForKey(a.siteUrl).localeCompare(normalizeSiteUrlForKey(b.siteUrl)))\n    },\n    getKey: (authData: NitroAuthData) => `${authData.user.userId}:sites`,\n  },\n)\n"
  },
  {
    "path": "server/utils/storage.ts",
    "content": "import type { Storage, StorageValue } from 'unstorage'\nimport { prefixStorage } from 'unstorage'\nimport { createDefu, defu } from 'defu'\nimport { parse, stringify } from 'devalue'\nimport type { User, UserOAuthToken, UserSite } from '~/types'\nimport { decryptToken, encryptToken } from '~/server/utils/crypto'\nimport { useRuntimeConfig } from '#imports'\n\nexport interface UserAppStorage {}\n\nexport function normalizeSiteUrlForKey(siteUrl: string) {\n  // strip https, strip sc-domain, strip non-alphanumeric\n  return siteUrl\n    .replace('https://', '')\n    .replace('sc-domain:', '')\n    .replace(/[^a-z0-9]/g, '')\n}\n\nexport const appStorage = prefixStorage(useStorage(), 'app')\n\nexport function userAppStorage<T extends StorageValue = UserAppStorage>(userId: string, namespace?: string) {\n  if (!userId)\n    throw new Error('userId is required')\n\n  return prefixStorage(appStorage as Storage<T>, `user:${userId}${namespace ? `:${namespace}` : ''}`)\n}\n\nfunction userSiteAppStorage<T extends StorageValue = UserAppStorage>(userId: string, siteUrl: string, namespace?: string) {\n  return prefixStorage(appStorage as Storage<T>, `user:${userId}:sites:${normalizeSiteUrlForKey(siteUrl)}:${namespace ? `:${namespace}` : ''}`)\n}\n\nexport const userMerger = createDefu((data, key, value) => {\n  // we want to override arrays when an empty one is provided\n  if (Array.isArray(data[key]) && Array.isArray(value)) {\n    data[key] = value\n    return true\n  }\n})\n\nexport async function updateUser(userId: string, value: Partial<User>) {\n  const updated = userMerger(value, await getUser(userId))\n  await userAppStorage(userId).setItem('me.json', updated)\n  return updated\n}\n\nexport function getUser(userId: string) {\n  return userAppStorage(userId).getItem('me.json')\n}\n\nexport async function getUserSite(userId: string, siteUrl: string) {\n  return (await userSiteAppStorage<UserSite>(userId, siteUrl)\n    .getItem('payload.json')) || { urls: [] } satisfies UserSite\n}\n\nconst userSiteMerger = createDefu((data, key, value) => {\n  if (key === 'urls' && Array.isArray(value)) {\n    // dedupe the array based on the url\n    const urlsToAdd = [...value].filter(Boolean)\n    data.urls = data.urls.filter(Boolean).map((u) => {\n      const existing = urlsToAdd.findIndex(v => v?.url === u?.url)\n      if (existing >= 0) {\n        const val = urlsToAdd[existing]\n        delete urlsToAdd[existing]\n        return defu(val, u)\n      }\n      return u\n    }).filter(Boolean)\n    // just append new urls\n    data.urls.push(...urlsToAdd)\n    return true\n  }\n})\nexport async function updateUserSite(userId: string, siteUrl: string, payload: Partial<UserSite>) {\n  const siteData = await getUserSite(userId, siteUrl)\n  return userSiteAppStorage<UserSite>(userId, siteUrl).setItem('payload.json', userSiteMerger(payload, siteData))\n}\n\nexport async function clearUserStorage(userId: string) {\n  const keys = await userAppStorage(userId).getKeys()\n  for (const key of keys)\n    await userAppStorage(userId).removeItem(key)\n}\n\nexport async function updateUserToken(userId: string, key: 'indexing' | 'login', value: Partial<UserOAuthToken>) {\n  // need to encrypt\n  const encrypted = encryptToken(stringify(value), useRuntimeConfig().key)\n  return userAppStorage(userId, 'tokens').setItem(`${key}.json`, encrypted)\n}\n\nexport async function getUserToken(userId: string, key: 'indexing' | 'login') {\n  const token = await userAppStorage<string>(userId, 'tokens').getItem(`${key}.json`)\n  if (token)\n    return parse(decryptToken(token, useRuntimeConfig().key))\n  return token\n}\n\nexport function deleteUserToken(userId: string, key: 'indexing' | 'login') {\n  return userAppStorage<string>(userId, 'tokens').removeItem(`${key}.json`)\n}\n\nexport async function incrementMetric(key: string) {\n  const analytics = prefixStorage<number>(appStorage, 'analytics')\n  const newVal = (await analytics.getItem(key) || 0) + 1\n  await analytics.setItem(key, newVal)\n  return newVal\n}\n\nexport async function getMetric(key: string) {\n  return (await prefixStorage<number>(appStorage, 'analytics').getItem(key)) || 0\n}\n"
  },
  {
    "path": "tailwind.config.ts",
    "content": "import type { Config } from 'tailwindcss'\nimport defaultTheme from 'tailwindcss/defaultTheme'\n\nexport default <Partial<Config>>{\n  theme: {\n    extend: {\n      fontFamily: {\n        sans: ['DM Sans', 'DM Sans fallback', ...defaultTheme.fontFamily.sans],\n      },\n    },\n  },\n}\n"
  },
  {
    "path": "tsconfig.json",
    "content": "{\n  // https://nuxt.com/docs/guide/concepts/typescript\n  \"extends\": \"./.nuxt/tsconfig.json\"\n}\n"
  },
  {
    "path": "types/auth.ts",
    "content": "import type { Credentials } from 'google-auth-library'\nimport type { RequiredNonNullable } from '~/types/util'\n\nexport interface NitroAuthData {\n  tokens: TokenPayload['tokens']\n  user: User\n}\n\nexport type UserOAuthToken = RequiredNonNullable<Credentials>\n\nexport interface TokenPayload {\n  updatedAt: number\n  sub: string\n  tokens: RequiredNonNullable<UserOAuthToken>\n}\n\nexport interface OAuthPoolPayload {\n  id: string\n  users: string[]\n}\n\nexport interface OAuthPoolToken {\n  id: string\n  client_id: string\n  client_secret: string\n  label: string\n}\n\nexport interface UserQuota {\n  indexingApi: number\n}\n\nexport interface User {\n  email: string\n  quota: UserQuota\n  userId: string\n  access?: 'pro'\n  picture: string\n  // legacy\n  indexingOAuthId?: string\n  indexingOAuthIdNext?: string\n  lastIndexingOAuthIdNext?: string\n  analyticsPeriod: string\n  hiddenSites?: string[]\n}\n\nexport interface UserSession {\n  sub: string\n  user: User\n  // used when redirecting to Web Indexing API OAuth\n  googleIndexingAuth?: {\n    indexingOAuthId: string\n    referrer: string\n    state: string\n  }\n}\n"
  },
  {
    "path": "types/data.ts",
    "content": "import type { searchconsole_v1 } from '@googleapis/searchconsole/v1'\nimport type { indexing_v3 } from '@googleapis/indexing/v3'\nimport type { RequiredNonNullable } from '~/types/util'\n\nexport interface IndexedUrl extends RequiredNonNullable<searchconsole_v1.Schema$ApiDataRow> {\n}\nexport interface SitePage {\n  url: string\n  lastInspected?: number\n  // inspect url gsc response\n  inspectionResult?: searchconsole_v1.Schema$UrlInspectionResult\n  // submit url for indexing response\n  urlNotificationMetadata?: indexing_v3.Schema$UrlNotificationMetadata\n}\n\nexport interface UserSite {\n  urls: SitePage[]\n  crawl?: {\n    updatedAt: number\n    urls: string[]\n  }\n}\n\nexport interface GoogleSearchConsoleSite {\n  siteUrl: string\n  permissionLevel: 'siteOwner' | 'siteRestrictedUser' | 'siteFullUser'\n}\n\nexport interface SiteAnalytics {\n  analytics: {\n    period: {\n      totalClicks: number\n      totalImpressions: number\n    }\n    prevPeriod: {\n      totalClicks: number\n      totalImpressions: number\n    }\n  }\n  sitemaps: searchconsole_v1.Schema$WmxSitemap[]\n  indexedUrls: string[]\n  period: {\n    url: string\n    clicks: number\n    clicksPercent: number\n    prevClicks: number\n    impressions: number\n    impressionsPercent: number\n    prevImpressions: number\n  }[]\n  keywords: {\n    keyword: string\n    position: number\n    prevPosition: number\n    positionPercent: number\n    ctr: number\n    ctrPercent: number\n    prevCtr: number\n    clicks: number\n  }[]\n  graph: {\n    keys?: undefined\n    time: string\n    clicks: number\n    impressions: number\n  }[]\n}\n\nexport interface SiteExpanded extends SiteAnalytics, GoogleSearchConsoleSite {\n  nonIndexedPercent: number\n  nonIndexedUrls: SitePage[]\n}\n"
  },
  {
    "path": "types/index.ts",
    "content": "export * from './auth'\nexport * from './data'\n"
  },
  {
    "path": "types/nitro.d.ts",
    "content": "import type { NitroAuthData } from '~/types/auth'\n\ndeclare module 'h3' {\n  export interface H3EventContext {\n    authenticatedData: NitroAuthData\n  }\n}\n\nexport {}\n"
  },
  {
    "path": "types/util.ts",
    "content": "export type NonNullable<T> = Exclude<T, null | undefined>\n\nexport type RequiredNonNullable<T> = Required<Exclude<T, null | undefined>>\n"
  },
  {
    "path": "vitest.config.ts",
    "content": "import { defineVitestConfig } from '@nuxt/test-utils/config'\n\nexport default defineVitestConfig({\n  // any custom Vitest config you require\n  test: {\n\n    watchExclude: ['.db/**', '.nuxt/**', '.saas'],\n  },\n})\n"
  }
]