4
\$\begingroup\$

I am working on a blog application with Codeigniter 3.1.8 and AngularJS v1.7.8.

The Dashboard of the application is "pure" Codeigniter, with Models, Controllers, and views, while the fronted is made up of JSONs managed and displayed by AngularJS.

I operated this clear separation between back-end and front-end in order to enable "themeing".

My app.js file:

angular.module('app', [
    'ngRoute',
    'app.controllers',
    'ngSanitize'
]).config(['$routeProvider', '$locationProvider', function($routeProvider, $locationProvider){
    $routeProvider.when('/', {
        templateUrl: 'themes/caminar/templates/posts.html',
        controller: 'PostsController'

    }).when('/posts/byauthor/:author_id', {
        templateUrl: 'themes/caminar/templates/posts.html',
        controller: 'PostsByAurhorController'

    }).when('/categories/posts/:cat_id', {
        templateUrl: 'themes/caminar/templates/posts.html',
        controller: 'PostsByCategoryController'

    }).when('/posts/search', {
        templateUrl: 'themes/caminar/templates/posts.html',
        controller: 'PostsSearchController'

    }).when('/:slug', {
        templateUrl: 'themes/caminar/templates/singlepost.html',
        controller: 'SinglePostController'

    }).when('/comments/create/', {
        templateUrl: 'themes/caminar/templates/singlepost.html',
        controller: 'PostCommentController'

    }).when('/pages/page/:id', {
        templateUrl: 'themes/caminar/templates/page.html',
        controller: 'PageController'

    }).otherwise({
        redirectTo: '/'
    })

    // Enable HTML5 mode
    $locationProvider.html5Mode({
      enabled: true,
      requireBase: true
    });

}])

// Date parser filter
.filter('dateParse', function() {
  return function(date) {
    return Date.parse(date);
  };
});

The controllers.js file contains all the front-end controllers:

angular.module('app.controllers', [])
    .controller('MainController', ['$scope', '$http', function($scope, $http) {
        $http.get('api').then(function(response) {

            //Site name
            $scope.siteTitle = response.data.site_title;

            // Tagline
            $scope.tagline = response.data.tagline;

            //Company name
            $scope.companyName = response.data.company_name;

            //Company email
            $scope.company_email = response.data.company_email;

            //Facebook
            $scope.facebook = response.data.facebook;

            //Instagram
            $scope.instagram = response.data.instagram;

            //Twitter
            $scope.twitter = response.data.twitter;

            // Pages
            $scope.pages = response.data.pages;

        });

    }])

    // All posts
    .controller('PostsController', ['$scope', '$http', function($scope, $http) {

        //Get current page (?page=2, ?page=3 etc)
        const currPage = window.location.search;

        // Get all the posts on the current page 
        $http.get('api/' + currPage).then(function(response) {

            //Categories
            $scope.categories = response.data.categories;

            // Posts
            $scope.posts = response.data.posts;

            // posts pagination
            $scope.pagination = response.data.pagination;

        });
    }])

    // Search posts
    .controller('PostsSearchController', ['$scope', '$http', '$routeParams', function($scope, $http, $routeParams) {

        //Get search term
        const searchTerm = window.location.search;

        $http.get('api/posts/search' + searchTerm).then(function(response) {

            //Categories
            $scope.categories = response.data.categories;

            // Posts
            $scope.posts = response.data.posts;

            // posts pagination
            $scope.pagination = response.data.pagination;

        });
    }])


    // Posts by Category
    .controller('PostsByCategoryController', ['$scope', '$http', '$routeParams', function($scope, $http, $routeParams) {

        //Get Category id
        const cat_id = $routeParams.cat_id;

        //Get current page (?page=2, ?page=3 etc)
        const currPage = window.location.search;

        $http.get('api/categories/posts/' + cat_id + currPage).then(function(response) {

            //Categories
            $scope.categories = response.data.categories;

            // Posts
            $scope.posts = response.data.posts;

            // posts pagination
            $scope.pagination = response.data.pagination;

        });
    }])

    // Posts by author
    .controller('PostsByAurhorController', ['$scope', '$http', '$routeParams', function($scope, $http, $routeParams) {

        //Get author id
        const author_id = $routeParams.author_id;

        //Get current page (?page=2, ?page=3 etc)
        const currPage = window.location.search;

        $http.get('api/posts/byauthor/' + author_id + currPage).then(function(response) {

            //Categories
            $scope.categories = response.data.categories;

            // Posts
            $scope.posts = response.data.posts;

            // posts pagination
            $scope.pagination = response.data.pagination;

        });
    }])

    // Single post
    .controller('SinglePostController', ['$scope', '$http', '$routeParams', function($scope, $http, $routeParams) {
        const slug = $routeParams.slug;
        $http.get('api/' + slug).then(function(response) {

            //Single post
            $scope.post = response.data.post;

            //Comments
            $scope.comments = response.data.comments;

        });
    }])

    // Post comment
    .controller('PostCommentController', ['$scope', '$http', '$routeParams', '$timeout', function($scope, $http, $routeParams, $timeout) {

        const slug = $routeParams.slug;

        $http.get('api/' + slug).then(function(response) {

            //Current post id
            let post_id = response.data.post.id;

            $scope.commentSubmitted = false;

            $scope.newComment = {
                slug: slug,
                post_id: post_id,
                name: $scope.name,
                email: $scope.email,
                comment: $scope.comment
            };

            $scope.createComment = function() {
                if ($scope.newComment.name !== undefined && $scope.newComment.email !== undefined && $scope.newComment.comment !== undefined) {
                    $http.post('api/comments/create/' + post_id, $scope.newComment)
                        .then(() => {
                            $scope.newComment = {};
                            $scope.commentForm.$setPristine();
                            $scope.commentForm.$setUntouched();
                            $scope.commentSuccessMsg = "Your comment was submitted. It will be published after aproval";
                            $scope.commentSubmitted = true;
                            $timeout(function() {
                                $scope.fadeout = "fadeout";
                            }, 3000);
                        });
                }
            };
        });
    }])

    // Page
    .controller('PageController', ['$scope', '$http', '$routeParams', function($scope, $http, $routeParams) {
        const pageId = $routeParams.id;
        $http.get('api/pages/page/' + pageId).then(function(response) {

            // Page
            $scope.page = response.data.page;

            //Categories
            $scope.categories = response.data.categories;

            // Posts
            $scope.posts = response.data.posts;

        });
}])

