eratic / packages / core / src / config.ts
772 words
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
/**
 * The single source of truth for who this site is and where it lives.
 *
 * Change things here, not in individual pages. Every site, feed, sitemap and
 * <link rel="canonical"> reads from this file.
 */

/**
 * Identity.
 *
 * Deliberately pseudonymous — see docs/06-privacy.md. Nothing in this file
 * should be a legal name, a street address, or a phone number, because
 * everything here is published in HTML, RSS and JSON Feed output.
 */
export const IDENTITY = {
	/** Shown in headers, feed <author> fields and the copyright line. */
	name: 'eratic',
	/** One line, used as the default meta description on the home page. */
	tagline: 'Writing, photographs, and things worth keeping.',
	/** A contact address you are happy to publish. Set to null to omit entirely. */
	email: 'hello@eratic.ro' as string | null,
	locale: 'en',
	/** Used to render dates consistently regardless of the build machine. */
	timeZone: 'Europe/Bucharest',
	/** Year the site started; renders as "2026–" in the footer. */
	since: 2026,
} as const;

export type SiteKey = 'home' | 'blog' | 'fav' | 'io';

export interface SiteDef {
	key: SiteKey;
	host: string;
	url: string;
	/** Short label used in the cross-site navigation. */
	label: string;
	/** <title> suffix and feed title. */
	title: string;
	description: string;
	/** Whether this site publishes RSS + JSON Feed. */
	feeds: boolean;
	/**
	 * Whether this site is advertised — in the header nav, on the home page,
	 * and in the footer's "elsewhere" line.
	 *
	 * A site with `listed: false` is still built, still deployed and still
	 * reachable at its own address; it is simply not linked to from anywhere.
	 * That is the honest way to keep a section unfinished: no dead ends, no
	 * "coming soon" page, and nothing to remember to undo — flip this to true
	 * when there is something there worth sending people to.
	 */
	listed: boolean;
}

export const SITES: Record<SiteKey, SiteDef> = {
	home: {
		key: 'home',
		host: 'eratic.ro',
		url: 'https://eratic.ro',
		label: 'eratic',
		title: 'eratic',
		description: IDENTITY.tagline,
		feeds: false,
		listed: true,
	},
	blog: {
		key: 'blog',
		host: 'blog.eratic.ro',
		url: 'https://blog.eratic.ro',
		label: 'blog',
		title: 'eratic · blog',
		description: 'Essays and photographs, in one feed.',
		feeds: true,
		listed: true,
	},
	fav: {
		key: 'fav',
		host: 'fav.eratic.ro',
		url: 'https://fav.eratic.ro',
		label: 'fav',
		title: 'eratic · fav',
		description: 'Objects, books, films and music worth keeping, with notes.',
		feeds: true,
		// Built and live, but not linked to from anywhere yet.
		listed: false,
	},
	io: {
		key: 'io',
		host: 'io.eratic.ro',
		url: 'https://io.eratic.ro',
		label: 'io',
		title: 'eratic · io',
		description: 'What I read, watched and listened to, once a week.',
		feeds: true,
		// Same — flip to true when the first real issue goes out.
		listed: false,
	},
};

/**
 * The sections shown in the cross-site nav, in order. Home is the wordmark
 * rather than a tab, so it is not in here.
 *
 * Derived from `listed` so the header, the home page and the footer can never
 * disagree about which sections exist.
 */
export const NAV_ORDER: SiteKey[] = (['blog', 'fav', 'io'] as SiteKey[]).filter(
	(key) => SITES[key].listed,
);

/**
 * Where large media lives.
 *
 * Photographs are committed to git and optimised at build time by Astro, so
 * they never come through here. This is for video and any oversized asset that
 * would bloat the repository — see docs/05-media.md.
 *
 * `base` is served by the same Caddy instance as the sites, from
 * /opt/eratic/media on the server. No third party is involved.
 *
 * IPFS: when ird ships IPFS hosting, set `ipfsGateway` to the gateway origin
 * and change individual references from "video/2026/foo.mp4" to
 * "ipfs://<cid>". Both forms resolve through mediaUrl(), so a reference can be
 * migrated one file at a time without touching any component or layout.
 */
export const MEDIA = {
	base: 'https://media.eratic.ro',
	ipfsGateway: null as string | null,
} as const;

/**
 * Resolve a media reference to a URL.
 *
 * Accepts three forms:
 *   "video/2026/walk.mp4"  → {MEDIA.base}/video/2026/walk.mp4
 *   "ipfs://bafy…"         → {MEDIA.ipfsGateway}/bafy…
 *   "https://…"            → returned unchanged
 */
export function mediaUrl(ref: string): string {
	if (/^https?:\/\//i.test(ref)) return ref;

	if (ref.startsWith('ipfs://')) {
		const cid = ref.slice('ipfs://'.length);
		if (!MEDIA.ipfsGateway) {
			throw new Error(
				`Media reference "${ref}" is an IPFS URI, but MEDIA.ipfsGateway is not set ` +
					`in packages/core/src/config.ts. Set it, or use a path under MEDIA.base.`,
			);
		}
		return `${MEDIA.ipfsGateway.replace(/\/+$/, '')}/${cid.replace(/^\/+/, '')}`;
	}

	return `${MEDIA.base.replace(/\/+$/, '')}/${ref.replace(/^\/+/, '')}`;
}

/** Absolute URL for a path on a given site. Feeds and canonicals need these. */
export function absoluteUrl(site: SiteKey, path = '/'): string {
	return new URL(path, SITES[site].url).href;
}

static mirror of HEAD · about · clone: git clone https://git.makefuture.ro/eratic.git