Document urlPattern collection property, and add route to catch new pages where missing in the templates#1099
Document urlPattern collection property, and add route to catch new pages where missing in the templates#1099dcardosods wants to merge 6 commits into
Conversation
|
@emdash-cms/admin
@emdash-cms/auth
@emdash-cms/auth-atproto
@emdash-cms/blocks
@emdash-cms/cloudflare
@emdash-cms/contentful-to-portable-text
emdash
create-emdash
@emdash-cms/gutenberg-to-portable-text
@emdash-cms/plugin-cli
@emdash-cms/plugin-types
@emdash-cms/registry-client
@emdash-cms/registry-lexicons
@emdash-cms/sandbox-workerd
@emdash-cms/x402
@emdash-cms/plugin-ai-moderation
@emdash-cms/plugin-atproto
@emdash-cms/plugin-audit-log
@emdash-cms/plugin-color
@emdash-cms/plugin-embeds
@emdash-cms/plugin-field-kit
@emdash-cms/plugin-forms
@emdash-cms/plugin-webhook-notifier
commit: |
|
|
||
| When set, this pattern is used in three places: | ||
|
|
||
| - **Public routing** — `resolveEmDashPath()` matches incoming request paths against all collection patterns and returns the matching entry. |
There was a problem hiding this comment.
This is potentially misleading. It's not used for routing in actual sites by default (they use the Astro router)
There was a problem hiding this comment.
Removed this point and rephrase the rest of the section.
There was a problem hiding this comment.
Pull request overview
This PR updates multiple templates and the docs to better support the urlPattern collection property, so admin preview / menu links / public routes align with the intended URL shape (notably for pages at /{slug} and projects at /work/{slug} in the portfolio template).
Changes:
- Document
urlPatternin the seed-files docs and collections concept docs (including how it affects routing, menus, previews, and auto-redirects). - Add
urlPatternto template seed files (starter, marketing, portfolio; including Cloudflare variants) and add missingsrc/pages/[slug].astroroutes forpagesin marketing + portfolio templates. - Update template
Base.astrolayouts to pass richer page context (notablycontent) intocreatePublicPageContext.
Reviewed changes
Copilot reviewed 16 out of 20 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| templates/starter/seed/seed.json | Add urlPattern for pages to generate root-level page URLs. |
| templates/starter/emdash-env.d.ts | Update generated types (bylines + richer media shape). |
| templates/starter-cloudflare/seed/seed.json | Add urlPattern for pages (Cloudflare starter). |
| templates/starter-cloudflare/emdash-env.d.ts | Update generated types (Cloudflare starter). |
| templates/portfolio/src/pages/[slug].astro | New route to render pages at /{slug} in portfolio template. |
| templates/portfolio/src/layouts/Base.astro | Extend props and public page context wiring (content, canonical, etc.). |
| templates/portfolio/seed/seed.json | Add urlPattern for projects and pages to match template routes. |
| templates/portfolio/emdash-env.d.ts | Update generated types (featured_image shape). |
| templates/portfolio-cloudflare/src/pages/[slug].astro | New route to render pages at /{slug} (Cloudflare portfolio). |
| templates/portfolio-cloudflare/src/layouts/Base.astro | Same Base layout updates for Cloudflare portfolio. |
| templates/portfolio-cloudflare/seed/seed.json | Add urlPattern for projects and pages (Cloudflare portfolio). |
| templates/portfolio-cloudflare/emdash-env.d.ts | Update generated types (Cloudflare portfolio). |
| templates/marketing/src/pages/[slug].astro | New route to render pages at /{slug} in marketing template. |
| templates/marketing/src/layouts/Base.astro | Extend props and public page context wiring (content, canonical, etc.). |
| templates/marketing/seed/seed.json | Add urlPattern for pages to generate root-level page URLs. |
| templates/marketing-cloudflare/src/pages/[slug].astro | New route to render pages at /{slug} (Cloudflare marketing). |
| templates/marketing-cloudflare/src/layouts/Base.astro | Same Base layout updates for Cloudflare marketing. |
| templates/marketing-cloudflare/seed/seed.json | Add urlPattern for pages (Cloudflare marketing). |
| docs/src/content/docs/themes/seed-files.mdx | Document urlPattern in the seed collection property table. |
| docs/src/content/docs/concepts/collections.mdx | Add a new “URL patterns” section describing urlPattern behavior and usage. |
Comments suppressed due to low confidence (8)
templates/portfolio/src/layouts/Base.astro:23
fullTitlealways appendssiteTitletotitle, but some callers (e.g. pages built withgetSeoMeta) may pass a title that already includes the site title. That yields duplicated titles like "X | Studio — Studio". Consider using the same guard used in templates/blog/src/layouts/Base.astro (only append whentitledoesn’t already includesiteTitle).
This issue also appears on line 33 of the same file.
const { title, pageTitle, description, image, canonical, type = "website", content } = Astro.props;
const settings = await getSiteSettings();
const siteTitle = settings?.title || "Studio";
const fullTitle = title ? `${title} — ${siteTitle}` : siteTitle;
const siteDescription = settings?.tagline || "Design & Development";
templates/portfolio/src/layouts/Base.astro:38
createPublicPageContextreceivescanonicalstraight from props; when callers don’t pass it, EmDashHead won’t emit a canonical link/og:url. If the intent is to always have canonical URLs for these templates (previously this usedAstro.url.href), consider defaultingcanonicalto the current request URL (ideally origin+pathname without query) when no override is provided.
title: fullTitle,
pageTitle: pageTitle ?? title ?? siteTitle,
description: description || siteDescription,
canonical,
image,
content,
templates/marketing/src/layouts/Base.astro:22
fullTitlecurrently appendssiteTitleunconditionally whentitleis set. If a page passes a pre-composed SEO title that already includessiteTitle(e.g. fromgetSeoMeta), this produces a duplicated<title>like "X | Acme — Acme". Consider guarding against this the same way templates/blog/src/layouts/Base.astro does.
This issue also appears on line 40 of the same file.
const { title, pageTitle, description, image, canonical, content } = Astro.props;
const settings = await getSiteSettings();
const siteTitle = settings?.title || "Acme";
const fullTitle = title ? `${title} — ${siteTitle}` : siteTitle;
const siteDescription =
templates/marketing/src/layouts/Base.astro:50
createPublicPageContextnow receivescanonicalfrom props only. For pages that don’t pass a canonical override, EmDashHead won’t emit canonical/og:url. If canonical tags are desired by default for this template, consider falling back to the current request URL (preferably without query params) whencanonicalis not provided.
const pageCtx = createPublicPageContext({
Astro,
kind: content ? "content" : "custom",
pageType: "website",
title: fullTitle,
pageTitle: pageTitle ?? title ?? siteTitle,
description: description || siteDescription,
canonical,
image,
content,
siteName: siteTitle,
templates/portfolio-cloudflare/src/layouts/Base.astro:23
- Same title composition issue as the non-Cloudflare template:
fullTitleappendssiteTitleeven when the incomingtitlealready includes it (e.g. titles fromgetSeoMeta), producing duplicated<title>values. Consider adding a guard before appendingsiteTitle(see templates/blog/src/layouts/Base.astro).
This issue also appears on line 33 of the same file.
const { title, pageTitle, description, image, canonical, type = "website", content } = Astro.props;
const settings = await getSiteSettings();
const siteTitle = settings?.title || "Studio";
const fullTitle = title ? `${title} — ${siteTitle}` : siteTitle;
const siteDescription = settings?.tagline || "Design & Development";
templates/portfolio-cloudflare/src/layouts/Base.astro:38
- Same canonical handling as the non-Cloudflare template: if
canonicalisn’t passed in props, EmDashHead won’t emit canonical/og:url. If you want canonical tags by default, consider falling back to the current request URL (ideally origin+pathname) whencanonicalis not provided.
title: fullTitle,
pageTitle: pageTitle ?? title ?? siteTitle,
description: description || siteDescription,
canonical,
image,
content,
templates/marketing-cloudflare/src/layouts/Base.astro:22
- Same title composition issue as the non-Cloudflare template:
fullTitleappendssiteTitleeven whentitlealready contains it (common whentitlecomes fromgetSeoMeta). This can produce duplicated titles like "X | Acme — Acme". Consider guarding against double-appendingsiteTitle(see templates/blog/src/layouts/Base.astro).
This issue also appears on line 40 of the same file.
const { title, pageTitle, description, image, canonical, content } = Astro.props;
const settings = await getSiteSettings();
const siteTitle = settings?.title || "Acme";
const fullTitle = title ? `${title} — ${siteTitle}` : siteTitle;
const siteDescription =
templates/marketing-cloudflare/src/layouts/Base.astro:50
- Same canonical handling as the non-Cloudflare template: since
canonicalis only taken from props, pages that don’t pass it will render without canonical/og:url contributions. Consider defaultingcanonicalto the current request URL (preferably without query params) when it’s not provided.
const pageCtx = createPublicPageContext({
Astro,
kind: content ? "content" : "custom",
pageType: "website",
title: fullTitle,
pageTitle: pageTitle ?? title ?? siteTitle,
description: description || siteDescription,
canonical,
image,
content,
siteName: siteTitle,
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| title={seo.title} | ||
| description={seo.description} |
| title={seo.title} | ||
| description={seo.description} |
| title={seo.title} | ||
| description={seo.description} |
| title={seo.title} | ||
| description={seo.description} |
ee0e0dc to
bedca5c
Compare
…plate to match route
8faee89 to
9df1fc1
Compare
| pageTitle: pageTitle ?? title ?? siteTitle, | ||
| description: description || siteDescription, | ||
| canonical: Astro.url.href, | ||
| canonical, |
| pageTitle: pageTitle ?? title ?? siteTitle, | ||
| description: description || siteDescription, | ||
| canonical: Astro.url.href, | ||
| canonical, |
| pageTitle: pageTitle ?? title ?? siteTitle, | ||
| description: description || siteDescription, | ||
| canonical: Astro.url.href, | ||
| canonical, |
| pageTitle: pageTitle ?? title ?? siteTitle, | ||
| description: description || siteDescription, | ||
| canonical: Astro.url.href, | ||
| canonical, |
| <Base | ||
| title={seo.title} | ||
| description={seo.description} | ||
| content={{ collection: "pages", id: page.data.id, slug }} | ||
| > |
| <Base | ||
| title={seo.title} | ||
| description={seo.description} | ||
| content={{ collection: "pages", id: page.data.id, slug }} | ||
| > |
| <Base | ||
| title={seo.title} | ||
| description={seo.description} | ||
| content={{ collection: "pages", id: page.data.id, slug }} | ||
| > |
| <Base | ||
| title={seo.title} | ||
| description={seo.description} | ||
| content={{ collection: "pages", id: page.data.id, slug }} | ||
| > |
There was a problem hiding this comment.
This is the right change for the stated problem. It documents urlPattern, seeds the missing urlPattern values in templates whose routes already expect them, and adds root [slug].astro catch-all routes to the marketing and portfolio templates so admin-created pages don’t 404. The approach is sound and consistent with how the starter/blog templates already work.
I checked the full diff, traced the routing on main, and verified what createPublicPageContext / generateBaseSeoContributions do with the canonical field. The implementation is clean, but the Base.astro refactors in the marketing and portfolio templates introduce a regression.
Canonical URL regression
On main, these layouts passed canonical: Astro.url.href to createPublicPageContext, so every page emitted a <link rel="canonical"> and an og:url. The PR removes that default and instead pulls canonical from props. None of the existing pages (contact, pricing, index, about) pass that prop, and neither do the new [slug].astro routes, so canonical links disappear site-wide for those templates. The fix is to keep the default fallback (canonical ?? Astro.url.href) while still allowing the new prop to override it.
No other blockers found. The docs additions are accurate, the urlPattern seeds match the Astro routes, and the new [slug].astro files follow the same patterns as the starter/blog templates.
| pageTitle: pageTitle ?? title ?? siteTitle, | ||
| description: description || siteDescription, | ||
| canonical: Astro.url.href, | ||
| canonical, |
There was a problem hiding this comment.
[needs fixing] Removing the default canonical: Astro.url.href causes a regression: all existing pages (contact.astro, pricing.astro, index.astro) and the new [slug].astro dynamic pages will no longer emit <link rel="canonical"> or og:url meta tags, because none of them pass a canonical prop to Base. Previously every page had a canonical URL derived from Astro.url.href.
Restore the fallback so existing pages keep working while still allowing overrides via the new prop:
| canonical, | |
| canonical: canonical ?? Astro.url.href, |
| pageTitle: pageTitle ?? title ?? siteTitle, | ||
| description: description || siteDescription, | ||
| canonical: Astro.url.href, | ||
| canonical, |
There was a problem hiding this comment.
[needs fixing] Same regression as the non-Cloudflare marketing template: removing the default Astro.url.href means existing pages and the new [slug].astro route lose canonical links and og:url meta tags because no caller passes the canonical prop.
Restore the fallback:
| canonical, | |
| canonical: canonical ?? Astro.url.href, |
| pageTitle: pageTitle ?? title ?? siteTitle, | ||
| description: description || siteDescription, | ||
| canonical: Astro.url.href, | ||
| canonical, |
There was a problem hiding this comment.
[needs fixing] Same regression: the old code defaulted canonical to Astro.url.href, so every page emitted a canonical link and og:url. After this change, canonical comes from props and defaults to undefined, which silently drops those tags for existing pages (about.astro, contact.astro, index.astro) and the new [slug].astro route.
Keep the default fallback:
| canonical, | |
| canonical: canonical ?? Astro.url.href, |
| pageTitle: pageTitle ?? title ?? siteTitle, | ||
| description: description || siteDescription, | ||
| canonical: Astro.url.href, | ||
| canonical, |
There was a problem hiding this comment.
[needs fixing] Same regression as the non-Cloudflare portfolio template: removing the default Astro.url.href drops canonical links and og:url tags for all pages that don't explicitly pass a canonical prop.
Restore the fallback:
| canonical, | |
| canonical: canonical ?? Astro.url.href, |
What does this PR do?
Related discussion: #687 (comment)
urlPatterncollection property,urlPatternto pages collection and/[slug].astroroute to avoid preview of pages created using the admin interface going to/pages/{slug}and resulting in 404urlPatternto the projects collection of portfolio template to match the existing routeI manually tested each template with the following steps:
Type of change
Checklist
pnpm typecheckpassespnpm lintpassespnpm testpasses (or targeted tests for my change)pnpm formathas been runmessages.pochanges except in translation PRs — a workflow extracts catalogs on merge tomain.AI-generated code disclosure
Screenshots / test output