Skip to content

Add meta OG tags for link previewsΒ #12

@AJaccP

Description

@AJaccP

🧠 Context

When a page is shared (e.g. pasted into Discord), the link unfurls into a preview card β€” title, description, and image. Right now the site emits only a <meta name="description"> and a <title>, so shared links have no image and a weak preview. This ticket adds Open Graph tags so links unfurl nicely.

The behavior we want:

  • List pages (/projects, /webring, and the / redirect target) β†’ a default site preview image, plus each page's own title and description.
  • Project detail pages (/projects/[slug]) β†’ the project's cover image and the project's title/description. If a project has no cover (covers are optional), it falls back to the default image.

Base.astro already receives title and description props, so it's the natural place to centralize this: it accepts an optional OG image (and reuses title/description for the OG title/description), builds the absolute URLs, and emits the tags. Individual pages only override what differs (the detail page passes its cover).

Open Graph image URLs must be absolute, which needs site set in astro.config.mjs. That's configured separately β€” write the code against Astro.site, and if it isn't set yet, leave a clear TODO and fall back to the request origin so dev still works.

Files you'll touch:

  • src/layouts/Base.astro (accept an OG image prop; emit the tags)
  • src/pages/projects/[slug].astro (pass the project's cover as the OG image)

Don't touch:

  • The existing <title> / <meta name="description"> and the favicon block in Base.astro β€” add alongside them.
  • The detail page's layout/body β€” it's worked on separately; your only change there is passing the OG image prop into <Base>.
  • The content schema and components.

πŸ› οΈ Implementation Plan

  1. Default preview image (TODO asset). Reference a default image at public/og-default.png (so it serves from /og-default.png). The asset itself needs to be supplied β€” leave a TODO comment and ask Jacc for a branded social-preview image (recommended Open Graph size ~1200Γ—630). The code should be fully wired so dropping the file in later is the only remaining step.

  2. Extend Base.astro props. Add an optional ogImage prop (a path/URL string). Reuse the existing title and description for the OG title/description so callers don't repeat themselves:

    ---
    interface Props {
      title: string;
      description?: string;
      ogImage?: string; // path or URL; defaults to the site default below
    }
    
    const {
      title,
      description = 'Projects and personal sites built by Carleton Computer Science students.',
      ogImage = '/og-default.png', // TODO: add public/og-default.png (ask Jacc)
    } = Astro.props;
    
    // Absolute URLs. `Astro.site` comes from astro.config.mjs `site` (configured
    // separately). Until it's set, fall back to the current request origin so dev
    // works. TODO: once `site` is set, this resolves to the real production URLs.
    const base = Astro.site ?? Astro.url;
    const ogImageUrl = new URL(ogImage, base).href;
    const ogUrl = new URL(Astro.url.pathname, base).href;
    ---
  3. Emit the OG tags in <head>, alongside the existing description/title:

    <meta property="og:type" content="website" />
    <meta property="og:site_name" content="Carleton Computer Science Showcase" />
    <meta property="og:title" content={title} />
    <meta property="og:description" content={description} />
    <meta property="og:url" content={ogUrl} />
    <meta property="og:image" content={ogImageUrl} />
    <meta property="og:image:alt" content={title} />
  4. Project detail page passes its cover. In src/pages/projects/[slug].astro, pass the project's optimized cover path as ogImage on the <Base> call. Because the cover is optional, pass it only when present so it falls back to the default; the title/description already flow through the existing props:

    <Base
      title={`${data.title} Β· Carleton Computer Science Showcase`}
      description={data.description}
      ogImage={data.cover?.src}
    />

    (data.cover.src is the built asset path; Base absolutizes it against Astro.site.)

  5. Verify. Run pnpm build and view-source a list page and a project page: the list page's og:image is …/og-default.png; the project page's og:image is its cover (and a cover-less project shows the default). Optionally check the unfurl in a preview debugger. Until site is configured the URLs will point at the dev/preview origin β€” that's the expected interim behavior noted by the TODO.


βœ… Acceptance Criteria

  • Base.astro emits og:type, og:site_name, og:title, og:description, og:url, og:image, og:image:alt, with absolute URLs.
  • List pages use the default image; the project detail page uses its cover, falling back to the default when the project has no cover.
  • The default image path is wired with a clear TODO to add public/og-default.png (the asset comes from Jacc).
  • Absolute URLs are built from Astro.site with a documented fallback + TODO for when site is configured.
  • The existing <title>, <meta name="description">, and favicon block are intact; the detail page's body/layout is unchanged apart from the ogImage prop.
  • pnpm format:check, pnpm check, and pnpm build all pass.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    Status
    Ready

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions