@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.
Files changed (63) hide show
  1. package/README.md +304 -0
  2. package/dist/client.d.ts +22 -0
  3. package/dist/client.d.ts.map +1 -0
  4. package/dist/client.js +65 -0
  5. package/dist/client.js.map +1 -0
  6. package/dist/css.d.ts +16 -0
  7. package/dist/css.d.ts.map +1 -0
  8. package/dist/css.js +186 -0
  9. package/dist/css.js.map +1 -0
  10. package/dist/index.d.ts +5 -0
  11. package/dist/index.d.ts.map +1 -0
  12. package/dist/index.js +13 -0
  13. package/dist/index.js.map +1 -0
  14. package/dist/next/back-link.d.ts +12 -0
  15. package/dist/next/back-link.d.ts.map +1 -0
  16. package/dist/next/back-link.js +15 -0
  17. package/dist/next/back-link.js.map +1 -0
  18. package/dist/next/blog-list-page.d.ts +22 -0
  19. package/dist/next/blog-list-page.d.ts.map +1 -0
  20. package/dist/next/blog-list-page.js +49 -0
  21. package/dist/next/blog-list-page.js.map +1 -0
  22. package/dist/next/blog-post-page.d.ts +65 -0
  23. package/dist/next/blog-post-page.d.ts.map +1 -0
  24. package/dist/next/blog-post-page.js +103 -0
  25. package/dist/next/blog-post-page.js.map +1 -0
  26. package/dist/next/index.d.ts +5 -0
  27. package/dist/next/index.d.ts.map +1 -0
  28. package/dist/next/index.js +11 -0
  29. package/dist/next/index.js.map +1 -0
  30. package/dist/react/blog-list.d.ts +27 -0
  31. package/dist/react/blog-list.d.ts.map +1 -0
  32. package/dist/react/blog-list.js +49 -0
  33. package/dist/react/blog-list.js.map +1 -0
  34. package/dist/react/blog-post.d.ts +35 -0
  35. package/dist/react/blog-post.d.ts.map +1 -0
  36. package/dist/react/blog-post.js +41 -0
  37. package/dist/react/blog-post.js.map +1 -0
  38. package/dist/react/index.d.ts +8 -0
  39. package/dist/react/index.d.ts.map +1 -0
  40. package/dist/react/index.js +15 -0
  41. package/dist/react/index.js.map +1 -0
  42. package/dist/react/provider.d.ts +30 -0
  43. package/dist/react/provider.d.ts.map +1 -0
  44. package/dist/react/provider.js +34 -0
  45. package/dist/react/provider.js.map +1 -0
  46. package/dist/react/styles.d.ts +11 -0
  47. package/dist/react/styles.d.ts.map +1 -0
  48. package/dist/react/styles.js +16 -0
  49. package/dist/react/styles.js.map +1 -0
  50. package/dist/react/use-article.d.ts +17 -0
  51. package/dist/react/use-article.d.ts.map +1 -0
  52. package/dist/react/use-article.js +53 -0
  53. package/dist/react/use-article.js.map +1 -0
  54. package/dist/react/use-articles.d.ts +22 -0
  55. package/dist/react/use-articles.d.ts.map +1 -0
  56. package/dist/react/use-articles.js +59 -0
  57. package/dist/react/use-articles.js.map +1 -0
  58. package/dist/types.d.ts +96 -0
  59. package/dist/types.d.ts.map +1 -0
  60. package/dist/types.js +14 -0
  61. package/dist/types.js.map +1 -0
  62. package/package.json +68 -0
  63. package/src/cli/init.mjs +188 -0
