Content & SEO Skill
SkillFiles & storageGuides your agent to write and edit pages and blog posts with proper meta tags, structured data, and SEO checks.
Available today. Use it from your connected AI after setup.
No other account needed.
Add ahel to your AI once: Claude, ChatGPT, Cursor, Claude Code or Codex. Then ask it to use this.
Then ask your AI: use the Content & SEO Skill skill
About this skill
Use when creating or editing pages, blog posts, or content files, adding meta tags, or when Lighthouse SEO drops below 90.
What this skill tells your AI
The instructions your AI receives, as published by holger1411/astrodeck in .claude/skills/content-seo/SKILL.md and read by ahel’s review.
Canonical Sources
This skill references the following globals — read them BEFORE starting work:
system/globals/imagery.md— Alt text conventions, image formats
Domain
Content Collections, Meta Tags, OpenGraph, Structured Data, RSS, Sitemap
KPIs
| Metric | Target | Measurement |
|---|---|---|
| Lighthouse SEO | >90 | Lighthouse JSON → categories.seo.score * 100 |
| Pages without Description | 0 | npm run check:kpis |
| Pages without OG-Image | 0 | HTML check in dist/ |
Rules
Content Collections Schema
// src/content.config.ts
import { defineCollection } from 'astro:content';
import { z } from 'astro/zod';
import { glob } from 'astro/loaders';
const blog = defineCollection({
loader: glob({ pattern: '**/*.{md,mdx}', base: './src/content/blog' }),
schema: z.object({
title: z.string(),
description: z.string().max(160),
pubDate: z.coerce.date(),
updatedDate: z.coerce.date().optional(),
heroImage: z.string().optional(),
draft: z.boolean().default(false),
tags: z.array(z.string()).default([]),
author: z.string().default('Team'),
}),
});
SEO Component Usage
---
import BaseLayout from '@/layouts/BaseLayout.astro';
const title = "Page Title — Brand Name";
const description = "Page description, 150-160 characters, relevant keyword at the start.";
---
<BaseLayout title={title} description={description}>
<!-- Content -->
</BaseLayout>
Meta-Description Best Practices
- Length: 150-160 characters (Google truncates at ~160)
- Keyword: Relevant keyword at the beginning
- Unique: Every page needs its own description
- Actionable: Call-to-action or value proposition
- No duplicates: Never the same description for multiple pages
OpenGraph Tags
<meta property="og:title" content="{title}" />
<meta property="og:description" content="{description}" />
<meta property="og:image" content="{siteUrl}/og-image.png" />
<meta property="og:url" content="{canonicalUrl}" />
<meta property="og:type" content="website" />
<meta property="og:site_name" content="{siteName}" />
- OG image: 1200x630px, PNG or JPG
- Always use absolute URLs
Twitter Cards
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content="{title}" />
<meta name="twitter:description" content="{description}" />
<meta name="twitter:image" content="{siteUrl}/og-image.png" />
Canonical URLs
<link rel="canonical" href={Astro.url.href} />
- Every page needs a canonical URL
- Prevents duplicate content issues
- AstroDeck sets this automatically in BaseLayout
Sitemap
// astro.config.mjs
import sitemap from '@astrojs/sitemap';
export default defineConfig({
site: 'https://example.com', // REQUIRED for sitemap
integrations: [sitemap()],
});
siteinastro.config.mjsMUST be set- Sitemap is generated automatically on
npm run build - Output:
dist/sitemap-index.xml
RSS Feed
// src/pages/rss.xml.ts
import rss from '@astrojs/rss';
import { getCollection } from 'astro:content';
export async function GET(context) {
const blog = await getCollection('blog');
return rss({
title: 'Blog Title',
description: 'Blog Description',
site: context.site,
items: blog.map((post) => ({
title: post.data.title,
pubDate: post.data.pubDate,
description: post.data.description,
link: `/blog/${post.id}/`,
})),
});
}
Structured Data (JSON-LD)
<script type="application/ld+json" set:html={JSON.stringify({
"@context": "https://schema.org",
"@type": "WebSite",
"name": siteName,
"url": siteUrl,
})} />
For blog posts:
<script type="application/ld+json" set:html={JSON.stringify({
"@context": "https://schema.org",
"@type": "BlogPosting",
"headline": title,
"description": description,
"datePublished": pubDate,
"author": { "@type": "Person", "name": author },
})} />
Non-Negotiable
These rules always apply — even under time pressure, even when "SEO isn't that important":
- Every page needs a meta description. "It's just an internal page" — Google indexes everything that's publicly accessible.
- Every page needs exactly one
<h1>. Zero or multiple h1s destroy heading hierarchy and cost SEO points. - No duplicate titles. "AstroDeck ... | AstroDeck" happens when the page title includes the brand — the layout appends it automatically.
- Canonical URL is mandatory. Even for "simple" pages. Without canonical, crawlers create duplicate content issues.
- Don't guess structured data. JSON-LD schema must be valid. Better to omit than to ship invalid schema — Google penalizes invalid markup.
Before Applying
Read LEARNINGS.md in this directory to avoid known anti-patterns.
Signals
- GitHub stars
- 76
- Forks
- 15
- Last commit
- Sep 2026
Advanced
- Catalog kind
- skill
- Key
content-seo-holger1411- Source
- github.com/holger1411/astrodeck