Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
897ba09
Add Wix CMS migration spec and plan
kylefritz May 13, 2026
cd61cdc
Add ContactMessage model for marketing-site contact form
kylefritz May 13, 2026
24cb651
Add ActiveAdmin resource for ContactMessage triage
kylefritz May 13, 2026
810fc26
Add selectable_column and id_column to ContactMessage admin index
kylefritz May 13, 2026
390daa3
Scaffold marketing controllers, layout, and routes for /about /subscr…
kylefritz May 13, 2026
88b8781
Add noreferrer to marketing header IG link
kylefritz May 13, 2026
2741827
Add marketing-site SCSS scaffold (typography, header, footer)
kylefritz May 13, 2026
64324b9
Add one-shot rake task to mirror Wix marketing assets to S3
kylefritz May 13, 2026
6b347a6
Port /about (Our Process) page from Wix
kylefritz May 13, 2026
a093458
Port /subscribe page from Wix
kylefritz May 13, 2026
07c927c
Port /contact static info (address, phone, hours) from Wix
kylefritz May 13, 2026
9bc60fd
Add contact form: persist ContactMessage with honeypot, render errors
kylefritz May 13, 2026
55a730e
Email contact form submissions to the bakery via ContactMailer
kylefritz May 13, 2026
15e6684
Throttle POST /contact at 5 per hour per IP via rack-attack
kylefritz May 13, 2026
1595d0c
Surface active holiday menu in marketing nav via helper
kylefritz May 13, 2026
61f5007
Replace home redirect with marketing home page (cutover from Wix)
kylefritz May 13, 2026
5aa8705
Add Playwright + Claude Haiku visual QA for marketing pages
kylefritz May 13, 2026
168f687
Use admin_contact_message_url helper instead of hand-rolled APP_HOST
kylefritz May 13, 2026
c6ed094
Replace ContactMessage with Feedback for contact form submissions
kylefritz May 13, 2026
4f2ca48
Extract MarketingController base for shared layout and auth skip
kylefritz May 13, 2026
730694f
Use Rails route helpers in marketing nav, footer, and contact form
kylefritz May 13, 2026
37ccecd
Truncate Feedback user_agent and url before validating
kylefritz May 13, 2026
2ea1a48
Skip visual specs cleanly when ANTHROPIC_API_KEY is unset
kylefritz May 13, 2026
a1afb9d
Apply /simplify findings: helper paths, presence, dedup tests, shared…
kylefritz May 13, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions app/admin/feedbacks.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
selectable_column
id_column
column :source
column :name
column :message do |f|
truncate(f.message, length: 80)
end
Expand All @@ -25,6 +26,8 @@
attributes_table do
row :id
row :source
row :name
row :phone
row :message
row :email do |f|
link_to(f.email, "mailto:#{f.email}") if f.email.present?
Expand Down
99 changes: 99 additions & 0 deletions app/assets/stylesheets/marketing.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
// Marketing site styles. Scoped under .marketing so they don't leak into
// /menu or /admin (which use application.scss + ActiveAdmin styles).

$marketing-bg: #FFECD6;
$marketing-text: #2E2927;
$marketing-accent: #D5482C;
$marketing-muted: #6B5E4F;
$marketing-display-font: "Playfair Display", Georgia, serif;
$marketing-body-font: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;

body.marketing {
margin: 0;
background: $marketing-bg;
color: $marketing-text;
font-family: $marketing-body-font;
font-size: 17px;
line-height: 1.6;

h1, h2, h3 {
font-family: $marketing-display-font;
font-weight: 600;
letter-spacing: -0.01em;
}

h1 { font-size: clamp(2rem, 4vw, 3rem); margin: 0 0 1rem; }
h2 { font-size: clamp(1.5rem, 3vw, 2rem); margin: 2rem 0 0.75rem; }

a { color: $marketing-accent; }
}

.marketing-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1.5rem 2rem;
border-bottom: 1px solid rgba($marketing-text, 0.08);

.marketing-logo {
font-family: $marketing-display-font;
font-size: 1.5rem;
text-decoration: none;
color: $marketing-text;
}

.marketing-nav {
display: flex;
gap: 1.25rem;

a {
color: $marketing-text;
text-decoration: none;
font-size: 0.95rem;

&:hover { color: $marketing-accent; }
}
}
}

.marketing-main {
max-width: 960px;
margin: 0 auto;
padding: 3rem 2rem;
}

.marketing-footer {
padding: 3rem 2rem 2rem;
background: rgba($marketing-text, 0.03);
text-align: center;

address {
font-style: normal;
margin-bottom: 1rem;
}

.marketing-footer-nav {
display: flex;
gap: 1.25rem;
justify-content: center;
margin-bottom: 1.5rem;

a { color: $marketing-text; text-decoration: none; }
}

.marketing-credits, .marketing-copyright {
font-size: 0.85rem;
color: $marketing-muted;
margin: 0.5rem 0;
}
}

@media (max-width: 640px) {
.marketing-header {
flex-direction: column;
gap: 1rem;
padding: 1rem;
}
.marketing-nav { flex-wrap: wrap; justify-content: center; }
.marketing-main { padding: 2rem 1rem; }
}
4 changes: 4 additions & 0 deletions app/controllers/about_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
class AboutController < MarketingController
def show
end
end
2 changes: 1 addition & 1 deletion app/controllers/api/feedbacks_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ def create
private

def feedback_params
params.require(:feedback).permit(:source, :message, :email, :url)
params.require(:feedback).permit(:source, :message, :email, :url, :name, :phone)
end

# Skip Turnstile for sources that don't include the widget:
Expand Down
30 changes: 30 additions & 0 deletions app/controllers/contact_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
class ContactController < MarketingController
def show
@feedback = Feedback.new(source: "contact")
end

def create
if params.dig(:feedback, :website).present?
redirect_to contact_path, notice: "Thanks! We'll be in touch."
return
end

@feedback = Feedback.new(feedback_params)
@feedback.source = "contact"
@feedback.user_agent = request.user_agent
@feedback.url = request.referer.presence || request.original_url

if @feedback.save
FeedbackMailer.with(feedback: @feedback).feedback_received.deliver_later
redirect_to contact_path, notice: "Thanks! We'll be in touch."
else
render :show, status: :unprocessable_entity
end
end

private

def feedback_params
params.require(:feedback).permit(:name, :email, :phone, :message)
end
end
5 changes: 1 addition & 4 deletions app/controllers/home_controller.rb
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
class HomeController < ApplicationController
skip_before_action :authenticate_user!

class HomeController < MarketingController
def show
redirect_to '/menu'
end

def signout
Expand Down
4 changes: 4 additions & 0 deletions app/controllers/marketing_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
class MarketingController < ApplicationController
layout "marketing"
skip_before_action :authenticate_user!
end
4 changes: 4 additions & 0 deletions app/controllers/subscribe_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
class SubscribeController < MarketingController
def show
end
end
8 changes: 8 additions & 0 deletions app/helpers/marketing_helper.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
module MarketingHelper
def holiday_menu_link
holiday = Menu.current_holiday
return nil if holiday.nil?

link_to(holiday.name, menu_path(holiday), class: "marketing-nav-holiday")
end
end
8 changes: 7 additions & 1 deletion app/mailers/feedback_mailer.rb
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
class FeedbackMailer < ApplicationMailer
def feedback_received
@feedback = params[:feedback]
subject = if @feedback.source == "contact" && @feedback.name.present?
"Contact form: #{@feedback.name}"
else
"Feedback from #{@feedback.source}"
end
mail(to: User.kyle.email_list,
subject: "Feedback from #{@feedback.source}") do |format|
subject: subject,
reply_to: @feedback.email.presence) do |format|
format.text
format.mjml
end
Expand Down
16 changes: 15 additions & 1 deletion app/models/feedback.rb
Original file line number Diff line number Diff line change
@@ -1,9 +1,23 @@
class Feedback < ApplicationRecord
SOURCES = %w[404 422 500 menu general].freeze
SOURCES = %w[404 422 500 menu general contact].freeze

# user_agent and url come from request headers, not user input — silently
# truncate so a long browser UA or referer can't make save fail and lose
# the submission.
before_validation :truncate_request_metadata

validates :source, presence: true, inclusion: { in: SOURCES }
validates :message, presence: true, length: { maximum: 5000 }
validates :email, format: { with: URI::MailTo::EMAIL_REGEXP }, allow_blank: true
validates :url, length: { maximum: 2048 }
validates :user_agent, length: { maximum: 512 }
validates :name, length: { maximum: 255 }
validates :phone, length: { maximum: 50 }

private

def truncate_request_metadata
self.user_agent = user_agent.truncate(512) if user_agent.present?
self.url = url.truncate(2048) if url.present?
end
end
45 changes: 45 additions & 0 deletions app/views/about/show.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<% content_for :title, "Our Process" %>

<article class="page-about">
<header class="page-hero">
<img src="https://motzi.s3.us-east-1.amazonaws.com/public/marketing/nsplsh_685133575a6e5933795a30_mv2_d_5906_3937_s_4_2.jpg" alt="Freshly baked loaves of sourdough bread">
<h1>Our Process</h1>
<p class="lede">Every part of our process contributes to the flavor and nutrition of the bread.</p>
</header>

<section>
<h2>Local Sourcing</h2>
<p>We partner with regional farmers practicing regenerative agriculture. Key suppliers include:</p>
<ul>
<li>Migrash Farm — Red Fife wheat, Bolles wheat, Abruzzi rye, Tuxpeño corn, spelt</li>
<li>Pecan Meadow Farms — Pennoll wheat</li>
<li>Buffalo Valley Pastures — Einkorn wheat</li>
<li>Next Step Produce — buckwheat, oats, sunflower seeds, sesame seeds</li>
</ul>
<figure>
<img src="https://motzi.s3.us-east-1.amazonaws.com/public/marketing/harvesting-hulless-oats.jpg" alt="Heinz Thomet of Next Step Produce harvesting hulless oats">
<figcaption>Heinz Thomet of Next Step Produce — Photo by Raphaelle Lajoie</figcaption>
</figure>
</section>

<section>
<h2>Fresh Milling &amp; Whole Grain</h2>
<p>The flavor of flour is at its peak the moment it's milled. We use a Zentrofan stone mill from Germany to mill whole grain in-house weekly.</p>
<figure>
<img src="https://motzi.s3.us-east-1.amazonaws.com/public/marketing/NM_20200723_14_12_12_websize_edited.jpg" alt="Stone mill grinding fresh whole grain flour">
</figure>
</section>

<section>
<h2>Long Fermentation</h2>
<p>Sourdough fermentation, run long, makes the bread more digestible and gives it a complex flavor and crisp crust.</p>
<figure>
<img src="https://motzi.s3.us-east-1.amazonaws.com/public/marketing/NM_20200702_12_58_37_websize.jpg" alt="Sourdough loaves during long fermentation">
</figure>
</section>

<section>
<h2>The Name</h2>
<p>"Motzi" references the Hebrew blessing over bread. We treat baking as a sacred act, and offer pay-what-you-can pricing to make this bread accessible.</p>
</section>
</article>
56 changes: 56 additions & 0 deletions app/views/contact/show.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
<% content_for :title, "Contact Us" %>

<article class="page-contact">
<header class="page-hero">
<h1>Contact Us</h1>
<p class="lede">We'd love to hear from you!</p>
</header>

<section class="contact-details">
<address>
2801 Guilford Ave<br>
Baltimore, MD 21218
</address>
<p><strong>Phone:</strong> 443-272-1515 (Tues – Sat)</p>
</section>

<%= form_with model: @feedback, url: contact_path, local: true, html: { class: "contact-form" } do |f| %>
<% if @feedback.errors.any? %>
<div class="form-errors">
<p>Please fix the following:</p>
<ul>
<% @feedback.errors.full_messages.each do |msg| %>
<li><%= msg %></li>
<% end %>
</ul>
</div>
<% end %>

<div class="field">
<%= f.label :name %>
<%= f.text_field :name, required: true, autocomplete: "name" %>
</div>

<div class="field">
<%= f.label :email %>
<%= f.email_field :email, required: true, autocomplete: "email" %>
</div>

<div class="field">
<%= f.label :phone, "Phone (optional)" %>
<%= f.telephone_field :phone, autocomplete: "tel" %>
</div>

<div class="field">
<%= f.label :message %>
<%= f.text_area :message, rows: 6, required: true %>
</div>

<div class="hp-field" aria-hidden="true" style="position:absolute;left:-9999px">
<label for="feedback_website">Website (leave blank)</label>
<input type="text" name="feedback[website]" id="feedback_website" tabindex="-1" autocomplete="off">
</div>

<%= f.submit "Send" %>
<% end %>
</article>
12 changes: 11 additions & 1 deletion app/views/feedback_mailer/feedback_received.mjml
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,21 @@
<%= @feedback.message %>
</mj-text>

<% if @feedback.email.present? %>
<% if @feedback.name.present? %>
<mj-text padding="20px 0 0 0" font-size="15px" line-height="1.6" color="#2E2927">
<strong>Name:</strong> <%= @feedback.name %>
</mj-text>
<% end %>
<% if @feedback.email.present? %>
<mj-text padding="<%= @feedback.name.present? ? '4px' : '20px' %> 0 0 0" font-size="15px" line-height="1.6" color="#2E2927">
<strong>Reply to:</strong> <a href="mailto:<%= @feedback.email %>"><%= @feedback.email %></a>
</mj-text>
<% end %>
<% if @feedback.phone.present? %>
<mj-text padding="4px 0 0 0" font-size="15px" line-height="1.6" color="#2E2927">
<strong>Phone:</strong> <%= @feedback.phone %>
</mj-text>
<% end %>

<mj-divider border-color="#e8e0d8" border-width="1px" padding="28px 0 16px 0" />

Expand Down
8 changes: 4 additions & 4 deletions app/views/feedback_mailer/feedback_received.text.erb
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@ Feedback: <%= @feedback.source %>

<%= @feedback.message %>

<% if @feedback.email.present? %>
Reply to: <%= @feedback.email %>
<% end %>
---
<% if @feedback.name.present? %>Name: <%= @feedback.name %>
<% end %><% if @feedback.email.present? %>Reply to: <%= @feedback.email %>
<% end %><% if @feedback.phone.present? %>Phone: <%= @feedback.phone %>
<% end %>---
Source: <%= @feedback.source %>
<% if @feedback.url.present? %>URL: <%= @feedback.url %><% end %>
Submitted: <%= @feedback.created_at.strftime("%Y-%m-%d %l:%M%P %Z") %>
Expand Down
Loading
Loading