@earlyseo/blog 1.0.1
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 +304 -0
- package/dist/client.d.ts +22 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +65 -0
- package/dist/client.js.map +1 -0
- package/dist/css.d.ts +16 -0
- package/dist/css.d.ts.map +1 -0
- package/dist/css.js +186 -0
- package/dist/css.js.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +13 -0
- package/dist/index.js.map +1 -0
- package/dist/next/back-link.d.ts +12 -0
- package/dist/next/back-link.d.ts.map +1 -0
- package/dist/next/back-link.js +15 -0
- package/dist/next/back-link.js.map +1 -0
- package/dist/next/blog-list-page.d.ts +22 -0
- package/dist/next/blog-list-page.d.ts.map +1 -0
- package/dist/next/blog-list-page.js +49 -0
- package/dist/next/blog-list-page.js.map +1 -0
- package/dist/next/blog-post-page.d.ts +65 -0
- package/dist/next/blog-post-page.d.ts.map +1 -0
- package/dist/next/blog-post-page.js +103 -0
- package/dist/next/blog-post-page.js.map +1 -0
- package/dist/next/index.d.ts +5 -0
- package/dist/next/index.d.ts.map +1 -0
- package/dist/next/index.js +11 -0
- package/dist/next/index.js.map +1 -0
- package/dist/react/blog-list.d.ts +27 -0
- package/dist/react/blog-list.d.ts.map +1 -0
- package/dist/react/blog-list.js +49 -0
- package/dist/react/blog-list.js.map +1 -0
- package/dist/react/blog-post.d.ts +35 -0
- package/dist/react/blog-post.d.ts.map +1 -0
- package/dist/react/blog-post.js +41 -0
- package/dist/react/blog-post.js.map +1 -0
- package/dist/react/index.d.ts +8 -0
- package/dist/react/index.d.ts.map +1 -0
- package/dist/react/index.js +15 -0
- package/dist/react/index.js.map +1 -0
- package/dist/react/provider.d.ts +30 -0
- package/dist/react/provider.d.ts.map +1 -0
- package/dist/react/provider.js +34 -0
- package/dist/react/provider.js.map +1 -0
- package/dist/react/styles.d.ts +11 -0
- package/dist/react/styles.d.ts.map +1 -0
- package/dist/react/styles.js +16 -0
- package/dist/react/styles.js.map +1 -0
- package/dist/react/use-article.d.ts +17 -0
- package/dist/react/use-article.d.ts.map +1 -0
- package/dist/react/use-article.js +53 -0
- package/dist/react/use-article.js.map +1 -0
- package/dist/react/use-articles.d.ts +22 -0
- package/dist/react/use-articles.d.ts.map +1 -0
- package/dist/react/use-articles.js +59 -0
- package/dist/react/use-articles.js.map +1 -0
- package/dist/types.d.ts +96 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +14 -0
- package/dist/types.js.map +1 -0
- package/package.json +68 -0
- package/src/cli/init.mjs +188 -0
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import { ARTICLE_CSS, BLOG_CSS } from "../css";
|
|
4
|
+
/**
|
|
5
|
+
* Injects EarlySEO article and blog CSS into the page.
|
|
6
|
+
* Place this once near the root of your app (or inside a layout).
|
|
7
|
+
*
|
|
8
|
+
* ```tsx
|
|
9
|
+
* import { ArticleStyles } from '@earlyseo/blog/react';
|
|
10
|
+
* <ArticleStyles />
|
|
11
|
+
* ```
|
|
12
|
+
*/
|
|
13
|
+
export function ArticleStyles() {
|
|
14
|
+
return (_jsx("style", { dangerouslySetInnerHTML: { __html: `${ARTICLE_CSS}\n${BLOG_CSS}` }, "data-earlyseo": "blog-styles" }));
|
|
15
|
+
}
|
|
16
|
+
//# sourceMappingURL=styles.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"styles.js","sourceRoot":"","sources":["../../src/react/styles.tsx"],"names":[],"mappings":"AAAA,YAAY,CAAC;;AAQb,OAAO,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,QAAQ,CAAC;AAE/C;;;;;;;;GAQG;AACH,MAAM,UAAU,aAAa;IAC3B,OAAO,CACL,gBACE,uBAAuB,EAAE,EAAE,MAAM,EAAE,GAAG,WAAW,KAAK,QAAQ,EAAE,EAAE,mBACpD,aAAa,GAC3B,CACH,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { Article } from "../types";
|
|
2
|
+
export interface UseArticleResult {
|
|
3
|
+
article: Article | null;
|
|
4
|
+
loading: boolean;
|
|
5
|
+
error: Error | null;
|
|
6
|
+
/** Refetch the article */
|
|
7
|
+
refresh: () => void;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Hook to fetch a single article by slug from the CDN.
|
|
11
|
+
*
|
|
12
|
+
* ```tsx
|
|
13
|
+
* const { article, loading } = useArticle('my-article-slug');
|
|
14
|
+
* ```
|
|
15
|
+
*/
|
|
16
|
+
export declare function useArticle(slug: string | undefined): UseArticleResult;
|
|
17
|
+
//# sourceMappingURL=use-article.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"use-article.d.ts","sourceRoot":"","sources":["../../src/react/use-article.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,UAAU,CAAC;AAExC,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,OAAO,GAAG,IAAI,CAAC;IACxB,OAAO,EAAE,OAAO,CAAC;IACjB,KAAK,EAAE,KAAK,GAAG,IAAI,CAAC;IACpB,0BAA0B;IAC1B,OAAO,EAAE,MAAM,IAAI,CAAC;CACrB;AAED;;;;;;GAMG;AACH,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,gBAAgB,CAwCrE"}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
3
|
+
// @earlyseo/blog — useArticle() hook
|
|
4
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
5
|
+
// Fetches a single article's full JSON from the CDN.
|
|
6
|
+
import { useState, useEffect, useCallback } from "react";
|
|
7
|
+
import { useEarlySeoContext } from "./provider";
|
|
8
|
+
/**
|
|
9
|
+
* Hook to fetch a single article by slug from the CDN.
|
|
10
|
+
*
|
|
11
|
+
* ```tsx
|
|
12
|
+
* const { article, loading } = useArticle('my-article-slug');
|
|
13
|
+
* ```
|
|
14
|
+
*/
|
|
15
|
+
export function useArticle(slug) {
|
|
16
|
+
const { client } = useEarlySeoContext();
|
|
17
|
+
const [article, setArticle] = useState(null);
|
|
18
|
+
const [loading, setLoading] = useState(true);
|
|
19
|
+
const [error, setError] = useState(null);
|
|
20
|
+
const [tick, setTick] = useState(0);
|
|
21
|
+
const refresh = useCallback(() => setTick((t) => t + 1), []);
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
if (!slug) {
|
|
24
|
+
setArticle(null);
|
|
25
|
+
setLoading(false);
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
let cancelled = false;
|
|
29
|
+
setLoading(true);
|
|
30
|
+
client
|
|
31
|
+
.getArticle(slug)
|
|
32
|
+
.then((result) => {
|
|
33
|
+
if (cancelled)
|
|
34
|
+
return;
|
|
35
|
+
setArticle(result);
|
|
36
|
+
setError(null);
|
|
37
|
+
})
|
|
38
|
+
.catch((err) => {
|
|
39
|
+
if (cancelled)
|
|
40
|
+
return;
|
|
41
|
+
setError(err instanceof Error ? err : new Error("Failed to load article"));
|
|
42
|
+
})
|
|
43
|
+
.finally(() => {
|
|
44
|
+
if (!cancelled)
|
|
45
|
+
setLoading(false);
|
|
46
|
+
});
|
|
47
|
+
return () => {
|
|
48
|
+
cancelled = true;
|
|
49
|
+
};
|
|
50
|
+
}, [client, slug, tick]);
|
|
51
|
+
return { article, loading, error, refresh };
|
|
52
|
+
}
|
|
53
|
+
//# sourceMappingURL=use-article.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"use-article.js","sourceRoot":"","sources":["../../src/react/use-article.ts"],"names":[],"mappings":"AAAA,YAAY,CAAC;AACb,gFAAgF;AAChF,qCAAqC;AACrC,gFAAgF;AAChF,qDAAqD;AAErD,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,OAAO,CAAC;AACzD,OAAO,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAWhD;;;;;;GAMG;AACH,MAAM,UAAU,UAAU,CAAC,IAAwB;IACjD,MAAM,EAAE,MAAM,EAAE,GAAG,kBAAkB,EAAE,CAAC;IACxC,MAAM,CAAC,OAAO,EAAE,UAAU,CAAC,GAAG,QAAQ,CAAiB,IAAI,CAAC,CAAC;IAC7D,MAAM,CAAC,OAAO,EAAE,UAAU,CAAC,GAAG,QAAQ,CAAC,IAAI,CAAC,CAAC;IAC7C,MAAM,CAAC,KAAK,EAAE,QAAQ,CAAC,GAAG,QAAQ,CAAe,IAAI,CAAC,CAAC;IACvD,MAAM,CAAC,IAAI,EAAE,OAAO,CAAC,GAAG,QAAQ,CAAC,CAAC,CAAC,CAAC;IAEpC,MAAM,OAAO,GAAG,WAAW,CAAC,GAAG,EAAE,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;IAE7D,SAAS,CAAC,GAAG,EAAE;QACb,IAAI,CAAC,IAAI,EAAE,CAAC;YACV,UAAU,CAAC,IAAI,CAAC,CAAC;YACjB,UAAU,CAAC,KAAK,CAAC,CAAC;YAClB,OAAO;QACT,CAAC;QAED,IAAI,SAAS,GAAG,KAAK,CAAC;QACtB,UAAU,CAAC,IAAI,CAAC,CAAC;QAEjB,MAAM;aACH,UAAU,CAAC,IAAI,CAAC;aAChB,IAAI,CAAC,CAAC,MAAM,EAAE,EAAE;YACf,IAAI,SAAS;gBAAE,OAAO;YACtB,UAAU,CAAC,MAAM,CAAC,CAAC;YACnB,QAAQ,CAAC,IAAI,CAAC,CAAC;QACjB,CAAC,CAAC;aACD,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE;YACb,IAAI,SAAS;gBAAE,OAAO;YACtB,QAAQ,CAAC,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,KAAK,CAAC,wBAAwB,CAAC,CAAC,CAAC;QAC7E,CAAC,CAAC;aACD,OAAO,CAAC,GAAG,EAAE;YACZ,IAAI,CAAC,SAAS;gBAAE,UAAU,CAAC,KAAK,CAAC,CAAC;QACpC,CAAC,CAAC,CAAC;QAEL,OAAO,GAAG,EAAE;YACV,SAAS,GAAG,IAAI,CAAC;QACnB,CAAC,CAAC;IACJ,CAAC,EAAE,CAAC,MAAM,EAAE,IAAI,EAAE,IAAI,CAAC,CAAC,CAAC;IAEzB,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,CAAC;AAC9C,CAAC"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { ArticleListItem } from "../types";
|
|
2
|
+
export interface UseArticlesResult {
|
|
3
|
+
articles: ArticleListItem[];
|
|
4
|
+
page: number;
|
|
5
|
+
totalPages: number;
|
|
6
|
+
totalArticles: number;
|
|
7
|
+
loading: boolean;
|
|
8
|
+
error: Error | null;
|
|
9
|
+
/** Navigate to a page (1-indexed) */
|
|
10
|
+
goToPage: (page: number) => void;
|
|
11
|
+
/** Refetch the current page */
|
|
12
|
+
refresh: () => void;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Hook to fetch a paginated article list page from the CDN.
|
|
16
|
+
*
|
|
17
|
+
* ```tsx
|
|
18
|
+
* const { articles, page, totalPages, goToPage, loading } = useArticles();
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
export declare function useArticles(initialPage?: number): UseArticlesResult;
|
|
22
|
+
//# sourceMappingURL=use-articles.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"use-articles.d.ts","sourceRoot":"","sources":["../../src/react/use-articles.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,eAAe,EAAmB,MAAM,UAAU,CAAC;AAEjE,MAAM,WAAW,iBAAiB;IAChC,QAAQ,EAAE,eAAe,EAAE,CAAC;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,MAAM,CAAC;IACnB,aAAa,EAAE,MAAM,CAAC;IACtB,OAAO,EAAE,OAAO,CAAC;IACjB,KAAK,EAAE,KAAK,GAAG,IAAI,CAAC;IACpB,qCAAqC;IACrC,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IACjC,+BAA+B;IAC/B,OAAO,EAAE,MAAM,IAAI,CAAC;CACrB;AAED;;;;;;GAMG;AACH,wBAAgB,WAAW,CAAC,WAAW,SAAI,GAAG,iBAAiB,CA6C9D"}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
3
|
+
// @earlyseo/blog — useArticles() hook
|
|
4
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
5
|
+
// Fetches a paginated list page from the CDN.
|
|
6
|
+
import { useState, useEffect, useCallback } from "react";
|
|
7
|
+
import { useEarlySeoContext } from "./provider";
|
|
8
|
+
/**
|
|
9
|
+
* Hook to fetch a paginated article list page from the CDN.
|
|
10
|
+
*
|
|
11
|
+
* ```tsx
|
|
12
|
+
* const { articles, page, totalPages, goToPage, loading } = useArticles();
|
|
13
|
+
* ```
|
|
14
|
+
*/
|
|
15
|
+
export function useArticles(initialPage = 1) {
|
|
16
|
+
const { client } = useEarlySeoContext();
|
|
17
|
+
const [page, setPage] = useState(initialPage);
|
|
18
|
+
const [data, setData] = useState(null);
|
|
19
|
+
const [loading, setLoading] = useState(true);
|
|
20
|
+
const [error, setError] = useState(null);
|
|
21
|
+
const [tick, setTick] = useState(0);
|
|
22
|
+
const refresh = useCallback(() => setTick((t) => t + 1), []);
|
|
23
|
+
const goToPage = useCallback((p) => setPage(Math.max(1, p)), []);
|
|
24
|
+
useEffect(() => {
|
|
25
|
+
let cancelled = false;
|
|
26
|
+
setLoading(true);
|
|
27
|
+
client
|
|
28
|
+
.getListPage(page)
|
|
29
|
+
.then((result) => {
|
|
30
|
+
if (cancelled)
|
|
31
|
+
return;
|
|
32
|
+
setData(result);
|
|
33
|
+
setError(null);
|
|
34
|
+
})
|
|
35
|
+
.catch((err) => {
|
|
36
|
+
if (cancelled)
|
|
37
|
+
return;
|
|
38
|
+
setError(err instanceof Error ? err : new Error("Failed to load articles"));
|
|
39
|
+
})
|
|
40
|
+
.finally(() => {
|
|
41
|
+
if (!cancelled)
|
|
42
|
+
setLoading(false);
|
|
43
|
+
});
|
|
44
|
+
return () => {
|
|
45
|
+
cancelled = true;
|
|
46
|
+
};
|
|
47
|
+
}, [client, page, tick]);
|
|
48
|
+
return {
|
|
49
|
+
articles: data?.articles ?? [],
|
|
50
|
+
page: data?.page ?? page,
|
|
51
|
+
totalPages: data?.totalPages ?? 0,
|
|
52
|
+
totalArticles: data?.totalArticles ?? 0,
|
|
53
|
+
loading,
|
|
54
|
+
error,
|
|
55
|
+
goToPage,
|
|
56
|
+
refresh,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
//# sourceMappingURL=use-articles.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"use-articles.js","sourceRoot":"","sources":["../../src/react/use-articles.ts"],"names":[],"mappings":"AAAA,YAAY,CAAC;AACb,gFAAgF;AAChF,sCAAsC;AACtC,gFAAgF;AAChF,8CAA8C;AAE9C,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,OAAO,CAAC;AACzD,OAAO,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAgBhD;;;;;;GAMG;AACH,MAAM,UAAU,WAAW,CAAC,WAAW,GAAG,CAAC;IACzC,MAAM,EAAE,MAAM,EAAE,GAAG,kBAAkB,EAAE,CAAC;IACxC,MAAM,CAAC,IAAI,EAAE,OAAO,CAAC,GAAG,QAAQ,CAAC,WAAW,CAAC,CAAC;IAC9C,MAAM,CAAC,IAAI,EAAE,OAAO,CAAC,GAAG,QAAQ,CAAyB,IAAI,CAAC,CAAC;IAC/D,MAAM,CAAC,OAAO,EAAE,UAAU,CAAC,GAAG,QAAQ,CAAC,IAAI,CAAC,CAAC;IAC7C,MAAM,CAAC,KAAK,EAAE,QAAQ,CAAC,GAAG,QAAQ,CAAe,IAAI,CAAC,CAAC;IACvD,MAAM,CAAC,IAAI,EAAE,OAAO,CAAC,GAAG,QAAQ,CAAC,CAAC,CAAC,CAAC;IAEpC,MAAM,OAAO,GAAG,WAAW,CAAC,GAAG,EAAE,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;IAC7D,MAAM,QAAQ,GAAG,WAAW,CAAC,CAAC,CAAS,EAAE,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;IAEzE,SAAS,CAAC,GAAG,EAAE;QACb,IAAI,SAAS,GAAG,KAAK,CAAC;QACtB,UAAU,CAAC,IAAI,CAAC,CAAC;QAEjB,MAAM;aACH,WAAW,CAAC,IAAI,CAAC;aACjB,IAAI,CAAC,CAAC,MAAM,EAAE,EAAE;YACf,IAAI,SAAS;gBAAE,OAAO;YACtB,OAAO,CAAC,MAAM,CAAC,CAAC;YAChB,QAAQ,CAAC,IAAI,CAAC,CAAC;QACjB,CAAC,CAAC;aACD,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE;YACb,IAAI,SAAS;gBAAE,OAAO;YACtB,QAAQ,CAAC,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,KAAK,CAAC,yBAAyB,CAAC,CAAC,CAAC;QAC9E,CAAC,CAAC;aACD,OAAO,CAAC,GAAG,EAAE;YACZ,IAAI,CAAC,SAAS;gBAAE,UAAU,CAAC,KAAK,CAAC,CAAC;QACpC,CAAC,CAAC,CAAC;QAEL,OAAO,GAAG,EAAE;YACV,SAAS,GAAG,IAAI,CAAC;QACnB,CAAC,CAAC;IACJ,CAAC,EAAE,CAAC,MAAM,EAAE,IAAI,EAAE,IAAI,CAAC,CAAC,CAAC;IAEzB,OAAO;QACL,QAAQ,EAAE,IAAI,EAAE,QAAQ,IAAI,EAAE;QAC9B,IAAI,EAAE,IAAI,EAAE,IAAI,IAAI,IAAI;QACxB,UAAU,EAAE,IAAI,EAAE,UAAU,IAAI,CAAC;QACjC,aAAa,EAAE,IAAI,EAAE,aAAa,IAAI,CAAC;QACvC,OAAO;QACP,KAAK;QACL,QAAQ;QACR,OAAO;KACR,CAAC;AACJ,CAAC"}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/** Top-level manifest stored at /site/{siteId}/manifest.json */
|
|
2
|
+
export interface Manifest {
|
|
3
|
+
/** Schema version for future migrations */
|
|
4
|
+
schemaVersion: 1;
|
|
5
|
+
/** Monotonically increasing version — bumped on every publish */
|
|
6
|
+
version: number;
|
|
7
|
+
/** Total published article count */
|
|
8
|
+
totalArticles: number;
|
|
9
|
+
/** Total list pages available */
|
|
10
|
+
totalPages: number;
|
|
11
|
+
/** Articles per list page */
|
|
12
|
+
pageSize: number;
|
|
13
|
+
/** ISO 8601 timestamp of latest update */
|
|
14
|
+
updatedAt: string;
|
|
15
|
+
/** Slug(s) of featured/pinned articles (for hero sections) */
|
|
16
|
+
featuredSlugs: string[];
|
|
17
|
+
}
|
|
18
|
+
/** A single article entry in a paginated list page */
|
|
19
|
+
export interface ArticleListItem {
|
|
20
|
+
/** Article title */
|
|
21
|
+
title: string;
|
|
22
|
+
/** URL-safe slug */
|
|
23
|
+
slug: string;
|
|
24
|
+
/** Featured image URL (hosted externally — we only store the URL) */
|
|
25
|
+
imageUrl?: string;
|
|
26
|
+
/** Featured image alt text */
|
|
27
|
+
imageAlt?: string;
|
|
28
|
+
/** Short description for cards / meta */
|
|
29
|
+
metaDescription: string;
|
|
30
|
+
/** ISO 8601 creation timestamp */
|
|
31
|
+
createdAt: string;
|
|
32
|
+
/** Article tags */
|
|
33
|
+
tags: string[];
|
|
34
|
+
}
|
|
35
|
+
/** Paginated list page stored at /site/{siteId}/articles/page-{n}.json */
|
|
36
|
+
export interface ArticleListPage {
|
|
37
|
+
/** Page number (1-indexed) */
|
|
38
|
+
page: number;
|
|
39
|
+
/** Total pages at time of generation */
|
|
40
|
+
totalPages: number;
|
|
41
|
+
/** Total article count at time of generation */
|
|
42
|
+
totalArticles: number;
|
|
43
|
+
/** Manifest version this page was generated from */
|
|
44
|
+
version: number;
|
|
45
|
+
/** Articles on this page (newest first) */
|
|
46
|
+
articles: ArticleListItem[];
|
|
47
|
+
}
|
|
48
|
+
/** Full article JSON stored at /site/{siteId}/article/{slug}.json */
|
|
49
|
+
export interface Article {
|
|
50
|
+
/** Unique article ID (from EarlySEO) */
|
|
51
|
+
id: string;
|
|
52
|
+
/** Article title */
|
|
53
|
+
title: string;
|
|
54
|
+
/** URL-safe slug */
|
|
55
|
+
slug: string;
|
|
56
|
+
/** Styled HTML content (wrapped in .earlyseo-article div with embedded CSS) */
|
|
57
|
+
contentHtml: string;
|
|
58
|
+
/** Raw HTML without EarlySEO wrapper div or styles — inherits your site theme */
|
|
59
|
+
contentRawHtml: string;
|
|
60
|
+
/** Standalone CSS for the .earlyseo-article wrapper */
|
|
61
|
+
contentCss: string;
|
|
62
|
+
/** Meta description for SEO */
|
|
63
|
+
metaDescription: string;
|
|
64
|
+
/** ISO 8601 creation timestamp */
|
|
65
|
+
createdAt: string;
|
|
66
|
+
/** ISO 8601 timestamp of last update */
|
|
67
|
+
updatedAt: string;
|
|
68
|
+
/** Featured image URL */
|
|
69
|
+
imageUrl?: string;
|
|
70
|
+
/** Featured image alt text */
|
|
71
|
+
imageAlt?: string;
|
|
72
|
+
/** Article tags / keywords */
|
|
73
|
+
tags: string[];
|
|
74
|
+
/** Short summary text */
|
|
75
|
+
summary?: string;
|
|
76
|
+
/** Canonical URL (if set by EarlySEO) */
|
|
77
|
+
canonicalUrl?: string;
|
|
78
|
+
/** Article version — bumped on re-publish/update */
|
|
79
|
+
version: number;
|
|
80
|
+
}
|
|
81
|
+
/** Configuration for the EarlySEO blog CDN client. */
|
|
82
|
+
export interface EarlySeoConfig {
|
|
83
|
+
/**
|
|
84
|
+
* Your site ID from the EarlySEO dashboard.
|
|
85
|
+
* This determines which CDN folder to read from.
|
|
86
|
+
*/
|
|
87
|
+
siteId: string;
|
|
88
|
+
/**
|
|
89
|
+
* CDN base URL. Defaults to the EarlySEO public CDN.
|
|
90
|
+
* Override only if using a custom domain or self-hosted R2.
|
|
91
|
+
*/
|
|
92
|
+
cdnBaseUrl?: string;
|
|
93
|
+
}
|
|
94
|
+
/** Default CDN base URL — the EarlySEO public R2 bucket. */
|
|
95
|
+
export declare const DEFAULT_CDN_BASE_URL = "https://media.earlyseo.com";
|
|
96
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAcA,gEAAgE;AAChE,MAAM,WAAW,QAAQ;IACvB,2CAA2C;IAC3C,aAAa,EAAE,CAAC,CAAC;IACjB,iEAAiE;IACjE,OAAO,EAAE,MAAM,CAAC;IAChB,oCAAoC;IACpC,aAAa,EAAE,MAAM,CAAC;IACtB,iCAAiC;IACjC,UAAU,EAAE,MAAM,CAAC;IACnB,6BAA6B;IAC7B,QAAQ,EAAE,MAAM,CAAC;IACjB,0CAA0C;IAC1C,SAAS,EAAE,MAAM,CAAC;IAClB,8DAA8D;IAC9D,aAAa,EAAE,MAAM,EAAE,CAAC;CACzB;AAED,sDAAsD;AACtD,MAAM,WAAW,eAAe;IAC9B,oBAAoB;IACpB,KAAK,EAAE,MAAM,CAAC;IACd,oBAAoB;IACpB,IAAI,EAAE,MAAM,CAAC;IACb,qEAAqE;IACrE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,8BAA8B;IAC9B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,yCAAyC;IACzC,eAAe,EAAE,MAAM,CAAC;IACxB,kCAAkC;IAClC,SAAS,EAAE,MAAM,CAAC;IAClB,mBAAmB;IACnB,IAAI,EAAE,MAAM,EAAE,CAAC;CAChB;AAED,0EAA0E;AAC1E,MAAM,WAAW,eAAe;IAC9B,8BAA8B;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,wCAAwC;IACxC,UAAU,EAAE,MAAM,CAAC;IACnB,gDAAgD;IAChD,aAAa,EAAE,MAAM,CAAC;IACtB,oDAAoD;IACpD,OAAO,EAAE,MAAM,CAAC;IAChB,2CAA2C;IAC3C,QAAQ,EAAE,eAAe,EAAE,CAAC;CAC7B;AAED,qEAAqE;AACrE,MAAM,WAAW,OAAO;IACtB,wCAAwC;IACxC,EAAE,EAAE,MAAM,CAAC;IACX,oBAAoB;IACpB,KAAK,EAAE,MAAM,CAAC;IACd,oBAAoB;IACpB,IAAI,EAAE,MAAM,CAAC;IACb,+EAA+E;IAC/E,WAAW,EAAE,MAAM,CAAC;IACpB,iFAAiF;IACjF,cAAc,EAAE,MAAM,CAAC;IACvB,uDAAuD;IACvD,UAAU,EAAE,MAAM,CAAC;IACnB,+BAA+B;IAC/B,eAAe,EAAE,MAAM,CAAC;IACxB,kCAAkC;IAClC,SAAS,EAAE,MAAM,CAAC;IAClB,wCAAwC;IACxC,SAAS,EAAE,MAAM,CAAC;IAClB,yBAAyB;IACzB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,8BAA8B;IAC9B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,8BAA8B;IAC9B,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,yBAAyB;IACzB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,yCAAyC;IACzC,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,oDAAoD;IACpD,OAAO,EAAE,MAAM,CAAC;CACjB;AAID,sDAAsD;AACtD,MAAM,WAAW,cAAc;IAC7B;;;OAGG;IACH,MAAM,EAAE,MAAM,CAAC;IACf;;;OAGG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,4DAA4D;AAC5D,eAAO,MAAM,oBAAoB,+BAA+B,CAAC"}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
2
|
+
// @earlyseo/blog — Core Types
|
|
3
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
4
|
+
// CDN JSON schema for the paginated, versioned article system.
|
|
5
|
+
//
|
|
6
|
+
// CDN layout:
|
|
7
|
+
// /site/{siteId}/manifest.json
|
|
8
|
+
// /site/{siteId}/articles/page-1.json
|
|
9
|
+
// /site/{siteId}/articles/page-2.json
|
|
10
|
+
// /site/{siteId}/article/{slug}.json
|
|
11
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
12
|
+
/** Default CDN base URL — the EarlySEO public R2 bucket. */
|
|
13
|
+
export const DEFAULT_CDN_BASE_URL = "https://media.earlyseo.com";
|
|
14
|
+
//# sourceMappingURL=types.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,gFAAgF;AAChF,8BAA8B;AAC9B,gFAAgF;AAChF,+DAA+D;AAC/D,EAAE;AACF,cAAc;AACd,iCAAiC;AACjC,wCAAwC;AACxC,wCAAwC;AACxC,uCAAuC;AACvC,gFAAgF;AAwGhF,4DAA4D;AAC5D,MAAM,CAAC,MAAM,oBAAoB,GAAG,4BAA4B,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@earlyseo/blog",
|
|
3
|
+
"version": "1.0.1",
|
|
4
|
+
"description": "Drop-in blog integration for React & Next.js — powered by EarlySEO. Articles are served from EarlySEO's CDN. Just add your site ID and render.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"earlyseo-blog": "./src/cli/init.mjs"
|
|
8
|
+
},
|
|
9
|
+
"main": "dist/index.js",
|
|
10
|
+
"module": "dist/index.js",
|
|
11
|
+
"types": "dist/index.d.ts",
|
|
12
|
+
"exports": {
|
|
13
|
+
".": {
|
|
14
|
+
"types": "./dist/index.d.ts",
|
|
15
|
+
"import": "./dist/index.js"
|
|
16
|
+
},
|
|
17
|
+
"./react": {
|
|
18
|
+
"types": "./dist/react/index.d.ts",
|
|
19
|
+
"import": "./dist/react/index.js"
|
|
20
|
+
},
|
|
21
|
+
"./next": {
|
|
22
|
+
"types": "./dist/next/index.d.ts",
|
|
23
|
+
"import": "./dist/next/index.js"
|
|
24
|
+
},
|
|
25
|
+
"./css": {
|
|
26
|
+
"types": "./dist/css.d.ts",
|
|
27
|
+
"import": "./dist/css.js"
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
"scripts": {
|
|
31
|
+
"build": "tsc -p tsconfig.json",
|
|
32
|
+
"prepublishOnly": "npm run build"
|
|
33
|
+
},
|
|
34
|
+
"files": [
|
|
35
|
+
"dist",
|
|
36
|
+
"src/cli",
|
|
37
|
+
"README.md"
|
|
38
|
+
],
|
|
39
|
+
"keywords": [
|
|
40
|
+
"earlyseo",
|
|
41
|
+
"blog",
|
|
42
|
+
"seo",
|
|
43
|
+
"react",
|
|
44
|
+
"nextjs",
|
|
45
|
+
"cms",
|
|
46
|
+
"headless",
|
|
47
|
+
"blog-engine",
|
|
48
|
+
"cdn"
|
|
49
|
+
],
|
|
50
|
+
"license": "MIT",
|
|
51
|
+
"peerDependencies": {
|
|
52
|
+
"react": ">=18.0.0",
|
|
53
|
+
"react-dom": ">=18.0.0",
|
|
54
|
+
"next": ">=13.0.0"
|
|
55
|
+
},
|
|
56
|
+
"peerDependenciesMeta": {
|
|
57
|
+
"next": {
|
|
58
|
+
"optional": true
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
"devDependencies": {
|
|
62
|
+
"@types/react": "^18.0.0",
|
|
63
|
+
"@types/node": "^20.0.0",
|
|
64
|
+
"react": "^18.0.0",
|
|
65
|
+
"react-dom": "^18.0.0",
|
|
66
|
+
"typescript": "^5.0.0"
|
|
67
|
+
}
|
|
68
|
+
}
|
package/src/cli/init.mjs
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
4
|
+
// @earlyseo/blog init — Generate Next.js blog pages automatically
|
|
5
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
6
|
+
// Usage: npx @earlyseo/blog init
|
|
7
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
8
|
+
|
|
9
|
+
import fs from "node:fs";
|
|
10
|
+
import path from "node:path";
|
|
11
|
+
import readline from "node:readline";
|
|
12
|
+
|
|
13
|
+
const cwd = process.cwd();
|
|
14
|
+
|
|
15
|
+
// ── Helpers ──────────────────────────────────────────────────────────────────
|
|
16
|
+
|
|
17
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
18
|
+
|
|
19
|
+
function ask(question) {
|
|
20
|
+
return new Promise((resolve) => {
|
|
21
|
+
rl.question(question, (answer) => {
|
|
22
|
+
resolve(answer.trim());
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function fileExists(relativePath) {
|
|
28
|
+
return fs.existsSync(path.join(cwd, relativePath));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function writeFileSafe(relativePath, content) {
|
|
32
|
+
const fullPath = path.join(cwd, relativePath);
|
|
33
|
+
if (fs.existsSync(fullPath)) {
|
|
34
|
+
console.log(` ⏭ Skipped ${relativePath} (already exists)`);
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
fs.mkdirSync(path.dirname(fullPath), { recursive: true });
|
|
38
|
+
fs.writeFileSync(fullPath, content, "utf-8");
|
|
39
|
+
console.log(` ✅ Created ${relativePath}`);
|
|
40
|
+
return true;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// ── Detection ────────────────────────────────────────────────────────────────
|
|
44
|
+
|
|
45
|
+
function detectNextJs() {
|
|
46
|
+
const patterns = [
|
|
47
|
+
"next.config.js",
|
|
48
|
+
"next.config.mjs",
|
|
49
|
+
"next.config.ts",
|
|
50
|
+
"next.config.cjs",
|
|
51
|
+
];
|
|
52
|
+
return patterns.some((p) => fileExists(p));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function detectAppDir() {
|
|
56
|
+
// Check common app directory locations
|
|
57
|
+
if (fileExists("app")) return "app";
|
|
58
|
+
if (fileExists("src/app")) return "src/app";
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function detectSrcDir() {
|
|
63
|
+
// Check if project uses src/ directory pattern
|
|
64
|
+
return fileExists("src/app");
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// ── Templates ────────────────────────────────────────────────────────────────
|
|
68
|
+
|
|
69
|
+
function blogListPage(siteId) {
|
|
70
|
+
return `import { createBlogListPage, createBlogListMetadata } from "@earlyseo/blog/next";
|
|
71
|
+
|
|
72
|
+
const siteId = process.env.EARLYSEO_SITE_ID ?? "${siteId}";
|
|
73
|
+
|
|
74
|
+
export default createBlogListPage({
|
|
75
|
+
siteId,
|
|
76
|
+
title: "Blog",
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
export const generateMetadata = createBlogListMetadata({
|
|
80
|
+
title: "Blog",
|
|
81
|
+
description: "Read our latest articles",
|
|
82
|
+
});
|
|
83
|
+
`;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function blogPostPage(siteId) {
|
|
87
|
+
return `import { createBlogPostPage, createBlogPostMetadata } from "@earlyseo/blog/next";
|
|
88
|
+
|
|
89
|
+
const siteId = process.env.EARLYSEO_SITE_ID ?? "${siteId}";
|
|
90
|
+
|
|
91
|
+
export default createBlogPostPage({ siteId });
|
|
92
|
+
|
|
93
|
+
export const generateMetadata = createBlogPostMetadata({
|
|
94
|
+
siteId,
|
|
95
|
+
});
|
|
96
|
+
`;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// ── Main ─────────────────────────────────────────────────────────────────────
|
|
100
|
+
|
|
101
|
+
async function main() {
|
|
102
|
+
console.log();
|
|
103
|
+
console.log(" @earlyseo/blog — Blog Setup");
|
|
104
|
+
console.log(" ───────────────────────────");
|
|
105
|
+
console.log();
|
|
106
|
+
|
|
107
|
+
// 1. Detect Next.js
|
|
108
|
+
if (!detectNextJs()) {
|
|
109
|
+
console.log(" ❌ Could not find a Next.js project in the current directory.");
|
|
110
|
+
console.log(" (Looked for next.config.js, next.config.mjs, or next.config.ts)");
|
|
111
|
+
console.log();
|
|
112
|
+
console.log(" This command generates pages for Next.js App Router projects.");
|
|
113
|
+
console.log(" For React (Vite, Remix, etc.), see: https://www.npmjs.com/package/@earlyseo/blog");
|
|
114
|
+
console.log();
|
|
115
|
+
process.exit(1);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// 2. Detect app directory
|
|
119
|
+
const appDir = detectAppDir();
|
|
120
|
+
if (!appDir) {
|
|
121
|
+
console.log(" ❌ Could not find an app/ directory (Next.js App Router).");
|
|
122
|
+
console.log(" Checked: app/ and src/app/");
|
|
123
|
+
console.log();
|
|
124
|
+
console.log(" Create your app directory first, then re-run this command.");
|
|
125
|
+
console.log();
|
|
126
|
+
process.exit(1);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
console.log(` ✓ Next.js project detected (${appDir}/)`);
|
|
130
|
+
console.log();
|
|
131
|
+
|
|
132
|
+
// 3. Ask for site ID
|
|
133
|
+
let siteId = process.env.EARLYSEO_SITE_ID || "";
|
|
134
|
+
if (siteId) {
|
|
135
|
+
console.log(` ✓ Found EARLYSEO_SITE_ID from environment: ${siteId}`);
|
|
136
|
+
} else {
|
|
137
|
+
siteId = await ask(" Enter your EarlySEO Site ID: ");
|
|
138
|
+
if (!siteId) {
|
|
139
|
+
console.log(" ❌ Site ID is required. Find it in your EarlySEO dashboard → Integrations → SDK.");
|
|
140
|
+
console.log();
|
|
141
|
+
process.exit(1);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
console.log();
|
|
146
|
+
|
|
147
|
+
// 5. Generate pages
|
|
148
|
+
const blogDir = `${appDir}/blog`;
|
|
149
|
+
const listPagePath = `${blogDir}/page.tsx`;
|
|
150
|
+
const postPagePath = `${blogDir}/[slug]/page.tsx`;
|
|
151
|
+
|
|
152
|
+
writeFileSafe(listPagePath, blogListPage(siteId));
|
|
153
|
+
writeFileSafe(postPagePath, blogPostPage(siteId));
|
|
154
|
+
|
|
155
|
+
// 6. Add EARLYSEO_SITE_ID to .env.local
|
|
156
|
+
const envPath = path.join(cwd, ".env.local");
|
|
157
|
+
const envKey = "EARLYSEO_SITE_ID";
|
|
158
|
+
if (fs.existsSync(envPath)) {
|
|
159
|
+
const envContent = fs.readFileSync(envPath, "utf-8");
|
|
160
|
+
if (envContent.includes(envKey)) {
|
|
161
|
+
console.log(` ⏭ Skipped .env.local (${envKey} already set)`);
|
|
162
|
+
} else {
|
|
163
|
+
fs.appendFileSync(envPath, `\n# EarlySEO Blog SDK\n${envKey}=${siteId}\n`);
|
|
164
|
+
console.log(` ✅ Added ${envKey} to .env.local`);
|
|
165
|
+
}
|
|
166
|
+
} else {
|
|
167
|
+
fs.writeFileSync(envPath, `# EarlySEO Blog SDK\n${envKey}=${siteId}\n`, "utf-8");
|
|
168
|
+
console.log(` ✅ Created .env.local with ${envKey}`);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// 7. Done
|
|
172
|
+
rl.close();
|
|
173
|
+
console.log();
|
|
174
|
+
console.log(" ✓ Setup complete! Your blog is ready at /blog");
|
|
175
|
+
console.log();
|
|
176
|
+
console.log(" Next steps:");
|
|
177
|
+
console.log(" 1. Start your dev server: npm run dev");
|
|
178
|
+
console.log(" 2. Visit: http://localhost:3000/blog");
|
|
179
|
+
console.log(" 3. Publish articles from your EarlySEO dashboard");
|
|
180
|
+
console.log();
|
|
181
|
+
console.log(" Docs: https://www.npmjs.com/package/@earlyseo/blog");
|
|
182
|
+
console.log();
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
main().catch((err) => {
|
|
186
|
+
console.error(" ❌ Unexpected error:", err.message);
|
|
187
|
+
process.exit(1);
|
|
188
|
+
});
|