JSPM

@dineway-ai/plugin-seo-graph

0.1.11
    • ESM via JSPM
    • ES Module Entrypoint
    • Export Map
    • Keywords
    • License
    • Repository URL
    • TypeScript Types
    • README
    • Created
    • Published
    • Downloads 167
    • Score
      100M100P100Q80193F
    • License MIT

    SEO Graph plugin for the Dineway Agentic Website builder — meta tags, Open Graph, canonical URLs, robots directives, and JSON-LD schema markup

    Package Exports

    • @dineway-ai/plugin-seo-graph

    Readme

    Dineway SEO Graph Plugin

    An SEO Graph plugin for Dineway Agentic Web that generates meta tags, Open Graph, Twitter Cards, canonical URLs, robots directives, and JSON-LD schema markup via the page:metadata hook.

    Features

    • Meta descriptions with configurable fallback chain
    • Meta robots with max-snippet, max-image-preview, and max-video-preview directives; noindex for search/utility pages; omitted on 404
    • Canonical URLs — absolute, normalized, with trailing slash and pagination support
    • Open Graphog:title without site name suffix, og:type: article for content pages, full set of OG tags
    • Twitter Cardssummary_large_image when image present, site handle from settings
    • JSON-LD schema graph with linked nodes:
      • Person or Organization (configurable), with publishingPrinciples
      • WebSite with SearchAction and optional SiteNavigationElement
      • Blog entity (when blog URL is configured)
      • WebPage (CollectionPage for archives, ProfilePage for /about), with about, copyright, and license fields
      • BlogPosting with author Person (for content pages), linked to Blog when configured, with taxonomy-derived keywords and articleSection
      • ImageObject for primary page images
      • BreadcrumbList with a back-reference from WebPage
    • Breadcrumbs — derived from the URL path by default, with segment label overrides (/blog/ → "Blog") and per-pageType rule overrides both editable in the admin UI. @id scheme matches joost.blog via @jdevalk/seo-graph-core
    • hreflang alternates — for multilingual Dineway sites (Astro i18n + translation_group), one <link rel="alternate" hreflang="…"> per published sibling plus an automatic x-default, with BCP 47 tag normalization (fr-cafr-CA). Zero cost on single-locale sites
    • llms.txt — exposes a small-form llms.txt index of published content at the plugin's llms/txt route. Enabled by default; flip the setting to disable
    • Schema map — exposes every published URL backed by schema markup at the public schema/map plugin route for agent/crawler discovery
    • Fuzzy Redirects — admin tool that mines the core 404 log, ranks live URLs by path similarity, and lets editors create 301 redirects for moved slugs, typos, and punctuation drift
    • NLWeb <link> tag — when the NLWeb endpoint URL setting is set, every rendered page carries <link rel="nlweb" href="…"> for conversational agent discovery
    • IndexNow — on publish/unpublish transitions, submits the affected URL to IndexNow so Bing, Yandex, Seznam, Naver, and Yep recrawl immediately. Opt-in via a single toggle in the settings UI; the key is generated and persisted automatically on first use
    • Admin settings UI — auto-generated from settingsSchema for configuring Person/Organization identity, social profiles, title separator, and default description

    Installation

    Copy the src/ directory into your Dineway theme's plugins/seo-graph/ directory, or install from this repo:

    # In your dineway theme directory
    cp -r path/to/dineway-plugin-seo-graph/src plugins/seo-graph/src
    cp path/to/dineway-plugin-seo-graph/package.json plugins/seo-graph/package.json

    Usage

    Register the plugin in your astro.config.mjs:

    import { seoGraphPlugin } from "./plugins/seo-graph/src/index.ts";
    
    export default defineConfig({
        integrations: [
            dineway({
                plugins: [seoGraphPlugin()],
            }),
        ],
    });

    Then configure your site identity and social profiles in the Dineway admin under Plugins > SEO Graph > Settings.

    Settings

    Setting Description
    Site represents Person or Organization
    Title separator Character between page title and site name (em dash, pipe, hyphen, dot)
    Default meta description Fallback for pages without their own
    Person name / bio / image / job title / URL Person schema fields
    Organization name / logo URL Organization schema fields
    Social URLs Twitter/X, Facebook, LinkedIn, Instagram, YouTube, GitHub, Bluesky, Mastodon, Wikipedia
    Publishing principles URL Link to editorial policy page
    Copyright year Year copyright was first asserted
    License URL Content license (e.g. Creative Commons)
    Blog URL / name Enables Blog schema entity linked to BlogPosting nodes
    Navigation items JSON array of {name, url} for SiteNavigationElement schema
    Breadcrumb segment labels segment → display label overrides (e.g. blog → Blog)
    Breadcrumb page type rules Per-pageType ordered crumb lists, JSON-edited, for themes that need full control over trail shape
    IndexNow submission Submit published/unpublished URLs to IndexNow. Disabled by default
    llms.txt Expose an llms.txt index of published content. Enabled by default
    llms.txt site description Optional blockquote text at the top of llms.txt. Falls back to the default meta description
    NLWeb endpoint URL Absolute URL of the site's conversational endpoint, emitted as <link rel="nlweb">

    Multilingual sites (hreflang)

    When your site has more than one locale configured in Astro's i18n block and content entries are linked via translation_group, the plugin automatically emits hreflang annotations for each content page. No configuration required — it activates as soon as isI18nEnabled() returns true.

    // astro.config.mjs
    export default defineConfig({
        i18n: {
            defaultLocale: "en",
            locales: ["en", "fr", "nl"],
            routing: { prefixDefaultLocale: false },
        },
        integrations: [dineway({ plugins: [seoGraphPlugin()] })],
    });

    A 3-locale post at /hello/, with published French (/fr/bonjour/) and Dutch (/nl/hallo/) translations in the same translation_group, renders:

    <link rel="alternate" hreflang="en" href="https://example.com/hello/" />
    <link rel="alternate" hreflang="fr" href="https://example.com/fr/bonjour/" />
    <link rel="alternate" hreflang="nl" href="https://example.com/nl/hallo/" />
    <link rel="alternate" hreflang="x-default" href="https://example.com/hello/" />

    Only published siblings are included. Drafts, scheduled entries, and siblings whose locale is no longer in your Astro config are dropped. If the page has fewer than two published locales, no hreflang tags are emitted (a single-locale page has no meaningful alternates).

    Region-specific locales (fr-CA vs fr-FR)

    If you need region-specific hreflang, use the BCP 47 code as the locale path directly:

    i18n: {
      defaultLocale: "en",
      locales: ["en", "fr-ca", "fr-fr"],
    }

    URLs become /fr-ca/… and /fr-fr/…, and the emitted hreflang attributes are normalized to conventional casing (fr-CA, fr-FR). Dineway core currently drops Astro's object-form { path, codes } shape at the integration boundary, so the code-as-path workaround is the supported path for region tags in this plugin version.

    IndexNow

    When enabled via the IndexNow submission setting, the plugin submits the canonical URL of any content item that transitions to or from published. A 32-character hex key is minted on first use and persisted in plugin KV.

    The front-end Astro site must serve the key-verification file at /<key>.txt. Fetch the key from the plugin's indexnow/key route and wire a route on the Astro side using createIndexNowKeyRoute:

    // src/pages/[your-key-here].txt.ts
    import { createIndexNowKeyRoute } from "@jdevalk/astro-seo-graph";
    
    export const GET = createIndexNowKeyRoute({ key: "your-key-here" });

    Deploy the key file before enabling the toggle. IndexNow verifies host ownership on every submission by fetching https://<host>/<key>.txt. Submissions sent before the key file is reachable in production are rejected (HTTP 403) and the key gets marked invalid — you'll have to delete the stored key from plugin KV and mint a new one. Ship the Astro route, deploy, confirm the .txt loads over HTTPS, then flip the IndexNow submission toggle.

    When rejections occur, the plugin logs on ctx.log.warn but does not throw — transitions still succeed locally.

    llms.txt

    The plugin generates a small-form llms.txt index of all published content, grouped by collection label, and exposes it on the plugin route llms/txt. Only collections with a urlPattern are included. The llms-full.txt variant is out of scope.

    Serve it from your Astro site by creating an endpoint that proxies the plugin route:

    // src/pages/llms.txt.ts
    import type { APIRoute } from "astro";
    
    export const GET: APIRoute = async ({ request }) => {
        const origin = new URL(request.url).origin;
        const res = await fetch(`${origin}/_dineway/api/plugins/seo-graph/llms/txt`, {
            method: "POST",
            headers: { "Content-Type": "application/json" },
            body: "{}",
        });
        const { data } = (await res.json()) as { data: { enabled: boolean; body: string } };
        if (!data.enabled) return new Response("Not found", { status: 404 });
        return new Response(data.body, { headers: { "Content-Type": "text/plain; charset=utf-8" } });
    };

    Fuzzy Redirects

    The admin page at Plugins > SEO Graph > Fuzzy Redirects turns Dineway's core 404 log into a redirect work queue. It fetches published URLs from schema/map, scores them against each missing path with Levenshtein distance, token overlap, and last-segment matching, then lets an editor create a 301 redirect. Created redirects are grouped under seo-graph-fuzzy-suggester for later auditing.

    Schema Map

    The public schema/map plugin route returns JSON shaped for a schemamap.xml endpoint:

    {
        "data": {
            "items": [
                {
                    "url": "https://example.com/journal/spring-menu/",
                    "collection": "posts",
                    "updatedAt": "2026-02-01T00:00:00Z"
                }
            ]
        }
    }

    Wire it to /schemamap.xml at your site root with a small Astro endpoint:

    // src/pages/schemamap.xml.ts
    import type { APIRoute } from "astro";
    
    interface SchemaMapEntry {
        url: string;
        collection: string;
        updatedAt: string;
    }
    
    export const GET: APIRoute = async ({ request }) => {
        const origin = new URL(request.url).origin;
        const res = await fetch(`${origin}/_dineway/api/plugins/seo-graph/schema/map`, {
            method: "POST",
            headers: { "Content-Type": "application/json" },
            body: "{}",
        });
        const { data } = (await res.json()) as { data: { items: SchemaMapEntry[] } };
    
        const urls = data.items
            .map(({ url, updatedAt }) => `  <url><loc>${url}</loc><lastmod>${updatedAt}</lastmod></url>`)
            .join("\n");
    
        return new Response(
            `<?xml version="1.0" encoding="UTF-8"?>
    <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
    ${urls}
    </urlset>`,
            { headers: { "Content-Type": "application/xml; charset=utf-8" } },
        );
    };