package/README.md ADDED
@@ -0,0 +1,304 @@
1
+ # @earlyseo/blog
2
+
3
+ Drop-in blog integration for **React** & **Next.js** — powered by [EarlySEO](https://earlyseo.com).
4
+
5
+ Articles are served from the EarlySEO CDN. No database, no webhooks, no storage config — just add your **site ID** and go.
6
+
7
+ ## Features
8
+
9
+ - **CDN-first** — Articles served from EarlySEO's global CDN. Zero infrastructure to manage.
10
+ - **Paginated** — Pre-built paginated JSON. No DB queries per visitor.
11
+ - **Next.js App Router** — Drop-in server components for `/blog` and `/blog/[slug]`
12
+ - **React hooks** — `useArticles()` and `useArticle(slug)` for custom UIs
13
+ - **Full SEO** — Open Graph, Twitter cards, meta descriptions, canonical URLs
14
+ - **Auto-styling** — CSS custom properties adapt to your site's theme
15
+ - **100% customizable** — Render props, custom components, or headless hooks
16
+
17
+ ## Quick Start (Next.js)
18
+
19
+ ### Option A: Auto-generate pages (recommended)
20
+
21
+ ```bash
22
+ npm install @earlyseo/blog
23
+ npx earlyseo-blog
24
+ ```
25
+
26
+ The CLI will detect your Next.js project, ask for your site ID, and generate:
27
+ - `app/blog/page.tsx` — Paginated blog listing
28
+ - `app/blog/[slug]/page.tsx` — Article detail with full SEO metadata
29
+ - `.env.local` — `EARLYSEO_SITE_ID` added automatically
30
+
31
+ That's it — run `npm run dev` and visit `/blog`.
32
+
33
+ ### Option B: Manual setup
34
+
35
+ ### 1. Install
36
+
37
+ ```bash
38
+ npm install @earlyseo/blog
39
+ ```
40
+
41
+ ### 2. Set your site ID
42
+
43
+ ```env
44
+ # .env.local
45
+ EARLYSEO_SITE_ID=your-site-id
46
+ ```
47
+
48
+ ### 3. Add the blog listing page
49
+
50
+ ```tsx
51
+ // app/blog/page.tsx
52
+ import { createBlogListPage, createBlogListMetadata } from "@earlyseo/blog/next";
53
+
54
+ export default createBlogListPage({
55
+ siteId: process.env.EARLYSEO_SITE_ID!,
56
+ title: "Our Blog",
57
+ });
58
+
59
+ export const generateMetadata = createBlogListMetadata({
60
+ title: "Our Blog",
61
+ description: "Read our latest articles about...",
62
+ siteName: "My Site",
63
+ });
64
+ ```
65
+
66
+ ### 4. Add the article detail page
67
+
68
+ ```tsx
69
+ // app/blog/[slug]/page.tsx
70
+ import { createBlogPostPage, createBlogPostMetadata } from "@earlyseo/blog/next";
71
+
72
+ const siteId = process.env.EARLYSEO_SITE_ID!;
73
+
74
+ export default createBlogPostPage({ siteId });
75
+ export const generateMetadata = createBlogPostMetadata({ siteId, siteName: "My Site" });
76
+ ```
77
+
78
+ That's it! Articles published from EarlySEO automatically appear on your blog via CDN.
79
+
80
+ ---
81
+
82
+ ## How It Works
83
+
84
+ When you publish an article in [EarlySEO](https://earlyseo.com), we write pre-built JSON files to our CDN:
85
+
86
+ ```
87
+ media.earlyseo.com/site/{siteId}/manifest.json → Total pages, version
88
+ media.earlyseo.com/site/{siteId}/articles/page-1.json → Paginated article list
89
+ media.earlyseo.com/site/{siteId}/article/my-slug.json → Full article content
90
+ ```
91
+
92
+ Your site reads from these JSON files — **no database, no API calls, no webhook handlers**.
93
+
94
+ Only the affected files are updated on each publish:
95
+ - The individual article JSON
96
+ - The paginated list pages
97
+ - The manifest
98
+
99
+ ---
100
+
101
+ ## React Components
102
+
103
+ ### Using with any React framework (Vite, Remix, etc.)
104
+
105
+ Wrap your app in `EarlySeoProvider` and use the components:
106
+
107
+ ```tsx
108
+ import { EarlySeoProvider, BlogList, BlogPost, ArticleStyles } from "@earlyseo/blog/react";
109
+
110
+ function App() {
111
+ return (
112
+ <EarlySeoProvider siteId="your-site-id" basePath="/blog">
113
+ <ArticleStyles />
114
+ <Routes>
115
+ <Route path="/blog" element={<BlogList />} />
116
+ <Route path="/blog/:slug" element={<BlogPostRoute />} />
117
+ </Routes>
118
+ </EarlySeoProvider>
119
+ );
120
+ }
121
+
122
+ function BlogPostRoute() {
123
+ const { slug } = useParams();
124
+ return <BlogPost slug={slug!} />;
125
+ }
126
+ ```
127
+
128
+ ### Headless Hooks
129
+
130
+ For full control over the UI, use the hooks directly:
131
+
132
+ ```tsx
133
+ import { useArticles, useArticle } from "@earlyseo/blog/react";
134
+
135
+ function CustomBlogList() {
136
+ const { articles, loading, page, totalPages, goToPage } = useArticles();
137
+
138
+ if (loading) return <Skeleton />;
139
+
140
+ return (
141
+ <div>
142
+ {articles.map((a) => (
143
+ <Link key={a.slug} to={`/blog/${a.slug}`}>
144
+ <h2>{a.title}</h2>
145
+ <p>{a.metaDescription}</p>
146
+ </Link>
147
+ ))}
148
+ <div>Page {page} of {totalPages}</div>
149
+ </div>
150
+ );
151
+ }
152
+
153
+ function CustomArticle({ slug }: { slug: string }) {
154
+ const { article, loading } = useArticle(slug);
155
+
156
+ if (loading) return <Skeleton />;
157
+ if (!article) return <NotFound />;
158
+
159
+ return (
160
+ <article>
161
+ <h1>{article.title}</h1>
162
+ <div dangerouslySetInnerHTML={{ __html: article.contentRawHtml }} />
163
+ </article>
164
+ );
165
+ }
166
+ ```
167
+
168
+ ### Custom Card Rendering
169
+
170
+ Override the default card in the blog list:
171
+
172
+ ```tsx
173
+ <BlogList
174
+ renderCard={(article, basePath) => (
175
+ <a href={`${basePath}/${article.slug}`} className="my-custom-card">
176
+ <img src={article.imageUrl} alt={article.title} />
177
+ <h3>{article.title}</h3>
178
+ </a>
179
+ )}
180
+ />
181
+ ```
182
+
183
+ ---
184
+
185
+ ## CDN Client
186
+
187
+ Use `EarlySeoClient` directly for full control:
188
+
189
+ ```ts
190
+ import { EarlySeoClient } from "@earlyseo/blog";
191
+
192
+ const client = new EarlySeoClient(
193
+ { siteId: "your-site-id" },
194
+ // Optional: Next.js ISR caching
195
+ { next: { revalidate: 60 } }
196
+ );
197
+
198
+ const manifest = await client.getManifest(); // Manifest | null
199
+ const page1 = await client.getListPage(1); // ArticleListPage | null
200
+ const article = await client.getArticle("slug"); // Article | null
201
+ ```
202
+
203
+ ---
204
+
205
+ ## Styling & Theming
206
+
207
+ Articles come with a `.earlyseo-article` CSS wrapper that uses CSS custom properties for full theming control:
208
+
209
+ ```css
210
+ :root {
211
+ --earlyseo-font-family: "Inter", sans-serif;
212
+ --earlyseo-text-color: #1a1a1a;
213
+ --earlyseo-heading-color: #111;
214
+ --earlyseo-link-color: #2563eb;
215
+ --earlyseo-border-color: #e5e7eb;
216
+ --earlyseo-code-bg: #f3f4f6;
217
+ --earlyseo-pre-bg: #f9fafb;
218
+ --earlyseo-th-bg: #f3f4f6;
219
+ --earlyseo-muted-color: #6b7280;
220
+ --earlyseo-tag-bg: #f3f4f6;
221
+ --earlyseo-blog-max-width: 72rem;
222
+ --earlyseo-post-max-width: 48rem;
223
+ --earlyseo-blog-padding: 2rem 1rem;
224
+ }
225
+ ```
226
+
227
+ Or use `htmlMode="raw"` to strip all EarlySEO styles and use your own:
228
+
229
+ ```tsx
230
+ // Next.js
231
+ export default createBlogPostPage({ siteId, htmlMode: "raw" });
232
+
233
+ // React
234
+ <BlogPost slug={slug} htmlMode="raw" />
235
+ ```
236
+
237
+ ---
238
+
239
+ ## API Reference
240
+
241
+ ### Types
242
+
243
+ ```ts
244
+ interface Manifest {
245
+ schemaVersion: 1;
246
+ version: number;
247
+ totalArticles: number;
248
+ totalPages: number;
249
+ pageSize: number;
250
+ updatedAt: string;
251
+ featuredSlugs: string[];
252
+ }
253
+
254
+ interface ArticleListItem {
255
+ title: string;
256
+ slug: string;
257
+ imageUrl?: string;
258
+ imageAlt?: string;
259
+ metaDescription: string;
260
+ createdAt: string;
261
+ tags: string[];
262
+ }
263
+
264
+ interface ArticleListPage {
265
+ page: number;
266
+ totalPages: number;
267
+ totalArticles: number;
268
+ version: number;
269
+ articles: ArticleListItem[];
270
+ }
271
+
272
+ interface Article {
273
+ id: string;
274
+ title: string;
275
+ slug: string;
276
+ contentHtml: string; // Styled HTML (with .earlyseo-article wrapper)
277
+ contentRawHtml: string; // Raw HTML (inherits your site theme)
278
+ contentCss: string; // Standalone CSS for .earlyseo-article
279
+ metaDescription: string;
280
+ createdAt: string;
281
+ updatedAt: string;
282
+ imageUrl?: string;
283
+ imageAlt?: string;
284
+ tags: string[];
285
+ summary?: string;
286
+ canonicalUrl?: string;
287
+ version: number;
288
+ }
289
+ ```
290
+
291
+ ### Imports
292
+
293
+ | Import Path | Contents |
294
+ |-------------|----------|
295
+ | `@earlyseo/blog` | `EarlySeoClient`, core types, CSS constants |
296
+ | `@earlyseo/blog/react` | Provider, hooks, components |
297
+ | `@earlyseo/blog/next` | Next.js page creators, metadata helpers |
298
+ | `@earlyseo/blog/css` | `ARTICLE_CSS`, `BLOG_CSS` |
299
+
300
+ ---
301
+
302
+ ## License
303
+
304
+ MIT
@@ -0,0 +1,22 @@
1
+ import { type EarlySeoConfig, type Manifest, type ArticleListPage, type Article } from "./types";
2
+ export declare class EarlySeoClient {
3
+ private baseUrl;
4
+ private siteId;
5
+ /** Additional fetch options (e.g. Next.js `{ next: { revalidate: 60 } }`) */
6
+ private fetchInit;
7
+ constructor(config: EarlySeoConfig, fetchInit?: RequestInit);
8
+ /** URL for the site manifest */
9
+ manifestUrl(): string;
10
+ /** URL for a paginated article list page (1-indexed) */
11
+ listPageUrl(page: number): string;
12
+ /** URL for a single article's full JSON */
13
+ articleUrl(slug: string): string;
14
+ /** Fetch the site manifest. Returns null on 404. */
15
+ getManifest(): Promise<Manifest | null>;
16
+ /** Fetch a paginated article list page (1-indexed). Returns null on 404. */
17
+ getListPage(page: number): Promise<ArticleListPage | null>;
18
+ /** Fetch a single article by slug. Returns null on 404. */
19
+ getArticle(slug: string): Promise<Article | null>;
20
+ private fetchJson;
21
+ }
22
+ //# sourceMappingURL=client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAYA,OAAO,EAEL,KAAK,cAAc,EACnB,KAAK,QAAQ,EACb,KAAK,eAAe,EACpB,KAAK,OAAO,EACb,MAAM,SAAS,CAAC;AAEjB,qBAAa,cAAc;IACzB,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,MAAM,CAAS;IACvB,6EAA6E;IAC7E,OAAO,CAAC,SAAS,CAAc;gBAEnB,MAAM,EAAE,cAAc,EAAE,SAAS,CAAC,EAAE,WAAW;IAQ3D,gCAAgC;IAChC,WAAW,IAAI,MAAM;IAIrB,wDAAwD;IACxD,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM;IAIjC,2CAA2C;IAC3C,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM;IAOhC,oDAAoD;IAC9C,WAAW,IAAI,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC;IAI7C,4EAA4E;IACtE,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,GAAG,IAAI,CAAC;IAKhE,2DAA2D;IACrD,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,GAAG,IAAI,CAAC;YAMzC,SAAS;CAcxB"}
package/dist/client.js ADDED
@@ -0,0 +1,65 @@
1
+ // ─────────────────────────────────────────────────────────────────────────────
2
+ // @earlyseo/blog — CDN Client
3
+ // ─────────────────────────────────────────────────────────────────────────────
4
+ // Thin read-only client that fetches pre-built JSON from the EarlySEO CDN.
5
+ // All writes happen server-side in our worker — users never write.
6
+ //
7
+ // CDN layout:
8
+ // /site/{siteId}/manifest.json → Manifest
9
+ // /site/{siteId}/articles/page-{n}.json → ArticleListPage
10
+ // /site/{siteId}/article/{slug}.json → Article
11
+ // ─────────────────────────────────────────────────────────────────────────────
12
+ import { DEFAULT_CDN_BASE_URL, } from "./types";
13
+ export class EarlySeoClient {
14
+ constructor(config, fetchInit) {
15
+ this.siteId = config.siteId;
16
+ this.baseUrl = (config.cdnBaseUrl ?? DEFAULT_CDN_BASE_URL).replace(/\/+$/, "");
17
+ this.fetchInit = fetchInit ?? {};
18
+ }
19
+ // ─── URL builders (public so users can prefetch / build links) ─────────
20
+ /** URL for the site manifest */
21
+ manifestUrl() {
22
+ return `${this.baseUrl}/site/${this.siteId}/manifest.json`;
23
+ }
24
+ /** URL for a paginated article list page (1-indexed) */
25
+ listPageUrl(page) {
26
+ return `${this.baseUrl}/site/${this.siteId}/articles/page-${page}.json`;
27
+ }
28
+ /** URL for a single article's full JSON */
29
+ articleUrl(slug) {
30
+ const safe = slug.replace(/[^a-zA-Z0-9_-]/g, "-");
31
+ return `${this.baseUrl}/site/${this.siteId}/article/${safe}.json`;
32
+ }
33
+ // ─── Data fetchers ─────────────────────────────────────────────────────
34
+ /** Fetch the site manifest. Returns null on 404. */
35
+ async getManifest() {
36
+ return this.fetchJson(this.manifestUrl());
37
+ }
38
+ /** Fetch a paginated article list page (1-indexed). Returns null on 404. */
39
+ async getListPage(page) {
40
+ if (page < 1)
41
+ return null;
42
+ return this.fetchJson(this.listPageUrl(page));
43
+ }
44
+ /** Fetch a single article by slug. Returns null on 404. */
45
+ async getArticle(slug) {
46
+ return this.fetchJson(this.articleUrl(slug));
47
+ }
48
+ // ─── Internal ──────────────────────────────────────────────────────────
49
+ async fetchJson(url) {
50
+ const res = await fetch(url, {
51
+ ...this.fetchInit,
52
+ headers: {
53
+ Accept: "application/json",
54
+ ...(this.fetchInit.headers ?? {}),
55
+ },
56
+ });
57
+ if (res.status === 404)
58
+ return null;
59
+ if (!res.ok) {
60
+ throw new Error(`EarlySEO CDN error: ${res.status} ${res.statusText} for ${url}`);
61
+ }
62
+ return res.json();
63
+ }
64
+ }
65
+ //# sourceMappingURL=client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA,gFAAgF;AAChF,8BAA8B;AAC9B,gFAAgF;AAChF,2EAA2E;AAC3E,mEAAmE;AACnE,EAAE;AACF,cAAc;AACd,sDAAsD;AACtD,6DAA6D;AAC7D,qDAAqD;AACrD,gFAAgF;AAEhF,OAAO,EACL,oBAAoB,GAKrB,MAAM,SAAS,CAAC;AAEjB,MAAM,OAAO,cAAc;IAMzB,YAAY,MAAsB,EAAE,SAAuB;QACzD,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC,MAAM,CAAC;QAC5B,IAAI,CAAC,OAAO,GAAG,CAAC,MAAM,CAAC,UAAU,IAAI,oBAAoB,CAAC,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;QAC/E,IAAI,CAAC,SAAS,GAAG,SAAS,IAAI,EAAE,CAAC;IACnC,CAAC;IAED,0EAA0E;IAE1E,gCAAgC;IAChC,WAAW;QACT,OAAO,GAAG,IAAI,CAAC,OAAO,SAAS,IAAI,CAAC,MAAM,gBAAgB,CAAC;IAC7D,CAAC;IAED,wDAAwD;IACxD,WAAW,CAAC,IAAY;QACtB,OAAO,GAAG,IAAI,CAAC,OAAO,SAAS,IAAI,CAAC,MAAM,kBAAkB,IAAI,OAAO,CAAC;IAC1E,CAAC;IAED,2CAA2C;IAC3C,UAAU,CAAC,IAAY;QACrB,MAAM,IAAI,GAAG,IAAI,CAAC,OAAO,CAAC,iBAAiB,EAAE,GAAG,CAAC,CAAC;QAClD,OAAO,GAAG,IAAI,CAAC,OAAO,SAAS,IAAI,CAAC,MAAM,YAAY,IAAI,OAAO,CAAC;IACpE,CAAC;IAED,0EAA0E;IAE1E,oDAAoD;IACpD,KAAK,CAAC,WAAW;QACf,OAAO,IAAI,CAAC,SAAS,CAAW,IAAI,CAAC,WAAW,EAAE,CAAC,CAAC;IACtD,CAAC;IAED,4EAA4E;IAC5E,KAAK,CAAC,WAAW,CAAC,IAAY;QAC5B,IAAI,IAAI,GAAG,CAAC;YAAE,OAAO,IAAI,CAAC;QAC1B,OAAO,IAAI,CAAC,SAAS,CAAkB,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,CAAC,CAAC;IACjE,CAAC;IAED,2DAA2D;IAC3D,KAAK,CAAC,UAAU,CAAC,IAAY;QAC3B,OAAO,IAAI,CAAC,SAAS,CAAU,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC;IACxD,CAAC;IAED,0EAA0E;IAElE,KAAK,CAAC,SAAS,CAAI,GAAW;QACpC,MAAM,GAAG,GAAG,MAAM,KAAK,CAAC,GAAG,EAAE;YAC3B,GAAG,IAAI,CAAC,SAAS;YACjB,OAAO,EAAE;gBACP,MAAM,EAAE,kBAAkB;gBAC1B,GAAG,CAAE,IAAI,CAAC,SAAS,CAAC,OAAkC,IAAI,EAAE,CAAC;aAC9D;SACF,CAAC,CAAC;QACH,IAAI,GAAG,CAAC,MAAM,KAAK,GAAG;YAAE,OAAO,IAAI,CAAC;QACpC,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC;YACZ,MAAM,IAAI,KAAK,CAAC,uBAAuB,GAAG,CAAC,MAAM,IAAI,GAAG,CAAC,UAAU,QAAQ,GAAG,EAAE,CAAC,CAAC;QACpF,CAAC;QACD,OAAO,GAAG,CAAC,IAAI,EAAgB,CAAC;IAClC,CAAC;CACF"}
package/dist/css.d.ts ADDED
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Companion CSS for the `.earlyseo-article` wrapper. All color defaults
3
+ * are semi-transparent so they adapt to both light and dark backgrounds.
4
+ * Every color is overridable via CSS custom properties.
5
+ *
6
+ * Usage:
7
+ * import { ARTICLE_CSS } from '@earlyseo/blog/css';
8
+ * // Inject via <style> tag or your CSS-in-JS solution
9
+ */
10
+ export declare const ARTICLE_CSS: string;
11
+ /**
12
+ * Minimal blog layout CSS — provides sensible defaults for the blog listing
13
+ * and article detail pages. All values are overridable via CSS custom properties.
14
+ */
15
+ export declare const BLOG_CSS: string;
16
+ //# sourceMappingURL=css.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"css.d.ts","sourceRoot":"","sources":["../src/css.ts"],"names":[],"mappings":"AAIA;;;;;;;;GAQG;AACH,eAAO,MAAM,WAAW,QAsChB,CAAC;AAET;;;GAGG;AACH,eAAO,MAAM,QAAQ,QAiIb,CAAC"}
package/dist/css.js ADDED
@@ -0,0 +1,186 @@
1
+ // ─────────────────────────────────────────────────────────────────────────────
2
+ // @earlyseo/blog — Article CSS
3
+ // ─────────────────────────────────────────────────────────────────────────────
4
+ /**
5
+ * Companion CSS for the `.earlyseo-article` wrapper. All color defaults
6
+ * are semi-transparent so they adapt to both light and dark backgrounds.
7
+ * Every color is overridable via CSS custom properties.
8
+ *
9
+ * Usage:
10
+ * import { ARTICLE_CSS } from '@earlyseo/blog/css';
11
+ * // Inject via <style> tag or your CSS-in-JS solution
12
+ */
13
+ export const ARTICLE_CSS = `
14
+ .earlyseo-article {
15
+ color: var(--earlyseo-text-color, inherit);
16
+ font-size: 16px;
17
+ line-height: 1.75;
18
+ word-wrap: break-word;
19
+ font-family: var(--earlyseo-font-family, inherit);
20
+ }
21
+ .earlyseo-article > :first-child { margin-top: 0; }
22
+ .earlyseo-article > :last-child { margin-bottom: 0; }
23
+ .earlyseo-article h1,.earlyseo-article h2,.earlyseo-article h3,.earlyseo-article h4,.earlyseo-article h5,.earlyseo-article h6 {
24
+ margin-top: 1.6em; margin-bottom: 0.6em; font-weight: 700; line-height: 1.3;
25
+ color: var(--earlyseo-heading-color, inherit);
26
+ }
27
+ .earlyseo-article h1 { font-size: 2rem; }
28
+ .earlyseo-article h2 { font-size: 1.5rem; padding-bottom: 0.25rem; border-bottom: 1px solid var(--earlyseo-border-color, rgba(128,128,128,0.2)); }
29
+ .earlyseo-article h3 { font-size: 1.25rem; }
30
+ .earlyseo-article h4 { font-size: 1.05rem; }
31
+ .earlyseo-article p,.earlyseo-article ul,.earlyseo-article ol,.earlyseo-article dl,.earlyseo-article table,.earlyseo-article blockquote,.earlyseo-article pre,.earlyseo-article figure {
32
+ margin-top: 0; margin-bottom: 1rem;
33
+ }
34
+ .earlyseo-article a { color: var(--earlyseo-link-color, inherit); text-decoration: underline; text-underline-offset: 2px; }
35
+ .earlyseo-article a:hover { text-decoration-thickness: 2px; }
36
+ .earlyseo-article ul,.earlyseo-article ol { padding-left: 1.5rem; }
37
+ .earlyseo-article li + li { margin-top: 0.3rem; }
38
+ .earlyseo-article li > p { margin-bottom: 0.4rem; }
39
+ .earlyseo-article blockquote { margin-left: 0; padding: 0.25rem 1rem; color: var(--earlyseo-muted-color, inherit); opacity: 0.75; border-left: 4px solid var(--earlyseo-border-color, rgba(128,128,128,0.2)); }
40
+ .earlyseo-article blockquote > * { opacity: 1; }
41
+ .earlyseo-article code { padding: 0.12rem 0.35rem; border-radius: 0.375rem; background: var(--earlyseo-code-bg, rgba(128,128,128,0.1)); font-size: 0.88em; font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; }
42
+ .earlyseo-article pre { overflow: auto; padding: 1rem; border-radius: 0.75rem; background: var(--earlyseo-pre-bg, rgba(128,128,128,0.06)); border: 1px solid var(--earlyseo-border-color, rgba(128,128,128,0.2)); }
43
+ .earlyseo-article pre code { background: transparent; padding: 0; border-radius: 0; font-size: 0.9em; line-height: 1.55; }
44
+ .earlyseo-article table { display: table; width: 100%; overflow-x: auto; border-collapse: collapse; }
45
+ .earlyseo-article th,.earlyseo-article td { padding: 0.55rem 0.75rem; border: 1px solid var(--earlyseo-border-color, rgba(128,128,128,0.2)); text-align: left; }
46
+ .earlyseo-article th { font-weight: 600; background: var(--earlyseo-th-bg, rgba(128,128,128,0.1)); }
47
+ .earlyseo-article img { max-width: 100%; height: auto; border-radius: 0.75rem; }
48
+ .earlyseo-article hr { margin: 1.5rem 0; border: 0; border-top: 1px solid var(--earlyseo-border-color, rgba(128,128,128,0.2)); }
49
+ .earlyseo-article .task-list-item { list-style: none; }
50
+ .earlyseo-article input[type="checkbox"] { margin-right: 0.45rem; }
51
+ `.trim();
52
+ /**
53
+ * Minimal blog layout CSS — provides sensible defaults for the blog listing
54
+ * and article detail pages. All values are overridable via CSS custom properties.
55
+ */
56
+ export const BLOG_CSS = `
57
+ .earlyseo-blog {
58
+ max-width: var(--earlyseo-blog-max-width, 72rem);
59
+ margin: 0 auto;
60
+ padding: var(--earlyseo-blog-padding, 2rem 1rem);
61
+ font-family: var(--earlyseo-font-family, inherit);
62
+ color: var(--earlyseo-text-color, inherit);
63
+ }
64
+ .earlyseo-blog-header {
65
+ margin-bottom: 2rem;
66
+ }
67
+ .earlyseo-blog-header h1 {
68
+ font-size: 2rem;
69
+ font-weight: 700;
70
+ margin: 0 0 0.5rem;
71
+ }
72
+ .earlyseo-blog-grid {
73
+ display: grid;
74
+ grid-template-columns: repeat(auto-fill, minmax(min(100%, 320px), 1fr));
75
+ gap: 2rem;
76
+ }
77
+ .earlyseo-blog-card {
78
+ display: flex;
79
+ flex-direction: column;
80
+ border-radius: 0.75rem;
81
+ overflow: hidden;
82
+ border: 1px solid var(--earlyseo-border-color, rgba(128,128,128,0.2));
83
+ transition: box-shadow 0.15s;
84
+ }
85
+ .earlyseo-blog-card:hover {
86
+ box-shadow: 0 4px 12px rgba(0,0,0,0.08);
87
+ }
88
+ .earlyseo-blog-card a {
89
+ text-decoration: none;
90
+ color: inherit;
91
+ display: flex;
92
+ flex-direction: column;
93
+ height: 100%;
94
+ }
95
+ .earlyseo-blog-card-image {
96
+ width: 100%;
97
+ aspect-ratio: 16/9;
98
+ object-fit: cover;
99
+ }
100
+ .earlyseo-blog-card-body {
101
+ padding: 1rem 1.25rem;
102
+ flex: 1;
103
+ display: flex;
104
+ flex-direction: column;
105
+ }
106
+ .earlyseo-blog-card-title {
107
+ font-size: 1.15rem;
108
+ font-weight: 600;
109
+ margin: 0 0 0.5rem;
110
+ line-height: 1.3;
111
+ }
112
+ .earlyseo-blog-card-desc {
113
+ font-size: 0.9rem;
114
+ opacity: 0.7;
115
+ margin: 0 0 0.75rem;
116
+ flex: 1;
117
+ display: -webkit-box;
118
+ -webkit-line-clamp: 3;
119
+ -webkit-box-orient: vertical;
120
+ overflow: hidden;
121
+ }
122
+ .earlyseo-blog-card-meta {
123
+ display: flex;
124
+ gap: 0.5rem;
125
+ flex-wrap: wrap;
126
+ font-size: 0.8rem;
127
+ opacity: 0.5;
128
+ }
129
+ .earlyseo-blog-tag {
130
+ display: inline-block;
131
+ padding: 0.15rem 0.5rem;
132
+ border-radius: 9999px;
133
+ background: var(--earlyseo-tag-bg, rgba(128,128,128,0.1));
134
+ font-size: 0.75rem;
135
+ }
136
+ .earlyseo-blog-post {
137
+ max-width: var(--earlyseo-post-max-width, 48rem);
138
+ margin: 0 auto;
139
+ padding: var(--earlyseo-blog-padding, 2rem 1rem);
140
+ }
141
+ .earlyseo-blog-post-header {
142
+ margin-bottom: 2rem;
143
+ }
144
+ .earlyseo-blog-post-header h1 {
145
+ font-size: 2.25rem;
146
+ font-weight: 700;
147
+ margin: 0 0 0.75rem;
148
+ line-height: 1.2;
149
+ }
150
+ .earlyseo-blog-post-meta {
151
+ display: flex;
152
+ gap: 1rem;
153
+ flex-wrap: wrap;
154
+ font-size: 0.85rem;
155
+ opacity: 0.6;
156
+ }
157
+ .earlyseo-blog-post-image {
158
+ width: 100%;
159
+ border-radius: 0.75rem;
160
+ margin-bottom: 2rem;
161
+ }
162
+ .earlyseo-blog-empty {
163
+ text-align: center;
164
+ padding: 4rem 1rem;
165
+ opacity: 0.5;
166
+ }
167
+ .earlyseo-blog-pagination {
168
+ display: flex;
169
+ justify-content: center;
170
+ gap: 0.5rem;
171
+ margin-top: 2rem;
172
+ }
173
+ .earlyseo-blog-pagination button {
174
+ padding: 0.5rem 1rem;
175
+ border: 1px solid var(--earlyseo-border-color, rgba(128,128,128,0.2));
176
+ border-radius: 0.375rem;
177
+ background: transparent;
178
+ cursor: pointer;
179
+ font-size: 0.85rem;
180
+ }
181
+ .earlyseo-blog-pagination button:disabled {
182
+ opacity: 0.3;
183
+ cursor: not-allowed;
184
+ }
185
+ `.trim();
186
+ //# sourceMappingURL=css.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"css.js","sourceRoot":"","sources":["../src/css.ts"],"names":[],"mappings":"AAAA,gFAAgF;AAChF,+BAA+B;AAC/B,gFAAgF;AAEhF;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,WAAW,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAsC1B,CAAC,IAAI,EAAE,CAAC;AAET;;;GAGG;AACH,MAAM,CAAC,MAAM,QAAQ,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAiIvB,CAAC,IAAI,EAAE,CAAC"}
@@ -0,0 +1,5 @@
1
+ export { EarlySeoClient } from "./client";
2
+ export type { Manifest, ArticleListItem, ArticleListPage, Article, EarlySeoConfig, } from "./types";
3
+ export { DEFAULT_CDN_BASE_URL } from "./types";
4
+ export { ARTICLE_CSS, BLOG_CSS } from "./css";
5
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AASA,OAAO,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AAG1C,YAAY,EACV,QAAQ,EACR,eAAe,EACf,eAAe,EACf,OAAO,EACP,cAAc,GACf,MAAM,SAAS,CAAC;AAEjB,OAAO,EAAE,oBAAoB,EAAE,MAAM,SAAS,CAAC;AAG/C,OAAO,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,13 @@
1
+ // ─────────────────────────────────────────────────────────────────────────────
2
+ // @earlyseo/blog — Main Entry Point
3
+ // ─────────────────────────────────────────────────────────────────────────────
4
+ // Core client, types, and CSS. For framework-specific imports use:
5
+ // @earlyseo/blog/react — React hooks & components
6
+ // @earlyseo/blog/next — Next.js pages & metadata helpers
7
+ // @earlyseo/blog/css — Article & blog CSS constants
8
+ // Client
9
+ export { EarlySeoClient } from "./client";
10
+ export { DEFAULT_CDN_BASE_URL } from "./types";
11
+ // CSS
12
+ export { ARTICLE_CSS, BLOG_CSS } from "./css";
13
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,gFAAgF;AAChF,oCAAoC;AACpC,gFAAgF;AAChF,mEAAmE;AACnE,qDAAqD;AACrD,6DAA6D;AAC7D,yDAAyD;AAEzD,SAAS;AACT,OAAO,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AAW1C,OAAO,EAAE,oBAAoB,EAAE,MAAM,SAAS,CAAC;AAE/C,MAAM;AACN,OAAO,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAC"}
@@ -0,0 +1,12 @@
1
+ import React from "react";
2
+ /**
3
+ * A "← Go back" link that navigates to the previous page via browser history,
4
+ * or falls back to `fallbackHref` (default: "/") if there is no history.
5
+ */
6
+ export declare function BackLink({ fallbackHref, children, style, className, }: {
7
+ fallbackHref?: string;
8
+ children?: React.ReactNode;
9
+ style?: React.CSSProperties;
10
+ className?: string;
11
+ }): import("react/jsx-runtime").JSX.Element;
12
+ //# sourceMappingURL=back-link.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"back-link.d.ts","sourceRoot":"","sources":["../../src/next/back-link.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,MAAM,OAAO,CAAC;AAE1B;;;GAGG;AACH,wBAAgB,QAAQ,CAAC,EACvB,YAAkB,EAClB,QAAsB,EACtB,KAAK,EACL,SAAS,GACV,EAAE;IACD,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IAC3B,KAAK,CAAC,EAAE,KAAK,CAAC,aAAa,CAAC;IAC5B,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,2CAgBA"}
@@ -0,0 +1,15 @@
1
+ "use client";
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ /**
4
+ * A "← Go back" link that navigates to the previous page via browser history,
5
+ * or falls back to `fallbackHref` (default: "/") if there is no history.
6
+ */
7
+ export function BackLink({ fallbackHref = "/", children = "← Go back", style, className, }) {
8
+ return (_jsx("a", { href: fallbackHref, className: className, style: style, onClick: (e) => {
9
+ if (window.history.length > 1) {
10
+ e.preventDefault();
11
+ window.history.back();
12
+ }
13
+ }, children: children }));
14
+ }
15
+ //# sourceMappingURL=back-link.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"back-link.js","sourceRoot":"","sources":["../../src/next/back-link.tsx"],"names":[],"mappings":"AAAA,YAAY,CAAC;;AAIb;;;GAGG;AACH,MAAM,UAAU,QAAQ,CAAC,EACvB,YAAY,GAAG,GAAG,EAClB,QAAQ,GAAG,WAAW,EACtB,KAAK,EACL,SAAS,GAMV;IACC,OAAO,CACL,YACE,IAAI,EAAE,YAAY,EAClB,SAAS,EAAE,SAAS,EACpB,KAAK,EAAE,KAAK,EACZ,OAAO,EAAE,CAAC,CAAC,EAAE,EAAE;YACb,IAAI,MAAM,CAAC,OAAO,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;gBAC9B,CAAC,CAAC,cAAc,EAAE,CAAC;gBACnB,MAAM,CAAC,OAAO,CAAC,IAAI,EAAE,CAAC;YACxB,CAAC;QACH,CAAC,YAEA,QAAQ,GACP,CACL,CAAC;AACJ,CAAC"}