Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ flowchart LR

## Reading

A request for a page maps to a file in R2 (`/blog/some-post/` is `site/blog/some-post/index.html`) and is answered with its ETag and Last-Modified, so repeat visits get `304 Not Modified`. On a custom domain, Cloudflare's cache sits in front. The read path never queries D1: a traffic spike costs R2 reads, not database load. Media under `content/` is served from R2 with range requests, so video seeks, and WebP copies at `srcset` widths are made on first request when the Images binding is present. So are share cards (`content/cards/<key>.png`) for pages without an image: drawn with satori and resvg the first time someone asks, then kept in R2.
A request for a page maps to a file in R2 (`/blog/some-post/` is `site/blog/some-post/index.html`) and is answered with its ETag and Last-Modified, so repeat visits get `304 Not Modified`. On a custom domain, Cloudflare's cache sits in front. The read path never queries D1: a traffic spike costs R2 reads, not database load. Media under `content/` is served from R2 with range requests, so video seeks, and WebP copies at `srcset` widths are made on first request when the Images binding is present. So are share cards (`content/cards/<key>.png`) for pages without an image (or for every post, with `appearance.shareCards.everyPost`): drawn with satori and resvg the first time someone asks, in the site's design (`packages/server/src/card-layout.ts`, centered, or `card-left.ts`), then kept in R2.

Hosts other than `SITE_URL`'s answer with `x-robots-tag: noindex`, so staging and workers.dev copies never compete with the real site. `PREVIEW_PATH` serves the whole site at a second path with links rewritten on the way out, for trying Masthead on a domain whose main path still belongs to another blog.

