浏览代码

Add contact form, simplify How it works, update hero copy

- Contact card in hero emailing inquiries via SMTP (secrets in .env)
- Three-step How it works section
- Point CTAs at the contact form
- Local site port 8082

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Contact
Daniel Covington 4 天前
父节点
当前提交
4611b2a7ea
共有 12 个文件被更改,包括 372 次插入 和 24 次删除
  1. +2
    -0
      .env.sample
  2. +3
    -0
      .gitignore
  3. +1
    -1
      README.md
  4. +48
    -0
      app/controllers/HomeController.asp
  5. +2
    -0
      app/controllers/autoload_controllers.asp
  6. +81
    -0
      app/services/ContactInquiryService.asp
  7. +56
    -0
      app/services/EnvService.asp
  8. +52
    -20
      app/views/Home/landing.asp
  9. +1
    -1
      applicationhost.config
  10. +1
    -0
      public/Default.asp
  11. +118
    -2
      public/css/home.css
  12. +7
    -0
      public/web.config

+ 2
- 0
.env.sample 查看文件

@@ -0,0 +1,2 @@
# Copy this file to .env and fill in the real values. .env is never committed.
SMTP_PASSWORD=your-mailbox-password

+ 3
- 0
.gitignore 查看文件

@@ -7,3 +7,6 @@
*.log
*.user
*.suo

# Secrets
/.env

+ 1
- 1
README.md 查看文件

@@ -6,7 +6,7 @@ A clean Classic ASP application scaffold based on the RouteKit MVC framework.

1. Install IIS Express with Classic ASP enabled.
2. Run `run_site.cmd`.
3. Browse to `http://localhost:8080/`.
3. Browse to `http://localhost:8082/`.

The landing page is served by `HomeController.Index` through
`app/views/Home/landing.asp`. The earlier logo-reference page remains at


+ 48
- 0
app/controllers/HomeController.asp 查看文件

@@ -3,9 +3,21 @@ Class HomeController_Class
Public Title
Public useLayout

' Contact form state shared with the landing view
Private contactErrors
Private contactName
Private contactEmail
Private contactPhone
Private contactBusiness
Private contactMessage
Private contactSent

Private Sub Class_Initialize()
Title = "PostcardMojo Logo Files"
useLayout = False
contactErrors = Array()
ReDim contactErrors(-1)
contactSent = (Request.QueryString("contact") = "sent")
End Sub

Public Sub Index()
@@ -13,6 +25,42 @@ Class HomeController_Class
<!--#include file="../views/Home/landing.asp" -->
<%
End Sub

' GetAntiCSRFToken returns an empty value the first time it creates a token, so read it again.
Private Function ContactCsrfToken()
ContactCsrfToken = HTMLSecurity().GetAntiCSRFToken("home.contact")
If Len(ContactCsrfToken) = 0 Then ContactCsrfToken = HTMLSecurity().GetAntiCSRFToken("home.contact")
End Function

' POST /contact - validate the inquiry and email the PostcardMojo team.
Public Sub Contact()
contactName = Trim(Request.Form("name"))
contactEmail = Trim(Request.Form("email"))
contactPhone = Trim(Request.Form("phone"))
contactBusiness = Trim(Request.Form("business"))
contactMessage = Trim(Request.Form("message"))

' Honeypot: real visitors never see or fill this field
If Len(Request.Form("zq_trap_field")) > 0 Then
Response.Redirect "/?contact=sent#contact"
End If

If Not HTMLSecurity().IsValidAntiCSRFToken("home.contact", Request.Form("csrf_token")) Then
contactErrors = Array("Your session expired. Please try again.")
Else
contactErrors = ContactInquiryService().ValidationErrors(contactName, contactEmail, contactBusiness, contactMessage)
End If

If UBound(contactErrors) < 0 Then
If ContactInquiryService().Notify(contactName, contactEmail, contactPhone, contactBusiness, contactMessage) Then
HTMLSecurity().ClearAntiCSRFToken "home.contact"
Response.Redirect "/?contact=sent#contact"
End If
contactErrors = Array("We could not send your message. Please email danielcovington@postcardmojo.com directly.")
End If

Index
End Sub
End Class

Dim HomeController_Class__Singleton


+ 2
- 0
app/controllers/autoload_controllers.asp 查看文件