There is a themes directory with each theme in its own directory, each theme directory containing the necessary assets and templates, like, for instance: mytheme/templates/singlepost.html:

<div class="wrapper style1">
  <div class="content">
    <h2>{{post.title}}</h2>
    <p class="meta clearfix">
      <span class="pull-left">By <a href="posts/byauthor/{{post.author_id}}" title="All posts by {{post.first_name}} {{post.last_name}}">{{post.first_name}} {{post.last_name}}</a></span>
      <span class="pull-right">Published on {{post.created_at  | dateParse | date : "MMMM dd y"}} in <a href="categories/posts/{{post.cat_id}}" title="All posts in {{post.category_name}}">{{post.category_name}}</a></span>
    </p>
    <div class="image fit">
      <img ng-src="api/assets/img/posts/{{post.post_image}}" alt="{{post.title}}">
    </div>
    <div class="post-content" ng-bind-html="post.content"></div>
    <div class="comments-form" ng-controller="PostCommentController">
      <h3>Leave a comment</h3>
      <form name="commentForm" novalidate>
        <div class="row uniform">
          <div class="form-controll 6u 12u$(xsmall)">
            <input type="text" name="name" id="name" ng-model="newComment.name" placeholder="Name" ng-required="true" />
            <span class="error" ng-show="(commentForm.name.$touched && commentForm.name.$invalid) || (commentForm.$submitted && commentForm.name.$invalid)">This field can not be empty</span>
          </div>
          <div class="form-controll 6u$ 12u$(xsmall)">
            <input type="email" name="email" id="email" ng-model="newComment.email" placeholder="Email" ng-required="true" />
            <span class="error" ng-show="(commentForm.email.$touched && commentForm.email.$invalid) || (commentForm.$submitted && commentForm.email.$invalid)">Enter a valid email address</span>
          </div>
          <div class="form-controll 12u$">
            <textarea name="comment" rows="6" id="message" ng-model="newComment.comment" placeholder="Comment" ng-required="true"></textarea>
            <span class="error" ng-show="(commentForm.comment.$touched && commentForm.comment.$invalid) || (commentForm.$submitted && commentForm.comment.$invalid)">This field can not be empty</span>
          </div>
          <!-- Break -->
          <div class="12u$">
            <input type="submit" value="Add comment" ng-click="createComment()" class="button special fit" />
          </div>
        </div>
      </form>
      <div class="alert alert-success {{fadeout}}" ng-show="commentSubmitted == true">
        {{commentSuccessMsg}}
      </div>
    </div>
    <div ng-if="comments.length > 0" class="comments">
      <h3>Comments</h3>
      <div ng-repeat="comment in comments" class="comment">
        <h4>On {{comment.created_at  | dateParse | date : "MMMM dd y"}}, {{comment.name}} wrote:</h4>
        <p>{{comment.comment}}</p>
      </div>
    </div>
  </div>
</div>

Thee AngularJS app entry point - index.html - has all the paths to the aforementioned theme assets:

<!DOCTYPE html>
<html ng-app="app" ng-controller="MainController">
  <head>
    <title>{{siteTitle}} | {{tagline}}</title>
    <base href="/">
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <script src="js/vendor/angular.min.js"></script>
    <script src="js/vendor/angular-route.min.js"></script>
    <script src="js/vendor/angular-sanitize.js"></script>
    <script src="js/app.js"></script>
    <script src="js/controllers.js"></script>
    <link rel="stylesheet" href="themes/caminar/assets/css/main.css" />
  </head>
  <body>
    <!-- Header -->
    <header id="header">
      <div class="logo"><a href="/">{{siteTitle}}</a></div>
    </header>
    <!-- Main -->
    <section id="main">
      <div class="inner" ng-view></div>
    </section>
    <!-- Footer -->
    <footer id="footer">
      <div class="container">
        <ul class="icons">
          <li><a href="{{twitter}}" target="_blank" class="icon fa-twitter"><span class="label">Twitter</span></a></li>
          <li><a href="{{facebook}}" target="_blank" class="icon fa-facebook"><span class="label">Facebook</span></a></li>
          <li><a href="{{instagram}}" target="_blank" class="icon fa-instagram"><span class="label">Instagram</span></a></li>
          <li><a href="mailto:{{company_email}}" class="icon fa-envelope-o"><span class="label">Email</span></a></li>
        </ul>
        <ul class="icons">
          <li ng-repeat="page in pages">
            <a href="pages/page/{{page.id}}">{{page.title}}</a>
          </li>
        </ul>
      </div>
      <div class="copyright">
        &copy; {{companyName}}. All rights reserved. Design by <a href="https://templated.co" target="_blank">TEMPLATED</a>
      </div>
    </footer>
  </body>
</html>

The obvious shortcoming of this application is: you are not able top see the templates (HTML) with [CTRL] + [U], which is bad for SEO.

Any constructive criticism and/or suggestions is welcomed.

\$\endgroup\$
1
+50
\$\begingroup\$

The code looks decent, though it would be better if it had more code for handling errors.

Indentation is very uniform throughout the code. There is some repeated code in the promise callbacks to some of the API endpoint requests in the controller functions (e.g. to set the categories, posts and pagination properties on $scope) that could be abstracted to a named function in order to DRY out that code. That function could also loop over the properties to set from response.data and assign them to $scope.

Some features like arrow functions are used, and thus the shorthand property definition notation could simplify object initialization - e.g. This block in the PostCommentController code:

      $scope.newComment = {
            slug: slug,
            post_id: post_id,
            name: $scope.name,
            email: $scope.email,
            comment: $scope.comment
        };

could be simplified to:

        $scope.newComment = {
            slug,
            post_id,
            name: $scope.name,
            email: $scope.email,
            comment: $scope.comment
        };

Also, in that function for the PostCommentController there is variable declared with let:

      //Current post id
        let post_id = response.data.post.id;

This variable never gets reassigned, and to prevent accidental reassignment const can be used instead of let.

\$\endgroup\$
  • \$\begingroup\$ There is also an obvious shortcoming of this application: no server-side rendering (have a look at the page source). Any suggestions for this issue? Thanks! \$\endgroup\$ – Razvan Zamfir Nov 26 '19 at 8:09
  • \$\begingroup\$ Hmm... I did note there wasn't really any server-side code included in your post. I tried the URL in your comment but it shows a 404 message: "404 Not Found The resource requested could not be found on this server!" \$\endgroup\$ – Sᴀᴍ Onᴇᴌᴀ Nov 26 '19 at 22:37
  • \$\begingroup\$ Then please try this one. :) \$\endgroup\$ – Razvan Zamfir Nov 27 '19 at 5:52
0
\$\begingroup\$

Regarding your comment about SEO:

The obvious shortcoming of this application is: you are not able top see the templates (HTML) with [CTRL] + [U], which is bad for SEO.

I did a search on the web for "angularJS SEO" and found quite a few results. The majority of posts on the topic suggest considering the use of pre-rendering services:

Search engines still need to see the content and elements of the page in the source code to guarantee that it will be indexed correctly. One current solution is to consider using a pre-rendering platform, such as Prerender.io. This is middleware that will crawl your web page, execute all javascript files, and host a cached version of your Angular pages on their content delivery network (CDN). When a request is received from a search engine bot it will show them the cached version, while the non-bot visitor will be shown the Angular page.

This may sound like cloaking, but Google has confirmed that it is not. It has also been stated by Google that as long as your intent is to improve user experience, and the content that’s available to the visitors is the same as what’s presented to Googlebot, you will not be penalized.1

Another post suggests the following:

There are three ways to do this.

The first one is to use a pre-rendering platform. Prerender.io, for example. Such a service would create cached versions of the content and render it in a form a Googlebot can crawl and index.

Unfortunately, it could prove a short-lived solution. Google can depreciate it easily, leaving the site without an indexable solution again.

The second solution is to modify SPA elements to render it as a hybrid between client- and server-side. Developers refer to this method as Initial Static Rendering.

In this method, you can leave certain elements, critical for SEO – title, meta description, headers, somebody copy and so on – on the server-side. As a result, those elements appear in the source code and Google can crawl them.

Unfortunately, once again, the solution might prove insufficient at times.

There is a viable option, however. It involves using Angular Universal extension to create static versions of pages to be rendered server-side Needless to say, these are indexable by Google fully, and there’s little chance that the technology will be depreciated any time soon.2

So consider options like those.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.