I'm working on a Meteor app which uses ms-seo package. I was wondering if there is a way to make URLs more SEO friendly?
Router.route('/item/:_id', {
name: 'item.detail',
controller: 'ItemsController',
action: 'detail',
where: 'client',
onAfterAction: function() {
var data = this.data();
if (data) {
SEO.set({
title: data.title + ' - ' + data.company + ' (' + data.city + ')',
meta: {
'description': data.descriptionHTML
}
});
}
});
While this works perfect, the URL it produces is /item/5RTxofPPn3LwifP24, I would like to push data.title up in the url, so I can get /item/i-am-a-lower-case-dash-replaced-unique-title/
Are there packages for that?
You need to create a slug. So your collection will have fields like:
_id
title
slug
content
Then to make your slug you can use something like https://atmospherejs.com/yasaricli/slugify to convert your title into a slug. Basically what it does is convert a title called "Unique Shopping Cart Item" to "unique-shopping-cart-item."
Then in your router you pass the slug in as your parameter.
Router.route('/blog/:slug',{
name:'blogPosts',
waitOn: function() { return Meteor.subscribe('collection'); },
data: function(){
var slug = this.params.slug;
return Collection.findOne({slug:slug});
// this is saying search the collection's slug for the passed in parameter which we're also calling "slug"
}
});
You can try slugify to push the data.title as a pretty url.
Related
Is it possible to alter (add more fields for each record) a returned collection in onBeforeAction or similar hook?
I have InvoiceHistory collection which I am paginating through. I also want to display as part of each invoice the company name, business address, email address and VAT registration number - and these four fields are stored in another collection. So I would like to add these four fields to each record returned from InvoiceHistory. If there is another way to do it I am open to suggestions. I am using Alethes meteor pagination which loses the helper fields returned in its itemTemplate when you navigate/browse/page to the second page. Alethes pagination also relies on iron-router, is it maybe possible to achieve what I want with the help of iron-router
========================================================================
#Sean. Thanks. Below is the code that uses meteor publish composite:
if (Meteor.isServer) {
import { publishComposite } from 'meteor/reywood:publish-composite';
publishComposite('invoicesWithCompanyDetails', function(userId, startingDate,endingDate) {
return {
find() {
// Find purchase history for userId and the two dates that were entered. Note arguments for callback function
// being used in query.
return PurchaseHistory.find({Id:userId,transactionDate:{$gte:new Date(decodeURIComponent(startingDate)),
$lt:new Date(decodeURIComponent(endingDate))}});
},
children: [
{
find() {
return CompanySharedNumbers.find(
{ _id:"firstOccurrence" },
{ fields: { companyName: 1, companyAddress: 1 } }
);
}
}
]
}
});
}
Router.route('/store/invoices/:_username/:startingDate/:endingDate', { //:_startingDate/:_endingDate
name: 'invoices',
template: 'invoices',
onBeforeAction: function()
{
...
},
waitOn: function() {
var startingDate = this.params.startingDate;
var endingDate = this.params.endingDate;
return [Meteor.subscribe('systemInfo'),Meteor.subscribe('testingOn'),Meteor.subscribe('invoicesWithCompanyDetails',startingDate,endingDate)];
}
});
Pages = new Meteor.Pagination(PurchaseHistory, {
itemTemplate: "invoice",
availableSettings: {filters: true},
filters: {},
route: "/store/invoices/:_username/:startingDate/:endingDate/",
router: "iron-router",
routerTemplate: "invoices",
routerLayout: "main",
sort: {
transactionDate: 1
},
perPage: 1,
templateName: "invoices",
homeRoute:"home"
});
Instead of adding new fields to all your document. What you could do is to add a helper to load a company document :
Template.OneInvoice.helpers({
loadCompany(companyId){
return Company.findOne({_id: companyId});
}
});
And use it in your template code:
<template name="OneInvoice">
<p>Invoice id: {{invoice._id}}</p>
<p>Invoice total: {{invoice.total}}</p>
{{#let company=(loadCompany invoice.companyId)}}
<p>Company name: {{company.name}}</p>
<p>Company business address: {{company.businessAddress}}</p>
{{/let}}
</template>
This way, the code is very simple and easily maintainable.
If I'm understanding you correctly, this sounds like a job for a composite publication.
Using a composite publication you can return related data from multiple collections at the same time.
So, you can find all your invoices, then using the company ID stored in the invoice objects you can return the company name and other info from the Companies collection at the same time.
I've used composite publications before for complex data structures and they worked perfectly.
Hope that helps
I have the following route defined in my iron-router:
this.route("/example/:id", {
name: "example",
template: "example",
action: function () {
this.wait(Meteor.subscribe('sub1', this.params.id));
this.wait(Meteor.subscribe('sub2', <<data of sub1 needed here>>));
if (this.ready()) {
this.render();
} else {
this.render('Loading');
}
}
});
I want to wait for sub1 and sub2 before rendering my actual template. The problem is that I need a piece of data which is part of the result of sub1 for the sub2 subscription.
How can I wait sequential for subscriptions? So that I can split the wait in two steps and wait for my first subscription to be finished. Then start the second subscription and then set this.ready() to render the template?
A workaround that I thought of was to use Reactive-Vars for the subscriptions and dont use .wait and .ready which is provided by iron-router. But I would like to use a more convenient solution provided by iron-router or Meteor itself. Do you know a better solution for this?
Thanks for your answers!
Publish Composite Package:
If the second subscription is reactively dependent on certain fields from the first dataset -- and if there will be a many-to-many "join" association, it might be worth looking into reywood:publish-composite package:
It provides a clean and easy way to manage associated subscriptions for collections with hierarchical relations.
Publication:
Meteor.publishComposite('compositeSub', function(id) {
return {
find: function() {
// return all documents from FirstCollection filtered by 'this.params.id' passed to subscription
return FirstCollection.find({ _id: id });
},
children: [
find: function(item) {
// return data from second collection filtered by using reference of each item's _id from results of first subscription
// you can also use any other field from 'item' as reference here, as per your database relations
return SecondCollection.find({ itemId: item._id });
}
]
}
});
Subscription:
Then you can just subscribe in the router using:
Meteor.subscribe('compositeSub', this.params.id);
Router hooks:
As a suggestion, hooks in iron-router are really useful, as they take care of a lot of things for you. So why not use the waitOn hook that manages this.wait and loading states neatly?
this.route('/example/:id', {
name: "example",
template: "example",
// this template will be rendered until the subscriptions are ready
loadingTemplate: 'loading',
waitOn: function () {
// return one handle, a function, or an array
return Meteor.subscribe('compositeSub', this.params.id);
// FYI, this can also return an array of subscriptions
},
action: function () {
this.render();
}
});
You can use the configure option to add a template for loading event:
Router.configure({
layoutTemplate: 'layout',
loadingTemplate: 'loading'
});
Note regarding the comment in question:
If both subscriptions only depend on the same id parameter passed to it, you can use the following, as mentioned by #abj27 in the comment above -- however, this does not seem to be the case, going by your example:
Publication:
Meteor.publish("subOneAndTwo", function (exampleId) {
check(exampleId, String);
return [
FirstCollection.find({ _id: exampleId });
SecondCollection.find({ firstId: exampleId })
];
});
Subscription:
Meteor.subscribe('subOneAndTwo', this.params.id);
So just check what you need and use a solution accordingly.
https://github.com/kadirahq/subs-manager
With this package, you can assign a subscription to a variable. Then, you can check that variable's ready state. I just got this working... after years of trying to understand.
Here is my code snippet that works, but oddly I had to wrap it in a 1ms timeout to work...
```
Router.route('/activity/:activityId', function (params) {
var params = this.params;
setTimeout(function(){
var thePage = window.location.href.split("/");;
window.thePage = thePage[4];
dbSubscriptionActivites.clear();
window.thisDbSubscriptionActivites = "";
window.thisDbSubscriptionActivites = dbSubscriptionActivites.subscribe("activityByPage", window.thePage);
Tracker.autorun(function() {
if(window.thisDbSubscriptionActivites.ready()) {
dbSubscriptionComments.clear();
window.thisDbSubscriptionComments = "";
window.thisDbSubscriptionComments = dbSubscriptionComments.subscribe('fetchComments', "activity", Activities.findOne({})._id);
BlazeLayout.render("activity");
$('body').removeClass("shards-app-promo-page--1");
}
});
},1); // Must wait for DOM?
});
```
Examine: window.thisDbSubscriptionActivites = dbSubscriptionActivites.subscribe("activityByPage", window.thePage);
I'm setting as a window variable, but you could do a const mySub = ...
Then, you check that in the autorun function later.
You can see there is where I am doing subscriptions.
I suppose I really should move the BlazeLayout render in to another .ready() check for the comments.
this is the router code
Router.route('screens', {
path: '/screenshots/:_id',
template: 'screens',
onBeforeAction: function(){
Session.set( "currentRoute", "screens" );
Session.set("screenshots", this.params._id);
this.next();
}
});
this is the helper for screenshots template
Template.screens.helpers({
ss: function () {
var screenshots = Session.get("screenshots");
return Products.findOne({ _id: screenshots});
}
});
and am calling it here
<h4>Click to view the Screenshots
When i click to view the screenshots URL, the URL should be this /screenshots/:_id based on my router configuration, but what i see in the browser is /screenshots/ without the _id and the page shows 404 - NOT FOUND.
Is it possible to create nested routes?
because before i click on the link that executes the above route. i will be in this route
Router.route('itemDetails', {
path: '/item/:_id',
template: 'itemDetails',
onBeforeAction: function(){
Session.set( "currentRoute", "itemDetails" );
Session.set("itemId", this.params._id);
this.next();
}
});
and this route works fine i can see the item _id, is it possible to create another route inside it that has for example this path /item/:_id/screenshots?
I have the _id stored in Session.get("itemId"). Is it possible to call it in the path of the route somehow?
I tried '/item' + '/screenshots' + '/' + Session.get("itemId") but didn't work
or there is other way to solve it?
The problem is not with the code in the question, the 404 page is occurring due to it not being passed an id into the path, the browser says /screenshots/ and not /screenshots/randomId because it is only being passed that from the link.
As per additions to the question and chat with Behrouz: Because the value is stored in session we can use
Template.registerHelper('session',function(input){
return Session.get(input);
});
to register a global template helper called session which can be called with {{session session_var_name}} and create the link as below:
<h4>Click to view the Screenshots
With Meteor 1.0.3.1 and Iron Router, I need to set the title dynamically for some pages, while defaulting to a certain title for other pages, using Manuel Schoebel's SEO package. How can I accomplish setting a dynamic page title for a certain route?
I've set SEO up generally like this:
Meteor.startup(->
[...]
SEO.config({
title: 'MusitechHub'
meta: {
'description': 'The hub for finding and publishing music technology projects'
}
})
undefined
)
As stated in the package README, you can use an iron:router onAfterAction hook to dynamically set the title to whatever computed value you want :
Router.route("/post/:slug", {
onAfterAction: function() {
var post = Posts.findOne({
slug: this.params.slug
});
SEO.set({
title: post.title
});
}
});
I'm trying to add attachments to a custom post type that hasn't editor support (only excerpt).
I've managed to show the Media Manager dialog box, but I can only see the "Insert into post" button (that does nothing anyway) and when uploading images, they don't get attached to the post.
To implement what I did so far, I've added a very simple meta box to the post type:
function add_gallery_post_media_meta_box()
{
add_meta_box(
'gallery_post_media',
'Gallery Media',
'gallery_post_media',
'gallery',
'side',
'high'
);
} // add_file_meta_box
add_action('add_meta_boxes', 'add_gallery_post_media_meta_box');
function gallery_post_media()
{
echo '' . __('Add media') .'';
} // end post_media
function register_admin_scripts() {
wp_enqueue_media();
wp_register_script( 'gallery_post_media_admin_script', get_template_directory_uri() . '/library/cpt/gallery.js' );
wp_enqueue_script( 'gallery_post_media_admin_script' );
} // end register_scripts
add_action( 'admin_enqueue_scripts', 'register_admin_scripts' );
And the script:
jQuery(document).ready(function ($) {
$('#gallery-add-media').click(function (e) {
var send_attachment_bkp = wp.media.editor.send.attachment;
var button = $(this);
var id = button.attr('id').replace('_button', '');
wp.media.editor.send.attachment = function (props, attachment) {
$("#" + id).val(attachment.url);
wp.media.editor.send.attachment = send_attachment_bkp;
}
wp.media.editor.open(button);
event.preventDefault();
return false;
});
});
If I would able to find some documentation about wp.media.editor.send.attachment, I'd probably manage to get what I want, but I can't find anything useful.
The only solutions I've found all relies on custom fields, instead I want to simply attach these images to the post, without inserting them in the post content, as I would do with normal posts.
As a side question: is it possible to tell the Media Manager to only accept images?
This is the JavaScript I use for media fields. Once you hit insert you can do whatever you want with the data from the image's selected
jQuery(document).ready(function() {
//uploading files variable
var custom_file_frame;
jQuery(document).on('click', '.meida-manager', function(event) {
event.preventDefault();
$this = jQuery(this);
//If the frame already exists, reopen it
if (typeof(custom_file_frame)!=="undefined") {
custom_file_frame.close();
}
//Create WP media frame.
custom_file_frame = wp.media.frames.customHeader = wp.media({
//Title of media manager frame
title: "Sample title of WP Media Uploader Frame",
library: {
type: 'image'
},
button: {
//Button text
text: "insert text"
},
//Do not allow multiple files, if you want multiple, set true
multiple: false
});
//callback for selected image
custom_file_frame.on('select', function() {
var attachment = custom_file_frame.state().get('selection').first().toJSON();
//do something with attachment variable, for example attachment.filename
//Object:
//attachment.alt - image alt
//attachment.author - author id
//attachment.caption
//attachment.dateFormatted - date of image uploaded
//attachment.description
//attachment.editLink - edit link of media
//attachment.filename
//attachment.height
//attachment.icon - don't know WTF?))
//attachment.id - id of attachment
//attachment.link - public link of attachment, for example ""http://site.com/?attachment_id=115""
//attachment.menuOrder
//attachment.mime - mime type, for example image/jpeg"
//attachment.name - name of attachment file, for example "my-image"
//attachment.status - usual is "inherit"
//attachment.subtype - "jpeg" if is "jpg"
//attachment.title
//attachment.type - "image"
//attachment.uploadedTo
//attachment.url - http url of image, for example "http://site.com/wp-content/uploads/2012/12/my-image.jpg"
//attachment.width
$this.val(attachment.url);
$this.siblings('img').attr('src',attachment.url);
});
//Open modal
custom_file_frame.open();
});
});