Förhandsvisning
Kafé Lindgren är ett påhittat exempel. Inget konto, inget sparas. Så här ser din rapport ut.
Kafé LindgrenPåhittat exempel
Förhandsvisning
Kafé Lindgren är ett påhittat exempel. Inget konto, inget sparas. Så här ser din rapport ut.
Din hemsida är byggd som en kodsajt. Koppla den en gång genom att klistra in en instruktion i AI-verktyget du byggde sajten med. Efter det gör vi ändringarna du godkänner automatiskt.
Byggd med ett AI-verktyg. Stämmer inte? Välj själv
Kopplingen lägger till ett litet skript på dina sidor. Det räknar sidvisningar (utan cookies och utan personuppgifter) och lägger in de textblock från SEOmotor som du har godkänt. Allt annat som ändrar din sajt, till exempel omdirigeringar och säkerhetsrubriker, kommer som förslag under Ändringar och görs först när du godkänt dem.
Görs en gång. Efter det hämtar din sajt de ändringar du godkänner hos oss, och skriver in dem i sidan innan den skickas. Då ser Google och AI-tjänster som ChatGPT dem direkt, och du behöver inte göra något mer. Skickas sajten i dag ut som färdiga filer gör instruktionen om den så att en liten server skickar ut sidorna. Det kan kosta lite i ditt AI-verktyg, till exempel i Replit.
Inte provad med det här verktyget än. Kopplingen är testad med Replit. Kör sajten utan egen server svarar AI-verktyget du byggde sajten med att den inte kan köra en server, och då går det inte att koppla på det här sättet. Steg 4 visar direkt om kopplingen fungerar: SEOmotor räknar sajten som kopplad först när kopplingens märkning syns i sidan som servern skickar.
Koppla sajten till SEOmotor så att rubriker och texter för Google kan uppdateras automatiskt. Gör exakt så här:
1. Skapa filen server/seomotor.ts i serverkoden med exakt det här innehållet, utan ändringar:
```ts
// SEOmotor-koppling (version 7). Ändra inte filen.
// Kör SEOmotors motor, som skriver in de SEO-ändringar du godkänt i SEOmotor i sidan innan den skickas. Motorn
// hämtas från SEOmotor och körs bara om den är signerad med SEOmotors nyckel; den uppdateras därifrån, så att du
// aldrig behöver ändra något här igen. Går SEOmotor inte att nå används de senaste ändringarna.
// Kopplingen lägger också in ett osynligt märke och ett litet skript som räknar sidvisningar (utan cookies) och visar textblock du
// godkänt. Omdirigeringar och säkerhetsrubriker görs bara efter ett godkännande i SEOmotor.
import { createHash, createPublicKey, verify } from "node:crypto";
const FEED = "https://seomotor.se/api/kod/a29kOmthZmVsaW5kZ3Jlbi5zZQ.Od4F8kG1Y5dl7FBPx5LSzl_4ZFxfL6W1yDqI6HGoRWI";
const MARKER = "13d99400c44266af";
const INDEXNOW = "e5392a61eba16b2eaec0da88ed6cf3c6";
const PUBLIC_KEY = "MCowBQYDK2VwAyEA/moXv8nDsQmU9CZwjFVNN/BIvvE4MK+QzCXiUr47v6U=";
const ORIGIN = FEED.split("/api/kod/")[0];
const MOTOR = ORIGIN + "/kod/motor.mjs";
// Besöksräknaren: räknar sidvisningar utan cookies och utan personuppgifter.
const COUNTER_SRC = ORIGIN + "/kod/r.js";
const COUNTER_KEY = FEED.split("/").pop();
type Motor = { withSeo: (html: string, url: string, ctx: unknown) => string; middleware: (req: unknown, res: unknown, ctx: unknown) => boolean | Promise<boolean> };
let feedData: unknown = null;
let feedAt = 0;
let feedTried = 0;
let feedSaved = "";
let feedWritten = 0;
let feedPending: Promise<unknown> | null = null;
let feedLoad: Promise<void> | null = null;
let feedDisk: Promise<FeedDisk | null> | null = null;
type FeedStore = { get: (k: string) => Promise<unknown>; set: (k: string, v: unknown, o?: { ttl?: number; name?: string }) => Promise<unknown>; delete?: (k: string) => Promise<unknown> };
type FeedDisk = { read: () => Promise<string>; write: (s: string) => Promise<void>; drop: () => Promise<void> };
// Ändringarna (version 7): sidan väntar aldrig på SEOmotor när det finns ett tidigare svar. Det senaste fungerande svaret
// används direkt och hämtas på nytt i bakgrunden högst var femte minut (en minut efter ett fel), hur lång tid SEOmotor än
// tar på sig. Svaret sparas också där servern tillåter det, så att en ny instans har det direkt: i Vercels Runtime Cache
// (finns kvar efter nya instanser och deployer) och i en fil i serverns tempkatalog. Bara en helt ny installation utan
// sparat svar väntar på första hämtningen, högst 1,5 sekunder; hämtningen fortsätter i bakgrunden, så att nästa sida har
// ändringarna. Svarar SEOmotor att kopplingen inte finns (404) tas det sparade bort.
const FEED_ID = "seomotor-feed-" + feedHash(FEED);
const FEED_TTL = 30 * 86_400;
function feedHash(s: string): string {
let h = 0x811c9dc5;
for (let i = 0; i < s.length; i++) { h ^= s.charCodeAt(i); h = Math.imul(h, 0x01000193); }
return (h >>> 0).toString(16);
}
const pause = (ms: number) => new Promise<null>((r) => setTimeout(() => r(null), ms));
// Vercels Runtime Cache och waitUntil, samma som getCache() och waitUntil() i @vercel/functions läser. Finns inte hos andra värdar.
function vercel(): { cache?: FeedStore; waitUntil?: (p: Promise<unknown>) => void } {
try {
const g = globalThis as unknown as Record<symbol, { get?: () => unknown } | undefined>;
const c = g[Symbol.for("@vercel/request-context")]?.get?.();
return c && typeof c === "object" ? (c as { cache?: FeedStore; waitUntil?: (p: Promise<unknown>) => void }) : {};
} catch {
return {};
}
}
function background(p: Promise<unknown>): void {
const q = p.catch(() => {});
try { vercel().waitUntil?.(q); } catch { /* körs ändå */ }
}
// En fil i serverns tempkatalog, bara där Node finns.
function disk(): Promise<FeedDisk | null> {
if (!feedDisk) feedDisk = (async () => {
try {
const fs = await import(/* webpackIgnore: true */ /* turbopackIgnore: true */ "node:fs");
const os = await import(/* webpackIgnore: true */ /* turbopackIgnore: true */ "node:os");
const file = os.tmpdir() + "/" + FEED_ID + ".json";
return {
read: () => fs.promises.readFile(file, "utf8"),
write: async (s: string) => { const part = file + "." + process.pid + ".part"; await fs.promises.writeFile(part, s); await fs.promises.rename(part, file); },
drop: () => fs.promises.unlink(file).catch(() => {}),
};
} catch {
return null;
}
})();
return feedDisk;
}
// Sparar svaret när det ändrats (och en gång om dygnet ändå, så att det inte går ut).
function keepFeed(data: unknown, at: number): void {
feedData = data;
feedAt = at;
let text = "";
try { text = JSON.stringify(data); } catch { return; }
if (text === feedSaved && at - feedWritten < 86_400_000) return;
feedSaved = text;
feedWritten = at;
const store = vercel().cache;
if (store) background(store.set(FEED_ID, { v: 1, at, feed: data }, { ttl: FEED_TTL, name: "seomotor-feed" }));
background(disk().then((d) => (d ? d.write('{"v":1,"at":' + at + ',"feed":' + text + "}") : undefined)));
}
function forgetFeed(): void {
feedData = null;
feedAt = 0;
feedSaved = "";
feedWritten = 0;
const store = vercel().cache;
if (store && store.delete) background(store.delete(FEED_ID));
background(disk().then((d) => (d ? d.drop() : undefined)));
}
function useSaved(x: unknown): void {
let d = x;
if (typeof d === "string") { try { d = JSON.parse(d); } catch { return; } }
const e = d && typeof d === "object" ? (d as { v?: unknown; at?: unknown; feed?: unknown }) : null;
if (feedData !== null || !e || e.v !== 1 || !e.feed || typeof e.feed !== "object") return;
feedData = e.feed;
feedAt = Number(e.at) || 0;
feedWritten = feedAt;
try { feedSaved = JSON.stringify(e.feed); } catch { feedSaved = ""; }
}
// Det sparade svaret läses en gång per instans, innan något hämtas.
function loadSaved(): Promise<void> {
if (!feedLoad) feedLoad = (async () => {
const store = vercel().cache;
if (store) { try { useSaved(await Promise.race([store.get(FEED_ID), pause(1000)])); } catch { /* inget sparat */ } }
if (feedData === null) { try { const d = await disk(); if (d) useSaved(await d.read()); } catch { /* inget sparat */ } }
})();
return feedLoad;
}
function refreshFeed(): Promise<unknown> {
if (feedPending) return feedPending;
feedTried = Date.now();
feedPending = (async () => {
await null;
try {
const res = await fetch(FEED, { signal: AbortSignal.timeout(15_000) });
if (res.status === 404) { forgetFeed(); throw new Error("404"); }
if (!res.ok) throw new Error(String(res.status));
const data: unknown = await res.json();
if (!data || typeof data !== "object") throw new Error("feed");
keepFeed(data, Date.now());
} catch {
/* det senaste fungerande svaret står kvar */
}
feedPending = null;
return feedData;
})();
return feedPending;
}
async function loadFeed(): Promise<unknown> {
await loadSaved();
const now = Date.now();
if (feedData !== null) {
if (now - feedAt >= 300_000 && now - feedTried >= 60_000) background(refreshFeed());
return feedData;
}
if (!feedPending && now - feedTried < 60_000) return null;
const started = feedPending ? feedTried : now;
const first = refreshFeed();
background(first);
return Promise.race([first, pause(Math.max(0, 1500 - (now - started)))]).then(() => feedData);
}
let motor: Motor | null = null;
let motorHash = "";
let motorAt = 0;
let motorPending: Promise<Motor | null> | null = null;
// Motorn hämtas högst var tionde minut och körs bara om signaturen stämmer. Annars används den förra.
function loadMotor(): Promise<Motor | null> {
if (Date.now() - motorAt < 600_000) return Promise.resolve(motor);
if (motorPending) return motorPending;
motorPending = (async () => {
try {
const get = async (u: string) => { const r = await fetch(u, { signal: AbortSignal.timeout(2500) }); if (!r.ok) throw new Error(String(r.status)); return r.text(); };
const [code, sig] = await Promise.all([get(MOTOR), get(MOTOR.replace(/\.mjs$/, ".sig"))]);
const key = createPublicKey({ key: Buffer.from(PUBLIC_KEY, "base64"), format: "der", type: "spki" });
if (!verify(null, Buffer.from(code), key, Buffer.from(sig.trim(), "base64"))) throw new Error("fel signatur");
const hash = createHash("sha256").update(code).digest("hex");
if (hash !== motorHash) {
motor = (await import("data:text/javascript;base64," + Buffer.from(code).toString("base64"))) as Motor;
motorHash = hash;
}
motorAt = Date.now();
} catch {
motorAt = Date.now() - 540_000;
}
motorPending = null;
return motor;
})();
return motorPending;
}
const ctx = (feed: unknown) => ({ feed, marker: MARKER, indexnow: INDEXNOW, counterSrc: COUNTER_SRC, counterKey: COUNTER_KEY });
function pathOf(url: string): string {
const p = (url || "/").split(/[?#]/)[0].replace(/\/+$/, "");
try { return decodeURI(p) || "/"; } catch { return p || "/"; }
}
// Utan motor: bara märket och besöksräknaren, sidan i övrigt orörd.
function basic(html: string): string {
if (/<meta name="seomotor"/i.test(html)) return html;
return html.replace(/<head([^>]*)>/i, (_m: string, a: string) => "<head" + a + '><meta name="seomotor" content="' + MARKER + '" data-v="7"><script src="' + COUNTER_SRC + '" data-k="' + COUNTER_KEY + '" defer></script>');
}
/** Kör sidans HTML genom den här funktionen innan den skickas. url = adressen som efterfrågas, t.ex. req.originalUrl. */
export async function withSeo(html: string, url: string): Promise<string> {
try {
if (INDEXNOW && pathOf(url) === "/" + INDEXNOW + ".txt") return INDEXNOW;
const [feed, m] = await Promise.all([loadFeed(), loadMotor()]);
if (m) { try { return m.withSeo(basic(html), url, ctx(feed)); } catch { /* sidan skickas utan ändringar */ } }
return basic(html);
} catch {
return html;
}
}
/** Lägg den här först i servern: app.use(seomotor). Allt som SEOmotor inte svarar på går vidare som vanligt. */
export function seomotor(req: any, res: any, next: () => void): void {
(async () => {
const [feed, m] = await Promise.all([loadFeed(), loadMotor()]);
if (m && (await m.middleware(req, res, ctx(feed))) === true) return;
if (INDEXNOW && pathOf(req.originalUrl || req.url || "/") === "/" + INDEXNOW + ".txt") { res.statusCode = 200; res.setHeader("Content-Type", "text/plain; charset=utf-8"); res.end(INDEXNOW); return; }
next();
})().catch(() => next());
}
```
2. Där servern skickar sidans HTML (index.html) till besökare, både i utvecklingsläget och i den publicerade versionen: läs HTML:en som text, kör `html = await withSeo(html, req.originalUrl)` och skicka resultatet med Content-Type text/html. Det gäller alla sidadresser som får index.html, inte filer under /assets.
3. Om servern använder express.static för de byggda filerna: sätt `index: false` (express.static(dist, { index: false })), annars skickas startsidan utan att gå genom withSeo. Adresser som slutar på .txt och inte finns som fil ska också gå genom withSeo: då svarar den med nyckeln som Bing behöver.
4. Lägg till `app.use(seomotor)` först i servern, före alla andra routes och före express.static (importera seomotor från samma fil). Den svarar bara på det SEOmotor ska svara på och skickar allt annat vidare.
Ändra ingenting annat: inga texter, ingen design och inga andra funktioner.
5. Publiceras sajten som statiska filer (ingen server skickar ut sidorna)? Gör då om publiceringen så att hela sajten körs av en server: finns det redan en server i projektet (till exempel en Express-server för /api), använd den; annars skapa en liten Express-server. Servern ska skicka ut den byggda frontend-appen för alla sidadresser, köra HTML:en genom withSeo enligt punkt 2-3, och hantera eventuella /api-anrop exakt som i dag. Byt publiceringstyp till en som kör servern (i Replit: Autoscale i stället för Static).
Går det inte alls att köra en server i det här verktyget? Gör då ingen ändring och svara "Verktyget kan inte köra en server".
6. Kontrollera i förhandsvisningen att sidans HTML (curl -s <adressen>/ | grep seomotor) innehåller raden <meta name="seomotor" ...>. När allt fungerar: publicera sajten igen så att det kommer ut på https://kafelindgren.se.Öppna ditt projekt i verktyget du byggde sajten med, klistra in texten i chatten och skicka. Låt den arbeta klart.
Publicera sajten igen i verktyget du byggde sajten med när den är klar. Annars syns ändringen inte på kafelindgren.se.
Vi läser din sida som Google gör och ser om kopplingen är på plats.
Google rankar efter hur snabb sidan är på mobilen. Din startsida får 58 av 100 i Googles mätning, och det största innehållet syns efter 6,1 sekunder. Under 2,5 sekunder och över 90 poäng är målet.
Gör sajten snabbare på mobilen utan att ändra utseende, text eller funktioner. Google PageSpeed hittade de här bristerna: - Bilder som kan göras mindre (2,9 s snabbare): Komprimera bilderna utan synlig kvalitetsförlust. - Filer som hindrar sidan från att visas (0,9 s snabbare): Ladda designfiler och kodfiler som inte behövs för första skärmen först efter att sidan visats. - Den största bilden laddas för sent (0,6 s snabbare): Ladda den största bilden först, med högsta prioritet. - Besökarens webbläsare sparar inte filerna (980 kB mindre): Låt besökarens webbläsare spara bilder, designfiler och kodfiler längre, så att de inte hämtas om vid varje besök. - Kod som laddas i onödan (0,3 s snabbare): Ta bort kod som inte används på sidan, eller dela upp den så att varje sida bara laddar det den behöver. - Texten syns först när typsnittet laddats (0,3 s snabbare): Visa texten direkt med ett vanligt typsnitt medan det riktiga typsnittet laddas. Gör så här: - Konvertera bilder till WebP eller AVIF i rätt storlek (inte större än de visas), med width och height satta. - Ladda den största bilden överst på sidan först (fetchpriority="high", ingen lazy-loading på den) och alla andra bilder med loading="lazy". - Dela upp JavaScript per sida (koddelning), ta bort oanvända paket och skjut upp skript som inte behövs direkt. - Typsnitt: font-display: swap, och ladda bara de vikter som används. - Sätt lång cachetid för filerna under /assets (Cache-Control: public, max-age=31536000, immutable). - Kontrollera efteråt på https://pagespeed.web.dev/ med https://kafelindgren.se att mobilpoängen har gått upp.
Öppna ditt projekt i verktyget du byggde sajten med, klistra in texten i chatten och skicka. Låt den arbeta klart.
Publicera sajten igen i verktyget du byggde sajten med när den är klar. Annars syns ändringen inte på kafelindgren.se.
Vi läser din sajt en gång i veckan och ser själva när ändringen syns.
Du kan gå vidare utan koppling. Då gör vi inga ändringar automatiskt, men alla förslag finns under Ändringar.
Påhittat exempel, ingen kopplingRubrikerna och texterna som Google visar, och företagsuppgifterna i sidans kod, bara enligt planen och bara det du har godkänt. Din sajt hämtar dem från oss. Vi rör aldrig design, texter på sidan, formulär eller sidor du har låst. Du ser allt under Ändringar och kan ångra varje ändring; då går sidan tillbaka av sig själv inom fem minuter.