diff --git a/.cache/pages.json b/.cache/pages.json index 114e5de58..5076c0f14 100644 --- a/.cache/pages.json +++ b/.cache/pages.json @@ -88,6 +88,75 @@ }, "consent", "contact", + { + "deepDives": [ + "alert-fatigue-reduction-triage-actionable-alerts/pdf", + "api-deprecation-sunset-headers-consumer-migration/pdf", + "api-gateway-metrics-traces-logs-debugging/pdf", + "api-usage-metering-quotas-cost-attribution/pdf", + "argocd-sync-failures-gitops-debugging-troubleshooting/pdf", + "availability-targets-five-nines-cost-benefit-analysis/pdf", + "backpressure-load-shedding-admission-control-overload/pdf", + "blameless-postmortem-incident-analysis-systemic-causes/pdf", + "blue-green-canary-deployment-strategy-comparison/pdf", + "cdn-edge-caching-cache-keys-vary-headers/pdf", + "chaos-engineering-failure-injection-low-cost-experiments/pdf", + "ci-pipeline-caching-docker-layers-dependency-cache/pdf", + "circuit-breaker-retry-budget-cascade-failure-prevention/pdf", + "consumer-driven-contract-testing-pact-internal-apis/pdf", + "container-vulnerability-scanning-ci-shift-left-security/pdf", + "database-schema-migrations-continuous-deployment-zero-downtime/pdf", + "dead-letter-queue-design-replay-debugging/pdf", + "distributed-tracing-sampling-strategies-head-tail/pdf", + "eol-runtime-upgrade-dependency-hell-migration/pdf", + "ephemeral-preview-environments-cost-control-cleanup/pdf", + "flaky-test-diagnosis-race-conditions-e2e-stabilization/pdf", + "golden-paths-developer-experience-standardization-autonomy/pdf", + "grafana-dashboard-hygiene-pruning-actionable-metrics/pdf", + "helm-release-management-drift-detection-debugging/pdf", + "idempotent-message-handlers-deduplication-retries/pdf", + "internal-cli-kubectl-terraform-wrapper-abstraction/pdf", + "internal-developer-portal-platform-self-service-actions/pdf", + "internal-platform-api-versioning-deprecation-breaking-changes/pdf", + "kubernetes-cluster-upgrade-playbook-risk-reduction/pdf", + "kubernetes-cost-optimization-resource-sizing-spot-instances/pdf", + "kubernetes-decision-framework-when-not-to-use/pdf", + "kubernetes-dns-debugging-ndots-coredns-troubleshooting/pdf", + "kubernetes-hpa-autoscaling-metrics-tuning-latency/pdf", + "kubernetes-ingress-gateway-api-comparison-migration/pdf", + "kubernetes-multi-cluster-fleet-management-configuration/pdf", + "kubernetes-pod-resource-requests-limits-qos-classes/pdf", + "kubernetes-secrets-external-secrets-operator-csi-vault/pdf", + "legacy-code-testing-characterization-tests-seams/pdf", + "monorepo-affected-builds-remote-caching-ci-optimization/pdf", + "mtls-certificate-rotation-service-mesh-authentication/pdf", + "nginx-haproxy-reverse-proxy-production-tuning/pdf", + "on-call-rotation-small-teams-sustainable-coverage/pdf", + "opa-conftest-policy-as-code-infrastructure-guardrails/pdf", + "openapi-spec-documentation-sdk-generation-validation/pdf", + "opentelemetry-span-design-granularity-overhead/pdf", + "performance-testing-load-models-benchmark-accuracy/pdf", + "platform-architecture-control-plane-data-plane-separation/pdf", + "platform-engineering-metrics-lead-time-developer-friction/pdf", + "postgresql-connection-pooling-saturation-sizing/pdf", + "private-networking-dns-routing-tls-debugging/pdf", + "prometheus-high-cardinality-metrics-label-design/pdf", + "rate-limiting-token-bucket-leaky-bucket-implementation/pdf", + "release-quality-gates-automated-deployment-validation/pdf", + "reverse-engineering-documentation-legacy-systems/pdf", + "service-catalog-metadata-schema-ownership-tracking/pdf", + "service-decommissioning-scream-test-shutdown/pdf", + "slo-error-budget-practical-guide/pdf", + "slsa-build-provenance-artifact-signing-supply-chain/pdf", + "strangler-fig-migration-complete-guide/pdf", + "structured-logging-correlation-ids-log-schema-design/pdf", + "symptom-based-alerting-runbooks-alert-design/pdf", + "synthetic-test-data-pii-anonymization-fixtures/pdf", + "terraform-module-design-defaults-versioning-interfaces/pdf", + "terraform-state-locking-corruption-recovery-backend/pdf", + "workload-identity-federation-keyless-cloud-authentication/pdf" + ] + }, "hero", "links", "newsletter", diff --git a/_TODO.md b/_TODO.md index 0636eab8f..e8bf99538 100644 --- a/_TODO.md +++ b/_TODO.md @@ -133,7 +133,6 @@ https://mermaid.js.org/config/directives.html - /404 - /about - /contact -- /downloads/[slug] - /newsletter - /newsletter/confirm/[token] - /offline @@ -191,25 +190,49 @@ Generate cover images for the skills and technologies tags pages ## Checklist - Need to make tables responsive on mobile + - Need to stop system from adding abbreviation html when used in headings, also the `abbr` presentation needs improved - right now it gives a question mark pointer and long delay to appear + - Re-enable link validator in `astro.config.ts` when pdf / downloads sorted out + - Add people who sign up for newsletter, download, or fill out contact form to Hubspot tracking. Need to configure it to remove them if they do the GDPR remove me. Also remove them from the newsletter. + - Mathjax not working on inline formulas: "Where $L$ is the average number of items in the system (queue depth), $\lambda$ is the arrival rate (requests per second), and $W$ is the average time in system (latency)." In backpressure-load-shedding-admission-control-overload, also "The Retry Amplification Problem" section in circuit-breaker-retry-budget-cascade-failure-prevention. + - Uppy, Tus server, whatever other server needed for file upload on Contact Form component + - Need a Q & A format to use in `blameless-postmortem-incident-analysis-systemic-causes` + - Add a cloud of tags on articles list view at top for quick navigation. Add a ToC for featured tags so it's available on mobile, but with something different on desktop view - maybe hide tag cloud on mobile, and show it with an HR between all tags cloud and featured on desktop. + - Use an in-project Image component to wrap Astro's Image and Picture. Show a magnifying glass with a "+" for the cursor on hover, and a modal to show a magnified view of images on click. + - Add a copyright notice to content + - DownloadLayout to wrap `downloads.mdx` in each folder + - Image for home page newsletter CTA + +- Add PDF download image to download CTA, it's in the directory + - Time in prose is causing a line break, and the colon and minutes to be removed - "2:47 AM" in `src/content/articles/mtls-certificate-rotation-service-mesh-authentication/pdf.mdx`. Times like "11:59:59" are breaking across two lines - `src/content/articles/rate-limiting-token-bucket-leaky-bucket-implementation/index.mdx` in "Algorithm Overview" section + - Should we exclude "Footnotes" from the ToC list? Right now it shows at the bottom if there's a Footnotes H2. + - If a reader has already given their email address - newsletter signup, contact form, download registration, then the download CTA on short form articles should go directly to the HTML version of the deep dive, and it should have a PDF download button. Think this workflow through - maybe a "Short / Deep-Dive" slider button on top and don't show the CTA + the PDF download button. + - The articles list page should show tags at top for quick navigation. There's another note about this. Show the count of articles per tag unless they're all the same on the tag. + - 404 page should show search results based on query + - Update EXIF data on all AI generated JPGs + - When you click a link to a heading anchor, the heading is hidden by the page header now that it's stickied to the page +- The Newsletter token page spins forever. It should time out and show an error page at some point. + +- Need a secret token to bypass the waiting state for the Newsletter token page, so we can style it. Set it via env var. + ## Header - Need to improve the "squish" animation where the header reduces in size on scroll down, and returns to full size on scroll up. Maybe reduce and expand the text and search / themepicker / hamburger menu sizes in place, and then slide them horizontally. @@ -245,9 +268,17 @@ This article has different approaches to [print pagination](https://www.customjs ## Downloads / Gated Content -- The PDF downloads are going to be gated - the user has to sign up for them and give an email address. How do we handle the keywords in these from a search perspective? +We can add a path like `/articles/pdf` or `/articles/deep-dive` for the long-form articles. Make that path SSR loaded so we can check if they've given their e-mail address. + +- Add a Pages layout for Articles so that we can use it for both the `/articles` and `/articles/pdf` content collections. + +- If they haven't and they get to the path from sharing a link or something, we should use the `index.astro` file in that directory to explain that they've accessed gated content, and ask for their e-mail address. Then redirect to the content they want when they give it. + +- If they've given their email link, both the regular article and the deep dive should have a button to switch between the two versions so they don't have to download the PDF version. -- We need a gating system, where the user gets a token to be able to download a PDF and the token is checked before downloading. +- How do we handle the keywords in the long form / pdf files from a search perspective? Can we return the result in the search results if the short form content is not returned in the search results, and highlight it somehow in the search results to show that it is gated content? And clicking on its link takes the user to the Download page for that item? + +- We need a gating system, where the user gets a token to be able to download a PDF and the token is checked before downloading. If they've already given their e-mail address, they should be able to download it immediately. ## Content Issues @@ -255,7 +286,7 @@ This article has different approaches to [print pagination](https://www.customjs - cover.jpg for reliability-and-testing needs touch up in GIMP -- We need to check for short form and deep article articles where the deep-dive index.pdf has a non-featured tag lik "argo-cd" only in the pdf.mdx. In those cases, we should make sure the callout for the deep dive includes the name of that non-featured (technology) tag and add the name to the tags: frontmatter key in the index.mdx +- We need to check for short form and deep article articles where the deep-dive index.pdf has a non-featured tag like "argo-cd" only in the pdf.mdx. In those cases, we should make sure the callout for the deep dive includes the name of that non-featured (technology) tag and add the name to the tags: frontmatter key in the index.mdx - Need an article on OpenStack @@ -268,15 +299,3 @@ This article has different approaches to [print pagination](https://www.customjs - Color headings blue and use the SVG icon instead of the image. Color if the blue shade. - Social shares - module CSS doesn't appear correct with nested button hover classes, not sure how network name is generated or styled but it needs improvement. Should be rendered inside article content column, not across both content and TOC columns. - -### Component Stylings - -The bottom margin issues on CTAs need to handle paragraphs differently than headers. Headers are properly spaced now; paragraphs are flush with the CTA with no top margin. - -- Download CTA needs some styling tweaks. The checkmark on the two-column list component is too low on multi-line list items. It needs some bottom margin. - -- Newsletter CTA needs some bottom margin. - -- Callouts should get round corners like the Download and Newsletter CTAs - -- Tags need styled at the top of articles item view pages diff --git a/src/components/CallToAction/Newsletter/client/@types/index.d.ts b/src/components/CallToAction/Newsletter/client/@types/index.d.ts index 63eb8faee..f4d2fd959 100644 --- a/src/components/CallToAction/Newsletter/client/@types/index.d.ts +++ b/src/components/CallToAction/Newsletter/client/@types/index.d.ts @@ -8,5 +8,5 @@ export interface NewsletterProps { /** Submit button text */ buttonText?: string /** Choose which variant to show */ - variant: 'article' | 'home' + variant: 'article' | 'home' | 'page' } diff --git a/src/components/CallToAction/Newsletter/index.astro b/src/components/CallToAction/Newsletter/index.astro index d4d08c96c..fc62c42c2 100644 --- a/src/components/CallToAction/Newsletter/index.astro +++ b/src/components/CallToAction/Newsletter/index.astro @@ -16,8 +16,9 @@ */ import type { NewsletterProps } from './client/@types' -import ArticleNewsletter from './article.astro' -import HomeNewsletter from './home.astro' +import ArticleNewsletter from './layouts/article.astro' +import HomeNewsletter from './layouts/home.astro' +import PageNewsletter from './layouts/page.astro' export type Props = NewsletterProps @@ -47,6 +48,14 @@ const { buttonText={buttonText} /> )} + {variant === 'page' && ( + + )} diff --git a/src/components/Head/Meta.astro b/src/components/Head/Meta.astro index 448230547..3b13cf3a0 100644 --- a/src/components/Head/Meta.astro +++ b/src/components/Head/Meta.astro @@ -13,7 +13,7 @@ export interface Props { /** Content type for OpenGraph metadata */ contentType?: 'article' | 'website' /** Meta description */ - description?: string + pageDescription?: string /** Last modified date */ modifiedDate?: Date /** Whether search engines should ignore the page, default: false */ @@ -33,7 +33,7 @@ export interface Props { const { author, contentType, - description, + pageDescription, modifiedDate, noindex = false, pageTitle, @@ -48,7 +48,7 @@ const defaultTheme = getMetaThemeData() - + {/* JSON-LD Structured Data for search engines */} { } const createSchemaContext = (params: StructuredDataParams): SchemaContext => { - const { astro, path, pageTitle, description, contentType, publishDate, modifiedDate, author } = + const { astro, path, pageTitle, pageDescription, contentType, publishDate, modifiedDate, author } = params if (!astro) { @@ -93,13 +93,13 @@ const createSchemaContext = (params: StructuredDataParams): SchemaContext => { const normalizedPath = normalizePath(path) const canonicalUrl = astro.url?.href ?? resolveRoute(normalizedPath, astro.site) - const descriptionFallback = description ?? contactData.company.description + const pageDescriptionFallback = pageDescription ?? contactData.company.description const socialImageUrl = getSocialImageLink(path) const context: SchemaContext = { path: normalizedPath, pageTitle, - description: descriptionFallback, + pageDescription: pageDescriptionFallback, site: astro.site, canonicalUrl, pathSegments: normalizedPath.split('/').filter(Boolean), @@ -158,7 +158,7 @@ const webSiteSchema: SchemaBuilder = context => { '@type': 'WebSite', name: contactData.company.name, url: contactData.company.url, - description: context.description, + description: context.pageDescription, publisher: { '@type': 'Organization', name: contactData.company.name, @@ -178,7 +178,7 @@ const articleSchema: SchemaBuilder = context => { '@context': 'https://schema.org', '@type': 'Article', headline: context.pageTitle, - description: context.description, + description: context.pageDescription, datePublished: context.publishDate.toISOString(), dateModified: (context.modifiedDate ?? context.publishDate).toISOString(), author: { @@ -241,7 +241,7 @@ const serviceSchema: SchemaBuilder = context => { '@context': 'https://schema.org', '@type': 'Service', name: context.pageTitle, - description: context.description, + description: context.pageDescription, provider: { '@type': 'Organization', name: contactData.company.name, @@ -260,7 +260,7 @@ const contactPageSchema: SchemaBuilder = context => { '@context': 'https://schema.org', '@type': 'ContactPage', name: context.pageTitle, - description: context.description, + description: context.pageDescription, url: context.canonicalUrl, mainEntity: { '@type': 'Organization', diff --git a/src/components/Icon/icons/check-circled.astro b/src/components/Icon/icons/check-circled.astro new file mode 100644 index 000000000..28ae8c313 --- /dev/null +++ b/src/components/Icon/icons/check-circled.astro @@ -0,0 +1,30 @@ +--- +export type Props = { + color: string + classes?: string + size: number + accessible: boolean + focusable: boolean + isListMarker?: boolean +} + +const { color, classes, size, accessible, focusable, isListMarker } = Astro.props +--- + + + {accessible && User Avatar Icon} + + diff --git a/src/components/Icon/icons/cloud.astro b/src/components/Icon/icons/cloud.astro new file mode 100644 index 000000000..d453c37a8 --- /dev/null +++ b/src/components/Icon/icons/cloud.astro @@ -0,0 +1,30 @@ +--- +export type Props = { + color: string + classes?: string + size: number + accessible: boolean + focusable: boolean + isListMarker?: boolean +} + +const { color, classes, size, accessible, focusable, isListMarker } = Astro.props +--- + + + {accessible && Cloud Icon} + + diff --git a/src/components/Icon/icons/folder-magnifying-glass.astro b/src/components/Icon/icons/folder-magnifying-glass.astro new file mode 100644 index 000000000..2b023d59e --- /dev/null +++ b/src/components/Icon/icons/folder-magnifying-glass.astro @@ -0,0 +1,30 @@ +--- +export type Props = { + color: string + classes?: string + size: number + accessible: boolean + focusable: boolean + isListMarker?: boolean +} + +const { color, classes, size, accessible, focusable, isListMarker } = Astro.props +--- + + + {accessible && Folder Search Icon} + + diff --git a/src/components/Icon/icons/gear.astro b/src/components/Icon/icons/gear.astro new file mode 100644 index 000000000..75c19d74e --- /dev/null +++ b/src/components/Icon/icons/gear.astro @@ -0,0 +1,36 @@ +--- +export type Props = { + color: string + classes?: string + size: number + accessible: boolean + focusable: boolean + isListMarker?: boolean +} + +const { color, classes, size, accessible, focusable, isListMarker } = Astro.props +--- + + + {accessible && Gear Icon} + + + diff --git a/src/components/Icon/icons/lightning.astro b/src/components/Icon/icons/lightning.astro new file mode 100644 index 000000000..b7ece8f64 --- /dev/null +++ b/src/components/Icon/icons/lightning.astro @@ -0,0 +1,30 @@ +--- +export type Props = { + color: string + classes?: string + size: number + accessible: boolean + focusable: boolean + isListMarker?: boolean +} + +const { color, classes, size, accessible, focusable, isListMarker } = Astro.props +--- + + + {accessible && Lightning Icon} + + diff --git a/src/components/Layout/SubHeader/index.astro b/src/components/Layout/SubHeader/index.astro new file mode 100644 index 000000000..6bd1f1958 --- /dev/null +++ b/src/components/Layout/SubHeader/index.astro @@ -0,0 +1,32 @@ +--- +import Icon from '@components/Icon/index.astro' + +export type Props = { + title: string + description: string + icon: string + subtitle?: string +} +const { title, description, icon, subtitle } = Astro.props +--- + +
+
+ +
+

