I'm trying to send an email using nodemailer with a template using handlebars, however, I can't get the template to show on the email. In network, the api that sends the email is showing the correct preview and response (in html) for what should be included in the template, but this information is not being displayed in the sent email. This is my first time doing this, and I have been trying to follow all the steps in examples/ tutorials on how to do this, but nothing seems to be working. I would really appreciate any help or advice on how to get my emails to send with the template. Thank you!
Note: orders.hbs is my handlebars document and is within the views folder
mailer.js
import express from 'express';
import expressAsyncHandler from 'express-async-handler';
import nodemailer from 'nodemailer';
import hbs from 'nodemailer-express-handlebars';
import handlebars from 'handlebars';
import Order from './models/orderModel.js';
import path from 'path';
import fs from 'fs';
const mailerRouter = express.Router();
/*
mailerRouter.engine('handlebar', exphbs());
mailerRouter.set('view engine', 'handlebars');
*/
mailerRouter.post (
'/order',
expressAsyncHandler(async (req, res) => {
const email = req.body.email
const orderId = req.body.orderId
const em = req.body.em;
const sender = req.body.sender;
const orderNum = req.body.orderNum
const emailBody = await Order.findById(orderId).lean()
if (emailBody) {
res.render('orders', {data: emailBody})
}
const sub = `Order: ${orderId}`
let transporter = nodemailer.createTransport({
host: 'smtp.gmail.com',
service: 'gmail',
auth: {
type: 'OAuth2',
user: sender,
pass: '',
clientId: '',
clientSecret: '',
refreshToken: '',
}
})
/*
transporter.use('compile', hbs({
viewEngine: 'express-handlebars',
viewPath:'./views/'
}))*/
const handlebarOptions = {
viewEngine: {
extName: '.hbs',
partialsDir: 'views',
layoutsDir: 'views',
defaultLayout: '',
},
viewPath: './views/',
extName: '.hbs',
};
transporter.use('compile', hbs(handlebarOptions));
const mailOptions = {
from: sender,
to: em,
subject: sub,
text: "Hello"
html: 'hi'
template: 'orders',
}
transporter.sendMail(mailOptions, function (err, info) {
if(err)
console.log(err)
else
console.log(info);
});
}))
export default mailerRouter;
server.js
import express from 'express';
import cors from 'cors';
import mongoose from 'mongoose';
import dotenv from 'dotenv';
import path from 'path';
import hbs from 'express-handlebars';
import orderRouter from './routers/orderRouter.js';
import mailerRouter from './mailer.js';
const app = express();
app.use(cors());
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
mongoose.connect(process.env.MONGODB_URL || 'mongodb://localhost/AM', {
useNewUrlParser: true,
useUnifiedTopology: true,
useCreateIndex: true,
});
app.use('/api/orders', orderRouter);
const __dirname = path.resolve();
app.use('/api/mailer', mailerRouter);
app.engine('hbs', hbs({
extname: 'hbs',
defaultLayout: false,
layoutDir:__dirname+'/backend/views/layouts/',
partialsDir: __dirname + '/backend/views/layouts',
}));
app.set('views', path.join( __dirname, '/backend/views/layouts'));
app.set('view engine', 'hbs');
app.get('/', (req, res) => {
res.send('Server is ready');
});
app.use((err, req, res, next) => {
res.status(500).send({ message: err.message });
});
const port = process.env.PORT || 5000;
app.listen(port, () => {
console.log(`Serve at http://localhost:${port}`);
});
Edit:
So, if I have:
...
const __dirname = path.resolve();
const emailTemplateSource = fs.readFileSync(path.join(__dirname, "backend/views/orders.hbs"), "utf8")
const mailOptions = {
from: sender,
to: em,
subject: sub,
text: '',
html: emailTemplateSource,
// template: emailTemplateSource,
...
The email body shows:
{{#each data.orderItems}}
{{name}}
{{/each}}
Instead of the names of the items. I have also tried adding const temp = handlebars.compile(emailTemplateSource) after const emailTemplateSource and then changing it to html: temp, but this sends a blank email. I have also tried adding these after template: , but no matter what I put there nothing seems to show up.
Additional Info:
console.log(emailTemplateSource)
Gives :
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
<style>
</style>
</head>
<body>
{{#each data.orderItems}}
<div>
<h3>{{name}}</h1>
</div>
{{/each}}
</body>
</html>
And
const temp = handlebars.compile(emailTemplateSource) console.log(temp)
Gives: Function: ret] { _setup: [Function (anonymous)], _child: [Function (anonymous)]}
Related
So i had Container Component that pass props to the meta
export default function Container({ children, ...customMeta }) {
const [mounted, setMounted] = useState(false);
const { resolvedTheme, setTheme } = useTheme();
// After mounting, we have access to the theme
useEffect(() => setMounted(true), []);
// const { children, ...customMeta } = props;
const router = useRouter();
const meta = {
title: 'title',
description: `description`,
image: '',
type: 'website',
...customMeta
};
return (
<div className="bg-gray-50 dark:bg-gray-900">
<Head>
<title>{meta.title}</title>
<meta name="robots" content="follow, index" />
<meta content={meta.description} name="description" />
.....
On development, the title showed up, but on production it didn't. It just showed the url.
I had tried to remove all fetch error but it still not work. Any help?
We have an existing app, where the root "/" gets redirected to "/search" by default. It's been working fine via our next-redirects.js file:
async function redirects() {
return [
{
source: '/',
destination: '/search',
permanent: true,
},
];
}
I have to implement translation to the app, using next-i18next, so that we can have translated text + routing out of the box with NextJS. I have followed the steps in the next-i8next docs. I added the next-i18next.config.js file as:
const path = require('path');
module.exports = {
i18n: {
defaultLocale: 'en',
locales: ['en', 'es'],
},
localePath: path.resolve('./public/static/locales'), // custom path file route
};
And the next.config looks like:
const { i18n } = require('./next-i18next.config');
const defaultConfig = {
env: {
SOME_ENV,
},
images: {
deviceSizes: [980],
domains: [
'd1',
'd2',
],
},
i18n,
redirects: require('./next-redirects'),
webpack: (config, options) => {
if (!options.isServer) {
config.resolve.alias['#sentry/node'] = '#sentry/browser';
}
if (
NODE_ENV === 'production'
) {
config.plugins.push(
new SentryWebpackPlugin({
include: '.next',
ignore: ['node_modules'],
urlPrefix: '~/_next',
release: VERCEL_GITHUB_COMMIT_SHA,
})
);
}
return config;
},
};
module.exports = withPlugins([withSourceMaps], defaultConfig);
We have a custom _app file getting wrapped with the appWithTranslation HOC, and it's setup with the getInitialProps, per nextJS docs:
function MyApp({ Component, pageProps }) {
const [mounted, setMounted] = useState(false);
useEffect(() => {
// Remove the server-side injected CSS.
const jssStyles = document.querySelector('#jss-server-side');
if (jssStyles) {
jssStyles.parentNode.removeChild(jssStyles);
}
TagManager.initialize(tagManagerArgs);
setMounted(true);
}, []);
const Layout = Component.Layout || Page;
return (
<>
<Head>
<link rel="icon" href="/favicon.png" type="image/ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
</Head>
<AppProviders>
<Context {...pageProps}>
<Layout {...pageProps}>
<>
<Component {...pageProps} />
<Feedback />
<PageLoader />
</>
</Layout>
</Context>
</AppProviders>
</>
);
}
MyApp.getInitialProps = async ({ Component, ctx }) => {
let pageProps = {};
if (Component.getInitialProps) {
pageProps = await Component.getInitialProps({ ctx });
}
const cookies = Cookie.parse(ctx?.req?.headers?.cookie || '');
if (Object.keys(cookies).length) {
const { token } = JSON.parse(cookies?.user || '{}');
let user = null;
if (token) {
const { data } = await get('api/users', { token });
if (data) {
user = data;
user.token = token;
}
}
pageProps.user = user;
pageProps.cart = cookies?.cart;
pageProps.defaultBilling = cookies?.defaultBilling;
pageProps.reservationEstimateItem = cookies?.reservationEstimateItem;
pageProps.reservationEstimate = cookies?.reservationEstimate;
}
return { pageProps };
};
export default appWithTranslation(MyApp);
And we have our _document file to handle some Emotion theming:
export default class MyDocument extends Document {
static async getInitialProps(ctx) {
const initialProps = await Document.getInitialProps(ctx);
const styles = extractCritical(initialProps.html);
return {
...initialProps,
styles: (
<>
{initialProps.styles}
<style
data-emotion-css={styles.ids.join(' ')}
// eslint-disable-next-line react/no-danger
dangerouslySetInnerHTML={{ __html: styles.css }}
/>
</>
),
};
}
render() {
return (
<Html lang="en">
<Head />
<body>
<Main />
<NextScript />
<script
type="text/javascript"
src="https://js.stripe.com/v2/"
async
/>
</body>
</Html>
);
}
}
// `getInitialProps` belongs to `_document` (instead of `_app`),
// it's compatible with server-side generation (SSG).
MyDocument.getInitialProps = async ctx => {
const sheets = new ServerStyleSheets();
const originalRenderPage = ctx.renderPage;
ctx.renderPage = () =>
originalRenderPage({
enhanceApp: App => props => sheets.collect(<App {...props} />),
});
const initialProps = await Document.getInitialProps(ctx);
return {
...initialProps,
// Styles fragment is rendered after the app and page rendering finish.
styles: [
...React.Children.toArray(initialProps.styles),
sheets.getStyleElement(),
],
};
};
At this point the redirect logic should continue to navigate to the search page which is setup like so:
export const SearchPage = () => {
const router = useRouter();
const { t } = useTranslation('search');
return (
<>
<Head>
<title>{`${t('searchTitle')}`}</title>
<meta
property="og:title"
content={`${t('searchTitle')}`}
key="title"
/>
<meta
name="description"
content={t('metaDescription')}
/>
</Head>
<Search />
</>
);
};
SearchPage.namespace = 'SearchPage';
export const getStaticPaths = () => ({
paths: [], // indicates that no page needs be created at build time
fallback: 'blocking' // indicates the type of fallback
});
export const getStaticProps = async ({ locale }) => ({
// exposes `_nextI18Next` as props which includes our namespaced files
props: {
...await serverSideTranslations(locale, ['common', 'search']),
}
});
export default SearchPage;
The search page has the getStaticPaths & getStaticProps functions, as needed on ALL page level files, per next-i18next.
Why does this setup no longer work with the redirect?
There are no errors in the terminal.
The network tab shows a 404 error on the root route of "/"
which implies the re-writing is not working. But what about the i18n makes this not behave?
Is it something in the _app or _document files?
If I navigate to /search directly, it loads fine, so the page routes are OK it seems.
Other notes:
NextJS "next": "^10.0.2",
next-i18next "next-i18next": "^7.0.1",
There seems to be some possible issues with Next & Locales...
https://github.com/vercel/next.js/issues/20488
https://github.com/vercel/next.js/issues/18349
My workaround is not pretty, but it works:
Delete the original next-rewrite
Add a new index.js page file that handles the redirect in the getServerSideProps:
const Index = () => null;
export async function getServerSideProps({ locale }) {
return {
redirect: {
destination: `${locale !== 'en' ? `/${locale}` : ''}/search`,
permanent: true,
},
};
}
export default Index;
Using express-handlebars#5.2.0 and express#4.17.1 using node on windows and still cant get it to render in the body:
File Setup:
testapp.js
[views]
main.handlebars
[views][layouts]
test.handlebars
//testapp.js
const express = require('express');
const exhbr = require('express-handlebars');
const app = express();
const port = 3031;
app.engine('handlebars', exhbr({}));
app.use(express.static('public'));
app.set('view engine', 'handlebars');
app.get('/', (req, res) => {
res.render('main', { layout: 'test' });
});
app.listen(port, () => console.log(`App listening to port ${port}`));
<!--test.handlebars-->
<h1>Test</h1>
<!-- main.handlebars -->
<!DOCTYPE html>
<html lang="en">
<body>
{{{body}}}
</body>
</html>
Only <h1>Test</h1> is returned and hasn't been inserted into the body.
I have also tried the following just to make sure its looking in the right spots.
res.render('doesnt_exist');
Error: Failed to lookup view "doesnt_exist" in views directory "c:\wamp\www\rethinkdb_rest\views\"
res.render('main', { layout: "doesnt_exist" });
Error: ENOENT: no such file or directory, open 'c:\wamp\www\rethinkdb_rest\views\layouts\doesnt_exist.handlebars'
Looks like I was putting the main.handlebars and test.handlebars in the wrong directories.
File Setup should be:
testapp.js
[views]
test.handlebars
[views][layouts]
main.handlebars
And the render function in testapp.js should be:
//testapp.js
const express = require('express');
const exhbr = require('express-handlebars');
const app = express();
const port = 3031;
app.engine('handlebars', exhbr({}));
app.use(express.static('public'));
app.set('view engine', 'handlebars');
app.get('/', (req, res) => {
res.render('test', { layout: 'main' }); //<----fixed
});
app.listen(port, () => console.log(`App listening to port ${port}`));
I tried to find a way to change the language by changing the site's sub-path in the next-i18next package, I searched the Internet (https://github.com/isaachinman/next-i18next/issues/32 , https://github.com/i18next/i18next-browser-languageDetector#detector-options) for an answer to this question, but it did not work. After changing the subpath in the url it is duplicated and redirects me to a page that does not exist.
my code:
// path-to-my-project/i18n.js
const NextI18Next = require('next-i18next').default;
const i18nextBrowserLanguageDetector = require('i18next-browser-languagedetector').default;
const { localeSubpaths } = require('next/config').default().publicRuntimeConfig;
const path = require('path');
module.exports = new NextI18Next({
otherLanguages: ['ru'],
defaultNS: 'common',
localeSubpaths,
localePath: path.resolve('./public/static/locales'),
use: [i18nextBrowserLanguageDetector],
});
// path-to-my-project/pages/_app.js
import '../styles/main.scss';
import NProgress from 'nprogress';
import 'nprogress/nprogress.css';
import Router from 'next/router';
import App from 'next/app';
import { appWithTranslation } from '../i18n';
Router.events.on('routeChangeStart', () => NProgress.start());
Router.events.on('routeChangeComplete', () => NProgress.done());
Router.events.on('routeChangeError', () => NProgress.done());
const MyApp = ({ Component, pageProps }) => (
<Component {...pageProps} />
);
MyApp.getInitialProps = async (appContext) => ({ ...await App.getInitialProps(appContext) });
export default appWithTranslation(MyApp);
maybe I just missed something, because it's my first project on next.js, so I'm asking for help in the community and would be grateful for any help or hint.
By default next-i18next will try to detect the language to show from users browser.
Try to disable it.
const NextI18Next = require('next-i18next').default
const { localeSubpaths } = require('next/config').default().publicRuntimeConfig
const path = require('path')
module.exports = new NextI18Next({
browserLanguageDetection: false, // <---
serverLanguageDetection: false, // <---
otherLanguages: ['de'],
localeSubpaths,
localePath: path.resolve('./public/static/locales')
})
in file next.config.js i have this settings:
// path/to/project/next.config.js
const { nextI18NextRewrites } = require('next-i18next/rewrites');
const localeSubpaths = {
ru: 'ru',
};
module.exports = {
rewrites: async () => nextI18NextRewrites(localeSubpaths),
publicRuntimeConfig: {
localeSubpaths,
},
devIndicators: {
autoPrerender: false,
},
};
but there was not enough configuration for English localization, so you just need to add it:
// path/to/project/next.config.js
const { nextI18NextRewrites } = require('next-i18next/rewrites');
const localeSubpaths = {
en: 'en', // <------
ru: 'ru',
};
module.exports = {
rewrites: async () => nextI18NextRewrites(localeSubpaths),
publicRuntimeConfig: {
localeSubpaths,
},
devIndicators: {
autoPrerender: false,
},
};
Refused to apply style from 'http://localhost:3001/css/style.css'
because its MIME type ('text/html') is not a supported stylesheet MIME
type, and strict MIME checking is enabled.
I know this has been discussed here 'style sheet not loading because of MIME type' but I cannot solve my issue using the tips provided.
I am honestly quite lost as to what to do... did anyone have any other suggests that were previously not mentioned in the other post?
client directoy
layouts
default.hbs
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<link href="https://fonts.googleapis.com/css?family=Nunito+Sans:400,600,700" rel="stylesheet">
<link rel="stylesheet" href="/css/style.css">
<!--<link rel="shortcut icon" href="img/favicon.png" type="image/x-icon">-->
<title>Spoty</title>
<title>{{title}}</title>
</head>
<body>
{{{body}}}
</body>
</html>
root directory
index.js
'use strict';
const server = require('./server')();
const config = require('./config');
server.create(config);
server.start();
server directory
index.js
'use strict';
const express = require('express');
const bodyParser = require('body-parser');
const expressHandlebars = require('express-handlebars');
module.exports = () => {
let server = express();
let create;
let start;
// creates server
create = (config) => {
let routes = require('./routes');
// server settings
server.set('env', config.env);
server.set('port', config.port);
server.set('hostname', config.hostname);
server.set('viewDir', config.viewDir);
// middleware for parsing json
server.use(bodyParser.json());
// Setup view engine
server.engine('.hbs', expressHandlebars({
defaultLayout: 'default',
layoutsDir: config.viewDir + '/layouts',
extname: '.hbs'
}));
server.set('views', server.get('viewDir'));
server.set('view engine', '.hbs');
server.use(express.static(__dirname + '/client'));
// Set up routes
routes.init(server);
};
// start server
start = () => {
let hostname = server.get('hostname');
let port = server.get('port');
server.listen(port, () => {
console.log('Express server listening on - http://' + hostname + ':' + port);
});
};
return {
create, start
}
};
server/routes
index.js
const homeRoute = require('./home');
function init(server) {
server.get('/', function (req, res) {
console.log('error here');
res.redirect('/home');
});
server.use('/home',homeRoute);
}
module.exports = {
init
};
home.js
const express = require('express');
const homeController = require('../controllers/home');
let router = express.Router();
router.get('/',homeController.index);
module.exports = router;
server/controllers
home.js
function index(req, res) {
res.render('home/index', {
title: 'Home Page'
});
}
module.exports = {
index
};
package.json
{
"name": "example",
"version": "0.0.0",
"private": true,
"scripts": {
"watch:sass": "node-sass /client/sass/main.scss /client/css/style.css -w",
"devserver": "live-server",
"start": " node index.js npm-run-all --parallel devserver watch:sass",
"compile:sass": "node-sass sass/main.scss css/style.comp.css",
"build:css": "npm-run-all compile:sass"
},
"dependencies": {
"body-parser": "^1.18.3",
"cookie-parser": "~1.4.3",
"debug": "~2.6.9",
"express": "~4.16.0",
"express-handlebars": "^3.0.0",
"hbs": "^4.0.1",
"http-errors": "~1.6.2",
"jade": "~1.11.0",
"lodash": "^4.17.10",
"morgan": "~1.9.0"
},
"devDependencies": {
"nodemon": "^1.18.3",
"autoprefixer": "^8.6.4",
"concat": "^1.0.3",
"node-sass": "^4.9.0",
"npm-run-all": "^4.1.3",
"postcss-cli": "^5.0.1"
}
}
config
index.js
const _ = require('lodash');
const env = process.env.NODE_ENV || 'local';
const envConfig = require(`./${env}`);
const defaultCofig = env;
module.exports = _.merge(defaultCofig, envConfig);
local.js
let localConfig = {
hostname: 'localhost',
port: 3001,
viewDir: './client/views'
};
module.exports = localConfig;
git repo: https://github.com/rostgoat/NodeExpress