The SEO panel can tell you whether a post ticks the boxes, but not whether anyone’s finding it. Search Console can. With this integration connected, the bottom of the SEO tab shows the page’s clicks, impressions, average click-through rate and average position for the last 28 days, and a table of the queries that brought people there, so you can see which words are already working before you decide what to optimise.
Like GitHub sign-in, this uses OAuth, and the token exchange needs a client secret that can’t live in the browser. The Pages Functions under functions/api/gsc/ handle it. Setting it up means creating a Google Cloud project, which is the tedious part; everything on the Emet side is three environment variables and three lines of YAML.
What you’ll need#
- A Google account that has access to the site’s property in Search Console.
- A Google Cloud project you can create credentials in.
- Emet deployed on Cloudflare Pages with
site.urlset inadmin/site.yaml. The page URL Emet queries is built fromsite.urlplus the entry’s permalink, so without it the queries go nowhere.
Creating the Google Cloud project#
- In the Google Cloud Console, open the project dropdown at the top and click New project. Name it something like “Emet CMS” and click Create, then select it.
- Go to APIs & Services, then Library, search for “Google Search Console API”, open it and click Enable.
Configuring the consent screen#
- Go to APIs & Services, then OAuth consent screen.
- Choose External (unless everyone who’ll use the CMS is in your Google Workspace, in which case Internal skips the test-user step) and click Create.
- Fill in App name, User support email and Developer contact information, then Save and continue.
- On Scopes, click Add or remove scopes, find
https://www.googleapis.com/auth/webmasters.readonlyand tick it. Emet asks for read-only access and nothing else. Update, then Save and continue. - On Test users, add the Google accounts of everyone who’ll connect Search Console, then Save and continue.
WarningTest mode means test users only. An external app in testing lets only the listed test users through; anyone else gets “Access blocked” from Google. Publishing the app for general use needs Google’s verification, which for a private CMS isn’t worth the paperwork. Just keep the test user list current.
Creating the OAuth client#
- Go to APIs & Services, then Credentials, and click Create credentials, then OAuth client ID.
- Set Application type to Web application and give it a name.
- Under Authorized JavaScript origins, add your CMS origin, e.g.
https://cms.example.com, andhttp://localhost:5173for local development. - Under Authorized redirect URIs, add the CMS URL with its path and trailing slash, e.g.
https://cms.example.com/andhttp://localhost:5173/. Emet redirects back to the page it was opened from, so this must match exactly. - Click Create and copy the Client ID and Client secret from the dialog.
Configuring Emet#
The client ID is needed in two places, which catches people out: once in the browser to start the flow, and once in the Pages Function to finish it.
| Variable | Where | Value |
|---|---|---|
VITE_GOOGLE_CLIENT_ID |
Build | The client ID. The Connect button errors with “Google Client ID not configured” without it. |
GOOGLE_CLIENT_ID |
Runtime | The same client ID, for the Pages Functions. |
GOOGLE_CLIENT_SECRET |
Runtime | The client secret. |
Add them on the Cloudflare Pages project under Settings, Variables and Secrets, and redeploy. The three functions (/api/gsc/token, /api/gsc/refresh and /api/gsc/revoke) return 501 Google OAuth not configured until both runtime variables are present.
Then tell Emet which Search Console property the site is, in admin/site.yaml:
gsc:
site_url: "https://example.com/"
cache_ttl: 3600| Key | Required | What it does |
|---|---|---|
site_url |
Yes | The property as Search Console shows it. For a URL-prefix property that’s the full URL with scheme and trailing slash; for a domain property it’s sc-domain:example.com. Get this wrong and every query returns a permissions error. |
cache_ttl |
No | How long, in seconds, to keep a page’s data in the browser before refetching. Default 3600. |
enabled |
No | Set to false to turn the integration off for this site without removing the block. |
The setup wizard writes site_url if you fill it in on the Integrations step.
Connecting#
- Open any entry and go to the SEO tab. At the bottom, the Search Console section shows Connect Search Console.
- Click it. You’re sent to Google’s consent screen; approve the read-only scope.
- Google sends you back to the CMS, which exchanges the code, stores the tokens and reloads on the entry you were editing.
The section now shows four figures, Clicks, Impressions, Avg CTR and Avg Position, the exact URL that was queried (handy for spotting a permalink mismatch), and the top ten queries with clicks, impressions, CTR and position. Each position has an arrow comparing it with the same query’s position over the last 56 days, as a rough trend. Show all N keywords expands the table to every query Google returned, up to 100.
Figures cover the 28 days ending three days ago, because Search Console’s data lags by about that much. Results are cached per page for cache_ttl; the Cached label turns into Refresh when you can fetch fresh data, and clicking it does so.
The connection is per person and per browser. Tokens are stored in local storage under a key that includes your GitHub username, so two people sharing a machine each connect their own Google account. The access token is refreshed five minutes before it expires. Disconnect revokes the tokens with Google and forgets them.
When it goes wrong#
- “Google OAuth not configured” (501):
GOOGLE_CLIENT_IDorGOOGLE_CLIENT_SECRETis missing on the Pages project, or the functions didn’t deploy. - “Google Client ID not configured” when clicking Connect:
VITE_GOOGLE_CLIENT_IDwasn’t set at build time. Set it and rebuild. - Google says “redirect_uri_mismatch”: the CMS URL isn’t in the client’s authorised redirect URIs, or differs in path or trailing slash.
- Google says “Access blocked”: your account isn’t a test user on the consent screen.
- “Invalid OAuth state” on return: the flow was started in one tab and finished in another, or session storage was cleared. Try again from the button.
- “No access to this site in Search Console” (403):
site_urldoesn’t match the property exactly, or the Google account you connected doesn’t have access to it. - “Authentication expired” (401): the refresh failed, usually because the token was revoked. Click Sign in again.
- “No keyword data for this page yet”: the queried URL (shown under the figures) doesn’t match how Google indexes the page, the page is new, or it genuinely gets no search traffic. Check the permalink settings for the section first.
Local development#
Add http://localhost:5173 and http://localhost:5173/ to the OAuth client as above, put GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET in .dev.vars, and run the dev server with VITE_FORCE_AUTH=1 and VITE_GOOGLE_CLIENT_ID set so /api is proxied to wrangler pages dev on port 8788. The GitHub App page has the full commands; the same two processes serve both integrations.
Where next?#
- SEO for the panel this data appears in.
- Environment variables for everything the deployment reads.