@ampless/plugin-seo 0.2.0-alpha.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/LICENSE +21 -0
- package/README.md +55 -0
- package/dist/index.d.ts +41 -0
- package/dist/index.js +120 -0
- package/package.json +41 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 ampless contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# @ampless/plugin-seo
|
|
2
|
+
|
|
3
|
+
SEO plugin for [ampless](https://github.com/heavymoons/ampless). Generates per-post and per-site metadata (Open Graph, Twitter card, canonical) and keeps a `sitemap.xml` regenerated to S3 whenever the post set changes.
|
|
4
|
+
|
|
5
|
+
> **Pre-release / alpha.** Breaking changes possible in any minor version until v1.0.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install @ampless/plugin-seo@alpha
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Configure
|
|
14
|
+
|
|
15
|
+
In `cms.config.ts`:
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
import { defineConfig } from 'ampless'
|
|
19
|
+
import seoPlugin from '@ampless/plugin-seo'
|
|
20
|
+
|
|
21
|
+
export default defineConfig({
|
|
22
|
+
// ...
|
|
23
|
+
plugins: [
|
|
24
|
+
seoPlugin({
|
|
25
|
+
// defaultOgImage: 'https://example.com/og-default.png',
|
|
26
|
+
// twitterSite: '@example',
|
|
27
|
+
// twitterCreator: '@author',
|
|
28
|
+
// twitterCard: 'summary_large_image',
|
|
29
|
+
}),
|
|
30
|
+
],
|
|
31
|
+
})
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
| Option | Default | Notes |
|
|
35
|
+
|---|---|---|
|
|
36
|
+
| `defaultOgImage` | none | Falls through to `og:image` and `twitter:image` for every post when set |
|
|
37
|
+
| `twitterSite` | none | `@handle` of the site |
|
|
38
|
+
| `twitterCreator` | none | `@handle` of the post author |
|
|
39
|
+
| `twitterCard` | `'summary_large_image'` | Card style |
|
|
40
|
+
| `siteUrl` | `site.url` | Override base URL (e.g. for staging) |
|
|
41
|
+
| `priority` / `changefreq` / `limit` | (sitemap defaults) | Sitemap entry tuning |
|
|
42
|
+
|
|
43
|
+
## What it produces
|
|
44
|
+
|
|
45
|
+
- **Per-post metadata** (Next.js `generateMetadata` shape): `title`, `description`, `alternates.canonical`, `openGraph` (article type, url, images), `twitter` (card, handles, images)
|
|
46
|
+
- **Site-level metadata**: defaults for the root layout — title, description, og:website
|
|
47
|
+
- **`/sitemap.xml`** — full URL set, regenerated to `s3://<bucket>/public/plugins/seo/sitemap.xml` on every `content.published` / `content.unpublished` / `content.deleted` / `content.updated` event. Served by the template's `/sitemap.xml` route handler.
|
|
48
|
+
|
|
49
|
+
## Trust level
|
|
50
|
+
|
|
51
|
+
`trusted` — the sitemap regeneration runs in the trusted Lambda processor with read access to the post table and write access under `public/plugins/seo/` in the site's S3 bucket. The metadata helpers (`metadata` / `siteMetadata`) are pure functions that run during Next.js SSR and do not need any AWS access.
|
|
52
|
+
|
|
53
|
+
## License
|
|
54
|
+
|
|
55
|
+
[MIT](../../LICENSE)
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { Post, Config, PluginMetadata, AmplessPlugin } from 'ampless';
|
|
2
|
+
|
|
3
|
+
interface SitemapOptions {
|
|
4
|
+
/** Maximum number of URLs to include. Default 5000. */
|
|
5
|
+
limit?: number;
|
|
6
|
+
/** Override base URL (defaults to config.site.url). */
|
|
7
|
+
siteUrl?: string;
|
|
8
|
+
/** Default changefreq for post URLs. */
|
|
9
|
+
changefreq?: 'always' | 'hourly' | 'daily' | 'weekly' | 'monthly' | 'yearly' | 'never';
|
|
10
|
+
/** Default priority (0..1) for post URLs. */
|
|
11
|
+
priority?: number;
|
|
12
|
+
}
|
|
13
|
+
declare function buildSitemap(posts: Post[], site: Config['site'], options?: SitemapOptions): string;
|
|
14
|
+
|
|
15
|
+
interface SeoMetadataOptions {
|
|
16
|
+
/** Default OGP image URL when a post has no image override. */
|
|
17
|
+
defaultOgImage?: string;
|
|
18
|
+
/** Twitter site handle, e.g. "@example". */
|
|
19
|
+
twitterSite?: string;
|
|
20
|
+
/** Twitter creator handle, e.g. "@author". */
|
|
21
|
+
twitterCreator?: string;
|
|
22
|
+
/** OGP card type for post pages. Default 'summary_large_image'. */
|
|
23
|
+
twitterCard?: 'summary' | 'summary_large_image';
|
|
24
|
+
}
|
|
25
|
+
declare function buildPostMetadata(post: Post, site: Config['site'], options?: SeoMetadataOptions): PluginMetadata;
|
|
26
|
+
declare function buildSiteMetadata(site: Config['site'], options?: SeoMetadataOptions): PluginMetadata;
|
|
27
|
+
|
|
28
|
+
interface SeoPluginOptions extends SitemapOptions, SeoMetadataOptions {
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* SEO plugin: regenerates `sitemap.xml` whenever a post's published state
|
|
32
|
+
* changes, and provides Next.js `generateMetadata`-compatible per-post and
|
|
33
|
+
* per-site metadata (title, description, OGP, Twitter card, canonical).
|
|
34
|
+
*
|
|
35
|
+
* Lambda side stores the sitemap at
|
|
36
|
+
* `s3://{bucket}/public/plugins/seo/sitemap.xml`. The Next.js
|
|
37
|
+
* `/sitemap.xml` route streams that object through.
|
|
38
|
+
*/
|
|
39
|
+
declare function seoPlugin(options?: SeoPluginOptions): AmplessPlugin;
|
|
40
|
+
|
|
41
|
+
export { type SeoMetadataOptions, type SeoPluginOptions, type SitemapOptions, buildPostMetadata, buildSiteMetadata, buildSitemap, seoPlugin as default };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import { definePlugin } from "ampless";
|
|
3
|
+
|
|
4
|
+
// src/sitemap.ts
|
|
5
|
+
import { escapeXml } from "ampless";
|
|
6
|
+
var toIsoDate = (iso) => {
|
|
7
|
+
const d = new Date(iso);
|
|
8
|
+
return Number.isNaN(d.getTime()) ? "" : d.toISOString();
|
|
9
|
+
};
|
|
10
|
+
function buildSitemap(posts, site, options = {}) {
|
|
11
|
+
const baseUrl = (options.siteUrl ?? site.url).replace(/\/$/, "");
|
|
12
|
+
const limit = options.limit ?? 5e3;
|
|
13
|
+
const changefreq = options.changefreq ?? "weekly";
|
|
14
|
+
const priority = options.priority ?? 0.7;
|
|
15
|
+
const homeEntry = ` <url>
|
|
16
|
+
<loc>${escapeXml(baseUrl + "/")}</loc>
|
|
17
|
+
<changefreq>daily</changefreq>
|
|
18
|
+
<priority>1.0</priority>
|
|
19
|
+
</url>`;
|
|
20
|
+
const postEntries = posts.filter((p) => p.status === "published").slice(0, limit).map((post) => {
|
|
21
|
+
const url = `${baseUrl}/${post.slug}`;
|
|
22
|
+
const lastmod = post.publishedAt ? toIsoDate(post.publishedAt) : "";
|
|
23
|
+
const lastmodTag = lastmod ? `
|
|
24
|
+
<lastmod>${lastmod}</lastmod>` : "";
|
|
25
|
+
return ` <url>
|
|
26
|
+
<loc>${escapeXml(url)}</loc>${lastmodTag}
|
|
27
|
+
<changefreq>${changefreq}</changefreq>
|
|
28
|
+
<priority>${priority}</priority>
|
|
29
|
+
</url>`;
|
|
30
|
+
}).join("\n");
|
|
31
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
32
|
+
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
|
33
|
+
${homeEntry}
|
|
34
|
+
${postEntries}
|
|
35
|
+
</urlset>
|
|
36
|
+
`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// src/metadata.ts
|
|
40
|
+
function buildPostMetadata(post, site, options = {}) {
|
|
41
|
+
const baseUrl = site.url.replace(/\/$/, "");
|
|
42
|
+
const url = `${baseUrl}/${post.slug}`;
|
|
43
|
+
const description = post.excerpt ?? site.description ?? "";
|
|
44
|
+
const ogImage = options.defaultOgImage;
|
|
45
|
+
return {
|
|
46
|
+
title: post.title,
|
|
47
|
+
description,
|
|
48
|
+
openGraph: {
|
|
49
|
+
title: post.title,
|
|
50
|
+
description,
|
|
51
|
+
type: "article",
|
|
52
|
+
url,
|
|
53
|
+
...ogImage && { images: [{ url: ogImage }] }
|
|
54
|
+
},
|
|
55
|
+
twitter: {
|
|
56
|
+
card: options.twitterCard ?? "summary_large_image",
|
|
57
|
+
title: post.title,
|
|
58
|
+
description,
|
|
59
|
+
...options.twitterSite && { site: options.twitterSite },
|
|
60
|
+
...options.twitterCreator && { creator: options.twitterCreator },
|
|
61
|
+
...ogImage && { images: [ogImage] }
|
|
62
|
+
},
|
|
63
|
+
alternates: { canonical: url }
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
function buildSiteMetadata(site, options = {}) {
|
|
67
|
+
const description = site.description ?? site.name;
|
|
68
|
+
return {
|
|
69
|
+
title: site.name,
|
|
70
|
+
description,
|
|
71
|
+
openGraph: {
|
|
72
|
+
title: site.name,
|
|
73
|
+
description,
|
|
74
|
+
type: "website",
|
|
75
|
+
url: site.url,
|
|
76
|
+
...options.defaultOgImage && { images: [{ url: options.defaultOgImage }] }
|
|
77
|
+
},
|
|
78
|
+
twitter: {
|
|
79
|
+
card: "summary",
|
|
80
|
+
title: site.name,
|
|
81
|
+
description,
|
|
82
|
+
...options.twitterSite && { site: options.twitterSite }
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// src/index.ts
|
|
88
|
+
function seoPlugin(options = {}) {
|
|
89
|
+
async function rebuild(_event, ctx) {
|
|
90
|
+
const posts = await ctx.listPublishedPosts();
|
|
91
|
+
const xml = buildSitemap(posts, ctx.site, options);
|
|
92
|
+
await ctx.writePublicAsset("sitemap.xml", xml, "application/xml; charset=utf-8");
|
|
93
|
+
}
|
|
94
|
+
return definePlugin({
|
|
95
|
+
name: "seo",
|
|
96
|
+
apiVersion: 1,
|
|
97
|
+
trust_level: "trusted",
|
|
98
|
+
hooks: {
|
|
99
|
+
"content.published": rebuild,
|
|
100
|
+
"content.unpublished": rebuild,
|
|
101
|
+
"content.deleted": rebuild,
|
|
102
|
+
// Updates can change title/slug of an already-published post, so we
|
|
103
|
+
// regenerate on every update too. listPublishedPosts() filters
|
|
104
|
+
// drafts, so an update on a draft just produces the same XML.
|
|
105
|
+
"content.updated": rebuild
|
|
106
|
+
},
|
|
107
|
+
metadata(post, site) {
|
|
108
|
+
return buildPostMetadata(post, site, options);
|
|
109
|
+
},
|
|
110
|
+
siteMetadata(site) {
|
|
111
|
+
return buildSiteMetadata(site, options);
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
export {
|
|
116
|
+
buildPostMetadata,
|
|
117
|
+
buildSiteMetadata,
|
|
118
|
+
buildSitemap,
|
|
119
|
+
seoPlugin as default
|
|
120
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@ampless/plugin-seo",
|
|
3
|
+
"version": "0.2.0-alpha.0",
|
|
4
|
+
"description": "SEO plugin for ampless",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"import": "./dist/index.js",
|
|
10
|
+
"types": "./dist/index.d.ts"
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"dist",
|
|
15
|
+
"README.md"
|
|
16
|
+
],
|
|
17
|
+
"publishConfig": {
|
|
18
|
+
"access": "public"
|
|
19
|
+
},
|
|
20
|
+
"repository": {
|
|
21
|
+
"type": "git",
|
|
22
|
+
"url": "https://github.com/heavymoons/ampless.git",
|
|
23
|
+
"directory": "packages/plugin-seo"
|
|
24
|
+
},
|
|
25
|
+
"homepage": "https://github.com/heavymoons/ampless/tree/main/packages/plugin-seo#readme",
|
|
26
|
+
"bugs": "https://github.com/heavymoons/ampless/issues",
|
|
27
|
+
"dependencies": {
|
|
28
|
+
"ampless": "0.2.0-alpha.0"
|
|
29
|
+
},
|
|
30
|
+
"keywords": [
|
|
31
|
+
"ampless",
|
|
32
|
+
"plugin",
|
|
33
|
+
"seo"
|
|
34
|
+
],
|
|
35
|
+
"scripts": {
|
|
36
|
+
"build": "tsup",
|
|
37
|
+
"dev": "tsup --watch",
|
|
38
|
+
"lint": "tsc --noEmit",
|
|
39
|
+
"test": "vitest run --passWithNoTests"
|
|
40
|
+
}
|
|
41
|
+
}
|