{title}

+ {description && ( +

+ {description} +

+ )} + {subtitle && ( +

+ {subtitle} +

+ )} +
diff --git a/src/components/List/index.astro b/src/components/List/index.astro index 2de65f946..84a23d309 100644 --- a/src/components/List/index.astro +++ b/src/components/List/index.astro @@ -20,6 +20,7 @@ export type Props = { title?: string lead?: string text: string + link?: string icon?: string color?: string inverseColor?: string diff --git a/src/components/List/layouts/ColoredMarkerList.astro b/src/components/List/layouts/ColoredMarkerList.astro index 9720db282..438b266ea 100644 --- a/src/components/List/layouts/ColoredMarkerList.astro +++ b/src/components/List/layouts/ColoredMarkerList.astro @@ -3,12 +3,14 @@ export type Props = { items: { lead?: string text: string - color: string + color?: string }[] classes?: { ul?: string li?: string em?: string + icon?: string + text?: string } size?: number } @@ -18,15 +20,17 @@ const { items, classes, size = 2 }: Props = Astro.props const ulClass = ["list-none pl-0 text-content mt-4 mb-2 space-y-2", classes?.ul] const liClass = ["flex items-start", classes?.li] const emClass = ["mr-2 not-italic font-bold", classes?.em] +const iconClass = [`w-${size} h-${size}`, "rounded-full mt-3 mr-3 shrink-0", classes?.icon] +const textClass = classes?.text ? [classes.text] : [] ---
    { items.map((item) => (
  • - - {item.lead} - {item.text} + + {item.lead && {item.lead}} + {item.text}
  • )) } diff --git a/src/components/Pages/About/index.astro b/src/components/Pages/About/index.astro new file mode 100644 index 000000000..5d41ea8b5 --- /dev/null +++ b/src/components/Pages/About/index.astro @@ -0,0 +1,171 @@ +--- +import Icon from '@components/Icon/index.astro' +import Contact from '@components/CallToAction/Contact/index.astro' + +export type Props = { + content: { + journey: { + header: string + itemlist: { + title: string + description: string + }[] + } + expertise: { + header: string + itemlist: { + title: string + description: string + }[] + } + proficiencies: { + header: string + cards: { + subheader: string + backgroundColor: string + headerColor: string + itemlist: { + title: string + link: string + }[] + }[] + philosophy: { + subheader: string + description: string + itemlist: { + name: string + text: string + icon: string + color: string + bgColor: string + }[] + } + cta: { + title: string + description: string + primaryLink: { + href: string + text: string + } + } + } + } +} + +const { content } = Astro.props +--- + +
    +
    +

    + My Journey +

    +
    + {content.journey.itemlist.map((item, index) => ( +
    +
    +
    +

    {item.title}

    +

    {item.description}

    + {index === 0 && Resume} +
    +
    + ))} +
    +
    + +
    +

    + {content.expertise.header} +

    +
    + {content.expertise.itemlist.map((item) => ( +
    +

    + {item.title} +

    +

    + {item.description} +

    +
    + ))} +
    +
    + +
    +

    + {content.proficiencies.header} +

    +
    + {content.proficiencies.cards.map((card) => ( +
    +

    + {card.subheader} +

    +
      + {card.itemlist.map((item) => ( +
    • + {item.title} +
    • + ))} +
    +
    + ))} +
    +
    + +
    +

    + {content.proficiencies.philosophy.subheader} +

    +
    + {content.proficiencies.philosophy.description} +
    +
    + {content.proficiencies.philosophy.itemlist.map((item) => ( +
    +
    + +
    +

    + {item.name} +

    +

    + {item.text} +

    +
    + ))} +
    +
    + +
    + +
    +
    diff --git a/src/components/Pages/Consent/index.astro b/src/components/Pages/Consent/index.astro index 959474d79..b412dfa6c 100644 --- a/src/components/Pages/Consent/index.astro +++ b/src/components/Pages/Consent/index.astro @@ -1,8 +1,8 @@ --- +import HeadingAnchor from '@components/Layout/HeadingAnchor/index.astro' import Icon from '@components/Icon/index.astro' import List from '@components/List/index.astro' import UnsavedConsentDialog from '@components/Pages/Consent/unsaved.astro' -import HeadingAnchor from '@components/Layout/HeadingAnchor/index.astro' export type Props = { content: { @@ -82,20 +82,6 @@ const { content } = Astro.props ---
    -
    -
    - -
    -

    {content.header.title}

    -

    - {content.header.description} -

    -
    -
    +
    +

    + {content.header.title} +

    +

    {content.header.description}

    +
    + {/* Introduction */}

    {content.intro}

    diff --git a/src/components/Forms/Contact/README.md b/src/components/Pages/Contact/README.md similarity index 100% rename from src/components/Forms/Contact/README.md rename to src/components/Pages/Contact/README.md diff --git a/src/components/Forms/Contact/client/@types/index.ts b/src/components/Pages/Contact/client/@types/index.ts similarity index 100% rename from src/components/Forms/Contact/client/@types/index.ts rename to src/components/Pages/Contact/client/@types/index.ts diff --git a/src/components/Pages/Contact/client/__fixtures__/contactForm.fixture.astro b/src/components/Pages/Contact/client/__fixtures__/contactForm.fixture.astro new file mode 100644 index 000000000..689d7921c --- /dev/null +++ b/src/components/Pages/Contact/client/__fixtures__/contactForm.fixture.astro @@ -0,0 +1,85 @@ +--- +import ContactFormComponent from '../../index.astro' +import type { Props as ContactPageProps } from '../../index.astro' + +const content = { + header: { + title: 'Contact', + description: 'Get in touch', + }, + contact: { + header: 'Contact Details', + details: [ + { + text: 'Email', + value: 'test@example.com', + icon: 'email', + }, + ], + expertise: { + header: 'Expertise', + areas: [{ text: 'Platform Engineering' }], + }, + chooseUs: { + header: 'Why Us', + areas: [{ text: 'Fast response' }], + }, + }, + form: { + header: 'Send a message', + description: 'Tell us about your project', + formErrorMssg: 'Please fix form errors.', + contact: { + header: 'Your Contact Info', + company: 'Company', + companyPlaceholder: 'Acme Inc', + email: 'Email', + emailPlaceholder: 'you@example.com', + fullName: 'Full Name', + fullNamePlaceholder: 'Jane Doe', + phone: 'Phone', + phonePlaceholder: '+1 555 555 5555', + }, + project: { + header: 'Project Details', + budget: 'Budget', + budgetOptions: [ + { text: 'Select budget', value: '' }, + { text: '$5,000 - $10,000', value: '5k-10k' }, + ], + budgetPlaceholder: 'Budget', + description: 'Description', + descriptionPlaceholder: 'Project details', + timeline: 'Timeline', + timelineOptions: [ + { text: 'Select timeline', value: '' }, + { text: 'Within 1 month', value: 'within-1-month' }, + ], + timelinePlaceholder: 'Timeline', + type: 'Project Type', + typeOptions: [ + { text: 'Select type', value: '' }, + { text: 'Infrastructure', value: 'infrastructure' }, + ], + typePlaceholder: 'Project Type', + }, + files: { + header: 'Attachments', + subheader: 'Optional uploads', + supportedFormats: 'PDF, DOCX', + maxFileSizeText: 'Max file size:', + maxFileSize: '10MB', + maxFilesText: 'Max files:', + maxFiles: '3', + }, + info: [{ lead: 'Note', text: 'We reply quickly.' }], + submitButtonText: 'Send', + waitingText: 'Sending...', + successText: 'Sent successfully.', + errorText: 'Submission failed.', + errorEmail: 'Please provide a valid email.', + }, +} satisfies ContactPageProps['content'] +--- + + diff --git a/src/components/Forms/Contact/client/__tests__/error.spec.ts b/src/components/Pages/Contact/client/__tests__/error.spec.ts similarity index 99% rename from src/components/Forms/Contact/client/__tests__/error.spec.ts rename to src/components/Pages/Contact/client/__tests__/error.spec.ts index 2e2f2aea7..40bb893a3 100644 --- a/src/components/Forms/Contact/client/__tests__/error.spec.ts +++ b/src/components/Pages/Contact/client/__tests__/error.spec.ts @@ -14,7 +14,7 @@ import { messageWarnLength, messageMaxLength, nameMaxLength, -} from '@components/Forms/Contact/client/errorMessages' +} from '@components/Pages/Contact/client/errorMessages' describe('Error Messages and Validation', () => { describe('Message Length Validation', () => { diff --git a/src/components/Forms/Contact/client/__tests__/feedback.spec.ts b/src/components/Pages/Contact/client/__tests__/feedback.spec.ts similarity index 98% rename from src/components/Forms/Contact/client/__tests__/feedback.spec.ts rename to src/components/Pages/Contact/client/__tests__/feedback.spec.ts index 7176b6857..4c51a3ef8 100644 --- a/src/components/Forms/Contact/client/__tests__/feedback.spec.ts +++ b/src/components/Pages/Contact/client/__tests__/feedback.spec.ts @@ -5,7 +5,7 @@ import { initLabelHandlers, showErrorBanner, showFieldFeedback, -} from '@components/Forms/Contact/client/feedback' +} from '@components/Pages/Contact/client/feedback' import { renderContactForm } from './testUtils' describe('ContactForm feedback helpers', () => { diff --git a/src/components/Forms/Contact/client/__tests__/formSubmission.spec.ts b/src/components/Pages/Contact/client/__tests__/formSubmission.spec.ts similarity index 99% rename from src/components/Forms/Contact/client/__tests__/formSubmission.spec.ts rename to src/components/Pages/Contact/client/__tests__/formSubmission.spec.ts index 77420cd89..e9380fcec 100644 --- a/src/components/Forms/Contact/client/__tests__/formSubmission.spec.ts +++ b/src/components/Pages/Contact/client/__tests__/formSubmission.spec.ts @@ -1,7 +1,7 @@ import { beforeAll, beforeEach, describe, expect, it, vi } from 'vitest' import { TestError } from '@test/errors' import type { RenderContactFormContext } from './testUtils' -import { appendUploadFiles } from '@components/Forms/Contact/client/formSubmission' +import { appendUploadFiles } from '@components/Pages/Contact/client/formSubmission' const contactSubmitMock = vi.hoisted(() => vi.fn()) const isInputErrorMock = vi.hoisted(() => vi.fn()) diff --git a/src/components/Forms/Contact/client/__tests__/index.spec.ts b/src/components/Pages/Contact/client/__tests__/index.spec.ts similarity index 92% rename from src/components/Forms/Contact/client/__tests__/index.spec.ts rename to src/components/Pages/Contact/client/__tests__/index.spec.ts index c6a974897..33b2f84bc 100644 --- a/src/components/Forms/Contact/client/__tests__/index.spec.ts +++ b/src/components/Pages/Contact/client/__tests__/index.spec.ts @@ -5,7 +5,7 @@ describe('ContactForm web component module', () => { it('exposes metadata for registration', async () => { await withJsdomEnvironment(async () => { const { webComponentModule, ContactFormElement } = - await import('@components/Forms/Contact/client') + await import('@components/Pages/Contact/client') expect(webComponentModule.registeredName).toBe('contact-form') expect(webComponentModule.componentCtor).toBe(ContactFormElement) @@ -15,7 +15,7 @@ describe('ContactForm web component module', () => { it('registers the custom element when window is available', async () => { await withJsdomEnvironment(async ({ window }) => { const { registerContactFormWebComponent, ContactFormElement } = - await import('@components/Forms/Contact/client') + await import('@components/Pages/Contact/client') const uniqueTag = `contact-form-${Math.random().toString(36).slice(2)}` const originalGet = window.customElements.get.bind(window.customElements) diff --git a/src/components/Forms/Contact/client/__tests__/message.spec.ts b/src/components/Pages/Contact/client/__tests__/message.spec.ts similarity index 97% rename from src/components/Forms/Contact/client/__tests__/message.spec.ts rename to src/components/Pages/Contact/client/__tests__/message.spec.ts index f8df473c8..5f938390a 100644 --- a/src/components/Forms/Contact/client/__tests__/message.spec.ts +++ b/src/components/Pages/Contact/client/__tests__/message.spec.ts @@ -2,7 +2,7 @@ import { describe, it, expect, vi } from 'vitest' import { initMssgLengthHandler, validateMessageField, -} from '@components/Forms/Contact/client/validation' +} from '@components/Pages/Contact/client/validation' import { renderContactForm } from './testUtils' describe('Message Validation', () => { diff --git a/src/components/Forms/Contact/client/__tests__/name.spec.ts b/src/components/Pages/Contact/client/__tests__/name.spec.ts similarity index 96% rename from src/components/Forms/Contact/client/__tests__/name.spec.ts rename to src/components/Pages/Contact/client/__tests__/name.spec.ts index 8bc8ce038..f4eba60d6 100644 --- a/src/components/Forms/Contact/client/__tests__/name.spec.ts +++ b/src/components/Pages/Contact/client/__tests__/name.spec.ts @@ -2,7 +2,7 @@ import { describe, it, expect, vi } from 'vitest' import { initNameLengthHandler, validateNameField, -} from '@components/Forms/Contact/client/validation' +} from '@components/Pages/Contact/client/validation' import { renderContactForm } from './testUtils' describe('Name Validation', () => { diff --git a/src/components/Forms/Contact/client/__tests__/selectors.spec.ts b/src/components/Pages/Contact/client/__tests__/selectors.spec.ts similarity index 98% rename from src/components/Forms/Contact/client/__tests__/selectors.spec.ts rename to src/components/Pages/Contact/client/__tests__/selectors.spec.ts index 9641dbaec..1d28fc70b 100644 --- a/src/components/Forms/Contact/client/__tests__/selectors.spec.ts +++ b/src/components/Pages/Contact/client/__tests__/selectors.spec.ts @@ -2,7 +2,7 @@ import { describe, it, expect } from 'vitest' import { getContactFormElements, queryContactFormGenericFields, -} from '@components/Forms/Contact/client/selectors' +} from '@components/Pages/Contact/client/selectors' import { renderContactForm } from './testUtils' describe('ContactForm selectors', () => { diff --git a/src/components/Forms/Contact/client/__tests__/testUtils.ts b/src/components/Pages/Contact/client/__tests__/testUtils.ts similarity index 82% rename from src/components/Forms/Contact/client/__tests__/testUtils.ts rename to src/components/Pages/Contact/client/__tests__/testUtils.ts index 7eb7ae35f..f7c7bab82 100644 --- a/src/components/Forms/Contact/client/__tests__/testUtils.ts +++ b/src/components/Pages/Contact/client/__tests__/testUtils.ts @@ -1,10 +1,10 @@ import { expect } from 'vitest' import { experimental_AstroContainer as AstroContainer } from 'astro/container' import { TestError } from '@test/errors' -import ContactFormFixture from '@components/Forms/Contact/client/__fixtures__/contactForm.fixture.astro' -import type { ContactFormElement } from '@components/Forms/Contact/client' -import { getContactFormElements } from '@components/Forms/Contact/client/selectors' -import type { ContactFormElements } from '@components/Forms/Contact/client/@types' +import ContactFormFixture from '@components/Pages/Contact/client/__fixtures__/contactForm.fixture.astro' +import type { ContactFormElement } from '@components/Pages/Contact/client' +import { getContactFormElements } from '@components/Pages/Contact/client/selectors' +import type { ContactFormElements } from '@components/Pages/Contact/client/@types' import type { WebComponentModule } from '@components/scripts/@types/webComponentModule' import { executeRender } from '@test/unit/helpers/litRuntime' @@ -27,7 +27,7 @@ export const renderContactForm = async (assertion: RenderContactFormAssertion): await executeRender({ container, component: ContactFormFixture, - moduleSpecifier: '@components/Forms/Contact/client/index', + moduleSpecifier: '@components/Pages/Contact/client/index', selector: 'contact-form', assert: async ({ element, module, window, renderResult }) => { if (!window) { diff --git a/src/components/Forms/Contact/client/__tests__/utils.spec.ts b/src/components/Pages/Contact/client/__tests__/utils.spec.ts similarity index 95% rename from src/components/Forms/Contact/client/__tests__/utils.spec.ts rename to src/components/Pages/Contact/client/__tests__/utils.spec.ts index 0145b8c28..12095febf 100644 --- a/src/components/Forms/Contact/client/__tests__/utils.spec.ts +++ b/src/components/Pages/Contact/client/__tests__/utils.spec.ts @@ -1,10 +1,10 @@ import { describe, it, expect } from 'vitest' import { TestError } from '@test/errors' -import { initCharacterCounter, initUploadPlaceholder } from '@components/Forms/Contact/client/utils' +import { initCharacterCounter, initUploadPlaceholder } from '@components/Pages/Contact/client/utils' import type { ContactFormConfig, ContactFormElements, -} from '@components/Forms/Contact/client/@types' +} from '@components/Pages/Contact/client/@types' import { renderContactForm } from './testUtils' const baseConfig: ContactFormConfig = { diff --git a/src/components/Forms/Contact/client/__tests__/validation.spec.ts b/src/components/Pages/Contact/client/__tests__/validation.spec.ts similarity index 99% rename from src/components/Forms/Contact/client/__tests__/validation.spec.ts rename to src/components/Pages/Contact/client/__tests__/validation.spec.ts index 540ebb370..c236a8c9d 100644 --- a/src/components/Forms/Contact/client/__tests__/validation.spec.ts +++ b/src/components/Pages/Contact/client/__tests__/validation.spec.ts @@ -9,7 +9,7 @@ import { validateGenericFields, validateMessageField, validateNameField, -} from '@components/Forms/Contact/client/validation' +} from '@components/Pages/Contact/client/validation' import { renderContactForm } from './testUtils' describe('Validation helpers', () => { diff --git a/src/components/Forms/Contact/client/email.ts b/src/components/Pages/Contact/client/email.ts similarity index 100% rename from src/components/Forms/Contact/client/email.ts rename to src/components/Pages/Contact/client/email.ts diff --git a/src/components/Forms/Contact/client/errorMessages.ts b/src/components/Pages/Contact/client/errorMessages.ts similarity index 100% rename from src/components/Forms/Contact/client/errorMessages.ts rename to src/components/Pages/Contact/client/errorMessages.ts diff --git a/src/components/Forms/Contact/client/feedback.ts b/src/components/Pages/Contact/client/feedback.ts similarity index 100% rename from src/components/Forms/Contact/client/feedback.ts rename to src/components/Pages/Contact/client/feedback.ts diff --git a/src/components/Forms/Contact/client/formSubmission.ts b/src/components/Pages/Contact/client/formSubmission.ts similarity index 100% rename from src/components/Forms/Contact/client/formSubmission.ts rename to src/components/Pages/Contact/client/formSubmission.ts diff --git a/src/components/Forms/Contact/client/index.ts b/src/components/Pages/Contact/client/index.ts similarity index 100% rename from src/components/Forms/Contact/client/index.ts rename to src/components/Pages/Contact/client/index.ts diff --git a/src/components/Forms/Contact/client/selectors.ts b/src/components/Pages/Contact/client/selectors.ts similarity index 97% rename from src/components/Forms/Contact/client/selectors.ts rename to src/components/Pages/Contact/client/selectors.ts index 5b0807832..29cd3ad2a 100644 --- a/src/components/Forms/Contact/client/selectors.ts +++ b/src/components/Pages/Contact/client/selectors.ts @@ -107,7 +107,8 @@ export const getContactFormElements = (): ContactFormElements => { export type ContactFormGenericField = HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement -const GENERIC_FIELD_SELECTOR = 'input, select, textarea' +const GENERIC_FIELD_SELECTOR = + 'input[type="text"], input[type="email"], input[type="tel"], select, textarea' const GENERATED_GENERIC_ERROR_SELECTOR = '.field-error' export const isContactFormGenericField = (element: unknown): element is ContactFormGenericField => { diff --git a/src/components/Forms/Contact/client/upload.ts b/src/components/Pages/Contact/client/upload.ts similarity index 100% rename from src/components/Forms/Contact/client/upload.ts rename to src/components/Pages/Contact/client/upload.ts diff --git a/src/components/Forms/Contact/client/utils.ts b/src/components/Pages/Contact/client/utils.ts similarity index 100% rename from src/components/Forms/Contact/client/utils.ts rename to src/components/Pages/Contact/client/utils.ts diff --git a/src/components/Forms/Contact/client/validation.ts b/src/components/Pages/Contact/client/validation.ts similarity index 100% rename from src/components/Forms/Contact/client/validation.ts rename to src/components/Pages/Contact/client/validation.ts diff --git a/src/components/Pages/Contact/index.astro b/src/components/Pages/Contact/index.astro index 9e70d8385..edf857f74 100644 --- a/src/components/Pages/Contact/index.astro +++ b/src/components/Pages/Contact/index.astro @@ -1,9 +1,15 @@ --- -// import List from '@components/List/index.astro' +import Confetti from '@components/Animations/Confetti/index.astro' +import GDPRConsent from '@components/Consent/Checkbox/index.astro' import Icon from '@components/Icon/index.astro' -// import HeadingAnchor from '@components/Layout/HeadingAnchor/index.astro' -import ContactForm from '@components/Forms/Contact/index.astro' -import Map from '@components/Map/index.astro' +import List from '@components/List/index.astro' +//import Map from '@components/Map/index.astro' + +import './uppy-audio.css' +import './uppy-core.css' +import './uppy-dashboard.css' +import './uppy-webcam.css' +import './validation.css' export type Props = { content: { @@ -11,197 +17,485 @@ export type Props = { title: string description: string } + contact: { + header: string + details: { + text: string + value: string + icon: string + iconClasses?: string + }[] + expertise: { + header: string + areas: { text: string }[] + }, + chooseUs: { + header: string, + areas: { text: string }[], + }, + } + form: { + header: string + description: string + formErrorMssg: string + contact: { + header: string + company: string + companyPlaceholder: string + email: string + emailPlaceholder: string + fullName: string + fullNamePlaceholder: string + phone: string + phonePlaceholder: string + }, + project: { + header: string + budget: string + budgetOptions: { + text: string + value: string + }[] + budgetPlaceholder: string + description: string + descriptionPlaceholder: string + timeline: string + timelineOptions: { + text: string + value: string + }[] + timelinePlaceholder: string + type: string + typeOptions: { + text: string + value: string + }[] + typePlaceholder: string + }, + files: { + header: string + subheader: string + supportedFormats: string + maxFileSizeText: string + maxFileSize: string + maxFilesText: string + maxFiles: string + }, + info: { + lead: string + text: string + email?: string + }[] + submitButtonText: string + waitingText: string + successText: string + errorText: string + errorEmail: string + } } } const { content } = Astro.props --- -
    - -
    -
    - -
    -

    {content.header.title}

    -

    - {content.header.description} -

    -
    - - - - - -
    -
    - - -
    -

    - Tell us about your project -

    -

    - We'll get back to you within 24 hours with a detailed proposal. -

    -
    - - -
    -
    -
    -

    Get In Touch

    - -
    -
    -
    - + +
    + {/** Google Maps embed */} + {/* */} + + {/** Contact Form Section */} +
    +
    +
    + {/** Contact Form */} +
    + {/** Contact FormHeader */} +
    +

    + {content.form.header} +

    +

    {content.form.description}

    +
    + +
    + + + {/** Personal Information */} +
    +

    +
    + {content.form.contact.header} +

    + +
    +
    + + + +
    + +
    + + + +
    -
    -

    Email

    -

    kevin@webstackbuilders.com

    + +
    +
    + + +
    + +
    + + +
    -
    -
    -

    +
    + {content.form.project.header} +

    + +
    +
    + + +
    + +
    + + +
    +
    + +
    + +
    -
    -

    Location

    -

    Remote • Worldwide

    + +
    + + + +
    + 0/2000 characters +
    -
    + {/** File Upload Section */} +
    +

    +
    + {content.form.files.header} +

    +

    {content.form.files.subheader}

    +
    -
    +
    +
    +

    + Supported formats: {content.form.files.supportedFormats} +

    +

    + {content.form.files.maxFileSizeText} {content.form.files.maxFileSize} • {content.form.files.maxFilesText} {content.form.files.maxFiles} +

    +
    +
    +
    + + {/** GDPR Consent */} + + + {/** Data Retention Notice */} +
    + {content.form.info.map((item) => ( +

    + {item.lead}{item.text} + {item.email && ( + {item.email} + )} +

    + ))} +
    + + +
    +
    -
    -

    Response Time

    -

    Within 24 hours

    + + + + {/** Form Messages */} +
    -
    -

    Our Expertise

    -
      -
    • - - Custom Web Development -
    • -
    • - - E-commerce Solutions -
    • -
    • - - Progressive Web Apps -
    • -
    • - - API Development & Integration -
    • -
    • - - Database Design & Optimization -
    • -
    • - - Performance & SEO Optimization -
    • -
    • - - Maintenance & Support -
    • -
    • - - Technical Consulting -
    • -
    -
    + {/** Contact Information */} +
    +
    +
    +

    {content.contact.header}

    + +
    + {content.contact.details.map((item) => ( +
    +
    + +
    +
    +

    {item.text}

    +

    +
    +
    + ))} +
    +
    -
    -

    Why Choose Us?

    -
      -
    • - - 15+ years of experience -
    • -
    • - - Modern tech stack -
    • -
    • - - Performance-focused -
    • -
    • - - SEO optimized -
    • -
    • - - Mobile-first approach -
    • -
    • - - Ongoing support -
    • -
    +
    +

    {content.contact.expertise.header}

    + +
    + +
    +

    {content.contact.chooseUs.header}

    + +
    +
    -
    - -
    -
    -
    +
    +
    +
    + + + diff --git a/src/components/Forms/Contact/uppy-audio.css b/src/components/Pages/Contact/uppy-audio.css similarity index 100% rename from src/components/Forms/Contact/uppy-audio.css rename to src/components/Pages/Contact/uppy-audio.css diff --git a/src/components/Forms/Contact/uppy-core.css b/src/components/Pages/Contact/uppy-core.css similarity index 100% rename from src/components/Forms/Contact/uppy-core.css rename to src/components/Pages/Contact/uppy-core.css diff --git a/src/components/Forms/Contact/uppy-dashboard.css b/src/components/Pages/Contact/uppy-dashboard.css similarity index 100% rename from src/components/Forms/Contact/uppy-dashboard.css rename to src/components/Pages/Contact/uppy-dashboard.css diff --git a/src/components/Forms/Contact/uppy-webcam.css b/src/components/Pages/Contact/uppy-webcam.css similarity index 100% rename from src/components/Forms/Contact/uppy-webcam.css rename to src/components/Pages/Contact/uppy-webcam.css diff --git a/src/components/Forms/Contact/validation.css b/src/components/Pages/Contact/validation.css similarity index 100% rename from src/components/Forms/Contact/validation.css rename to src/components/Pages/Contact/validation.css diff --git a/src/components/Pages/Download/client/__tests__/__fixtures__/downloadForm.fixture.astro b/src/components/Pages/Downloads/client/__tests__/__fixtures__/downloadForm.fixture.astro similarity index 100% rename from src/components/Pages/Download/client/__tests__/__fixtures__/downloadForm.fixture.astro rename to src/components/Pages/Downloads/client/__tests__/__fixtures__/downloadForm.fixture.astro diff --git a/src/components/Pages/Download/client/__tests__/index.spec.ts b/src/components/Pages/Downloads/client/__tests__/index.spec.ts similarity index 100% rename from src/components/Pages/Download/client/__tests__/index.spec.ts rename to src/components/Pages/Downloads/client/__tests__/index.spec.ts diff --git a/src/components/Pages/Download/client/__tests__/selectors.spec.ts b/src/components/Pages/Downloads/client/__tests__/selectors.spec.ts similarity index 99% rename from src/components/Pages/Download/client/__tests__/selectors.spec.ts rename to src/components/Pages/Downloads/client/__tests__/selectors.spec.ts index 936de4967..e719dcf58 100644 --- a/src/components/Pages/Download/client/__tests__/selectors.spec.ts +++ b/src/components/Pages/Downloads/client/__tests__/selectors.spec.ts @@ -11,7 +11,7 @@ import { getDownloadSubmitButton, getDownloadWorkEmailInput, queryDownloadFormInvalidatableControls, -} from '@components/Pages/Download/client/selectors' +} from '@components/Pages/Downloads/client/selectors' import { isButtonElement, isDivElement, diff --git a/src/components/Pages/Download/client/__tests__/testUtils.ts b/src/components/Pages/Downloads/client/__tests__/testUtils.ts similarity index 91% rename from src/components/Pages/Download/client/__tests__/testUtils.ts rename to src/components/Pages/Downloads/client/__tests__/testUtils.ts index 9b7b26aa1..1623299dc 100644 --- a/src/components/Pages/Download/client/__tests__/testUtils.ts +++ b/src/components/Pages/Downloads/client/__tests__/testUtils.ts @@ -1,8 +1,8 @@ import { expect } from 'vitest' import { experimental_AstroContainer as AstroContainer } from 'astro/container' import { TestError } from '@test/errors' -import DownloadFormFixture from '@components/Pages/Download/client/__tests__/__fixtures__/downloadForm.fixture.astro' -import type { DownloadFormElement } from '@components/Pages/Download/client' +import DownloadFormFixture from '@components/Pages/Downloads/client/__tests__/__fixtures__/downloadForm.fixture.astro' +import type { DownloadFormElement } from '@components/Pages/Downloads/client' import { getDownloadButtonWrapper, getDownloadCompanyNameInput, @@ -13,7 +13,7 @@ import { getDownloadStatusDiv, getDownloadSubmitButton, getDownloadWorkEmailInput, -} from '@components/Pages/Download/client/selectors' +} from '@components/Pages/Downloads/client/selectors' import type { WebComponentModule } from '@components/scripts/@types/webComponentModule' import { executeRender } from '@test/unit/helpers/litRuntime' @@ -48,7 +48,7 @@ export const renderDownloadForm = async (assertion: RenderDownloadFormAssertion) await executeRender({ container, component: DownloadFormFixture, - moduleSpecifier: '@components/Pages/Download/client/index', + moduleSpecifier: '@components/Pages/Downloads/client/index', selector: 'download-form', assert: async ({ element, module, window, renderResult }) => { if (!window) { diff --git a/src/components/Pages/Download/client/index.ts b/src/components/Pages/Downloads/client/index.ts similarity index 100% rename from src/components/Pages/Download/client/index.ts rename to src/components/Pages/Downloads/client/index.ts diff --git a/src/components/Pages/Download/client/selectors.ts b/src/components/Pages/Downloads/client/selectors.ts similarity index 100% rename from src/components/Pages/Download/client/selectors.ts rename to src/components/Pages/Downloads/client/selectors.ts diff --git a/src/components/Pages/Download/download-icon.jpg b/src/components/Pages/Downloads/download-icon.jpg similarity index 100% rename from src/components/Pages/Download/download-icon.jpg rename to src/components/Pages/Downloads/download-icon.jpg diff --git a/src/components/Pages/Download/index.astro b/src/components/Pages/Downloads/index.astro similarity index 94% rename from src/components/Pages/Download/index.astro rename to src/components/Pages/Downloads/index.astro index de7575c3a..9a2618a7d 100644 --- a/src/components/Pages/Download/index.astro +++ b/src/components/Pages/Downloads/index.astro @@ -12,15 +12,9 @@ const Components = { List } export interface Props { download: CollectionEntry<'downloads'> - content: { - header: { - title: string - description: string - } - } } -const { download, content } = Astro.props as Props +const { download } = Astro.props as Props const { cover, @@ -38,17 +32,6 @@ const normalizedDownloadUrl = `/downloads/${fileName}` --- -
    -
    - -
    -

    {content.header.title}

    -
    - {/* Two-column layout on desktop: info left, form right */}
    {/* Left column: cover, meta, content */} @@ -82,7 +65,7 @@ const normalizedDownloadUrl = `/downloads/${fileName}` aria-label="Resource details" >

    - {content.header.description} + {download.data.description}

    @@ -270,6 +253,6 @@ const normalizedDownloadUrl = `/downloads/${fileName}` diff --git a/src/components/Pages/Download/index.css b/src/components/Pages/Downloads/index.css similarity index 100% rename from src/components/Pages/Download/index.css rename to src/components/Pages/Downloads/index.css diff --git a/src/components/Pages/MyData/index.astro b/src/components/Pages/MyData/index.astro index da794a995..693aa14b8 100644 --- a/src/components/Pages/MyData/index.astro +++ b/src/components/Pages/MyData/index.astro @@ -41,20 +41,6 @@ const { content } = Astro.props
    -
    -
    - -
    -

    {content.header.title}

    -

    - {content.header.description} -

    -
    -
    +
    +
    + + {/* Left column: what you get */} +
    +
    +

    {content.intro.heading}

    +

    + {content.intro.detail} +

    +
    + +
      + {content.intro.itemList.map((item) => ( +
    • + + + +
      +

      {item.lead}

      +

      {item.text}

      +
      +
    • + ))} +
    + +
    +

    + {content.intro.close.lead} + {content.intro.close.text} +

    +
    +
    + + {/* Right column: signup form */} + +
    +
    +
    + +{/* Topics section */} +
    +
    +
    +

    {content.topics.heading}

    +

    + {content.topics.detail} +

    +
    + +
    + {content.topics.itemList.map((item) => ( +
    +
    + +
    +

    {item.lead}

    +

    {item.text}

    +
    + ))} +
    +
    +
    + +{/* Sample issue preview */} +
    +
    +
    +

    {content.preview.heading}

    +

    {content.preview.detail}

    +
    + +
    + {/* Terminal header */} +
    +
    +
    +
    + webstack-dispatch-001.md +
    + +
    +

    + ## + {` Deep Dive: The Kubernetes DNS Bug That Wasted 40 Hours`} +

    +

    + How the default ndots:5 setting caused cascading DNS failures under + load, the misleading metrics that sent us down the wrong path, and the + two-line fix that resolved it. +

    + +

    + ## + {` Quick Wins`} +

    +
    +

    - Terraform state backup one-liner before every apply

    +

    - PromQL query to find unused Grafana dashboards

    +

    - Pre-commit hook for secrets scanning in monorepos

    +
    + +

    + ## + {` What We're Reading`} +

    +
    +

    - Cloudflare's routing incident post-mortem

    +

    - OpenTelemetry's new tail-sampling processor

    +

    - Google SRE on SLO-based alerting

    +
    + +

    + ## + {` Try This Month`} +

    +

    + Check your CoreDNS cache hit rate. Below 80%? You're hammering upstream + resolvers unnecessarily. +

    + +

    + $ 5 min read · unsubscribe anytime +

    +
    +
    +
    +
    diff --git a/src/components/Pages/NotFound/index.astro b/src/components/Pages/NotFound/index.astro index 1d314d4a3..7ee513fc3 100644 --- a/src/components/Pages/NotFound/index.astro +++ b/src/components/Pages/NotFound/index.astro @@ -1,33 +1,14 @@ --- -import Icon from '@components/Icon/index.astro' - -/* export interface Props { - heading?: string + content: { + buttonText: string + } } -const { - heading = '' -} = Astro.props -*/ +const { content } = Astro.props ---
    - -
    -
    - -
    -

    404

    -

    - Sorry, the page you are looking for doesn't exist or has been moved. -

    -
    -
    @@ -35,7 +16,7 @@ const { href={"/"} class="inline-flex items-center justify-center px-6 py-3 border border-transparent text-base font-medium rounded-lg text-white bg-spotlight hover:bg-spotlight-offset focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary transition-colors duration-200" > - Go Home + {content.buttonText}
    diff --git a/src/components/Pages/Offline/index.astro b/src/components/Pages/Offline/index.astro index 4b1e451e4..5184d6117 100644 --- a/src/components/Pages/Offline/index.astro +++ b/src/components/Pages/Offline/index.astro @@ -4,8 +4,8 @@ */ import contactData from '@content/contact.json' import HeadingAnchor from '@components/Layout/HeadingAnchor/index.astro' -import List from '@components/List/index.astro' import Icon from '@components/Icon/index.astro' +import List from '@components/List/index.astro' export type Props = { content: { @@ -46,122 +46,107 @@ const actionCards = content.items.cards.map((card) => { }) --- -
    - -
    -
    - -
    -

    {content.title}

    -

    {content.message}

    -
    - - -
    -

    - {content.items.label} - -

    - -
    - - -
    -

    - {content.about.heading} - -

    -

    - {contactData.company.description} -

    -
    - {content.about.items.map((item) => { - return ( -
    -
    {item.stat}
    -
    {item.area}
    -
    - ) - })} -
    -
    +{/** What You Can Do Offline Section */} +
    +

    + {content.items.label} + +

    + +
    - -
    -

    - {content.close} - -

    -
    -
    -
    - -
    +{/** About Webstack Builders */} +
    +

    + {content.about.heading} + +

    +

    + {contactData.company.description} +

    +
    + {content.about.items.map((item) => { + return (
    -
    Email
    -
    {contactData.company.email}
    +
    {item.stat}
    +
    {item.area}
    + ) + })} +
    +
    + +{/** Get In Touch */} +
    +

    + {content.close} + +

    +
    +
    +
    +
    -
    -
    - -
    -
    -
    Website
    -
    {contactData.company.url.replace('https://', '')}
    -
    +
    +
    Email
    +
    {contactData.company.email}
    -
    +
    +
    + +
    +
    +
    Website
    +
    {contactData.company.url.replace('https://', '')}
    +
    +
    +
    +
    - -
    -

    Try again

    - -
    +{/** Retry Button */} +
    +

    Try again

    + +
    - -
    +