@acms-subir/core 0.3.27 → 0.3.28
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/CHANGELOG.md +10 -0
- package/integration.mjs +12 -4
- package/package.json +1 -1
- package/src/lib/content.ts +15 -1
- package/src/middleware.ts +5 -4
- package/src/pages/admin/settings.astro +10 -3
- package/src/virtual.d.ts +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# @acms-subir/core
|
|
2
2
|
|
|
3
|
+
## 0.3.28
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 9964864: Collections get a `localizeRedirects` option, so a site whose posts are each written in one language can turn off 0.3.27's "keep the visitor's language" for them. It sits next to `base` and `redirectFrom`: `acms({ content: { posts: { base: '/sermon', redirectFrom: ['/sermons'], localizeRedirects: false } } })`, or under `content.collections.<key>` for any other collection. It defaults to `true`, the 0.3.27 behaviour, because ACMS posts can carry translations (`/es/sermon/<slug>` shows a post's Spanish words), so other sites don't change: `/es/blog` → `/es/sermon`.
|
|
8
|
+
|
|
9
|
+
With `localizeRedirects: false`, a redirect whose target is one of that collection's addresses (its base or an entry under it) goes to the target without adding the language: `/es/blog` → `/sermon`, `/es/blog/x` → `/sermon/x`, `/es/sermons` (a `redirectFrom` base) → `/sermon`, as in 0.3.26. Admin → Redirects rules are treated the same way: on a `/es/…` request, a rule `/old` → `/sermon/x` goes to `/sermon/x`. Tag addresses have no setting of their own. Tag archives are shared by every collection, and they replaced the built-in collection's old `/blog/tag/…`, so they follow `posts`: with it off, `/es/blog/tag/x` → `/tag/x`, and so do rules pointing at `/tag/…`. Redirects to pages keep the language either way, and a rule written for the full `/es/old` address is still used exactly as written. On a site with more than one language, the Redirects card names the addresses that stay as written. The value must be `true` or `false`; anything else fails the build. The app API (`/api/v1/site`) doesn't list it: apps don't follow the website's language redirects.
|
|
10
|
+
|
|
11
|
+
The option only covers redirects. Whether `/es/<collection>` pages render, their hreflang tags, the sitemap, and links from Spanish pages to `/es/sermon/…` are unchanged.
|
|
12
|
+
|
|
3
13
|
## 0.3.27
|
|
4
14
|
|
|
5
15
|
### Patch Changes
|
package/integration.mjs
CHANGED
|
@@ -161,6 +161,9 @@ function resolveContent(content = {}) {
|
|
|
161
161
|
if (key !== 'posts' && RESERVED_COLLECTION_KEYS.has(key)) {
|
|
162
162
|
throw new Error(`acms: collection key "${key}" is reserved (it collides with an admin section) — pick another key`);
|
|
163
163
|
}
|
|
164
|
+
if (def.localizeRedirects !== undefined && typeof def.localizeRedirects !== 'boolean') {
|
|
165
|
+
throw new Error(`acms: content.collections.${key}.localizeRedirects must be true or false`);
|
|
166
|
+
}
|
|
164
167
|
const fallbackPlural = key === 'posts' ? 'Posts' : key.charAt(0).toUpperCase() + key.slice(1);
|
|
165
168
|
const plural = def.plural ?? fallbackPlural;
|
|
166
169
|
collections[key] = {
|
|
@@ -168,6 +171,7 @@ function resolveContent(content = {}) {
|
|
|
168
171
|
plural,
|
|
169
172
|
base: def.base ?? (key === 'posts' ? '/blog' : `/${key}`),
|
|
170
173
|
redirectFrom: def.redirectFrom ?? [],
|
|
174
|
+
localizeRedirects: def.localizeRedirects ?? true,
|
|
171
175
|
};
|
|
172
176
|
}
|
|
173
177
|
const resolved = { collections, tags: { base: (content.tags ?? {}).base ?? '/tag' } };
|
|
@@ -571,14 +575,18 @@ function acmsVitePlugin(userConfig) {
|
|
|
571
575
|
* User-visible identity in the admin chrome (sidebar, titles, breadcrumb).
|
|
572
576
|
* @param {{siteTitle?: string, tagline?: string, heroHeading?: string, heroSub?: string}} [options.seed]
|
|
573
577
|
* First-boot database seed content. Only read when the database is empty.
|
|
574
|
-
* @param {{posts?: {singular?: string, plural?: string, base?: string, redirectFrom?: string[]}, collections?: Record<string, {singular?: string, plural?: string, base?: string, redirectFrom?: string[]}>, tags?: {base?: string}}} [options.content]
|
|
578
|
+
* @param {{posts?: {singular?: string, plural?: string, base?: string, redirectFrom?: string[], localizeRedirects?: boolean}, collections?: Record<string, {singular?: string, plural?: string, base?: string, redirectFrom?: string[], localizeRedirects?: boolean}>, tags?: {base?: string}}} [options.content]
|
|
575
579
|
* Content collections. `collections` is a keyed map — each key gets its own
|
|
576
580
|
* public routes at `base`, admin section at /admin/c/<key>, and labels from
|
|
577
581
|
* `singular`/`plural`. The `posts` key is the built-in collection (default
|
|
578
582
|
* base '/blog'); `content.posts` is shorthand for `collections.posts`.
|
|
579
|
-
* `redirectFrom` 301s extra legacy bases.
|
|
580
|
-
*
|
|
581
|
-
*
|
|
583
|
+
* `redirectFrom` 301s extra legacy bases. On a site with `i18n`, a redirect
|
|
584
|
+
* from /es/… to the collection keeps the language (/es/blog → /es/sermon);
|
|
585
|
+
* `localizeRedirects: false` drops it (→ /sermon) for entries each written
|
|
586
|
+
* in one language (tag addresses follow `posts`). Tags are standalone at
|
|
587
|
+
* `tags.base` (default '/tag'), shared across collections. Collection keys
|
|
588
|
+
* are permanent internal identifiers (DB rows, backups, admin URLs) — never
|
|
589
|
+
* renamed.
|
|
582
590
|
* @param {{locales?: string[], defaultLocale?: string}} [options.i18n]
|
|
583
591
|
* Site languages. The default locale (first entry unless `defaultLocale`
|
|
584
592
|
* says otherwise) is served at `/`; every other locale gets its own home
|
package/package.json
CHANGED
package/src/lib/content.ts
CHANGED
|
@@ -14,9 +14,12 @@ export interface CollectionConfig {
|
|
|
14
14
|
plural: string;
|
|
15
15
|
base: string;
|
|
16
16
|
redirectFrom: string[];
|
|
17
|
+
// false: a redirect from a /<locale>/… address to this collection drops the
|
|
18
|
+
// language, for entries each written in one language (localizesRedirectsTo)
|
|
19
|
+
localizeRedirects: boolean;
|
|
17
20
|
}
|
|
18
21
|
|
|
19
|
-
const FALLBACK_POSTS: CollectionConfig = { singular: 'Post', plural: 'Posts', base: '/blog', redirectFrom: [] };
|
|
22
|
+
const FALLBACK_POSTS: CollectionConfig = { singular: 'Post', plural: 'Posts', base: '/blog', redirectFrom: [], localizeRedirects: true };
|
|
20
23
|
|
|
21
24
|
const collections: Record<string, CollectionConfig> =
|
|
22
25
|
userConfig.content?.collections ?? { posts: FALLBACK_POSTS };
|
|
@@ -53,6 +56,17 @@ export function collectionFromPath(pathname: string): (CollectionConfig & { key:
|
|
|
53
56
|
return best;
|
|
54
57
|
}
|
|
55
58
|
|
|
59
|
+
// Whether a redirect from a /<locale>/… address to this path on the site
|
|
60
|
+
// keeps the visitor's language (middleware.ts localizedRedirect): not for a
|
|
61
|
+
// collection whose localizeRedirects is off. Tag archives are shared by every
|
|
62
|
+
// collection and follow the built-in one, whose old /blog/tag/… they replace.
|
|
63
|
+
export function localizesRedirectsTo(path: string): boolean {
|
|
64
|
+
const pathname = path.split(/[?#]/)[0];
|
|
65
|
+
const { base } = CONTENT.tags;
|
|
66
|
+
const c = pathname === base || pathname.startsWith(`${base}/`) ? CONTENT.posts : collectionFromPath(pathname);
|
|
67
|
+
return c?.localizeRedirects !== false;
|
|
68
|
+
}
|
|
69
|
+
|
|
56
70
|
// Aliases for the built-in collection — the bulk of call sites.
|
|
57
71
|
export const postsIndexUrl = () => CONTENT.posts.base;
|
|
58
72
|
export const postUrl = (slug: string) => `${CONTENT.posts.base}/${slug}`;
|
package/src/middleware.ts
CHANGED
|
@@ -5,7 +5,7 @@ import { bindAuthEnvironment, getRequestAuth, requestPerson, shouldResolveClerk,
|
|
|
5
5
|
import { roleHome } from './lib/roles';
|
|
6
6
|
import { AUTH } from './lib/auth/config';
|
|
7
7
|
import { getRepo } from './lib/repo';
|
|
8
|
-
import { CONTENT, legacyBases } from './lib/content';
|
|
8
|
+
import { CONTENT, legacyBases, localizesRedirectsTo } from './lib/content';
|
|
9
9
|
import { processDueDeliveries, waitUntilFrom } from './lib/webhooks';
|
|
10
10
|
import { dispatchEmail } from './lib/email/service';
|
|
11
11
|
import { decideOverdue, sendDueReminders } from './lib/booking/service';
|
|
@@ -46,10 +46,11 @@ export function legacyContentRedirect(pathname: string): string | null {
|
|
|
46
46
|
|
|
47
47
|
// A /es/… request reaches the redirect lookup rewritten to its bare path (see
|
|
48
48
|
// handle), so a target on this site gets the visitor's language back: /blog →
|
|
49
|
-
// /sermon serves /es/blog → /es/sermon. Full URLs
|
|
50
|
-
//
|
|
49
|
+
// /sermon serves /es/blog → /es/sermon. Full URLs, targets that already name a
|
|
50
|
+
// language, and addresses of a collection with localizeRedirects off (tag
|
|
51
|
+
// pages go with posts) stay as written.
|
|
51
52
|
export function localizedRedirect(to: string, locale: string | undefined): string {
|
|
52
|
-
if (!locale || !to.startsWith('/') || to.startsWith('//') || stripLocale(to) !== to) return to;
|
|
53
|
+
if (!locale || !to.startsWith('/') || to.startsWith('//') || stripLocale(to) !== to || !localizesRedirectsTo(to)) return to;
|
|
53
54
|
return localePath(to, locale);
|
|
54
55
|
}
|
|
55
56
|
|
|
@@ -7,7 +7,7 @@ import { accessFor } from '../../lib/permissions/access';
|
|
|
7
7
|
import type { Permission } from '../../lib/permissions/catalog';
|
|
8
8
|
import { AUTH } from '../../lib/auth/config';
|
|
9
9
|
import { invalidateRedirectCache } from '../../middleware';
|
|
10
|
-
import { CONTENT } from '../../lib/content';
|
|
10
|
+
import { CONTENT, localizesRedirectsTo } from '../../lib/content';
|
|
11
11
|
import { extraLocales } from '../../lib/i18n';
|
|
12
12
|
import { UMAMI_ID_RE, META_PIXEL_ID_RE, TIKTOK_PIXEL_ID_RE, normalizeScriptUrl } from '../../lib/analytics';
|
|
13
13
|
|
|
@@ -201,8 +201,15 @@ if (Astro.request.method === 'POST') {
|
|
|
201
201
|
|
|
202
202
|
const settings = await repo.getSettings();
|
|
203
203
|
const redirects = await repo.listRedirects();
|
|
204
|
-
// the redirect card explains /<locale>/… rules with the site's first extra
|
|
204
|
+
// the redirect card explains /<locale>/… rules with the site's first extra
|
|
205
|
+
// language, and names the bases whose targets keep no language (collections
|
|
206
|
+
// with localizeRedirects: false, and /tag with posts)
|
|
205
207
|
const extraLang = extraLocales()[0] ?? '';
|
|
208
|
+
const asWritten = [...Object.values(CONTENT.collections).map((c) => c.base), CONTENT.tags.base].filter((b) => !localizesRedirectsTo(b));
|
|
209
|
+
const asWrittenList = asWritten.length > 1 ? `${asWritten.slice(0, -1).join(', ')} or ${asWritten.at(-1)}` : asWritten[0];
|
|
210
|
+
const redirectsLangNote = extraLang
|
|
211
|
+
? ` A rule for /old-path also sends /${extraLang}/old-path to /${extraLang}/new-path${asWrittenList ? `, or to the new path as written when it's under ${asWrittenList}` : ''}; rules that start with /${extraLang}/ are used exactly as written.`
|
|
212
|
+
: '';
|
|
206
213
|
const backend = (Astro.locals as { runtime?: { env?: { DB?: unknown } } }).runtime?.env?.DB
|
|
207
214
|
? 'd1'
|
|
208
215
|
: 'sqlite';
|
|
@@ -444,7 +451,7 @@ const groups = [
|
|
|
444
451
|
<section class="scard" id="redirects">
|
|
445
452
|
<header class="scard-head">
|
|
446
453
|
<a class="scard-back" href="#" aria-label="All settings"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M15 6l-6 6 6 6" /></svg></a>
|
|
447
|
-
<div class="grow"><h2>Redirects</h2><p>Forward old URLs to new ones. They only fire when a URL would otherwise 404, and renaming a published post adds one automatically.{
|
|
454
|
+
<div class="grow"><h2>Redirects</h2><p>Forward old URLs to new ones. They only fire when a URL would otherwise 404, and renaming a published post adds one automatically.{redirectsLangNote}</p></div>
|
|
448
455
|
</header>
|
|
449
456
|
{redirects.length > 0 && (
|
|
450
457
|
<div class="rd-list">
|
package/src/virtual.d.ts
CHANGED
|
@@ -9,7 +9,7 @@ declare module 'virtual:acms-config' {
|
|
|
9
9
|
// resolved (defaults applied + validated) by integration.mjs; `posts`
|
|
10
10
|
// always exists in collections, insertion order = display order
|
|
11
11
|
content: {
|
|
12
|
-
collections: Record<string, { singular: string; plural: string; base: string; redirectFrom: string[] }>;
|
|
12
|
+
collections: Record<string, { singular: string; plural: string; base: string; redirectFrom: string[]; localizeRedirects: boolean }>;
|
|
13
13
|
tags: { base: string };
|
|
14
14
|
};
|
|
15
15
|
// resolved by integration.mjs; empty locales = i18n not configured
|