Expand Down
3 changes: 2 additions & 1 deletion docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ It saves `site`, `newsletter`, `ai`, `style` and `workflow`, adds `memory` entri
| Field | What it does |
| --- | --- |
| `title`, `description`, `locale` | The publication's name, one-line description and language (default `en`). |
| `metaTitle`, `metaDescription`, `ogTitle`, `ogDescription` | Title, description and share card for the front page, when they differ from the above. |
| `metaTitle`, `metaDescription`, `ogTitle`, `ogDescription` | Title, description and share card for the front page, when they differ from the above. Shared, a page goes by its own name (`og:title`): the front page by `ogTitle`, else `appearance.hero.title`, else `title`; a post by its `ogTitle` or title, a topic by its `ogTitle` or name, an author by their name. `metaTitle` stays in `<title>`. |
| `logo`, `icon`, `shareImage` | Logo in the header and footer, favicon, and the default share image for pages without their own. |
| `accentColor` | Brand color for links and buttons. |
| `twitter` | The publication's X handle, for share cards. |
Expand All @@ -138,6 +138,7 @@ It saves `site`, `newsletter`, `ai`, `style` and `workflow`, adds `memory` entri
| `appearance.backdrop` | A backdrop behind the header: an `image`, an optional `mobileImage` and twinkling `sparkles`. Inverted in the light scheme. |
| `appearance.headerCta` | A button at the end of the header, with an optional `signedIn` variant chosen by a cookie your product sets. |
| `appearance.subscribe` | Title and text of the signup band. |
| `appearance.shareCards` | The share cards drawn for pages without a share image of their own. `design`: `centered` (the default; the logo and title on the night sky) or `left` (the logo and a trail such as `ACME · BLOG · TOPIC` on top, the headline and a support line, then the byline or post count and the page's address under a hairline, on a sky of still stars). `everyPost`: posts and pages with a cover image are shared with a card too; the cover stays on the page, and an `ogImage` set on the post still wins. `home`: the front page card's `title` (default: `appearance.hero.title`, then `title`), a dimmer second line `dim` (left design) and `text` (default: the front page's share description). |
| `publisher` | The organization in structured data: `name`, `url`, `logo`, `sameAs`, `description`, `knowsAbout`. Defaults to the site. Every page carries it as a short entity with a stable `@id`; the front page carries the full one. |
| `about` | A short paragraph on who publishes the blog and what they do. It leads `llms.txt` and `llms-full.txt`, closes every post's Markdown copy, and is the RSS channel description, so a model that reads any one of them knows who is speaking. |
| `offerings` | What the publisher offers, as `{ name, url, description }`. Listed under About in `llms.txt` and published as offers on the organization. |
Expand Down
3 changes: 2 additions & 1 deletion packages/admin/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ export const base = location.pathname.replace(/admin(\/.*)?$/, '');

export interface Me {
user: { staffId: string; email: string; name: string; role: 'owner' | 'admin' | 'editor' | 'author' | 'contributor'; via: string };
site: { title: string; url: string; icon: string | null };
/** `everyPostCard`: posts are shared with a drawn card even when they have a cover (appearance.shareCards.everyPost). */
site: { title: string; url: string; icon: string | null; everyPostCard?: boolean };
appUrl: string;
testMode: boolean;
}
Expand Down
3 changes: 2 additions & 1 deletion packages/admin/src/pages/post-tools.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,8 @@ export function SearchPanel(props: { draft: { id?: string; title: string; slug:
const d = props.draft;
const checks = seoChecks(d, props.getHtml());
const passed = checks.filter(c => c.ok).length;
const image = d.ogImage || d.featureImage;
// A share image set by hand wins; the cover is shared only when posts don't all get a card.
const image = d.ogImage || (session.value?.site.everyPostCard ? null : d.featureImage);
// Without an image the post is shared with a drawn card: show it, redrawn once the title settles.
const [cardTitle, setCardTitle] = useState(d.ogTitle || d.title);
useEffect(() => {
Expand Down
37 changes: 37 additions & 0 deletions packages/admin/src/pages/site-design.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,13 @@ interface Appearance {
headerCta?: { label: string; url: string; signedIn?: { cookie: string; label: string; url: string } | null } | null;
hero?: { title?: string | null; text?: string | null } | null;
subscribe?: { title?: string | null; text?: string | null } | null;
shareCards?: ShareCards | null;
}

interface ShareCards {
design?: 'centered' | 'left';
everyPost?: boolean;
home?: { title?: string | null; dim?: string | null; text?: string | null } | null;
}

const ICONS = ['', 'terminal', 'window', 'workflow', 'pointer', 'flask', 'book', 'rss', 'mail'];
Expand Down Expand Up @@ -201,7 +208,11 @@ export function AppearanceEditor({ value, onChange }: { value: Appearance; onCha
const cta = value.headerCta ?? { label: '', url: '' };
const hero = value.hero ?? {};
const sub = value.subscribe ?? {};
const cards = value.shareCards ?? {};
const home = cards.home ?? {};
const setCta = (c: typeof cta) => onChange({ ...value, headerCta: c.label || c.url ? c : null });
const setCards = (c: ShareCards) => onChange({ ...value, shareCards: c });
const setHome = (h: NonNullable<ShareCards['home']>) => setCards({ ...cards, home: h.title || h.dim || h.text ? h : null });
return (
<div class="stack">
<div class="grid2">
Expand Down Expand Up @@ -271,6 +282,32 @@ export function AppearanceEditor({ value, onChange }: { value: Appearance; onCha
</Field>
</div>
</Group>
<Group title="Share cards">
<p class="muted small">The picture link previews show for pages without a share image of their own.</p>
<div class="grid2">
<Field label="Design">
<select value={cards.design ?? 'centered'} onChange={e => setCards({ ...cards, design: e.currentTarget.value as ShareCards['design'] })}>
<option value="centered">Centered: the logo and title on the night sky</option>
<option value="left">Left: a trail on top, the headline, a byline and address below</option>
</select>
</Field>
</div>
<label class="check">
<input type="checkbox" checked={!!cards.everyPost} onChange={e => setCards({ ...cards, everyPost: e.currentTarget.checked })} /> Share every post with a card, also
posts with a cover image (a share image set on the post still wins)
</label>
<div class="grid2">
<Field label="Front page card headline" hint="Blank: the masthead title.">
<input value={home.title ?? ''} placeholder="News from Acme" onInput={e => setHome({ ...home, title: text(e.currentTarget.value) })} />
</Field>
<Field label="Second line, set dimmer" hint="Left design. Blank: none.">
<input value={home.dim ?? ''} onInput={e => setHome({ ...home, dim: text(e.currentTarget.value) })} />
</Field>
<Field label="Front page card text" hint="Blank: the front page's share description.">
<input value={home.text ?? ''} onInput={e => setHome({ ...home, text: text(e.currentTarget.value) })} />
</Field>
</div>
</Group>
<Group title="Newsletter signup">
<div class="grid2">
<Field label="Heading">
Expand Down
35 changes: 34 additions & 1 deletion packages/core/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,28 @@ export interface Appearance {
} | null;
/** Heading and text of the newsletter signup band. */
subscribe?: { title?: string | null; text?: string | null } | null;
/** The share cards drawn for pages without their own share image. */
shareCards?: ShareCardSettings | null;
}

export interface ShareCardSettings {
/**
* `centered` (the default): the logo and the page's title in the middle of the site's night sky.
* `left`: the logo and a trail of small capitals on top (site, section, the page's topic or kind),
* the headline and one support line set left, and a footer band with the byline or post count on
* the left and the page's address on the right, on a sky of still stars.
*/
design?: 'centered' | 'left';
/**
* Every post and page is shared with a drawn card, also when it has a cover image (the cover
* stays on the page and in its structured data). A share image set by hand (ogImage) still wins.
*/
everyPost?: boolean;
/**
* The front page's card. `title` defaults to the masthead title, then the site title; `dim`
* is a second headline line set dimmer (left design); `text` defaults to the share description.
*/
home?: { title?: string | null; dim?: string | null; text?: string | null } | null;
}

export interface SiteSettings {
Expand Down Expand Up @@ -248,10 +270,17 @@ export interface ShareCard {
date?: string | null;
/** A line under the title: the blog's tagline, a topic's or author's description. */
text?: string | null;
/** The bottom line, e.g. "Jane Doe · 6 min read". */
/**
* The bottom line, e.g. "Jane Doe · 6 min read". On the left design: the byline alone (the
* card adds the date) or a post count, e.g. "9 posts".
*/
meta?: string | null;
/** The author's portrait on author cards. */
image?: string | null;
/** A second headline line, set dimmer (left design). */
dim?: string | null;
/** The address the card shows, e.g. "example.com/blog/tag/news" (left design). */
address?: string | null;
}

/** What every card shares: the lockup and the sky. */
Expand All @@ -264,6 +293,10 @@ export interface ShareCardSite {
backdrop?: string | null;
locale: string;
url: string;
/** The card design (appearance.shareCards.design); centered when unset. */
design?: 'centered' | 'left';
/** The left design's trail before each card's own part, e.g. ["Acme", "Blog"]. */
trail?: string[];
}

/** The cards of a published site by key; the server draws content/cards/<key>.png from it. */
Expand Down
4 changes: 2 additions & 2 deletions packages/render/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,5 @@ export { headTags, blogPostingLd, breadcrumbLd, blogLd, collectionLd, profileLd,
export { rss, urlset, sitemapIndex } from './feeds';
export { llmsFull, llmsTxt, markdownCopy } from './llms';
export type { LlmsEntry, LlmsLinks } from './llms';
export { escapeHtml, plainText, autoExcerpt, readingMinutes, wordCount, tagLinks, shortHash } from './util';
export { postShareCard, shareCardSite, themeAssetsVersion } from './site';
export { escapeHtml, plainText, autoExcerpt, readingMinutes, sentenceSummary, wordCount, tagLinks, shortHash } from './util';
export { postShareCard, shareCardAlt, shareCardSite, shownAddress, themeAssetsVersion } from './site';
Loading
Loading