@aglyn/tenant-runtime 1.0.0-beta.143
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/LICENSE +201 -0
- package/README.md +26 -0
- package/package.json +39 -0
- package/src/index.d.ts +21 -0
- package/src/index.js +22 -0
- package/src/index.js.map +1 -0
- package/src/lib/apply-publish-schedule.d.ts +34 -0
- package/src/lib/apply-publish-schedule.js +285 -0
- package/src/lib/apply-publish-schedule.js.map +1 -0
- package/src/lib/assign-contact-owner.d.ts +162 -0
- package/src/lib/assign-contact-owner.js +322 -0
- package/src/lib/assign-contact-owner.js.map +1 -0
- package/src/lib/associate-company-by-domain.d.ts +70 -0
- package/src/lib/associate-company-by-domain.js +150 -0
- package/src/lib/associate-company-by-domain.js.map +1 -0
- package/src/lib/author-page-nodes.d.ts +36 -0
- package/src/lib/author-page-nodes.js +296 -0
- package/src/lib/author-page-nodes.js.map +1 -0
- package/src/lib/built-in-page-layout.d.ts +48 -0
- package/src/lib/built-in-page-layout.js +107 -0
- package/src/lib/built-in-page-layout.js.map +1 -0
- package/src/lib/capture-host-contact.d.ts +95 -0
- package/src/lib/capture-host-contact.js +137 -0
- package/src/lib/capture-host-contact.js.map +1 -0
- package/src/lib/collection-fallback-nodes.d.ts +90 -0
- package/src/lib/collection-fallback-nodes.js +602 -0
- package/src/lib/collection-fallback-nodes.js.map +1 -0
- package/src/lib/compose-author-page.d.ts +109 -0
- package/src/lib/compose-author-page.js +225 -0
- package/src/lib/compose-author-page.js.map +1 -0
- package/src/lib/compose-collection-page.d.ts +94 -0
- package/src/lib/compose-collection-page.js +256 -0
- package/src/lib/compose-collection-page.js.map +1 -0
- package/src/lib/compose-screen-nodes.d.ts +156 -0
- package/src/lib/compose-screen-nodes.js +547 -0
- package/src/lib/compose-screen-nodes.js.map +1 -0
- package/src/lib/compose-search-page.d.ts +39 -0
- package/src/lib/compose-search-page.js +57 -0
- package/src/lib/compose-search-page.js.map +1 -0
- package/src/lib/convert-host-lead.d.ts +189 -0
- package/src/lib/convert-host-lead.js +443 -0
- package/src/lib/convert-host-lead.js.map +1 -0
- package/src/lib/defer-lazy-panels.d.ts +58 -0
- package/src/lib/defer-lazy-panels.js +171 -0
- package/src/lib/defer-lazy-panels.js.map +1 -0
- package/src/lib/element-animation-assets.d.ts +62 -0
- package/src/lib/element-animation-assets.js +290 -0
- package/src/lib/element-animation-assets.js.map +1 -0
- package/src/lib/emit-host-event.d.ts +29 -0
- package/src/lib/emit-host-event.js +30 -0
- package/src/lib/emit-host-event.js.map +1 -0
- package/src/lib/enrich-gated-page.d.ts +67 -0
- package/src/lib/enrich-gated-page.js +134 -0
- package/src/lib/enrich-gated-page.js.map +1 -0
- package/src/lib/entry-link-routes.d.ts +61 -0
- package/src/lib/entry-link-routes.js +169 -0
- package/src/lib/entry-link-routes.js.map +1 -0
- package/src/lib/get-author-content.d.ts +119 -0
- package/src/lib/get-author-content.js +288 -0
- package/src/lib/get-author-content.js.map +1 -0
- package/src/lib/get-collection-content.d.ts +290 -0
- package/src/lib/get-collection-content.js +565 -0
- package/src/lib/get-collection-content.js.map +1 -0
- package/src/lib/get-components.d.ts +24 -0
- package/src/lib/get-components.js +102 -0
- package/src/lib/get-components.js.map +1 -0
- package/src/lib/get-datasets.d.ts +45 -0
- package/src/lib/get-datasets.js +169 -0
- package/src/lib/get-datasets.js.map +1 -0
- package/src/lib/get-forms.d.ts +24 -0
- package/src/lib/get-forms.js +116 -0
- package/src/lib/get-forms.js.map +1 -0
- package/src/lib/get-layout-version.d.ts +26 -0
- package/src/lib/get-layout-version.js +99 -0
- package/src/lib/get-layout-version.js.map +1 -0
- package/src/lib/get-media-asset-facts.d.ts +74 -0
- package/src/lib/get-media-asset-facts.js +116 -0
- package/src/lib/get-media-asset-facts.js.map +1 -0
- package/src/lib/get-plugin-installs.d.ts +29 -0
- package/src/lib/get-plugin-installs.js +94 -0
- package/src/lib/get-plugin-installs.js.map +1 -0
- package/src/lib/get-screen-version.d.ts +27 -0
- package/src/lib/get-screen-version.js +66 -0
- package/src/lib/get-screen-version.js.map +1 -0
- package/src/lib/get-screen.d.ts +39 -0
- package/src/lib/get-screen.js +126 -0
- package/src/lib/get-screen.js.map +1 -0
- package/src/lib/get-variables.d.ts +41 -0
- package/src/lib/get-variables.js +128 -0
- package/src/lib/get-variables.js.map +1 -0
- package/src/lib/get-video-asset-facts.d.ts +22 -0
- package/src/lib/get-video-asset-facts.js +22 -0
- package/src/lib/get-video-asset-facts.js.map +1 -0
- package/src/lib/host-event-listeners.d.ts +83 -0
- package/src/lib/host-event-listeners.js +91 -0
- package/src/lib/host-event-listeners.js.map +1 -0
- package/src/lib/org-permissions.d.ts +63 -0
- package/src/lib/org-permissions.js +187 -0
- package/src/lib/org-permissions.js.map +1 -0
- package/src/lib/repeat-record-pages.d.ts +48 -0
- package/src/lib/repeat-record-pages.js +64 -0
- package/src/lib/repeat-record-pages.js.map +1 -0
- package/src/lib/required-site-plugins.d.ts +114 -0
- package/src/lib/required-site-plugins.js +121 -0
- package/src/lib/required-site-plugins.js.map +1 -0
- package/src/lib/resolve-dataset.d.ts +40 -0
- package/src/lib/resolve-dataset.js +64 -0
- package/src/lib/resolve-dataset.js.map +1 -0
- package/src/lib/search-results-nodes.d.ts +48 -0
- package/src/lib/search-results-nodes.js +302 -0
- package/src/lib/search-results-nodes.js.map +1 -0
- package/src/lib/social-image-facts.d.ts +107 -0
- package/src/lib/social-image-facts.js +133 -0
- package/src/lib/social-image-facts.js.map +1 -0
- package/src/lib/stamp-form-dataset-bindings.d.ts +32 -0
- package/src/lib/stamp-form-dataset-bindings.js +59 -0
- package/src/lib/stamp-form-dataset-bindings.js.map +1 -0
- package/src/lib/template-screens.d.ts +227 -0
- package/src/lib/template-screens.js +364 -0
- package/src/lib/template-screens.js.map +1 -0
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/
|
|
17
|
+
import { type HostContactCreated } from '@aglyn/tenant-data-admin';
|
|
18
|
+
/** What the association did, for the spec and the log. */
|
|
19
|
+
export type CompanyAssociation = {
|
|
20
|
+
outcome: 'linked' | 'created';
|
|
21
|
+
companyId: string;
|
|
22
|
+
} | {
|
|
23
|
+
outcome: 'none';
|
|
24
|
+
reason: 'no-domain' | 'no-org' | 'ambiguous' | 'no-match' | 'failed';
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* Link a NEW contact to the company its email domain names (AGL-2613).
|
|
28
|
+
*
|
|
29
|
+
* HubSpot associates a contact with a company on domain the moment the
|
|
30
|
+
* contact exists, and that is the moment this runs: once, from the capture
|
|
31
|
+
* door, for a person the org did not hold before. A repeat visit by somebody
|
|
32
|
+
* already held is not a new person and re-asks nothing; a door that already
|
|
33
|
+
* knew the company — the console's create drawer, an import row naming one
|
|
34
|
+
* — has said so in the facet and this is not called at all.
|
|
35
|
+
*
|
|
36
|
+
* ## Bounded: one query, one commit
|
|
37
|
+
*
|
|
38
|
+
* The query is `companies where domain == d and visibleTo array-contains-any
|
|
39
|
+
* (the capturing scope's read tokens)`, capped at two — the `(visibleTo
|
|
40
|
+
* CONTAINS, domain ASC)` composite serves it. Two rather than one because
|
|
41
|
+
* the answer has to be "exactly one": two companies at one domain visible
|
|
42
|
+
* to this site is an ambiguity a capture must not resolve by picking the
|
|
43
|
+
* first, and the contact waits for a person. The scope predicate is the
|
|
44
|
+
* console's own listener predicate, so a company this site could not open
|
|
45
|
+
* is not a match — an agency's client at `acme.com` is not linked to a
|
|
46
|
+
* sibling client's Acme.
|
|
47
|
+
*
|
|
48
|
+
* The write is one batch: the facet and the mirror on the contact, and the
|
|
49
|
+
* count on the company. The contact is fresh, so the plan is the trivial
|
|
50
|
+
* one — no previous link, nothing held elsewhere — and needs no read of the
|
|
51
|
+
* document.
|
|
52
|
+
*
|
|
53
|
+
* ## Creating is a setting, and off by default
|
|
54
|
+
*
|
|
55
|
+
* With `crm.autoCreateCompanies` on the org document, a domain no visible
|
|
56
|
+
* company carries becomes one, named after the domain and scoped exactly as
|
|
57
|
+
* the contact was — `crmScopeTokens`, the stamp every CRM creator uses —
|
|
58
|
+
* with the count starting at one. Public mailbox domains never reach this
|
|
59
|
+
* branch, because `companyDomainForEmail` answers `null` for them: a
|
|
60
|
+
* workspace's consumer list is not a list of accounts.
|
|
61
|
+
*
|
|
62
|
+
* ## The same posture as the capture
|
|
63
|
+
*
|
|
64
|
+
* Never throws. The capture has already succeeded and a form submission
|
|
65
|
+
* must not fail because the CRM could not file its author; a failure is
|
|
66
|
+
* logged and answered as `none`, and the person is exactly as capturable
|
|
67
|
+
* by hand as they were before this existed.
|
|
68
|
+
*/
|
|
69
|
+
export declare function associateCompanyByDomain(created: Pick<HostContactCreated, 'hostId' | 'contactId' | 'email'>): Promise<CompanyAssociation>;
|
|
70
|
+
export default associateCompanyByDomain;
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
2
|
+
/**
|
|
3
|
+
* @license
|
|
4
|
+
* Copyright 2026 Aglyn LLC
|
|
5
|
+
*
|
|
6
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
7
|
+
* you may not use this file except in compliance with the License.
|
|
8
|
+
* You may obtain a copy of the License at
|
|
9
|
+
*
|
|
10
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
11
|
+
*
|
|
12
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
13
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
14
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
15
|
+
* See the License for the specific language governing permissions and
|
|
16
|
+
* limitations under the License.
|
|
17
|
+
*/ import { companyDomainForEmail, companyNameForDomain, CRM_COLLECTIONS, crmReadTokens, crmScopeTokens, nameSearchFields, orgAutoCreatesCompanies } from "@aglyn/aglyn/server";
|
|
18
|
+
import { consentGroupForSite, contactCompanyLinkFields, firebaseAdmin, getOrgForHost, writeContactCompanyLink } from "@aglyn/tenant-data-admin";
|
|
19
|
+
import { COMPANY_CONTACTS_COUNT_FIELD, planContactCompanyLink } from "@aglyn/aglyn/server";
|
|
20
|
+
import { FieldValue } from "firebase-admin/firestore";
|
|
21
|
+
/**
|
|
22
|
+
* Link a NEW contact to the company its email domain names (AGL-2613).
|
|
23
|
+
*
|
|
24
|
+
* HubSpot associates a contact with a company on domain the moment the
|
|
25
|
+
* contact exists, and that is the moment this runs: once, from the capture
|
|
26
|
+
* door, for a person the org did not hold before. A repeat visit by somebody
|
|
27
|
+
* already held is not a new person and re-asks nothing; a door that already
|
|
28
|
+
* knew the company — the console's create drawer, an import row naming one
|
|
29
|
+
* — has said so in the facet and this is not called at all.
|
|
30
|
+
*
|
|
31
|
+
* ## Bounded: one query, one commit
|
|
32
|
+
*
|
|
33
|
+
* The query is `companies where domain == d and visibleTo array-contains-any
|
|
34
|
+
* (the capturing scope's read tokens)`, capped at two — the `(visibleTo
|
|
35
|
+
* CONTAINS, domain ASC)` composite serves it. Two rather than one because
|
|
36
|
+
* the answer has to be "exactly one": two companies at one domain visible
|
|
37
|
+
* to this site is an ambiguity a capture must not resolve by picking the
|
|
38
|
+
* first, and the contact waits for a person. The scope predicate is the
|
|
39
|
+
* console's own listener predicate, so a company this site could not open
|
|
40
|
+
* is not a match — an agency's client at `acme.com` is not linked to a
|
|
41
|
+
* sibling client's Acme.
|
|
42
|
+
*
|
|
43
|
+
* The write is one batch: the facet and the mirror on the contact, and the
|
|
44
|
+
* count on the company. The contact is fresh, so the plan is the trivial
|
|
45
|
+
* one — no previous link, nothing held elsewhere — and needs no read of the
|
|
46
|
+
* document.
|
|
47
|
+
*
|
|
48
|
+
* ## Creating is a setting, and off by default
|
|
49
|
+
*
|
|
50
|
+
* With `crm.autoCreateCompanies` on the org document, a domain no visible
|
|
51
|
+
* company carries becomes one, named after the domain and scoped exactly as
|
|
52
|
+
* the contact was — `crmScopeTokens`, the stamp every CRM creator uses —
|
|
53
|
+
* with the count starting at one. Public mailbox domains never reach this
|
|
54
|
+
* branch, because `companyDomainForEmail` answers `null` for them: a
|
|
55
|
+
* workspace's consumer list is not a list of accounts.
|
|
56
|
+
*
|
|
57
|
+
* ## The same posture as the capture
|
|
58
|
+
*
|
|
59
|
+
* Never throws. The capture has already succeeded and a form submission
|
|
60
|
+
* must not fail because the CRM could not file its author; a failure is
|
|
61
|
+
* logged and answered as `none`, and the person is exactly as capturable
|
|
62
|
+
* by hand as they were before this existed.
|
|
63
|
+
*/ export async function associateCompanyByDomain(created) {
|
|
64
|
+
const domain = companyDomainForEmail(created.email);
|
|
65
|
+
if (!domain) return {
|
|
66
|
+
outcome: 'none',
|
|
67
|
+
reason: 'no-domain'
|
|
68
|
+
};
|
|
69
|
+
try {
|
|
70
|
+
const resolved = await getOrgForHost(created.hostId);
|
|
71
|
+
if (!resolved) return {
|
|
72
|
+
outcome: 'none',
|
|
73
|
+
reason: 'no-org'
|
|
74
|
+
};
|
|
75
|
+
const org = resolved.org;
|
|
76
|
+
const group = await consentGroupForSite(created.hostId, org);
|
|
77
|
+
const firestore = firebaseAdmin.app().firestore();
|
|
78
|
+
const orgRef = firestore.collection('orgs').doc(resolved.orgId);
|
|
79
|
+
const companiesRef = orgRef.collection(CRM_COLLECTIONS.companies);
|
|
80
|
+
const contactRef = orgRef.collection('contacts').doc(created.contactId);
|
|
81
|
+
const matches = await companiesRef.where('domain', '==', domain).where('visibleTo', 'array-contains-any', crmReadTokens(group)).limit(2).get();
|
|
82
|
+
if (matches.size > 1) return {
|
|
83
|
+
outcome: 'none',
|
|
84
|
+
reason: 'ambiguous'
|
|
85
|
+
};
|
|
86
|
+
if (matches.size === 1) {
|
|
87
|
+
const companyId = matches.docs[0].id;
|
|
88
|
+
await writeContactCompanyLink({
|
|
89
|
+
firestore,
|
|
90
|
+
contactRef,
|
|
91
|
+
contact: null,
|
|
92
|
+
companiesRef,
|
|
93
|
+
groupId: group.groupId,
|
|
94
|
+
companyId
|
|
95
|
+
});
|
|
96
|
+
return {
|
|
97
|
+
outcome: 'linked',
|
|
98
|
+
companyId
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
if (!orgAutoCreatesCompanies(org)) {
|
|
102
|
+
return {
|
|
103
|
+
outcome: 'none',
|
|
104
|
+
reason: 'no-match'
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
/*
|
|
108
|
+
* The company and the link in one commit, so a company never exists
|
|
109
|
+
* with a count of one and no contact naming it — or the reverse. The
|
|
110
|
+
* count is written as a value rather than an increment because the
|
|
111
|
+
* document is being created in the same batch, and the plan's `+1` is
|
|
112
|
+
* exactly what a fresh company with one contact stores.
|
|
113
|
+
*/ const plan = planContactCompanyLink({
|
|
114
|
+
companyId: null,
|
|
115
|
+
companyIds: [],
|
|
116
|
+
heldElsewhere: []
|
|
117
|
+
}, companiesRef.doc().id);
|
|
118
|
+
if (!(plan == null ? void 0 : plan.companyId)) return {
|
|
119
|
+
outcome: 'none',
|
|
120
|
+
reason: 'failed'
|
|
121
|
+
};
|
|
122
|
+
const companyRef = companiesRef.doc(plan.companyId);
|
|
123
|
+
const batch = firestore.batch();
|
|
124
|
+
batch.set(companyRef, _extends({}, nameSearchFields(companyNameForDomain(domain)), {
|
|
125
|
+
domain,
|
|
126
|
+
visibleTo: crmScopeTokens(org, group),
|
|
127
|
+
hostId: created.hostId,
|
|
128
|
+
[COMPANY_CONTACTS_COUNT_FIELD]: 1,
|
|
129
|
+
createdAt: FieldValue.serverTimestamp(),
|
|
130
|
+
updatedAt: FieldValue.serverTimestamp()
|
|
131
|
+
}));
|
|
132
|
+
batch.update(contactRef, _extends({}, contactCompanyLinkFields(plan, group.groupId), {
|
|
133
|
+
updatedAt: FieldValue.serverTimestamp()
|
|
134
|
+
}));
|
|
135
|
+
await batch.commit();
|
|
136
|
+
return {
|
|
137
|
+
outcome: 'created',
|
|
138
|
+
companyId: plan.companyId
|
|
139
|
+
};
|
|
140
|
+
} catch (error) {
|
|
141
|
+
console.error('associateCompanyByDomain failed', created.hostId, created.contactId, error);
|
|
142
|
+
return {
|
|
143
|
+
outcome: 'none',
|
|
144
|
+
reason: 'failed'
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
export default associateCompanyByDomain;
|
|
149
|
+
|
|
150
|
+
//# sourceMappingURL=associate-company-by-domain.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../libs/tenant/runtime/src/lib/associate-company-by-domain.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n companyDomainForEmail,\n companyNameForDomain,\n CRM_COLLECTIONS,\n crmReadTokens,\n crmScopeTokens,\n nameSearchFields,\n orgAutoCreatesCompanies,\n} from '@aglyn/aglyn/server'\nimport {\n consentGroupForSite,\n contactCompanyLinkFields,\n firebaseAdmin,\n getOrgForHost,\n type HostContactCreated,\n writeContactCompanyLink,\n} from '@aglyn/tenant-data-admin'\nimport { COMPANY_CONTACTS_COUNT_FIELD, planContactCompanyLink } from '@aglyn/aglyn/server'\nimport { FieldValue } from 'firebase-admin/firestore'\n\n/** What the association did, for the spec and the log. */\nexport type CompanyAssociation =\n | { outcome: 'linked' | 'created'; companyId: string }\n | {\n outcome: 'none'\n reason: 'no-domain' | 'no-org' | 'ambiguous' | 'no-match' | 'failed'\n }\n\n/**\n * Link a NEW contact to the company its email domain names (AGL-2613).\n *\n * HubSpot associates a contact with a company on domain the moment the\n * contact exists, and that is the moment this runs: once, from the capture\n * door, for a person the org did not hold before. A repeat visit by somebody\n * already held is not a new person and re-asks nothing; a door that already\n * knew the company — the console's create drawer, an import row naming one\n * — has said so in the facet and this is not called at all.\n *\n * ## Bounded: one query, one commit\n *\n * The query is `companies where domain == d and visibleTo array-contains-any\n * (the capturing scope's read tokens)`, capped at two — the `(visibleTo\n * CONTAINS, domain ASC)` composite serves it. Two rather than one because\n * the answer has to be \"exactly one\": two companies at one domain visible\n * to this site is an ambiguity a capture must not resolve by picking the\n * first, and the contact waits for a person. The scope predicate is the\n * console's own listener predicate, so a company this site could not open\n * is not a match — an agency's client at `acme.com` is not linked to a\n * sibling client's Acme.\n *\n * The write is one batch: the facet and the mirror on the contact, and the\n * count on the company. The contact is fresh, so the plan is the trivial\n * one — no previous link, nothing held elsewhere — and needs no read of the\n * document.\n *\n * ## Creating is a setting, and off by default\n *\n * With `crm.autoCreateCompanies` on the org document, a domain no visible\n * company carries becomes one, named after the domain and scoped exactly as\n * the contact was — `crmScopeTokens`, the stamp every CRM creator uses —\n * with the count starting at one. Public mailbox domains never reach this\n * branch, because `companyDomainForEmail` answers `null` for them: a\n * workspace's consumer list is not a list of accounts.\n *\n * ## The same posture as the capture\n *\n * Never throws. The capture has already succeeded and a form submission\n * must not fail because the CRM could not file its author; a failure is\n * logged and answered as `none`, and the person is exactly as capturable\n * by hand as they were before this existed.\n */\nexport async function associateCompanyByDomain(\n created: Pick<HostContactCreated, 'hostId' | 'contactId' | 'email'>,\n): Promise<CompanyAssociation> {\n const domain = companyDomainForEmail(created.email)\n if (!domain) return { outcome: 'none', reason: 'no-domain' }\n try {\n const resolved = await getOrgForHost(created.hostId)\n if (!resolved) return { outcome: 'none', reason: 'no-org' }\n const org = resolved.org as Record<string, unknown>\n const group = await consentGroupForSite(created.hostId, org)\n const firestore = firebaseAdmin.app().firestore()\n const orgRef = firestore.collection('orgs').doc(resolved.orgId)\n const companiesRef = orgRef.collection(CRM_COLLECTIONS.companies)\n const contactRef = orgRef.collection('contacts').doc(created.contactId)\n\n const matches = await companiesRef\n .where('domain', '==', domain)\n .where('visibleTo', 'array-contains-any', crmReadTokens(group))\n .limit(2)\n .get()\n if (matches.size > 1) return { outcome: 'none', reason: 'ambiguous' }\n if (matches.size === 1) {\n const companyId = matches.docs[0].id\n await writeContactCompanyLink({\n firestore,\n contactRef,\n contact: null,\n companiesRef,\n groupId: group.groupId,\n companyId,\n })\n return { outcome: 'linked', companyId }\n }\n if (!orgAutoCreatesCompanies(org)) {\n return { outcome: 'none', reason: 'no-match' }\n }\n\n /*\n * The company and the link in one commit, so a company never exists\n * with a count of one and no contact naming it — or the reverse. The\n * count is written as a value rather than an increment because the\n * document is being created in the same batch, and the plan's `+1` is\n * exactly what a fresh company with one contact stores.\n */\n const plan = planContactCompanyLink(\n { companyId: null, companyIds: [], heldElsewhere: [] },\n companiesRef.doc().id,\n )\n if (!plan?.companyId) return { outcome: 'none', reason: 'failed' }\n const companyRef = companiesRef.doc(plan.companyId)\n const batch = firestore.batch()\n batch.set(companyRef, {\n ...nameSearchFields(companyNameForDomain(domain)),\n domain,\n visibleTo: crmScopeTokens(org, group),\n hostId: created.hostId,\n [COMPANY_CONTACTS_COUNT_FIELD]: 1,\n createdAt: FieldValue.serverTimestamp(),\n updatedAt: FieldValue.serverTimestamp(),\n })\n batch.update(contactRef, {\n ...contactCompanyLinkFields(plan, group.groupId),\n updatedAt: FieldValue.serverTimestamp(),\n })\n await batch.commit()\n return { outcome: 'created', companyId: plan.companyId }\n } catch (error) {\n console.error(\n 'associateCompanyByDomain failed',\n created.hostId,\n created.contactId,\n error,\n )\n return { outcome: 'none', reason: 'failed' }\n }\n}\n\nexport default associateCompanyByDomain\n"],"names":["companyDomainForEmail","companyNameForDomain","CRM_COLLECTIONS","crmReadTokens","crmScopeTokens","nameSearchFields","orgAutoCreatesCompanies","consentGroupForSite","contactCompanyLinkFields","firebaseAdmin","getOrgForHost","writeContactCompanyLink","COMPANY_CONTACTS_COUNT_FIELD","planContactCompanyLink","FieldValue","associateCompanyByDomain","created","domain","email","outcome","reason","resolved","hostId","org","group","firestore","app","orgRef","collection","doc","orgId","companiesRef","companies","contactRef","contactId","matches","where","limit","get","size","companyId","docs","id","contact","groupId","plan","companyIds","heldElsewhere","companyRef","batch","set","visibleTo","createdAt","serverTimestamp","updatedAt","update","commit","error","console"],"mappings":"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"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/
|
|
17
|
+
import * as Aglyn from '@aglyn/aglyn/server';
|
|
18
|
+
type NodesMap = Record<string, Aglyn.AglynNodeSchema>;
|
|
19
|
+
export interface AuthorPageNodesOptions {
|
|
20
|
+
/** The addressed segment, for building this page's own pager links. */
|
|
21
|
+
slug: string;
|
|
22
|
+
/** The byline — the record's name, or the raw segment for an unknown one. */
|
|
23
|
+
name: string;
|
|
24
|
+
/** The record, when the slug named one. */
|
|
25
|
+
author?: Aglyn.ContentAuthorRecord | null;
|
|
26
|
+
hasEntries: boolean;
|
|
27
|
+
page: number;
|
|
28
|
+
perPage: number;
|
|
29
|
+
totalPages: number;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* The author page's node tree, root first. Grafted into a layout's slot by
|
|
33
|
+
* `composeNodesWithChrome`, exactly like a screen's own nodes.
|
|
34
|
+
*/
|
|
35
|
+
export declare function buildAuthorPageNodes(options: AuthorPageNodesOptions): NodesMap;
|
|
36
|
+
export default buildAuthorPageNodes;
|
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
2
|
+
/**
|
|
3
|
+
* @license
|
|
4
|
+
* Copyright 2026 Aglyn LLC
|
|
5
|
+
*
|
|
6
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
7
|
+
* you may not use this file except in compliance with the License.
|
|
8
|
+
* You may obtain a copy of the License at
|
|
9
|
+
*
|
|
10
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
11
|
+
*
|
|
12
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
13
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
14
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
15
|
+
* See the License for the specific language governing permissions and
|
|
16
|
+
* limitations under the License.
|
|
17
|
+
*/ import * as Aglyn from "@aglyn/aglyn/server";
|
|
18
|
+
/**
|
|
19
|
+
* The built-in author page as canvas nodes (AGL-2518).
|
|
20
|
+
*
|
|
21
|
+
* A designated template screen is the intended way to build this page, and
|
|
22
|
+
* this is what renders when a site has not designated one — which is every
|
|
23
|
+
* site on the day the feature ships, and every site that never designates one.
|
|
24
|
+
* It cannot be optional: bylines link here, so the address has to answer.
|
|
25
|
+
*
|
|
26
|
+
* The `collection-fallback-nodes.ts` precedent (AGL-551), down to the
|
|
27
|
+
* synthetic id prefix — building the body as NODES rather than as React is
|
|
28
|
+
* what lets it sit inside the site's shared layout, on the same render path
|
|
29
|
+
* as everything else, instead of on a second chrome-less one.
|
|
30
|
+
*
|
|
31
|
+
* Ids are synthetic and NEVER persisted; the `apx__` prefix keeps them out of
|
|
32
|
+
* every id space an author's document uses.
|
|
33
|
+
*/ const ID_PREFIX = 'apx__';
|
|
34
|
+
const id = (suffix)=>`${ID_PREFIX}${suffix}`;
|
|
35
|
+
/**
|
|
36
|
+
* The author page's node tree, root first. Grafted into a layout's slot by
|
|
37
|
+
* `composeNodesWithChrome`, exactly like a screen's own nodes.
|
|
38
|
+
*/ export function buildAuthorPageNodes(options) {
|
|
39
|
+
var _author_links;
|
|
40
|
+
const { slug, name, author, hasEntries, page, perPage, totalPages } = options;
|
|
41
|
+
const nodes = {};
|
|
42
|
+
const stackChildren = [];
|
|
43
|
+
const push = (nodeId, parentId, node)=>{
|
|
44
|
+
nodes[nodeId] = _extends({
|
|
45
|
+
$id: nodeId,
|
|
46
|
+
parentId
|
|
47
|
+
}, node);
|
|
48
|
+
stackChildren.push(nodeId);
|
|
49
|
+
return nodeId;
|
|
50
|
+
};
|
|
51
|
+
nodes[Aglyn.NODE_ROOT_ID] = {
|
|
52
|
+
$id: Aglyn.NODE_ROOT_ID,
|
|
53
|
+
componentId: 'div',
|
|
54
|
+
nodes: [
|
|
55
|
+
id('container')
|
|
56
|
+
]
|
|
57
|
+
};
|
|
58
|
+
nodes[id('container')] = {
|
|
59
|
+
$id: id('container'),
|
|
60
|
+
parentId: Aglyn.NODE_ROOT_ID,
|
|
61
|
+
componentId: 'muiContainer',
|
|
62
|
+
pluginId: 'mui',
|
|
63
|
+
// `md`, as a collection article takes: this page is a profile above a
|
|
64
|
+
// reading list, not a dense grid.
|
|
65
|
+
props: {
|
|
66
|
+
maxWidth: 'md',
|
|
67
|
+
sx: {
|
|
68
|
+
paddingTop: 6,
|
|
69
|
+
paddingBottom: 8
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
nodes: [
|
|
73
|
+
id('stack')
|
|
74
|
+
]
|
|
75
|
+
};
|
|
76
|
+
nodes[id('stack')] = {
|
|
77
|
+
$id: id('stack'),
|
|
78
|
+
parentId: id('container'),
|
|
79
|
+
componentId: 'muiStack',
|
|
80
|
+
pluginId: 'mui',
|
|
81
|
+
props: {
|
|
82
|
+
spacing: 5
|
|
83
|
+
},
|
|
84
|
+
nodes: stackChildren
|
|
85
|
+
};
|
|
86
|
+
/*
|
|
87
|
+
The profile. Props are set from the record HERE rather than left to
|
|
88
|
+
`expandContentAuthorProfile`, because this body is built with the record
|
|
89
|
+
already in hand — the expansion exists for a designed template, where the
|
|
90
|
+
block was placed long before any author was routed through it.
|
|
91
|
+
|
|
92
|
+
`name` falls back to the raw segment so an unknown slug still renders a
|
|
93
|
+
page with a heading on it, which is the same "empty archive, not a crash"
|
|
94
|
+
the category route settled on.
|
|
95
|
+
*/ push(id('profile'), id('stack'), {
|
|
96
|
+
componentId: Aglyn.CONTENT_AUTHOR_PROFILE_COMPONENT_ID,
|
|
97
|
+
pluginId: 'mui',
|
|
98
|
+
props: _extends({
|
|
99
|
+
name: name || (author == null ? void 0 : author.name) || ''
|
|
100
|
+
}, (author == null ? void 0 : author.bio) ? {
|
|
101
|
+
bio: author.bio
|
|
102
|
+
} : {}, (author == null ? void 0 : author.image) ? {
|
|
103
|
+
image: author.image
|
|
104
|
+
} : {}, (author == null ? void 0 : author.jobTitle) ? {
|
|
105
|
+
jobTitle: author.jobTitle
|
|
106
|
+
} : {}, (author == null ? void 0 : author.worksFor) ? {
|
|
107
|
+
worksFor: author.worksFor
|
|
108
|
+
} : {}, (author == null ? void 0 : author.url) ? {
|
|
109
|
+
url: author.url
|
|
110
|
+
} : {}, (author == null ? void 0 : (_author_links = author.links) == null ? void 0 : _author_links.length) ? {
|
|
111
|
+
links: author.links
|
|
112
|
+
} : {})
|
|
113
|
+
});
|
|
114
|
+
if (!hasEntries) {
|
|
115
|
+
push(id('empty'), id('stack'), {
|
|
116
|
+
componentId: 'muiTypography',
|
|
117
|
+
pluginId: 'mui',
|
|
118
|
+
props: {
|
|
119
|
+
variant: 'body1',
|
|
120
|
+
// Named, not "This author has published nothing" — the page is about
|
|
121
|
+
// a person, and the sentence should read like one.
|
|
122
|
+
children: `${name} hasn’t published anything yet.`,
|
|
123
|
+
sx: {
|
|
124
|
+
color: 'text.secondary'
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
return nodes;
|
|
129
|
+
}
|
|
130
|
+
push(id('heading'), id('stack'), {
|
|
131
|
+
componentId: 'muiTypography',
|
|
132
|
+
pluginId: 'mui',
|
|
133
|
+
props: {
|
|
134
|
+
variant: 'h5',
|
|
135
|
+
// `h2`: the Author Profile block above owns the page's `h1`.
|
|
136
|
+
component: 'h2',
|
|
137
|
+
children: `Posts by ${name}`
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
/*
|
|
141
|
+
The entries block, over the author's OWN entries.
|
|
142
|
+
|
|
143
|
+
`page`/`perPage` are pinned rather than inherited, because the routed
|
|
144
|
+
"collection" here is synthetic (`AUTHOR_ENTRIES_SOURCE_SLUG`) and the
|
|
145
|
+
window the loader already computed is the one this page is paging.
|
|
146
|
+
|
|
147
|
+
Each card labels which section its post came from — the one thing a
|
|
148
|
+
single-collection listing never has to say and a cross-collection one
|
|
149
|
+
always does. `{{entry.collection}}` is empty on every other listing, so
|
|
150
|
+
the same card template is correct everywhere.
|
|
151
|
+
*/ push(id('entries'), id('stack'), {
|
|
152
|
+
componentId: Aglyn.COLLECTION_ENTRIES_COMPONENT_ID,
|
|
153
|
+
pluginId: 'mui',
|
|
154
|
+
props: {
|
|
155
|
+
spacing: 4,
|
|
156
|
+
perPage,
|
|
157
|
+
page
|
|
158
|
+
},
|
|
159
|
+
nodes: [
|
|
160
|
+
id('item')
|
|
161
|
+
]
|
|
162
|
+
});
|
|
163
|
+
nodes[id('item')] = {
|
|
164
|
+
$id: id('item'),
|
|
165
|
+
parentId: id('entries'),
|
|
166
|
+
componentId: 'muiStack',
|
|
167
|
+
pluginId: 'mui',
|
|
168
|
+
props: {
|
|
169
|
+
spacing: 0.5
|
|
170
|
+
},
|
|
171
|
+
nodes: [
|
|
172
|
+
id('item-collection'),
|
|
173
|
+
id('item-title'),
|
|
174
|
+
id('item-date'),
|
|
175
|
+
id('item-excerpt'),
|
|
176
|
+
id('item-link')
|
|
177
|
+
]
|
|
178
|
+
};
|
|
179
|
+
const item = (suffix, props, componentId = 'muiTypography')=>{
|
|
180
|
+
nodes[id(suffix)] = {
|
|
181
|
+
$id: id(suffix),
|
|
182
|
+
parentId: id('item'),
|
|
183
|
+
componentId,
|
|
184
|
+
pluginId: 'mui',
|
|
185
|
+
props
|
|
186
|
+
};
|
|
187
|
+
};
|
|
188
|
+
/*
|
|
189
|
+
The collection and the date are two NODES, not one string joined by a
|
|
190
|
+
middle dot.
|
|
191
|
+
|
|
192
|
+
A `·` written into the template is a literal, and a token beside it is
|
|
193
|
+
not: an entry that is live with no `publishedAt` — a legacy post, or one
|
|
194
|
+
flipped due without a timestamp — would render `Blog · ` with the
|
|
195
|
+
separator dangling off the end. The joined form is right where the values
|
|
196
|
+
are computed together and the empty ones can be dropped (Entry Meta does
|
|
197
|
+
exactly that); it is wrong in a template, which has no conditional and
|
|
198
|
+
substitutes each token independently.
|
|
199
|
+
|
|
200
|
+
Stacked rather than in a row for the same reason, and it puts the card on
|
|
201
|
+
the collection fallback's own shape (AGL-551) — one extra line above it,
|
|
202
|
+
nothing else different.
|
|
203
|
+
*/ item('item-collection', {
|
|
204
|
+
variant: 'overline',
|
|
205
|
+
children: '{{entry.collection}}',
|
|
206
|
+
sx: {
|
|
207
|
+
color: 'text.secondary'
|
|
208
|
+
}
|
|
209
|
+
});
|
|
210
|
+
item('item-title', {
|
|
211
|
+
variant: 'h6',
|
|
212
|
+
component: 'h3',
|
|
213
|
+
children: '{{entry.title}}'
|
|
214
|
+
});
|
|
215
|
+
item('item-date', {
|
|
216
|
+
variant: 'caption',
|
|
217
|
+
children: '{{entry.date}}',
|
|
218
|
+
sx: {
|
|
219
|
+
color: 'text.secondary'
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
item('item-excerpt', {
|
|
223
|
+
variant: 'body1',
|
|
224
|
+
children: '{{entry.excerpt}}'
|
|
225
|
+
});
|
|
226
|
+
item('item-link', {
|
|
227
|
+
href: '{{entry.url}}',
|
|
228
|
+
children: 'Read more',
|
|
229
|
+
size: 'small',
|
|
230
|
+
sx: {
|
|
231
|
+
alignSelf: 'flex-start'
|
|
232
|
+
}
|
|
233
|
+
}, 'muiScreenLink');
|
|
234
|
+
if (totalPages > 1) {
|
|
235
|
+
// Through the shared pager computation, so this fallback and the
|
|
236
|
+
// `{{pagination.*}}` tokens a designed template binds cannot disagree
|
|
237
|
+
// about which page they are on. An edge is the empty string, which is
|
|
238
|
+
// exactly the "no link here" this body already meant.
|
|
239
|
+
const pager = Aglyn.contentAuthorPaginationLinks(_extends({}, author ? {
|
|
240
|
+
author
|
|
241
|
+
} : {
|
|
242
|
+
authorName: slug
|
|
243
|
+
}, {
|
|
244
|
+
page,
|
|
245
|
+
totalPages
|
|
246
|
+
}));
|
|
247
|
+
const pagerChildren = [];
|
|
248
|
+
const pagerLink = (suffix, href, label)=>{
|
|
249
|
+
nodes[id(suffix)] = {
|
|
250
|
+
$id: id(suffix),
|
|
251
|
+
parentId: id('pager'),
|
|
252
|
+
componentId: 'muiScreenLink',
|
|
253
|
+
pluginId: 'mui',
|
|
254
|
+
props: {
|
|
255
|
+
href,
|
|
256
|
+
children: label
|
|
257
|
+
}
|
|
258
|
+
};
|
|
259
|
+
pagerChildren.push(id(suffix));
|
|
260
|
+
};
|
|
261
|
+
if (pager.prevUrl) pagerLink('prev', pager.prevUrl, '← Newer');
|
|
262
|
+
nodes[id('pageinfo')] = {
|
|
263
|
+
$id: id('pageinfo'),
|
|
264
|
+
parentId: id('pager'),
|
|
265
|
+
componentId: 'muiTypography',
|
|
266
|
+
pluginId: 'mui',
|
|
267
|
+
props: {
|
|
268
|
+
variant: 'body2',
|
|
269
|
+
children: `Page ${pager.page} of ${pager.totalPages}`,
|
|
270
|
+
sx: {
|
|
271
|
+
color: 'text.secondary'
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
};
|
|
275
|
+
pagerChildren.push(id('pageinfo'));
|
|
276
|
+
if (pager.nextUrl) pagerLink('next', pager.nextUrl, 'Older →');
|
|
277
|
+
push(id('pager'), id('stack'), {
|
|
278
|
+
componentId: 'muiStack',
|
|
279
|
+
pluginId: 'mui',
|
|
280
|
+
props: {
|
|
281
|
+
direction: 'row',
|
|
282
|
+
spacing: 3,
|
|
283
|
+
sx: {
|
|
284
|
+
alignItems: 'center',
|
|
285
|
+
justifyContent: 'center',
|
|
286
|
+
paddingTop: 2
|
|
287
|
+
}
|
|
288
|
+
},
|
|
289
|
+
nodes: pagerChildren
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
return nodes;
|
|
293
|
+
}
|
|
294
|
+
export default buildAuthorPageNodes;
|
|
295
|
+
|
|
296
|
+
//# sourceMappingURL=author-page-nodes.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../libs/tenant/runtime/src/lib/author-page-nodes.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport * as Aglyn from '@aglyn/aglyn/server'\n\n/**\n * The built-in author page as canvas nodes (AGL-2518).\n *\n * A designated template screen is the intended way to build this page, and\n * this is what renders when a site has not designated one — which is every\n * site on the day the feature ships, and every site that never designates one.\n * It cannot be optional: bylines link here, so the address has to answer.\n *\n * The `collection-fallback-nodes.ts` precedent (AGL-551), down to the\n * synthetic id prefix — building the body as NODES rather than as React is\n * what lets it sit inside the site's shared layout, on the same render path\n * as everything else, instead of on a second chrome-less one.\n *\n * Ids are synthetic and NEVER persisted; the `apx__` prefix keeps them out of\n * every id space an author's document uses.\n */\n\nconst ID_PREFIX = 'apx__'\nconst id = (suffix: string) => `${ID_PREFIX}${suffix}`\n\ntype NodesMap = Record<string, Aglyn.AglynNodeSchema>\n\nexport interface AuthorPageNodesOptions {\n /** The addressed segment, for building this page's own pager links. */\n slug: string\n /** The byline — the record's name, or the raw segment for an unknown one. */\n name: string\n /** The record, when the slug named one. */\n author?: Aglyn.ContentAuthorRecord | null\n hasEntries: boolean\n page: number\n perPage: number\n totalPages: number\n}\n\n/**\n * The author page's node tree, root first. Grafted into a layout's slot by\n * `composeNodesWithChrome`, exactly like a screen's own nodes.\n */\nexport function buildAuthorPageNodes(\n options: AuthorPageNodesOptions,\n): NodesMap {\n const { slug, name, author, hasEntries, page, perPage, totalPages } = options\n const nodes: NodesMap = {}\n const stackChildren: string[] = []\n\n const push = (\n nodeId: string,\n parentId: string,\n node: Omit<Aglyn.AglynNodeSchema, '$id' | 'parentId'>,\n ) => {\n nodes[nodeId] = { $id: nodeId, parentId, ...node } as Aglyn.AglynNodeSchema\n stackChildren.push(nodeId)\n return nodeId\n }\n\n nodes[Aglyn.NODE_ROOT_ID] = {\n $id: Aglyn.NODE_ROOT_ID,\n componentId: 'div',\n nodes: [id('container')],\n } as Aglyn.AglynNodeSchema\n nodes[id('container')] = {\n $id: id('container'),\n parentId: Aglyn.NODE_ROOT_ID,\n componentId: 'muiContainer',\n pluginId: 'mui',\n // `md`, as a collection article takes: this page is a profile above a\n // reading list, not a dense grid.\n props: { maxWidth: 'md', sx: { paddingTop: 6, paddingBottom: 8 } },\n nodes: [id('stack')],\n } as Aglyn.AglynNodeSchema\n nodes[id('stack')] = {\n $id: id('stack'),\n parentId: id('container'),\n componentId: 'muiStack',\n pluginId: 'mui',\n props: { spacing: 5 },\n nodes: stackChildren,\n } as Aglyn.AglynNodeSchema\n\n /*\n The profile. Props are set from the record HERE rather than left to\n `expandContentAuthorProfile`, because this body is built with the record\n already in hand — the expansion exists for a designed template, where the\n block was placed long before any author was routed through it.\n\n `name` falls back to the raw segment so an unknown slug still renders a\n page with a heading on it, which is the same \"empty archive, not a crash\"\n the category route settled on.\n */\n push(id('profile'), id('stack'), {\n componentId: Aglyn.CONTENT_AUTHOR_PROFILE_COMPONENT_ID,\n pluginId: 'mui',\n props: {\n name: name || author?.name || '',\n ...(author?.bio ? { bio: author.bio } : {}),\n ...(author?.image ? { image: author.image } : {}),\n ...(author?.jobTitle ? { jobTitle: author.jobTitle } : {}),\n ...(author?.worksFor ? { worksFor: author.worksFor } : {}),\n ...(author?.url ? { url: author.url } : {}),\n ...(author?.links?.length ? { links: author.links } : {}),\n },\n })\n\n if (!hasEntries) {\n push(id('empty'), id('stack'), {\n componentId: 'muiTypography',\n pluginId: 'mui',\n props: {\n variant: 'body1',\n // Named, not \"This author has published nothing\" — the page is about\n // a person, and the sentence should read like one.\n children: `${name} hasn’t published anything yet.`,\n sx: { color: 'text.secondary' },\n },\n })\n return nodes\n }\n\n push(id('heading'), id('stack'), {\n componentId: 'muiTypography',\n pluginId: 'mui',\n props: {\n variant: 'h5',\n // `h2`: the Author Profile block above owns the page's `h1`.\n component: 'h2',\n children: `Posts by ${name}`,\n },\n })\n\n /*\n The entries block, over the author's OWN entries.\n\n `page`/`perPage` are pinned rather than inherited, because the routed\n \"collection\" here is synthetic (`AUTHOR_ENTRIES_SOURCE_SLUG`) and the\n window the loader already computed is the one this page is paging.\n\n Each card labels which section its post came from — the one thing a\n single-collection listing never has to say and a cross-collection one\n always does. `{{entry.collection}}` is empty on every other listing, so\n the same card template is correct everywhere.\n */\n push(id('entries'), id('stack'), {\n componentId: Aglyn.COLLECTION_ENTRIES_COMPONENT_ID,\n pluginId: 'mui',\n props: { spacing: 4, perPage, page },\n nodes: [id('item')],\n })\n nodes[id('item')] = {\n $id: id('item'),\n parentId: id('entries'),\n componentId: 'muiStack',\n pluginId: 'mui',\n props: { spacing: 0.5 },\n nodes: [\n id('item-collection'),\n id('item-title'),\n id('item-date'),\n id('item-excerpt'),\n id('item-link'),\n ],\n } as Aglyn.AglynNodeSchema\n const item = (\n suffix: string,\n props: Record<string, unknown>,\n componentId = 'muiTypography',\n ) => {\n nodes[id(suffix)] = {\n $id: id(suffix),\n parentId: id('item'),\n componentId,\n pluginId: 'mui',\n props,\n } as Aglyn.AglynNodeSchema\n }\n /*\n The collection and the date are two NODES, not one string joined by a\n middle dot.\n\n A `·` written into the template is a literal, and a token beside it is\n not: an entry that is live with no `publishedAt` — a legacy post, or one\n flipped due without a timestamp — would render `Blog · ` with the\n separator dangling off the end. The joined form is right where the values\n are computed together and the empty ones can be dropped (Entry Meta does\n exactly that); it is wrong in a template, which has no conditional and\n substitutes each token independently.\n\n Stacked rather than in a row for the same reason, and it puts the card on\n the collection fallback's own shape (AGL-551) — one extra line above it,\n nothing else different.\n */\n item('item-collection', {\n variant: 'overline',\n children: '{{entry.collection}}',\n sx: { color: 'text.secondary' },\n })\n item('item-title', {\n variant: 'h6',\n component: 'h3',\n children: '{{entry.title}}',\n })\n item('item-date', {\n variant: 'caption',\n children: '{{entry.date}}',\n sx: { color: 'text.secondary' },\n })\n item('item-excerpt', { variant: 'body1', children: '{{entry.excerpt}}' })\n item(\n 'item-link',\n {\n href: '{{entry.url}}',\n children: 'Read more',\n size: 'small',\n sx: { alignSelf: 'flex-start' },\n },\n 'muiScreenLink',\n )\n\n if (totalPages > 1) {\n // Through the shared pager computation, so this fallback and the\n // `{{pagination.*}}` tokens a designed template binds cannot disagree\n // about which page they are on. An edge is the empty string, which is\n // exactly the \"no link here\" this body already meant.\n const pager = Aglyn.contentAuthorPaginationLinks({\n ...(author ? { author } : { authorName: slug }),\n page,\n totalPages,\n })\n const pagerChildren: string[] = []\n const pagerLink = (suffix: string, href: string, label: string) => {\n nodes[id(suffix)] = {\n $id: id(suffix),\n parentId: id('pager'),\n componentId: 'muiScreenLink',\n pluginId: 'mui',\n props: { href, children: label },\n } as Aglyn.AglynNodeSchema\n pagerChildren.push(id(suffix))\n }\n if (pager.prevUrl) pagerLink('prev', pager.prevUrl, '← Newer')\n nodes[id('pageinfo')] = {\n $id: id('pageinfo'),\n parentId: id('pager'),\n componentId: 'muiTypography',\n pluginId: 'mui',\n props: {\n variant: 'body2',\n children: `Page ${pager.page} of ${pager.totalPages}`,\n sx: { color: 'text.secondary' },\n },\n } as Aglyn.AglynNodeSchema\n pagerChildren.push(id('pageinfo'))\n if (pager.nextUrl) pagerLink('next', pager.nextUrl, 'Older →')\n push(id('pager'), id('stack'), {\n componentId: 'muiStack',\n pluginId: 'mui',\n props: {\n direction: 'row',\n spacing: 3,\n sx: { alignItems: 'center', justifyContent: 'center', paddingTop: 2 },\n },\n nodes: pagerChildren,\n })\n }\n\n return nodes\n}\n\nexport default buildAuthorPageNodes\n"],"names":["Aglyn","ID_PREFIX","id","suffix","buildAuthorPageNodes","options","author","slug","name","hasEntries","page","perPage","totalPages","nodes","stackChildren","push","nodeId","parentId","node","$id","NODE_ROOT_ID","componentId","pluginId","props","maxWidth","sx","paddingTop","paddingBottom","spacing","CONTENT_AUTHOR_PROFILE_COMPONENT_ID","bio","image","jobTitle","worksFor","url","links","length","variant","children","color","component","COLLECTION_ENTRIES_COMPONENT_ID","item","href","size","alignSelf","pager","contentAuthorPaginationLinks","authorName","pagerChildren","pagerLink","label","prevUrl","nextUrl","direction","alignItems","justifyContent"],"mappings":"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"}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/
|
|
17
|
+
/**
|
|
18
|
+
* Which shared layout wraps the pages the platform builds rather than the
|
|
19
|
+
* author does (AGL-2513) — site search today, and the collection article a
|
|
20
|
+
* site with no entry template falls back to.
|
|
21
|
+
*
|
|
22
|
+
* ## Why a setting and not just the home screen's layout
|
|
23
|
+
*
|
|
24
|
+
* The home screen's layout was already the implicit answer for the collection
|
|
25
|
+
* fallback, and it is the right DEFAULT — a site with one layout needs to
|
|
26
|
+
* configure nothing, and every site has a home page. It is the wrong RULE.
|
|
27
|
+
* Plenty of sites give the home page a layout of its own: a transparent
|
|
28
|
+
* header over a hero, no breadcrumb, a fat marketing footer. Search results
|
|
29
|
+
* inheriting that is how `/search` ends up with a header designed to sit on
|
|
30
|
+
* top of an image it does not have.
|
|
31
|
+
*
|
|
32
|
+
* `host.builtInPageLayoutId` is the escape hatch, and it is a LAYOUT id
|
|
33
|
+
* rather than a screen id on purpose. What a built-in page needs is the
|
|
34
|
+
* site's chrome around a body the platform composes; designating a screen
|
|
35
|
+
* would mean designating a page whose content is ignored — a slot the author
|
|
36
|
+
* cannot fill and cannot see the shape of.
|
|
37
|
+
*
|
|
38
|
+
* Returns `undefined` when neither is available, and every caller treats that
|
|
39
|
+
* as "render the body with no chrome" rather than as a failure: a search page
|
|
40
|
+
* without a header is worse than one with, and a search page that 500s
|
|
41
|
+
* because a layout was deleted is worse than both.
|
|
42
|
+
*/
|
|
43
|
+
export declare const BUILT_IN_PAGE_LAYOUT_FIELD: "builtInPageLayoutId";
|
|
44
|
+
export declare function resolveBuiltInPageLayoutId(options: {
|
|
45
|
+
hostId: string;
|
|
46
|
+
host: any;
|
|
47
|
+
}): Promise<string | undefined>;
|
|
48
|
+
export default resolveBuiltInPageLayoutId;
|