@easy-web/seo 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,35 @@
1
+ # @easy-web/seo
2
+
3
+ Shared SEO primitives for the `@easy-web/*` ecosystem.
4
+
5
+ ## Usage
6
+
7
+ ```ts
8
+ // astro.config.mjs
9
+ import easyWebSeo from '@easy-web/seo';
10
+
11
+ export default defineConfig({
12
+ site: 'https://yoursite.example',
13
+ integrations: [
14
+ easyWebSeo({
15
+ sitemapLocales: { de: 'de-DE', en: 'en-US' },
16
+ // noIndex: true // set on staging/dev sites
17
+ }),
18
+ ],
19
+ });
20
+ ```
21
+
22
+ ```astro
23
+ // In your Base layout:
24
+ import SeoHead from '@easy-web/seo/components/SeoHead.astro';
25
+
26
+ <SeoHead
27
+ title={title}
28
+ description={description}
29
+ pathname={pathname}
30
+ locale={locale}
31
+ siteName="My Site"
32
+ />
33
+ ```
34
+
35
+ Full implementation: see package source.
@@ -0,0 +1,8 @@
1
+ import type { AstroIntegration } from 'astro';
2
+ export type Options = {
3
+ noIndex?: boolean;
4
+ sitemapLocales?: Record<string, string>;
5
+ filter?: (page: string) => boolean;
6
+ };
7
+ export default function easyWebSeo(options?: Options): AstroIntegration;
8
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,OAAO,CAAC;AAG9C,MAAM,MAAM,OAAO,GAAG;IACpB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,cAAc,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACxC,MAAM,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC;CACpC,CAAC;AAEF,MAAM,CAAC,OAAO,UAAU,UAAU,CAAC,OAAO,GAAE,OAAY,GAAG,gBAAgB,CA0D1E"}
package/dist/index.js ADDED
@@ -0,0 +1,53 @@
1
+ import sitemap from '@astrojs/sitemap';
2
+ export default function easyWebSeo(options = {}) {
3
+ return {
4
+ name: '@easy-web/seo',
5
+ hooks: {
6
+ 'astro:config:setup': ({ config, updateConfig, injectRoute, logger }) => {
7
+ // 1. Validate site URL
8
+ if (!config.site) {
9
+ logger.error('@easy-web/seo: astro.config.mjs must set `site` (a full https:// URL). ' +
10
+ 'Example: site: "https://yoursite.example"');
11
+ throw new Error('@easy-web/seo: `site` is required in astro.config.mjs');
12
+ }
13
+ // 2. Build BCP-47 locale map for sitemap
14
+ let sitemapI18n;
15
+ if (config.i18n && config.i18n.locales.length > 0) {
16
+ const localeMap = options.sitemapLocales ?? {};
17
+ if (!options.sitemapLocales) {
18
+ // Auto-generate BCP-47 tags: 'de' → 'de-DE', 'en' → 'en-US'
19
+ for (const locale of config.i18n.locales) {
20
+ const id = typeof locale === 'string' ? locale : locale.path;
21
+ localeMap[id] = `${id}-${id.toUpperCase()}`;
22
+ }
23
+ logger.warn('@easy-web/seo: auto-generated BCP-47 locale tags (' +
24
+ Object.entries(localeMap).map(([k, v]) => `${k}→${v}`).join(', ') +
25
+ '). Provide the `sitemapLocales` option for region-specific tags (e.g. { de: "de-DE", en: "en-US" }).');
26
+ }
27
+ sitemapI18n = {
28
+ defaultLocale: config.i18n.defaultLocale,
29
+ locales: localeMap,
30
+ };
31
+ }
32
+ // 3. Compose @astrojs/sitemap via updateConfig
33
+ updateConfig({
34
+ integrations: [
35
+ sitemap({
36
+ ...(sitemapI18n ? { i18n: sitemapI18n } : {}),
37
+ ...(options.filter ? { filter: options.filter } : {}),
38
+ }),
39
+ ],
40
+ });
41
+ // 4. Pass noIndex flag to the robots.txt route handler via env
42
+ process.env.EASY_WEB_SEO_NO_INDEX = String(options.noIndex ?? false);
43
+ // 5. Inject dynamic robots.txt route
44
+ injectRoute({
45
+ pattern: '/robots.txt',
46
+ entrypoint: new URL('./routes/robots-txt.js', import.meta.url),
47
+ prerender: true,
48
+ });
49
+ },
50
+ },
51
+ };
52
+ }
53
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,OAAO,MAAM,kBAAkB,CAAC;AAQvC,MAAM,CAAC,OAAO,UAAU,UAAU,CAAC,UAAmB,EAAE;IACtD,OAAO;QACL,IAAI,EAAE,eAAe;QACrB,KAAK,EAAE;YACL,oBAAoB,EAAE,CAAC,EAAE,MAAM,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,EAAE,EAAE,EAAE;gBACtE,uBAAuB;gBACvB,IAAI,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC;oBACjB,MAAM,CAAC,KAAK,CACV,yEAAyE;wBACzE,2CAA2C,CAC5C,CAAC;oBACF,MAAM,IAAI,KAAK,CAAC,uDAAuD,CAAC,CAAC;gBAC3E,CAAC;gBAED,yCAAyC;gBACzC,IAAI,WAAmF,CAAC;gBACxF,IAAI,MAAM,CAAC,IAAI,IAAI,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;oBAClD,MAAM,SAAS,GAA2B,OAAO,CAAC,cAAc,IAAI,EAAE,CAAC;oBACvE,IAAI,CAAC,OAAO,CAAC,cAAc,EAAE,CAAC;wBAC5B,4DAA4D;wBAC5D,KAAK,MAAM,MAAM,IAAI,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC;4BACzC,MAAM,EAAE,GAAG,OAAO,MAAM,KAAK,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC;4BAC7D,SAAS,CAAC,EAAE,CAAC,GAAG,GAAG,EAAE,IAAI,EAAE,CAAC,WAAW,EAAE,EAAE,CAAC;wBAC9C,CAAC;wBACD,MAAM,CAAC,IAAI,CACT,oDAAoD;4BACpD,MAAM,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC;4BACjE,sGAAsG,CACvG,CAAC;oBACJ,CAAC;oBACD,WAAW,GAAG;wBACZ,aAAa,EAAE,MAAM,CAAC,IAAI,CAAC,aAAa;wBACxC,OAAO,EAAE,SAAS;qBACnB,CAAC;gBACJ,CAAC;gBAED,+CAA+C;gBAC/C,YAAY,CAAC;oBACX,YAAY,EAAE;wBACZ,OAAO,CAAC;4BACN,GAAG,CAAC,WAAW,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,WAAW,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;4BAC7C,GAAG,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;yBACtD,CAAC;qBACH;iBACF,CAAC,CAAC;gBAEH,+DAA+D;gBAC/D,OAAO,CAAC,GAAG,CAAC,qBAAqB,GAAG,MAAM,CAAC,OAAO,CAAC,OAAO,IAAI,KAAK,CAAC,CAAC;gBAErE,qCAAqC;gBACrC,WAAW,CAAC;oBACV,OAAO,EAAE,aAAa;oBACtB,UAAU,EAAE,IAAI,GAAG,CAAC,wBAAwB,EAAE,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC;oBAC9D,SAAS,EAAE,IAAI;iBAChB,CAAC,CAAC;YACL,CAAC;SACF;KACF,CAAC;AACJ,CAAC"}
@@ -0,0 +1,15 @@
1
+ import type { APIRoute } from 'astro';
2
+ /**
3
+ * Dynamic robots.txt endpoint injected by @easy-web/seo integration.
4
+ *
5
+ * Behavior:
6
+ * - noIndex mode (process.env.EASY_WEB_SEO_NO_INDEX === 'true'):
7
+ * Emits Disallow: / (blocks all crawlers — for auth-gated/staging sites)
8
+ * - Public mode (default):
9
+ * Emits Disallow: /admin/, Allow: /, Sitemap: <site>/sitemap-index.xml
10
+ *
11
+ * The noIndex flag is set by the integration factory in astro:config:setup
12
+ * via process.env.EASY_WEB_SEO_NO_INDEX before this route is injected.
13
+ */
14
+ export declare const GET: APIRoute;
15
+ //# sourceMappingURL=robots-txt.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"robots-txt.d.ts","sourceRoot":"","sources":["../../src/routes/robots-txt.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAC;AAEtC;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,GAAG,EAAE,QA2BjB,CAAC"}
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Dynamic robots.txt endpoint injected by @easy-web/seo integration.
3
+ *
4
+ * Behavior:
5
+ * - noIndex mode (process.env.EASY_WEB_SEO_NO_INDEX === 'true'):
6
+ * Emits Disallow: / (blocks all crawlers — for auth-gated/staging sites)
7
+ * - Public mode (default):
8
+ * Emits Disallow: /admin/, Allow: /, Sitemap: <site>/sitemap-index.xml
9
+ *
10
+ * The noIndex flag is set by the integration factory in astro:config:setup
11
+ * via process.env.EASY_WEB_SEO_NO_INDEX before this route is injected.
12
+ */
13
+ export const GET = ({ site }) => {
14
+ const noIndex = process.env.EASY_WEB_SEO_NO_INDEX === 'true';
15
+ if (!site) {
16
+ // Fail safe: if site is not configured, block all crawlers
17
+ return new Response('User-agent: *\nDisallow: /\n', {
18
+ status: 500,
19
+ headers: { 'Content-Type': 'text/plain; charset=utf-8' },
20
+ });
21
+ }
22
+ const lines = ['User-agent: *'];
23
+ if (noIndex) {
24
+ // Auth-gated / staging / dev site — block all crawlers
25
+ lines.push('Disallow: /');
26
+ }
27
+ else {
28
+ // Public site — allow all, block CMS admin, link sitemap
29
+ lines.push('Disallow: /admin/');
30
+ lines.push('Allow: /');
31
+ lines.push('');
32
+ lines.push(`Sitemap: ${new URL('sitemap-index.xml', site).href}`);
33
+ }
34
+ return new Response(lines.join('\n') + '\n', {
35
+ headers: { 'Content-Type': 'text/plain; charset=utf-8' },
36
+ });
37
+ };
38
+ //# sourceMappingURL=robots-txt.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"robots-txt.js","sourceRoot":"","sources":["../../src/routes/robots-txt.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;GAWG;AACH,MAAM,CAAC,MAAM,GAAG,GAAa,CAAC,EAAE,IAAI,EAAE,EAAE,EAAE;IACxC,MAAM,OAAO,GAAG,OAAO,CAAC,GAAG,CAAC,qBAAqB,KAAK,MAAM,CAAC;IAE7D,IAAI,CAAC,IAAI,EAAE,CAAC;QACV,2DAA2D;QAC3D,OAAO,IAAI,QAAQ,CAAC,8BAA8B,EAAE;YAClD,MAAM,EAAE,GAAG;YACX,OAAO,EAAE,EAAE,cAAc,EAAE,2BAA2B,EAAE;SACzD,CAAC,CAAC;IACL,CAAC;IAED,MAAM,KAAK,GAAa,CAAC,eAAe,CAAC,CAAC;IAE1C,IAAI,OAAO,EAAE,CAAC;QACZ,uDAAuD;QACvD,KAAK,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC;IAC5B,CAAC;SAAM,CAAC;QACN,yDAAyD;QACzD,KAAK,CAAC,IAAI,CAAC,mBAAmB,CAAC,CAAC;QAChC,KAAK,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;QACvB,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QACf,KAAK,CAAC,IAAI,CAAC,YAAY,IAAI,GAAG,CAAC,mBAAmB,EAAE,IAAI,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC;IACpE,CAAC;IAED,OAAO,IAAI,QAAQ,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,IAAI,EAAE;QAC3C,OAAO,EAAE,EAAE,cAAc,EAAE,2BAA2B,EAAE;KACzD,CAAC,CAAC;AACL,CAAC,CAAC"}
package/package.json ADDED
@@ -0,0 +1,60 @@
1
+ {
2
+ "name": "@easy-web/seo",
3
+ "version": "1.0.0",
4
+ "type": "module",
5
+ "description": "Shared SEO primitives for @easy-web/* consumer instances — Astro integration wrapping @astrojs/sitemap with i18n hreflang, dynamic robots.txt route with noIndex mode, and <SeoHead> component (canonical, OpenGraph, Twitter Cards, hreflang, theme-color, manifest).",
6
+ "keywords": [
7
+ "astro-integration",
8
+ "astro-component",
9
+ "seo",
10
+ "sitemap",
11
+ "robots.txt",
12
+ "opengraph",
13
+ "hreflang",
14
+ "canonical"
15
+ ],
16
+ "exports": {
17
+ ".": {
18
+ "types": "./dist/index.d.ts",
19
+ "import": "./dist/index.js"
20
+ },
21
+ "./components/*": "./src/components/*.astro"
22
+ },
23
+ "files": [
24
+ "dist",
25
+ "src/components",
26
+ "src/routes",
27
+ "README.md"
28
+ ],
29
+ "sideEffects": false,
30
+ "scripts": {
31
+ "build": "tsc -p tsconfig.build.json",
32
+ "test": "vitest run",
33
+ "lint": "eslint src",
34
+ "typecheck": "tsc --noEmit"
35
+ },
36
+ "peerDependencies": {
37
+ "astro": ">=6.0.0 <8.0.0",
38
+ "@easy-web/i18n": ">=1.0.0"
39
+ },
40
+ "dependencies": {
41
+ "@astrojs/sitemap": "^3.7.3"
42
+ },
43
+ "devDependencies": {
44
+ "astro": "^6.4.0",
45
+ "@easy-web/i18n": "workspace:*",
46
+ "vitest": "^3.2.4",
47
+ "@vitest/coverage-v8": "^3.2.4",
48
+ "typescript": "^5.5.0"
49
+ },
50
+ "publishConfig": {
51
+ "access": "public",
52
+ "provenance": true
53
+ },
54
+ "repository": {
55
+ "type": "git",
56
+ "url": "https://github.com/achimismaili/easy-web.git",
57
+ "directory": "packages/seo"
58
+ },
59
+ "license": "MIT"
60
+ }
@@ -0,0 +1,76 @@
1
+ ---
2
+ export interface AlternateLink {
3
+ hreflang: string;
4
+ href: string;
5
+ }
6
+
7
+ export interface Props {
8
+ title: string;
9
+ description?: string;
10
+ pathname: string;
11
+ locale: string;
12
+ siteName: string;
13
+ ogType?: 'website' | 'article';
14
+ ogImage?: string;
15
+ themeColor?: string;
16
+ manifest?: string;
17
+ noIndex?: boolean;
18
+ twitterCard?: 'summary' | 'summary_large_image';
19
+ twitterSite?: string;
20
+ alternates?: AlternateLink[];
21
+ }
22
+
23
+ const {
24
+ title,
25
+ description,
26
+ pathname,
27
+ locale,
28
+ siteName,
29
+ ogType = 'website',
30
+ ogImage,
31
+ themeColor,
32
+ manifest,
33
+ noIndex = false,
34
+ twitterCard = 'summary',
35
+ twitterSite,
36
+ alternates = [],
37
+ } = Astro.props;
38
+
39
+ const site = Astro.site;
40
+ if (!site) {
41
+ throw new Error(
42
+ '@easy-web/seo: <SeoHead> requires astro.config.mjs to set `site` (a full https:// URL).'
43
+ );
44
+ }
45
+
46
+ const canonical = new URL(pathname, site).href;
47
+ const absoluteOgImage = ogImage ? new URL(ogImage, site).href : undefined;
48
+ // Map locale to OG locale format: 'de' → 'de_DE', 'en' → 'en_US'
49
+ const ogLocale = locale === 'en' ? 'en_US' : locale === 'de' ? 'de_DE' : locale.replace('-', '_');
50
+ const ogLocaleAlternate = locale === 'en' ? 'de_DE' : 'en_US';
51
+ ---
52
+ <title>{title}</title>
53
+ {description && <meta name="description" content={description} />}
54
+ <link rel="canonical" href={canonical} />
55
+ {noIndex && <meta name="robots" content="noindex, nofollow" />}
56
+ {themeColor && <meta name="theme-color" content={themeColor} />}
57
+ {manifest && <link rel="manifest" href={manifest} />}
58
+ <!-- Open Graph -->
59
+ <meta property="og:title" content={title} />
60
+ {description && <meta property="og:description" content={description} />}
61
+ <meta property="og:url" content={canonical} />
62
+ <meta property="og:type" content={ogType} />
63
+ <meta property="og:locale" content={ogLocale} />
64
+ <meta property="og:locale:alternate" content={ogLocaleAlternate} />
65
+ <meta property="og:site_name" content={siteName} />
66
+ {absoluteOgImage && <meta property="og:image" content={absoluteOgImage} />}
67
+ <!-- Twitter Cards -->
68
+ <meta name="twitter:card" content={twitterCard} />
69
+ <meta name="twitter:title" content={title} />
70
+ {description && <meta name="twitter:description" content={description} />}
71
+ {absoluteOgImage && <meta name="twitter:image" content={absoluteOgImage} />}
72
+ {twitterSite && <meta name="twitter:site" content={twitterSite} />}
73
+ <!-- hreflang alternates -->
74
+ {alternates.map((link) => (
75
+ <link rel="alternate" hreflang={link.hreflang} href={link.href} />
76
+ ))}
@@ -0,0 +1,42 @@
1
+ import type { APIRoute } from 'astro';
2
+
3
+ /**
4
+ * Dynamic robots.txt endpoint injected by @easy-web/seo integration.
5
+ *
6
+ * Behavior:
7
+ * - noIndex mode (process.env.EASY_WEB_SEO_NO_INDEX === 'true'):
8
+ * Emits Disallow: / (blocks all crawlers — for auth-gated/staging sites)
9
+ * - Public mode (default):
10
+ * Emits Disallow: /admin/, Allow: /, Sitemap: <site>/sitemap-index.xml
11
+ *
12
+ * The noIndex flag is set by the integration factory in astro:config:setup
13
+ * via process.env.EASY_WEB_SEO_NO_INDEX before this route is injected.
14
+ */
15
+ export const GET: APIRoute = ({ site }) => {
16
+ const noIndex = process.env.EASY_WEB_SEO_NO_INDEX === 'true';
17
+
18
+ if (!site) {
19
+ // Fail safe: if site is not configured, block all crawlers
20
+ return new Response('User-agent: *\nDisallow: /\n', {
21
+ status: 500,
22
+ headers: { 'Content-Type': 'text/plain; charset=utf-8' },
23
+ });
24
+ }
25
+
26
+ const lines: string[] = ['User-agent: *'];
27
+
28
+ if (noIndex) {
29
+ // Auth-gated / staging / dev site — block all crawlers
30
+ lines.push('Disallow: /');
31
+ } else {
32
+ // Public site — allow all, block CMS admin, link sitemap
33
+ lines.push('Disallow: /admin/');
34
+ lines.push('Allow: /');
35
+ lines.push('');
36
+ lines.push(`Sitemap: ${new URL('sitemap-index.xml', site).href}`);
37
+ }
38
+
39
+ return new Response(lines.join('\n') + '\n', {
40
+ headers: { 'Content-Type': 'text/plain; charset=utf-8' },
41
+ });
42
+ };