@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 +35 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +53 -0
- package/dist/index.js.map +1 -0
- package/dist/routes/robots-txt.d.ts +15 -0
- package/dist/routes/robots-txt.d.ts.map +1 -0
- package/dist/routes/robots-txt.js +38 -0
- package/dist/routes/robots-txt.js.map +1 -0
- package/package.json +60 -0
- package/src/components/SeoHead.astro +76 -0
- package/src/routes/robots-txt.ts +42 -0
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.
|
package/dist/index.d.ts
ADDED
|
@@ -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
|
+
};
|