@@ -1 +1,3 @@
<!--#include file="../services/EnvService.asp" -->
<!--#include file="../services/ContactInquiryService.asp" -->
<!--#include file="HomeController.asp" -->

+ 81
- 0
app/services/ContactInquiryService.asp 查看文件

@@ -0,0 +1,81 @@
<%
' Validates a visitor inquiry and notifies the PostcardMojo team by email.
Class ContactInquiryService_Class
Public Function ValidationErrors(name, email, business, message)
Dim errs : errs = Array()
ReDim errs(-1)

If Len(name) = 0 Then AddMessage errs, "Please enter your name."
If Len(name) > 100 Then AddMessage errs, "Name must be 100 characters or fewer."

If Len(email) = 0 Then
AddMessage errs, "Please enter your email address."
ElseIf Len(email) > 150 Or Not IsValidEmail(email) Then
AddMessage errs, "Please enter a valid email address."
End If

If Len(business) > 150 Then AddMessage errs, "Business name must be 150 characters or fewer."
If Len(message) > 2000 Then AddMessage errs, "Message must be 2000 characters or fewer."

ValidationErrors = errs
End Function

Public Function Notify(name, email, phone, business, message)
Dim recipient, smtpServer, smtpPort, smtpUser, body
recipient = Setting("ContactRecipient", "covington.daniel@gmail.com")
smtpServer = Setting("SmtpServer", "localhost")
smtpPort = Setting("SmtpPort", "25")
smtpUser = Setting("SmtpUsername", "")

body = "New PostcardMojo website inquiry" & vbCrLf & vbCrLf & _
"Name: " & name & vbCrLf & _
"Email: " & email & vbCrLf & _
"Phone: " & phone & vbCrLf & _
"Business: " & business & vbCrLf & vbCrLf & _
"Message:" & vbCrLf & message & vbCrLf & vbCrLf & _
"Received: " & Now() & vbCrLf & _
"IP: " & Request.ServerVariables("REMOTE_ADDR")

CDOEmail.From = Setting("SmtpFrom", Choice(Len(smtpUser) > 0, smtpUser, recipient))
CDOEmail.Subject = "PostcardMojo inquiry from " & StripLineBreaks(name)
CDOEmail.Body = body
CDOEmail.IsBodyHTML = False
CDOEmail.SMTPServer = smtpServer
CDOEmail.SMTPPort = CLng(smtpPort)
CDOEmail.SMTPUsername = smtpUser
CDOEmail.SMTPPassword = EnvService().Value("SMTP_PASSWORD", "")
CDOEmail.SMTPUseSSL = (LCase(Setting("SmtpUseSsl", "false")) = "true")
CDOEmail.AddRecipient "TO", recipient
Notify = CDOEmail.Send()
End Function

Public Function StripLineBreaks(value)
StripLineBreaks = Replace(Replace(value, vbCr, " "), vbLf, " ")
End Function

Private Function Setting(key, fallback)
Dim value : value = GetAppSetting(key)
If value = "nothing" Or Len(value) = 0 Then value = fallback
Setting = value
End Function

Private Function IsValidEmail(value)
Dim re : Set re = New RegExp
re.Pattern = "^[^\s@<>,;""]+@[^\s@<>,;""]+\.[A-Za-z]{2,}$"
IsValidEmail = re.Test(value)
End Function

Private Sub AddMessage(ByRef errs, msg)
ReDim Preserve errs(UBound(errs) + 1)
errs(UBound(errs)) = msg
End Sub
End Class

Dim ContactInquiryService_Class__Singleton
Function ContactInquiryService()
If IsEmpty(ContactInquiryService_Class__Singleton) Then
Set ContactInquiryService_Class__Singleton = New ContactInquiryService_Class
End If
Set ContactInquiryService = ContactInquiryService_Class__Singleton
End Function
%>

+ 56
- 0
app/services/EnvService.asp 查看文件

@@ -0,0 +1,56 @@
<%
' Reads KEY=VALUE secrets from the .env file in the project root (outside the
' public web root). Real environment variables take precedence over the file.
Class EnvService_Class
Private values

Private Sub Class_Initialize()
Set values = Server.CreateObject("Scripting.Dictionary")
values.CompareMode = 1
LoadFile
End Sub

Public Function Value(key, fallback)
Dim shell : Set shell = Server.CreateObject("WScript.Shell")
Value = shell.Environment("Process")(key)
Set shell = Nothing
If Len(Value) > 0 Then Exit Function
If values.Exists(key) Then
Value = values(key)
Else
Value = fallback
End If
End Function

Private Sub LoadFile()
Dim fso, path, stream, line, pos, key, val
Set fso = Server.CreateObject("Scripting.FileSystemObject")
path = Server.MapPath("..\.env")
If Not fso.FileExists(path) Then Exit Sub
Set stream = fso.OpenTextFile(path, 1, False)
Do Until stream.AtEndOfStream
line = Trim(stream.ReadLine())
If Len(line) > 0 And Left(line, 1) <> "#" Then
pos = InStr(line, "=")
If pos > 1 Then
key = Trim(Left(line, pos - 1))
val = Trim(Mid(line, pos + 1))
If Len(val) > 1 Then
If (Left(val, 1) = """" And Right(val, 1) = """") Or (Left(val, 1) = "'" And Right(val, 1) = "'") Then val = Mid(val, 2, Len(val) - 2)
End If
values(key) = val
End If
End If
Loop
stream.Close
End Sub
End Class

Dim EnvService_Class__Singleton
Function EnvService()
If IsEmpty(EnvService_Class__Singleton) Then
Set EnvService_Class__Singleton = New EnvService_Class
End If
Set EnvService = EnvService_Class__Singleton
End Function
%>

+ 52
- 20
app/views/Home/landing.asp 查看文件

@@ -30,7 +30,7 @@ Response.CodePage = 65001
<a href="#good-fit">Who it is for</a>
<a href="#faq">FAQ</a>
</nav>
<a class="button button-small button-primary" href="#get-started">Find your spot</a>
<a class="button button-small button-primary" href="#contact">Find your spot</a>
</div>
</header>

@@ -42,9 +42,9 @@ Response.CodePage = 65001
<div class="hero-copy">
<p class="eyebrow"><span></span> Shared direct mail for local businesses</p>
<h1>Make your business <em>impossible to miss</em> in the mailbox.</h1>
<p class="hero-lede">PostcardMojo puts a curated lineup of local businesses on one oversized 9×12 postcard. Your offer gets a bold, tangible place in nearby homes while participating businesses share the cost of printing and postage.</p>
<p class="hero-lede">PostcardMojo puts a curated lineup of local businesses on one oversized 9×12 postcard and is delivered to 5,000 mailboxes. Your offer gets a bold, tangible place in nearby homes while participating businesses share the cost of printing and postage.</p>
<div class="hero-actions">
<a class="button button-primary" href="#get-started">Explore a campaign</a>
<a class="button button-primary" href="#contact">Explore a campaign</a>
<a class="button button-quiet" href="#how-it-works">See how it works <span aria-hidden="true">→</span></a>
</div>
<ul class="hero-points" aria-label="Campaign highlights">
@@ -61,6 +61,44 @@ Response.CodePage = 65001
</div>
<div class="stage-note"><span aria-hidden="true">↗</span> One card. Multiple businesses. A whole neighborhood of opportunity.</div>
</div>

<aside class="contact-card" id="contact" aria-labelledby="contact-title">
<p class="eyebrow eyebrow-on-dark"><span></span> Contact us</p>
<h2 id="contact-title">Interested in a spot on the next card?</h2>
<% If contactSent Then %>
<p class="contact-success" role="status">Thank you! Your message was sent and we will be in touch soon.</p>
<% Else %>
<form class="contact-form" method="post" action="/contact#contact" novalidate>
<% If UBound(contactErrors) >= 0 Then %>
<div class="contact-errors" role="alert">
<ul>
<% Dim contactErrorIndex : For contactErrorIndex = 0 To UBound(contactErrors) %>
<li><%= Server.HTMLEncode(contactErrors(contactErrorIndex)) %></li>
<% Next %>
</ul>
</div>
<% End If %>
<input type="hidden" name="csrf_token" value="<%= Server.HTMLEncode(ContactCsrfToken()) %>">
<div class="contact-trap" aria-hidden="true"><label>Leave this empty <input type="text" name="zq_trap_field" tabindex="-1" autocomplete="off"></label></div>
<label>Name <span aria-hidden="true">*</span>
<input type="text" name="name" maxlength="100" required autocomplete="name" value="<%= Server.HTMLEncode(contactName & "") %>">
</label>
<label>Email <span aria-hidden="true">*</span>
<input type="email" name="email" maxlength="150" required autocomplete="email" value="<%= Server.HTMLEncode(contactEmail & "") %>">
</label>
<label>Phone
<input type="tel" name="phone" maxlength="40" autocomplete="tel" value="<%= Server.HTMLEncode(contactPhone & "") %>">
</label>
<label>Business
<input type="text" name="business" maxlength="150" autocomplete="organization" value="<%= Server.HTMLEncode(contactBusiness & "") %>">
</label>
<label class="contact-wide">How can we help?
<textarea name="message" rows="3" maxlength="2000"><%= Server.HTMLEncode(contactMessage & "") %></textarea>
</label>
<button class="button button-primary" type="submit">Send my interest</button>
</form>
<% End If %>
</aside>
</div>
</section>

@@ -77,33 +115,27 @@ Response.CodePage = 65001
<div class="shell">
<div class="section-heading heading-centered">
<p class="eyebrow eyebrow-centered"><span></span> Simple by design</p>
<h2>From your offer to their mailbox.</h2>
<p>PostcardMojo turns a group of complementary local businesses into one well-organized, high-visibility mail piece.</p>
<h2>Three steps to the mailbox.</h2>
<p>Pick your size, give us your ad, and we handle the printing and mailing.</p>
</div>
<ol class="steps">
<li>
<span class="step-number">01</span>
<div class="step-icon" aria-hidden="true">◎</div>
<h3>Choose your campaign</h3>
<p>Start with the local area and timing that fit your business goals.</p>
<h3>Choose your ad size</h3>
<p>Pick a 3×4 or a 6×8 space on the shared 9×12 postcard.</p>
</li>
<li>
<span class="step-number">02</span>
<div class="step-icon" aria-hidden="true">◇</div>
<h3>Claim your category</h3>
<p>Reserve a space for your type of business, subject to campaign availability.</p>
</li>
<li>
<span class="step-number">03</span>
<div class="step-icon" aria-hidden="true">✦</div>
<h3>Shape the offer</h3>
<p>Bring your logo, message, and promotion together in a clear, focused ad.</p>
<h3>Send your layout or let us design it</h3>
<p>Already have an ad? Send it over. Need one? We will design it for you.</p>
</li>
<li>
<span class="step-number">04</span>
<span class="step-number">03</span>
<div class="step-icon" aria-hidden="true">↗</div>
<h3>Reach the mailbox</h3>
<p>The finished postcard is printed and mailed as one coordinated campaign.</p>
<h3>We mail it out</h3>
<p>We print the postcard and deliver it to mailboxes in your area.</p>
</li>
</ol>
</div>
@@ -235,7 +267,7 @@ Response.CodePage = 65001
<p>Tell us what you do and where you want to be seen. We will help you explore the right PostcardMojo campaign.</p>
</div>
<div class="cta-actions">
<a class="button button-gold" href="mailto:info@postcardmojo.com?subject=PostcardMojo%209x12%20campaign%20inquiry">Request campaign details</a>
<a class="button button-gold" href="#contact">Request campaign details</a>
<span>Campaign availability varies by area.</span>
</div>
</div>
@@ -251,7 +283,7 @@ Response.CodePage = 65001
<nav aria-label="Footer navigation">
<a href="#how-it-works">How it works</a>
<a href="#faq">FAQ</a>
<a href="mailto:info@postcardmojo.com">Contact</a>
<a href="#contact">Contact</a>
</nav>
<small>&copy; <%= Year(Date()) %> PostcardMojo. All rights reserved.</small>
</div>


+ 1
- 1
applicationhost.config 查看文件

@@ -159,7 +159,7 @@
<virtualDirectory path="/" physicalPath="%ASPC_STARTER_ROOT%public" />
</application>
<bindings>
<binding protocol="http" bindingInformation=":8080:localhost" />
<binding protocol="http" bindingInformation=":8082:localhost" />
</bindings>
</site>
<siteDefaults>


+ 1
- 0
public/Default.asp 查看文件

@@ -5,6 +5,7 @@
router.AddRoute "GET", "/", "HomeController", "Index"
router.AddRoute "GET", "", "HomeController", "Index"
router.AddRoute "GET", "/home", "HomeController", "Index"
router.AddRoute "POST", "/contact", "HomeController", "Contact"

' Dispatch the request (resolves route and executes controller action)
MVC.DispatchRequest Request.ServerVariables("REQUEST_METHOD"), _


+ 118
- 2
public/css/home.css 查看文件

@@ -570,7 +570,7 @@ summary {
.steps {
position: relative;
display: grid;
grid-template-columns: repeat(4, 1fr);
grid-template-columns: repeat(3, 1fr);
gap: 18px;
margin: 0;
padding: 0;
@@ -1123,7 +1123,6 @@ summary {
gap: 30px;
}

.steps,
.fit-grid {
grid-template-columns: repeat(2, 1fr);
}
@@ -1428,3 +1427,120 @@ summary {
transition-duration: 0.01ms !important;
}
}

/* Contact card */
.contact-card {
position: relative;
z-index: 2;
padding: clamp(22px, 3vw, 34px);
border-radius: 18px;
background: var(--navy);
color: var(--white);
box-shadow: 0 24px 60px rgba(5, 47, 79, 0.28);
}

.contact-card h2 {
margin: 0 0 18px;
font-family: var(--font-display);
font-size: clamp(1.5rem, 2.4vw, 2rem);
line-height: 1.15;
}

.contact-form {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 14px 16px;
}

.contact-form label {
display: grid;
gap: 6px;
font-size: 0.9rem;
font-weight: 600;
}

.contact-form input,
.contact-form textarea {
width: 100%;
padding: 11px 12px;
border: 1px solid rgba(255, 255, 255, 0.35);
border-radius: 8px;
background: var(--white);
color: var(--ink);
font: inherit;
}

.contact-wide,
.contact-errors,
.contact-form .button {
grid-column: 1 / -1;
}

.contact-form .button {
justify-self: start;
}

.contact-errors {
padding: 10px 14px;
border-radius: 8px;
background: #fde8e8;
color: #8a1c1c;
font-size: 0.92rem;
}

.contact-errors ul {
margin: 0;
padding-left: 18px;
}

.contact-success {
margin: 0;
color: var(--gold);
font-weight: 700;
}

.contact-trap {
position: absolute;
left: -9999px;
}

.hero .hero-grid {
width: min(1640px, calc(100% - 32px));
grid-template-columns: minmax(0, 0.75fr) minmax(0, 1.6fr) minmax(280px, 0.75fr);
grid-template-areas: "copy card contact";
align-items: center;
gap: clamp(20px, 2.5vw, 40px);
}

.hero-grid .hero-copy { grid-area: copy; }
.hero-grid .contact-card { grid-area: contact; }
.hero-grid .postcard-stage { grid-area: card; min-height: 0; }
.hero-grid .postcard { width: 100%; }

.hero-grid h1 { font-size: clamp(2rem, 3vw, 3.1rem); }
.hero-grid .contact-form { grid-template-columns: 1fr; gap: 12px; }
.hero-grid .stage-note { position: static; margin-top: 16px; max-width: none; }

@media (max-width: 1100px) {
.hero .hero-grid {
grid-template-columns: minmax(0, 1fr) minmax(300px, 0.9fr);
grid-template-areas: "copy contact" "card card";
align-items: start;
}

.hero-grid .postcard { width: min(100%, 610px); }
.hero-grid .postcard-stage { min-height: 520px; }
}

@media (max-width: 700px) {
.hero .hero-grid {
grid-template-columns: 1fr;
grid-template-areas: "copy" "contact" "card";
}
}

@media (max-width: 700px) {
.contact-form {
grid-template-columns: 1fr;
}
}

+ 7
- 0
public/web.config 查看文件

@@ -38,6 +38,13 @@

<!-- Cache-bust parameter name (default: "v") -->
<add key="CacheBustParamName" value="v" />

<!-- Contact form email (the mailbox password comes from SMTP_PASSWORD in the project-root .env file) -->
<add key="ContactRecipient" value="covington.daniel@gmail.com" />
<add key="SmtpServer" value="netsol-smtp-oxcs.hostingplatform.com" />
<add key="SmtpPort" value="465" />
<add key="SmtpUsername" value="danielcovington@postcardmojo.com" />
<add key="SmtpUseSsl" value="true" />
</appSettings>

<system.webServer>


正在加载...
取消
保存

Powered by TurnKey Linux.